pi-subagents 0.17.4 → 0.18.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.
@@ -23,9 +23,11 @@ import {
23
23
  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
- import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
26
+ import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "./intercom-bridge.ts";
27
+ import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } 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";
30
+ import { inspectSubagentStatus } from "./run-status.ts";
29
31
  import { applyForceTopLevelAsyncOverride } from "./top-level-async.ts";
30
32
  import {
31
33
  cleanupWorktrees,
@@ -40,12 +42,17 @@ import {
40
42
  type AgentProgress,
41
43
  type ArtifactConfig,
42
44
  type ArtifactPaths,
45
+ type ControlConfig,
46
+ type ControlEvent,
43
47
  type Details,
44
48
  type ExtensionConfig,
45
49
  type MaxOutputConfig,
50
+ type ResolvedControlConfig,
46
51
  type SingleResult,
47
52
  type SubagentState,
48
53
  DEFAULT_ARTIFACT_CONFIG,
54
+ SUBAGENT_CONTROL_EVENT,
55
+ SUBAGENT_CONTROL_INTERCOM_EVENT,
49
56
  checkSubagentDepth,
50
57
  resolveTopLevelParallelConcurrency,
51
58
  resolveTopLevelParallelMaxTasks,
@@ -54,6 +61,8 @@ import {
54
61
  wrapForkTask,
55
62
  } from "./types.ts";
56
63
 
64
+ const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
65
+
57
66
  interface TaskParam {
58
67
  agent: string;
59
68
  task: string;
@@ -65,6 +74,9 @@ interface TaskParam {
65
74
 
66
75
  export interface SubagentParamsLike {
67
76
  action?: string;
77
+ id?: string;
78
+ runId?: string;
79
+ dir?: string;
68
80
  agent?: string;
69
81
  task?: string;
70
82
  chain?: ChainStep[];
@@ -75,6 +87,7 @@ export interface SubagentParamsLike {
75
87
  async?: boolean;
76
88
  clarify?: boolean;
77
89
  share?: boolean;
90
+ control?: ControlConfig;
78
91
  sessionDir?: string;
79
92
  cwd?: string;
80
93
  maxOutput?: MaxOutputConfig;
@@ -114,12 +127,131 @@ interface ExecutionContextData {
114
127
  artifactsDir: string;
115
128
  backgroundRequestedWhileClarifying: boolean;
116
129
  effectiveAsync: boolean;
130
+ controlConfig: ResolvedControlConfig;
131
+ intercomBridge: IntercomBridgeState;
117
132
  }
118
133
 
119
134
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
120
135
  return requestedCwd ? path.resolve(runtimeCwd, requestedCwd) : runtimeCwd;
121
136
  }
122
137
 
138
+ function getForegroundControl(state: SubagentState, runId: string | undefined) {
139
+ if (runId) return state.foregroundControls.get(runId);
140
+ if (state.lastForegroundControlId) {
141
+ const latest = state.foregroundControls.get(state.lastForegroundControlId);
142
+ if (latest) return latest;
143
+ }
144
+ let newest: (SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never) | undefined;
145
+ for (const control of state.foregroundControls.values()) {
146
+ if (!newest || control.updatedAt > newest.updatedAt) newest = control;
147
+ }
148
+ return newest;
149
+ }
150
+
151
+ function formatForegroundActivity(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): string | undefined {
152
+ if (control.currentTool && control.currentToolStartedAt) {
153
+ return `tool ${control.currentTool} for ${Math.floor(Math.max(0, Date.now() - control.currentToolStartedAt) / 1000)}s`;
154
+ }
155
+ if (!control.lastActivityAt) return control.currentActivityState === "needs_attention" ? "needs attention" : undefined;
156
+ const seconds = Math.floor(Math.max(0, Date.now() - control.lastActivityAt) / 1000);
157
+ return control.currentActivityState === "needs_attention" ? `no activity for ${seconds}s` : `active ${seconds}s ago`;
158
+ }
159
+
160
+ function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
161
+ const lines = [
162
+ `Run: ${control.runId}`,
163
+ "State: running",
164
+ `Mode: ${control.mode}`,
165
+ control.currentAgent ? `Current: ${control.currentAgent}${control.currentIndex !== undefined ? ` step ${control.currentIndex + 1}` : ""}` : undefined,
166
+ formatForegroundActivity(control) ? `Activity: ${formatForegroundActivity(control)}` : undefined,
167
+ ].filter((line): line is string => Boolean(line));
168
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
169
+ }
170
+
171
+ function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
172
+ if (runId) {
173
+ const direct = state.asyncJobs.get(runId);
174
+ if (direct) return { asyncId: direct.asyncId, asyncDir: direct.asyncDir };
175
+ }
176
+ let newest: { asyncId: string; asyncDir: string; updatedAt: number } | undefined;
177
+ for (const job of state.asyncJobs.values()) {
178
+ if (job.status !== "running") continue;
179
+ if (!newest || (job.updatedAt ?? 0) > newest.updatedAt) {
180
+ newest = { asyncId: job.asyncId, asyncDir: job.asyncDir, updatedAt: job.updatedAt ?? 0 };
181
+ }
182
+ }
183
+ return newest ? { asyncId: newest.asyncId, asyncDir: newest.asyncDir } : undefined;
184
+ }
185
+
186
+ function emitControlNotification(input: {
187
+ pi: ExtensionAPI;
188
+ controlConfig: ResolvedControlConfig;
189
+ intercomBridge: IntercomBridgeState;
190
+ event: ControlEvent;
191
+ }): void {
192
+ if (!shouldNotifyControlEvent(input.controlConfig, input.event)) return;
193
+ const childIntercomTarget = input.intercomBridge.active
194
+ ? resolveSubagentIntercomTarget(input.event.runId, input.event.agent, input.event.index)
195
+ : undefined;
196
+ const payload = {
197
+ event: input.event,
198
+ source: "foreground" as const,
199
+ childIntercomTarget,
200
+ noticeText: formatControlNoticeMessage(input.event, childIntercomTarget),
201
+ };
202
+ if (input.controlConfig.notifyChannels.includes("event")) {
203
+ input.pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
204
+ }
205
+ if (input.controlConfig.notifyChannels.includes("intercom") && input.intercomBridge.active && input.intercomBridge.orchestratorTarget) {
206
+ input.pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
207
+ ...payload,
208
+ to: input.intercomBridge.orchestratorTarget,
209
+ message: formatControlIntercomMessage(input.event, childIntercomTarget),
210
+ });
211
+ }
212
+ }
213
+
214
+ function interruptAsyncRun(state: SubagentState, runId: string | undefined): AgentToolResult<Details> | null {
215
+ const target = getAsyncInterruptTarget(state, runId);
216
+ if (!target) return null;
217
+ const status = readStatus(target.asyncDir);
218
+ if (!status || status.state !== "running" || typeof status.pid !== "number") {
219
+ return {
220
+ content: [{ type: "text", text: `No running async run with an interrupt-capable pid was found for '${runId ?? "current"}'.` }],
221
+ isError: true,
222
+ details: { mode: "management", results: [] },
223
+ };
224
+ }
225
+ try {
226
+ process.kill(status.pid, ASYNC_INTERRUPT_SIGNAL);
227
+ const tracked = state.asyncJobs.get(target.asyncId);
228
+ if (tracked) {
229
+ tracked.activityState = undefined;
230
+ tracked.updatedAt = Date.now();
231
+ }
232
+ return {
233
+ content: [{ type: "text", text: `Interrupt requested for async run ${target.asyncId}.` }],
234
+ details: { mode: "management", results: [] },
235
+ };
236
+ } catch (error) {
237
+ const message = error instanceof Error ? error.message : String(error);
238
+ return {
239
+ content: [{ type: "text", text: `Failed to interrupt async run ${target.asyncId}: ${message}` }],
240
+ isError: true,
241
+ details: { mode: "management", results: [] },
242
+ };
243
+ }
244
+ }
245
+
246
+ function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi">): (event: ControlEvent) => void {
247
+ return (event) => emitControlNotification({
248
+ pi: deps.pi,
249
+ controlConfig: data.controlConfig,
250
+ intercomBridge: data.intercomBridge,
251
+ event,
252
+ });
253
+ }
254
+
123
255
  function validateExecutionInput(
124
256
  params: SubagentParamsLike,
125
257
  agents: AgentConfig[],
@@ -346,6 +478,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
346
478
  artifactConfig,
347
479
  artifactsDir,
348
480
  effectiveAsync,
481
+ controlConfig,
482
+ intercomBridge,
349
483
  } = data;
