pi-subagents 0.63.0 → 0.65.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 (107) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +92 -114
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +6 -5
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +46 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/notify.ts +41 -2
  34. package/src/runs/background/process-terminal.ts +1 -1
  35. package/src/runs/background/run-child-session.ts +613 -0
  36. package/src/runs/background/run-status.ts +0 -1
  37. package/src/runs/background/runner-aliases.ts +125 -0
  38. package/src/runs/background/runner-child-sessions.ts +31 -0
  39. package/src/runs/background/scheduled-runs.ts +18 -4
  40. package/src/runs/background/subagent-runner.ts +203 -898
  41. package/src/runs/foreground/async-steering-action.ts +1 -17
  42. package/src/runs/foreground/execution.ts +196 -378
  43. package/src/runs/foreground/foreground-control.ts +4 -0
  44. package/src/runs/foreground/subagent-executor.ts +121 -58
  45. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  46. package/src/runs/shared/abort-recovery.ts +3 -3
  47. package/src/runs/shared/acceptance.ts +10 -0
  48. package/src/runs/shared/async-status-projection.ts +11 -43
  49. package/src/runs/shared/capability-ceiling.ts +1 -2
  50. package/src/runs/shared/child-hooks.ts +25 -0
  51. package/src/runs/shared/child-identity.ts +13 -2
  52. package/src/runs/shared/child-launch.ts +314 -0
  53. package/src/runs/shared/child-lifecycle.ts +25 -0
  54. package/src/runs/shared/child-runtime-config.ts +126 -0
  55. package/src/runs/shared/child-session.ts +342 -0
  56. package/src/runs/shared/child-tool-plan.ts +530 -0
  57. package/src/runs/shared/claude-code-adapter.ts +5 -1
  58. package/src/runs/shared/completion-guard.ts +1 -1
  59. package/src/runs/shared/external-cli-preflight.ts +16 -0
  60. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  61. package/src/runs/shared/model-exclusions.ts +82 -14
  62. package/src/runs/shared/model-fallback.ts +47 -4
  63. package/src/runs/shared/nested-events.ts +29 -45
  64. package/src/runs/shared/nested-path.ts +0 -14
  65. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  66. package/src/runs/shared/parallel-utils.ts +0 -2
  67. package/src/runs/shared/permissions.ts +0 -13
  68. package/src/runs/shared/process-signal.ts +4 -1
  69. package/src/runs/shared/run-fanout-budget.ts +0 -13
  70. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  71. package/src/runs/shared/structured-output.ts +17 -4
  72. package/src/runs/shared/subagent-control.ts +6 -2
  73. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  74. package/src/runs/shared/tool-availability.ts +18 -62
  75. package/src/runs/shared/tool-budget.ts +0 -14
  76. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  77. package/src/runs/shared/worktree.ts +117 -30
  78. package/src/shared/child-session-name.ts +1 -1
  79. package/src/shared/jsonl-writer.ts +11 -0
  80. package/src/shared/thinking-ceiling.ts +0 -6
  81. package/src/shared/types.ts +64 -30
  82. package/src/shared/utils.ts +3 -4
  83. package/src/slash/slash-commands.ts +0 -6
  84. package/src/tui/fleet.ts +0 -1
  85. package/src/tui/render.ts +238 -31
  86. package/src/watchdog/child-status.ts +54 -34
  87. package/src/watchdog/diff-tool.ts +77 -0
  88. package/src/watchdog/emission-guard.ts +5 -3
  89. package/src/watchdog/guidance.ts +20 -0
  90. package/src/watchdog/register-child.ts +28 -25
  91. package/src/watchdog/register-main.ts +10 -9
  92. package/src/watchdog/render.ts +4 -5
  93. package/src/watchdog/review.ts +15 -4
  94. package/src/watchdog/rules.ts +70 -0
  95. package/src/watchdog/runtime.ts +75 -92
  96. package/src/watchdog/scope.ts +0 -11
  97. package/src/watchdog/settings.ts +48 -104
  98. package/src/watchdog/types.ts +18 -32
  99. package/src/watchdog/warning-format.ts +0 -1
  100. package/src/workflows/chat-progress.ts +3 -2
  101. package/src/workflows/scripted-workflow.ts +48 -1
  102. package/src/workflows/workflow-checklist.ts +10 -12
  103. package/src/workflows/workflow-preflight.ts +28 -1
  104. package/src/runs/shared/child-protocol.ts +0 -415
  105. package/src/runs/shared/pi-args.ts +0 -1062
  106. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  107. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -3,41 +3,28 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { BeforeProviderRequestEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { registerNativeSupervisorClient } from "../../intercom/native-supervisor-channel.ts";
6
- import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
7
- import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, PERMISSION_POLICY_ENV } from "./permissions.ts";
8
- import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
9
- import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_FORK_CACHE_KEY_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
10
- import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
11
- import { createStructuredOutputToolParameters, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
6
+ import { permissionDecision } from "./permissions.ts";
7
+ import type { SteerRequest } from "../background/control-channel.ts";
8
+ import { RUNTIME_EXTENSION_ACK_EVENT, isRuntimeAcknowledgedExtensionId } from "./runtime-acknowledged-extensions.ts";
9
+ import { createStructuredOutputToolParameters, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, validateStructuredOutputValue } from "./structured-output.ts";
12
10
  import { validateAcceptanceReport } from "./acceptance.ts";
13
- import {
14
- CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
15
- formatChildToolDiagnostic,
16
- MCP_DIRECT_CHILD_TOOLS_ENV,
17
- REQUIRED_CHILD_TOOLS_ENV,
18
- writeChildToolDiagnostic,
19
- type ChildToolDiagnostic,
20
- } from "./tool-availability.ts";
21
- import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV, decodeToolBudgetEnv, shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge } from "./tool-budget.ts";
22
- import type { JsonSchemaObject, ResolvedToolBudget, SubagentState } from "../../shared/types.ts";
11
+ import { formatChildToolDiagnostic } from "./tool-availability.ts";
12
+ import { shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge } from "./tool-budget.ts";
13
+ import type { ResolvedToolBudget, SubagentState } from "../../shared/types.ts";
23
14
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
24
- import { getAgentDir, resolveWatchPath } from "../../shared/utils.ts";
15
+ import { getAgentDir } from "../../shared/utils.ts";
25
16
  import { registerChildWatchdog } from "../../watchdog/register-child.ts";
