pi-subagents 0.17.3 → 0.17.5

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.
@@ -24,8 +24,9 @@ import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
24
24
  import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
25
25
  import { createForkContextResolver } from "./fork-context.ts";
26
26
  import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
27
+ import { resolveControlConfig } from "./subagent-control.ts";
27
28
  import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
28
- import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "./utils.ts";
29
+ import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "./utils.ts";
29
30
  import { applyForceTopLevelAsyncOverride } from "./top-level-async.ts";
30
31
  import {
31
32
  cleanupWorktrees,
@@ -40,9 +41,11 @@ import {
40
41
  type AgentProgress,
41
42
  type ArtifactConfig,
42
43
  type ArtifactPaths,
44
+ type ControlConfig,
43
45
  type Details,
44
46
  type ExtensionConfig,
45
47
  type MaxOutputConfig,
48
+ type ResolvedControlConfig,
46
49
  type SingleResult,
47
50
  type SubagentState,
48
51
  DEFAULT_ARTIFACT_CONFIG,
@@ -54,6 +57,8 @@ import {
54
57
  wrapForkTask,
55
58
  } from "./types.ts";
56
59
 
60
+ const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
61
+
57
62
  interface TaskParam {
58
63
  agent: string;
59
64
  task: string;
@@ -65,6 +70,7 @@ interface TaskParam {
65
70
 
66
71
  export interface SubagentParamsLike {
67
72
  action?: string;
73
+ runId?: string;
68
74
  agent?: string;
69
75
  task?: string;
70
76
  chain?: ChainStep[];
@@ -75,6 +81,7 @@ export interface SubagentParamsLike {
75
81
  async?: boolean;
76
82
  clarify?: boolean;
77
83
  share?: boolean;
84
+ control?: ControlConfig;
78
85
  sessionDir?: string;
79
86
  cwd?: string;
80
87
  maxOutput?: MaxOutputConfig;
@@ -114,12 +121,73 @@ interface ExecutionContextData {
114
121
  artifactsDir: string;
115
122
  backgroundRequestedWhileClarifying: boolean;
116
123
  effectiveAsync: boolean;
124
+ controlConfig: ResolvedControlConfig;
117
125
  }
118
126
 
119
127
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
120
128
  return requestedCwd ? path.resolve(runtimeCwd, requestedCwd) : runtimeCwd;
121
129
  }
122
130
 
131
+ function getForegroundControl(state: SubagentState, runId: string | undefined) {
132
+ if (runId) return state.foregroundControls.get(runId);
133
+ if (state.lastForegroundControlId) {
134
+ const latest = state.foregroundControls.get(state.lastForegroundControlId);
135
+ if (latest) return latest;
136
+ }
137
+ let newest: (SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never) | undefined;
138
+ for (const control of state.foregroundControls.values()) {
139
+ if (!newest || control.updatedAt > newest.updatedAt) newest = control;
140
+ }
141
+ return newest;
142
+ }
143
+
144
+ function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
145
+ if (runId) {
146
+ const direct = state.asyncJobs.get(runId);
147
+ if (direct) return { asyncId: direct.asyncId, asyncDir: direct.asyncDir };
148
+ }
149
+ let newest: { asyncId: string; asyncDir: string; updatedAt: number } | undefined;
150
+ for (const job of state.asyncJobs.values()) {
151
+ if (job.status !== "running") continue;
152
+ if (!newest || (job.updatedAt ?? 0) > newest.updatedAt) {
153
+ newest = { asyncId: job.asyncId, asyncDir: job.asyncDir, updatedAt: job.updatedAt ?? 0 };
154
+ }
155
+ }
156
+ return newest ? { asyncId: newest.asyncId, asyncDir: newest.asyncDir } : undefined;
157
+ }
158
+
159
+ function interruptAsyncRun(state: SubagentState, runId: string | undefined): AgentToolResult<Details> | null {
160
+ const target = getAsyncInterruptTarget(state, runId);
161
+ if (!target) return null;
162
+ const status = readStatus(target.asyncDir);
163
+ if (!status || status.state !== "running" || typeof status.pid !== "number") {
164
+ return {
165
+ content: [{ type: "text", text: `No running async run with an interrupt-capable pid was found for '${runId ?? "current"}'.` }],
166
+ isError: true,
167
+ details: { mode: "management", results: [] },
168
+ };
169
+ }
170
+ try {
171
+ process.kill(status.pid, ASYNC_INTERRUPT_SIGNAL);
172
+ const tracked = state.asyncJobs.get(target.asyncId);
173
+ if (tracked) {
174
+ tracked.activityState = "paused";
175
+ tracked.updatedAt = Date.now();
176
+ }
177
+ return {
178
+ content: [{ type: "text", text: `Interrupt requested for async run ${target.asyncId}.` }],
179
+ details: { mode: "management", results: [] },
180
+ };
181
+ } catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ return {
184
+ content: [{ type: "text", text: `Failed to interrupt async run ${target.asyncId}: ${message}` }],
185
+ isError: true,
186
+ details: { mode: "management", results: [] },
187
+ };
188
+ }
189
+ }
190
+
123
191
  function validateExecutionInput(
124
192
  params: SubagentParamsLike,
125
193
  agents: AgentConfig[],
@@ -346,6 +414,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
346
414
  artifactConfig,
347
415
  artifactsDir,
348
416
  effectiveAsync,
417
+ controlConfig,
349
418
  } = data;
350
419
  const hasChain = (params.chain?.length ?? 0) > 0;
351
420
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -429,6 +498,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
429
498
  maxSubagentDepth: currentMaxSubagentDepth,
430
499
  worktreeSetupHook: deps.config.worktreeSetupHook,
431
500
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
501
+ controlConfig,
432
502
  });
433
503
  }
