Skip to content

.NET: Python: [Bug]: AG-UI host builds a fresh AgentSession per request — session-stateful harness features silently lose their state between runs #6920

Description

@antsok

Description

1. Summary

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"] 9 items
10:41–10:43 several runs new requests, new sessions empty (unnoticed — nothing read it)
10:43:57 c9036a00 mode_set("execute") executes in a fresh session empty
10:44:06 c9036a00 todos_complete(items=[{id:1,…},{id:4,…}]){"completed": 0} empty

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)

  1. agent = create_harness_agent(chat_client=…) (defaults: TodoProvider and AgentModeProvider enabled).
  2. Host it with add_agent_framework_fastapi_endpoint(app, agent, …).
  3. Turn 1: send a prompt that makes the agent build a plan (todos_add runs; response references the todo list).
  4. Turn 2 (same thread): send "mark the first step complete".
  5. 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.

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_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:

  1. 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.
  2. Pluggable session store / session reuse keyed by thread id (in-memory default, same lifecycle as pending_approvals), for deployments that accept in-process state.
  3. 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)

Issue / PR State Relationship
#4920 open .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
#2517 open .NET: MapAGUI passes thread: null, so ChatMessageStoreFactory thread persistence never engages — same per-request-lifecycle gap, message-store angle
#6471 merged Python: added opt-in AG-UI thread snapshot persistence/hydration — the exact seam proposed fix 1 extends (AGUIThreadSnapshot currently persists messages + AG-UI state, but not session.state)
#4177 open .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)
#5197 open Python: not a duplicate, the inbound sibling — AG-UI fails to pass request state into AgentSession 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
#3167 closed Python: origin of state_update() — deterministic tool-driven state visibility; orthogonal to state survival
#6910 open Python: approval-flow defect in the same host where per-request session construction is a contributing factor; independent report

8. Environment

  • agent-framework-core 1.10.0, agent-framework-ag-ui 1.0.0rc7, Python 3.12, Windows 11
  • Frontend: CopilotKit over the FastAPI AG-UI endpoint; evidence captured via .NET Aspire telemetry with DEBUG logging

Metadata

Metadata

Assignees

Labels

ag-uiUsage: [Issues, PRs], Target: AG-UI protocol integrationpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions