@ferris1225/pi-subagents 0.32.2 → 1.0.1
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 +45 -22
- package/agents/reviewer.md +2 -0
- package/package.json +2 -2
- package/src/agents.ts +2 -7
- package/src/announcements.ts +62 -0
- package/src/completion.ts +7 -36
- package/src/config.ts +327 -364
- package/src/dispatch.ts +1682 -1878
- package/src/fixloop.ts +0 -16
- package/src/format.ts +4 -8
- package/src/index.ts +8 -9
- package/src/models.ts +17 -39
- package/src/monitor.ts +64 -175
- package/src/rpc-run.ts +2 -41
- package/src/runtime.ts +272 -285
- package/src/session-fork.ts +0 -4
- package/src/setup.ts +4 -4
- package/src/spawn.ts +542 -562
- package/src/tools.ts +706 -748
- package/src/ui.ts +3 -7
- package/src/widget.ts +90 -178
- package/src/worktree.ts +1 -1
- package/src/inspector-panel.ts +0 -363
- package/src/inspector.ts +0 -369
- package/src/trajectory.ts +0 -503
package/README.md
CHANGED
|
@@ -11,6 +11,30 @@ Focused background delegation for [pi](https://pi.dev): `explore` / `worker` /
|
|
|
11
11
|
to the main agent automatically. Install it, and the main model starts using it
|
|
12
12
|
on its own — no prompt engineering, no babysitting.
|
|
13
13
|
|
|
14
|
+
## 1.0 — controllable agent threads
|
|
15
|
+
|
|
16
|
+
Version 1.0 turns pi-subagents from a one-shot background runner into a small
|
|
17
|
+
thread runtime. Every dispatch has a stable run id and retained Pi session, so
|
|
18
|
+
work can be steered while it runs, parked without losing context, resumed after
|
|
19
|
+
settlement, retargeted, or forked into another path. Generation ownership keeps
|
|
20
|
+
retries and stale child processes from corrupting the logical thread without
|
|
21
|
+
retaining a duplicate event history.
|
|
22
|
+
|
|
23
|
+
The common quality loop now runs end to end without waking the main agent between
|
|
24
|
+
steps:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
reviewer (find blockers) → worker (fix) → reviewer (verify) → final PASS/FAIL
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Each chain is delivered as one concise completion group, while full per-run
|
|
31
|
+
reports remain available through `subagent_status`. Its parent stays `running`
|
|
32
|
+
until the whole chain settles; completed internal rounds leave active status
|
|
33
|
+
immediately, so no `done` row keeps accumulating elapsed time. Ordered model
|
|
34
|
+
pools keep the same retained context across provider fallback, and isolated parallel workers
|
|
35
|
+
use detached Git worktrees whose changes are applied back without touching the
|
|
36
|
+
parent index.
|
|
37
|
+
|
|
14
38
|
## Highlights
|
|
15
39
|
|
|
16
40
|
- **Zero-setup proactive dispatch** — the extension injects a delegation directive
|
|
@@ -19,14 +43,17 @@ on its own — no prompt engineering, no babysitting.
|
|
|
19
43
|
to `reviewer`. You just use pi; delegation happens by itself.
|
|
20
44
|
- **Vision-capable image tasks** — flag screenshot/mockup/design work with
|
|
21
45
|
`vision: true`. The configured vision primary is followed by that agent's
|
|
22
|
-
backup and the current main-window model.
|
|
23
|
-
image-capable models
|
|
46
|
+
backup and the current main-window model. Setup lists only in-scope,
|
|
47
|
+
image-capable models from providers with a configured API key or OAuth session,
|
|
24
48
|
and runtime failures never silently rewrite your configuration.
|
|
25
49
|
- **Results come back on their own** — completions are delivered as messages that
|
|
26
50
|
wake the main agent automatically, even mid-turn. No polling, no `sleep`, no
|
|
27
51
|
"go check" step. `subagent_wait` is a **non-blocking** in-turn lookup by default
|
|
28
52
|
(pass `timeoutMs` to block); `subagent_status` inspects runs; `subagent_stop`
|
|
29
53
|
cancels one and delivers its partial output.
|
|
54
|
+
- **Active-only live widget** — the TUI shows queued and running sub-agents above
|
|
55
|
+
the editor with live activity and elapsed time. Settled and parked rows disappear
|
|
56
|
+
immediately; an auto-fix parent remains `running` until its whole chain settles.
|
|
30
57
|
- **Results are not re-narrated** — a sub-agent's completion is shown to you
|
|
31
58
|
verbatim, and the main agent is told not to paraphrase it back. It replies with
|
|
32
59
|
only its own conclusion or next step, so the same findings are never paid for
|
|
@@ -52,12 +79,6 @@ on its own — no prompt engineering, no babysitting.
|
|
|
52
79
|
configurable limit (default 4). Parallel workers default to detached Git
|
|
53
80
|
worktrees; tracked, deleted, untracked, and binary changes are applied back
|
|
54
81
|
without touching the parent index. Failed integration keeps recovery artifacts.
|
|
55
|
-
- **Live inspector** — `/subagents-inspect` opens a master/detail
|
|
56
|
-
overlay with thread state, model chain, usage, transcript, recent tools,
|
|
57
|
-
worktree/fork relations, and the append-only control trajectory.
|
|
58
|
-
- **Live progress widget** — each run's status, current activity, model, token
|
|
59
|
-
usage, and elapsed time; auto-fix chain rounds hang under their triggering
|
|
60
|
-
review as a tree, each finished round keeping a one-line outcome.
|
|
61
82
|
- **Recursion is structurally impossible** — children are leaf processes; the
|
|
62
83
|
`subagent` tool is excluded from their toolset.
|
|
63
84
|
- **Zero runtime dependencies** — agents are plain Markdown files; overriding or
|
|
@@ -134,7 +155,10 @@ once, then skipped as a permanent candidate error; it is not rewritten. A
|
|
|
134
155
|
vision-flagged auto-fix chain keeps the flag for worker/re-review rounds because
|
|
135
156
|
they may need to inspect the same images.
|
|
136
157
|
|
|
137
|
-
### Controlling
|
|
158
|
+
### Controlling and stopping
|
|
159
|
+
|
|
160
|
+
Dispatch confirmations, tool result rows, and completion blocks all show the
|
|
161
|
+
stable `#id`, so a thread remains directly controllable after its live UI is gone.
|
|
138
162
|
|
|
139
163
|
- `subagent_control` — `steer`, `retarget`, `park`, `resume`, or `fork` a logical
|
|
140
164
|
thread by stable run id. Resume accepts an optional replacement objective;
|
|
@@ -144,8 +168,6 @@ they may need to inspect the same images.
|
|
|
144
168
|
run returns immediately; an active run tells the model to end its turn. Pass
|
|
145
169
|
`timeoutMs` only when you must stay in the turn.
|
|
146
170
|
- `subagent_status` — active/parked/finished runs and full result by run id.
|
|
147
|
-
- `/subagents-inspect` — interactive live thread/transcript/tool/trajectory
|
|
148
|
-
overlay; press `p` to park or resume the selected thread.
|
|
149
171
|
- `subagent_stop` — destructive cancellation. It retires that thread's retained
|
|
150
172
|
session (independent forks survive) and delivers exactly one aborted partial
|
|
151
173
|
result after the run and any worktree integration have quiesced.
|
|
@@ -187,8 +209,9 @@ Stored at `~/.pi/agent/pi-subagents.json` (follows `PI_CODING_AGENT_DIR` when
|
|
|
187
209
|
set). The `/subagents-setup` wizard drives every field interactively — models, the
|
|
188
210
|
default plus each enabled agent's thinking level, the vision model, concurrency,
|
|
189
211
|
fix rounds, idle timeout, scope, and injection — with a per-agent "configure
|
|
190
|
-
one" menu when the config already exists.
|
|
191
|
-
|
|
212
|
+
one" menu when the config already exists. Model pickers show only models in the
|
|
213
|
+
current session scope that are available through a configured API key or OAuth
|
|
214
|
+
session. `notifyOnReviewPass` and `maxResultLines` are edited directly in the file.
|
|
192
215
|
|
|
193
216
|
```json
|
|
194
217
|
{
|
|
@@ -268,11 +291,11 @@ sessions live until the parent Pi session shuts down.
|
|
|
268
291
|
|
|
269
292
|
### Configuration migration
|
|
270
293
|
|
|
271
|
-
The config file
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
after an update via a toast (marker persisted in `announcedFeatures`).
|
|
294
|
+
The config file normalizes itself on load — no manual steps after an upgrade:
|
|
295
|
+
configured non-empty agent names are preserved, invalid values and unsupported
|
|
296
|
+
keys (including `maxParallelTasks` and `maxSubagentDepth`) are dropped, and
|
|
297
|
+
missing current fields are filled with defaults. New features are announced to
|
|
298
|
+
you once after an update via a toast (marker persisted in `announcedFeatures`).
|
|
276
299
|
|
|
277
300
|
## Agent discovery and overrides
|
|
278
301
|
|
|
@@ -298,6 +321,7 @@ after an update via a toast (marker persisted in `announcedFeatures`).
|
|
|
298
321
|
- **Dispatch failures surface** — partial parallel startup reports every failed
|
|
299
322
|
item and reason; if none start, the tool throws so Pi records a real tool error.
|
|
300
323
|
Dispatch crashes likewise produce a failed result instead of a silent hang.
|
|
324
|
+
- **Safe live status** — tool activity is credential-redacted and stripped of terminal control sequences before `subagent_status` can return it.
|
|
301
325
|
- **Leaf children** — no nested delegation, no runaway trees.
|
|
302
326
|
|
|
303
327
|
## Development
|
|
@@ -310,10 +334,9 @@ npm test
|
|
|
310
334
|
|
|
311
335
|
The source is modular: `dispatch.ts` (dispatch, controls, isolation, auto-fix),
|
|
312
336
|
`rpc-run.ts` / `spawn.ts` (persistent child transport + model pools),
|
|
313
|
-
`worktree.ts` / `session-fork.ts` (filesystem/session branching),
|
|
314
|
-
|
|
315
|
-
(
|
|
316
|
-
`runtime.ts` (session-scoped ownership). No runtime dependencies beyond pi peer
|
|
337
|
+
`worktree.ts` / `session-fork.ts` (filesystem/session branching), `tools.ts`
|
|
338
|
+
(wait/status/control/stop), `widget.ts` (active-only TUI status), `announcements.ts`
|
|
339
|
+
(recovery and feature notices), and `runtime.ts` (session-scoped ownership). No runtime dependencies beyond pi peer
|
|
317
340
|
dependencies.
|
|
318
341
|
|
|
319
342
|
## License
|
package/agents/reviewer.md
CHANGED
|
@@ -60,5 +60,7 @@ Understand the context first, then verify: the fix addresses the root cause, cha
|
|
|
60
60
|
One of: APPROVE / APPROVE_WITH_NITS / REQUEST_CHANGES, plus a 2-3 sentence rationale.
|
|
61
61
|
End with exactly one machine-readable line: `VERDICT: REVIEW_PASS` for APPROVE or APPROVE_WITH_NITS; `VERDICT: REVIEW_FAIL` for REQUEST_CHANGES.
|
|
62
62
|
|
|
63
|
+
REQUEST_CHANGES (or review-blocking concern) on this extension automatically starts an auto-fix loop: a worker is briefed with your findings, then you re-review, up to the configured round limit. Choose REVIEW_PASS when nothing objectively blocks the change (style preferences, optional refactors, or items the caller can knowingly accept are not blockers) — do not force a verdict that would trigger another loop just to hedge.
|
|
64
|
+
|
|
63
65
|
## Quality standards
|
|
64
66
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Controllable background sub-agent threads for pi: explore, worker, reviewer, auto-fix chains, model pools, and Git worktree isolation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"publishConfig": {
|
package/src/agents.ts
CHANGED
|
@@ -31,11 +31,6 @@ export interface AgentConfig {
|
|
|
31
31
|
filePath: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export interface AgentDiscoveryResult {
|
|
35
|
-
agents: AgentConfig[];
|
|
36
|
-
projectAgentsDir: string | null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
34
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
40
35
|
/** <package>/agents — the agents shipped with this extension. */
|
|
41
36
|
export const BUILTIN_AGENTS_DIR = join(here, "..", "agents");
|
|
@@ -135,7 +130,7 @@ export interface DiscoverOptions {
|
|
|
135
130
|
* Discover agents across scopes and apply the enabled-name filter.
|
|
136
131
|
* Override priority for the same name: project > user > builtin.
|
|
137
132
|
*/
|
|
138
|
-
export function discoverAgents(cwd: string, options: DiscoverOptions = {}):
|
|
133
|
+
export function discoverAgents(cwd: string, options: DiscoverOptions = {}): { agents: AgentConfig[] } {
|
|
139
134
|
const scope = options.scope ?? "user";
|
|
140
135
|
const builtinDir = options.builtinDir ?? BUILTIN_AGENTS_DIR;
|
|
141
136
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
@@ -160,7 +155,7 @@ export function discoverAgents(cwd: string, options: DiscoverOptions = {}): Agen
|
|
|
160
155
|
agents = agents.filter((agent) => enabled.has(agent.name));
|
|
161
156
|
}
|
|
162
157
|
|
|
163
|
-
return { agents
|
|
158
|
+
return { agents };
|
|
164
159
|
}
|
|
165
160
|
|
|
166
161
|
/** One-line catalog entry for system-prompt injection and error messages. */
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Session-start recovery and one-time feature announcements. */
|
|
2
|
+
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { loadConfig, saveConfig } from "./config.ts";
|
|
6
|
+
import { announceRecoveryRecords } from "./recovery.ts";
|
|
7
|
+
import type { SubagentRuntime } from "./runtime.ts";
|
|
8
|
+
import { installActiveRunsWidget } from "./widget.ts";
|
|
9
|
+
|
|
10
|
+
const ANNOUNCEMENTS: Array<{
|
|
11
|
+
key: string;
|
|
12
|
+
condition: (config: Awaited<ReturnType<typeof loadConfig>>) => boolean;
|
|
13
|
+
message: string;
|
|
14
|
+
}> = [
|
|
15
|
+
{
|
|
16
|
+
key: "visionModel",
|
|
17
|
+
condition: (config) => config.visionModel === undefined,
|
|
18
|
+
message:
|
|
19
|
+
"pi-subagents: new — a vision-capable model can now handle image tasks (screenshots, mockups, designs). Run /subagents-setup to configure it; until set, vision tasks use the main session's current model.",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function announceNewFeatures(
|
|
24
|
+
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } },
|
|
25
|
+
runtime: SubagentRuntime,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
try {
|
|
28
|
+
let configExists = true;
|
|
29
|
+
try {
|
|
30
|
+
await stat(runtime.configPath);
|
|
31
|
+
} catch {
|
|
32
|
+
configExists = false;
|
|
33
|
+
}
|
|
34
|
+
if (!configExists) return;
|
|
35
|
+
|
|
36
|
+
const config = await loadConfig(runtime.configPath);
|
|
37
|
+
const pending = ANNOUNCEMENTS.filter(
|
|
38
|
+
(announcement) =>
|
|
39
|
+
announcement.condition(config) && !config.announcedFeatures.includes(announcement.key),
|
|
40
|
+
);
|
|
41
|
+
if (pending.length === 0) return;
|
|
42
|
+
await saveConfig(
|
|
43
|
+
{
|
|
44
|
+
...config,
|
|
45
|
+
announcedFeatures: [...config.announcedFeatures, ...pending.map((announcement) => announcement.key)],
|
|
46
|
+
},
|
|
47
|
+
runtime.configPath,
|
|
48
|
+
);
|
|
49
|
+
for (const announcement of pending) ctx.ui.notify(announcement.message, "info");
|
|
50
|
+
} catch {
|
|
51
|
+
/* announcement failures are non-fatal */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
56
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
57
|
+
await announceRecoveryRecords(runtime.configPath, ctx);
|
|
58
|
+
if (ctx.mode !== "tui") return;
|
|
59
|
+
installActiveRunsWidget(ctx);
|
|
60
|
+
await announceNewFeatures(ctx, runtime);
|
|
61
|
+
});
|
|
62
|
+
}
|
package/src/completion.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Smart batching for successful background completions.
|
|
3
3
|
*
|
|
4
4
|
* A short debounce coalesces sibling runs while a max-wait timer, measured from
|
|
5
|
-
* the first item in the open group, bounds delivery latency.
|
|
6
|
-
* shortly after an emitted group use a smaller straggler window. Failures are
|
|
5
|
+
* the first item in the open group, bounds delivery latency. Failures are
|
|
7
6
|
* intentionally handled by the caller: flush held successes, then emit the
|
|
8
7
|
* failure directly so it is never delayed.
|
|
9
8
|
*/
|
|
@@ -13,30 +12,14 @@ import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } fro
|
|
|
13
12
|
export interface CompletionBatchTimings {
|
|
14
13
|
debounceMs: number;
|
|
15
14
|
maxWaitMs: number;
|
|
16
|
-
stragglerDebounceMs: number;
|
|
17
|
-
stragglerMaxWaitMs: number;
|
|
18
|
-
stragglerWindowMs: number;
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
|
|
22
18
|
debounceMs: 150,
|
|
23
19
|
maxWaitMs: 1_000,
|
|
24
|
-
stragglerDebounceMs: 75,
|
|
25
|
-
stragglerMaxWaitMs: 400,
|
|
26
|
-
stragglerWindowMs: 2_000,
|
|
27
20
|
};
|
|
28
21
|
|
|
29
|
-
type TimerHandle =
|
|
30
|
-
|
|
31
|
-
export interface TimerApi {
|
|
32
|
-
setTimeout(handler: () => void, delayMs: number): TimerHandle;
|
|
33
|
-
clearTimeout(handle: TimerHandle): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const defaultTimers: TimerApi = {
|
|
37
|
-
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
38
|
-
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
39
|
-
};
|
|
22
|
+
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
40
23
|
|
|
41
24
|
function unrefHandle(handle: TimerHandle): void {
|
|
42
25
|
if (
|
|
@@ -52,8 +35,6 @@ function unrefHandle(handle: TimerHandle): void {
|
|
|
52
35
|
export interface CompletionBatcherOptions<T> {
|
|
53
36
|
emit: (items: T[]) => void;
|
|
54
37
|
timings?: Partial<CompletionBatchTimings>;
|
|
55
|
-
timers?: TimerApi;
|
|
56
|
-
now?: () => number;
|
|
57
38
|
}
|
|
58
39
|
|
|
59
40
|
export interface CompletionBatcher<T> {
|
|
@@ -66,22 +47,18 @@ export interface CompletionBatcher<T> {
|
|
|
66
47
|
}
|
|
67
48
|
|
|
68
49
|
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
69
|
-
const timers = options.timers ?? defaultTimers;
|
|
70
|
-
const now = options.now ?? Date.now;
|
|
71
50
|
const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
|
|
72
51
|
let pending: T[] = [];
|
|
73
52
|
let debounceTimer: TimerHandle | null = null;
|
|
74
53
|
let maxWaitTimer: TimerHandle | null = null;
|
|
75
|
-
let straggler = false;
|
|
76
|
-
let lastEmitAt: number | null = null;
|
|
77
54
|
|
|
78
55
|
const clearTimers = (): void => {
|
|
79
56
|
if (debounceTimer !== null) {
|
|
80
|
-
|
|
57
|
+
clearTimeout(debounceTimer);
|
|
81
58
|
debounceTimer = null;
|
|
82
59
|
}
|
|
83
60
|
if (maxWaitTimer !== null) {
|
|
84
|
-
|
|
61
|
+
clearTimeout(maxWaitTimer);
|
|
85
62
|
maxWaitTimer = null;
|
|
86
63
|
}
|
|
87
64
|
};
|
|
@@ -91,25 +68,19 @@ export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>)
|
|
|
91
68
|
if (pending.length === 0) return;
|
|
92
69
|
const items = pending;
|
|
93
70
|
pending = [];
|
|
94
|
-
lastEmitAt = now();
|
|
95
71
|
options.emit(items);
|
|
96
72
|
};
|
|
97
73
|
|
|
98
74
|
return {
|
|
99
75
|
push(item: T): void {
|
|
100
|
-
if (pending.length === 0) {
|
|
101
|
-
straggler = lastEmitAt !== null && now() - lastEmitAt < timings.stragglerWindowMs;
|
|
102
|
-
}
|
|
103
76
|
pending.push(item);
|
|
104
77
|
|
|
105
|
-
if (debounceTimer !== null)
|
|
106
|
-
|
|
107
|
-
debounceTimer = timers.setTimeout(emitGroup, debounceDelay);
|
|
78
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
79
|
+
debounceTimer = setTimeout(emitGroup, timings.debounceMs);
|
|
108
80
|
unrefHandle(debounceTimer);
|
|
109
81
|
|
|
110
82
|
if (maxWaitTimer === null) {
|
|
111
|
-
|
|
112
|
-
maxWaitTimer = timers.setTimeout(emitGroup, maxWaitDelay);
|
|
83
|
+
maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
|
|
113
84
|
unrefHandle(maxWaitTimer);
|
|
114
85
|
}
|
|
115
86
|
},
|