AG-UI activity support is non-functional: events are never processed, and `role: 'activity'` messages are destroyed
Maintainers usually reply within 1 day
@jherr is already working on this.
Since Sep 17, 2026.
Assessment
This issue has not been assessed yet.
Description
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_SNAPSHOTandACTIVITY_DELTAare
absent from theAGUIEventunion 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_SNAPSHOTis 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:
{ "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. 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:
{ "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 — activityType and content gone,
role changed:
{ "id": "act-1", "role": "assistant", "parts": [] }
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-paramsentries read as an endorsement, and the non-support
should be documented. - Is
CUSTOMthe sanctioned escape hatch (SKILL.md:200calls it "Extension point")?
If so, what is the guidance for survivingMESSAGES_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
- I agree to follow this project's Code of Conduct
- I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.
- Dominant language
- TypeScript
- Stars
- 3.1k
- Forks
- 340
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 170
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from TanStack/ai
-
otelMiddleware: media generation spans never capture prompt, input media or output, even with captureContentPossibly taken @AlemTuzlak claimed this today. Openwaiting-on: maintainer
TanStack/ai#1526 · 1 assignee ·
Maintainers usually reply within 1 day
-
otelMiddleware: captureContent drops image/audio/video/document parts, recording only '[image]' placeholdersPossibly taken @AlemTuzlak claimed this today. Openhas-pr waiting-on: maintainer
TanStack/ai#1525 · 1 assignee ·
Maintainers usually reply within 1 day
-
feat(ai): add a timeout option to chat(), matching the media activitiesPossibly taken @AlemTuzlak claimed this 1 day ago. Openwaiting-on: maintainer
TanStack/ai#1517 · 1 assignee ·
Maintainers usually reply within 1 day
-
Does a hosted provider package have to carry a key?Possibly taken @AlemTuzlak claimed this 2 days ago. Openwaiting-on: author
TanStack/ai#1506 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
Client tools never execute against spec-compliant AG-UI servers (plain `success` RUN_FINISHED)Possibly taken @jherr claimed this 4 days ago. Openwaiting-on: maintainer
TanStack/ai#1464 · 1 reaction · 1 assignee ·
Maintainers usually reply within 1 day
Similar issues
-
check:passed streams:add
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Fission-AI/OpenSpec#1986 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
nestjs/docs.nestjs.com#3554 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
publicodes/publicodes#868 ·
-
namespace operations
Difficulty 1/5 Under an hour Newbie friendliness 78/100
EclipseFdn/open-vsx.org#13488 ·
Maintainers usually reply within 2 days