350
484
  const hasChain = (params.chain?.length ?? 0) > 0;
351
485
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -395,6 +529,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
395
529
  }));
396
530
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
397
531
  const currentProvider = ctx.model?.provider;
532
+ const controlIntercomTarget = intercomBridge.active ? intercomBridge.orchestratorTarget : undefined;
533
+ const childIntercomTarget = intercomBridge.active ? (agent: string, index: number) => resolveSubagentIntercomTarget(id, agent, index) : undefined;
398
534
 
399
535
  if (hasTasks && params.tasks) {
400
536
  const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent));
@@ -429,6 +565,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
429
565
  maxSubagentDepth: currentMaxSubagentDepth,
430
566
  worktreeSetupHook: deps.config.worktreeSetupHook,
431
567
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
568
+ controlConfig,
569
+ controlIntercomTarget,
570
+ childIntercomTarget,
432
571
  });
433
572
  }
434
573
 
@@ -452,6 +591,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
452
591
  maxSubagentDepth: currentMaxSubagentDepth,
453
592
  worktreeSetupHook: deps.config.worktreeSetupHook,
454
593
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
594
+ controlConfig,
595
+ controlIntercomTarget,
596
+ childIntercomTarget,
455
597
  });
456
598
  }
457
599
 
@@ -489,6 +631,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
489
631
  maxSubagentDepth,
