pi-subagents 0.45.1 → 0.46.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 (48) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +342 -0
  4. package/docs/configuration.md +320 -0
  5. package/docs/extension-api.md +308 -0
  6. package/docs/missions.md +117 -0
  7. package/docs/models.md +190 -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 +2 -2
  14. package/src/agents/agents.ts +17 -8
  15. package/src/agents/frontmatter.ts +7 -3
  16. package/src/agents/skills.ts +2 -9
  17. package/src/api/project-panes.ts +30 -0
  18. package/src/extension/config.ts +15 -1
  19. package/src/extension/index.ts +36 -16
  20. package/src/extension/schemas.ts +3 -2
  21. package/src/extension/subagent-guide.ts +39 -0
  22. package/src/extension/tool-description.ts +4 -4
  23. package/src/inspectors/herdr/project-panes.ts +457 -62
  24. package/src/missions/actions.ts +25 -2
  25. package/src/missions/lifecycle.ts +21 -2
  26. package/src/missions/store.ts +77 -1
  27. package/src/missions/types.ts +33 -0
  28. package/src/runs/background/async-execution.ts +7 -1
  29. package/src/runs/background/completion-replay.ts +267 -0
  30. package/src/runs/background/result-watcher.ts +12 -4
  31. package/src/runs/background/resume-guidance.ts +33 -0
  32. package/src/runs/background/subagent-wait.ts +5 -2
  33. package/src/runs/background/wait-completions.ts +39 -5
  34. package/src/runs/background/wait-subscriptions.ts +21 -5
  35. package/src/runs/foreground/execution.ts +4 -0
  36. package/src/runs/foreground/foreground-history.ts +137 -0
  37. package/src/runs/foreground/subagent-executor.ts +326 -38
  38. package/src/runs/shared/subagent-prompt-runtime.ts +71 -5
  39. package/src/shared/fork-context.ts +13 -0
  40. package/src/shared/prompt-resources.ts +51 -0
  41. package/src/shared/types.ts +30 -1
  42. package/src/shared/utf8.ts +11 -0
  43. package/src/slash/prompt-workflows.ts +2 -15
  44. package/src/slash/slash-commands.ts +19 -1
  45. package/src/tui/fleet-status.ts +8 -2
  46. package/src/tui/fleet.ts +135 -25
  47. package/src/tui/render.ts +120 -7
  48. package/src/workflows/scripted-workflow.ts +187 -11
@@ -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";
@@ -49,8 +51,8 @@ import {
49
51
  type StepOverrides,
50
52
  } from "../../shared/settings.ts";
51
53
  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";
54
+ import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
55
+ import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
54
56
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
55
57
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
56
58
  import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
@@ -84,20 +86,22 @@ import { updateSteeringTarget, waitForSteeringAction } from "../background/steer
84
86
  import { steerAsyncRun } from "./async-steering-action.ts";
85
87
  import { stopAsyncRun } from "./async-stop-action.ts";
86
88
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
87
- import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
89
+ import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
88
90
  import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
89
91
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
90
92
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
91
93
  import { inspectSubagentStatus } from "../background/run-status.ts";
