pi-subagents 0.45.2 → 0.47.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 (70) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +342 -0
  4. package/docs/configuration.md +328 -0
  5. package/docs/extension-api.md +308 -0
  6. package/docs/missions.md +119 -0
  7. package/docs/models.md +192 -0
  8. package/docs/observability.md +174 -0
  9. package/docs/tool-reference.md +343 -0
  10. package/docs/watchdog.md +176 -0
  11. package/docs/workflows.md +163 -0
  12. package/package.json +4 -2
  13. package/skills/pi-subagents/references/execution-controls.md +6 -6
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  15. package/src/agents/agents.ts +17 -8
  16. package/src/agents/frontmatter.ts +7 -3
  17. package/src/agents/skills.ts +2 -9
  18. package/src/api/project-panes.ts +30 -0
  19. package/src/extension/config.ts +18 -1
  20. package/src/extension/fanout-child.ts +5 -4
  21. package/src/extension/index.ts +66 -19
  22. package/src/extension/rpc.ts +3 -6
  23. package/src/extension/schemas.ts +28 -7
  24. package/src/extension/subagent-guide.ts +39 -0
  25. package/src/extension/tool-description.ts +30 -12
  26. package/src/inspectors/herdr/project-panes.ts +459 -63
  27. package/src/missions/actions.ts +25 -2
  28. package/src/missions/lifecycle.ts +21 -2
  29. package/src/missions/store.ts +79 -2
  30. package/src/missions/types.ts +33 -0
  31. package/src/missions/workflow-state.ts +19 -13
  32. package/src/runs/background/async-execution.ts +17 -6
  33. package/src/runs/background/async-job-tracker.ts +15 -0
  34. package/src/runs/background/async-resume.ts +19 -3
  35. package/src/runs/background/async-status.ts +6 -1
  36. package/src/runs/background/completion-replay.ts +267 -0
  37. package/src/runs/background/control-channel.ts +36 -0
  38. package/src/runs/background/result-watcher.ts +28 -6
  39. package/src/runs/background/scheduled-runs.ts +2 -1
  40. package/src/runs/background/stale-run-reconciler.ts +2 -21
  41. package/src/runs/background/subagent-runner.ts +47 -6
  42. package/src/runs/background/wait-completions.ts +39 -5
  43. package/src/runs/background/wait-subscriptions.ts +18 -3
  44. package/src/runs/foreground/async-steering-action.ts +1 -1
  45. package/src/runs/foreground/chain-execution.ts +3 -0
  46. package/src/runs/foreground/execution.ts +7 -0
  47. package/src/runs/foreground/foreground-history.ts +137 -0
  48. package/src/runs/foreground/subagent-executor.ts +403 -54
  49. package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
  50. package/src/runs/shared/dynamic-fanout.ts +1 -1
  51. package/src/runs/shared/model-fallback.ts +8 -4
  52. package/src/runs/shared/model-scope.ts +12 -2
  53. package/src/runs/shared/parallel-utils.ts +1 -0
  54. package/src/runs/shared/worktree.ts +3 -2
  55. package/src/shared/artifacts.ts +14 -14
  56. package/src/shared/display-text.ts +100 -0
  57. package/src/shared/fork-context.ts +13 -0
  58. package/src/shared/formatters.ts +4 -6
  59. package/src/shared/prompt-resources.ts +51 -0
  60. package/src/shared/settings.ts +15 -2
  61. package/src/shared/types.ts +41 -2
  62. package/src/shared/utf8.ts +11 -0
  63. package/src/shared/utils.ts +43 -33
  64. package/src/slash/prompt-workflows.ts +2 -15
  65. package/src/slash/slash-commands.ts +22 -2
  66. package/src/tui/fleet-status.ts +22 -12
  67. package/src/tui/fleet.ts +135 -25
  68. package/src/tui/render.ts +150 -33
  69. package/src/watchdog/change-signature.ts +4 -3
  70. package/src/workflows/scripted-workflow.ts +167 -10
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import { resolveAgentName, type AgentConfig, type AgentScope } from "../../agents/agents.ts";
7
- import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning } from "../../shared/artifacts.ts";
7
+ import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning, getProjectSubagentsDir } from "../../shared/artifacts.ts";
8
8
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
9
9
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
10
10
  import { resolveEffectiveThinking, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
@@ -17,10 +17,12 @@ import {
17
17
  settleForegroundSchedulingOwner,
18
18
  updateForegroundChild,
19
19
  } from "./foreground-control.ts";
20
+ import { persistForegroundRunHistory, MAX_REMEMBERED_FOREGROUND_RUNS } from "./foreground-history.ts";
20
21
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
21
22
  import { handleManagementAction } from "../../agents/agent-management.ts";
22
23
  import { handleRefinementAction } from "../../agents/agent-refinements.ts";
23
24
  import { buildDoctorReport } from "../../extension/doctor.ts";
25
+ import { readSubagentGuide } from "../../extension/subagent-guide.ts";
24
26
  import { normalizePublicSubagentExecution } from "../../extension/public-execution.ts";
25
27
  import { runSync } from "./execution.ts";
26
28
  import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
@@ -37,6 +39,7 @@ import {
37
39
  isParallelStep,
38
40
  isDynamicParallelStep,
39
41
  resolveChainPath,
42
+ resolveExistingReadPaths,
40
43
  resolveStepBehavior,
41
44
  suppressProgressForReadOnlyTask,
42
45
  taskDisallowsFileUpdates,
@@ -49,8 +52,8 @@ import {
49
52
  type StepOverrides,
50
53
  } from "../../shared/settings.ts";
51
54
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
52
- import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "../background/async-execution.ts";
53
- import type { ScheduledRunAction } from "../background/scheduled-runs.ts";
55
+ import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
56
+ import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
54
57
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
55
58
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
56
59
  import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
@@ -82,22 +85,31 @@ import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncRe
82
85
  import { deliverCheckpointDecisionRequest, deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
83
86
  import { updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
84
87
  import { steerAsyncRun } from "./async-steering-action.ts";
88
+ import {
89
+ removeWorkflowForegroundSteeringRoute,
90
+ resolveWorkflowForegroundSteeringTarget,
91
+ steerWorkflowForegroundTarget,
92
+ workflowForegroundSteeringDir,
93
+ workflowForegroundSteeringLaunchOptions,
94
+ } from "./workflow-foreground-steering.ts";
85
95
  import { stopAsyncRun } from "./async-stop-action.ts";
86
96
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
87
- import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
97
+ import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
88
98
  import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
89
99
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
90
100
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
91
101
  import { inspectSubagentStatus } from "../background/run-status.ts";
92
102
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
93
103
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
94
- import { attachMissionToLaunchResult, prepareMissionLaunch, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
104
+ import { attachMissionToLaunchResult, prepareMissionLaunch, writeMissionAsyncBinding, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
95
105
  import { updateMission } from "../../missions/store.ts";
106
+ import type { MissionWorkflowChildUpdate } from "../../missions/types.ts";
96
107
  import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
97
108
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
98
109
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
99
110
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
100
111
  import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
112
+ import { renderWorkflowPrompt } from "../../shared/prompt-resources.ts";
101
113
  import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
102
114
  import {
103
115
  cleanupWorktrees,
@@ -150,7 +162,48 @@ import {
150
162
  wrapForkTask,
151
163
  } from "../../shared/types.ts";
152
164
 
153
- const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
165
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
166
+ const DESTRUCTIVE_MANAGEMENT_ACTIONS = new Set(["delete", "eject", "disable", "reset", "mission.close", "worktree.discard", "refine.rollback", "inspector.close", "project.close", "stop", "interrupt", "reject-checkpoint", "schedule.delete"]);
167
+
168
+ function editDistance(left: string, right: string): number {
169
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
170
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
171
+ let diagonal = previous[0]!;
172
+ previous[0] = leftIndex;
173
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
174
+ const above = previous[rightIndex]!;
175
+ previous[rightIndex] = left[leftIndex - 1] === right[rightIndex - 1]
176
+ ? diagonal
177
+ : Math.min(diagonal, above, previous[rightIndex - 1]!) + 1;
178
+ diagonal = above;
179
+ }
180
+ }
181
+ return previous[right.length]!;
182
+ }
183
+
184
+ function hasSingleAdjacentTransposition(left: string, right: string): boolean {
185
+ if (left.length !== right.length) return false;
186
+ const mismatch = [...left].findIndex((character, index) => character !== right[index]);
187
+ return mismatch >= 0
188
+ && left[mismatch] === right[mismatch + 1]
189
+ && left[mismatch + 1] === right[mismatch]
190
+ && left.slice(mismatch + 2) === right.slice(mismatch + 2);
191
+ }
192
+
193
+ export function unknownSubagentActionMessage(action: string): string {
194
+ const requested = action.toLowerCase();
195
+ const suggestion = SUBAGENT_ACTIONS.find((candidate) => {
196
+ const distance = editDistance(requested, candidate);
197
+ const closeMatch = distance <= Math.max(1, Math.floor(candidate.length / 4)) || hasSingleAdjacentTransposition(requested, candidate);
198
+ if (DESTRUCTIVE_MANAGEMENT_ACTIONS.has(candidate)) return distance === 1 && requested.length >= candidate.length - 1;
199
+ return closeMatch;
200
+ });
201
+ const nextStep = 'Use subagent({ action: "status" }) to inspect runs or subagent({ action: "list" }) to inspect agents.';
202
+ const validActions = `Valid: ${SUBAGENT_ACTIONS.join(", ")}.`;
203
+ return suggestion
204
+ ? `Unknown action: ${action}. Did you mean ${suggestion}? ${nextStep} ${validActions}`
205
+ : `Unknown action: ${action}. ${nextStep} ${validActions}`;
206
+ }
154
207
 
155
208
  type UndefinedOmitted<T extends object> = {
156
209
  [K in keyof T as undefined extends T[K] ? never : K]: T[K];
@@ -209,6 +262,7 @@ export interface SubagentParamsLike {
209
262
  index?: number;
210
263
  view?: "fleet" | "transcript";
211
264
  lines?: number;
265
+ topic?: string;
212
266
  chainName?: string;
213
267
  config?: unknown;
214
268
  name?: string;
@@ -226,6 +280,7 @@ export interface SubagentParamsLike {
226
280
  /** Internal workflow ownership metadata; not part of the public schema. */
227
281
  workflowParentRunId?: string;
228
282
  workflowKey?: string;
283
+ workflowChildAsyncId?: string;
229
284
  suppressRoutineResultIntercom?: boolean;
230
285
  /** Internal durable-run compatibility fields. Public callers must use workflowScript. */
231
286
  chain?: ChainStep[];
@@ -356,6 +411,7 @@ function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefine
356
411
  function removeForegroundControlIfIdle(state: SubagentState, runId: string): boolean {
357
412
  const control = state.foregroundControls.get(runId);
358
413
  if (control && (!foregroundSchedulingSettled(control) || (control.activeChildren?.size ?? 0) > 0)) return false;
414
+ if (control) removeWorkflowForegroundSteeringRoute(control);
359
415
  state.foregroundControls.delete(runId);
360
416
  if (state.lastForegroundControlId === runId) state.lastForegroundControlId = null;
361
417
  return true;
@@ -473,7 +529,7 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
473
529
 
474
530
  function trimRememberedForegroundRuns(state: SubagentState): void {
475
531
  if (!state.foregroundRuns) return;
476
- while (state.foregroundRuns.size > 50) {
532
+ while (state.foregroundRuns.size > MAX_REMEMBERED_FOREGROUND_RUNS) {
477
533
  const oldestTerminal = [...state.foregroundRuns.values()]
478
534
  .filter((run) => !run.children.some((child) => child.status === "detached"))
479
535
  .sort((left, right) => left.updatedAt - right.updatedAt)[0];
@@ -482,6 +538,14 @@ function trimRememberedForegroundRuns(state: SubagentState): void {
482
538
  }
483
539
  }
484
540
 
541
+ function persistRememberedForegroundRuns(state: SubagentState): void {
542
+ try {
543
+ persistForegroundRunHistory(state);
544
+ } catch (error) {
545
+ console.error("Failed to persist foreground run history:", error);
546
+ }
547
+ }
548
+
485
549
  function foregroundChildActivityFromProgress(progress: SingleResult["progress"] | undefined) {
486
550
  return {
487
551
  ...(progress?.activityState ? { activityState: progress.activityState } : {}),
@@ -548,6 +612,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
548
612
  }),
549
613
  });
550
614
  trimRememberedForegroundRuns(state);
615
+ persistRememberedForegroundRuns(state);
551
616
  }
552
617
 
553
618
  function applyControlEventToRememberedForegroundRun(state: SubagentState, event: ControlEvent): void {
@@ -623,6 +688,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
623
688
  ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
624
689
  });
625
690
  trimRememberedForegroundRuns(state);
691
+ persistRememberedForegroundRuns(state);
626
692
  const output = getSingleResultOutput(input.result).trim();
627
693
  const success = terminalStatus === "completed";
628
694
  const summary = !success && input.result.error
@@ -1277,6 +1343,7 @@ async function resumeAsyncRun(input: {
1277
1343
  deps: ExecutorDeps;
1278
1344
  parentModel?: ParentModel;
1279
1345
  absoluteDeadlineAt?: number;
1346
+ signal?: AbortSignal;
1280
1347
  }): Promise<AgentToolResult<Details>> {
1281
1348
  const followUp = (input.params.message ?? input.params.task ?? "").trim();
1282
1349
  const attachChain = (input.params.chain?.length ?? 0) > 0 ? input.params.chain as ChainStep[] : undefined;
@@ -1392,7 +1459,7 @@ async function resumeAsyncRun(input: {
1392
1459
  inheritProjectContext: recoveryDescriptor.inheritProjectContext,
1393
1460
  inheritSkills: recoveryDescriptor.inheritSkills,
1394
1461
  source: "project",
1395
- filePath: recoveryDescriptor.agentFilePath ?? path.join(recoveryDescriptor.cwd, ".pi-subagents-recovery-agent"),
1462
+ filePath: recoveryDescriptor.agentFilePath ?? path.join(getProjectSubagentsDir(recoveryDescriptor.cwd), "recovery-agent"),
1396
1463
  } : undefined);
1397
1464
  if (!agentConfig) {
1398
1465
  return {
@@ -1563,6 +1630,58 @@ async function resumeAsyncRun(input: {
1563
1630
  }
1564
1631
 
1565
1632
  const revivedId = result.details.asyncId ?? runId;
1633
+ if (input.params.workflowParentRunId !== undefined && result.details.asyncDir) {
1634
+ const asyncDir = result.details.asyncDir;
1635
+ const resultPath = workflowAwaitedAsyncResultPath(asyncDir);
1636
+ const stopOnAbort = () => { stopAsyncRun(input.deps.state, revivedId, input.deps.kill, { asyncDir, resolvedId: revivedId }); };
1637
+ if (input.signal?.aborted) stopOnAbort();
1638
+ else input.signal?.addEventListener("abort", stopOnAbort, { once: true });
1639
+ let completed: Awaited<ReturnType<typeof waitForImportedAsyncRoot>>;
1640
+ try {
1641
+ completed = await waitForImportedAsyncRoot({ runId: revivedId, asyncDir, resultPath, index: 0 });
1642
+ } finally {
1643
+ input.signal?.removeEventListener("abort", stopOnAbort);
1644
+ }
1645
+ fs.rmSync(resultPath, { force: true });
1646
+ const totalCost = completed.totalCost;
1647
+ const childResult: SingleResult = {
1648
+ index: 0,
1649
+ agent: completed.agent,
1650
+ task: effectiveFollowUp,
1651
+ exitCode: completed.exitCode,
1652
+ usage: {
1653
+ input: totalCost?.inputTokens ?? 0,
1654
+ output: totalCost?.outputTokens ?? 0,
1655
+ cacheRead: 0,
1656
+ cacheWrite: 0,
1657
+ cost: totalCost?.costUsd ?? 0,
1658
+ turns: 0,
1659
+ },
1660
+ finalOutput: completed.output,
1661
+ outputState: completed.output.trim() ? "present" : "absent",
1662
+ ...(completed.error ? { error: completed.error } : {}),
1663
+ ...(completed.timedOut ? { timedOut: true } : {}),
1664
+ ...(completed.stopped ? { stopped: true } : {}),
1665
+ ...(completed.sessionFile ? { sessionFile: completed.sessionFile } : {}),
1666
+ ...(completed.model ? { model: completed.model } : {}),
1667
+ ...(completed.attemptedModels ? { attemptedModels: completed.attemptedModels } : {}),
1668
+ ...(completed.modelAttempts ? { modelAttempts: completed.modelAttempts } : {}),
1669
+ ...(completed.structuredOutput !== undefined ? { structuredOutput: completed.structuredOutput } : {}),
1670
+ ...(completed.structuredOutputPath ? { structuredOutputPath: completed.structuredOutputPath } : {}),
1671
+ ...(completed.structuredOutputSchemaPath ? { structuredOutputSchemaPath: completed.structuredOutputSchemaPath } : {}),
1672
+ ...(completed.acceptance ? { acceptance: completed.acceptance } : {}),
1673
+ };
1674
+ return {
1675
+ content: [{ type: "text", text: completed.output || completed.error || `Revived ${target.source} subagent ${revivedId} completed without output.` }],
1676
+ ...(completed.success ? {} : { isError: true }),
1677
+ details: {
1678
+ ...result.details,
1679
+ runId: revivedId,
1680
+ results: [childResult],
1681
+ ...(target.launchContractDigest ? { sourceLaunchContractDigest: target.launchContractDigest } : {}),
1682
+ },
1683
+ };
1684
+ }
1566
1685
  const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
1567
1686
  const sourceLabel = target.source;
1568
1687
  const lines = [
@@ -1986,7 +2105,17 @@ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: Agen
1986
2105
 
1987
2106
  export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000;
1988
2107
 
1989
- function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2108
+ // Async single-agent runs also need a wall-clock backstop: a child whose bash
2109
+ // tool blocks forever (e.g. a background process inheriting the terminal with
2110
+ // no bash `timeout` arg) would otherwise hang the parent indefinitely with
2111
+ // zero signal. Same generous default as foreground; explicit timeoutMs/
2112
+ // maxRuntimeMs and agent-level defaultTimeoutMs remain authoritative.
2113
+ //
2114
+ // Deliberately NOT applied at the workflow level: async scripted workflows
2115
+ // stay unbounded as a whole, while each runner child has its own deadline.
2116
+ export { DEFAULT_ASYNC_TIMEOUT_MS };
2117
+
2118
+ export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
1990
2119
  const rawTimeout = params.timeoutMs;
1991
2120
  const rawMaxRuntime = params.maxRuntimeMs;
1992
2121
  if (rawTimeout === undefined && rawMaxRuntime === undefined) {
@@ -2005,6 +2134,21 @@ function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?:
2005
2134
  return timeoutMs === undefined ? {} : { timeoutMs };
2006
2135
  }
2007
2136
 
2137
+ /**
2138
+ * Resolve the effective launch timeout for a single-agent run, applying the
2139
+ * async/foreground default when neither the caller nor the agent set one.
2140
+ *
2141
+ * The async default is deliberately applied only to plain single-agent
2142
+ * launches. Composite launches keep their top-level execution unbounded when
2143
+ * no timeout is set; their runner children resolve separate deadlines.
2144
+ * Exported so the executor wiring is directly testable.
2145
+ */
2146
+ export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean): { timeoutMs?: number; error?: string } {
2147
+ const isComposite = (params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || params.workflowScript !== undefined;
2148
+ const defaultTimeoutMs = !async ? DEFAULT_FOREGROUND_TIMEOUT_MS : isComposite ? undefined : DEFAULT_ASYNC_TIMEOUT_MS;
2149
+ return resolveForegroundTimeout(params, defaultTimeoutMs);
2150
+ }
2151
+
2008
2152
  function resolveToolBudget(
2009
2153
  raw: unknown,
2010
2154
  label = "toolBudget",
@@ -2414,7 +2558,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2414
2558
  details: { mode: "single" as const, results: [] },
2415
2559
  };
2416
2560
  }
2417
- const id = randomUUID();
2561
+ const requestedWorkflowChildAsyncId = typeof params.workflowChildAsyncId === "string" ? params.workflowChildAsyncId.trim() : "";
2562
+ const id = requestedWorkflowChildAsyncId && path.basename(requestedWorkflowChildAsyncId) === requestedWorkflowChildAsyncId ? requestedWorkflowChildAsyncId : randomUUID();
2418
2563
  const parentModel = data.parentModel;
2419
2564
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2420
2565
  pi: deps.pi,
@@ -3044,6 +3189,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3044
3189
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
3045
3190
  permissions: input.permissions,
3046
3191
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
3192
+ ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
3047
3193
  context: input.contextPolicy.contextForAgent(task.agent),
3048
3194
  cwd: taskCwd,
3049
3195
  signal: input.signal,
@@ -3719,8 +3865,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3719
3865
  // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
3720
3866
  // absolute paths pass through; relative paths resolve against the child cwd.
3721
3867
  const reads = readsOverride !== undefined ? readsOverride : agentConfig.defaultReads ?? false;
3722
- const readsInstruction = Array.isArray(reads) && reads.length > 0
3723
- ? `[Read from: ${reads.map((f) => resolveChainPath(f, effectiveCwd)).join(", ")}]\n\n`
3868
+ const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, effectiveCwd) : [];
3869
+ const readsInstruction = readPaths.length > 0
3870
+ ? `[Read from: ${readPaths.join(", ")}]\n\n`
3724
3871
  : "";
3725
3872
  task = readsInstruction + task;
3726
3873
  task = injectSingleOutputInstruction(task, outputPath, agentConfig);
@@ -3764,6 +3911,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3764
3911
  r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3765
3912
  permissions: deps.config.permissions,
3766
3913
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3914
+ ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
3767
3915
  context: data.contextPolicy.contextForAgent(params.agent!),
3768
3916
  cwd: effectiveCwd,
3769
3917
  signal,
@@ -3944,6 +4092,61 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
3944
4092
 
3945
4093
  const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string }) => void>();
3946
4094
 
4095
+ function recordMissionWorkflowChild(
4096
+ binding: MissionLaunchBinding | undefined,
4097
+ workflowRunId: string,
4098
+ key: string,
4099
+ update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
4100
+ ): void {
4101
+ if (!binding) return;
4102
+ try {
4103
+ updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...update }] });
4104
+ } catch (error) {
4105
+ console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
4106
+ }
4107
+ }
4108
+
4109
+ export function missionWorkflowChildStatus(result: AgentToolResult<Details>): string {
4110
+ const childResults = result.details.results;
4111
+ if (childResults.some((child) => child.detached || child.interrupted)) return "paused";
4112
+ if (result.isError === true || childResults.some((child) => child.exitCode !== 0)) return "failed";
4113
+ if (childResults.length === 0 && (result.details.asyncId || result.details.asyncDir)) return "running";
4114
+ return "completed";
4115
+ }
4116
+
4117
+ export async function runMissionWorkflowChild(
4118
+ binding: MissionLaunchBinding | undefined,
4119
+ workflowRunId: string,
4120
+ key: string,
4121
+ phase: string | undefined,
4122
+ run: () => Promise<AgentToolResult<Details>>,
4123
+ ): Promise<AgentToolResult<Details>> {
4124
+ try {
4125
+ return await run();
4126
+ } catch (error) {
4127
+ recordMissionWorkflowChild(binding, workflowRunId, key, {
4128
+ status: "failed",
4129
+ completedAt: new Date().toISOString(),
4130
+ heartbeat: { status: "failed", ...(phase ? { phase } : {}), message: error instanceof Error ? error.message : String(error) },
4131
+ });
4132
+ throw error;
4133
+ }
4134
+ }
4135
+
4136
+ export function bindMissionWorkflowChildAsyncLaunch(
4137
+ params: SubagentParamsLike,
4138
+ binding: MissionLaunchBinding | undefined,
4139
+ asyncByDefault: boolean,
4140
+ asyncId: string = randomUUID(),
4141
+ ): SubagentParamsLike {
4142
+ const requestedAsync = params.async ?? asyncByDefault;
4143
+ if (!binding || !requestedAsync || params.clarify === true) return params;
4144
+ const id = asyncId.trim();
4145
+ if (!id || path.basename(id) !== id) throw new Error("workflow child async id must be a single path segment");
4146
+ writeMissionAsyncBinding(path.join(DIRS.async, id), binding);
4147
+ return { ...params, workflowChildAsyncId: id };
4148
+ }
4149
+
3947
4150
  function workflowChildResult(key: string, result: AgentToolResult<Details>): WorkflowScriptChildResult {
3948
4151
  const receiptOutput = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
3949
4152
  const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
@@ -3960,9 +4163,11 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
3960
4163
  if (child.sessionFile) artifactPaths.add(child.sessionFile);
3961
4164
  }
3962
4165
  const structured = result.details.results.map((child) => child.structuredOutput).filter((value) => value !== undefined);
4166
+ const resolvedAgents = [...new Set(result.details.results.map((child) => child.agent).filter((agent): agent is string => Boolean(agent)))];
3963
4167
  return {
3964
4168
  key,
3965
4169
  ok,
4170
+ ...(resolvedAgents.length === 1 ? { agent: resolvedAgents[0] } : {}),
3966
4171
  ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
3967
4172
  output,
3968
4173
  ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
@@ -4000,6 +4205,7 @@ export function prepareWorkflowLaunchParams(
4000
4205
  }
4001
4206
  const launchParams = {
4002
4207
  ...workflowDefaults,
4208
+ async: false,
4003
4209
  ...childParams,
4004
4210
  ...(options.missionDetached ? { mission: false } : {}),
4005
4211
  workflowParentRunId: parentWorkflowRunId,
@@ -4178,6 +4384,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4178
4384
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4179
4385
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4180
4386
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
4387
+ const workflowPrompts = { render: (ref: string, vars?: unknown) => renderWorkflowPrompt(ref, vars, workflowCwd) };
4181
4388
  const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd, workflowCwd, background: requestParams.async !== false });
4182
4389
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
4183
4390
  const chatProgress = chatProgressResult.projection!;
@@ -4313,22 +4520,41 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4313
4520
  void Promise.resolve().then(async () => {
4314
4521
  const workflowResults: SingleResult[] = [];
4315
4522
  const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = workflowRequest;
4523
+ const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
4524
+ let projectedTraceLength = 0;
4525
+ let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
4316
4526
  const updateTrace = (trace: NonNullable<Details["workflow"]>["trace"]) => {
4317
4527
  status.workflow = { ...(status.workflow ?? { emits: [], console: [] }), trace };
4318
- for (const entry of trace.filter((candidate) => candidate.operation === "run")) {
4319
- const existing = status.steps?.find((step) => step.workflowKey === entry.key);
4528
+ const rebuild = trace.length < projectedTraceLength
4529
+ || (projectedTraceLength > 0 && trace[projectedTraceLength - 1] !== projectedTraceTail);
4530
+ if (rebuild) {
4531
+ workflowSteps.clear();
4532
+ for (const step of status.steps ?? []) {
4533
+ if (step.workflowKey) workflowSteps.set(step.workflowKey, step);
4534
+ }
4535
+ projectedTraceLength = 0;
4536
+ }
4537
+ for (let index = projectedTraceLength; index < trace.length; index += 1) {
4538
+ const entry = trace[index]!;
4539
+ if (entry.operation !== "run") continue;
4540
+ const existing = workflowSteps.get(entry.key);
4320
4541
  if (entry.state === "reused" && existing) continue;
4321
4542
  const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
4322
4543
  if (existing) {
4323
4544
  existing.status = mapped;
4545
+ if (entry.agent) existing.agent = entry.agent;
4324
4546
  if (entry.error === undefined) delete existing.error;
4325
4547
  else existing.error = entry.error;
4326
4548
  if (entry.durationMs === undefined) delete existing.durationMs;
4327
4549
  else existing.durationMs = entry.durationMs;
4328
4550
  } else {
4329
- status.steps?.push({ agent: entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() });
4551
+ const step: NonNullable<AsyncStatus["steps"]>[number] = { agent: entry.agent ?? entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() };
4552
+ status.steps?.push(step);
4553
+ workflowSteps.set(entry.key, step);
4330
4554
  }
4331
4555
  }
4556
+ projectedTraceLength = trace.length;
4557
+ projectedTraceTail = trace.at(-1);
4332
4558
  projectWorkflowActivity();
4333
4559
  persist();
4334
4560
  appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
@@ -4338,6 +4564,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4338
4564
  script: workflowScript,
4339
4565
  timeoutMs: timeout,
4340
4566
  signal: controller.signal,
4567
+ prompts: workflowPrompts,
4341
4568
  ...(workflowState ? { state: workflowState } : {}),
4342
4569
  onTrace: updateTrace,
4343
4570
  onEmit: (emits) => {
@@ -4351,37 +4578,70 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4351
4578
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4352
4579
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4353
4580
  patchMissionObjective(childParams.task);
4354
- const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions });
4355
- workflowLaunchObservers.set(childRequest, (launch) => {
4356
- const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4357
- if (!step) return;
4358
- step.agent = launch.agent;
4359
- step.sessionFile = launch.sessionFile;
4360
- persist();
4581
+ const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4582
+ const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4583
+ recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4584
+ status: "running",
4585
+ ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4586
+ ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4587
+ ...(childLabel ? { label: childLabel } : {}),
4588
+ ...(childPhase ? { phase: childPhase } : {}),
4589
+ heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4590
+ });
4591
+ const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4592
+ const childRequest = bindMissionWorkflowChildAsyncLaunch(
4593
+ prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions }),
4594
+ missionBinding,
4595
+ deps.asyncByDefault,
4596
+ );
4597
+ workflowLaunchObservers.set(childRequest, (launch) => {
4598
+ const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4599
+ if (step) {
4600
+ step.agent = launch.agent;
4601
+ step.sessionFile = launch.sessionFile;
4602
+ persist();
4603
+ }
4604
+ recordMissionWorkflowChild(missionBinding, workflowRunId, key, { status: "running", agent: launch.agent, ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}) });
4605
+ });
4606
+ return execute(randomUUID(), childRequest, workflowSignal, (update) => {
4607
+ const progress = update.details.progress?.[0];
4608
+ const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4609
+ if (!progress || !step) return;
4610
+ step.status = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4611
+ step.activityState = progress.activityState;
4612
+ step.lastActivityAt = progress.lastActivityAt;
4613
+ step.currentTool = progress.currentTool;
4614
+ step.currentToolArgs = progress.currentToolArgs;
4615
+ step.currentToolStartedAt = progress.currentToolStartedAt;
4616
+ step.currentPath = progress.currentPath;
4617
+ step.recentTools = progress.recentTools.map((tool) => ({ ...tool }));
4618
+ step.recentOutput = [...progress.recentOutput];
4619
+ step.turnCount = progress.turnCount;
4620
+ step.toolCount = progress.toolCount;
4621
+ step.model = progress.model;
4622
+ step.thinking = progress.thinking;
4623
+ step.error = progress.error;
4624
+ projectWorkflowActivity();
4625
+ persist();
4626
+ recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4627
+ status: step.status,
4628
+ heartbeat: { status: step.status, ...(childPhase ? { phase: childPhase } : {}) },
4629
+ });
4630
+ }, ctx, preserveActiveSession);
4361
4631
  });
