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,11 +3,12 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
- import { type AgentConfig, type AgentScope } from "../../agents/agents.ts";
6
+ import type { AgentConfig, AgentScope } from "../../agents/agents.ts";
7
7
  import { getArtifactsDir, getProjectChainRunsDir } from "../../shared/artifacts.ts";
8
8
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
9
9
  import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
10
10
  import { executeChain } from "./chain-execution.ts";
11
+ import { beginForegroundChild, finishForegroundChild, updateForegroundChild } from "./foreground-control.ts";
11
12
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
12
13
  import { handleManagementAction } from "../../agents/agent-management.ts";
13
14
  import { buildDoctorReport } from "../../extension/doctor.ts";
@@ -46,9 +47,14 @@ import { formatControlIntercomMessage, formatControlNoticeMessage, resolveContro
46
47
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
47
48
  import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
48
49
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
50
+ import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
51
+ import { isAgentContractV1 } from "../shared/agent-contract.ts";
49
52
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
53
+ import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
50
54
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
51
55
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
56
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
57
+ import { summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
52
58
  import {
53
59
  attachNestedChildrenToResultChildren,
54
60
  buildSubagentResultIntercomPayload,
@@ -81,13 +87,16 @@ import {
81
87
  type AgentProgress,
82
88
  type AsyncStatus,
83
89
  type AcceptanceInput,
90
+ type AgentContract,
84
91
  type ArtifactConfig,
85
92
  type ArtifactPaths,
86
93
  type ControlConfig,
87
94
  type ControlEvent,
88
95
  type Details,
89
96
  type ExtensionConfig,
97
+ type ForegroundRunControl,
90
98
  type IntercomEventBus,
99
+ type JsonSchemaObject,
91
100
  type MaxOutputConfig,
92
101
  type NestedRouteInfo,
93
102
  type NestedRunSummary,
@@ -127,7 +136,9 @@ interface TaskParam {
127
136
  progress?: boolean;
128
137
  model?: string;
129
138
  skill?: string | string[] | boolean;
139
+ outputSchema?: JsonSchemaObject;
130
140
  acceptance?: AcceptanceInput;
141
+ agentContract?: AgentContract;
131
142
  toolBudget?: ToolBudgetConfig;
132
143
  }
133
144
 
@@ -142,6 +153,7 @@ export interface SubagentParamsLike {
142
153
  agent?: string;
143
154
  task?: string;
144
155
  message?: string;
156
+ steeringRecovery?: boolean;
145
157
  chain?: ChainStep[];
146
158
  tasks?: TaskParam[];
147
159
  concurrency?: number;
@@ -152,6 +164,8 @@ export interface SubagentParamsLike {
152
164
  timeoutMs?: number;
153
165
  maxRuntimeMs?: number;
154
166
  turnBudget?: TurnBudgetConfig;
167
+ /** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */
168
+ enforceHardTurnLimit?: boolean;
155
169
  toolBudget?: ToolBudgetConfig;
156
170
  clarify?: boolean;
157
171
  share?: boolean;
@@ -168,9 +182,11 @@ export interface SubagentParamsLike {
168
182
  skill?: string | string[] | boolean;
169
183
  output?: string | boolean;
170
184
  outputMode?: "inline" | "file-only";
185
+ outputSchema?: JsonSchemaObject;
171
186
  agentScope?: unknown;
172
187
  chainDir?: string;
173
188
  acceptance?: AcceptanceInput;
189
+ agentContract?: AgentContract;
174
190
  schedule?: string;
175
191
  scheduleName?: string;
176
192
  additional?: number;
@@ -220,10 +236,12 @@ interface ExecutionContextData {
220
236
  deadlineAt?: number;
221
237
  turnBudget?: ResolvedTurnBudget;
222
238
  toolBudget?: ResolvedToolBudget;
239
+ allowZeroToolBudget?: boolean;
223
240
  configToolBudget?: ResolvedToolBudget;
224
241
  contextPolicy: AgentDefaultContextPolicy;
225
242
  modelScope?: ModelScopeConfig;
226
243
  parentSessionId: string | null;
244
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
227
245
  }
228
246
 
229
247
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -351,6 +369,19 @@ function trimRememberedForegroundRuns(state: SubagentState): void {
351
369
  }
352
370
  }
353
371
 
372
+ function foregroundChildActivityFromProgress(progress: SingleResult["progress"] | undefined) {
373
+ return {
374
+ ...(progress?.activityState ? { activityState: progress.activityState } : {}),
375
+ ...(progress?.lastActivityAt !== undefined ? { lastActivityAt: progress.lastActivityAt } : {}),
376
+ ...(progress?.currentTool ? { currentTool: progress.currentTool } : {}),
377
+ ...(progress?.currentToolStartedAt !== undefined ? { currentToolStartedAt: progress.currentToolStartedAt } : {}),
378
+ ...(progress?.currentPath ? { currentPath: progress.currentPath } : {}),
379
+ ...(progress?.turnCount !== undefined ? { turnCount: progress.turnCount } : {}),
380
+ ...(progress?.tokens !== undefined ? { tokens: progress.tokens } : {}),
381
+ ...(progress?.toolCount !== undefined ? { toolCount: progress.toolCount } : {}),
382
+ };
383
+ }
384
+
354
385
  function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[] }): void {
355
386
  state.foregroundRuns ??= new Map();
356
387
  const previous = state.foregroundRuns.get(input.runId);
@@ -365,7 +396,9 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
365
396
  const child = {
366
397
  agent: result.agent,
367
398
  index,
399
+ ...(result.context ? { context: result.context } : {}),
368
400
  status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
401
+ ...foregroundChildActivityFromProgress(result.progress),
369
402
  updatedAt,
370
403
  ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
371
404
  ...(result.error ? { error: result.error } : {}),
@@ -379,6 +412,8 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
379
412
  ...(result.transcriptError ? { transcriptError: result.transcriptError } : {}),
380
413
  ...(result.detachedReason ? { detachedReason: result.detachedReason } : {}),
381
414
  ...(result.acceptance ? { acceptance: result.acceptance } : {}),
415
+ ...(result.capabilityCeiling ? { capabilityCeiling: result.capabilityCeiling } : {}),
416
+ ...(result.capabilityAudit ? { capabilityAudit: result.capabilityAudit } : {}),
382
417
  };
383
418
  const recovered = previous?.children[index];
384
419
  return child.status === "detached" && recovered && recovered.status !== "detached" ? recovered : child;
@@ -387,6 +422,29 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
387
422
  trimRememberedForegroundRuns(state);
388
423
  }
389
424
 
425
+ function applyControlEventToRememberedForegroundRun(state: SubagentState, event: ControlEvent): void {
426
+ const run = state.foregroundRuns?.get(event.runId);
427
+ if (!run) return;
428
+ const index = event.index ?? (run.children.length === 1 ? run.children[0]?.index : undefined);
429
+ if (index === undefined) return;
430
+ const child = run.children[index];
431
+ if (!child || child.status !== "detached") return;
432
+ const updatedAt = event.ts;
433
+ run.updatedAt = updatedAt;
434
+ run.children[index] = {
435
+ ...child,
436
+ activityState: event.to,
437
+ updatedAt,
438
+ ...(event.elapsedMs !== undefined ? { lastActivityAt: event.ts - event.elapsedMs } : {}),
439
+ ...(event.currentTool ? { currentTool: event.currentTool } : {}),
440
+ ...(event.currentToolDurationMs !== undefined ? { currentToolStartedAt: event.ts - event.currentToolDurationMs } : {}),
441
+ ...(event.currentPath ? { currentPath: event.currentPath } : {}),
442
+ ...(event.turns !== undefined ? { turnCount: event.turns } : {}),
443
+ ...(event.tokens !== undefined ? { tokens: event.tokens } : {}),
444
+ ...(event.toolCount !== undefined ? { toolCount: event.toolCount } : {}),
445
+ };
446
+ }
447
+
390
448
  function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; index: number; result: SingleResult; events: IntercomEventBus }): void {
391
449
  state.foregroundRuns ??= new Map();
392
450
  const updatedAt = Date.now();
@@ -401,9 +459,11 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
401
459
  ...child,
402
460
  agent: input.result.agent,
403
461
  index: input.index,
462
+ ...(input.result.context ? { context: input.result.context } : {}),
404
463
  status: input.result.acceptance?.status === "rejected"
405
464
  ? "failed"
406
465
  : resolveSubagentResultStatus({ exitCode: input.result.exitCode, interrupted: input.result.interrupted, detached: false }),
466
+ ...foregroundChildActivityFromProgress(input.result.progress),
407
467
  updatedAt,
408
468
  ...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}),
409
469
  ...(input.result.error ? { error: input.result.error } : {}),
@@ -417,6 +477,8 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
417
477
  ...(input.result.transcriptError ? { transcriptError: input.result.transcriptError } : {}),
418
478
  ...(input.result.detachedReason ? { detachedReason: input.result.detachedReason } : {}),
419
479
  ...(input.result.acceptance ? { acceptance: input.result.acceptance } : {}),
480
+ ...(input.result.capabilityCeiling ? { capabilityCeiling: input.result.capabilityCeiling } : {}),
481
+ ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
420
482
  };
