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/subagent-runner.ts
CHANGED
|
@@ -8,15 +8,26 @@ 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 {
|
|
23
|
+
DEFAULT_CONTROL_CONFIG,
|
|
24
|
+
buildControlEvent,
|
|
25
|
+
deriveActivityState,
|
|
26
|
+
claimControlNotification,
|
|
27
|
+
formatControlIntercomMessage,
|
|
28
|
+
formatControlNoticeMessage,
|
|
29
|
+
shouldEmitControlEvent,
|
|
30
|
+
} from "./subagent-control.ts";
|
|
20
31
|
import {
|
|
21
32
|
type RunnerSubagentStep as SubagentStep,
|
|
22
33
|
type RunnerStep,
|
|
@@ -60,6 +71,9 @@ interface SubagentRunConfig {
|
|
|
60
71
|
piArgv1?: string;
|
|
61
72
|
worktreeSetupHook?: string;
|
|
62
73
|
worktreeSetupHookTimeoutMs?: number;
|
|
74
|
+
controlConfig?: ResolvedControlConfig;
|
|
75
|
+
controlIntercomTarget?: string;
|
|
76
|
+
childIntercomTargets?: Array<string | undefined>;
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
interface StepResult {
|
|
@@ -75,6 +89,7 @@ interface StepResult {
|
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
const require = createRequire(import.meta.url);
|
|
92
|
+
const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
78
93
|
|
|
79
94
|
function findLatestSessionFile(sessionDir: string): string | null {
|
|
80
95
|
try {
|
|
@@ -95,6 +110,18 @@ function emptyUsage(): Usage {
|
|
|
95
110
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
96
111
|
}
|
|
97
112
|
|
|
113
|
+
function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsage | null {
|
|
114
|
+
if (!attempts || attempts.length === 0) return null;
|
|
115
|
+
let input = 0;
|
|
116
|
+
let output = 0;
|
|
117
|
+
for (const attempt of attempts) {
|
|
118
|
+
input += attempt.usage?.input ?? 0;
|
|
119
|
+
output += attempt.usage?.output ?? 0;
|
|
120
|
+
}
|
|
121
|
+
const total = input + output;
|
|
122
|
+
return total > 0 ? { input, output, total } : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
98
125
|
interface ChildEventContext {
|
|
99
126
|
eventsPath: string;
|
|
100
127
|
runId: string;
|
|
@@ -133,6 +160,7 @@ interface RunPiStreamingResult {
|
|
|
133
160
|
model?: string;
|
|
134
161
|
error?: string;
|
|
135
162
|
finalOutput: string;
|
|
163
|
+
interrupted?: boolean;
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
function runPiStreaming(
|
|
@@ -144,6 +172,7 @@ function runPiStreaming(
|
|
|
144
172
|
piArgv1?: string,
|
|
145
173
|
maxSubagentDepth?: number,
|
|
146
174
|
childEventContext?: ChildEventContext,
|
|
175
|
+
registerInterrupt?: (interrupt: (() => void) | undefined) => void,
|
|
147
176
|
): Promise<RunPiStreamingResult> {
|
|
148
177
|
return new Promise((resolve) => {
|
|
149
178
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
@@ -160,6 +189,7 @@ function runPiStreaming(
|
|
|
160
189
|
const usage = emptyUsage();
|
|
161
190
|
let model: string | undefined;
|
|
162
191
|
let error: string | undefined;
|
|
192
|
+
let interrupted = false;
|
|
163
193
|
const rawStdoutLines: string[] = [];
|
|
164
194
|
|
|
165
195
|
const writeOutputLine = (line: string) => {
|
|
@@ -267,6 +297,15 @@ function runPiStreaming(
|
|
|
267
297
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
268
298
|
processStderrText(chunk.toString());
|
|
269
299
|
});
|
|
300
|
+
registerInterrupt?.(() => {
|
|
301
|
+
if (settled) return;
|
|
302
|
+
interrupted = true;
|
|
303
|
+
if (!error) error = "Interrupted. Waiting for explicit next action.";
|
|
304
|
+
trySignalChild(child, "SIGINT");
|
|
305
|
+
setTimeout(() => {
|
|
306
|
+
if (!settled) trySignalChild(child, "SIGTERM");
|
|
307
|
+
}, 1000).unref?.();
|
|
308
|
+
});
|
|
270
309
|
const clearDrainTimers = () => {
|
|
271
310
|
if (finalDrainTimer) {
|
|
272
311
|
clearTimeout(finalDrainTimer);
|
|
@@ -301,6 +340,7 @@ function runPiStreaming(
|
|
|
301
340
|
});
|
|
302
341
|
child.on("close", (exitCode, signal) => {
|
|
303
342
|
settled = true;
|
|
343
|
+
registerInterrupt?.(undefined);
|
|
304
344
|
clearDrainTimers();
|
|
305
345
|
clearStdioGuard();
|
|
306
346
|
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
@@ -309,17 +349,19 @@ function runPiStreaming(
|
|
|
309
349
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
310
350
|
resolve({
|
|
311
351
|
stderr,
|
|
312
|
-
exitCode: forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
352
|
+
exitCode: interrupted ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
313
353
|
messages,
|
|
314
354
|
usage,
|
|
315
355
|
model,
|
|
316
|
-
error,
|
|
356
|
+
error: interrupted ? undefined : error,
|
|
317
357
|
finalOutput,
|
|
358
|
+
interrupted,
|
|
318
359
|
});
|
|
319
360
|
});
|
|
320
361
|
|
|
321
362
|
child.on("error", (spawnError) => {
|
|
322
363
|
settled = true;
|
|
364
|
+
registerInterrupt?.(undefined);
|
|
323
365
|
clearDrainTimers();
|
|
324
366
|
clearStdioGuard();
|
|
325
367
|
outputStream.end();
|
|
@@ -481,6 +523,8 @@ interface SingleStepContext {
|
|
|
481
523
|
outputFile: string;
|
|
482
524
|
piPackageRoot?: string;
|
|
483
525
|
piArgv1?: string;
|
|
526
|
+
registerInterrupt?: (interrupt: (() => void) | undefined) => void;
|
|
527
|
+
childIntercomTarget?: string;
|
|
484
528
|
}
|
|
485
529
|
|
|
486
530
|
/** Run a single pi agent step, returning output and metadata */
|
|
@@ -496,6 +540,7 @@ async function runSingleStep(
|
|
|
496
540
|
attemptedModels?: string[];
|
|
497
541
|
modelAttempts?: ModelAttempt[];
|
|
498
542
|
artifactPaths?: ArtifactPaths;
|
|
543
|
+
interrupted?: boolean;
|
|
499
544
|
}> {
|
|
500
545
|
const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
501
546
|
const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
|
|
@@ -541,6 +586,7 @@ async function runSingleStep(
|
|
|
541
586
|
systemPromptMode: step.systemPromptMode,
|
|
542
587
|
mcpDirectTools: step.mcpDirectTools,
|
|
543
588
|
promptFileStem: step.agent,
|
|
589
|
+
intercomSessionName: ctx.childIntercomTarget,
|
|
544
590
|
});
|
|
545
591
|
const run = await runPiStreaming(
|
|
546
592
|
args,
|
|
@@ -551,6 +597,7 @@ async function runSingleStep(
|
|
|
551
597
|
ctx.piArgv1,
|
|
552
598
|
step.maxSubagentDepth,
|
|
553
599
|
{ eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
600
|
+
ctx.registerInterrupt,
|
|
554
601
|
);
|
|
555
602
|
cleanupTempDir(tempDir);
|
|
556
603
|
|
|
@@ -627,13 +674,18 @@ async function runSingleStep(
|
|
|
627
674
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
628
675
|
modelAttempts,
|
|
629
676
|
artifactPaths,
|
|
677
|
+
interrupted: finalResult?.interrupted,
|
|
630
678
|
};
|
|
631
679
|
}
|
|
632
680
|
|
|
633
681
|
type RunnerStatusPayload = {
|
|
634
682
|
runId: string;
|
|
635
683
|
mode: "single" | "chain";
|
|
636
|
-
state: "queued" | "running" | "complete" | "failed";
|
|
684
|
+
state: "queued" | "running" | "complete" | "failed" | "paused";
|
|
685
|
+
activityState?: ActivityState;
|
|
686
|
+
lastActivityAt?: number;
|
|
687
|
+
currentTool?: string;
|
|
688
|
+
currentToolStartedAt?: number;
|
|
637
689
|
startedAt: number;
|
|
638
690
|
endedAt?: number;
|
|
639
691
|
lastUpdate: number;
|
|
@@ -643,6 +695,10 @@ type RunnerStatusPayload = {
|
|
|
643
695
|
steps: Array<{
|
|
644
696
|
agent: string;
|
|
645
697
|
status: "pending" | "running" | "complete" | "failed";
|
|
698
|
+
activityState?: ActivityState;
|
|
699
|
+
lastActivityAt?: number;
|
|
700
|
+
currentTool?: string;
|
|
701
|
+
currentToolStartedAt?: number;
|
|
646
702
|
startedAt?: number;
|
|
647
703
|
endedAt?: number;
|
|
648
704
|
durationMs?: number;
|
|
@@ -715,8 +771,11 @@ function markParallelGroupRunning(input: {
|
|
|
715
771
|
const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
|
|
716
772
|
input.statusPayload.steps[flatTaskIndex].status = "running";
|
|
717
773
|
input.statusPayload.steps[flatTaskIndex].startedAt = input.groupStartTime;
|
|
774
|
+
input.statusPayload.steps[flatTaskIndex].lastActivityAt = input.groupStartTime;
|
|
718
775
|
}
|
|
719
776
|
input.statusPayload.currentStep = input.groupStartFlatIndex;
|
|
777
|
+
input.statusPayload.activityState = undefined;
|
|
778
|
+
input.statusPayload.lastActivityAt = input.groupStartTime;
|
|
720
779
|
input.statusPayload.lastUpdate = input.groupStartTime;
|
|
721
780
|
input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
|
|
722
781
|
writeJson(input.statusPath, input.statusPayload);
|
|
@@ -769,6 +828,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
769
828
|
const statusPath = path.join(asyncDir, "status.json");
|
|
770
829
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
771
830
|
const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
|
|
831
|
+
const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
832
|
+
let activeChildInterrupt: (() => void) | undefined;
|
|
833
|
+
let interrupted = false;
|
|
834
|
+
let currentActivityState: ActivityState | undefined;
|
|
835
|
+
let activityTimer: NodeJS.Timeout | undefined;
|
|
772
836
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
773
837
|
let latestSessionFile: string | undefined;
|
|
774
838
|
|
|
@@ -780,6 +844,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
780
844
|
runId: id,
|
|
781
845
|
mode: flatSteps.length > 1 ? "chain" : "single",
|
|
782
846
|
state: "running",
|
|
847
|
+
lastActivityAt: overallStartTime,
|
|
783
848
|
startedAt: overallStartTime,
|
|
784
849
|
lastUpdate: overallStartTime,
|
|
785
850
|
pid: process.pid,
|
|
@@ -799,6 +864,107 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
799
864
|
|
|
800
865
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
801
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
|
+
|
|
945
|
+
const interruptRunner = () => {
|
|
946
|
+
if (interrupted || statusPayload.state !== "running") return;
|
|
947
|
+
interrupted = true;
|
|
948
|
+
const now = Date.now();
|
|
949
|
+
statusPayload.state = "paused";
|
|
950
|
+
currentActivityState = undefined;
|
|
951
|
+
statusPayload.activityState = undefined;
|
|
952
|
+
statusPayload.lastUpdate = now;
|
|
953
|
+
const current = statusPayload.steps[statusPayload.currentStep];
|
|
954
|
+
if (current?.status === "running") {
|
|
955
|
+
current.activityState = undefined;
|
|
956
|
+
current.endedAt = now;
|
|
957
|
+
current.durationMs = current.startedAt ? now - current.startedAt : undefined;
|
|
958
|
+
}
|
|
959
|
+
writeJson(statusPath, statusPayload);
|
|
960
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
961
|
+
type: "subagent.run.paused",
|
|
962
|
+
ts: now,
|
|
963
|
+
runId: id,
|
|
964
|
+
}));
|
|
965
|
+
activeChildInterrupt?.();
|
|
966
|
+
};
|
|
967
|
+
process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
|
|
802
968
|
appendJsonl(
|
|
803
969
|
eventsPath,
|
|
804
970
|
JSON.stringify({
|
|
@@ -814,6 +980,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
814
980
|
let flatIndex = 0;
|
|
815
981
|
|
|
816
982
|
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
983
|
+
if (interrupted) break;
|
|
817
984
|
const step = steps[stepIndex];
|
|
818
985
|
|
|
819
986
|
if (isParallelGroup(step)) {
|
|
@@ -912,6 +1079,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
912
1079
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
913
1080
|
piPackageRoot: config.piPackageRoot,
|
|
914
1081
|
piArgv1: config.piArgv1,
|
|
1082
|
+
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
1083
|
+
registerInterrupt: (interrupt) => {
|
|
1084
|
+
activeChildInterrupt = interrupt;
|
|
1085
|
+
},
|
|
915
1086
|
});
|
|
916
1087
|
if (task.sessionFile) {
|
|
917
1088
|
latestSessionFile = task.sessionFile;
|
|
@@ -944,24 +1115,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
944
1115
|
|
|
945
1116
|
flatIndex += group.parallel.length;
|
|
946
1117
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
statusPayload.totalTokens = { ...previousCumulativeTokens };
|
|
962
|
-
statusPayload.lastUpdate = Date.now();
|
|
963
|
-
writeJson(statusPath, statusPayload);
|
|
1118
|
+
for (let t = 0; t < group.parallel.length; t++) {
|
|
1119
|
+
const fi = groupStartFlatIndex + t;
|
|
1120
|
+
const sessionTokens = config.sessionDir
|
|
1121
|
+
? parseSessionTokens(path.join(config.sessionDir, `parallel-${t}`))
|
|
1122
|
+
: null;
|
|
1123
|
+
const taskTokens = sessionTokens ?? tokenUsageFromAttempts(parallelResults[t]?.modelAttempts);
|
|
1124
|
+
if (!taskTokens) continue;
|
|
1125
|
+
statusPayload.steps[fi].tokens = taskTokens;
|
|
1126
|
+
previousCumulativeTokens = {
|
|
1127
|
+
input: previousCumulativeTokens.input + taskTokens.input,
|
|
1128
|
+
output: previousCumulativeTokens.output + taskTokens.output,
|
|
1129
|
+
total: previousCumulativeTokens.total + taskTokens.total,
|
|
1130
|
+
};
|
|
964
1131
|
}
|
|
1132
|
+
statusPayload.totalTokens = { ...previousCumulativeTokens };
|
|
1133
|
+
statusPayload.lastUpdate = Date.now();
|
|
1134
|
+
writeJson(statusPath, statusPayload);
|
|
965
1135
|
|
|
966
1136
|
for (const pr of parallelResults) {
|
|
967
1137
|
results.push({
|
|
@@ -1007,8 +1177,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1007
1177
|
const stepStartTime = Date.now();
|
|
1008
1178
|
statusPayload.currentStep = flatIndex;
|
|
1009
1179
|
statusPayload.steps[flatIndex].status = "running";
|
|
1180
|
+
statusPayload.steps[flatIndex].activityState = undefined;
|
|
1181
|
+
statusPayload.activityState = undefined;
|
|
1010
1182
|
statusPayload.steps[flatIndex].skills = seqStep.skills;
|
|
1011
1183
|
statusPayload.steps[flatIndex].startedAt = stepStartTime;
|
|
1184
|
+
statusPayload.steps[flatIndex].lastActivityAt = stepStartTime;
|
|
1185
|
+
statusPayload.lastActivityAt = stepStartTime;
|
|
1012
1186
|
statusPayload.lastUpdate = stepStartTime;
|
|
1013
1187
|
statusPayload.outputFile = path.join(asyncDir, `output-${flatIndex}.log`);
|
|
1014
1188
|
writeJson(statusPath, statusPayload);
|
|
@@ -1029,6 +1203,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1029
1203
|
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
1030
1204
|
piPackageRoot: config.piPackageRoot,
|
|
1031
1205
|
piArgv1: config.piArgv1,
|
|
1206
|
+
childIntercomTarget: config.childIntercomTargets?.[flatIndex],
|
|
1207
|
+
registerInterrupt: (interrupt) => {
|
|
1208
|
+
activeChildInterrupt = interrupt;
|
|
1209
|
+
},
|
|
1032
1210
|
});
|
|
1033
1211
|
if (seqStep.sessionFile) {
|
|
1034
1212
|
latestSessionFile = seqStep.sessionFile;
|
|
@@ -1046,7 +1224,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1046
1224
|
});
|
|
1047
1225
|
|
|
1048
1226
|
const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
|
|
1049
|
-
|
|
1227
|
+
let stepTokens: TokenUsage | null = cumulativeTokens
|
|
1050
1228
|
? {
|
|
1051
1229
|
input: cumulativeTokens.input - previousCumulativeTokens.input,
|
|
1052
1230
|
output: cumulativeTokens.output - previousCumulativeTokens.output,
|
|
@@ -1055,6 +1233,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1055
1233
|
: null;
|
|
1056
1234
|
if (cumulativeTokens) {
|
|
1057
1235
|
previousCumulativeTokens = cumulativeTokens;
|
|
1236
|
+
} else {
|
|
1237
|
+
stepTokens = tokenUsageFromAttempts(singleResult.modelAttempts);
|
|
1238
|
+
if (stepTokens) {
|
|
1239
|
+
previousCumulativeTokens = {
|
|
1240
|
+
input: previousCumulativeTokens.input + stepTokens.input,
|
|
1241
|
+
output: previousCumulativeTokens.output + stepTokens.output,
|
|
1242
|
+
total: previousCumulativeTokens.total + stepTokens.total,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1058
1245
|
}
|
|
1059
1246
|
|
|
1060
1247
|
const stepEndTime = Date.now();
|
|
@@ -1137,9 +1324,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1137
1324
|
}
|
|
1138
1325
|
}
|
|
1139
1326
|
|
|
1327
|
+
if (activityTimer) {
|
|
1328
|
+
clearInterval(activityTimer);
|
|
1329
|
+
activityTimer = undefined;
|
|
1330
|
+
}
|
|
1140
1331
|
const effectiveSessionFile = sessionFile ?? latestSessionFile;
|
|
1141
1332
|
const runEndedAt = Date.now();
|
|
1142
|
-
statusPayload.state = results.every((r) => r.success) ? "complete" : "failed";
|
|
1333
|
+
statusPayload.state = interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
|
|
1334
|
+
statusPayload.activityState = undefined;
|
|
1143
1335
|
statusPayload.endedAt = runEndedAt;
|
|
1144
1336
|
statusPayload.lastUpdate = runEndedAt;
|
|
1145
1337
|
statusPayload.sessionFile = effectiveSessionFile;
|
|
@@ -1186,8 +1378,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1186
1378
|
writeJson(resultPath, {
|
|
1187
1379
|
id,
|
|
1188
1380
|
agent: agentName,
|
|
1189
|
-
success: results.every((r) => r.success),
|
|
1190
|
-
|
|
1381
|
+
success: !interrupted && results.every((r) => r.success),
|
|
1382
|
+
state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
|
|
1383
|
+
summary: interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
|
|
1191
1384
|
results: results.map((r) => ({
|
|
1192
1385
|
agent: r.agent,
|
|
1193
1386
|
output: r.output,
|
|
@@ -1199,7 +1392,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1199
1392
|
artifactPaths: r.artifactPaths,
|
|
1200
1393
|
truncated: r.truncated,
|
|
1201
1394
|
})),
|
|
1202
|
-
exitCode: results.every((r) => r.success) ? 0 : 1,
|
|
1395
|
+
exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
1203
1396
|
timestamp: runEndedAt,
|
|
1204
1397
|
durationMs: runEndedAt - overallStartTime,
|
|
1205
1398
|
truncated,
|
package/subagents-status.ts
CHANGED
|
@@ -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,12 @@ 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
|
|
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
|
+
: "";
|
|
180
|
+
const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${activity}${model}${attempts}${duration}${tokens}`;
|
|
174
181
|
lines.push(row(truncateToWidth(line, innerW), width, this.theme));
|
|
175
182
|
if (step.error) {
|
|
176
183
|
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 = "needs_attention";
|
|
44
|
+
export type ControlEventType = "needs_attention";
|
|
45
|
+
export type ControlNotificationChannel = "event" | "async" | "intercom";
|
|
46
|
+
|
|
47
|
+
export interface ControlConfig {
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
needsAttentionAfterMs?: number;
|
|
50
|
+
notifyOn?: ControlEventType[];
|
|
51
|
+
notifyChannels?: ControlNotificationChannel[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ResolvedControlConfig {
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
needsAttentionAfterMs: number;
|
|
57
|
+
notifyOn: ControlEventType[];
|
|
58
|
+
notifyChannels: ControlNotificationChannel[];
|
|
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,26 @@ 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;
|
|
200
|
+
lastActivityAt?: number;
|
|
201
|
+
currentTool?: string;
|
|
202
|
+
currentToolStartedAt?: number;
|
|
166
203
|
startedAt: number;
|
|
167
204
|
endedAt?: number;
|
|
168
205
|
lastUpdate?: number;
|
|
206
|
+
pid?: number;
|
|
169
207
|
cwd?: string;
|
|
170
208
|
currentStep?: number;
|
|
171
209
|
steps?: Array<{
|
|
172
210
|
agent: string;
|
|
173
211
|
status: string;
|
|
212
|
+
activityState?: ActivityState;
|
|
213
|
+
lastActivityAt?: number;
|
|
214
|
+
currentTool?: string;
|
|
215
|
+
currentToolStartedAt?: number;
|
|
216
|
+
startedAt?: number;
|
|
217
|
+
endedAt?: number;
|
|
174
218
|
durationMs?: number;
|
|
175
219
|
tokens?: TokenUsage;
|
|
176
220
|
skills?: string[];
|
|
@@ -188,7 +232,11 @@ export interface AsyncStatus {
|
|
|
188
232
|
export interface AsyncJobState {
|
|
189
233
|
asyncId: string;
|
|
190
234
|
asyncDir: string;
|
|
191
|
-
status: "queued" | "running" | "complete" | "failed";
|
|
235
|
+
status: "queued" | "running" | "complete" | "failed" | "paused";
|
|
236
|
+
activityState?: ActivityState;
|
|
237
|
+
lastActivityAt?: number;
|
|
238
|
+
currentTool?: string;
|
|
239
|
+
currentToolStartedAt?: number;
|
|
192
240
|
mode?: "single" | "chain";
|
|
193
241
|
agents?: string[];
|
|
194
242
|
currentStep?: number;
|
|
@@ -199,12 +247,27 @@ export interface AsyncJobState {
|
|
|
199
247
|
outputFile?: string;
|
|
200
248
|
totalTokens?: TokenUsage;
|
|
201
249
|
sessionFile?: string;
|
|
250
|
+
controlEventCursor?: number;
|
|
202
251
|
}
|
|
203
252
|
|
|
204
253
|
export interface SubagentState {
|
|
205
254
|
baseCwd: string;
|
|
206
255
|
currentSessionId: string | null;
|
|
207
256
|
asyncJobs: Map<string, AsyncJobState>;
|
|
257
|
+
foregroundControls: Map<string, {
|
|
258
|
+
runId: string;
|
|
259
|
+
mode: "single" | "parallel" | "chain";
|
|
260
|
+
startedAt: number;
|
|
261
|
+
updatedAt: number;
|
|
262
|
+
currentAgent?: string;
|
|
263
|
+
currentIndex?: number;
|
|
264
|
+
currentActivityState?: ActivityState;
|
|
265
|
+
lastActivityAt?: number;
|
|
266
|
+
currentTool?: string;
|
|
267
|
+
currentToolStartedAt?: number;
|
|
268
|
+
interrupt?: () => boolean;
|
|
269
|
+
}>;
|
|
270
|
+
lastForegroundControlId: string | null;
|
|
208
271
|
cleanupTimers: Map<string, ReturnType<typeof setTimeout>>;
|
|
209
272
|
lastUiContext: ExtensionContext | null;
|
|
210
273
|
poller: NodeJS.Timeout | null;
|
|
@@ -243,6 +306,10 @@ export interface IntercomEventBus {
|
|
|
243
306
|
|
|
244
307
|
export const INTERCOM_DETACH_REQUEST_EVENT = "pi-intercom:detach-request";
|
|
245
308
|
export const INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
|
|
309
|
+
export const SUBAGENT_ASYNC_STARTED_EVENT = "subagent:async-started";
|
|
310
|
+
export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
|
|
311
|
+
export const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
|
|
312
|
+
export const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
|
|
246
313
|
|
|
247
314
|
// ============================================================================
|
|
248
315
|
// Execution Options
|
|
@@ -251,9 +318,13 @@ export const INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
|
|
|
251
318
|
export interface RunSyncOptions {
|
|
252
319
|
cwd?: string;
|
|
253
320
|
signal?: AbortSignal;
|
|
321
|
+
interruptSignal?: AbortSignal;
|
|
254
322
|
allowIntercomDetach?: boolean;
|
|
255
323
|
intercomEvents?: IntercomEventBus;
|
|
256
324
|
onUpdate?: (r: import("@mariozechner/pi-agent-core").AgentToolResult<Details>) => void;
|
|
325
|
+
onControlEvent?: (event: ControlEvent) => void;
|
|
326
|
+
controlConfig?: ResolvedControlConfig;
|
|
327
|
+
intercomSessionName?: string;
|
|
257
328
|
maxOutput?: MaxOutputConfig;
|
|
258
329
|
artifactsDir?: string;
|
|
259
330
|
artifactConfig?: ArtifactConfig;
|
|
@@ -291,6 +362,7 @@ export interface ExtensionConfig {
|
|
|
291
362
|
forceTopLevelAsync?: boolean;
|
|
292
363
|
defaultSessionDir?: string;
|
|
293
364
|
maxSubagentDepth?: number;
|
|
365
|
+
control?: ControlConfig;
|
|
294
366
|
parallel?: TopLevelParallelConfig;
|
|
295
367
|
worktreeSetupHook?: string;
|
|
296
368
|
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,
|