pi-subagents 0.35.0 → 0.36.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 (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +132 -24
  3. package/agents/advisor.md +73 -0
  4. package/package.json +8 -12
  5. package/skills/pi-subagents/SKILL.md +22 -9
  6. package/src/agents/agents.ts +22 -5
  7. package/src/api/delegation.ts +125 -0
  8. package/src/extension/config.ts +7 -1
  9. package/src/extension/index.ts +50 -38
  10. package/src/extension/rpc.ts +27 -2
  11. package/src/extension/schemas.ts +22 -2
  12. package/src/extension/tool-description.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +1 -1
  14. package/src/intercom/native-supervisor-channel.ts +45 -6
  15. package/src/intercom/result-intercom.ts +7 -0
  16. package/src/runs/background/async-execution.ts +34 -4
  17. package/src/runs/background/async-job-tracker.ts +4 -0
  18. package/src/runs/background/async-resume.ts +27 -5
  19. package/src/runs/background/async-status.ts +76 -3
  20. package/src/runs/background/chain-append.ts +2 -0
  21. package/src/runs/background/completion-batcher.ts +6 -4
  22. package/src/runs/background/completion-dedupe.ts +2 -11
  23. package/src/runs/background/fleet-view.ts +9 -4
  24. package/src/runs/background/notify.ts +132 -120
  25. package/src/runs/background/result-watcher.ts +138 -78
  26. package/src/runs/background/run-status.ts +3 -1
  27. package/src/runs/background/subagent-runner.ts +225 -43
  28. package/src/runs/background/subagent-wait.ts +130 -4
  29. package/src/runs/background/wait-tool.ts +2 -2
  30. package/src/runs/foreground/chain-execution.ts +176 -111
  31. package/src/runs/foreground/execution.ts +90 -36
  32. package/src/runs/foreground/foreground-control.ts +90 -0
  33. package/src/runs/foreground/subagent-executor.ts +394 -163
  34. package/src/runs/shared/acceptance.ts +55 -13
  35. package/src/runs/shared/agent-contract.ts +38 -0
  36. package/src/runs/shared/child-protocol.ts +1 -1
  37. package/src/runs/shared/completion-guard.ts +36 -5
  38. package/src/runs/shared/context-mode.ts +44 -0
  39. package/src/runs/shared/dynamic-fanout.ts +4 -4
  40. package/src/runs/shared/long-running-guard.ts +4 -0
  41. package/src/runs/shared/nested-events.ts +27 -2
  42. package/src/runs/shared/parallel-handoff.ts +154 -0
  43. package/src/runs/shared/parallel-utils.ts +6 -0
  44. package/src/runs/shared/pi-args.ts +23 -14
  45. package/src/runs/shared/run-history.ts +90 -5
  46. package/src/runs/shared/structured-output.ts +112 -7
  47. package/src/runs/shared/subagent-control.ts +4 -0
  48. package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
  49. package/src/runs/shared/task-intent.ts +10 -5
  50. package/src/runs/shared/tool-availability.ts +3 -1
  51. package/src/runs/shared/tool-budget.ts +11 -5
  52. package/src/runs/shared/turn-budget.ts +2 -1
  53. package/src/runs/shared/worktree.ts +63 -14
  54. package/src/shared/accessible-dir.ts +25 -0
  55. package/src/shared/artifacts.ts +37 -7
  56. package/src/shared/atomic-json.ts +14 -42
  57. package/src/shared/child-transcript.ts +52 -0
  58. package/src/shared/file-system-retry.ts +47 -0
  59. package/src/shared/settings.ts +9 -1
  60. package/src/shared/types.ts +211 -22
  61. package/src/slash/delegation-adapters.ts +152 -5
  62. package/src/slash/delegation-json.ts +108 -0
  63. package/src/slash/delegation-request.ts +182 -36
  64. package/src/slash/prompt-template-bridge.ts +222 -37
  65. package/src/slash/selector.ts +147 -0
  66. package/src/slash/slash-commands.ts +14 -5
  67. package/src/slash/slash-live-state.ts +2 -2
  68. package/src/slash/subagents-admin.ts +42 -42
  69. package/src/tui/fleet-status.ts +362 -0
  70. package/src/tui/fleet-transcript.ts +472 -0
  71. package/src/tui/fleet.ts +318 -59
  72. package/src/tui/render.ts +25 -15
  73. package/src/watchdog/change-signature.ts +105 -12
  74. package/src/watchdog/review.ts +7 -2
  75. package/src/watchdog/runtime.ts +5 -3
  76. 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,13 @@ 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 { isAgentContractV1 } from "../shared/agent-contract.ts";
49
51
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
52
+ import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
50
53
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
51
54
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
55
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
56
+ import { summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
52
57
  import {
53
58
  attachNestedChildrenToResultChildren,
54
59
  buildSubagentResultIntercomPayload,
@@ -81,13 +86,16 @@ import {
81
86
  type AgentProgress,
82
87
  type AsyncStatus,
83
88
  type AcceptanceInput,
89
+ type AgentContract,
84
90
  type ArtifactConfig,
85
91
  type ArtifactPaths,
86
92
  type ControlConfig,
87
93
  type ControlEvent,
88
94
  type Details,
89
95
  type ExtensionConfig,
96
+ type ForegroundRunControl,
90
97
  type IntercomEventBus,
98
+ type JsonSchemaObject,
91
99
  type MaxOutputConfig,
92
100
  type NestedRouteInfo,
93
101
  type NestedRunSummary,
@@ -127,7 +135,9 @@ interface TaskParam {
127
135
  progress?: boolean;
128
136
  model?: string;
129
137
  skill?: string | string[] | boolean;
138
+ outputSchema?: JsonSchemaObject;
130
139
  acceptance?: AcceptanceInput;
140
+ agentContract?: AgentContract;
131
141
  toolBudget?: ToolBudgetConfig;
132
142
  }
133
143
 
@@ -142,6 +152,7 @@ export interface SubagentParamsLike {
142
152
  agent?: string;
143
153
  task?: string;
144
154
  message?: string;
155
+ steeringRecovery?: boolean;
145
156
  chain?: ChainStep[];
146
157
  tasks?: TaskParam[];
147
158
  concurrency?: number;
@@ -152,6 +163,8 @@ export interface SubagentParamsLike {
152
163
  timeoutMs?: number;
153
164
  maxRuntimeMs?: number;
154
165
  turnBudget?: TurnBudgetConfig;
166
+ /** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */
167
+ enforceHardTurnLimit?: boolean;
155
168
  toolBudget?: ToolBudgetConfig;
156
169
  clarify?: boolean;
157
170
  share?: boolean;
@@ -168,9 +181,11 @@ export interface SubagentParamsLike {
168
181
  skill?: string | string[] | boolean;
169
182
  output?: string | boolean;
170
183
  outputMode?: "inline" | "file-only";
184
+ outputSchema?: JsonSchemaObject;
171
185
  agentScope?: unknown;
172
186
  chainDir?: string;
173
187
  acceptance?: AcceptanceInput;
188
+ agentContract?: AgentContract;
174
189
  schedule?: string;
175
190
  scheduleName?: string;
176
191
  additional?: number;
@@ -220,6 +235,7 @@ interface ExecutionContextData {
220
235
  deadlineAt?: number;
221
236
  turnBudget?: ResolvedTurnBudget;
222
237
  toolBudget?: ResolvedToolBudget;
238
+ allowZeroToolBudget?: boolean;
223
239
  configToolBudget?: ResolvedToolBudget;
224
240
  contextPolicy: AgentDefaultContextPolicy;
225
241
  modelScope?: ModelScopeConfig;
@@ -351,6 +367,19 @@ function trimRememberedForegroundRuns(state: SubagentState): void {
351
367
  }
352
368
  }
353
369
 
370
+ function foregroundChildActivityFromProgress(progress: SingleResult["progress"] | undefined) {
371
+ return {
372
+ ...(progress?.activityState ? { activityState: progress.activityState } : {}),
373
+ ...(progress?.lastActivityAt !== undefined ? { lastActivityAt: progress.lastActivityAt } : {}),
374
+ ...(progress?.currentTool ? { currentTool: progress.currentTool } : {}),
375
+ ...(progress?.currentToolStartedAt !== undefined ? { currentToolStartedAt: progress.currentToolStartedAt } : {}),
376
+ ...(progress?.currentPath ? { currentPath: progress.currentPath } : {}),
377
+ ...(progress?.turnCount !== undefined ? { turnCount: progress.turnCount } : {}),
378
+ ...(progress?.tokens !== undefined ? { tokens: progress.tokens } : {}),
379
+ ...(progress?.toolCount !== undefined ? { toolCount: progress.toolCount } : {}),
380
+ };
381
+ }
382
+
354
383
  function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[] }): void {
355
384
  state.foregroundRuns ??= new Map();
356
385
  const previous = state.foregroundRuns.get(input.runId);
@@ -365,7 +394,9 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
365
394
  const child = {
366
395
  agent: result.agent,
367
396
  index,
397
+ ...(result.context ? { context: result.context } : {}),
368
398
  status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
399
+ ...foregroundChildActivityFromProgress(result.progress),
369
400
  updatedAt,
370
401
  ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
371
402
  ...(result.error ? { error: result.error } : {}),
@@ -387,6 +418,29 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
387
418
  trimRememberedForegroundRuns(state);
388
419
  }
389
420
 
421
+ function applyControlEventToRememberedForegroundRun(state: SubagentState, event: ControlEvent): void {
422
+ const run = state.foregroundRuns?.get(event.runId);
423
+ if (!run) return;
424
+ const index = event.index ?? (run.children.length === 1 ? run.children[0]?.index : undefined);
425
+ if (index === undefined) return;
426
+ const child = run.children[index];
427
+ if (!child || child.status !== "detached") return;
428
+ const updatedAt = event.ts;
429
+ run.updatedAt = updatedAt;
430
+ run.children[index] = {
431
+ ...child,
432
+ activityState: event.to,
433
+ updatedAt,
434
+ ...(event.elapsedMs !== undefined ? { lastActivityAt: event.ts - event.elapsedMs } : {}),
435
+ ...(event.currentTool ? { currentTool: event.currentTool } : {}),
436
+ ...(event.currentToolDurationMs !== undefined ? { currentToolStartedAt: event.ts - event.currentToolDurationMs } : {}),
437
+ ...(event.currentPath ? { currentPath: event.currentPath } : {}),
438
+ ...(event.turns !== undefined ? { turnCount: event.turns } : {}),
439
+ ...(event.tokens !== undefined ? { tokens: event.tokens } : {}),
440
+ ...(event.toolCount !== undefined ? { toolCount: event.toolCount } : {}),
441
+ };
442
+ }
443
+
390
444
  function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; index: number; result: SingleResult; events: IntercomEventBus }): void {
391
445
  state.foregroundRuns ??= new Map();
392
446
  const updatedAt = Date.now();
@@ -401,9 +455,11 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
401
455
  ...child,
402
456
  agent: input.result.agent,
403
457
  index: input.index,
458
+ ...(input.result.context ? { context: input.result.context } : {}),
404
459
  status: input.result.acceptance?.status === "rejected"
405
460
  ? "failed"
406
461
  : resolveSubagentResultStatus({ exitCode: input.result.exitCode, interrupted: input.result.interrupted, detached: false }),
462
+ ...foregroundChildActivityFromProgress(input.result.progress),
407
463
  updatedAt,
408
464
  ...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}),
409
465
  ...(input.result.error ? { error: input.result.error } : {}),
@@ -424,6 +480,9 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
424
480
  const summary = !success && input.result.error
425
481
  ? `${input.result.error}${output ? `\n\nOutput:\n${output}` : ""}`
426
482
  : output || input.result.error || "Detached child exited without final output.";
483
+ // A detached callback may outlive its extension runtime. Stale sessions are
484
+ // intentionally dropped rather than routed through a replacement runtime.
485
+ if (!input.sessionId || input.sessionId !== state.currentSessionId) return;
427
486
  input.events.emit(SUBAGENT_FOREGROUND_COMPLETE_EVENT, {
428
487
  id: `${input.runId}:${input.index}`,
429
488
  runId: input.runId,
@@ -824,6 +883,7 @@ function appendStepToAsyncChain(input: {
824
883
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
825
884
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
826
885
  waitToolEnabled: input.deps.waitToolEnabled,
886
+ contextForAgent: contextPolicy.contextForAgent,
827
887
  asyncDir: resolved.location.asyncDir,
828
888
  validateOutputBindings: false,
829
889
  });
@@ -1174,7 +1234,7 @@ async function resumeAsyncRun(input: {
1174
1234
  };
1175
1235
  }
1176
1236
  const runId = randomUUID().slice(0, 8);
1177
- const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
1237
+ const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1178
1238
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1179
1239
  const contextPolicy = resolveExplicitContextPolicy(input.params);
1180
1240
  const workflowTask = (input.params.task ?? followUp) || undefined;
@@ -1207,11 +1267,12 @@ async function resumeAsyncRun(input: {
1207
1267
  availableModels,
1208
1268
  cwd: effectiveCwd,
1209
1269
  maxOutput: input.params.maxOutput,
1210
- artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd),
1270
+ artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir),
1211
1271
  artifactConfig,
1212
1272
  shareEnabled: input.params.share === true,
1213
1273
  sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
1214
1274
  chainSkills: normalized === false ? [] : (normalized ?? []),
1275
+ agentContract: input.params.agentContract,
1215
1276
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
1216
1277
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1217
1278
  waitToolEnabled: input.deps.waitToolEnabled,
@@ -1237,8 +1298,8 @@ async function resumeAsyncRun(input: {
1237
1298
 
1238
1299
  const runId = randomUUID().slice(0, 8);
1239
1300
  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);
1301
+ const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1302
+ const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1242
1303
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1243
1304
  const result = executeAsyncSingle(runId, {
1244
1305
  agent: target.agent,
@@ -1283,6 +1344,8 @@ async function resumeAsyncRun(input: {
1283
1344
  availableModels,
1284
1345
  output: recoveryDescriptor?.outputPath,
1285
1346
  outputMode: recoveryDescriptor?.outputMode,
1347
+ ...(recoveryDescriptor?.agentContract ? { agentContract: recoveryDescriptor.agentContract } : {}),
1348
+ ...(recoveryDescriptor?.structuredOutputSchema ? { structuredOutputSchema: recoveryDescriptor.structuredOutputSchema } : {}),
1286
1349
  ...(recoveryDescriptor?.skills ? { skills: [...recoveryDescriptor.skills] } : {}),
1287
1350
  ...(recoveryDescriptor?.acceptance !== undefined && input.params.acceptance === undefined ? { acceptance: recoveryDescriptor.acceptance } : {}),
1288
1351
  ...(input.params.timeoutMs !== undefined ? { timeoutMs: input.params.timeoutMs } : {}),
@@ -1322,19 +1385,22 @@ function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string
1322
1385
  if (output && output !== error.trim()) {
1323
1386
  lines.push("", "Output:", output);
1324
1387
  }
1325
- if (result.artifactPaths?.outputPath) {
1388
+ if (result.artifactPaths?.outputPath && fs.existsSync(result.artifactPaths.outputPath)) {
1326
1389
  lines.push("", `Output artifact: ${result.artifactPaths.outputPath}`);
1327
1390
  }
1328
1391
  return lines.join("\n");
1329
1392
  }
1330
1393
 
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
- });
1394
+ function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi" | "state">): (event: ControlEvent) => void {
1395
+ return (event) => {
1396
+ applyControlEventToRememberedForegroundRun(deps.state, event);
1397
+ emitControlNotification({
1398
+ pi: deps.pi,
1399
+ controlConfig: data.controlConfig,
1400
+ intercomBridge: data.intercomBridge,
1401
+ event,
1402
+ });
1403
+ };
1338
1404
  }
1339
1405
 
1340
1406
  async function emitForegroundResultIntercom(input: {
@@ -1345,6 +1411,7 @@ async function emitForegroundResultIntercom(input: {
1345
1411
  results: SingleResult[];
1346
1412
  chainSteps?: number;
1347
1413
  nestedChildren?: NestedRunSummary[];
1414
+ parallelHandoff?: Details["parallelHandoff"];
1348
1415
  }): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
1349
1416
  if (!input.intercomBridge.active || !input.intercomBridge.orchestratorTarget) return null;
1350
1417
  const children = input.results.flatMap((result, index) => result.detached ? [] : [{
@@ -1368,6 +1435,7 @@ async function emitForegroundResultIntercom(input: {
1368
1435
  source: "foreground",
1369
1436
  children: attachNestedChildrenToResultChildren(input.runId, children, input.nestedChildren),
1370
1437
  ...(typeof input.chainSteps === "number" ? { chainSteps: input.chainSteps } : {}),
1438
+ ...(input.parallelHandoff ? { parallelHandoff: input.parallelHandoff } : {}),
1371
1439
  });
1372
1440
  const delivered = await deliverSubagentResultIntercomEvent(input.pi.events, payload);
1373
1441
  if (!delivered) return null;
@@ -1390,6 +1458,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1390
1458
  results: input.details.results,
1391
1459
  ...(typeof input.details.totalSteps === "number" ? { chainSteps: input.details.totalSteps } : {}),
1392
1460
  ...(input.nestedChildren?.length ? { nestedChildren: input.nestedChildren } : {}),
1461
+ ...(input.details.parallelHandoff ? { parallelHandoff: input.details.parallelHandoff } : {}),
1393
1462
  });
1394
1463
  if (!payload) return null;
1395
1464
  return {
@@ -1531,7 +1600,8 @@ function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1531
1600
 
1532
1601
  interface AgentDefaultContextPolicy {
1533
1602
  params: SubagentParamsLike;
1534
- contextForAgent(agentName: string): "fresh" | "fork";
1603
+ contextForAgent(agentName: string): ContextMode;
1604
+ contextSummary?: ContextSummary;
1535
1605
  usesFork: boolean;
1536
1606
  }
1537
1607
 
@@ -1540,12 +1610,15 @@ function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: Ag
1540
1610
  return resolveExplicitContextPolicy(params);
1541
1611
  }
1542
1612
  const byName = new Map(agents.map((agent) => [agent.name, agent]));
1543
- const contextForAgent = (agentName: string): "fresh" | "fork" =>
1613
+ const contextForAgent = (agentName: string): ContextMode =>
1544
1614
  byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh";
1545
- const usesFork = collectRequestedAgentNames(params).some((name) => contextForAgent(name) === "fork");
1615
+ const requestedAgentNames = collectRequestedAgentNames(params);
1616
+ const contextSummary = summarizeContextModes(requestedAgentNames.map((name) => contextForAgent(name)));
1617
+ const usesFork = contextSummary === "fork" || contextSummary === "mixed";
1546
1618
  return {
1547
- params: usesFork ? { ...params, context: "fork" } : params,
1619
+ params,
1548
1620
  contextForAgent,
1621
+ contextSummary,
1549
1622
  usesFork,
1550
1623
  };
1551
1624
  }
@@ -1555,6 +1628,7 @@ function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultC
1555
1628
  return {
1556
1629
  params,
1557
1630
  contextForAgent: () => context,
1631
+ contextSummary: context,
1558
1632
  usesFork: context === "fork",
1559
1633
  };
1560
1634
  }
@@ -1571,8 +1645,12 @@ function shouldForkAgent(contextPolicy: AgentDefaultContextPolicy, agentName: st
1571
1645
  return contextPolicy.contextForAgent(agentName) === "fork";
1572
1646
  }
1573
1647
 
1648
+ function summarizeResultContext(details: Details, fallback: ContextSummary | undefined): ContextSummary | undefined {
1649
+ return summarizeContextModes(details.results.map((result) => result.context)) ?? fallback;
1650
+ }
1651
+
1574
1652
  function buildRequestedModeError(params: SubagentParamsLike, message: string): AgentToolResult<Details> {
1575
- return withForkContext(
1653
+ return withResolvedContext(
1576
1654
  {
1577
1655
  content: [{ type: "text", text: message }],
1578
1656
  isError: true,
@@ -1617,8 +1695,12 @@ function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: num
1617
1695
  return { timeoutMs: rawTimeout ?? rawMaxRuntime };
1618
1696
  }
1619
1697
 
1620
- function resolveToolBudget(raw: unknown, label = "toolBudget"): { toolBudget?: ResolvedToolBudget; error?: string } {
1621
- const resolved = validateToolBudgetConfig(raw, label);
1698
+ function resolveToolBudget(
1699
+ raw: unknown,
1700
+ label = "toolBudget",
1701
+ options?: { minimumHard?: 0 | 1 },
1702
+ ): { toolBudget?: ResolvedToolBudget; error?: string } {
1703
+ const resolved = validateToolBudgetConfig(raw, label, options);
1622
1704
  return { toolBudget: resolved.budget, error: resolved.error };
1623
1705
  }
1624
1706
 
@@ -1688,16 +1770,18 @@ function normalizeRepeatedParallelCounts(params: SubagentParamsLike): { params?:
1688
1770
  return { params };
1689
1771
  }
1690
1772
 
1691
- function withForkContext(
1773
+ function withResolvedContext(
1692
1774
  result: AgentToolResult<Details>,
1693
- context: SubagentParamsLike["context"],
1775
+ fallback: ContextSummary | undefined,
1694
1776
  ): AgentToolResult<Details> {
1695
- if (context !== "fork" || !result.details) return result;
1777
+ if (!result.details) return result;
1778
+ const context = summarizeResultContext(result.details, fallback);
1779
+ if (!context) return result;
1696
1780
  return {
1697
1781
  ...result,
1698
1782
  details: {
1699
1783
  ...result.details,
1700
- context: "fork",
1784
+ context,
1701
1785
  },
1702
1786
  };
1703
1787
  }
@@ -1715,15 +1799,15 @@ function withForkThinkingNotes(
1715
1799
  return { ...result, content: [...result.content, { type: "text", text: note }] };
1716
1800
  }
1717
1801
 
1718
- function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): AgentToolResult<Details> {
1802
+ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown, contextSummary?: ContextSummary): AgentToolResult<Details> {
1719
1803
  const message = error instanceof Error ? error.message : String(error);
1720
- return withForkContext(
1804
+ return withResolvedContext(
1721
1805
  {
1722
1806
  content: [{ type: "text", text: message }],
1723
1807
  isError: true,
1724
1808
  details: { mode: getRequestedModeLabel(params), results: [] },
1725
1809
  },
1726
- params.context,
1810
+ contextSummary,
1727
1811
  );
1728
1812
  }
1729
1813
 
@@ -1969,6 +2053,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1969
2053
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
1970
2054
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
1971
2055
  ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
2056
+ ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
2057
+ ...(task.agentContract !== undefined ? { agentContract: task.agentContract } : {}),
1972
2058
  ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
1973
2059
  }));
1974
2060
  return executeAsyncChain(id, {
@@ -1991,12 +2077,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1991
2077
  chainSkills: [],
1992
2078
  sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
1993
2079
  thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
2080
+ contextForAgent: contextPolicy.contextForAgent,
1994
2081
  maxSubagentDepth: currentMaxSubagentDepth,
1995
2082
  waitToolEnabled: deps.waitToolEnabled,
1996
2083
  worktreeSetupHook: deps.config.worktreeSetupHook,
1997
2084
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1998
2085
  worktreeBaseDir: deps.config.worktreeBaseDir,
1999
2086
  controlConfig,
2087
+ agentContract: params.agentContract,
2000
2088
  controlIntercomTarget,
2001
2089
  childIntercomTarget,
2002
2090
  nestedRoute,
@@ -2029,6 +2117,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2029
2117
  chainSkills,
2030
2118
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2031
2119
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2120
+ contextForAgent: contextPolicy.contextForAgent,
2032
2121
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2033
2122
  maxSubagentDepth: currentMaxSubagentDepth,
2034
2123
  waitToolEnabled: deps.waitToolEnabled,
@@ -2036,6 +2125,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2036
2125
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2037
2126
  worktreeBaseDir: deps.config.worktreeBaseDir,
2038
2127
  controlConfig,
2128
+ agentContract: params.agentContract,
2039
2129
  controlIntercomTarget,
2040
2130
  childIntercomTarget,
2041
2131
  nestedRoute,
@@ -2077,6 +2167,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2077
2167
  shareEnabled,
2078
2168
  sessionRoot,
2079
2169
  sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2170
+ context: contextPolicy.contextForAgent(params.agent!),
2080
2171
  skills,
2081
2172
  output: effectiveOutput,
2082
2173
  outputMode: effectiveOutputMode,
@@ -2092,6 +2183,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2092
2183
  controlIntercomTarget,
2093
2184
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
2094
2185
  nestedRoute,
2186
+ agentContract: params.agentContract,
2187
+ structuredOutputSchema: params.outputSchema,
2095
2188
  acceptance: params.acceptance,
2096
2189
  timeoutMs: data.timeoutMs,
2097
2190
  turnBudget: data.turnBudget,
@@ -2145,6 +2238,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2145
2238
  sessionFileForIndex,
2146
2239
  sessionFileForTask,
2147
2240
  thinkingOverrideForTask,
2241
+ contextForAgent: contextPolicy.contextForAgent,
2148
2242
  artifactsDir,
2149
2243
  artifactConfig,
2150
2244
  includeProgress: params.includeProgress,
@@ -2152,6 +2246,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2152
2246
  onUpdate,
2153
2247
  onControlEvent,
2154
2248
  controlConfig,
2249
+ agentContract: params.agentContract,
2155
2250
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
2156
2251
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2157
2252
  foregroundControl,
@@ -2210,6 +2305,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2210
2305
  chainSkills: chainResult.requestedAsync.chainSkills,
2211
2306
  sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2212
2307
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2308
+ contextForAgent: contextPolicy.contextForAgent,
2213
2309
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2214
2310
  maxSubagentDepth: currentMaxSubagentDepth,
2215
2311
  waitToolEnabled: deps.waitToolEnabled,
@@ -2217,6 +2313,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2217
2313
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2218
2314
  worktreeBaseDir: deps.config.worktreeBaseDir,
2219
2315
  controlConfig,
2316
+ agentContract: params.agentContract,
2220
2317
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2221
2318
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2222
2319
  nestedRoute: data.nestedRoute,
@@ -2260,6 +2357,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2260
2357
  interface ForegroundParallelRunInput {
2261
2358
  tasks: TaskParam[];
2262
2359
  taskTexts: string[];
2360
+ taskDescriptions: string[];
2263
2361
  agents: AgentConfig[];
2264
2362
  ctx: ExtensionContext;
2265
2363
  state: SubagentState;
@@ -2286,6 +2384,7 @@ interface ForegroundParallelRunInput {
2286
2384
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
2287
2385
  firstProgressIndex: number;
2288
2386
  controlConfig: ResolvedControlConfig;
2387
+ contextPolicy: AgentDefaultContextPolicy;
2289
2388
  onControlEvent?: (event: ControlEvent) => void;
2290
2389
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
2291
2390
  orchestratorIntercomTarget?: string;
@@ -2300,8 +2399,8 @@ interface ForegroundParallelRunInput {
2300
2399
  deadlineAt?: number;
2301
2400
  turnBudget?: ResolvedTurnBudget;
2302
2401
  toolBudgets: (ResolvedToolBudget | undefined)[];
2402
+ agentContract?: AgentContract;
2303
2403
  }
2304
-
2305
2404
  function buildParallelModeError(message: string): AgentToolResult<Details> {
2306
2405
  return {
2307
2406
  content: [{ type: "text", text: message }],
@@ -2374,15 +2473,52 @@ function resolveParallelTaskCwd(
2374
2473
  return resolveChildCwd(paramsCwd, task.cwd);
2375
2474
  }
2376
2475
 
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);
2476
+ function finalizeParallelWorktreeHandoff(input: {
2477
+ worktreeSetup: WorktreeSetup;
2478
+ artifactsDir: string;
2479
+ runId: string;
2480
+ cwd: string;
2481
+ tasks: TaskParam[];
2482
+ results: SingleResult[];
2483
+ }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
2484
+ const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
2485
+ const diffs = diffWorktrees(input.worktreeSetup, input.tasks.map((task) => task.agent), diffsDir);
2486
+ const cleanup = cleanupWorktrees(input.worktreeSetup);
2487
+ const diffSummary = formatWorktreeDiffSummary(diffs);
2488
+ try {
2489
+ const reference = writeParallelHandoffGroup({
2490
+ manifestPath: parallelHandoffPath(input.artifactsDir, input.runId),
2491
+ runId: input.runId,
2492
+ mode: "parallel",
2493
+ source: "foreground",
2494
+ cwd: input.cwd,
2495
+ stepIndex: 0,
2496
+ flatStartIndex: 0,
2497
+ setup: input.worktreeSetup,
2498
+ diffs,
2499
+ cleanup,
2500
+ results: input.results.map((result) => ({
2501
+ agent: result.agent,
2502
+ status: resolveSubagentResultStatus({
2503
+ exitCode: result.exitCode,
2504
+ interrupted: result.interrupted,
2505
+ detached: result.detached,
2506
+ state: result.stopped ? "stopped" : undefined,
2507
+ }),
2508
+ summary: resultSummaryForIntercom(result),
2509
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
2510
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
2511
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
2512
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
2513
+ })),
2514
+ });
2515
+ return {
2516
+ suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"),
2517
+ reference,
2518
+ };
2519
+ } catch (error) {
2520
+ return { suffix: [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n") };
2521
+ }
2386
2522
  }
2387
2523
 
2388
2524
  function findDuplicateParallelOutputPath(input: {
@@ -2435,20 +2571,23 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2435
2571
  );
2436
2572
  const interruptController = new AbortController();
2437
2573
  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
- };
2574
+ beginForegroundChild(input.foregroundControl, {
2575
+ index,
2576
+ agent: task.agent,
2577
+ description: input.taskDescriptions[index],
2578
+ interrupt: () => {
2579
+ if (interruptController.signal.aborted) return false;
2580
+ interruptController.abort();
2581
+ return true;
2582
+ },
2583
+ });
2449
2584
  }
2585
+ const structuredRuntime = task.outputSchema
2586
+ ? createStructuredOutputRuntime(task.outputSchema, path.join(input.artifactsDir, "structured-output", input.runId))
2587
+ : undefined;
2450
2588
  return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
2451
2589
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
2590
+ context: input.contextPolicy.contextForAgent(task.agent),
2452
2591
  cwd: taskCwd,
2453
2592
  signal: input.signal,
2454
2593
  interruptSignal: interruptController.signal,
@@ -2478,6 +2617,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2478
2617
  preferredModelProvider: input.ctx.model?.provider,
2479
2618
  modelScope: input.modelScope,
2480
2619
  skills: effectiveSkills === false ? [] : effectiveSkills,
2620
+ structuredOutput: structuredRuntime,
2621
+ agentContract: task.agentContract ?? input.agentContract,
2481
2622
  acceptance: task.acceptance,
2482
2623
  acceptanceContext: { mode: "parallel" },
2483
2624
  timeoutMs: input.timeoutMs,
@@ -2489,18 +2630,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2489
2630
  const stepResults = progressUpdate.details?.results || [];
2490
2631
  const stepProgress = progressUpdate.details?.progress || [];
2491
2632
  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();
2633
+ updateForegroundChild(input.foregroundControl, index, stepProgress[0]);
2504
2634
  }
2505
2635
  if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
2506
2636
  if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
@@ -2519,10 +2649,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2519
2649
  }
2520
2650
  : undefined,
2521
2651
  }).finally(() => {
2522
- if (input.foregroundControl?.currentIndex === index) {
2523
- input.foregroundControl.interrupt = undefined;
2524
- input.foregroundControl.updatedAt = Date.now();
2525
- }
2652
+ if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
2526
2653
  });
2527
2654
  }, input.globalSemaphore);
2528
2655
  }
@@ -2687,7 +2814,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2687
2814
  ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
2688
2815
  ...(progress !== undefined ? { progress } : {}),
2689
2816
  ...(t.toolBudget !== undefined ? { toolBudget: t.toolBudget } : {}),
2817
+ ...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
2690
2818
  ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
2819
+ ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
2691
2820
  };
2692
2821
  });
2693
2822
  return executeAsyncChain(id, {
@@ -2706,22 +2835,33 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2706
2835
  chainSkills: [],
2707
2836
  sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
2708
2837
  thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
2838
+ contextForAgent: contextPolicy.contextForAgent,
2709
2839
  maxSubagentDepth: currentMaxSubagentDepth,
2710
2840
  waitToolEnabled: deps.waitToolEnabled,
2711
2841
  worktreeSetupHook: deps.config.worktreeSetupHook,
2712
2842
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2713
2843
  worktreeBaseDir: deps.config.worktreeBaseDir,
2714
2844
  controlConfig,
2845
+ agentContract: params.agentContract,
2715
2846
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2716
2847
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2848
+ nestedRoute: data.nestedRoute,
2717
2849
  timeoutMs: data.timeoutMs,
2718
2850
  turnBudget: data.turnBudget,
2851
+ toolBudget: data.toolBudget,
2852
+ configToolBudget: data.configToolBudget,
2719
2853
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2720
2854
  });
2721
2855
  }
2722
2856
  }
2723
2857
 
2724
- const behaviors = agentConfigs.map((config, index) => suppressProgressForReadOnlyTask(resolveStepBehavior(config, behaviorOverrides[index]!), taskTexts[index]));
2858
+ const behaviors = tasks.map((task, index) => {
2859
+ let behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfigs[index]!, behaviorOverrides[index]!), taskTexts[index]);
2860
+ if (behaviorOverrides[index]?.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output)) {
2861
+ behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, behavior.output) };
2862
+ }
2863
+ return behavior;
2864
+ });
2725
2865
  const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