421
483
  trimRememberedForegroundRuns(state);
422
484
  const output = getSingleResultOutput(input.result).trim();
@@ -424,6 +486,9 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
424
486
  const summary = !success && input.result.error
425
487
  ? `${input.result.error}${output ? `\n\nOutput:\n${output}` : ""}`
426
488
  : output || input.result.error || "Detached child exited without final output.";
489
+ // A detached callback may outlive its extension runtime. Stale sessions are
490
+ // intentionally dropped rather than routed through a replacement runtime.
491
+ if (!input.sessionId || input.sessionId !== state.currentSessionId) return;
427
492
  input.events.emit(SUBAGENT_FOREGROUND_COMPLETE_EVENT, {
428
493
  id: `${input.runId}:${input.index}`,
429
494
  runId: input.runId,
@@ -442,7 +507,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
442
507
  });
443
508
  }
444
509
 
445
- function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string } | undefined {
510
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
446
511
  const requested = (params.id ?? params.runId)?.trim();
447
512
  if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
448
513
  const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId === state.currentSessionId);
@@ -461,7 +526,16 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
461
526
  if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
462
527
  const sessionFile = path.resolve(child.sessionFile);
463
528
  if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
464
- return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, cwd: run.cwd, sessionFile };
529
+ return {
530
+ runId: run.runId,
531
+ mode: run.mode,
532
+ state: "complete",
533
+ agent: child.agent,
534
+ index,
535
+ cwd: run.cwd,
536
+ sessionFile,
537
+ ...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
538
+ };
465
539
  }
466
540
 
467
541
  type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
@@ -475,6 +549,7 @@ type NestedResumeSourceTarget = {
475
549
  index: number;
476
550
  cwd?: string;
477
551
  sessionFile: string;
552
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
478
553
  };
479
554
  type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
480
555
 
@@ -824,8 +899,10 @@ function appendStepToAsyncChain(input: {
824
899
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
825
900
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
826
901
  waitToolEnabled: input.deps.waitToolEnabled,
902
+ contextForAgent: contextPolicy.contextForAgent,
827
903
  asyncDir: resolved.location.asyncDir,
828
904
  validateOutputBindings: false,
905
+ capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
829
906
  });
830
907
  if ("error" in built) {
831
908
  return {
@@ -930,6 +1007,7 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
930
1007
  index: 0,
931
1008
  cwd: asyncDir ? path.dirname(asyncDir) : undefined,
932
1009
  sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
1010
+ ...(run.capabilityCeiling ? { capabilityCeiling: run.capabilityCeiling } : {}),
933
1011
  };
934
1012
  }
935
1013
 
@@ -1174,7 +1252,7 @@ async function resumeAsyncRun(input: {
1174
1252
  };
1175
1253
  }
1176
1254
  const runId = randomUUID().slice(0, 8);
1177
- const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
1255
+ const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1178
1256
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1179
1257
  const contextPolicy = resolveExplicitContextPolicy(input.params);
1180
1258
  const workflowTask = (input.params.task ?? followUp) || undefined;
@@ -1207,11 +1285,12 @@ async function resumeAsyncRun(input: {
1207
1285
  availableModels,
1208
1286
  cwd: effectiveCwd,
1209
1287
  maxOutput: input.params.maxOutput,
1210
- artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd),
1288
+ artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir),
1211
1289
  artifactConfig,
1212
1290
  shareEnabled: input.params.share === true,
1213
1291
  sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
1214
1292
  chainSkills: normalized === false ? [] : (normalized ?? []),
1293
+ agentContract: input.params.agentContract,
1215
1294
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
1216
1295
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1217
1296
  waitToolEnabled: input.deps.waitToolEnabled,
@@ -1222,6 +1301,7 @@ async function resumeAsyncRun(input: {
1222
1301
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1223
1302
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1224
1303
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1304
+ capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1225
1305
  });
1226
1306
  if (result.isError) return result;
1227
1307
  const attachedId = result.details.asyncId ?? runId;
