@ferris1225/pi-subagents 0.26.2 → 0.28.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 +38 -19
- package/package.json +1 -1
- package/src/fixloop.ts +93 -0
- package/src/index.ts +87 -59
- package/src/monitor.ts +14 -0
- package/src/prompt.ts +7 -3
package/README.md
CHANGED
|
@@ -18,16 +18,21 @@ report their results back to the main agent automatically.
|
|
|
18
18
|
agent automatically: injected as soon as the current tool call finishes (even
|
|
19
19
|
mid-turn), or starting a new turn when idle. No polling, no "go check" step.
|
|
20
20
|
- **Sub-agent toolbelt** — three companion tools that replace the classic
|
|
21
|
-
sleep/poll anti-pattern: `subagent_wait`
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
sleep/poll anti-pattern: `subagent_wait` looks results up in-turn — **non-blocking
|
|
22
|
+
by default** (a settled run returns its result immediately, a still-active run
|
|
23
|
+
tells the model to end its turn and wait for the wake-up message; pass
|
|
24
|
+
`timeoutMs` to block) — `subagent_status` inspects active and finished runs, and
|
|
25
|
+
`subagent_stop` cancels a run (delivering its partial output as an aborted result).
|
|
24
26
|
- **Honest completions** — a run that exited cleanly but whose tool calls failed
|
|
25
27
|
(e.g. a broken build) is reported as `completed with N failed tool call(s)`
|
|
26
28
|
with the errors attached, so a rosy final text can never hide a failure.
|
|
27
29
|
- **Parallel fan-out** — independent tasks run at the same time, with a configurable
|
|
28
30
|
concurrency limit.
|
|
29
31
|
- **Live progress** — a TUI widget shows each run's status, current activity, model,
|
|
30
|
-
token usage (input/output and cache read/write), and elapsed time.
|
|
32
|
+
token usage (input/output and cache read/write), and elapsed time. Auto-fix chain
|
|
33
|
+
rounds hang under their triggering review as a tree, and each finished round stays
|
|
34
|
+
visible with a one-line outcome (what a worker changed, or a re-review's
|
|
35
|
+
PASS/FAIL and what it found) until the chain resolves.
|
|
31
36
|
- **Per-agent configuration** — enable agents, choose a model and thinking level per
|
|
32
37
|
agent, and tune limits from `/subagents-setup`.
|
|
33
38
|
- **Automatic model fallback** — if an agent's model fails at the provider level before
|
|
@@ -52,9 +57,10 @@ Several tools now offer some form of sub-agents. What this extension does differ
|
|
|
52
57
|
- **Results come back on their own.** The extension turns the child's completion
|
|
53
58
|
into a message that wakes the main agent automatically — delivered even
|
|
54
59
|
mid-turn, right after the current tool call. No polling, no "go check
|
|
55
|
-
the other window" step, and **no `sleep
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
the other window" step, and **no `sleep`** and no waiting: the model ends its
|
|
61
|
+
turn and the result wakes it. A settled result can be fetched in-turn with
|
|
62
|
+
`subagent_wait` (a non-blocking lookup by default; `timeoutMs` opts into
|
|
63
|
+
blocking) instead of sleeping or polling.
|
|
58
64
|
- **Failures are handled, not reported.** Three layers of resilience: a provider-
|
|
59
65
|
level model failure first retries the same model up to five times on a transient
|
|
60
66
|
provider error, then retries once with the main window's model; terminal errors
|
|
@@ -64,8 +70,9 @@ Several tools now offer some form of sub-agents. What this extension does differ
|
|
|
64
70
|
tell you when any of these happened.
|
|
65
71
|
- **A quality gate that closes the loop.** When a reviewer returns `REVIEW_FAIL`,
|
|
66
72
|
the extension dispatches a worker briefed with the concrete findings, then a
|
|
67
|
-
re-review — up to `maxFixRounds` times — and only then wakes the main agent
|
|
68
|
-
|
|
73
|
+
re-review — up to `maxFixRounds` times — and only then wakes the main agent.
|
|
74
|
+
The gate runs itself instead of asking you to babysit it, and the widget shows
|
|
75
|
+
every round as it happens instead of a black box.
|
|
69
76
|
- **Honest results.** A sub-agent can end its turn with "still working" while its
|
|
70
77
|
last build actually failed. The completion message surfaces the failed tool
|
|
71
78
|
calls from the run's final attempt (`completed with N failed tool call(s)`) with
|
|
@@ -74,7 +81,12 @@ Several tools now offer some form of sub-agents. What this extension does differ
|
|
|
74
81
|
attempt's tool calls are counted — never stale errors from an abandoned one.)
|
|
75
82
|
- **You can see what it is doing.** The widget shows each run's status, current
|
|
76
83
|
activity (which tool, which file), model, token usage including cache reads and
|
|
77
|
-
writes, and elapsed time — plus soft warnings when a run looks stuck.
|
|
84
|
+
writes, and elapsed time — plus soft warnings when a run looks stuck. When a
|
|
85
|
+
chain finishes, the delivered message is one condensed summary (one line per
|
|
86
|
+
round: verdict + what changed/found, plus aggregate usage) instead of every
|
|
87
|
+
round's raw output stacked together; the final round's full report is attached
|
|
88
|
+
only when its detail is actionable (a FAIL verdict or a crash), and any round's
|
|
89
|
+
full report stays one `subagent_status <id>` call away.
|
|
78
90
|
- **Recursion is structurally impossible.** Children are leaf processes: the
|
|
79
91
|
`subagent` tool is excluded from their toolset. No runaway delegation trees.
|
|
80
92
|
- **Zero runtime dependencies.** It is a plain pi extension — install, configure,
|
|
@@ -324,10 +336,13 @@ main agent
|
|
|
324
336
|
The extension registers three companion tools so the main agent never has to
|
|
325
337
|
`sleep`/poll for a background run:
|
|
326
338
|
|
|
327
|
-
- `subagent_wait` —
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
339
|
+
- `subagent_wait` — looks up a run's result in-turn. It does **not block by
|
|
340
|
+
default**: a settled run returns its result immediately; a still-active run
|
|
341
|
+
returns a note telling the model to end its turn (the completion message then
|
|
342
|
+
wakes it). Pass `timeoutMs` to block inside the tool call (event-driven, wakes
|
|
343
|
+
on the run's completion) — only when the current turn must receive the result
|
|
344
|
+
right now (sequential dependent steps). Otherwise end the turn and the
|
|
345
|
+
completion message wakes you.
|
|
331
346
|
- `subagent_status` — lists active runs (id, agent, model, usage, elapsed, activity)
|
|
332
347
|
and finished results; pass an id to read a finished run's full result.
|
|
333
348
|
- `subagent_stop` — cancels an active run (or `all: true`); the child is terminated
|
|
@@ -376,9 +391,11 @@ Start dependent work only after the relevant result has been delivered.
|
|
|
376
391
|
|
|
377
392
|
### Waiting for a result in-turn
|
|
378
393
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
394
|
+
Results arrive as messages that wake the main agent automatically, so waiting is
|
|
395
|
+
usually unnecessary: end your turn and the result resumes you. When a result must
|
|
396
|
+
be fetched in-turn, `subagent_wait` is a **non-blocking lookup by default** — a
|
|
397
|
+
settled run returns its result immediately, a still-active run returns a note
|
|
398
|
+
telling the model to end its turn:
|
|
382
399
|
|
|
383
400
|
```json
|
|
384
401
|
{
|
|
@@ -386,8 +403,10 @@ When the next step depends on a run's result and the turn must not end, use
|
|
|
386
403
|
}
|
|
387
404
|
```
|
|
388
405
|
|
|
389
|
-
|
|
390
|
-
|
|
406
|
+
Only when the turn must not end AND the result is needed right now (e.g. the user
|
|
407
|
+
asked for it) pass `timeoutMs` to block; on timeout it reports the still-running
|
|
408
|
+
runs and the model ends the turn (the completion message then wakes it) or
|
|
409
|
+
re-invokes with a longer timeout.
|
|
391
410
|
|
|
392
411
|
### Inspecting runs
|
|
393
412
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/fixloop.ts
CHANGED
|
@@ -12,6 +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
16
|
import type { SubagentsConfig } from "./config.ts";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -62,6 +63,98 @@ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, m
|
|
|
62
63
|
].join("\n");
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* One step of an auto-fix chain as delivered: the run id (so the condensed
|
|
68
|
+
* summary can point at per-run detail via subagent_status), the result, and
|
|
69
|
+
* the human-readable role within the chain ("initial review", "fix round 1",
|
|
70
|
+
* "re-review round 2"). runId is undefined for steps that never spawned a run
|
|
71
|
+
* (e.g. an unknown agent).
|
|
72
|
+
*/
|
|
73
|
+
export interface ChainStep {
|
|
74
|
+
runId?: number;
|
|
75
|
+
result: SingleResult;
|
|
76
|
+
relation: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Max distinguishing fragments kept in a one-line chain summary. */
|
|
80
|
+
export const CHAIN_SUMMARY_FRAGMENTS_MAX = 3;
|
|
81
|
+
|
|
82
|
+
/** The most telling fragments (paths, quoted phrases, symbols) of a run's final
|
|
83
|
+
* output: for a worker these are the paths it changed, for a reviewer the
|
|
84
|
+
* issues it found. Capped so summaries stay one line. */
|
|
85
|
+
export function chainKeyFragments(result: SingleResult): string[] {
|
|
86
|
+
return extractKeyFragments(getResultOutput(result)).slice(0, CHAIN_SUMMARY_FRAGMENTS_MAX);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compact one-line outcome for a finished chain run, shown in the widget so
|
|
91
|
+
* each round reads as what it did: a reviewer reports its verdict plus the
|
|
92
|
+
* key fragments of what it found ("fail · src/index.ts · render()"), a worker
|
|
93
|
+
* the fragments of what it changed. Failed runs and runs with nothing
|
|
94
|
+
* distinctive get no summary.
|
|
95
|
+
*/
|
|
96
|
+
export function summarizeChainResult(result: SingleResult): string | undefined {
|
|
97
|
+
if (isFailedResult(result)) return undefined;
|
|
98
|
+
const verdict = result.agent === "reviewer" ? reviewVerdict(getResultOutput(result)) : undefined;
|
|
99
|
+
if (verdict === "pass") return "pass";
|
|
100
|
+
const fragments = chainKeyFragments(result);
|
|
101
|
+
if (verdict === "fail") return fragments.length > 0 ? `fail · ${fragments.join(" · ")}` : "fail";
|
|
102
|
+
return fragments.length > 0 ? fragments.join(" · ") : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Condensed, readable summary of a completed auto-fix chain: one line per step
|
|
107
|
+
* (run id, role, verdict / what changed) plus aggregate usage. Full per-step
|
|
108
|
+
* reports stay addressable via `subagent_status <id>`; the caller appends the
|
|
109
|
+
* final step's full block only when its detail is actionable (FAIL verdict or a
|
|
110
|
+
* crash), so the delivered message stays short instead of stacking every
|
|
111
|
+
* round's raw output.
|
|
112
|
+
*/
|
|
113
|
+
export function formatChainSummary(steps: readonly ChainStep[]): string {
|
|
114
|
+
const rounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
|
|
115
|
+
const last = steps[steps.length - 1];
|
|
116
|
+
const stepStatus = (step: ChainStep): string => {
|
|
117
|
+
const { result } = step;
|
|
118
|
+
if (result.agent === "reviewer") {
|
|
119
|
+
const verdict = reviewVerdict(getResultOutput(result));
|
|
120
|
+
if (verdict) return verdict.toUpperCase();
|
|
121
|
+
}
|
|
122
|
+
return isFailedResult(result) ? "failed" : "completed";
|
|
123
|
+
};
|
|
124
|
+
const lines = [
|
|
125
|
+
`## Auto-fix chain: ${Math.max(1, rounds)} round${rounds === 1 ? "" : "s"} — final ${stepStatus(last)}`,
|
|
126
|
+
"",
|
|
127
|
+
];
|
|
128
|
+
for (const step of steps) {
|
|
129
|
+
const fragments = chainKeyFragments(step.result);
|
|
130
|
+
const suffix =
|
|
131
|
+
fragments.length > 0
|
|
132
|
+
? step.result.agent === "worker"
|
|
133
|
+
? ` — changed: ${fragments.join(" · ")}`
|
|
134
|
+
: ` — ${fragments.join(" · ")}`
|
|
135
|
+
: "";
|
|
136
|
+
const id = step.runId !== undefined ? `#${step.runId} ` : "";
|
|
137
|
+
lines.push(`- ${id}${step.result.agent} · ${step.relation} · ${stepStatus(step)}${suffix}`);
|
|
138
|
+
}
|
|
139
|
+
const total = steps.reduce(
|
|
140
|
+
(acc, step) => {
|
|
141
|
+
acc.input += step.result.usage.input;
|
|
142
|
+
acc.output += step.result.usage.output;
|
|
143
|
+
acc.cacheRead += step.result.usage.cacheRead;
|
|
144
|
+
acc.cacheWrite += step.result.usage.cacheWrite;
|
|
145
|
+
acc.cost += step.result.usage.cost;
|
|
146
|
+
acc.turns += step.result.usage.turns;
|
|
147
|
+
return acc;
|
|
148
|
+
},
|
|
149
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
150
|
+
);
|
|
151
|
+
const usage = formatUsageCompact(total);
|
|
152
|
+
lines.push("", `Totals: ${steps.length} run${steps.length === 1 ? "" : "s"}${usage ? ` · ${usage}` : ""}`);
|
|
153
|
+
const ids = steps.filter((step) => step.runId !== undefined).map((step) => `#${step.runId}`);
|
|
154
|
+
lines.push(`Full per-run reports (output, usage, failed tools): subagent_status ${ids.join(" ")}`);
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
65
158
|
/**
|
|
66
159
|
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
67
160
|
* the prior review so the reviewer can verify the fixes without re-discovering
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
type SubagentLiveEvent,
|
|
43
43
|
type UsageStats,
|
|
44
44
|
} from "./spawn.ts";
|
|
45
|
-
import { buildFixTaskBrief, buildReReviewBrief, shouldTriggerFixLoop } from "./fixloop.ts";
|
|
45
|
+
import { buildFixTaskBrief, buildReReviewBrief, formatChainSummary, shouldTriggerFixLoop, summarizeChainResult, type ChainStep } from "./fixloop.ts";
|
|
46
46
|
import {
|
|
47
47
|
activityStateLabel,
|
|
48
48
|
compactLine,
|
|
@@ -310,7 +310,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
310
310
|
"Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
|
|
311
311
|
"It starts agents in the background and immediately returns control to the main window; completion messages automatically wake the main agent to continue.",
|
|
312
312
|
"Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output).",
|
|
313
|
-
"
|
|
313
|
+
"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)."
|
|
314
314
|
].join(" "),
|
|
315
315
|
promptSnippet:
|
|
316
316
|
"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.",
|
|
@@ -321,8 +321,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
321
321
|
"Use subagent with agent 'reviewer' for a fresh read-only review before reporting work done or committing.",
|
|
322
322
|
"subagent launches work in the background and ends the current turn; when a result arrives, the main agent is automatically resumed with it.",
|
|
323
323
|
"Run independent tasks in parallel by passing a tasks array to subagent; let the automatically resumed main agent start dependent work after results arrive.",
|
|
324
|
-
"NEVER sleep
|
|
325
|
-
"If you must keep the turn for a result, call subagent_wait
|
|
324
|
+
"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.",
|
|
325
|
+
"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.",
|
|
326
326
|
],
|
|
327
327
|
parameters: SubagentParams,
|
|
328
328
|
|
|
@@ -469,9 +469,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
469
469
|
task: string,
|
|
470
470
|
signal: AbortSignal,
|
|
471
471
|
meta: RunChainMeta,
|
|
472
|
-
): Promise<SingleResult> => {
|
|
472
|
+
): Promise<{ runId?: number; result: SingleResult }> => {
|
|
473
473
|
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
474
|
-
if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
474
|
+
if (!agent) return { result: failedStartResult(agentName, task, `Unknown agent: "${agentName}".`) };
|
|
475
475
|
const thinkingLevel = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? config.thinkingLevel;
|
|
476
476
|
const runId = monitor.addRun(agent.name, task, agent.model, thinkingLevel, meta);
|
|
477
477
|
const onLive = makeLiveHandler(runId);
|
|
@@ -490,11 +490,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
490
490
|
},
|
|
491
491
|
sessionRef,
|
|
492
492
|
);
|
|
493
|
-
|
|
493
|
+
// Keep the finished round visible in the widget while the chain is
|
|
494
|
+
// still running, with a one-line summary of what it did; the whole
|
|
495
|
+
// group is dropped when the chain resolves (see removeChainGroup).
|
|
496
|
+
monitor.setSummary(runId, summarizeChainResult(result));
|
|
497
|
+
finishRun(runId, isFailedResult(result) ? "failed" : "done", { retain: true });
|
|
494
498
|
registerRunResult(runId, result);
|
|
495
|
-
return result;
|
|
499
|
+
return { runId, result };
|
|
496
500
|
} catch (error) {
|
|
497
|
-
finishRun(runId, "failed");
|
|
501
|
+
finishRun(runId, "failed", { retain: true });
|
|
498
502
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
499
503
|
const crashed = {
|
|
500
504
|
...queuedResult(agent, task, thinkingLevel),
|
|
@@ -505,7 +509,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
505
509
|
dispatchFailed: true,
|
|
506
510
|
};
|
|
507
511
|
registerRunResult(runId, crashed);
|
|
508
|
-
return crashed;
|
|
512
|
+
return { runId, result: crashed };
|
|
509
513
|
}
|
|
510
514
|
};
|
|
511
515
|
|
|
@@ -517,73 +521,90 @@ export default function (pi: ExtensionAPI): void {
|
|
|
517
521
|
* The triggering reviewer's run stays visible in the widget (annotated) until
|
|
518
522
|
* the chain resolves, so the ↳ rows have an obvious parent.
|
|
519
523
|
*/
|
|
524
|
+
/** Drop every widget row belonging to an auto-fix chain; the retained
|
|
525
|
+
* parent row is removed separately (it does not carry the groupId). */
|
|
526
|
+
const removeChainGroup = (groupId: string): void => {
|
|
527
|
+
for (const run of [...monitor.getRuns()]) {
|
|
528
|
+
if (run.groupId === groupId) monitor.removeRun(run.id);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
520
532
|
const startFixLoop = (initialReviewerResult: SingleResult, parentGroupId: string, parentRunId: number): void => {
|
|
521
533
|
runControllers.set(parentRunId, backgroundQueue.enqueue(
|
|
522
534
|
async (signal) => {
|
|
523
|
-
const chain:
|
|
535
|
+
const chain: ChainStep[] = [
|
|
536
|
+
{ runId: parentRunId, result: initialReviewerResult, relation: "initial review" },
|
|
537
|
+
];
|
|
524
538
|
let lastReviewer = initialReviewerResult;
|
|
525
539
|
for (let round = 1; round <= config.maxFixRounds; round++) {
|
|
526
540
|
if (!sessionActive) break;
|
|
527
541
|
const fixBrief = buildFixTaskBrief(lastReviewer, round, config.maxFixRounds);
|
|
528
|
-
const
|
|
542
|
+
const workerStep = await launchInLoop("worker", fixBrief, signal, {
|
|
529
543
|
groupId: parentGroupId,
|
|
530
544
|
relationLabel: `fix round ${round}`,
|
|
531
545
|
});
|
|
532
|
-
chain.push(
|
|
533
|
-
if (!sessionActive || isFailedResult(
|
|
546
|
+
chain.push({ ...workerStep, relation: `fix round ${round}` });
|
|
547
|
+
if (!sessionActive || isFailedResult(workerStep.result)) break;
|
|
534
548
|
const reReviewBrief = buildReReviewBrief(lastReviewer, round);
|
|
535
|
-
const
|
|
549
|
+
const reviewStep = await launchInLoop("reviewer", reReviewBrief, signal, {
|
|
536
550
|
groupId: parentGroupId,
|
|
537
551
|
relationLabel: `re-review round ${round}`,
|
|
538
552
|
});
|
|
539
|
-
chain.push(
|
|
540
|
-
lastReviewer =
|
|
553
|
+
chain.push({ ...reviewStep, relation: `re-review round ${round}` });
|
|
554
|
+
lastReviewer = reviewStep.result;
|
|
541
555
|
// A crashed re-review must stop the chain like a crashed worker: its
|
|
542
556
|
// output (if any) is not a verdict, and feeding it to the next fix
|
|
543
557
|
// round would brief the worker from garbage.
|
|
544
|
-
if (!sessionActive || isFailedResult(
|
|
545
|
-
if (reviewVerdict(getResultOutput(
|
|
558
|
+
if (!sessionActive || isFailedResult(reviewStep.result)) break;
|
|
559
|
+
if (reviewVerdict(getResultOutput(reviewStep.result)) === "pass") break;
|
|
546
560
|
}
|
|
547
561
|
// The chain is done (success, exhaustion, or abort): drop the retained
|
|
548
|
-
// parent row
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
// resolve it.
|
|
553
|
-
registerRunResult(parentRunId, chain[chain.length - 1]);
|
|
562
|
+
// parent row and its retained round rows, then deliver one condensed
|
|
563
|
+
// summary. Register the parent's final state (the last chain result)
|
|
564
|
+
// before removal so subagent_wait can resolve it.
|
|
565
|
+
registerRunResult(parentRunId, chain[chain.length - 1].result);
|
|
554
566
|
runControllers.delete(parentRunId);
|
|
567
|
+
removeChainGroup(parentGroupId);
|
|
555
568
|
monitor.removeRun(parentRunId);
|
|
556
569
|
if (!sessionActive) return;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
570
|
+
// One compact message instead of every round's raw output: the summary
|
|
571
|
+
// lines cover each step (verdict + what changed/found), and the final
|
|
572
|
+
// step's full report is appended only when its detail is actionable
|
|
573
|
+
// (a FAIL verdict, a crash, or a model-level failure the main agent
|
|
574
|
+
// must take over). Everything else stays one `subagent_status #id`
|
|
575
|
+
// call away.
|
|
576
|
+
const last = chain[chain.length - 1];
|
|
577
|
+
let block = formatChainSummary(chain);
|
|
578
|
+
if (isFailedResult(last.result) && isModelLevelFailure(last.result)) {
|
|
579
|
+
block = `${block}\n\n${formatCompletionBlock(last.result, config.maxResultLines, ctx.cwd)}\n\n${modelLevelTakeoverNote(last.result)}`;
|
|
580
|
+
} else if (isFailedResult(last.result) || reviewVerdict(getResultOutput(last.result)) === "fail") {
|
|
581
|
+
block = `${block}\n\n${formatCompletionBlock(last.result, config.maxResultLines, ctx.cwd)}`;
|
|
582
|
+
}
|
|
583
|
+
sendCompletionGroup([
|
|
584
|
+
{
|
|
585
|
+
agent: `auto-fix chain (${last.result.agent})`,
|
|
586
|
+
block,
|
|
568
587
|
triggerTurn: true,
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
sendCompletionGroup(items);
|
|
588
|
+
},
|
|
589
|
+
]);
|
|
572
590
|
completionBatcher.flush();
|
|
573
591
|
},
|
|
574
592
|
() => {
|
|
575
|
-
// Cancelled before delivery: clean up the retained parent row
|
|
576
|
-
// in-flight chain run was already
|
|
593
|
+
// Cancelled before delivery: clean up the retained parent row and
|
|
594
|
+
// every retained chain row (each in-flight chain run was already
|
|
595
|
+
// finished by its launchInLoop path).
|
|
577
596
|
runControllers.delete(parentRunId);
|
|
597
|
+
removeChainGroup(parentGroupId);
|
|
578
598
|
monitor.removeRun(parentRunId);
|
|
579
599
|
},
|
|
580
600
|
(error) => {
|
|
581
601
|
// A crash inside the chain orchestration (failed runs are caught by
|
|
582
602
|
// launchInLoop and delivered as part of the chain) must not vanish:
|
|
583
|
-
// drop the retained
|
|
603
|
+
// drop the retained rows, notify, and deliver a failed result
|
|
584
604
|
// so the main agent knows the chain never completed.
|
|
585
605
|
registerRunResult(parentRunId, initialReviewerResult);
|
|
586
606
|
runControllers.delete(parentRunId);
|
|
607
|
+
removeChainGroup(parentGroupId);
|
|
587
608
|
monitor.removeRun(parentRunId);
|
|
588
609
|
if (!sessionActive) return;
|
|
589
610
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -842,13 +863,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
842
863
|
},
|
|
843
864
|
});
|
|
844
865
|
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
// for
|
|
851
|
-
const SUBAGENT_WAIT_DEFAULT_TIMEOUT_MS =
|
|
866
|
+
// In-turn result lookup. Dispatch already ended the turn and results arrive as
|
|
867
|
+
// wake-up messages, so the default must NOT block: a settled run returns its
|
|
868
|
+
// result immediately, a still-active run returns a "still running — end your
|
|
869
|
+
// turn" note and the model finishes (the completion then wakes it). Blocking
|
|
870
|
+
// is opt-in via an explicit timeoutMs — a long default would hold the turn
|
|
871
|
+
// hostage for nothing, since the result arrives on its own either way.
|
|
872
|
+
const SUBAGENT_WAIT_DEFAULT_TIMEOUT_MS = 0;
|
|
852
873
|
|
|
853
874
|
const SubagentWaitParams = Type.Object({
|
|
854
875
|
id: Type.Optional(
|
|
@@ -858,7 +879,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
858
879
|
),
|
|
859
880
|
timeoutMs: Type.Optional(
|
|
860
881
|
Type.Number({
|
|
861
|
-
|
|
882
|
+
description: `Block for up to this many milliseconds and report the still-running runs. Default ${SUBAGENT_WAIT_DEFAULT_TIMEOUT_MS}: no blocking — settled runs return their result immediately, active runs return a note telling the model to end its turn.`,
|
|
862
883
|
}),
|
|
863
884
|
),
|
|
864
885
|
});
|
|
@@ -867,18 +888,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
867
888
|
name: "subagent_wait",
|
|
868
889
|
label: "Subagent Wait",
|
|
869
890
|
description: [
|
|
870
|
-
"
|
|
871
|
-
"
|
|
872
|
-
"
|
|
891
|
+
"Look up background sub-agent run(s) and return their results.",
|
|
892
|
+
"PREFER NOT CALLING THIS: dispatching already ended your turn and results arrive as a message that wakes you automatically.",
|
|
893
|
+
"By default it does NOT block: a settled run returns its result immediately; a still-active run returns a 'still running — end your turn' note.",
|
|
894
|
+
"Pass an explicit timeoutMs ONLY when you must stay in the turn and need the result right now (sequential dependent steps).",
|
|
873
895
|
"NEVER sleep, poll, or wait with bash to get a sub-agent result: end the turn, or call this tool.",
|
|
874
896
|
"The same result is also delivered as a completion message that resumes the main agent, so you may see it twice (once here, once as a wake-up) — that is expected, not a duplicate.",
|
|
875
897
|
].join(" "),
|
|
876
|
-
promptSnippet: "
|
|
898
|
+
promptSnippet: "Look up a background subagent result in-turn (id: run id from the widget; omit for all). Non-blocking by default; pass timeoutMs to block.",
|
|
877
899
|
promptGuidelines: [
|
|
878
|
-
"
|
|
879
|
-
"
|
|
900
|
+
"Do NOT call subagent_wait to hold the turn: results arrive as wake-up messages automatically. The default call is a non-blocking lookup — settled results return immediately, active runs return a note telling you to end your turn.",
|
|
901
|
+
"Pass an explicit timeoutMs only when you must keep the turn AND the next step depends on the result right now — e.g. the user asked you to wait for it.",
|
|
880
902
|
"Never use bash sleep/timeout/polling to wait for a sub-agent — it blocks the turn and delays result delivery.",
|
|
881
|
-
"If subagent_wait times out,
|
|
903
|
+
"If subagent_wait times out, end the turn and wait for the wake-up message, or call it again with a longer timeoutMs.",
|
|
882
904
|
],
|
|
883
905
|
parameters: SubagentWaitParams,
|
|
884
906
|
|
|
@@ -978,7 +1000,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
978
1000
|
timer = setTimeout(
|
|
979
1001
|
() =>
|
|
980
1002
|
finish({
|
|
981
|
-
note:
|
|
1003
|
+
note:
|
|
1004
|
+
timeoutMs === 0
|
|
1005
|
+
? `run #${runId} is still active — end your turn: the result will wake you (or call subagent_wait again with an explicit timeoutMs to block)`
|
|
1006
|
+
: `wait timed out after ${Math.round(timeoutMs / 1000)}s — run #${runId} is still active; call subagent_wait again or end the turn (the result will wake you when ready)`,
|
|
982
1007
|
}),
|
|
983
1008
|
Math.max(1, timeoutMs),
|
|
984
1009
|
);
|
|
@@ -1307,7 +1332,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1307
1332
|
const usage = formatUsageCompact(r.usage);
|
|
1308
1333
|
const tools = r.toolCount ? `${r.toolCount} tool${r.toolCount === 1 ? "" : "s"}` : "";
|
|
1309
1334
|
const elapsed = formatElapsed(r, now);
|
|
1310
|
-
|
|
1335
|
+
// The round outcome summary leads the metadata so a finished chain
|
|
1336
|
+
// row reads as what it did ("fail · src/index.ts · render()",
|
|
1337
|
+
// "pass", "src/index.ts · tests/monitor.test.ts").
|
|
1338
|
+
const metaParts = [r.summary, model, usage, tools, elapsed].filter(Boolean);
|
|
1311
1339
|
// Running is conveyed by the icon + elapsed; spell out the label only for
|
|
1312
1340
|
// the other states (ready / done / stopped) so they are unambiguous.
|
|
1313
1341
|
if (r.status !== "running") metaParts.push(statusLabel(r.status));
|
package/src/monitor.ts
CHANGED
|
@@ -64,6 +64,11 @@ export interface RunView {
|
|
|
64
64
|
relationLabel?: string;
|
|
65
65
|
/** Free-form note shown in the widget next to the status label (e.g. "auto-fix chain running"). */
|
|
66
66
|
annotation?: string;
|
|
67
|
+
/** One-line outcome summary of a finished chain run, shown in the widget so
|
|
68
|
+
* each auto-fix round reads as what it did: a reviewer reports its verdict
|
|
69
|
+
* plus key fragments of what it found ("fail · src/index.ts · render()"), a
|
|
70
|
+
* worker the fragments of what it changed. Unset for non-chain runs. */
|
|
71
|
+
summary?: string;
|
|
67
72
|
/** True when a finished run is intentionally kept in the widget (e.g. an
|
|
68
73
|
* auto-fix chain parent whose chain is still running). beginTurn preserves
|
|
69
74
|
* retained runs so they are not swept between turns. */
|
|
@@ -472,6 +477,14 @@ export class MonitorStore {
|
|
|
472
477
|
this.notify();
|
|
473
478
|
}
|
|
474
479
|
|
|
480
|
+
/** Set the run's one-line outcome summary (what a finished chain round did). */
|
|
481
|
+
setSummary(id: number, text: string | undefined): void {
|
|
482
|
+
const run = this.find(id);
|
|
483
|
+
if (!run) return;
|
|
484
|
+
run.summary = text;
|
|
485
|
+
this.notify();
|
|
486
|
+
}
|
|
487
|
+
|
|
475
488
|
/** Mark a run as retained (kept in the widget despite being finished). */
|
|
476
489
|
setRetained(id: number, retained: boolean): void {
|
|
477
490
|
const run = this.find(id);
|
|
@@ -517,6 +530,7 @@ export class MonitorStore {
|
|
|
517
530
|
const usage = formatUsageCompact(run.usage);
|
|
518
531
|
const parts = [run.agent];
|
|
519
532
|
if (run.relationLabel) parts.push(run.relationLabel);
|
|
533
|
+
if (run.summary) parts.push(run.summary);
|
|
520
534
|
if (run.model) parts.push(run.model);
|
|
521
535
|
if (run.thinking) parts.push(`thinking ${run.thinking}`);
|
|
522
536
|
if (usage) parts.push(usage);
|
package/src/prompt.ts
CHANGED
|
@@ -41,9 +41,13 @@ It immediately ends the current main-agent turn so the user can keep working. Wh
|
|
|
41
41
|
finishes, its result is sent back as a message that automatically resumes the main agent;
|
|
42
42
|
if the main agent is busy, the result waits as a follow-up.
|
|
43
43
|
|
|
44
|
-
NEVER run sleep, wait, or polling commands (e.g. Start-Sleep, sleep, timeout)
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
NEVER run sleep, wait, or polling commands (e.g. Start-Sleep, sleep, timeout), and do NOT
|
|
45
|
+
call subagent_wait to hold the turn — dispatching already ended it, and results arrive as
|
|
46
|
+
messages that resume the main agent automatically (even mid-turn). Ending your turn is the
|
|
47
|
+
default and the only correct way to wait; subagent_wait blocks the turn so the user cannot
|
|
48
|
+
give you other work meanwhile. It is non-blocking by default: settled results return
|
|
49
|
+
immediately, active runs return a "still running — end your turn" note. Pass an explicit
|
|
50
|
+
timeoutMs only when you must stay in the turn (e.g. the user asked you to wait).
|
|
47
51
|
|
|
48
52
|
Available agents:
|
|
49
53
|
${catalog}
|