92
94
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
93
95
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
94
- import { attachMissionToLaunchResult, prepareMissionLaunch, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
96
+ import { attachMissionToLaunchResult, prepareMissionLaunch, writeMissionAsyncBinding, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
95
97
  import { updateMission } from "../../missions/store.ts";
98
+ import type { MissionWorkflowChildUpdate } from "../../missions/types.ts";
96
99
  import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
97
100
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
98
101
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
99
102
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
100
103
  import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
104
+ import { renderWorkflowPrompt } from "../../shared/prompt-resources.ts";
101
105
  import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
102
106
  import {
103
107
  cleanupWorktrees,
@@ -150,7 +154,48 @@ import {
150
154
  wrapForkTask,
151
155
  } from "../../shared/types.ts";
152
156
 
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"]);
157
+ 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"]);
158
+ 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"]);
159
+
160
+ function editDistance(left: string, right: string): number {
161
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
162
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
163
+ let diagonal = previous[0]!;
164
+ previous[0] = leftIndex;
165
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
166
+ const above = previous[rightIndex]!;
167
+ previous[rightIndex] = left[leftIndex - 1] === right[rightIndex - 1]
168
+ ? diagonal
169
+ : Math.min(diagonal, above, previous[rightIndex - 1]!) + 1;
170
+ diagonal = above;
171
+ }
172
+ }
173
+ return previous[right.length]!;
174
+ }
175
+
176
+ function hasSingleAdjacentTransposition(left: string, right: string): boolean {
177
+ if (left.length !== right.length) return false;
178
+ const mismatch = [...left].findIndex((character, index) => character !== right[index]);
179
+ return mismatch >= 0
180
+ && left[mismatch] === right[mismatch + 1]
181
+ && left[mismatch + 1] === right[mismatch]
182
+ && left.slice(mismatch + 2) === right.slice(mismatch + 2);
183
+ }
184
+
185
+ export function unknownSubagentActionMessage(action: string): string {
186
+ const requested = action.toLowerCase();
187
+ const suggestion = SUBAGENT_ACTIONS.find((candidate) => {
188
+ const distance = editDistance(requested, candidate);
189
+ const closeMatch = distance <= Math.max(1, Math.floor(candidate.length / 4)) || hasSingleAdjacentTransposition(requested, candidate);
190
+ if (DESTRUCTIVE_MANAGEMENT_ACTIONS.has(candidate)) return distance === 1 && requested.length >= candidate.length - 1;
191
+ return closeMatch;
192
+ });
193
+ const nextStep = 'Use subagent({ action: "status" }) to inspect runs or subagent({ action: "list" }) to inspect agents.';
194
+ const validActions = `Valid: ${SUBAGENT_ACTIONS.join(", ")}.`;
195
+ return suggestion
196
+ ? `Unknown action: ${action}. Did you mean ${suggestion}? ${nextStep} ${validActions}`
197
+ : `Unknown action: ${action}. ${nextStep} ${validActions}`;
198
+ }
154
199
 
155
200
  type UndefinedOmitted<T extends object> = {
156
201
  [K in keyof T as undefined extends T[K] ? never : K]: T[K];
@@ -209,6 +254,7 @@ export interface SubagentParamsLike {
209
254
  index?: number;
210
255
  view?: "fleet" | "transcript";
211
256
  lines?: number;
257
+ topic?: string;
212
258
  chainName?: string;
213
259
  config?: unknown;
214
260
  name?: string;
@@ -226,6 +272,7 @@ export interface SubagentParamsLike {
226
272
  /** Internal workflow ownership metadata; not part of the public schema. */
227
273
  workflowParentRunId?: string;
228
274
  workflowKey?: string;
275
+ workflowChildAsyncId?: string;
229
276
  suppressRoutineResultIntercom?: boolean;
230
277
  /** Internal durable-run compatibility fields. Public callers must use workflowScript. */
231
278
  chain?: ChainStep[];
@@ -473,7 +520,7 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
473
520
 
474
521
  function trimRememberedForegroundRuns(state: SubagentState): void {
475
522
  if (!state.foregroundRuns) return;
476
- while (state.foregroundRuns.size > 50) {
523
+ while (state.foregroundRuns.size > MAX_REMEMBERED_FOREGROUND_RUNS) {
477
524
  const oldestTerminal = [...state.foregroundRuns.values()]
478
525
  .filter((run) => !run.children.some((child) => child.status === "detached"))
479
526
  .sort((left, right) => left.updatedAt - right.updatedAt)[0];
@@ -482,6 +529,14 @@ function trimRememberedForegroundRuns(state: SubagentState): void {
482
529
  }
483
530
  }
484
531
 
532
+ function persistRememberedForegroundRuns(state: SubagentState): void {
533
+ try {
534
+ persistForegroundRunHistory(state);
535
+ } catch (error) {
536
+ console.error("Failed to persist foreground run history:", error);
537
+ }
538
+ }
539
+
485
540
  function foregroundChildActivityFromProgress(progress: SingleResult["progress"] | undefined) {
486
541
  return {
487
542
  ...(progress?.activityState ? { activityState: progress.activityState } : {}),
@@ -548,6 +603,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
548
603
  }),
549
604
  });
550
605
  trimRememberedForegroundRuns(state);
606
+ persistRememberedForegroundRuns(state);
551
607
  }
552
608
 
553
609
  function applyControlEventToRememberedForegroundRun(state: SubagentState, event: ControlEvent): void {
@@ -623,6 +679,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
623
679
  ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
624
680
  });
625
681
  trimRememberedForegroundRuns(state);
682
+ persistRememberedForegroundRuns(state);
626
683
  const output = getSingleResultOutput(input.result).trim();
627
684
  const success = terminalStatus === "completed";
628
685
  const summary = !success && input.result.error
@@ -1277,6 +1334,7 @@ async function resumeAsyncRun(input: {
1277
1334
  deps: ExecutorDeps;
1278
1335
  parentModel?: ParentModel;
1279
1336
  absoluteDeadlineAt?: number;
1337
+ signal?: AbortSignal;
1280
1338
  }): Promise<AgentToolResult<Details>> {
1281
1339
  const followUp = (input.params.message ?? input.params.task ?? "").trim();
1282
1340
  const attachChain = (input.params.chain?.length ?? 0) > 0 ? input.params.chain as ChainStep[] : undefined;
@@ -1563,6 +1621,58 @@ async function resumeAsyncRun(input: {
1563
1621
  }
1564
1622
 
1565
1623
  const revivedId = result.details.asyncId ?? runId;
1624
+ if (input.params.workflowParentRunId !== undefined && result.details.asyncDir) {
1625
+ const asyncDir = result.details.asyncDir;
1626
+ const resultPath = workflowAwaitedAsyncResultPath(asyncDir);
1627
+ const stopOnAbort = () => { stopAsyncRun(input.deps.state, revivedId, input.deps.kill, { asyncDir, resolvedId: revivedId }); };
1628
+ if (input.signal?.aborted) stopOnAbort();
1629
+ else input.signal?.addEventListener("abort", stopOnAbort, { once: true });
1630
+ let completed: Awaited<ReturnType<typeof waitForImportedAsyncRoot>>;
1631
+ try {
1632
+ completed = await waitForImportedAsyncRoot({ runId: revivedId, asyncDir, resultPath, index: 0 });
1633
+ } finally {
1634
+ input.signal?.removeEventListener("abort", stopOnAbort);
1635
+ }
1636
+ fs.rmSync(resultPath, { force: true });
1637
+ const totalCost = completed.totalCost;
1638
+ const childResult: SingleResult = {
1639
+ index: 0,
1640
+ agent: completed.agent,
1641
+ task: effectiveFollowUp,
1642
+ exitCode: completed.exitCode,
1643
+ usage: {
1644
+ input: totalCost?.inputTokens ?? 0,
1645
+ output: totalCost?.outputTokens ?? 0,
1646
+ cacheRead: 0,
1647
+ cacheWrite: 0,
1648
+ cost: totalCost?.costUsd ?? 0,
1649
+ turns: 0,
1650
+ },
1651
+ finalOutput: completed.output,
1652
+ outputState: completed.output.trim() ? "present" : "absent",
1653
+ ...(completed.error ? { error: completed.error } : {}),
1654
+ ...(completed.timedOut ? { timedOut: true } : {}),
1655
+ ...(completed.stopped ? { stopped: true } : {}),
1656
+ ...(completed.sessionFile ? { sessionFile: completed.sessionFile } : {}),
1657
+ ...(completed.model ? { model: completed.model } : {}),
1658
+ ...(completed.attemptedModels ? { attemptedModels: completed.attemptedModels } : {}),
1659
+ ...(completed.modelAttempts ? { modelAttempts: completed.modelAttempts } : {}),
1660
+ ...(completed.structuredOutput !== undefined ? { structuredOutput: completed.structuredOutput } : {}),
1661
+ ...(completed.structuredOutputPath ? { structuredOutputPath: completed.structuredOutputPath } : {}),
1662
+ ...(completed.structuredOutputSchemaPath ? { structuredOutputSchemaPath: completed.structuredOutputSchemaPath } : {}),
1663
+ ...(completed.acceptance ? { acceptance: completed.acceptance } : {}),
1664
+ };
1665
+ return {
1666
+ content: [{ type: "text", text: completed.output || completed.error || `Revived ${target.source} subagent ${revivedId} completed without output.` }],
1667
+ ...(completed.success ? {} : { isError: true }),
1668
+ details: {
1669
+ ...result.details,
1670
+ runId: revivedId,
1671
+ results: [childResult],
1672
+ ...(target.launchContractDigest ? { sourceLaunchContractDigest: target.launchContractDigest } : {}),
1673
+ },
1674
+ };
1675
+ }
1566
1676
  const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
1567
1677
  const sourceLabel = target.source;
1568
1678
  const lines = [
@@ -2414,7 +2524,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2414
2524
  details: { mode: "single" as const, results: [] },
2415
2525
  };
2416
2526
  }
2417
- const id = randomUUID();
2527
+ const requestedWorkflowChildAsyncId = typeof params.workflowChildAsyncId === "string" ? params.workflowChildAsyncId.trim() : "";
2528
+ const id = requestedWorkflowChildAsyncId && path.basename(requestedWorkflowChildAsyncId) === requestedWorkflowChildAsyncId ? requestedWorkflowChildAsyncId : randomUUID();
2418
2529
  const parentModel = data.parentModel;
2419
2530
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2420
2531
  pi: deps.pi,
@@ -3942,6 +4053,63 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
3942
4053
  };
3943
4054
  }
3944
4055
 
4056
+ const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string }) => void>();
4057
+
4058
+ function recordMissionWorkflowChild(
4059
+ binding: MissionLaunchBinding | undefined,
4060
+ workflowRunId: string,
4061
+ key: string,
4062
+ update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
4063
+ ): void {
4064
+ if (!binding) return;
4065
+ try {
4066
+ updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...update }] });
4067
+ } catch (error) {
4068
+ console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
4069
+ }
4070
+ }
4071
+
4072
+ export function missionWorkflowChildStatus(result: AgentToolResult<Details>): string {
4073
+ const childResults = result.details.results;
4074
+ if (childResults.some((child) => child.detached || child.interrupted)) return "paused";
4075
+ if (result.isError === true || childResults.some((child) => child.exitCode !== 0)) return "failed";
4076
+ if (childResults.length === 0 && (result.details.asyncId || result.details.asyncDir)) return "running";
4077
+ return "completed";
4078
+ }
4079
+
4080
+ export async function runMissionWorkflowChild(
4081
+ binding: MissionLaunchBinding | undefined,
4082
+ workflowRunId: string,
4083
+ key: string,
4084
+ phase: string | undefined,
4085
+ run: () => Promise<AgentToolResult<Details>>,
4086
+ ): Promise<AgentToolResult<Details>> {
4087
+ try {
4088
+ return await run();
4089
+ } catch (error) {
4090
+ recordMissionWorkflowChild(binding, workflowRunId, key, {
4091
+ status: "failed",
4092
+ completedAt: new Date().toISOString(),
4093
+ heartbeat: { status: "failed", ...(phase ? { phase } : {}), message: error instanceof Error ? error.message : String(error) },
4094
+ });
4095
+ throw error;
4096
+ }
4097
+ }
4098
+
4099
+ export function bindMissionWorkflowChildAsyncLaunch(
4100
+ params: SubagentParamsLike,
4101
+ binding: MissionLaunchBinding | undefined,
4102
+ asyncByDefault: boolean,
4103
+ asyncId: string = randomUUID(),
4104
+ ): SubagentParamsLike {
4105
+ const requestedAsync = params.async ?? asyncByDefault;
4106
+ if (!binding || !requestedAsync || params.clarify === true) return params;
4107
+ const id = asyncId.trim();
4108
+ if (!id || path.basename(id) !== id) throw new Error("workflow child async id must be a single path segment");
4109
+ writeMissionAsyncBinding(path.join(DIRS.async, id), binding);
4110
+ return { ...params, workflowChildAsyncId: id };
4111
+ }
4112
+
3945
4113
  function workflowChildResult(key: string, result: AgentToolResult<Details>): WorkflowScriptChildResult {
3946
4114
  const receiptOutput = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
3947
4115
  const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
@@ -3958,9 +4126,11 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
3958
4126
  if (child.sessionFile) artifactPaths.add(child.sessionFile);
3959
4127
  }
