From 3399f4b95386803e9c378982abfeaab65ed54e50 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:14:33 +0800 Subject: [PATCH] feat(events): id + result text on workflow subagent tool events Workflow subagent tool_call/tool_result events carried neither an id nor the child's report, so a --json consumer rendering the live agent tree could only pair parallel rows by arrival order and had no failure reason to show. Reuse the sa- id agent_usage already emits on both events, and put the report head (or the API error) on tool_result. Port of June's bb090f1 (feat/live-agent-tree) onto the split-out subagent_run.zig, switched from that commit's call_id to the id field the protocol already uses for subagent identity. Co-Authored-By: Codegraff --- src/schema_protocol.zig | 4 ++-- src/subagent_run.zig | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/schema_protocol.zig b/src/schema_protocol.zig index 7ccecaea..b90992a2 100644 --- a/src/schema_protocol.zig +++ b/src/schema_protocol.zig @@ -39,11 +39,11 @@ pub const json = \\ {"type": "started", "provider": "provider", "model": "model"}, \\ {"type": "model_call_started", "provider": "provider", "model": "model"}, \\ {"type": "model_call_finished", "provider": "provider", "model": "model", "ok": true, "ms": 0}, - \\ {"type": "tool_call", "name": "tool", "input": {}}, + \\ {"type": "tool_call", "name": "tool", "input": {}, "id": "sa-… on workflow subagent rows only; pairs with tool_result/agent_usage"}, \\ {"type": "tool_call_started", "name": "tool", "input": {}}, \\ {"type": "tool_rejected", "name": "tool", "reason": "budget|duplicate", "input": {}, "message": "..."}, \\ {"type": "ask_user", "call_id": "...", "question": "...", "input": {"question": "...", "options": ["..."]}}, - \\ {"type": "tool_result", "name": "tool", "is_error": false, "text": "..."}, + \\ {"type": "tool_result", "name": "tool", "is_error": false, "text": "...", "id": "sa-… on workflow subagent rows only"}, \\ {"type": "tool_call_finished", "name": "tool", "is_error": false, "ms": 0}, \\ {"type": "agent_usage", "id": "sa-...", "ok": true, "duration_ms": 0, "tool_calls": 0, "context_tokens": 0, "cache_read_tokens": 0}, \\ {"type": "finalizing"}, diff --git a/src/subagent_run.zig b/src/subagent_run.zig index 44e21f56..d61bb99f 100644 --- a/src/subagent_run.zig +++ b/src/subagent_run.zig @@ -255,7 +255,7 @@ pub fn runSub(ctx: ToolCtx, kind: []const u8, label: []const u8, prompt: []const agent.agent_cwd = if (wt) |w| w.path else null; const wf_task = std.mem.eql(u8, kind, "workflow_task"); - if (wf_task) guiEmit(ctx.io, .{ .type = "tool_call", .name = "subagent", .input = .{ .description = label } }); + if (wf_task) guiEmit(ctx.io, .{ .type = "tool_call", .name = "subagent", .input = .{ .description = label }, .id = sub_id }); // A /loop deadline on the parent reaches the child as guidance on its own // task prompt (goal_pacing.childTaskPrompt): same absolute deadline, minus // the margin the parent needs to integrate the result. No-op without one. @@ -265,7 +265,15 @@ pub fn runSub(ctx: ToolCtx, kind: []const u8, label: []const u8, prompt: []const const report = agent.runTurn(); const run_ms: i64 = @intCast(@max(0, sub_start.untilNow(ctx.io, .awake).toMilliseconds())); const run_ok = if (report) |r| r.len > 0 else |_| false; - if (wf_task) guiEmit(ctx.io, .{ .type = "tool_result", .name = "subagent", .is_error = !run_ok }); + if (wf_task) { + // The sa- id (shared with agent_usage) pairs parallel workflow rows; + // without it a consumer can only match tool_call/tool_result by order. + const sub_report: []const u8 = if (report) |r| + (if (r.len > 0) r else "subagent returned no report") + else |_| + (agent.last_api_error orelse "subagent failed"); + guiEmit(ctx.io, .{ .type = "tool_result", .name = "subagent", .is_error = !run_ok, .text = util.utf8Prefix(sub_report, 600), .id = sub_id }); + } const used_tools = agent.tools_used.render(arena); const usage: AgentUsage = .{ .duration_ms = @intCast(run_ms),