fix(coding-agent): improve subagent parallel summaries closes #4710
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.75.3] - 2026-05-18
|
||||||
|
|
||||||
### Fixed
|
### 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)
|
- Shows all tasks with live status (⏳ running, ✓ done, ✗ failed)
|
||||||
- Updates as each task makes progress
|
- Updates as each task makes progress
|
||||||
- Shows "2/3 done, 1 running" status
|
- 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):
|
**Tool call formatting** (mimics built-in tools):
|
||||||
- `$ command` for bash
|
- `$ command` for bash
|
||||||
@@ -168,5 +170,6 @@ Project agents override user agents with the same name when `agentScope: "both"`
|
|||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- Output truncated to last 10 items in collapsed view (expand to see all)
|
- 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)
|
- Agents discovered fresh on each invocation (allows editing mid-session)
|
||||||
- Parallel mode limited to 8 tasks, 4 concurrent
|
- 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_PARALLEL_TASKS = 8;
|
||||||
const MAX_CONCURRENCY = 4;
|
const MAX_CONCURRENCY = 4;
|
||||||
const COLLAPSED_ITEM_COUNT = 10;
|
const COLLAPSED_ITEM_COUNT = 10;
|
||||||
|
const PER_TASK_OUTPUT_CAP = 50 * 1024;
|
||||||
|
|
||||||
function formatTokens(count: number): string {
|
function formatTokens(count: number): string {
|
||||||
if (count < 1000) return count.toString();
|
if (count < 1000) return count.toString();
|
||||||
@@ -172,6 +173,28 @@ function getFinalOutput(messages: Message[]): string {
|
|||||||
return "";
|
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> };
|
type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, any> };
|
||||||
|
|
||||||
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
||||||
@@ -534,11 +557,9 @@ export default function (pi: ExtensionAPI) {
|
|||||||
);
|
);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
|
|
||||||
const isError =
|
const isError = isFailedResult(result);
|
||||||
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
const errorMsg =
|
const errorMsg = getResultOutput(result);
|
||||||
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
||||||
details: makeDetails("chain")(results),
|
details: makeDetails("chain")(results),
|
||||||
@@ -617,17 +638,19 @@ export default function (pi: ExtensionAPI) {
|
|||||||
return result;
|
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 summaries = results.map((r) => {
|
||||||
const output = getFinalOutput(r.messages);
|
const output = truncateParallelOutput(getResultOutput(r));
|
||||||
const preview = output.slice(0, 100) + (output.length > 100 ? "..." : "");
|
const status = isFailedResult(r)
|
||||||
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
|
? `failed${r.stopReason && r.stopReason !== "end" ? ` (${r.stopReason})` : ""}`
|
||||||
|
: "completed";
|
||||||
|
return `### [${r.agent}] ${status}\n\n${output}`;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
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),
|
details: makeDetails("parallel")(results),
|
||||||
@@ -646,10 +669,9 @@ export default function (pi: ExtensionAPI) {
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
makeDetails("single"),
|
makeDetails("single"),
|
||||||
);
|
);
|
||||||
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
const isError = isFailedResult(result);
|
||||||
if (isError) {
|
if (isError) {
|
||||||
const errorMsg =
|
const errorMsg = getResultOutput(result);
|
||||||
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
||||||
details: makeDetails("single")([result]),
|
details: makeDetails("single")([result]),
|
||||||
@@ -740,7 +762,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
|
|
||||||
if (details.mode === "single" && details.results.length === 1) {
|
if (details.mode === "single" && details.results.length === 1) {
|
||||||
const r = details.results[0];
|
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 icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
||||||
const displayItems = getDisplayItems(r.messages);
|
const displayItems = getDisplayItems(r.messages);
|
||||||
const finalOutput = getFinalOutput(r.messages);
|
const finalOutput = getFinalOutput(r.messages);
|
||||||
@@ -893,8 +915,8 @@ export default function (pi: ExtensionAPI) {
|
|||||||
|
|
||||||
if (details.mode === "parallel") {
|
if (details.mode === "parallel") {
|
||||||
const running = details.results.filter((r) => r.exitCode === -1).length;
|
const running = details.results.filter((r) => r.exitCode === -1).length;
|
||||||
const successCount = 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 > 0).length;
|
const failCount = details.results.filter((r) => r.exitCode !== -1 && isFailedResult(r)).length;
|
||||||
const isRunning = running > 0;
|
const isRunning = running > 0;
|
||||||
const icon = isRunning
|
const icon = isRunning
|
||||||
? theme.fg("warning", "⏳")
|
? theme.fg("warning", "⏳")
|
||||||
@@ -916,7 +938,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const r of details.results) {
|
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 displayItems = getDisplayItems(r.messages);
|
||||||
const finalOutput = getFinalOutput(r.messages);
|
const finalOutput = getFinalOutput(r.messages);
|
||||||
|
|
||||||
@@ -963,9 +985,9 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const rIcon =
|
const rIcon =
|
||||||
r.exitCode === -1
|
r.exitCode === -1
|
||||||
? theme.fg("warning", "⏳")
|
? theme.fg("warning", "⏳")
|
||||||
: r.exitCode === 0
|
: isFailedResult(r)
|
||||||
? theme.fg("success", "✓")
|
? theme.fg("error", "✗")
|
||||||
: theme.fg("error", "✗");
|
: theme.fg("success", "✓");
|
||||||
const displayItems = getDisplayItems(r.messages);
|
const displayItems = getDisplayItems(r.messages);
|
||||||
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
||||||
if (displayItems.length === 0)
|
if (displayItems.length === 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user