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.
@@ -8,15 +8,18 @@ import { appendJsonl, getArtifactPaths } from "./artifacts.ts";
8
8
  import { getPiSpawnCommand } from "./pi-spawn.ts";
9
9
  import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
10
10
  import {
11
+ type ActivityState,
11
12
  type ArtifactConfig,
12
13
  type ArtifactPaths,
13
14
  type ModelAttempt,
15
+ type ResolvedControlConfig,
14
16
  type Usage,
15
17
  DEFAULT_MAX_OUTPUT,
16
18
  type MaxOutputConfig,
17
19
  truncateOutput,
18
20
  getSubagentDepthEnv,
19
21
  } from "./types.ts";
22
+ import { DEFAULT_CONTROL_CONFIG } from "./subagent-control.ts";
20
23
  import {
21
24
  type RunnerSubagentStep as SubagentStep,
22
25
  type RunnerStep,
@@ -60,6 +63,7 @@ interface SubagentRunConfig {
60
63
  piArgv1?: string;
61
64
  worktreeSetupHook?: string;
62
65
  worktreeSetupHookTimeoutMs?: number;
66
+ controlConfig?: ResolvedControlConfig;
63
67
  }
64
68
 
65
69
  interface StepResult {
@@ -75,6 +79,7 @@ interface StepResult {
75
79
  }
76
80
 
77
81
  const require = createRequire(import.meta.url);
82
+ const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
78
83
 
79
84
  function findLatestSessionFile(sessionDir: string): string | null {
80
85
  try {
@@ -95,6 +100,18 @@ function emptyUsage(): Usage {
95
100
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
96
101
  }
97
102
 
103
+ function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsage | null {
104
+ if (!attempts || attempts.length === 0) return null;
105
+ let input = 0;
106
+ let output = 0;
107
+ for (const attempt of attempts) {
108
+ input += attempt.usage?.input ?? 0;
109
+ output += attempt.usage?.output ?? 0;
110
+ }
111
+ const total = input + output;
112
+ return total > 0 ? { input, output, total } : null;
113
+ }
114
+
98
115
  interface ChildEventContext {
99
116
  eventsPath: string;
100
117
  runId: string;
@@ -133,6 +150,7 @@ interface RunPiStreamingResult {
133
150
  model?: string;
134
151
  error?: string;
135
152
  finalOutput: string;
153
+ interrupted?: boolean;
136
154
  }
137
155
 
138
156
  function runPiStreaming(
@@ -144,6 +162,7 @@ function runPiStreaming(
144
162
  piArgv1?: string,
145
163
  maxSubagentDepth?: number,
146
164
  childEventContext?: ChildEventContext,
165
+ registerInterrupt?: (interrupt: (() => void) | undefined) => void,
147
166
  ): Promise<RunPiStreamingResult> {
148
167
  return new Promise((resolve) => {
149
168
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
@@ -160,6 +179,7 @@ function runPiStreaming(
160
179
  const usage = emptyUsage();
161
180
  let model: string | undefined;
162
181
  let error: string | undefined;
182
+ let interrupted = false;
163
183
  const rawStdoutLines: string[] = [];
164
184
 
165
185
  const writeOutputLine = (line: string) => {
@@ -267,6 +287,15 @@ function runPiStreaming(
267
287
  child.stderr.on("data", (chunk: Buffer) => {
268
288
  processStderrText(chunk.toString());
269
289
  });
290
+ registerInterrupt?.(() => {
291
+ if (settled) return;
292
+ interrupted = true;
293
+ if (!error) error = "Interrupted. Waiting for explicit next action.";
294
+ trySignalChild(child, "SIGINT");
295
+ setTimeout(() => {
296
+ if (!settled) trySignalChild(child, "SIGTERM");
297
+ }, 1000).unref?.();
298
+ });
270
299
  const clearDrainTimers = () => {
271
300
  if (finalDrainTimer) {
272
301
  clearTimeout(finalDrainTimer);
@@ -301,6 +330,7 @@ function runPiStreaming(
301
330
  });
302
331
  child.on("close", (exitCode, signal) => {
303
332
  settled = true;
333
+ registerInterrupt?.(undefined);
304
334
  clearDrainTimers();
305
335
  clearStdioGuard();
306
336
  if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
@@ -309,17 +339,19 @@ function runPiStreaming(
309
339
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
310
340
  resolve({
311
341
  stderr,
312
- exitCode: forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
342
+ exitCode: interrupted ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
313
343
  messages,
314
344
  usage,
315
345
  model,
316
- error,
346
+ error: interrupted ? undefined : error,
317
347
  finalOutput,
348
+ interrupted,
318
349
  });
319
350
  });
320
351
 
321
352
  child.on("error", (spawnError) => {
322
353
  settled = true;
354
+ registerInterrupt?.(undefined);
323
355
  clearDrainTimers();
324
356
  clearStdioGuard();
325
357
  outputStream.end();
@@ -481,6 +513,7 @@ interface SingleStepContext {
481
513
  outputFile: string;
482
514
  piPackageRoot?: string;
483
515
  piArgv1?: string;
516
+ registerInterrupt?: (interrupt: (() => void) | undefined) => void;
484
517
  }
485
518
 
486
519
  /** Run a single pi agent step, returning output and metadata */
@@ -496,6 +529,7 @@ async function runSingleStep(
496
529
  attemptedModels?: string[];
497
530
  modelAttempts?: ModelAttempt[];
498
531
  artifactPaths?: ArtifactPaths;
532
+ interrupted?: boolean;
499
533
  }> {
500
534
  const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
501
535
  const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
@@ -551,6 +585,7 @@ async function runSingleStep(
551
585
  ctx.piArgv1,
552
586
  step.maxSubagentDepth,
553
587
  { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
588
+ ctx.registerInterrupt,
554
589
  );
555
590
  cleanupTempDir(tempDir);
556
591
 
@@ -627,13 +662,15 @@ async function runSingleStep(
627
662
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
628
663
  modelAttempts,
629
664
  artifactPaths,
665
+ interrupted: finalResult?.interrupted,
630
666
  };
631
667
  }
632
668
 
633
669
  type RunnerStatusPayload = {
634
670
  runId: string;
635
671
  mode: "single" | "chain";
636
- state: "queued" | "running" | "complete" | "failed";
672
+ state: "queued" | "running" | "complete" | "failed" | "paused";
673
+ activityState?: ActivityState;
637
674
  startedAt: number;
638
675
  endedAt?: number;
639
676
  lastUpdate: number;
@@ -643,6 +680,7 @@ type RunnerStatusPayload = {
643
680
  steps: Array<{
644
681
  agent: string;
645
682
  status: "pending" | "running" | "complete" | "failed";
683
+ activityState?: ActivityState;
646
684
  startedAt?: number;
647
685
  endedAt?: number;
648
686
  durationMs?: number;
@@ -714,9 +752,11 @@ function markParallelGroupRunning(input: {
714
752
  for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
715
753
  const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
716
754
  input.statusPayload.steps[flatTaskIndex].status = "running";
755
+ input.statusPayload.steps[flatTaskIndex].activityState = "active";
717
756
  input.statusPayload.steps[flatTaskIndex].startedAt = input.groupStartTime;
718
757
  }
719
758
  input.statusPayload.currentStep = input.groupStartFlatIndex;
759
+ input.statusPayload.activityState = "active";
720
760
  input.statusPayload.lastUpdate = input.groupStartTime;
721
761
  input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
722
762
  writeJson(input.statusPath, input.statusPayload);
@@ -769,6 +809,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
769
809
  const statusPath = path.join(asyncDir, "status.json");
770
810
  const eventsPath = path.join(asyncDir, "events.jsonl");
771
811
  const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
812
+ const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
813
+ let activeChildInterrupt: (() => void) | undefined;
814
+ let interrupted = false;
772
815
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
773
816
  let latestSessionFile: string | undefined;
774
817
 
@@ -780,6 +823,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
780
823
  runId: id,
781
824
  mode: flatSteps.length > 1 ? "chain" : "single",
782
825
  state: "running",
826
+ activityState: controlConfig.enabled ? "starting" : undefined,
783
827
  startedAt: overallStartTime,
784
828
  lastUpdate: overallStartTime,
785
829
  pid: process.pid,
@@ -788,6 +832,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
788
832
  steps: flatSteps.map((step) => ({
789
833
  agent: step.agent,
790
834
  status: "pending",
835
+ activityState: controlConfig.enabled ? "starting" : undefined,
791
836
  skills: step.skills,
792
837
  model: step.model,
793
838
  attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
@@ -799,6 +844,28 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
799
844
 
800
845
  fs.mkdirSync(asyncDir, { recursive: true });
801
846
  writeJson(statusPath, statusPayload);
847
+ const interruptRunner = () => {
848
+ if (interrupted || statusPayload.state !== "running") return;
849
+ interrupted = true;
850
+ const now = Date.now();
851
+ statusPayload.state = "paused";
852
+ statusPayload.activityState = "paused";
853
+ statusPayload.lastUpdate = now;
854
+ const current = statusPayload.steps[statusPayload.currentStep];
855
+ if (current?.status === "running") {
856
+ current.activityState = "paused";
857
+ current.endedAt = now;
858
+ current.durationMs = current.startedAt ? now - current.startedAt : undefined;
859
+ }
860
+ writeJson(statusPath, statusPayload);
861
+ appendJsonl(eventsPath, JSON.stringify({
862
+ type: "subagent.run.paused",
863
+ ts: now,
864
+ runId: id,
865
+ }));
866
+ activeChildInterrupt?.();
867
+ };
868
+ process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
802
869
  appendJsonl(
803
870
  eventsPath,
804
871
  JSON.stringify({
@@ -814,6 +881,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
814
881
  let flatIndex = 0;
815
882
 
816
883
  for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
884
+ if (interrupted) break;
817
885
  const step = steps[stepIndex];
818
886
 
819
887
  if (isParallelGroup(step)) {
@@ -912,6 +980,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
912
980
  outputFile: path.join(asyncDir, `output-${fi}.log`),
913
981
  piPackageRoot: config.piPackageRoot,
914
982
  piArgv1: config.piArgv1,
983
+ registerInterrupt: (interrupt) => {
984
+ activeChildInterrupt = interrupt;
985
+ },
915
986
  });
916
987
  if (task.sessionFile) {
917
988
  latestSessionFile = task.sessionFile;
@@ -944,24 +1015,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
944
1015
 
945
1016
  flatIndex += group.parallel.length;
946
1017
 
947
- if (config.sessionDir) {
948
- for (let t = 0; t < group.parallel.length; t++) {
949
- const taskSessionDir = path.join(config.sessionDir, `parallel-${t}`);
950
- const taskTokens = parseSessionTokens(taskSessionDir);
951
- if (taskTokens) {
952
- const fi = groupStartFlatIndex + t;
953
- statusPayload.steps[fi].tokens = taskTokens;
954
- previousCumulativeTokens = {
955
- input: previousCumulativeTokens.input + taskTokens.input,
956
- output: previousCumulativeTokens.output + taskTokens.output,
957
- total: previousCumulativeTokens.total + taskTokens.total,
958
- };
959
- }
960
- }
961
- statusPayload.totalTokens = { ...previousCumulativeTokens };
962
- statusPayload.lastUpdate = Date.now();
963
- writeJson(statusPath, statusPayload);
1018
+ for (let t = 0; t < group.parallel.length; t++) {
1019
+ const fi = groupStartFlatIndex + t;
1020
+ const sessionTokens = config.sessionDir
1021
+ ? parseSessionTokens(path.join(config.sessionDir, `parallel-${t}`))
1022
+ : null;
1023
+ const taskTokens = sessionTokens ?? tokenUsageFromAttempts(parallelResults[t]?.modelAttempts);
1024
+ if (!taskTokens) continue;
1025
+ statusPayload.steps[fi].tokens = taskTokens;
1026
+ previousCumulativeTokens = {
1027
+ input: previousCumulativeTokens.input + taskTokens.input,
1028
+ output: previousCumulativeTokens.output + taskTokens.output,
1029
+ total: previousCumulativeTokens.total + taskTokens.total,
1030
+ };
964
1031
  }
1032
+ statusPayload.totalTokens = { ...previousCumulativeTokens };
1033
+ statusPayload.lastUpdate = Date.now();
1034
+ writeJson(statusPath, statusPayload);
965
1035
 
966
1036
  for (const pr of parallelResults) {
967
1037
  results.push({
@@ -1007,6 +1077,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1007
1077
  const stepStartTime = Date.now();
1008
1078
  statusPayload.currentStep = flatIndex;
1009
1079
  statusPayload.steps[flatIndex].status = "running";
1080
+ statusPayload.steps[flatIndex].activityState = "active";
1081
+ statusPayload.activityState = "active";
1010
1082
  statusPayload.steps[flatIndex].skills = seqStep.skills;
1011
1083
  statusPayload.steps[flatIndex].startedAt = stepStartTime;
1012
1084
  statusPayload.lastUpdate = stepStartTime;
@@ -1029,6 +1101,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1029
1101
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
1030
1102
  piPackageRoot: config.piPackageRoot,
1031
1103
  piArgv1: config.piArgv1,
1104
+ registerInterrupt: (interrupt) => {
1105
+ activeChildInterrupt = interrupt;
1106
+ },
1032
1107
  });
1033
1108
  if (seqStep.sessionFile) {
1034
1109
  latestSessionFile = seqStep.sessionFile;
@@ -1046,7 +1121,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1046
1121
  });
1047
1122
 
1048
1123
  const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
1049
- const stepTokens: TokenUsage | null = cumulativeTokens
1124
+ let stepTokens: TokenUsage | null = cumulativeTokens
1050
1125
  ? {
1051
1126
  input: cumulativeTokens.input - previousCumulativeTokens.input,
1052
1127
  output: cumulativeTokens.output - previousCumulativeTokens.output,
@@ -1055,6 +1130,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1055
1130
  : null;
1056
1131
  if (cumulativeTokens) {
1057
1132
  previousCumulativeTokens = cumulativeTokens;
1133
+ } else {
1134
+ stepTokens = tokenUsageFromAttempts(singleResult.modelAttempts);
1135
+ if (stepTokens) {
1136
+ previousCumulativeTokens = {
1137
+ input: previousCumulativeTokens.input + stepTokens.input,
1138
+ output: previousCumulativeTokens.output + stepTokens.output,
1139
+ total: previousCumulativeTokens.total + stepTokens.total,
1140
+ };
1141
+ }
1058
1142
  }
1059
1143
 
1060
1144
  const stepEndTime = Date.now();
@@ -1139,7 +1223,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1139
1223
 
1140
1224
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
1141
1225
  const runEndedAt = Date.now();
1142
- statusPayload.state = results.every((r) => r.success) ? "complete" : "failed";
1226
+ statusPayload.state = interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
1227
+ statusPayload.activityState = interrupted ? "paused" : undefined;
1143
1228
  statusPayload.endedAt = runEndedAt;
1144
1229
  statusPayload.lastUpdate = runEndedAt;
1145
1230
  statusPayload.sessionFile = effectiveSessionFile;
@@ -1186,8 +1271,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1186
1271
  writeJson(resultPath, {
1187
1272
  id,
1188
1273
  agent: agentName,
1189
- success: results.every((r) => r.success),
1190
- summary,
1274
+ success: !interrupted && results.every((r) => r.success),
1275
+ summary: interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
1191
1276
  results: results.map((r) => ({
1192
1277
  agent: r.agent,
1193
1278
  output: r.output,
@@ -1199,7 +1284,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1199
1284
  artifactPaths: r.artifactPaths,
1200
1285
  truncated: r.truncated,
1201
1286
  })),
1202
- exitCode: results.every((r) => r.success) ? 0 : 1,
1287
+ exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
1203
1288
  timestamp: runEndedAt,
1204
1289
  durationMs: runEndedAt - overallStartTime,
1205
1290
  truncated,
@@ -25,6 +25,7 @@ function statusColor(theme: Theme, status: AsyncRunSummary["state"]): string {
25
25
  case "queued": return theme.fg("accent", status);
26
26
  case "complete": return theme.fg("success", status);
27
27
  case "failed": return theme.fg("error", status);
28
+ case "paused": return theme.fg("warning", status);
28
29
  }
29
30
  }
30
31
 
@@ -33,6 +34,7 @@ function stepStatusColor(theme: Theme, status: string): string {
33
34
  if (status === "pending") return theme.fg("dim", status);
34
35
  if (status === "complete" || status === "completed") return theme.fg("success", status);
35
36
  if (status === "failed") return theme.fg("error", status);
37
+ if (status === "paused") return theme.fg("warning", status);
36
38
  return status;
37
39
  }
38
40
 
@@ -170,7 +172,8 @@ export class SubagentsStatusComponent implements Component {
170
172
  : "";
171
173
  const duration = step.durationMs !== undefined ? ` | ${formatDuration(step.durationMs)}` : "";
172
174
  const tokens = step.tokens ? ` | ${formatTokens(step.tokens.total)} tok` : "";
173
- const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${model}${attempts}${duration}${tokens}`;
175
+ const activity = step.activityState ? `/${step.activityState}` : "";
176
+ const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${activity}${model}${attempts}${duration}${tokens}`;
174
177
  lines.push(row(truncateToWidth(line, innerW), width, this.theme));
175
178
  if (step.error) {
176
179
  lines.push(row(truncateToWidth(` ${step.error}`, innerW), width, this.theme));
package/types.ts CHANGED
@@ -40,6 +40,35 @@ export interface TokenUsage {
40
40
  total: number;
41
41
  }
42
42
 
43
+ export type ActivityState = "starting" | "active" | "quiet" | "stalled" | "paused";
44
+ export type ControlParentMode = "transitions" | "verbose";
45
+ export type ControlEventType = "stalled" | "recovered" | "paused" | "resumed" | "activity";
46
+
47
+ export interface ControlConfig {
48
+ enabled?: boolean;
49
+ quietAfterMs?: number;
50
+ stalledAfterMs?: number;
51
+ parentMode?: ControlParentMode;
52
+ }
53
+
54
+ export interface ResolvedControlConfig {
55
+ enabled: boolean;
56
+ quietAfterMs: number;
57
+ stalledAfterMs: number;
58
+ parentMode: ControlParentMode;
59
+ }
60
+
61
+ export interface ControlEvent {
62
+ type: ControlEventType;
63
+ from?: ActivityState;
64
+ to: ActivityState;
65
+ ts: number;
66
+ agent: string;
67
+ index?: number;
68
+ runId: string;
69
+ message: string;
70
+ }
71
+
43
72
  // ============================================================================
44
73
  // Progress Tracking
45
74
  // ============================================================================
@@ -48,6 +77,7 @@ export interface AgentProgress {
48
77
  index: number;
49
78
  agent: string;
50
79
  status: "pending" | "running" | "completed" | "failed" | "detached";
80
+ activityState?: ActivityState;
51
81
  task: string;
52
82
  skills?: string[];
53
83
  lastActivityAt?: number;
@@ -92,11 +122,13 @@ export interface SingleResult {
92
122
  exitCode: number;
93
123
  detached?: boolean;
94
124
  detachedReason?: string;
125
+ interrupted?: boolean;
95
126
  messages?: Message[];
96
127
  usage: Usage;
97
128
  model?: string;
98
129
  attemptedModels?: string[];
99
130
  modelAttempts?: ModelAttempt[];
131
+ controlEvents?: ControlEvent[];
100
132
  error?: string;
101
133
  sessionFile?: string;
102
134
  skills?: string[];
@@ -115,6 +147,7 @@ export interface Details {
115
147
  mode: "single" | "parallel" | "chain" | "management";
116
148
  context?: "fresh" | "fork";
117
149
  results: SingleResult[];
150
+ controlEvents?: ControlEvent[];
118
151
  asyncId?: string;
119
152
  asyncDir?: string;
120
153
  progress?: AgentProgress[];
@@ -162,15 +195,18 @@ export interface ArtifactConfig {
162
195
  export interface AsyncStatus {
163
196
  runId: string;
164
197
  mode: "single" | "chain";
165
- state: "queued" | "running" | "complete" | "failed";
198
+ state: "queued" | "running" | "complete" | "failed" | "paused";
199
+ activityState?: ActivityState;
166
200
  startedAt: number;
167
201
  endedAt?: number;
168
202
  lastUpdate?: number;
203
+ pid?: number;
169
204
  cwd?: string;
170
205
  currentStep?: number;
171
206
  steps?: Array<{
172
207
  agent: string;
173
208
  status: string;
209
+ activityState?: ActivityState;
174
210
  durationMs?: number;
175
211
  tokens?: TokenUsage;
176
212
  skills?: string[];
@@ -188,7 +224,8 @@ export interface AsyncStatus {
188
224
  export interface AsyncJobState {
189
225
  asyncId: string;
190
226
  asyncDir: string;
191
- status: "queued" | "running" | "complete" | "failed";
227
+ status: "queued" | "running" | "complete" | "failed" | "paused";
228
+ activityState?: ActivityState;
192
229
  mode?: "single" | "chain";
193
230
  agents?: string[];
194
231
  currentStep?: number;
@@ -205,6 +242,17 @@ export interface SubagentState {
205
242
  baseCwd: string;
206
243
  currentSessionId: string | null;
207
244
  asyncJobs: Map<string, AsyncJobState>;
245
+ foregroundControls: Map<string, {
246
+ runId: string;
247
+ mode: "single" | "parallel" | "chain";
248
+ startedAt: number;
249
+ updatedAt: number;
250
+ currentAgent?: string;
251
+ currentIndex?: number;
252
+ currentActivityState?: ActivityState;
253
+ interrupt?: () => boolean;
254
+ }>;
255
+ lastForegroundControlId: string | null;
208
256
  cleanupTimers: Map<string, ReturnType<typeof setTimeout>>;
209
257
  lastUiContext: ExtensionContext | null;
210
258
  poller: NodeJS.Timeout | null;
@@ -251,9 +299,12 @@ export const INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
251
299
  export interface RunSyncOptions {
252
300
  cwd?: string;
253
301
  signal?: AbortSignal;
302
+ interruptSignal?: AbortSignal;
254
303
  allowIntercomDetach?: boolean;
255
304
  intercomEvents?: IntercomEventBus;
256
305
  onUpdate?: (r: import("@mariozechner/pi-agent-core").AgentToolResult<Details>) => void;
306
+ onControlEvent?: (event: ControlEvent) => void;
307
+ controlConfig?: ResolvedControlConfig;
257
308
  maxOutput?: MaxOutputConfig;
258
309
  artifactsDir?: string;
259
310
  artifactConfig?: ArtifactConfig;
@@ -291,6 +342,7 @@ export interface ExtensionConfig {
291
342
  forceTopLevelAsync?: boolean;
292
343
  defaultSessionDir?: string;
293
344
  maxSubagentDepth?: number;
345
+ control?: ControlConfig;
294
346
  parallel?: TopLevelParallelConfig;
295
347
  worktreeSetupHook?: string;
296
348
  worktreeSetupHookTimeoutMs?: number;
package/utils.ts CHANGED
@@ -231,6 +231,7 @@ function compactCompletedProgress(progress: AgentProgress): AgentProgress {
231
231
  index: progress.index,
232
232
  agent: progress.agent,
233
233
  status: progress.status,
234
+ activityState: progress.activityState,
234
235
  task: progress.task,
235
236
  skills: progress.skills,
236
237
  toolCount: progress.toolCount,