434
504
 
@@ -452,6 +522,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
452
522
  maxSubagentDepth: currentMaxSubagentDepth,
453
523
  worktreeSetupHook: deps.config.worktreeSetupHook,
454
524
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
525
+ controlConfig,
455
526
  });
456
527
  }
457
528
 
@@ -489,6 +560,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
489
560
  maxSubagentDepth,
490
561
  worktreeSetupHook: deps.config.worktreeSetupHook,
491
562
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
563
+ controlConfig,
492
564
  });
493
565
  }
494
566
 
@@ -510,7 +582,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
510
582
  artifactConfig,
511
583
  onUpdate,
512
584
  sessionRoot,
585
+ controlConfig,
513
586
  } = data;
587
+ const foregroundControl = deps.state.foregroundControls.get(runId);
514
588
  const normalized = normalizeSkillInput(params.skill);
515
589
  const chainSkills = normalized === false ? [] : (normalized ?? []);
516
590
  const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
@@ -531,6 +605,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
531
605
  includeProgress: params.includeProgress,
532
606
  clarify: params.clarify,
533
607
  onUpdate,
608
+ controlConfig,
609
+ foregroundControl,
534
610
  chainSkills,
535
611
  chainDir: params.chainDir,
536
612
  maxSubagentDepth: currentMaxSubagentDepth,
@@ -574,6 +650,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
574
650
  maxSubagentDepth: currentMaxSubagentDepth,
575
651
  worktreeSetupHook: deps.config.worktreeSetupHook,
576
652
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
653
+ controlConfig,
577
654
  });
578
655
  }
579
656
 
