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)
- Client sends the documented resume shape (same as
ag_ui_workflow_handoff):
{
"threadId": "...",
"messages": [],
"resume": {
"interrupts": [
{
"id": "<request_info id>",
"value": [
{ "role": "user", "contents": [{ "type": "text", "text": "我要退货" }] }
]
}
]
}
}
- Framework comments explicitly treat resume as interrupt-response-only and seed the snapshot builder via
resume_seeded_messages(stored + incoming) (_workflow.py, _snapshot_session.py):
Resume requests carry only the synthesized interrupt response; seeding with stored history keeps the persisted thread from being truncated.
-
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.
-
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). 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)
- Enable
snapshot_store on an AG-UI workflow endpoint (e.g. handoff + request_info after agent reply).
- Run until
RUN_FINISHED with an interrupt whose value is a user-input request (not only tool approval).
- Resume with
messages: [] and resume.interrupts[0].value = [{ role: "user", ... }].
- Confirm the run continues (assistant asks for order id, etc.).
- Hydrate the same
threadId with messages: [] and no resume.
- 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:
- Extract user-visible content from each resolved interrupt value (message list / plain text / known handoff shapes).
- 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(...)).
- 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*
Summary
For workflow AG-UI runs with Thread Snapshot persistence enabled, a HITL
resumethat carries the user’s reply only inresume.interrupts[].value(withmessages: []) 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: []+resumeonly), so demos that onlypushMessagelocally look fine until hydrate.Current behavior (Python AG-UI workflow path)
ag_ui_workflow_handoff):{ "threadId": "...", "messages": [], "resume": { "interrupts": [ { "id": "<request_info id>", "value": [ { "role": "user", "contents": [{ "type": "text", "text": "我要退货" }] } ] } ] } }resume_seeded_messages(stored + incoming)(_workflow.py,_snapshot_session.py):With
incoming == [], the builder seed is only prior snapshot messages._WorkflowSnapshotBuilder.observethen appends newly streamed assistant/tool events. The human text insideresume.valueis used as a workflowresponsespayload, not folded into snapshot messages as arole: "user"(or equivalent) turn.Hydrate (
messages: [], noresume) 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). 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_infowith 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.:role: "user"message fromresume.valueinto the snapshot seed (preferred for chat UIs), and/ormessages(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)
snapshot_storeon an AG-UI workflow endpoint (e.g. handoff +request_infoafter agent reply).RUN_FINISHEDwith an interrupt whose value is a user-input request (not only tool approval).messages: []andresume.interrupts[0].value = [{ role: "user", ... }].threadIdwithmessages: []and noresume.Proposal
In
AgentFrameworkWorkflow.run/_WorkflowSnapshotBuilder(workflow path), whenresume_payloadis present and snapshot persistence is enabled:builder_seed_messagesafterresume_seeded_messages(...), before observing the new run’s stream (mirroring agent-pathsnapshot_seed_messages.extend(...)).Happy to adjust if the intended contract is “clients must put HITL user text in
messageson resume”; in that case samples + README hydrate/resume docs should state it explicitly, because they currently demonstratemessages: [].Environment
agent-frameworkPython AG-UI + orchestrations handoffpython/samples/05-end-to-end/ag_ui_workflow_handoff*