fix(coding-agent): improve subagent parallel summaries closes #4710

This commit is contained in:
Mario Zechner
2026-05-18 22:52:43 +02:00
parent 7670563374
commit 93ecdbea35
3 changed files with 48 additions and 19 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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<string, any> };
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)