pi-subagents 0.65.1 → 0.66.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 (85) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +16 -2
  5. package/docs/configuration.md +18 -0
  6. package/docs/extension-api.md +91 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +3 -3
  10. package/docs/workflows.md +14 -7
  11. package/package.json +2 -1
  12. package/skills/pi-subagents/references/execution-controls.md +14 -1
  13. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  14. package/src/agents/advertised-agent-prompt.ts +63 -0
  15. package/src/agents/agent-management.ts +14 -1
  16. package/src/agents/agent-serializer.ts +2 -0
  17. package/src/agents/agents.ts +8 -0
  18. package/src/api/shared-types.ts +1 -1
  19. package/src/api/workflow-resources.ts +6 -0
  20. package/src/extension/index.ts +40 -2
  21. package/src/extension/public-execution.ts +0 -1
  22. package/src/extension/rpc.ts +4 -21
  23. package/src/extension/schemas.ts +8 -6
  24. package/src/extension/tool-description.ts +6 -5
  25. package/src/intercom/native-supervisor-channel.ts +82 -54
  26. package/src/runs/background/active-async-capacity.ts +18 -18
  27. package/src/runs/background/async-job-tracker.ts +35 -3
  28. package/src/runs/background/async-status-snapshot.ts +10 -12
  29. package/src/runs/background/async-status.ts +17 -9
  30. package/src/runs/background/auto-drain.ts +40 -29
  31. package/src/runs/background/chain-root-attachment.ts +8 -0
  32. package/src/runs/background/control-channel.ts +78 -44
  33. package/src/runs/background/notify.ts +86 -12
  34. package/src/runs/background/owned-process-tree.ts +6 -6
  35. package/src/runs/background/process-terminal.ts +23 -23
  36. package/src/runs/background/run-child-session.ts +60 -32
  37. package/src/runs/background/run-status.ts +75 -5
  38. package/src/runs/background/runner-aliases.ts +18 -7
  39. package/src/runs/background/runner-child-launch.ts +86 -0
  40. package/src/runs/background/stale-run-reconciler.ts +3 -1
  41. package/src/runs/background/subagent-runner.ts +412 -209
  42. package/src/runs/background/subagent-wait.ts +3 -0
  43. package/src/runs/background/wait-completions.ts +4 -0
  44. package/src/runs/foreground/async-steering-action.ts +19 -0
  45. package/src/runs/foreground/execution.ts +101 -25
  46. package/src/runs/foreground/subagent-executor.ts +474 -197
  47. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  48. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  49. package/src/runs/shared/acceptance.ts +2 -2
  50. package/src/runs/shared/agent-contract.ts +1 -1
  51. package/src/runs/shared/async-status-projection.ts +47 -47
  52. package/src/runs/shared/child-hooks.ts +151 -2
  53. package/src/runs/shared/child-launch.ts +18 -13
  54. package/src/runs/shared/child-session.ts +42 -6
  55. package/src/runs/shared/child-tool-plan.ts +2 -2
  56. package/src/runs/shared/completion-evidence.ts +2 -2
  57. package/src/runs/shared/completion-guard.ts +1 -0
  58. package/src/runs/shared/host-step-status.ts +11 -11
  59. package/src/runs/shared/llm-intent-arbiter.ts +10 -9
  60. package/src/runs/shared/model-fallback.ts +10 -6
  61. package/src/runs/shared/nested-events.ts +5 -5
  62. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  63. package/src/runs/shared/parallel-handoff.ts +57 -12
  64. package/src/runs/shared/parallel-utils.ts +2 -2
  65. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  66. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  67. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  68. package/src/runs/shared/run-fanout-budget.ts +8 -8
  69. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  70. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  71. package/src/runs/shared/worktree-setup-command.ts +190 -0
  72. package/src/runs/shared/worktree.ts +332 -204
  73. package/src/shared/types.ts +81 -60
  74. package/src/shared/utils.ts +7 -2
  75. package/src/shared/workflow-child-permit.ts +18 -13
  76. package/src/tui/fleet.ts +11 -5
  77. package/src/tui/render.ts +23 -5
  78. package/src/workflows/chat-progress.ts +3 -3
  79. package/src/workflows/scripted-workflow.ts +38 -10
  80. package/src/workflows/workflow-checklist.ts +11 -15
  81. package/src/workflows/workflow-child-summary.ts +57 -8
  82. package/src/workflows/workflow-preflight.ts +19 -19
  83. package/src/workflows/workflow-receipt.ts +3 -3
  84. package/src/workflows/workflow-resources.ts +96 -21
  85. package/src/workflows/workflow-settlement.ts +3 -0
