Skip to content

fix(crew-ai): isolate Crew(memory=True) per AG-UI threadId - #2253

Open
ranst91 wants to merge 4 commits into
mainfrom
ran/cpk-7740-community-scope-crewai-memorytrue-stores-per-thread-state
Open

fix(crew-ai): isolate Crew(memory=True) per AG-UI threadId#2253
ranst91 wants to merge 4 commits into
mainfrom
ran/cpk-7740-community-scope-crewai-memorytrue-stores-per-thread-state

Conversation

@ranst91

@ranst91 ranst91 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

The bug

With a CrewAI Crew(memory=True), memory leaked between chats that had distinct AG-UI threadIds. CrewAI's memory is backed by on-disk storage; the bridge isolates each request by copying the flow, and a copy does not change where that storage lives, so every thread read and wrote the same records. Passing inputs["id"] = thread_id does not help, because that scopes CrewAI's flow-state persistence, a different subsystem.

Reproduced on current main (crewai 1.15.7), offline, with a real crew and a deterministic embedder:

thread B saw A's secret: True    | A kept its own: True

This is the ag-ui half of CopilotKit#2254.

The fix

crewai 1.x replaced the old per-type stores with a single Memory on one LanceDB directory, namespaced by a root_scope that defaults to the crew name and derives nothing from the thread. So the fix is to give each request its own scope rather than to relocate any files.

Per request, immediately after the flow copy and before a run driver is selected, apply_thread_memory_scope finds the crew on the per-request copy, derives "/thread/<sanitized>-<sha256[:16]>" from the threadId, and installs memory.scope(...) on a shallow crew view. After the fix:

thread B saw A's secret: False   | A kept its own: True

Notes on the shape of it:

  • The shared template is never mutated. The crew is shared across concurrent requests, so the scoped memory goes on a per-request view, with a fail-loud post-condition if that ever stops holding.
  • Agent-level memory is covered too. crewai resolves agent.memory or crew._memory and picks the executing agent off task.agent, so the request gets wired views of the crew, its agents and its tasks. Without that, an agent carrying its own memory kept leaking. This also closed a race on the crew-level path, since Crew.kickoff assigns agent.crew on every shared agent.
  • Both run paths are covered, the kickoff_async path and the astream / StreamFrame path, because the scope is applied before the driver is chosen.
  • Capability-detected, not version-gated. Symbols resolve through _capabilities.py in its existing idiom. Where the API is unavailable the bridge logs once that isolation is not active rather than failing.
  • set_memory_storage_factory was evaluated and rejected: it binds at Memory construction, and the crew endpoint builds its crew lazily inside the first request, which would pin every later thread to the first thread's store. That makes the bug deterministic instead of fixing it. Evidence is in the commit history.

AGUI_CREWAI_THREAD_SCOPED_MEMORY=false restores the previous shared behaviour. Unrecognised values leave isolation ON, so a typo cannot silently reopen the leak.

Known limits (documented in the README)

  • A crew constructed inside a flow method is not reachable from the copy, so it is not scoped.

Tests

319 passed, 2 skipped (from 289 + 2), 30 new, all offline with a fake embedder against a real Crew(memory=True) and a real store. The load-bearing test writes as one thread and asserts a second thread cannot recall it, alongside a test that the same thread still recalls its own memory across sequential runs, and the opt-out behaving as documented.

Each new test was checked by reverting the fix in a scratch copy: 17 of 30 fail, including the leak assertion. The 10 that still pass are the ones that should (no-op guards, pure helper units, and the opt-out test, whose expected behaviour is the pre-fix behaviour).

Test runs are now hermetic: conftest pins an absolute CREWAI_STORAGE_DIR before the package is imported, taking files created under a throwaway HOME from 9 to 1 (the remaining one is the crewai_core issue above).

History

An earlier version of this branch targeted crewai 0.130.0 and patched crewai.utilities.paths.db_storage_path. crewai 1.x moved that function to crewai_core.paths and left a deprecation shim behind, so that approach is a verified no-op on current main. It was rebased away rather than carried forward; the previous 35 commits are preserved on backup/cpk-7740-full-history.

Linear: https://linear.app/copilotkit/issue/PNI-127 (formerly CPK-7740; parent PNI-152, formerly CPK-7726)