@@ -1237,8 +1317,8 @@ async function resumeAsyncRun(input: {
1237
1317
 
1238
1318
  const runId = randomUUID().slice(0, 8);
1239
1319
  const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1240
- const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
1241
- const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd);
1320
+ const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1321
+ const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1242
1322
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1243
1323
  const result = executeAsyncSingle(runId, {
1244
1324
  agent: target.agent,
@@ -1283,12 +1363,15 @@ async function resumeAsyncRun(input: {
1283
1363
  availableModels,
1284
1364
  output: recoveryDescriptor?.outputPath,
1285
1365
  outputMode: recoveryDescriptor?.outputMode,
1366
+ ...(recoveryDescriptor?.agentContract ? { agentContract: recoveryDescriptor.agentContract } : {}),
1367
+ ...(recoveryDescriptor?.structuredOutputSchema ? { structuredOutputSchema: recoveryDescriptor.structuredOutputSchema } : {}),
1286
1368
  ...(recoveryDescriptor?.skills ? { skills: [...recoveryDescriptor.skills] } : {}),
1287
1369
  ...(recoveryDescriptor?.acceptance !== undefined && input.params.acceptance === undefined ? { acceptance: recoveryDescriptor.acceptance } : {}),
1288
1370
  ...(input.params.timeoutMs !== undefined ? { timeoutMs: input.params.timeoutMs } : {}),
1289
1371
  ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
1290
1372
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1291
1373
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1374
+ capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1292
1375
  });
1293
1376
  if (result.isError) return result;
1294
1377
 
@@ -1322,19 +1405,22 @@ function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string
1322
1405
  if (output && output !== error.trim()) {
1323
1406
  lines.push("", "Output:", output);
1324
1407
  }
1325
- if (result.artifactPaths?.outputPath) {
1408
+ if (result.artifactPaths?.outputPath && fs.existsSync(result.artifactPaths.outputPath)) {
1326
1409
  lines.push("", `Output artifact: ${result.artifactPaths.outputPath}`);
1327
1410
  }
1328
1411
  return lines.join("\n");
1329
1412
  }
1330
1413
 
1331
- function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi">): (event: ControlEvent) => void {
1332
- return (event) => emitControlNotification({
1333
- pi: deps.pi,
1334
- controlConfig: data.controlConfig,
1335
- intercomBridge: data.intercomBridge,
1336
- event,
1337
- });
1414
+ function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi" | "state">): (event: ControlEvent) => void {
1415
+ return (event) => {
1416
+ applyControlEventToRememberedForegroundRun(deps.state, event);
1417
+ emitControlNotification({
1418
+ pi: deps.pi,
1419
+ controlConfig: data.controlConfig,
1420
+ intercomBridge: data.intercomBridge,
1421
+ event,
1422
+ });
1423
+ };
1338
1424
  }
1339
1425
 
1340
1426
  async function emitForegroundResultIntercom(input: {
@@ -1345,6 +1431,7 @@ async function emitForegroundResultIntercom(input: {
1345
1431
  results: SingleResult[];
1346
1432
  chainSteps?: number;
1347
1433
  nestedChildren?: NestedRunSummary[];
1434
+ parallelHandoff?: Details["parallelHandoff"];
1348
1435
  }): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
1349
1436
  if (!input.intercomBridge.active || !input.intercomBridge.orchestratorTarget) return null;
1350
1437
  const children = input.results.flatMap((result, index) => result.detached ? [] : [{
@@ -1368,6 +1455,7 @@ async function emitForegroundResultIntercom(input: {
1368
1455
  source: "foreground",
1369
1456
  children: attachNestedChildrenToResultChildren(input.runId, children, input.nestedChildren),
1370
1457
  ...(typeof input.chainSteps === "number" ? { chainSteps: input.chainSteps } : {}),
1458
+ ...(input.parallelHandoff ? { parallelHandoff: input.parallelHandoff } : {}),
1371
1459
  });
1372
1460
  const delivered = await deliverSubagentResultIntercomEvent(input.pi.events, payload);
1373
1461
  if (!delivered) return null;
@@ -1390,6 +1478,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1390
1478
  results: input.details.results,
1391
1479
  ...(typeof input.details.totalSteps === "number" ? { chainSteps: input.details.totalSteps } : {}),
1392
1480
  ...(input.nestedChildren?.length ? { nestedChildren: input.nestedChildren } : {}),
1481
+ ...(input.details.parallelHandoff ? { parallelHandoff: input.details.parallelHandoff } : {}),
1393
1482
  });
1394
1483
  if (!payload) return null;
1395
1484
  return {
@@ -1531,7 +1620,8 @@ function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1531
1620
 
1532
1621
  interface AgentDefaultContextPolicy {
1533
1622
  params: SubagentParamsLike;
1534
- contextForAgent(agentName: string): "fresh" | "fork";
1623
+ contextForAgent(agentName: string): ContextMode;
1624
+ contextSummary?: ContextSummary;
1535
1625
  usesFork: boolean;
1536
1626
  }
1537
1627
 
@@ -1540,12 +1630,15 @@ function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: Ag
1540
1630
  return resolveExplicitContextPolicy(params);
1541
1631
  }
1542
1632
  const byName = new Map(agents.map((agent) => [agent.name, agent]));
1543
- const contextForAgent = (agentName: string): "fresh" | "fork" =>
1633
+ const contextForAgent = (agentName: string): ContextMode =>
1544
1634
  byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh";
1545
- const usesFork = collectRequestedAgentNames(params).some((name) => contextForAgent(name) === "fork");
1635
+ const requestedAgentNames = collectRequestedAgentNames(params);
1636
+ const contextSummary = summarizeContextModes(requestedAgentNames.map((name) => contextForAgent(name)));
1637
+ const usesFork = contextSummary === "fork" || contextSummary === "mixed";
1546
1638
  return {
1547
- params: usesFork ? { ...params, context: "fork" } : params,
1639
+ params,
1548
1640
  contextForAgent,
1641
+ contextSummary,
1549
1642
  usesFork,
1550
1643
  };
1551
1644
  }
@@ -1555,6 +1648,7 @@ function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultC
1555
1648
  return {
1556
1649
  params,
1557
1650
  contextForAgent: () => context,
1651
+ contextSummary: context,
1558
1652
  usesFork: context === "fork",
1559
1653
  };
1560
1654
  }
@@ -1571,8 +1665,12 @@ function shouldForkAgent(contextPolicy: AgentDefaultContextPolicy, agentName: st
1571
1665
  return contextPolicy.contextForAgent(agentName) === "fork";
1572
1666
  }
1573
1667
 
1668
+ function summarizeResultContext(details: Details, fallback: ContextSummary | undefined): ContextSummary | undefined {
1669
+ return summarizeContextModes(details.results.map((result) => result.context)) ?? fallback;
1670
+ }
1671
+
1574
1672
  function buildRequestedModeError(params: SubagentParamsLike, message: string): AgentToolResult<Details> {
1575
- return withForkContext(
1673
+ return withResolvedContext(
1576
1674
  {
1577
1675
  content: [{ type: "text", text: message }],
1578
1676
  isError: true,
@@ -1617,8 +1715,12 @@ function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: num
1617
1715
  return { timeoutMs: rawTimeout ?? rawMaxRuntime };
1618
1716
  }
1619
1717
 
1620
- function resolveToolBudget(raw: unknown, label = "toolBudget"): { toolBudget?: ResolvedToolBudget; error?: string } {
1621
- const resolved = validateToolBudgetConfig(raw, label);
1718
+ function resolveToolBudget(
1719
+ raw: unknown,
1720
+ label = "toolBudget",
1721
+ options?: { minimumHard?: 0 | 1 },
1722
+ ): { toolBudget?: ResolvedToolBudget; error?: string } {
1723
+ const resolved = validateToolBudgetConfig(raw, label, options);
1622
1724
  return { toolBudget: resolved.budget, error: resolved.error };
1623
1725
  }
1624
1726
 
@@ -1688,16 +1790,18 @@ function normalizeRepeatedParallelCounts(params: SubagentParamsLike): { params?:
1688
1790
  return { params };
1689
1791
  }
1690
1792
 
1691
- function withForkContext(
1793
+ function withResolvedContext(
1692
1794
  result: AgentToolResult<Details>,
1693
- context: SubagentParamsLike["context"],
1795
+ fallback: ContextSummary | undefined,
1694
1796
  ): AgentToolResult<Details> {
1695
- if (context !== "fork" || !result.details) return result;
1797
+ if (!result.details) return result;
1798
+ const context = summarizeResultContext(result.details, fallback);
1799
+ if (!context) return result;
1696
1800
  return {
1697
1801
  ...result,
1698
1802
  details: {
1699
1803
  ...result.details,
1700
- context: "fork",
1804
+ context,
1701
1805
  },
1702
1806
  };
1703
1807
  }
@@ -1715,15 +1819,15 @@ function withForkThinkingNotes(
1715
1819
  return { ...result, content: [...result.content, { type: "text", text: note }] };
1716
1820
  }
1717
1821
 
1718
- function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): AgentToolResult<Details> {
1822
+ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown, contextSummary?: ContextSummary): AgentToolResult<Details> {
1719
1823
  const message = error instanceof Error ? error.message : String(error);
1720
- return withForkContext(
1824
+ return withResolvedContext(
1721
1825
  {
1722
1826
  content: [{ type: "text", text: message }],
1723
1827
  isError: true,
1724
1828
  details: { mode: getRequestedModeLabel(params), results: [] },
1725
1829
  },
1726
- params.context,
1830
+ contextSummary,
1727
1831
  );
1728
1832
  }
1729
1833
 
@@ -1969,6 +2073,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1969
2073
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
1970
2074
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
1971
2075
  ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
2076
+ ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
2077
+ ...(task.agentContract !== undefined ? { agentContract: task.agentContract } : {}),
1972
2078
  ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
1973
2079
  }));
1974
2080
  return executeAsyncChain(id, {
@@ -1991,12 +2097,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1991
2097
  chainSkills: [],
1992
2098
  sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
1993
2099
  thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
2100
+ contextForAgent: contextPolicy.contextForAgent,
1994
2101
  maxSubagentDepth: currentMaxSubagentDepth,
1995
2102
  waitToolEnabled: deps.waitToolEnabled,
1996
2103
  worktreeSetupHook: deps.config.worktreeSetupHook,
1997
2104
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1998
2105
  worktreeBaseDir: deps.config.worktreeBaseDir,
1999
2106
  controlConfig,
2107
+ agentContract: params.agentContract,
2000
2108
  controlIntercomTarget,
2001
2109
  childIntercomTarget,
2002
2110
  nestedRoute,
@@ -2004,6 +2112,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2004
2112
  turnBudget: data.turnBudget,
2005
2113
  toolBudget: data.toolBudget,
2006
2114
  configToolBudget: data.configToolBudget,
2115
+ capabilityCeiling: data.capabilityCeiling,
2007
2116
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2008
2117
  });
2009
2118
  }
@@ -2029,6 +2138,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2029
2138
  chainSkills,
2030
2139
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2031
2140
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2141
+ contextForAgent: contextPolicy.contextForAgent,
2032
2142
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2033
2143
  maxSubagentDepth: currentMaxSubagentDepth,
2034
2144
  waitToolEnabled: deps.waitToolEnabled,
@@ -2036,6 +2146,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2036
2146
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2037
2147
  worktreeBaseDir: deps.config.worktreeBaseDir,
2038
2148
  controlConfig,
2149
+ agentContract: params.agentContract,
2039
2150
  controlIntercomTarget,
2040
2151
  childIntercomTarget,
2041
2152
  nestedRoute,
@@ -2043,6 +2154,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2043
2154
  turnBudget: data.turnBudget,
2044
2155
  toolBudget: data.toolBudget,
2045
2156
  configToolBudget: data.configToolBudget,
2157
+ capabilityCeiling: data.capabilityCeiling,
2046
2158
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2047
2159
  });
2048
2160
  }
@@ -2077,6 +2189,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2077
2189
  shareEnabled,
2078
2190
  sessionRoot,
2079
2191
  sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2192
+ context: contextPolicy.contextForAgent(params.agent!),
2080
2193
  skills,
2081
2194
  output: effectiveOutput,
2082
2195
  outputMode: effectiveOutputMode,
@@ -2092,11 +2205,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2092
2205
  controlIntercomTarget,
2093
2206
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
2094
2207
  nestedRoute,
2208
+ agentContract: params.agentContract,
2209
+ structuredOutputSchema: params.outputSchema,
2095
2210
  acceptance: params.acceptance,
2096
2211
  timeoutMs: data.timeoutMs,
2097
2212
  turnBudget: data.turnBudget,
2098
2213
  toolBudget: data.toolBudget,
2099
2214
  configToolBudget: data.configToolBudget,
2215
+ capabilityCeiling: data.capabilityCeiling,
2100
2216
  });
2101
2217
  }
2102
2218
 
@@ -2145,6 +2261,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2145
2261
  sessionFileForIndex,
2146
2262
  sessionFileForTask,
2147
2263
  thinkingOverrideForTask,
2264
+ contextForAgent: contextPolicy.contextForAgent,
2148
2265
  artifactsDir,
2149
2266
  artifactConfig,
2150
2267
  includeProgress: params.includeProgress,
@@ -2152,6 +2269,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2152
2269
  onUpdate,
2153
2270
  onControlEvent,
2154
2271
  controlConfig,
2272
+ agentContract: params.agentContract,
2155
2273
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
2156
2274
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2157
2275
  foregroundControl,
@@ -2170,6 +2288,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2170
2288
  toolBudget: data.toolBudget,
2171
2289
  configToolBudget: data.configToolBudget,
2172
2290
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2291
+ capabilityCeiling: data.capabilityCeiling,
2173
2292
  });
2174
2293
 
2175
2294
  if (chainResult.requestedAsync) {
@@ -2210,6 +2329,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2210
2329
  chainSkills: chainResult.requestedAsync.chainSkills,
2211
2330
  sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2212
2331
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2332
+ contextForAgent: contextPolicy.contextForAgent,
2213
2333
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2214
2334
  maxSubagentDepth: currentMaxSubagentDepth,
2215
2335
  waitToolEnabled: deps.waitToolEnabled,
@@ -2217,6 +2337,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2217
2337
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2218
2338
  worktreeBaseDir: deps.config.worktreeBaseDir,
2219
2339
  controlConfig,
2340
+ agentContract: params.agentContract,
2220
2341
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2221
2342
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2222
2343
  nestedRoute: data.nestedRoute,
@@ -2224,6 +2345,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2224
2345
  turnBudget: data.turnBudget,
2225
2346
  toolBudget: data.toolBudget,
2226
2347
  configToolBudget: data.configToolBudget,
2348
+ capabilityCeiling: data.capabilityCeiling,
2227
2349
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2228
2350
  });
2229
2351
  }