@@ -599,6 +676,8 @@ interface ForegroundParallelRunInput {
599
676
  modelOverrides: (string | undefined)[];
600
677
  skillOverrides: (string[] | false | undefined)[];
601
678
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
679
+ controlConfig: ResolvedControlConfig;
680
+ foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
602
681
  concurrencyLimit: number;
603
682
  liveResults: (SingleResult | undefined)[];
604
683
  liveProgress: (AgentProgress | undefined)[];
@@ -687,9 +766,24 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
687
766
  const overrideSkills = input.skillOverrides[index];
688
767
  const effectiveSkills = overrideSkills === undefined ? input.behaviors[index]?.skills : overrideSkills;
689
768
  const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
769
+ const interruptController = new AbortController();
770
+ if (input.foregroundControl) {
771
+ input.foregroundControl.currentAgent = task.agent;
772
+ input.foregroundControl.currentIndex = index;
773
+ input.foregroundControl.currentActivityState = "starting";
774
+ input.foregroundControl.updatedAt = Date.now();
775
+ input.foregroundControl.interrupt = () => {
776
+ if (interruptController.signal.aborted) return false;
777
+ interruptController.abort();
778
+ input.foregroundControl!.currentActivityState = "paused";
779
+ input.foregroundControl!.updatedAt = Date.now();
780
+ return true;
781
+ };
782
+ }
690
783
  return runSync(input.ctx.cwd, input.agents, task.agent, input.taskTexts[index]!, {
691
784
  cwd: taskCwd,
692
785
  signal: input.signal,
786
+ interruptSignal: interruptController.signal,
693
787
  runId: input.runId,
694
788
  index,
695
789
  sessionDir: input.sessionDirForIndex(index),
@@ -699,14 +793,22 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
699
793
  artifactConfig: input.artifactConfig,
700
794
  maxOutput: input.maxOutput,
701
795
  maxSubagentDepth: input.maxSubagentDepths[index],
796
+ controlConfig: input.controlConfig,
702
797
  modelOverride: input.modelOverrides[index],
703
798
  availableModels: input.availableModels,
704
799
  preferredModelProvider: input.ctx.model?.provider,
705
800
  skills: effectiveSkills === false ? [] : effectiveSkills,
706
- onUpdate: input.onUpdate
707
- ? (progressUpdate) => {
801
+ onUpdate: input.onUpdate
802
+ ? (progressUpdate) => {
708
803
  const stepResults = progressUpdate.details?.results || [];
709
804
  const stepProgress = progressUpdate.details?.progress || [];
805
+ if (input.foregroundControl && stepProgress.length > 0) {
806
+ const current = stepProgress[0];
807
+ input.foregroundControl.currentAgent = task.agent;
808
+ input.foregroundControl.currentIndex = index;
809
+ input.foregroundControl.currentActivityState = current?.activityState;
810
+ input.foregroundControl.updatedAt = Date.now();
811
+ }
710
812
  if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
711
813
  if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
712
814
  const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
@@ -717,11 +819,17 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
717
819
  mode: "parallel",
718
820
  results: mergedResults,
719
821
  progress: mergedProgress,
822
+ controlEvents: progressUpdate.details?.controlEvents,
720
823
  totalSteps: input.tasks.length,
721
824
  },
722
825
  });
723
826
  }
724
827
  : undefined,
828
+ }).finally(() => {
829
+ if (input.foregroundControl?.currentIndex === index) {
830
+ input.foregroundControl.interrupt = undefined;
831
+ input.foregroundControl.updatedAt = Date.now();
832
+ }
725
833
  });
726
834
  });
727
835
  }
@@ -742,6 +850,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
742
850
  backgroundRequestedWhileClarifying,
743
851
  onUpdate,
744
852
  sessionRoot,
853
+ controlConfig,
745
854
  } = data;
746
855
  const allProgress: AgentProgress[] = [];
747
856
  const allArtifactPaths: ArtifactPaths[] = [];
@@ -866,6 +975,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
866
975
  maxSubagentDepth: currentMaxSubagentDepth,
867
976
  worktreeSetupHook: deps.config.worktreeSetupHook,
868
977
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
978
+ controlConfig,
869
979
  });
870
980
  }
871
981
  }
@@ -873,6 +983,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
873
983
  const behaviors = agentConfigs.map((config) => resolveStepBehavior(config, {}));
874
984
  const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
875
985
  const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
986
+ const foregroundControl = deps.state.foregroundControls.get(runId);
876
987
  const { setup: worktreeSetup, errorResult } = createParallelWorktreeSetup(
877
988
  params.worktree,
878
989
  effectiveCwd,
@@ -908,6 +1019,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
908
1019
  modelOverrides,
909
1020
  skillOverrides,
910
1021
  behaviors,
1022
+ controlConfig,
1023
+ foregroundControl,
911
1024
  concurrencyLimit: parallelConcurrency,
912
1025
  maxSubagentDepths,
913
1026
  liveResults,
@@ -925,6 +1038,19 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
925
1038
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
926
1039
  }
927
1040
 
1041
+ const interrupted = results.find((result) => result.interrupted);
1042
+ if (interrupted) {
1043
+ return {
1044
+ content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
1045
+ details: compactForegroundDetails({
1046
+ mode: "parallel",
1047
+ results,
1048
+ progress: params.includeProgress ? allProgress : undefined,
1049
+ artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1050
+ }),
1051
+ };
1052
+ }
1053
+
928
1054
  const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks);