2726
2866
  const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
2727
2867
  const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
@@ -2737,6 +2877,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2737
2877
  );
2738
2878
  if (errorResult) return errorResult;
2739
2879
 
2880
+ let worktreeFinalized = false;
2740
2881
  try {
2741
2882
  const outputBaseDir = path.join(artifactsDir, "outputs", runId);
2742
2883
  const duplicateOutputError = findDuplicateParallelOutputPath({
@@ -2759,6 +2900,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2759
2900
  const parallelProgressDir = path.join(artifactsDir, "progress", runId);
2760
2901
  if (parallelProgressPrecreated) writeInitialProgressFile(parallelProgressDir);
2761
2902
 
2903
+ const taskDescriptions = taskTexts.map((taskText) => taskText.trim());
2762
2904
  for (let i = 0; i < taskTexts.length; i++) {
2763
2905
  if (shouldForkAgent(contextPolicy, tasks[i]!.agent)) taskTexts[i] = wrapForkTask(taskTexts[i]!);
2764
2906
  }
@@ -2767,6 +2909,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2767
2909
  const results = await runForegroundParallelTasks({
2768
2910
  tasks,
2769
2911
  taskTexts,
2912
+ taskDescriptions,
2770
2913
  agents,
2771
2914
  ctx,
2772
2915
  state: deps.state,
@@ -2791,6 +2934,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2791
2934
  behaviors,
2792
2935
  firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
2793
2936
  controlConfig,
2937
+ contextPolicy,
2794
2938
  onControlEvent,
2795
2939
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
2796
2940
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
@@ -2807,6 +2951,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2807
2951
  deadlineAt,
2808
2952
  turnBudget: data.turnBudget,
2809
2953
  toolBudgets,
2954
+ agentContract: params.agentContract,
2810
2955
  });
2811
2956
  for (let i = 0; i < results.length; i++) {
2812
2957
  const run = results[i]!;
@@ -2822,6 +2967,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2822
2967
  updateForegroundNestedProjection(foregroundControl);
2823
2968
  attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
2824
2969
  }
2970
+ let handoff: ReturnType<typeof finalizeParallelWorktreeHandoff> | undefined;
2971
+ if (worktreeSetup) {
2972
+ worktreeFinalized = true;
2973
+ handoff = finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
2974
+ }
2825
2975
  const interrupted = results.find((result) => result.interrupted);
2826
2976
  const details = compactForegroundDetails({
2827
2977
  mode: "parallel",
@@ -2831,6 +2981,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2831
2981
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
2832
2982
  totalChildUsage: sumResultsUsage(results),
2833
2983
  totalCost: sumResultsCost(results),
2984
+ ...(handoff?.reference ? { parallelHandoff: handoff.reference } : {}),
2834
2985
  });
2835
2986
  rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
2836
2987
  if (interrupted) {
@@ -2864,7 +3015,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2864
3015
  };
2865
3016
  }
2866
3017
 
2867
- const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks);
3018
+ const worktreeSuffix = handoff?.suffix ?? "";
2868
3019
  const ok = results.filter((result) => result.exitCode === 0).length;
2869
3020
  const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
2870
3021
  const aggregatedOutput = aggregateParallelOutputs(
@@ -2888,7 +3039,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2888
3039
  details,
2889
3040
  };
2890
3041
  } finally {
2891
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
3042
+ if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
2892
3043
  }
