@ferris1225/pi-subagents 1.0.1 → 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 +181 -92
- package/agents/cleaner.md +51 -0
- package/agents/explore.md +6 -4
- package/package.json +9 -7
- package/src/announcements.ts +8 -0
- package/src/config.ts +10 -27
- package/src/dispatch.ts +98 -77
- package/src/format.ts +26 -23
- package/src/index.ts +1 -1
- package/src/models.ts +91 -98
- package/src/monitor.ts +11 -3
- package/src/prompt.ts +13 -8
- package/src/rpc-run.ts +89 -6
- package/src/runtime.ts +0 -2
- package/src/setup.ts +437 -639
- package/src/spawn.ts +152 -107
- package/src/tools.ts +22 -6
- package/src/widget.ts +51 -14
package/src/config.ts
CHANGED
|
@@ -17,10 +17,10 @@ import { dirname, join } from "node:path";
|
|
|
17
17
|
import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
|
|
19
19
|
/** Full catalog of agents shipped with the package (selectable in /subagents-setup). */
|
|
20
|
-
export const BUILTIN_AGENT_NAMES = ["explore", "worker", "reviewer"] as const;
|
|
20
|
+
export const BUILTIN_AGENT_NAMES = ["explore", "worker", "cleaner", "reviewer"] as const;
|
|
21
21
|
|
|
22
|
-
/** Agents enabled out of the box. */
|
|
23
|
-
export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explore", "worker", "reviewer"];
|
|
22
|
+
/** Agents enabled out of the box on a fresh install. Explicit configured lists are preserved. */
|
|
23
|
+
export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explore", "worker", "cleaner", "reviewer"];
|
|
24
24
|
|
|
25
25
|
export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
|
|
26
26
|
export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
|
|
@@ -52,24 +52,20 @@ export const MAX_FIX_ROUNDS_LIMIT = 5;
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Default idle timeout in seconds: a sub-agent whose stdout (JSON event stream)
|
|
55
|
-
* goes silent for this long is terminated
|
|
56
|
-
*
|
|
55
|
+
* goes silent for this long is terminated; a selected model then hands the
|
|
56
|
+
* retained session to current main. 0 disables the watchdog. Default: 90.
|
|
57
57
|
*/
|
|
58
58
|
export const DEFAULT_IDLE_TIMEOUT_SEC = 90;
|
|
59
59
|
/** Upper bound accepted for idleTimeoutSec (defensive clamp). 0 disables. */
|
|
60
60
|
export const IDLE_TIMEOUT_SEC_LIMIT = 600;
|
|
61
61
|
|
|
62
62
|
export interface SubagentsConfig {
|
|
63
|
-
/** Agent names that are discoverable and injected.
|
|
63
|
+
/** Agent names that are discoverable and injected. Fresh-install default: explore, worker, cleaner, reviewer. */
|
|
64
64
|
enabledAgents: string[];
|
|
65
|
-
/** Per-agent
|
|
65
|
+
/** Per-agent model override, keyed by agent name, as "provider/model-id". */
|
|
66
66
|
agentModels: Record<string, string>;
|
|
67
|
-
/** Optional per-agent
|
|
68
|
-
agentBackupModels: Record<string, string>;
|
|
69
|
-
/** Per-agent thinking-level override, keyed by agent name. */
|
|
67
|
+
/** Optional per-agent thinking preference. Runtime clamps it to the effective model's supported levels. */
|
|
70
68
|
agentThinkingLevels: Record<string, ThinkingLevel>;
|
|
71
|
-
/** Thinking level for sub-agents without a per-agent override or frontmatter default. Default: "high". */
|
|
72
|
-
thinkingLevel: ThinkingLevel;
|
|
73
69
|
/**
|
|
74
70
|
* When a review passes (REVIEW_PASS verdict), deliver it without waking the
|
|
75
71
|
* main agent. Disabled by default so passing reviews still resume orchestration.
|
|
@@ -98,8 +94,8 @@ export interface SubagentsConfig {
|
|
|
98
94
|
maxFixRounds: number;
|
|
99
95
|
/**
|
|
100
96
|
* Idle timeout in seconds: a sub-agent whose stdout (JSON event stream) goes
|
|
101
|
-
* silent for this long is terminated
|
|
102
|
-
* model. 0 disables the idle watchdog. Default: 90.
|
|
97
|
+
* silent for this long is terminated; a configured agent model then hands
|
|
98
|
+
* off to the current main model. 0 disables the idle watchdog. Default: 90.
|
|
103
99
|
*/
|
|
104
100
|
idleTimeoutSec: number;
|
|
105
101
|
/**
|
|
@@ -119,9 +115,7 @@ export interface SubagentsConfig {
|
|
|
119
115
|
export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
120
116
|
enabledAgents: [...DEFAULT_ENABLED_AGENTS],
|
|
121
117
|
agentModels: {},
|
|
122
|
-
agentBackupModels: {},
|
|
123
118
|
agentThinkingLevels: {},
|
|
124
|
-
thinkingLevel: DEFAULT_THINKING_LEVEL,
|
|
125
119
|
notifyOnReviewPass: false,
|
|
126
120
|
maxResultLines: DEFAULT_MAX_RESULT_LINES,
|
|
127
121
|
proactiveInjection: true,
|
|
@@ -179,12 +173,6 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
179
173
|
}
|
|
180
174
|
}
|
|
181
175
|
|
|
182
|
-
if (isRecord(raw.agentBackupModels)) {
|
|
183
|
-
for (const [key, value] of Object.entries(raw.agentBackupModels)) {
|
|
184
|
-
if (isModelReference(value)) config.agentBackupModels[key.trim()] = value.trim();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
176
|
if (isRecord(raw.agentThinkingLevels)) {
|
|
189
177
|
for (const [key, value] of Object.entries(raw.agentThinkingLevels)) {
|
|
190
178
|
if (
|
|
@@ -196,10 +184,6 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
196
184
|
}
|
|
197
185
|
}
|
|
198
186
|
|
|
199
|
-
if (typeof raw.thinkingLevel === "string" && (THINKING_LEVEL_VALUES as readonly string[]).includes(raw.thinkingLevel)) {
|
|
200
|
-
config.thinkingLevel = raw.thinkingLevel as ThinkingLevel;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
187
|
if (typeof raw.notifyOnReviewPass === "boolean") {
|
|
204
188
|
config.notifyOnReviewPass = raw.notifyOnReviewPass;
|
|
205
189
|
}
|
|
@@ -246,7 +230,6 @@ function defaultConfig(): SubagentsConfig {
|
|
|
246
230
|
...DEFAULT_CONFIG,
|
|
247
231
|
enabledAgents: [...DEFAULT_CONFIG.enabledAgents],
|
|
248
232
|
agentModels: {},
|
|
249
|
-
agentBackupModels: {},
|
|
250
233
|
agentThinkingLevels: {},
|
|
251
234
|
announcedFeatures: [],
|
|
252
235
|
};
|
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,
|
|
@@ -37,7 +41,14 @@ import {
|
|
|
37
41
|
shouldTriggerFixLoop,
|
|
38
42
|
type ChainStep,
|
|
39
43
|
} from "./fixloop.ts";
|
|
40
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
availableModelsInScope,
|
|
46
|
+
currentModelRef,
|
|
47
|
+
findModelByRef,
|
|
48
|
+
modelRef,
|
|
49
|
+
resolveAgentModelRoute,
|
|
50
|
+
resolveThinkingLevel,
|
|
51
|
+
} from "./models.ts";
|
|
41
52
|
import {
|
|
42
53
|
formatTaskSummary,
|
|
43
54
|
formatToolActivity,
|
|
@@ -49,14 +60,13 @@ import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts"
|
|
|
49
60
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
|
|
50
61
|
import { forkRetainedSession } from "./session-fork.ts";
|
|
51
62
|
import {
|
|
52
|
-
buildFallbackResumeReason,
|
|
53
63
|
buildResumePrompt,
|
|
54
64
|
RpcRunControl,
|
|
55
65
|
getResultOutput,
|
|
56
66
|
isFailedResult,
|
|
57
67
|
isModelLevelFailure,
|
|
58
68
|
reviewVerdict,
|
|
59
|
-
|
|
69
|
+
runSingleAgentWithMainFallback,
|
|
60
70
|
type SingleResult,
|
|
61
71
|
type SubagentDetails,
|
|
62
72
|
type SubagentLiveEvent,
|
|
@@ -86,14 +96,13 @@ interface DispatchEnvironment {
|
|
|
86
96
|
ctx: ExtensionContext;
|
|
87
97
|
config: SubagentsConfig;
|
|
88
98
|
agents: AgentConfig[];
|
|
89
|
-
sessionRef?: string;
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
const VISION_DESCRIPTION =
|
|
93
|
-
"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";
|
|
94
103
|
|
|
95
104
|
const ISOLATION_DESCRIPTION =
|
|
96
|
-
"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)";
|
|
97
106
|
|
|
98
107
|
const IsolationSchema = Type.Optional(
|
|
99
108
|
StringEnum(["shared", "worktree"] as const, { description: ISOLATION_DESCRIPTION }),
|
|
@@ -175,21 +184,39 @@ function serializeAutoFixChain(
|
|
|
175
184
|
};
|
|
176
185
|
}
|
|
177
186
|
|
|
178
|
-
|
|
187
|
+
interface DispatchModelRoute {
|
|
188
|
+
agent: AgentConfig;
|
|
189
|
+
mainFallbackRef?: string;
|
|
190
|
+
thinkingLevel: ThinkingLevel;
|
|
191
|
+
thinkingLevelForModel: (ref?: string) => ThinkingLevel;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveDispatchModelRoute(
|
|
179
195
|
agent: AgentConfig,
|
|
180
196
|
config: SubagentsConfig,
|
|
181
|
-
|
|
197
|
+
ctx: ExtensionContext,
|
|
182
198
|
vision: boolean,
|
|
183
|
-
):
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
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],
|
|
187
204
|
mainRef,
|
|
188
205
|
declaredDefaultRef: agent.model,
|
|
206
|
+
availableRefs: availableModels.map(modelRef),
|
|
189
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
|
+
};
|
|
190
215
|
return {
|
|
191
|
-
agent: { ...agent, model:
|
|
192
|
-
|
|
216
|
+
agent: { ...agent, model: route.primaryRef },
|
|
217
|
+
mainFallbackRef: route.mainFallbackRef,
|
|
218
|
+
thinkingLevel: thinkingLevelForModel(route.primaryRef),
|
|
219
|
+
thinkingLevelForModel,
|
|
193
220
|
};
|
|
194
221
|
}
|
|
195
222
|
|
|
@@ -199,28 +226,31 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
199
226
|
label: "Subagent",
|
|
200
227
|
description: [
|
|
201
228
|
"Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
|
|
202
|
-
"
|
|
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.",
|
|
203
231
|
"Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
|
|
204
|
-
"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.",
|
|
205
233
|
"Use subagent_control to steer, retarget, park, resume, or fork a thread by its stable run id.",
|
|
206
234
|
"It starts agents in the background and immediately returns control to the main window; completion messages automatically wake the main agent to continue.",
|
|
207
235
|
"Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output).",
|
|
208
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).",
|
|
209
|
-
"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.",
|
|
210
238
|
].join(" "),
|
|
211
239
|
promptSnippet:
|
|
212
|
-
"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.",
|
|
213
241
|
promptGuidelines: [
|
|
214
|
-
"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.",
|
|
215
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.",
|
|
216
245
|
"Use subagent with agent 'worker' for a self-contained implementation task worth a separate context; it plans internally.",
|
|
217
|
-
"
|
|
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.",
|
|
218
248
|
"subagent launches work in the background and ends the current turn; when a result arrives, the main agent is automatically resumed with it.",
|
|
219
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.",
|
|
220
|
-
"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.",
|
|
221
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.",
|
|
222
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.",
|
|
223
|
-
"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.",
|
|
224
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.",
|
|
225
255
|
],
|
|
226
256
|
parameters: SubagentParams,
|
|
@@ -267,6 +297,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
267
297
|
break;
|
|
268
298
|
case "model":
|
|
269
299
|
monitor.setModel(runId, e.model, e.fallbackFrom);
|
|
300
|
+
monitor.setThinking(runId, e.thinking);
|
|
270
301
|
break;
|
|
271
302
|
case "usage":
|
|
272
303
|
monitor.setUsage(runId, e.usage, e.model);
|
|
@@ -292,7 +323,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
292
323
|
enabledNames: config.enabledAgents,
|
|
293
324
|
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
294
325
|
});
|
|
295
|
-
const sessionRef = currentModelRef(ctx);
|
|
296
326
|
const agents = discovery.agents;
|
|
297
327
|
|
|
298
328
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
@@ -356,32 +386,35 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
356
386
|
): Promise<{ runId?: number; result: SingleResult }> => {
|
|
357
387
|
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
358
388
|
if (!agent) return { result: failedStartResult(agentName, task, `Unknown agent: "${agentName}".`) };
|
|
359
|
-
// Vision chains
|
|
360
|
-
//
|
|
361
|
-
const
|
|
362
|
-
const thinkingLevel =
|
|
363
|
-
const runId = monitor.addRun(agent.name, task,
|
|
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);
|
|
364
394
|
const onLive = makeLiveHandler(runId);
|
|
365
395
|
try {
|
|
366
|
-
const result = await
|
|
396
|
+
const result = await runSingleAgentWithMainFallback(
|
|
367
397
|
{
|
|
368
398
|
defaultCwd: executionCwd,
|
|
369
399
|
cwd: executionCwd,
|
|
370
|
-
agent:
|
|
400
|
+
agent: route.agent,
|
|
371
401
|
agentName,
|
|
372
402
|
task,
|
|
373
403
|
thinkingLevel,
|
|
404
|
+
thinkingLevelForModel: route.thinkingLevelForModel,
|
|
374
405
|
signal,
|
|
375
406
|
onLive,
|
|
376
407
|
makeDetails: makeDetails("single", true),
|
|
377
408
|
idleTimeoutMs: config.idleTimeoutSec * 1000,
|
|
378
409
|
},
|
|
379
|
-
|
|
410
|
+
route.mainFallbackRef,
|
|
380
411
|
);
|
|
381
412
|
result.runId = runId;
|
|
413
|
+
result.projectCwd = executionCwd;
|
|
382
414
|
result.isolation = "shared";
|
|
383
415
|
runtime.retainSession(result);
|
|
384
416
|
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
417
|
+
monitor.setThinking(runId, result.thinking);
|
|
385
418
|
// The parent row represents the chain. Internal rounds leave live
|
|
386
419
|
// status as soon as they settle; their reports remain addressable by id.
|
|
387
420
|
finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
|
|
@@ -391,8 +424,9 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
391
424
|
finishRun(runId, "failed", { silent: true });
|
|
392
425
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
393
426
|
const crashed: SingleResult = {
|
|
394
|
-
...queuedResult(
|
|
427
|
+
...queuedResult(route.agent, task, thinkingLevel),
|
|
395
428
|
runId,
|
|
429
|
+
projectCwd: executionCwd,
|
|
396
430
|
isolation: "shared",
|
|
397
431
|
exitCode: 1,
|
|
398
432
|
stderr: errorMessage,
|
|
@@ -646,8 +680,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
646
680
|
worktree?: WorktreeIsolation;
|
|
647
681
|
forkedFromRunId?: number;
|
|
648
682
|
forkObjective?: string;
|
|
649
|
-
modelPool?: string[];
|
|
650
|
-
thinkingLevel?: SubagentThread["thinkingLevel"];
|
|
651
683
|
}
|
|
652
684
|
|
|
653
685
|
interface ResumeReservation {
|
|
@@ -703,12 +735,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
703
735
|
const runCtx = environment?.ctx ?? ctx;
|
|
704
736
|
const runConfig = environment?.config ?? config;
|
|
705
737
|
const runAgents = environment?.agents ?? agents;
|
|
706
|
-
const runSessionRef = environment?.sessionRef ?? sessionRef;
|
|
707
738
|
const agent = runAgents.find((candidate) => candidate.name === agentName);
|
|
708
739
|
if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
709
740
|
if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
|
|
710
741
|
return {
|
|
711
|
-
...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.`),
|
|
712
743
|
isolation,
|
|
713
744
|
};
|
|
714
745
|
}
|
|
@@ -736,36 +767,29 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
736
767
|
}
|
|
737
768
|
}
|
|
738
769
|
const executionCwd = worktree?.cwd ?? originalCwd;
|
|
739
|
-
const
|
|
740
|
-
const inheritedPool = seed?.modelPool?.filter((ref) => ref.trim().length > 0) ?? [];
|
|
741
|
-
const rawPool = inheritedPool.length > 0
|
|
742
|
-
? {
|
|
743
|
-
agent: { ...agent, model: inheritedPool[0] },
|
|
744
|
-
fallbackModelRefs: inheritedPool.slice(1),
|
|
745
|
-
}
|
|
746
|
-
: resolvedPool;
|
|
770
|
+
const resolvedRoute = resolveDispatchModelRoute(agent, runConfig, runCtx, vision);
|
|
747
771
|
// Isolation is a persistent system-level invariant, not a one-shot task
|
|
748
|
-
// prefix: queued retargets, live retargets, resumes, and model
|
|
749
|
-
// all keep the same worktree boundary.
|
|
750
|
-
const
|
|
751
|
-
? { ...
|
|
752
|
-
:
|
|
753
|
-
const thinkingLevel =
|
|
754
|
-
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;
|
|
755
778
|
const priorTask = existingThread?.task;
|
|
756
779
|
const priorSessionId = seed?.sessionId ?? existingThread?.sessionId;
|
|
757
780
|
const priorSessionDir = seed?.sessionDir ?? existingThread?.sessionDir;
|
|
758
781
|
if (existingThread && resumeReservation && !ownsResumeReservation(existingThread, resumeReservation)) {
|
|
759
782
|
return failedStartResult(agentName, task, `Run #${existingThread.id} changed while resume was preparing; no new generation was started.`);
|
|
760
783
|
}
|
|
761
|
-
const runId = existingThread?.id ?? monitor.addRun(agent.name, task,
|
|
784
|
+
const runId = existingThread?.id ?? monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
762
785
|
isolation,
|
|
763
786
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
764
787
|
});
|
|
765
788
|
const generation = (existingThread?.generation ?? 0) + 1;
|
|
766
789
|
const pending: SingleResult = {
|
|
767
|
-
...queuedResult(
|
|
790
|
+
...queuedResult(route.agent, task, thinkingLevel),
|
|
768
791
|
runId,
|
|
792
|
+
projectCwd: originalCwd,
|
|
769
793
|
isolation,
|
|
770
794
|
...(isolation === "worktree" ? { integrationStatus: "pending" as const } : {}),
|
|
771
795
|
...(seed?.sessionId && seed.sessionDir
|
|
@@ -774,7 +798,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
774
798
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
775
799
|
};
|
|
776
800
|
if (existingThread) {
|
|
777
|
-
monitor.restartRun(runId, agent.name, task,
|
|
801
|
+
monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation);
|
|
778
802
|
runtime.settledRuns.delete(runId);
|
|
779
803
|
}
|
|
780
804
|
|
|
@@ -810,7 +834,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
810
834
|
thread.cwd = originalCwd;
|
|
811
835
|
thread.executionCwd = executionCwd;
|
|
812
836
|
thread.vision = vision;
|
|
813
|
-
thread.modelPool = modelPool;
|
|
814
837
|
thread.thinkingLevel = thinkingLevel;
|
|
815
838
|
thread.isolation = isolation;
|
|
816
839
|
thread.worktree = worktree;
|
|
@@ -835,7 +858,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
835
858
|
cwd: originalCwd,
|
|
836
859
|
executionCwd,
|
|
837
860
|
vision,
|
|
838
|
-
modelPool,
|
|
839
861
|
thinkingLevel,
|
|
840
862
|
isolation,
|
|
841
863
|
worktree,
|
|
@@ -859,7 +881,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
859
881
|
thread.notifyIsolationFailure = (finalization) => {
|
|
860
882
|
const paths = [finalization.worktreePath, finalization.patchPath].filter(Boolean).join(" · ");
|
|
861
883
|
runCtx.ui.notify(
|
|
862
|
-
`✗
|
|
884
|
+
`✗ ${agent.name} worktree ${finalization.integrated ? "cleanup" : "integration"} failed${paths ? ` · retained ${paths}` : ""}: ${finalization.error ?? "unknown Git integration error"}`,
|
|
863
885
|
"error",
|
|
864
886
|
);
|
|
865
887
|
};
|
|
@@ -1122,7 +1144,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1122
1144
|
ctx: currentCtx,
|
|
1123
1145
|
config: currentConfig,
|
|
1124
1146
|
agents: currentAgents,
|
|
1125
|
-
sessionRef: currentModelRef(currentCtx),
|
|
1126
1147
|
},
|
|
1127
1148
|
seed,
|
|
1128
1149
|
reservation,
|
|
@@ -1285,7 +1306,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1285
1306
|
ctx: currentCtx,
|
|
1286
1307
|
config: currentConfig,
|
|
1287
1308
|
agents: currentAgents,
|
|
1288
|
-
sessionRef: currentModelRef(currentCtx),
|
|
1289
1309
|
},
|
|
1290
1310
|
{
|
|
1291
1311
|
sessionId: forkedSession.sessionId,
|
|
@@ -1294,8 +1314,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1294
1314
|
worktree: childWorktree,
|
|
1295
1315
|
forkedFromRunId: runId,
|
|
1296
1316
|
forkObjective,
|
|
1297
|
-
modelPool: [...thread.modelPool],
|
|
1298
|
-
thinkingLevel: thread.thinkingLevel,
|
|
1299
1317
|
},
|
|
1300
1318
|
);
|
|
1301
1319
|
if (child.exitCode !== -1 || child.runId === undefined) {
|
|
@@ -1345,14 +1363,15 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1345
1363
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
1346
1364
|
let result: SingleResult;
|
|
1347
1365
|
try {
|
|
1348
|
-
result = await
|
|
1366
|
+
result = await runSingleAgentWithMainFallback(
|
|
1349
1367
|
{
|
|
1350
1368
|
defaultCwd: executionCwd,
|
|
1351
|
-
agent:
|
|
1369
|
+
agent: route.agent,
|
|
1352
1370
|
agentName,
|
|
1353
1371
|
task,
|
|
1354
1372
|
cwd: executionCwd,
|
|
1355
1373
|
thinkingLevel,
|
|
1374
|
+
thinkingLevelForModel: route.thinkingLevelForModel,
|
|
1356
1375
|
signal: backgroundSignal,
|
|
1357
1376
|
onLive,
|
|
1358
1377
|
control,
|
|
@@ -1364,11 +1383,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1364
1383
|
sessionDir: priorSessionDir,
|
|
1365
1384
|
stdinText: seed?.prompt ?? (newObjectiveOnResume
|
|
1366
1385
|
? task
|
|
1367
|
-
: buildResumePrompt(priorTask ?? task,
|
|
1386
|
+
: buildResumePrompt(priorTask ?? task, "the retained thread was resumed")),
|
|
1368
1387
|
}
|
|
1369
1388
|
: {}),
|
|
1370
1389
|
},
|
|
1371
|
-
|
|
1390
|
+
route.mainFallbackRef,
|
|
1372
1391
|
);
|
|
1373
1392
|
} catch (error) {
|
|
1374
1393
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -1387,6 +1406,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1387
1406
|
// no monitor mutation, result registration, or completion delivery.
|
|
1388
1407
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
1389
1408
|
result.runId = runId;
|
|
1409
|
+
result.projectCwd = originalCwd;
|
|
1390
1410
|
result.isolation = isolation;
|
|
1391
1411
|
result.forkedFromRunId = thread.forkedFromRunId;
|
|
1392
1412
|
result.forkChildRunIds = [...thread.forkChildRunIds];
|
|
@@ -1398,6 +1418,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1398
1418
|
thread.lastResult = result;
|
|
1399
1419
|
runtime.retainSession(result);
|
|
1400
1420
|
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
1421
|
+
monitor.setThinking(runId, result.thinking);
|
|
1401
1422
|
|
|
1402
1423
|
// Destructive stop owns publication once it has synchronously claimed
|
|
1403
1424
|
// the lifecycle. Leave the partial result/session on the thread; the
|
|
@@ -1457,8 +1478,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1457
1478
|
const completion: CompletionMessageItem = {
|
|
1458
1479
|
agent: result.agent,
|
|
1459
1480
|
block: modelLevel
|
|
1460
|
-
? `${formatCompletionBlock(result, runConfig.maxResultLines,
|
|
1461
|
-
: 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),
|
|
1462
1483
|
triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
|
|
1463
1484
|
};
|
|
1464
1485
|
if (modelLevel) {
|
|
@@ -1512,7 +1533,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1512
1533
|
!thread.retired;
|
|
1513
1534
|
try {
|
|
1514
1535
|
const crashed: SingleResult = {
|
|
1515
|
-
...dispatchFailedResult(
|
|
1536
|
+
...dispatchFailedResult(route.agent, control.getObjective(), error, thinkingLevel),
|
|
1516
1537
|
runId,
|
|
1517
1538
|
isolation,
|
|
1518
1539
|
forkedFromRunId: thread.forkedFromRunId,
|
|
@@ -1531,7 +1552,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1531
1552
|
runtime.sendCompletionGroup([
|
|
1532
1553
|
{
|
|
1533
1554
|
agent: agent.name,
|
|
1534
|
-
block: formatCompletionBlock(crashed, runConfig.maxResultLines,
|
|
1555
|
+
block: formatCompletionBlock(crashed, runConfig.maxResultLines, crashed.projectCwd ?? originalCwd),
|
|
1535
1556
|
triggerTurn: true,
|
|
1536
1557
|
},
|
|
1537
1558
|
]);
|
|
@@ -1656,7 +1677,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1656
1677
|
const pending = r.exitCode === -1;
|
|
1657
1678
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
1658
1679
|
const usage = formatUsage(r.usage);
|
|
1659
|
-
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (
|
|
1680
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
1660
1681
|
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
1661
1682
|
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
1662
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}` : ""}`)}`;
|
|
@@ -1671,7 +1692,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
1671
1692
|
const pending = r.exitCode === -1;
|
|
1672
1693
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
1673
1694
|
const usage = formatUsage(r.usage);
|
|
1674
|
-
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (
|
|
1695
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
1675
1696
|
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
1676
1697
|
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
1677
1698
|
lines.push(` ${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|