Skip to content

Commit e365bda

Browse files
committed
fix(appkit): forward sub-agent events into the parent SSE stream
After dispatchToolCall consolidated the sub-agent path, runSubAgent called consumeAdapterStream without forwarding the child's adapter events. The parent stream therefore only emitted the outer agent-<name> function call; nested tool_call / tool_result events from the sub-agent never reached the client. The smart-dashboard query agent delegates to dashboard_pilot whose UI- action tools (apply_filter, highlight_period, focus_chart, etc.) rely on the SSE stream to apply React state mutations. Without the forwarding, the user asks for a highlight and nothing visible happens. Forward every sub-agent event except metadata. Sub-agents have their own threadId; emitting it would overwrite the parent's thread state on the client and break multi-turn continuity. Test exercises the metadata-skipping rule and asserts tool_call, tool_result, and message_delta all reach outboundEvents. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
1 parent 233b388 commit e365bda

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

packages/appkit/src/plugins/agents/agents.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1129,7 +1129,25 @@ export class AgentsPlugin extends Plugin implements ToolProvider {
11291129
},
11301130
runContext,
11311131
),
1132-
{ signal: runState.signal },
1132+
{
1133+
signal: runState.signal,
1134+
// Forward every sub-agent event into the parent's outbound SSE
1135+
// stream so the client sees nested tool_call / tool_result events
1136+
// (UI-action tools like apply_filter / highlight_period rely on
1137+
// this) and the sub-agent's streaming text as it's generated.
1138+
//
1139+
// `metadata` is the one exception: sub-agents have their own
1140+
// threadId, and forwarding it would overwrite the parent's
1141+
// thread state on the client and break multi-turn continuity.
1142+
// Approval-pending events emitted by `dispatchToolCall` already
1143+
// reach `outboundEvents` directly, so they are not routed here.
1144+
onEvent: (event) => {
1145+
if (event.type === "metadata") return;
1146+
for (const translated of runState.translator.translate(event)) {
1147+
runState.outboundEvents.push(translated);
1148+
}
1149+
},
1150+
},
11331151
);
11341152
}
11351153

packages/appkit/src/plugins/agents/tests/dispatch-tool-call.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,48 @@ describe("dispatchToolCall — shared tool-call budget", () => {
278278
expect(runState.signal.aborted).toBe(true);
279279
});
280280
});
281+
282+
describe("runSubAgent — sub-agent event forwarding", () => {
283+
/**
284+
* The smart-dashboard `query` agent delegates to `dashboard_pilot`, which
285+
* emits UI-action `tool_call` events (apply_filter, highlight_period) that
286+
* the client reads off the parent's SSE stream. Without forwarding those
287+
* inner events, the user asks for a highlight and nothing visible
288+
* happens. `metadata` events are NOT forwarded because the sub-agent has
289+
* its own threadId and overwriting the parent's would break multi-turn.
290+
*/
291+
test("forwards every sub-agent event into the parent stream except metadata", async () => {
292+
const plugin = new AgentsPlugin({ dir: false, agents: {} });
293+
const { runState, pushed } = makeRunState(plugin);
294+
295+
const child = {
296+
name: "child",
297+
instructions: "test",
298+
adapter: {
299+
// biome-ignore lint/suspicious/noExplicitAny: stub adapter shape
300+
async *run(): any {
301+
yield { type: "metadata", data: { threadId: "child-thread" } };
302+
yield {
303+
type: "tool_call",
304+
id: "call-1",
305+
name: "highlight_period",
306+
arguments: '{"start":"2016-03-01","end":"2016-03-31"}',
307+
};
308+
yield { type: "tool_result", id: "call-1", output: "highlighted" };
309+
yield { type: "message_delta", content: "Done." };
310+
},
311+
},
312+
toolIndex: new Map(),
313+
// biome-ignore lint/suspicious/noExplicitAny: minimal stub
314+
} as any;
315+
316+
// biome-ignore lint/suspicious/noExplicitAny: call private
317+
await (plugin as any).runSubAgent(runState, child, { input: "go" }, 1);
318+
319+
const types = pushed.map((e) => (e as { type: string }).type);
320+
expect(types).not.toContain("metadata");
321+
expect(types).toContain("tool_call");
322+
expect(types).toContain("tool_result");
323+
expect(types).toContain("message_delta");
324+
});
325+
});

0 commit comments

Comments
 (0)