2893
3044
  }
2894
3045
 
@@ -2924,7 +3075,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2924
3075
  };
2925
3076
  }
2926
3077
  const effectiveToolBudget = resolveEffectiveToolBudget({ runBudget: data.toolBudget, agentBudget: agentConfig.toolBudget, configBudget: data.configToolBudget });
2927
- if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error));
3078
+ if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error), data.contextPolicy.contextSummary);
2928
3079
 
2929
3080
  const currentProvider = ctx.model?.provider;
2930
3081
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
@@ -3009,6 +3160,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3009
3160
  shareEnabled,
3010
3161
  sessionRoot,
3011
3162
  sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3163
+ context: contextPolicy.contextForAgent(params.agent!),
3012
3164
  skills: skillOverride === false ? [] : skillOverride,
3013
3165
  output: effectiveOutput,
3014
3166
  outputMode: effectiveOutputMode,
@@ -3023,9 +3175,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3023
3175
  controlConfig,
3024
3176
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3025
3177
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3178
+ nestedRoute: data.nestedRoute,
3179
+ agentContract: params.agentContract,
3180
+ structuredOutputSchema: params.outputSchema,
3181
+ acceptance: params.acceptance,
3026
3182
  timeoutMs: data.timeoutMs,
3027
3183
  turnBudget: data.turnBudget,
