@ferris1225/pi-subagents 2.1.0 → 2.2.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 +33 -16
- package/agents/reviewer.md +7 -9
- package/package.json +1 -1
- package/src/completion.ts +10 -2
- package/src/dispatch.ts +6 -1
- package/src/fixloop.ts +24 -21
- package/src/format.ts +3 -6
- package/src/monitor.ts +19 -0
- package/src/prompt.ts +2 -2
- package/src/thread-lifecycle.ts +2 -0
- package/src/tools.ts +1 -0
- package/src/widget.ts +94 -52
package/README.md
CHANGED
|
@@ -32,15 +32,23 @@ The common quality loop now runs end to end without waking the main agent betwee
|
|
|
32
32
|
steps:
|
|
33
33
|
|
|
34
34
|
```text
|
|
35
|
-
reviewer (find
|
|
35
|
+
reviewer (find issues) → worker (fix every finding) → reviewer (verify) → final PASS/FAIL
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
Reviews use a single flat findings list — no severity triage. Every reported
|
|
39
|
+
finding is fixed before the change is accepted, and each re-review converges on
|
|
40
|
+
an open-finding set: the worker's explicit rejections are adjudicated once, only
|
|
41
|
+
defects a fix round introduced or exposed are added, and resolved items never
|
|
42
|
+
re-open. `maxFixRounds` stays the hard cap, so a chain always settles and wakes
|
|
43
|
+
the main agent with the full picture.
|
|
44
|
+
|
|
38
45
|
Cleanup stays a separate lifecycle: explicit cleanup intent can dispatch the
|
|
39
46
|
evidence-first `cleaner`; any edits still go through the independent `reviewer`
|
|
40
47
|
gate.
|
|
41
48
|
|
|
42
|
-
Each chain is delivered as one concise completion group
|
|
43
|
-
|
|
49
|
+
Each chain is delivered as one concise completion group whose footer totals the
|
|
50
|
+
aggregate token usage and cost of every included run, while full per-run reports
|
|
51
|
+
remain available through `subagent_status`. Its parent stays `running`
|
|
44
52
|
until the whole chain settles; completed internal rounds leave active status
|
|
45
53
|
immediately, so no `done` row keeps accumulating elapsed time. Selected-to-main
|
|
46
54
|
model handoffs keep the same retained context, and isolated parallel workers use
|
|
@@ -63,12 +71,16 @@ index.
|
|
|
63
71
|
"go check" step. `subagent_wait` is a **non-blocking** in-turn lookup by default
|
|
64
72
|
(pass `timeoutMs` to block); `subagent_status` inspects runs; `subagent_stop`
|
|
65
73
|
cancels one and delivers its partial output.
|
|
66
|
-
- **Active-only live widget** — each queued or running sub-agent gets one compact
|
|
67
|
-
width-aware primary line with task, effective model/thinking, and elapsed time;
|
|
68
|
-
|
|
74
|
+
- **Active-only live widget, as a tree** — each queued or running sub-agent gets one compact
|
|
75
|
+
width-aware primary line with task, effective model/thinking, and elapsed time; current
|
|
76
|
+
activity appears only when present on an indented second line. Auto-fix rounds nest under
|
|
77
|
+
the triggering reviewer row that owns the chain, so it is always visible who dispatched
|
|
78
|
+
what; no run ids appear here — the tree and the task label identify each row:
|
|
69
79
|
```text
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
● reviewer · review diff of src/foo.ts · claude-sonnet-4-5/high · 42s
|
|
81
|
+
├ ● worker · fix round 1 · src/foo.ts · claude-sonnet-4-5/high · 10s
|
|
82
|
+
│ grep cacheKey
|
|
83
|
+
└ ○ reviewer · re-review round 1 · claude-sonnet-4-5/high · 3s
|
|
72
84
|
```
|
|
73
85
|
Long tasks and activity paths truncate first (preserving a useful path tail when
|
|
74
86
|
possible), groups have no blank rows, and settled/parked runs disappear immediately.
|
|
@@ -83,7 +95,10 @@ index.
|
|
|
83
95
|
- **A quality gate that closes the loop** — when a reviewer returns `REVIEW_FAIL`,
|
|
84
96
|
the extension dispatches a worker briefed with the concrete findings, then a
|
|
85
97
|
re-review, up to `maxFixRounds` times — and only then wakes the main agent.
|
|
86
|
-
Every
|
|
98
|
+
Every reported finding gets fixed (no severity triage), and re-reviews converge
|
|
99
|
+
on an open-finding set instead of ping-ponging: worker rejections are adjudicated
|
|
100
|
+
once, only defects the fix round introduced are added, and resolved items never
|
|
101
|
+
re-open. Every round stays in the triggering reviewer's cwd, and chains that target the
|
|
87
102
|
same repository are serialized so shared-checkout edits cannot race.
|
|
88
103
|
- **Direct fallback with real thinking capabilities** — each agent has at most
|
|
89
104
|
one selected model. An unavailable selection, rate limit, invalid key, quota,
|
|
@@ -98,9 +113,9 @@ index.
|
|
|
98
113
|
`subagent_control` can steer active work, retarget it after a stable abort,
|
|
99
114
|
park/resume it under the same run id, or fork a parked/settled checkpoint into
|
|
100
115
|
a new independent run. Concurrent resume calls are serialized.
|
|
101
|
-
- **Concise but honest completions** —
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
- **Concise but honest completions** — group completions end with aggregate token
|
|
117
|
+
and cost totals across every included run; failed-tool diagnostics stay out of the
|
|
118
|
+
delivered message and remain one `subagent_status` call away. Actual process,
|
|
104
119
|
model, and integration failures still surface as failures.
|
|
105
120
|
- **Parallel fan-out with filesystem isolation** — independent tasks run up to a
|
|
106
121
|
configurable limit (default 4). Parallel workers default to detached Git
|
|
@@ -123,8 +138,9 @@ an undifferentiated child-agent launcher:
|
|
|
123
138
|
independent reviewer gate;
|
|
124
139
|
- isolated, retained threads that can be steered, parked, resumed, retargeted, or
|
|
125
140
|
forked under stable run ids;
|
|
126
|
-
- the reviewer → worker auto-fix → reviewer loop
|
|
127
|
-
|
|
141
|
+
- the reviewer → worker auto-fix → reviewer loop, fixing every finding under a
|
|
142
|
+
convergence contract with a hard round cap;
|
|
143
|
+
- failed-tool diagnostics available by run id through `subagent_status`;
|
|
128
144
|
- direct selected→main fallback plus capability-aware Auto thinking;
|
|
129
145
|
- detached Git worktree isolation for parallel workers and opt-in write-capable
|
|
130
146
|
cleaner runs.
|
|
@@ -226,7 +242,8 @@ because they may need to inspect the same images.
|
|
|
226
242
|
### Controlling and stopping
|
|
227
243
|
|
|
228
244
|
Dispatch confirmations, tool result rows, and completion blocks all show the
|
|
229
|
-
stable `#id`, so a thread remains directly controllable after its live UI is gone
|
|
245
|
+
stable `#id`, so a thread remains directly controllable after its live UI is gone
|
|
246
|
+
(the widget itself identifies rows by tree position and task instead of ids).
|
|
230
247
|
|
|
231
248
|
- `subagent_control` — `steer`, `retarget`, `park`, `resume`, or `fork` a logical
|
|
232
249
|
thread by stable run id. Resume accepts an optional replacement objective;
|
|
@@ -317,7 +334,7 @@ direct-file settings.
|
|
|
317
334
|
| `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
|
|
318
335
|
| `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
|
|
319
336
|
| `maxConcurrency` | Max sub-agent processes running at once (1–16, default 4), and the max tasks one parallel `subagent` call accepts. Extra work waits in the queue. |
|
|
320
|
-
| `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL` (default 2; `0` disables the loop). |
|
|
337
|
+
| `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL` (default 2; `0` disables the loop). Hard cap: the chain always settles, delivers its condensed summary, and wakes the main agent. |
|
|
321
338
|
| `idleTimeoutSec` | Idle watchdog: a sub-agent whose stdout goes silent for this long is terminated; a selected model then hands to current main. `0` disables it. Default 90. |
|
|
322
339
|
|
|
323
340
|
### Model routing and thinking
|
package/agents/reviewer.md
CHANGED
|
@@ -42,25 +42,23 @@ Understand the context first, then verify: the fix addresses the root cause, cha
|
|
|
42
42
|
- Concurrency: shared mutable state, locks held across await, races.
|
|
43
43
|
- Encoding/Unicode: assuming `char*`/files/CLI text is UTF-8; wrong `A` vs `W` Win32 APIs; boundary conversions.
|
|
44
44
|
- Resource leaks; violations of the project's stated conventions.
|
|
45
|
-
-
|
|
45
|
+
- Every finding you report gets fixed by the auto-fix loop — there are no severity tiers and no optional nits. Report genuine defects and risks only, each defensible with file:line evidence; leave out style preferences and anything you would merely "suggest". Order findings most important first.
|
|
46
46
|
|
|
47
47
|
## Collaboration
|
|
48
48
|
- Independent of `worker` by design — your verdict is the gate before commit. Fix nothing yourself; report so the caller can dispatch a worker.
|
|
49
|
+
- Re-reviews converge on an open-finding set: rule on each previously reported finding once (adjudicating the worker's explicit rejections — uphold only with a concrete refutation), add only defects the fix round introduced or exposed, and never re-open an item you verified resolved. Rounds are hard-capped, so padding a re-review with restated findings just burns them.
|
|
49
50
|
|
|
50
51
|
## Output format
|
|
51
52
|
## Files Reviewed
|
|
52
53
|
- `path/to/file.ts`
|
|
53
|
-
##
|
|
54
|
+
## Findings
|
|
54
55
|
- `file.ts:42` — concrete issue and why it breaks.
|
|
55
|
-
|
|
56
|
-
- `file.ts:10` — issue and suggested direction.
|
|
57
|
-
## Suggestions (consider)
|
|
58
|
-
- Optional improvements.
|
|
56
|
+
(Write "None" when you found nothing; an empty findings list is a valid, honest result.)
|
|
59
57
|
## Verdict
|
|
60
|
-
One of: APPROVE /
|
|
61
|
-
End with exactly one machine-readable line: `VERDICT: REVIEW_PASS` for APPROVE
|
|
58
|
+
One of: APPROVE / REQUEST_CHANGES, plus a 2-3 sentence rationale.
|
|
59
|
+
End with exactly one machine-readable line: `VERDICT: REVIEW_PASS` for APPROVE; `VERDICT: REVIEW_FAIL` for REQUEST_CHANGES.
|
|
62
60
|
|
|
63
|
-
REQUEST_CHANGES
|
|
61
|
+
REQUEST_CHANGES whenever even one finding remains. On this extension REQUEST_CHANGES automatically starts an auto-fix loop: a worker is briefed with your findings, fixes every one of them, and you re-review, up to the configured round limit. APPROVE means the findings list is empty — or, on a re-review, that every previously reported finding is resolved. Never quietly wave an issue through, and never invent findings to hedge: report each genuine issue once, accurately.
|
|
64
62
|
|
|
65
63
|
## Quality standards
|
|
66
64
|
Specific file paths and line numbers. No vague feedback. A clean report means you looked hard, not that you found nothing to say.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Controllable background sub-agent threads for pi: specialized roles, capability-aware thinking, direct main-model fallback, auto-fix chains, and Git worktree isolation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/completion.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
11
|
+
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
12
|
+
import type { UsageStats } from "./rpc-run.ts";
|
|
11
13
|
|
|
12
14
|
export interface CompletionBatchTimings {
|
|
13
15
|
debounceMs: number;
|
|
@@ -98,14 +100,20 @@ export interface CompletionMessageItem {
|
|
|
98
100
|
agent: string;
|
|
99
101
|
block: string;
|
|
100
102
|
triggerTurn: boolean;
|
|
103
|
+
/** Final usage of the underlying run (or chain); aggregated into the group totals. */
|
|
104
|
+
usage?: UsageStats;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
/** Keep the established single-result shape; add a
|
|
107
|
+
/** Keep the established single-result shape; add a group header and an aggregate
|
|
108
|
+
* token/cost footer only for real groups. */
|
|
104
109
|
export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
|
|
105
110
|
if (items.length === 0) return "";
|
|
106
111
|
if (items.length === 1) return items[0].block;
|
|
107
112
|
const agents = items.map((item) => item.agent).join(", ");
|
|
108
|
-
|
|
113
|
+
const withUsage = items.filter((item) => item.usage !== undefined);
|
|
114
|
+
const totals = withUsage.length > 0 ? formatUsageCompact(sumUsage(withUsage.map((item) => item.usage!))) : "";
|
|
115
|
+
const footer = totals ? `\n\nTotals: ${items.length} runs · ${totals}` : "";
|
|
116
|
+
return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}${footer}`;
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
/** A grouped completion wakes the main agent when any member requires a turn. */
|
package/src/dispatch.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
formatToolActivity,
|
|
35
35
|
monitor,
|
|
36
36
|
statusIcon,
|
|
37
|
+
sumUsage,
|
|
37
38
|
type RunChainMeta,
|
|
38
39
|
} from "./monitor.ts";
|
|
39
40
|
import type { SubagentRuntime } from "./runtime.ts";
|
|
@@ -423,6 +424,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
423
424
|
const workerStep = await launchInLoop("worker", fixBrief, executionCwd, signal, {
|
|
424
425
|
groupId: parentGroupId,
|
|
425
426
|
relationLabel: `fix round ${round}`,
|
|
427
|
+
parentRunId,
|
|
426
428
|
}, vision);
|
|
427
429
|
// Preserve the newest sub-step before checking chain ownership. A
|
|
428
430
|
// destructive stop invalidates ownsParent() while this child is
|
|
@@ -442,10 +444,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
442
444
|
if (!ownsParent()) return;
|
|
443
445
|
chain.push({ ...workerStep, relation: `fix round ${round}` });
|
|
444
446
|
if (!runtime.sessionActive || isFailedResult(workerStep.result)) break;
|
|
445
|
-
const reReviewBrief = buildReReviewBrief(lastReviewer, round);
|
|
447
|
+
const reReviewBrief = buildReReviewBrief(lastReviewer, round, workerStep.result);
|
|
446
448
|
const reviewStep = await launchInLoop("reviewer", reReviewBrief, executionCwd, signal, {
|
|
447
449
|
groupId: parentGroupId,
|
|
448
450
|
relationLabel: `re-review round ${round}`,
|
|
451
|
+
parentRunId,
|
|
449
452
|
}, vision);
|
|
450
453
|
if (
|
|
451
454
|
runtime.threads.get(parentRunId) === parentThreadAtStart &&
|
|
@@ -527,6 +530,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
527
530
|
agent: `auto-fix chain (${last.result.agent})`,
|
|
528
531
|
block,
|
|
529
532
|
triggerTurn: true,
|
|
533
|
+
usage: sumUsage(chain.map((step) => step.result.usage)),
|
|
530
534
|
},
|
|
531
535
|
]);
|
|
532
536
|
runtime.completionBatcher.flush();
|
|
@@ -570,6 +574,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
570
574
|
agent: initialReviewerResult.agent,
|
|
571
575
|
block: `${formatCompletionBlock(initialReviewerResult, config.maxResultLines, executionCwd)}\n\nAuto-fix chain crashed before completion: ${errorMessage}. The planned fix rounds did not run; the review above is the triggering reviewer's full output.`,
|
|
572
576
|
triggerTurn: true,
|
|
577
|
+
usage: initialReviewerResult.usage,
|
|
573
578
|
},
|
|
574
579
|
]);
|
|
575
580
|
runtime.completionBatcher.flush();
|
package/src/fixloop.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
15
|
-
import { extractKeyFragments, formatUsageCompact } from "./monitor.ts";
|
|
15
|
+
import { extractKeyFragments, formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
16
16
|
import type { SubagentsConfig } from "./config.ts";
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -40,7 +40,7 @@ export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConf
|
|
|
40
40
|
/**
|
|
41
41
|
* Build the worker task brief for one fix round from a reviewer's findings.
|
|
42
42
|
* The worker gets the full review text so it can address concrete file:line
|
|
43
|
-
* issues, with instructions to fix
|
|
43
|
+
* issues, with instructions to fix every reported finding and self-verify.
|
|
44
44
|
*/
|
|
45
45
|
export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
|
|
46
46
|
const review = getResultOutput(reviewerResult);
|
|
@@ -53,8 +53,9 @@ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, m
|
|
|
53
53
|
review,
|
|
54
54
|
`---`,
|
|
55
55
|
``,
|
|
56
|
-
`Fix
|
|
57
|
-
`
|
|
56
|
+
`Fix EVERY finding in the reviewer's findings list — there is no severity triage; all of them get fixed.`,
|
|
57
|
+
`If a finding is factually wrong or clearly out of scope, say so explicitly instead of fixing it.`,
|
|
58
|
+
`Do NOT refactor unrelated code beyond what the findings require.`,
|
|
58
59
|
`After editing, run the project's format/build/tests when they exist and report`,
|
|
59
60
|
`exactly what you changed (paths + short rationale) so a reviewer can verify.`,
|
|
60
61
|
remaining > 0
|
|
@@ -120,18 +121,7 @@ export function formatChainSummary(steps: readonly ChainStep[]): string {
|
|
|
120
121
|
const id = step.runId !== undefined ? `#${step.runId} ` : "";
|
|
121
122
|
lines.push(`- ${id}${step.result.agent} · ${step.relation} · ${stepStatus(step)}${suffix}`);
|
|
122
123
|
}
|
|
123
|
-
const total = steps.
|
|
124
|
-
(acc, step) => {
|
|
125
|
-
acc.input += step.result.usage.input;
|
|
126
|
-
acc.output += step.result.usage.output;
|
|
127
|
-
acc.cacheRead += step.result.usage.cacheRead;
|
|
128
|
-
acc.cacheWrite += step.result.usage.cacheWrite;
|
|
129
|
-
acc.cost += step.result.usage.cost;
|
|
130
|
-
acc.turns += step.result.usage.turns;
|
|
131
|
-
return acc;
|
|
132
|
-
},
|
|
133
|
-
{ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
134
|
-
);
|
|
124
|
+
const total = sumUsage(steps.map((step) => step.result.usage));
|
|
135
125
|
const usage = formatUsageCompact(total);
|
|
136
126
|
lines.push("", `Totals: ${steps.length} run${steps.length === 1 ? "" : "s"}${usage ? ` · ${usage}` : ""}`);
|
|
137
127
|
const ids = steps.filter((step) => step.runId !== undefined).map((step) => `#${step.runId}`);
|
|
@@ -141,11 +131,14 @@ export function formatChainSummary(steps: readonly ChainStep[]): string {
|
|
|
141
131
|
|
|
142
132
|
/**
|
|
143
133
|
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
144
|
-
* the prior review
|
|
145
|
-
*
|
|
134
|
+
* the prior review AND the worker's report so the reviewer can adjudicate
|
|
135
|
+
* rejections instead of restating findings. The convergence contract keeps
|
|
136
|
+
* rounds from ping-ponging: rule on the open findings once, add only defects
|
|
137
|
+
* this round's edits introduced, never re-open a verified resolution.
|
|
146
138
|
*/
|
|
147
|
-
export function buildReReviewBrief(reviewerResult: SingleResult, round: number): string {
|
|
139
|
+
export function buildReReviewBrief(reviewerResult: SingleResult, round: number, workerResult: SingleResult): string {
|
|
148
140
|
const review = getResultOutput(reviewerResult);
|
|
141
|
+
const workerReport = getResultOutput(workerResult);
|
|
149
142
|
return [
|
|
150
143
|
`Re-review after auto-fix round ${round}.`,
|
|
151
144
|
``,
|
|
@@ -154,8 +147,18 @@ export function buildReReviewBrief(reviewerResult: SingleResult, round: number):
|
|
|
154
147
|
review,
|
|
155
148
|
`---`,
|
|
156
149
|
``,
|
|
157
|
-
`
|
|
158
|
-
|
|
150
|
+
`The worker's report (what it changed, plus any finding it rejected as factually wrong or out of scope):`,
|
|
151
|
+
`---`,
|
|
152
|
+
workerReport,
|
|
153
|
+
`---`,
|
|
154
|
+
``,
|
|
155
|
+
`Rule on EVERY previous finding: resolved, or still open. A finding the worker rejected must be`,
|
|
156
|
+
`adjudicated ONCE — accept the rejection unless you can concretely refute the worker's reasoning;`,
|
|
157
|
+
`never simply restate the finding for another round.`,
|
|
158
|
+
`Run \`git diff\` to see what changed, then add NEW findings only when they are defects this round's`,
|
|
159
|
+
`edits introduced or exposed (or a load-bearing issue the earlier review genuinely missed).`,
|
|
160
|
+
`Do NOT re-open a finding you verified as resolved.`,
|
|
161
|
+
`REQUEST_CHANGES only while an open finding remains; otherwise APPROVE.`,
|
|
159
162
|
`End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
|
|
160
163
|
].join("\n");
|
|
161
164
|
}
|
package/src/format.ts
CHANGED
|
@@ -127,18 +127,15 @@ export function formatCompletionBlock(
|
|
|
127
127
|
lines.push(`Relation: ${relations.join(" · ")}`, "");
|
|
128
128
|
}
|
|
129
129
|
lines.push(text);
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
130
|
+
// Failed-tool diagnostics are deliberate opt-in via subagent_status: agents
|
|
131
|
+
// report their own verification in the output above, and a transient failed
|
|
132
|
+
// call (no-match grep, rejected edit) is noise in an automatic delivery.
|
|
133
133
|
if (options.failedToolDetails && failedTools.length > 0) {
|
|
134
134
|
lines.push(
|
|
135
135
|
"",
|
|
136
136
|
`⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}:`,
|
|
137
137
|
...failedTools.map((tool) => `- ${tool.toolName}: ${tool.error.trim() || "(no output)"}`),
|
|
138
138
|
);
|
|
139
|
-
} else if (!failed && failedTools.length > 0) {
|
|
140
|
-
const lookup = result.runId !== undefined ? ` · details: subagent_status #${result.runId}` : "";
|
|
141
|
-
lines.push("", `⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}${lookup}`);
|
|
142
139
|
}
|
|
143
140
|
if (truncated) {
|
|
144
141
|
// The full text lives on disk so the main agent can read it on demand.
|
package/src/monitor.ts
CHANGED
|
@@ -53,12 +53,15 @@ export interface RunView {
|
|
|
53
53
|
groupId?: string;
|
|
54
54
|
/** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
|
|
55
55
|
relationLabel?: string;
|
|
56
|
+
/** Owning run for chain children: the triggering reviewer whose row represents the chain. */
|
|
57
|
+
parentRunId?: number;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** Optional chain metadata for runs spawned by an auto-fix loop. */
|
|
59
61
|
export interface RunChainMeta {
|
|
60
62
|
groupId?: string;
|
|
61
63
|
relationLabel?: string;
|
|
64
|
+
parentRunId?: number;
|
|
62
65
|
isolation?: IsolationMode;
|
|
63
66
|
forkedFromRunId?: number;
|
|
64
67
|
}
|
|
@@ -248,6 +251,21 @@ export function formatUsageCompact(usage: UsageStats): string {
|
|
|
248
251
|
return parts.join(" ");
|
|
249
252
|
}
|
|
250
253
|
|
|
254
|
+
/** Aggregate usage across several runs (chain steps or a completion group). */
|
|
255
|
+
export function sumUsage(parts: readonly UsageStats[]): UsageStats {
|
|
256
|
+
const total: UsageStats = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
257
|
+
for (const part of parts) {
|
|
258
|
+
total.input += part.input;
|
|
259
|
+
total.output += part.output;
|
|
260
|
+
total.cacheRead += part.cacheRead;
|
|
261
|
+
total.cacheWrite += part.cacheWrite;
|
|
262
|
+
total.cost += part.cost;
|
|
263
|
+
total.contextTokens += part.contextTokens;
|
|
264
|
+
total.turns += part.turns;
|
|
265
|
+
}
|
|
266
|
+
return total;
|
|
267
|
+
}
|
|
268
|
+
|
|
251
269
|
export function formatDuration(ms: number): string {
|
|
252
270
|
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
253
271
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
@@ -393,6 +411,7 @@ export class MonitorStore {
|
|
|
393
411
|
usage: emptyUsage(),
|
|
394
412
|
...(meta?.groupId ? { groupId: meta.groupId } : {}),
|
|
395
413
|
...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
|
|
414
|
+
...(meta?.parentRunId !== undefined ? { parentRunId: meta.parentRunId } : {}),
|
|
396
415
|
...(meta?.isolation ? { isolation: meta.isolation, integrationStatus: meta.isolation === "worktree" ? "pending" : undefined } : {}),
|
|
397
416
|
...(meta?.forkedFromRunId !== undefined ? { forkedFromRunId: meta.forkedFromRunId } : {}),
|
|
398
417
|
});
|
package/src/prompt.ts
CHANGED
|
@@ -78,7 +78,7 @@ Result handoff (do not re-state):
|
|
|
78
78
|
|
|
79
79
|
Review & verification:
|
|
80
80
|
- Never report an unrun check as passed; report it as unavailable or as a pre-existing failure.
|
|
81
|
-
${hasReviewer ? `- For non-trivial diffs${hasCleaner ? " (including cleaner edits)" : ""}, run one fresh read-only \`reviewer\` sub-agent before reporting done. Fix
|
|
81
|
+
${hasReviewer ? `- For non-trivial diffs${hasCleaner ? " (including cleaner edits)" : ""}, run one fresh read-only \`reviewer\` sub-agent before reporting done. Fix every finding the reviewer reports and re-review at most once.
|
|
82
82
|
- Use multi-model cross-review only when explicitly requested or for genuinely high-risk changes (security, unsafe/FFI, persistence-migration, concurrency). Reviewers are read-only; only the main agent edits.
|
|
83
|
-
` : ""}- Commit or push only when explicitly requested, applicable checks pass, and no
|
|
83
|
+
` : ""}- Commit or push only when explicitly requested, applicable checks pass, and no unresolved review findings remain.`;
|
|
84
84
|
}
|
package/src/thread-lifecycle.ts
CHANGED
|
@@ -966,6 +966,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
966
966
|
? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
|
|
967
967
|
: formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
|
|
968
968
|
triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
|
|
969
|
+
usage: result.usage,
|
|
969
970
|
};
|
|
970
971
|
if (modelLevel) {
|
|
971
972
|
const detail = result.errorMessage?.trim() || "model unavailable or broken";
|
|
@@ -1040,6 +1041,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
1040
1041
|
agent: agent.name,
|
|
1041
1042
|
block: formatCompletionBlock(crashed, runConfig.maxResultLines, crashed.projectCwd ?? originalCwd),
|
|
1042
1043
|
triggerTurn: true,
|
|
1044
|
+
usage: crashed.usage,
|
|
1043
1045
|
},
|
|
1044
1046
|
]);
|
|
1045
1047
|
runtime.completionBatcher.flush();
|
package/src/tools.ts
CHANGED
|
@@ -695,6 +695,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
695
695
|
agent: result.agent,
|
|
696
696
|
block: formatCompletionBlock(result, maxResultLines, result.projectCwd ?? ctx.cwd),
|
|
697
697
|
triggerTurn: true,
|
|
698
|
+
usage: result.usage,
|
|
698
699
|
})));
|
|
699
700
|
runtime.completionBatcher.flush();
|
|
700
701
|
}
|
package/src/widget.ts
CHANGED
|
@@ -29,65 +29,107 @@ function compactLine(left: string, right: string, width: number): string {
|
|
|
29
29
|
* activity line. The primary line reserves effective model/thinking and elapsed
|
|
30
30
|
* width before truncating the task. Settled and parked threads never appear, so
|
|
31
31
|
* elapsed time cannot keep ticking beside a terminal status. */
|
|
32
|
+
function runPrimaryLine(
|
|
33
|
+
run: RunView,
|
|
34
|
+
theme: Theme,
|
|
35
|
+
width: number,
|
|
36
|
+
now: number,
|
|
37
|
+
prefix: string,
|
|
38
|
+
): string {
|
|
39
|
+
const dim = (text: string): string => theme.fg("dim", text);
|
|
40
|
+
const icon = statusIcon(run.status, theme);
|
|
41
|
+
const name = theme.fg("accent", theme.bold(run.agent));
|
|
42
|
+
const identity = `${prefix}${icon} ${name}`;
|
|
43
|
+
const elapsed = formatElapsed(run, now);
|
|
44
|
+
// Render only the resolved model id plus thinking level. Provider auth and
|
|
45
|
+
// other configuration never enter monitor state or this line.
|
|
46
|
+
const modelId = run.model?.split("/").at(-1);
|
|
47
|
+
const modelSource = formatTaskSummary(
|
|
48
|
+
modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
|
|
49
|
+
64,
|
|
50
|
+
false,
|
|
51
|
+
);
|
|
52
|
+
// A chain child shows its role in the chain plus a task-derived label; the
|
|
53
|
+
// templated fix brief itself would only repeat the parent review's content.
|
|
54
|
+
const taskSource = run.parentRunId !== undefined
|
|
55
|
+
? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(" · ")
|
|
56
|
+
: formatTaskSummary(run.task, 64);
|
|
57
|
+
const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
|
|
58
|
+
const contentWidth = Math.max(
|
|
59
|
+
0,
|
|
60
|
+
width -
|
|
61
|
+
visibleWidth(identity) -
|
|
62
|
+
visibleWidth(elapsed) -
|
|
63
|
+
(primaryPartCount - 1) * visibleWidth(" · "),
|
|
64
|
+
);
|
|
65
|
+
const modelDesired = visibleWidth(modelSource);
|
|
66
|
+
const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
|
|
67
|
+
let modelWidth = modelSource
|
|
68
|
+
? Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8))
|
|
69
|
+
: 0;
|
|
70
|
+
let taskWidth = contentWidth - modelWidth;
|
|
71
|
+
if (visibleWidth(taskSource) < taskWidth) {
|
|
72
|
+
modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskSource));
|
|
73
|
+
taskWidth = contentWidth - modelWidth;
|
|
74
|
+
}
|
|
75
|
+
const task = taskWidth > 0 ? formatTaskSummary(taskSource, taskWidth, run.parentRunId === undefined) : "";
|
|
76
|
+
const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
|
|
77
|
+
const primaryLeft = [
|
|
78
|
+
identity,
|
|
79
|
+
task ? dim(task) : undefined,
|
|
80
|
+
modelThinking ? dim(modelThinking) : undefined,
|
|
81
|
+
].filter((part): part is string => Boolean(part)).join(" · ");
|
|
82
|
+
return compactLine(primaryLeft, elapsed ? dim(`· ${elapsed}`) : "", width);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runActivityLine(run: RunView, theme: Theme, width: number, indent: string): string[] {
|
|
86
|
+
const dim = (text: string): string => theme.fg("dim", text);
|
|
87
|
+
const activity = run.activity?.trim();
|
|
88
|
+
if (!activity) return [];
|
|
89
|
+
const activityWidth = width - visibleWidth(indent);
|
|
90
|
+
if (activityWidth <= 0) return [];
|
|
91
|
+
const activitySummary = formatTaskSummary(activity, activityWidth);
|
|
92
|
+
if (!activitySummary) return [];
|
|
93
|
+
return [truncateToWidth(`${indent}${dim(activitySummary)}`, width, "")];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Render active runs as a tree: main-agent dispatches are roots, auto-fix chain
|
|
97
|
+
* rounds nest under the triggering reviewer row that owns the chain. No run ids
|
|
98
|
+
* appear here — the tree and the task label say what each row is, and ids stay
|
|
99
|
+
* available through subagent_status when a thread must be controlled. */
|
|
32
100
|
export function formatActiveRunLines(
|
|
33
101
|
runs: readonly RunView[],
|
|
34
102
|
theme: Theme,
|
|
35
103
|
width: number,
|
|
36
104
|
now: number = Date.now(),
|
|
37
105
|
): string[] {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
63
|
-
const modelDesired = visibleWidth(modelSource);
|
|
64
|
-
const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
|
|
65
|
-
let modelWidth = modelSource
|
|
66
|
-
? Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8))
|
|
67
|
-
: 0;
|
|
68
|
-
let taskWidth = contentWidth - modelWidth;
|
|
69
|
-
if (visibleWidth(taskSource) < taskWidth) {
|
|
70
|
-
modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskSource));
|
|
71
|
-
taskWidth = contentWidth - modelWidth;
|
|
72
|
-
}
|
|
73
|
-
const task = taskWidth > 0 ? formatTaskSummary(taskSource, taskWidth) : "";
|
|
74
|
-
const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
|
|
75
|
-
const primaryLeft = [
|
|
76
|
-
identity,
|
|
77
|
-
task ? dim(task) : undefined,
|
|
78
|
-
modelThinking ? dim(modelThinking) : undefined,
|
|
79
|
-
].filter((part): part is string => Boolean(part)).join(" · ");
|
|
80
|
-
const primary = compactLine(primaryLeft, elapsed ? dim(`· ${elapsed}`) : "", width);
|
|
81
|
-
|
|
82
|
-
const activity = run.activity?.trim();
|
|
83
|
-
if (!activity) return [primary];
|
|
84
|
-
const activityIndent = " ";
|
|
85
|
-
const activityWidth = width - visibleWidth(activityIndent);
|
|
86
|
-
if (activityWidth <= 0) return [primary];
|
|
87
|
-
const activitySummary = formatTaskSummary(activity, activityWidth);
|
|
88
|
-
if (!activitySummary) return [primary];
|
|
89
|
-
return [primary, truncateToWidth(`${activityIndent}${dim(activitySummary)}`, width, "")];
|
|
106
|
+
const active = runs.filter((run) => isRunActiveStatus(run.status));
|
|
107
|
+
const activeIds = new Set(active.map((run) => run.id));
|
|
108
|
+
const childrenOf = new Map<number, RunView[]>();
|
|
109
|
+
const roots: RunView[] = [];
|
|
110
|
+
for (const run of active) {
|
|
111
|
+
if (run.parentRunId !== undefined && activeIds.has(run.parentRunId)) {
|
|
112
|
+
const siblings = childrenOf.get(run.parentRunId);
|
|
113
|
+
if (siblings) siblings.push(run);
|
|
114
|
+
else childrenOf.set(run.parentRunId, [run]);
|
|
115
|
+
} else {
|
|
116
|
+
roots.push(run);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
for (const root of roots) {
|
|
121
|
+
const children = childrenOf.get(root.id) ?? [];
|
|
122
|
+
lines.push(runPrimaryLine(root, theme, width, now, ""));
|
|
123
|
+
// The parent's "auto-fix chain running" placeholder is redundant while its
|
|
124
|
+
// child rows show live progress; keep it only between rounds.
|
|
125
|
+
if (children.length === 0) lines.push(...runActivityLine(root, theme, width, " "));
|
|
126
|
+
children.forEach((child, index) => {
|
|
127
|
+
const connector = index === children.length - 1 ? "└ " : "├ ";
|
|
128
|
+
lines.push(runPrimaryLine(child, theme, width, now, theme.fg("dim", ` ${connector}`)));
|
|
129
|
+
lines.push(...runActivityLine(child, theme, width, " "));
|
|
90
130
|
});
|
|
131
|
+
}
|
|
132
|
+
return lines;
|
|
91
133
|
}
|
|
92
134
|
|
|
93
135
|
function hasTickingRun(): boolean {
|