@@ -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";
@@ -88,6 +90,7 @@ function formatRunLifecycleDebug(input: { status: AsyncStatus; asyncDir: string;
88
90
  `Run: ${status.runId}`,
89
91
  `Dir: ${asyncDir}`,
90
92
  `Status file: ${path.join(asyncDir, "status.json")}`,
93
+ status.workflowReceiptPath ? `Workflow receipt: ${status.workflowReceiptPath}` : undefined,
91
94
  `Process terminal file: ${path.join(asyncDir, "process-terminal.json")}`,
92
95
  `Session: ${status.sessionId ?? "unknown"}`,
93
96
  `State: ${status.state}`,
@@ -241,6 +244,54 @@ function formatRememberedForegroundStatus(run: ForegroundResumeRun): string {
241
244
  return lines.join("\n");
242
245
  }
243
246
 
247
+ /** Request-only snapshot of the same artifact used by Fleet; no session or output fallback. */
248
+ function formatLiveForegroundTranscript(control: ForegroundRunControl, state: SubagentState, options: { index?: number; lines?: number }): string {
249
+ if (!state.currentSessionId || control.sessionId !== state.currentSessionId) {
250
+ throw new Error(`Foreground run '${control.runId}' is not owned by the current session.`);
251
+ }
252
+ if (options.index !== undefined && (!Number.isInteger(options.index) || options.index < 0)) {
253
+ throw new Error("Transcript index must be a non-negative integer.");
254
+ }
255
+ const children = control.activeChildren
256
+ ? [...control.activeChildren.values()]
257
+ : control.currentAgent ? [{ index: control.currentIndex ?? 0, agent: control.currentAgent, sessionName: control.sessionName }] : [];
258
+ const header = [`Run: ${control.runId}`, "State: live foreground"];
259
+ if (children.length === 0) return `${header.map((line) => safeTerminalText(line)).join("\n")}\nTranscript unavailable: no active foreground child.`;
260
+ if (options.index === undefined && children.length > 1) {
261
+ throw new Error(`Transcript view requires index for foreground run '${control.runId}'. Active child indexes: ${children.map((child) => child.index).join(", ")}.`);
262
+ }
263
+ const child = options.index === undefined ? children[0]! : children.find((child) => child.index === options.index);
264
+ if (!child) throw new Error(`Transcript index ${options.index} is not an active foreground child of '${control.runId}'.`);
265
+ const root = getArtifactsDir(state.parentSessionFile ?? null, control.cwd ?? state.baseCwd, state.artifactDirPreference);
266
+ const transcriptPath = getArtifactPaths(root, control.runId, child.agent, child.index).transcriptPath;
267
+ const transcript = readFleetTranscript(transcriptPath, { trustedRoots: [root] });
268
+ const lineLimit = Number.isFinite(options.lines) ? Math.max(1, Math.min(500, Math.trunc(options.lines!))) : 80;
269
+ const allLines = transcript.events.flatMap((event) => {
270
+ const text = event.kind === "tool"
271
+ ? [`Tool: ${event.name} (${event.status})`, event.argsPayload ?? event.args, event.output ?? event.error].filter(Boolean).join("\n")
272
+ : event.kind === "notice" ? event.text : `${event.kind === "assistant" ? "Assistant" : "Supervisor"}: ${event.text}`;
273
+ return text.split(/\r?\n/);
274
+ });
275
+ let body = allLines.slice(-lineLimit).join("\n");
276
+ let truncated = transcript.truncated || allLines.length > lineLimit
277
+ || transcript.events.some((event) => event.kind === "tool" && event.outputTruncated)
278
+ || body.includes("… message truncated");
279
+ const maxOutputBytes = 32 * 1024;
280
+ const bytes = Buffer.from(body);
281
+ if (bytes.length > maxOutputBytes) {
282
+ // Skip UTF-8 continuation bytes at the beginning of the bounded tail.
283
+ let start = bytes.length - maxOutputBytes;
284
+ while ((bytes[start]! & 0xc0) === 0x80) start++;
285
+ body = bytes.subarray(start).toString("utf-8");
286
+ truncated = true;
287
+ }
288
+ header.push(`Child: ${child.index} (${child.sessionName?.trim() || child.agent})`, `Transcript: ${transcriptPath}`);
289
+ if (transcript.warning) header.push(`Transcript warning: ${transcript.warning}`);
290
+ header.push(`Live transcript tail${truncated ? " (tail truncated)" : ""}:`);
291
+ if (!body) header.push("Transcript unavailable: no readable activity in the bounded artifact tail yet.");
292
+ return [...header.map((line) => safeTerminalText(line)), body].filter(Boolean).join("\n");
293
+ }
294
+
244
295
  function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options: { index?: number; lines?: number }): string {
245
296
  let index = options.index;
246
297
  if (index !== undefined && !Number.isInteger(index)) throw new Error("Transcript index must be an integer.");
@@ -313,6 +364,12 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
313
364
  return inspectSubagentFleet(params, { asyncDirRoot, resultsDir, kill: deps.kill, now: deps.now, state: deps.state, childSafe: Boolean(deps.nested) });
314
365
  }
315
366
  if (!params.id && !params.runId && !params.dir) {
367
+ if (params.view === "transcript" && deps.state?.currentSessionId) {
368
+ const controls = [...deps.state.foregroundControls.values()].filter((control) => control.sessionId === currentSessionId);
369
+ const foreground = controls.find((control) => control.runId === deps.state?.lastForegroundControlId)
370
+ ?? controls.sort((left, right) => right.updatedAt - left.updatedAt)[0];
371
+ if (foreground) return inspectSubagentStatus({ ...params, id: foreground.runId }, deps);
372
+ }
316
373
  if (deps.nested) {
317
374
  return {
318
375
  content: [{ type: "text", text: "Child-safe subagent status requires an id when no foreground run is active." }],
@@ -353,6 +410,13 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
353
410
  } else if (!params.dir && requestedId) {
354
411
  const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
355
412
  if (resolved?.kind === "foreground") {
413
+ const control = deps.state?.foregroundControls.get(resolved.id);
414
+ if (control && deps.state && params.view === "transcript") {
415
+ return {
416
+ content: [{ type: "text", text: formatLiveForegroundTranscript(control, deps.state, params) }],
417
+ details: { mode: "management", results: [] },
418
+ };
419
+ }
356
420
  const run = deps.state?.foregroundRuns?.get(resolved.id);
357
421
  if (run) {
358
422
  try {
@@ -423,7 +487,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
423
487
  const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: deps.abandonedSlotReleaseAfterMs });
424
488
  return {
425
489
  content: [{ type: "text", text: formatRunLifecycleDebug({ status: diskStatus, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
426
- details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
490
+ details: { mode: "single", results: [], ...(diskStatus.workflowReceiptPath ? { workflowReceiptPath: diskStatus.workflowReceiptPath } : {}), ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
427
491
  };
428
492
  }
429
493
  if (params.view === "transcript") {
@@ -450,7 +514,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
450
514
  const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: deps.abandonedSlotReleaseAfterMs });
451
515
  return {
452
516
  content: [{ type: "text", text: formatRunLifecycleDebug({ status, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
453
- details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
517
+ details: { mode: "single", results: [], ...(status.workflowReceiptPath ? { workflowReceiptPath: status.workflowReceiptPath } : {}), ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
454
518
  };
455
519
  }
456
520
  if (params.view === "transcript") {
@@ -626,6 +690,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
626
690
  }
627
691
  }
628
692
  if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
693
+ if (status.workflowReceiptPath) lines.push(`Workflow receipt: ${status.workflowReceiptPath}`);
629
694
  if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
630
695
  const allExternal = (status.steps?.length ?? 0) > 0 && status.steps!.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job");
631
696
  if (status.state === "running" && !allExternal && status.mode !== "workflow") lines.push(`Steer running child: subagent({ action: "steer", id: "${status.runId}", message: "..." })`);
@@ -639,7 +704,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
639
704
 
640
705
  const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
641
706
  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 } } : {}) } };
707
+ 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
708
  }
644
709
  }
645
710
 
@@ -680,6 +745,11 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
680
745
  const runId = data.runId ?? data.id ?? resolvedId;
681
746
  const lines = [`Run: ${runId}`, data.toolCallId ? `Tool call: ${data.toolCallId}` : undefined, `State: ${status}`, `Result: ${resultPath}`].filter((line): line is string => Boolean(line));
682
747
  if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
748
+ const receipt = (data as Record<string, unknown>).workflowReceipt;
749
+ const receiptPath = receipt && typeof receipt === "object" && !Array.isArray(receipt)
750
+ ? (receipt as Record<string, unknown>).path : undefined;
751
+ const workflowReceiptPath = typeof receiptPath === "string" && receiptPath ? receiptPath : undefined;
752
+ if (workflowReceiptPath) lines.push(`Workflow receipt: ${workflowReceiptPath}`);
683
753
  const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
684
754
  lines.push(...formatTimeoutRecoveryLines(data.timeoutRecovery, " "));
685
755
  for (const [index, child] of children.entries()) {
@@ -694,7 +764,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
694
764
  if (data.summary) lines.push("", data.summary);
695
765
  const workflowChildren = parseWorkflowChildSummary((data as unknown as Record<string, unknown>).workflowChildren);
696
766
  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 } : {}) } };
767
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowReceiptPath ? { workflowReceiptPath } : {}), ...(workflowChildren ? { workflowChildren } : {}) } };
698
768
  } catch (error) {
699
769
  const message = error instanceof Error ? error.message : String(error);
700
770
  return {
@@ -23,9 +23,6 @@ export const HOST_PEER_ALIASES: ReadonlyArray<{ specifier: string; pkg: string;
23
23
  { specifier: "@earendil-works/pi-agent-core/node", pkg: "@earendil-works/pi-agent-core", subpath: "./node" },
24
24
  { specifier: "@earendil-works/chord", pkg: "@earendil-works/chord", subpath: "." },
25
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
26
  { specifier: "@earendil-works/pi-tui", pkg: "@earendil-works/pi-tui", subpath: "." },
30
27
  { specifier: "@earendil-works/pi-ai", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
31
28
  { specifier: "@earendil-works/pi-ai/compat", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
@@ -36,6 +33,13 @@ export const HOST_PEER_ALIASES: ReadonlyArray<{ specifier: string; pkg: string;
36
33
  { specifier: "typebox/value", pkg: "typebox", subpath: "./value" },
37
34
  ];
38
35
 
36
+ /** Experimental dependencies accidentally published in exactly Pi 0.85.0. */
37
+ const PI0850_PEER_ALIASES = [
38
+ { specifier: "@earendil-works/pi-server", pkg: "@earendil-works/pi-server", subpath: "." },
39
+ { specifier: "@earendil-works/pi-server/unix", pkg: "@earendil-works/pi-server", subpath: "./unix" },
40
+ { specifier: "@earendil-works/pi-client/unix", pkg: "@earendil-works/pi-client", subpath: "./unix" },
41
+ ];
42
+
39
43
  interface PackageManifest {
40
44
  name?: unknown;
41
45
  version?: unknown;
@@ -103,7 +107,11 @@ export function resolvePackageSubpath(packageDir: string, subpath: string): stri
103
107
 
104
108
  /** Find `pkg` as the pi package itself, one of its dependencies, or a sibling in a hoisted install. */
105
109
  export function findHostPeerPackageDir(piPackageRoot: string, pkg: string): string | undefined {
106
- if (readManifest(piPackageRoot)?.name === pkg) return piPackageRoot;
110
+ return findPeerPackageDir(piPackageRoot, pkg, readManifest(piPackageRoot)?.name);
111
+ }
112
+
113
+ function findPeerPackageDir(piPackageRoot: string, pkg: string, hostName: unknown): string | undefined {
114
+ if (hostName === pkg) return piPackageRoot;
107
115
  const candidates = [path.join(piPackageRoot, "node_modules", pkg)];
108
116
  let dir = piPackageRoot;
109
117
  for (;;) {
@@ -127,14 +135,17 @@ export function resolveHostPeerAliases(
127
135
  const aliases: Record<string, string> = {};
128
136
  const missing: string[] = [];
129
137
  const supplemental: string[] = [];
130
- for (const { specifier, pkg, subpath } of HOST_PEER_ALIASES) {
131
- const packageDir = findHostPeerPackageDir(piPackageRoot, pkg);
138
+ const hostManifest = readManifest(piPackageRoot);
139
+ const isPi0850 = hostManifest?.version === "0.85.0";
140
+ const required = isPi0850 ? [...HOST_PEER_ALIASES, ...PI0850_PEER_ALIASES] : HOST_PEER_ALIASES;
141
+ for (const { specifier, pkg, subpath } of required) {
142
+ const packageDir = findPeerPackageDir(piPackageRoot, pkg, hostManifest?.name);
132
143
  let target = packageDir ? resolvePackageSubpath(packageDir, subpath) : undefined;
133
144
  // Pi 0.85.0 omitted this runtime dependency. Never replace working host
134
145
  // exports or extend this exact version contract to other peers/hosts.
135
146
  if ((!target || !fs.existsSync(target))
136
147
  && (specifier === "@earendil-works/pi-server" || specifier === "@earendil-works/pi-server/unix")
137
- && readManifest(piPackageRoot)?.version === "0.85.0"
148
+ && isPi0850
138
149
  && (!packageDir || readManifest(packageDir)?.version === "0.85.0")) {
139
150
  const localDir = findHostPeerPackageDir(extensionRoot, pkg);
140
151
  if (localDir && readManifest(localDir)?.version === "0.85.0") {
@@ -0,0 +1,86 @@
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
+ }
22
+
23
+ export function buildRunnerChildLaunch(step: RunnerSubagentStep, ctx: RunnerChildLaunchContext, attempt: {
24
+ model?: string;
25
+ sessionEnabled: boolean;
26
+ sessionDir?: string;
27
+ sessionName?: string;
28
+ structuredOutput?: BuildInProcessChildLaunchInput["structuredOutput"];
29
+ childWatchdog?: BuildInProcessChildLaunchInput["childWatchdog"];
30
+ watchdogStatus: NonNullable<BuildInProcessChildLaunchInput["watchdogStatus"]>;
31
+ }) {
32
+ // Resource-backed system instructions survive SDK split-turn compaction verbatim.
33
+ const acceptancePrompt = step.effectiveAcceptance
34
+ ? formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContract(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) })
35
+ : "";
36
+ return buildInProcessChildLaunch({
37
+ parentSessionId: step.parentSessionId,
38
+ forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
39
+ sessionEnabled: attempt.sessionEnabled,
40
+ sessionDir: attempt.sessionDir,
41
+ sessionFile: step.sessionFile,
42
+ model: attempt.model,
43
+ inheritProjectContext: step.inheritProjectContext,
44
+ inheritGlobalContext: step.inheritGlobalContext,
45
+ inheritSkills: step.inheritSkills,
46
+ requireReadTool: Boolean(step.skills?.length),
47
+ tools: step.tools,
48
+ excludeTools: step.excludeTools,
49
+ allowNestedSubagents: step.allowNestedSubagents,
50
+ extensions: step.extensions,
51
+ subagentOnlyExtensions: step.subagentOnlyExtensions,
52
+ fast: step.fast,
53
+ modelCandidates: step.modelCandidates,
54
+ systemPrompt: acceptancePrompt ? `${step.systemPrompt ?? ""}\n${acceptancePrompt}` : step.systemPrompt ?? "",
55
+ systemPromptMode: step.systemPromptMode,
56
+ mcpDirectTools: step.mcpDirectTools,
57
+ extensionBindings: normalizeExtensionBindings(step.extensionBindings)?.value,
58
+ capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
59
+ cwd: step.cwd ?? ctx.cwd,
60
+ intercomSessionName: ctx.childIntercomTarget,
61
+ sessionName: attempt.sessionName,
62
+ orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
63
+ runId: ctx.id,
64
+ childAgentName: step.agent,
65
+ childIndex: ctx.flatIndex,
66
+ nestedRoute: ctx.nestedRoute,
67
+ runFanoutBudget: ctx.runFanoutBudget ? {
68
+ ...ctx.runFanoutBudget,
69
+ ...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
70
+ } : undefined,
71
+ structuredOutput: attempt.structuredOutput,
72
+ toolBudget: step.toolBudget,
73
+ permissionRules: step.permissionRules,
74
+ permissionAuditPath: step.permissionRules && ctx.artifactsDir
75
+ ? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`) : undefined,
76
+ childWatchdog: attempt.childWatchdog,
77
+ // registerChildWatchdog returns before reading the sink when unconfigured.
78
+ ...(attempt.childWatchdog ? { watchdogStatus: attempt.watchdogStatus } : {}),
79
+ waitToolEnabled: step.waitToolEnabled,
80
+ waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
81
+ thinkingCeiling: step.thinkingCeiling,
82
+ maxSubagentDepth: step.maxSubagentDepth,
83
+ inherited: ctx.inheritedChildRuntime,
84
+ host: "runner",
85
+ });
86
+ }
@@ -9,6 +9,7 @@ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
9
9
  import { normalizeParallelGroups } from "./parallel-groups.ts";
10
10
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent, type NestedRoute } from "../shared/nested-events.ts";
11
11
  import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
12
+ import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
12
13
 
13
14
  export type PidLiveness = "alive" | "dead" | "unknown";
14
15
 
@@ -352,9 +353,10 @@ export function checkPidLiveness(pid: number, kill: KillFn = process.kill): PidL
352
353
  }
353
354
  }
354
355
 
355
- export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOptions = {}): ReconcileAsyncRunResult {
356
+ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOptions = {}, observeStatus?: RawDrainStatusObserver): ReconcileAsyncRunResult {
356
357
  const now = options.now?.() ?? Date.now();
357
358
  const status = readStatus(asyncDir);
359
+ observeStatus?.(status);
358
360
  const startedStatus = !status && options.startedRun ? buildStartedStatus(asyncDir, options.startedRun, now) : undefined;
359
361
  const effectiveStatus = status ?? startedStatus;
360
362
  if (!effectiveStatus) return { status: null, repaired: false };