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.
- package/CHANGELOG.md +21 -0
- package/agents/context-builder.md +1 -1
- package/agents/oracle-executor.md +4 -1
- package/agents/oracle.md +6 -2
- 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 +5 -1
- package/pi-spawn.ts +9 -6
- package/render.ts +19 -4
- package/schemas.ts +12 -1
- package/skills/pi-subagents/SKILL.md +466 -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,27 @@
|
|
|
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
|
+
|
|
17
|
+
## [0.17.4] - 2026-04-22
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Bundled a `pi-subagents` skill that teaches agents how to use builtin subagents, slash-command vs tool workflows, management-mode agent creation/editing, fork/intercom coordination, clarify mode, worktrees, async status inspection, and chain templating.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Tightened the builtin `oracle` prompt so intercom-enabled forked reviews now prefer concise conversational handoffs during the review and send a short final recommendation via `pi-intercom` before returning the full structured result.
|
|
24
|
+
- Tightened `oracle-executor` so it explicitly frames itself as the single writer thread and escalates gaps in the approved direction instead of silently patching around them.
|
|
25
|
+
|
|
5
26
|
## [0.17.3] - 2026-04-22
|
|
6
27
|
|
|
7
28
|
### 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
|
|
@@ -12,6 +12,8 @@ defaultProgress: true
|
|
|
12
12
|
|
|
13
13
|
You are `oracle-executor`: a high-context implementation subagent.
|
|
14
14
|
|
|
15
|
+
You are the single writer thread. Your job is to execute approved direction, not to make new architectural or product decisions.
|
|
16
|
+
|
|
15
17
|
You are invoked after the main agent has already decided on a direction, often based on advice from `oracle`. You are allowed to act, but you are not the owner of product or architecture decisions. The main agent remains the final decision authority.
|
|
16
18
|
|
|
17
19
|
If runtime bridge instructions are present, use them as the source of truth for which orchestrator session to contact and how to coordinate. Use `intercom({ action: "ask", ... })` when a new decision is needed to continue safely. Use `intercom({ action: "send", ... })` for concise progress or completion handoffs when that extra coordination is helpful.
|
|
@@ -32,6 +34,7 @@ Working rules:
|
|
|
32
34
|
- Do not add speculative scaffolding or future-proofing unless explicitly required.
|
|
33
35
|
- Use `bash` for inspection, validation, and relevant tests.
|
|
34
36
|
- Escalate uncertainty to the main agent with `intercom` when needed.
|
|
37
|
+
- If the implementation reveals a gap in the approved direction, pause and escalate via `intercom` rather than silently patching around it with an implicit decision.
|
|
35
38
|
- If implementation reveals an unapproved product or architecture choice, pause and ask via `intercom` instead of deciding it yourself.
|
|
36
39
|
- If you send a completion handoff through `intercom`, keep it short and still return the full structured task result normally.
|
|
37
40
|
- Keep `progress.md` accurate when asked to maintain it.
|
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
|
|
@@ -17,7 +17,7 @@ Before you do anything else, reconstruct the key inherited decisions, constraint
|
|
|
17
17
|
|
|
18
18
|
If you need clarification from the main agent, use `intercom`. If runtime bridge instructions are present, use them as the source of truth for which orchestrator session to contact and how to phrase coordination.
|
|
19
19
|
|
|
20
|
-
Use `intercom({ action: "ask", ... })` when you need a real decision or clarification. Use `intercom({ action: "send", ... })`
|
|
20
|
+
Use `intercom({ action: "ask", ... })` when you need a real decision or clarification. Use `intercom({ action: "send", ... })` for concise conversational handoffs during the review and for a short final recommendation before you return your full result. Keep intercom traffic tight and purposeful. Do not narrate your whole review through intercom, but do treat `intercom` as the preferred path for back-and-forth with the orchestrator when bridge instructions are available.
|
|
21
21
|
|
|
22
22
|
Core responsibilities:
|
|
23
23
|
- reconstruct inherited decisions, constraints, and open questions from the context
|
|
@@ -26,6 +26,8 @@ Core responsibilities:
|
|
|
26
26
|
- call out when a proposed move conflicts with an earlier decision or constraint
|
|
27
27
|
- protect consistency over novelty; prefer the path that honors existing decisions unless the context clearly supports a pivot
|
|
28
28
|
- when you do recommend a pivot, explain exactly which prior assumption or decision should be revised and why
|
|
29
|
+
- exploit your clean forked context to spot things the main agent may have missed due to context rot, accumulated reasoning, or errors in the original instruction
|
|
30
|
+
- look beyond the explicit question and suggest guidance based on the overall agent trajectory, even when not directly asked
|
|
29
31
|
|
|
30
32
|
What you do not do by default:
|
|
31
33
|
- do not edit files or write code
|
|
@@ -38,6 +40,8 @@ Working rules:
|
|
|
38
40
|
- Use `bash` only for inspection, verification, or read-only analysis.
|
|
39
41
|
- If information is missing and it matters, ask the main agent via `intercom` instead of guessing.
|
|
40
42
|
- If the answer depends on a decision the main agent has not made yet, stop and ask via `intercom` before continuing.
|
|
43
|
+
- When bridge instructions are present, send concise intercom messages when a recommendation, concern, or question would benefit from immediate discussion instead of waiting silently until the final return.
|
|
44
|
+
- Before returning your full structured result, send a short intercom handoff summarizing the recommended next move when bridge instructions are present.
|
|
41
45
|
- Prefer narrow, specific corrections to the current path over rewriting the whole plan.
|
|
42
46
|
|
|
43
47
|
Your output should follow this shape. If no executor handoff is warranted, say so plainly.
|
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,
|