@ranst91
ranst91 requested a review from a team as a code owner July 27, 2026 13:23
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1785407391 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then install the packages you need:

# Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1785407391' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1785407391' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1785407391' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1785407391' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1785407391' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1785407391' --index testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1785407391

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: fddf96b

@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@2253

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@2253

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@2253

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@2253

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@2253

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@2253

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@2253

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@2253

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@2253

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@2253

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@2253

@ag-ui/claude-managed-agents

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-managed-agents@2253

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@2253

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@2253

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@2253

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@2253

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@2253

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@2253

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@2253

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@2253

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@2253

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@2253

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@2253

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@2253

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@2253

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@2253

commit: 3431551

ranst91 added 3 commits July 30, 2026 13:23
crewai 1.x keeps crew memory in one on-disk store namespaced by a
root_scope derived from the CREW NAME, so every threadId served by an
endpoint shared one namespace and one chat could recall another chat's
facts. Copying the flow never touched it, and inputs["id"] = thread_id
scopes flow-state persistence, a different subsystem.

Each request now gets a MemoryScope view of the crew's memory rooted at
a path derived from its threadId. The scope is installed on the
per-request flow copy before a run driver is selected, so both the
legacy kickoff_async path and the astream StreamFrame path are covered.
The template crew is shared across concurrent requests, so it is never
mutated: a shallow crew view carries the scoped memory and everything
else stays shared exactly as before.

Symbols are resolved in _capabilities (never version-gated); a crewai
build without the unified memory view API degrades to one warning
saying isolation is not active. AGUI_CREWAI_THREAD_SCOPED_MEMORY=false
restores the previous shared-namespace behaviour.
…un hermetic

Drives real Crew(memory=True) memory offline: a deterministic fake
embedder plus explicit scope/categories/importance, which is the
condition under which crewai's EncodingFlow skips its LLM analysis.
Covers the reported leak in both directions, a thread still seeing its
own memory across sequential runs, the shared template crew staying
unmutated, thread ids that sanitise alike staying apart, the opt-out
(including a typo failing safe), degradation without the view API, and
both endpoint factories invoking the hook.

conftest also redirects CREWAI_STORAGE_DIR to a temporary absolute path
before crewai is imported: crewai resolves (and mkdirs) its storage root
at module-import time, so the suite used to write into the developer's
home directory before any fixture could run.
Names what is isolated, the opt-out env var, and the four real
limitations: only crews reachable from the flow are scoped, agent-level
memory is not, isolation is logical rather than physical, and older
crewai degrades with a warning.
@ranst91
ranst91 force-pushed the ran/cpk-7740-community-scope-crewai-memorytrue-stores-per-thread-state branch from 115b34f to 71acf8e Compare July 30, 2026 10:29
@ranst91 ranst91 changed the title fix(crewai): scope memory=True stores per threadId fix(crew-ai): isolate Crew(memory=True) per AG-UI threadId Jul 30, 2026
`Crew(memory=True)` was already scoped per thread, but an agent built with
its own memory was not: crewai's executor resolves
`agent.memory or crew._memory`, so an agent-level memory bypassed the
crew-level scope entirely and thread B could recall what thread A wrote.

Scope the agents too. crewai picks the executing agent off `task.agent`
(or `manager_agent` under the hierarchical process), not off `Crew.agents`,
and reaches the crew's memory through `agent.crew` -- which `Crew.kickoff`
assigns on every agent it runs. So the executed graph is re-pointed
together: per-request agent views carrying scoped memory, per-request task
views pointing at those agents (with `task.context` remapped so downstream
tasks read this request's outputs), all hung off the per-request crew view.
A standalone agent held as a flow attribute is scoped the same way.

Nothing shared between concurrent requests is mutated, and the
post-condition that guarded the crew now covers agents and tasks as well:
a write that lands on a template raises rather than serving a wider leak
than the one being fixed. `AGUI_CREWAI_THREAD_SCOPED_MEMORY=false` still
turns all of it off.

Tests run offline: `Agent(memory=True)` builds its Memory on crewai's
default embedder, so behavioural tests hand the agent the equivalent
`Memory(embedder=<deterministic>)` while the reported `memory=True` shape
is still asserted structurally.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant