@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 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. The setup picker offers only
23
- image-capable models (while clearly marking an incompatible stale saved ref),
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, inspecting, and stopping
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. `notifyOnReviewPass` and
191
- `maxResultLines` are edited directly in the file.
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 migrates itself on load — no manual steps after an upgrade:
272
- schema upgrades are normalized and saved back, removed agents are stripped,
273
- legacy keys (`maxParallelTasks`, `maxSubagentDepth`) are folded in or dropped,
274
- and new fields are filled with defaults. New features are announced to you once
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
- `trajectory.ts` / `inspector*.ts` (safe live inspection), `tools.ts`
315
- (wait/status/control/stop), `widget.ts` (widget + recovery announcements), and
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
@@ -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.32.2",
4
- "description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
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 = {}): AgentDiscoveryResult {
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, projectAgentsDir };
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. Runs that finish
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 = unknown;
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
- timers.clearTimeout(debounceTimer);
57
+ clearTimeout(debounceTimer);
81
58
  debounceTimer = null;
82
59
  }
83
60
  if (maxWaitTimer !== null) {
84
- timers.clearTimeout(maxWaitTimer);
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) timers.clearTimeout(debounceTimer);
106
- const debounceDelay = straggler ? timings.stragglerDebounceMs : timings.debounceMs;
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
- const maxWaitDelay = straggler ? timings.stragglerMaxWaitMs : timings.maxWaitMs;
112
- maxWaitTimer = timers.setTimeout(emitGroup, maxWaitDelay);
83
+ maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
113
84
  unrefHandle(maxWaitTimer);
114
85
  }
115
86
  },