pi-subagents 0.35.1 → 0.37.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 (89) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +224 -31
  3. package/agents/advisor.md +73 -0
  4. package/agents/delegate.md +2 -0
  5. package/agents/worker.md +2 -0
  6. package/package.json +11 -13
  7. package/skills/pi-subagents/SKILL.md +42 -13
  8. package/src/agents/agent-management.ts +7 -1
  9. package/src/agents/agents.ts +121 -17
  10. package/src/api/capability-ceiling.ts +17 -0
  11. package/src/api/delegation.ts +127 -0
  12. package/src/api/preflight.ts +399 -0
  13. package/src/extension/config.ts +7 -1
  14. package/src/extension/index.ts +52 -38
  15. package/src/extension/rpc.ts +31 -2
  16. package/src/extension/schemas.ts +29 -3
  17. package/src/extension/tool-description.ts +4 -2
  18. package/src/intercom/intercom-bridge.ts +1 -1
  19. package/src/intercom/native-supervisor-channel.ts +45 -6
  20. package/src/intercom/result-intercom.ts +7 -0
  21. package/src/runs/background/async-execution.ts +158 -17
  22. package/src/runs/background/async-job-tracker.ts +4 -0
  23. package/src/runs/background/async-resume.ts +47 -8
  24. package/src/runs/background/async-status.ts +94 -3
  25. package/src/runs/background/chain-append.ts +2 -0
  26. package/src/runs/background/completion-batcher.ts +6 -4
  27. package/src/runs/background/completion-dedupe.ts +2 -11
  28. package/src/runs/background/fleet-view.ts +9 -4
  29. package/src/runs/background/notify.ts +132 -120
  30. package/src/runs/background/process-terminal.ts +280 -0
  31. package/src/runs/background/result-watcher.ts +138 -78
  32. package/src/runs/background/run-status.ts +10 -2
  33. package/src/runs/background/scheduled-runs.ts +6 -1
  34. package/src/runs/background/stale-run-reconciler.ts +6 -0
  35. package/src/runs/background/subagent-runner.ts +406 -54
  36. package/src/runs/background/subagent-wait.ts +130 -4
  37. package/src/runs/background/wait-tool.ts +2 -2
  38. package/src/runs/foreground/chain-execution.ts +181 -111
  39. package/src/runs/foreground/execution.ts +147 -47
  40. package/src/runs/foreground/foreground-control.ts +90 -0
  41. package/src/runs/foreground/subagent-executor.ts +426 -165
  42. package/src/runs/shared/acceptance.ts +94 -41
  43. package/src/runs/shared/agent-contract.ts +38 -0
  44. package/src/runs/shared/capability-ceiling.ts +177 -0
  45. package/src/runs/shared/child-protocol.ts +1 -1
  46. package/src/runs/shared/completion-guard.ts +36 -5
  47. package/src/runs/shared/context-mode.ts +44 -0
  48. package/src/runs/shared/dynamic-fanout.ts +5 -5
  49. package/src/runs/shared/long-running-guard.ts +4 -0
  50. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  51. package/src/runs/shared/nested-events.ts +35 -3
  52. package/src/runs/shared/parallel-handoff.ts +154 -0
  53. package/src/runs/shared/parallel-utils.ts +11 -0
  54. package/src/runs/shared/pi-args.ts +148 -56
  55. package/src/runs/shared/run-history.ts +90 -5
  56. package/src/runs/shared/session-lease.ts +25 -5
  57. package/src/runs/shared/structured-output.ts +112 -7
  58. package/src/runs/shared/subagent-control.ts +4 -0
  59. package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
  60. package/src/runs/shared/task-intent.ts +10 -5
  61. package/src/runs/shared/tool-availability.ts +21 -3
  62. package/src/runs/shared/tool-budget.ts +11 -5
  63. package/src/runs/shared/turn-budget.ts +2 -1
  64. package/src/runs/shared/worktree.ts +63 -14
  65. package/src/shared/accessible-dir.ts +25 -0
  66. package/src/shared/artifacts.ts +37 -7
  67. package/src/shared/atomic-json.ts +14 -42
  68. package/src/shared/child-transcript.ts +52 -0
  69. package/src/shared/file-system-retry.ts +47 -0
  70. package/src/shared/launch-contract.ts +123 -0
  71. package/src/shared/settings.ts +9 -1
  72. package/src/shared/types.ts +314 -28
  73. package/src/shared/utils.ts +17 -42
  74. package/src/slash/delegation-adapters.ts +153 -6
  75. package/src/slash/delegation-json.ts +108 -0
  76. package/src/slash/delegation-request.ts +182 -36
  77. package/src/slash/prompt-template-bridge.ts +222 -37
  78. package/src/slash/selector.ts +147 -0
  79. package/src/slash/slash-commands.ts +15 -6
  80. package/src/slash/slash-live-state.ts +2 -2
  81. package/src/slash/subagents-admin.ts +42 -42
  82. package/src/tui/fleet-status.ts +362 -0
  83. package/src/tui/fleet-transcript.ts +472 -0
  84. package/src/tui/fleet.ts +318 -59
  85. package/src/tui/render.ts +17 -15
  86. package/src/watchdog/change-signature.ts +105 -12
  87. package/src/watchdog/review.ts +7 -2
  88. package/src/watchdog/runtime.ts +5 -3
  89. package/src/slash/subagents-editor.ts +0 -86
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { spawn } from "node:child_process";
6
+ import { randomUUID } from "node:crypto";
6
7
  import * as fs from "node:fs";