4362
- const result = await execute(randomUUID(), childRequest, workflowSignal, (update) => {
4363
- const progress = update.details.progress?.[0];
4364
- const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4365
- if (!progress || !step) return;
4366
- step.status = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4367
- step.activityState = progress.activityState;
4368
- step.lastActivityAt = progress.lastActivityAt;
4369
- step.currentTool = progress.currentTool;
4370
- step.currentToolArgs = progress.currentToolArgs;
4371
- step.currentToolStartedAt = progress.currentToolStartedAt;
4372
- step.currentPath = progress.currentPath;
4373
- step.recentTools = progress.recentTools.map((tool) => ({ ...tool }));
4374
- step.recentOutput = [...progress.recentOutput];
4375
- step.turnCount = progress.turnCount;
4376
- step.toolCount = progress.toolCount;
4377
- step.model = progress.model;
4378
- step.thinking = progress.thinking;
4379
- step.error = progress.error;
4380
- projectWorkflowActivity();
4381
- persist();
4382
- }, ctx, preserveActiveSession);
4383
4632
  workflowResults.push(...result.details.results);
4633
+ if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4384
4634
  const child = workflowChildResult(key, result);
4635
+ const childStatus = missionWorkflowChildStatus(result);
4636
+ recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4637
+ status: childStatus,
4638
+ ...(child.runId ? { runId: child.runId } : {}),
4639
+ ...(result.details.results[0]?.agent ? { agent: result.details.results[0].agent } : {}),
4640
+ ...(result.details.results[0]?.sessionFile ? { sessionPath: result.details.results[0].sessionFile } : {}),
4641
+ artifactPaths: child.artifactPaths,
4642
+ ...(["completed", "failed"].includes(childStatus) ? { completedAt: new Date().toISOString() } : {}),
4643
+ heartbeat: { status: childStatus, ...(childPhase ? { phase: childPhase } : {}) },
4644
+ });
4385
4645
  if (result.details.asyncId) {
4386
4646
  const childJob = deps.state.asyncJobs.get(result.details.asyncId);
4387
4647
  if (childJob) { childJob.parentWorkflowRunId = workflowRunId; childJob.workflowKey = key; }
@@ -4395,14 +4655,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4395
4655
  const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
4396
4656
  const workflowUsage = sumResultsUsage(workflowResults);
4397
4657
  status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4398
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ agent: child.key, ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4658
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4399
4659
  persist();
4400
4660
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4401
4661
  } catch (error) {
4402
4662
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4403
4663
  const stopped = controller.signal.aborted;
4404
4664
  status = compactOptional<AsyncStatus>({ ...status, state: stopped ? "stopped" : "failed", stopped: stopped || undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
4405
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ agent: child.key, ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4665
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4406
4666
  persist();
4407
4667
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4408
4668
  } finally {
@@ -4426,6 +4686,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4426
4686
  script: requestParams.workflowScript,
4427
4687
  timeoutMs: timeout,
4428
4688
  signal,
4689
+ prompts: workflowPrompts,
4429
4690
  ...(workflowState ? { state: workflowState } : {}),
4430
4691
  onTrace: (trace) => {
4431
4692
  liveWorkflow = { ...liveWorkflow, trace };
@@ -4440,10 +4701,51 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4440
4701
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4441
4702
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4442
4703
  patchMissionObjective(childParams.task);
4443
- const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" });
4444
- const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4704
+ const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4705
+ const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4706
+ recordMissionWorkflowChild(missionBinding, _id, key, {
4707
+ status: "running",
4708
+ ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4709
+ ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4710
+ ...(childLabel ? { label: childLabel } : {}),
4711
+ ...(childPhase ? { phase: childPhase } : {}),
4712
+ heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4713
+ });
4714
+ const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
4715
+ const childRequest = bindMissionWorkflowChildAsyncLaunch(
4716
+ prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" }),
4717
+ missionBinding,
4718
+ deps.asyncByDefault,
4719
+ );
4720
+ workflowLaunchObservers.set(childRequest, (launch) => recordMissionWorkflowChild(missionBinding, _id, key, {
4721
+ status: "running",
4722
+ agent: launch.agent,
4723
+ ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}),
4724
+ }));
4725
+ return execute(randomUUID(), childRequest, workflowSignal, (update) => {
4726
+ const progress = update.details.progress?.[0];
4727
+ if (!progress) return;
4728
+ const progressStatus = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4729
+ recordMissionWorkflowChild(missionBinding, _id, key, {
4730
+ status: progressStatus,
4731
+ heartbeat: { status: progressStatus, ...(childPhase ? { phase: childPhase } : {}) },
4732
+ });
4733
+ }, ctx, preserveActiveSession);
4734
+ });
4445
4735
  workflowResults.push(...result.details.results);
