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.
- package/CHANGELOG.md +28 -0
- package/README.md +19 -19
- package/agents/context-builder.md +1 -1
- package/agents/oracle-executor.md +1 -1
- package/agents/oracle.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/worker.md +1 -1
- package/async-execution.ts +29 -2
- package/async-job-tracker.ts +74 -7
- package/async-status.ts +74 -17
- package/chain-execution.ts +162 -26
- package/execution.ts +122 -4
- package/index.ts +124 -128
- package/install.mjs +2 -3
- package/intercom-bridge.ts +9 -0
- package/notify.ts +25 -6
- package/package.json +3 -6
- package/pi-args.ts +4 -0
- package/pi-spawn.ts +9 -6
- package/render.ts +20 -12
- package/result-watcher.ts +3 -5
- package/run-status.ts +134 -0
- package/schemas.ts +22 -7
- package/skills/pi-subagents/SKILL.md +50 -10
- package/subagent-control.ts +148 -0
- package/subagent-executor.ts +348 -6
- package/subagent-prompt-runtime.ts +6 -0
- package/subagent-runner.ts +218 -25
- package/subagents-status.ts +8 -1
- package/types.ts +74 -2
- package/utils.ts +1 -0
package/chain-execution.ts
CHANGED
|
@@ -40,10 +40,13 @@ import {
|
|
|
40
40
|
type WorktreeSetup,
|
|
41
41
|
} from "./worktree.ts";
|
|
42
42
|
import {
|
|
43
|
+
type ActivityState,
|
|
43
44
|
type AgentProgress,
|
|
44
45
|
type ArtifactConfig,
|
|
45
46
|
type ArtifactPaths,
|
|
47
|
+
type ControlEvent,
|
|
46
48
|
type Details,
|
|
49
|
+
type ResolvedControlConfig,
|
|
47
50
|
type SingleResult,
|
|
48
51
|
MAX_CONCURRENCY,
|
|
49
52
|
resolveChildMaxSubagentDepth,
|
|
@@ -82,6 +85,19 @@ interface ParallelChainRunInput {
|
|
|
82
85
|
artifactsDir: string;
|
|
83
86
|
signal?: AbortSignal;
|
|
84
87
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
88
|
+
onControlEvent?: (event: ControlEvent) => void;
|
|
89
|
+
controlConfig: ResolvedControlConfig;
|
|
90
|
+
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
91
|
+
foregroundControl?: {
|
|
92
|
+
updatedAt: number;
|
|
93
|
+
currentAgent?: string;
|
|
94
|
+
currentIndex?: number;
|
|
95
|
+
currentActivityState?: ActivityState;
|
|
96
|
+
lastActivityAt?: number;
|
|
97
|
+
currentTool?: string;
|
|
98
|
+
currentToolStartedAt?: number;
|
|
99
|
+
interrupt?: () => boolean;
|
|
100
|
+
};
|
|
85
101
|
results: SingleResult[];
|
|
86
102
|
allProgress: AgentProgress[];
|
|
87
103
|
chainAgents: string[];
|
|
@@ -186,10 +202,25 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
186
202
|
const outputPath = typeof behavior.output === "string"
|
|
187
203
|
? (path.isAbsolute(behavior.output) ? behavior.output : path.join(input.chainDir, behavior.output))
|
|
188
204
|
: undefined;
|
|
205
|
+
const interruptController = new AbortController();
|
|
206
|
+
if (input.foregroundControl) {
|
|
207
|
+
input.foregroundControl.currentAgent = task.agent;
|
|
208
|
+
input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
|
|
209
|
+
input.foregroundControl.currentActivityState = undefined;
|
|
210
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
211
|
+
input.foregroundControl.interrupt = () => {
|
|
212
|
+
if (interruptController.signal.aborted) return false;
|
|
213
|
+
interruptController.abort();
|
|
214
|
+
input.foregroundControl!.currentActivityState = undefined;
|
|
215
|
+
input.foregroundControl!.updatedAt = Date.now();
|
|
216
|
+
return true;
|
|
217
|
+
};
|
|
218
|
+
}
|
|
189
219
|
|
|
190
220
|
const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
|
|
191
221
|
cwd: taskCwd,
|
|
192
222
|
signal: input.signal,
|
|
223
|
+
interruptSignal: interruptController.signal,
|
|
193
224
|
runId: input.runId,
|
|
194
225
|
index: input.globalTaskIndex + taskIndex,
|
|
195
226
|
sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
|
|
@@ -199,28 +230,46 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
199
230
|
artifactConfig: input.artifactConfig,
|
|
200
231
|
outputPath,
|
|
201
232
|
maxSubagentDepth,
|
|
233
|
+
controlConfig: input.controlConfig,
|
|
234
|
+
onControlEvent: input.onControlEvent,
|
|
235
|
+
intercomSessionName: input.childIntercomTarget?.(task.agent, input.globalTaskIndex + taskIndex),
|
|
202
236
|
modelOverride: effectiveModel,
|
|
203
237
|
availableModels: input.availableModels,
|
|
204
238
|
preferredModelProvider: input.ctx.model?.provider,
|
|
205
239
|
skills: behavior.skills === false ? [] : behavior.skills,
|
|
206
240
|
onUpdate: input.onUpdate
|
|
207
241
|
? (progressUpdate) => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
},
|
|
220
|
-
});
|
|
242
|
+
const stepResults = progressUpdate.details?.results || [];
|
|
243
|
+
const stepProgress = progressUpdate.details?.progress || [];
|
|
244
|
+
if (input.foregroundControl && stepProgress.length > 0) {
|
|
245
|
+
const current = stepProgress[0];
|
|
246
|
+
input.foregroundControl.currentAgent = task.agent;
|
|
247
|
+
input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
|
|
248
|
+
input.foregroundControl.currentActivityState = current?.activityState;
|
|
249
|
+
input.foregroundControl.lastActivityAt = current?.lastActivityAt;
|
|
250
|
+
input.foregroundControl.currentTool = current?.currentTool;
|
|
251
|
+
input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
|
|
252
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
221
253
|
}
|
|
254
|
+
input.onUpdate?.({
|
|
255
|
+
...progressUpdate,
|
|
256
|
+
details: {
|
|
257
|
+
mode: "chain",
|
|
258
|
+
results: input.results.concat(stepResults),
|
|
259
|
+
progress: input.allProgress.concat(stepProgress),
|
|
260
|
+
controlEvents: progressUpdate.details?.controlEvents,
|
|
261
|
+
chainAgents: input.chainAgents,
|
|
262
|
+
totalSteps: input.totalSteps,
|
|
263
|
+
currentStepIndex: input.stepIndex,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
222
267
|
: undefined,
|
|
223
268
|
});
|
|
269
|
+
if (input.foregroundControl?.currentIndex === input.globalTaskIndex + taskIndex) {
|
|
270
|
+
input.foregroundControl.interrupt = undefined;
|
|
271
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
272
|
+
}
|
|
224
273
|
|
|
225
274
|
if (result.exitCode !== 0 && failFast) {
|
|
226
275
|
aborted = true;
|
|
@@ -249,6 +298,19 @@ export interface ChainExecutionParams {
|
|
|
249
298
|
includeProgress?: boolean;
|
|
250
299
|
clarify?: boolean;
|
|
251
300
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
301
|
+
onControlEvent?: (event: ControlEvent) => void;
|
|
302
|
+
controlConfig: ResolvedControlConfig;
|
|
303
|
+
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
304
|
+
foregroundControl?: {
|
|
305
|
+
updatedAt: number;
|
|
306
|
+
currentAgent?: string;
|
|
307
|
+
currentIndex?: number;
|
|
308
|
+
currentActivityState?: ActivityState;
|
|
309
|
+
lastActivityAt?: number;
|
|
310
|
+
currentTool?: string;
|
|
311
|
+
currentToolStartedAt?: number;
|
|
312
|
+
interrupt?: () => boolean;
|
|
313
|
+
};
|
|
252
314
|
chainSkills?: string[];
|
|
253
315
|
chainDir?: string;
|
|
254
316
|
maxSubagentDepth: number;
|
|
@@ -286,6 +348,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
286
348
|
includeProgress,
|
|
287
349
|
clarify,
|
|
288
350
|
onUpdate,
|
|
351
|
+
onControlEvent,
|
|
352
|
+
controlConfig,
|
|
353
|
+
childIntercomTarget,
|
|
354
|
+
foregroundControl,
|
|
289
355
|
chainSkills: chainSkillsParam,
|
|
290
356
|
chainDir: chainDirBase,
|
|
291
357
|
} = params;
|
|
@@ -484,6 +550,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
484
550
|
allProgress,
|
|
485
551
|
chainAgents,
|
|
486
552
|
totalSteps,
|
|
553
|
+
controlConfig,
|
|
554
|
+
onControlEvent,
|
|
555
|
+
childIntercomTarget,
|
|
556
|
+
foregroundControl,
|
|
487
557
|
worktreeSetup,
|
|
488
558
|
maxSubagentDepth: params.maxSubagentDepth,
|
|
489
559
|
});
|
|
@@ -495,6 +565,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
495
565
|
if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
|
|
496
566
|
}
|
|
497
567
|
|
|
568
|
+
const interrupted = parallelResults.find((result) => result.interrupted);
|
|
569
|
+
if (interrupted) {
|
|
570
|
+
return {
|
|
571
|
+
content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
|
|
572
|
+
details: buildChainExecutionDetails({
|
|
573
|
+
results,
|
|
574
|
+
includeProgress,
|
|
575
|
+
allProgress,
|
|
576
|
+
allArtifactPaths,
|
|
577
|
+
artifactsDir,
|
|
578
|
+
chainAgents,
|
|
579
|
+
totalSteps,
|
|
580
|
+
currentStepIndex: stepIndex,
|
|
581
|
+
}),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
498
585
|
const failures = parallelResults
|
|
499
586
|
.map((result, originalIndex) => ({ ...result, originalIndex }))
|
|
500
587
|
.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
|
|
@@ -603,10 +690,25 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
603
690
|
? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
|
|
604
691
|
: undefined;
|
|
605
692
|
const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
|
|
693
|
+
const interruptController = new AbortController();
|
|
694
|
+
if (foregroundControl) {
|
|
695
|
+
foregroundControl.currentAgent = seqStep.agent;
|
|
696
|
+
foregroundControl.currentIndex = globalTaskIndex;
|
|
697
|
+
foregroundControl.currentActivityState = undefined;
|
|
698
|
+
foregroundControl.updatedAt = Date.now();
|
|
699
|
+
foregroundControl.interrupt = () => {
|
|
700
|
+
if (interruptController.signal.aborted) return false;
|
|
701
|
+
interruptController.abort();
|
|
702
|
+
foregroundControl.currentActivityState = undefined;
|
|
703
|
+
foregroundControl.updatedAt = Date.now();
|
|
704
|
+
return true;
|
|
705
|
+
};
|
|
706
|
+
}
|
|
606
707
|
|
|
607
708
|
const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
608
709
|
cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
|
|
609
710
|
signal,
|
|
711
|
+
interruptSignal: interruptController.signal,
|
|
610
712
|
runId,
|
|
611
713
|
index: globalTaskIndex,
|
|
612
714
|
sessionDir: sessionDirForIndex(globalTaskIndex),
|
|
@@ -616,28 +718,46 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
616
718
|
artifactConfig,
|
|
617
719
|
outputPath,
|
|
618
720
|
maxSubagentDepth,
|
|
721
|
+
controlConfig,
|
|
722
|
+
onControlEvent,
|
|
723
|
+
intercomSessionName: childIntercomTarget?.(seqStep.agent, globalTaskIndex),
|
|
619
724
|
modelOverride: effectiveModel,
|
|
620
725
|
availableModels,
|
|
621
726
|
preferredModelProvider: ctx.model?.provider,
|
|
622
727
|
skills: behavior.skills === false ? [] : behavior.skills,
|
|
623
728
|
onUpdate: onUpdate
|
|
624
729
|
? (p) => {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
},
|
|
637
|
-
});
|
|
730
|
+
const stepResults = p.details?.results || [];
|
|
731
|
+
const stepProgress = p.details?.progress || [];
|
|
732
|
+
if (foregroundControl && stepProgress.length > 0) {
|
|
733
|
+
const current = stepProgress[0];
|
|
734
|
+
foregroundControl.currentAgent = seqStep.agent;
|
|
735
|
+
foregroundControl.currentIndex = globalTaskIndex;
|
|
736
|
+
foregroundControl.currentActivityState = current?.activityState;
|
|
737
|
+
foregroundControl.lastActivityAt = current?.lastActivityAt;
|
|
738
|
+
foregroundControl.currentTool = current?.currentTool;
|
|
739
|
+
foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
|
|
740
|
+
foregroundControl.updatedAt = Date.now();
|
|
638
741
|
}
|
|
742
|
+
onUpdate({
|
|
743
|
+
...p,
|
|
744
|
+
details: {
|
|
745
|
+
mode: "chain",
|
|
746
|
+
results: results.concat(stepResults),
|
|
747
|
+
progress: allProgress.concat(stepProgress),
|
|
748
|
+
controlEvents: p.details?.controlEvents,
|
|
749
|
+
chainAgents,
|
|
750
|
+
totalSteps,
|
|
751
|
+
currentStepIndex: stepIndex,
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
}
|
|
639
755
|
: undefined,
|
|
640
756
|
});
|
|
757
|
+
if (foregroundControl?.currentIndex === globalTaskIndex) {
|
|
758
|
+
foregroundControl.interrupt = undefined;
|
|
759
|
+
foregroundControl.updatedAt = Date.now();
|
|
760
|
+
}
|
|
641
761
|
recordRun(seqStep.agent, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
|
|
642
762
|
|
|
643
763
|
globalTaskIndex++;
|
|
@@ -663,6 +783,22 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
663
783
|
}
|
|
664
784
|
}
|
|
665
785
|
|
|
786
|
+
if (r.interrupted) {
|
|
787
|
+
return {
|
|
788
|
+
content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
|
|
789
|
+
details: buildChainExecutionDetails({
|
|
790
|
+
results,
|
|
791
|
+
includeProgress,
|
|
792
|
+
allProgress,
|
|
793
|
+
allArtifactPaths,
|
|
794
|
+
artifactsDir,
|
|
795
|
+
chainAgents,
|
|
796
|
+
totalSteps,
|
|
797
|
+
currentStepIndex: stepIndex,
|
|
798
|
+
}),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
666
802
|
if (r.exitCode !== 0) {
|
|
667
803
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
668
804
|
index: stepIndex,
|
package/execution.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
type AgentProgress,
|
|
16
16
|
type ArtifactPaths,
|
|
17
|
+
type ControlEvent,
|
|
17
18
|
type ModelAttempt,
|
|
18
19
|
type RunSyncOptions,
|
|
19
20
|
type SingleResult,
|
|
@@ -24,6 +25,14 @@ import {
|
|
|
24
25
|
truncateOutput,
|
|
25
26
|
getSubagentDepthEnv,
|
|
26
27
|
} from "./types.ts";
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_CONTROL_CONFIG,
|
|
30
|
+
buildControlEvent,
|
|
31
|
+
claimControlNotification,
|
|
32
|
+
deriveActivityState,
|
|
33
|
+
shouldEmitControlEvent,
|
|
34
|
+
shouldNotifyControlEvent,
|
|
35
|
+
} from "./subagent-control.ts";
|
|
27
36
|
import {
|
|
28
37
|
getFinalOutput,
|
|
29
38
|
findLatestSessionFile,
|
|
@@ -86,6 +95,7 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
|
|
|
86
95
|
usage: attempt.usage ? { ...attempt.usage } : undefined,
|
|
87
96
|
}))
|
|
88
97
|
: undefined,
|
|
98
|
+
controlEvents: result.controlEvents ? result.controlEvents.map((event) => ({ ...event })) : undefined,
|
|
89
99
|
progress,
|
|
90
100
|
progressSummary: result.progressSummary ? { ...result.progressSummary } : undefined,
|
|
91
101
|
artifactPaths: result.artifactPaths ? { ...result.artifactPaths } : undefined,
|
|
@@ -127,6 +137,7 @@ async function runSingleAttempt(
|
|
|
127
137
|
systemPrompt: shared.systemPrompt,
|
|
128
138
|
mcpDirectTools: agent.mcpDirectTools,
|
|
129
139
|
promptFileStem: agent.name,
|
|
140
|
+
intercomSessionName: options.intercomSessionName,
|
|
130
141
|
});
|
|
131
142
|
|
|
132
143
|
const result: SingleResult = {
|
|
@@ -140,6 +151,11 @@ async function runSingleAttempt(
|
|
|
140
151
|
skills: shared.resolvedSkillNames,
|
|
141
152
|
skillsWarning: shared.skillsWarning,
|
|
142
153
|
};
|
|
154
|
+
const startTime = Date.now();
|
|
155
|
+
const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
156
|
+
let interruptedByControl = false;
|
|
157
|
+
const allControlEvents: ControlEvent[] = [];
|
|
158
|
+
let pendingControlEvents: ControlEvent[] = [];
|
|
143
159
|
|
|
144
160
|
const progress: AgentProgress = {
|
|
145
161
|
index: options.index ?? 0,
|
|
@@ -152,11 +168,9 @@ async function runSingleAttempt(
|
|
|
152
168
|
toolCount: 0,
|
|
153
169
|
tokens: 0,
|
|
154
170
|
durationMs: 0,
|
|
155
|
-
lastActivityAt:
|
|
171
|
+
lastActivityAt: startTime,
|
|
156
172
|
};
|
|
157
173
|
result.progress = progress;
|
|
158
|
-
|
|
159
|
-
const startTime = Date.now();
|
|
160
174
|
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
|
|
161
175
|
|
|
162
176
|
const exitCode = await new Promise<number>((resolve) => {
|
|
@@ -173,6 +187,8 @@ async function runSingleAttempt(
|
|
|
173
187
|
let detached = false;
|
|
174
188
|
let intercomStarted = false;
|
|
175
189
|
let removeAbortListener: (() => void) | undefined;
|
|
190
|
+
let removeInterruptListener: (() => void) | undefined;
|
|
191
|
+
let activityTimer: NodeJS.Timeout | undefined;
|
|
176
192
|
|
|
177
193
|
const detachForIntercom = () => {
|
|
178
194
|
detached = true;
|
|
@@ -241,18 +257,69 @@ async function runSingleAttempt(
|
|
|
241
257
|
settled = true;
|
|
242
258
|
clearFinalDrainTimers();
|
|
243
259
|
clearStdioGuard();
|
|
260
|
+
if (activityTimer) {
|
|
261
|
+
clearInterval(activityTimer);
|
|
262
|
+
activityTimer = undefined;
|
|
263
|
+
}
|
|
244
264
|
unsubscribeIntercomDetach?.();
|
|
245
265
|
removeAbortListener?.();
|
|
266
|
+
removeInterruptListener?.();
|
|
246
267
|
resolve(code);
|
|
247
268
|
};
|
|
248
269
|
|
|
270
|
+
const drainPendingControlEvents = (): ControlEvent[] | undefined => {
|
|
271
|
+
if (pendingControlEvents.length === 0) return undefined;
|
|
272
|
+
const events = pendingControlEvents;
|
|
273
|
+
pendingControlEvents = [];
|
|
274
|
+
return events;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const emittedControlEventKeys = new Set<string>();
|
|
278
|
+
const emitControlEvent = (event: ControlEvent) => {
|
|
279
|
+
if (shouldNotifyControlEvent(controlConfig, event) && !claimControlNotification(controlConfig, event, emittedControlEventKeys)) return;
|
|
280
|
+
allControlEvents.push(event);
|
|
281
|
+
pendingControlEvents.push(event);
|
|
282
|
+
options.onControlEvent?.(event);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const updateActivityState = (now: number): boolean => {
|
|
286
|
+
const next = deriveActivityState({
|
|
287
|
+
config: controlConfig,
|
|
288
|
+
startedAt: startTime,
|
|
289
|
+
lastActivityAt: progress.lastActivityAt,
|
|
290
|
+
now,
|
|
291
|
+
});
|
|
292
|
+
if (next === progress.activityState) return false;
|
|
293
|
+
const previous = progress.activityState;
|
|
294
|
+
progress.activityState = next;
|
|
295
|
+
if (shouldEmitControlEvent(controlConfig, previous, next)) {
|
|
296
|
+
emitControlEvent(buildControlEvent({
|
|
297
|
+
from: previous,
|
|
298
|
+
to: next,
|
|
299
|
+
runId: options.runId,
|
|
300
|
+
agent: agent.name,
|
|
301
|
+
index: options.index,
|
|
302
|
+
ts: now,
|
|
303
|
+
lastActivityAt: progress.lastActivityAt,
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
|
|
249
310
|
const emitUpdateSnapshot = (text: string) => {
|
|
250
311
|
if (!options.onUpdate || processClosed) return;
|
|
251
312
|
const progressSnapshot = snapshotProgress(progress);
|
|
252
313
|
const resultSnapshot = snapshotResult(result, progressSnapshot);
|
|
314
|
+
const controlEvents = drainPendingControlEvents();
|
|
253
315
|
options.onUpdate({
|
|
254
316
|
content: [{ type: "text", text }],
|
|
255
|
-
details: {
|
|
317
|
+
details: {
|
|
318
|
+
mode: "single",
|
|
319
|
+
results: [resultSnapshot],
|
|
320
|
+
progress: [progressSnapshot],
|
|
321
|
+
controlEvents,
|
|
322
|
+
},
|
|
256
323
|
});
|
|
257
324
|
};
|
|
258
325
|
|
|
@@ -276,6 +343,7 @@ async function runSingleAttempt(
|
|
|
276
343
|
const now = Date.now();
|
|
277
344
|
progress.durationMs = now - startTime;
|
|
278
345
|
progress.lastActivityAt = now;
|
|
346
|
+
updateActivityState(now);
|
|
279
347
|
|
|
280
348
|
if (evt.type === "tool_execution_start") {
|
|
281
349
|
if (options.allowIntercomDetach && evt.toolName === "intercom") {
|
|
@@ -336,6 +404,18 @@ async function runSingleAttempt(
|
|
|
336
404
|
}
|
|
337
405
|
};
|
|
338
406
|
|
|
407
|
+
if (controlConfig.enabled) {
|
|
408
|
+
activityTimer = setInterval(() => {
|
|
409
|
+
if (processClosed || settled || detached) return;
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
if (updateActivityState(now)) {
|
|
412
|
+
progress.durationMs = now - startTime;
|
|
413
|
+
fireUpdate();
|
|
414
|
+
}
|
|
415
|
+
}, 1000);
|
|
416
|
+
activityTimer.unref?.();
|
|
417
|
+
}
|
|
418
|
+
|
|
339
419
|
let stderrBuf = "";
|
|
340
420
|
|
|
341
421
|
const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
|
|
@@ -400,8 +480,46 @@ async function runSingleAttempt(
|
|
|
400
480
|
removeAbortListener = () => options.signal?.removeEventListener("abort", kill);
|
|
401
481
|
}
|
|
402
482
|
}
|
|
483
|
+
|
|
484
|
+
if (options.interruptSignal) {
|
|
485
|
+
const interrupt = () => {
|
|
486
|
+
if (processClosed || detached || settled) return;
|
|
487
|
+
interruptedByControl = true;
|
|
488
|
+
progress.status = "running";
|
|
489
|
+
progress.durationMs = Date.now() - startTime;
|
|
490
|
+
result.interrupted = true;
|
|
491
|
+
result.finalOutput = "Interrupted. Waiting for explicit next action.";
|
|
492
|
+
progress.activityState = undefined;
|
|
493
|
+
fireUpdate();
|
|
494
|
+
trySignalChild(proc, "SIGINT");
|
|
495
|
+
setTimeout(() => {
|
|
496
|
+
if (settled || processClosed || detached) return;
|
|
497
|
+
trySignalChild(proc, "SIGTERM");
|
|
498
|
+
}, 1000).unref?.();
|
|
499
|
+
};
|
|
500
|
+
if (options.interruptSignal.aborted) interrupt();
|
|
501
|
+
else {
|
|
502
|
+
options.interruptSignal.addEventListener("abort", interrupt, { once: true });
|
|
503
|
+
removeInterruptListener = () => options.interruptSignal?.removeEventListener("abort", interrupt);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
403
506
|
});
|
|
404
507
|
result.exitCode = exitCode;
|
|
508
|
+
if (interruptedByControl) {
|
|
509
|
+
result.exitCode = 0;
|
|
510
|
+
result.interrupted = true;
|
|
511
|
+
result.error = undefined;
|
|
512
|
+
result.finalOutput = result.finalOutput || "Interrupted. Waiting for explicit next action.";
|
|
513
|
+
result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
|
|
514
|
+
progress.activityState = undefined;
|
|
515
|
+
progress.durationMs = Date.now() - startTime;
|
|
516
|
+
result.progressSummary = {
|
|
517
|
+
toolCount: progress.toolCount,
|
|
518
|
+
tokens: progress.tokens,
|
|
519
|
+
durationMs: progress.durationMs,
|
|
520
|
+
};
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
405
523
|
if (result.detached) {
|
|
406
524
|
result.exitCode = 0;
|
|
407
525
|
result.finalOutput = "Detached for intercom coordination.";
|