3960
4128
  const structured = result.details.results.map((child) => child.structuredOutput).filter((value) => value !== undefined);
4129
+ const resolvedAgents = [...new Set(result.details.results.map((child) => child.agent).filter((agent): agent is string => Boolean(agent)))];
3961
4130
  return {
3962
4131
  key,
3963
4132
  ok,
4133
+ ...(resolvedAgents.length === 1 ? { agent: resolvedAgents[0] } : {}),
3964
4134
  ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
3965
4135
  output,
3966
4136
  ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
@@ -3998,6 +4168,7 @@ export function prepareWorkflowLaunchParams(
3998
4168
  }
3999
4169
  const launchParams = {
4000
4170
  ...workflowDefaults,
4171
+ async: false,
4001
4172
  ...childParams,
4002
4173
  ...(options.missionDetached ? { mission: false } : {}),
4003
4174
  workflowParentRunId: parentWorkflowRunId,
@@ -4159,6 +4330,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4159
4330
  ctx: ExtensionContext,
4160
4331
  preserveActiveSession = false,
4161
4332
  ): Promise<AgentToolResult<Details>> => {
4333
+ const workflowLaunchObserver = workflowLaunchObservers.get(params);
4162
4334
  const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
4163
4335
  const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
4164
4336
  if (!preserveActiveSession) deps.state.baseCwd = ctx.cwd;
@@ -4175,6 +4347,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4175
4347
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4176
4348
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4177
4349
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
4350
+ const workflowPrompts = { render: (ref: string, vars?: unknown) => renderWorkflowPrompt(ref, vars, workflowCwd) };
4178
4351
  const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd, workflowCwd, background: requestParams.async !== false });
