pi-subagents 0.25.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +175 -19
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/skills/pi-subagents/SKILL.md +60 -17
- package/src/agents/agent-management.ts +71 -15
- package/src/agents/agent-serializer.ts +13 -2
- package/src/agents/agents.ts +88 -17
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +2 -0
- package/src/extension/index.ts +5 -2
- package/src/extension/schemas.ts +132 -6
- package/src/intercom/result-intercom.ts +5 -0
- package/src/runs/background/async-execution.ts +88 -6
- package/src/runs/background/async-status.ts +11 -1
- package/src/runs/background/run-status.ts +10 -1
- package/src/runs/background/subagent-runner.ts +665 -39
- package/src/runs/foreground/chain-execution.ts +369 -118
- package/src/runs/foreground/execution.ts +392 -19
- package/src/runs/foreground/subagent-executor.ts +126 -3
- package/src/runs/shared/acceptance-contract.ts +318 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +173 -0
- package/src/runs/shared/acceptance-reports.ts +127 -0
- package/src/runs/shared/acceptance.ts +22 -0
- package/src/runs/shared/chain-outputs.ts +101 -0
- package/src/runs/shared/completion-guard.ts +26 -3
- package/src/runs/shared/dynamic-fanout.ts +293 -0
- package/src/runs/shared/parallel-utils.ts +33 -1
- package/src/runs/shared/pi-args.ts +11 -0
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
- package/src/runs/shared/workflow-graph.ts +210 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +265 -1
- package/src/shared/utils.ts +7 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +178 -45
package/src/shared/utils.ts
CHANGED
|
@@ -204,6 +204,13 @@ export function getSingleResultOutput(result: Pick<SingleResult, "finalOutput" |
|
|
|
204
204
|
return result.finalOutput ?? getFinalOutput(result.messages ?? []);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
export function formatResourceLimitExceeded(input: { agent: string; kind: "maxExecutionTimeMs" | "maxTokens"; limit: number; observed?: number }): string {
|
|
208
|
+
if (input.kind === "maxExecutionTimeMs") {
|
|
209
|
+
return `Resource limit exceeded for ${input.agent}: maxExecutionTimeMs ${input.limit}ms.`;
|
|
210
|
+
}
|
|
211
|
+
return `Resource limit exceeded for ${input.agent}: maxTokens ${input.limit}${input.observed !== undefined ? ` (observed ${input.observed})` : ""}.`;
|
|
212
|
+
}
|
|
213
|
+
|
|
207
214
|
/**
|
|
208
215
|
* Extract display items (text and tool calls) from messages
|
|
209
216
|
*/
|
|
@@ -5,7 +5,8 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
5
5
|
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
6
6
|
import { discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
|
|
7
7
|
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
8
|
-
import { isParallelStep, type ChainStep } from "../shared/settings.ts";
|
|
8
|
+
import { isDynamicParallelStep, isParallelStep, type ChainStep } from "../shared/settings.ts";
|
|
9
|
+
import { assertJsonSchemaObject } from "../runs/shared/structured-output.ts";
|
|
9
10
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
10
11
|
import {
|
|
11
12
|
applySlashUpdate,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
21
22
|
SLASH_SUBAGENT_STARTED_EVENT,
|
|
22
23
|
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
24
|
+
type JsonSchemaObject,
|
|
23
25
|
type SingleResult,
|
|
24
26
|
type SubagentState,
|
|
25
27
|
} from "../shared/types.ts";
|
|
@@ -123,12 +125,48 @@ const makeChainCompletions = (state: SubagentState) => (prefix: string) => {
|
|
|
123
125
|
.map((chain) => ({ value: chain.name, label: chain.name }));
|
|
124
126
|
};
|
|
125
127
|
|
|
128
|
+
function loadSavedOutputSchema(chain: ChainConfig, stepAgent: string, outputSchema: unknown): JsonSchemaObject | undefined {
|
|
129
|
+
if (outputSchema === undefined) return undefined;
|
|
130
|
+
if (typeof outputSchema === "string") {
|
|
131
|
+
const schemaPath = path.isAbsolute(outputSchema)
|
|
132
|
+
? outputSchema
|
|
133
|
+
: path.join(path.dirname(chain.filePath), outputSchema);
|
|
134
|
+
const parsed = JSON.parse(fs.readFileSync(schemaPath, "utf-8")) as unknown;
|
|
135
|
+
assertJsonSchemaObject(parsed, `outputSchema for chain '${chain.name}' step '${stepAgent}' (${schemaPath})`);
|
|
136
|
+
return parsed;
|
|
137
|
+
}
|
|
138
|
+
assertJsonSchemaObject(outputSchema, `outputSchema for chain '${chain.name}' step '${stepAgent}'`);
|
|
139
|
+
return outputSchema;
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
const mapSavedChainSteps = (chain: ChainConfig, worktree = false): ChainStep[] => {
|
|
127
|
-
return (chain.steps as Array<ChainStep & { skills?: string[] | false }>).map((step) => {
|
|
128
|
-
if (isParallelStep(step))
|
|
143
|
+
return (chain.steps as unknown as Array<ChainStep & { skills?: string[] | false }>).map((step) => {
|
|
144
|
+
if (isParallelStep(step)) {
|
|
145
|
+
const parallel = step.parallel.map((task) => {
|
|
146
|
+
const { outputSchema: rawOutputSchema, ...rest } = task as typeof task & { outputSchema?: unknown };
|
|
147
|
+
const outputSchema = loadSavedOutputSchema(chain, task.agent, rawOutputSchema);
|
|
148
|
+
return { ...rest, ...(outputSchema ? { outputSchema } : {}) };
|
|
149
|
+
});
|
|
150
|
+
return { ...step, parallel, ...(worktree ? { worktree: true } : {}) };
|
|
151
|
+
}
|
|
152
|
+
if (isDynamicParallelStep(step)) {
|
|
153
|
+
const { outputSchema: rawOutputSchema, ...parallelRest } = step.parallel as typeof step.parallel & { outputSchema?: unknown };
|
|
154
|
+
const outputSchema = loadSavedOutputSchema(chain, step.parallel.agent, rawOutputSchema);
|
|
155
|
+
const collectSchema = loadSavedOutputSchema(chain, `${step.collect.as} collection`, step.collect.outputSchema);
|
|
156
|
+
return {
|
|
157
|
+
...step,
|
|
158
|
+
parallel: { ...parallelRest, ...(outputSchema ? { outputSchema } : {}) },
|
|
159
|
+
collect: { ...step.collect, ...(collectSchema ? { outputSchema: collectSchema } : {}) },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const outputSchema = loadSavedOutputSchema(chain, step.agent, (step as { outputSchema?: unknown }).outputSchema);
|
|
129
163
|
return {
|
|
130
164
|
agent: step.agent,
|
|
131
165
|
task: step.task || undefined,
|
|
166
|
+
...(step.phase ? { phase: step.phase } : {}),
|
|
167
|
+
...(step.label ? { label: step.label } : {}),
|
|
168
|
+
...(step.as ? { as: step.as } : {}),
|
|
169
|
+
...(outputSchema ? { outputSchema } : {}),
|
|
132
170
|
output: step.output,
|
|
133
171
|
outputMode: step.outputMode,
|
|
134
172
|
reads: step.reads,
|
package/src/tui/render.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type Details,
|
|
15
15
|
type NestedRunSummary,
|
|
16
16
|
type NestedStepSummary,
|
|
17
|
+
type WorkflowNodeStatus,
|
|
17
18
|
MAX_WIDGET_JOBS,
|
|
18
19
|
WIDGET_KEY,
|
|
19
20
|
} from "../shared/types.ts";
|
|
@@ -221,10 +222,22 @@ function firstOutputLine(text: string): string {
|
|
|
221
222
|
return text.split("\n").find((line) => line.trim())?.trim() ?? "";
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
function formatAcceptanceStatus(result: Details["results"][number]): string | undefined {
|
|
226
|
+
const acceptance = result.acceptance;
|
|
227
|
+
if (!acceptance?.status || acceptance.status === "not-required") return undefined;
|
|
228
|
+
const finalization = acceptance.finalization
|
|
229
|
+
? ` · finalization: ${acceptance.finalization.status} after ${acceptance.finalization.turns.length}/${acceptance.finalization.maxTurns} turns`
|
|
230
|
+
: "";
|
|
231
|
+
return `acceptance: ${acceptance.status}${finalization}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
224
234
|
function resultStatusLine(result: Details["results"][number], output: string): string {
|
|
225
235
|
if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
|
|
236
|
+
if (result.timedOut) return `Timed out${result.error ? `: ${result.error}` : ""}`;
|
|
226
237
|
if (result.interrupted) return "Paused";
|
|
227
238
|
if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
|
|
239
|
+
const acceptance = formatAcceptanceStatus(result);
|
|
240
|
+
if (acceptance) return `Done · ${acceptance}`;
|
|
228
241
|
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
|
|
229
242
|
return "Done";
|
|
230
243
|
}
|
|
@@ -232,6 +245,7 @@ function resultStatusLine(result: Details["results"][number], output: string): s
|
|
|
232
245
|
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary)): string {
|
|
233
246
|
if (running) return theme.fg("accent", runningGlyph(seed));
|
|
234
247
|
if (result.detached) return theme.fg("warning", "■");
|
|
248
|
+
if (result.timedOut) return theme.fg("error", "✗");
|
|
235
249
|
if (result.interrupted) return theme.fg("warning", "■");
|
|
236
250
|
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
237
251
|
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
|
|
@@ -351,18 +365,19 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
|
351
365
|
return theme.fg("error", "✗");
|
|
352
366
|
}
|
|
353
367
|
|
|
354
|
-
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number): string {
|
|
368
|
+
function widgetStepGlyph(status: AsyncJobStep["status"] | WorkflowNodeStatus, theme: Theme, seed?: number): string {
|
|
355
369
|
if (status === "running") return theme.fg("accent", runningGlyph(seed));
|
|
356
370
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
357
|
-
if (status === "failed") return theme.fg("error", "✗");
|
|
371
|
+
if (status === "failed" || status === "timed-out") return theme.fg("error", "✗");
|
|
358
372
|
if (status === "paused") return theme.fg("warning", "■");
|
|
359
373
|
return theme.fg("muted", "◦");
|
|
360
374
|
}
|
|
361
375
|
|
|
362
|
-
function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string {
|
|
376
|
+
function widgetStepStatus(status: AsyncJobStep["status"] | WorkflowNodeStatus, theme: Theme): string {
|
|
363
377
|
if (status === "running") return theme.fg("accent", "running");
|
|
364
378
|
if (status === "complete" || status === "completed") return theme.fg("success", "complete");
|
|
365
379
|
if (status === "failed") return theme.fg("error", "failed");
|
|
380
|
+
if (status === "timed-out") return theme.fg("error", "timed out");
|
|
366
381
|
if (status === "paused") return theme.fg("warning", "paused");
|
|
367
382
|
return theme.fg("dim", status);
|
|
368
383
|
}
|
|
@@ -427,26 +442,44 @@ function parseParallelGroupAgentCount(label: string | undefined): number | undef
|
|
|
427
442
|
return inner.split("+").map((part) => part.trim()).filter(Boolean).length;
|
|
428
443
|
}
|
|
429
444
|
|
|
430
|
-
function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex">): boolean {
|
|
431
|
-
if (details.mode !== "chain") return false;
|
|
432
|
-
if (details.currentStepIndex === undefined) return false;
|
|
433
|
-
const currentLabel = details.chainAgents?.[details.currentStepIndex];
|
|
434
|
-
return parseParallelGroupAgentCount(currentLabel) !== undefined;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
445
|
interface ChainStepSpan {
|
|
438
446
|
stepIndex: number;
|
|
439
447
|
start: number;
|
|
440
448
|
count: number;
|
|
441
449
|
isParallel: boolean;
|
|
442
|
-
|
|
450
|
+
status?: WorkflowNodeStatus;
|
|
451
|
+
label?: string;
|
|
452
|
+
error?: string;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function buildChainStepSpans(details: Pick<Details, "chainAgents" | "workflowGraph">): ChainStepSpan[] {
|
|
456
|
+
if (details.workflowGraph?.nodes?.length) {
|
|
457
|
+
const spans: ChainStepSpan[] = [];
|
|
458
|
+
let flatCursor = 0;
|
|
459
|
+
for (const node of details.workflowGraph.nodes) {
|
|
460
|
+
if (node.stepIndex === undefined) continue;
|
|
461
|
+
if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
|
|
462
|
+
const childFlatIndexes = (node.children ?? [])
|
|
463
|
+
.map((child) => child.flatIndex)
|
|
464
|
+
.filter((value): value is number => typeof value === "number");
|
|
465
|
+
const start = childFlatIndexes.length ? Math.min(...childFlatIndexes) : flatCursor;
|
|
466
|
+
const count = node.children?.length ?? 0;
|
|
467
|
+
spans.push({ stepIndex: node.stepIndex, start, count, isParallel: true, status: node.status, label: node.label, error: node.error });
|
|
468
|
+
flatCursor = Math.max(flatCursor, start + count);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const start = node.flatIndex ?? flatCursor;
|
|
472
|
+
spans.push({ stepIndex: node.stepIndex, start, count: 1, isParallel: false, status: node.status, label: node.label, error: node.error });
|
|
473
|
+
flatCursor = Math.max(flatCursor, start + 1);
|
|
474
|
+
}
|
|
475
|
+
if (spans.length) return spans.sort((left, right) => left.stepIndex - right.stepIndex);
|
|
476
|
+
}
|
|
443
477
|
|
|
444
|
-
|
|
445
|
-
if (!chainAgents?.length) return [];
|
|
478
|
+
if (!details.chainAgents?.length) return [];
|
|
446
479
|
const spans: ChainStepSpan[] = [];
|
|
447
480
|
let start = 0;
|
|
448
|
-
for (let stepIndex = 0; stepIndex < chainAgents.length; stepIndex++) {
|
|
449
|
-
const label = chainAgents[stepIndex]!;
|
|
481
|
+
for (let stepIndex = 0; stepIndex < details.chainAgents.length; stepIndex++) {
|
|
482
|
+
const label = details.chainAgents[stepIndex]!;
|
|
450
483
|
const parsedCount = parseParallelGroupAgentCount(label);
|
|
451
484
|
const count = parsedCount ?? 1;
|
|
452
485
|
spans.push({ stepIndex, start, count, isParallel: parsedCount !== undefined });
|
|
@@ -455,6 +488,12 @@ function buildChainStepSpans(chainAgents: string[] | undefined): ChainStepSpan[]
|
|
|
455
488
|
return spans;
|
|
456
489
|
}
|
|
457
490
|
|
|
491
|
+
function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex" | "workflowGraph">): boolean {
|
|
492
|
+
if (details.mode !== "chain") return false;
|
|
493
|
+
if (details.currentStepIndex === undefined) return false;
|
|
494
|
+
return buildChainStepSpans(details).some((span) => span.stepIndex === details.currentStepIndex && span.isParallel);
|
|
495
|
+
}
|
|
496
|
+
|
|
458
497
|
function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGroups: AsyncParallelGroupStatus[] = []): ChainStepSpan[] {
|
|
459
498
|
const spans: ChainStepSpan[] = [];
|
|
460
499
|
let flatIndex = 0;
|
|
@@ -475,10 +514,59 @@ function isDoneResult(result: Details["results"][number]): boolean {
|
|
|
475
514
|
const status = result.progress?.status;
|
|
476
515
|
if (status === "completed") return true;
|
|
477
516
|
if (status === "running" || status === "pending") return false;
|
|
478
|
-
if (result.interrupted || result.detached) return false;
|
|
517
|
+
if (result.interrupted || result.detached || result.timedOut) return false;
|
|
479
518
|
return result.exitCode === 0;
|
|
480
519
|
}
|
|
481
520
|
|
|
521
|
+
function workflowGraphHasStatus(details: Pick<Details, "workflowGraph">, statuses: WorkflowNodeStatus[]): boolean {
|
|
522
|
+
return details.workflowGraph?.nodes.some((node) => statuses.includes(node.status)) ?? false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
interface ChainRenderResultEntry {
|
|
526
|
+
kind: "result";
|
|
527
|
+
resultIndex: number;
|
|
528
|
+
rowNumber: number;
|
|
529
|
+
agentName: string;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
interface ChainRenderPlaceholderEntry {
|
|
533
|
+
kind: "placeholder";
|
|
534
|
+
rowNumber: number;
|
|
535
|
+
stepLabel: string;
|
|
536
|
+
agentName: string;
|
|
537
|
+
status: WorkflowNodeStatus;
|
|
538
|
+
error?: string;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
type ChainRenderEntry = ChainRenderResultEntry | ChainRenderPlaceholderEntry;
|
|
542
|
+
|
|
543
|
+
function buildChainRenderEntries(details: Details, label: MultiProgressLabel): ChainRenderEntry[] | undefined {
|
|
544
|
+
if (details.mode !== "chain" || !label.hasParallelInChain || label.showActiveGroupOnly) return undefined;
|
|
545
|
+
const entries: ChainRenderEntry[] = [];
|
|
546
|
+
for (const span of buildChainStepSpans(details)) {
|
|
547
|
+
if (span.isParallel && span.count === 0) {
|
|
548
|
+
entries.push({
|
|
549
|
+
kind: "placeholder",
|
|
550
|
+
rowNumber: span.stepIndex + 1,
|
|
551
|
+
stepLabel: `Step ${span.stepIndex + 1}`,
|
|
552
|
+
agentName: span.label ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
|
|
553
|
+
status: span.status ?? "pending",
|
|
554
|
+
error: span.error,
|
|
555
|
+
});
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
for (let index = span.start; index < span.start + span.count; index++) {
|
|
559
|
+
entries.push({
|
|
560
|
+
kind: "result",
|
|
561
|
+
resultIndex: index,
|
|
562
|
+
rowNumber: index + 1,
|
|
563
|
+
agentName: details.results[index]?.agent ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return entries;
|
|
568
|
+
}
|
|
569
|
+
|
|
482
570
|
interface MultiProgressLabel {
|
|
483
571
|
headerLabel: string;
|
|
484
572
|
itemTitle: "Step" | "Agent";
|
|
@@ -490,15 +578,15 @@ interface MultiProgressLabel {
|
|
|
490
578
|
showActiveGroupOnly: boolean;
|
|
491
579
|
}
|
|
492
580
|
|
|
493
|
-
function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents">, hasRunning: boolean): MultiProgressLabel {
|
|
494
|
-
const stepSpans = buildChainStepSpans(details
|
|
581
|
+
function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents" | "workflowGraph">, hasRunning: boolean): MultiProgressLabel {
|
|
582
|
+
const stepSpans = buildChainStepSpans(details);
|
|
495
583
|
const hasParallelInChain = details.mode === "chain" && stepSpans.some((span) => span.isParallel);
|
|
496
584
|
const activeParallelGroup = isChainParallelGroupActive(details);
|
|
497
585
|
const itemTitle: "Step" | "Agent" = details.mode === "parallel" || activeParallelGroup ? "Agent" : "Step";
|
|
498
586
|
|
|
499
587
|
if (details.mode === "parallel") {
|
|
500
588
|
const totalCount = details.totalSteps ?? details.results.length;
|
|
501
|
-
const statuses = new Array(totalCount).fill("pending") as
|
|
589
|
+
const statuses = new Array(totalCount).fill("pending") as WorkflowNodeStatus[];
|
|
502
590
|
for (const progress of details.progress ?? []) {
|
|
503
591
|
if (progress.index >= 0 && progress.index < totalCount) statuses[progress.index] = progress.status;
|
|
504
592
|
}
|
|
@@ -509,11 +597,13 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
509
597
|
const index = result.progress?.index ?? progressFromArray?.index ?? i;
|
|
510
598
|
if (index < 0 || index >= totalCount) continue;
|
|
511
599
|
const status = result.progress?.status
|
|
512
|
-
?? (result.
|
|
513
|
-
? "
|
|
514
|
-
: result.
|
|
515
|
-
? "
|
|
516
|
-
:
|
|
600
|
+
?? (result.timedOut
|
|
601
|
+
? "timed-out"
|
|
602
|
+
: result.interrupted || result.detached
|
|
603
|
+
? "detached"
|
|
604
|
+
: result.exitCode === 0
|
|
605
|
+
? "completed"
|
|
606
|
+
: "failed");
|
|
517
607
|
statuses[index] = status;
|
|
518
608
|
}
|
|
519
609
|
const running = statuses.filter((status) => status === "running").length;
|
|
@@ -555,11 +645,13 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
555
645
|
if (details.mode === "chain" && details.chainAgents?.length) {
|
|
556
646
|
const totalCount = details.totalSteps ?? details.chainAgents.length;
|
|
557
647
|
const doneLogical = stepSpans.filter((span) => {
|
|
648
|
+
if (span.status && span.status !== "completed") return false;
|
|
649
|
+
if (span.count === 0) return span.status === "completed";
|
|
558
650
|
for (let index = span.start; index < span.start + span.count; index++) {
|
|
559
651
|
const progressEntry = details.progress?.find((progress) => progress.index === index);
|
|
560
652
|
const resultEntry = details.results.find((result) => result.progress?.index === index) ?? details.results[index];
|
|
561
|
-
if (progressEntry?.status === "running" || progressEntry?.status === "pending") return false;
|
|
562
|
-
if (resultEntry
|
|
653
|
+
if (progressEntry?.status === "running" || progressEntry?.status === "pending" || progressEntry?.status === "failed") return false;
|
|
654
|
+
if (!resultEntry || !isDoneResult(resultEntry)) return false;
|
|
563
655
|
}
|
|
564
656
|
return true;
|
|
565
657
|
}).length;
|
|
@@ -575,9 +667,9 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
575
667
|
return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
|
|
576
668
|
}
|
|
577
669
|
|
|
578
|
-
function resultRowLabel(details: Pick<Details, "mode" | "chainAgents">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
|
|
670
|
+
function resultRowLabel(details: Pick<Details, "mode" | "chainAgents" | "workflowGraph">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
|
|
579
671
|
if (details.mode === "chain" && label.hasParallelInChain) {
|
|
580
|
-
const span = buildChainStepSpans(details
|
|
672
|
+
const span = buildChainStepSpans(details).find((candidate) => resultIndex >= candidate.start && resultIndex < candidate.start + candidate.count);
|
|
581
673
|
if (span?.isParallel) return `Agent ${resultIndex - span.start + 1}/${span.count}`;
|
|
582
674
|
if (span) return `Step ${span.stepIndex + 1}`;
|
|
583
675
|
}
|
|
@@ -970,9 +1062,12 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
|
|
|
970
1062
|
|
|
971
1063
|
function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
972
1064
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
973
|
-
|| d.results.some((r) => r.progress?.status === "running")
|
|
974
|
-
|
|
975
|
-
const
|
|
1065
|
+
|| d.results.some((r) => r.progress?.status === "running")
|
|
1066
|
+
|| workflowGraphHasStatus(d, ["running"]);
|
|
1067
|
+
const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running")
|
|
1068
|
+
|| workflowGraphHasStatus(d, ["failed", "timed-out"]);
|
|
1069
|
+
const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
|
|
1070
|
+
|| workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
976
1071
|
let totalSummary = d.progressSummary;
|
|
977
1072
|
if (!totalSummary) {
|
|
978
1073
|
let sawProgress = false;
|
|
@@ -1005,13 +1100,29 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1005
1100
|
const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
|
|
1006
1101
|
const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
|
|
1007
1102
|
const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
|
|
1008
|
-
|
|
1103
|
+
const chainEntries = buildChainRenderEntries(d, multiLabel);
|
|
1104
|
+
const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
|
|
1105
|
+
const i = displayStart + offset;
|
|
1009
1106
|
const r = d.results[i];
|
|
1010
1107
|
const fallbackLabel = itemTitle.toLowerCase();
|
|
1011
1108
|
const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
|
|
1012
|
-
|
|
1109
|
+
return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`) };
|
|
1110
|
+
});
|
|
1111
|
+
for (const entry of renderEntries) {
|
|
1112
|
+
if (entry.kind === "placeholder") {
|
|
1113
|
+
const glyph = widgetStepGlyph(entry.status as AsyncJobStep["status"], theme);
|
|
1114
|
+
const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
|
|
1115
|
+
c.addChild(new Text(truncLine(` ${glyph} ${entry.stepLabel}: ${themeBold(theme, entry.agentName)} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
|
|
1116
|
+
if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", ` ⎿ Error: ${entry.error}`), width), 0, 0));
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const i = entry.resultIndex;
|
|
1120
|
+
const r = d.results[i];
|
|
1121
|
+
const rowNumber = entry.rowNumber;
|
|
1122
|
+
const agentName = entry.agentName;
|
|
1013
1123
|
if (!r) {
|
|
1014
|
-
|
|
1124
|
+
const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
|
|
1125
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
|
|
1015
1126
|
continue;
|
|
1016
1127
|
}
|
|
1017
1128
|
const output = getSingleResultOutput(r);
|
|
@@ -1030,7 +1141,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1030
1141
|
const activity = compactCurrentActivity(rProg);
|
|
1031
1142
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
1032
1143
|
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
1033
|
-
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
1144
|
+
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || r.timedOut || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
1034
1145
|
c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
1035
1146
|
}
|
|
1036
1147
|
const outputTarget = extractOutputTarget(r.task);
|
|
@@ -1159,20 +1270,27 @@ export function renderSubagentResult(
|
|
|
1159
1270
|
if (!expanded) return renderMultiCompact(d, theme);
|
|
1160
1271
|
|
|
1161
1272
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
1162
|
-
|| d.results.some((r) => r.progress?.status === "running")
|
|
1273
|
+
|| d.results.some((r) => r.progress?.status === "running")
|
|
1274
|
+
|| workflowGraphHasStatus(d, ["running"]);
|
|
1163
1275
|
const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
|
|
1164
1276
|
const hasEmptyWithoutTarget = d.results.some((r) =>
|
|
1165
1277
|
r.exitCode === 0
|
|
1166
1278
|
&& r.progress?.status !== "running"
|
|
1167
1279
|
&& hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
|
|
1168
1280
|
);
|
|
1281
|
+
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed", "timed-out"]);
|
|
1282
|
+
const hasWorkflowPause = workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
1169
1283
|
const icon = hasRunning
|
|
1170
1284
|
? theme.fg("warning", "running")
|
|
1171
1285
|
: hasEmptyWithoutTarget
|
|
1172
1286
|
? theme.fg("warning", "warning")
|
|
1173
|
-
:
|
|
1174
|
-
? theme.fg("
|
|
1175
|
-
:
|
|
1287
|
+
: hasWorkflowFailure
|
|
1288
|
+
? theme.fg("error", "failed")
|
|
1289
|
+
: hasWorkflowPause
|
|
1290
|
+
? theme.fg("warning", "paused")
|
|
1291
|
+
: ok === d.results.length
|
|
1292
|
+
? theme.fg("success", "ok")
|
|
1293
|
+
: theme.fg("error", "failed");
|
|
1176
1294
|
|
|
1177
1295
|
const totalSummary =
|
|
1178
1296
|
d.progressSummary ||
|
|
@@ -1243,18 +1361,33 @@ export function renderSubagentResult(
|
|
|
1243
1361
|
const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
|
|
1244
1362
|
const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
|
|
1245
1363
|
const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
|
|
1364
|
+
const chainEntries = buildChainRenderEntries(d, multiLabel);
|
|
1365
|
+
const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
|
|
1366
|
+
const i = displayStart + offset;
|
|
1367
|
+
const r = d.results[i];
|
|
1368
|
+
const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
|
|
1369
|
+
return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `step-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`) };
|
|
1370
|
+
});
|
|
1246
1371
|
|
|
1247
1372
|
c.addChild(new Spacer(1));
|
|
1248
1373
|
|
|
1249
|
-
for (
|
|
1374
|
+
for (const entry of renderEntries) {
|
|
1375
|
+
if (entry.kind === "placeholder") {
|
|
1376
|
+
const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
|
|
1377
|
+
c.addChild(new Text(fit(` ${statusLabel} ${entry.stepLabel}: ${theme.bold(entry.agentName)}`), 0, 0));
|
|
1378
|
+
c.addChild(new Text(theme.fg(entry.status === "failed" ? "error" : "dim", ` status: ${entry.status}`), 0, 0));
|
|
1379
|
+
if (entry.error) c.addChild(new Text(theme.fg("error", ` error: ${entry.error}`), 0, 0));
|
|
1380
|
+
c.addChild(new Spacer(1));
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
const i = entry.resultIndex;
|
|
1250
1384
|
const r = d.results[i];
|
|
1251
|
-
const rowNumber =
|
|
1252
|
-
const agentName =
|
|
1253
|
-
? (r?.agent || `step-${rowNumber}`)
|
|
1254
|
-
: (d.chainAgents![i] || r?.agent || `step-${rowNumber}`);
|
|
1385
|
+
const rowNumber = entry.rowNumber;
|
|
1386
|
+
const agentName = entry.agentName;
|
|
1255
1387
|
|
|
1256
1388
|
if (!r) {
|
|
1257
|
-
|
|
1389
|
+
const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
|
|
1390
|
+
c.addChild(new Text(fit(theme.fg("dim", ` ${pendingLabel}: ${agentName}`)), 0, 0));
|
|
1258
1391
|
c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
|
|
1259
1392
|
c.addChild(new Spacer(1));
|
|
1260
1393
|
continue;
|