@ferris1225/pi-subagents 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -0
- package/README.md +188 -95
- package/agents/cleaner.md +51 -0
- package/agents/explore.md +6 -4
- package/agents/reviewer.md +2 -0
- package/package.json +9 -7
- package/src/agents.ts +2 -7
- package/src/announcements.ts +12 -1
- package/src/completion.ts +7 -36
- package/src/config.ts +310 -364
- package/src/dispatch.ts +145 -275
- package/src/fixloop.ts +0 -16
- package/src/format.ts +28 -30
- package/src/index.ts +6 -3
- package/src/models.ts +89 -106
- package/src/monitor.ts +57 -101
- package/src/prompt.ts +13 -8
- package/src/rpc-run.ts +90 -21
- package/src/runtime.ts +3 -17
- package/src/session-fork.ts +0 -4
- package/src/setup.ts +437 -639
- package/src/spawn.ts +587 -557
- package/src/tools.ts +27 -35
- package/src/ui.ts +3 -7
- package/src/widget.ts +144 -0
- package/src/worktree.ts +1 -1
- package/src/trajectory.ts +0 -312
package/src/dispatch.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `subagent` tool: dispatches explore/worker/reviewer agents as isolated pi
|
|
2
|
+
* The `subagent` tool: dispatches explore/worker/cleaner/reviewer agents as isolated pi
|
|
3
3
|
* child processes, single or parallel. Owns the dispatch pipeline: config load,
|
|
4
|
-
* per-agent model
|
|
4
|
+
* per-agent selected→main model routing, per-run status tracking, the auto-fix chain
|
|
5
5
|
* (REVIEW_FAIL → worker → re-review), and completion delivery.
|
|
6
6
|
*
|
|
7
|
-
* Vision: a task flagged `vision: true` uses the configured vision model
|
|
8
|
-
*
|
|
9
|
-
* main-window model. Stale refs remain in the pool and fail normally at runtime.
|
|
7
|
+
* Vision: a task flagged `vision: true` uses the configured vision model, then
|
|
8
|
+
* hands directly to the current main-window model on model/provider failure.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
@@ -21,7 +20,12 @@ import {
|
|
|
21
20
|
completionTriggersTurn,
|
|
22
21
|
type CompletionMessageItem,
|
|
23
22
|
} from "./completion.ts";
|
|
24
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_THINKING_LEVEL,
|
|
25
|
+
loadConfig,
|
|
26
|
+
type SubagentsConfig,
|
|
27
|
+
type ThinkingLevel,
|
|
28
|
+
} from "./config.ts";
|
|
25
29
|
import {
|
|
26
30
|
dispatchFailedResult,
|
|
27
31
|
failedStartResult,
|
|
@@ -35,10 +39,16 @@ import {
|
|
|
35
39
|
buildReReviewBrief,
|
|
36
40
|
formatChainSummary,
|
|
37
41
|
shouldTriggerFixLoop,
|
|
38
|
-
summarizeChainResult,
|
|
39
42
|
type ChainStep,
|
|
40
43
|
} from "./fixloop.ts";
|
|
41
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
availableModelsInScope,
|
|
46
|
+
currentModelRef,
|
|
47
|
+
findModelByRef,
|
|
48
|
+
modelRef,
|
|
49
|
+
resolveAgentModelRoute,
|
|
50
|
+
resolveThinkingLevel,
|
|
51
|
+
} from "./models.ts";
|
|
42
52
|
import {
|
|
43
53
|
formatTaskSummary,
|
|
44
54
|
formatToolActivity,
|
|
@@ -50,19 +60,17 @@ import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts"
|
|
|
50
60
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
|
|
51
61
|
import { forkRetainedSession } from "./session-fork.ts";
|
|
52
62
|
import {
|
|
53
|
-
buildFallbackResumeReason,
|
|
54
63
|
buildResumePrompt,
|
|
55
64
|
RpcRunControl,
|
|
56
65
|
getResultOutput,
|
|
57
66
|
isFailedResult,
|
|
58
67
|
isModelLevelFailure,
|
|
59
68
|
reviewVerdict,
|
|
60
|
-
|
|
69
|
+
runSingleAgentWithMainFallback,
|
|
61
70
|
type SingleResult,
|
|
62
71
|
type SubagentDetails,
|
|
63
72
|
type SubagentLiveEvent,
|
|
64
73
|
} from "./spawn.ts";
|
|
65
|
-
import { trajectoryStore, summarizeToolArgs } from "./trajectory.ts";
|
|
66
74
|
import {
|
|
67
75
|
createWorktreeIsolation,
|
|
68
76
|
resolveWorktreeTarget,
|
|
@@ -74,13 +82,9 @@ import {
|
|
|
74
82
|
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
75
83
|
export const FORK_CONTINUATION_PROMPT =
|
|
76
84
|
"Continue from the retained context above. Review the prior work, then take the most useful next step toward completing the existing objective without repeating completed work.";
|
|
77
|
-
|
|
85
|
+
const WORKTREE_ISOLATION_INSTRUCTIONS =
|
|
78
86
|
"You are running in a temporary detached Git worktree. Work only in the current cwd; do not create another worktree or manually copy/apply changes to the original checkout. The parent dispatcher will integrate your tracked, deleted, and untracked changes when this thread finally settles.";
|
|
79
87
|
|
|
80
|
-
export function buildWorktreeTaskPrompt(task: string): string {
|
|
81
|
-
return `${WORKTREE_ISOLATION_INSTRUCTIONS}\n\nTask: ${task}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
88
|
function withWorktreeSystemPrompt(agent: AgentConfig): AgentConfig {
|
|
85
89
|
return {
|
|
86
90
|
...agent,
|
|
@@ -92,14 +96,13 @@ interface DispatchEnvironment {
|
|
|
92
96
|
ctx: ExtensionContext;
|
|
93
97
|
config: SubagentsConfig;
|
|
94
98
|
agents: AgentConfig[];
|
|
95
|
-
sessionRef?: string;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
const VISION_DESCRIPTION =
|
|
99
|
-
"Set true when the task may require viewing images (screenshots, mockups, designs) — the configured vision model
|
|
102
|
+
"Set true when the task may require viewing images (screenshots, mockups, designs) — the configured vision model is used first, then model-level failures hand directly to the current main-window model";
|
|
100
103
|
|
|
101
104
|
const ISOLATION_DESCRIPTION =
|
|
102
|
-
"Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents only)";
|
|
105
|
+
"Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents, including worker and cleaner, only)";
|
|
103
106
|
|
|
104
107
|
const IsolationSchema = Type.Optional(
|
|
105
108
|
StringEnum(["shared", "worktree"] as const, { description: ISOLATION_DESCRIPTION }),
|
|
@@ -181,21 +184,39 @@ function serializeAutoFixChain(
|
|
|
181
184
|
};
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
interface DispatchModelRoute {
|
|
188
|
+
agent: AgentConfig;
|
|
189
|
+
mainFallbackRef?: string;
|
|
190
|
+
thinkingLevel: ThinkingLevel;
|
|
191
|
+
thinkingLevelForModel: (ref?: string) => ThinkingLevel;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveDispatchModelRoute(
|
|
185
195
|
agent: AgentConfig,
|
|
186
196
|
config: SubagentsConfig,
|
|
187
|
-
|
|
197
|
+
ctx: ExtensionContext,
|
|
188
198
|
vision: boolean,
|
|
189
|
-
):
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
199
|
+
): DispatchModelRoute {
|
|
200
|
+
const availableModels = availableModelsInScope(ctx);
|
|
201
|
+
const mainRef = currentModelRef(ctx);
|
|
202
|
+
const route = resolveAgentModelRoute({
|
|
203
|
+
selectedRef: vision ? config.visionModel : config.agentModels[agent.name],
|
|
193
204
|
mainRef,
|
|
194
205
|
declaredDefaultRef: agent.model,
|
|
206
|
+
availableRefs: availableModels.map(modelRef),
|
|
195
207
|
});
|
|
208
|
+
const preferred = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? DEFAULT_THINKING_LEVEL;
|
|
209
|
+
const thinkingLevelForModel = (ref?: string): ThinkingLevel => {
|
|
210
|
+
const model = ref === mainRef && ctx.model
|
|
211
|
+
? ctx.model
|
|
212
|
+
: findModelByRef(availableModels, ref);
|
|
213
|
+
return resolveThinkingLevel(model, preferred);
|
|
214
|
+
};
|
|
196
215
|
return {
|
|
197
|
-
agent: { ...agent, model:
|
|
198
|
-
|
|
216
|
+
agent: { ...agent, model: route.primaryRef },
|
|
217
|
+
mainFallbackRef: route.mainFallbackRef,
|
|
218
|
+
thinkingLevel: thinkingLevelForModel(route.primaryRef),
|
|
219
|
+
thinkingLevelForModel,
|
|
199
220
|
};
|
|
200
221
|
}
|
|
201
222
|
|
|
@@ -205,33 +226,36 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
205
226
|
label: "Subagent",
|
|
206
227
|
description: [
|
|
207
228
|
"Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
|
|
208
|
-
"
|
|
229
|
+
"Built-in roles (only configured enabled agents can dispatch): explore (read-only codebase recon), worker (implement/fix/refactor/test, full tools), cleaner (explicit evidence-first cleanup, full/write tools), reviewer (adversarial pre-commit gate, read-only).",
|
|
230
|
+
"When cleaner is enabled, route explicit cleanup intent in any language (for example dead code, redundancy, simplification, or over-engineering), including a requested periodic cleanup pass. Audit/find/inspect/report is read-only evidence, while explicit remove/clean/simplify/refactor permits verified edits. Generic code review goes to reviewer. Never route cleaner by PR count or as the pre-commit gate; reviewer reviews its edits.",
|
|
209
231
|
"Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
|
|
210
|
-
"Isolation: single tasks default to shared; parallel worker tasks default to detached Git worktrees unless isolation: shared is explicit. explore/reviewer cannot use
|
|
232
|
+
"Isolation: single tasks default to shared; parallel worker tasks default to detached Git worktrees unless isolation: shared is explicit. Cleaner is write-capable and can opt into worktree isolation; explore/reviewer cannot use it.",
|
|
211
233
|
"Use subagent_control to steer, retarget, park, resume, or fork a thread by its stable run id.",
|
|
212
234
|
"It starts agents in the background and immediately returns control to the main window; completion messages automatically wake the main agent to continue.",
|
|
213
235
|
"Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output).",
|
|
214
236
|
"Results arrive as wake-up messages automatically — you do NOT need to wait. If you must get a result in-turn, subagent_wait is a non-blocking lookup by default (pass timeoutMs to block).",
|
|
215
|
-
"Vision: set vision: true when the task may require viewing images (screenshots, mockups, design files — e.g. frontend work) — the configured vision model is
|
|
237
|
+
"Vision: set vision: true when the task may require viewing images (screenshots, mockups, design files — e.g. frontend work) — the configured vision model is used first, then model-level failures hand directly to the current main-window model.",
|
|
216
238
|
].join(" "),
|
|
217
239
|
promptSnippet:
|
|
218
|
-
"Start background subagents: explore (read-only search), worker (implement), reviewer (
|
|
240
|
+
"Start background subagents: explore (read-only search), worker (implement), cleaner (explicit evidence-first cleanup), reviewer (pre-commit review); completion automatically resumes the main agent. Simple tasks: use direct tools, not subagents.",
|
|
219
241
|
promptGuidelines: [
|
|
220
|
-
"Delegate only when an isolated context genuinely pays: broad exploration, a self-contained implementation, or a review gate. Handle simple lookups and one-line edits inline with direct tools — never spawn a sub-agent for them.",
|
|
242
|
+
"Delegate only when an isolated context genuinely pays: broad exploration, a self-contained implementation, explicit evidence-first cleanup, or a review gate. Handle simple lookups and one-line edits inline with direct tools — never spawn a sub-agent for them.",
|
|
221
243
|
"Use subagent with agent 'explore' for broad or open-ended code search before large changes; a targeted 'where is X' is a direct grep/read.",
|
|
244
|
+
"Treat explore output as a retrieval index, not authority: re-read load-bearing files before editing or deciding deletion, security, compatibility, persistence, or dynamic reachability. The cheapest model can cost more through rework on complex dynamic, concurrent, migration, or security-sensitive code; choose a stronger model or specialist there.",
|
|
222
245
|
"Use subagent with agent 'worker' for a self-contained implementation task worth a separate context; it plans internally.",
|
|
223
|
-
"
|
|
246
|
+
"When cleaner is enabled, use subagent with agent 'cleaner' only for explicit cleanup intent in any language (for example dead code, redundancy, simplification, or over-engineering) or a requested periodic cleanup pass. Audit/find/inspect/report means read-only ranked evidence; apply only for explicit remove/clean/simplify/refactor wording. Generic code review goes to reviewer. Never trigger cleaner from PR count or as a pre-commit gate; send non-trivial cleaner edits to reviewer.",
|
|
247
|
+
"Use subagent with agent 'reviewer' for the fresh read-only gate before reporting non-trivial work done or committing, including after cleaner edits.",
|
|
224
248
|
"subagent launches work in the background and ends the current turn; when a result arrives, the main agent is automatically resumed with it.",
|
|
225
249
|
"Run independent tasks in parallel by passing a tasks array to subagent; parallel worker items default to isolation: worktree so their edits are integrated independently. Pass isolation: shared only when workers intentionally need the caller's live uncommitted tree.",
|
|
226
|
-
"Use isolation: worktree only for worker
|
|
250
|
+
"Use isolation: worktree only for worker, cleaner, or another write-capable agent and only inside a Git repository with a committed HEAD; parallel worker tasks default to worktree, while cleaner must opt in. Setup or integration failures never silently fall back to shared.",
|
|
227
251
|
"NEVER sleep or poll, and do NOT call subagent_wait to hold the turn — subagent ends the turn immediately and the result arrives as a message that wakes you automatically (even mid-turn). Ending your turn is the default and the only correct way to wait.",
|
|
228
252
|
"If you must keep the turn for a result, call subagent_wait with an explicit timeoutMs (non-blocking by default) — never bash sleep/timeout to wait for a sub-agent.",
|
|
229
|
-
"When a delegated task may require viewing images (frontend screenshots, mockups, design comparisons), pass vision: true and give the sub-agent the exact image paths — it reads them with its read tool. The configured vision model
|
|
253
|
+
"When a delegated task may require viewing images (frontend screenshots, mockups, design comparisons), pass vision: true and give the sub-agent the exact image paths — it reads them with its read tool. The configured vision model is used first; model-level failures hand directly to the current main-window model.",
|
|
230
254
|
"When a sub-agent result arrives it is already shown to the user — do NOT restate, paraphrase, or summarize it; reply with only your own conclusion or next action (often just one line), since duplicating the result wastes tokens for nothing.",
|
|
231
255
|
],
|
|
232
256
|
parameters: SubagentParams,
|
|
233
257
|
|
|
234
|
-
async execute(_toolCallId, params,
|
|
258
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
235
259
|
monitor.beginTurn();
|
|
236
260
|
const config = await loadConfig(runtime.configPath);
|
|
237
261
|
// Pick up concurrency changes from /subagents-setup without a restart.
|
|
@@ -242,29 +266,25 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
242
266
|
const finishRun = (
|
|
243
267
|
runId: number,
|
|
244
268
|
status: "done" | "failed",
|
|
245
|
-
opts?: { silent?: boolean
|
|
269
|
+
opts?: { silent?: boolean },
|
|
246
270
|
): void => {
|
|
247
271
|
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
248
|
-
const run =
|
|
272
|
+
const run = monitor.removeRun(runId);
|
|
249
273
|
if (!run) return; // already finished — stay idempotent
|
|
250
|
-
if (opts?.retain) monitor.setRetained(runId, true);
|
|
251
274
|
if (opts?.silent || !runtime.sessionActive) return;
|
|
252
275
|
const icon = status === "done" ? "✓" : "✗";
|
|
253
276
|
ctx.ui.notify(`${icon} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
254
277
|
};
|
|
255
278
|
|
|
256
279
|
// Live sub-agent activity → concise one-line status ("thinking",
|
|
257
|
-
// "read src/index.ts", ...), never a raw args blob.
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// args summary). The live handler only updates monitor state; finishing
|
|
261
|
-
// (removeRun + notify) is
|
|
262
|
-
// owned by the queue task / launchInLoop. That keeps a startup retry —
|
|
280
|
+
// "read src/index.ts", ...), never a raw args blob. The live handler
|
|
281
|
+
// only updates monitor state; finishing (removeRun + notify) is owned
|
|
282
|
+
// by the queue task / launchInLoop. That keeps a startup retry —
|
|
263
283
|
// which fires a transient "failed" status before relaunching — from
|
|
264
284
|
// ripping the row out early, and lets the queue task decide between
|
|
265
285
|
// delivering a reviewer's result and starting an auto-fix chain.
|
|
266
286
|
const makeLiveHandler =
|
|
267
|
-
(runId: number,
|
|
287
|
+
(runId: number, generation?: number) =>
|
|
268
288
|
(e: SubagentLiveEvent): void => {
|
|
269
289
|
if (generation !== undefined && runtime.threads.get(runId)?.generation !== generation) return;
|
|
270
290
|
switch (e.kind) {
|
|
@@ -277,6 +297,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
277
297
|
break;
|
|
278
298
|
case "model":
|
|
279
299
|
monitor.setModel(runId, e.model, e.fallbackFrom);
|
|
300
|
+
monitor.setThinking(runId, e.thinking);
|
|
280
301
|
break;
|
|
281
302
|
case "usage":
|
|
282
303
|
monitor.setUsage(runId, e.usage, e.model);
|
|
@@ -295,31 +316,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
295
316
|
monitor.setActivity(runId, "responding");
|
|
296
317
|
break;
|
|
297
318
|
}
|
|
298
|
-
if (threadId !== undefined) {
|
|
299
|
-
const trajectory = trajectoryStore.get(threadId).trajectory;
|
|
300
|
-
switch (e.kind) {
|
|
301
|
-
case "status":
|
|
302
|
-
trajectory.append({ kind: "status", status: e.status });
|
|
303
|
-
break;
|
|
304
|
-
case "model":
|
|
305
|
-
trajectory.append({ kind: "candidate", model: e.model, fallbackFrom: e.fallbackFrom });
|
|
306
|
-
break;
|
|
307
|
-
case "usage":
|
|
308
|
-
trajectory.append({ kind: "usage", usage: { ...e.usage }, model: e.model });
|
|
309
|
-
break;
|
|
310
|
-
case "tool_start":
|
|
311
|
-
trajectory.append({
|
|
312
|
-
kind: "tool_start",
|
|
313
|
-
tool: e.toolName,
|
|
314
|
-
toolCallId: e.toolCallId,
|
|
315
|
-
summary: summarizeToolArgs(e.args),
|
|
316
|
-
});
|
|
317
|
-
break;
|
|
318
|
-
case "tool_end":
|
|
319
|
-
trajectory.append({ kind: "tool_end", tool: e.toolName, toolCallId: e.toolCallId, isError: e.isError });
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
319
|
};
|
|
324
320
|
|
|
325
321
|
const discovery = discoverAgents(ctx.cwd, {
|
|
@@ -327,7 +323,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
327
323
|
enabledNames: config.enabledAgents,
|
|
328
324
|
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
329
325
|
});
|
|
330
|
-
const sessionRef = currentModelRef(ctx);
|
|
331
326
|
const agents = discovery.agents;
|
|
332
327
|
|
|
333
328
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
@@ -391,70 +386,48 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
391
386
|
): Promise<{ runId?: number; result: SingleResult }> => {
|
|
392
387
|
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
393
388
|
if (!agent) return { result: failedStartResult(agentName, task, `Unknown agent: "${agentName}".`) };
|
|
394
|
-
// Vision chains
|
|
395
|
-
//
|
|
396
|
-
const
|
|
397
|
-
const thinkingLevel =
|
|
398
|
-
const runId = monitor.addRun(agent.name, task,
|
|
399
|
-
|
|
400
|
-
const chainState = trajectoryStore.get(runId);
|
|
401
|
-
chainState.trajectory.append({
|
|
402
|
-
kind: "dispatch",
|
|
403
|
-
agent: agent.name,
|
|
404
|
-
task,
|
|
405
|
-
model: pool.agent.model,
|
|
406
|
-
thinking: thinkingLevel,
|
|
407
|
-
pool: pool.fallbackModelRefs,
|
|
408
|
-
vision,
|
|
409
|
-
isolation: "shared",
|
|
410
|
-
originalCwd: executionCwd,
|
|
411
|
-
isolationCwd: executionCwd,
|
|
412
|
-
});
|
|
413
|
-
const onLive = makeLiveHandler(runId, runId);
|
|
389
|
+
// Vision chains use the vision override first; every model-level failure
|
|
390
|
+
// hands directly to the current main model with re-clamped thinking.
|
|
391
|
+
const route = resolveDispatchModelRoute(agent, config, ctx, vision);
|
|
392
|
+
const thinkingLevel = route.thinkingLevel;
|
|
393
|
+
const runId = monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, meta);
|
|
394
|
+
const onLive = makeLiveHandler(runId);
|
|
414
395
|
try {
|
|
415
|
-
const result = await
|
|
396
|
+
const result = await runSingleAgentWithMainFallback(
|
|
416
397
|
{
|
|
417
398
|
defaultCwd: executionCwd,
|
|
418
399
|
cwd: executionCwd,
|
|
419
|
-
agent:
|
|
400
|
+
agent: route.agent,
|
|
420
401
|
agentName,
|
|
421
402
|
task,
|
|
422
403
|
thinkingLevel,
|
|
404
|
+
thinkingLevelForModel: route.thinkingLevelForModel,
|
|
423
405
|
signal,
|
|
424
406
|
onLive,
|
|
425
407
|
makeDetails: makeDetails("single", true),
|
|
426
408
|
idleTimeoutMs: config.idleTimeoutSec * 1000,
|
|
427
409
|
},
|
|
428
|
-
|
|
410
|
+
route.mainFallbackRef,
|
|
429
411
|
);
|
|
430
412
|
result.runId = runId;
|
|
413
|
+
result.projectCwd = executionCwd;
|
|
431
414
|
result.isolation = "shared";
|
|
432
|
-
result.originalCwd = executionCwd;
|
|
433
|
-
result.isolationCwd = executionCwd;
|
|
434
415
|
runtime.retainSession(result);
|
|
435
416
|
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
});
|
|
441
|
-
// Keep the finished round in status state while the chain is
|
|
442
|
-
// still running, with a one-line summary of what it did; the whole
|
|
443
|
-
// group is dropped when the chain resolves (see removeChainGroup).
|
|
444
|
-
monitor.setSummary(runId, summarizeChainResult(result));
|
|
445
|
-
finishRun(runId, isFailedResult(result) ? "failed" : "done", { retain: true });
|
|
417
|
+
monitor.setThinking(runId, result.thinking);
|
|
418
|
+
// The parent row represents the chain. Internal rounds leave live
|
|
419
|
+
// status as soon as they settle; their reports remain addressable by id.
|
|
420
|
+
finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
|
|
446
421
|
runtime.registerRunResult(runId, result);
|
|
447
422
|
return { runId, result };
|
|
448
423
|
} catch (error) {
|
|
449
|
-
finishRun(runId, "failed", {
|
|
450
|
-
chainState.trajectory.append({ kind: "settled", status: "failed", model: pool.agent.model });
|
|
424
|
+
finishRun(runId, "failed", { silent: true });
|
|
451
425
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
452
426
|
const crashed: SingleResult = {
|
|
453
|
-
...queuedResult(
|
|
427
|
+
...queuedResult(route.agent, task, thinkingLevel),
|
|
454
428
|
runId,
|
|
429
|
+
projectCwd: executionCwd,
|
|
455
430
|
isolation: "shared",
|
|
456
|
-
originalCwd: executionCwd,
|
|
457
|
-
isolationCwd: executionCwd,
|
|
458
431
|
exitCode: 1,
|
|
459
432
|
stderr: errorMessage,
|
|
460
433
|
stopReason: signal.aborted ? "aborted" : "error",
|
|
@@ -473,7 +446,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
473
446
|
* Failures short-circuit: a crashed worker skips its re-review and delivers.
|
|
474
447
|
* The triggering reviewer stays in monitor state until the chain resolves.
|
|
475
448
|
*/
|
|
476
|
-
/** Drop
|
|
449
|
+
/** Drop any in-flight monitor row belonging to an auto-fix chain; the
|
|
477
450
|
* parent is removed separately (it does not carry the groupId). */
|
|
478
451
|
const removeChainGroup = (groupId: string): void => {
|
|
479
452
|
for (const run of [...monitor.getRuns()]) {
|
|
@@ -514,10 +487,20 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
514
487
|
};
|
|
515
488
|
fixController = runtime.backgroundQueue.enqueue(
|
|
516
489
|
serializeAutoFixChain(executionCwd, async (signal) => {
|
|
490
|
+
if (!ownsParent()) return;
|
|
491
|
+
// The parent id belongs to the stable logical thread and will point at
|
|
492
|
+
// the chain outcome. Archive the triggering review under its own id so
|
|
493
|
+
// every id advertised by the chain summary resolves to that exact step.
|
|
494
|
+
const initialStepRunId = monitor.reserveRunId();
|
|
495
|
+
const initialStepResult: SingleResult = {
|
|
496
|
+
...initialReviewerResult,
|
|
497
|
+
runId: initialStepRunId,
|
|
498
|
+
};
|
|
499
|
+
runtime.registerRunResult(initialStepRunId, initialStepResult);
|
|
517
500
|
const chain: ChainStep[] = [
|
|
518
|
-
{ runId:
|
|
501
|
+
{ runId: initialStepRunId, result: initialStepResult, relation: "initial review" },
|
|
519
502
|
];
|
|
520
|
-
let lastReviewer =
|
|
503
|
+
let lastReviewer = initialStepResult;
|
|
521
504
|
for (let round = 1; round <= config.maxFixRounds; round++) {
|
|
522
505
|
if (!runtime.sessionActive) break;
|
|
523
506
|
const fixBrief = buildFixTaskBrief(lastReviewer, round, config.maxFixRounds);
|
|
@@ -579,37 +562,34 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
579
562
|
return;
|
|
580
563
|
}
|
|
581
564
|
// Parking an auto-fix chain aborts its in-flight child but preserves the
|
|
582
|
-
// parent's
|
|
565
|
+
// parent's checkpoint and suppresses an aborted chain delivery.
|
|
583
566
|
if (controlledParent.state === "parked") {
|
|
584
567
|
clearOwnedController();
|
|
585
568
|
removeChainGroup(parentGroupId);
|
|
586
|
-
monitor.setRetained(parentRunId, false);
|
|
587
569
|
monitor.setStatus(parentRunId, "parked");
|
|
588
570
|
return;
|
|
589
571
|
}
|
|
590
|
-
// The chain is done (success, exhaustion, or abort): drop
|
|
591
|
-
//
|
|
572
|
+
// The chain is done (success, exhaustion, or abort): drop its monitor
|
|
573
|
+
// rows, then deliver one condensed
|
|
592
574
|
// summary. Register the parent's final state (the last chain result)
|
|
593
|
-
// before removal so subagent_wait can resolve it.
|
|
575
|
+
// before removal so subagent_wait can resolve it. Clone instead of
|
|
576
|
+
// mutating: the internal step remains addressable under its own run id.
|
|
594
577
|
const last = chain[chain.length - 1];
|
|
595
|
-
|
|
578
|
+
const parentResult: SingleResult = {
|
|
579
|
+
...last.result,
|
|
580
|
+
runId: parentRunId,
|
|
581
|
+
};
|
|
582
|
+
runtime.registerRunResult(parentRunId, parentResult);
|
|
596
583
|
removeChainGroup(parentGroupId);
|
|
597
584
|
monitor.removeRun(parentRunId);
|
|
598
|
-
runtime.retainSession(
|
|
585
|
+
runtime.retainSession(parentResult);
|
|
599
586
|
const parentThread = parentThreadAtStart;
|
|
587
|
+
parentThread.lastResult = parentResult;
|
|
600
588
|
parentThread.agentName = last.result.agent;
|
|
601
589
|
parentThread.task = last.result.task;
|
|
602
590
|
parentThread.sessionId = last.result.sessionId;
|
|
603
591
|
parentThread.sessionDir = last.result.sessionDir;
|
|
604
592
|
parentThread.state = isFailedResult(last.result) ? "failed" : "completed";
|
|
605
|
-
// The chain outcome settles the parent thread's trajectory: the
|
|
606
|
-
// last chain step is its final state.
|
|
607
|
-
const parentTrajectory = trajectoryStore.get(parentRunId);
|
|
608
|
-
parentTrajectory.trajectory.append({
|
|
609
|
-
kind: "settled",
|
|
610
|
-
status: parentThread.state === "failed" ? "failed" : "done",
|
|
611
|
-
model: last.result.model,
|
|
612
|
-
});
|
|
613
593
|
if (!runtime.sessionActive) {
|
|
614
594
|
clearOwnedController();
|
|
615
595
|
return;
|
|
@@ -642,7 +622,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
642
622
|
clearOwnedController();
|
|
643
623
|
removeChainGroup(parentGroupId);
|
|
644
624
|
if (controlledParent.state === "parked") {
|
|
645
|
-
monitor.setRetained(parentRunId, false);
|
|
646
625
|
monitor.setStatus(parentRunId, "parked");
|
|
647
626
|
return;
|
|
648
627
|
}
|
|
@@ -701,8 +680,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
701
680
|
worktree?: WorktreeIsolation;
|
|
702
681
|
forkedFromRunId?: number;
|
|
703
682
|
forkObjective?: string;
|
|
704
|
-
modelPool?: string[];
|
|
705
|
-
thinkingLevel?: SubagentThread["thinkingLevel"];
|
|
706
683
|
}
|
|
707
684
|
|
|
708
685
|
interface ResumeReservation {
|
|
@@ -758,12 +735,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
758
735
|
const runCtx = environment?.ctx ?? ctx;
|
|
759
736
|
const runConfig = environment?.config ?? config;
|
|
760
737
|
const runAgents = environment?.agents ?? agents;
|
|
761
|
-
const runSessionRef = environment?.sessionRef ?? sessionRef;
|
|
762
738
|
const agent = runAgents.find((candidate) => candidate.name === agentName);
|
|
763
739
|
if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
764
740
|
if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
|
|
765
741
|
return {
|
|
766
|
-
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to
|
|
742
|
+
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker or cleaner.`),
|
|
767
743
|
isolation,
|
|
768
744
|
};
|
|
769
745
|
}
|
|
@@ -776,7 +752,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
776
752
|
return {
|
|
777
753
|
...failedStartResult(agentName, task, `Run #${existingThread?.id ?? "?"} has no active continuation worktree.`),
|
|
778
754
|
isolation,
|
|
779
|
-
originalCwd,
|
|
780
755
|
integrationStatus: worktree.state === "finalizing" ? "pending" : worktree.state,
|
|
781
756
|
};
|
|
782
757
|
}
|
|
@@ -787,65 +762,49 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
787
762
|
return {
|
|
788
763
|
...failedStartResult(agentName, task, error instanceof Error ? error.message : String(error)),
|
|
789
764
|
isolation,
|
|
790
|
-
originalCwd,
|
|
791
765
|
};
|
|
792
766
|
}
|
|
793
767
|
}
|
|
794
768
|
}
|
|
795
769
|
const executionCwd = worktree?.cwd ?? originalCwd;
|
|
796
|
-
const
|
|
797
|
-
const inheritedPool = seed?.modelPool?.filter((ref) => ref.trim().length > 0) ?? [];
|
|
798
|
-
const rawPool = inheritedPool.length > 0
|
|
799
|
-
? {
|
|
800
|
-
agent: { ...agent, model: inheritedPool[0] },
|
|
801
|
-
fallbackModelRefs: inheritedPool.slice(1),
|
|
802
|
-
}
|
|
803
|
-
: resolvedPool;
|
|
770
|
+
const resolvedRoute = resolveDispatchModelRoute(agent, runConfig, runCtx, vision);
|
|
804
771
|
// Isolation is a persistent system-level invariant, not a one-shot task
|
|
805
|
-
// prefix: queued retargets, live retargets, resumes, and model
|
|
806
|
-
// all keep the same worktree boundary.
|
|
807
|
-
const
|
|
808
|
-
? { ...
|
|
809
|
-
:
|
|
810
|
-
const thinkingLevel =
|
|
811
|
-
const modelPool = [pool.agent.model, ...pool.fallbackModelRefs].filter((ref): ref is string => Boolean(ref));
|
|
772
|
+
// prefix: queued retargets, live retargets, resumes, and main-model
|
|
773
|
+
// handoffs all keep the same worktree boundary.
|
|
774
|
+
const route = isolation === "worktree"
|
|
775
|
+
? { ...resolvedRoute, agent: withWorktreeSystemPrompt(resolvedRoute.agent) }
|
|
776
|
+
: resolvedRoute;
|
|
777
|
+
const thinkingLevel = route.thinkingLevel;
|
|
812
778
|
const priorTask = existingThread?.task;
|
|
813
779
|
const priorSessionId = seed?.sessionId ?? existingThread?.sessionId;
|
|
814
780
|
const priorSessionDir = seed?.sessionDir ?? existingThread?.sessionDir;
|
|
815
781
|
if (existingThread && resumeReservation && !ownsResumeReservation(existingThread, resumeReservation)) {
|
|
816
782
|
return failedStartResult(agentName, task, `Run #${existingThread.id} changed while resume was preparing; no new generation was started.`);
|
|
817
783
|
}
|
|
818
|
-
const runId = existingThread?.id ?? monitor.addRun(agent.name, task,
|
|
784
|
+
const runId = existingThread?.id ?? monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
819
785
|
isolation,
|
|
820
786
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
821
787
|
});
|
|
822
788
|
const generation = (existingThread?.generation ?? 0) + 1;
|
|
823
789
|
const pending: SingleResult = {
|
|
824
|
-
...queuedResult(
|
|
790
|
+
...queuedResult(route.agent, task, thinkingLevel),
|
|
825
791
|
runId,
|
|
792
|
+
projectCwd: originalCwd,
|
|
826
793
|
isolation,
|
|
827
|
-
originalCwd,
|
|
828
|
-
isolationCwd: executionCwd,
|
|
829
794
|
...(isolation === "worktree" ? { integrationStatus: "pending" as const } : {}),
|
|
830
795
|
...(seed?.sessionId && seed.sessionDir
|
|
831
|
-
? { sessionId: seed.sessionId, sessionDir: seed.sessionDir
|
|
796
|
+
? { sessionId: seed.sessionId, sessionDir: seed.sessionDir }
|
|
832
797
|
: {}),
|
|
833
798
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
834
799
|
};
|
|
835
800
|
if (existingThread) {
|
|
836
|
-
monitor.restartRun(runId, agent.name, task,
|
|
801
|
+
monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation);
|
|
837
802
|
runtime.settledRuns.delete(runId);
|
|
838
803
|
}
|
|
839
804
|
|
|
840
805
|
let thread!: SubagentThread;
|
|
841
806
|
const control = new RpcRunControl(task, generation, (phase) => {
|
|
842
807
|
if (runtime.threads.get(runId)?.generation !== generation || phase === "settled") return;
|
|
843
|
-
// Orchestration transitions are part of the trajectory (retrying →
|
|
844
|
-
// retry event, park/stop → terminal control events).
|
|
845
|
-
const trajectory = trajectoryStore.get(runId).trajectory;
|
|
846
|
-
if (phase === "retrying") trajectory.append({ kind: "retry", reason: "retrying" });
|
|
847
|
-
else if (phase === "parked") trajectory.append({ kind: "park" });
|
|
848
|
-
else if (phase === "stopped") trajectory.append({ kind: "stop", reason: control.getStopMessage() });
|
|
849
808
|
const state: ThreadState =
|
|
850
809
|
phase === "queued" || phase === "starting"
|
|
851
810
|
? "queued"
|
|
@@ -866,45 +825,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
866
825
|
else if (state === "running") monitor.setStatus(runId, "running");
|
|
867
826
|
});
|
|
868
827
|
|
|
869
|
-
// Restart bumps the generation while preserving append-only history.
|
|
870
|
-
const trajectoryState = trajectoryStore.get(runId);
|
|
871
|
-
if (existingThread) {
|
|
872
|
-
trajectoryState.trajectory.restart();
|
|
873
|
-
trajectoryState.trajectory.append({
|
|
874
|
-
kind: "resume",
|
|
875
|
-
objective: newObjectiveOnResume ? task : undefined,
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
if (seed?.forkedFromRunId !== undefined) {
|
|
879
|
-
trajectoryState.trajectory.append({
|
|
880
|
-
kind: "fork",
|
|
881
|
-
sourceRunId: seed.forkedFromRunId,
|
|
882
|
-
childRunId: runId,
|
|
883
|
-
objective: seed.forkObjective,
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
trajectoryState.trajectory.append({
|
|
887
|
-
kind: "dispatch",
|
|
888
|
-
agent: agent.name,
|
|
889
|
-
task,
|
|
890
|
-
model: pool.agent.model,
|
|
891
|
-
thinking: thinkingLevel,
|
|
892
|
-
pool: pool.fallbackModelRefs,
|
|
893
|
-
vision,
|
|
894
|
-
resumed: existingThread !== undefined || seed !== undefined,
|
|
895
|
-
isolation,
|
|
896
|
-
originalCwd,
|
|
897
|
-
isolationCwd: executionCwd,
|
|
898
|
-
});
|
|
899
|
-
if (worktree && worktree !== previousWorktree) {
|
|
900
|
-
trajectoryState.trajectory.append({
|
|
901
|
-
kind: "worktree",
|
|
902
|
-
status: "created",
|
|
903
|
-
originalCwd,
|
|
904
|
-
isolationCwd: executionCwd,
|
|
905
|
-
worktreePath: worktree.worktreePath,
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
828
|
|
|
909
829
|
if (existingThread) {
|
|
910
830
|
thread = existingThread;
|
|
@@ -914,7 +834,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
914
834
|
thread.cwd = originalCwd;
|
|
915
835
|
thread.executionCwd = executionCwd;
|
|
916
836
|
thread.vision = vision;
|
|
917
|
-
thread.modelPool = modelPool;
|
|
918
837
|
thread.thinkingLevel = thinkingLevel;
|
|
919
838
|
thread.isolation = isolation;
|
|
920
839
|
thread.worktree = worktree;
|
|
@@ -939,7 +858,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
939
858
|
cwd: originalCwd,
|
|
940
859
|
executionCwd,
|
|
941
860
|
vision,
|
|
942
|
-
modelPool,
|
|
943
861
|
thinkingLevel,
|
|
944
862
|
isolation,
|
|
945
863
|
worktree,
|
|
@@ -963,7 +881,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
963
881
|
thread.notifyIsolationFailure = (finalization) => {
|
|
964
882
|
const paths = [finalization.worktreePath, finalization.patchPath].filter(Boolean).join(" · ");
|
|
965
883
|
runCtx.ui.notify(
|
|
966
|
-
`✗
|
|
884
|
+
`✗ ${agent.name} worktree ${finalization.integrated ? "cleanup" : "integration"} failed${paths ? ` · retained ${paths}` : ""}: ${finalization.error ?? "unknown Git integration error"}`,
|
|
967
885
|
"error",
|
|
968
886
|
);
|
|
969
887
|
};
|
|
@@ -975,21 +893,9 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
975
893
|
if (thread.generation !== expectedGeneration) return undefined;
|
|
976
894
|
const finalization = await thread.worktree.finalize();
|
|
977
895
|
monitor.setIsolation(runId, "worktree", finalization.status);
|
|
978
|
-
trajectoryState.trajectory.append({
|
|
979
|
-
kind: "worktree",
|
|
980
|
-
status: finalization.status,
|
|
981
|
-
originalCwd: thread.cwd,
|
|
982
|
-
isolationCwd: thread.executionCwd,
|
|
983
|
-
worktreePath: finalization.worktreePath,
|
|
984
|
-
patchPath: finalization.patchPath,
|
|
985
|
-
integrated: finalization.integrated,
|
|
986
|
-
error: finalization.error,
|
|
987
|
-
});
|
|
988
896
|
if (result) {
|
|
989
897
|
result.runId = runId;
|
|
990
898
|
result.isolation = "worktree";
|
|
991
|
-
result.originalCwd = thread.cwd;
|
|
992
|
-
result.isolationCwd = thread.executionCwd;
|
|
993
899
|
result.integrationStatus = finalization.status;
|
|
994
900
|
result.integrationApplied = finalization.integrated;
|
|
995
901
|
result.integrationError = finalization.error;
|
|
@@ -1014,7 +920,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1014
920
|
}
|
|
1015
921
|
}
|
|
1016
922
|
if (finalization.status === "retained") {
|
|
1017
|
-
runtime.retainWorktreeArtifacts(finalization);
|
|
1018
923
|
if (!thread.isolationFailureNotified) {
|
|
1019
924
|
thread.isolationFailureNotified = true;
|
|
1020
925
|
try {
|
|
@@ -1048,13 +953,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1048
953
|
const discardUnusedWorktree = async (candidate: WorktreeIsolation | undefined): Promise<void> => {
|
|
1049
954
|
if (!candidate) return;
|
|
1050
955
|
try {
|
|
1051
|
-
|
|
1052
|
-
await candidate.discard();
|
|
1053
|
-
return;
|
|
1054
|
-
}
|
|
1055
|
-
// Compatibility for externally supplied/test handles. Production handles
|
|
1056
|
-
// expose discard(), so this fallback never integrates a seeded worktree.
|
|
1057
|
-
if (candidate.state === "active") await candidate.finalize();
|
|
956
|
+
await candidate.discard();
|
|
1058
957
|
} catch (error) {
|
|
1059
958
|
const retainedPath = existsSync(candidate.worktreePath)
|
|
1060
959
|
? candidate.worktreePath
|
|
@@ -1069,7 +968,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1069
968
|
...(existsSync(candidate.patchPath) ? { patchPath: candidate.patchPath } : {}),
|
|
1070
969
|
error: `Discarding unused continuation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1071
970
|
};
|
|
1072
|
-
runtime.retainWorktreeArtifacts(finalization);
|
|
1073
971
|
await persistRecoveryRecords(runtime.configPath, [
|
|
1074
972
|
recoveryRecordFromFinalization(runId, finalization),
|
|
1075
973
|
]).catch(() => undefined);
|
|
@@ -1246,7 +1144,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1246
1144
|
ctx: currentCtx,
|
|
1247
1145
|
config: currentConfig,
|
|
1248
1146
|
agents: currentAgents,
|
|
1249
|
-
sessionRef: currentModelRef(currentCtx),
|
|
1250
1147
|
},
|
|
1251
1148
|
seed,
|
|
1252
1149
|
reservation,
|
|
@@ -1409,7 +1306,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1409
1306
|
ctx: currentCtx,
|
|
1410
1307
|
config: currentConfig,
|
|
1411
1308
|
agents: currentAgents,
|
|
1412
|
-
sessionRef: currentModelRef(currentCtx),
|
|
1413
1309
|
},
|
|
1414
1310
|
{
|
|
1415
1311
|
sessionId: forkedSession.sessionId,
|
|
@@ -1418,8 +1314,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1418
1314
|
worktree: childWorktree,
|
|
1419
1315
|
forkedFromRunId: runId,
|
|
1420
1316
|
forkObjective,
|
|
1421
|
-
modelPool: [...thread.modelPool],
|
|
1422
|
-
thinkingLevel: thread.thinkingLevel,
|
|
1423
1317
|
},
|
|
1424
1318
|
);
|
|
1425
1319
|
if (child.exitCode !== -1 || child.runId === undefined) {
|
|
@@ -1439,12 +1333,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1439
1333
|
const childThread = runtime.threads.get(childRunId);
|
|
1440
1334
|
if (childThread) childThread.forkedFromRunId = runId;
|
|
1441
1335
|
monitor.setForkRelation(runId, childRunId);
|
|
1442
|
-
trajectoryStore.get(runId).trajectory.append({
|
|
1443
|
-
kind: "fork",
|
|
1444
|
-
sourceRunId: runId,
|
|
1445
|
-
childRunId,
|
|
1446
|
-
objective: forkObjective,
|
|
1447
|
-
});
|
|
1448
1336
|
const sourceResult = runtime.settledRuns.get(runId) ?? thread.lastResult;
|
|
1449
1337
|
if (sourceResult) sourceResult.forkChildRunIds = [...thread.forkChildRunIds];
|
|
1450
1338
|
return child;
|
|
@@ -1469,20 +1357,21 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1469
1357
|
}
|
|
1470
1358
|
};
|
|
1471
1359
|
|
|
1472
|
-
const onLive = makeLiveHandler(runId,
|
|
1360
|
+
const onLive = makeLiveHandler(runId, generation);
|
|
1473
1361
|
const queueController = runtime.backgroundQueue.enqueue(
|
|
1474
1362
|
async (backgroundSignal) => {
|
|
1475
1363
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
1476
1364
|
let result: SingleResult;
|
|
1477
1365
|
try {
|
|
1478
|
-
result = await
|
|
1366
|
+
result = await runSingleAgentWithMainFallback(
|
|
1479
1367
|
{
|
|
1480
1368
|
defaultCwd: executionCwd,
|
|
1481
|
-
agent:
|
|
1369
|
+
agent: route.agent,
|
|
1482
1370
|
agentName,
|
|
1483
1371
|
task,
|
|
1484
1372
|
cwd: executionCwd,
|
|
1485
1373
|
thinkingLevel,
|
|
1374
|
+
thinkingLevelForModel: route.thinkingLevelForModel,
|
|
1486
1375
|
signal: backgroundSignal,
|
|
1487
1376
|
onLive,
|
|
1488
1377
|
control,
|
|
@@ -1494,11 +1383,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1494
1383
|
sessionDir: priorSessionDir,
|
|
1495
1384
|
stdinText: seed?.prompt ?? (newObjectiveOnResume
|
|
1496
1385
|
? task
|
|
1497
|
-
: buildResumePrompt(priorTask ?? task,
|
|
1386
|
+
: buildResumePrompt(priorTask ?? task, "the retained thread was resumed")),
|
|
1498
1387
|
}
|
|
1499
1388
|
: {}),
|
|
1500
1389
|
},
|
|
1501
|
-
|
|
1390
|
+
route.mainFallbackRef,
|
|
1502
1391
|
);
|
|
1503
1392
|
} catch (error) {
|
|
1504
1393
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -1517,9 +1406,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1517
1406
|
// no monitor mutation, result registration, or completion delivery.
|
|
1518
1407
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
1519
1408
|
result.runId = runId;
|
|
1409
|
+
result.projectCwd = originalCwd;
|
|
1520
1410
|
result.isolation = isolation;
|
|
1521
|
-
result.originalCwd = originalCwd;
|
|
1522
|
-
result.isolationCwd = executionCwd;
|
|
1523
1411
|
result.forkedFromRunId = thread.forkedFromRunId;
|
|
1524
1412
|
result.forkChildRunIds = [...thread.forkChildRunIds];
|
|
1525
1413
|
thread.queueController = undefined;
|
|
@@ -1530,6 +1418,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1530
1418
|
thread.lastResult = result;
|
|
1531
1419
|
runtime.retainSession(result);
|
|
1532
1420
|
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
1421
|
+
monitor.setThinking(runId, result.thinking);
|
|
1533
1422
|
|
|
1534
1423
|
// Destructive stop owns publication once it has synchronously claimed
|
|
1535
1424
|
// the lifecycle. Leave the partial result/session on the thread; the
|
|
@@ -1548,8 +1437,10 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1548
1437
|
const wantsFixLoop = shouldTriggerFixLoop(result, runConfig);
|
|
1549
1438
|
if (wantsFixLoop && isolation === "shared" && runtime.sessionActive) {
|
|
1550
1439
|
thread.state = "running";
|
|
1551
|
-
|
|
1552
|
-
|
|
1440
|
+
// The review being done does not mean the logical run is over:
|
|
1441
|
+
// the same row now represents the chain until it resolves.
|
|
1442
|
+
monitor.setStatus(runId, "running");
|
|
1443
|
+
monitor.setActivity(runId, "auto-fix chain running");
|
|
1553
1444
|
startFixLoop(result, `fix-${runId}`, runId, thread.executionCwd, vision);
|
|
1554
1445
|
return;
|
|
1555
1446
|
}
|
|
@@ -1578,15 +1469,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1578
1469
|
// Stamp the terminal monitor state before projecting it. This gives every
|
|
1579
1470
|
// path a fixed endedAt even when the row is removed immediately.
|
|
1580
1471
|
monitor.setStatus(runId, failed ? "failed" : "done");
|
|
1581
|
-
trajectoryState.trajectory.append({
|
|
1582
|
-
kind: "settled",
|
|
1583
|
-
status: failed ? "failed" : "done",
|
|
1584
|
-
model: result.model,
|
|
1585
|
-
isolation,
|
|
1586
|
-
...(result.integrationStatus && result.integrationStatus !== "pending"
|
|
1587
|
-
? { integrationStatus: result.integrationStatus }
|
|
1588
|
-
: {}),
|
|
1589
|
-
});
|
|
1590
1472
|
if (!runtime.sessionActive || !ownsSettlement()) return;
|
|
1591
1473
|
|
|
1592
1474
|
const modelLevel = failed && isModelLevelFailure(result);
|
|
@@ -1596,8 +1478,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1596
1478
|
const completion: CompletionMessageItem = {
|
|
1597
1479
|
agent: result.agent,
|
|
1598
1480
|
block: modelLevel
|
|
1599
|
-
? `${formatCompletionBlock(result, runConfig.maxResultLines,
|
|
1600
|
-
: formatCompletionBlock(result, runConfig.maxResultLines,
|
|
1481
|
+
? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
|
|
1482
|
+
: formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
|
|
1601
1483
|
triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
|
|
1602
1484
|
};
|
|
1603
1485
|
if (modelLevel) {
|
|
@@ -1618,8 +1500,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1618
1500
|
() => {
|
|
1619
1501
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
1620
1502
|
// Queued park/stop owns publication and may still be finalizing an
|
|
1621
|
-
// isolated worktree. Do not expose a terminal monitor
|
|
1622
|
-
//
|
|
1503
|
+
// isolated worktree. Do not expose a terminal monitor state before
|
|
1504
|
+
// that owner records the checkpoint or aborted result.
|
|
1623
1505
|
if (thread.lifecycleOperation === "park" || thread.lifecycleOperation === "stop") return;
|
|
1624
1506
|
runtime.runControllers.delete(runId);
|
|
1625
1507
|
thread.queueController = undefined;
|
|
@@ -1629,7 +1511,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1629
1511
|
}
|
|
1630
1512
|
thread.state = "stopped";
|
|
1631
1513
|
monitor.setStatus(runId, "failed");
|
|
1632
|
-
trajectoryState.trajectory.append({ kind: "settled", status: "stopped", model: monitor.findRun(runId)?.model, isolation });
|
|
1633
1514
|
if (!runtime.sessionActive) {
|
|
1634
1515
|
monitor.removeRun(runId);
|
|
1635
1516
|
return;
|
|
@@ -1652,26 +1533,15 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1652
1533
|
!thread.retired;
|
|
1653
1534
|
try {
|
|
1654
1535
|
const crashed: SingleResult = {
|
|
1655
|
-
...dispatchFailedResult(
|
|
1536
|
+
...dispatchFailedResult(route.agent, control.getObjective(), error, thinkingLevel),
|
|
1656
1537
|
runId,
|
|
1657
1538
|
isolation,
|
|
1658
|
-
originalCwd,
|
|
1659
|
-
isolationCwd: executionCwd,
|
|
1660
1539
|
forkedFromRunId: thread.forkedFromRunId,
|
|
1661
1540
|
};
|
|
1662
1541
|
await thread.finalizeIsolation(generation, crashed);
|
|
1663
1542
|
if (!ownsSettlement()) return;
|
|
1664
1543
|
thread.state = "failed";
|
|
1665
1544
|
monitor.setStatus(runId, "failed");
|
|
1666
|
-
trajectoryState.trajectory.append({
|
|
1667
|
-
kind: "settled",
|
|
1668
|
-
status: "failed",
|
|
1669
|
-
model: crashed.model,
|
|
1670
|
-
isolation,
|
|
1671
|
-
...(crashed.integrationStatus && crashed.integrationStatus !== "pending"
|
|
1672
|
-
? { integrationStatus: crashed.integrationStatus }
|
|
1673
|
-
: {}),
|
|
1674
|
-
});
|
|
1675
1545
|
finishRun(runId, "failed", { silent: true });
|
|
1676
1546
|
runtime.registerRunResult(runId, crashed);
|
|
1677
1547
|
runtime.runControllers.delete(runId);
|
|
@@ -1682,7 +1552,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1682
1552
|
runtime.sendCompletionGroup([
|
|
1683
1553
|
{
|
|
1684
1554
|
agent: agent.name,
|
|
1685
|
-
block: formatCompletionBlock(crashed, runConfig.maxResultLines,
|
|
1555
|
+
block: formatCompletionBlock(crashed, runConfig.maxResultLines, crashed.projectCwd ?? originalCwd),
|
|
1686
1556
|
triggerTurn: true,
|
|
1687
1557
|
},
|
|
1688
1558
|
]);
|
|
@@ -1807,7 +1677,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1807
1677
|
const pending = r.exitCode === -1;
|
|
1808
1678
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
1809
1679
|
const usage = formatUsage(r.usage);
|
|
1810
|
-
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (
|
|
1680
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
1811
1681
|
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
1812
1682
|
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
1813
1683
|
const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
|
|
@@ -1822,7 +1692,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1822
1692
|
const pending = r.exitCode === -1;
|
|
1823
1693
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
1824
1694
|
const usage = formatUsage(r.usage);
|
|
1825
|
-
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (
|
|
1695
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
1826
1696
|
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
1827
1697
|
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
1828
1698
|
lines.push(` ${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|