7
8
  import * as os from "node:os";
8
9
  import * as path from "node:path";
@@ -11,10 +12,11 @@ import { createRequire } from "node:module";
11
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
13
  import type { AgentConfig } from "../../agents/agents.ts";
13
14
  import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
14
- import { applyThinkingSuffix } from "../shared/pi-args.ts";
15
+ import { applyThinkingSuffix, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
15
16
  import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
16
17
  import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
17
18
  import type { RunnerStep } from "../shared/parallel-utils.ts";
19
+ import type { ContextMode } from "../shared/context-mode.ts";
18
20
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
19
21
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
20
22
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
@@ -29,8 +31,10 @@ import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
29
31
  import { resolveEffectiveAcceptance } from "../shared/acceptance.ts";
30
32
  import {
31
33
  type AcceptanceInput,
34
+ type AgentContract,
32
35
  type ArtifactConfig,
33
36
  type Details,
37
+ type JsonSchemaObject,
34
38
  type MaxOutputConfig,
35
39
  type NestedRouteInfo,
36
40
  type ResolvedControlConfig,
@@ -46,11 +50,15 @@ import {
46
50
  getAsyncConfigPath,
47
51
  resolveChildMaxSubagentDepth,
48
52
  } from "../../shared/types.ts";
49
- import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
50
- import { initialTurnBudgetState } from "../shared/turn-budget.ts";
53
+ import { nestedResultsPath, nestedSummaryFromAsyncStatus, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
54
+ import { appendTurnBudgetSystemPrompt, initialTurnBudgetState } from "../shared/turn-budget.ts";
51
55
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
52
56
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
53
57
  import type { SessionLeaseRequest } from "../shared/session-lease.ts";
58
+ import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
59
+ import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
60
+ import { decodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
61
+ import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
54
62
 
55
63
  const require = createRequire(import.meta.url);
56
64
  const piPackageRoot = resolvePiPackageRoot();
@@ -131,9 +139,11 @@ interface AsyncChainParams {
131
139
  artifactConfig: ArtifactConfig;
132
140
  shareEnabled: boolean;
133
141
  sessionRoot?: string;
142
+ agentContract?: AgentContract;
134
143
  chainSkills?: string[];
135
144
  sessionFilesByFlatIndex?: (string | undefined)[];
136
145
  thinkingOverridesByFlatIndex?: (AgentConfig["thinking"] | undefined)[];
146
+ contextForAgent?: (agentName: string) => ContextMode;
137
147
  progressDir?: string;
138
148
  dynamicFanoutMaxItems?: number;
139
149
  maxSubagentDepth: number;
@@ -152,6 +162,7 @@ interface AsyncChainParams {
152
162
  configToolBudget?: ResolvedToolBudget;
153
163
  /** Global cap on simultaneously-running subagent tasks within the async run. */
154
164
  globalConcurrencyLimit?: number;
165
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
155
166
  }
156
167
 
157
168
  interface AsyncSingleParams {
@@ -170,10 +181,13 @@ interface AsyncSingleParams {
170
181
  sessionDir?: string;
171
182
  sessionFile?: string;
172
183
  revivalLease?: SessionLeaseRequest;
184
+ context?: ContextMode;
173
185
  skills?: string[];
174
186
  output?: string | boolean;
175
187
  outputMode?: "inline" | "file-only";
176
188
  outputBaseDir?: string;
189
+ agentContract?: AgentContract;
190
+ structuredOutputSchema?: JsonSchemaObject;
177
191
  modelOverride?: string;
178
192
  thinkingOverride?: AgentConfig["thinking"];
179
193
  availableModels?: AvailableModelInfo[];
@@ -192,6 +206,7 @@ interface AsyncSingleParams {
192
206
  turnBudget?: ResolvedTurnBudget;
193
207
  toolBudget?: ResolvedToolBudget;
194
208
  configToolBudget?: ResolvedToolBudget;
209
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
195
210
  }
196
211
 
197
212
  interface AsyncExecutionResult {
@@ -212,7 +227,9 @@ export interface AsyncRunnerStepBuildParams {
212
227
  chainSkills?: string[];
213
228
  sessionFilesByFlatIndex?: (string | undefined)[];
214
229
  thinkingOverridesByFlatIndex?: (AgentConfig["thinking"] | undefined)[];
230
+ contextForAgent?: (agentName: string) => ContextMode;
215
231
  progressDir?: string;
232
+ agentContract?: AgentContract;
216
233
  dynamicFanoutMaxItems?: number;
217
234
  maxSubagentDepth: number;
218
235
  waitToolEnabled?: boolean;
@@ -222,6 +239,7 @@ export interface AsyncRunnerStepBuildParams {
222
239
  validateOutputBindings?: boolean;
223
240
  toolBudget?: ResolvedToolBudget;
224
241
  configToolBudget?: ResolvedToolBudget;
242
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
225
243
  }
226
244
 
227
245
  export type AsyncRunnerStepBuildResult =
@@ -387,7 +405,7 @@ function terminateRunnerBeforeProceed(pid: number): void {
387
405
  }
388
406
  }
389
407
 
390
- function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number; error?: string } {
408
+ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal?: (proof: unknown) => void): { pid?: number; error?: string } {
391
409
  if (!jitiCliPath) {
392
410
  return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
393
411
  }
@@ -403,12 +421,14 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
403
421
 
404
422
  fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
405
423
  const cfgPath = getAsyncConfigPath(suffix);
406
- fs.writeFileSync(cfgPath, JSON.stringify(cfg));
424
+ const runnerProcessInstanceId = randomUUID();
425
+ const launchConfig = { ...cfg, runnerProcessInstanceId };
426
+ fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
407
427
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
408
428
  const nodeCommand = resolveAsyncRunnerNodeCommand();
409
- const startupPath = typeof (cfg as { revivalLease?: unknown; asyncDir?: unknown }).revivalLease === "object"
410
- && typeof (cfg as { asyncDir?: unknown }).asyncDir === "string"
411
- ? path.join((cfg as { asyncDir: string }).asyncDir, "runner-startup.json")
429
+ const startupPath = typeof (launchConfig as { revivalLease?: unknown; asyncDir?: unknown }).revivalLease === "object"
430
+ && typeof (launchConfig as { asyncDir?: unknown }).asyncDir === "string"
431
+ ? path.join((launchConfig as { asyncDir: string }).asyncDir, "runner-startup.json")
412
432
  : undefined;
413
433
  const startupAckPath = startupPath ? path.join(path.dirname(startupPath), "runner-startup-ack.json") : undefined;
414
434
  const startupProceedPath = startupPath ? path.join(path.dirname(startupPath), "runner-startup-proceed.json") : undefined;
@@ -416,7 +436,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
416
436
  if (startupAckPath) fs.rmSync(startupAckPath, { force: true });
417
437
  if (startupProceedPath) fs.rmSync(startupProceedPath, { force: true });
418
438
 
419
- const logPaths = resolveAsyncRunnerLogPaths(cfg);
439
+ const logPaths = resolveAsyncRunnerLogPaths(launchConfig);
420
440
  let stdoutFd: number | undefined;
421
441
  let stderrFd: number | undefined;
422
442
  try {
@@ -440,6 +460,56 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
440
460
  proc.on("error", (error) => {
441
461
  console.error(`[pi-subagents] async spawn failed: ${error.message}`);
442
462
  });
463
+ proc.once("close", (exitCode, signal) => {
464
+ const launch = launchConfig as { asyncDir?: unknown; id?: unknown; nestedRoute?: NestedRouteInfo; nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> } };
465
+ const asyncDir = launch.asyncDir;
466
+ const runId = launch.id;
467
+ if (typeof asyncDir !== "string" || typeof runId !== "string") return;
468
+ finalizeProcessTerminal(asyncDir, runId, {
469
+ processInstanceId: runnerProcessInstanceId,
470
+ closeObservedAt: Date.now(),
471
+ exitCode,
472
+ signal,
473
+ });
474
+ const persisted = readProcessTerminal(asyncDir, { runId, runnerProcessInstanceId });
475
+ if (!persisted) return;
476
+ if (launch.nestedRoute && launch.nestedSelf) {
477
+ try {
478
+ let status: import("../../shared/types.ts").AsyncStatus;
479
+ try {
480
+ status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as import("../../shared/types.ts").AsyncStatus;
481
+ status.processTerminal = persisted;
482
+ } catch {
483
+ status = {
484
+ runId,
485
+ mode: "single",
486
+ state: persisted.state === "observed" ? "complete" : "failed",
487
+ startedAt: persisted.observedAt ?? Date.now(),
488
+ lastUpdate: Date.now(),
489
+ processTerminal: persisted,
490
+ };
491
+ }
492
+ writeNestedEvent(launch.nestedRoute, {
493
+ type: "subagent.nested.completed",
494
+ ts: Date.now(),
495
+ parentRunId: launch.nestedSelf.parentRunId,
496
+ parentStepIndex: launch.nestedSelf.parentStepIndex,
497
+ child: nestedSummaryFromAsyncStatus(status, asyncDir, {
498
+ id: runId,
499
+ parentRunId: launch.nestedSelf.parentRunId,
500
+ parentStepIndex: launch.nestedSelf.parentStepIndex,
501
+ depth: launch.nestedSelf.depth,
502
+ path: launch.nestedSelf.path,
503
+ mode: status.mode,
504
+ ts: Date.now(),
505
+ }),
506
+ });
507
+ } catch (error) {
508
+ console.error("Failed to emit final nested process-terminal status:", error);
509
+ }
510
+ }
511
+ onProcessTerminal?.(persisted);
512
+ });
443
513
  if (typeof proc.pid !== "number") {
444
514
  return { error: `async runner did not produce a pid for cwd: ${cwd}` };
445
515
  }
@@ -624,10 +694,14 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
624
694
  const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
625
695
  const effectiveThinking = thinkingOverride ?? a.thinking;
626
696
  const model = applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
697
+ const agentContract = s.agentContract ?? params.agentContract;
627
698
  return {
628
699
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
700
+ ...(params.capabilityCeiling ? { capabilityCeiling: params.capabilityCeiling } : {}),
629
701
  agent: s.agent,
630
702
  task,
703
+ ...(params.contextForAgent ? { context: params.contextForAgent(s.agent) } : {}),
704
+ ...(agentContract ? { agentContract } : {}),
631
705
  phase: s.phase,
632
706
  label: s.label,
633
707
  outputName: s.as,
@@ -662,9 +736,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
662
736
  mode: resultMode,
663
737
  async: true,
664
738
  dynamic: false,
739
+ agentContract,
665
740
  }),
666
741
  acceptanceInput: s.acceptance,
667
742
  acceptanceRole: a.acceptanceRole,
743
+ ...(s.gateOn ? { gateOn: s.gateOn } : {}),
668
744
  ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
669
745
  ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
670
746
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
@@ -707,7 +783,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
707
783
  }
708
784
  }
709
785
  const staticStep = nextFlatStep();
710
- return buildSeqStep(t, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex });
786
+ return buildSeqStep({ ...t, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex });
711
787
  }),
712
788
  concurrency: s.concurrency,
713
789
  failFast: s.failFast,
@@ -724,7 +800,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
724
800
  }
725
801
  const maxItems = s.expand.maxItems ?? params.dynamicFanoutMaxItems ?? 0;
726
802
  const dynamicFlatSteps = Array.from({ length: maxItems }, () => nextFlatStep());
727
- const parallel = buildSeqStep(s.parallel as SequentialStep, undefined, undefined, progressPrecreated, behavior, undefined, { stepIndex });
803
+ const parallel = buildSeqStep({ ...(s.parallel as SequentialStep), agentContract: s.parallel.agentContract ?? s.agentContract, gateOn: s.parallel.gateOn ?? s.gateOn }, undefined, undefined, progressPrecreated, behavior, undefined, { stepIndex });
728
804
  return {
729
805
  expand: s.expand,
730
806
  parallel,
@@ -743,9 +819,12 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
743
819
  mode: resultMode,
744
820
  async: true,
745
821
  dynamicGroup: true,
822
+ agentContract: s.agentContract ?? params.agentContract,
746
823
  }),
