Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,14 @@ Its module docstring says so outright:

```text
Mirrors cockpit/chat/subagents' orchestrator + `task` tool + `_run_subagent`
structure, but each dispatch emits `subagent_activity` CUSTOM events
structure, but each dispatch emits `subagent_activity` CUSTOM events [...]
The backend's SubagentEmittingAgent expands those CUSTOM events into the
protocol's standard SUBAGENT_STARTED / TEXT_MESSAGE_* (attributed via
subagentRunId) / SUBAGENT_FINISHED / SUBAGENT_ERROR events
```

The thing that differs is the transport: AG-UI's already carries a first-class delegation event.
So the specialists stayed a flat `async` helper and progress reaches the frontend as a custom event dispatched from the tool body.
The thing that differs is the transport: AG-UI already carries first-class delegation events — `SUBAGENT_STARTED`, `SUBAGENT_FINISHED`, and content events attributed to a child run.
So the specialists stayed a flat `async` helper, the tool body dispatches its progress as custom events, and a thin wrapper on the server expands those into the protocol's standard subagent events on the wire.

The subgraph was never required by the feature.
It was required by the transport.
Expand All @@ -209,7 +212,7 @@ A node is already a unit.

When the child really is a different graph — and the repo has exactly one of those, which is the case I owe you after arguing the other side this whole time.

Our `examples/ag-ui` demo runs on that same AG-UI transport, and it emits the same `subagent_activity` events from the tool body.
Our `examples/ag-ui` demo runs on that same AG-UI transport, and its research tool reaches the frontend the same way: the protocol's standard `SUBAGENT_*` events, with the child's messages and its own `lookup` tool call attributed to the child run.
So it is not buying observability.
It already had it.
It compiles a child graph anyway.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ Our LangGraph subagent tracker is 543 lines.
It infers subagent identity from stream namespaces, correlates namespaces back to tool-call ids, and requires you to configure `subagentToolNames: ['task']` in the provider so it knows which tool calls are delegations.
That is client-side inference of a server-side fact, and inference is exactly as reliable as it sounds.

AG-UI ships `ACTIVITY_SNAPSHOT` and `ACTIVITY_DELTA` as first-class events.
The server declares "this is a subagent, here is its type, here is its status, here is its content."
AG-UI ships `SUBAGENT_STARTED`, `SUBAGENT_FINISHED`, and `SUBAGENT_ERROR` as first-class events, and every content event can carry a `subagentRunId`.
The server declares "this is a subagent, here is its name, here is the tool call that dispatched it, and these messages and tool calls belong to it."
Our reducer projects those onto the neutral `Subagent` contract, and the AG-UI provider config for the subagents demo needs no subagent option at all.

Declared beats inferred.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 9 additions & 7 deletions examples/ag-ui/angular/e2e/subagent-card.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ interface SubagentProbe {
// each carrying `toolCallIds`/reasoning) and `toolCalls()` (the child's own
// `lookup` calls, rendered as <chat-tool-call-card>). We read the projected map
// directly rather than scraping the rendered card: it IS the data the card
// renders, and asserting on it proves the ACTIVITY snapshot/delta pipeline
// reconstructed the full reason→tool→answer transcript and settled to
// `complete`, independent of card layout.
// renders, and asserting on it proves the SUBAGENT_* + subagentRunId-attributed
// event stream reconstructed the full reason→tool→answer transcript and
// settled to `complete`, independent of card layout.
async function readSubagents(page: Page): Promise<SubagentProbe> {
return page.evaluate(() => {
const ng = (window as unknown as { ng?: { getComponent?: (el: Element) => unknown } }).ng;
Expand Down Expand Up @@ -64,10 +64,12 @@ async function readSubagents(page: Page): Promise<SubagentProbe> {
// `research` tool, the langgraph child subgraph runs a genuine reason → tool →
// answer loop (an LLM call that returns a `lookup` tool_call, the offline
// `lookup` tool, then a second plain LLM call that writes the summary), and the
// AG-UI server converts the subagent_activity CUSTOM events into native
// ACTIVITY_SNAPSHOT/ACTIVITY_DELTA. The @threadplane/ag-ui reducer projects the
// activity to agent.subagents() (the ordered transcript chat-subagent-card
// renders) and the child's research text must stay OUT of the parent's bubble.
// AG-UI server's SubagentEmittingAgent expands the subagent_activity CUSTOM
// events into the protocol's SUBAGENT_STARTED/FINISHED plus subagentRunId-
// attributed TEXT_MESSAGE_* / TOOL_CALL_* events. The @threadplane/ag-ui
// reducer projects them to agent.subagents() (the ordered transcript
// chat-subagent-card renders) and the child's research text must stay OUT of
// the parent's bubble.
test('research delegation reconstructs the multi-message subagent transcript', async ({
page,
}) => {
Expand Down
Loading
Loading