pi-subagents 0.17.5 → 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,10 +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";
27
- import { resolveControlConfig } from "./subagent-control.ts";
26
+ import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "./intercom-bridge.ts";
27
+ import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "./subagent-control.ts";
28
28
  import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
29
29
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "./utils.ts";
30
+ import { inspectSubagentStatus } from "./run-status.ts";
30
31
  import { applyForceTopLevelAsyncOverride } from "./top-level-async.ts";
31
32
  import {
32
33
  cleanupWorktrees,
@@ -42,6 +43,7 @@ import {
42
43
  type ArtifactConfig,
43
44
  type ArtifactPaths,
44
45
  type ControlConfig,
46
+ type ControlEvent,
45
47
  type Details,
46
48
  type ExtensionConfig,
47
49
  type MaxOutputConfig,
@@ -49,6 +51,8 @@ import {
49
51
  type SingleResult,
50
52
  type SubagentState,
51
53
  DEFAULT_ARTIFACT_CONFIG,
54
+ SUBAGENT_CONTROL_EVENT,
55
+ SUBAGENT_CONTROL_INTERCOM_EVENT,
52
56
  checkSubagentDepth,
53
57
  resolveTopLevelParallelConcurrency,
54
58
  resolveTopLevelParallelMaxTasks,
@@ -70,7 +74,9 @@ interface TaskParam {
70
74
 
71
75
  export interface SubagentParamsLike {
72
76
  action?: string;
77
+ id?: string;
73
78
  runId?: string;
79
+ dir?: string;
74
80
  agent?: string;
75
81
  task?: string;
76
82
  chain?: ChainStep[];
@@ -122,6 +128,7 @@ interface ExecutionContextData {
122
128
  backgroundRequestedWhileClarifying: boolean;
123
129
  effectiveAsync: boolean;
124
130
  controlConfig: ResolvedControlConfig;
131
+ intercomBridge: IntercomBridgeState;
125
132
  }
126
133
 
127
134
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -141,6 +148,26 @@ function getForegroundControl(state: SubagentState, runId: string | undefined) {
141
148
  return newest;
142
149
  }
143
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
+
144
171
  function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
145
172
  if (runId) {
146
173
  const direct = state.asyncJobs.get(runId);
@@ -156,6 +183,34 @@ function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined
156
183
  return newest ? { asyncId: newest.asyncId, asyncDir: newest.asyncDir } : undefined;
157
184
  }
158
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
+
159
214
  function interruptAsyncRun(state: SubagentState, runId: string | undefined): AgentToolResult<Details> | null {
160
215
  const target = getAsyncInterruptTarget(state, runId);
161
216
  if (!target) return null;
@@ -171,7 +226,7 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
171
226
  process.kill(status.pid, ASYNC_INTERRUPT_SIGNAL);
172
227
  const tracked = state.asyncJobs.get(target.asyncId);
173
228
  if (tracked) {
174
- tracked.activityState = "paused";
229
+ tracked.activityState = undefined;
175
230
  tracked.updatedAt = Date.now();
176
231
  }
177
232
  return {
@@ -188,6 +243,15 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
188
243
  }
189
244
  }
190
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
+
191
255
  function validateExecutionInput(
192
256
  params: SubagentParamsLike,
193
257
  agents: AgentConfig[],
@@ -415,6 +479,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
415
479
  artifactsDir,
416
480
  effectiveAsync,
417
481
  controlConfig,
482
+ intercomBridge,
418
483
  } = data;
419
484
  const hasChain = (params.chain?.length ?? 0) > 0;
420
485
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -464,6 +529,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
464
529
  }));
465
530
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
466
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;
467
534
 
468
535
  if (hasTasks && params.tasks) {
469
536
  const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent));
@@ -499,6 +566,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
499
566
  worktreeSetupHook: deps.config.worktreeSetupHook,
500
567
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
501
568
  controlConfig,
569
+ controlIntercomTarget,
570
+ childIntercomTarget,
502
571
  });
