Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/coding-agent/src/features/step-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,12 @@ export interface StepSubagentResultRecord extends StepSubagentRunResult {
agent: string;
agentSource: StepAgentConfig["source"] | "unknown";
task: string;
status: "running" | "completed" | "failed" | "aborted";
/**
* `queued`: not started yet (a later chain step, or a parallel task waiting
* on maxConcurrency). `skipped`: a chain step that never started because an
* earlier step did not complete.
*/
status: "queued" | "running" | "completed" | "failed" | "aborted" | "skipped";
step?: number;
worktreePath?: string;
worktreeBranch?: string;
Expand Down
54 changes: 50 additions & 4 deletions packages/coding-agent/src/features/subagent/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,47 @@ function makeEmptyResult(agent: string, task: string): StepSubagentResultRecord
agent,
agentSource: "unknown",
task,
status: "running",
status: "queued",
exitCode: -1,
messages: [],
stderr: "",
usage: emptyUsage(),
startedAt: Date.now(),
updatedAt: Date.now(),
};
}

function makeSkippedResult(record: StepSubagentResultRecord): StepSubagentResultRecord {
return { ...record, status: "skipped", updatedAt: Date.now() };
}

/**
* The parent model only sees `content`, never `details`, so a chain's text must
* carry both the outcome and the output that matters: the last completed step
* on success, the stopping step's error otherwise. Earlier step bodies stay in
* `details.results` instead of piling into the parent's context.
*/
function chainResultText(records: readonly StepSubagentResultRecord[]): string {
const steps = records.map((record, index) => `${index + 1}. ${record.agent}: ${record.status}`).join("\n");
const stopped = records.findIndex((record) => record.status !== "completed");
if (stopped === -1) {
const last = records.at(-1);
if (!last) return "(no output)";
return `Chain: ${records.length}/${records.length} steps completed\n${steps}\n\n### Final output (step ${records.length}, ${last.agent})\n\n${resultText(last)}`;
}
const failed = records[stopped];
const sections = [
`Chain stopped at step ${stopped + 1}/${records.length} (${failed.agent}: ${failed.status}); the chain did not complete.\n${steps}`,
`### Error (step ${stopped + 1}, ${failed.agent})\n\n${resultText(failed)}`,
];
const previous = stopped > 0 ? records[stopped - 1] : undefined;
if (previous) {
sections.push(
`### Last completed output (step ${stopped}, ${previous.agent})\n\n${truncateText(resultText(previous), 50_000)}`,
);
}
return sections.join("\n\n");
}

function resultDetailsText(details: StepSubagentDetails): string {
return details.results
.map((result) => {
Expand Down Expand Up @@ -320,6 +351,16 @@ export async function executeSubagent(
return failed;
}
reportCreated(agent, index);
// Flip the queued placeholder before spawning, so the step reads as running
// (with a clock) from the moment it is dispatched, not from its first event.
records[index] = {
...records[index],
agentSource: agent.source,
status: "running",
startedAt: Date.now(),
updatedAt: Date.now(),
};
emit(mode);
const baseCwd = path.resolve(ctx.cwd, task.cwd ?? ".");
let worktree: StepWorktreeLease | undefined;
let childCwd = baseCwd;
Expand All @@ -334,7 +375,7 @@ export async function executeSubagent(
worktreePath: worktree?.path,
worktreeBranch: worktree?.branch,
});
emit(parallel ? "parallel" : "single");
emit(mode);
};
const child = await options.runner({
agent,
Expand Down Expand Up @@ -394,7 +435,11 @@ export async function executeSubagent(
};
const record = await runOne(task, index);
previous = finalOutput(record.messages) || resultText(record);
if (record.status !== "completed") break;
if (record.status !== "completed") {
for (let rest = index + 1; rest < chain.length; rest++) records[rest] = makeSkippedResult(records[rest]);
emit(mode);
break;
}
}
} else await runOne(tasks[0], 0);
const finalDetails = buildDetails(mode, discovery, scope, records);
Expand All @@ -408,6 +453,7 @@ export async function executeSubagent(
`Parallel: ${success}/${records.length} succeeded\n\n${summaries.join("\n\n---\n\n")}`,
);
}
if (mode === "chain") return makeToolResult(finalDetails, chainResultText(records));
const record = records[0];
return makeToolResult(finalDetails, record ? resultText(record) : "(no output)");
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function getLiveLaneSessions(sessionId: string | undefined): StepSubagentRpcSess
}