490
632
  worktreeSetupHook: deps.config.worktreeSetupHook,
491
633
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
634
+ controlConfig,
635
+ controlIntercomTarget,
636
+ childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
492
637
  });
493
638
  }
494
639
 
@@ -510,7 +655,11 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
510
655
  artifactConfig,
511
656
  onUpdate,
512
657
  sessionRoot,
658
+ controlConfig,
513
659
  } = data;
660
+ const onControlEvent = createForegroundControlNotifier(data, deps);
661
+ const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
662
+ const foregroundControl = deps.state.foregroundControls.get(runId);
514
663
  const normalized = normalizeSkillInput(params.skill);
515
664
  const chainSkills = normalized === false ? [] : (normalized ?? []);
516
665
  const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
@@ -531,6 +680,10 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
531
680
  includeProgress: params.includeProgress,
532
681
  clarify: params.clarify,
533
682
  onUpdate,
683
+ onControlEvent,
684
+ controlConfig,
685
+ childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
686
+ foregroundControl,
534
687
  chainSkills,
535
688
  chainDir: params.chainDir,
536
689
  maxSubagentDepth: currentMaxSubagentDepth,
@@ -574,6 +727,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
574
727
  maxSubagentDepth: currentMaxSubagentDepth,
575
728
  worktreeSetupHook: deps.config.worktreeSetupHook,
576
729
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
730
+ controlConfig,
731
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
732
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
577
733
  });
578
734
  }
579
735
 
