Skip to content

AG-UI activity support is non-functional: events are never processed, and role: 'activity' messages are destroyed #1286

Description

@DoronYitz

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:

{ "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:

  1. 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.
  2. 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

  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions