microsoft / microsoft/agent-framework

Python: Python / AG-UI: Is stacking Thread Snapshot history with per-agent HistoryProvider intended?

Open
#8,075 1 comment 0 reactions 1 assignee View on GitHub

@eavanvalkenburg is already working on this.

Since Sep 8, 2026.

ag-ui python
Dominant language
Python
Stars
13.6k
Forks
2.3k
Avg merge
2d 45m
Merged PRs (30d)
358

Description

## Summary

When an AG-UI **workflow** is backed by a Thread Snapshot store, follow-up turns that send `messages` without `resume` cause `AgentFrameworkWorkflow` to reconstruct the **full prior thread transcript** and pass it into `workflow.run(message=...)`.

At the same time, each workflow participant still has its own agent session / `InMemoryHistoryProvider` (especially with `require_per_service_call_history_persistence=True` on local Chat Completions clients).

On turn 2+, the model call therefore sees roughly:

**AG-UI snapshot replay (outer) + participant HistoryProvider (inner)**

We are looking for maintainer guidance on whether this composition is intentional, and how conversation history is meant to be managed in this setup. We are **not** proposing a particular fix—we do not know what the intended practice is.

Related (similar dual-layer history, different surface): #7756 (`SequentialBuilder` + `workflow.as_agent()` + outer `AgentSession`). This issue is about the **AG-UI Workflow + Thread Snapshot** path.

## What we observed

Setup:

- `AgentFrameworkWorkflow(workflow_factory=...)` + AG-UI Thread Snapshot store
- `SequentialBuilder(participants=[writer, reviewer])` (easy to hit: every chat turn is `messages` without `resume`)
- Local OpenAI-compatible client (`store=False` / no service-managed conversation)
- Participants with `require_per_service_call_history_persistence=True`

| Layer | Turn 1 | Turn 2 (`Make it shorter.`) |
| --- | --- | --- |
| AG-UI reconstructed `messages` | 1 | >1 (prior transcript + new user) |
| Same cached Workflow instance | created | reused |
| Writer `HistoryProvider` | 0 | >0 |
| Writer final messages to model | ≈ input only | ≈ **input + history** (`stacked=True` in our probe) |

Concrete probe line from a local run:

```text
agent=writer workflow_input=2 history_provider=2 final_to_model=4 predicted_sum=4 stacked=True
```

Notes:

- On a **Handoff** AG-UI demo, mid-case turns are usually `resume`, so this stacking is harder to notice; a post-complete `messages`-without-`resume` kickoff on the same instance shows the same pattern.
- On **Sequential**, turn 2 of normal chat is enough.

## Snapshot growth (second concern)

Thread Snapshot history appears to grow without compaction / truncation. For long multi-turn threads, the reconstructed `messages` list alone can become very large before it is even combined with per-agent history. We would also appreciate guidance on how this is expected to be handled.

## Minimal reproduction (backend)

```python
from agent_framework import Agent, ChatContext, ChatMiddleware
from agent_framework.ag_ui import (
AgentFrameworkWorkflow,
InMemoryAGUIThreadSnapshotStore,
add_agent_framework_fastapi_endpoint,
)
from agent_framework.openai import OpenAIChatCompletionClient
from agent_framework.orchestrations import SequentialBuilder
from fastapi import FastAPI

class ContextProbe(ChatMiddleware):
def __init__(self, agent_id: str) -> None:
self._agent_id = agent_id

async def process(self, context: ChatContext, call_next):
before = list(context.messages)
history = []
if context.session is not None:
history = list(context.session.state.get("in_memory", {}).get("messages", []) or [])
await call_next()
final = list(context.messages)
print(
f"[probe] {self._agent_id}: input={len(before)} "
f"history={len(history)} final={len(final)} "
f"stacked={len(final) == len(before) + len(history) and len(history) > 0 and len(before) > 1}"
)

client = OpenAIChatCompletionClient(
model="...",
api_key="...",
base_url="...", # local Chat Completions; no service-managed history
)

writer = Agent(
id="writer",
name="writer",
instructions="You are a concise copywriter.",
client=client,
middleware=[ContextProbe("writer")],
require_per_service_call_history_persistence=True,
)
reviewer = Agent(
id="reviewer",
name="reviewer",
instructions="You are a short reviewer.",
client=client,
middleware=[ContextProbe("reviewer")],
require_per_service_call_history_persistence=True,
)

snapshot_store = InMemoryAGUIThreadSnapshotStore()

def workflow_factory(_thread_id: str):
return SequentialBuilder(participants=[writer, reviewer]).build()

app = FastAPI()
add_agent_framework_fastapi_endpoint(
app=app,
agent=AgentFrameworkWorkflow(
workflow_factory=workflow_factory,
snapshot_store=snapshot_store,
),
path="/sequential_demo",
snapshot_store=snapshot_store,
snapshot_scope_resolver=lambda _request: "demo",
)
```

Then POST twice on the same `threadId` (no `resume`):

1. `messages: [{ "role": "user", "content": "Write a tagline for a budget-friendly eBike." }]`
2. `messages: [{ "role": "user", "content": "Make it shorter." }]`

Turn 2 should log writer `stacked=True` (or equivalent: `final ≈ input + history`).

## Questions for maintainers

1. Is stacking AG-UI Thread Snapshot history with each participant’s HistoryProvider **intended** for AG-UI workflows?
2. For **AG-UI Workflow + SnapshotStore + agents with HistoryProvider**, what is the recommended way to manage conversation history?
3. Thread Snapshots appear to grow without compaction/truncation—what is the recommended way to think about long multi-turn threads in this model?

## Environment

- `microsoft/agent-framework` Python packages (workspace checkout)
- `agent-framework-ag-ui` + `agent-framework-orchestrations`
- Python 3.12+
- OpenAI-compatible local Chat Completions endpoint (no service-side conversation store)

Happy to add more traces or adjust the repro if that helps.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.