26
- import { CHILD_WATCHDOG_CONFIG_ENV, decodeChildWatchdogConfig } from "../../watchdog/child-status.ts";
17
+ import type { ChildWatchdogConfig } from "../../watchdog/child-status.ts";
27
18
  import { requestWatchdogPermission, type WatchdogPermissionRequest, type WatchdogPermissionResult } from "../../watchdog/permission-arbiter.ts";
28
19
  import { SUBAGENT_WATCHDOG_WARNING_TYPE } from "../../watchdog/types.ts";
29
- import { resolveWaitToolConfig } from "../background/wait-config.ts";
30
20
  import { registerWaitTool } from "../background/wait-tool.ts";
31
21
  import { drainOutstandingWork } from "../background/auto-drain.ts";
32
-
33
- const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
34
- const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
35
- export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
36
- /** Human-readable child display name (agent + task excerpt) set by the parent
37
- * at launch; applied via pi.setSessionName when no intercom target exists. */
38
- export const SUBAGENT_SESSION_NAME_ENV = "PI_SUBAGENT_SESSION_NAME";
39
- const STEERING_LEGACY_SETTLE_FALLBACK_MS = 1000;
40
- const STEERING_SAFETY_POLL_INTERVAL_MS = 5000;
22
+ import {
23
+ childSupervisorMetadata,
24
+ evaluateChildToolDiagnostic,
25
+ type ChildPermissions,
26
+ type ChildRuntimeConfig,
27
+ } from "./child-runtime-config.ts";
41
28
 
