pi-subagents 0.25.0 → 0.28.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 (40) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +175 -19
  3. package/package.json +1 -1
  4. package/prompts/parallel-context-build.md +3 -1
  5. package/prompts/parallel-handoff-plan.md +3 -1
  6. package/skills/pi-subagents/SKILL.md +60 -17
  7. package/src/agents/agent-management.ts +71 -15
  8. package/src/agents/agent-serializer.ts +13 -2
  9. package/src/agents/agents.ts +88 -17
  10. package/src/agents/chain-serializer.ts +120 -0
  11. package/src/extension/fanout-child.ts +2 -0
  12. package/src/extension/index.ts +5 -2
  13. package/src/extension/schemas.ts +132 -6
  14. package/src/intercom/result-intercom.ts +5 -0
  15. package/src/runs/background/async-execution.ts +88 -6
  16. package/src/runs/background/async-status.ts +11 -1
  17. package/src/runs/background/run-status.ts +10 -1
  18. package/src/runs/background/subagent-runner.ts +665 -39
  19. package/src/runs/foreground/chain-execution.ts +369 -118
  20. package/src/runs/foreground/execution.ts +392 -19
  21. package/src/runs/foreground/subagent-executor.ts +126 -3
  22. package/src/runs/shared/acceptance-contract.ts +318 -0
  23. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  24. package/src/runs/shared/acceptance-finalization.ts +173 -0
  25. package/src/runs/shared/acceptance-reports.ts +127 -0
  26. package/src/runs/shared/acceptance.ts +22 -0
  27. package/src/runs/shared/chain-outputs.ts +101 -0
  28. package/src/runs/shared/completion-guard.ts +26 -3
  29. package/src/runs/shared/dynamic-fanout.ts +293 -0
  30. package/src/runs/shared/parallel-utils.ts +33 -1
  31. package/src/runs/shared/pi-args.ts +11 -0
  32. package/src/runs/shared/structured-output.ts +77 -0
  33. package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
  34. package/src/runs/shared/workflow-graph.ts +210 -0
  35. package/src/shared/formatters.ts +2 -2
  36. package/src/shared/settings.ts +53 -4
  37. package/src/shared/types.ts +265 -1
  38. package/src/shared/utils.ts +7 -0
  39. package/src/slash/slash-commands.ts +41 -3
  40. package/src/tui/render.ts +178 -45