@@ -2260,11 +2382,13 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2260
2382
  interface ForegroundParallelRunInput {
2261
2383
  tasks: TaskParam[];
2262
2384
  taskTexts: string[];
2385
+ taskDescriptions: string[];
2263
2386
  agents: AgentConfig[];
2264
2387
  ctx: ExtensionContext;
2265
2388
  state: SubagentState;
2266
2389
  intercomEvents: IntercomEventBus;
2267
2390
  parentSessionId: string | null;
2391
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
2268
2392
  signal: AbortSignal;
2269
2393
  runId: string;
2270
2394
  sessionDirForIndex: (idx?: number) => string | undefined;
@@ -2286,6 +2410,7 @@ interface ForegroundParallelRunInput {
2286
2410
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
2287
2411
  firstProgressIndex: number;
2288
2412
  controlConfig: ResolvedControlConfig;
2413
+ contextPolicy: AgentDefaultContextPolicy;
2289
2414
  onControlEvent?: (event: ControlEvent) => void;
2290
2415
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
2291
2416
  orchestratorIntercomTarget?: string;
@@ -2300,8 +2425,8 @@ interface ForegroundParallelRunInput {
2300
2425
  deadlineAt?: number;
2301
2426
  turnBudget?: ResolvedTurnBudget;
2302
2427
  toolBudgets: (ResolvedToolBudget | undefined)[];
2428
+ agentContract?: AgentContract;
2303
2429
  }
2304
-
2305
2430
  function buildParallelModeError(message: string): AgentToolResult<Details> {
2306
2431
  return {
2307
2432
  content: [{ type: "text", text: message }],
@@ -2374,15 +2499,52 @@ function resolveParallelTaskCwd(
2374
2499
  return resolveChildCwd(paramsCwd, task.cwd);
2375
2500
  }
2376
2501
 
2377
- function buildParallelWorktreeSuffix(
2378
- worktreeSetup: WorktreeSetup | undefined,
2379
- artifactsDir: string,
2380
- tasks: TaskParam[],
2381
- ): string {
2382
- if (!worktreeSetup) return "";
2383
- const diffsDir = path.join(artifactsDir, "worktree-diffs");
2384
- const diffs = diffWorktrees(worktreeSetup, tasks.map((task) => task.agent), diffsDir);
2385
- return formatWorktreeDiffSummary(diffs);
2502
+ function finalizeParallelWorktreeHandoff(input: {
2503
+ worktreeSetup: WorktreeSetup;
2504
+ artifactsDir: string;
2505
+ runId: string;
2506
+ cwd: string;
2507
+ tasks: TaskParam[];
2508
+ results: SingleResult[];
2509
+ }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
2510
+ const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
2511
+ const diffs = diffWorktrees(input.worktreeSetup, input.tasks.map((task) => task.agent), diffsDir);
2512
+ const cleanup = cleanupWorktrees(input.worktreeSetup);
2513
+ const diffSummary = formatWorktreeDiffSummary(diffs);
2514
+ try {
2515
+ const reference = writeParallelHandoffGroup({
2516
+ manifestPath: parallelHandoffPath(input.artifactsDir, input.runId),
2517
+ runId: input.runId,
2518
+ mode: "parallel",
2519
+ source: "foreground",
2520
+ cwd: input.cwd,
2521
+ stepIndex: 0,
2522
+ flatStartIndex: 0,
2523
+ setup: input.worktreeSetup,
2524
+ diffs,
2525
+ cleanup,
2526
+ results: input.results.map((result) => ({
2527
+ agent: result.agent,
2528
+ status: resolveSubagentResultStatus({
2529
+ exitCode: result.exitCode,
2530
+ interrupted: result.interrupted,
2531
+ detached: result.detached,
2532
+ state: result.stopped ? "stopped" : undefined,
2533
+ }),
2534
+ summary: resultSummaryForIntercom(result),
2535
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
2536
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
2537
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
2538
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
2539
+ })),
2540
+ });
2541
+ return {
2542
+ suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"),
2543
+ reference,
2544
+ };
2545
+ } catch (error) {
2546
+ return { suffix: [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n") };
2547
+ }
2386
2548
  }
2387
2549
 
2388
2550
  function findDuplicateParallelOutputPath(input: {
@@ -2435,20 +2597,23 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2435
2597
  );
2436
2598
  const interruptController = new AbortController();
2437
2599
  if (input.foregroundControl) {
2438
- input.foregroundControl.currentAgent = task.agent;
2439
- input.foregroundControl.currentIndex = index;
2440
- input.foregroundControl.currentActivityState = undefined;
2441
- input.foregroundControl.updatedAt = Date.now();
2442
- input.foregroundControl.interrupt = () => {
2443
- if (interruptController.signal.aborted) return false;
2444
- interruptController.abort();
2445
- input.foregroundControl!.currentActivityState = undefined;
2446
- input.foregroundControl!.updatedAt = Date.now();
2447
- return true;
2448
- };
2600
+ beginForegroundChild(input.foregroundControl, {
2601
+ index,
2602
+ agent: task.agent,
2603
+ description: input.taskDescriptions[index],
2604
+ interrupt: () => {
2605
+ if (interruptController.signal.aborted) return false;
2606
+ interruptController.abort();
2607
+ return true;
2608
+ },
2609
+ });
2449
2610
  }
2611
+ const structuredRuntime = task.outputSchema
2612
+ ? createStructuredOutputRuntime(task.outputSchema, path.join(input.artifactsDir, "structured-output", input.runId))
2613
+ : undefined;
2450
2614
  return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
2451
2615
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
2616
+ context: input.contextPolicy.contextForAgent(task.agent),
2452
2617
  cwd: taskCwd,
2453
2618
  signal: input.signal,
2454
2619
  interruptSignal: interruptController.signal,
@@ -2466,6 +2631,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2466
2631
  outputMode: behavior?.outputMode,
2467
2632
  maxSubagentDepth: input.maxSubagentDepths[index],
2468
2633
  waitToolEnabled: input.waitToolEnabled,
2634
+ capabilityCeiling: input.capabilityCeiling,
2469
2635
  controlConfig: input.controlConfig,
2470
2636
  onControlEvent: input.onControlEvent,
2471
2637
  onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents }),
@@ -2478,6 +2644,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2478
2644
  preferredModelProvider: input.ctx.model?.provider,
2479
2645
  modelScope: input.modelScope,
2480
2646
  skills: effectiveSkills === false ? [] : effectiveSkills,
2647
+ structuredOutput: structuredRuntime,
2648
+ agentContract: task.agentContract ?? input.agentContract,
2481
2649
  acceptance: task.acceptance,
2482
2650
  acceptanceContext: { mode: "parallel" },
2483
2651
  timeoutMs: input.timeoutMs,
@@ -2489,18 +2657,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2489
2657
  const stepResults = progressUpdate.details?.results || [];
2490
2658
  const stepProgress = progressUpdate.details?.progress || [];
2491
2659
  if (input.foregroundControl && stepProgress.length > 0) {
2492
- const current = stepProgress[0];
2493
- input.foregroundControl.currentAgent = task.agent;
2494
- input.foregroundControl.currentIndex = index;
2495
- input.foregroundControl.currentActivityState = current?.activityState;
2496
- input.foregroundControl.lastActivityAt = current?.lastActivityAt;
2497
- input.foregroundControl.currentTool = current?.currentTool;
2498
- input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
2499
- input.foregroundControl.currentPath = current?.currentPath;
2500
- input.foregroundControl.turnCount = current?.turnCount;
2501
- input.foregroundControl.tokens = current?.tokens;
2502
- input.foregroundControl.toolCount = current?.toolCount;
2503
- input.foregroundControl.updatedAt = Date.now();
2660
+ updateForegroundChild(input.foregroundControl, index, stepProgress[0]);
2504
2661
  }
2505
2662
  if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
2506
2663
  if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
@@ -2519,10 +2676,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2519
2676
  }
2520
2677
  : undefined,
2521
2678
  }).finally(() => {
2522
- if (input.foregroundControl?.currentIndex === index) {
2523
- input.foregroundControl.interrupt = undefined;
2524
- input.foregroundControl.updatedAt = Date.now();
2525
- }
2679
+ if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
2526
2680
  });
2527
2681
  }, input.globalSemaphore);
2528
2682
  }
@@ -2687,7 +2841,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2687
2841
  ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
2688
2842
  ...(progress !== undefined ? { progress } : {}),
2689
2843
  ...(t.toolBudget !== undefined ? { toolBudget: t.toolBudget } : {}),
2844
+ ...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
2690
2845
  ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
2846
+ ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
2691
2847
  };
2692
2848
  });
2693
2849
  return executeAsyncChain(id, {
@@ -2706,22 +2862,33 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2706
2862
  chainSkills: [],
2707
2863
  sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
2708
2864
  thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
2865
+ contextForAgent: contextPolicy.contextForAgent,
2709
2866
  maxSubagentDepth: currentMaxSubagentDepth,
2710
2867
  waitToolEnabled: deps.waitToolEnabled,
2711
2868
  worktreeSetupHook: deps.config.worktreeSetupHook,
2712
2869
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2713
2870
  worktreeBaseDir: deps.config.worktreeBaseDir,
2714
2871
  controlConfig,
2872
+ agentContract: params.agentContract,
2715
2873
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2716
2874
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2875
+ nestedRoute: data.nestedRoute,
2717
2876
  timeoutMs: data.timeoutMs,
2718
2877
  turnBudget: data.turnBudget,
2878
+ toolBudget: data.toolBudget,
2879
+ configToolBudget: data.configToolBudget,
2719
2880
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2720
2881
  });
2721
2882
  }
2722
2883
  }
2723
2884
 
2724
- const behaviors = agentConfigs.map((config, index) => suppressProgressForReadOnlyTask(resolveStepBehavior(config, behaviorOverrides[index]!), taskTexts[index]));
2885
+ const behaviors = tasks.map((task, index) => {
2886
+ let behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfigs[index]!, behaviorOverrides[index]!), taskTexts[index]);
2887
+ if (behaviorOverrides[index]?.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output)) {
2888
+ behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, behavior.output) };
2889
+ }
2890
+ return behavior;
2891
+ });
2725
2892
  const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
2726
2893
  const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
2727
2894
  const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
@@ -2737,6 +2904,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2737
2904
  );
2738
2905
  if (errorResult) return errorResult;
2739
2906
 
