pi-subagents 0.65.1 → 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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -8,7 +8,7 @@
8
8
  import type { Message } from "@earendil-works/pi-ai";
9
9
  import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
10
10
  import { extractTextFromContent, extractToolArgsPreview, getFinalOutput, hasEmptyTerminalAssistantResponse } from "../../shared/utils.ts";
11
- import type { EffectsProjection, RuntimeAcknowledgedChildExtensionsV1, SubagentOutputState, ToolBudgetState, Usage } from "../../shared/types.ts";
11
+ import type { EffectsProjection, RuntimeAcknowledgedChildExtensions, SubagentOutputState, ToolBudgetState, Usage } from "../../shared/types.ts";
12
12
  import {
13
13
  acceptChildWatchdogEvent,
14
14
  applyChildWatchdogMessage,
@@ -22,10 +22,12 @@ import { projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleSt
22
22
  import { formatSubagentModelVerificationError } from "../shared/model-fallback.ts";
23
23
  import { isMutatingTool, resolveCurrentPath } from "../shared/long-running-guard.ts";
24
24
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
25
- import type { InProcessChildLaunch } from "../shared/child-launch.ts";
26
- import { projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent, type ChildSessionFactory } from "../shared/child-session.ts";
25
+ import { createReportedChildSessionInput, type InProcessChildLaunch } from "../shared/child-launch.ts";
26
+ import { childSessionHasQueuedMessages, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent, type ChildSessionFactory } from "../shared/child-session.ts";
27
27
  import { formatSteerMessage } from "../shared/subagent-prompt-runtime.ts";
28
+ import { getReadonlySessionEvidence, requestReadonlySessionEvidence, type SettledReadonlyEvidence } from "../shared/readonly-session-evidence.ts";
28
29
  import type { SteerDeliveryStatus, SteerRequest } from "./control-channel.ts";
30
+ import { takeMatchingAcceptedSteer, unconsumedSteerReason } from "./steering.ts";
29
31
 
30
32
  export interface ChildEventContext {
31
33
  runId: string;
@@ -83,6 +85,8 @@ export interface RunChildSessionInput {
83
85
  registerTimeout?: (interrupt: (() => void) | undefined) => void;
84
86
  registerStop?: (stop: (() => void) | undefined) => void;
85
87
  registerSteer?: (steer: StepSteerHandler | undefined) => void;
88
+ /** Consumption (or unconsumed settlement) after the child accepted a steer or follow-up. */
89
+ onSteerOutcome?: (request: SteerRequest, delivery: SteerDelivery) => void;
86
90
  /** Receives the sink the child's watchdog hook reports status through; the launch's `watchdogStatus` must forward to it. */
87
91
  registerWatchdogStatus?: (sink: ((event: ChildWatchdogStatusEvent) => void) | undefined) => void;
88
92
  timeoutMessage?: string;
@@ -95,6 +99,16 @@ export interface RunChildSessionInput {
95
99
  modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>;
96
100
  modelResponseAliases?: Record<string, string[]>;
97
101
  mutationTools?: readonly string[];
102
+ /** Internal guarded continuation handoff; never part of persisted results. */
103
+ readonlyContinuation?: { source: ChildSession; expected: SettledReadonlyEvidence; modelId: string };
104
+ collectReadonlyEvidence?: boolean;
105
+ canContinue?: () => boolean;
106
+ }
107
+
108
+ const settledChildren = new WeakMap<RunChildSessionResult, ChildSession>();
109
+ export function getSettledReadonlyChild(result: RunChildSessionResult): ChildSession | undefined {
110
+ const child = settledChildren.get(result);
111
+ return child && getReadonlySessionEvidence(child) ? child : undefined;
98
112
  }
99
113
 
100
114
  export interface RunChildSessionResult {
@@ -123,7 +137,7 @@ export interface RunChildSessionResult {
123
137
  toolBudget?: ToolBudgetState;
124
138
  toolBudgetBlocked?: boolean;
125
139
  structuredOutput?: unknown;
126
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
140
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
127
141
  abortRecoveryDiagnostic?: string;
128
142
  effects?: EffectsProjection;
129
143
  }
@@ -176,12 +190,14 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
176
190
  let currentPath: string | undefined;
177
191
  let toolCount = 0;
178
192
  let session: ChildSession | undefined;
193
+ const acceptedSteers: Array<{ request: SteerRequest; text: string }> = [];
179
194
  let unsubscribe: (() => void) | undefined;
180
195
  let settled = false;
181
196
  let promptSettled = false;
182
197
  let forcedTermination = false;
183
198
  let cleanTerminalAssistantStopReceived = false;
184
199
  let agentSettledReceived = false;
200
+ let queuedDrainHold = false;
185
201
  let compactionStartedReceived = false;
186
202
  let afterCompactionSettlement = false;
187
203
  let finalDrainTimer: NodeJS.Timeout | undefined;
@@ -300,14 +316,28 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
300
316
  }
301
317
  // If the child emits its terminal event but its run never settles (a hook
302
318
  // is stuck), abort it after a short grace period and then finish without it.
319
+ const observeQueuedDrainHold = (): boolean => {
320
+ if (childSessionHasQueuedMessages(session)) queuedDrainHold = true;
321
+ return queuedDrainHold;
322
+ };
303
323
  function startFinalDrain(): void {
304
324
  if (childWatchdogIsActive(childWatchdogState)) {
305
325
  armWatchdogTail();
306
326
  return;
307
327
  }
328
+ if (promptSettled || finalDrainTimer || settled) return;
329
+ if (observeQueuedDrainHold()) return;
330
+ armFinalDrainTimer();
331
+ }
332
+ function armFinalDrainTimer(): void {
308
333
  if (promptSettled || finalDrainTimer || settled) return;
309
334
  finalDrainTimer = setTimeout(() => {
310
335
  if (settled || promptSettled) return;
336
+ if (input.launch.capture.finalDrainHeld() || observeQueuedDrainHold()) {
337
+ finalDrainTimer = undefined;
338
+ armFinalDrainTimer();
339
+ return;
340
+ }
311
341
  forcedTermination = true;
312
342
  if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !error && !assistantError) {
313
343
  error = `Subagent session did not settle within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Aborting it.`;
@@ -323,6 +353,8 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
323
353
  }
324
354
  const applyChildLifecycle = (action: ChildLifecycleAction): void => {
325
355
  if (action === "cancel-drain") {
356
+ cleanTerminalAssistantStopReceived = false;
357
+ agentSettledReceived = false;
326
358
  clearFinalDrainTimers();
327
359
  clearWatchdogTailTimer();
328
360
  return;
@@ -390,6 +422,9 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
390
422
  compactionStartedReceived = false;
391
423
  afterCompactionSettlement = false;
392
424
  }
425
+ if (event.type === "turn_start" || event.type === "agent_start" || event.type === "auto_retry_start") {
426
+ queuedDrainHold = false;
427
+ }
393
428
  if (event.type === "agent_start" || event.type === "auto_retry_start") {
394
429
  compactionStartedReceived = false;
395
430
  afterCompactionSettlement = false;
@@ -459,6 +494,16 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
459
494
  messages.push(event.message);
460
495
  const text = extractTextFromContent(event.message.content);
461
496
  if (text) writeOutputText(text);
497
+ if (event.type === "message_end" && event.message.role === "user" && text) {
498
+ const matched = takeMatchingAcceptedSteer(acceptedSteers, text);
499
+ if (matched) {
500
+ input.onSteerOutcome?.(matched.request, {
501
+ state: "delivered",
502
+ deliveryStatus: "delivered",
503
+ message: "Child consumed the steering input.",
504
+ });
505
+ }
506
+ }
462
507
 
463
508
  if (input.childWatchdog && event.type === "message_end") {
464
509
  const next = applyChildWatchdogMessage(childWatchdogState, event.message);
@@ -474,6 +519,10 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
474
519
  }
475
520
  }
476
521
  if (event.message.errorMessage) assistantError = event.message.errorMessage;
522
+ else if (hasToolCall && event.message.stopReason === "toolUse") {
523
+ // A recovered request can finish via a terminating tool, without a text stop.
524
+ assistantError = undefined;
525
+ }
477
526
  const eventUsage = event.message.usage;
478
527
  if (eventUsage) {
479
528
  usage.turns++;
@@ -514,9 +563,15 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
514
563
  };
515
564
 
516
565
  /** The child run ended (or was forced to end); fold in the outcome once the child's shutdown work is done. */
566
+ const failUnconsumedSteers = (): void => {
567
+ for (const entry of acceptedSteers.splice(0)) {
568
+ input.onSteerOutcome?.(entry.request, { state: "failed", message: unconsumedSteerReason(entry.request.mode) });
569
+ }
570
+ };
517
571
  const settle = (promptError: unknown, forced = false): void => {
518
572
  if (settled) return;
519
573
  settled = true;
574
+ failUnconsumedSteers();
520
575
  const closed = finish();
521
576
  const finalOutput = getFinalOutput(messages);
522
577
  let finalError = error ?? assistantError;
@@ -533,29 +588,33 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
533
588
  : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal)
534
589
  ? 0
535
590
  : finalError || promptError !== undefined ? 1 : 0;
536
- void closed.then(() => resolve(omitUndefined({
537
- exitCode,
538
- messages,
539
- usage,
540
- toolCount,
541
- durationMs: Date.now() - startedAt,
542
- model,
543
- error: stopped ? stopMessage() : timedOut ? (error ?? timeoutMessage()) : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? undefined : finalError,
544
- finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage() : error ?? timeoutMessage()) : finalOutput,
545
- outputState: finalOutput.trim() ? "present" : "absent",
546
- interrupted: interrupted || undefined,
547
- timedOut: timedOut || undefined,
548
- stopped: stopped || undefined,
549
- observedMutationAttempt,
550
- structuredOutputToolInvoked,
551
- structuredOutputMessageStartIndex,
552
- watchdog: childWatchdogState,
553
- sessionFile: session?.sessionFile,
554
- currentTool,
555
- currentToolArgs,
556
- currentPath,
557
- afterCompactionSettlement: afterCompactionSettlement || undefined,
558
- })));
591
+ void closed.then(() => {
592
+ const result: RunChildSessionResult = omitUndefined({
593
+ exitCode,
594
+ messages,
595
+ usage,
596
+ toolCount,
597
+ durationMs: Date.now() - startedAt,
598
+ model,
599
+ error: stopped ? stopMessage() : timedOut ? (error ?? timeoutMessage()) : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? undefined : finalError,
600
+ finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage() : error ?? timeoutMessage()) : finalOutput,
601
+ outputState: finalOutput.trim() ? "present" : "absent",
602
+ interrupted: interrupted || undefined,
603
+ timedOut: timedOut || undefined,
604
+ stopped: stopped || undefined,
605
+ observedMutationAttempt,
606
+ structuredOutputToolInvoked,
607
+ structuredOutputMessageStartIndex,
608
+ watchdog: childWatchdogState,
609
+ sessionFile: session?.sessionFile,
610
+ currentTool,
611
+ currentToolArgs,
612
+ currentPath,
613
+ afterCompactionSettlement: afterCompactionSettlement || undefined,
614
+ });
615
+ if (session && !forced && !forcedTermination && !interrupted && !timedOut && !stopped && getReadonlySessionEvidence(session)) settledChildren.set(result, session);
616
+ resolve(result);
617
+ });
559
618
  };
560
619
 
561
620
  input.registerInterrupt?.(() => {
@@ -575,33 +634,59 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
575
634
 
576
635
  void (async () => {
577
636
  try {
578
- const created = await input.factory.create({
579
- ...input.launch.session,
580
- onExtensionError: (extensionError) => {
581
- input.transcriptWriter?.writeStderrLine(`Extension error (${extensionError.extensionPath}, ${extensionError.event}): ${extensionError.error instanceof Error ? extensionError.error.message : String(extensionError.error)}`);
582
- },
583
- });
637
+ const continuation = input.readonlyContinuation;
638
+ const checkContinuation = () => {
639
+ if (!continuation) return;
640
+ if (interrupted || timedOut || stopped || input.canContinue?.() !== true
641
+ || (input.runDeadlineAt !== undefined && Date.now() >= input.runDeadlineAt)
642
+ || getReadonlySessionEvidence(continuation.source) !== continuation.expected
643
+ || continuation.source.detached || continuation.source.shutDown) throw new Error("Read-only continuation handoff vetoed");
644
+ };
645
+ checkContinuation();
646
+ const createInput = createReportedChildSessionInput(input.launch, input.transcriptWriter);
647
+ if (input.collectReadonlyEvidence || continuation) requestReadonlySessionEvidence(createInput, continuation?.expected);
648
+ const created = await input.factory.create(createInput);
584
649
  if (settled) {
585
650
  void created.dispose();
586
651
  return;
587
652
  }
588
653
  session = created;
654
+ const steer = created.steer.bind(created);
655
+ const followUp = created.followUp.bind(created);
656
+ created.steer = async (text) => {
657
+ if (cleanTerminalAssistantStopReceived || agentSettledReceived) queuedDrainHold = true;
658
+ return steer(text);
659
+ };
660
+ created.followUp = async (text) => {
661
+ if (cleanTerminalAssistantStopReceived || agentSettledReceived) queuedDrainHold = true;
662
+ return followUp(text);
663
+ };
664
+ checkContinuation();
665
+ if (continuation && (created.modelId !== continuation.modelId || input.launch.capture.completionIntentContext?.()?.model?.api !== continuation.expected.api)) throw new Error("Read-only continuation model changed");
589
666
  unsubscribe = created.subscribe(processEvent);
590
667
  input.registerWatchdogStatus?.((event) => processEvent(event as unknown as ChildSessionEvent));
591
668
  input.registerSteer?.(async (request) => {
592
669
  const text = formatSteerMessage(request);
593
670
  const followUp = request.mode === "follow_up";
671
+ const accepted = { request, text };
672
+ acceptedSteers.push(accepted);
673
+ const queued: SteerDelivery = {
674
+ state: "queued",
675
+ deliveryStatus: "queued",
676
+ message: followUp ? "Pi queued the follow-up input." : "Pi accepted the steering input.",
677
+ };
594
678
  try {
595
679
  if (followUp) await created.followUp(text);
596
680
  else await created.steer(text);
597
681
  } catch (steerError) {
682
+ const index = acceptedSteers.indexOf(accepted);
683
+ if (index >= 0) acceptedSteers.splice(index, 1);
598
684
  return { state: "failed", message: steerError instanceof Error ? steerError.message : String(steerError) };
599
685
  }
600
- return followUp
601
- ? { state: "queued", deliveryStatus: "queued", message: "Pi queued the follow-up input." }
602
- : { state: "delivered", deliveryStatus: "delivered", message: "Pi accepted the steering input." };
686
+ return queued;
603
687
  });
604
688
  if (interrupted || timedOut || stopped) abortChild();
689
+ checkContinuation();
605
690
  await created.prompt(input.prompt);
606
691
  promptSettled = true;
607
692
  settle(undefined);
@@ -2,12 +2,14 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import { safeTerminalText } from "../../shared/display-text.ts";
5
+ import { getArtifactPaths, getArtifactsDir } from "../../shared/artifacts.ts";
6
+ import { readFleetTranscript } from "../../tui/fleet-transcript.ts";
5
7
  import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, formatWorkflowStageLine, listAsyncRuns } from "./async-status.ts";
6
8
  import { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
7
9
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
8
10
  import { formatModelThinking } from "../../shared/formatters.ts";
9
11
  import { formatActivityLabel } from "../../shared/status-format.ts";
10
- import { DIRS, type AsyncStatus, type Details, type ForegroundResumeRun, type NestedRunSummary, type SteeringStatus, type SubagentState } from "../../shared/types.ts";
12
+ import { DIRS, type AsyncStatus, type Details, type ForegroundRunControl, type ForegroundResumeRun, type NestedRunSummary, type SteeringStatus, type SubagentState } from "../../shared/types.ts";
11
13
  import { inspectActiveAsyncCapacityOwner, type ActiveAsyncCapacityInspection } from "./active-async-capacity.ts";
12
14
  import { readStatus } from "../../shared/utils.ts";
13
15
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
@@ -30,6 +32,7 @@ import { getExternalJobProvider } from "../../api/external-job-provider.ts";
30
32
  import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
31
33
  import { formatWorkflowChecklistText, projectWorkflowChecklist } from "../../workflows/workflow-checklist.ts";
32
34
  import { validHostStepNodes } from "../shared/host-step-status.ts";
35
+ import { workflowAsyncChildSteeringGuidance } from "../shared/workflow-async-child-guidance.ts";
33
36
 
34
37
  interface RunStatusParams {
35
38
  action?: string;
@@ -88,6 +91,7 @@ function formatRunLifecycleDebug(input: { status: AsyncStatus; asyncDir: string;
88
91
  `Run: ${status.runId}`,
89
92
  `Dir: ${asyncDir}`,
90
93
  `Status file: ${path.join(asyncDir, "status.json")}`,
94
+ status.workflowReceiptPath ? `Workflow receipt: ${status.workflowReceiptPath}` : undefined,
91
95
  `Process terminal file: ${path.join(asyncDir, "process-terminal.json")}`,
92
96
  `Session: ${status.sessionId ?? "unknown"}`,
93
97
  `State: ${status.state}`,
@@ -241,6 +245,54 @@ function formatRememberedForegroundStatus(run: ForegroundResumeRun): string {
241
245
  return lines.join("\n");
242
246
  }
243
247
 
248
+ /** Request-only snapshot of the same artifact used by Fleet; no session or output fallback. */
249
+ function formatLiveForegroundTranscript(control: ForegroundRunControl, state: SubagentState, options: { index?: number; lines?: number }): string {
250
+ if (!state.currentSessionId || control.sessionId !== state.currentSessionId) {
251
+ throw new Error(`Foreground run '${control.runId}' is not owned by the current session.`);
252
+ }
253
+ if (options.index !== undefined && (!Number.isInteger(options.index) || options.index < 0)) {
254
+ throw new Error("Transcript index must be a non-negative integer.");
255
+ }
256
+ const children = control.activeChildren
257
+ ? [...control.activeChildren.values()]
258
+ : control.currentAgent ? [{ index: control.currentIndex ?? 0, agent: control.currentAgent, sessionName: control.sessionName }] : [];
259
+ const header = [`Run: ${control.runId}`, "State: live foreground"];
260
+ if (children.length === 0) return `${header.map((line) => safeTerminalText(line)).join("\n")}\nTranscript unavailable: no active foreground child.`;
261
+ if (options.index === undefined && children.length > 1) {
262
+ throw new Error(`Transcript view requires index for foreground run '${control.runId}'. Active child indexes: ${children.map((child) => child.index).join(", ")}.`);
263
+ }
264
+ const child = options.index === undefined ? children[0]! : children.find((child) => child.index === options.index);
265
+ if (!child) throw new Error(`Transcript index ${options.index} is not an active foreground child of '${control.runId}'.`);
266
+ const root = getArtifactsDir(state.parentSessionFile ?? null, control.cwd ?? state.baseCwd, state.artifactDirPreference);
267
+ const transcriptPath = getArtifactPaths(root, control.runId, child.agent, child.index).transcriptPath;
268
+ const transcript = readFleetTranscript(transcriptPath, { trustedRoots: [root] });
269
+ const lineLimit = Number.isFinite(options.lines) ? Math.max(1, Math.min(500, Math.trunc(options.lines!))) : 80;
270
+ const allLines = transcript.events.flatMap((event) => {
271
+ const text = event.kind === "tool"
272
+ ? [`Tool: ${event.name} (${event.status})`, event.argsPayload ?? event.args, event.output ?? event.error].filter(Boolean).join("\n")
273
+ : event.kind === "notice" ? event.text : `${event.kind === "assistant" ? "Assistant" : "Supervisor"}: ${event.text}`;
274
+ return text.split(/\r?\n/);
275
+ });
276
+ let body = allLines.slice(-lineLimit).join("\n");
277
+ let truncated = transcript.truncated || allLines.length > lineLimit
278
+ || transcript.events.some((event) => event.kind === "tool" && event.outputTruncated)
279
+ || body.includes("… message truncated");
280
+ const maxOutputBytes = 32 * 1024;
281
+ const bytes = Buffer.from(body);
282
+ if (bytes.length > maxOutputBytes) {
283
+ // Skip UTF-8 continuation bytes at the beginning of the bounded tail.
284
+ let start = bytes.length - maxOutputBytes;
285
+ while ((bytes[start]! & 0xc0) === 0x80) start++;
286
+ body = bytes.subarray(start).toString("utf-8");
287
+ truncated = true;
288
+ }
289
+ header.push(`Child: ${child.index} (${child.sessionName?.trim() || child.agent})`, `Transcript: ${transcriptPath}`);
290
+ if (transcript.warning) header.push(`Transcript warning: ${transcript.warning}`);
291
+ header.push(`Live transcript tail${truncated ? " (tail truncated)" : ""}:`);
292
+ if (!body) header.push("Transcript unavailable: no readable activity in the bounded artifact tail yet.");
293
+ return [...header.map((line) => safeTerminalText(line)), body].filter(Boolean).join("\n");
294
+ }
295
+
244
296
  function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options: { index?: number; lines?: number }): string {
245
297
  let index = options.index;
246
298
  if (index !== undefined && !Number.isInteger(index)) throw new Error("Transcript index must be an integer.");
@@ -313,6 +365,12 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
313
365
  return inspectSubagentFleet(params, { asyncDirRoot, resultsDir, kill: deps.kill, now: deps.now, state: deps.state, childSafe: Boolean(deps.nested) });
314
366
  }
315
367
  if (!params.id && !params.runId && !params.dir) {
368
+ if (params.view === "transcript" && deps.state?.currentSessionId) {
369
+ const controls = [...deps.state.foregroundControls.values()].filter((control) => control.sessionId === currentSessionId);
370
+ const foreground = controls.find((control) => control.runId === deps.state?.lastForegroundControlId)
371
+ ?? controls.sort((left, right) => right.updatedAt - left.updatedAt)[0];
372
+ if (foreground) return inspectSubagentStatus({ ...params, id: foreground.runId }, deps);
373
+ }
316
374
  if (deps.nested) {
317
375
  return {
318
376
  content: [{ type: "text", text: "Child-safe subagent status requires an id when no foreground run is active." }],
@@ -353,6 +411,13 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
353
411
  } else if (!params.dir && requestedId) {
354
412
  const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
355
413
  if (resolved?.kind === "foreground") {
414
+ const control = deps.state?.foregroundControls.get(resolved.id);
415
+ if (control && deps.state && params.view === "transcript") {
416
+ return {
417
+ content: [{ type: "text", text: formatLiveForegroundTranscript(control, deps.state, params) }],
418
+ details: { mode: "management", results: [] },
419
+ };
420
+ }
356
421
  const run = deps.state?.foregroundRuns?.get(resolved.id);
357
422
  if (run) {
358
423
  try {
@@ -423,7 +488,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
423
488
  const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: deps.abandonedSlotReleaseAfterMs });
424
489
  return {
425
490
  content: [{ type: "text", text: formatRunLifecycleDebug({ status: diskStatus, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
426
- details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
491
+ details: { mode: "single", results: [], ...(diskStatus.workflowReceiptPath ? { workflowReceiptPath: diskStatus.workflowReceiptPath } : {}), ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
427
492
  };
428
493
  }
429
494
  if (params.view === "transcript") {
@@ -450,7 +515,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
450
515
  const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: deps.abandonedSlotReleaseAfterMs });
451
516
  return {
452
517
  content: [{ type: "text", text: formatRunLifecycleDebug({ status, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
453
- details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
518
+ details: { mode: "single", results: [], ...(status.workflowReceiptPath ? { workflowReceiptPath: status.workflowReceiptPath } : {}), ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
454
519
  };
455
520
  }
456
521
  if (params.view === "transcript") {
@@ -560,6 +625,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
560
625
  const display = runStatusStepDisplayName(step);
561
626
  const phase = step.phase ? `[${step.phase}] ` : "";
562
627
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
628
+ if (status.mode === "workflow" && step.runId) lines.push(` Child run: ${step.runId}`);
563
629
  const structuredOutputPreview = step.structuredOutput === undefined ? undefined : formatWorkflowJsonPreview(step.structuredOutput, 4_000);
564
630
  if (structuredOutputPreview !== undefined) lines.push(` Structured output: ${structuredOutputPreview}`);
565
631
  if (step.structuredOutputPath) lines.push(` Structured output path: ${step.structuredOutputPath}`);
@@ -624,8 +690,10 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
624
690
  lines.push(`Steer live foreground child: subagent({ action: "steer", id: "${control.runId}", index: ${index}, message: "..." })`);
625
691
  }
626
692
  }
693
+ lines.push(...workflowAsyncChildSteeringGuidance(status, deps.state));
627
694
  }
628
695
  if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
696
+ if (status.workflowReceiptPath) lines.push(`Workflow receipt: ${status.workflowReceiptPath}`);
629
697
  if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
630
698
  const allExternal = (status.steps?.length ?? 0) > 0 && status.steps!.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job");
631
699
  if (status.state === "running" && !allExternal && status.mode !== "workflow") lines.push(`Steer running child: subagent({ action: "steer", id: "${status.runId}", message: "..." })`);
@@ -639,7 +707,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
639
707
 
640
708
  const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
641
709
  if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
642
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(status.preflight ? { preflight: status.preflight } : {}), ...(status.workflow?.preflightWarnings?.length ? { preflightWarnings: status.workflow.preflightWarnings } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
710
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(status.workflowReceiptPath ? { workflowReceiptPath: status.workflowReceiptPath } : {}), ...(status.preflight ? { preflight: status.preflight } : {}), ...(status.workflow?.preflightWarnings?.length ? { preflightWarnings: status.workflow.preflightWarnings } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
643
711
  }
644
712
  }
645
713
 
@@ -680,6 +748,11 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
680
748
  const runId = data.runId ?? data.id ?? resolvedId;
681
749
  const lines = [`Run: ${runId}`, data.toolCallId ? `Tool call: ${data.toolCallId}` : undefined, `State: ${status}`, `Result: ${resultPath}`].filter((line): line is string => Boolean(line));
682
750
  if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
751
+ const receipt = (data as Record<string, unknown>).workflowReceipt;
752
+ const receiptPath = receipt && typeof receipt === "object" && !Array.isArray(receipt)
753
+ ? (receipt as Record<string, unknown>).path : undefined;
754
+ const workflowReceiptPath = typeof receiptPath === "string" && receiptPath ? receiptPath : undefined;
755
+ if (workflowReceiptPath) lines.push(`Workflow receipt: ${workflowReceiptPath}`);
683
756
  const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
684
757
  lines.push(...formatTimeoutRecoveryLines(data.timeoutRecovery, " "));
685
758
  for (const [index, child] of children.entries()) {
@@ -694,7 +767,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
694
767
  if (data.summary) lines.push("", data.summary);
695
768
  const workflowChildren = parseWorkflowChildSummary((data as unknown as Record<string, unknown>).workflowChildren);
696
769
  if (workflowChildren && workflowChildren.workflowRunId !== runId) throw new Error("workflowChildren.workflowRunId does not match the result run id.");
697
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowChildren ? { workflowChildren } : {}) } };
770
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowReceiptPath ? { workflowReceiptPath } : {}), ...(workflowChildren ? { workflowChildren } : {}) } };
698
771
  } catch (error) {
699
772
  const message = error instanceof Error ? error.message : String(error);
700
773
  return {
@@ -21,11 +21,6 @@ export const HOST_PEER_ALIASES: ReadonlyArray<{ specifier: string; pkg: string;
21
21
  { specifier: "@earendil-works/pi-coding-agent", pkg: "@earendil-works/pi-coding-agent", subpath: "." },
22
22
  { specifier: "@earendil-works/pi-agent-core", pkg: "@earendil-works/pi-agent-core", subpath: "." },
23
23
  { specifier: "@earendil-works/pi-agent-core/node", pkg: "@earendil-works/pi-agent-core", subpath: "./node" },
24
- { specifier: "@earendil-works/chord", pkg: "@earendil-works/chord", subpath: "." },
25
- { specifier: "@earendil-works/chord/context", pkg: "@earendil-works/chord", subpath: "./context" },
26
- { specifier: "@earendil-works/pi-server", pkg: "@earendil-works/pi-server", subpath: "." },
27
- { specifier: "@earendil-works/pi-server/unix", pkg: "@earendil-works/pi-server", subpath: "./unix" },
28
- { specifier: "@earendil-works/pi-client/unix", pkg: "@earendil-works/pi-client", subpath: "./unix" },
29
24
  { specifier: "@earendil-works/pi-tui", pkg: "@earendil-works/pi-tui", subpath: "." },
30
25
  { specifier: "@earendil-works/pi-ai", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
31
26
  { specifier: "@earendil-works/pi-ai/compat", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
@@ -36,6 +31,19 @@ export const HOST_PEER_ALIASES: ReadonlyArray<{ specifier: string; pkg: string;
36
31
  { specifier: "typebox/value", pkg: "typebox", subpath: "./value" },
37
32
  ];
38
33
 
34
+ /** Public Pi manifests introduce chord in 0.85.0 (absent through 0.84.4). */
35
+ const CHORD_PEER_ALIASES = [
36
+ { specifier: "@earendil-works/chord", pkg: "@earendil-works/chord", subpath: "." },
37
+ { specifier: "@earendil-works/chord/context", pkg: "@earendil-works/chord", subpath: "./context" },
38
+ ];
39
+
40
+ /** Experimental dependencies accidentally published in exactly Pi 0.85.0. */
41
+ const PI0850_PEER_ALIASES = [
42
+ { specifier: "@earendil-works/pi-server", pkg: "@earendil-works/pi-server", subpath: "." },
43
+ { specifier: "@earendil-works/pi-server/unix", pkg: "@earendil-works/pi-server", subpath: "./unix" },
44
+ { specifier: "@earendil-works/pi-client/unix", pkg: "@earendil-works/pi-client", subpath: "./unix" },
45
+ ];
46
+
39
47
  interface PackageManifest {
40
48
  name?: unknown;
41
49
  version?: unknown;
@@ -103,7 +111,11 @@ export function resolvePackageSubpath(packageDir: string, subpath: string): stri
103
111
 
104
112
  /** Find `pkg` as the pi package itself, one of its dependencies, or a sibling in a hoisted install. */
105
113
  export function findHostPeerPackageDir(piPackageRoot: string, pkg: string): string | undefined {
106
- if (readManifest(piPackageRoot)?.name === pkg) return piPackageRoot;
114
+ return findPeerPackageDir(piPackageRoot, pkg, readManifest(piPackageRoot)?.name);
115
+ }
116
+
117
+ function findPeerPackageDir(piPackageRoot: string, pkg: string, hostName: unknown): string | undefined {
118
+ if (hostName === pkg) return piPackageRoot;
107
119
  const candidates = [path.join(piPackageRoot, "node_modules", pkg)];
108
120
  let dir = piPackageRoot;
109
121
  for (;;) {
@@ -127,14 +139,21 @@ export function resolveHostPeerAliases(
127
139
  const aliases: Record<string, string> = {};
128
140
  const missing: string[] = [];
129
141
  const supplemental: string[] = [];
130
- for (const { specifier, pkg, subpath } of HOST_PEER_ALIASES) {
131
- const packageDir = findHostPeerPackageDir(piPackageRoot, pkg);
142
+ const hostManifest = readManifest(piPackageRoot);
143
+ const isPi0850 = hostManifest?.version === "0.85.0";
144
+ // Only known stable pre-chord versions may omit it. Unknown/prerelease
145
+ // hosts retain the required aliases, rather than hiding a broken install.
146
+ const stableVersion = typeof hostManifest?.version === "string" ? /^0\.(\d+)\.\d+$/.exec(hostManifest.version) : null;
147
+ const isPreChord = stableVersion !== null && Number(stableVersion[1]) < 85;
148
+ const required = [...HOST_PEER_ALIASES, ...(isPreChord ? [] : CHORD_PEER_ALIASES), ...(isPi0850 ? PI0850_PEER_ALIASES : [])];
149
+ for (const { specifier, pkg, subpath } of required) {
150
+ const packageDir = findPeerPackageDir(piPackageRoot, pkg, hostManifest?.name);
132
151
  let target = packageDir ? resolvePackageSubpath(packageDir, subpath) : undefined;
133
152
  // Pi 0.85.0 omitted this runtime dependency. Never replace working host
134
153
  // exports or extend this exact version contract to other peers/hosts.
135
154
  if ((!target || !fs.existsSync(target))
136
155
  && (specifier === "@earendil-works/pi-server" || specifier === "@earendil-works/pi-server/unix")
137
- && readManifest(piPackageRoot)?.version === "0.85.0"
156
+ && isPi0850
138
157
  && (!packageDir || readManifest(packageDir)?.version === "0.85.0")) {
139
158
  const localDir = findHostPeerPackageDir(extensionRoot, pkg);
140
159
  if (localDir && readManifest(localDir)?.version === "0.85.0") {
@@ -0,0 +1,88 @@
1
+ /** Native runner construction, separate from its executable entrypoint and attempt loop. */
2
+ import * as path from "node:path";
3
+ import { buildInProcessChildLaunch, type BuildInProcessChildLaunchInput, type InheritedChildRuntime } from "../shared/child-launch.ts";
4
+ import { deriveForkPromptCacheKey } from "../shared/child-tool-plan.ts";
5
+ import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
6
+ import type { RunnerSubagentStep } from "../shared/parallel-utils.ts";
7
+ import { formatAcceptancePrompt } from "../shared/acceptance.ts";
8
+ import { isAgentContract } from "../shared/agent-contract.ts";
9
+
10
+ export interface RunnerChildLaunchContext {
11
+ cwd: string;
12
+ id: string;
13
+ flatIndex: number;
14
+ artifactsDir?: string;
15
+ childIntercomTarget?: string;
16
+ orchestratorIntercomTarget?: string;
17
+ nestedRoute?: BuildInProcessChildLaunchInput["nestedRoute"];
18
+ runFanoutBudget?: BuildInProcessChildLaunchInput["runFanoutBudget"];
19
+ capabilityCeiling?: BuildInProcessChildLaunchInput["capabilityCeiling"];
20
+ inheritedChildRuntime?: InheritedChildRuntime;
21
+ hostAvailableBuiltins?: readonly string[];
22
+ }
23
+
24
+ export function buildRunnerChildLaunch(step: RunnerSubagentStep, ctx: RunnerChildLaunchContext, attempt: {
25
+ model?: string;
26
+ sessionEnabled: boolean;
27
+ sessionDir?: string;
28
+ sessionName?: string;
29
+ structuredOutput?: BuildInProcessChildLaunchInput["structuredOutput"];
30
+ childWatchdog?: BuildInProcessChildLaunchInput["childWatchdog"];
31
+ watchdogStatus: NonNullable<BuildInProcessChildLaunchInput["watchdogStatus"]>;
32
+ }) {
33
+ // Resource-backed system instructions survive SDK split-turn compaction verbatim.
34
+ const acceptancePrompt = step.effectiveAcceptance
35
+ ? formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContract(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) })
36
+ : "";
37
+ return buildInProcessChildLaunch({
38
+ parentSessionId: step.parentSessionId,
39
+ forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
40
+ sessionEnabled: attempt.sessionEnabled,
41
+ sessionDir: attempt.sessionDir,
42
+ sessionFile: step.sessionFile,
43
+ model: attempt.model,
44
+ inheritProjectContext: step.inheritProjectContext,
45
+ inheritGlobalContext: step.inheritGlobalContext,
46
+ inheritSkills: step.inheritSkills,
47
+ requireReadTool: Boolean(step.skills?.length),
48
+ tools: step.tools,
49
+ excludeTools: step.excludeTools,
50
+ allowNestedSubagents: step.allowNestedSubagents,
51
+ extensions: step.extensions,
52
+ subagentOnlyExtensions: step.subagentOnlyExtensions,
53
+ fast: step.fast,
54
+ modelCandidates: step.modelCandidates,
55
+ systemPrompt: acceptancePrompt ? `${step.systemPrompt ?? ""}\n${acceptancePrompt}` : step.systemPrompt ?? "",
56
+ systemPromptMode: step.systemPromptMode,
57
+ mcpDirectTools: step.mcpDirectTools,
58
+ extensionBindings: normalizeExtensionBindings(step.extensionBindings)?.value,
59
+ capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
60
+ cwd: step.cwd ?? ctx.cwd,
61
+ intercomSessionName: ctx.childIntercomTarget,
62
+ sessionName: attempt.sessionName,
63
+ orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
64
+ runId: ctx.id,
65
+ childAgentName: step.agent,
66
+ childIndex: ctx.flatIndex,
67
+ nestedRoute: ctx.nestedRoute,
68
+ runFanoutBudget: ctx.runFanoutBudget ? {
69
+ ...ctx.runFanoutBudget,
70
+ ...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
71
+ } : undefined,
72
+ structuredOutput: attempt.structuredOutput,
73
+ toolBudget: step.toolBudget,
74
+ permissionRules: step.permissionRules,
75
+ permissionAuditPath: step.permissionRules && ctx.artifactsDir
76
+ ? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`) : undefined,
77
+ childWatchdog: attempt.childWatchdog,
78
+ // registerChildWatchdog returns before reading the sink when unconfigured.
79
+ ...(attempt.childWatchdog ? { watchdogStatus: attempt.watchdogStatus } : {}),
80
+ waitToolEnabled: step.waitToolEnabled,
81
+ waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
82
+ thinkingCeiling: step.thinkingCeiling,
83
+ maxSubagentDepth: step.maxSubagentDepth,
84
+ inherited: ctx.inheritedChildRuntime,
85
+ hostAvailableBuiltins: ctx.hostAvailableBuiltins,
86
+ host: "runner",
87
+ });
88
+ }