3028
3184
  toolBudget: effectiveToolBudget.toolBudget,
3185
+ allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3029
3186
  });
3030
3187
  }
3031
3188
  }
@@ -3039,6 +3196,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3039
3196
  if (validationError) {
3040
3197
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
3041
3198
  }
3199
+ const structuredRuntime = params.outputSchema
3200
+ ? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined)
3201
+ : undefined;
3042
3202
  task = injectSingleOutputInstruction(task, outputPath, agentConfig);
3043
3203
 
3044
3204
  let effectiveSkills: string[] | undefined;
@@ -3050,91 +3210,76 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3050
3210
  const interruptController = new AbortController();
3051
3211
  const foregroundControl = deps.state.foregroundControls.get(runId);
3052
3212
  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
- };
3213
+ beginForegroundChild(foregroundControl, {
3214
+ index: 0,
3215
+ agent: params.agent!,
3216
+ description: foregroundControl.description,
3217
+ interrupt: () => {
3218
+ if (interruptController.signal.aborted) return false;
3219
+ interruptController.abort();
3220
+ return true;
3221
+ },
3222
+ });
3064
3223
  }
3065
3224
 
3066
3225
  const forwardSingleUpdate = onUpdate
3067
3226
  ? (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
- }
3227
+ if (foregroundControl) updateForegroundChild(foregroundControl, 0, update.details?.progress?.[0]);
3082
3228
  onUpdate(update);