747
824
  acceptanceInput: s.acceptance,
748
825
  acceptanceRole: agent.acceptanceRole,
826
+ ...(s.agentContract ?? params.agentContract ? { agentContract: s.agentContract ?? params.agentContract } : {}),
827
+ ...(s.gateOn ? { gateOn: s.gateOn } : {}),
749
828
  };
750
829
  }
751
830
  const staticStep = nextFlatStep();
@@ -816,6 +895,7 @@ export function executeAsyncChain(
816
895
  nestedRoute,
817
896
  } = params;
818
897
  const resultMode = params.resultMode ?? "chain";
898
+ const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
819
899
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
820
900
  const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
821
901
  const asyncDir = inheritedNestedRoute
@@ -844,7 +924,9 @@ export function executeAsyncChain(
844
924
  chainSkills: params.chainSkills,
845
925
  sessionFilesByFlatIndex,
846
926
  thinkingOverridesByFlatIndex,
927
+ contextForAgent: params.contextForAgent,
847
928
  progressDir: params.progressDir ?? (artifactsDir ? path.join(artifactsDir, "progress", id) : resultMode === "parallel" ? path.join(asyncDir, "progress") : undefined),
929
+ agentContract: params.agentContract,
848
930
  outputBaseDir: artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined,
849
931
  dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
850
932
  maxSubagentDepth,
@@ -853,6 +935,7 @@ export function executeAsyncChain(
853
935
  asyncDir,
854
936
  toolBudget: params.toolBudget,
855
937
  configToolBudget: params.configToolBudget,
938
+ capabilityCeiling,
856
939
  });
