pi-subagents 0.59.0 → 0.60.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/docs/tool-reference.md +4 -1
  3. package/docs/workflows.md +2 -2
  4. package/package.json +1 -1
  5. package/skills/council-mode/SKILL.md +48 -243
  6. package/skills/council-mode/references/pass-contracts.md +150 -0
  7. package/skills/pi-subagents/SKILL.md +87 -37
  8. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  9. package/skills/pi-subagents/references/execution-controls.md +47 -6
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  11. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  12. package/skills/pi-subagents/references/prompting-and-roles.md +34 -27
  13. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  14. package/src/agents/agent-management.ts +154 -25
  15. package/src/api/shared-types.ts +2 -0
  16. package/src/extension/public-execution.ts +1 -0
  17. package/src/extension/schemas.ts +1 -0
  18. package/src/extension/tool-description.ts +8 -4
  19. package/src/runs/background/async-execution.ts +2 -2
  20. package/src/runs/background/async-job-tracker.ts +3 -0
  21. package/src/runs/background/async-status.ts +45 -2
  22. package/src/runs/background/control-channel.ts +3 -2
  23. package/src/runs/background/run-status.ts +13 -2
  24. package/src/runs/background/subagent-runner.ts +5 -1
  25. package/src/runs/background/subagent-wait.ts +10 -2
  26. package/src/runs/background/wait-completions.ts +3 -0
  27. package/src/runs/foreground/execution.ts +11 -2
  28. package/src/runs/foreground/subagent-executor.ts +98 -1
  29. package/src/runs/shared/async-status-projection.ts +138 -4
  30. package/src/runs/shared/background-process-options.ts +9 -0
  31. package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  32. package/src/runs/shared/mutation-evidence.ts +52 -3
  33. package/src/runs/shared/pi-args.ts +47 -1
  34. package/src/runs/shared/single-output.ts +45 -18
  35. package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
  36. package/src/runs/shared/workflow-graph.ts +15 -0
  37. package/src/shared/types.ts +34 -1
  38. package/src/tui/fleet-status.ts +11 -3
  39. package/src/tui/render-helpers.ts +31 -0
  40. package/src/tui/render.ts +597 -112
  41. package/src/watchdog/change-signature.ts +40 -1
  42. package/src/workflows/host-command.ts +6 -1
  43. package/src/workflows/scripted-workflow.ts +53 -2
@@ -1,8 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
+ import { previewDisplayText } from "../../shared/display-text.ts";
4
5
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNodeV1, type HostStepState, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorkflowPreflightV1 } from "../../shared/types.ts";
6
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNodeV1, type HostStepState, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TimeoutRecoveryProjection, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorkflowPreflightV1, type WorkflowGraphSnapshot } from "../../shared/types.ts";
6
7
  import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
7
8
  import { readStatus } from "../../shared/utils.ts";
8
9
  import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
