pi-subagents 0.45.2 → 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 (45) hide show
  1. package/CHANGELOG.md +24 -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/wait-completions.ts +39 -5
  32. package/src/runs/background/wait-subscriptions.ts +18 -3
  33. package/src/runs/foreground/execution.ts +4 -0
  34. package/src/runs/foreground/foreground-history.ts +137 -0
  35. package/src/runs/foreground/subagent-executor.ts +310 -44
  36. package/src/shared/fork-context.ts +13 -0
  37. package/src/shared/prompt-resources.ts +51 -0
  38. package/src/shared/types.ts +30 -1
  39. package/src/shared/utf8.ts +11 -0
  40. package/src/slash/prompt-workflows.ts +2 -15
  41. package/src/slash/slash-commands.ts +19 -1
  42. package/src/tui/fleet-status.ts +8 -2
  43. package/src/tui/fleet.ts +135 -25
  44. package/src/tui/render.ts +120 -7
  45. package/src/workflows/scripted-workflow.ts +167 -10
@@ -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,
@@ -3944,6 +4055,61 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
3944
4055
 
3945
4056
  const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string }) => void>();
3946
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
+
3947
4113
  function workflowChildResult(key: string, result: AgentToolResult<Details>): WorkflowScriptChildResult {
3948
4114
  const receiptOutput = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
3949
4115
  const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
@@ -3960,9 +4126,11 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
3960
4126
  if (child.sessionFile) artifactPaths.add(child.sessionFile);
3961
4127
  }
3962
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)))];
3963
4130
  return {
3964
4131
  key,
3965
4132
  ok,
4133
+ ...(resolvedAgents.length === 1 ? { agent: resolvedAgents[0] } : {}),
3966
4134
  ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
3967
4135
  output,
3968
4136
  ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
@@ -4000,6 +4168,7 @@ export function prepareWorkflowLaunchParams(
4000
4168
  }
4001
4169
  const launchParams = {
4002
4170
  ...workflowDefaults,
4171
+ async: false,
4003
4172
  ...childParams,
4004
4173
  ...(options.missionDetached ? { mission: false } : {}),
4005
4174
  workflowParentRunId: parentWorkflowRunId,
@@ -4178,6 +4347,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4178
4347
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4179
4348
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4180
4349
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
4350
+ const workflowPrompts = { render: (ref: string, vars?: unknown) => renderWorkflowPrompt(ref, vars, workflowCwd) };
4181
4351
  const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd, workflowCwd, background: requestParams.async !== false });
4182
4352
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
4183
4353
  const chatProgress = chatProgressResult.projection!;
@@ -4321,12 +4491,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4321
4491
  const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
4322
4492
  if (existing) {
4323
4493
  existing.status = mapped;
4494
+ if (entry.agent) existing.agent = entry.agent;
4324
4495
  if (entry.error === undefined) delete existing.error;
4325
4496
  else existing.error = entry.error;
4326
4497
  if (entry.durationMs === undefined) delete existing.durationMs;
4327
4498
  else existing.durationMs = entry.durationMs;
4328
4499
  } else {
4329
- 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() });
4330
4501
  }
4331
4502
  }
4332
4503
  projectWorkflowActivity();
@@ -4338,6 +4509,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4338
4509
  script: workflowScript,
4339
4510
  timeoutMs: timeout,
4340
4511
  signal: controller.signal,
4512
+ prompts: workflowPrompts,
4341
4513
  ...(workflowState ? { state: workflowState } : {}),
4342
4514
  onTrace: updateTrace,
4343
4515
  onEmit: (emits) => {
@@ -4351,37 +4523,70 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4351
4523
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4352
4524
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4353
4525
  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();
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);
4361
4576
  });
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
4577
  workflowResults.push(...result.details.results);
4578
+ if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4384
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
+ });
4385
4590
  if (result.details.asyncId) {
4386
4591
  const childJob = deps.state.asyncJobs.get(result.details.asyncId);
4387
4592
  if (childJob) { childJob.parentWorkflowRunId = workflowRunId; childJob.workflowKey = key; }
@@ -4395,14 +4600,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4395
4600
  const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
4396
4601
  const workflowUsage = sumResultsUsage(workflowResults);
4397
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) };
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 });
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 });
4399
4604
  persist();
4400
4605
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4401
4606
  } catch (error) {
4402
4607
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4403
4608
  const stopped = controller.signal.aborted;
4404
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 } });
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 });
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 });
4406
4611
  persist();
