pi-subagents 0.66.0 → 0.67.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 (115) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +4 -3
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +6 -3
  6. package/docs/configuration.md +7 -5
  7. package/docs/extension-api.md +33 -18
  8. package/docs/missions.md +8 -0
  9. package/docs/models.md +1 -1
  10. package/docs/observability.md +4 -4
  11. package/docs/standalone-background.md +49 -0
  12. package/docs/tool-reference.md +18 -8
  13. package/docs/watchdog.md +35 -4
  14. package/docs/workflows.md +26 -12
  15. package/inspector-runner.mjs +2 -2
  16. package/package.json +1 -1
  17. package/prompts/parallel-review.md +1 -1
  18. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  19. package/skills/pi-subagents/SKILL.md +14 -0
  20. package/skills/pi-subagents/references/execution-controls.md +7 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  22. package/src/agents/advertised-agent-prompt.ts +34 -3
  23. package/src/agents/agents.ts +6 -0
  24. package/src/agents/builtin-names.ts +1 -0
  25. package/src/api/delegation.ts +4 -0
  26. package/src/api/preflight.ts +76 -45
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +63 -4
  29. package/src/extension/index.ts +20 -8
  30. package/src/extension/public-execution.ts +4 -2
  31. package/src/extension/rpc.ts +4 -0
  32. package/src/extension/schemas.ts +67 -78
  33. package/src/extension/tool-description.ts +29 -82
  34. package/src/inspectors/actions.ts +148 -0
  35. package/src/inspectors/ghostty/actions.ts +74 -0
  36. package/src/inspectors/ghostty/plugin.ts +17 -0
  37. package/src/inspectors/herdr/actions.ts +99 -179
  38. package/src/inspectors/herdr/plugin.ts +20 -0
  39. package/src/inspectors/herdr/project-panes.ts +1 -1
  40. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  41. package/src/inspectors/plugins.ts +8 -0
  42. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  43. package/src/inspectors/types.ts +51 -0
  44. package/src/intercom/intercom-bridge.ts +50 -8
  45. package/src/intercom/native-supervisor-channel.ts +22 -13
  46. package/src/runs/background/active-async-capacity.ts +4 -0
  47. package/src/runs/background/async-execution.ts +45 -56
  48. package/src/runs/background/async-resume.ts +5 -9
  49. package/src/runs/background/auto-drain.ts +6 -3
  50. package/src/runs/background/binary-bootstrap.ts +33 -0
  51. package/src/runs/background/fleet-view.ts +30 -2
  52. package/src/runs/background/notify.ts +31 -1
  53. package/src/runs/background/owned-process-tree.ts +29 -2
  54. package/src/runs/background/run-child-session.ts +61 -4
  55. package/src/runs/background/run-status.ts +3 -0
  56. package/src/runs/background/runner-aliases.ts +11 -3
  57. package/src/runs/background/runner-child-launch.ts +2 -0
  58. package/src/runs/background/runner-child-sessions.ts +5 -4
  59. package/src/runs/background/scheduled-runs.ts +40 -13
  60. package/src/runs/background/steering.ts +20 -2
  61. package/src/runs/background/subagent-runner.ts +47 -31
  62. package/src/runs/background/subagent-wait.ts +51 -8
  63. package/src/runs/background/wait-tool.ts +1 -1
  64. package/src/runs/foreground/async-steering-action.ts +18 -7
  65. package/src/runs/foreground/execution.ts +44 -31
  66. package/src/runs/foreground/prompt-audit.ts +3 -1
  67. package/src/runs/foreground/subagent-executor.ts +110 -100
  68. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  69. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  70. package/src/runs/shared/acceptance.ts +5 -2
  71. package/src/runs/shared/async-status-projection.ts +4 -0
  72. package/src/runs/shared/capability-ceiling.ts +2 -0
  73. package/src/runs/shared/child-hooks.ts +25 -10
  74. package/src/runs/shared/child-launch.ts +12 -2
  75. package/src/runs/shared/child-lifecycle.ts +6 -3
  76. package/src/runs/shared/child-runtime-config.ts +3 -1
  77. package/src/runs/shared/child-session.ts +33 -2
  78. package/src/runs/shared/child-tool-plan.ts +122 -3
  79. package/src/runs/shared/completion-guard.ts +5 -3
  80. package/src/runs/shared/effective-system-prompt.ts +33 -0
  81. package/src/runs/shared/external-cli-runner.ts +9 -7
  82. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  83. package/src/runs/shared/model-fallback.ts +2 -0
  84. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  85. package/src/runs/shared/pi-spawn.ts +10 -0
  86. package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
  87. package/src/runs/shared/task-intent.ts +46 -13
  88. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  89. package/src/runs/shared/worktree.ts +42 -12
  90. package/src/shared/fork-context.ts +15 -72
  91. package/src/shared/launch-contract.ts +65 -2
  92. package/src/shared/opencode-session-headers.ts +30 -0
  93. package/src/shared/types.ts +4 -1
  94. package/src/slash/delegation-adapters.ts +3 -1
  95. package/src/slash/delegation-request.ts +14 -0
  96. package/src/slash/slash-commands.ts +2 -1
  97. package/src/slash/subagents-admin.ts +11 -4
  98. package/src/tui/fleet-status.ts +164 -19
  99. package/src/tui/fleet.ts +16 -14
  100. package/src/tui/render.ts +149 -28
  101. package/src/watchdog/child-status.ts +8 -0
  102. package/src/watchdog/model-selection.ts +20 -0
  103. package/src/watchdog/permission-arbiter.ts +3 -1
  104. package/src/watchdog/register-child.ts +1 -0
  105. package/src/watchdog/register-main.ts +31 -27
  106. package/src/watchdog/review.ts +132 -67
  107. package/src/watchdog/runtime.ts +82 -20
  108. package/src/watchdog/scope.ts +1 -1
  109. package/src/watchdog/settings.ts +9 -3
  110. package/src/watchdog/tool-actions.ts +13 -12
  111. package/src/watchdog/turn-delta.ts +23 -0
  112. package/src/watchdog/types.ts +4 -0
  113. package/src/workflows/scripted-workflow.ts +237 -7
  114. package/src/workflows/workflow-checklist.ts +2 -2
  115. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -29,6 +29,7 @@ import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/