857
940
  if ("error" in built) {
858
941
  try {
@@ -897,6 +980,7 @@ export function executeAsyncChain(
897
980
  sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
898
981
  asyncDir,
899
982
  sessionId: ctx.currentSessionId,
983
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
900
984
  piPackageRoot,
901
985
  piArgv1: process.argv[1],
902
986
  worktreeSetupHook,
@@ -923,6 +1007,7 @@ export function executeAsyncChain(
923
1007
  },
924
1008
  id,
925
1009
  runnerCwd,
1010
+ (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
926
1011
  );
927
1012
  } catch (error) {
928
1013
  const message = error instanceof Error ? error.message : String(error);
@@ -994,6 +1079,7 @@ export function executeAsyncChain(
994
1079
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
995
1080
  startedAt: now,
996
1081
  lastUpdate: now,
1082
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
997
1083
  },
998
1084
  });
999
1085
  } catch (error) {
@@ -1020,6 +1106,7 @@ export function executeAsyncChain(
1020
1106
  asyncDir,
1021
1107
  ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
1022
1108
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1109
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1023
1110
  nestedRoute,
1024
1111
  });
1025
1112
  }
@@ -1032,7 +1119,7 @@ export function executeAsyncChain(
1032
1119
 
1033
1120
  return {
1034
1121
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`, ctx.interactive === true) }],
1035
- details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1122
+ details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1036
1123
  };
1037
1124
  }
1038
1125
 
@@ -1064,6 +1151,7 @@ export function executeAsyncSingle(
1064
1151
  nestedRoute,
1065
1152
  } = params;
1066
1153
  const task = params.task ?? "";
1154
+ const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
1067
1155
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
1068
1156
  const skillNames = params.skills ?? agentConfig.skills ?? [];
1069
1157
  const availableModels = params.availableModels;
@@ -1126,6 +1214,42 @@ export function executeAsyncSingle(
1126
1214
  if (timeoutMs !== undefined && timeoutMs <= 0) return formatAsyncStartError("single", "The source run's absolute deadline expired before recovery could launch.");
1127
1215
  const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
1128
1216
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1217
+ const structuredOutput = params.structuredOutputSchema
1218
+ ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
1219
+ : undefined;
1220
+ const modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
1221
+ applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined),
1222
+ );
1223
+ const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1224
+ const toolPlan = resolvePiLaunchToolPlan({
1225
+ tools: agentConfig.tools,
1226
+ extensions: agentConfig.extensions,
1227
+ subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
1228
+ mcpDirectTools: agentConfig.mcpDirectTools,
1229
+ cwd: runnerCwd,
1230
+ requireReadTool: Boolean(resolvedSkills.length),
1231
+ structuredOutput: Boolean(params.structuredOutputSchema),
1232
+ capabilityCeiling,
1233
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1234
+ });
1235
+ const launchContractDigest = launchBindingDigest({
1236
+ definitionDigest: agentDefinitionDigest(agentConfig),
1237
+ task,
1238
+ ...(model ? { model } : {}),
1239
+ modelCandidates,
1240
+ ...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1241
+ systemPrompt: effectiveSystemPrompt,
1242
+ systemPromptMode: agentConfig.systemPromptMode,
1243
+ inheritProjectContext: agentConfig.inheritProjectContext,
1244
+ inheritSkills: agentConfig.inheritSkills,
1245
+ skills: resolvedSkills.map((skill) => skill.name),
1246
+ tools: toolPlan.effectiveToolAllowlist,
1247
+ extensions: toolPlan.extensionArgs,
1248
+ mcpDirectTools: toolPlan.effectiveMcpTools,
1249
+ ...(outputPath ? { outputPath } : {}),
1250
+ outputMode,
1251
+ ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1252
+ });
1129
1253
  const resolvedAcceptance = resolveEffectiveAcceptance({
1130
1254
  explicit: params.acceptance,
1131
1255
  agentName: agent,
@@ -1133,10 +1257,13 @@ export function executeAsyncSingle(
1133
1257
  task,
1134
1258
  mode: "single",
1135
1259
  async: true,
1260
+ agentContract: params.agentContract,
1136
1261
  });
1137
1262
  const recoveryDescriptor: SteeringRecoveryDescriptor = {
1138
1263
  version: 1,
1264
+ launchContractDigest,
1139
1265
  sourceRunId: id,
1266
+ ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1140
1267
  agent,
1141
1268
  ...(sessionFile ? { sessionFile } : {}),
1142
1269
  cwd: runnerCwd,
@@ -1158,7 +1285,8 @@ export function executeAsyncSingle(
1158
1285
  ...(agentConfig.memory ? { memory: { ...agentConfig.memory } } : {}),
1159
1286
  ...(outputPath ? { outputPath } : {}),
1160
1287
  outputMode,
1161
- ...(resolvedAcceptance ? { acceptance: resolvedAcceptance } : {}),
1288
+ ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1289
+ ...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
1162
1290
  ...(controlConfig ? { controlConfig } : {}),
1163
1291
  ...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
1164
1292
  ...(params.turnBudget ? { initialTurnBudget: params.turnBudget } : {}),
@@ -1169,6 +1297,7 @@ export function executeAsyncSingle(
1169
1297
  ...(resolvedSessionDir ? { sessionDir: resolvedSessionDir } : {}),
1170
1298
  ...(artifactsDir ? { artifactsDir } : {}),
1171
1299
  artifactConfig,
1300
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1172
1301
  };
1173
1302
  try {
1174
1303
  writePrivateAtomicJson(path.join(asyncDir, "recovery-descriptor.json"), recoveryDescriptor);
@@ -1183,14 +1312,14 @@ export function executeAsyncSingle(
1183
1312
  steps: [
1184
1313
  {
1185
1314
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
1315
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1186
1316
  agent,
1187
1317
  task: taskWithOutputInstruction,
1318
+ ...(params.context ? { context: params.context } : {}),
1188
1319
  cwd: runnerCwd,
1189
1320
  model,
1190
1321
  thinking: resolveEffectiveThinking(model, effectiveThinking),
1191
- modelCandidates: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
1192
- applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined),
1193
- ),
1322
+ modelCandidates,
1194
1323
  tools: agentConfig.tools,
1195
1324
  extensions: agentConfig.extensions,
1196
1325
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1206,7 +1335,13 @@ export function executeAsyncSingle(
1206
1335
  sessionFile,
1207
1336
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
1208
1337
  waitToolEnabled: params.waitToolEnabled,
1338
+ ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1339
+ definitionDigest: agentDefinitionDigest(agentConfig),
1340
+ launchBindingTask: task,
1341
+ launchContractDigest,
1209
1342
  effectiveAcceptance: resolvedAcceptance,
1343
+ ...(structuredOutput ? { structuredOutput } : {}),
1344
+ ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1210
1345
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1211
1346
  },
1212
1347
  ],
@@ -1220,6 +1355,7 @@ export function executeAsyncSingle(
1220
1355
  sessionDir: resolvedSessionDir,
1221
1356
  asyncDir,
1222
1357
  sessionId: ctx.currentSessionId,
1358
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1223
1359
  piPackageRoot,
1224
1360
  piArgv1: process.argv[1],
1225
1361
  worktreeSetupHook,
@@ -1233,6 +1369,7 @@ export function executeAsyncSingle(
1233
1369
  controlIntercomTarget,
1234
1370
  childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
1235
1371
  resultMode: "single",
1372
+ launchContractDigest,
1236
1373
  ...(params.revivalLease ? { revivalLease: params.revivalLease } : {}),
1237
1374
  nestedRoute: nestedRoute ?? inheritedNestedRoute,
1238
1375
  nestedSelf: inheritedNestedRoute && nestedAddress ? {
@@ -1244,6 +1381,7 @@ export function executeAsyncSingle(
1244
1381
  },
1245
1382
  id,
1246
1383
  runnerCwd,
1384
+ (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
1247
1385
  );
1248
1386
  } catch (error) {
1249
1387
  const message = error instanceof Error ? error.message : String(error);
@@ -1284,6 +1422,7 @@ export function executeAsyncSingle(
1284
1422
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1285
1423
  startedAt: now,
1286
1424
  lastUpdate: now,
1425
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1287
1426
  },
1288
1427
  });
1289
1428
  } catch (error) {
@@ -1301,14 +1440,16 @@ export function executeAsyncSingle(
1301
1440
  goal: (params.goal ?? task).slice(0, 120),
1302
1441
  cwd: runnerCwd,
1303
1442
  asyncDir,
1443
+ launchContractDigest,
1304
1444
  ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}),
1305
1445
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1446
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1306
1447
  nestedRoute,
1307
1448
  });
1308
1449
  }
1309
1450
 
1310
1451
  return {
1311
1452
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`, ctx.interactive === true) }],
1312
- details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1453
+ details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, launchContractDigest, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1313
1454
  };
1314
1455
  }
@@ -91,6 +91,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
91
91
  toolCount: run.toolCount,
92
92
  steering: run.steering,
93
93
  mode: run.mode,
94
+ context: run.context,
95
+ cwd: run.cwd,
94
96
  agents: visibleSteps.map((step) => step.agent),
95
97
  currentStep: run.currentStep,
96
98
  chainStepCount: run.chainStepCount,
@@ -397,10 +399,12 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
397
399
  state.asyncJobs.set(info.id, {
398
400
  asyncId: info.id,
399
401
  asyncDir,
402
+ ...(typeof info.cwd === "string" ? { cwd: path.resolve(info.cwd) } : {}),
400
403
  status: "queued",
401
404
  pid: typeof info.pid === "number" ? info.pid : undefined,
402
405
  ...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
403
406
  mode: info.mode ?? (info.chain ? "chain" : "single"),
407
+ description: info.goal ?? info.task,
404
408
  agents,
405
409
  chainStepCount: info.chainStepCount,
406
410
  parallelGroups: validParallelGroups,