2907
+ let worktreeFinalized = false;
2740
2908
  try {
2741
2909
  const outputBaseDir = path.join(artifactsDir, "outputs", runId);
2742
2910
  const duplicateOutputError = findDuplicateParallelOutputPath({
@@ -2759,6 +2927,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2759
2927
  const parallelProgressDir = path.join(artifactsDir, "progress", runId);
2760
2928
  if (parallelProgressPrecreated) writeInitialProgressFile(parallelProgressDir);
2761
2929
 
2930
+ const taskDescriptions = taskTexts.map((taskText) => taskText.trim());
2762
2931
  for (let i = 0; i < taskTexts.length; i++) {
2763
2932
  if (shouldForkAgent(contextPolicy, tasks[i]!.agent)) taskTexts[i] = wrapForkTask(taskTexts[i]!);
2764
2933
  }
@@ -2767,11 +2936,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2767
2936
  const results = await runForegroundParallelTasks({
2768
2937
  tasks,
2769
2938
  taskTexts,
2939
+ taskDescriptions,
2770
2940
  agents,
2771
2941
  ctx,
2772
2942
  state: deps.state,
2773
2943
  intercomEvents: deps.pi.events,
2774
2944
  parentSessionId: data.parentSessionId,
2945
+ capabilityCeiling: data.capabilityCeiling,
2775
2946
  signal,
2776
2947
  runId,
2777
2948
  sessionDirForIndex,
@@ -2791,6 +2962,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2791
2962
  behaviors,
2792
2963
  firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
2793
2964
  controlConfig,
2965
+ contextPolicy,
2794
2966
  onControlEvent,
2795
2967
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
2796
2968
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
@@ -2807,6 +2979,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2807
2979
  deadlineAt,
2808
2980
  turnBudget: data.turnBudget,
2809
2981
  toolBudgets,
2982
+ agentContract: params.agentContract,
2810
2983
  });
2811
2984
  for (let i = 0; i < results.length; i++) {
2812
2985
  const run = results[i]!;
@@ -2822,6 +2995,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2822
2995
  updateForegroundNestedProjection(foregroundControl);
2823
2996
  attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
2824
2997
  }
2998
+ let handoff: ReturnType<typeof finalizeParallelWorktreeHandoff> | undefined;
2999
+ if (worktreeSetup) {
3000
+ worktreeFinalized = true;
3001
+ handoff = finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
3002
+ }
2825
3003
  const interrupted = results.find((result) => result.interrupted);
2826
3004
  const details = compactForegroundDetails({
2827
3005
  mode: "parallel",
@@ -2831,6 +3009,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2831
3009
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
2832
3010
  totalChildUsage: sumResultsUsage(results),
2833
3011
  totalCost: sumResultsCost(results),
3012
+ ...(handoff?.reference ? { parallelHandoff: handoff.reference } : {}),
2834
3013
  });
2835
3014
  rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
2836
3015
  if (interrupted) {
@@ -2864,7 +3043,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2864
3043
  };
2865
3044
  }
2866
3045
 
2867
- const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks);
3046
+ const worktreeSuffix = handoff?.suffix ?? "";
2868
3047
  const ok = results.filter((result) => result.exitCode === 0).length;
2869
3048
  const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
2870
3049
  const aggregatedOutput = aggregateParallelOutputs(
@@ -2888,7 +3067,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2888
3067
  details,
2889
3068
  };
2890
3069
  } finally {
2891
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
3070
+ if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
2892
3071
  }
2893
3072
  }
2894
3073
 
@@ -2924,7 +3103,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2924
3103
  };
2925
3104
  }
2926
3105
  const effectiveToolBudget = resolveEffectiveToolBudget({ runBudget: data.toolBudget, agentBudget: agentConfig.toolBudget, configBudget: data.configToolBudget });
2927
- if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error));
3106
+ if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error), data.contextPolicy.contextSummary);
2928
3107
 
2929
3108
  const currentProvider = ctx.model?.provider;
2930
3109
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
@@ -3009,6 +3188,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3009
3188
  shareEnabled,
3010
3189
  sessionRoot,
3011
3190
  sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3191
+ context: contextPolicy.contextForAgent(params.agent!),
3012
3192
  skills: skillOverride === false ? [] : skillOverride,
3013
3193
  output: effectiveOutput,
3014
3194
  outputMode: effectiveOutputMode,
@@ -3023,9 +3203,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3023
3203
  controlConfig,
3024
3204
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3025
3205
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3206
+ nestedRoute: data.nestedRoute,
3207
+ agentContract: params.agentContract,
3208
+ structuredOutputSchema: params.outputSchema,
3209
+ acceptance: params.acceptance,
3026
3210
  timeoutMs: data.timeoutMs,
3027
3211
  turnBudget: data.turnBudget,
3028
3212
  toolBudget: effectiveToolBudget.toolBudget,
3213
+ allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3029
3214
  });
3030
3215
  }
3031
3216
  }
@@ -3039,6 +3224,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3039
3224
  if (validationError) {
3040
3225
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
3041
3226
  }
3227
+ const structuredRuntime = params.outputSchema
3228
+ ? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined)
3229
+ : undefined;
3042
3230
  task = injectSingleOutputInstruction(task, outputPath, agentConfig);
3043
3231
 
3044
3232
  let effectiveSkills: string[] | undefined;
@@ -3050,91 +3238,77 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3050
3238
  const interruptController = new AbortController();
3051
3239
  const foregroundControl = deps.state.foregroundControls.get(runId);
3052
3240
  if (foregroundControl) {
3053
- foregroundControl.currentAgent = params.agent;
3054
- foregroundControl.currentIndex = 0;
3055
- foregroundControl.currentActivityState = undefined;
3056
- foregroundControl.updatedAt = Date.now();
3057
- foregroundControl.interrupt = () => {
3058
- if (interruptController.signal.aborted) return false;
3059
- interruptController.abort();
3060
- foregroundControl.currentActivityState = undefined;
3061
- foregroundControl.updatedAt = Date.now();
3062
- return true;
3063
- };
3241
+ beginForegroundChild(foregroundControl, {
3242
+ index: 0,
3243
+ agent: params.agent!,
3244
+ description: foregroundControl.description,
3245
+ interrupt: () => {
3246
+ if (interruptController.signal.aborted) return false;
3247
+ interruptController.abort();
3248
+ return true;
3249
+ },
3250
+ });
3064
3251
  }
3065
3252
 
3066
3253
  const forwardSingleUpdate = onUpdate
3067
3254
  ? (update: AgentToolResult<Details>) => {
3068
- if (foregroundControl) {
3069
- const firstProgress = update.details?.progress?.[0];
3070
- foregroundControl.currentAgent = params.agent;
3071
- foregroundControl.currentIndex = firstProgress?.index ?? 0;
3072
- foregroundControl.currentActivityState = firstProgress?.activityState;
3073
- foregroundControl.lastActivityAt = firstProgress?.lastActivityAt;
3074
- foregroundControl.currentTool = firstProgress?.currentTool;
3075
- foregroundControl.currentToolStartedAt = firstProgress?.currentToolStartedAt;
3076
- foregroundControl.currentPath = firstProgress?.currentPath;
3077
- foregroundControl.turnCount = firstProgress?.turnCount;
3078
- foregroundControl.tokens = firstProgress?.tokens;
3079
- foregroundControl.toolCount = firstProgress?.toolCount;
3080
- foregroundControl.updatedAt = Date.now();
3081
- }
3255
+ if (foregroundControl) updateForegroundChild(foregroundControl, 0, update.details?.progress?.[0]);
3082
3256
  onUpdate(update);
3083
3257
  }
3084
3258
  : undefined;
3085
3259
 
3086
3260
  const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
