@ferris1225/pi-subagents 0.9.0 → 0.11.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/README.md +29 -6
- package/agents/explore.md +1 -0
- package/agents/reviewer.md +63 -45
- package/agents/worker.md +1 -0
- package/package.json +1 -1
- package/src/agents.ts +23 -6
- package/src/completion.ts +153 -0
- package/src/config.ts +74 -3
- package/src/fixloop.ts +76 -0
- package/src/index.ts +172 -24
- package/src/monitor.ts +18 -1
- package/src/prompt.ts +5 -4
- package/src/setup.ts +124 -19
- package/src/spawn.ts +52 -1
package/src/fixloop.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-fix loop: when a reviewer returns REVIEW_FAIL, the extension dispatches a
|
|
3
|
+
* worker (briefed with the review's concrete findings) and then a reviewer
|
|
4
|
+
* re-review, repeating up to maxFixRounds times before waking the main agent with
|
|
5
|
+
* the full chain. The reviewer stays read-only and in its own context; the loop
|
|
6
|
+
* is orchestrated by the extension layer, not by the reviewer itself, so the
|
|
7
|
+
* independence guarantee (no self-confirmation bias) is preserved.
|
|
8
|
+
*
|
|
9
|
+
* The main agent is never woken mid-loop: the reviewer's FAIL result is intercepted
|
|
10
|
+
* before delivery, the chain runs in the background, and only the final group
|
|
11
|
+
* (initial review → worker fixes → re-reviews) is delivered at the end.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
15
|
+
import type { SubagentsConfig } from "./config.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether a completed result should trigger the auto-fix loop instead of being
|
|
19
|
+
* delivered to the main agent. Only a REVIEW_FAIL verdict from a healthy
|
|
20
|
+
* reviewer run counts; failed processes and passing reviews are delivered
|
|
21
|
+
* normally. Loop-internal re-review results never reach this path (they are
|
|
22
|
+
* awaited inside the loop, not delivered through the completion flow).
|
|
23
|
+
*/
|
|
24
|
+
export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConfig): boolean {
|
|
25
|
+
if (config.maxFixRounds <= 0) return false;
|
|
26
|
+
if (result.agent !== "reviewer") return false;
|
|
27
|
+
if (isFailedResult(result)) return false;
|
|
28
|
+
return reviewVerdict(getResultOutput(result)) === "fail";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the worker task brief for one fix round from a reviewer's findings.
|
|
33
|
+
* The worker gets the full review text so it can address concrete file:line
|
|
34
|
+
* issues, with instructions to fix only blockers and self-verify.
|
|
35
|
+
*/
|
|
36
|
+
export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
|
|
37
|
+
const review = getResultOutput(reviewerResult);
|
|
38
|
+
const remaining = maxRounds - round;
|
|
39
|
+
return [
|
|
40
|
+
`Auto-fix round ${round} of ${maxRounds} (triggered by a failed review).`,
|
|
41
|
+
``,
|
|
42
|
+
`A reviewer ran in an isolated context and returned REQUEST_CHANGES. Its full report:`,
|
|
43
|
+
`---`,
|
|
44
|
+
review,
|
|
45
|
+
`---`,
|
|
46
|
+
``,
|
|
47
|
+
`Fix the concrete blockers the reviewer flagged. Do NOT refactor unrelated code.`,
|
|
48
|
+
`Address every "Critical" item; address "Warnings" only if they are genuine.`,
|
|
49
|
+
`After editing, run the project's format/build/tests when they exist and report`,
|
|
50
|
+
`exactly what you changed (paths + short rationale) so a reviewer can verify.`,
|
|
51
|
+
remaining > 0
|
|
52
|
+
? `A reviewer will re-review your changes automatically after you finish.`
|
|
53
|
+
: `This is the last auto-fix round; the main agent will be woken with the full chain.`,
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
59
|
+
* the prior review so the reviewer can verify the fixes without re-discovering
|
|
60
|
+
* the original issues.
|
|
61
|
+
*/
|
|
62
|
+
export function buildReReviewBrief(reviewerResult: SingleResult, round: number): string {
|
|
63
|
+
const review = getResultOutput(reviewerResult);
|
|
64
|
+
return [
|
|
65
|
+
`Re-review after auto-fix round ${round}.`,
|
|
66
|
+
``,
|
|
67
|
+
`The previous review (REQUEST_CHANGES) found these issues:`,
|
|
68
|
+
`---`,
|
|
69
|
+
review,
|
|
70
|
+
`---`,
|
|
71
|
+
``,
|
|
72
|
+
`Verify the worker's fixes address each blocker. Run \`git diff\` to see what changed.`,
|
|
73
|
+
`Classify honestly: APPROVE if blockers are resolved, REQUEST_CHANGES if not.`,
|
|
74
|
+
`End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,13 @@ import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
18
18
|
import { Type } from "typebox";
|
|
19
19
|
import { discoverAgents, type AgentConfig } from "./agents.ts";
|
|
20
20
|
import { BackgroundTaskQueue } from "./background.ts";
|
|
21
|
+
import {
|
|
22
|
+
completionGroupTriggersTurn,
|
|
23
|
+
completionTriggersTurn,
|
|
24
|
+
createCompletionBatcher,
|
|
25
|
+
formatCompletionMessage,
|
|
26
|
+
type CompletionMessageItem,
|
|
27
|
+
} from "./completion.ts";
|
|
21
28
|
import { getConfigPath, loadConfig, loadConfigSync, saveConfig } from "./config.ts";
|
|
22
29
|
import { repairUnavailableModelOverrides } from "./models.ts";
|
|
23
30
|
import { buildDelegationDirective } from "./prompt.ts";
|
|
@@ -27,13 +34,17 @@ import {
|
|
|
27
34
|
getFinalOutput,
|
|
28
35
|
getResultOutput,
|
|
29
36
|
isFailedResult,
|
|
37
|
+
reviewVerdict,
|
|
30
38
|
runSingleAgent,
|
|
39
|
+
truncateResultOutput,
|
|
40
|
+
writeResultArtifact,
|
|
31
41
|
type SingleResult,
|
|
32
42
|
type SubagentDetails,
|
|
33
43
|
type SubagentLiveEvent,
|
|
34
44
|
type UsageStats,
|
|
35
45
|
} from "./spawn.ts";
|
|
36
|
-
import {
|
|
46
|
+
import { buildFixTaskBrief, buildReReviewBrief, shouldTriggerFixLoop } from "./fixloop.ts";
|
|
47
|
+
import { formatTaskSummary, formatToolActivity, monitor, statusColor, statusIcon, statusLabel, type RunChainMeta } from "./monitor.ts";
|
|
37
48
|
|
|
38
49
|
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
39
50
|
|
|
@@ -59,7 +70,7 @@ function emptyUsage(): UsageStats {
|
|
|
59
70
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
function queuedResult(agent: AgentConfig, task: string): SingleResult {
|
|
73
|
+
function queuedResult(agent: AgentConfig, task: string, thinking?: string): SingleResult {
|
|
63
74
|
return {
|
|
64
75
|
agent: agent.name,
|
|
65
76
|
agentSource: agent.source,
|
|
@@ -69,6 +80,7 @@ function queuedResult(agent: AgentConfig, task: string): SingleResult {
|
|
|
69
80
|
stderr: "",
|
|
70
81
|
usage: emptyUsage(),
|
|
71
82
|
model: agent.model,
|
|
83
|
+
...(thinking ? { thinking } : {}),
|
|
72
84
|
};
|
|
73
85
|
}
|
|
74
86
|
|
|
@@ -114,6 +126,19 @@ function formatUsage(usage: UsageStats): string {
|
|
|
114
126
|
return parts.join(" ");
|
|
115
127
|
}
|
|
116
128
|
|
|
129
|
+
function formatCompletionBlock(result: SingleResult, maxResultLines: number): string {
|
|
130
|
+
const status = isFailedResult(result) ? "failed" : "completed";
|
|
131
|
+
const usage = formatUsage(result.usage);
|
|
132
|
+
const output = getResultOutput(result);
|
|
133
|
+
const { text, truncated } = truncateResultOutput(output, maxResultLines);
|
|
134
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}`, "", `Task: ${formatTaskSummary(result.task)}`, "", text];
|
|
135
|
+
if (truncated) {
|
|
136
|
+
// The full text lives on disk so the main agent can read it on demand.
|
|
137
|
+
lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${writeResultArtifact(output, result.agent)})`);
|
|
138
|
+
}
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
117
142
|
export default function (pi: ExtensionAPI): void {
|
|
118
143
|
const configPath = getConfigPath(getAgentDir());
|
|
119
144
|
// Init-time decisions need the config synchronously; the full (migrating)
|
|
@@ -121,6 +146,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
121
146
|
const initialConfig = loadConfigSync(configPath);
|
|
122
147
|
const backgroundQueue = new BackgroundTaskQueue(initialConfig.maxConcurrency);
|
|
123
148
|
let sessionActive = true;
|
|
149
|
+
const sendCompletionGroup = (items: CompletionMessageItem[]): void => {
|
|
150
|
+
if (!sessionActive || items.length === 0) return;
|
|
151
|
+
const message = {
|
|
152
|
+
customType: "subagent-result",
|
|
153
|
+
content: formatCompletionMessage(items),
|
|
154
|
+
display: true,
|
|
155
|
+
};
|
|
156
|
+
if (completionGroupTriggersTurn(items)) {
|
|
157
|
+
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
158
|
+
} else {
|
|
159
|
+
// No-wake delivery: nextTurn rides along with the next user turn and can
|
|
160
|
+
// never start a continuation by itself. followUp would auto-continue
|
|
161
|
+
// whenever pi is already streaming, defeating the opt-out.
|
|
162
|
+
pi.sendMessage(message, { deliverAs: "nextTurn" });
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const completionBatcher = createCompletionBatcher<CompletionMessageItem>({ emit: sendCompletionGroup });
|
|
124
166
|
|
|
125
167
|
// Recursion guard: sub-agents at the configured depth are leaf processes and
|
|
126
168
|
// cannot delegate again. maxSubagentDepth 0 disables the tool entirely.
|
|
@@ -148,6 +190,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
148
190
|
|
|
149
191
|
pi.on("session_shutdown", () => {
|
|
150
192
|
sessionActive = false;
|
|
193
|
+
completionBatcher.dispose();
|
|
151
194
|
backgroundQueue.cancelAll();
|
|
152
195
|
});
|
|
153
196
|
|
|
@@ -162,11 +205,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
162
205
|
"Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output)."
|
|
163
206
|
].join(" "),
|
|
164
207
|
promptSnippet:
|
|
165
|
-
"Start background subagents: explore (read-only search), worker (implement), reviewer (adversarial review); completion automatically resumes the main agent.",
|
|
208
|
+
"Start background subagents: explore (read-only search), worker (implement), reviewer (adversarial review); completion automatically resumes the main agent. Simple tasks: use direct tools, not subagents.",
|
|
166
209
|
promptGuidelines: [
|
|
167
|
-
"
|
|
168
|
-
"Use subagent with agent 'explore' for broad or open-ended code search before large changes.",
|
|
169
|
-
"Use subagent with agent 'worker'
|
|
210
|
+
"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.",
|
|
211
|
+
"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.",
|
|
212
|
+
"Use subagent with agent 'worker' for a self-contained implementation task worth a separate context; it plans internally.",
|
|
170
213
|
"Use subagent with agent 'reviewer' for a fresh read-only review before reporting work done or committing.",
|
|
171
214
|
"subagent launches work in the background and ends the current turn; when a result arrives, the main agent is automatically resumed with it.",
|
|
172
215
|
"Run independent tasks in parallel by passing a tasks array to subagent; let the automatically resumed main agent start dependent work after results arrive.",
|
|
@@ -292,12 +335,103 @@ export default function (pi: ExtensionAPI): void {
|
|
|
292
335
|
};
|
|
293
336
|
}
|
|
294
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Dispatch one agent inside an auto-fix chain: tracked in the widget with a
|
|
340
|
+
* groupId/relationLabel, but NOT delivered through the completion flow — the
|
|
341
|
+
* chain owner assembles and delivers the whole group at the end.
|
|
342
|
+
*/
|
|
343
|
+
const launchInLoop = async (
|
|
344
|
+
agentName: string,
|
|
345
|
+
task: string,
|
|
346
|
+
signal: AbortSignal,
|
|
347
|
+
meta: RunChainMeta,
|
|
348
|
+
): Promise<SingleResult> => {
|
|
349
|
+
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
350
|
+
if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
351
|
+
const thinkingLevel = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? config.thinkingLevel;
|
|
352
|
+
const runId = monitor.addRun(agent.name, task, agent.model, thinkingLevel, meta);
|
|
353
|
+
const onLive = makeLiveHandler(runId);
|
|
354
|
+
try {
|
|
355
|
+
const result = await runSingleAgent({
|
|
356
|
+
defaultCwd: ctx.cwd,
|
|
357
|
+
agent,
|
|
358
|
+
agentName,
|
|
359
|
+
task,
|
|
360
|
+
thinkingLevel,
|
|
361
|
+
signal,
|
|
362
|
+
onLive,
|
|
363
|
+
makeDetails: makeDetails("single", true),
|
|
364
|
+
});
|
|
365
|
+
finishRun(runId, isFailedResult(result) ? "failed" : "done");
|
|
366
|
+
return result;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
finishRun(runId, "failed");
|
|
369
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
370
|
+
return {
|
|
371
|
+
...queuedResult(agent, task, thinkingLevel),
|
|
372
|
+
exitCode: 1,
|
|
373
|
+
stderr: errorMessage,
|
|
374
|
+
stopReason: signal.aborted ? "aborted" : "error",
|
|
375
|
+
errorMessage,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Run the auto-fix chain in the background: worker (briefed with the review's
|
|
382
|
+
* findings) → reviewer re-review, up to maxFixRounds times. The main agent is
|
|
383
|
+
* not woken mid-loop; the full chain is delivered as one group at the end.
|
|
384
|
+
* Failures short-circuit: a crashed worker skips its re-review and delivers.
|
|
385
|
+
*/
|
|
386
|
+
const startFixLoop = (initialReviewerResult: SingleResult, parentGroupId: string): void => {
|
|
387
|
+
backgroundQueue.enqueue(
|
|
388
|
+
async (signal) => {
|
|
389
|
+
const chain: SingleResult[] = [initialReviewerResult];
|
|
390
|
+
let lastReviewer = initialReviewerResult;
|
|
391
|
+
for (let round = 1; round <= config.maxFixRounds; round++) {
|
|
392
|
+
if (!sessionActive) break;
|
|
393
|
+
const fixBrief = buildFixTaskBrief(lastReviewer, round, config.maxFixRounds);
|
|
394
|
+
const workerResult = await launchInLoop("worker", fixBrief, signal, {
|
|
395
|
+
groupId: parentGroupId,
|
|
396
|
+
relationLabel: `fix round ${round}`,
|
|
397
|
+
});
|
|
398
|
+
chain.push(workerResult);
|
|
399
|
+
if (!sessionActive || isFailedResult(workerResult)) break;
|
|
400
|
+
const reReviewBrief = buildReReviewBrief(lastReviewer, round);
|
|
401
|
+
const reviewResult = await launchInLoop("reviewer", reReviewBrief, signal, {
|
|
402
|
+
groupId: parentGroupId,
|
|
403
|
+
relationLabel: `re-review round ${round}`,
|
|
404
|
+
});
|
|
405
|
+
chain.push(reviewResult);
|
|
406
|
+
lastReviewer = reviewResult;
|
|
407
|
+
if (!sessionActive) break;
|
|
408
|
+
if (reviewVerdict(getResultOutput(reviewResult)) === "pass") break;
|
|
409
|
+
}
|
|
410
|
+
if (!sessionActive) return;
|
|
411
|
+
// Deliver the whole chain as one group; the loop's outcome always wakes
|
|
412
|
+
// the main agent (a passing chain reports success, a stuck one needs a human).
|
|
413
|
+
const items: CompletionMessageItem[] = chain.map((r) => ({
|
|
414
|
+
agent: r.agent,
|
|
415
|
+
block: formatCompletionBlock(r, config.maxResultLines),
|
|
416
|
+
triggerTurn: true,
|
|
417
|
+
}));
|
|
418
|
+
sendCompletionGroup(items);
|
|
419
|
+
completionBatcher.flush();
|
|
420
|
+
},
|
|
421
|
+
() => {
|
|
422
|
+
// Cancelled: each in-flight run was already finished by its launchInLoop path.
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
};
|
|
426
|
+
|
|
295
427
|
const startBackground = (agentName: string, task: string, cwd?: string): SingleResult => {
|
|
296
428
|
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
297
429
|
if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
298
430
|
|
|
299
|
-
|
|
300
|
-
const
|
|
431
|
+
// Effective strength: config override > agent frontmatter default > global default.
|
|
432
|
+
const thinkingLevel = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? config.thinkingLevel;
|
|
433
|
+
const pending = queuedResult(agent, task, thinkingLevel);
|
|
434
|
+
const runId = monitor.addRun(agent.name, task, agent.model, thinkingLevel);
|
|
301
435
|
const onLive = makeLiveHandler(runId);
|
|
302
436
|
|
|
303
437
|
backgroundQueue.enqueue(
|
|
@@ -310,7 +444,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
310
444
|
agentName,
|
|
311
445
|
task,
|
|
312
446
|
cwd,
|
|
313
|
-
thinkingLevel
|
|
447
|
+
thinkingLevel,
|
|
314
448
|
signal: backgroundSignal,
|
|
315
449
|
onLive,
|
|
316
450
|
makeDetails: makeDetails("single", true),
|
|
@@ -328,18 +462,29 @@ export default function (pi: ExtensionAPI): void {
|
|
|
328
462
|
}
|
|
329
463
|
|
|
330
464
|
if (!sessionActive) return;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
465
|
+
// Auto-fix loop: a REVIEW_FAIL from a main-agent-dispatched reviewer
|
|
466
|
+
// triggers a worker→reviewer chain (up to maxFixRounds) without waking
|
|
467
|
+
// the main agent. Loop-internal re-reviews never reach here (they are
|
|
468
|
+
// awaited inside launchInLoop); the initial review is delivered with
|
|
469
|
+
// the chain at the end.
|
|
470
|
+
if (shouldTriggerFixLoop(result, config)) {
|
|
471
|
+
startFixLoop(result, `fix-${runId}`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const failed = isFailedResult(result);
|
|
475
|
+
const completion: CompletionMessageItem = {
|
|
476
|
+
agent: result.agent,
|
|
477
|
+
block: formatCompletionBlock(result, config.maxResultLines),
|
|
478
|
+
triggerTurn: completionTriggersTurn(result, config.notifyOnReviewPass),
|
|
479
|
+
};
|
|
480
|
+
if (failed) {
|
|
481
|
+
// Failures never wait and never hide behind a success turn: deliver
|
|
482
|
+
// first so the wake-up leads with the failure; held successes follow.
|
|
483
|
+
sendCompletionGroup([completion]);
|
|
484
|
+
completionBatcher.flush();
|
|
485
|
+
} else {
|
|
486
|
+
completionBatcher.push(completion);
|
|
487
|
+
}
|
|
343
488
|
},
|
|
344
489
|
() => finishRun(runId, "failed"),
|
|
345
490
|
);
|
|
@@ -426,7 +571,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
426
571
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
427
572
|
const usage = formatUsage(r.usage);
|
|
428
573
|
const model = r.model ?? "?";
|
|
429
|
-
const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
|
|
574
|
+
const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
|
|
430
575
|
return new Text(line, 0, 0);
|
|
431
576
|
}
|
|
432
577
|
|
|
@@ -439,7 +584,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
439
584
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
440
585
|
const usage = formatUsage(r.usage);
|
|
441
586
|
const model = r.model ?? "?";
|
|
442
|
-
lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|
|
587
|
+
lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|
|
443
588
|
}
|
|
444
589
|
return new Text(lines.join("\n"), 0, 0);
|
|
445
590
|
},
|
|
@@ -473,7 +618,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
473
618
|
for (const r of runs) {
|
|
474
619
|
const icon = statusIcon(r.status, theme);
|
|
475
620
|
const label = theme.fg(statusColor(r.status), statusLabel(r.status));
|
|
476
|
-
|
|
621
|
+
// Chain-internal runs (auto-fix worker/reviewer) indent under their
|
|
622
|
+
// parent reviewer; summarize() already carries the relationLabel.
|
|
623
|
+
const head = r.groupId ? theme.fg("dim", " ↳ ") : " ";
|
|
624
|
+
lines.push(truncateToWidth(`${head}${icon} ${monitor.summarize(r)} · ${label}`, width, ""));
|
|
477
625
|
if (r.status === "queued" || r.status === "running") {
|
|
478
626
|
lines.push(truncateToWidth(theme.fg("dim", ` task: ${formatTaskSummary(r.task)}`), width, ""));
|
|
479
627
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface RunView {
|
|
|
26
26
|
agent: string;
|
|
27
27
|
task: string;
|
|
28
28
|
model?: string;
|
|
29
|
+
/** Effective thinking strength this run was launched with (frontmatter/config/global). */
|
|
30
|
+
thinking?: string;
|
|
29
31
|
status: RunStatus;
|
|
30
32
|
usage: UsageStats;
|
|
31
33
|
/** Concise current activity ("thinking", "read src/index.ts"); last writer wins. */
|
|
@@ -34,6 +36,16 @@ export interface RunView {
|
|
|
34
36
|
startedAt?: number;
|
|
35
37
|
/** Epoch ms when the run finished (set on "done"/"failed"). */
|
|
36
38
|
endedAt?: number;
|
|
39
|
+
/** When set, this run belongs to an auto-fix chain (e.g. worker fixing a reviewer's findings). */
|
|
40
|
+
groupId?: string;
|
|
41
|
+
/** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
|
|
42
|
+
relationLabel?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Optional chain metadata for runs spawned by an auto-fix loop. */
|
|
46
|
+
export interface RunChainMeta {
|
|
47
|
+
groupId?: string;
|
|
48
|
+
relationLabel?: string;
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
// ---------------------------------------------------------------------------
|
|
@@ -166,15 +178,18 @@ export class MonitorStore {
|
|
|
166
178
|
this.notify();
|
|
167
179
|
}
|
|
168
180
|
|
|
169
|
-
addRun(agent: string, task: string, model?: string): number {
|
|
181
|
+
addRun(agent: string, task: string, model?: string, thinking?: string, meta?: RunChainMeta): number {
|
|
170
182
|
const id = this.nextId++;
|
|
171
183
|
this.runs.push({
|
|
172
184
|
id,
|
|
173
185
|
agent,
|
|
174
186
|
task,
|
|
175
187
|
model,
|
|
188
|
+
thinking,
|
|
176
189
|
status: "queued",
|
|
177
190
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
191
|
+
...(meta?.groupId ? { groupId: meta.groupId } : {}),
|
|
192
|
+
...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
|
|
178
193
|
});
|
|
179
194
|
this.notify();
|
|
180
195
|
return id;
|
|
@@ -230,7 +245,9 @@ export class MonitorStore {
|
|
|
230
245
|
summarize(run: RunView): string {
|
|
231
246
|
const usage = formatUsageCompact(run.usage);
|
|
232
247
|
const parts = [run.agent];
|
|
248
|
+
if (run.relationLabel) parts.push(run.relationLabel);
|
|
233
249
|
if (run.model) parts.push(run.model);
|
|
250
|
+
if (run.thinking) parts.push(`thinking ${run.thinking}`);
|
|
234
251
|
if (usage) parts.push(usage);
|
|
235
252
|
const elapsed = formatElapsed(run);
|
|
236
253
|
if (elapsed) parts.push(elapsed);
|
package/src/prompt.ts
CHANGED
|
@@ -16,8 +16,8 @@ import { formatCatalogEntry } from "./agents.ts";
|
|
|
16
16
|
|
|
17
17
|
/** Compact role routing hints, emitted only for roles that are enabled. */
|
|
18
18
|
const ROLE_ROUTING: Record<string, string> = {
|
|
19
|
-
explore: "explore — broad/open-ended code search,
|
|
20
|
-
worker: "worker — implement/fix/refactor/test a
|
|
19
|
+
explore: "explore — broad/open-ended code search, multi-file lookups (read-only, cheap); NOT for one-line lookups.",
|
|
20
|
+
worker: "worker — implement/fix/refactor/test a self-contained task worth a separate context (full tools; plans internally).",
|
|
21
21
|
reviewer: "reviewer — adversarial pre-commit review of a diff (read-only; independent context).",
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -45,8 +45,9 @@ Available agents:
|
|
|
45
45
|
${catalog}
|
|
46
46
|
|
|
47
47
|
${routing ? `Routing:\n${routing}\n` : ""}Dispatch discipline:
|
|
48
|
-
-
|
|
49
|
-
-
|
|
48
|
+
- Handle SIMPLE work INLINE with direct tools: a single lookup, one-line edit, or a quick question is a grep/read/edit in the main context — never a sub-agent. Sub-agents cost startup time, tokens, and a context switch.
|
|
49
|
+
- Delegate only when isolation genuinely pays: broad exploration of an unfamiliar area, a self-contained implementation/fix with its own validation, or a fresh-context review gate.
|
|
50
|
+
- When in doubt, start with a direct tool call in the main context; escalate to a sub-agent only if the work turns out broad.
|
|
50
51
|
- For an already-known or trivial target, use a direct search/read tool (e.g. grep/find/read) — do not over-delegate a one-line lookup.
|
|
51
52
|
${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list. Let the automatically resumed main agent launch dependent work only after its prerequisite result arrives (e.g. explore, then worker, then reviewer).\n" : ""}- Brief each sub-agent as self-contained: goal, exact paths, constraints, expected output. It has NO memory of this conversation.
|
|
52
53
|
- Treat delegated agents as leaf workers: do not ask a sub-agent to dispatch another sub-agent; child processes do not have this tool.
|