Skip to content

Commit 133a9d9

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/error-ux-agent-error
2 parents a626483 + 05cd321 commit 133a9d9

16 files changed

Lines changed: 1471 additions & 112 deletions

apps/website/content/docs/chat/api/api-docs.json

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3656,12 +3656,6 @@
36563656
"params": [],
36573657
"examples": [],
36583658
"properties": [
3659-
{
3660-
"name": "latestMessageContent",
3661-
"type": "Signal<string>",
3662-
"description": "",
3663-
"optional": false
3664-
},
36653659
{
36663660
"name": "state",
36673661
"type": "Signal<TraceState>",
@@ -3675,7 +3669,47 @@
36753669
"optional": false
36763670
}
36773671
],
3678-
"methods": []
3672+
"methods": [
3673+
{
3674+
"name": "textOf",
3675+
"signature": "textOf(m: Message): string",
3676+
"description": "",
3677+
"params": [
3678+
{
3679+
"name": "m",
3680+
"type": "Message",
3681+
"description": "",
3682+
"optional": false
3683+
}
3684+
]
3685+
},
3686+
{
3687+
"name": "toolCallsFor",
3688+
"signature": "toolCallsFor(m: Message): ToolCall[]",
3689+
"description": "",
3690+
"params": [
3691+
{
3692+
"name": "m",
3693+
"type": "Message",
3694+
"description": "",
3695+
"optional": false
3696+
}
3697+
]
3698+
},
3699+
{
3700+
"name": "toToolCallInfo",
3701+
"signature": "toToolCallInfo(tc: ToolCall): ToolCallInfo",
3702+
"description": "",
3703+
"params": [
3704+
{
3705+
"name": "tc",
3706+
"type": "ToolCall",
3707+
"description": "",
3708+
"optional": false
3709+
}
3710+
]
3711+
}
3712+
]
36793713
},
36803714
{
36813715
"name": "ChatSubagentsComponent",
@@ -6725,6 +6759,12 @@
67256759
"type": "string",
67266760
"description": "Tool call ID that spawned this subagent.",
67276761
"optional": false
6762+
},
6763+
{
6764+
"name": "toolCalls",
6765+
"type": "Signal<ToolCall[]>",
6766+
"description": "The subagent's own tool calls (name/args/result), referenced by\n`Message.toolCallIds` in `messages`. Optional: adapters that don't surface\nsubagent tool calls omit it; consumers default to `[]`.",
6767+
"optional": true
67286768
}
67296769
],
67306770
"examples": []

docs/superpowers/plans/2026-06-18-ag-ui-multi-message-subagent-cards.md