3087
- const r = await runSync(ctx.cwd, agents, params.agent!, task, {
3088
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3089
- cwd: effectiveCwd,
3090
- signal,
3091
- interruptSignal: interruptController.signal,
3092
- allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
3093
- intercomEvents: deps.pi.events,
3094
- runId,
3095
- sessionDir: sessionDirForIndex(0),
3096
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3097
- share: shareEnabled,
3098
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3099
- artifactConfig,
3100
- maxOutput: params.maxOutput,
3101
- outputPath,
3102
- outputMode: effectiveOutputMode,
3103
- maxSubagentDepth,
3104
- waitToolEnabled: deps.waitToolEnabled,
3105
- onUpdate: forwardSingleUpdate,
3106
- controlConfig,
3107
- onControlEvent,
3108
- intercomSessionName: childIntercomTarget,
3109
- orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3110
- nestedRoute: foregroundControl?.nestedRoute,
3111
- index: 0,
3112
- modelOverride,
3113
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
3114
- availableModels,
3115
- preferredModelProvider: currentProvider,
3116
- modelScope: data.modelScope,
3117
- skills: effectiveSkills,
3118
- acceptance: params.acceptance,
3119
- acceptanceContext: { mode: "single" },
3120
- onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events }),
3121
- timeoutMs: data.timeoutMs,
3122
- deadlineAt,
3123
- turnBudget: data.turnBudget,
3124
- toolBudget: effectiveToolBudget.toolBudget,
3125
- });
3126
- if (foregroundControl?.currentIndex === 0) {
3127
- foregroundControl.interrupt = undefined;
3128
- foregroundControl.currentActivityState = r.progress?.activityState;
3129
- foregroundControl.lastActivityAt = r.progress?.lastActivityAt;
3130
- foregroundControl.currentTool = r.progress?.currentTool;
3131
- foregroundControl.currentToolStartedAt = r.progress?.currentToolStartedAt;
3132
- foregroundControl.currentPath = r.progress?.currentPath;
3133
- foregroundControl.turnCount = r.progress?.turnCount;
3134
- foregroundControl.tokens = r.progress?.tokens;
3135
- foregroundControl.toolCount = r.progress?.toolCount;
3136
- foregroundControl.updatedAt = Date.now();
3261
+ let r: Awaited<ReturnType<typeof runSync>>;
3262
+ try {
3263
+ r = await runSync(ctx.cwd, agents, params.agent!, task, {
3264
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3265
+ context: data.contextPolicy.contextForAgent(params.agent!),
3266
+ cwd: effectiveCwd,
3267
+ signal,
3268
+ interruptSignal: interruptController.signal,
3269
+ allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
3270
+ intercomEvents: deps.pi.events,
3271
+ runId,
3272
+ sessionDir: sessionDirForIndex(0),
3273
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3274
+ share: shareEnabled,
3275
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3276
+ artifactConfig,
3277
+ maxOutput: params.maxOutput,
3278
+ outputPath,
3279
+ outputMode: effectiveOutputMode,
3280
+ maxSubagentDepth,
3281
+ waitToolEnabled: deps.waitToolEnabled,
3282
+ onUpdate: forwardSingleUpdate,
3283
+ controlConfig,
3284
+ onControlEvent,
3285
+ intercomSessionName: childIntercomTarget,
3286
+ orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3287
+ nestedRoute: foregroundControl?.nestedRoute,
3288
+ index: 0,
3289
+ modelOverride,
3290
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
3291
+ availableModels,
3292
+ preferredModelProvider: currentProvider,
3293
+ modelScope: data.modelScope,
3294
+ skills: effectiveSkills,
3295
+ structuredOutput: structuredRuntime,
3296
+ agentContract: params.agentContract,
3297
+ acceptance: params.acceptance,
3298
+ acceptanceContext: { mode: "single" },
3299
+ onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events }),
3300
+ timeoutMs: data.timeoutMs,
3301
+ deadlineAt,
3302
+ turnBudget: data.turnBudget,
3303
+ enforceHardTurnLimit: params.enforceHardTurnLimit,
3304
+ toolBudget: effectiveToolBudget.toolBudget,
3305
+ capabilityCeiling: data.capabilityCeiling,
3306
+ allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3307
+ });
3308
+ } finally {
3309
+ if (!artifactConfig.enabled) cleanupStructuredOutputRuntime(structuredRuntime);
3137
3310
  }
3311
+ if (foregroundControl) finishForegroundChild(foregroundControl, 0);
3138
3312
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
3139
3313
 
3140
3314
  if (r.progress) allProgress.push(r.progress);
@@ -3254,7 +3428,21 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3254
3428
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3255
3429
  ctx: ExtensionContext,
3256
3430
  ) => Promise<AgentToolResult<Details>>;
3431
+ /**
3432
+ * Correlated extension-to-extension delegation owns its request IDs and
3433
+ * cancellation controllers, so independent requests may execute concurrently.
3434
+ * The ordinary model-facing tool keeps the one-foreground-call-per-turn guard.
3435
+ */
3436
+ executeDelegated: (
3437
+ id: string,
3438
+ params: SubagentParamsLike,
3439
+ signal: AbortSignal,
3440
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3441
+ ctx: ExtensionContext,
3442
+ ) => Promise<AgentToolResult<Details>>;
3257
3443
  } {
3444
+ const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
3445
+ const delegatedZeroToolBudgets = new WeakSet<object>();
3258
3446
  const execute = async (
3259
3447
  _id: string,
3260
3448
  params: SubagentParamsLike,
@@ -3262,6 +3450,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3262
3450
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3263
3451
  ctx: ExtensionContext,
3264
3452
  ): Promise<AgentToolResult<Details>> => {
3453
+ const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
3454
+ const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
3265
3455
  deps.state.baseCwd = ctx.cwd;
3266
3456
  deps.state.foregroundRuns ??= new Map();
3267
3457
  deps.state.foregroundControls ??= new Map();
@@ -3438,7 +3628,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3438
3628
  try {
3439
3629
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR);
3440
3630
  const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir);
3441
- return steerAsyncRun({ state: deps.state, runId, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location, signal, recover: ({ absoluteDeadlineAt, ...limits }) => resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, absoluteDeadlineAt }) });
3631
+ return steerAsyncRun({
3632
+ state: deps.state,
3633
+ runId,
3634
+ message,
3635
+ index: paramsWithResolvedCwd.index,
3636
+ kill: deps.kill,
3637
+ location,
3638
+ signal,
3639
+ ...(paramsWithResolvedCwd.steeringRecovery === false
3640
+ ? {}
3641
+ : {
3642
+ recover: ({ absoluteDeadlineAt, ...limits }) =>
3643
+ resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, absoluteDeadlineAt }),
3644
+ }
3645
+ ),
3646
+ });
3442
3647
  } catch (error) {
3443
3648
  const text = error instanceof Error ? error.message : String(error);
3444
3649
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
@@ -3455,7 +3660,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3455
3660
  if (resolved?.kind === "nested") return steerNestedRun({ target: resolved, message, index: paramsWithResolvedCwd.index, signal });
3456
3661
  if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='steer' currently supports live async Pi child sessions only; use action='interrupt' or action='resume' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
3457
3662
  if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
3458
- return steerAsyncRun({ state: deps.state, runId: resolved.id, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location: resolved.location, signal, recover: ({ absoluteDeadlineAt, ...limits }) => resumeAsyncRun({ params: { ...limits, action: "resume", id: resolved!.id, message }, requestCwd, ctx, deps, absoluteDeadlineAt }) });
3663
+ return steerAsyncRun({
3664
+ state: deps.state,
3665
+ runId: resolved.id,
3666
+ message,
3667
+ index: paramsWithResolvedCwd.index,
3668
+ kill: deps.kill,
3669
+ location: resolved.location,
3670
+ signal,
3671
+ ...(paramsWithResolvedCwd.steeringRecovery === false
3672
+ ? {}
3673
+ : {
3674
+ recover: ({ absoluteDeadlineAt, ...limits }) =>
3675
+ resumeAsyncRun({
3676
+ params: { ...limits, action: "resume", id: resolved!.id, message },
3677
+ requestCwd,
3678
+ ctx,
3679
+ deps,
3680
+ absoluteDeadlineAt,
3681
+ }),
3682
+ }
3683
+ ),
3684
+ });
3459
3685
  }
3460
3686
  if (action === "append-step") {
3461
3687
  return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
@@ -3593,7 +3819,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3593
3819
  depth,
3594
3820
  deps.config.forceTopLevelAsync === true,
3595
3821
  );
3596
- const runToolBudget = resolveToolBudget(effectiveParams.toolBudget, "toolBudget");
3822
+ const runToolBudget = resolveToolBudget(
3823
+ effectiveParams.toolBudget,
3824
+ "toolBudget",
3825
+ allowZeroToolBudget ? { minimumHard: 0 } : undefined,
3826
+ );
3597
3827
  if (runToolBudget.error) return buildRequestedModeError(effectiveParams, runToolBudget.error);
3598
3828
  const configToolBudget = resolveToolBudget(deps.config.toolBudget, "config.toolBudget");
3599
3829
  if (configToolBudget.error) return buildRequestedModeError(effectiveParams, configToolBudget.error);
@@ -3615,7 +3845,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3615
3845
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
3616
3846
  const intercomBridge = resolveIntercomBridge({
3617
3847
  config: deps.config.intercomBridge,
3618
- context: effectiveParams.context,
3848
+ context: effectiveParams.context ?? (contextPolicy.usesFork ? "fork" : undefined),
3619
3849
  orchestratorTarget: sessionName,
3620
3850
  });
