TanStack AI version
0.48.0
Framework/Library version
Angular v20
Describe the bug and the steps to reproduce it
Summary
AG-UI defines activity as a two-part mechanism: ACTIVITY_SNAPSHOT / ACTIVITY_DELTA
events carry live updates, and role: 'activity' messages persist them in
MESSAGES_SNAPSHOT. @tanstack/ai recognises both in its type and validation layers, but
neither works, for two independent reasons:
- Bug 1 — the events are never processed.
ACTIVITY_SNAPSHOT and ACTIVITY_DELTA are
absent from the AGUIEvent union and have no case in the stream processor. Emitting one
is a silent no-op.
- Bug 2 — the messages are destroyed. A
role: 'activity' message in a
MESSAGES_SNAPSHOT is rewritten to {role: 'assistant', parts: []}, discarding its
payload — and that rewrite defeats the library's own guarantee that activity never
reaches the model.
Either bug alone makes activity unusable; together they mean neither the live path nor the
persistence path functions. Bug 2 additionally causes data loss and an outbound protocol
violation that stand regardless of whether the events are ever implemented.
Are we missing something? We may have overlooked a supported consumption path and would rather be corrected.
Bug 1 — ACTIVITY_SNAPSHOT / ACTIVITY_DELTA are never processed
Reproduction
Send a spec-valid event:
Expected: something observable — a message, a part, a callback, or a documented
"unsupported" warning.
Actual: nothing. onChunk fires, then the processor's switch falls through to
default: break. No state change, no error, no warning. Same for ACTIVITY_DELTA.
Root cause
Both events are recognised at three layers and implemented at none:
| Layer |
Status |
Location |
EventType enum |
present |
src/client.ts:210-211 |
| Wire-key allowlist |
present |
src/utilities/spec-event-keys.ts:41-45 |
| Inbound validator |
present |
src/utilities/chat-params.ts:99-103 |
AGUIEvent union |
absent |
src/types.ts:1641-1664 |
Processor case |
absent |
src/activities/chat/stream/processor.ts:552-658 |
The field names match @ag-ui/core's ActivitySnapshotEventSchema exactly, including
replace — a faithful transcription of the spec. But StreamChunk = AGUIEvent
(src/types.ts:1669), and no ActivitySnapshotEvent / ActivityDeltaEvent interface
exists, so the events cannot be represented as chunks at all. The missing processor
case follows mechanically. The shipped dist/esm/.../processor.js confirms it: exactly 20
case labels, none for activity.
Fix
Add ActivitySnapshotEvent / ActivityDeltaEvent interfaces to the AGUIEvent union, add
the corresponding processor cases, apply replace semantics keyed by messageId
(defaulting to true per @ag-ui/core), and apply ACTIVITY_DELTA as an RFC 6902 patch.
Bug 2 — role: 'activity' messages are silently destroyed
Reproduction
Server sends a spec-valid snapshot:
chat.messages() now contains a phantom third message — activityType and content gone,
role changed:
Send a follow-up turn: uiMessagesToWire serialises that phantom, so the outbound request
carries an empty assistant turn the agent never produced.
Root cause
src/activities/chat/messages.ts:705-712 — note the branch is shared with default, i.e.
this is the unknown-role fallback rather than deliberate handling:
case 'activity':
default:
// `activity` (and any future role) has no text/parts equivalent today.
return applySnapshotMetadata(message, { id, role: 'assistant', parts: [] })
This also defeats the library's own outbound guarantee at
src/activities/chat/messages.ts:165-167:
if (role === 'activity') {
continue // strips activity before the model — correct
}
After a snapshot the role is already assistant, so this guard never matches. The
"activity is never forwarded to the agent" property holds on the live stream and silently
breaks across a reload — the case where it matters most.
Note the inconsistency: the same message is validated as legal on the way in
(chat-params.ts:99-103 requires activityType: string and a record content) and then
relabelled with its payload dropped on the way through.
Fix
Split case 'activity' from default and either drop the message outright, or preserve
the role so the strip at messages.ts:165 keeps working. Either is strictly better than
losing the payload and leaking an empty turn to the model. This fix is worth applying
independently of Bug 1 — an unsupported message should be dropped or surfaced, never
relabelled.
Questions:
- Is
ACTIVITY_* intentionally excluded from the client? If so, the enum /
spec-event-keys / chat-params entries read as an endorsement, and the non-support
should be documented.
- Is
CUSTOM the sanctioned escape hatch (SKILL.md:200 calls it "Extension point")?
If so, what is the guidance for surviving MESSAGES_SNAPSHOT?
What we are trying to do
Render a live agent task-list — a checklist the agent ticks off as it works — that updates
during a run, survives a page reload, and is never fed back to the model.
ACTIVITY_SNAPSHOT with replace: true plus role: 'activity' persistence is precisely
the right primitive for this; the semantics match exactly. Both halves are currently
unusable.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
Attached above
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
None
Terms & Code of Conduct
TanStack AI version
0.48.0
Framework/Library version
Angular v20
Describe the bug and the steps to reproduce it
Summary
AG-UI defines activity as a two-part mechanism:
ACTIVITY_SNAPSHOT/ACTIVITY_DELTAevents carry live updates, and
role: 'activity'messages persist them inMESSAGES_SNAPSHOT.@tanstack/airecognises both in its type and validation layers, butneither works, for two independent reasons:
ACTIVITY_SNAPSHOTandACTIVITY_DELTAareabsent from the
AGUIEventunion and have no case in the stream processor. Emitting oneis a silent no-op.
role: 'activity'message in aMESSAGES_SNAPSHOTis rewritten to{role: 'assistant', parts: []}, discarding itspayload — and that rewrite defeats the library's own guarantee that activity never
reaches the model.
Either bug alone makes activity unusable; together they mean neither the live path nor the
persistence path functions. Bug 2 additionally causes data loss and an outbound protocol
violation that stand regardless of whether the events are ever implemented.
Are we missing something? We may have overlooked a supported consumption path and would rather be corrected.
Bug 1 —
ACTIVITY_SNAPSHOT/ACTIVITY_DELTAare never processedReproduction
Send a spec-valid event:
{ "type": "ACTIVITY_SNAPSHOT", "messageId": "act-1", "activityType": "PLAN", "replace": true, "content": { "steps": [{ "content": "Getting Items", "status": "in_progress" }] } }Expected: something observable — a message, a part, a callback, or a documented
"unsupported" warning.
Actual: nothing.
onChunkfires, then the processor's switch falls through todefault: break. No state change, no error, no warning. Same forACTIVITY_DELTA.Root cause
Both events are recognised at three layers and implemented at none:
EventTypeenumsrc/client.ts:210-211src/utilities/spec-event-keys.ts:41-45src/utilities/chat-params.ts:99-103AGUIEventunionsrc/types.ts:1641-1664casesrc/activities/chat/stream/processor.ts:552-658The field names match
@ag-ui/core'sActivitySnapshotEventSchemaexactly, includingreplace— a faithful transcription of the spec. ButStreamChunk = AGUIEvent(
src/types.ts:1669), and noActivitySnapshotEvent/ActivityDeltaEventinterfaceexists, so the events cannot be represented as chunks at all. The missing processor
case follows mechanically. The shipped
dist/esm/.../processor.jsconfirms it: exactly 20caselabels, none for activity.Fix
Add
ActivitySnapshotEvent/ActivityDeltaEventinterfaces to theAGUIEventunion, addthe corresponding processor cases, apply
replacesemantics keyed bymessageId(defaulting to
trueper@ag-ui/core), and applyACTIVITY_DELTAas an RFC 6902 patch.Bug 2 —
role: 'activity'messages are silently destroyedReproduction
Server sends a spec-valid snapshot:
{ "type": "MESSAGES_SNAPSHOT", "messages": [ { "id": "u-1", "role": "user", "content": "investigate" }, { "id": "act-1", "role": "activity", "activityType": "PLAN", "content": { "steps": [{ "content": "Retrieve incidents", "status": "completed" }] } }, { "id": "m-1", "role": "assistant", "content": "Three hosts affected." } ]}chat.messages()now contains a phantom third message —activityTypeandcontentgone,role changed:
{ "id": "act-1", "role": "assistant", "parts": [] }Send a follow-up turn:
uiMessagesToWireserialises that phantom, so the outbound requestcarries an empty assistant turn the agent never produced.
Root cause
src/activities/chat/messages.ts:705-712— note the branch is shared withdefault, i.e.this is the unknown-role fallback rather than deliberate handling:
This also defeats the library's own outbound guarantee at
src/activities/chat/messages.ts:165-167:After a snapshot the role is already
assistant, so this guard never matches. The"activity is never forwarded to the agent" property holds on the live stream and silently
breaks across a reload — the case where it matters most.
Note the inconsistency: the same message is validated as legal on the way in
(
chat-params.ts:99-103requiresactivityType: stringand a recordcontent) and thenrelabelled with its payload dropped on the way through.
Fix
Split
case 'activity'fromdefaultand either drop the message outright, or preservethe role so the strip at
messages.ts:165keeps working. Either is strictly better thanlosing the payload and leaking an empty turn to the model. This fix is worth applying
independently of Bug 1 — an unsupported message should be dropped or surfaced, never
relabelled.
Questions:
ACTIVITY_*intentionally excluded from the client? If so, the enum /spec-event-keys/chat-paramsentries read as an endorsement, and the non-supportshould be documented.
CUSTOMthe sanctioned escape hatch (SKILL.md:200calls it "Extension point")?If so, what is the guidance for surviving
MESSAGES_SNAPSHOT?What we are trying to do
Render a live agent task-list — a checklist the agent ticks off as it works — that updates
during a run, survives a page reload, and is never fed back to the model.
ACTIVITY_SNAPSHOTwithreplace: trueplusrole: 'activity'persistence is preciselythe right primitive for this; the semantics match exactly. Both halves are currently
unusable.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
Attached above
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
None
Terms & Code of Conduct