AG-UI activity support is non-functional: events are never processed, and `role: 'activity'` messages are destroyed
维护者通常 1 天内回复
@jherr 已经在做这个了。
开始于 2026年9月17日。
评估
这个 Issue 还没有评估数据。
描述
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.
- 主要语言
- TypeScript
- 星标
- 3.1k
- 派生
- 340
- 平均合并
- 2 天 10 小时
- 30 天内合并 PR
- 174
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
TanStack/ai 的其他 Issue
-
StreamProcessor ignores the stream's message ids and files agent-loop calls on the wrong assistant message可能已有人在做 @tombeckenham 今天认领。 未关闭waiting-on: maintainer
维护者通常 1 天内回复
-
StreamProcessor drops TEXT_MESSAGE_CHUNK events without a warning可能已有人在做 @AlemTuzlak 于 1 天前认领。 未关闭waiting-on: maintainer
维护者通常 1 天内回复
-
feat(ai): add a timeout option to chat(), matching the media activities可能已有人在做 @AlemTuzlak 于 2 天前认领。 未关闭waiting-on: maintainer
维护者通常 1 天内回复
-
Does a hosted provider package have to carry a key?可能已有人在做 @AlemTuzlak 于 3 天前认领。 未关闭waiting-on: author
TanStack/ai#1506 · 1 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
Client tools never execute against spec-compliant AG-UI servers (plain `success` RUN_FINISHED)可能已有人在做 @jherr 于 5 天前认领。 未关闭waiting-on: maintainer
TanStack/ai#1464 · 1 个 reaction · 已指派 1 人 ·
维护者通常 1 天内回复
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 68/100
microsoft/vscode-livepreview#876 ·
维护者通常 1 天内回复
-
needs-triage
难度 1/5 1 小时以内 新手友好度 90/100
JustJarethB/invoicer#54 ·
-
ICP 1.2.0 shows a scheduled task's interval in milliseconds under the label "Interval (In seconds)"未关闭Needs Triage Type/Bug
难度 2/5 1-3 小时 新手友好度 68/100
wso2/product-integrator#2585 ·
维护者通常 1 天内回复
-
check:passed streams:add
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 1 天内回复
-
design
难度 2/5 1-3 小时 新手友好度 72/100
MTES-MCT/monitor-field#119 ·
维护者通常 1 天内回复