3621
3851
  const agents = intercomBridge.active
@@ -3688,7 +3918,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3688
3918
  forkSessionFileForIndex = forkContextResolver.sessionFileForIndex;
3689
3919
  forkThinkingOverrideForIndex = forkContextResolver.thinkingOverrideForIndex;
3690
3920
  } catch (error) {
3691
- return toExecutionErrorResult(effectiveParams, error);
3921
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
3692
3922
  }
3693
3923
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
3694
3924
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
@@ -3698,8 +3928,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3698
3928
  const artifactConfig: ArtifactConfig = {
3699
3929
  ...DEFAULT_ARTIFACT_CONFIG,
3700
3930
  enabled: effectiveParams.artifacts !== false,
3931
+ dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
3701
3932
  };
3702
- const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd);
3933
+ const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
3703
3934
 
3704
3935
  let sessionRoot: string;
3705
3936
  if (effectiveParams.sessionDir) {
@@ -3717,6 +3948,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3717
3948
  return toExecutionErrorResult(
3718
3949
  effectiveParams,
3719
3950
  new Error(`Failed to create session directory '${sessionRoot}': ${message}`),
3951
+ contextPolicy.contextSummary,
3720
3952
  );
3721
3953
  }
3722
3954
  const sessionDirForIndex = (idx?: number) =>
@@ -3727,11 +3959,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3727
3959
  return forkSessionFileForIndex(idx);
3728
3960
  };
3729
3961
  const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride) => {
3730
- if (!shouldForkAgent(contextPolicy, agentName)) return undefined;
3962
+ if (!shouldForkAgent(contextPolicy, agentName)) return delegatedThinkingOverride;
3731
3963
  prepareForkThinking(agentName, idx, modelOverride);
3732
3964
  const override = forkThinkingOverrideForIndex(idx);
3733
3965
  if (override === "off") forkThinkingDowngrades.set(idx, agentName);
3734
- return override;
3966
+ return override ?? delegatedThinkingOverride;
3735
3967
  };
3736
3968
  const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride) =>
3737
3969
  forkSessionFileForTask(agentName, idx, modelOverride) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
@@ -3742,13 +3974,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3742
3974
  preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
3743
3975
  }
3744
3976
  } catch (error) {
3745
- return toExecutionErrorResult(effectiveParams, error);
3977
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
3746
3978
  }
3747
3979
  const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
3748
- if (chainBindingsError) return chainBindingsError;
3980
+ if (chainBindingsError) return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
3749
3981
 
3750
3982
  const onUpdateWithContext = onUpdate
3751
- ? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
3983
+ ? (r: AgentToolResult<Details>) => onUpdate(withResolvedContext(r, contextPolicy.contextSummary))
3752
3984
  : undefined;
3753
3985
 
3754
3986
  const reservation = reserveSpawnBudget(
@@ -3783,22 +4015,30 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3783
4015
  timeoutMs: foregroundTimeout.timeoutMs,
3784
4016
  turnBudget: turnBudget.turnBudget,
3785
4017
  toolBudget: runToolBudget.toolBudget,
4018
+ allowZeroToolBudget,
3786
4019
  configToolBudget: configToolBudget.toolBudget,
3787
4020
  contextPolicy,
3788
4021
  modelScope,
3789
4022
  parentSessionId: deps.state.currentSessionId,
4023
+ capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(deps.state.currentSessionId ?? undefined),
3790
4024
  };
3791
4025
 
3792
- const foregroundControl = effectiveAsync
4026
+ const foregroundDescription = effectiveParams.task?.trim()
4027
+ || effectiveParams.tasks?.[0]?.task?.trim()
4028
+ || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
4029
+ const foregroundControl: ForegroundRunControl | undefined = effectiveAsync
3793
4030
  ? undefined
3794
4031
  : {
3795
4032
  runId,
3796
4033
  mode: foregroundMode,
3797
4034
  startedAt: Date.now(),
3798
4035
  updatedAt: Date.now(),
4036
+ cwd: effectiveCwd,
3799
4037
  currentAgent: undefined,
3800
4038
  currentIndex: undefined,
4039
+ description: foregroundDescription,
3801
4040
  currentActivityState: undefined,
4041
+ activeChildren: new Map(),
3802
4042
  nestedRoute,
3803
4043
  interrupt: undefined,
3804
4044
  };
@@ -3870,7 +4110,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3870
4110
  let nestedForegroundStarted = false;
3871
4111
  try {
3872
4112
  const asyncResult = runAsyncPath(execData, deps);
3873
- if (asyncResult) return withForkContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), effectiveParams.context);
4113
+ if (asyncResult) return withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary);
3874
4114
  if (foregroundControl) {
3875
4115
  writeNestedForegroundEvent("subagent.nested.started");
3876
4116
  nestedForegroundStarted = true;
@@ -3878,20 +4118,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3878
4118
  if (hasChain && effectiveParams.chain) {
3879
4119
  const result = await runChainPath(execData, deps);
3880
4120
  writeNestedForegroundEvent("subagent.nested.completed", result);
3881
- return withForkContext(withForkThinkingNotes(result, forkThinkingDowngrades), effectiveParams.context);
4121
+ return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
3882
4122
  }
3883
4123
  if (hasTasks && effectiveParams.tasks) {
3884
4124
  const result = await runParallelPath(execData, deps);
3885
4125
  writeNestedForegroundEvent("subagent.nested.completed", result);
3886
- return withForkContext(withForkThinkingNotes(result, forkThinkingDowngrades), effectiveParams.context);
4126
+ return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
3887
4127
  }
3888
4128
  if (hasSingle) {
3889
4129
  const result = await runSinglePath(execData, deps);
3890
4130
  writeNestedForegroundEvent("subagent.nested.completed", result);
3891
- return withForkContext(withForkThinkingNotes(result, forkThinkingDowngrades), effectiveParams.context);
4131
+ return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
3892
4132
  }
3893
4133
  } catch (error) {
3894
- const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error), forkThinkingDowngrades);
4134
+ const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
3895
4135
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
3896
4136
  return errorResult;
3897
4137
  } finally {
@@ -3904,11 +4144,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3904
4144
  }
3905
4145
  }
3906
4146
 
3907
- return withForkContext({
4147
+ return withResolvedContext({
3908
4148
  content: [{ type: "text", text: "Invalid params" }],
3909
4149
  isError: true,
3910
4150
  details: { mode: "single" as const, results: [] },
3911
- }, effectiveParams.context);
4151
+ }, contextPolicy.contextSummary);
3912
4152
  };
3913
4153
 
3914
4154
  const executeWithSingleDispatchGuard = async (
@@ -3933,5 +4173,26 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3933
4173
  }
3934
4174
  };
3935
4175
 
3936
- return { execute: executeWithSingleDispatchGuard };
4176
+ const executeDelegated = async (
4177
+ id: string,
4178
+ params: SubagentParamsLike,
4179
+ signal: AbortSignal,
4180
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
4181
+ ctx: ExtensionContext,
4182
+ ): Promise<AgentToolResult<Details>> => {
4183
+ const delegatedParams = { ...params };
4184
+ const privateParams = delegatedParams as SubagentParamsLike & {
4185
+ delegatedThinkingOverride?: AgentConfig["thinking"];
4186
+ delegatedAllowZeroToolBudget?: true;
4187
+ };
4188
+ const thinkingOverride = privateParams.delegatedThinkingOverride;
4189
+ const allowZeroToolBudget = privateParams.delegatedAllowZeroToolBudget === true;
4190
+ delete privateParams.delegatedThinkingOverride;
4191
+ delete privateParams.delegatedAllowZeroToolBudget;
4192
+ if (thinkingOverride !== undefined) delegatedThinkingOverrides.set(delegatedParams, thinkingOverride);
4193
+ if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams);
4194
+ return execute(id, delegatedParams, signal, onUpdate, ctx);
4195
+ };
4196
+
4197
+ return { execute: executeWithSingleDispatchGuard, executeDelegated };
3937
4198
  }