function laneStatusFromDetails(details: StepSubagentDetails): BackgroundAgentLane["status"] {
if (details.results.some((record) => record.status === "running")) {
if (details.results.some((record) => record.status === "running" || record.status === "queued")) {
return "running";
}
if (details.results.some((record) => record.status === "failed")) {
Expand Down
32 changes: 28 additions & 4 deletions packages/coding-agent/src/features/subagent/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,31 @@ function formatElapsed(record: StepSubagentResultRecord): string {
function statusIcon(status: StepSubagentResultRecord["status"], theme: Theme): string {
if (status === "running") return theme.fg("warning", "~");
if (status === "completed") return theme.fg("success", "\u2713");
if (status === "queued") return theme.fg("dim", "\u00b7");
if (status === "skipped") return theme.fg("dim", "-");
return theme.fg("error", "x");
}

function chainHeading(records: readonly StepSubagentResultRecord[]): string {
const index = records.findIndex(
(record) => record.status !== "completed" && record.status !== "queued" && record.status !== "skipped",
);
if (index !== -1) return `${records[index].status} at step ${index + 1}/${records.length}`;
const completed = records.filter((record) => record.status === "completed").length;
return completed === records.length
? `${completed}/${records.length} steps completed`
: `queued at step ${completed + 1}/${records.length}`;
}

function renderRecordSummary(record: StepSubagentResultRecord, theme: Theme): string {
const output =
record.activeText ||
finalOutput(record.messages) ||
(record.status === "running" ? "(running...)" : resultText(record));
(record.status === "running"
? "(running...)"
: record.status === "queued" || record.status === "skipped"
? `(${record.status})`
: resultText(record));
const lines = output.split(/\r?\n/u).filter((line) => line.trim().length > 0);
const preview = lines.slice(-COLLAPSED_OUTPUT_LINES).join("\n");
const omitted = Math.max(0, lines.length - COLLAPSED_OUTPUT_LINES);
Expand Down Expand Up @@ -201,10 +218,14 @@ export class SubagentListWidget implements Component {
const theme = this.theme;
const running = records.filter((record) => record.status === "running").length;
const completed = records.filter((record) => record.status === "completed").length;
const failed = records.length - running - completed;
const queued = records.filter((record) => record.status === "queued").length;
const skipped = records.filter((record) => record.status === "skipped").length;
const failed = records.length - running - completed - queued - skipped;
const summary = [`${completed}/${records.length} complete`];
if (running > 0) summary.push(`${running} running`);
if (queued > 0) summary.push(`${queued} queued`);
if (failed > 0) summary.push(`${failed} failed`);
if (skipped > 0) summary.push(`${skipped} skipped`);
const header = ` ${theme.fg("toolTitle", theme.bold("subagent"))} ${theme.fg("accent", summary.join(", "))}`;
const lines = [visibleWidth(header) > width ? truncateToWidth(header, width, "\u2026") : header];

Expand Down Expand Up @@ -255,11 +276,14 @@ export function renderSubagentResult(
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
}
const running = details.results.filter((record) => record.status === "running").length;
const queued = details.results.filter((record) => record.status === "queued").length;
const completed = details.results.filter((record) => record.status === "completed").length;
const heading =
details.mode === "parallel"
? `${completed}/${details.results.length} complete${running > 0 ? `, ${running} running` : ""}`
: (details.results[0]?.status ?? "done");
? `${completed}/${details.results.length} complete${running > 0 ? `, ${running} running` : ""}${queued > 0 ? `, ${queued} queued` : ""}`
: details.mode === "chain"
? chainHeading(details.results)
: (details.results[0]?.status ?? "done");
if (options.expanded) {
const container = new Container();
container.addChild(new Text(`${theme.bold("agent")} ${theme.fg("accent", heading)}`, 0, 0));
Expand Down
Loading
Loading