Skip to content

Commit 521168f

Browse files
bloveclaude
andauthored
feat(ag-ui): customEvents signal for live a2ui streaming parity (#606)
* docs(spec): standalone examples/ag-ui design (full-parity ag-ui demo) Peer to examples/chat over the AG-UI transport. Three sequenced parts: (1) add customEvents signal to @threadplane/ag-ui (closes the one live- a2ui-streaming gap; valuable beyond the example) + docs; (2) the examples/ag-ui app/python/smoke with full feature parity; (3) deploy via a dedicated Railway service + ag-ui-demo.threadplane.ai. Feasibility resolved by static spike: everything above the neutral Agent contract is transport-agnostic; only the customEvents signal is missing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(plan): ag-ui customEvents signal implementation plan (PR 1) * feat(ag-ui): accumulate non-interrupt CUSTOM events into a customEvents store signal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ag-ui): expose customEvents signal on toAgent (AgUiAgent) for live a2ui streaming Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(ag-ui): document customEvents signal + live a2ui streaming; regen API reference Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8bcc973 commit 521168f

9 files changed

Lines changed: 748 additions & 24 deletions

File tree

apps/website/content/docs/ag-ui/api/api-docs.json

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,112 @@
402402
],
403403
"examples": []
404404
},
405+
{
406+
"name": "AgUiAgent",
407+
"kind": "interface",
408+
"description": "The neutral Agent contract, widened with the AG-UI adapter's optional\n`customEvents` signal (the chat composition feature-detects it to enable\nlive a2ui streaming). Mirrors langgraph's LangGraphAgent extension.",
409+
"properties": [
410+
{
411+
"name": "customEvents",
412+
"type": "Signal<CustomStreamEvent[]>",
413+
"description": "",
414+
"optional": false
415+
},
416+
{
417+
"name": "error",
418+
"type": "Signal<unknown>",
419+
"description": "",
420+
"optional": false
421+
},
422+
{
423+
"name": "events$",
424+
"type": "Observable<AgentEvent>",
425+
"description": "",
426+
"optional": false
427+
},
428+
{
429+
"name": "interrupt",
430+
"type": "Signal<AgentInterrupt | undefined>",
431+
"description": "",
432+
"optional": true
433+
},
434+
{
435+
"name": "isLoading",
436+
"type": "Signal<boolean>",
437+
"description": "",
438+
"optional": false
439+
},
440+
{
441+
"name": "messages",
442+
"type": "Signal<Message[]>",
443+
"description": "",
444+
"optional": false
445+
},
446+
{
447+
"name": "regenerate",
448+
"type": "(assistantMessageIndex: number) => Promise<void>",
449+
"description": "Discards the assistant message at the given index AND all messages after\nit, then re-runs the agent against the trimmed conversation tail. The\npreceding user message (at index - 1) is preserved and re-submitted as\nthe agent's input. No new user message is added to the history.\n\nThrows if the message at `index` is not 'assistant' role, or if the\nagent is currently loading another response.",
450+
"optional": false
451+
},
452+
{
453+
"name": "state",
454+
"type": "Signal<Record<string, unknown>>",
455+
"description": "",
456+
"optional": false
457+
},
458+
{
459+
"name": "status",
460+
"type": "Signal<AgentStatus>",
461+
"description": "",
462+
"optional": false
463+
},
464+
{
465+
"name": "stop",
466+
"type": "() => Promise<void>",
467+
"description": "",
468+
"optional": false
469+
},
470+
{
471+
"name": "subagents",
472+
"type": "Signal<Map<string, Subagent>>",
473+
"description": "",
474+
"optional": true
475+
},
476+
{
477+
"name": "submit",
478+
"type": "(input: AgentSubmitInput, opts: AgentSubmitOptions) => Promise<void>",
479+
"description": "",
480+
"optional": false
481+
},
482+
{
483+
"name": "toolCalls",
484+
"type": "Signal<ToolCall[]>",
485+
"description": "",
486+
"optional": false
487+
}
488+
],
489+
"examples": []
490+
},
491+
{
492+
"name": "CustomStreamEvent",
493+
"kind": "interface",
494+
"description": "A custom event surfaced to consumers via the agent's `customEvents` signal.\nMirrors the LangGraph adapter's CustomStreamEvent shape so the chat\na2ui partial-args bridge consumes both transports identically.",
495+
"properties": [
496+
{
497+
"name": "data",
498+
"type": "unknown",
499+
"description": "Arbitrary payload from the backend (JSON-string values are parsed).",
500+
"optional": false
501+
},
502+
{
503+
"name": "name",
504+
"type": "string",
505+
"description": "Event name set by the backend (e.g. 'a2ui-partial', 'state_update').",
506+
"optional": false
507+
}
508+
],
509+
"examples": []
510+
},
405511
{
406512
"name": "ToAgentOptions",
407513
"kind": "interface",
@@ -495,7 +601,7 @@
495601
"name": "toAgent",
496602
"kind": "function",
497603
"description": "Wraps an AG-UI AbstractAgent into the runtime-neutral Agent contract.\n\nThe adapter subscribes to source.subscribe({ onEvent }) and reduces every\nevent into the produced Agent's signals. submit() optimistically appends the\nuser message to both our signals and the source agent's internal message\nlist, then calls source.runAgent(). stop() calls source.abortRun().\n\nSubscription cleanup: the returned Agent does NOT manage its own lifetime.\nCallers using DI should rely on the provider's destroy hook; direct callers\nof toAgent() should treat the returned object's lifecycle as tied to the\nagent instance they constructed. The subscriber registered via\nsource.subscribe() will fire for the lifetime of source.",
498-
"signature": "toAgent(source: AbstractAgent<>, options: ToAgentOptions): Agent",
604+
"signature": "toAgent(source: AbstractAgent<>, options: ToAgentOptions): AgUiAgent",
499605
"params": [
500606
{
501607
"name": "source",
@@ -511,7 +617,7 @@
511617
}
512618
],
513619
"returns": {
514-
"type": "Agent",
620+
"type": "AgUiAgent",
515621
"description": ""
516622
},
517623
"examples": []

apps/website/content/docs/ag-ui/concepts/architecture.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ When the user submits input, the adapter builds a user message, appends it local
9494

9595
This is optimistic on purpose. The user message appears immediately while the backend starts the run.
9696

97+
## Live a2ui streaming via `customEvents`
98+
99+
The adapter exposes a `customEvents` signal on the agent returned by
100+
`toAgent` / `injectAgent`, accumulating every non-`on_interrupt` `CUSTOM`
101+
AG-UI event for the current run (reset on each `RUN_STARTED`). The chat
102+
composition feature-detects this signal to drive **progressive** a2ui
103+
surface rendering — token-by-token, as the backend streams `a2ui-partial`
104+
events — matching the LangGraph adapter. Without it, a2ui still renders from
105+
the final tool-call surface; with it, surfaces build up live.
106+
97107
## Provider choices
98108

99109
Use `provideAgent()` when you have a real AG-UI HTTP endpoint.

0 commit comments

Comments
 (0)