Skip to content

Commit 16a804e

Browse files
authored
[codex] Track LangGraph subagent streams (#165)
* feat(langgraph): track subagent streams * fix(langgraph): keep subagent tracking bundle-light
1 parent 8aa2bcd commit 16a804e

15 files changed

Lines changed: 757 additions & 35 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
9494
| Tool call progress | `toolProgress()` | `toolProgress` |
9595
| Tool calls with results | `toolCalls()` | `toolCalls` |
9696
| Branch / history | `branch()` / `history()` | `branch` / `history` |
97-
| Subagent streaming | Planned next | `subagents` / `activeSubagents` |
97+
| Subagent streaming | `subagents()` / `activeSubagents()` | `subagents` / `activeSubagents` |
9898
| Reactive thread switching | `Signal<string \| null>` input | prop |
9999
| Submit | `submit(values, opts?)` | `submit(values, opts?)` |
100100
| Stop | `stop()` | `stop()` |

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -739,9 +739,27 @@
739739
"kind": "interface",
740740
"description": "An event emitted by a LangGraph stream.",
741741
"properties": [
742+
{
743+
"name": "messageMetadata",
744+
"type": "Record<string, unknown>",
745+
"description": "",
746+
"optional": true
747+
},
748+
{
749+
"name": "messages",
750+
"type": "unknown[]",
751+
"description": "",
752+
"optional": true
753+
},
754+
{
755+
"name": "namespace",
756+
"type": "string[]",
757+
"description": "",
758+
"optional": true
759+
},
742760
{
743761
"name": "type",
744-
"type": "\"error\" | \"values\" | \"messages\" | `messages/${string}` | \"updates\" | \"tools\" | \"custom\" | \"metadata\" | \"checkpoints\" | \"tasks\" | \"debug\" | \"events\" | \"interrupt\" | \"interrupts\"",
762+
"type": "\"error\" | \"values\" | `values|${string}` | \"messages\" | `messages|${string}` | `messages/${string}` | `messages/${string}|${string}` | \"updates\" | `updates|${string}` | \"tools\" | `tools|${string}` | \"custom\" | `custom|${string}` | `error|${string}` | \"metadata\" | \"checkpoints\" | `checkpoints|${string}` | \"tasks\" | `tasks|${string}` | \"debug\" | `debug|${string}` | \"events\" | `events|${string}` | \"interrupt\" | \"interrupts\"",
745763
"description": "Event type identifier (e.g., 'values', 'messages', 'error', 'interrupt').",
746764
"optional": false
747765
}
@@ -759,6 +777,12 @@
759777
"description": "Messages from the subagent conversation.",
760778
"optional": false
761779
},
780+
{
781+
"name": "name",
782+
"type": "string",
783+
"description": "Optional human-readable subagent type/name.",
784+
"optional": true
785+
},
762786
{
763787
"name": "status",
764788
"type": "Signal<\"running\" | \"error\" | \"pending\" | \"complete\">",

apps/website/content/docs/agent/concepts/agent-architecture.mdx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -460,12 +460,8 @@ export class MultiAgentComponent {
460460
</Tab>
461461
</Tabs>
462462

463-
<Callout type="warning" title="Subagent signal status">
464-
Tool calls, tool progress, and tool results stream today. Dedicated `subagents()` and `activeSubagents()` tracking is planned for the next implementation phase; use `toolCalls()` and `toolProgress()` for current delegated-work visibility.
465-
</Callout>
466-
467463
<Callout type="tip" title="subagentToolNames is the key">
468-
The `subagentToolNames` option will tell agent() which graph nodes are subagents. Until dedicated tracking lands, subagent execution appears through the regular tool-call and tool-progress signals.
464+
The `subagentToolNames` option tells agent() which tool calls spawn subagents. The default Deep Agents tool name is `task`; set this option when your graph uses custom delegation tool names.
469465
</Callout>
470466

471467
## Error Handling and Recovery
@@ -662,7 +658,7 @@ builder.add_node("analyst", analyst_subgraph)
662658
builder.add_conditional_edges("supervisor", route_to_agent)
663659
```
664660

665-
**Angular signals used today:** `messages()`, `toolCalls()`, `toolProgress()`, `status()`; dedicated `subagents()` / `activeSubagents()` tracking is planned next.
661+
**Angular signals used:** `messages()`, `subagents()`, `activeSubagents()`, `toolCalls()`, `toolProgress()`, `status()`
666662

667663
### Decision Matrix
668664

apps/website/content/docs/agent/concepts/langgraph-basics.mdx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,16 @@ builder.add_node("analyst", analyst_subgraph)
227227
builder.add_conditional_edges("supervisor", lambda s: s["next_agent"])
228228
```
229229

230-
**Angular connection:** Dedicated subagent tracking is planned for the next implementation phase. Today, track delegated work through tool progress and tool results:
230+
**Angular connection:** Track delegated work through dedicated subagent signals:
231231
```typescript
232232
const orchestrator = agent<OrchestratorState>({
233233
assistantId: 'orchestrator',
234+
subagentToolNames: ['task'],
235+
filterSubagentMessages: true,
234236
});
235237

236-
const activeTools = computed(() => orchestrator.toolProgress());
237-
const completedTools = computed(() => orchestrator.toolCalls());
238+
const workers = computed(() => orchestrator.activeSubagents());
239+
const workerCount = computed(() => workers().length);
238240
```
239241

240242
### Pattern 4: Persistent Conversations
@@ -322,7 +324,8 @@ agent.branch() // Signal<string> — time-travel branch
322324

323325
agent.toolCalls() // Signal<ToolCallWithResult[]> — tool results
324326
agent.toolProgress() // Signal<ToolProgress[]> — active tool execution
325-
// Dedicated subagent signals are planned next.
327+
agent.subagents() // Signal<Map<string, Subagent>> — delegated agents
328+
agent.activeSubagents() // Signal<SubagentStreamRef[]> — running workers
326329
```
327330

328331
</Tab>

apps/website/content/docs/agent/guides/subgraphs.mdx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ Subgraphs let you compose complex agents from smaller, focused units. agent() tr
66
LangGraph calls them subgraphs (modular graph composition). Deep Agents calls them subagents (task delegation). agent() supports both patterns through the same API.
77
</Callout>
88

9-
<Callout type="warning" title="Implementation status">
10-
Tool calls, tool progress, and tool results stream today. The `subagents()` / `activeSubagents()` examples below describe the planned Phase 2 API; until that lands, use `toolCalls()` and `toolProgress()` for visibility into delegated work.
11-
</Callout>
12-
139
## How subgraph composition works
1410

1511
Subgraph composition starts on the agent side. Each subgraph is a fully compiled `StateGraph` that can be added as a node in a parent graph.

apps/website/src/components/docs/mdx/FeatureChips.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const CHIPS: ChipData[] = [
1717
{ icon: '💾', title: 'Persistence', signal: 'threadId', href: '/docs/guides/persistence', gradient: 'linear-gradient(135deg, rgba(16,185,129,0.06), rgba(52,199,89,0.08))', border: 'rgba(16,185,129,0.1)' },
1818
{ icon: '✋', title: 'Interrupts', signal: 'chat.interrupt()', href: '/docs/guides/interrupts', gradient: 'linear-gradient(135deg, rgba(232,147,12,0.06), rgba(245,180,60,0.08))', border: 'rgba(232,147,12,0.1)' },
1919
{ icon: '⏪', title: 'Time Travel', signal: 'chat.history()', href: '/docs/guides/time-travel', gradient: 'linear-gradient(135deg, rgba(221,0,49,0.05), rgba(255,100,130,0.07))', border: 'rgba(221,0,49,0.08)' },
20-
{ icon: '🔀', title: 'Subagents', signal: 'Phase 2', href: '/docs/guides/subgraphs', gradient: 'linear-gradient(135deg, rgba(0,64,144,0.05), rgba(0,100,180,0.07))', border: 'rgba(0,64,144,0.08)' },
20+
{ icon: '🔀', title: 'Subagents', signal: 'chat.subagents()', href: '/docs/guides/subgraphs', gradient: 'linear-gradient(135deg, rgba(0,64,144,0.05), rgba(0,100,180,0.07))', border: 'rgba(0,64,144,0.08)' },
2121
{ icon: '🔧', title: 'Tool Calls', signal: 'chat.toolCalls()', href: '/docs/guides/streaming', gradient: 'linear-gradient(135deg, rgba(100,80,200,0.05), rgba(120,100,210,0.07))', border: 'rgba(100,80,200,0.08)' },
2222
{ icon: '🧪', title: 'Testing', signal: 'MockTransport', href: '/docs/guides/testing', gradient: 'linear-gradient(135deg, rgba(16,185,129,0.05), rgba(40,200,140,0.07))', border: 'rgba(16,185,129,0.08)' },
2323
];

docs/limitations.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,17 @@ automatically on `submit()` calls.
7979

8080
---
8181

82-
### Limitation: subagent tracking is deferred
82+
### Limitation: subagent helper methods are not exposed
8383

84-
**Feature:** `subagents()` / `activeSubagents()` / `filterSubagentMessages` /
85-
`subagentToolNames`
84+
**Feature:** `getSubagent()` / `getSubagentsByType()` /
85+
`getSubagentsByMessage()`
8686

87-
**React behavior:** `useStream()` can track Deep Agent subagent execution by
88-
combining subgraph stream events with tool-call registration.
87+
**React behavior:** `useStream()` exposes helper methods for looking up
88+
subagent streams by tool call ID, subagent type, or triggering message.
8989

90-
**Angular behavior:** Tool calls, tool progress, message metadata, and
91-
per-message tool results are implemented. Subagent-specific stream routing is
92-
deferred to the next implementation phase.
90+
**Angular behavior:** `subagents()` and `activeSubagents()` are implemented.
91+
Use the `subagents()` map directly for lookups. Helper methods can be added
92+
later if Angular consumers need parity beyond the signal surface.
9393

94-
**Workaround:** Use `toolCalls()` and `toolProgress()` for tool-level visibility
95-
until dedicated subagent tracking lands.
94+
**Workaround:** Read from `subagents().get(toolCallId)` or filter
95+
`[...subagents().values()]` in a computed signal.

libs/langgraph/src/lib/agent.fn.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,62 @@ describe('agent', () => {
330330
});
331331
});
332332

333+
it('subagents() and activeSubagents() expose delegated work as signals', async () => {
334+
const transport = new MockAgentTransport();
335+
const ref = withInjectionContext(() =>
336+
agent({
337+
apiUrl: '',
338+
assistantId: 'a',
339+
transport,
340+
throttle: false,
341+
subagentToolNames: ['task'],
342+
filterSubagentMessages: true,
343+
})
344+
);
345+
346+
ref.submit({ message: 'hello' });
347+
transport.emit([{
348+
type: 'messages',
349+
messages: [{
350+
id: 'ai-1',
351+
type: 'ai',
352+
content: '',
353+
tool_calls: [{
354+
id: 'call-1',
355+
name: 'task',
356+
args: { subagent_type: 'researcher', description: 'Research Angular signals' },
357+
}],
358+
}],
359+
} satisfies StreamEvent]);
360+
transport.emit([{
361+
type: 'messages|tools:call-1' as StreamEvent['type'],
362+
namespace: ['tools:call-1'],
363+
messages: [{ id: 'sub-ai-1', type: 'ai', content: 'Subagent note' }],
364+
messageMetadata: { checkpoint_ns: 'tools:call-1|model:abc' },
365+
} satisfies StreamEvent]);
366+
367+
await new Promise(r => setTimeout(r, 20));
368+
369+
expect(ref.activeSubagents()).toHaveLength(1);
370+
expect(ref.activeSubagents()[0].status()).toBe('running');
371+
expect(ref.subagents().get('call-1')?.name).toBe('researcher');
372+
expect(ref.subagents().get('call-1')?.messages()).toEqual([
373+
expect.objectContaining({ id: 'sub-ai-1', role: 'assistant', content: 'Subagent note' }),
374+
]);
375+
expect(ref.messages()).toHaveLength(1);
376+
expect(ref.messages()[0].id).toBe('ai-1');
377+
378+
transport.emit([{
379+
type: 'messages',
380+
messages: [{ id: 'tool-1', type: 'tool', tool_call_id: 'call-1', content: 'done', status: 'success' }],
381+
} satisfies StreamEvent]);
382+
transport.close();
383+
await new Promise(r => setTimeout(r, 20));
384+
385+
expect(ref.activeSubagents()).toHaveLength(0);
386+
expect(ref.subagents().get('call-1')?.status()).toBe('complete');
387+
});
388+
333389
it('events$ is an Observable-like with .subscribe', () => {
334390
const transport = new MockAgentTransport();
335391
const ref = withInjectionContext(() =>

libs/langgraph/src/lib/agent.fn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ function toInterrupt(ix: Interrupt<unknown>): AgentInterrupt {
348348
function toSubagent(sa: SubagentStreamRef): Subagent {
349349
return {
350350
toolCallId: sa.toolCallId,
351+
name: sa.name,
351352
status: sa.status,
352353
messages: computed(() => sa.messages().map(toMessage)) as Signal<Message[]>,
353354
state: sa.values as Signal<Record<string, unknown>>,

libs/langgraph/src/lib/agent.types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,31 @@ export interface StreamEvent {
4343
/** Event type identifier (e.g., 'values', 'messages', 'error', 'interrupt'). */
4444
type:
4545
| 'values'
46+
| `values|${string}`
4647
| 'messages'
48+
| `messages|${string}`
4749
| `messages/${string}`
50+
| `messages/${string}|${string}`
4851
| 'updates'
52+
| `updates|${string}`
4953
| 'tools'
54+
| `tools|${string}`
5055
| 'custom'
56+
| `custom|${string}`
5157
| 'error'
58+
| `error|${string}`
5259
| 'metadata'
5360
| 'checkpoints'
61+
| `checkpoints|${string}`
5462
| 'tasks'
63+
| `tasks|${string}`
5564
| 'debug'
65+
| `debug|${string}`
5666
| 'events'
67+
| `events|${string}`
5768
| 'interrupt'
5869
| 'interrupts';
70+
namespace?: string[];
5971
messages?: unknown[];
6072
messageMetadata?: Record<string, unknown>;
6173
[key: string]: unknown;
@@ -122,6 +134,8 @@ export interface AgentOptions<T, ResolvedBag extends BagTemplate> {
122134
export interface SubagentStreamRef {
123135
/** The tool call ID that spawned this subagent. */
124136
toolCallId: string;
137+
/** Optional human-readable subagent type/name. */
138+
name?: string;
125139
/** Current execution status of the subagent. */
126140
status: Signal<'pending' | 'running' | 'complete' | 'error'>;
127141
/** Current state values from the subagent. */

0 commit comments

Comments
 (0)