42
29
  const STRUCTURED_OUTPUT_INSTRUCTIONS = [
43
30
  "This subagent step has a strict structured output contract.",
@@ -77,45 +64,8 @@ const PROJECT_CONTEXT_LEGACY_HEADER = "\n\n# Project Context\n\nProject-specific
77
64
  const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
78
65
  const DATE_HEADER = "\nCurrent date:";
79
66
 
80
- function readBooleanEnv(name: string): boolean | undefined {
81
- const value = process.env[name];
82
- if (value === undefined) return undefined;
83
- return value !== "0";
84
- }
85
-
86
- function readRequiredChildTools(): string[] | undefined {
87
- const encoded = process.env[REQUIRED_CHILD_TOOLS_ENV]?.trim();
88
- if (!encoded) return undefined;
89
- const required = JSON.parse(encoded) as unknown;
90
- if (!Array.isArray(required) || required.some((name) => typeof name !== "string" || !name)) {
91
- throw new Error(`Invalid ${REQUIRED_CHILD_TOOLS_ENV} payload.`);
92
- }
93
- return required;
94
- }
95
-
96
- function readMcpDirectChildTools(): string[] | undefined {
97
- const encoded = process.env[MCP_DIRECT_CHILD_TOOLS_ENV]?.trim();
98
- if (!encoded) return undefined;
99
- try {
100
- const tools = JSON.parse(encoded) as unknown;
101
- if (!Array.isArray(tools) || tools.some((name) => typeof name !== "string" || !name)) return undefined;
102
- return tools;
103
- } catch {
104
- return undefined;
105
- }
106
- }
107
-
108
- function refreshChildToolDiagnostic(pi: ExtensionAPI): ChildToolDiagnostic | undefined {
109
- const filePath = process.env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV]?.trim();
110
- const required = readRequiredChildTools();
111
- if (!filePath || !required) return undefined;
112
- const available = pi.getAllTools().map((tool) => tool.name);
113
- return writeChildToolDiagnostic(filePath, required, available, process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim(), readMcpDirectChildTools());
114
- }
115
-
116
- function registerRuntimeExtensionAcknowledgements(pi: ExtensionAPI): void {
117
- const outputPath = process.env[RUNTIME_EXTENSION_ACK_PATH_ENV]?.trim();
118
- if (!outputPath) return;
67
+ function registerRuntimeExtensionAcknowledgements(pi: ExtensionAPI, sink: ((ids: string[]) => void) | undefined): void {
68
+ if (!sink) return;
119
69
  const ids: string[] = [];
120
70
  let finalized = false;
121
71
  const acknowledge = (payload: unknown): undefined => {
@@ -127,7 +77,7 @@ function registerRuntimeExtensionAcknowledgements(pi: ExtensionAPI): void {
127
77
  const finalize = (): undefined => {
128
78
  if (finalized) return undefined;
129
79
  finalized = true;
130
- writeRuntimeAcknowledgedExtensions(outputPath, ids);
80
+ sink(ids);
131
81
  return undefined;
132
82
  };
133
83
  try {
@@ -252,7 +202,7 @@ function stripChildBoundaryInstructions(prompt: string): string {
252
202
 
253
203
  export function rewriteSubagentPrompt(
254
204
  prompt: string,
255
- options: { inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean; fanoutChild?: boolean },
205
+ options: { inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean; fanoutChild?: boolean; structuredOutput?: boolean },
256
206
  ): string {
257
207
  let rewritten = prompt;
258
208
  if (!options.inheritProjectContext) {
@@ -267,7 +217,7 @@ export function rewriteSubagentPrompt(
267
217
  rewritten = stripSubagentOrchestrationSkill(rewritten);
268
218
  rewritten = stripChildBoundaryInstructions(rewritten);
269
219
  const boundary = options.fanoutChild ? CHILD_FANOUT_BOUNDARY_INSTRUCTIONS : CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS;
270
- const structured = process.env[STRUCTURED_OUTPUT_CAPTURE_ENV] ? `\n\n${STRUCTURED_OUTPUT_INSTRUCTIONS}` : "";
220
+ const structured = options.structuredOutput ? `\n\n${STRUCTURED_OUTPUT_INSTRUCTIONS}` : "";
271
221
  return `${boundary}${structured}\n\n${rewritten}`;
272
222
  }
273
223
 
@@ -303,14 +253,14 @@ const PROMPT_CACHE_KEY_APIS = new Set([
303
253
  "openai-responses",
304
254
  ]);
305
255
 
306
- export function rewriteForkCacheProviderRequest(event: BeforeProviderRequestEvent, ctx?: Pick<ExtensionContext, "model">): unknown {
307
- const forkCacheKey = process.env[SUBAGENT_FORK_CACHE_KEY_ENV]?.trim();
308
- if (!forkCacheKey || !PROMPT_CACHE_KEY_APIS.has(ctx?.model?.api ?? "")) return undefined;
256
+ export function rewriteForkCacheProviderRequest(event: BeforeProviderRequestEvent, ctx: Pick<ExtensionContext, "model"> | undefined, forkCacheKey: string | undefined): unknown {
257
+ const key = forkCacheKey?.trim();
258
+ if (!key || !PROMPT_CACHE_KEY_APIS.has(ctx?.model?.api ?? "")) return undefined;
309
259
  if (!event || typeof event !== "object") return undefined;
310
260
  const payload = event.payload;
311
261
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
312
262
  if (typeof (payload as { prompt_cache_key?: unknown }).prompt_cache_key !== "string") return undefined;
313
- return { ...payload, prompt_cache_key: forkCacheKey };
263
+ return { ...payload, prompt_cache_key: key };
314
264
  }
315
265
 
316
266
  function portableToolId(id: string): string {
@@ -348,8 +298,8 @@ function stripAssistantSubagentToolCallBlocks(message: unknown): unknown | undef
348
298
  return { ...m, content: filteredContent };
349
299
  }
350
300
 
351
- export function stripParentOnlySubagentMessages(messages: unknown[], options: { sanitizeToolIds?: boolean } = {}): unknown[] {
352
- const preserveCurrentFanoutToolHistory = process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1";
301
+ export function stripParentOnlySubagentMessages(messages: unknown[], options: { sanitizeToolIds?: boolean; preserveFanoutToolHistory?: boolean } = {}): unknown[] {
302
+ const preserveCurrentFanoutToolHistory = options.preserveFanoutToolHistory === true;
353
303
  const sanitizeToolIds = options.sanitizeToolIds ?? true;
354
304
  let changed = false;
355
305
  const filtered: unknown[] = [];
@@ -382,23 +332,20 @@ export function formatSteerMessage(request: SteerRequest): string {
382
332
 
383
333
  export function registerPermissionGate(
384
334
  pi: ExtensionAPI,
335
+ permissions: ChildPermissions | undefined,
336
+ childWatchdog: ChildWatchdogConfig | undefined,
385
337
  requestPermission: (request: WatchdogPermissionRequest) => Promise<WatchdogPermissionResult> = requestWatchdogPermission,
386
338
  ): void {
387
- const rules = decodePermissionRules(process.env[PERMISSION_POLICY_ENV]);
388
- if (!rules) return;
339
+ const rules = permissions?.rules;
340
+ if (!rules || Object.keys(rules).length === 0) return;
341
+ const rawWatchdogConfig = childWatchdog ? JSON.stringify(childWatchdog) : undefined;
342
+ const timeoutMs = childWatchdog?.agentEndTimeoutMs ?? 30_000;
389
343
  const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: { toolName?: string; input?: unknown }, ctx: ExtensionContext) => unknown) => void;
390
344
  onRuntimeEvent("tool_call", async (event, ctx) => {
391
345
  const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
392
346
  const decision = permissionDecision(rules, toolName);
393
347
  if (decision === "allow") return undefined;
394
348
  if (decision === "deny") return { block: true, reason: `Blocked by pi-subagents permission rule: '${toolName}' is denied.` };
395
- const rawWatchdogConfig = process.env[CHILD_WATCHDOG_CONFIG_ENV];
396
- let timeoutMs = 30_000;
397
- try {
398
- timeoutMs = decodeChildWatchdogConfig(rawWatchdogConfig)?.agentEndTimeoutMs ?? timeoutMs;
399
- } catch {
400
- // The arbiter reports invalid configuration with the concrete decode error.
401
- }
402
349
  if (ctx.signal?.aborted) return { block: true, reason: "Blocked by pi-subagents permission rule: Watchdog permission decision was cancelled." };
403
350
  let timeout: ReturnType<typeof setTimeout> | undefined;
404
351
  let abort: (() => void) | undefined;
@@ -410,7 +357,7 @@ export function registerPermissionGate(
410
357
  toolName,
411
358
  args: event.input ?? {},
412
359
  rawWatchdogConfig,
413
- auditPath: process.env[PERMISSION_AUDIT_PATH_ENV],
360
+ auditPath: permissions.auditPath,
414
361
  ...(ctx.signal ? { signal: ctx.signal } : {}),
415
362
  }),
416
363
  new Promise<WatchdogPermissionResult>((resolve) => {
@@ -454,245 +401,51 @@ function registerToolBudget(pi: ExtensionAPI, budget: ResolvedToolBudget | undef
454
401
  });
455
402
  }
456
403
 
457
- export function registerSteeringInbox(
458
- pi: ExtensionAPI,
459
- deps: {
460
- watch?: typeof fs.watch;
461
- nativeRealpath?: (filePath: string) => string;
462
- legacySettleFallbackMs?: number;
463
- safetyPollIntervalMs?: number;
464
- platform?: NodeJS.Platform;
465
- timers?: Pick<typeof globalThis, "setInterval" | "clearInterval">;
466
- } = {},
467
- ): void {
468
- const steerInbox = process.env[SUBAGENT_STEER_INBOX_ENV]?.trim();
469
- if (!steerInbox) return;
470
- const capabilityPath = process.env[SUBAGENT_STEER_CAPABILITY_ENV]?.trim();
471
- const ackDir = process.env[SUBAGENT_STEER_ACK_DIR_ENV]?.trim();
472
- const sendUserMessage = (pi as { sendUserMessage?: (content: string, options?: { deliverAs: "steer" | "followUp" }) => unknown }).sendUserMessage;
473
- const childIndex = Number(process.env[SUBAGENT_CHILD_INDEX_ENV]);
474
- const pending = new Map<string, Array<{ request: SteerRequest; deliveryStatus: SteerDeliveryStatus }>>();
475
- const queued: Array<{ request: SteerRequest; ready: boolean }> = [];
476
- let disposed = false;
477
- let agentRunning = false;
478
- let inTurn = false;
479
- let awaitingSettlement = false;
480
- let flushing = false;
481
- let started = false;
482
- let canSteer = typeof sendUserMessage === "function";
483
- let watcher: fs.FSWatcher | undefined;
484
- let interval: NodeJS.Timeout | undefined;
485
- let safetyInterval: NodeJS.Timeout | undefined;
486
- let settleFallback: NodeJS.Timeout | undefined;
487
- const legacySettleFallbackMs = deps.legacySettleFallbackMs ?? STEERING_LEGACY_SETTLE_FALLBACK_MS;
488
- const acknowledge = (request: SteerRequest, state: "delivered" | "queued" | "failed", message: string, deliveryStatus?: SteerDeliveryStatus): void => {
489
- if (!ackDir || !Number.isInteger(childIndex) || childIndex < 0) return;
490
- writeSteerAckAt(steerAckPathFromDir(ackDir, request.id), {
491
- requestId: request.id,
492
- index: childIndex,
493
- ts: Date.now(),
494
- state,
495
- ...(deliveryStatus ? { deliveryStatus } : {}),
496
- message,
497
- });
498
- };
499
- const publishCapability = (): void => {
500
- if (!capabilityPath || !Number.isInteger(childIndex) || childIndex < 0) return;
501
- writeSteerCapabilityAt(capabilityPath, { index: childIndex, pid: process.pid, readyAt: Date.now(), supported: canSteer });
502
- };
503
- const flush = (): void => {
504
- if (disposed || flushing) return;
505
- flushing = true;
506
- try {
507
- const requests = consumeSteerRequestsFromDir(steerInbox);
508
- for (let index = 0; index < requests.length; index++) {
509
- const request = requests[index]!;
510
- if (!canSteer || typeof sendUserMessage !== "function") {
511
- acknowledge(request, "failed", "Child Pi session does not support sendUserMessage steering.");
512
- continue;
513
- }
514
- const requestedMode = request.mode ?? "steer";
515
- const autoCanUseIdle = requestedMode === "auto" && !agentRunning && !awaitingSettlement;
516
- const delivery = requestedMode === "follow_up" || (requestedMode === "auto" && (inTurn || awaitingSettlement)) ? "followUp" as const : "steer" as const;
517
- const pendingFollowUps = [...pending.values()].reduce((count, entries) => count + entries.filter((entry) => entry.deliveryStatus === "queued").length, 0);
518
- if (delivery === "followUp" && queued.length + pendingFollowUps >= MAX_STEER_QUEUE_SIZE) {
519
- acknowledge(request, "failed", `Follow-up queue is full (${MAX_STEER_QUEUE_SIZE} messages).`);
520
- continue;
521
- }
522
- const formatted = formatSteerMessage(request);
523
- const entries = pending.get(formatted) ?? [];
524
- entries.push({ request, deliveryStatus: delivery === "followUp" ? "queued" : "delivered" });
525
- pending.set(formatted, entries);
526
- try {
527
- sendUserMessage(formatted, autoCanUseIdle ? undefined : { deliverAs: delivery });
528
- } catch (error) {
529
- entries.pop();
530
- if (entries.length === 0) pending.delete(formatted);
531
- acknowledge(request, "failed", error instanceof Error ? error.message : String(error));
532
- for (const retry of requests.slice(index + 1)) writeSteerRequestToDir(steerInbox, retry);
533
- break;
534
- }
404
+ function registerStructuredOutputTool(pi: ExtensionAPI, structured: NonNullable<ChildRuntimeConfig["structuredOutput"]>): void {
405
+ const required = structured.acceptanceReport === "required";
406
+ const parameters = createStructuredOutputToolParameters(structured.schema, { acceptanceReport: structured.acceptanceReport });
407
+ const registerTool = pi.registerTool as unknown as (tool: {
408
+ name: string;
409
+ label: string;
410
+ description: string;
411
+ parameters: unknown;
412
+ execute: (_id: string, params: { value: unknown; acceptanceReport?: unknown }) => Promise<unknown>;
413
+ }) => void;
414
+ registerTool({
415
+ name: "structured_output",
416
+ label: "Structured Output",
417
+ description: "Submit the required final structured output for this subagent step. This terminates the step.",
418
+ parameters,
419
+ async execute(_id: string, params: { value: unknown; acceptanceReport?: unknown }) {
420
+ const validation = await validateStructuredOutputValue(structured.schema, params.value);
421
+ if (validation.status === "invalid") {
422
+ throw new Error(`Structured output validation failed: ${validation.message}`);
535
423
  }
536
- } finally {
537
- flushing = false;
538
- }
539
- };
540
- const onInput = (event: unknown): undefined => {
541
- if (disposed || !event || typeof event !== "object") return undefined;
542
- const input = event as { source?: unknown; streamingBehavior?: unknown; text?: unknown; content?: unknown };
543
- if (input.source !== "extension") return undefined;
544
- const text = typeof input.text === "string" ? input.text : typeof input.content === "string" ? input.content : undefined;
545
- if (!text) return undefined;
546
- const entries = pending.get(text);
547
- const entry = entries?.shift();
548
- if (!entry) return undefined;
549
- if (entries?.length === 0) pending.delete(text);
550
- if (entry.deliveryStatus === "queued") {
551
- queued.push({ request: entry.request, ready: !inTurn });
552
- acknowledge(entry.request, "queued", "Pi queued the correlated follow-up input.", "queued");
553
- } else {
554
- acknowledge(entry.request, "delivered", "Pi accepted the correlated steering input.", "delivered");
555
- }
556
- return undefined;
557
- };
558
- const start = (): void => {
559
- if (started || disposed) return;
560
- try {
561
- fs.mkdirSync(steerInbox, { recursive: true });
562
- publishCapability();
563
- } catch {
564
- return;
565
- }
566
- started = true;
567
- const startPolling = (): void => {
568
- if (interval || disposed) return;
569
- interval = (deps.timers?.setInterval ?? setInterval)(flush, 250) as NodeJS.Timeout;
570
- interval.unref?.();
571
- };
572
- const startSafetyPolling = (): void => {
573
- if (safetyInterval || disposed) return;
574
- safetyInterval = (deps.timers?.setInterval ?? setInterval)(flush, deps.safetyPollIntervalMs ?? STEERING_SAFETY_POLL_INTERVAL_MS) as NodeJS.Timeout;
575
- safetyInterval.unref?.();
576
- };
577
- if (!shouldUseNativeFsWatch("child-steering-inbox", deps.platform)) {
578
- startPolling();
579
- } else {
580
- try {
581
- watcher = (deps.watch ?? fs.watch)(resolveWatchPath(steerInbox, deps.nativeRealpath), () => flush());
582
- watcher.on("error", startPolling);
583
- startSafetyPolling();
584
- } catch {
585
- watcher = undefined;
586
- startPolling();
424
+ if (required && params.acceptanceReport === undefined) {
425
+ throw new Error(MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR);
587
426
  }
588
- }
589
- };
590
- const activate = (): undefined => {
591
- start();
592
- flush();
593
- return undefined;
594
- };
595
- const clearSettleFallback = (): void => {
596
- if (!settleFallback) return;
597
- clearTimeout(settleFallback);
598
- settleFallback = undefined;
599
- };
600
- const markSettled = (): undefined => {
601
- clearSettleFallback();
602
- agentRunning = false;
603
- inTurn = false;
604
- awaitingSettlement = false;
605
- return activate();
606
- };
607
- const armLegacySettleFallback = (): void => {
608
- clearSettleFallback();
609
- settleFallback = setTimeout(() => {
610
- settleFallback = undefined;
611
- if (disposed || !awaitingSettlement) return;
612
- agentRunning = false;
613
- inTurn = false;
614
- awaitingSettlement = false;
615
- activate();
616
- }, legacySettleFallbackMs);
617
- settleFallback.unref?.();
618
- };
619
-
620
- const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown, ctx?: unknown) => unknown) => void;
621
- // Register input before the watcher so an accepted extension input cannot race request dispatch.
622
- onRuntimeEvent("input", onInput);
623
- onRuntimeEvent("session_start", () => start());
624
- onRuntimeEvent("agent_start", () => {
625
- clearSettleFallback();
626
- agentRunning = true;
627
- awaitingSettlement = false;
628
- return activate();
629
- });
630
- onRuntimeEvent("agent_end", (event) => {
631
- inTurn = false;
632
- if ((event as { willRetry?: unknown } | undefined)?.willRetry === true) {
633
- clearSettleFallback();
634
- agentRunning = true;
635
- awaitingSettlement = true;
636
- return activate();
637
- }
638
- agentRunning = true;
639
- awaitingSettlement = true;
640
- armLegacySettleFallback();
641
- return activate();
642
- });
643
- onRuntimeEvent("agent_settled", markSettled);
644
- onRuntimeEvent("session_compact", () => {
645
- const unresolved = [...pending.values()].flat();
646
- pending.clear();
647
- for (const entry of unresolved) {
648
- try {
649
- writeSteerRequestToDir(steerInbox, { ...entry.request, mode: "follow_up" });
650
- } catch (error) {
651
- acknowledge(entry.request, "failed", `Could not retry steering after compaction: ${error instanceof Error ? error.message : String(error)}`);
427
+ if (required && params.acceptanceReport !== undefined) {
428
+ const acceptanceValidation = validateAcceptanceReport(params.acceptanceReport, "acceptanceReport");
429
+ if (!acceptanceValidation.report) {
430
+ throw new Error(`Invalid structured output acceptance report: ${acceptanceValidation.errors.join("; ")}`);
431
+ }
652
432
  }
653
- }
654
- return activate();
655
- });
656
- onRuntimeEvent("turn_start", () => {
657
- clearSettleFallback();
658
- agentRunning = true;
659
- awaitingSettlement = false;
660
- inTurn = true;
661
- const next = queued.findIndex((entry) => entry.ready);
662
- if (next >= 0) {
663
- const [entry] = queued.splice(next, 1);
664
- if (entry) acknowledge(entry.request, "delivered", "Pi delivered the queued follow-up at a turn boundary.", "delivered");
665
- }
666
- return activate();
667
- });
668
- onRuntimeEvent("turn_end", () => {
669
- inTurn = false;
670
- for (const entry of queued) entry.ready = true;
671
- return activate();
672
- });
673
- for (const eventName of ["message_start", "message_update", "message_end", "tool_execution_start", "tool_execution_end"] as const) {
674
- onRuntimeEvent(eventName, activate);
675
- }
676
- onRuntimeEvent("session_shutdown", () => {
677
- for (const entry of queued) acknowledge(entry.request, "failed", "Run ended before queued follow-up delivery.", "queued");
678
- for (const entries of pending.values()) {
679
- for (const entry of entries) acknowledge(entry.request, "failed", "Run ended before Pi confirmed steering input delivery.");
680
- }
681
- disposed = true;
682
- clearSettleFallback();
683
- try { watcher?.close(); } catch {}
684
- if (interval) (deps.timers?.clearInterval ?? clearInterval)(interval);
685
- if (safetyInterval) (deps.timers?.clearInterval ?? clearInterval)(safetyInterval);
433
+ structured.capture(params.value, structured.acceptanceReport ? params.acceptanceReport : undefined);
434
+ return {
435
+ content: [{ type: "text", text: "Structured output captured." }],
436
+ details: {},
437
+ terminate: true,
438
+ };
439
+ },
686
440
  });
687
441
  }
688
442
 
689
- export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
690
- registerRuntimeExtensionAcknowledgements(pi);
691
- registerSteeringInbox(pi);
692
- registerPermissionGate(pi);
693
- registerToolBudget(pi, decodeToolBudgetEnv(process.env[TOOL_BUDGET_ENV], { allowZero: process.env[TOOL_BUDGET_ZERO_AUTH_ENV] === "1" }));
694
- registerChildWatchdog(pi);
695
- const waitToolConfig = resolveWaitToolConfig();
443
+ /** Register every child-side hook the prompt runtime owns for one child session. */
444
+ export default function registerSubagentPromptRuntime(pi: ExtensionAPI, config: ChildRuntimeConfig): void {
445
+ registerRuntimeExtensionAcknowledgements(pi, config.runtimeAcknowledgements);
446
+ registerPermissionGate(pi, config.permissions, config.childWatchdog);
447
+ registerToolBudget(pi, config.toolBudget);
448
+ registerChildWatchdog(pi, config.childWatchdog, config.watchdogStatus);
696
449
  const waitState = {
697
450
  baseCwd: "",
698
451
  currentSessionId: null,
@@ -707,12 +460,13 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
707
460
  watcherRestartTimer: null,
708
461
  resultFileCoalescer: { schedule: () => false, clear: () => {} },
709
462
  } as unknown as SubagentState;
710
- if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, waitToolConfig.enabled, undefined, waitToolConfig.defaultTimeoutMs);
463
+ if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, config.waitTool.enabled, undefined, config.waitTool.defaultTimeoutMs);
464
+ const supervisorMetadata = childSupervisorMetadata(config);
711
465
  let nativeSupervisorClientRegistered = false;
712
466
  const registerNativeSupervisorClientOnce = (): void => {
713
467
  if (nativeSupervisorClientRegistered) return;
714
468
  nativeSupervisorClientRegistered = true;
715
- registerNativeSupervisorClient(pi);
469
+ registerNativeSupervisorClient(pi, supervisorMetadata);
716
470
  };
717
471
  const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown, ctx?: ExtensionContext) => unknown) => void;
718
472
  onRuntimeEvent("session_start", (_event: unknown, ctx?: ExtensionContext) => {
@@ -721,72 +475,24 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
721
475
  registerNativeSupervisorClientOnce();
722
476
  });
723
477
  onRuntimeEvent("agent_start", () => {
724
- const diagnostic = refreshChildToolDiagnostic(pi);
478
+ if (!config.requiredTools) return;
479
+ const diagnostic = evaluateChildToolDiagnostic(config, pi.getAllTools().map((tool) => tool.name));
480
+ config.toolDiagnostic?.(diagnostic);
725
481
  if (diagnostic) throw new Error(formatChildToolDiagnostic(diagnostic));
726
482
  });
727
483
  onRuntimeEvent("agent_end", async (_event: unknown, ctx: unknown) => {
728
484
  if ((ctx as { hasUI?: boolean } | undefined)?.hasUI === true) return;
729
485
  await drainOutstandingWork({ state: waitState, events: pi.events });
730
486
  });
731
- const structuredOutputPath = process.env[STRUCTURED_OUTPUT_CAPTURE_ENV];
732
- const structuredAcceptanceReportPath = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV];
733
- const structuredAcceptanceReportRequired = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] === "1";
734
- const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
735
- if (structuredOutputPath && structuredSchemaPath) {
736
- const schema = JSON.parse(fs.readFileSync(structuredSchemaPath, "utf-8")) as JsonSchemaObject;
737
- const acceptanceReportMode = structuredAcceptanceReportPath
738
- ? structuredAcceptanceReportRequired ? "required" as const : "optional" as const
739
- : undefined;
740
- const parameters = createStructuredOutputToolParameters(schema, { acceptanceReport: acceptanceReportMode });
741
- const registerTool = pi.registerTool as unknown as (tool: {
742
- name: string;
743
- label: string;
744
- description: string;
745
- parameters: unknown;
746
- execute: (_id: string, params: { value: unknown; acceptanceReport?: unknown }) => Promise<unknown>;
747
- }) => void;
748
- registerTool({
749
- name: "structured_output",
750
- label: "Structured Output",
751
- description: "Submit the required final structured output for this subagent step. This terminates the step.",
752
- parameters,
753
- async execute(_id: string, params: { value: unknown; acceptanceReport?: unknown }) {
754
- const validation = await validateStructuredOutputValue(schema, params.value);
755
- if (validation.status === "invalid") {
756
- throw new Error(`Structured output validation failed: ${validation.message}`);
757
- }
758
- if (structuredAcceptanceReportRequired && params.acceptanceReport === undefined) {
759
- throw new Error(MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR);
760
- }
761
- if (structuredAcceptanceReportRequired && params.acceptanceReport !== undefined) {
762
- const acceptanceValidation = validateAcceptanceReport(params.acceptanceReport, "acceptanceReport");
763
- if (!acceptanceValidation.report) {
764
- throw new Error(`Invalid structured output acceptance report: ${acceptanceValidation.errors.join("; ")}`);
765
- }
766
- }
767
- fs.mkdirSync(path.dirname(structuredOutputPath), { recursive: true });
768
- if (structuredAcceptanceReportPath && params.acceptanceReport !== undefined) {
769
- fs.mkdirSync(path.dirname(structuredAcceptanceReportPath), { recursive: true });
770
- fs.writeFileSync(structuredAcceptanceReportPath, JSON.stringify(params.acceptanceReport), { mode: 0o600 });
771
- } else if (structuredAcceptanceReportPath && fs.existsSync(structuredAcceptanceReportPath)) {
772
- fs.unlinkSync(structuredAcceptanceReportPath);
773
- }
774
- fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
775
- return {
776
- content: [{ type: "text", text: "Structured output captured." }],
777
- details: { path: structuredOutputPath },
778
- terminate: true,
779
- };
780
- },
781
- });
782
- }
487
+ if (config.structuredOutput) registerStructuredOutputTool(pi, config.structuredOutput);
783
488
 
784
- onRuntimeEvent("before_provider_request", (event: unknown, ctx?: ExtensionContext) => rewriteForkCacheProviderRequest(event as BeforeProviderRequestEvent, ctx));
489
+ onRuntimeEvent("before_provider_request", (event: unknown, ctx?: ExtensionContext) => rewriteForkCacheProviderRequest(event as BeforeProviderRequestEvent, ctx, config.forkCacheKey));
785
490
 
786
491
  onRuntimeEvent("context", (event: unknown, ctx?: ExtensionContext) => {
787
492
  if (!event || typeof event !== "object" || !("messages" in event) || !Array.isArray(event.messages)) return undefined;
788
493
  const messages = stripParentOnlySubagentMessages(event.messages, {
789
494
  sanitizeToolIds: !COMPOSITE_TOOL_ID_APIS.has(ctx?.model?.api ?? ""),
495
+ preserveFanoutToolHistory: config.fanoutChild,
790
496
  });
791
497
  if (messages === event.messages) return undefined;
792
498
  return { messages };
@@ -798,24 +504,21 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
798
504
  // The intercom target is a routing address and always wins; the display
799
505
  // name (agent + task excerpt, computed by the parent at launch) only
800
506
  // applies when the bridge is not addressing this child.
801
- const intercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
802
- const displaySessionName = process.env[SUBAGENT_SESSION_NAME_ENV]?.trim();
803
- const childSessionName = intercomSessionName || displaySessionName;
507
+ const childSessionName = config.intercomSessionName || config.sessionName;
804
508
  if (childSessionName && typeof pi.setSessionName === "function") {
805
509
  pi.setSessionName(childSessionName);
806
510
  }
807
511
 
808
- const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV);
809
- const inheritGlobalContext = readBooleanEnv(SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV);
810
- const inheritSkills = readBooleanEnv(SUBAGENT_INHERIT_SKILLS_ENV);
811
- const fanoutChild = readBooleanEnv(SUBAGENT_FANOUT_CHILD_ENV);
512
+ const { inheritProjectContext, inheritGlobalContext, inheritSkills } = config;
513
+ const fanoutChild = config.fanoutChild;
812
514
  let rewritten = event.systemPrompt;
813
- if (inheritProjectContext !== undefined || inheritGlobalContext !== undefined || inheritSkills !== undefined || fanoutChild !== undefined) {
515
+ if (inheritProjectContext !== undefined || inheritGlobalContext !== undefined || inheritSkills !== undefined || fanoutChild) {
814
516
  rewritten = rewriteSubagentPrompt(event.systemPrompt, {
815
517
  inheritProjectContext: inheritProjectContext ?? true,
816
518
  inheritGlobalContext: inheritGlobalContext ?? true,
817
519
  inheritSkills: inheritSkills ?? true,
818
- fanoutChild: fanoutChild === true,
520
+ fanoutChild,
521
+ structuredOutput: Boolean(config.structuredOutput),
819
522
  });
820
523
  }
821
524
  if (rewritten === event.systemPrompt) return;