4179
4352
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
4180
4353
  const chatProgress = chatProgressResult.projection!;
@@ -4318,12 +4491,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4318
4491
  const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
4319
4492
  if (existing) {
4320
4493
  existing.status = mapped;
4494
+ if (entry.agent) existing.agent = entry.agent;
4321
4495
  if (entry.error === undefined) delete existing.error;
4322
4496
  else existing.error = entry.error;
4323
4497
  if (entry.durationMs === undefined) delete existing.durationMs;
4324
4498
  else existing.durationMs = entry.durationMs;
4325
4499
  } else {
4326
- status.steps?.push({ agent: entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() });
4500
+ status.steps?.push({ agent: entry.agent ?? entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() });
4327
4501
  }
4328
4502
  }
4329
4503
  projectWorkflowActivity();
@@ -4335,6 +4509,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4335
4509
  script: workflowScript,
4336
4510
  timeoutMs: timeout,
4337
4511
  signal: controller.signal,
4512
+ prompts: workflowPrompts,
4338
4513
  ...(workflowState ? { state: workflowState } : {}),
4339
4514
  onTrace: updateTrace,
4340
4515
  onEmit: (emits) => {
@@ -4348,30 +4523,70 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4348
4523
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4349
4524
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4350
4525
  patchMissionObjective(childParams.task);
4351
- const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions });
4352
- const result = await execute(randomUUID(), childRequest, workflowSignal, (update) => {
4353
- const progress = update.details.progress?.[0];
4354
- const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4355
- if (!progress || !step) return;
4356
- step.status = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4357
- step.activityState = progress.activityState;
4358
- step.lastActivityAt = progress.lastActivityAt;
4359
- step.currentTool = progress.currentTool;
4360
- step.currentToolArgs = progress.currentToolArgs;
4361
- step.currentToolStartedAt = progress.currentToolStartedAt;
4362
- step.currentPath = progress.currentPath;
4363
- step.recentTools = progress.recentTools.map((tool) => ({ ...tool }));
4364
- step.recentOutput = [...progress.recentOutput];
4365
- step.turnCount = progress.turnCount;
4366
- step.toolCount = progress.toolCount;
4367
- step.model = progress.model;
4368
- step.thinking = progress.thinking;
4369
- step.error = progress.error;
4370
- projectWorkflowActivity();
4371
- persist();
4372
- }, ctx, preserveActiveSession);
4526
+ const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4527
+ const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4528
+ recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4529
+ status: "running",
4530
+ ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4531
+ ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4532
+ ...(childLabel ? { label: childLabel } : {}),
4533
+ ...(childPhase ? { phase: childPhase } : {}),
4534
+ heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4535
+ });
4536
+ const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4537
+ const childRequest = bindMissionWorkflowChildAsyncLaunch(
4538
+ prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions }),
4539
+ missionBinding,
4540
+ deps.asyncByDefault,
4541
+ );
4542
+ workflowLaunchObservers.set(childRequest, (launch) => {
4543
+ const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4544
+ if (step) {
4545
+ step.agent = launch.agent;
4546
+ step.sessionFile = launch.sessionFile;
4547
+ persist();
4548
+ }
4549
+ recordMissionWorkflowChild(missionBinding, workflowRunId, key, { status: "running", agent: launch.agent, ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}) });
4550
+ });
4551
+ return execute(randomUUID(), childRequest, workflowSignal, (update) => {
4552
+ const progress = update.details.progress?.[0];
4553
+ const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4554
+ if (!progress || !step) return;
4555
+ step.status = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4556
+ step.activityState = progress.activityState;
4557
+ step.lastActivityAt = progress.lastActivityAt;
4558
+ step.currentTool = progress.currentTool;
4559
+ step.currentToolArgs = progress.currentToolArgs;
4560
+ step.currentToolStartedAt = progress.currentToolStartedAt;
4561
+ step.currentPath = progress.currentPath;
4562
+ step.recentTools = progress.recentTools.map((tool) => ({ ...tool }));
4563
+ step.recentOutput = [...progress.recentOutput];
4564
+ step.turnCount = progress.turnCount;
4565
+ step.toolCount = progress.toolCount;
4566
+ step.model = progress.model;
4567
+ step.thinking = progress.thinking;
4568
+ step.error = progress.error;
4569
+ projectWorkflowActivity();
4570
+ persist();
4571
+ recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4572
+ status: step.status,
4573
+ heartbeat: { status: step.status, ...(childPhase ? { phase: childPhase } : {}) },
4574
+ });
4575
+ }, ctx, preserveActiveSession);
4576
+ });
4373
4577
  workflowResults.push(...result.details.results);