3083
3229
  }
3084
3230
  : undefined;
3085
3231
 
3086
3232
  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();
3233
+ let r: Awaited<ReturnType<typeof runSync>>;
3234
+ try {
3235
+ r = await runSync(ctx.cwd, agents, params.agent!, task, {
3236
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3237
+ context: data.contextPolicy.contextForAgent(params.agent!),
3238
+ cwd: effectiveCwd,
3239
+ signal,
3240
+ interruptSignal: interruptController.signal,
3241
+ allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
3242
+ intercomEvents: deps.pi.events,
3243
+ runId,
3244
+ sessionDir: sessionDirForIndex(0),
3245
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3246
+ share: shareEnabled,
3247
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3248
+ artifactConfig,
3249
+ maxOutput: params.maxOutput,
3250
+ outputPath,
3251
+ outputMode: effectiveOutputMode,
3252
+ maxSubagentDepth,
3253
+ waitToolEnabled: deps.waitToolEnabled,
3254
+ onUpdate: forwardSingleUpdate,
3255
+ controlConfig,
3256
+ onControlEvent,
3257
+ intercomSessionName: childIntercomTarget,
3258
+ orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3259
+ nestedRoute: foregroundControl?.nestedRoute,
3260
+ index: 0,
3261
+ modelOverride,
3262
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
3263
+ availableModels,
3264
+ preferredModelProvider: currentProvider,
3265
+ modelScope: data.modelScope,
3266
+ skills: effectiveSkills,
3267
+ structuredOutput: structuredRuntime,
3268
+ agentContract: params.agentContract,
3269
+ acceptance: params.acceptance,
3270
+ acceptanceContext: { mode: "single" },
3271
+ onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events }),
3272
+ timeoutMs: data.timeoutMs,
3273
+ deadlineAt,
3274
+ turnBudget: data.turnBudget,
3275
+ enforceHardTurnLimit: params.enforceHardTurnLimit,
3276
+ toolBudget: effectiveToolBudget.toolBudget,
3277
+ allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3278
+ });
3279
+ } finally {
3280
+ if (!artifactConfig.enabled) cleanupStructuredOutputRuntime(structuredRuntime);
3137
3281
  }