@@ -599,6 +755,10 @@ interface ForegroundParallelRunInput {
599
755
  modelOverrides: (string | undefined)[];
600
756
  skillOverrides: (string[] | false | undefined)[];
601
757
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
758
+ controlConfig: ResolvedControlConfig;
759
+ onControlEvent?: (event: ControlEvent) => void;
760
+ childIntercomTarget?: (agent: string, index: number) => string | undefined;
761
+ foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
602
762
  concurrencyLimit: number;
603
763
  liveResults: (SingleResult | undefined)[];
604
764
  liveProgress: (AgentProgress | undefined)[];
@@ -687,9 +847,24 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
687
847
  const overrideSkills = input.skillOverrides[index];
688
848
  const effectiveSkills = overrideSkills === undefined ? input.behaviors[index]?.skills : overrideSkills;
689
849
  const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
850
+ const interruptController = new AbortController();
851
+ if (input.foregroundControl) {
852
+ input.foregroundControl.currentAgent = task.agent;
853
+ input.foregroundControl.currentIndex = index;
854
+ input.foregroundControl.currentActivityState = undefined;
855
+ input.foregroundControl.updatedAt = Date.now();
856
+ input.foregroundControl.interrupt = () => {
857
+ if (interruptController.signal.aborted) return false;
858
+ interruptController.abort();
859
+ input.foregroundControl!.currentActivityState = undefined;
860
+ input.foregroundControl!.updatedAt = Date.now();
861
+ return true;
862
+ };
863
+ }
690
864
  return runSync(input.ctx.cwd, input.agents, task.agent, input.taskTexts[index]!, {
691
865
  cwd: taskCwd,
692
866
  signal: input.signal,
867
+ interruptSignal: interruptController.signal,
693
868
  runId: input.runId,
694
869
  index,
695
870
  sessionDir: input.sessionDirForIndex(index),
@@ -699,14 +874,27 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
699
874
  artifactConfig: input.artifactConfig,
700
875
  maxOutput: input.maxOutput,
701
876
  maxSubagentDepth: input.maxSubagentDepths[index],
877
+ controlConfig: input.controlConfig,
878
+ onControlEvent: input.onControlEvent,
879
+ intercomSessionName: input.childIntercomTarget?.(task.agent, index),
702
880
  modelOverride: input.modelOverrides[index],
703
881
  availableModels: input.availableModels,
704
882
  preferredModelProvider: input.ctx.model?.provider,
705
883
  skills: effectiveSkills === false ? [] : effectiveSkills,
706
- onUpdate: input.onUpdate
707
- ? (progressUpdate) => {
884
+ onUpdate: input.onUpdate
885
+ ? (progressUpdate) => {
708
886
  const stepResults = progressUpdate.details?.results || [];
709
887
  const stepProgress = progressUpdate.details?.progress || [];
888
+ if (input.foregroundControl && stepProgress.length > 0) {
889
+ const current = stepProgress[0];
890
+ input.foregroundControl.currentAgent = task.agent;
891
+ input.foregroundControl.currentIndex = index;
892
+ input.foregroundControl.currentActivityState = current?.activityState;
893
+ input.foregroundControl.lastActivityAt = current?.lastActivityAt;
894
+ input.foregroundControl.currentTool = current?.currentTool;
895
+ input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
896
+ input.foregroundControl.updatedAt = Date.now();
897
+ }
710
898
  if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
711
899
  if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
712
900
  const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
@@ -717,11 +905,17 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
717
905
  mode: "parallel",
718
906
  results: mergedResults,
719
907
  progress: mergedProgress,
908
+ controlEvents: progressUpdate.details?.controlEvents,
720
909
  totalSteps: input.tasks.length,
721
910
  },
722
911
  });
723
912
  }
724
913
  : undefined,
914
+ }).finally(() => {
915
+ if (input.foregroundControl?.currentIndex === index) {
916
+ input.foregroundControl.interrupt = undefined;
917
+ input.foregroundControl.updatedAt = Date.now();
918
+ }
725
919
  });
726
920
  });
727
921
  }
@@ -742,7 +936,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
742
936
  backgroundRequestedWhileClarifying,
743
937
  onUpdate,
744
938
  sessionRoot,
939
+ controlConfig,
745
940
  } = data;
941
+ const onControlEvent = createForegroundControlNotifier(data, deps);
942
+ const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
746
943
  const allProgress: AgentProgress[] = [];