@@ -21,6 +21,7 @@ import {
21
21
  writeInitialProgressFile,
22
22
  getStepAgents,
23
23
  isParallelStep,
24
+ isDynamicParallelStep,
24
25
  resolveStepBehavior,
25
26
  suppressProgressForReadOnlyTask,
26
27
  taskDisallowsFileUpdates,
@@ -52,6 +53,7 @@ import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/
52
53
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
53
54
  import { inspectSubagentStatus } from "../background/run-status.ts";
54
55
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
56
+ import { validateAcceptanceInput } from "../shared/acceptance.ts";
55
57
  import {
56
58
  cleanupWorktrees,
57
59
  createWorktrees,
@@ -63,6 +65,7 @@ import {
63
65
  } from "../shared/worktree.ts";
64
66
  import {
65
67
  type AgentProgress,
68
+ type AcceptanceInput,
66
69
  type ArtifactConfig,
67
70
  type ArtifactPaths,
68
71
  type ControlConfig,
@@ -103,6 +106,7 @@ interface TaskParam {
103
106
  progress?: boolean;
104
107
  model?: string;
105
108
  skill?: string | string[] | boolean;
109
+ acceptance?: AcceptanceInput;
106
110
  }
107
111
 
108
112
  export interface SubagentParamsLike {
@@ -117,6 +121,8 @@ export interface SubagentParamsLike {
117
121
  chain?: ChainStep[];
118
122
  tasks?: TaskParam[];
119
123
  concurrency?: number;
124
+ timeoutMs?: number;
125
+ maxRuntimeMs?: number;
120
126
  worktree?: boolean;
121
127
  context?: "fresh" | "fork";
122
128
  async?: boolean;
@@ -134,6 +140,7 @@ export interface SubagentParamsLike {
134
140
  outputMode?: "inline" | "file-only";
135
141
  agentScope?: unknown;
136
142
  chainDir?: string;
143
+ acceptance?: AcceptanceInput;
137
144
  }
138
145
 
139
146
  interface ExecutorDeps {
@@ -164,6 +171,7 @@ interface ExecutionContextData {
164
171
  artifactsDir: string;
165
172
  backgroundRequestedWhileClarifying: boolean;
166
173
  effectiveAsync: boolean;
174
+ foregroundTimeoutMs?: number;
167
175
  controlConfig: ResolvedControlConfig;
168
176
  intercomBridge: IntercomBridgeState;
169
177
  nestedRoute?: NestedRouteInfo;
@@ -245,7 +253,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
245
253
  children: input.results.map((result, index) => ({
246
254
  agent: result.agent,
247
255
  index,
248
- status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
256
+ status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached, timedOut: result.timedOut }),
249
257
  ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
250
258
  })),
251
259
  });
@@ -711,6 +719,7 @@ async function emitForegroundResultIntercom(input: {
711
719
  exitCode: result.exitCode,
712
720
  interrupted: result.interrupted,
713
721
  detached: result.detached,
722
+ timedOut: result.timedOut,
714
723
  }),
715
724
  summary: resultSummaryForIntercom(result),
716
725
  index,
@@ -756,6 +765,51 @@ async function maybeBuildForegroundIntercomReceipt(input: {
756
765
  };
757
766
  }
758
767
 
768
+ function validationErrorResult(mode: Details["mode"], text: string): AgentToolResult<Details> {
769
+ return { content: [{ type: "text", text }], isError: true, details: { mode, results: [] } };
770
+ }
771
+
772
+ function resolveForegroundTimeoutMs(params: SubagentParamsLike): { timeoutMs?: number; error?: string } {
773
+ const rawTimeout = (params as { timeoutMs?: unknown }).timeoutMs;
774
+ const rawMaxRuntime = (params as { maxRuntimeMs?: unknown }).maxRuntimeMs;
775
+ for (const [name, value] of [["timeoutMs", rawTimeout], ["maxRuntimeMs", rawMaxRuntime]] as const) {
776
+ if (value !== undefined && (typeof value !== "number" || !Number.isInteger(value) || value < 1)) {
777
+ return { error: `${name} must be a positive integer.` };
778
+ }
779
+ }
780
+ if (rawTimeout !== undefined && rawMaxRuntime !== undefined && rawTimeout !== rawMaxRuntime) {
781
+ return { error: "timeoutMs and maxRuntimeMs are aliases; provide only one or use identical values." };
782
+ }
783
+ const timeoutMs = rawTimeout ?? rawMaxRuntime;
784
+ return timeoutMs === undefined ? {} : { timeoutMs };
785
+ }
786
+
787
+ function validateAcceptanceForExecution(params: SubagentParamsLike): AgentToolResult<Details> | null {
788
+ const topLevelErrors = validateAcceptanceInput(params.acceptance);
789
+ if (topLevelErrors.length > 0) return validationErrorResult("single", topLevelErrors.join(" "));
790
+ for (const [index, task] of (params.tasks ?? []).entries()) {
791
+ const errors = validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`);
792
+ if (errors.length > 0) return validationErrorResult("parallel", errors.join(" "));
793
+ }
794
+ for (const [stepIndex, step] of (params.chain ?? []).entries()) {
795
+ if (isParallelStep(step)) {
796
+ if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
797
+ for (const [taskIndex, task] of step.parallel.entries()) {
798
+ const errors = validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`);
799
+ if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
800
+ }
801
+ } else if (isDynamicParallelStep(step)) {
802
+ if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on dynamic fanout groups; set acceptance on chain[${stepIndex}].parallel.acceptance for each materialized child.`);
803
+ const errors = validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`);
804
+ if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
805
+ } else {
806
+ const stepErrors = validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`);
807
+ if (stepErrors.length > 0) return validationErrorResult("chain", stepErrors.join(" "));
808
+ }
809
+ }
810
+ return null;
811
+ }
812
+
759
813
  function validateExecutionInput(
760
814
  params: SubagentParamsLike,
761
815
  agents: AgentConfig[],
@@ -764,6 +818,9 @@ function validateExecutionInput(
764
818
  hasSingle: boolean,
765
819
  allowClarifyTaskPrompt: boolean,
766
820
  ): AgentToolResult<Details> | null {
821
+ const acceptanceError = validateAcceptanceForExecution(params);
822
+ if (acceptanceError) return acceptanceError;
823
+
767
824
  if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
768
825
  return {
769
826
  content: [
@@ -777,6 +834,9 @@ function validateExecutionInput(
777
834
  };
778
835
  }
779
836
 
837
+ const timeoutResolution = resolveForegroundTimeoutMs(params);
838
+ if (timeoutResolution.error) return validationErrorResult(getRequestedModeLabel(params), timeoutResolution.error);
839
+
780
840
  if (hasSingle && params.agent && !agents.find((agent) => agent.name === params.agent)) {
781
841
  return {
782
842
  content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
@@ -816,6 +876,12 @@ function validateExecutionInput(
816
876
  details: { mode: "chain" as const, results: [] },
817
877
  };
818
878
  }
879
+ } else if (isDynamicParallelStep(firstStep)) {
880
+ return {
881
+ content: [{ type: "text", text: "First step in chain cannot be dynamic fanout; expand.from requires a prior structured named output" }],
882
+ isError: true,
883
+ details: { mode: "chain" as const, results: [] },
884
+ };
819
885
  } else if (!(firstStep as SequentialStep).task && !params.task && !allowClarifyTaskPrompt) {
820
886
  return {
821
887
  content: [{ type: "text", text: "First step in chain must have a task" }],
@@ -977,6 +1043,10 @@ function collectChainSessionFiles(
977
1043
  }
978
1044
  continue;
979
1045
  }
1046
+ if (isDynamicParallelStep(step)) {
1047
+ sessionFiles.push(undefined);
1048
+ continue;
1049
+ }
980
1050
  sessionFiles.push(sessionFileForIndex(flatIndex));
981
1051
  flatIndex++;
982
1052
  }
@@ -995,6 +1065,15 @@ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["
995
1065
  })),
996
1066
  };
997
1067
  }
1068
+ if (isDynamicParallelStep(step)) {
1069
+ return {
1070
+ ...step,
1071
+ parallel: {
1072
+ ...step.parallel,
1073
+ task: wrapForkTask(step.parallel.task ?? "{previous}"),
1074
+ },
1075
+ };
1076
+ }
998
1077
  const sequential = step as SequentialStep;
999
1078
  return {
1000
1079
  ...sequential,
@@ -1082,6 +1161,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1082
1161
  ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
1083
1162
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
1084
1163
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
1164
+ ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
1085
1165
  }));
1086
1166
  return executeAsyncChain(id, {
1087
1167
  chain: [{
@@ -1129,6 +1209,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1129
1209
  sessionRoot,
1130
1210
  chainSkills,
1131
1211
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex),
1212
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1132
1213
  maxSubagentDepth: currentMaxSubagentDepth,
1133
1214
  worktreeSetupHook: deps.config.worktreeSetupHook,
1134
1215
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -1179,6 +1260,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1179
1260
  controlIntercomTarget,
1180
1261
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
1181
1262
  nestedRoute,
1263
+ acceptance: params.acceptance,
1182
1264
  });
1183
1265
  }
1184
1266
 
@@ -1228,12 +1310,14 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1228
1310
  onUpdate,
1229
1311
  onControlEvent,
1230
1312
  controlConfig,
1313
+ ...(data.foregroundTimeoutMs !== undefined ? { timeoutMs: data.foregroundTimeoutMs } : {}),
1231
1314
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
1232
1315
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1233
1316
  foregroundControl,
1234
1317
  nestedRoute: foregroundControl?.nestedRoute,
1235
1318
  chainSkills,
1236
1319
  chainDir: params.chainDir,
1320
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1237
1321
  maxSubagentDepth: currentMaxSubagentDepth,
1238
1322
  worktreeSetupHook: deps.config.worktreeSetupHook,
1239
1323
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -1269,6 +1353,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1269
1353
  sessionRoot,
1270
1354
  chainSkills: chainResult.requestedAsync.chainSkills,
1271
1355
  sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForIndex),
1356
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1272
1357
  maxSubagentDepth: currentMaxSubagentDepth,
1273
1358
  worktreeSetupHook: deps.config.worktreeSetupHook,
1274
1359
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -1282,7 +1367,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1282
1367
  const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
1283
1368
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1284
1369
  if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1285
- const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
1370
+ const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached || result.timedOut)
1286
1371
  ? await maybeBuildForegroundIntercomReceipt({
1287
1372
  pi: deps.pi,
1288
1373
  intercomBridge: data.intercomBridge,
@@ -1317,6 +1402,8 @@ interface ForegroundParallelRunInput {
1317
1402
  artifactConfig: ArtifactConfig;
1318
1403
  artifactsDir: string;
1319
1404
  maxOutput?: MaxOutputConfig;
1405
+ timeoutMs?: number;
1406
+ timeoutAt?: number;
1320
1407
  paramsCwd: string;
1321
1408
  maxSubagentDepths: number[];
1322
1409
  availableModels: ModelInfo[];
@@ -1469,6 +1556,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1469
1556
  cwd: taskCwd,
1470
1557
  signal: input.signal,
1471
1558
  interruptSignal: interruptController.signal,
1559
+ ...(input.timeoutMs !== undefined && input.timeoutAt !== undefined ? { timeoutMs: input.timeoutMs, timeoutAt: input.timeoutAt } : {}),
1472
1560
  allowIntercomDetach: agentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
1473
1561
  intercomEvents: input.intercomEvents,
1474
1562
  runId: input.runId,
@@ -1482,6 +1570,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1482
1570
  outputPath,
1483
1571
  outputMode: behavior?.outputMode,
1484
1572
  maxSubagentDepth: input.maxSubagentDepths[index],
1573
+ maxExecutionTimeMs: agentConfig?.maxExecutionTimeMs,
1574
+ maxTokens: agentConfig?.maxTokens,
1485
1575
  controlConfig: input.controlConfig,
1486
1576
  onControlEvent: input.onControlEvent,
1487
1577
  intercomSessionName: input.childIntercomTarget?.(task.agent, index),
@@ -1491,6 +1581,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1491
1581
  availableModels: input.availableModels,
1492
1582
  preferredModelProvider: input.ctx.model?.provider,
1493
1583
  skills: effectiveSkills === false ? [] : effectiveSkills,
1584
+ acceptance: task.acceptance,
1585
+ acceptanceContext: { mode: "parallel" },
1494
1586
  onUpdate: input.onUpdate
1495
1587
  ? (progressUpdate) => {
1496
1588
  const stepResults = progressUpdate.details?.results || [];
@@ -1680,6 +1772,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1680
1772
  ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
1681
1773
  ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
1682
1774
  ...(progress !== undefined ? { progress } : {}),
1775
+ ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
1683
1776
  };
1684
1777
  });
1685
1778
  return executeAsyncChain(id, {
@@ -1746,6 +1839,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1746
1839
  }
1747
1840
  }
1748
1841
 
1842
+ const timeoutAt = data.foregroundTimeoutMs !== undefined ? Date.now() + data.foregroundTimeoutMs : undefined;
1749
1843
  const results = await runForegroundParallelTasks({
1750
1844
  tasks,
1751
1845
  taskTexts,
@@ -1760,6 +1854,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1760
1854
  artifactConfig,
1761
1855
  artifactsDir,
1762
1856
  maxOutput: params.maxOutput,
1857
+ ...(data.foregroundTimeoutMs !== undefined && timeoutAt !== undefined ? { timeoutMs: data.foregroundTimeoutMs, timeoutAt } : {}),
1763
1858
  paramsCwd: effectiveCwd,
1764
1859
  availableModels,
1765
1860
  modelOverrides,
@@ -1787,6 +1882,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1787
1882
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
1788
1883
  }
1789
1884
 
1885
+ const timedOut = results.find((result) => result.timedOut);
1790
1886
  const interrupted = results.find((result) => result.interrupted);
1791
1887
  const details = compactForegroundDetails({
1792
1888
  mode: "parallel",
@@ -1796,6 +1892,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1796
1892
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1797
1893
  });
1798
1894
  rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
1895
+ if (timedOut) {
1896
+ return {
1897
+ content: [{ type: "text", text: `Parallel run timed out (${timedOut.agent}): ${timedOut.error ?? "timeout expired"}` }],
1898
+ details,
1899
+ isError: true,
1900
+ };
1901
+ }
1799
1902
  if (interrupted) {
1800
1903
  return {
1801
1904
  content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
@@ -2026,10 +2129,12 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2026
2129
  }
2027
2130
  : undefined;
2028
2131
 
2132
+ const timeoutAt = data.foregroundTimeoutMs !== undefined ? Date.now() + data.foregroundTimeoutMs : undefined;
2029
2133
  const r = await runSync(ctx.cwd, agents, params.agent!, task, {
2030
2134
  cwd: effectiveCwd,
2031
2135
  signal,
2032
2136
  interruptSignal: interruptController.signal,
2137
+ ...(data.foregroundTimeoutMs !== undefined && timeoutAt !== undefined ? { timeoutMs: data.foregroundTimeoutMs, timeoutAt } : {}),
2033
2138
  allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
2034
2139
  intercomEvents: deps.pi.events,
2035
2140
  runId,
@@ -2042,6 +2147,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2042
2147
  outputPath,
2043
2148
  outputMode: effectiveOutputMode,
2044
2149
  maxSubagentDepth,
2150
+ maxExecutionTimeMs: agentConfig.maxExecutionTimeMs,
2151
+ maxTokens: agentConfig.maxTokens,
2045
2152
  onUpdate: forwardSingleUpdate,
2046
2153
  controlConfig,
2047
2154
  onControlEvent,
@@ -2053,6 +2160,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2053
2160
  availableModels,
2054
2161
  preferredModelProvider: currentProvider,
2055
2162
  skills: effectiveSkills,
2163
+ acceptance: params.acceptance,
2164
+ acceptanceContext: { mode: "single" },
2056
2165
  });
2057
2166
  if (foregroundControl?.currentIndex === 0) {
2058
2167
  foregroundControl.interrupt = undefined;
@@ -2092,7 +2201,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2092
2201
  });
2093
2202
  rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
2094
2203
 
2095
- if (!r.detached && !r.interrupted) {
2204
+ if (!r.detached && !r.interrupted && !r.timedOut) {
2096
2205
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
2097
2206
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
2098
2207
  pi: deps.pi,
@@ -2118,6 +2227,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2118
2227
  };
2119
2228
  }
2120
2229
 
2230
+ if (r.timedOut) {
2231
+ return {
2232
+ content: [{ type: "text", text: `Run timed out (${params.agent}): ${r.error ?? "timeout expired"}` }],
2233
+ details,
2234
+ isError: true,
2235
+ };
2236
+ }
2237
+
2121
2238
  if (r.interrupted) {
2122
2239
  return {
2123
2240
  content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.` }],
@@ -2345,6 +2462,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2345
2462
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
2346
2463
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
2347
2464
  const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
2465
+ const foregroundTimeout = resolveForegroundTimeoutMs(effectiveParams);
2466
+ if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
2467
+ if (effectiveAsync && foregroundTimeout.timeoutMs !== undefined) {
2468
+ return buildRequestedModeError(effectiveParams, "timeoutMs/maxRuntimeMs only applies to foreground subagent runs. Omit async:true or use action:'interrupt' for background runs.");
2469
+ }
2348
2470
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
2349
2471
 
2350
2472
  const artifactConfig: ArtifactConfig = {
@@ -2396,6 +2518,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2396
2518
  artifactsDir,
2397
2519
  backgroundRequestedWhileClarifying,
2398
2520
  effectiveAsync,
2521
+ ...(foregroundTimeout.timeoutMs !== undefined ? { foregroundTimeoutMs: foregroundTimeout.timeoutMs } : {}),
2399
2522
  controlConfig,
2400
2523
  intercomBridge,
2401
2524
  nestedRoute,