4578
+ if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4374
4579
  const child = workflowChildResult(key, result);
4580
+ const childStatus = missionWorkflowChildStatus(result);
4581
+ recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4582
+ status: childStatus,
4583
+ ...(child.runId ? { runId: child.runId } : {}),
4584
+ ...(result.details.results[0]?.agent ? { agent: result.details.results[0].agent } : {}),
4585
+ ...(result.details.results[0]?.sessionFile ? { sessionPath: result.details.results[0].sessionFile } : {}),
4586
+ artifactPaths: child.artifactPaths,
4587
+ ...(["completed", "failed"].includes(childStatus) ? { completedAt: new Date().toISOString() } : {}),
4588
+ heartbeat: { status: childStatus, ...(childPhase ? { phase: childPhase } : {}) },
4589
+ });
4375
4590
  if (result.details.asyncId) {
4376
4591
  const childJob = deps.state.asyncJobs.get(result.details.asyncId);
4377
4592
  if (childJob) { childJob.parentWorkflowRunId = workflowRunId; childJob.workflowKey = key; }
@@ -4385,14 +4600,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4385
4600
  const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
4386
4601
  const workflowUsage = sumResultsUsage(workflowResults);
4387
4602
  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) };
4388
- 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 });
4603
+ 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 });
4389
4604
  persist();
