From 93ecdbea3551d24e6ba375f82a1751eeedadb4a9 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 22:52:43 +0200 Subject: [PATCH] fix(coding-agent): improve subagent parallel summaries closes #4710 --- packages/coding-agent/CHANGELOG.md | 4 ++ .../examples/extensions/subagent/README.md | 3 + .../examples/extensions/subagent/index.ts | 60 +++++++++++++------ 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 280658eb..87dd1e88 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). + ## [0.75.3] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/examples/extensions/subagent/README.md b/packages/coding-agent/examples/extensions/subagent/README.md index 8599679f..f98b6458 100644 --- a/packages/coding-agent/examples/extensions/subagent/README.md +++ b/packages/coding-agent/examples/extensions/subagent/README.md @@ -113,6 +113,8 @@ Use a chain: first have scout find the read tool, then have planner suggest impr - Shows all tasks with live status (⏳ running, ✓ done, ✗ failed) - Updates as each task makes progress - Shows "2/3 done, 1 running" status +- Returns each completed task's final output to the parent model, capped at 50 KB per task +- Returns failure diagnostics from stderr/error messages when a child exits before producing output **Tool call formatting** (mimics built-in tools): - `$ command` for bash @@ -168,5 +170,6 @@ Project agents override user agents with the same name when `agentScope: "both"` ## Limitations - Output truncated to last 10 items in collapsed view (expand to see all) +- Parallel model-visible output is capped at 50 KB per task; full results remain in tool details - Agents discovered fresh on each invocation (allows editing mid-session) - Parallel mode limited to 8 tasks, 4 concurrent diff --git a/packages/coding-agent/examples/extensions/subagent/index.ts b/packages/coding-agent/examples/extensions/subagent/index.ts index a9b6da42..2853ad78 100644 --- a/packages/coding-agent/examples/extensions/subagent/index.ts +++ b/packages/coding-agent/examples/extensions/subagent/index.ts @@ -27,6 +27,7 @@ import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js"; const MAX_PARALLEL_TASKS = 8; const MAX_CONCURRENCY = 4; const COLLAPSED_ITEM_COUNT = 10; +const PER_TASK_OUTPUT_CAP = 50 * 1024; function formatTokens(count: number): string { if (count < 1000) return count.toString(); @@ -172,6 +173,28 @@ function getFinalOutput(messages: Message[]): string { return ""; } +function isFailedResult(result: SingleResult): boolean { + return result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted"; +} + +function getResultOutput(result: SingleResult): string { + if (isFailedResult(result)) { + return result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)"; + } + return getFinalOutput(result.messages) || "(no output)"; +} + +function truncateParallelOutput(output: string): string { + const byteLength = Buffer.byteLength(output, "utf8"); + if (byteLength <= PER_TASK_OUTPUT_CAP) return output; + + let truncated = output.slice(0, PER_TASK_OUTPUT_CAP); + while (Buffer.byteLength(truncated, "utf8") > PER_TASK_OUTPUT_CAP) { + truncated = truncated.slice(0, -1); + } + return `${truncated}\n\n[Output truncated: ${byteLength - Buffer.byteLength(truncated, "utf8")} bytes omitted. Full output preserved in tool details.]`; +} + type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record }; function getDisplayItems(messages: Message[]): DisplayItem[] { @@ -534,11 +557,9 @@ export default function (pi: ExtensionAPI) { ); results.push(result); - const isError = - result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted"; + const isError = isFailedResult(result); if (isError) { - const errorMsg = - result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)"; + const errorMsg = getResultOutput(result); return { content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }], details: makeDetails("chain")(results), @@ -617,17 +638,19 @@ export default function (pi: ExtensionAPI) { return result; }); - const successCount = results.filter((r) => r.exitCode === 0).length; + const successCount = results.filter((r) => !isFailedResult(r)).length; const summaries = results.map((r) => { - const output = getFinalOutput(r.messages); - const preview = output.slice(0, 100) + (output.length > 100 ? "..." : ""); - return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`; + const output = truncateParallelOutput(getResultOutput(r)); + const status = isFailedResult(r) + ? `failed${r.stopReason && r.stopReason !== "end" ? ` (${r.stopReason})` : ""}` + : "completed"; + return `### [${r.agent}] ${status}\n\n${output}`; }); return { content: [ { type: "text", - text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`, + text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n---\n\n")}`, }, ], details: makeDetails("parallel")(results), @@ -646,10 +669,9 @@ export default function (pi: ExtensionAPI) { onUpdate, makeDetails("single"), ); - const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted"; + const isError = isFailedResult(result); if (isError) { - const errorMsg = - result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)"; + const errorMsg = getResultOutput(result); return { content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }], details: makeDetails("single")([result]), @@ -740,7 +762,7 @@ export default function (pi: ExtensionAPI) { if (details.mode === "single" && details.results.length === 1) { const r = details.results[0]; - const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted"; + const isError = isFailedResult(r); const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓"); const displayItems = getDisplayItems(r.messages); const finalOutput = getFinalOutput(r.messages); @@ -893,8 +915,8 @@ export default function (pi: ExtensionAPI) { if (details.mode === "parallel") { const running = details.results.filter((r) => r.exitCode === -1).length; - const successCount = details.results.filter((r) => r.exitCode === 0).length; - const failCount = details.results.filter((r) => r.exitCode > 0).length; + const successCount = details.results.filter((r) => r.exitCode !== -1 && !isFailedResult(r)).length; + const failCount = details.results.filter((r) => r.exitCode !== -1 && isFailedResult(r)).length; const isRunning = running > 0; const icon = isRunning ? theme.fg("warning", "⏳") @@ -916,7 +938,7 @@ export default function (pi: ExtensionAPI) { ); for (const r of details.results) { - const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗"); + const rIcon = isFailedResult(r) ? theme.fg("error", "✗") : theme.fg("success", "✓"); const displayItems = getDisplayItems(r.messages); const finalOutput = getFinalOutput(r.messages); @@ -963,9 +985,9 @@ export default function (pi: ExtensionAPI) { const rIcon = r.exitCode === -1 ? theme.fg("warning", "⏳") - : r.exitCode === 0 - ? theme.fg("success", "✓") - : theme.fg("error", "✗"); + : isFailedResult(r) + ? theme.fg("error", "✗") + : theme.fg("success", "✓"); const displayItems = getDisplayItems(r.messages); text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`; if (displayItems.length === 0)