4446
- return workflowChildResult(key, result);
4736
+ if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4737
+ const child = workflowChildResult(key, result);
4738
+ const childStatus = missionWorkflowChildStatus(result);
4739
+ recordMissionWorkflowChild(missionBinding, _id, key, {
4740
+ status: childStatus,
4741
+ ...(child.runId ? { runId: child.runId } : {}),
4742
+ ...(result.details.results[0]?.agent ? { agent: result.details.results[0].agent } : {}),
4743
+ ...(result.details.results[0]?.sessionFile ? { sessionPath: result.details.results[0].sessionFile } : {}),
4744
+ artifactPaths: child.artifactPaths,
4745
+ ...(["completed", "failed"].includes(childStatus) ? { completedAt: new Date().toISOString() } : {}),
4746
+ heartbeat: { status: childStatus, ...(childPhase ? { phase: childPhase } : {}) },
4747
+ });
4748
+ return child;
4447
4749
  },
4448
4750
  status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4449
4751
  });
@@ -4485,7 +4787,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4485
4787
  ? normalizeParentModel(ctx.model)
4486
4788
  : rememberParentModel(deps.state, requestSessionId, ctx.model);
4487
4789
  } catch (error) {
4488
- if (action?.toLowerCase() !== "doctor") throw error;
4790
+ if (action?.toLowerCase() !== "doctor" && action?.toLowerCase() !== "guide") throw error;
4489
4791
  requestParentModel = normalizeParentModel(ctx.model);
4490
4792
  }