503
572
  }
504
573
 
@@ -523,6 +592,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
523
592
  worktreeSetupHook: deps.config.worktreeSetupHook,
524
593
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
525
594
  controlConfig,
595
+ controlIntercomTarget,
596
+ childIntercomTarget,
526
597
  });
527
598
  }
528
599
 
@@ -561,6 +632,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
561
632
  worktreeSetupHook: deps.config.worktreeSetupHook,
562
633
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
563
634
  controlConfig,
635
+ controlIntercomTarget,
636
+ childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
564
637
  });
565
638
  }
566
639
 
@@ -584,6 +657,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
584
657
  sessionRoot,
585
658
  controlConfig,
586
659
  } = data;
660
+ const onControlEvent = createForegroundControlNotifier(data, deps);
661
+ const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
587
662
  const foregroundControl = deps.state.foregroundControls.get(runId);
588
663
  const normalized = normalizeSkillInput(params.skill);
589
664
  const chainSkills = normalized === false ? [] : (normalized ?? []);
@@ -605,7 +680,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
605
680
  includeProgress: params.includeProgress,
606
681
  clarify: params.clarify,
607
682
  onUpdate,
683
+ onControlEvent,
608
684
  controlConfig,
685
+ childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
609
686
  foregroundControl,
610
687
  chainSkills,
611
688
  chainDir: params.chainDir,
@@ -651,6 +728,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
651
728
  worktreeSetupHook: deps.config.worktreeSetupHook,
652
729
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
653
730
  controlConfig,
731
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
732
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
654
733
  });
655
734
  }
656
735
 
@@ -677,6 +756,8 @@ interface ForegroundParallelRunInput {
677
756
  skillOverrides: (string[] | false | undefined)[];
678
757
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
679
758
  controlConfig: ResolvedControlConfig;
759
+ onControlEvent?: (event: ControlEvent) => void;
760
+ childIntercomTarget?: (agent: string, index: number) => string | undefined;
680
761
  foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
681
762
  concurrencyLimit: number;
682
763
  liveResults: (SingleResult | undefined)[];
@@ -770,12 +851,12 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
770
851
  if (input.foregroundControl) {
771
852
  input.foregroundControl.currentAgent = task.agent;
772
853
  input.foregroundControl.currentIndex = index;
773
- input.foregroundControl.currentActivityState = "starting";
854
+ input.foregroundControl.currentActivityState = undefined;
774
855
  input.foregroundControl.updatedAt = Date.now();
775
856
  input.foregroundControl.interrupt = () => {
776
857
  if (interruptController.signal.aborted) return false;
777
858
  interruptController.abort();
778
- input.foregroundControl!.currentActivityState = "paused";
859
+ input.foregroundControl!.currentActivityState = undefined;
779
860
  input.foregroundControl!.updatedAt = Date.now();
780
861
  return true;
781
862
  };
@@ -794,6 +875,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
794
875
  maxOutput: input.maxOutput,
795
876
  maxSubagentDepth: input.maxSubagentDepths[index],
796
877
  controlConfig: input.controlConfig,
878
+ onControlEvent: input.onControlEvent,
879
+ intercomSessionName: input.childIntercomTarget?.(task.agent, index),
797
880
  modelOverride: input.modelOverrides[index],
798
881
  availableModels: input.availableModels,
799
882
  preferredModelProvider: input.ctx.model?.provider,
@@ -807,6 +890,9 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
807
890
  input.foregroundControl.currentAgent = task.agent;
808
891
  input.foregroundControl.currentIndex = index;
809
892
  input.foregroundControl.currentActivityState = current?.activityState;
893
+ input.foregroundControl.lastActivityAt = current?.lastActivityAt;
894
+ input.foregroundControl.currentTool = current?.currentTool;
895
+ input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
810
896
  input.foregroundControl.updatedAt = Date.now();
811
897
  }
812
898
  if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
@@ -852,6 +938,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
852
938
  sessionRoot,
853
939
  controlConfig,
854
940
  } = data;
