pi-subagents 0.17.4 → 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.
- package/CHANGELOG.md +12 -0
- 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/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/async-execution.ts +7 -0
- package/async-job-tracker.ts +5 -1
- package/async-status.ts +53 -18
- package/chain-execution.ts +137 -26
- package/execution.ts +134 -4
- package/index.ts +12 -3
- package/package.json +1 -1
- package/pi-spawn.ts +9 -6
- package/render.ts +19 -4
- package/schemas.ts +12 -1
- package/skills/pi-subagents/SKILL.md +40 -0
- package/slash-live-state.ts +4 -0
- package/subagent-control.ts +106 -0
- package/subagent-executor.ts +236 -5
- package/subagent-runner.ts +110 -25
- package/subagents-status.ts +4 -1
- package/types.ts +54 -2
- package/utils.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.17.5] - 2026-04-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added subagent control activity state for foreground and async runs, including `starting`/`active`/`quiet`/`stalled`/`paused` tracking, compact stalled/recovered/paused control events, and an in-tool `action: "interrupt"` soft interrupt that pauses the current child turn without adding another top-level tool.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Updated bundled agents to use `openai-codex/gpt-5.5` defaults, with `scout` on `openai-codex/gpt-5.5-mini` and `oracle-executor` on `openai-codex/gpt-5.5:xhigh`.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Async/background status token reporting now falls back to in-memory model-attempt usage when detached runs do not produce session `.jsonl` files, which also preserves token totals across model fallback retries.
|
|
15
|
+
- Non-Windows subagent launches now use plain `pi` again instead of reusing the current CLI script path, avoiding runs that get confused by installed `dist/cli.js` entrypoints.
|
|
16
|
+
|
|
5
17
|
## [0.17.4] - 2026-04-22
|
|
6
18
|
|
|
7
19
|
### Added
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: context-builder
|
|
3
3
|
description: Analyzes requirements and codebase, generates context and meta-prompt
|
|
4
4
|
tools: read, grep, find, ls, bash, write, web_search
|
|
5
|
-
model: openai-codex/gpt-5.
|
|
5
|
+
model: openai-codex/gpt-5.5
|
|
6
6
|
systemPromptMode: replace
|
|
7
7
|
inheritProjectContext: true
|
|
8
8
|
inheritSkills: false
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: oracle-executor
|
|
3
3
|
description: High-context implementation agent that executes only after main-agent approval
|
|
4
4
|
tools: read, grep, find, ls, bash, edit, write, intercom
|
|
5
|
-
model: openai-codex/gpt-5.
|
|
5
|
+
model: openai-codex/gpt-5.5:xhigh
|
|
6
6
|
thinking: high
|
|
7
7
|
systemPromptMode: replace
|
|
8
8
|
inheritProjectContext: true
|
package/agents/oracle.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: oracle
|
|
3
3
|
description: High-context decision-consistency oracle that protects inherited state and prevents drift
|
|
4
4
|
tools: read, grep, find, ls, bash, intercom
|
|
5
|
-
model: openai-codex/gpt-5.
|
|
5
|
+
model: openai-codex/gpt-5.5
|
|
6
6
|
thinking: high
|
|
7
7
|
systemPromptMode: replace
|
|
8
8
|
inheritProjectContext: true
|
package/agents/planner.md
CHANGED
package/agents/researcher.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: researcher
|
|
3
3
|
description: Autonomous web researcher — searches, evaluates, and synthesizes a focused research brief
|
|
4
4
|
tools: read, write, web_search, fetch_content, get_search_content
|
|
5
|
-
model: openai-codex/gpt-5.
|
|
5
|
+
model: openai-codex/gpt-5.5
|
|
6
6
|
systemPromptMode: replace
|
|
7
7
|
inheritProjectContext: true
|
|
8
8
|
inheritSkills: false
|
package/agents/reviewer.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Code review specialist that validates implementation and fixes issues
|
|
4
4
|
tools: read, grep, find, ls, bash, edit, write
|
|
5
|
-
model: openai-codex/gpt-5.
|
|
5
|
+
model: openai-codex/gpt-5.5
|
|
6
6
|
thinking: high
|
|
7
7
|
systemPromptMode: replace
|
|
8
8
|
inheritProjectContext: true
|
package/agents/scout.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: scout
|
|
3
3
|
description: Fast codebase recon that returns compressed context for handoff
|
|
4
4
|
tools: read, grep, find, ls, bash, write
|
|
5
|
-
model: openai-codex/gpt-5.
|
|
5
|
+
model: openai-codex/gpt-5.5-mini
|
|
6
6
|
systemPromptMode: replace
|
|
7
7
|
inheritProjectContext: true
|
|
8
8
|
inheritSkills: false
|
package/agents/worker.md
CHANGED
package/async-execution.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type ArtifactConfig,
|
|
23
23
|
type Details,
|
|
24
24
|
type MaxOutputConfig,
|
|
25
|
+
type ResolvedControlConfig,
|
|
25
26
|
ASYNC_DIR,
|
|
26
27
|
RESULTS_DIR,
|
|
27
28
|
TEMP_ROOT_DIR,
|
|
@@ -75,6 +76,7 @@ export interface AsyncChainParams {
|
|
|
75
76
|
maxSubagentDepth: number;
|
|
76
77
|
worktreeSetupHook?: string;
|
|
77
78
|
worktreeSetupHookTimeoutMs?: number;
|
|
79
|
+
controlConfig?: ResolvedControlConfig;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
export interface AsyncSingleParams {
|
|
@@ -96,6 +98,7 @@ export interface AsyncSingleParams {
|
|
|
96
98
|
maxSubagentDepth: number;
|
|
97
99
|
worktreeSetupHook?: string;
|
|
98
100
|
worktreeSetupHookTimeoutMs?: number;
|
|
101
|
+
controlConfig?: ResolvedControlConfig;
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
export interface AsyncExecutionResult {
|
|
@@ -178,6 +181,7 @@ export function executeAsyncChain(
|
|
|
178
181
|
maxSubagentDepth,
|
|
179
182
|
worktreeSetupHook,
|
|
180
183
|
worktreeSetupHookTimeoutMs,
|
|
184
|
+
controlConfig,
|
|
181
185
|
} = params;
|
|
182
186
|
const chainSkills = params.chainSkills ?? [];
|
|
183
187
|
const availableModels = params.availableModels;
|
|
@@ -297,6 +301,7 @@ export function executeAsyncChain(
|
|
|
297
301
|
piArgv1: process.argv[1],
|
|
298
302
|
worktreeSetupHook,
|
|
299
303
|
worktreeSetupHookTimeoutMs,
|
|
304
|
+
controlConfig,
|
|
300
305
|
},
|
|
301
306
|
id,
|
|
302
307
|
runnerCwd,
|
|
@@ -364,6 +369,7 @@ export function executeAsyncSingle(
|
|
|
364
369
|
maxSubagentDepth,
|
|
365
370
|
worktreeSetupHook,
|
|
366
371
|
worktreeSetupHookTimeoutMs,
|
|
372
|
+
controlConfig,
|
|
367
373
|
} = params;
|
|
368
374
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
369
375
|
const skillNames = params.skills ?? agentConfig.skills ?? [];
|
|
@@ -430,6 +436,7 @@ export function executeAsyncSingle(
|
|
|
430
436
|
piArgv1: process.argv[1],
|
|
431
437
|
worktreeSetupHook,
|
|
432
438
|
worktreeSetupHookTimeoutMs,
|
|
439
|
+
controlConfig,
|
|
433
440
|
},
|
|
434
441
|
id,
|
|
435
442
|
runnerCwd,
|
package/async-job-tracker.ts
CHANGED
|
@@ -56,6 +56,7 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
56
56
|
if (status) {
|
|
57
57
|
const previousStatus = job.status;
|
|
58
58
|
job.status = status.state;
|
|
59
|
+
job.activityState = status.activityState;
|
|
59
60
|
job.mode = status.mode;
|
|
60
61
|
job.currentStep = status.currentStep ?? job.currentStep;
|
|
61
62
|
job.stepsTotal = status.steps?.length ?? job.stepsTotal;
|
|
@@ -68,7 +69,7 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
68
69
|
job.outputFile = status.outputFile ?? job.outputFile;
|
|
69
70
|
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
70
71
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
71
|
-
if ((job.status === "complete" || job.status === "failed") && previousStatus !== job.status) {
|
|
72
|
+
if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && previousStatus !== job.status) {
|
|
72
73
|
scheduleCleanup(job.asyncId);
|
|
73
74
|
}
|
|
74
75
|
continue;
|
|
@@ -102,6 +103,7 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
102
103
|
asyncId: info.id,
|
|
103
104
|
asyncDir,
|
|
104
105
|
status: "queued",
|
|
106
|
+
activityState: "starting",
|
|
105
107
|
mode: info.chain ? "chain" : "single",
|
|
106
108
|
agents,
|
|
107
109
|
stepsTotal: agents?.length,
|
|
@@ -136,6 +138,8 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
136
138
|
}
|
|
137
139
|
state.cleanupTimers.clear();
|
|
138
140
|
state.asyncJobs.clear();
|
|
141
|
+
state.foregroundControls?.clear();
|
|
142
|
+
state.lastForegroundControlId = null;
|
|
139
143
|
state.resultFileCoalescer.clear();
|
|
140
144
|
if (ctx?.hasUI) {
|
|
141
145
|
state.lastUiContext = ctx;
|
package/async-status.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatTokens, shortenPath } from "./formatters.ts";
|
|
4
|
-
import { type AsyncStatus, type TokenUsage } from "./types.ts";
|
|
4
|
+
import { type ActivityState, type AsyncStatus, type TokenUsage } from "./types.ts";
|
|
5
|
+
import { DEFAULT_CONTROL_CONFIG, deriveActivityState } from "./subagent-control.ts";
|
|
5
6
|
import { readStatus } from "./utils.ts";
|
|
6
7
|
|
|
7
8
|
export interface AsyncRunStepSummary {
|
|
8
9
|
index: number;
|
|
9
10
|
agent: string;
|
|
10
11
|
status: string;
|
|
12
|
+
activityState?: ActivityState;
|
|
11
13
|
durationMs?: number;
|
|
12
14
|
tokens?: TokenUsage;
|
|
13
15
|
skills?: string[];
|
|
@@ -19,7 +21,8 @@ export interface AsyncRunStepSummary {
|
|
|
19
21
|
export interface AsyncRunSummary {
|
|
20
22
|
id: string;
|
|
21
23
|
asyncDir: string;
|
|
22
|
-
state: "queued" | "running" | "complete" | "failed";
|
|
24
|
+
state: "queued" | "running" | "complete" | "failed" | "paused";
|
|
25
|
+
activityState?: ActivityState;
|
|
23
26
|
mode: "single" | "chain";
|
|
24
27
|
cwd?: string;
|
|
25
28
|
startedAt: number;
|
|
@@ -66,28 +69,57 @@ function isAsyncRunDir(root: string, entry: string): boolean {
|
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
function outputFileMtime(outputFile: string | undefined): number | undefined {
|
|
73
|
+
if (!outputFile) return undefined;
|
|
74
|
+
try {
|
|
75
|
+
return fs.statSync(outputFile).mtimeMs;
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): ActivityState | undefined {
|
|
82
|
+
if (status.state === "paused") return "paused";
|
|
83
|
+
if (status.state !== "running") return status.activityState;
|
|
84
|
+
const outputPath = status.outputFile ? (path.isAbsolute(status.outputFile) ? status.outputFile : path.join(asyncDir, status.outputFile)) : undefined;
|
|
85
|
+
const lastActivityAt = outputFileMtime(outputPath) ?? status.lastUpdate;
|
|
86
|
+
return deriveActivityState({
|
|
87
|
+
config: DEFAULT_CONTROL_CONFIG,
|
|
88
|
+
startedAt: status.startedAt,
|
|
89
|
+
lastActivityAt,
|
|
90
|
+
hasSeenActivity: Boolean(lastActivityAt),
|
|
91
|
+
paused: false,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
|
|
96
|
+
const activityState = deriveAsyncActivityState(asyncDir, status);
|
|
70
97
|
return {
|
|
71
98
|
id: status.runId || path.basename(asyncDir),
|
|
72
99
|
asyncDir,
|
|
73
100
|
state: status.state,
|
|
101
|
+
activityState,
|
|
74
102
|
mode: status.mode,
|
|
75
103
|
cwd: status.cwd,
|
|
76
104
|
startedAt: status.startedAt,
|
|
77
105
|
lastUpdate: status.lastUpdate,
|
|
78
106
|
endedAt: status.endedAt,
|
|
79
107
|
currentStep: status.currentStep,
|
|
80
|
-
steps: (status.steps ?? []).map((step, index) =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
steps: (status.steps ?? []).map((step, index) => {
|
|
109
|
+
const stepActivityState = step.activityState ?? (step.status === "running" ? activityState : undefined);
|
|
110
|
+
return {
|
|
111
|
+
index,
|
|
112
|
+
agent: step.agent,
|
|
113
|
+
status: step.status,
|
|
114
|
+
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
115
|
+
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
116
|
+
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
117
|
+
...(step.skills ? { skills: step.skills } : {}),
|
|
118
|
+
...(step.model ? { model: step.model } : {}),
|
|
119
|
+
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
120
|
+
...(step.error ? { error: step.error } : {}),
|
|
121
|
+
};
|
|
122
|
+
}),
|
|
91
123
|
...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
|
|
92
124
|
...(status.outputFile ? { outputFile: status.outputFile } : {}),
|
|
93
125
|
...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
|
|
@@ -100,8 +132,9 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
100
132
|
switch (state) {
|
|
101
133
|
case "running": return 0;
|
|
102
134
|
case "queued": return 1;
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
case "failed": return 2;
|
|
136
|
+
case "paused": return 2;
|
|
137
|
+
case "complete": return 3;
|
|
105
138
|
}
|
|
106
139
|
};
|
|
107
140
|
return [...runs].sort((a, b) => {
|
|
@@ -142,7 +175,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
142
175
|
export function listAsyncRunsForOverlay(asyncDirRoot: string, recentLimit = 5): AsyncRunOverlayData {
|
|
143
176
|
const all = listAsyncRuns(asyncDirRoot);
|
|
144
177
|
const recent = all
|
|
145
|
-
.filter((run) => run.state === "complete" || run.state === "failed")
|
|
178
|
+
.filter((run) => run.state === "complete" || run.state === "failed" || run.state === "paused")
|
|
146
179
|
.sort((a, b) => (b.lastUpdate ?? b.endedAt ?? b.startedAt) - (a.lastUpdate ?? a.endedAt ?? a.startedAt))
|
|
147
180
|
.slice(0, recentLimit);
|
|
148
181
|
return {
|
|
@@ -152,7 +185,8 @@ export function listAsyncRunsForOverlay(asyncDirRoot: string, recentLimit = 5):
|
|
|
152
185
|
}
|
|
153
186
|
|
|
154
187
|
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
155
|
-
const
|
|
188
|
+
const state = step.activityState ? `${step.status}/${step.activityState}` : step.status;
|
|
189
|
+
const parts = [`${step.index + 1}. ${step.agent}`, state];
|
|
156
190
|
if (step.model) parts.push(step.model);
|
|
157
191
|
if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
|
|
158
192
|
if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
|
|
@@ -163,7 +197,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
|
|
|
163
197
|
const stepCount = run.steps.length || 1;
|
|
164
198
|
const stepLabel = run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
|
|
165
199
|
const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
|
|
166
|
-
|
|
200
|
+
const state = run.activityState ? `${run.state}/${run.activityState}` : run.state;
|
|
201
|
+
return `${run.id} | ${state} | ${run.mode} | ${stepLabel} | ${cwd}`;
|
|
167
202
|
}
|
|
168
203
|
|
|
169
204
|
export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
|
package/chain-execution.ts
CHANGED
|
@@ -40,10 +40,12 @@ 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,
|
|
46
47
|
type Details,
|
|
48
|
+
type ResolvedControlConfig,
|
|
47
49
|
type SingleResult,
|
|
48
50
|
MAX_CONCURRENCY,
|
|
49
51
|
resolveChildMaxSubagentDepth,
|
|
@@ -82,6 +84,14 @@ interface ParallelChainRunInput {
|
|
|
82
84
|
artifactsDir: string;
|
|
83
85
|
signal?: AbortSignal;
|
|
84
86
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
87
|
+
controlConfig: ResolvedControlConfig;
|
|
88
|
+
foregroundControl?: {
|
|
89
|
+
updatedAt: number;
|
|
90
|
+
currentAgent?: string;
|
|
91
|
+
currentIndex?: number;
|
|
92
|
+
currentActivityState?: ActivityState;
|
|
93
|
+
interrupt?: () => boolean;
|
|
94
|
+
};
|
|
85
95
|
results: SingleResult[];
|
|
86
96
|
allProgress: AgentProgress[];
|
|
87
97
|
chainAgents: string[];
|
|
@@ -186,10 +196,25 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
186
196
|
const outputPath = typeof behavior.output === "string"
|
|
187
197
|
? (path.isAbsolute(behavior.output) ? behavior.output : path.join(input.chainDir, behavior.output))
|
|
188
198
|
: undefined;
|
|
199
|
+
const interruptController = new AbortController();
|
|
200
|
+
if (input.foregroundControl) {
|
|
201
|
+
input.foregroundControl.currentAgent = task.agent;
|
|
202
|
+
input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
|
|
203
|
+
input.foregroundControl.currentActivityState = "starting";
|
|
204
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
205
|
+
input.foregroundControl.interrupt = () => {
|
|
206
|
+
if (interruptController.signal.aborted) return false;
|
|
207
|
+
interruptController.abort();
|
|
208
|
+
input.foregroundControl!.currentActivityState = "paused";
|
|
209
|
+
input.foregroundControl!.updatedAt = Date.now();
|
|
210
|
+
return true;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
189
213
|
|
|
190
214
|
const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
|
|
191
215
|
cwd: taskCwd,
|
|
192
216
|
signal: input.signal,
|
|
217
|
+
interruptSignal: interruptController.signal,
|
|
193
218
|
runId: input.runId,
|
|
194
219
|
index: input.globalTaskIndex + taskIndex,
|
|
195
220
|
sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
|
|
@@ -199,28 +224,41 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
199
224
|
artifactConfig: input.artifactConfig,
|
|
200
225
|
outputPath,
|
|
201
226
|
maxSubagentDepth,
|
|
227
|
+
controlConfig: input.controlConfig,
|
|
202
228
|
modelOverride: effectiveModel,
|
|
203
229
|
availableModels: input.availableModels,
|
|
204
230
|
preferredModelProvider: input.ctx.model?.provider,
|
|
205
231
|
skills: behavior.skills === false ? [] : behavior.skills,
|
|
206
232
|
onUpdate: input.onUpdate
|
|
207
233
|
? (progressUpdate) => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
chainAgents: input.chainAgents,
|
|
217
|
-
totalSteps: input.totalSteps,
|
|
218
|
-
currentStepIndex: input.stepIndex,
|
|
219
|
-
},
|
|
220
|
-
});
|
|
234
|
+
const stepResults = progressUpdate.details?.results || [];
|
|
235
|
+
const stepProgress = progressUpdate.details?.progress || [];
|
|
236
|
+
if (input.foregroundControl && stepProgress.length > 0) {
|
|
237
|
+
const current = stepProgress[0];
|
|
238
|
+
input.foregroundControl.currentAgent = task.agent;
|
|
239
|
+
input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
|
|
240
|
+
input.foregroundControl.currentActivityState = current?.activityState;
|
|
241
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
221
242
|
}
|
|
243
|
+
input.onUpdate?.({
|
|
244
|
+
...progressUpdate,
|
|
245
|
+
details: {
|
|
246
|
+
mode: "chain",
|
|
247
|
+
results: input.results.concat(stepResults),
|
|
248
|
+
progress: input.allProgress.concat(stepProgress),
|
|
249
|
+
controlEvents: progressUpdate.details?.controlEvents,
|
|
250
|
+
chainAgents: input.chainAgents,
|
|
251
|
+
totalSteps: input.totalSteps,
|
|
252
|
+
currentStepIndex: input.stepIndex,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
222
256
|
: undefined,
|
|
223
257
|
});
|
|
258
|
+
if (input.foregroundControl?.currentIndex === input.globalTaskIndex + taskIndex) {
|
|
259
|
+
input.foregroundControl.interrupt = undefined;
|
|
260
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
261
|
+
}
|
|
224
262
|
|
|
225
263
|
if (result.exitCode !== 0 && failFast) {
|
|
226
264
|
aborted = true;
|
|
@@ -249,6 +287,14 @@ export interface ChainExecutionParams {
|
|
|
249
287
|
includeProgress?: boolean;
|
|
250
288
|
clarify?: boolean;
|
|
251
289
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
290
|
+
controlConfig: ResolvedControlConfig;
|
|
291
|
+
foregroundControl?: {
|
|
292
|
+
updatedAt: number;
|
|
293
|
+
currentAgent?: string;
|
|
294
|
+
currentIndex?: number;
|
|
295
|
+
currentActivityState?: ActivityState;
|
|
296
|
+
interrupt?: () => boolean;
|
|
297
|
+
};
|
|
252
298
|
chainSkills?: string[];
|
|
253
299
|
chainDir?: string;
|
|
254
300
|
maxSubagentDepth: number;
|
|
@@ -286,6 +332,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
286
332
|
includeProgress,
|
|
287
333
|
clarify,
|
|
288
334
|
onUpdate,
|
|
335
|
+
controlConfig,
|
|
336
|
+
foregroundControl,
|
|
289
337
|
chainSkills: chainSkillsParam,
|
|
290
338
|
chainDir: chainDirBase,
|
|
291
339
|
} = params;
|
|
@@ -484,6 +532,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
484
532
|
allProgress,
|
|
485
533
|
chainAgents,
|
|
486
534
|
totalSteps,
|
|
535
|
+
controlConfig,
|
|
536
|
+
foregroundControl,
|
|
487
537
|
worktreeSetup,
|
|
488
538
|
maxSubagentDepth: params.maxSubagentDepth,
|
|
489
539
|
});
|
|
@@ -495,6 +545,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
495
545
|
if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
|
|
496
546
|
}
|
|
497
547
|
|
|
548
|
+
const interrupted = parallelResults.find((result) => result.interrupted);
|
|
549
|
+
if (interrupted) {
|
|
550
|
+
return {
|
|
551
|
+
content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
|
|
552
|
+
details: buildChainExecutionDetails({
|
|
553
|
+
results,
|
|
554
|
+
includeProgress,
|
|
555
|
+
allProgress,
|
|
556
|
+
allArtifactPaths,
|
|
557
|
+
artifactsDir,
|
|
558
|
+
chainAgents,
|
|
559
|
+
totalSteps,
|
|
560
|
+
currentStepIndex: stepIndex,
|
|
561
|
+
}),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
498
565
|
const failures = parallelResults
|
|
499
566
|
.map((result, originalIndex) => ({ ...result, originalIndex }))
|
|
500
567
|
.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
|
|
@@ -603,10 +670,25 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
603
670
|
? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
|
|
604
671
|
: undefined;
|
|
605
672
|
const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
|
|
673
|
+
const interruptController = new AbortController();
|
|
674
|
+
if (foregroundControl) {
|
|
675
|
+
foregroundControl.currentAgent = seqStep.agent;
|
|
676
|
+
foregroundControl.currentIndex = globalTaskIndex;
|
|
677
|
+
foregroundControl.currentActivityState = "starting";
|
|
678
|
+
foregroundControl.updatedAt = Date.now();
|
|
679
|
+
foregroundControl.interrupt = () => {
|
|
680
|
+
if (interruptController.signal.aborted) return false;
|
|
681
|
+
interruptController.abort();
|
|
682
|
+
foregroundControl.currentActivityState = "paused";
|
|
683
|
+
foregroundControl.updatedAt = Date.now();
|
|
684
|
+
return true;
|
|
685
|
+
};
|
|
686
|
+
}
|
|
606
687
|
|
|
607
688
|
const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
608
689
|
cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
|
|
609
690
|
signal,
|
|
691
|
+
interruptSignal: interruptController.signal,
|
|
610
692
|
runId,
|
|
611
693
|
index: globalTaskIndex,
|
|
612
694
|
sessionDir: sessionDirForIndex(globalTaskIndex),
|
|
@@ -616,28 +698,41 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
616
698
|
artifactConfig,
|
|
617
699
|
outputPath,
|
|
618
700
|
maxSubagentDepth,
|
|
701
|
+
controlConfig,
|
|
619
702
|
modelOverride: effectiveModel,
|
|
620
703
|
availableModels,
|
|
621
704
|
preferredModelProvider: ctx.model?.provider,
|
|
622
705
|
skills: behavior.skills === false ? [] : behavior.skills,
|
|
623
706
|
onUpdate: onUpdate
|
|
624
707
|
? (p) => {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
chainAgents,
|
|
634
|
-
totalSteps,
|
|
635
|
-
currentStepIndex: stepIndex,
|
|
636
|
-
},
|
|
637
|
-
});
|
|
708
|
+
const stepResults = p.details?.results || [];
|
|
709
|
+
const stepProgress = p.details?.progress || [];
|
|
710
|
+
if (foregroundControl && stepProgress.length > 0) {
|
|
711
|
+
const current = stepProgress[0];
|
|
712
|
+
foregroundControl.currentAgent = seqStep.agent;
|
|
713
|
+
foregroundControl.currentIndex = globalTaskIndex;
|
|
714
|
+
foregroundControl.currentActivityState = current?.activityState;
|
|
715
|
+
foregroundControl.updatedAt = Date.now();
|
|
638
716
|
}
|
|
717
|
+
onUpdate({
|
|
718
|
+
...p,
|
|
719
|
+
details: {
|
|
720
|
+
mode: "chain",
|
|
721
|
+
results: results.concat(stepResults),
|
|
722
|
+
progress: allProgress.concat(stepProgress),
|
|
723
|
+
controlEvents: p.details?.controlEvents,
|
|
724
|
+
chainAgents,
|
|
725
|
+
totalSteps,
|
|
726
|
+
currentStepIndex: stepIndex,
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
}
|
|
639
730
|
: undefined,
|
|
640
731
|
});
|
|
732
|
+
if (foregroundControl?.currentIndex === globalTaskIndex) {
|
|
733
|
+
foregroundControl.interrupt = undefined;
|
|
734
|
+
foregroundControl.updatedAt = Date.now();
|
|
735
|
+
}
|
|
641
736
|
recordRun(seqStep.agent, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
|
|
642
737
|
|
|
643
738
|
globalTaskIndex++;
|
|
@@ -663,6 +758,22 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
663
758
|
}
|
|
664
759
|
}
|
|
665
760
|
|
|
761
|
+
if (r.interrupted) {
|
|
762
|
+
return {
|
|
763
|
+
content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
|
|
764
|
+
details: buildChainExecutionDetails({
|
|
765
|
+
results,
|
|
766
|
+
includeProgress,
|
|
767
|
+
allProgress,
|
|
768
|
+
allArtifactPaths,
|
|
769
|
+
artifactsDir,
|
|
770
|
+
chainAgents,
|
|
771
|
+
totalSteps,
|
|
772
|
+
currentStepIndex: stepIndex,
|
|
773
|
+
}),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
666
777
|
if (r.exitCode !== 0) {
|
|
667
778
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
668
779
|
index: stepIndex,
|