fix(coding-agent): improve subagent parallel summaries closes #4710
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user