@@ -21,6 +22,8 @@ import { assertWorkflowGraphHostSteps, hostStepReportName, hostStepVerdictLabel,
21
22
  import { projectAsyncWorkflowRows } from "../shared/async-status-projection.ts";
22
23
  import { validateAsyncStatusLaneMetadata } from "../shared/lane-metadata.ts";
23
24
  import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
25
+ import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
26
+ import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
24
27
 
25
28
  interface AsyncRunStepSummary {
26
29
  index: number;
@@ -75,6 +78,7 @@ interface AsyncRunStepSummary {
75
78
  review?: AsyncJobStep["review"];
76
79
  effects?: AsyncJobStep["effects"];
77
80
  processTerminal?: AsyncJobStep["processTerminal"];
81
+ timeoutRecovery?: TimeoutRecoveryProjection;
78
82
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
79
83
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
80
84
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
@@ -116,6 +120,7 @@ export interface AsyncRunSummary {
116
120
  pendingAppends?: number;
117
121
  parallelGroups?: AsyncParallelGroupStatus[];
118
122
  hostSteps?: HostStepNodeV1[];
123
+ workflowGraph?: AsyncStatus["workflowGraph"];
119
124
  steps: AsyncRunStepSummary[];
120
125
  sessionDir?: string;
121
126
  outputFile?: string;
@@ -282,6 +287,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
282
287
  const summarizedSteps = steps.map((step, index) => {
283
288
  const stepActivityState = step.activityState;
284
289
  const stepLastActivityAt = step.lastActivityAt;
290
+ const timeoutRecovery = projectTimeoutRecovery(step.timeoutRecovery);
285
291
  return {
286
292
  index,
287
293
  childId: asyncStatusChildIdentity(step, index),
@@ -341,6 +347,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
341
347
  ...(step.effects ? { effects: step.effects } : {}),
342
348
  ...(step.watchdog ? { watchdog: step.watchdog } : {}),
343
349
  ...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
350
+ ...(timeoutRecovery ? { timeoutRecovery } : {}),
344
351
  ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
345
352
  ...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
346
353
  ...(step.children?.length ? { children: step.children } : {}),
@@ -381,6 +388,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
381
388
  ...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
382
389
  ...(parallelGroups.length ? { parallelGroups } : {}),
383
390
  ...(hostSteps.length ? { hostSteps } : {}),
391
+ ...(status.mode === "workflow" && status.workflowGraph ? { workflowGraph: status.workflowGraph } : {}),
384
392
  steps: summarizedSteps,
385
393
  ...(nestedChildren.length ? { nestedChildren } : {}),
386
394
  ...(nestedWarnings.length ? { nestedWarnings } : {}),
@@ -575,12 +583,41 @@ function formatHostStepLine(row: ReturnType<typeof projectAsyncWorkflowRows>[num
575
583
  return `host ${row.kind}: ${row.name} | ${state}${details.length ? ` | ${details.join(" | ")}` : ""}`;
576
584
  }
577
585
 
586
+ function workflowStageStateLabel(status: WorkflowGraphSnapshot["nodes"][number]["status"]): string {
587
+ switch (status) {
588
+ case "completed":
589
+ return "complete";
590
+ case "detached":
591
+ return "paused";
592
+ default:
593
+ return status;
594
+ }
595
+ }
596
+
597
+ export function formatWorkflowStageLine(node: WorkflowGraphSnapshot["nodes"][number], index: number, total: number): string {
598
+ const state = workflowStageStateLabel(node.status);
599
+ const id = previewDisplayText(node.id, 160);
600
+ const label = node.label ? previewDisplayText(node.label, 160) : "";
601
+ const display = label && label !== id ? ` | ${label}` : "";
602
+ const agent = node.agent ? ` | ${previewDisplayText(node.agent, 80)}` : "";
603
+ const error = node.error ? ` | ${previewDisplayText(node.error, 240)}` : "";
604
+ return `stage ${index + 1}/${total}: ${id}${display}${agent} | ${state}${error}`;
605
+ }
606
+
578
607
  export function formatAsyncRunOutputPath(run: Pick<AsyncRunSummary, "asyncDir" | "outputFile">): string | undefined {
579
608
  if (!run.outputFile) return undefined;
580
609
  return path.isAbsolute(run.outputFile) ? run.outputFile : path.join(run.asyncDir, run.outputFile);
581
610
  }
582
611
 
583
- export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps">): string {
612
+ export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps"> & { workflowGraph?: WorkflowGraphSnapshot }): string {
613
+ const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
614
+ if (graphStages.length > 0) {
615
+ const currentNode = graphStages.find((node) => node.id === run.workflowGraph?.currentNodeId);
616
+ if (currentNode && currentNode.status !== "completed") return `stage ${graphStages.indexOf(currentNode) + 1}/${graphStages.length}`;
617
+ const activeNode = graphStages.find((node) => node.status === "running") ?? graphStages.find((node) => node.status !== "completed");
618
+ if (activeNode) return `stage ${graphStages.indexOf(activeNode) + 1}/${graphStages.length}`;
619
+ return `stage ${graphStages.length}/${graphStages.length}`;
620
+ }
584
621
  const stepCount = run.steps.length || 1;
585
622
  const chainStepCount = run.chainStepCount ?? stepCount;
586
623
  const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, chainStepCount);
@@ -622,8 +659,14 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
622
659
  if (preflightWarning) lines.push(preflightWarning);
623
660
  for (const step of run.steps) {
624
661
  lines.push(` ${formatStepLine(step)}`);
662
+ lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));
625
663
  lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
626
664
  }
665
+ const loadedWorkflowKeys = new Set(run.steps.flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
666
+ const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
667
+ for (const [index, node] of graphStages.entries()) {
668
+ if (!loadedWorkflowKeys.has(node.id)) lines.push(` ${formatWorkflowStageLine(node, index, graphStages.length)}`);
669
+ }
627
670
  for (const row of projectAsyncWorkflowRows([], run.hostSteps)) {
628
671
  const line = formatHostStepLine(row);
629
672
  if (line) lines.push(` ${line}`);
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Cross-OS control channel for async subagent runs.
3
3
  *
4
- * Background runs are detached OS processes. The original control path delivered
5
- * an interrupt with `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot
4
+ * Background runs use child processes. Unix detaches them from the parent process.
5
+ * The original control path delivered an interrupt with
6
+ * `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot
6
7
  * deliver those signals cross-process via `process.kill` and throws `ENOSYS`,
7
8
  * which left async runs uninterruptible (no stop, no live steer) on Windows.
8
9
  *
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import { safeTerminalText } from "../../shared/display-text.ts";
5
- import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
5
+ import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, formatWorkflowStageLine, listAsyncRuns } from "./async-status.ts";
6
6
  import { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
7
7
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
8
8
  import { formatModelThinking } from "../../shared/formatters.ts";
@@ -25,7 +25,9 @@ import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts"
25
25
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
26
26
  import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
27
27
  import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
28
+ import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
28
29
  import { getExternalJobProvider } from "../../api/external-job-provider.ts";
30
+ import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
29
31
 
30
32
  interface RunStatusParams {
31
33
  action?: string;
@@ -481,6 +483,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
481
483
  currentStep: status.currentStep,
482
484
  chainStepCount: status.chainStepCount,
483
485
  parallelGroups: status.parallelGroups,
486
+ workflowGraph: status.workflowGraph,
484
487
  steps: (status.steps ?? []).map((step, index) => ({ index, agent: step.agent, status: step.status })),
485
488
  });
486
489
  const started = new Date(status.startedAt).toISOString();
@@ -548,6 +551,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
548
551
  const display = runStatusStepDisplayName(step);
549
552
  const phase = step.phase ? `[${step.phase}] ` : "";
550
553
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
554
+ lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));
551
555
  if (step.runner?.type === "external-cli") {
552
556
  const runner = normalizeExternalCliRunnerStatus(step.runner);
553
557
  if (runner) {
@@ -593,6 +597,11 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
593
597
  lines.push(" Steer: unavailable; external runners do not accept live messages.");
594
598
  }
595
599
  }
600
+ const loadedWorkflowKeys = new Set((status.steps ?? []).flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
601
+ const graphStages = status.mode === "workflow" ? workflowGraphStageNodes(status.workflowGraph) : [];
602
+ for (const [index, node] of graphStages.entries()) {
603
+ if (!loadedWorkflowKeys.has(node.id)) lines.push(` ${formatWorkflowStageLine(node, index, graphStages.length)}`);
604
+ }
596
605
  const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
597
606
  const unattached = nestedChildren.filter((child) => !attached.has(child.id));
598
607
  lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
@@ -632,7 +641,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
632
641
  }
633
642
  try {
634
643
  const raw = fs.readFileSync(resultPath, "utf-8");
635
- const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; sessionName?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
644
+ const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; timeoutRecovery?: unknown; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; sessionName?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null; timeoutRecovery?: unknown }> };
636
645
  if (params.view === "transcript") {
637
646
  try {
638
647
  return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
@@ -660,6 +669,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
660
669
  const lines = [`Run: ${runId}`, data.toolCallId ? `Tool call: ${data.toolCallId}` : undefined, `State: ${status}`, `Result: ${resultPath}`].filter((line): line is string => Boolean(line));
661
670
  if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
662
671
  const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
672
+ lines.push(...formatTimeoutRecoveryLines(data.timeoutRecovery, " "));
673
+ for (const child of children) lines.push(...formatTimeoutRecoveryLines(child.timeoutRecovery, " "));
663
674
  lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
664
675
  if (data.summary) lines.push("", data.summary);
665
676
  const workflowChildren = parseWorkflowChildSummary((data as unknown as Record<string, unknown>).workflowChildren);
@@ -75,7 +75,7 @@ import {
75
75
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
76
76
  Semaphore,
77
77
  } from "../shared/parallel-utils.ts";
78
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
78
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
79
79
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
80
80
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
81
81
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
@@ -1640,6 +1640,7 @@ async function runSingleStepInner(
1640
1640
  const writerProcesses: PiWriterProcessInstanceExitV1[] = [];
1641
1641
  let writerAttemptCount = 0;
1642
1642
  const attemptNotes: string[] = [];
1643
+ let finalRequiredOutputMissing: boolean | undefined;
1643
1644
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1644
1645
  let finalResult: RunPiStreamingResult | undefined;
1645
1646
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
@@ -1698,6 +1699,7 @@ async function runSingleStepInner(
1698
1699
  : undefined;
1699
1700
  const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1700
1701
  parentSessionId: step.parentSessionId,
1702
+ forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
1701
1703
  baseArgs: ["--mode", "json", "-p"],
1702
1704
  task: attemptTask,
1703
1705
  taskDelivery: taskDeliveryOverride,
@@ -1929,6 +1931,7 @@ async function runSingleStepInner(
1929
1931
  : effectiveStructuredOutput
1930
1932
  ? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
1931
1933
  : undefined;
1934
+ finalRequiredOutputMissing = requiredOutput?.missing;
1932
1935
  const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
1933
1936
  const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
1934
1937
  const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
@@ -2117,6 +2120,7 @@ async function runSingleStepInner(
2117
2120
  ? buildTimeoutRecoverySummary({
2118
2121
  termination: "timed-out",
2119
2122
  evidence: finalMutationEvidence,
2123
+ requiredOutputMissing: finalRequiredOutputMissing,
2120
2124
  currentTool: finalResult?.currentTool,
2121
2125
  currentToolArgs: finalResult?.currentToolArgs,
2122
2126
  currentPath: finalResult?.currentPath,
@@ -65,6 +65,7 @@ import { formatDuration, shortenPath } from "../../shared/formatters.ts";
65
65
  import { toAgentToolUsage } from "../../shared/utils.ts";
66
66
  import { collectWaitCompletions } from "./wait-completions.ts";
67
67
  import { formatResumeFirstFailedRunsNote } from "./resume-guidance.ts";
68
+ import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
68
69
  export { WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV, WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, type ResolvedWaitToolConfig } from "./wait-config.ts";
69
70
 
70
71
  /** States that mean a run is still in flight (not yet resolved). */
@@ -349,6 +350,12 @@ function result(text: string, isError = false, completions?: WaitCompletion[]):
349
350
  };
350
351
  }
351
352
 
353
+ function formatCompletionRecovery(completions: WaitCompletion[] | undefined): string {
354
+ const lines = (completions ?? []).flatMap((completion) =>
355
+ (completion.results ?? []).flatMap((child) => formatTimeoutRecoveryLines(child.timeoutRecovery)));
356
+ return lines.length > 0 ? `\n${lines.join("\n")}` : "";
357
+ }
358
+
352
359
  function windowElapsedResult(
353
360
  text: string,
354
361
  activeRunIds: string[],
@@ -685,6 +692,7 @@ export async function waitForSubagents(
685
692
  + providerActive.filter((item) => initialProviderIds.has(backgroundWorkIdentity(item))).length;
686
693
  const elapsed = formatDuration(now() - startedAt);
687
694
  const outcome = terminalSummary ? ` Outcome: ${terminalSummary}.` : "";
695
+ const recoveryNote = formatCompletionRecovery(completions);
688
696
 
689
697
  if (waitForAll) {
690
698
  const scope = params.id
@@ -694,7 +702,7 @@ export async function waitForSubagents(
694
702
  : `${initialAsyncIds.size} async run(s) and ${initialProviderIds.size} provider item(s)`;
695
703
  const status = relevantAttention.length > 0 ? "attention required" : "done";
696
704
  return result(
697
- `Waited ${elapsed} for ${scope}; ${status}.${outcome}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
705
+ `Waited ${elapsed} for ${scope}; ${status}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
698
706
  (deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
699
707
  completions,
700
708
  );
@@ -711,7 +719,7 @@ export async function waitForSubagents(
711
719
  ? `${relevantAttention.length} of ${initialCount} ${subject} need attention`
712
720
  : `${finishedCount} of ${initialCount} ${subject} finished`;
713
721
  return result(
714
- `Waited ${elapsed}; ${progress}.${outcome}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
722
+ `Waited ${elapsed}; ${progress}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
715
723
  (deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
716
724
  completions,
717
725
  );
@@ -4,6 +4,7 @@ import type { AsyncRunSummary } from "./async-status.ts";
4
4
  import { readCompletionReplay, writeCompletionReplay } from "./completion-replay.ts";
5
5
  import { fallbackResultPayloadPathForSessionRun, resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
6
6
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
7
+ import { projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
7
8
 
8
9
  function asNonEmptyString(value: unknown): string | undefined {
9
10
  return typeof value === "string" && value ? value : undefined;
@@ -65,6 +66,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
65
66
  const error = asNonEmptyString(child.error);
66
67
  const model = asNonEmptyString(child.model);
67
68
  const contextOverflow = child.contextOverflow === true;
69
+ const timeoutRecovery = projectTimeoutRecovery(child.timeoutRecovery);
68
70
  return [{
69
71
  ...(agent ? { agent } : {}),
70
72
  ...(childRunId ? { runId: childRunId } : {}),
@@ -76,6 +78,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
76
78
  ...(model ? { model } : {}),
77
79
  ...(contextOverflow ? { contextOverflow: true } : {}),
78
80
  ...(artifactPaths ? { artifactPaths } : {}),
81
+ ...(timeoutRecovery ? { timeoutRecovery } : {}),
79
82
  }];
80
83
  })
81
84
  : undefined;
@@ -66,7 +66,7 @@ import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
66
66
  import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
67
67
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
68
68
  import { resolvePermissionRules } from "../shared/permissions.ts";
69
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
69
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
70
70
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
71
71
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
72
72
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
@@ -76,7 +76,7 @@ import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructu
76
76
  import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
77
77
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
78
78
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
79
- import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
79
+ import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, hasSingleOutputChangedSinceSnapshot, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
80
80
  import {
81
81
  buildModelCandidates,
82
82
  formatSubagentModelVerificationError,
@@ -380,6 +380,7 @@ async function runSingleAttempt(
380
380
  parentCapabilityToken: options.nestedRoute?.capabilityToken,
381
381
  runFanoutBudget: options.runFanoutBudget,
382
382
  parentSessionId: options.parentSessionId,
383
+ forkCacheKey: options.context === "fork" ? deriveForkPromptCacheKey(options.parentSessionId) : undefined,
383
384
  steerInboxDir: options.steerInboxDir,
384
385
  steerCapabilityPath: options.steerCapabilityPath,
385
386
  steerAckDir: options.steerAckDir,
@@ -1527,9 +1528,17 @@ async function runSingleAttempt(
1527
1528
  result.outputState = fullOutput.trim() || result.structuredOutput !== undefined ? "present" : "absent";
1528
1529
  if (result.timedOut) {
1529
1530
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
1531
+ let requiredOutputMissing: boolean | undefined;
1532
+ if (options.outputMode === "file-only" && options.outputPath) {
1533
+ const outputChanged = hasSingleOutputChangedSinceSnapshot(options.outputPath, shared.outputSnapshot);
1534
+ requiredOutputMissing = outputChanged === undefined ? undefined : !outputChanged;
1535
+ } else if (options.structuredOutput) {
1536
+ requiredOutputMissing = !existsSync(options.structuredOutput.outputPath);
1537
+ }
1530
1538
  result.timeoutRecovery = buildTimeoutRecoverySummary({
1531
1539
  termination: "timed-out",
1532
1540
  evidence: mutationEvidence,
1541
+ requiredOutputMissing,
1533
1542
  currentTool: progress.currentTool,
1534
1543
  currentToolArgs: progress.currentToolArgs,
1535
1544
  currentPath: progress.currentPath,
@@ -112,7 +112,7 @@ import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
112
112
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
113
113
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
114
114
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
115
- import { previewSimpleWorkflowRun, runWorkflowScript, validateWorkflowScript, WorkflowScriptError, type WorkflowReceiptResumeReference, type WorkflowScriptChildResult, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
115
+ import { previewSimpleWorkflowRun, runWorkflowScript, validateWorkflowScript, WorkflowScriptError, type WorkflowLanePlan, type WorkflowReceiptResumeReference, type WorkflowScriptChildResult, type WorkflowScriptTraceEntry, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
116
116
  import { executeWorkflowHostCommand, resolveWorkflowHostOutputClaimPath, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "../../workflows/host-command.ts";
117
117
  import { buildWorkflowReceipt, resolveWorkflowReceiptResumeEntry, writeWorkflowReceipt, type WorkflowReceipt, type WorkflowReceiptState } from "../../workflows/workflow-receipt.ts";
118
118
  import { upsertHostStep, validHostStepNodes } from "../shared/host-step-status.ts";
@@ -160,6 +160,9 @@ import {
160
160
  type ToolBudgetConfig,
161
161
  type Usage,
162
162
  type UsageBudgetConfig,
163
+ type WorkflowGraphNode,
164
+ type WorkflowGraphSnapshot,
165
+ type WorkflowNodeStatus,
163
166
  type WorkflowTerminalOutcome,
164
167
  type SubagentRunMode,
165
168
  type SubagentState,
@@ -295,6 +298,7 @@ export interface SubagentParamsLike {
295
298
  type?: string;
296
299
  agent?: string;
297
300
  task?: string;
301
+ capabilities?: boolean;
298
302
  extensionBindings?: ExtensionBindings;
299
303
  /** Retained async child run id. Valid only on workflow runs.run items. */
300
304
  resume?: string;
@@ -4411,6 +4415,90 @@ function formatWorkflowValue(value: unknown): string {
4411
4415
  }
4412
4416
  }
4413
4417
 
4418
+ function buildWorkflowLaneGraph(runId: string, lanes: WorkflowLanePlan[], existing?: WorkflowGraphSnapshot): WorkflowGraphSnapshot {
4419
+ const plannedIds = new Set(lanes.flatMap((lane) => lane.stages.map((stage) => stage.generatedKey)));
4420
+ const previousById = new Map((existing?.nodes ?? []).map((node) => [node.id, node]));
4421
+ const nodes: WorkflowGraphNode[] = [];
4422
+ const phases: WorkflowGraphSnapshot["phases"] = [];
4423
+ let flatIndex = 0;
4424
+ for (const lane of lanes) {
4425
+ const nodeIds: string[] = [];
4426
+ for (const [stageIndex, stage] of lane.stages.entries()) {
4427
+ const previous = previousById.get(stage.generatedKey);
4428
+ const outputName = stage.outputName || previous?.outputName;
4429
+ const structured = stage.structured ?? previous?.structured;
4430
+ const node: WorkflowGraphNode = {
4431
+ id: stage.generatedKey,
4432
+ kind: "step",
4433
+ agent: stage.agent ?? previous?.agent,
4434
+ phase: stage.phase ?? previous?.phase,
4435
+ label: stage.label ?? stage.key,
4436
+ status: previous?.status ?? (stageIndex === 0 ? "running" : "pending"),
4437
+ flatIndex,
4438
+ stepIndex: flatIndex,
4439
+ ...(outputName ? { outputName } : {}),
4440
+ ...(structured !== undefined ? { structured } : {}),
4441
+ ...(previous?.acceptanceStatus ? { acceptanceStatus: previous.acceptanceStatus } : {}),
4442
+ ...(previous?.error ? { error: previous.error } : {}),
4443
+ };
4444
+ nodes.push(node);
4445
+ nodeIds.push(node.id);
4446
+ flatIndex++;
4447
+ }
4448
+ if (nodeIds.length > 0) phases.push({ title: lane.key, nodeIds });
4449
+ }
4450
+ for (const node of existing?.nodes ?? []) {
4451
+ if (!plannedIds.has(node.id)) nodes.push(node);
4452
+ }
4453
+ for (const phase of existing?.phases ?? []) {
4454
+ const retainedNodeIds = phase.nodeIds.filter((nodeId) => !plannedIds.has(nodeId));
4455
+ if (retainedNodeIds.length === 0) continue;
4456
+ const currentPhase = phases.find((candidate) => candidate.title === phase.title);
4457
+ if (currentPhase) currentPhase.nodeIds.push(...retainedNodeIds);
4458
+ else phases.push({ title: phase.title, nodeIds: retainedNodeIds });
4459
+ }
4460
+ const currentNodeId = nodes.find((node) => node.status === "running")?.id ?? existing?.currentNodeId;
4461
+ return { runId, mode: "workflow", phases, nodes, ...(currentNodeId ? { currentNodeId } : {}) };
4462
+ }
4463
+
4464
+ function workflowLaneTraceStatus(state: WorkflowScriptTraceEntry["state"]): WorkflowNodeStatus | undefined {
4465
+ switch (state) {
4466
+ case "started":
4467
+ return "running";
4468
+ case "reused":
4469
+ return undefined;
4470
+ case "completed":
4471
+ return "completed";
4472
+ case "failed":
4473
+ return "failed";
4474
+ case "detached":
4475
+ return "detached";
4476
+ case "stopped":
4477
+ return "stopped";
4478
+ default:
4479
+ return undefined;
4480
+ }
4481
+ }
4482
+
4483
+ function applyWorkflowLaneTrace(graph: WorkflowGraphSnapshot, trace: WorkflowScriptTraceEntry[]): void {
4484
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
4485
+ for (const entry of trace) {
4486
+ if (entry.operation !== "run") continue;
4487
+ const node = nodesById.get(entry.key);
4488
+ if (!node) continue;
4489
+ const status = workflowLaneTraceStatus(entry.state);
4490
+ if (status) node.status = status;
4491
+ if (entry.agent) node.agent = entry.agent;
4492
+ if (entry.phase && (entry.phase !== "auto-resume" || node.phase === undefined)) node.phase = entry.phase;
4493
+ if (entry.label) node.label = entry.label;
4494
+ if (entry.error) node.error = entry.error;
4495
+ else if (status === "completed" || status === "running") delete node.error;
4496
+ }
4497
+ const currentNodeId = graph.nodes.find((node) => node.status === "running")?.id;
4498
+ if (currentNodeId) graph.currentNodeId = currentNodeId;
4499
+ else delete graph.currentNodeId;
4500
+ }
4501
+
4414
4502
  function workflowChatProgressUpdate(
4415
4503
  runId: string,
4416
4504
  chatProgress: WorkflowChatProgressProjection,
@@ -4800,6 +4888,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4800
4888
  liveJob.toolCount = status.toolCount;
4801
4889
  liveJob.currentStep = status.currentStep;
4802
4890
  liveJob.preflight = status.preflight;
4891
+ liveJob.workflowGraph = status.workflowGraph;
4803
4892
  if (status.steps) {
4804
4893
  liveJob.steps = status.steps.map((step, index) => ({ ...step, index }));
4805
4894
  liveJob.agents = status.steps.map((step) => step.agent);
@@ -4883,6 +4972,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4883
4972
  trace: projectedTrace,
4884
4973
  ...(preflightWarnings.length ? { preflightWarnings } : {}),
4885
4974
  };
4975
+ if (status.workflowGraph) applyWorkflowLaneTrace(status.workflowGraph, trace);
4886
4976
  const rebuild = trace.length < projectedTraceLength
4887
4977
  || (projectedTraceLength > 0 && trace[projectedTraceLength - 1] !== projectedTraceTail);
4888
4978
  if (rebuild) {
@@ -4986,6 +5076,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4986
5076
  },
4987
5077
  ...(workflowState ? { state: workflowState } : {}),
4988
5078
  onTrace: updateTrace,
5079
+ onLanePlan: (lanes) => {
5080
+ status.workflowGraph = buildWorkflowLaneGraph(workflowRunId, lanes, status.workflowGraph);
5081
+ applyWorkflowLaneTrace(status.workflowGraph, status.workflow?.trace ?? []);
5082
+ persist({ tolerateStatusWriteFailure: true });
5083
+ },
4989
5084
  host: runHostCommand,
4990
5085
  onHostStep: (hostStep) => {
4991
5086
  status = upsertHostStep({ status, hostStep, persist: (nextStatus) => {
@@ -5121,6 +5216,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5121
5216
  }
5122
5217
  if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, workflowChildren, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), ...(status.steps?.find((step) => step.workflowKey === child.key)?.sessionName ? { sessionName: status.steps?.find((step) => step.workflowKey === child.key)?.sessionName } : {}), ...workflowChildAccountingFields(child), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.outputReference ? { outputReference: child.outputReference } : {}), ...(child.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5123
5218
  persist();
5219
+ deps.refreshResultDelivery?.();
5124
5220
  persistClosed = true;
5125
5221
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
5126
5222
  } catch (error) {
@@ -5166,6 +5262,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5166
5262
  }
5167
5263
  if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, workflowChildren, ...(terminalOutcome ? { terminalOutcome } : {}), results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), ...(status.steps?.find((step) => step.workflowKey === child.key)?.sessionName ? { sessionName: status.steps?.find((step) => step.workflowKey === child.key)?.sessionName } : {}), ...workflowChildAccountingFields(child), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.outputReference ? { outputReference: child.outputReference } : {}), ...(child.terminalOutcome ? { terminalOutcome: child.terminalOutcome } : {}), ...(child.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5168
5264
  persist();
5265
+ deps.refreshResultDelivery?.();
5169
5266
  persistClosed = true;
5170
5267
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(terminalOutcome ? { terminalOutcome } : {}), ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
5171
5268
  } finally {