4491
4793
  if (action) {
@@ -4667,6 +4969,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4667
4969
  details: { mode: "management", results: [], spawnBudget: granted.snapshot },
4668
4970
  };
4669
4971
  }
4972
+ if (action === "guide") {
4973
+ try {
4974
+ return {
4975
+ content: [{ type: "text", text: readSubagentGuide(paramsWithResolvedCwd.topic) }],
4976
+ details: { mode: "management", results: [] },
4977
+ };
4978
+ } catch (error) {
4979
+ return {
4980
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
4981
+ isError: true,
4982
+ details: { mode: "management", results: [] },
4983
+ };
4984
+ }
4985
+ }
4670
4986
  if (action === "children.list") {
4671
4987
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4672
4988
  const children = listRetainedChildren(DIRS.async, deps.state.currentSessionId);
@@ -4817,7 +5133,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4817
5133
  }
4818
5134
  }
4819
5135
  if (action === "resume") {
4820
- return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
5136
+ return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel, signal }));
4821
5137
  }
4822
5138
  if (action === "steer") {
4823
5139
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
@@ -4828,6 +5144,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4828
5144
  try {
4829
5145
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
4830
5146
  const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir);
5147
+ const directoryStatus = location.asyncDir ? readStatus(location.asyncDir) : null;
5148
+ if (directoryStatus?.mode === "workflow") {
5149
+ const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: directoryStatus.runId || runId, asyncDirRoot: DIRS.async });
5150
+ if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
5151
+ return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
5152
+ }
4831
5153
  if (location.asyncDir) {
4832
5154
  const unsupported = externalRunnerControlError(location.asyncDir, "steer");
4833
5155
  if (unsupported) return unsupported;
@@ -4863,8 +5185,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4863
5185
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
4864
5186
  }
