You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
.NET: Python: [Bug]: AG-UI host builds a fresh AgentSession per request — session-stateful harness features silently lose their state between runs #6920
agent_framework_ag_ui constructs a brand-new AgentSession for every HTTP request (_agent_run.py, AgentSession(session_id=thread_id) — the id matches the thread, but the state starts empty). Meanwhile, several MAF features are designed to keep per-conversation working state in session.state and assume the session lives as long as the conversation:
Feature
Bucket in session.state
Effect of per-request sessions over AG-UI
TodoProvider (harness, on by default)
"todo"
todo list dies between turns; todos_complete on a plan from a previous turn returns {"completed": 0}; provider instructions tell the model it has no todos while the chat history describes the plan it just made
AgentModeProvider (harness, on by default)
mode source bucket
agent mode resets to default every turn unless the application re-applies it
any custom ContextProvider
app-defined
same loss, undiagnosable for app developers who followed the ContextProvider docs
Every hosting surface except AG-UI keeps sessions conversation-scoped (interactive clients hold one session object; agent-framework-hosting keys sessions by isolation key across turns and offers explicit reset). The AG-UI adapter is the outlier, and nothing warns the developer: the agent appears to work in single-turn tests and degrades only on the second turn of a stateful interaction.
sequenceDiagram
autonumber
participant C as Client (AG-UI)
participant EP as AG-UI endpoint
participant S1 as AgentSession (run 1)
participant S2 as AgentSession (run 2)
C->>EP: turn 1 - "plan this migration"
EP->>S1: fresh AgentSession(session_id=thread)
S1->>S1: todos_add -> session.state["todo"] = 9 items
EP-->>C: STATE_SNAPSHOT / instructions reflect 9 todos
Note over S1: request ends - session garbage-collected,<br/>todo store dies with it
C->>EP: turn 2 - "complete steps 1 and 4"
EP->>S2: fresh AgentSession(session_id=thread) - state {}
S2->>S2: TodoProvider injects "no todos" instructions<br/>(chat history still describes the 9-step plan)
S2->>S2: todos_complete(items=[1,4]) -> {"completed": 0}
EP-->>C: state/instructions now contradict the conversation
Loading
2. Evidence — live capture (2026-07-05)
Harness agent (create_harness_agent, default providers) hosted via add_agent_framework_fastapi_endpoint, CopilotKit frontend, single thread 1dc4b564…. Timeline from the endpoint's DEBUG event log:
Time
Run
Event
Server-side todo store
10:40:56
d42cfd3c
todos_add executes; 9 items written to session.state["todo"]
The model called todos_complete because the chat history (replayed from the client per the AG-UI protocol) describes the 9-step plan it created two turns earlier. The store it completed against was born empty seconds before. From the model's perspective its own tools are gaslighting it: todos_get_remaining and the provider's injected instructions say there is no plan, while the transcript says there is.
The visible UI symptom in our app (a shared-state snapshot suddenly carrying todos: [] and wiping the panel) is app-specific, but {"completed": 0} is pure framework behavior reproducible with stock components.
3. Reproduction (minimal, stock components)
agent = create_harness_agent(chat_client=…) (defaults: TodoProvider and AgentModeProvider enabled).
Host it with add_agent_framework_fastapi_endpoint(app, agent, …).
Turn 1: send a prompt that makes the agent build a plan (todos_add runs; response references the todo list).
Turn 2 (same thread): send "mark the first step complete".
Observe todos_complete → {"completed": 0} and todos_get_remaining → empty; the agent either apologizes, re-creates the plan from history, or silently claims success depending on the model.
No custom middleware, wrappers, or configuration required — default harness providers over the stock endpoint.
run_agent_stream builds the session per request (_agent_run.py):
session=AgentSession(session_id=thread_id) # state = {} every request
The adapter's implicit state model is that conversation state has exactly two homes: the wire (AG-UI RunAgentInput carries full messages + state each POST) or the model service (the use_service_session option creates AgentSession(session_id=…, service_session_id=…) for server-managed threads). Local session.state — the third home, used by the harness context providers — was never reconciled with that model: it is neither sent to the client nor held by the service, so per-request construction destroys it.
Notably, the adapter is already stateful per thread where it had no alternative: its pending_approvals registry and the AGUIThreadSnapshotStore both survive across requests keyed by thread. The infrastructure for per-thread server-side continuity exists; sessions were just not included.
5. Proposed fix
Any of the following, in our order of preference:
Persist and restore session.state per thread through the existing snapshot-store seam. Extend AGUIThreadSnapshot with a session_state field, save the (JSON-safe, size-bounded) state at run end, and seed the fresh session from it at run start — scoped by the same snapshot_scope_resolver that already isolates tenants. This fixes all session-stateful providers at once; applications that want statelessness could opt out per key or entirely.
Pluggable session store / session reuse keyed by thread id (in-memory default, same lifecycle as pending_approvals), for deployments that accept in-process state.
At minimum, document the constraint loudly in the AG-UI integration docs and the harness docs: "session-stateful providers (todo, mode, custom ContextProviders) do not persist across AG-UI runs", with a supported extension point for applications to hydrate sessions themselves. Today the failure is silent and looks like model misbehavior.
6. Application-level workaround (what we do today)
mode: re-applied every run from the client's round-tripped AG-UI state via agent middleware (set_agent_mode pre-run).
todos: an app-side per-thread store keyed by AG-UI thread id; the "todo" session bucket is saved when a run's stream drains and restored into the next request's fresh session before the run.
Both are re-implementations of session longevity the framework already provides on its other hosting surfaces.
7. Related issues (checked 2026-07-05 — no Python duplicate found)
.NET twin of this report: MapAGUI() hardcodes session: null despite a configured session store — "every AG-UI request starts a fresh workflow run with no memory of the previous conversation state". Confirms the defect exists on both tracks; this report is the Python counterpart with the harness providers as concrete victims
.NET: feature request to bridge session StateBag mutations into AG-UI state events — the visibility half of the same session/AG-UI disconnect (Python counterpart: our harness-state-sync feature request)
Python: not a duplicate, the inbound sibling — AG-UI fails to pass request state intoAgentSession for ContextProviders to read (user_id/tenant_id to a HistoryProvider). This report is the opposite direction: state written by providers into the session does not survive to the next request. Fixing either does not fix the other (the todo store never rides in AG-UI request state), but both stem from the session↔AG-UI state model never being reconciled
Description
1. Summary
agent_framework_ag_uiconstructs a brand-newAgentSessionfor every HTTP request (_agent_run.py,AgentSession(session_id=thread_id)— the id matches the thread, but the state starts empty). Meanwhile, several MAF features are designed to keep per-conversation working state insession.stateand assume the session lives as long as the conversation:session.stateTodoProvider(harness, on by default)"todo"todos_completeon a plan from a previous turn returns{"completed": 0}; provider instructions tell the model it has no todos while the chat history describes the plan it just madeAgentModeProvider(harness, on by default)ContextProviderEvery hosting surface except AG-UI keeps sessions conversation-scoped (interactive clients hold one session object;
agent-framework-hostingkeys sessions by isolation key across turns and offers explicit reset). The AG-UI adapter is the outlier, and nothing warns the developer: the agent appears to work in single-turn tests and degrades only on the second turn of a stateful interaction.sequenceDiagram autonumber participant C as Client (AG-UI) participant EP as AG-UI endpoint participant S1 as AgentSession (run 1) participant S2 as AgentSession (run 2) C->>EP: turn 1 - "plan this migration" EP->>S1: fresh AgentSession(session_id=thread) S1->>S1: todos_add -> session.state["todo"] = 9 items EP-->>C: STATE_SNAPSHOT / instructions reflect 9 todos Note over S1: request ends - session garbage-collected,<br/>todo store dies with it C->>EP: turn 2 - "complete steps 1 and 4" EP->>S2: fresh AgentSession(session_id=thread) - state {} S2->>S2: TodoProvider injects "no todos" instructions<br/>(chat history still describes the 9-step plan) S2->>S2: todos_complete(items=[1,4]) -> {"completed": 0} EP-->>C: state/instructions now contradict the conversation2. Evidence — live capture (2026-07-05)
Harness agent (
create_harness_agent, default providers) hosted viaadd_agent_framework_fastapi_endpoint, CopilotKit frontend, single thread1dc4b564…. Timeline from the endpoint's DEBUG event log:d42cfd3ctodos_addexecutes; 9 items written tosession.state["todo"]c9036a00mode_set("execute")executes in a fresh sessionc9036a00todos_complete(items=[{id:1,…},{id:4,…}])→{"completed": 0}The model called
todos_completebecause the chat history (replayed from the client per the AG-UI protocol) describes the 9-step plan it created two turns earlier. The store it completed against was born empty seconds before. From the model's perspective its own tools are gaslighting it:todos_get_remainingand the provider's injected instructions say there is no plan, while the transcript says there is.The visible UI symptom in our app (a shared-state snapshot suddenly carrying
todos: []and wiping the panel) is app-specific, but{"completed": 0}is pure framework behavior reproducible with stock components.3. Reproduction (minimal, stock components)
agent = create_harness_agent(chat_client=…)(defaults:TodoProviderandAgentModeProviderenabled).add_agent_framework_fastapi_endpoint(app, agent, …).todos_addruns; response references the todo list).todos_complete→{"completed": 0}andtodos_get_remaining→ empty; the agent either apologizes, re-creates the plan from history, or silently claims success depending on the model.No custom middleware, wrappers, or configuration required — default harness providers over the stock endpoint.
Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-ag-ui: 1.0.0rc7, agent-framework-core: 1.10.0
Python Version
Python 3.12
Additional Context
4. Root cause
run_agent_streambuilds the session per request (_agent_run.py):The adapter's implicit state model is that conversation state has exactly two homes: the wire (AG-UI
RunAgentInputcarries full messages + state each POST) or the model service (theuse_service_sessionoption createsAgentSession(session_id=…, service_session_id=…)for server-managed threads). Localsession.state— the third home, used by the harness context providers — was never reconciled with that model: it is neither sent to the client nor held by the service, so per-request construction destroys it.Notably, the adapter is already stateful per thread where it had no alternative: its
pending_approvalsregistry and theAGUIThreadSnapshotStoreboth survive across requests keyed by thread. The infrastructure for per-thread server-side continuity exists; sessions were just not included.5. Proposed fix
Any of the following, in our order of preference:
session.stateper thread through the existing snapshot-store seam. ExtendAGUIThreadSnapshotwith asession_statefield, save the (JSON-safe, size-bounded) state at run end, and seed the fresh session from it at run start — scoped by the samesnapshot_scope_resolverthat already isolates tenants. This fixes all session-stateful providers at once; applications that want statelessness could opt out per key or entirely.pending_approvals), for deployments that accept in-process state.6. Application-level workaround (what we do today)
set_agent_modepre-run)."todo"session bucket is saved when a run's stream drains and restored into the next request's fresh session before the run.Both are re-implementations of session longevity the framework already provides on its other hosting surfaces.
7. Related issues (checked 2026-07-05 — no Python duplicate found)
MapAGUI()hardcodessession: nulldespite a configured session store — "every AG-UI request starts a fresh workflow run with no memory of the previous conversation state". Confirms the defect exists on both tracks; this report is the Python counterpart with the harness providers as concrete victimsMapAGUIpassesthread: null, soChatMessageStoreFactorythread persistence never engages — same per-request-lifecycle gap, message-store angleAGUIThreadSnapshotcurrently persists messages + AG-UI state, but notsession.state)StateBagmutations into AG-UI state events — the visibility half of the same session/AG-UI disconnect (Python counterpart: our harness-state-sync feature request)AgentSessionfor ContextProviders to read (user_id/tenant_id to a HistoryProvider). This report is the opposite direction: state written by providers into the session does not survive to the next request. Fixing either does not fix the other (the todo store never rides in AG-UI request state), but both stem from the session↔AG-UI state model never being reconciledstate_update()— deterministic tool-driven state visibility; orthogonal to state survival8. Environment
agent-framework-core1.10.0,agent-framework-ag-ui1.0.0rc7, Python 3.12, Windows 11