929
1055
  const ok = results.filter((result) => result.exitCode === 0).length;
930
1056
  const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
@@ -972,6 +1098,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
972
1098
  artifactsDir,
973
1099
  onUpdate,
974
1100
  sessionRoot,
1101
+ controlConfig,
975
1102
  } = data;
976
1103
  const allProgress: AgentProgress[] = [];
977
1104
  const allArtifactPaths: ArtifactPaths[] = [];
@@ -1068,6 +1195,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1068
1195
  maxSubagentDepth,
1069
1196
  worktreeSetupHook: deps.config.worktreeSetupHook,
1070
1197
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1198
+ controlConfig,
1071
1199
  });
1072
1200
  }
1073
1201
  }
@@ -1085,10 +1213,39 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1085
1213
  } else {
1086
1214
  effectiveSkills = skillOverride;
1087
1215
  }
1216
+ const interruptController = new AbortController();
1217
+ const foregroundControl = deps.state.foregroundControls.get(runId);
1218
+ if (foregroundControl) {
1219
+ foregroundControl.currentAgent = params.agent;
1220
+ foregroundControl.currentIndex = 0;
1221
+ foregroundControl.currentActivityState = "starting";
1222
+ foregroundControl.updatedAt = Date.now();
1223
+ foregroundControl.interrupt = () => {
1224
+ if (interruptController.signal.aborted) return false;
1225
+ interruptController.abort();
1226
+ foregroundControl.currentActivityState = "paused";
1227
+ foregroundControl.updatedAt = Date.now();
1228
+ return true;
1229
+ };
1230
+ }
1231
+
1232
+ const forwardSingleUpdate = onUpdate
1233
+ ? (update: AgentToolResult<Details>) => {
1234
+ if (foregroundControl) {
1235
+ const firstProgress = update.details?.progress?.[0];
1236
+ foregroundControl.currentAgent = params.agent;
1237
+ foregroundControl.currentIndex = firstProgress?.index ?? 0;
1238
+ foregroundControl.currentActivityState = firstProgress?.activityState;
1239
+ foregroundControl.updatedAt = Date.now();
1240
+ }
1241
+ onUpdate(update);
1242
+ }
1243
+ : undefined;
1088
1244
 
1089
1245
  const r = await runSync(ctx.cwd, agents, params.agent!, task, {
1090
1246
  cwd: effectiveCwd,
1091
1247
  signal,
1248
+ interruptSignal: interruptController.signal,
1092
1249
  allowIntercomDetach: agentConfig.systemPrompt?.includes("Intercom orchestration channel:") === true,
1093
1250
  intercomEvents: deps.pi.events,
1094
1251
  runId,
@@ -1100,12 +1257,18 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1100
1257
  maxOutput: params.maxOutput,
1101
1258
  outputPath,
1102
1259
  maxSubagentDepth,
1103
- onUpdate,
1260
+ onUpdate: forwardSingleUpdate,
1261
+ controlConfig,
1104
1262
  modelOverride,
1105
1263
  availableModels,
1106
1264
  preferredModelProvider: currentProvider,
1107
1265
  skills: effectiveSkills,
1108
1266
  });
1267
+ if (foregroundControl?.currentIndex === 0) {
1268
+ foregroundControl.interrupt = undefined;
1269
+ foregroundControl.currentActivityState = r.progress?.activityState;
1270
+ foregroundControl.updatedAt = Date.now();
1271
+ }
1109
1272
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
1110
1273
 
1111
1274
  if (r.progress) allProgress.push(r.progress);
@@ -1134,6 +1297,19 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1134
1297
  };
1135
1298
  }
1136
1299
 