4865
5187
  if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal }));
4866
- if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='steer' currently supports live async Pi child sessions only; use action='interrupt' or action='resume' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
5188
+ if (resolved?.kind === "foreground") {
5189
+ const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, childRunId: resolved.id, asyncDirRoot: DIRS.async });
5190
+ if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
5191
+ return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
5192
+ }
4867
5193
  if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
5194
+ const resolvedStatus = resolved.location.asyncDir ? readStatus(resolved.location.asyncDir) : null;
5195
+ if (resolvedStatus?.mode === "workflow") {
5196
+ const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: resolvedStatus.runId || resolved.id, asyncDirRoot: DIRS.async });
5197
+ if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
5198
+ return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
5199
+ }
4868
5200
  if (resolved.location.asyncDir) {
4869
5201
  const unsupported = externalRunnerControlError(resolved.location.asyncDir, "steer");
4870
5202
  if (unsupported) return unsupported;
@@ -4898,6 +5230,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4898
5230
  return appendStepToAsyncChain(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
4899
5231
  }
4900
5232
  if (action.startsWith("schedule.")) {
5233
+ if (!isScheduledRunAction(action)) {
5234
+ return { content: [{ type: "text", text: unknownSubagentActionMessage(action) }], isError: true, details: { mode: "management", results: [] } };
5235
+ }
4901
5236
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4902
5237
  return {
4903
5238
  content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
@@ -5004,7 +5339,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5004
5339
  }
5005
5340
  if (!(SUBAGENT_ACTIONS as readonly string[]).includes(action)) {
5006
5341
  return {
5007
- content: [{ type: "text", text: `Unknown action: ${action}. Valid: ${SUBAGENT_ACTIONS.join(", ")}` }],
5342
+ content: [{ type: "text", text: unknownSubagentActionMessage(action) }],
5008
5343
  isError: true,
5009
5344
  details: { mode: "management" as const, results: [] },
5010
5345
  };
@@ -5168,9 +5503,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5168
5503
  if (externalAgent && (!effectiveAsync || effectiveParams.foregroundOnly === true)) {
5169
5504
  return buildRequestedModeError(effectiveParams, `Agent '${externalAgent.name}' uses runner.type='external-cli', which currently supports async/background execution only. Omit async or pass async:true; clarify and foregroundOnly are unsupported.`);
5170
5505
  }
5171
- const foregroundTimeout = resolveForegroundTimeout(
5506
+ const foregroundTimeout = resolveSingleAgentLaunchTimeout(
5172
5507
  effectiveParams,
5173
- effectiveAsync ? undefined : DEFAULT_FOREGROUND_TIMEOUT_MS,
5508
+ effectiveAsync,
5174
5509
  );
5175
5510
  if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
5176
5511
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
@@ -5320,12 +5655,26 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5320
5655
  const foregroundDescription = effectiveParams.task?.trim()
5321
5656
  || effectiveParams.tasks?.[0]?.task?.trim()
5322
5657
  || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
5658
+ const parentWorkflowStatus = effectiveParams.workflowParentRunId
5659
+ ? readStatus(path.join(DIRS.async, effectiveParams.workflowParentRunId))
5660
+ : null;
5661
+ const workflowSteeringDir = effectiveParams.workflowParentRunId
5662
+ && requestSessionId
5663
+ && deps.state.workflowControllers?.has(effectiveParams.workflowParentRunId)
5664
+ && parentWorkflowStatus?.mode === "workflow"
5665
+ && (parentWorkflowStatus.state === "running" || parentWorkflowStatus.state === "queued")
5666
+ && parentWorkflowStatus.sessionId === requestSessionId
5667
+ ? workflowForegroundSteeringDir(DIRS.async, effectiveParams.workflowParentRunId, runId)
5668
+ : undefined;
5323
5669
  const foregroundControl: ForegroundRunControl | undefined = effectiveAsync
5324
5670
  ? undefined
5325
5671
  : compactOptional<ForegroundRunControl>({
5326
5672
  runId,
5327
5673
  sessionId: requestSessionId,
5328
5674
  mode: foregroundMode,
5675
+ ...(effectiveParams.workflowParentRunId ? { parentWorkflowRunId: effectiveParams.workflowParentRunId } : {}),
5676
+ ...(effectiveParams.workflowKey ? { workflowKey: effectiveParams.workflowKey } : {}),
5677
+ workflowSteeringDir,
5329
5678
  startedAt: Date.now(),
5330
5679
  updatedAt: Date.now(),
5331
5680
  cwd: effectiveCwd,