747
944
  const allArtifactPaths: ArtifactPaths[] = [];
748
945
  const tasks = params.tasks!;
@@ -866,6 +1063,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
866
1063
  maxSubagentDepth: currentMaxSubagentDepth,
867
1064
  worktreeSetupHook: deps.config.worktreeSetupHook,
868
1065
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1066
+ controlConfig,
1067
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1068
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
869
1069
  });
870
1070
  }
871
1071
  }
@@ -873,6 +1073,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
873
1073
  const behaviors = agentConfigs.map((config) => resolveStepBehavior(config, {}));
874
1074
  const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
875
1075
  const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
1076
+ const foregroundControl = deps.state.foregroundControls.get(runId);
876
1077
  const { setup: worktreeSetup, errorResult } = createParallelWorktreeSetup(
877
1078
  params.worktree,
878
1079
  effectiveCwd,
@@ -908,6 +1109,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
908
1109
  modelOverrides,
909
1110
  skillOverrides,
910
1111
  behaviors,
1112
+ controlConfig,
1113
+ onControlEvent,
1114
+ childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
1115
+ foregroundControl,
911
1116
  concurrencyLimit: parallelConcurrency,
912
1117
  maxSubagentDepths,
913
1118
  liveResults,
@@ -925,6 +1130,19 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
925
1130
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
926
1131
  }
927
1132
 
1133
+ const interrupted = results.find((result) => result.interrupted);
1134
+ if (interrupted) {
1135
+ return {
1136
+ content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
1137
+ details: compactForegroundDetails({
1138
+ mode: "parallel",
1139
+ results,
1140
+ progress: params.includeProgress ? allProgress : undefined,
1141
+ artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1142
+ }),
1143
+ };
1144
+ }
1145
+
928
1146
  const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks);
929
1147
  const ok = results.filter((result) => result.exitCode === 0).length;
930
1148
  const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
@@ -972,7 +1190,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
972
1190
  artifactsDir,
973
1191
  onUpdate,
974
1192
  sessionRoot,
1193
+ controlConfig,
975
1194
  } = data;
1195
+ const onControlEvent = createForegroundControlNotifier(data, deps);
1196
+ const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget(runId, params.agent!, undefined) : undefined;
976
1197
  const allProgress: AgentProgress[] = [];
977
1198
  const allArtifactPaths: ArtifactPaths[] = [];
978
1199
  const agentConfig = agents.find((a) => a.name === params.agent);
@@ -1068,6 +1289,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1068
1289
  maxSubagentDepth,
1069
1290
  worktreeSetupHook: deps.config.worktreeSetupHook,
1070
1291
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1292
+ controlConfig,
1293
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1294
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1071
1295
  });
1072
1296
  }
1073
1297
  }
@@ -1085,10 +1309,42 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1085
1309
  } else {
1086
1310
  effectiveSkills = skillOverride;
1087
1311
  }
1312
+ const interruptController = new AbortController();
1313
+ const foregroundControl = deps.state.foregroundControls.get(runId);
1314
+ if (foregroundControl) {
1315
+ foregroundControl.currentAgent = params.agent;
1316
+ foregroundControl.currentIndex = 0;
1317
+ foregroundControl.currentActivityState = undefined;
1318
+ foregroundControl.updatedAt = Date.now();
1319
+ foregroundControl.interrupt = () => {
1320
+ if (interruptController.signal.aborted) return false;
1321
+ interruptController.abort();
1322
+ foregroundControl.currentActivityState = undefined;
1323
+ foregroundControl.updatedAt = Date.now();
1324
+ return true;
1325
+ };
1326
+ }
1327
+
1328
+ const forwardSingleUpdate = onUpdate
1329
+ ? (update: AgentToolResult<Details>) => {
1330
+ if (foregroundControl) {
1331
+ const firstProgress = update.details?.progress?.[0];
1332
+ foregroundControl.currentAgent = params.agent;
1333
+ foregroundControl.currentIndex = firstProgress?.index ?? 0;
1334
+ foregroundControl.currentActivityState = firstProgress?.activityState;
1335
+ foregroundControl.lastActivityAt = firstProgress?.lastActivityAt;
1336
+ foregroundControl.currentTool = firstProgress?.currentTool;
1337
+ foregroundControl.currentToolStartedAt = firstProgress?.currentToolStartedAt;
1338
+ foregroundControl.updatedAt = Date.now();
1339
+ }
1340
+ onUpdate(update);
1341
+ }
1342
+ : undefined;
1088
1343
 