4390
4605
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4391
4606
  } catch (error) {
4392
4607
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4393
4608
  const stopped = controller.signal.aborted;
4394
4609
  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 } });
4395
- 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 });
4610
+ 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 });
4396
4611
  persist();
4397
4612
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4398
4613
  } finally {
@@ -4416,6 +4631,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4416
4631
  script: requestParams.workflowScript,
4417
4632
  timeoutMs: timeout,
4418
4633
  signal,
4634
+ prompts: workflowPrompts,
4419
4635
  ...(workflowState ? { state: workflowState } : {}),
4420
4636
  onTrace: (trace) => {
4421
4637
  liveWorkflow = { ...liveWorkflow, trace };
@@ -4430,10 +4646,51 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4430
4646
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4431
4647
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4432
4648
  patchMissionObjective(childParams.task);
4433
- const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" });
4434
- const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4649
+ const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4650
+ const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4651
+ recordMissionWorkflowChild(missionBinding, _id, key, {
4652
+ status: "running",
4653
+ ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4654
+ ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4655
+ ...(childLabel ? { label: childLabel } : {}),
4656
+ ...(childPhase ? { phase: childPhase } : {}),
4657
+ heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4658
+ });
4659
+ const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
4660
+ const childRequest = bindMissionWorkflowChildAsyncLaunch(
4661
+ prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" }),
4662
+ missionBinding,
4663
+ deps.asyncByDefault,
4664
+ );
4665
+ workflowLaunchObservers.set(childRequest, (launch) => recordMissionWorkflowChild(missionBinding, _id, key, {
4666
+ status: "running",
4667
+ agent: launch.agent,
4668
+ ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}),
4669
+ }));
4670
+ return execute(randomUUID(), childRequest, workflowSignal, (update) => {
4671
+ const progress = update.details.progress?.[0];
4672
+ if (!progress) return;
4673
+ const progressStatus = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4674
+ recordMissionWorkflowChild(missionBinding, _id, key, {
4675
+ status: progressStatus,
4676
+ heartbeat: { status: progressStatus, ...(childPhase ? { phase: childPhase } : {}) },
4677
+ });
4678
+ }, ctx, preserveActiveSession);
4679
+ });
4435
4680
  workflowResults.push(...result.details.results);
