@ferris1225/pi-subagents 4.1.1 → 4.1.3
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/README.md +461 -381
- package/agents/cleaner.md +16 -6
- package/agents/documenter.md +46 -0
- package/agents/explorer.md +15 -11
- package/agents/reviewer.md +8 -4
- package/agents/worker.md +9 -4
- package/package.json +55 -55
- package/src/agents.ts +53 -0
- package/src/announcements.ts +18 -1
- package/src/completion.ts +160 -160
- package/src/config.ts +43 -13
- package/src/dispatch.ts +233 -303
- package/src/fixloop.ts +259 -62
- package/src/index.ts +3 -3
- package/src/models.ts +189 -189
- package/src/monitor.ts +101 -22
- package/src/prompt.ts +47 -12
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +29 -10
- package/src/runtime.ts +13 -7
- package/src/session-fork.ts +80 -80
- package/src/setup.ts +162 -130
- package/src/spawn.ts +53 -13
- package/src/thread-lifecycle.ts +240 -54
- package/src/tools.ts +65 -37
- package/src/widget.ts +68 -22
- package/src/worktree.ts +27 -4
package/src/dispatch.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `subagent` tool: dispatches explorer/worker/cleaner/reviewer agents as isolated pi
|
|
2
|
+
* The `subagent` tool: dispatches explorer/worker/cleaner/documenter/reviewer agents as isolated pi
|
|
3
3
|
* child processes, single or parallel. Owns the public dispatch contract,
|
|
4
|
-
* per-run status tracking,
|
|
5
|
-
*
|
|
4
|
+
* per-run status tracking, managed writer → documenter → reviewer workflows,
|
|
5
|
+
* reviewer auto-fix rounds, and internal step launching. Stable thread
|
|
6
|
+
* generations, final integration, and completion ownership live in
|
|
7
|
+
* thread-lifecycle.ts.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
@@ -11,34 +13,30 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
11
13
|
import { realpath } from "node:fs/promises";
|
|
12
14
|
import { resolve } from "node:path";
|
|
13
15
|
import { Type } from "typebox";
|
|
14
|
-
import { discoverAgents } from "./agents.ts";
|
|
16
|
+
import { discoverAgents, resolveAgentTools, type AgentConfig } from "./agents.ts";
|
|
15
17
|
import { loadConfig } from "./config.ts";
|
|
18
|
+
import { formatUsage, queuedResult } from "./format.ts";
|
|
16
19
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
formatUsage,
|
|
20
|
-
modelLevelTakeoverNote,
|
|
21
|
-
queuedResult,
|
|
22
|
-
} from "./format.ts";
|
|
23
|
-
import {
|
|
20
|
+
buildDocumenterTaskBrief,
|
|
21
|
+
buildFinalReviewBrief,
|
|
24
22
|
buildFixTaskBrief,
|
|
23
|
+
buildPostWriterDocumenterBrief,
|
|
25
24
|
buildReReviewBrief,
|
|
26
|
-
|
|
25
|
+
buildReviewPassDocumenterBrief,
|
|
27
26
|
type ChainStep,
|
|
27
|
+
type ManagedWorkflowOutcome,
|
|
28
28
|
} from "./fixloop.ts";
|
|
29
29
|
import {
|
|
30
30
|
formatTaskSummary,
|
|
31
31
|
formatToolActivity,
|
|
32
32
|
monitor,
|
|
33
33
|
statusIcon,
|
|
34
|
-
sumUsage,
|
|
35
34
|
type RunChainMeta,
|
|
36
35
|
} from "./monitor.ts";
|
|
37
36
|
import type { SubagentRuntime } from "./runtime.ts";
|
|
38
37
|
import {
|
|
39
38
|
getResultOutput,
|
|
40
39
|
isFailedResult,
|
|
41
|
-
isModelLevelFailure,
|
|
42
40
|
reviewVerdict,
|
|
43
41
|
runSingleAgentWithMainFallback,
|
|
44
42
|
type SingleResult,
|
|
@@ -48,15 +46,17 @@ import {
|
|
|
48
46
|
import {
|
|
49
47
|
createBackgroundDispatcher,
|
|
50
48
|
resolveDispatchModelRoute,
|
|
49
|
+
withWorktreeSystemPrompt,
|
|
50
|
+
type ManagedWorkflowRequest,
|
|
51
51
|
} from "./thread-lifecycle.ts";
|
|
52
|
-
import {
|
|
52
|
+
import { resolveRepositoryRoot, type IsolationMode } from "./worktree.ts";
|
|
53
53
|
|
|
54
54
|
export { FORK_CONTINUATION_PROMPT, isWorktreeCapableAgent } from "./thread-lifecycle.ts";
|
|
55
55
|
|
|
56
56
|
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
57
57
|
|
|
58
58
|
const ISOLATION_DESCRIPTION =
|
|
59
|
-
"Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents, including worker and
|
|
59
|
+
"Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents, including worker, cleaner, and documenter, only)";
|
|
60
60
|
|
|
61
61
|
const IsolationSchema = Type.Optional(
|
|
62
62
|
StringEnum(["shared", "worktree"] as const, { description: ISOLATION_DESCRIPTION }),
|
|
@@ -87,11 +87,13 @@ export function defaultIsolationMode(mode: "single" | "parallel", agentName: str
|
|
|
87
87
|
return mode === "parallel" && agentName === "worker" ? "worktree" : "shared";
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
const
|
|
90
|
+
const managedRepositoryRootTails = new Map<string, Promise<void>>();
|
|
91
91
|
|
|
92
|
-
async function
|
|
92
|
+
async function canonicalManagedRepositoryRoot(cwd: string): Promise<string> {
|
|
93
93
|
try {
|
|
94
|
-
|
|
94
|
+
// Repository identity does not depend on HEAD: empty repositories must
|
|
95
|
+
// serialize root and nested cwd requests under the same lane too.
|
|
96
|
+
return await resolveRepositoryRoot(cwd);
|
|
95
97
|
} catch {
|
|
96
98
|
try {
|
|
97
99
|
return await realpath(resolve(cwd));
|
|
@@ -101,32 +103,65 @@ async function canonicalAutoFixRoot(cwd: string): Promise<string> {
|
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
|
|
106
|
+
/** Run one operation under the canonical original-repository lane.
|
|
107
|
+
*
|
|
108
|
+
* Shared managed generations use the abortable overload for their complete
|
|
109
|
+
* writer/reviewer workflow. Isolated generations use the non-abortable overload
|
|
110
|
+
* only for their final worktree apply, so model work remains parallel while the
|
|
111
|
+
* original checkout mutation cannot race a shared writer or reviewer snapshot.
|
|
112
|
+
*/
|
|
113
|
+
async function runInManagedRepositoryLane<T>(
|
|
108
114
|
cwd: string,
|
|
109
|
-
task: (
|
|
110
|
-
):
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
115
|
+
task: () => Promise<T>,
|
|
116
|
+
): Promise<T>;
|
|
117
|
+
async function runInManagedRepositoryLane<T>(
|
|
118
|
+
cwd: string,
|
|
119
|
+
task: () => Promise<T>,
|
|
120
|
+
signal: AbortSignal,
|
|
121
|
+
): Promise<T | undefined>;
|
|
122
|
+
async function runInManagedRepositoryLane<T>(
|
|
123
|
+
cwd: string,
|
|
124
|
+
task: () => Promise<T>,
|
|
125
|
+
signal?: AbortSignal,
|
|
126
|
+
): Promise<T | undefined> {
|
|
127
|
+
if (signal?.aborted) return undefined;
|
|
128
|
+
const root = await canonicalManagedRepositoryRoot(cwd);
|
|
129
|
+
const key = process.platform === "win32" ? root.toLowerCase() : root;
|
|
130
|
+
const previous = managedRepositoryRootTails.get(key) ?? Promise.resolve();
|
|
131
|
+
let release!: () => void;
|
|
132
|
+
const gate = new Promise<void>((resolveGate) => {
|
|
133
|
+
release = resolveGate;
|
|
134
|
+
});
|
|
135
|
+
const tail = previous.catch(() => undefined).then(() => gate);
|
|
136
|
+
managedRepositoryRootTails.set(key, tail);
|
|
137
|
+
let onAbort: (() => void) | undefined;
|
|
138
|
+
try {
|
|
139
|
+
if (signal) {
|
|
140
|
+
await Promise.race([
|
|
141
|
+
previous.catch(() => undefined),
|
|
142
|
+
new Promise<void>((resolveAborted) => {
|
|
143
|
+
if (signal.aborted) resolveAborted();
|
|
144
|
+
else {
|
|
145
|
+
onAbort = resolveAborted;
|
|
146
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
} else {
|
|
151
|
+
await previous.catch(() => undefined);
|
|
128
152
|
}
|
|
129
|
-
|
|
153
|
+
if (signal?.aborted) return undefined;
|
|
154
|
+
return await task();
|
|
155
|
+
} finally {
|
|
156
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
157
|
+
release();
|
|
158
|
+
// An aborted waiter may finish before the prior owner. Keep its chained
|
|
159
|
+
// tail installed until that owner also settles, otherwise a newcomer could
|
|
160
|
+
// observe an empty map and race the still-running workflow.
|
|
161
|
+
void tail.then(() => {
|
|
162
|
+
if (managedRepositoryRootTails.get(key) === tail) managedRepositoryRootTails.delete(key);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
130
165
|
}
|
|
131
166
|
|
|
132
167
|
export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
@@ -135,14 +170,14 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
135
170
|
label: "Subagent",
|
|
136
171
|
description: [
|
|
137
172
|
"Dispatch enabled specialized agents as isolated leaf Pi child processes, singly or in parallel.",
|
|
138
|
-
"Built-ins: explorer for broad read-only reconnaissance; worker for implementation; cleaner
|
|
139
|
-
"Work starts in the background;
|
|
173
|
+
"Built-ins: explorer for broad read-only reconnaissance; worker for implementation; cleaner for explicitly authorized cleanup, removal, simplification, and duplicate-code consolidation; documenter for pre-commit diff sync or explicitly requested whole-codebase comment/README/docs maintenance; reviewer for generic read-only assessments and final gates.",
|
|
174
|
+
"Work starts in the background; successful top-level writers automatically continue through enabled documenter/reviewer stages and return one final completion. Results resume the main agent and are already shown to the user, so do not poll, duplicate downstream roles, or restate them. Give each child a self-contained brief because it has no conversation memory.",
|
|
140
175
|
"Single tasks default to shared; parallel workers default to detached Git worktrees. Only write-capable agents can use worktree isolation, and failures never fall back silently to shared.",
|
|
141
176
|
"A selected-model or provider failure continues the retained session on the current main model; ordinary tool/task failures do not.",
|
|
142
|
-
"Use subagent_control to steer
|
|
177
|
+
"Use subagent_control to steer/retarget an active top-level child, park/stop a managed downstream stage, or resume/fork retained context by stable run id.",
|
|
143
178
|
].join(" "),
|
|
144
179
|
promptSnippet:
|
|
145
|
-
"Dispatch isolated background agents: explorer (recon), worker (implementation), cleaner (authorized cleanup), reviewer (read-only assessment/gate); results resume automatically. Use direct tools for trivial work.",
|
|
180
|
+
"Dispatch isolated background agents: explorer (recon), worker (implementation), cleaner (authorized cleanup/deduplication), documenter (docs sync), reviewer (read-only assessment/gate); enabled post-writer stages run automatically, results resume automatically, and the workflow delivers once. Use direct tools for trivial work.",
|
|
146
181
|
parameters: SubagentParams,
|
|
147
182
|
|
|
148
183
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -168,21 +203,14 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
168
203
|
|
|
169
204
|
// Live sub-agent activity → concise one-line status ("thinking",
|
|
170
205
|
// "read src/index.ts", ...), never a raw args blob. The live handler
|
|
171
|
-
// only updates monitor state;
|
|
172
|
-
//
|
|
173
|
-
// which fires a transient "failed" status before relaunching — from
|
|
174
|
-
// ripping the row out early, and lets the queue task decide between
|
|
175
|
-
// delivering a reviewer's result and starting an auto-fix chain.
|
|
206
|
+
// only updates monitor state; the queue task / launchInWorkflow owns
|
|
207
|
+
// terminal removal, notification, and downstream workflow decisions.
|
|
176
208
|
const makeLiveHandler =
|
|
177
209
|
(runId: number, generation?: number) =>
|
|
178
210
|
(e: SubagentLiveEvent): void => {
|
|
179
211
|
if (generation !== undefined && runtime.threads.get(runId)?.generation !== generation) return;
|
|
180
212
|
switch (e.kind) {
|
|
181
213
|
case "status":
|
|
182
|
-
// Only update monitor status here. Finishing (removeRun + notify) is
|
|
183
|
-
// owned by the queue task / launchInLoop so that a startup retry — which
|
|
184
|
-
// fires a transient "failed" status before relaunching the child — never
|
|
185
|
-
// rips the row out from under the retry or emits a premature "✗" toast.
|
|
186
214
|
monitor.setStatus(runId, e.status);
|
|
187
215
|
break;
|
|
188
216
|
case "model":
|
|
@@ -261,49 +289,56 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
261
289
|
};
|
|
262
290
|
}
|
|
263
291
|
|
|
264
|
-
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const launchInLoop = async (
|
|
292
|
+
/** Launch one workflow-internal child in a fresh model context. It sees the
|
|
293
|
+
* parent's exact repository/worktree state and is registered by its own id,
|
|
294
|
+
* but never enters top-level lifecycle policy or completion delivery. */
|
|
295
|
+
const launchInWorkflow = async (
|
|
296
|
+
request: ManagedWorkflowRequest,
|
|
270
297
|
agentName: string,
|
|
271
298
|
task: string,
|
|
272
|
-
executionCwd: string,
|
|
273
|
-
signal: AbortSignal,
|
|
274
299
|
meta: RunChainMeta,
|
|
275
|
-
): Promise<{ runId
|
|
276
|
-
const
|
|
277
|
-
if (!
|
|
278
|
-
|
|
300
|
+
): Promise<{ runId: number; result: SingleResult }> => {
|
|
301
|
+
const discoveredAgent = request.agents.find((candidate) => candidate.name === agentName);
|
|
302
|
+
if (!discoveredAgent) {
|
|
303
|
+
throw new Error(`Managed workflow requires enabled agent "${agentName}", but discovery did not provide it.`);
|
|
304
|
+
}
|
|
305
|
+
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
306
|
+
resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
|
|
307
|
+
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
308
|
+
const resolvedRoute = resolveDispatchModelRoute(agent, request.config, request.ctx);
|
|
309
|
+
const route = request.isolation === "worktree"
|
|
310
|
+
? { ...resolvedRoute, agent: withWorktreeSystemPrompt(resolvedRoute.agent) }
|
|
311
|
+
: resolvedRoute;
|
|
279
312
|
const thinkingLevel = route.thinkingLevel;
|
|
280
|
-
const runId = monitor.addRun(agent.name, task, route.agent.model, thinkingLevel,
|
|
313
|
+
const runId = monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
314
|
+
...meta,
|
|
315
|
+
isolation: request.isolation,
|
|
316
|
+
});
|
|
281
317
|
const onLive = makeLiveHandler(runId);
|
|
282
318
|
try {
|
|
283
319
|
const result = await runSingleAgentWithMainFallback(
|
|
284
320
|
{
|
|
285
|
-
defaultCwd: executionCwd,
|
|
286
|
-
cwd: executionCwd,
|
|
321
|
+
defaultCwd: request.executionCwd,
|
|
322
|
+
cwd: request.executionCwd,
|
|
287
323
|
agent: route.agent,
|
|
324
|
+
resolveAgentForAttempt: resolveLiveAgentTools,
|
|
288
325
|
agentName,
|
|
289
326
|
task,
|
|
290
327
|
thinkingLevel,
|
|
291
328
|
thinkingLevelForModel: route.thinkingLevelForModel,
|
|
292
|
-
signal,
|
|
329
|
+
signal: request.signal,
|
|
293
330
|
onLive,
|
|
294
331
|
makeDetails: makeDetails("single", true),
|
|
295
|
-
idleTimeoutMs: config.idleTimeoutSec * 1000,
|
|
332
|
+
idleTimeoutMs: request.config.idleTimeoutSec * 1000,
|
|
296
333
|
},
|
|
297
334
|
route.mainFallbackRef,
|
|
298
335
|
);
|
|
299
336
|
result.runId = runId;
|
|
300
|
-
result.projectCwd =
|
|
301
|
-
result.isolation =
|
|
337
|
+
result.projectCwd = request.projectCwd;
|
|
338
|
+
result.isolation = request.isolation;
|
|
302
339
|
runtime.retainSession(result);
|
|
303
340
|
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
304
341
|
monitor.setThinking(runId, result.thinking);
|
|
305
|
-
// The parent row represents the chain. Internal rounds leave live
|
|
306
|
-
// status as soon as they settle; their reports remain addressable by id.
|
|
307
342
|
finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
|
|
308
343
|
runtime.registerRunResult(runId, result);
|
|
309
344
|
return { runId, result };
|
|
@@ -313,11 +348,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
313
348
|
const crashed: SingleResult = {
|
|
314
349
|
...queuedResult(route.agent, task, thinkingLevel),
|
|
315
350
|
runId,
|
|
316
|
-
projectCwd:
|
|
317
|
-
isolation:
|
|
351
|
+
projectCwd: request.projectCwd,
|
|
352
|
+
isolation: request.isolation,
|
|
318
353
|
exitCode: 1,
|
|
319
354
|
stderr: errorMessage,
|
|
320
|
-
stopReason: signal.aborted ? "aborted" : "error",
|
|
355
|
+
stopReason: request.signal.aborted ? "aborted" : "error",
|
|
321
356
|
errorMessage,
|
|
322
357
|
dispatchFailed: true,
|
|
323
358
|
};
|
|
@@ -326,241 +361,135 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
326
361
|
}
|
|
327
362
|
};
|
|
328
363
|
|
|
329
|
-
/**
|
|
330
|
-
*
|
|
331
|
-
|
|
332
|
-
* not woken mid-loop; the full chain is delivered as one group at the end.
|
|
333
|
-
* Failures short-circuit: a crashed worker skips its re-review and delivers.
|
|
334
|
-
* The triggering reviewer stays in monitor state until the chain resolves.
|
|
335
|
-
*/
|
|
336
|
-
/** Drop any in-flight monitor row belonging to an auto-fix chain; the
|
|
337
|
-
* parent is removed separately (it does not carry the groupId). */
|
|
338
|
-
const removeChainGroup = (groupId: string): void => {
|
|
364
|
+
/** Drop any in-flight internal row. Normal internal settlement already
|
|
365
|
+
* removes rows; this is a cancellation/crash guard. */
|
|
366
|
+
const removeWorkflowGroup = (groupId: string): void => {
|
|
339
367
|
for (const run of [...monitor.getRuns()]) {
|
|
340
368
|
if (run.groupId === groupId) monitor.removeRun(run.id);
|
|
341
369
|
}
|
|
342
370
|
};
|
|
343
371
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
):
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
let fixController: AbortController | undefined;
|
|
355
|
-
const ownsParent = (): boolean => {
|
|
356
|
-
const current = runtime.threads.get(parentRunId);
|
|
357
|
-
return fixController !== undefined &&
|
|
358
|
-
current === parentThreadAtStart &&
|
|
359
|
-
current.generation === parentGeneration &&
|
|
360
|
-
current.control === parentControl &&
|
|
361
|
-
current.queueController === fixController &&
|
|
362
|
-
runtime.runControllers.get(parentRunId) === fixController;
|
|
372
|
+
/** Run every downstream role inline under the parent generation's queue
|
|
373
|
+
* controller. That gives park/stop/shutdown one lifecycle owner and keeps
|
|
374
|
+
* isolated worktrees unintegrated until the final reviewer settles. */
|
|
375
|
+
const runManagedWorkflow = async (
|
|
376
|
+
request: ManagedWorkflowRequest,
|
|
377
|
+
): Promise<ManagedWorkflowOutcome> => {
|
|
378
|
+
const initialStepRunId = monitor.reserveRunId();
|
|
379
|
+
const initialStepResult: SingleResult = {
|
|
380
|
+
...request.initialResult,
|
|
381
|
+
runId: initialStepRunId,
|
|
363
382
|
};
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
383
|
+
runtime.registerRunResult(initialStepRunId, initialStepResult);
|
|
384
|
+
const steps: ChainStep[] = [{
|
|
385
|
+
runId: initialStepRunId,
|
|
386
|
+
result: initialStepResult,
|
|
387
|
+
relation: request.plan.initialRelation,
|
|
388
|
+
}];
|
|
389
|
+
const enabled = (name: string): boolean =>
|
|
390
|
+
request.agents.some((candidate) => candidate.name === name);
|
|
391
|
+
const canContinue = (): boolean => runtime.sessionActive && !request.signal.aborted;
|
|
392
|
+
const launchStep = async (
|
|
393
|
+
agentName: string,
|
|
394
|
+
task: string,
|
|
395
|
+
relation: string,
|
|
396
|
+
): Promise<SingleResult> => {
|
|
397
|
+
if (!enabled(agentName)) {
|
|
398
|
+
throw new Error(`Managed workflow cannot launch disabled or missing agent "${agentName}".`);
|
|
368
399
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
400
|
+
const step = await launchInWorkflow(request, agentName, task, {
|
|
401
|
+
groupId: request.groupId,
|
|
402
|
+
relationLabel: relation,
|
|
403
|
+
parentRunId: request.parentRunId,
|
|
404
|
+
});
|
|
405
|
+
request.rememberLatest(step.result);
|
|
406
|
+
steps.push({ ...step, relation });
|
|
407
|
+
return step.result;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const runFixRounds = async (triggeringReviewer: SingleResult): Promise<void> => {
|
|
411
|
+
let lastReviewer = triggeringReviewer;
|
|
412
|
+
for (let round = 1; round <= request.config.maxFixRounds; round++) {
|
|
413
|
+
if (!canContinue()) break;
|
|
414
|
+
const workerResult = await launchStep(
|
|
415
|
+
"worker",
|
|
416
|
+
buildFixTaskBrief(lastReviewer, round, request.config.maxFixRounds),
|
|
417
|
+
`fix round ${round}`,
|
|
418
|
+
);
|
|
419
|
+
if (!canContinue() || isFailedResult(workerResult)) break;
|
|
420
|
+
|
|
421
|
+
let documenterResult: SingleResult | undefined;
|
|
422
|
+
if (enabled("documenter")) {
|
|
423
|
+
documenterResult = await launchStep(
|
|
424
|
+
"documenter",
|
|
425
|
+
buildDocumenterTaskBrief(workerResult, round, lastReviewer),
|
|
426
|
+
`docs round ${round}`,
|
|
427
|
+
);
|
|
428
|
+
if (!canContinue() || isFailedResult(documenterResult)) break;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const reviewResult = await launchStep(
|
|
432
|
+
"reviewer",
|
|
433
|
+
buildReReviewBrief(lastReviewer, round, workerResult, documenterResult),
|
|
434
|
+
`re-review round ${round}`,
|
|
435
|
+
);
|
|
436
|
+
if (!canContinue() || isFailedResult(reviewResult)) break;
|
|
437
|
+
const verdict = reviewVerdict(getResultOutput(reviewResult));
|
|
438
|
+
// REVIEW_PASS settles. No verdict is advisory/malformed and must never
|
|
439
|
+
// trigger another writer. Only an explicit REVIEW_FAIL consumes a fix.
|
|
440
|
+
if (verdict !== "fail") break;
|
|
441
|
+
lastReviewer = reviewResult;
|
|
372
442
|
}
|
|
373
443
|
};
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
parentThreadAtStart.task = workerStep.result.task;
|
|
409
|
-
parentThreadAtStart.sessionId = workerStep.result.sessionId;
|
|
410
|
-
parentThreadAtStart.sessionDir = workerStep.result.sessionDir;
|
|
411
|
-
runtime.retainSession(workerStep.result);
|
|
412
|
-
}
|
|
413
|
-
if (!ownsParent()) return;
|
|
414
|
-
chain.push({ ...workerStep, relation: `fix round ${round}` });
|
|
415
|
-
if (!runtime.sessionActive || isFailedResult(workerStep.result)) break;
|
|
416
|
-
const reReviewBrief = buildReReviewBrief(lastReviewer, round, workerStep.result);
|
|
417
|
-
const reviewStep = await launchInLoop("reviewer", reReviewBrief, executionCwd, signal, {
|
|
418
|
-
groupId: parentGroupId,
|
|
419
|
-
relationLabel: `re-review round ${round}`,
|
|
420
|
-
parentRunId,
|
|
421
|
-
});
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
// Park/stop/shutdown may win after the top-level child settles but
|
|
447
|
+
// before this continuation starts. Preserve that stable checkpoint and
|
|
448
|
+
// never create an already-aborted downstream child.
|
|
449
|
+
if (!canContinue()) return { kind: request.plan.kind, steps };
|
|
450
|
+
if (request.plan.kind === "auto-fix") {
|
|
451
|
+
await runFixRounds(initialStepResult);
|
|
452
|
+
} else {
|
|
453
|
+
let documenterResult: SingleResult | undefined;
|
|
454
|
+
if (request.plan.kind === "review-pass-sync") {
|
|
455
|
+
documenterResult = await launchStep(
|
|
456
|
+
"documenter",
|
|
457
|
+
buildReviewPassDocumenterBrief(initialStepResult),
|
|
458
|
+
"documentation sync",
|
|
459
|
+
);
|
|
460
|
+
} else if (initialStepResult.agent !== "documenter" && enabled("documenter")) {
|
|
461
|
+
documenterResult = await launchStep(
|
|
462
|
+
"documenter",
|
|
463
|
+
buildPostWriterDocumenterBrief(initialStepResult),
|
|
464
|
+
"documentation sync",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (
|
|
469
|
+
canContinue() &&
|
|
470
|
+
(!documenterResult || !isFailedResult(documenterResult)) &&
|
|
471
|
+
enabled("reviewer")
|
|
472
|
+
) {
|
|
473
|
+
const reviewResult = await launchStep(
|
|
474
|
+
"reviewer",
|
|
475
|
+
buildFinalReviewBrief(initialStepResult, documenterResult),
|
|
476
|
+
"final review",
|
|
477
|
+
);
|
|
422
478
|
if (
|
|
423
|
-
|
|
424
|
-
|
|
479
|
+
canContinue() &&
|
|
480
|
+
!isFailedResult(reviewResult) &&
|
|
481
|
+
reviewVerdict(getResultOutput(reviewResult)) === "fail" &&
|
|
482
|
+
enabled("worker") &&
|
|
483
|
+
request.config.maxFixRounds > 0
|
|
425
484
|
) {
|
|
426
|
-
|
|
427
|
-
parentThreadAtStart.agentName = reviewStep.result.agent;
|
|
428
|
-
parentThreadAtStart.task = reviewStep.result.task;
|
|
429
|
-
parentThreadAtStart.sessionId = reviewStep.result.sessionId;
|
|
430
|
-
parentThreadAtStart.sessionDir = reviewStep.result.sessionDir;
|
|
431
|
-
runtime.retainSession(reviewStep.result);
|
|
485
|
+
await runFixRounds(reviewResult);
|
|
432
486
|
}
|
|
433
|
-
if (!ownsParent()) return;
|
|
434
|
-
chain.push({ ...reviewStep, relation: `re-review round ${round}` });
|
|
435
|
-
lastReviewer = reviewStep.result;
|
|
436
|
-
// A crashed re-review must stop the chain like a crashed worker: its
|
|
437
|
-
// output (if any) is not a verdict, and feeding it to the next fix
|
|
438
|
-
// round would brief the worker from garbage.
|
|
439
|
-
if (!runtime.sessionActive || isFailedResult(reviewStep.result)) break;
|
|
440
|
-
if (reviewVerdict(getResultOutput(reviewStep.result)) === "pass") break;
|
|
441
|
-
}
|
|
442
|
-
// Every parent mutation is guarded by the exact generation, control, and
|
|
443
|
-
// queue controller that started this chain. A parked/resumed generation or
|
|
444
|
-
// destructive stop must make this old orchestration a no-op.
|
|
445
|
-
if (!ownsParent()) return;
|
|
446
|
-
const controlledParent = parentThreadAtStart;
|
|
447
|
-
if (controlledParent.retired || controlledParent.state === "stopped") {
|
|
448
|
-
clearOwnedController();
|
|
449
|
-
removeChainGroup(parentGroupId);
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
// Parking an auto-fix chain aborts its in-flight child but preserves the
|
|
453
|
-
// parent's checkpoint and suppresses an aborted chain delivery.
|
|
454
|
-
if (controlledParent.state === "parked") {
|
|
455
|
-
clearOwnedController();
|
|
456
|
-
removeChainGroup(parentGroupId);
|
|
457
|
-
monitor.setStatus(parentRunId, "parked");
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
// The chain is done (success, exhaustion, or abort): drop its monitor
|
|
461
|
-
// rows, then deliver one condensed
|
|
462
|
-
// summary. Register the parent's final state (the last chain result)
|
|
463
|
-
// before removal so subagent_wait can resolve it. Clone instead of
|
|
464
|
-
// mutating: the internal step remains addressable under its own run id.
|
|
465
|
-
const last = chain[chain.length - 1];
|
|
466
|
-
const parentResult: SingleResult = {
|
|
467
|
-
...last.result,
|
|
468
|
-
runId: parentRunId,
|
|
469
|
-
};
|
|
470
|
-
runtime.registerRunResult(parentRunId, parentResult);
|
|
471
|
-
removeChainGroup(parentGroupId);
|
|
472
|
-
monitor.removeRun(parentRunId);
|
|
473
|
-
runtime.retainSession(parentResult);
|
|
474
|
-
const parentThread = parentThreadAtStart;
|
|
475
|
-
parentThread.lastResult = parentResult;
|
|
476
|
-
parentThread.agentName = last.result.agent;
|
|
477
|
-
parentThread.task = last.result.task;
|
|
478
|
-
parentThread.sessionId = last.result.sessionId;
|
|
479
|
-
parentThread.sessionDir = last.result.sessionDir;
|
|
480
|
-
parentThread.state = isFailedResult(last.result) ? "failed" : "completed";
|
|
481
|
-
if (!runtime.sessionActive) {
|
|
482
|
-
clearOwnedController();
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
// One compact message instead of every round's raw output: the summary
|
|
486
|
-
// lines cover each step (verdict + what changed/found), and the final
|
|
487
|
-
// step's full report is appended only when its detail is actionable
|
|
488
|
-
// (a FAIL verdict, a crash, or a model-level failure the main agent
|
|
489
|
-
// must take over). Everything else stays one `subagent_status #id`
|
|
490
|
-
// call away.
|
|
491
|
-
let block = formatChainSummary(chain);
|
|
492
|
-
if (isFailedResult(last.result) && isModelLevelFailure(last.result)) {
|
|
493
|
-
block = `${block}\n\n${formatCompletionBlock(last.result, config.maxResultLines, executionCwd)}\n\n${modelLevelTakeoverNote(last.result, { runId: parentRunId })}`;
|
|
494
|
-
} else if (isFailedResult(last.result) || reviewVerdict(getResultOutput(last.result)) === "fail") {
|
|
495
|
-
block = `${block}\n\n${formatCompletionBlock(last.result, config.maxResultLines, executionCwd)}`;
|
|
496
487
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
usage: sumUsage(chain.map((step) => step.result.usage)),
|
|
503
|
-
},
|
|
504
|
-
]);
|
|
505
|
-
runtime.completionBatcher.flush();
|
|
506
|
-
clearOwnedController();
|
|
507
|
-
}),
|
|
508
|
-
() => {
|
|
509
|
-
if (!ownsParent()) return;
|
|
510
|
-
const controlledParent = parentThreadAtStart;
|
|
511
|
-
clearOwnedController();
|
|
512
|
-
removeChainGroup(parentGroupId);
|
|
513
|
-
if (controlledParent.state === "parked") {
|
|
514
|
-
monitor.setStatus(parentRunId, "parked");
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
if (!controlledParent.retired) monitor.removeRun(parentRunId);
|
|
518
|
-
},
|
|
519
|
-
(error) => {
|
|
520
|
-
// A crash inside the chain orchestration (failed runs are caught by
|
|
521
|
-
// launchInLoop and delivered as part of the chain) must not vanish, but
|
|
522
|
-
// an obsolete generation/controller must never publish it.
|
|
523
|
-
if (!ownsParent()) return;
|
|
524
|
-
if (parentThreadAtStart.retired || parentThreadAtStart.state === "stopped") {
|
|
525
|
-
clearOwnedController();
|
|
526
|
-
removeChainGroup(parentGroupId);
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
runtime.registerRunResult(parentRunId, initialReviewerResult);
|
|
530
|
-
removeChainGroup(parentGroupId);
|
|
531
|
-
monitor.removeRun(parentRunId);
|
|
532
|
-
if (!runtime.sessionActive) {
|
|
533
|
-
clearOwnedController();
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
537
|
-
try {
|
|
538
|
-
ctx.ui.notify(`✗ auto-fix chain dispatch failed: ${errorMessage}`, "error");
|
|
539
|
-
// Keep the triggering review's findings: the chain crashed before any
|
|
540
|
-
// fix round ran, and the main agent needs the review to act on it.
|
|
541
|
-
runtime.sendCompletionGroup([
|
|
542
|
-
{
|
|
543
|
-
agent: initialReviewerResult.agent,
|
|
544
|
-
block: `${formatCompletionBlock(initialReviewerResult, config.maxResultLines, executionCwd)}\n\nAuto-fix chain crashed before completion: ${errorMessage}. The planned fix rounds did not run; the review above is the triggering reviewer's full output.`,
|
|
545
|
-
triggerTurn: true,
|
|
546
|
-
usage: initialReviewerResult.usage,
|
|
547
|
-
},
|
|
548
|
-
]);
|
|
549
|
-
runtime.completionBatcher.flush();
|
|
550
|
-
} catch {
|
|
551
|
-
/* a second delivery failure must not throw through the queue */
|
|
552
|
-
} finally {
|
|
553
|
-
clearOwnedController();
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
);
|
|
557
|
-
runtime.runControllers.set(parentRunId, fixController);
|
|
558
|
-
parentThreadAtStart.queueController = fixController;
|
|
559
|
-
const priorCompletion = parentThreadAtStart.generationCompletion;
|
|
560
|
-
parentThreadAtStart.generationCompletion = Promise.all([
|
|
561
|
-
priorCompletion,
|
|
562
|
-
runtime.backgroundQueue.waitForTask(fixController),
|
|
563
|
-
]).then(() => undefined);
|
|
488
|
+
}
|
|
489
|
+
return { kind: request.plan.kind, steps };
|
|
490
|
+
} finally {
|
|
491
|
+
removeWorkflowGroup(request.groupId);
|
|
492
|
+
}
|
|
564
493
|
};
|
|
565
494
|
|
|
566
495
|
const startBackground = createBackgroundDispatcher({
|
|
@@ -571,7 +500,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
571
500
|
finishRun,
|
|
572
501
|
makeLiveHandler,
|
|
573
502
|
makeDetails,
|
|
574
|
-
|
|
503
|
+
runManagedWorkflow,
|
|
504
|
+
runInManagedRepositoryLane,
|
|
575
505
|
});
|
|
576
506
|
|
|
577
507
|
// Sub-agents intentionally detach from the foreground turn. This makes the
|