1089
1344
  const r = await runSync(ctx.cwd, agents, params.agent!, task, {
1090
1345
  cwd: effectiveCwd,
1091
1346
  signal,
1347
+ interruptSignal: interruptController.signal,
1092
1348
  allowIntercomDetach: agentConfig.systemPrompt?.includes("Intercom orchestration channel:") === true,
1093
1349
  intercomEvents: deps.pi.events,
1094
1350
  runId,
@@ -1100,12 +1356,23 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1100
1356
  maxOutput: params.maxOutput,
1101
1357
  outputPath,
1102
1358
  maxSubagentDepth,
1103
- onUpdate,
1359
+ onUpdate: forwardSingleUpdate,
1360
+ controlConfig,
1361
+ onControlEvent,
1362
+ intercomSessionName: childIntercomTarget,
1104
1363
  modelOverride,
1105
1364
  availableModels,
1106
1365
  preferredModelProvider: currentProvider,
1107
1366
  skills: effectiveSkills,
1108
1367
  });
1368
+ if (foregroundControl?.currentIndex === 0) {
1369
+ foregroundControl.interrupt = undefined;
1370
+ foregroundControl.currentActivityState = r.progress?.activityState;
1371
+ foregroundControl.lastActivityAt = r.progress?.lastActivityAt;
1372
+ foregroundControl.currentTool = r.progress?.currentTool;
1373
+ foregroundControl.currentToolStartedAt = r.progress?.currentToolStartedAt;
1374
+ foregroundControl.updatedAt = Date.now();
1375
+ }
1109
1376
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
1110
1377
 
1111
1378
  if (r.progress) allProgress.push(r.progress);
@@ -1134,6 +1401,19 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1134
1401
  };
1135
1402
  }
1136
1403
 
