fix(crew-ai): isolate Crew(memory=True) per AG-UI threadId - #2253
Open
ranst91 wants to merge 4 commits into
Open
fix(crew-ai): isolate Crew(memory=True) per AG-UI threadId#2253ranst91 wants to merge 4 commits into
ranst91 wants to merge 4 commits into
Conversation
Contributor
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen 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 testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1785407391
Commit: fddf96b |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/mcp-middleware
@ag-ui/a2a
@ag-ui/adk
@ag-ui/ag2
@ag-ui/agno
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@ag-ui/claude-managed-agents
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/vercel-ai-sdk
@ag-ui/watsonx
@ag-ui/a2ui-toolkit
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
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
force-pushed
the
ran/cpk-7740-community-scope-crewai-memorytrue-stores-per-thread-state
branch
from
July 30, 2026 10:29
115b34f to
71acf8e
Compare
`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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
With a CrewAI
Crew(memory=True), memory leaked between chats that had distinct AG-UIthreadIds. 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. Passinginputs["id"] = thread_iddoes 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:This is the ag-ui half of CopilotKit#2254.
The fix
crewai 1.x replaced the old per-type stores with a single
Memoryon one LanceDB directory, namespaced by aroot_scopethat 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_scopefinds the crew on the per-request copy, derives"/thread/<sanitized>-<sha256[:16]>"from thethreadId, and installsmemory.scope(...)on a shallow crew view. After the fix:Notes on the shape of it:
agent.memory or crew._memoryand picks the executing agent offtask.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, sinceCrew.kickoffassignsagent.crewon every shared agent.kickoff_asyncpath and theastream/StreamFramepath, because the scope is applied before the driver is chosen._capabilities.pyin its existing idiom. Where the API is unavailable the bridge logs once that isolation is not active rather than failing.set_memory_storage_factorywas evaluated and rejected: it binds atMemoryconstruction, 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=falserestores the previous shared behaviour. Unrecognised values leave isolation ON, so a typo cannot silently reopen the leak.Known limits (documented in the README)
Tests
319 passed, 2 skipped(from 289 + 2), 30 new, all offline with a fake embedder against a realCrew(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:
conftestpins an absoluteCREWAI_STORAGE_DIRbefore the package is imported, taking files created under a throwawayHOMEfrom 9 to 1 (the remaining one is thecrewai_coreissue 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 tocrewai_core.pathsand left a deprecation shim behind, so that approach is a verified no-op on currentmain. It was rebased away rather than carried forward; the previous 35 commits are preserved onbackup/cpk-7740-full-history.Linear: https://linear.app/copilotkit/issue/PNI-127 (formerly CPK-7740; parent PNI-152, formerly CPK-7726)