941
+ const onControlEvent = createForegroundControlNotifier(data, deps);
942
+ const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
855
943
  const allProgress: AgentProgress[] = [];
856
944
  const allArtifactPaths: ArtifactPaths[] = [];
857
945
  const tasks = params.tasks!;
@@ -976,6 +1064,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
976
1064
  worktreeSetupHook: deps.config.worktreeSetupHook,
977
1065
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
978
1066
  controlConfig,
1067
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1068
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
979
1069
  });
980
1070
  }
981
1071
  }
@@ -1020,6 +1110,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1020
1110
  skillOverrides,
1021
1111
  behaviors,
1022
1112
  controlConfig,
1113
+ onControlEvent,
1114
+ childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
1023
1115
  foregroundControl,
1024
1116
  concurrencyLimit: parallelConcurrency,
1025
1117
  maxSubagentDepths,
@@ -1100,6 +1192,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1100
1192
  sessionRoot,
1101
1193
  controlConfig,
1102
1194
  } = data;
1195
+ const onControlEvent = createForegroundControlNotifier(data, deps);
1196
+ const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget(runId, params.agent!, undefined) : undefined;
1103
1197
  const allProgress: AgentProgress[] = [];
1104
1198
  const allArtifactPaths: ArtifactPaths[] = [];
1105
1199
  const agentConfig = agents.find((a) => a.name === params.agent);
@@ -1196,6 +1290,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1196
1290
  worktreeSetupHook: deps.config.worktreeSetupHook,
1197
1291
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1198
1292
  controlConfig,
1293
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1294
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1199
1295
  });
1200
1296
  }
1201
1297
  }
@@ -1218,12 +1314,12 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1218
1314
  if (foregroundControl) {
1219
1315
  foregroundControl.currentAgent = params.agent;
1220
1316
  foregroundControl.currentIndex = 0;
1221
- foregroundControl.currentActivityState = "starting";
1317
+ foregroundControl.currentActivityState = undefined;
1222
1318
  foregroundControl.updatedAt = Date.now();
1223
1319
  foregroundControl.interrupt = () => {
1224
1320
  if (interruptController.signal.aborted) return false;
1225
1321
  interruptController.abort();
1226
- foregroundControl.currentActivityState = "paused";
1322
+ foregroundControl.currentActivityState = undefined;
1227
1323
  foregroundControl.updatedAt = Date.now();
1228
1324
  return true;
1229
1325
  };
@@ -1236,6 +1332,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1236
1332
  foregroundControl.currentAgent = params.agent;
1237
1333
  foregroundControl.currentIndex = firstProgress?.index ?? 0;
1238
1334
  foregroundControl.currentActivityState = firstProgress?.activityState;
1335
+ foregroundControl.lastActivityAt = firstProgress?.lastActivityAt;
1336
+ foregroundControl.currentTool = firstProgress?.currentTool;
1337
+ foregroundControl.currentToolStartedAt = firstProgress?.currentToolStartedAt;
1239
1338
  foregroundControl.updatedAt = Date.now();
1240
1339
  }
1241
1340
  onUpdate(update);
@@ -1259,6 +1358,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1259
1358
  maxSubagentDepth,
1260
1359
  onUpdate: forwardSingleUpdate,
1261
1360
  controlConfig,
1361
+ onControlEvent,
1362
+ intercomSessionName: childIntercomTarget,
1262
1363
  modelOverride,
1263
1364
  availableModels,
1264
1365
  preferredModelProvider: currentProvider,