Lines changed: 438 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Multi-message subagent cards (AG-UI) — Design
2+
3+
**Status:** Approved (brainstorm 2026-06-18)
4+
5+
**Goal:** Extend the AG-UI subagent cards so each subagent accumulates and renders a **full transcript** — multiple assistant text turns, reasoning, and tool calls (with results) — streamed live, instead of a single accumulating text blob.
6+
7+
## Background
8+
9+
F5 shipped subagent cards over AG-UI via native ACTIVITY events: a graph emits a `subagent_activity` CUSTOM event → an owned `ActivityEmittingAgent._dispatch_event` maps it to `ACTIVITY_SNAPSHOT`/`ACTIVITY_DELTA` → the L1 reducer (`libs/ag-ui`) accumulates activities generically → `toAgent()` projects `activityType==='subagent'` to the neutral `Subagent` contract → `chat-subagents`/`chat-subagent-card` render.
10+
11+
Today the activity `content` carries a single accumulating `text` field; the projection synthesizes **one** `Message` from it (`to-agent.ts` `subagentFor`), and the card shows that latest text. This design carries the subagent's full message transcript instead.
12+
13+
The neutral types already support a transcript: `Message` has `role: 'tool'`, `toolCallId`, `toolCallIds`, and `reasoning`; `ToolCall` has `{id, name, args, status, result?, error?}`. The only neutral-contract addition is exposing the subagent's tool calls.
14+
15+
## Architecture (Approach 1 — fat activity content)
16+
17+
One ACTIVITY per subagent (unchanged identity model), whose `content` carries the transcript as arrays; streamed via JSON-Patch DELTAs. The L1 reducer is **untouched** (it already applies arbitrary ACTIVITY patches).
18+
19+
### A. Neutral contract (libs/chat)
20+
21+
Add `toolCalls` to `Subagent` (`libs/chat/src/lib/agent/subagent.ts`):
22+
23+
```ts
24+
export interface Subagent {
25+
toolCallId: string;
26+
name?: string;
27+
status: Signal<SubagentStatus>;
28+
messages: Signal<Message[]>;
29+
toolCalls: Signal<ToolCall[]>; // NEW — the subagent's tool calls (name/args/result)
30+
state: Signal<Record<string, unknown>>;
31+
}
32+
```
33+
34+
`Message` and `ToolCall` are unchanged.
35+
36+
### B. Wire shape (L3 ↔ L1 activity `content`)
37+
38+
```
39+
{ toolCallId, name, status,
40+
messages: [{ id, role, content, toolCallIds?, reasoning? }, …],
41+
toolCalls: [{ id, name, args, status, result?, error? }, …] }
42+
```
43+
44+
DELTAs (RFC-6902 JSON-Patch), live:
45+
- `ACTIVITY_SNAPSHOT` on subagent start → `{toolCallId, name, status:'running', messages:[], toolCalls:[]}`.
46+
- New message: `{op:'add', path:'/messages/-', value:{id, role:'assistant', content:''}}`.
47+
- Token stream into the in-progress message: `{op:'replace', path:'/messages/<n>/content', value:<accumulated text>}` (text_so_far pattern — replace, since JSON-Patch has no string append).
48+
- Tool call: `{op:'add', path:'/toolCalls/-', value:{id, name, args, status:'running'}}` plus `{op:'replace', path:'/messages/<n>/toolCallIds', value:[…]}`.
49+
- Tool result: `{op:'replace', path:'/toolCalls/<k>/status', value:'complete'}` + `{op:'replace', path:'/toolCalls/<k>/result', value:<result>}`.
50+
- Finish: `{op:'replace', path:'/status', value:'complete'}`.
51+
52+
The reducer's existing `applyPatch` handles all of these — **no reducer change**.
53+
54+
### C. Projection (libs/ag-ui `to-agent.ts`)
55+
56+
`subagentFor` maps the new arrays, with explicit back-compat:
57+
58+
```ts
59+
messages: computed<Message[]>(() => {
60+
const c = entry.content();
61+
if (Array.isArray(c['messages'])) {
62+
return (c['messages'] as RawMsg[]).map((m) => ({
63+
id: m.id, role: m.role, content: m.content ?? '',
64+
...(m.toolCallIds ? { toolCallIds: m.toolCallIds } : {}),
65+
...(m.reasoning ? { reasoning: m.reasoning } : {}),
66+
}));
67+
}
68+
// Back-compat: single accumulating text (current shipped emitter).
69+
return [{ id, role: 'assistant', content: String(c['text'] ?? '') }];
70+
}),
71+
toolCalls: computed<ToolCall[]>(() => {
72+
const c = entry.content();
73+
return Array.isArray(c['toolCalls']) ? (c['toolCalls'] as ToolCall[]) : [];
74+
}),
75+
```
76+
77+
The stable per-subagent wrapper (keyed by messageId) and the prune loop are unchanged.
78+
79+
### D. Card rendering (libs/chat `chat-subagent-card`, hybrid)
80+
81+
Replace the single "latest message" block with an ordered transcript:
82+
83+
```html
84+
@for (m of subagent().messages(); track m.id) {
85+
<div class="sac__msg" [attr.data-role]="m.role">
86+
@if (m.reasoning) { <div class="sac__reasoning">{{ m.reasoning }}</div> }
87+
@if (textOf(m); as t) { <!-- markdown render of t --> }
88+
@for (tc of toolCallsFor(m); track tc.id) {
89+
<chat-tool-call-card [toolCall]="toToolCallInfo(tc)" />
90+
}
91+
</div>
92+
}
93+
```
94+
95+
- `toolCallsFor(m)` resolves `m.toolCallIds` against `subagent().toolCalls()`.
96+
- Reuse the existing **`ChatToolCallCardComponent`** (per-call card; result included) — the "hard part" of tool rendering — without the `Agent`-coupled `chat-tool-calls` wrapper.
97+
- The in-progress (last) message's `content` updates in place; `track m.id` keeps the DOM stable (no `@for` re-creation).
98+
- Compact, nested styling distinct from the main thread.
99+
100+
### E. L3 emission (examples/ag-ui graph)
101+
102+
Extend `SubagentStreamHandler` + `activity_transform.py`:
103+
- on assistant token → `add` a message (first token) then `replace` its content (text_so_far);
104+
- on tool call → `add` to `toolCalls` + `replace` the assistant message's `toolCallIds`;
105+
- on tool result → `replace` that tool call's `result`/`status`.
106+
107+
Transport unchanged from F5 (`adispatch_custom_event``subagent_activity` CUSTOM → `ActivityEmittingAgent._dispatch_event` → ACTIVITY_DELTA). `activity_transform` gains small pure patch-builders for messages/toolcalls. The research subagraph is already LLM-driven (reasoning + a search tool + a summary), so it produces a genuine multi-message transcript for the demo.
108+
109+
The cockpit `ag-ui/subagents` capability stays on the single-text path (exercises the back-compat branch) — not migrated here.
110+
111+
## Data flow
112+
113+
```
114+
research subgraph stream
115+
→ SubagentStreamHandler (delineates messages/tool calls)
116+
→ adispatch_custom_event('subagent_activity', {phase, ...})
117+
→ ActivityEmittingAgent._dispatch_event → activity_transform → ACTIVITY_SNAPSHOT/DELTA
118+
→ L1 reducer applyPatch (generic, unchanged) → activities store
119+
→ toAgent subagentFor → Subagent{ messages[], toolCalls[] }
120+
→ chat-subagents → chat-subagent-card → ordered transcript + reused tool-call cards
121+
```
122+
123+
## Error handling
124+
125+
- Malformed/partial wire entries: the projection defensively defaults (`content ?? ''`, non-array → `[]`); a message missing an `id` falls back to its index for `track`.
126+
- A tool result arriving before its call (out-of-order): `toolCallsFor` simply finds nothing yet; the card renders the call once it appears (id lookup, never positional).
127+
- Back-compat: emitters that still send `text` render as a single assistant message; emitters sending `messages` render the full transcript. Both paths are unit-tested.
128+
129+
## Testing
130+
131+
- **Unit (libs/ag-ui):** projection maps `content.messages``Message[]` and `content.toolCalls``ToolCall[]`; the `text` back-compat fallback; stable wrapper identity + content liveness across DELTAs.
132+
- **Unit (libs/chat):** `chat-subagent-card` renders an ordered transcript (≥2 messages), reasoning, and a reused tool-call card; updates live when the last message's content changes; `track m.id` stability.
133+
- **Unit (python):** `activity_transform` message/toolcall patch-builders produce the expected JSON-Patch ops for each phase.
134+
- **e2e (examples/ag-ui):** during a research run, a subagent card surfaces ≥2 messages and a tool-call card — durable-signal assertions (F5 e2e precedent; robust under aimock replay).
135+
- **Gates:** `ag-ui` + `chat` lint/test; examples/ag-ui e2e; cockpit `ag-ui/subagents` e2e still green (back-compat); Railway deploy regen if any `cockpit/ag-ui/*` source changes; api-docs regen (the `Subagent.toolCalls` addition is public API).
136+
137+
## Scope guardrails (YAGNI)
138+
139+
- No neutral-`Message` change; the L1 reducer is untouched.
140+
- `role:'tool'` result messages are not separately rendered (the tool-call card already shows the result).
141+
- The cockpit `ag-ui/subagents` capability is not migrated (it validates the back-compat branch).
142+
- No new chat-tool-calls decoupling refactor — reuse the per-call card directly.
143+
144+
## Public API delta
145+
146+
- `Subagent.toolCalls: Signal<ToolCall[]>` (additive).
147+
- No other public surface changes.