3282
+ if (foregroundControl) finishForegroundChild(foregroundControl, 0);
3138
3283
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
3139
3284
 
3140
3285
  if (r.progress) allProgress.push(r.progress);
@@ -3254,7 +3399,21 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3254
3399
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3255
3400
  ctx: ExtensionContext,
3256
3401
  ) => Promise<AgentToolResult<Details>>;
3402
+ /**
3403
+ * Correlated extension-to-extension delegation owns its request IDs and
3404
+ * cancellation controllers, so independent requests may execute concurrently.
3405
+ * The ordinary model-facing tool keeps the one-foreground-call-per-turn guard.
3406
+ */
3407
+ executeDelegated: (
3408
+ id: string,
3409
+ params: SubagentParamsLike,
3410
+ signal: AbortSignal,
3411
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3412
+ ctx: ExtensionContext,
3413
+ ) => Promise<AgentToolResult<Details>>;
3257
3414
  } {
3415
+ const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
3416
+ const delegatedZeroToolBudgets = new WeakSet<object>();
3258
3417
  const execute = async (
3259
3418
  _id: string,
3260
3419
  params: SubagentParamsLike,
@@ -3262,6 +3421,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3262
3421
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3263
3422
  ctx: ExtensionContext,
3264
3423
  ): Promise<AgentToolResult<Details>> => {
3424
+ const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
3425
+ const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
3265
3426
  deps.state.baseCwd = ctx.cwd;
3266
3427
  deps.state.foregroundRuns ??= new Map();
3267
3428
  deps.state.foregroundControls ??= new Map();
@@ -3438,7 +3599,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3438
3599
  try {
3439
3600
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR);
3440
3601
  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 }) });