@@ -1267,6 +1368,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1267
1368
  if (foregroundControl?.currentIndex === 0) {
1268
1369
  foregroundControl.interrupt = undefined;
1269
1370
  foregroundControl.currentActivityState = r.progress?.activityState;
1371
+ foregroundControl.lastActivityAt = r.progress?.lastActivityAt;
1372
+ foregroundControl.currentTool = r.progress?.currentTool;
1373
+ foregroundControl.currentToolStartedAt = r.progress?.currentToolStartedAt;
1270
1374
  foregroundControl.updatedAt = Date.now();
1271
1375
  }
1272
1376
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
@@ -1356,13 +1460,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1356
1460
  const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
1357
1461
  const paramsWithResolvedCwd = params.cwd === undefined ? params : { ...params, cwd: requestCwd };
1358
1462
  if (params.action) {
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
+ }
1359
1468
  if (params.action === "interrupt") {
1360
- const foreground = getForegroundControl(deps.state, paramsWithResolvedCwd.runId);
1469
+ const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
1470
+ const foreground = getForegroundControl(deps.state, targetRunId);
1361
1471
  if (foreground?.interrupt) {
1362
1472
  const interrupted = foreground.interrupt();
1363
1473
  if (interrupted) {
1364
1474
  foreground.updatedAt = Date.now();
1365
- foreground.currentActivityState = "paused";
1475
+ foreground.currentActivityState = undefined;
1366
1476
  return {
1367
1477
  content: [{ type: "text", text: `Interrupt requested for foreground run ${foreground.runId}.` }],
1368
1478
  details: { mode: "management", results: [] },
@@ -1374,7 +1484,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1374
1484
  details: { mode: "management", results: [] },
1375
1485
  };
1376
1486
  }
1377
- const asyncInterruptResult = interruptAsyncRun(deps.state, paramsWithResolvedCwd.runId);
1487
+ const asyncInterruptResult = interruptAsyncRun(deps.state, targetRunId);
1378
1488
  if (asyncInterruptResult) return asyncInterruptResult;
1379
1489
  return {
1380
1490
  content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
@@ -1382,7 +1492,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1382
1492
  details: { mode: "management", results: [] },
1383
1493
  };
1384
1494
  }
1385
- const validActions = ["list", "get", "create", "update", "delete", "interrupt"];
1495
+ const validActions = ["list", "get", "create", "update", "delete", "status", "interrupt"];
1386
1496
  if (!validActions.includes(params.action)) {
1387
1497
  return {
1388
1498
  content: [{ type: "text", text: `Unknown action: ${params.action}. Valid: ${validActions.join(", ")}` }],
@@ -1514,6 +1624,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1514
1624
  backgroundRequestedWhileClarifying,
1515
1625
  effectiveAsync,
1516
1626
  controlConfig,
1627
+ intercomBridge,
1517
1628
  };
1518
1629
 
1519
1630
  const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
@@ -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;
@@ -19,7 +19,15 @@ import {
19
19
  truncateOutput,
20
20
  getSubagentDepthEnv,
21
21
  } from "./types.ts";
22
- import { DEFAULT_CONTROL_CONFIG } from "./subagent-control.ts";
22
+ import {
23
+ DEFAULT_CONTROL_CONFIG,
24
+ buildControlEvent,
25
+ deriveActivityState,
26
+ claimControlNotification,
27
+ formatControlIntercomMessage,
28
+ formatControlNoticeMessage,
29
+ shouldEmitControlEvent,
30
+ } from "./subagent-control.ts";
23
31
  import {
24
32
  type RunnerSubagentStep as SubagentStep,
25
33
  type RunnerStep,
@@ -64,6 +72,8 @@ interface SubagentRunConfig {
64
72
  worktreeSetupHook?: string;
65
73
  worktreeSetupHookTimeoutMs?: number;
66
74
  controlConfig?: ResolvedControlConfig;
75
+ controlIntercomTarget?: string;
76
+ childIntercomTargets?: Array<string | undefined>;
67
77
  }
68
78
 
69
79
  interface StepResult {
@@ -514,6 +524,7 @@ interface SingleStepContext {
514
524
  piPackageRoot?: string;
515
525
  piArgv1?: string;
516
526
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
527
+ childIntercomTarget?: string;
517
528
  }
518
529
 
519
530
  /** Run a single pi agent step, returning output and metadata */
@@ -575,6 +586,7 @@ async function runSingleStep(
575
586
  systemPromptMode: step.systemPromptMode,
576
587
  mcpDirectTools: step.mcpDirectTools,
577
588
  promptFileStem: step.agent,
589
+ intercomSessionName: ctx.childIntercomTarget,
578
590
  });
579
591
  const run = await runPiStreaming(
580
592
  args,
@@ -671,6 +683,9 @@ type RunnerStatusPayload = {
671
683
  mode: "single" | "chain";
672
684
  state: "queued" | "running" | "complete" | "failed" | "paused";
673
685
  activityState?: ActivityState;
686
+ lastActivityAt?: number;
687
+ currentTool?: string;
688
+ currentToolStartedAt?: number;
674
689
  startedAt: number;
675
690
  endedAt?: number;
676
691
  lastUpdate: number;
@@ -681,6 +696,9 @@ type RunnerStatusPayload = {
681
696
  agent: string;
682
697
  status: "pending" | "running" | "complete" | "failed";
683
698
  activityState?: ActivityState;
699
+ lastActivityAt?: number;
700
+ currentTool?: string;
701
+ currentToolStartedAt?: number;
684
702
  startedAt?: number;
685
703
  endedAt?: number;
686
704
  durationMs?: number;
@@ -752,11 +770,12 @@ function markParallelGroupRunning(input: {
752
770
  for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
753
771
  const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
754
772
  input.statusPayload.steps[flatTaskIndex].status = "running";
755
- input.statusPayload.steps[flatTaskIndex].activityState = "active";
756
773
  input.statusPayload.steps[flatTaskIndex].startedAt = input.groupStartTime;
774
+ input.statusPayload.steps[flatTaskIndex].lastActivityAt = input.groupStartTime;
757
775
  }
758
776
  input.statusPayload.currentStep = input.groupStartFlatIndex;
759
- input.statusPayload.activityState = "active";
777
+ input.statusPayload.activityState = undefined;
778
+ input.statusPayload.lastActivityAt = input.groupStartTime;
760
779
  input.statusPayload.lastUpdate = input.groupStartTime;
761
780
  input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
762
781
  writeJson(input.statusPath, input.statusPayload);
@@ -812,6 +831,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
812
831
  const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
813
832
  let activeChildInterrupt: (() => void) | undefined;
814
833
  let interrupted = false;
834
+ let currentActivityState: ActivityState | undefined;
835
+ let activityTimer: NodeJS.Timeout | undefined;
815
836
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
816
837
  let latestSessionFile: string | undefined;
817
838
 
@@ -823,7 +844,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
823
844
  runId: id,
824
845
  mode: flatSteps.length > 1 ? "chain" : "single",
825
846
  state: "running",
826
- activityState: controlConfig.enabled ? "starting" : undefined,
847
+ lastActivityAt: overallStartTime,
827
848
  startedAt: overallStartTime,
828
849
  lastUpdate: overallStartTime,
829
850
  pid: process.pid,
@@ -832,7 +853,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
832
853
  steps: flatSteps.map((step) => ({
833
854
  agent: step.agent,
834
855
  status: "pending",
835
- activityState: controlConfig.enabled ? "starting" : undefined,
836
856
  skills: step.skills,
837
857
  model: step.model,
838
858
  attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
@@ -844,16 +864,95 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
844
864
 
845
865
  fs.mkdirSync(asyncDir, { recursive: true });
846
866
  writeJson(statusPath, statusPayload);
867
+
868
+ const currentStepAgent = () => statusPayload.steps[statusPayload.currentStep]?.agent ?? flatSteps[statusPayload.currentStep]?.agent ?? "subagent";
869
+ const currentOutputActivityAt = (): number => {
870
+ const runningIndexes = statusPayload.steps
871
+ .map((step, index) => step.status === "running" ? index : -1)
872
+ .filter((index) => index >= 0);
873
+ let lastActivityAt = statusPayload.steps[statusPayload.currentStep]?.startedAt ?? overallStartTime;
874
+ for (const index of runningIndexes.length > 0 ? runningIndexes : [statusPayload.currentStep]) {
875
+ try {
876
+ lastActivityAt = Math.max(lastActivityAt, fs.statSync(path.join(asyncDir, `output-${index}.log`)).mtimeMs);
877
+ } catch {
878
+ // Missing output files are normal before a child writes its first line.
879
+ }
880
+ }
881
+ return lastActivityAt;
882
+ };
883
+ const emittedControlEventKeys = new Set<string>();
884
+ const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
885
+ const childIntercomTarget = config.childIntercomTargets?.[statusPayload.currentStep];
886
+ if (controlConfig.notifyChannels.length === 0 || !claimControlNotification(controlConfig, event, emittedControlEventKeys, childIntercomTarget)) return;
887
+ appendJsonl(eventsPath, JSON.stringify({
888
+ type: "subagent.control",
889
+ event,
890
+ channels: controlConfig.notifyChannels,
891
+ childIntercomTarget,
892
+ noticeText: formatControlNoticeMessage(event, childIntercomTarget),
893
+ ...(config.controlIntercomTarget && controlConfig.notifyChannels.includes("intercom") ? {
894
+ intercom: {
895
+ to: config.controlIntercomTarget,
896
+ message: formatControlIntercomMessage(event, childIntercomTarget),
897
+ },
898
+ } : {}),
899
+ }));
900
+ };
901
+ const updateRunnerActivityState = (now: number): boolean => {
902
+ const lastActivityAt = currentOutputActivityAt();
903
+ const next = deriveActivityState({
904
+ config: controlConfig,
905
+ startedAt: overallStartTime,
906
+ lastActivityAt,
907
+ now,
908
+ });
909
+ if (next === currentActivityState && statusPayload.lastActivityAt === lastActivityAt) return false;
910
+ const previous = currentActivityState;
911
+ currentActivityState = next;
912
+ statusPayload.activityState = next;
913
+ statusPayload.lastActivityAt = lastActivityAt;
914
+ for (const step of statusPayload.steps) {
915
+ if (step.status === "running") {
916
+ step.activityState = next;
917
+ step.lastActivityAt = lastActivityAt;
918
+ }
919
+ }
920
+ statusPayload.lastUpdate = now;
921
+ if (shouldEmitControlEvent(controlConfig, previous, next)) {
922
+ const event = buildControlEvent({
923
+ from: previous,
924
+ to: next,
925
+ runId: id,
926
+ agent: currentStepAgent(),
927
+ index: statusPayload.currentStep,
928
+ ts: now,
929
+ lastActivityAt,
930
+ });
931
+ appendControlEvent(event);
932
+ }
933
+ writeJson(statusPath, statusPayload);
934
+ return true;
935
+ };
936
+ if (controlConfig.enabled) {
937
+ activityTimer = setInterval(() => {
938
+ if (statusPayload.state !== "running") return;
939
+ const now = Date.now();
940
+ updateRunnerActivityState(now);
941
+ }, 1000);
942
+ activityTimer.unref?.();
943
+ }
944
+
847
945
  const interruptRunner = () => {
848
946
  if (interrupted || statusPayload.state !== "running") return;
849
947
  interrupted = true;
850
948
  const now = Date.now();
851
949
  statusPayload.state = "paused";
852
- statusPayload.activityState = "paused";
950
+ currentActivityState = undefined;
951
+ statusPayload.activityState = undefined;
853
952
  statusPayload.lastUpdate = now;
854
953
  const current = statusPayload.steps[statusPayload.currentStep];
855
954
  if (current?.status === "running") {
856
- current.activityState = "paused";
955
+ current.activityState = undefined;
857
956
  current.endedAt = now;
858
957
  current.durationMs = current.startedAt ? now - current.startedAt : undefined;
859
958
  }
@@ -980,6 +1079,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
980
1079
  outputFile: path.join(asyncDir, `output-${fi}.log`),
981
1080
  piPackageRoot: config.piPackageRoot,
982
1081
  piArgv1: config.piArgv1,
1082
+ childIntercomTarget: config.childIntercomTargets?.[fi],
983
1083
  registerInterrupt: (interrupt) => {
984
1084
  activeChildInterrupt = interrupt;
985
1085
  },
@@ -1077,10 +1177,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1077
1177
  const stepStartTime = Date.now();
1078
1178
  statusPayload.currentStep = flatIndex;
1079
1179
  statusPayload.steps[flatIndex].status = "running";
1080
- statusPayload.steps[flatIndex].activityState = "active";
1081
- statusPayload.activityState = "active";
1180
+ statusPayload.steps[flatIndex].activityState = undefined;
1181
+ statusPayload.activityState = undefined;
1082
1182
  statusPayload.steps[flatIndex].skills = seqStep.skills;
1083
1183
  statusPayload.steps[flatIndex].startedAt = stepStartTime;
1184
+ statusPayload.steps[flatIndex].lastActivityAt = stepStartTime;
1185
+ statusPayload.lastActivityAt = stepStartTime;
1084
1186
  statusPayload.lastUpdate = stepStartTime;
1085
1187
  statusPayload.outputFile = path.join(asyncDir, `output-${flatIndex}.log`);
1086
1188
  writeJson(statusPath, statusPayload);
@@ -1101,6 +1203,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1101
1203
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
1102
1204
  piPackageRoot: config.piPackageRoot,
1103
1205
  piArgv1: config.piArgv1,
1206
+ childIntercomTarget: config.childIntercomTargets?.[flatIndex],
1104
1207
  registerInterrupt: (interrupt) => {
1105
1208
  activeChildInterrupt = interrupt;
1106
1209
  },
@@ -1221,10 +1324,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1221
1324
  }
1222
1325
  }
1223
1326
 
1327
+ if (activityTimer) {
1328
+ clearInterval(activityTimer);
1329
+ activityTimer = undefined;
1330
+ }
1224
1331
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
1225
1332
  const runEndedAt = Date.now();
1226
1333
  statusPayload.state = interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
1227
- statusPayload.activityState = interrupted ? "paused" : undefined;
1334
+ statusPayload.activityState = undefined;
1228
1335
  statusPayload.endedAt = runEndedAt;
1229
1336
  statusPayload.lastUpdate = runEndedAt;
1230
1337
  statusPayload.sessionFile = effectiveSessionFile;
@@ -1272,6 +1379,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1272
1379
  id,
1273
1380
  agent: agentName,
1274
1381
  success: !interrupted && results.every((r) => r.success),
1382
+ state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
1275
1383
  summary: interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
1276
1384
  results: results.map((r) => ({
1277
1385
  agent: r.agent,
@@ -172,7 +172,11 @@ export class SubagentsStatusComponent implements Component {
172
172
  : "";
173
173
  const duration = step.durationMs !== undefined ? ` | ${formatDuration(step.durationMs)}` : "";
174
174
  const tokens = step.tokens ? ` | ${formatTokens(step.tokens.total)} tok` : "";
175
- const activity = step.activityState ? `/${step.activityState}` : "";
175
+ const activity = step.lastActivityAt
176
+ ? step.activityState === "needs_attention"
177
+ ? ` | no activity for ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))}`
178
+ : ` | active ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))} ago`
179
+ : "";
176
180
  const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${activity}${model}${attempts}${duration}${tokens}`;
177
181
  lines.push(row(truncateToWidth(line, innerW), width, this.theme));
178
182
  if (step.error) {