1404
+ if (r.interrupted) {
1405
+ return {
1406
+ content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.` }],
1407
+ details: compactForegroundDetails({
1408
+ mode: "single",
1409
+ results: [r],
1410
+ progress: params.includeProgress ? allProgress : undefined,
1411
+ artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1412
+ truncation: r.truncation,
1413
+ }),
1414
+ };
1415
+ }
1416
+
1137
1417
  if (r.exitCode !== 0)
1138
1418
  return {
1139
1419
  content: [{ type: "text", text: r.error || "Failed" }],
@@ -1175,10 +1455,44 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1175
1455
  ctx: ExtensionContext,
1176
1456
  ): Promise<AgentToolResult<Details>> => {
1177
1457
  deps.state.baseCwd = ctx.cwd;
1458
+ deps.state.foregroundControls ??= new Map();
1459
+ deps.state.lastForegroundControlId ??= null;
1178
1460
  const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
1179
1461
  const paramsWithResolvedCwd = params.cwd === undefined ? params : { ...params, cwd: requestCwd };
1180
1462
  if (params.action) {
1181
- const validActions = ["list", "get", "create", "update", "delete"];
1463
+ if (params.action === "status") {
1464
+ const foreground = getForegroundControl(deps.state, paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId);
1465
+ if (foreground) return foregroundStatusResult(foreground);
1466
+ return inspectSubagentStatus(paramsWithResolvedCwd);
1467
+ }
1468
+ if (params.action === "interrupt") {
1469
+ const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
1470
+ const foreground = getForegroundControl(deps.state, targetRunId);
1471
+ if (foreground?.interrupt) {
1472
+ const interrupted = foreground.interrupt();
1473
+ if (interrupted) {
1474
+ foreground.updatedAt = Date.now();
1475
+ foreground.currentActivityState = undefined;
1476
+ return {
1477
+ content: [{ type: "text", text: `Interrupt requested for foreground run ${foreground.runId}.` }],
1478
+ details: { mode: "management", results: [] },
1479
+ };
1480
+ }
1481
+ return {
1482
+ content: [{ type: "text", text: `Foreground run ${foreground.runId} has no active child step to interrupt.` }],
1483
+ isError: true,
1484
+ details: { mode: "management", results: [] },
1485
+ };
1486
+ }
1487
+ const asyncInterruptResult = interruptAsyncRun(deps.state, targetRunId);
1488
+ if (asyncInterruptResult) return asyncInterruptResult;
1489
+ return {
1490
+ content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
1491
+ isError: true,
1492
+ details: { mode: "management", results: [] },
1493
+ };
1494
+ }
1495
+ const validActions = ["list", "get", "create", "update", "delete", "status", "interrupt"];
1182
1496
  if (!validActions.includes(params.action)) {
1183
1497
  return {
1184
1498
  content: [{ type: "text", text: `Unknown action: ${params.action}. Valid: ${validActions.join(", ")}` }],
@@ -1260,6 +1574,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1260
1574
  const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && effectiveParams.clarify === true;
1261
1575
  const effectiveAsync = requestedAsync
1262
1576
  && (hasChain ? effectiveParams.clarify === false : effectiveParams.clarify !== true);
1577
+ const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
1263
1578
 
1264
1579
  const artifactConfig: ArtifactConfig = {
1265
1580
  ...DEFAULT_ARTIFACT_CONFIG,
@@ -1308,8 +1623,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1308
1623
  artifactsDir,
1309
1624
  backgroundRequestedWhileClarifying,
1310
1625
  effectiveAsync,
1626
+ controlConfig,
1627
+ intercomBridge,
1311
1628
  };
1312
1629
 
1630
+ const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
1631
+ const foregroundControl = effectiveAsync
1632
+ ? undefined
1633
+ : {
1634
+ runId,
1635
+ mode: foregroundMode,
1636
+ startedAt: Date.now(),
1637
+ updatedAt: Date.now(),
1638
+ currentAgent: undefined,
1639
+ currentIndex: undefined,
1640
+ currentActivityState: undefined,
1641
+ interrupt: undefined,
1642
+ };
1643
+ if (foregroundControl) {
1644
+ deps.state.foregroundControls.set(runId, foregroundControl);
1645
+ deps.state.lastForegroundControlId = runId;
1646
+ }
1647
+
1313
1648
  try {
1314
1649
  const asyncResult = runAsyncPath(execData, deps);
1315
1650
  if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
@@ -1327,6 +1662,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1327
1662
  }
1328
1663
  } catch (error) {
1329
1664
  return toExecutionErrorResult(normalizedParams, error);
1665
+ } finally {
1666
+ if (foregroundControl) {
1667
+ deps.state.foregroundControls.delete(runId);
1668
+ if (deps.state.lastForegroundControlId === runId) {
1669
+ deps.state.lastForegroundControlId = null;
1670
+ }
1671
+ }
1330
1672
  }
1331
1673
 
1332
1674
  return withForkContext({
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
 
3
3
  export const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
4
4
  export const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
5
+ export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
5
6
 
6
7
  const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
7
8
  const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
@@ -54,6 +55,11 @@ export function rewriteSubagentPrompt(
54
55
 
55
56
  export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
56
57
  pi.on("before_agent_start", async (event) => {
58
+ const intercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
59
+ if (intercomSessionName && typeof pi.setSessionName === "function") {
60
+ pi.setSessionName(intercomSessionName);
61
+ }
62
+
57
63
  const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV);
58
64
  const inheritSkills = readBooleanEnv(SUBAGENT_INHERIT_SKILLS_ENV);
59
65
  if (inheritProjectContext === undefined && inheritSkills === undefined) return;