4436
- return workflowChildResult(key, result);
4681
+ if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4682
+ const child = workflowChildResult(key, result);
4683
+ const childStatus = missionWorkflowChildStatus(result);
4684
+ recordMissionWorkflowChild(missionBinding, _id, key, {
4685
+ status: childStatus,
4686
+ ...(child.runId ? { runId: child.runId } : {}),
4687
+ ...(result.details.results[0]?.agent ? { agent: result.details.results[0].agent } : {}),
4688
+ ...(result.details.results[0]?.sessionFile ? { sessionPath: result.details.results[0].sessionFile } : {}),
4689
+ artifactPaths: child.artifactPaths,
4690
+ ...(["completed", "failed"].includes(childStatus) ? { completedAt: new Date().toISOString() } : {}),
4691
+ heartbeat: { status: childStatus, ...(childPhase ? { phase: childPhase } : {}) },
4692
+ });
4693
+ return child;
4437
4694
  },
4438
4695
  status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4439
4696
  });
@@ -4475,7 +4732,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4475
4732
  ? normalizeParentModel(ctx.model)
4476
4733
  : rememberParentModel(deps.state, requestSessionId, ctx.model);
4477
4734
  } catch (error) {
4478
- if (action?.toLowerCase() !== "doctor") throw error;
4735
+ if (action?.toLowerCase() !== "doctor" && action?.toLowerCase() !== "guide") throw error;
4479
4736
  requestParentModel = normalizeParentModel(ctx.model);
4480
4737
  }
