@ferris1225/pi-subagents 0.24.0 → 0.26.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 +579 -476
- package/package.json +1 -1
- package/src/background.ts +112 -106
- package/src/fixloop.ts +84 -76
- package/src/index.ts +503 -14
- package/src/monitor.ts +6 -1
- package/src/spawn.ts +171 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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/background.ts
CHANGED
|
@@ -1,106 +1,112 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bounded background task scheduler.
|
|
3
|
-
*
|
|
4
|
-
* Tasks get their own AbortSignal rather than inheriting the foreground agent
|
|
5
|
-
* turn's signal. The owning extension cancels all work only on session teardown.
|
|
6
|
-
*
|
|
7
|
-
* Task exceptions are never swallowed: the per-task onError callback receives
|
|
8
|
-
* them (unless the task was cancelled) so callers can surface the failure to
|
|
9
|
-
* the user and the main agent instead of it vanishing into the queue.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export type BackgroundTask = (signal: AbortSignal) => Promise<void>;
|
|
13
|
-
|
|
14
|
-
interface PendingTask {
|
|
15
|
-
task: BackgroundTask;
|
|
16
|
-
controller: AbortController;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Bounded background task scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Tasks get their own AbortSignal rather than inheriting the foreground agent
|
|
5
|
+
* turn's signal. The owning extension cancels all work only on session teardown.
|
|
6
|
+
*
|
|
7
|
+
* Task exceptions are never swallowed: the per-task onError callback receives
|
|
8
|
+
* them (unless the task was cancelled) so callers can surface the failure to
|
|
9
|
+
* the user and the main agent instead of it vanishing into the queue.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type BackgroundTask = (signal: AbortSignal) => Promise<void>;
|
|
13
|
+
|
|
14
|
+
interface PendingTask {
|
|
15
|
+
task: BackgroundTask;
|
|
16
|
+
controller: AbortController;
|
|
17
|
+
/** Called when a queued task is aborted before its body ever runs (drain skips
|
|
18
|
+
* an already-aborted entry; cancelAll aborts every pending entry), so the
|
|
19
|
+
* task body never produces a result. Callers that resolve waiters on a run id
|
|
20
|
+
* must register a synthetic result here (or via a stop path) — otherwise a
|
|
21
|
+
* waiter resolves via a "removed before its result was recorded" note. Never
|
|
22
|
+
* called for a task whose body already started; that path owns its result. */
|
|
23
|
+
onCancelled?: () => void;
|
|
24
|
+
/** Invoked when the task throws and was not cancelled (cancellation is not a
|
|
25
|
+
* failure — e.g. session shutdown races must never be reported as errors). */
|
|
26
|
+
onError?: (error: unknown) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class BackgroundTaskQueue {
|
|
30
|
+
private concurrency: number;
|
|
31
|
+
private readonly pending: PendingTask[] = [];
|
|
32
|
+
private readonly active = new Set<AbortController>();
|
|
33
|
+
private stopped = false;
|
|
34
|
+
|
|
35
|
+
constructor(concurrency: number) {
|
|
36
|
+
this.concurrency = Math.max(1, concurrency);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update the concurrency limit (e.g. after a config change). Raising it
|
|
41
|
+
* immediately starts more queued work; lowering it takes effect as running
|
|
42
|
+
* tasks finish — already-running tasks are never interrupted.
|
|
43
|
+
*/
|
|
44
|
+
setConcurrency(concurrency: number): void {
|
|
45
|
+
this.concurrency = Math.max(1, concurrency);
|
|
46
|
+
this.drain();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
enqueue(task: BackgroundTask, onCancelled?: () => void, onError?: (error: unknown) => void): AbortController {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
if (this.stopped) {
|
|
52
|
+
controller.abort();
|
|
53
|
+
this.runCancelled(onCancelled);
|
|
54
|
+
return controller;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.pending.push({ task, controller, onCancelled, onError });
|
|
58
|
+
this.drain();
|
|
59
|
+
return controller;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Stop queued work and request cancellation for running work. */
|
|
63
|
+
cancelAll(): void {
|
|
64
|
+
if (this.stopped) return;
|
|
65
|
+
this.stopped = true;
|
|
66
|
+
|
|
67
|
+
for (const entry of this.pending.splice(0)) {
|
|
68
|
+
entry.controller.abort();
|
|
69
|
+
this.runCancelled(entry.onCancelled);
|
|
70
|
+
}
|
|
71
|
+
for (const controller of this.active) controller.abort();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Cancellation callbacks are user-supplied: a throw must never break the queue
|
|
75
|
+
* (mirrors the try/catch around onError in drain). */
|
|
76
|
+
private runCancelled(callback: (() => void) | undefined): void {
|
|
77
|
+
if (!callback) return;
|
|
78
|
+
try {
|
|
79
|
+
callback();
|
|
80
|
+
} catch {
|
|
81
|
+
/* cancellation callbacks must never break the queue */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private drain(): void {
|
|
86
|
+
while (!this.stopped && this.active.size < this.concurrency) {
|
|
87
|
+
const entry = this.pending.shift();
|
|
88
|
+
if (!entry) return;
|
|
89
|
+
if (entry.controller.signal.aborted) {
|
|
90
|
+
this.runCancelled(entry.onCancelled);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.active.add(entry.controller);
|
|
95
|
+
void entry.task(entry.controller.signal)
|
|
96
|
+
.catch((error: unknown) => {
|
|
97
|
+
// Cancellation is not a failure: aborted work (e.g. session
|
|
98
|
+
// shutdown) must never be reported as an exception.
|
|
99
|
+
if (entry.controller.signal.aborted) return;
|
|
100
|
+
try {
|
|
101
|
+
entry.onError?.(error);
|
|
102
|
+
} catch {
|
|
103
|
+
/* error reporting must never break the queue */
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
.finally(() => {
|
|
107
|
+
this.active.delete(entry.controller);
|
|
108
|
+
this.drain();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/fixloop.ts
CHANGED
|
@@ -1,76 +1,84 @@
|
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
// A dispatch crash (spawn infra, delivery API, ...) is never a real review
|
|
29
|
+
// verdict: its output is an error message plus whatever partial text the
|
|
30
|
+
// child happened to emit, which could end in a stray `VERDICT: REVIEW_FAIL`.
|
|
31
|
+
// Guard explicitly in addition to isFailedResult so the intent is clear and
|
|
32
|
+
// a future change to isFailedResult can never let a crashed reviewer start a
|
|
33
|
+
// phantom auto-fix chain (and re-add a run controller id that was deleted in
|
|
34
|
+
// the catch path).
|
|
35
|
+
if (result.dispatchFailed) return false;
|
|
36
|
+
return reviewVerdict(getResultOutput(result)) === "fail";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the worker task brief for one fix round from a reviewer's findings.
|
|
41
|
+
* The worker gets the full review text so it can address concrete file:line
|
|
42
|
+
* issues, with instructions to fix only blockers and self-verify.
|
|
43
|
+
*/
|
|
44
|
+
export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
|
|
45
|
+
const review = getResultOutput(reviewerResult);
|
|
46
|
+
const remaining = maxRounds - round;
|
|
47
|
+
return [
|
|
48
|
+
`Auto-fix round ${round} of ${maxRounds} (triggered by a failed review).`,
|
|
49
|
+
``,
|
|
50
|
+
`A reviewer ran in an isolated context and returned REQUEST_CHANGES. Its full report:`,
|
|
51
|
+
`---`,
|
|
52
|
+
review,
|
|
53
|
+
`---`,
|
|
54
|
+
``,
|
|
55
|
+
`Fix the concrete blockers the reviewer flagged. Do NOT refactor unrelated code.`,
|
|
56
|
+
`Address every "Critical" item; address "Warnings" only if they are genuine.`,
|
|
57
|
+
`After editing, run the project's format/build/tests when they exist and report`,
|
|
58
|
+
`exactly what you changed (paths + short rationale) so a reviewer can verify.`,
|
|
59
|
+
remaining > 0
|
|
60
|
+
? `A reviewer will re-review your changes automatically after you finish.`
|
|
61
|
+
: `This is the last auto-fix round; the main agent will be woken with the full chain.`,
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
67
|
+
* the prior review so the reviewer can verify the fixes without re-discovering
|
|
68
|
+
* the original issues.
|
|
69
|
+
*/
|
|
70
|
+
export function buildReReviewBrief(reviewerResult: SingleResult, round: number): string {
|
|
71
|
+
const review = getResultOutput(reviewerResult);
|
|
72
|
+
return [
|
|
73
|
+
`Re-review after auto-fix round ${round}.`,
|
|
74
|
+
``,
|
|
75
|
+
`The previous review (REQUEST_CHANGES) found these issues:`,
|
|
76
|
+
`---`,
|
|
77
|
+
review,
|
|
78
|
+
`---`,
|
|
79
|
+
``,
|
|
80
|
+
`Verify the worker's fixes address each blocker. Run \`git diff\` to see what changed.`,
|
|
81
|
+
`Classify honestly: APPROVE if blockers are resolved, REQUEST_CHANGES if not.`,
|
|
82
|
+
`End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|