3602
+ return steerAsyncRun({
3603
+ state: deps.state,
3604
+ runId,
3605
+ message,
3606
+ index: paramsWithResolvedCwd.index,
3607
+ kill: deps.kill,
3608
+ location,
3609
+ signal,
3610
+ ...(paramsWithResolvedCwd.steeringRecovery === false
3611
+ ? {}
3612
+ : {
3613
+ recover: ({ absoluteDeadlineAt, ...limits }) =>
3614
+ resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, absoluteDeadlineAt }),
3615
+ }
3616
+ ),
3617
+ });
3442
3618
  } catch (error) {
3443
3619
  const text = error instanceof Error ? error.message : String(error);
3444
3620
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
@@ -3455,7 +3631,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3455
3631
  if (resolved?.kind === "nested") return steerNestedRun({ target: resolved, message, index: paramsWithResolvedCwd.index, signal });
3456
3632
  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
3633
  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 }) });
3634
+ return steerAsyncRun({
3635
+ state: deps.state,
3636
+ runId: resolved.id,
3637
+ message,
3638
+ index: paramsWithResolvedCwd.index,
3639
+ kill: deps.kill,
3640
+ location: resolved.location,
3641
+ signal,
3642
+ ...(paramsWithResolvedCwd.steeringRecovery === false
3643
+ ? {}
3644
+ : {
3645
+ recover: ({ absoluteDeadlineAt, ...limits }) =>
3646
+ resumeAsyncRun({
3647
+ params: { ...limits, action: "resume", id: resolved!.id, message },
3648
+ requestCwd,
3649
+ ctx,
3650
+ deps,
3651
+ absoluteDeadlineAt,
3652
+ }),
3653
+ }
3654
+ ),
3655
+ });
3459
3656
  }
3460
3657
  if (action === "append-step") {
3461
3658
  return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
@@ -3593,7 +3790,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3593
3790
  depth,
3594
3791
  deps.config.forceTopLevelAsync === true,
3595
3792
  );
3596
- const runToolBudget = resolveToolBudget(effectiveParams.toolBudget, "toolBudget");
3793
+ const runToolBudget = resolveToolBudget(
3794
+ effectiveParams.toolBudget,
3795
+ "toolBudget",
3796
+ allowZeroToolBudget ? { minimumHard: 0 } : undefined,
3797
+ );
3597
3798
  if (runToolBudget.error) return buildRequestedModeError(effectiveParams, runToolBudget.error);
3598
3799
  const configToolBudget = resolveToolBudget(deps.config.toolBudget, "config.toolBudget");
3599
3800
  if (configToolBudget.error) return buildRequestedModeError(effectiveParams, configToolBudget.error);
@@ -3615,7 +3816,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3615
3816
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
3616
3817
  const intercomBridge = resolveIntercomBridge({
3617
3818
  config: deps.config.intercomBridge,
3618
- context: effectiveParams.context,
3819
+ context: effectiveParams.context ?? (contextPolicy.usesFork ? "fork" : undefined),
3619
3820
  orchestratorTarget: sessionName,
3620
3821
  });
3621
3822
  const agents = intercomBridge.active
@@ -3688,7 +3889,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3688
3889
  forkSessionFileForIndex = forkContextResolver.sessionFileForIndex;
3689
3890
  forkThinkingOverrideForIndex = forkContextResolver.thinkingOverrideForIndex;
3690
3891
  } catch (error) {
3691
- return toExecutionErrorResult(effectiveParams, error);
3892
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
3692
3893
  }
3693
3894
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
3694
3895
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
@@ -3698,8 +3899,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3698
3899
  const artifactConfig: ArtifactConfig = {
3699
3900
  ...DEFAULT_ARTIFACT_CONFIG,
3700
3901
  enabled: effectiveParams.artifacts !== false,
3902
+ dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
3701
3903
  };
3702
- const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd);
3904
+ const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
3703
3905
 
3704
3906
  let sessionRoot: string;