1300
+ if (r.interrupted) {
1301
+ return {
1302
+ content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.` }],
1303
+ details: compactForegroundDetails({
1304
+ mode: "single",
1305
+ results: [r],
1306
+ progress: params.includeProgress ? allProgress : undefined,
1307
+ artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1308
+ truncation: r.truncation,
1309
+ }),
1310
+ };
1311
+ }
1312
+
1137
1313
  if (r.exitCode !== 0)
1138
1314
  return {
1139
1315
  content: [{ type: "text", text: r.error || "Failed" }],
@@ -1175,10 +1351,38 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1175
1351
  ctx: ExtensionContext,
1176
1352
  ): Promise<AgentToolResult<Details>> => {
1177
1353
  deps.state.baseCwd = ctx.cwd;
1354
+ deps.state.foregroundControls ??= new Map();
1355
+ deps.state.lastForegroundControlId ??= null;
1178
1356
  const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
1179
1357
  const paramsWithResolvedCwd = params.cwd === undefined ? params : { ...params, cwd: requestCwd };
1180
1358
  if (params.action) {
1181
- const validActions = ["list", "get", "create", "update", "delete"];
1359
+ if (params.action === "interrupt") {
1360
+ const foreground = getForegroundControl(deps.state, paramsWithResolvedCwd.runId);
1361
+ if (foreground?.interrupt) {
1362
+ const interrupted = foreground.interrupt();
1363
+ if (interrupted) {
1364
+ foreground.updatedAt = Date.now();
1365
+ foreground.currentActivityState = "paused";
1366
+ return {
1367
+ content: [{ type: "text", text: `Interrupt requested for foreground run ${foreground.runId}.` }],
1368
+ details: { mode: "management", results: [] },
1369
+ };
1370
+ }
1371
+ return {
1372
+ content: [{ type: "text", text: `Foreground run ${foreground.runId} has no active child step to interrupt.` }],
1373
+ isError: true,
1374
+ details: { mode: "management", results: [] },
1375
+ };
1376
+ }
1377
+ const asyncInterruptResult = interruptAsyncRun(deps.state, paramsWithResolvedCwd.runId);
1378
+ if (asyncInterruptResult) return asyncInterruptResult;
1379
+ return {
1380
+ content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
1381
+ isError: true,
1382
+ details: { mode: "management", results: [] },
1383
+ };
1384
+ }
1385
+ const validActions = ["list", "get", "create", "update", "delete", "interrupt"];
1182
1386
  if (!validActions.includes(params.action)) {
1183
1387
  return {
1184
1388
  content: [{ type: "text", text: `Unknown action: ${params.action}. Valid: ${validActions.join(", ")}` }],
@@ -1260,6 +1464,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1260
1464
  const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && effectiveParams.clarify === true;
1261
1465
  const effectiveAsync = requestedAsync
1262
1466
  && (hasChain ? effectiveParams.clarify === false : effectiveParams.clarify !== true);
1467
+ const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
1263
1468
 
1264
1469
  const artifactConfig: ArtifactConfig = {
1265
1470
  ...DEFAULT_ARTIFACT_CONFIG,
@@ -1308,8 +1513,27 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1308
1513
  artifactsDir,
1309
1514
  backgroundRequestedWhileClarifying,
1310
1515
  effectiveAsync,
1516
+ controlConfig,
1311
1517
  };
1312
1518
 
1519
+ const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
1520
+ const foregroundControl = effectiveAsync
1521
+ ? undefined
1522
+ : {
1523
+ runId,
1524
+ mode: foregroundMode,
1525
+ startedAt: Date.now(),
1526
+ updatedAt: Date.now(),
1527
+ currentAgent: undefined,
1528
+ currentIndex: undefined,
1529
+ currentActivityState: undefined,
1530
+ interrupt: undefined,
1531
+ };
1532
+ if (foregroundControl) {
1533
+ deps.state.foregroundControls.set(runId, foregroundControl);
1534
+ deps.state.lastForegroundControlId = runId;
1535
+ }
1536
+
1313
1537
  try {
1314
1538
  const asyncResult = runAsyncPath(execData, deps);
1315
1539
  if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
@@ -1327,6 +1551,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1327
1551
  }
1328
1552
  } catch (error) {
1329
1553
  return toExecutionErrorResult(normalizedParams, error);
1554
+ } finally {
1555
+ if (foregroundControl) {
1556
+ deps.state.foregroundControls.delete(runId);
1557
+ if (deps.state.lastForegroundControlId === runId) {
1558
+ deps.state.lastForegroundControlId = null;
1559
+ }
1560
+ }
1330
1561
  }
1331
1562
 
1332
1563
  return withForkContext({