29
29
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
30
30
  import { applyWatchdogLaunchRules } from "../../watchdog/rules.ts";
31
31
  import { buildModelCandidates, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelOrigin, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
32
+ import { getHostBuiltinToolNames } from "../shared/child-tool-plan.ts";
32
33
  import { formatRetainedChildren, listRetainedChildren } from "../background/retained-children.ts";
33
34
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
34
35
  import { recordRun } from "../shared/run-history.ts";
@@ -53,7 +54,7 @@ import { encodeIndexSegment } from "../background/index-segment.ts";
53
54
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
54
55
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
55
56
  import { normalizeGateAcceptance, resolveAcceptanceReportMode, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
56
- import { canPreferFork, createForkContextResolver, forkedChildRequiresThinkingOff, resolveSubagentLaunchContext } from "../../shared/fork-context.ts";
57
+ import { canPreferFork, createForkContextResolver, resolveSubagentLaunchContext } from "../../shared/fork-context.ts";
57
58
  import { createPrunedForkSessionWriter } from "../../shared/pruned-fork.ts";
58
59
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
59
60
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
@@ -112,9 +113,11 @@ import { MissionNotFoundError, updateMission } from "../../missions/store.ts";
112
113
  import type { MissionWorkflowChildUpdate } from "../../missions/types.ts";
113
114
  import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
114
115
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
115
- import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
116
+ import { handleInspectorAction, INSPECTOR_ACTIONS } from "../../inspectors/actions.ts";
117
+ import { createBuiltinInspectorPlugins } from "../../inspectors/plugins.ts";
116
118
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
117
- import { previewSimpleWorkflowRun, runWorkflowScript, validateWorkflowScript, WorkflowScriptError, type WorkflowLanePlan, type WorkflowReceiptResumeReference, type WorkflowScriptChildResult, type WorkflowScriptTraceEntry, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
119
+ import { previewSimpleWorkflowRun, runWorkflowScript, validateWorkflowScript, WorkflowScriptError, type WorkflowChildSettledNotification, type WorkflowLanePlan, type WorkflowReceiptResumeReference, type WorkflowScriptChildResult, type WorkflowScriptTraceEntry, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
120
+ import { formatIncrementalChildCompletion, scheduledCompletionTriggersTurn } from "../background/notify.ts";
118
121
  import { executeWorkflowHostCommand, resolveWorkflowHostOutputClaimPath, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "../../workflows/host-command.ts";
119
122
  import { buildWorkflowReceipt, readWorkflowReceipt, workflowReceiptPath, resolveWorkflowReceiptResumeEntry, writeWorkflowReceipt, type WorkflowReceipt, type WorkflowReceiptState } from "../../workflows/workflow-receipt.ts";
120
123
  import { upsertHostStep, validHostStepNodes } from "../shared/host-step-status.ts";
@@ -136,6 +139,7 @@ import { resolveWorkflowResource } from "../../workflows/workflow-resources.ts";
136
139
  import {
137
140
  cleanupWorktrees,
138
141
  createWorktrees,
142
+ preflightWorktreeSource,
139
143
  withWorktreeTransaction,
140
144
  type WorktreeSetupProgress,
141
145
  diffWorktrees,
@@ -404,6 +408,7 @@ export interface SubagentParamsLike {
404
408
  at?: string;
405
409
  every?: string;
406
410
  sessionOnly?: boolean;
411
+ quiet?: boolean;
407
412
  on?: string | number;
408
413
  timezone?: string;
409
414
  overlap?: "skip";
@@ -466,7 +471,7 @@ function inheritedRunFanoutBudget(deps: Pick<ExecutorDeps, "childRuntime">): Run
466
471
 
467
472
  type ForkSessionFileForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean, modelOrigin?: ModelOrigin) => string | undefined;
468
473
  type PrepareForkSessionForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean, modelOrigin?: ModelOrigin) => Promise<void>;
469
- type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean, modelOrigin?: ModelOrigin) => AgentConfig["thinking"] | undefined;
474
+ type ThinkingOverrideForTask = () => AgentConfig["thinking"] | undefined;
470
475
 
471
476
  interface ExecutionContextData {
472
477
  params: SubagentParamsLike;
@@ -486,7 +491,7 @@ interface ExecutionContextData {
486
491
  sessionDirForIndex: (idx?: number) => string;
487
492
  sessionFileForIndex: (idx?: number) => string | undefined;
488
493
  sessionFileForTask: ForkSessionFileForTask;
489
- thinkingOverrideForTask: ForkThinkingOverrideForTask;
494
+ thinkingOverrideForTask: ThinkingOverrideForTask;
490
495
  artifactConfig: ArtifactConfig;
491
496
  artifactsDir: string;
492
497
  backgroundRequestedWhileClarifying: boolean;
@@ -2911,19 +2916,6 @@ function withAggregatedToolUsage(result: AgentToolResult<Details>): AgentToolRes
2911
2916
  : result;
2912
2917
  }
2913
2918
 
2914
- function withForkThinkingNotes(
2915
- result: AgentToolResult<Details>,
2916
- downgrades: Map<number, string>,
2917
- ): AgentToolResult<Details> {
2918
- if (downgrades.size === 0) return result;
2919
- const children = [...downgrades.entries()]
2920
- .sort(([a], [b]) => a - b)
2921
- .map(([index, agent]) => `${agent} (child ${index})`)
2922
- .join(", ");
2923
- const note = `Note: fork context forced thinking off for ${children}. The forked transcript contained signed Anthropic thinking blocks that were sanitized, and Anthropic children cannot resume such a transcript with thinking enabled. Use context: "fresh" when an Anthropic child needs thinking.`;
2924
- return { ...result, content: [...result.content, { type: "text", text: note }] };
2925
- }
2926
-
2927
2919
  function toExecutionErrorResult(params: SubagentParamsLike, error: unknown, contextSummary?: ContextSummary): AgentToolResult<Details> {
2928
2920
  const message = error instanceof Error ? error.message : String(error);
2929
2921
  return withResolvedContext(
@@ -2947,7 +2939,7 @@ function resolveStaticLaunchSummary(input: {
2947
2939
  availableModels: ModelInfo[];
2948
2940
  currentProvider?: string;
2949
2941
  modelScope?: ModelScopeConfig;
2950
- thinkingOverrideForTask: ForkThinkingOverrideForTask;
2942
+ thinkingOverrideForTask: ThinkingOverrideForTask;
2951
2943
  }): StaticLaunchSummary {
2952
2944
  const agentConfig = input.agents.find((agent) => agent.name === input.agent);
2953
2945
  const externalRunner = agentConfig?.runner?.type === "external-cli" || agentConfig?.runner?.type === "external-job";
@@ -2962,7 +2954,7 @@ function resolveStaticLaunchSummary(input: {
2962
2954
  agentConfig?.modelProvider ?? input.currentProvider,
2963
2955
  modelScopes.length === 0 ? {} : { scope: modelScopes },
2964
2956
  );
2965
- const thinkingOverride = externalRunner ? undefined : input.thinkingOverrideForTask(input.agent, input.index, model);
2957
+ const thinkingOverride = externalRunner ? undefined : input.thinkingOverrideForTask();
2966
2958
  const thinking = externalRunner ? undefined : resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
2967
2959
  return {
2968
2960
  agent: input.agent,
@@ -2978,7 +2970,7 @@ function collectStaticLaunchSummaries(input: {
2978
2970
  availableModels: ModelInfo[];
2979
2971
  currentProvider?: string;
2980
2972
  modelScope?: ModelScopeConfig;
2981
- thinkingOverrideForTask: ForkThinkingOverrideForTask;
2973
+ thinkingOverrideForTask: ThinkingOverrideForTask;
2982
2974
  dynamicFanoutMaxItems?: number;
2983
2975
  }): StaticLaunchSummary[] {
2984
2976
  const summary = (agent: string, index: number, explicitModel?: string) => resolveStaticLaunchSummary({
@@ -3311,7 +3303,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3311
3303
  fast: params.fast,
3312
3304
  modelOverrideFromParent,
3313
3305
  modelOrigin,
3314
- thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent, modelOrigin),
3306
+ thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(),
3315
3307
  thinkingCeiling: a.maxThinking,
3316
3308
  maxSubagentDepth,
3317
3309
  waitToolEnabled: deps.waitToolEnabled,
@@ -3846,7 +3838,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3846
3838
  let childSessionControls: ForegroundChildSessionControls | undefined;
3847
3839
  const foregroundControl = deps.state.foregroundControls.get(runId);
3848
3840
  if (foregroundControl) {
3849
- const thinking = resolveEffectiveThinking(modelOverride, thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent, modelOrigin));
3841
+ const thinking = resolveEffectiveThinking(modelOverride, thinkingOverrideForTask());
3850
3842
  beginForegroundChild(foregroundControl, omitUndefinedProperties({
3851
3843
  index: 0,
3852
3844
  agent: params.agent!,
@@ -3878,6 +3870,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3878
3870
  }
3879
3871
  },
3880
3872
  }));
3873
+ // Capture the owned mailbox before a child can start and finish between polling ticks.
3874
+ if (deps.childRuntime?.fanoutChild) deps.activateSupervisorTransport?.();
3881
3875
  }
3882
3876
 
3883
3877
  const modelResponseAliases = deps.config.modelResponseAliases === undefined ? undefined : structuredClone(deps.config.modelResponseAliases);
@@ -3898,8 +3892,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3898
3892
  const launched = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3899
3893
  permissions: deps.config.permissions,
3900
3894
  runtimeSnapshotHost: deps.pi,
3895
+ hostAvailableBuiltins: getHostBuiltinToolNames(deps.pi),
3901
3896
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3902
- llmIntentArbiter: createTaskMutationArbiter(ctx),
3897
+ llmIntentArbiter: createTaskMutationArbiter({ model: ctx.model, modelRegistry: ctx.modelRegistry, sessionId: ctx.sessionManager.getSessionId() }),
3903
3898
  childRuntime: deps.childRuntime,
3904
3899
  onChildSession: (controls) => { childSessionControls = controls; },
3905
3900
  context: data.contextPolicy.contextForAgent(params.agent!),
@@ -3936,7 +3931,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3936
3931
  fast: params.fast,
3937
3932
  modelOverrideFromParent,
3938
3933
  modelOrigin,
3939
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent, modelOrigin),
3934
+ thinkingOverride: thinkingOverrideForTask(),
3940
3935
  thinkingCeiling: agentConfig.maxThinking,
3941
3936
  extensionBindings: params.extensionBindings,
3942
3937
  availableModels,
@@ -4545,6 +4540,35 @@ export async function steerWorkflowChildByKey(input: {
4545
4540
  }
4546
4541
  }
4547
4542
 
4543
+ export async function preflightWorkflowWorktrees(input: {
4544
+ workflowDefaults: SubagentParamsLike;
4545
+ defaultWorktree?: boolean;
4546
+ calls: Array<{ key: string; params: Record<string, unknown> }>;
4547
+ ctxCwd: string;
4548
+ signal: AbortSignal;
4549
+ deadlineAt?: number;
4550
+ }): Promise<void> {
4551
+ const sources = new Map<string, string[]>();
4552
+ for (const { key, params } of input.calls) {
4553
+ // The workflow validates retained IDs/receipt references before admission; resolution follows it.
4554
+ if (params.resume !== undefined) continue;
4555
+ const effective = prepareWorkflowLaunchParams(input.workflowDefaults, params, "preflight", key);
4556
+ if ((effective.worktree ?? input.defaultWorktree) !== true) continue;
4557
+ // Match recursive execute's resolution, including relative default/child cwd.
4558
+ const cwd = resolveRequestedCwd(input.ctxCwd, effective.cwd);
4559
+ const keys = sources.get(cwd) ?? [];
4560
+ keys.push(key);
4561
+ sources.set(cwd, keys);
4562
+ }
4563
+ for (const [cwd, keys] of sources) {
4564
+ try { await preflightWorktreeSource(cwd, { signal: input.signal, deadlineAt: input.deadlineAt }); }
4565
+ catch (error) {
4566
+ throw new Error(`Worktree admission failed for ${keys.map((key) => `'${key}'`).join(", ")} at ${cwd}: ${error instanceof Error ? error.message : String(error)} Select the correct cwd or arrange an operator-approved commit/stash.`, { cause: error });
4567
+ }
4568
+ }
4569
+ input.signal.throwIfAborted();
4570
+ }
4571
+
4548
4572
  export function prepareWorkflowLaunchParams(
4549
4573
  workflowDefaults: SubagentParamsLike,
4550
4574
  childParams: Record<string, unknown>,
@@ -4890,7 +4914,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4890
4914
  } catch (error) {
4891
4915
  const message = error instanceof Error ? error.message : String(error);
4892
4916
  if (requestParams.action?.trim() === "validate") {
4893
- const validation = validateWorkflowScript(requestParams.workflowScript ?? "");
4917
+ const validation = validateWorkflowScript(requestParams.workflowScript ?? "", {
4918
+ maxSubagentSpawnsPerRun: requestParams.maxSubagentSpawnsPerRun ?? resolveMaxSubagentSpawnsPerRun(deps.config.maxSubagentSpawnsPerRun),
4919
+ });
4894
4920
  const invalidValidation = { ...validation, ok: false, errors: [...validation.errors, { message }] };
4895
4921
  return {
4896
4922
  content: [{ type: "text", text: JSON.stringify(invalidValidation) }],
@@ -4901,7 +4927,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4901
4927
  return buildRequestedModeError(requestParams, message);
4902
4928
  }
4903
4929
  if (requestParams.action?.trim() === "validate") {
4904
- const validation = validateWorkflowScript(requestParams.workflowScript ?? "");
4930
+ const validation = validateWorkflowScript(requestParams.workflowScript ?? "", {
4931
+ maxSubagentSpawnsPerRun: requestParams.maxSubagentSpawnsPerRun ?? resolveMaxSubagentSpawnsPerRun(deps.config.maxSubagentSpawnsPerRun),
4932
+ });
4905
4933
  const payload = workflowPreflight ? { ...validation, preflight: workflowPreflight } : validation;
4906
4934
  return {
4907
4935
  content: [{ type: "text", text: JSON.stringify(payload) }],
@@ -4919,6 +4947,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4919
4947
  workflowResource = { permit: workflowResourcePermit, ...consumed };
4920
4948
  }
4921
4949
  if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
4950
+ const spawnBudgetValidation = validateWorkflowScript(requestParams.workflowScript, {
4951
+ maxSubagentSpawnsPerRun: requestParams.maxSubagentSpawnsPerRun ?? resolveMaxSubagentSpawnsPerRun(deps.config.maxSubagentSpawnsPerRun),
4952
+ });
4953
+ const spawnBudgetErrors = spawnBudgetValidation.errors.filter((error) => error.kind === "spawn-budget");
4954
+ if (spawnBudgetErrors.length > 0) {
4955
+ return buildRequestedModeError(requestParams, `workflowScript validation failed before child launch; no children launched. ${spawnBudgetErrors.map((error) => error.message).join(" ")}`);
4956
+ }
4957
+ for (const warning of spawnBudgetValidation.warnings ?? []) console.warn(`[pi-subagents] ${warning.message}`);
4922
4958
  const acceptanceErrors = validateAcceptanceInput(requestParams.acceptance);
4923
4959
  if (acceptanceErrors.length > 0) return buildRequestedModeError(requestParams, acceptanceErrors.join(" "));
4924
4960
  const foregroundWorkflowRunId = encodeIndexSegment(_id);
@@ -5512,6 +5548,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5512
5548
  try {
5513
5549
  const workflow = await runWorkflowScript({
5514
5550
  script: workflowScript,
5551
+ workflowRunId,
5515
5552
  globalConcurrencyLimit: requestParams.globalConcurrencyLimit ?? deps.config.globalConcurrencyLimit,
5516
5553
  timeoutMs: timeout,
5517
5554
  signal: controller.signal,
@@ -5527,6 +5564,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5527
5564
  },
5528
5565
  ...(workflowState ? { state: workflowState } : {}),
5529
5566
  onTrace: updateTrace,
5567
+ onChildSettled: (notification) => {
5568
+ appendWorkflowEvent({
5569
+ type: "subagent.workflow.child_settled",
5570
+ childKey: notification.childKey,
5571
+ ...(notification.childRunId ? { childRunId: notification.childRunId } : {}),
5572
+ outcome: notification.outcome,
5573
+ workflowRunning: notification.workflowRunning,
5574
+ });
5575
+ try {
5576
+ deps.pi.sendMessage(
5577
+ {
5578
+ customType: "subagent-incremental-child-notify",
5579
+ content: formatIncrementalChildCompletion(notification),
5580
+ display: notification.outcome !== "completed",
5581
+ },
5582
+ { triggerTurn: scheduledCompletionTriggersTurn(requestParams.scheduleOrigin, notification.outcome) },
5583
+ );
5584
+ } catch (sendError) {
5585
+ console.error(`Failed to send incremental child completion notification for '${notification.childKey}':`, sendError);
5586
+ }
5587
+ deps.refreshResultDelivery?.();
5588
+ },
5530
5589
  onLanePlan: (lanes) => {
5531
5590
  status.workflowGraph = buildWorkflowLaneGraph(workflowRunId, lanes, status.workflowGraph);
5532
5591
  applyWorkflowLaneTrace(status.workflowGraph, status.workflow?.trace ?? []);
@@ -5539,7 +5598,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5539
5598
  persist({ tolerateStatusWriteFailure: true });
5540
5599
  } });
5541
5600
  },
5542
- admit: (calls) => {
5601
+ admit: async (calls, admissionSignal) => {
5602
+ await preflightWorkflowWorktrees({ workflowDefaults: workflowChildDefaults, defaultWorktree: deps.config.worktree, calls, ctxCwd: parentCwd, signal: admissionSignal, deadlineAt: workflowDeadlineAt });
5543
5603
  const outputClaims = workflowChildOutputClaims({ ctxCwd: parentCwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
5544
5604
  if (outputClaims.error) throw new Error(outputClaims.error);
5545
5605
  status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
@@ -5786,6 +5846,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5786
5846
  try {
5787
5847
  const workflow = await runWorkflowScript({
5788
5848
  script: requestParams.workflowScript,
5849
+ workflowRunId: foregroundWorkflowRunId,
5789
5850
  ...(delegatedWorkflowPermit ? { oneUsePermit: { claim: (key: string) => claimWorkflowChildPermit(delegatedWorkflowPermit, foregroundWorkflowRunId, key) } } : {}),
5790
5851
  globalConcurrencyLimit: requestParams.globalConcurrencyLimit ?? deps.config.globalConcurrencyLimit,
5791
5852
  timeoutMs: timeout,
@@ -5804,7 +5865,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5804
5865
  }
5805
5866
  sendWorkflowProgress();
5806
5867
  },
5807
- admit: (calls) => {
5868
+ admit: async (calls, admissionSignal) => {
5869
+ await preflightWorkflowWorktrees({ workflowDefaults: workflowChildDefaults, defaultWorktree: deps.config.worktree, calls, ctxCwd: ctx.cwd, signal: admissionSignal, deadlineAt: workflowDeadlineAt });
5808
5870
  const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: foregroundWorkflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
5809
5871
  if (outputClaims.error) throw new Error(outputClaims.error);
5810
5872
  claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
@@ -6062,16 +6124,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6062
6124
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
6063
6125
  return handleHerdrProjectPaneAction(action as (typeof HERDR_PROJECT_PANE_ACTIONS)[number], paramsWithResolvedCwd, { cwd: requestCwd, state: deps.state, signal });
6064
6126
  }
6065
- if ((HERDR_INSPECTOR_ACTIONS as readonly string[]).includes(action)) {
6127
+ if ((INSPECTOR_ACTIONS as readonly string[]).includes(action)) {
6066
6128
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
6067
6129
  return { content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }], isError: true, details: { mode: "management", results: [] } };
6068
6130
  }
6069
6131
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
6070
- return handleHerdrInspectorAction(action as (typeof HERDR_INSPECTOR_ACTIONS)[number], paramsWithResolvedCwd, {
6132
+ return handleInspectorAction(action as (typeof INSPECTOR_ACTIONS)[number], paramsWithResolvedCwd, {
6071
6133
  state: deps.state,
6072
6134
  cwd: requestCwd,
6073
6135
  ...(deps.config.missions ? { missions: deps.config.missions } : {}),
6074
6136
  ...(deps.config.authorityPolicy ? { authorityPolicy: deps.config.authorityPolicy } : {}),
6137
+ plugins: createBuiltinInspectorPlugins(),
6075
6138
  signal,
6076
6139
  });
6077
6140
  }
@@ -6702,65 +6765,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6702
6765
 
6703
6766
  let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
6704
6767
  let prepareForkSessionForIndex: (idx?: number) => Promise<void> = async () => {};
6705
- let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined;
6706
- let prepareForkThinking = (_agentName: string, _index: number, _modelOverride?: string, _modelOverrideFromParent?: boolean, _modelOrigin?: ModelOrigin): void => {};
6707
- const forkThinkingRequirements = new Map<number, boolean>();
6708
- const forkThinkingDowngrades = new Map<number, string>();
6768
+ // Forked children keep their requested thinking level. Signed Anthropic thinking
6769
+ // blocks are stripped from the inherited transcript by the resolver (they are bound
6770
+ // to the parent session), which is not a reason to disable the child's own reasoning.
6709
6771
  try {
6710
- const forkAvailableModels = contextPolicy.usesFork ? ctx.modelRegistry.getAvailable().map(toModelInfo) : [];
6711
- const parentModel = requestParentModel;
6712
- prepareForkThinking = (agentName, index, modelOverride, modelOverrideFromParent, storedOrigin) => {
6713
- const agentConfig = agents.find((agent) => agent.name === agentName);
6714
- if (agentConfig?.runner?.type === "external-cli" || agentConfig?.runner?.type === "external-job") {
6715
- forkThinkingRequirements.set(index, true);
6716
- return;
6717
- }
6718
- const effectiveStoredOrigin = storedOrigin === "configured" && modelOverride === undefined && agentConfig?.model === undefined
6719
- ? undefined
6720
- : storedOrigin;
6721
- const origin = resolveModelOrigin({
6722
- fromParent: modelOverrideFromParent,
6723
- storedOrigin: effectiveStoredOrigin,
6724
- explicitModel: modelOverrideFromParent || effectiveStoredOrigin === "configured" ? undefined : modelOverride,
6725
- agentModel: agentConfig?.model,
6726
- parentModel,
6727
- });
6728
- const primaryModel = modelOverrideFromParent
6729
- ? modelOverride
6730
- : resolveEffectiveSubagentModel(
6731
- effectiveStoredOrigin === "configured" ? undefined : modelOverride,
6732
- agentConfig?.model,
6733
- parentModel,
6734
- forkAvailableModels,
6735
- agentConfig?.modelProvider ?? parentModel?.provider,
6736
- { source: origin === "explicit" ? "explicit" : "inherited" },
6737
- );
6738
- const candidates = buildModelCandidates(
6739
- primaryModel,
6740
- agentConfig?.fallbackModels,
6741
- forkAvailableModels,
6742
- agentConfig?.modelProvider ?? parentModel?.provider,
6743
- {
6744
- primaryModelFromParent: origin === "inherited",
6745
- origin,
6746
- },
6747
- );
6748
- forkThinkingRequirements.set(
6749
- index,
6750
- candidates.length === 0
6751
- || candidates.some((candidate) => forkedChildRequiresThinkingOff(candidate, forkAvailableModels, parentModel?.provider)),
6752
- );
6753
- };
6754
6772
  const pruneSession = contextPolicy.usesFork && deps.config.forkContext?.mode === "pruned"
6755
6773
  ? await createPrunedForkSessionWriter(ctx, deps.config.forkContext, signal)
6756
6774
  : undefined;
6757
- const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined, {
6758
- forceThinkingOffForIndex: (index) => forkThinkingRequirements.get(index) ?? true,
6759
- ...(pruneSession ? { pruneSession } : {}),
6760
- });
6775
+ const forkContextResolver = createForkContextResolver(
6776
+ ctx.sessionManager,
6777
+ contextPolicy.usesFork ? "fork" : undefined,
6778
+ pruneSession ? { pruneSession } : {},
6779
+ );
6761
6780
  prepareForkSessionForIndex = forkContextResolver.prepareSessionForIndex;
6762
6781
  forkSessionFileForIndex = forkContextResolver.sessionFileForIndex;
6763
- forkThinkingOverrideForIndex = forkContextResolver.thinkingOverrideForIndex;
6764
6782
  } catch (error) {
6765
6783
  return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
6766
6784
  }
@@ -6863,23 +6881,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6863
6881
  }
6864
6882
  const sessionDirForIndex = (idx?: number) =>
6865
6883
  path.join(sessionRoot, `run-${idx ?? 0}`);
6866
- const forkSessionFileForTask: ForkSessionFileForTask = (agentName, idx = 0, modelOverride, modelOverrideFromParent, modelOrigin) => {
6884
+ const forkSessionFileForTask: ForkSessionFileForTask = (agentName, idx = 0) => {
6867
6885
  if (!shouldForkAgent(contextPolicy, agentName)) return undefined;
6868
- prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent, modelOrigin);
6869
6886
  return forkSessionFileForIndex(idx);
6870
6887
  };
6871
- const prepareForkSessionForTask: PrepareForkSessionForTask = async (agentName, idx = 0, modelOverride, modelOverrideFromParent, modelOrigin) => {
6888
+ const prepareForkSessionForTask: PrepareForkSessionForTask = async (agentName, idx = 0) => {
6872
6889
  if (!shouldForkAgent(contextPolicy, agentName)) return;
6873
- prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent, modelOrigin);
6874
6890
  await prepareForkSessionForIndex(idx);
6875
6891
  };
6876
- const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride, modelOverrideFromParent, modelOrigin) => {
6877
- if (!shouldForkAgent(contextPolicy, agentName)) return delegatedThinkingOverride;
6878
- prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent, modelOrigin);
6879
- const override = forkThinkingOverrideForIndex(idx);
6880
- if (override === "off") forkThinkingDowngrades.set(idx, agentName);
6881
- return override ?? delegatedThinkingOverride;
6882
- };
6892
+ const thinkingOverrideForTask: ThinkingOverrideForTask = () => delegatedThinkingOverride;
6883
6893
  const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride, modelOverrideFromParent, modelOrigin) =>
6884
6894
  forkSessionFileForTask(agentName, idx, modelOverride, modelOverrideFromParent, modelOrigin) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
6885
6895
  const childSessionFileForIndex = (idx?: number) =>
@@ -6968,7 +6978,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6968
6978
  sessionDirForIndex,
6969
6979
  sessionFileForIndex: childSessionFileForIndex,
6970
6980
  sessionFileForTask: childSessionFileForTask,
6971
- thinkingOverrideForTask: forkThinkingOverrideForTask,
6981
+ thinkingOverrideForTask,
6972
6982
  artifactConfig,
6973
6983
  artifactsDir,
6974
6984
  backgroundRequestedWhileClarifying,
@@ -7082,7 +7092,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
7082
7092
  availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
7083
7093
  currentProvider: requestParentModel?.provider,
7084
7094
  modelScope,
7085
- thinkingOverrideForTask: forkThinkingOverrideForTask,
7095
+ thinkingOverrideForTask,
7086
7096
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
7087
7097
  });
7088
7098
  } catch (error) {
@@ -7173,7 +7183,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
7173
7183
  const asyncResult = await runAsyncPath(execData, deps);
7174
7184
  if (asyncResult) {
7175
7185
  asyncLaunchFailed = asyncResult.isError === true;
7176
- return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));
7186
+ return attachMission(withRunFanoutBudget(withResolvedContext(asyncResult, contextPolicy.contextSummary), runFanoutBudget));
7177
7187
  }
7178
7188
  if (foregroundControl) {
7179
7189
  writeNestedForegroundEvent("subagent.nested.started");
@@ -7183,11 +7193,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
7183
7193
  if (hasSingle) {
7184
7194
  const result = await runSinglePath(execData, deps);
7185
7195
  writeNestedForegroundEvent("subagent.nested.completed", result);
7186
- return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
7196
+ return attachMission(withRunFanoutBudget(withResolvedContext(result, contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
7187
7197
  }
7188
7198
  } catch (error) {
7189
7199
  asyncLaunchFailed = effectiveAsync;
7190
- const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
7200
+ const errorResult = toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
7191
7201
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
7192
7202
  return attachMission(errorResult);
7193
7203
  } finally {
@@ -243,6 +243,7 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
243
243
  cwd: next.cwd,
244
244
  sessionId: next.sessionId ?? (typeof existing?.sessionId === "string" ? existing.sessionId : undefined),
245
245
  completionOwnerId: next.completionOwnerId,
246
+ ...(next.scheduleOrigin ? { scheduleOrigin: next.scheduleOrigin } : {}),
246
247
  },
247
248
  receipt,
248
249
  receiptPath,
@@ -294,6 +295,7 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
294
295
  results,
295
296
  sessionId: plan.status.sessionId,
296
297
  completionOwnerId: plan.status.completionOwnerId,
298
+ ...(plan.status.scheduleOrigin ? { scheduleOrigin: plan.status.scheduleOrigin } : {}),
297
299
  timestamp: Date.now(),
298
300
  triggerTurn: true,
299
301
  });
@@ -6,6 +6,7 @@ import { readStatus } from "../../shared/utils.ts";
6
6
  import { steeringReceipt, waitForSteeringAction } from "../background/steering.ts";
7
7
  import { requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
8
8
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
9
+ import { workflowAsyncChildSteeringGuidance } from "../shared/workflow-async-child-guidance.ts";
9
10
 
10
11
  export interface WorkflowForegroundSteeringTarget {
11
12
  control: ForegroundRunControl;
@@ -85,7 +86,7 @@ export async function steerWorkflowRun(input: {
85
86
  }
86
87
  if (state.workflowControllers?.has(runId)) {
87
88
  const route = resolveWorkflowForegroundSteeringTarget({ state, workflowRunId: runId, asyncDirRoot: path.dirname(asyncDir) });
88
- if (!route.ok) return managementError(route.message);
89
+ if (!route.ok) return managementError([route.message, ...workflowAsyncChildSteeringGuidance(status, state)].join("\n"));
89
90
  return steerWorkflowForegroundTarget({ target: route.target, message: input.message, mode: input.mode, index: input.index });
90
91
  }
91
92
  if (!state.currentSessionId) return managementError("Workflow steering requires an active parent session.");
@@ -105,11 +105,14 @@ function inferLevel(input: {
105
105
  const inferredReadOnly = readOnlyTask || ((readOnlyAgent || input.acceptanceRole === "read-only") && !taskMayWrite);
106
106
  const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
107
107
  const dynamicResolvesReadOnly = inferredReadOnly && !writeTask;
108
- const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
108
+ const riskyKeywordPattern = /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/;
109
+ const keywordRiskReadOnly = input.acceptanceRole === undefined
110
+ ? intent.kind === "read-only" && !riskyKeywordPattern.test(task)
111
+ : inferredReadOnly;
109
112
  const risky = Boolean(input.async && writeTask)
110
113
  || (Boolean(input.dynamic) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
111
114
  || (Boolean(input.dynamicGroup) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
112
- || (!keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task));
115
+ || (!keywordRiskReadOnly && riskyKeywordPattern.test(task));
113
116
 
114
117
  if (risky) {
115
118
  reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
@@ -108,6 +108,8 @@ export interface AsyncStatusWorkflowRow {
108
108
  modelThinking?: string;
109
109
  activity?: string;
110
110
  startedAt?: number;
111
+ endedAt?: number;
112
+ durationMs?: number;
111
113
  tokens?: number;
112
114
  window?: number;
113
115
  overflow?: number;
@@ -536,6 +538,8 @@ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?:
536
538
  ...(modelThinking ? { modelThinking } : {}),
537
539
  ...(activity ? { activity } : {}),
538
540
  ...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
541
+ ...(step.endedAt !== undefined ? { endedAt: step.endedAt } : {}),
542
+ ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
539
543
  ...(step.tokens?.total !== undefined ? { tokens: step.tokens.total } : {}),
540
544
  ...(step.tokens?.window !== undefined ? { window: step.tokens.window } : {}),
541
545
  ...(preflight ? { preflight } : {}),
@@ -29,6 +29,8 @@ export interface SubagentCapabilityAudit {
29
29
  effectiveMcpTools: string[];
30
30
  agentAllowed: boolean;
31
31
  agentRestrictionSources?: string[];
32
+ /** Builtin tools declared but unavailable on the host runtime. */
33
+ unavailableHostBuiltins?: string[];
32
34
  }
33
35
 
34
36
  export interface RegisterSubagentCapabilityCeilingOptions {
@@ -1,12 +1,13 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { listBackgroundWorkProviders } from "../../api/background-work.ts";
3
3
  import { ReadonlyDrainObservation } from "./readonly-drain-observation.ts";
4
- import registerFanoutChildSubagentExtension from "../../extension/fanout-child.ts";
4
+ import registerFanoutChildSubagentExtension, { createChildSafeState } from "../../extension/fanout-child.ts";
5
5
  import registerSubagentFastModeExtension from "./fast-mode-extension.ts";
6
6
  import registerSubagentPromptRuntime from "./subagent-prompt-runtime.ts";
7
7
  import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
8
8
  import type { ChildToolDiagnostic } from "./tool-availability.ts";
9
9
  import type { ChildSessionLaunch } from "./child-session.ts";
10
+ import type { ArbiterModelContext } from "./llm-intent-arbiter.ts";
10
11
  import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
11
12
  import { projectRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
12
13
 
@@ -130,17 +131,22 @@ export function createChildHooks(config: ChildRuntimeConfig): ChildHookExtension
130
131
  export function createCapturedChildHooks(config: ChildRuntimeConfig, runner = false) {
131
132
  let diagnostic: ChildToolDiagnostic | undefined;
132
133
  let acknowledgedIds: string[] | undefined;
133
- let completionIntentContext: Pick<ExtensionContext, "model" | "modelRegistry"> | undefined;
134
+ let completionIntentContext: ArbiterModelContext | undefined;
135
+ let finalDrainHeld = false;
134
136
  const capture: OwnedCapture = {
135
137
  toolDiagnostic: (value) => { diagnostic = value; },
136
138
  runtimeAcknowledgements: (ids) => { acknowledgedIds = ids; },
137
139
  };
138
140
  Object.assign(config, capture);
139
- const hooks = childHooks(config, capture);
141
+ const hooks = childHooks(config, capture, (held) => { finalDrainHeld = held; });
140
142
  if (runner) {
141
143
  hooks.push({ name: "pi-subagents:completion-intent", factory: (pi) => pi.on("session_start", (_event, childCtx) => {
142
- // Retain only attempt model services, not the live child session.
143
- completionIntentContext = { model: childCtx.model, modelRegistry: childCtx.modelRegistry };
144
+ // Retain only attempt model services and the session id string, not the live child session.
145
+ completionIntentContext = {
146
+ model: childCtx.model,
147
+ modelRegistry: childCtx.modelRegistry,
148
+ sessionId: childCtx.sessionManager.getSessionId(),
149
+ };
144
150
  }) });
145
151
  const proof = promptProofs.get(hooks[0]!.factory);
146
152
  if (proof) proof.factories = hooks.map((hook) => hook.factory);
@@ -150,18 +156,27 @@ export function createCapturedChildHooks(config: ChildRuntimeConfig, runner = fa
150
156
  completionIntentContext: () => completionIntentContext,
151
157
  toolDiagnostic: () => diagnostic,
152
158
  runtimeAcknowledgedExtensions: () => acknowledgedIds ? projectRuntimeAcknowledgedExtensions(acknowledgedIds) : undefined,
159
+ finalDrainHeld: () => finalDrainHeld,
153
160
  };
154
161
  }
155
162
 
156
- function childHooks(config: ChildRuntimeConfig, capture?: OwnedCapture): ChildHookExtension[] {
163
+ function childHooks(config: ChildRuntimeConfig, capture?: OwnedCapture, holdFinalDrain?: (held: boolean) => void): ChildHookExtension[] {
157
164
  const snapshot = readonlyConfig(config, capture);
158
165
  const proof: PromptProof | undefined = snapshot === undefined ? undefined : { config, snapshot, capture };
166
+ const runtime = Object.create(config) as ChildRuntimeConfig;
167
+ const ownedState = Object.getOwnPropertyDescriptor(config, "runtimeState");
168
+ if (!ownedState || !("value" in ownedState) || ownedState.value == null) {
169
+ Object.defineProperty(runtime, "runtimeState", { configurable: true, enumerable: true, writable: true, value: createChildSafeState() });
170
+ }
171
+ if (holdFinalDrain) {
172
+ Object.defineProperty(runtime, "holdFinalDrain", { configurable: true, enumerable: true, writable: true, value: holdFinalDrain });
173
+ }
159
174
  const hooks: ChildHookExtension[] = [
160
175
  { name: "pi-subagents:prompt-runtime", factory: function promptRuntime(pi) {
161
- if (!proof?.observeNext) return registerSubagentPromptRuntime(pi, config);
176
+ if (!proof?.observeNext) return registerSubagentPromptRuntime(pi, runtime);
162
177
  proof.observation = proof.observeNext;
163
178
  proof.observeNext = undefined;
164
- registerSubagentPromptRuntime(pi, config, proof.observation);
179
+ registerSubagentPromptRuntime(pi, runtime, proof.observation);
165
180
  } },
166
181
  ];
167
182
  if (proof) {
@@ -169,6 +184,6 @@ function childHooks(config: ChildRuntimeConfig, capture?: OwnedCapture): ChildHo
169
184
  promptProofs.set(hooks[0]!.factory, proof);
170
185
  }
171
186
  if (config.fast) hooks.push({ name: "pi-subagents:fast-mode", factory: (pi) => registerSubagentFastModeExtension(pi) });
172
- if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi, config) });
187
+ if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi, runtime) });
173
188
  return hooks;
174
189
  }