3705
3907
  if (effectiveParams.sessionDir) {
@@ -3717,6 +3919,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3717
3919
  return toExecutionErrorResult(
3718
3920
  effectiveParams,
3719
3921
  new Error(`Failed to create session directory '${sessionRoot}': ${message}`),
3922
+ contextPolicy.contextSummary,
3720
3923
  );
3721
3924
  }
3722
3925
  const sessionDirForIndex = (idx?: number) =>
@@ -3727,11 +3930,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3727
3930
  return forkSessionFileForIndex(idx);
3728
3931
  };
3729
3932
  const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride) => {
3730
- if (!shouldForkAgent(contextPolicy, agentName)) return undefined;
3933
+ if (!shouldForkAgent(contextPolicy, agentName)) return delegatedThinkingOverride;
3731
3934
  prepareForkThinking(agentName, idx, modelOverride);
3732
3935
  const override = forkThinkingOverrideForIndex(idx);
3733
3936
  if (override === "off") forkThinkingDowngrades.set(idx, agentName);
3734
- return override;
3937
+ return override ?? delegatedThinkingOverride;
3735
3938
  };
3736
3939
  const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride) =>
3737
3940
  forkSessionFileForTask(agentName, idx, modelOverride) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
@@ -3742,13 +3945,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3742
3945
  preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
3743
3946
  }
3744
3947
  } catch (error) {
3745
- return toExecutionErrorResult(effectiveParams, error);
3948
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
3746
3949
  }
3747
3950
  const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
3748
- if (chainBindingsError) return chainBindingsError;
3951
+ if (chainBindingsError) return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
3749
3952
 
3750
3953
  const onUpdateWithContext = onUpdate
3751
- ? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
3954
+ ? (r: AgentToolResult<Details>) => onUpdate(withResolvedContext(r, contextPolicy.contextSummary))
3752
3955
  : undefined;
3753
3956
 
3754
3957
  const reservation = reserveSpawnBudget(
@@ -3783,22 +3986,29 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3783
3986
  timeoutMs: foregroundTimeout.timeoutMs,
3784
3987
  turnBudget: turnBudget.turnBudget,
3785
3988
  toolBudget: runToolBudget.toolBudget,
3989
+ allowZeroToolBudget,
3786
3990
  configToolBudget: configToolBudget.toolBudget,
3787
3991
  contextPolicy,
3788
3992
  modelScope,
3789
3993
  parentSessionId: deps.state.currentSessionId,
3790
3994
  };
3791
3995
 
3792
- const foregroundControl = effectiveAsync
3996
+ const foregroundDescription = effectiveParams.task?.trim()
3997
+ || effectiveParams.tasks?.[0]?.task?.trim()
3998
+ || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
3999
+ const foregroundControl: ForegroundRunControl | undefined = effectiveAsync
3793
4000
  ? undefined
3794
4001
  : {
3795
4002
  runId,
3796
4003
  mode: foregroundMode,
3797
4004
  startedAt: Date.now(),
3798
4005
  updatedAt: Date.now(),
4006
+ cwd: effectiveCwd,
3799
4007
  currentAgent: undefined,
3800
4008
  currentIndex: undefined,
4009
+ description: foregroundDescription,
3801
4010
  currentActivityState: undefined,
4011
+ activeChildren: new Map(),
3802
4012
  nestedRoute,
3803
4013
  interrupt: undefined,
3804
4014
  };
@@ -3870,7 +4080,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3870
4080
  let nestedForegroundStarted = false;
3871
4081
  try {
3872
4082
  const asyncResult = runAsyncPath(execData, deps);
3873
- if (asyncResult) return withForkContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), effectiveParams.context);
4083
+ if (asyncResult) return withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary);
3874
4084
  if (foregroundControl) {
3875
4085
  writeNestedForegroundEvent("subagent.nested.started");
3876
4086
  nestedForegroundStarted = true;
@@ -3878,20 +4088,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3878
4088
  if (hasChain && effectiveParams.chain) {
3879
4089
  const result = await runChainPath(execData, deps);
3880
4090
  writeNestedForegroundEvent("subagent.nested.completed", result);
3881
- return withForkContext(withForkThinkingNotes(result, forkThinkingDowngrades), effectiveParams.context);
4091
+ return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
3882
4092
  }
3883
4093
  if (hasTasks && effectiveParams.tasks) {
3884
4094
  const result = await runParallelPath(execData, deps);
3885
4095
  writeNestedForegroundEvent("subagent.nested.completed", result);
3886
- return withForkContext(withForkThinkingNotes(result, forkThinkingDowngrades), effectiveParams.context);
4096
+ return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
3887
4097
  }
3888
4098
  if (hasSingle) {
3889
4099
  const result = await runSinglePath(execData, deps);
3890
4100
  writeNestedForegroundEvent("subagent.nested.completed", result);
3891
- return withForkContext(withForkThinkingNotes(result, forkThinkingDowngrades), effectiveParams.context);
4101
+ return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
3892
4102
  }
3893
4103
  } catch (error) {
3894
- const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error), forkThinkingDowngrades);
4104
+ const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
3895
4105
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
3896
4106
  return errorResult;
3897
4107
  } finally {
@@ -3904,11 +4114,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3904
4114
  }
3905
4115
  }
3906
4116
 
3907
- return withForkContext({
4117
+ return withResolvedContext({
3908
4118
  content: [{ type: "text", text: "Invalid params" }],
3909
4119
  isError: true,
3910
4120
  details: { mode: "single" as const, results: [] },
3911
- }, effectiveParams.context);
4121
+ }, contextPolicy.contextSummary);
3912
4122
  };
3913
4123
 
3914
4124
  const executeWithSingleDispatchGuard = async (
@@ -3933,5 +4143,26 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3933
4143
  }
3934
4144
  };
3935
4145
 
3936
- return { execute: executeWithSingleDispatchGuard };
4146
+ const executeDelegated = async (
4147
+ id: string,
4148
+ params: SubagentParamsLike,
4149
+ signal: AbortSignal,
4150
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
4151
+ ctx: ExtensionContext,
4152
+ ): Promise<AgentToolResult<Details>> => {
4153
+ const delegatedParams = { ...params };
4154
+ const privateParams = delegatedParams as SubagentParamsLike & {
4155
+ delegatedThinkingOverride?: AgentConfig["thinking"];
4156
+ delegatedAllowZeroToolBudget?: true;
4157
+ };
4158
+ const thinkingOverride = privateParams.delegatedThinkingOverride;
4159
+ const allowZeroToolBudget = privateParams.delegatedAllowZeroToolBudget === true;
4160
+ delete privateParams.delegatedThinkingOverride;
4161
+ delete privateParams.delegatedAllowZeroToolBudget;
4162
+ if (thinkingOverride !== undefined) delegatedThinkingOverrides.set(delegatedParams, thinkingOverride);
4163
+ if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams);
4164
+ return execute(id, delegatedParams, signal, onUpdate, ctx);
4165
+ };
4166
+
4167
+ return { execute: executeWithSingleDispatchGuard, executeDelegated };
3937
4168
  }