4481
4738
  if (action) {
@@ -4657,6 +4914,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4657
4914
  details: { mode: "management", results: [], spawnBudget: granted.snapshot },
4658
4915
  };
4659
4916
  }
4917
+ if (action === "guide") {
4918
+ try {
4919
+ return {
4920
+ content: [{ type: "text", text: readSubagentGuide(paramsWithResolvedCwd.topic) }],
4921
+ details: { mode: "management", results: [] },
4922
+ };
4923
+ } catch (error) {
4924
+ return {
4925
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
4926
+ isError: true,
4927
+ details: { mode: "management", results: [] },
4928
+ };
4929
+ }
4930
+ }
4660
4931
  if (action === "children.list") {
4661
4932
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4662
4933
  const children = listRetainedChildren(DIRS.async, deps.state.currentSessionId);
@@ -4807,7 +5078,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4807
5078
  }
4808
5079
  }
4809
5080
  if (action === "resume") {
4810
- return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
5081
+ return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel, signal }));
4811
5082
  }
4812
5083
  if (action === "steer") {
4813
5084
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
@@ -4888,6 +5159,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4888
5159
  return appendStepToAsyncChain(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
4889
5160
  }
4890
5161
  if (action.startsWith("schedule.")) {
5162
+ if (!isScheduledRunAction(action)) {
5163
+ return { content: [{ type: "text", text: unknownSubagentActionMessage(action) }], isError: true, details: { mode: "management", results: [] } };
5164
+ }
4891
5165
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4892
5166
  return {
4893
5167
  content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
@@ -4994,7 +5268,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4994
5268
  }
4995
5269
  if (!(SUBAGENT_ACTIONS as readonly string[]).includes(action)) {
4996
5270
  return {
4997
- content: [{ type: "text", text: `Unknown action: ${action}. Valid: ${SUBAGENT_ACTIONS.join(", ")}` }],
5271
+ content: [{ type: "text", text: unknownSubagentActionMessage(action) }],
4998
5272
  isError: true,
4999
5273
  details: { mode: "management" as const, results: [] },
5000
5274
  };
@@ -5316,6 +5590,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5316
5590
  runId,
5317
5591
  sessionId: requestSessionId,
5318
5592
  mode: foregroundMode,
5593
+ ...(effectiveParams.workflowParentRunId ? { parentWorkflowRunId: effectiveParams.workflowParentRunId } : {}),
5594
+ ...(effectiveParams.workflowKey ? { workflowKey: effectiveParams.workflowKey } : {}),
5319
5595
  startedAt: Date.now(),
5320
5596
  updatedAt: Date.now(),
5321
5597
  cwd: effectiveCwd,
@@ -5421,6 +5697,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5421
5697
 
5422
5698
  let nestedForegroundStarted = false;
5423
5699
  try {
5700
+ if (workflowLaunchObserver) {
5701
+ const singleTask = hasTasks && effectiveParams.tasks?.length === 1 ? effectiveParams.tasks[0] : undefined;
5702
+ const launch = hasSingle
5703
+ ? { agent: effectiveParams.agent!, sessionFile: childSessionFileForTask(effectiveParams.agent!, 0, effectiveParams.model) }
5704
+ : singleTask
5705
+ ? { agent: singleTask.agent, sessionFile: childSessionFileForTask(singleTask.agent, 0, singleTask.model) }
5706
+ : undefined;
5707
+ if (launch) {
5708
+ workflowLaunchObservers.delete(params);
5709
+ workflowLaunchObserver(launch);
5710
+ }
5711
+ }
5424
5712
  const asyncResult = runAsyncPath(execData, deps);
5425
5713
  if (asyncResult) return attachMission(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary));
5426
5714
  if (foregroundControl) {