examples/ag-ui/angular/e2e/fixtures/subagent.json

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22
"fixtures": [
33
{
44
"match": {
5-
"userMessage": "Research the Louvre and summarize",
5+
"userMessage": "Research Angular signals and summarize",
66
"hasToolResult": true
77
},
88
"response": {
9-
"content": "Here's what I found about the Louvre: it's a major Paris museum and one of the most visited in the world, opened in the late 18th century and home to tens of thousands of works across many collections. Let me know if you'd like detail on a particular wing or era."
9+
"content": "Here's what the research subagent found about Angular signals: they are a fine-grained reactivity primitive — a signal holds a value, computed() derives from other signals, and effect() reacts to changes, all without manual subscriptions. Let me know if you'd like a code example."
1010
}
1111
},
1212
{
1313
"match": {
14-
"userMessage": "Research the Louvre and summarize"
14+
"userMessage": "Research Angular signals and summarize"
1515
},
1616
"response": {
1717
"toolCalls": [
1818
{
1919
"name": "research",
2020
"arguments": {
21-
"topic": "the Louvre",
21+
"topic": "Angular signals",
2222
"subagent_type": "research"
2323
}
2424
}
@@ -27,10 +27,29 @@
2727
},
2828
{
2929
"match": {
30-
"userMessage": "Topic: the Louvre"
30+
"userMessage": "Topic: Angular signals",
31+
"systemMessage": "Do NOT call any more tools",
32+
"hasToolResult": true
33+
},
34+
"response": {
35+
"content": "Angular signals are a fine-grained reactivity primitive. A signal holds a value, computed() derives from other signals, and effect() reacts to changes — all without manual subscriptions or zone.js. This makes change detection more precise and is the foundation for zoneless Angular."
36+
}
37+
},
38+
{
39+
"match": {
40+
"userMessage": "Topic: Angular signals",
41+
"systemMessage": "Call `lookup` exactly once",
42+
"hasToolResult": false
3143
},
3244
"response": {
33-
"content": "The Louvre opened in 1793 and holds about 35,000 works."
45+
"toolCalls": [
46+
{
47+
"name": "lookup",
48+
"arguments": {
49+
"query": "Angular signals"
50+
}
51+
}
52+
]
3453
}
3554
}
3655
]

0 commit comments

Comments
 (0)