microsoft / microsoft/agent-framework
Python: [Bug]: AG-UI workflow resume does not persist user HITL text into Thread Snapshot
- Dominant language
- Python
- Stars
- 13.6k
- Forks
- 2.3k
- Avg merge
- 2d 45m
- Merged PRs (30d)
- 358
Description
## Summary
For **workflow** AG-UI runs with Thread Snapshot persistence enabled, a HITL `resume` that carries the user’s reply only in `resume.interrupts[].value` (with `messages: []`) continues execution correctly, but the **persisted Thread Snapshot never records that user turn**. After refresh/hydrate, the transcript jumps from one assistant message to the next and the user’s replies (e.g. “I want a refund”, an order id) are missing.
This matches how official samples resume (`messages: []` + `resume` only), so demos that only `pushMessage` locally look fine until hydrate.
## Current behavior (Python AG-UI workflow path)
1. Client sends the documented resume shape (same as `ag_ui_workflow_handoff`):
```json
{
"threadId": "...",
"messages": [],
"resume": {
"interrupts": [
{
"id": "",
"value": [
{ "role": "user", "contents": [{ "type": "text", "text": "我要退货" }] }
]
}
]
}
}
```
2. Framework comments explicitly treat resume as interrupt-response-only and seed the snapshot builder via `resume_seeded_messages(stored + incoming)` ([`_workflow.py`](https://github.com/microsoft/agent-framework/blob/main/python/packages/ag-ui/agent_framework_ag_ui/_workflow.py), [`_snapshot_session.py`](https://github.com/microsoft/agent-framework/blob/main/python/packages/ag-ui/agent_framework_ag_ui/_snapshot_session.py)):
> Resume requests carry only the synthesized interrupt response; seeding with stored history keeps the persisted thread from being truncated.
3. With `incoming == []`, the builder seed is **only prior snapshot messages**. `_WorkflowSnapshotBuilder.observe` then appends newly streamed assistant/tool events. The human text inside `resume.value` is used as a workflow `responses` payload, **not** folded into snapshot messages as a `role: "user"` (or equivalent) turn.
4. Hydrate (`messages: []`, no `resume`) correctly replays whatever was stored — so the gap becomes visible after refresh.
### Contrast: agent path
On the **agent** AG-UI path, resume is synthesized into messages that are also appended to `snapshot_seed_messages` (e.g. `_resume_to_tool_messages` / approval resume messages in [`_agent_run.py`](https://github.com/microsoft/agent-framework/blob/main/python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py)). The **workflow** path has no equivalent synthesis for handoff-style user text resumes.
## Expected behavior
When snapshot persistence is enabled and a workflow resume resolves a pending `request_info` with user-visible content (message list / text), the saved Thread Snapshot should include a replayable representation of that user turn (at least for hydrate UX), e.g.:
- synthesize a `role: "user"` message from `resume.value` into the snapshot seed (preferred for chat UIs), and/or
- document a required client contract that resume must also send those turns in `messages` (if that is the intended design)
Today neither is done for workflow handoff HITL: samples use `messages: []`, and the server drops the user text from the snapshot.
## Repro (conceptual)
1. Enable `snapshot_store` on an AG-UI workflow endpoint (e.g. handoff + `request_info` after agent reply).
2. Run until `RUN_FINISHED` with an interrupt whose value is a user-input request (not only tool approval).
3. Resume with `messages: []` and `resume.interrupts[0].value = [{ role: "user", ... }]`.
4. Confirm the run continues (assistant asks for order id, etc.).
5. Hydrate the same `threadId` with `messages: []` and no `resume`.
6. **Observe:** stored messages contain assistant turns after the resume, but not the user’s resume text.
## Proposal
In `AgentFrameworkWorkflow.run` / `_WorkflowSnapshotBuilder` (workflow path), when `resume_payload` is present and snapshot persistence is enabled:
1. Extract user-visible content from each resolved interrupt value (message list / plain text / known handoff shapes).
2. Append synthesized snapshot messages to `builder_seed_messages` **after** `resume_seeded_messages(...)`, before observing the new run’s stream (mirroring agent-path `snapshot_seed_messages.extend(...)`).
3. Keep tool-approval resumes as tool/system-style entries if that is preferred, but do not leave conversational HITL replies out of the hydrate transcript.
Happy to adjust if the intended contract is “clients must put HITL user text in `messages` on resume”; in that case samples + README hydrate/resume docs should state it explicitly, because they currently demonstrate `messages: []`.
## Environment
- Package: `agent-framework` Python AG-UI + orchestrations handoff
- Path: workflow AG-UI + Thread Snapshot store (SQLite/in-memory)
- Related: samples `python/samples/05-end-to-end/ag_ui_workflow_handoff*`
Contributor guide
Assessment
This issue has not been assessed yet.