4407
4612
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4408
4613
  } finally {
@@ -4426,6 +4631,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4426
4631
  script: requestParams.workflowScript,
4427
4632
  timeoutMs: timeout,
4428
4633
  signal,
4634
+ prompts: workflowPrompts,
4429
4635
  ...(workflowState ? { state: workflowState } : {}),
4430
4636
  onTrace: (trace) => {
4431
4637
  liveWorkflow = { ...liveWorkflow, trace };
@@ -4440,10 +4646,51 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4440
4646
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4441
4647
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4442
4648
  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);
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
+ });
4445
4680
  workflowResults.push(...result.details.results);
4446
- 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;
4447
4694
  },
4448
4695
  status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4449
4696
  });
@@ -4485,7 +4732,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4485
4732
  ? normalizeParentModel(ctx.model)
4486
4733
  : rememberParentModel(deps.state, requestSessionId, ctx.model);
4487
4734
  } catch (error) {
4488
- if (action?.toLowerCase() !== "doctor") throw error;
4735
+ if (action?.toLowerCase() !== "doctor" && action?.toLowerCase() !== "guide") throw error;
4489
4736
  requestParentModel = normalizeParentModel(ctx.model);
4490
4737
  }
4491
4738
  if (action) {
@@ -4667,6 +4914,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4667
4914
  details: { mode: "management", results: [], spawnBudget: granted.snapshot },
4668
4915
  };
4669
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
+ }
4670
4931
  if (action === "children.list") {
4671
4932
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4672
4933
  const children = listRetainedChildren(DIRS.async, deps.state.currentSessionId);
@@ -4817,7 +5078,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4817
5078
  }
4818
5079
  }
4819
5080
  if (action === "resume") {
4820
- return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
5081
+ return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel, signal }));
4821
5082
  }
4822
5083
  if (action === "steer") {
4823
5084
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
@@ -4898,6 +5159,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4898
5159
  return appendStepToAsyncChain(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
4899
5160
  }
4900
5161
  if (action.startsWith("schedule.")) {
5162
+ if (!isScheduledRunAction(action)) {
5163
+ return { content: [{ type: "text", text: unknownSubagentActionMessage(action) }], isError: true, details: { mode: "management", results: [] } };
5164
+ }
4901
5165
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4902
5166
  return {
4903
5167
  content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
@@ -5004,7 +5268,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5004
5268
  }
5005
5269
  if (!(SUBAGENT_ACTIONS as readonly string[]).includes(action)) {
5006
5270
  return {
5007
- content: [{ type: "text", text: `Unknown action: ${action}. Valid: ${SUBAGENT_ACTIONS.join(", ")}` }],
5271
+ content: [{ type: "text", text: unknownSubagentActionMessage(action) }],
5008
5272
  isError: true,
5009
5273
  details: { mode: "management" as const, results: [] },
5010
5274
  };
@@ -5326,6 +5590,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5326
5590
  runId,
5327
5591
  sessionId: requestSessionId,
5328
5592
  mode: foregroundMode,
5593
+ ...(effectiveParams.workflowParentRunId ? { parentWorkflowRunId: effectiveParams.workflowParentRunId } : {}),
5594
+ ...(effectiveParams.workflowKey ? { workflowKey: effectiveParams.workflowKey } : {}),
5329
5595
  startedAt: Date.now(),
5330
5596
  updatedAt: Date.now(),
5331
5597
  cwd: effectiveCwd,
@@ -9,6 +9,7 @@ type SubagentExecutionContext = "fresh" | "fork";
9
9
  interface BranchSessionEntry {
10
10
  type: string;
11
11
  id?: string;
12
+ cwd?: string;
12
13
  parentId?: string | null;
13
14
  timestamp?: string;
14
15
  message?: {
@@ -128,6 +129,18 @@ function readSessionEntries(sessionFile: string): BranchSessionEntry[] {
128
129
  });
129
130
  }
130
131
 
132
+ /** Keep Pi from restoring a forked session into the parent's cwd instead of the child launch cwd. */
133
+ export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
134
+ const entries = readSessionEntries(sessionFile);
135
+ const header = entries[0];
136
+ if (header?.type !== "session") throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
137
+ const resolvedCwd = path.resolve(cwd);
138
+ const effectiveCwd = fs.realpathSync.native(resolvedCwd);
139
+ if (header.cwd === effectiveCwd) return;
140
+ header.cwd = effectiveCwd;
141
+ fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
142
+ }
143
+
131
144
  export function createForkContextResolver(
132
145
  sessionManager: ForkableSessionManager,
133
146
  requestedContext: unknown,