@d3ara1n/pi-subagent 0.10.3 → 1.0.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Role-based subagent orchestration for [pi](https://github.com/earendil-works/pi).
4
4
 
5
- Provides a `delegate` tool that lets the main model offload tasks to specialized pi child processes with configurable model roles, real-time TUI progress, and AI-generated summaries.
5
+ Provides a `subagent_delegate` tool that lets the main model offload tasks to specialized pi child processes with configurable model roles, real-time TUI progress, and AI-generated summaries. Runs can be foreground (blocking) or background (asynchronous, collected later via `subagent_wait`/`subagent_check`).
6
6
 
7
7
  ## Design Philosophy
8
8
 
@@ -12,16 +12,16 @@ Your primary AI has the most complete context — it knows the full conversation
12
12
 
13
13
  This means:
14
14
  - **Subagents don't plan** — the main model decides what needs to be done and provides a clear task description
15
- - **Subagents don't orchestrate** — if a task requires multiple steps, the main model examines each result and decides the next move
15
+ - **Subagents don't orchestrate the overall plan** — the main model decides what to do and examines each result to pick the next move; nested delegation (worker → explorer) only offloads self-contained exploration/research inside one task
16
16
  - **Subagents don't inherit history** — they don't need the full conversation; just a precise task description
17
- - **Multiple subagents can run in parallel** — emit multiple `delegate` calls in one turn; pi executes them concurrently
17
+ - **Multiple subagents can run in parallel** — emit multiple `subagent_delegate` calls in one turn; pi executes them concurrently
18
18
  - **Subagents can nest subagents** — a `worker` can delegate exploration to `explorer` without returning to the main model
19
19
 
20
20
  > This design currently focuses on single-task delegation rather than chain pipelines or context-forking — those patterns fit better when subagents act as advisors (planner, oracle) rather than executors.
21
21
 
22
22
  ## How it works
23
23
 
24
- 1. Main model calls the `delegate` tool with a role and task description
24
+ 1. Main model calls the `subagent_delegate` tool with a role and task description
25
25
  2. The extension resolves the role to a model via pi-model-roles
26
26
  3. Spawns an isolated pi child process with the configured model, tools, and system prompt
27
27
  4. **Real-time TUI progress** shows tool calls, turns, and elapsed time as the subagent runs
@@ -39,19 +39,21 @@ This means:
39
39
 
40
40
  **Nested delegation**: `worker` and `researcher` can spawn their own subagents. This keeps the main model's context clean — a worker can explore unfamiliar code via an `explorer` subagent without returning intermediate results to the main model.
41
41
 
42
- **Parallel execution**: To run multiple subagents concurrently, emit multiple `delegate` calls in a single turn. Pi's framework executes them in parallel automatically, with each subagent getting its own TUI progress display.
42
+ **Parallel execution**: To run multiple subagents concurrently, emit multiple `subagent_delegate` calls in a single turn. Pi's framework executes them in parallel automatically, with each subagent getting its own TUI progress display.
43
43
 
44
44
  ## TUI Display
45
45
 
46
- - **During execution**: Shows role, elapsed time, turn count, and live tool calls
47
- - **Collapsed result**: `✓ explorer · Found login, registration, and token logic` + recent tool calls + usage stats
48
- - **Expanded result** (Ctrl+O): Full task text, all tool calls, final output as rendered Markdown, and usage details
46
+ - **During execution**: the task's first line with a ⏳ (or ⏸ queued) indicator, a live stream of thinking blocks and tool calls (latest 5 collapsed, everything expanded), and a usage line (elapsed/budget time, turns, tokens, peak context, cost, model)
47
+ - **Collapsed result**: the task's first line, then `✓` + the AI-generated summary (or the first line of the output), then the usage line — no activity replay
48
+ - **Expanded result** (Ctrl+O): reference files, context size, the full task, the complete activity stream, the final output as rendered Markdown, and usage details
49
+ - **Fallback trace**: when a provider error (429, quota, timeout, ...) kills a run and it is retried on the role's `fallbackRole`, a `⚠ fallback: first attempt <model> failed (<reason>)` line appears in both views — also while the retry is running (see [Fallback observability](#fallback-observability))
49
50
 
50
51
  ## Commands
51
52
 
52
53
  | Command | Description |
53
54
  |---------|-------------|
54
55
  | `/subagent:doctor` | Diagnose pi invocation, model-role resolution, configuration, and role references |
56
+ | `/subagent:status` | List background subagent runs and their current state |
55
57
 
56
58
  ## Dependencies
57
59
 
@@ -99,7 +101,7 @@ Edit `~/.pi/agent/settings.json`:
99
101
 
100
102
  All fields are optional. Defaults: `maxConcurrency: 4`, `maxDepth: 3`, `maxTurns: 0` (unlimited), `maxCost: 0` (unlimited), `history.enabled: true`, `summary.role: "utility"`, `summary.enabled: true`.
101
103
 
102
- Timeouts are defined per role. Built-in defaults are `explorer: 900`, `reviewer: 3600`, `worker: 2400`, and `researcher: 2400` seconds. The timeout is active time — the clock pauses while the child is inside a nested `delegate` call, so delegate-capable roles need no extra headroom.
104
+ Timeouts are defined per role. Built-in defaults are `explorer: 900`, `reviewer: 3600`, `worker: 2400`, and `researcher: 2400` seconds. The timeout is active time — the clock pauses while the child is inside a nested `subagent_delegate` call, so delegate-capable roles need no extra headroom.
103
105
 
104
106
  All numeric limits accept `0` for unlimited: `maxConcurrency`, `maxDepth`, `maxTurns`, `maxCost`, and per-role `timeout`. Negative values are normalized to `0`; non-numeric or non-finite values fall back to their defaults. `maxConcurrency: 0` runs delegates without queuing, and `maxDepth: 0` permits unrestricted nesting.
105
107
 
@@ -138,7 +140,7 @@ Override, disable, or add subagent roles via `agentOverrides`. Built-in and cust
138
140
 
139
141
  **Required fields for custom roles:** `role`, `description`, `examples`, `decisionTrigger`, `tools`, `systemPrompt`.
140
142
 
141
- **Optional fields:** `subagentRoles` (roles this role can spawn via delegate), `timeout` (per-role active-time timeout in seconds; unset or `0` is unlimited, negative values normalize to `0`), `maxTurns` / `maxCost` (per-role budget overrides; unset uses the top-level `maxTurns` / `maxCost` setting, `0` is unlimited, negative values normalize to `0`), `fallbackRole` (backup pi-model-roles role on provider errors).
143
+ **Optional fields:** `subagentRoles` (roles this role can spawn via delegate), `timeout` (per-role active-time timeout in seconds; unset or `0` is unlimited, negative values normalize to `0`), `maxTurns` / `maxCost` (per-role budget overrides; unset uses the top-level `maxTurns` / `maxCost` setting, `0` is unlimited, negative values normalize to `0`), `fallbackRole` (backup pi-model-roles role the whole run is retried on after a provider error; unset means no retry — see [Fallback observability](#fallback-observability)).
142
144
 
143
145
  Invalid custom roles (missing required fields) are silently skipped with an error notification at session start.
144
146
 
@@ -162,7 +164,7 @@ Delegate tasks that would generate many tool calls or verbose output to keep you
162
164
  | `worker` | `"Rename all snake_case fields to camelCase in src/models/"` | Your context stays focused on high-level intent |
163
165
  | `researcher` | `"Find the React 19 migration guide and summarize breaking changes"` | Search results are noisy; get a clean summary |
164
166
 
165
- **Parallel usage:** emit multiple `delegate` calls in a single turn:
167
+ **Parallel usage:** emit multiple `subagent_delegate` calls in a single turn:
166
168
 
167
169
  ```json
168
170
  [
@@ -171,6 +173,49 @@ Delegate tasks that would generate many tool calls or verbose output to keep you
171
173
  ]
172
174
  ```
173
175
 
176
+ ## Background Delegation
177
+
178
+ Foreground and background delegation share one async run engine — foreground is simply background-but-blocking. With `background: true`, `subagent_delegate` returns immediately with a run id, and two companion tools collect the outcome:
179
+
180
+ | Tool | Purpose | Returns to the model |
181
+ |------|---------|---------------------|
182
+ | `subagent_delegate(background: true)` | Start an async run | Just the id (`sub-N`) |
183
+ | `subagent_wait(ids?, timeout_ms?)` | Block until **all** listed runs finish (omit `ids` for all current background runs) | Statuses only, one `id (role): finished/failed` line per run — never results; errors when the timeout hits with runs unfinished |
184
+ | `subagent_check(id)` | One-shot snapshot of a single run | `queued` / `running` + current activity / the **full output** once finished / failure reason + partial output |
185
+
186
+ Typical flow:
187
+
188
+ ```json
189
+ [
190
+ { "role": "worker", "task": "Implement the export module", "background": true },
191
+ { "role": "researcher", "task": "Find the CSV escaping spec", "background": true }
192
+ ]
193
+ ```
194
+
195
+ …continue other work, then:
196
+
197
+ ```json
198
+ { "ids": ["sub-1", "sub-2"] }
199
+ ```
200
+
201
+ (call `subagent_wait`), and finally `subagent_check` each finished id to fetch its result. `subagent_check` accepts one id per call because results can be large.
202
+
203
+ Semantics worth knowing:
204
+
205
+ - **Background runs survive turn cancellation** and are unaffected by a cancelled `subagent_wait` — cancelling the wait never cancels the runs; call `subagent_wait` or `subagent_check` again later.
206
+ - **`timeout_ms` is optional.** Without it, `subagent_wait` blocks until every run finishes; each run is still bounded by its own role timeout.
207
+ - Background runs share the global `maxConcurrency` gate — extra runs show up as `queued` in wait/check views.
208
+ - **Top-level only:** nested subagents cannot delegate in the background (a subagent process exits when its task finishes, which would orphan the run).
209
+ - The run registry lives in the pi process: a `/reload` or restart orphans in-flight background runs (their ids stop resolving). `/subagent:status` lists every registered run and its current state.
210
+
211
+ ### Background TUI display
212
+
213
+ Each tool row renders one aspect of the same decomposition the foreground row shows all at once (input · process · result · usage):
214
+
215
+ - **Background subagent_delegate row = input only.** Collapsed: `▶ sub-1 <task first line>`. Expanded: plus `@file` references, context size, and the full task text. Static — the run progresses invisibly until a subagent_wait/subagent_check row picks it up.
216
+ - **subagent_wait row = process + usage.** One block per watched run: status line (`⏸ queued / ⏳ running` + id + task preview; bare, icon-free once terminal), a live activity stream (collapsed keeps the latest 5 items with a leading ellipsis; expanded shows everything) and a ticking usage bar. Once a run finishes, its process stream is replaced by a **status-only** result line (`✓ finished` / `⏲ budget-exceeded with the reason` / `✗ <reason>`) — the output itself never appears in a subagent_wait row; expanded keeps the full process stream instead. A timed-out wait freezes the view.
217
+ - **subagent_check row = the result view.** Same block shape as subagent_wait's single-run view (no id — there is only one), but the result line shows `✓ <AI summary>` (or the budget/failure reason when the run stopped early) and the expanded view renders the **full output** — subagent_check is where the conclusion lives.
218
+
174
219
  ### Passing context and reference files
175
220
 
176
221
  pi-subagent delivers context to the child as **independent channels**, never fused into the task string. This keeps the task an unambiguous directive and lets each channel be sized independently.
@@ -203,15 +248,19 @@ Each path is injected as an independent `@file` attachment the subagent reads di
203
248
 
204
249
  ### Budget enforcement
205
250
 
206
- `maxTurns` / `maxCost` cap a run. When exceeded, the child is killed and the last completed output is returned with `stopReason: "budget_exceeded"` (shown in the expanded TUI). Defaults are unlimited (`0`); set global defaults in config or per-role overrides in `agentOverrides`. Negative values are normalized to `0`.
251
+ `maxTurns` / `maxCost` cap a run. When exceeded, the child is killed and the last completed output is returned with `stopReason: "budget_exceeded"`. Budget stops are **intentional finishes** — the output is partial but valid: the TUI marks the run with a ⏲ line stating the reason, `subagent_wait` reports `finished (budget exceeded — output is partial)`, and the tool result (and `subagent_check`) append a `--- Budget exceeded (...) ---` note so the model knows to treat the output as partial. Defaults are unlimited (`0`); set global defaults in config or per-role overrides in `agentOverrides`. Negative values are normalized to `0`.
207
252
 
208
253
  ### Oversized outputs
209
254
 
210
255
  When a run's output exceeds the size limit (50,000 chars), pi-subagent first tries to **compress** it with the summary model (same role configured under `summary.role`) into a compact form that preserves conclusions, code, file paths, and errors. If compression fails or doesn't shrink enough, it falls back to mechanical head+tail truncation. The prepared text is what the main model receives and what the expanded TUI renders; a hint line notes which method was used. The **full raw output is always kept in the history file** for auditing.
211
256
 
257
+ ### Fallback observability
258
+
259
+ When a provider error (429, quota, timeout, ...) kills a run and the whole task is retried on the role's `fallbackRole`, the retry no longer hides the failure. The first attempt's model, stop reason, error message, and a stderr tail are snapshotted into `fallbackFrom` and surfaced everywhere: a `⚠ fallback:` line in the TUI (collapsed and expanded, including while the retry runs), a `--- fallback: ... ---` note in the tool result the main model reads (on success and failure alike — foreground delegate results and `subagent_check` snapshots), and a `fallbackFrom` field in the history file. When the child dies before its first message (e.g. an instant 429), the reason is recovered from stderr and the model name from what the parent requested.
260
+
212
261
  ### Run history
213
262
 
214
- Every completed delegate run is written (best-effort) to `~/.pi/subagent/history/{sessionId}/{toolCallId}.json`, recording role, task, usage, activity log, and the **full raw output** (even when the main model saw a compressed/truncated version). Useful for auditing what subagents did and how much they cost. Disable with `history.enabled: false`.
263
+ Every completed delegate run is written (best-effort) to `~/.pi/subagent/history/{sessionId}/{toolCallId}.json`, recording role, task, usage, activity log, the **full raw output** (even when the main model saw a compressed/truncated version), and the `fallbackFrom` snapshot when the run was retried on the fallback role. Useful for auditing what subagents did and how much they cost. Disable with `history.enabled: false`.
215
264
 
216
265
  ## License
217
266
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-subagent",
3
- "version": "0.10.3",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Role-based subagent orchestration for pi — delegates tasks to specialized pi child processes with configurable model roles",
6
6
  "main": "src/index.ts",
@@ -27,7 +27,7 @@
27
27
  }
28
28
  },
29
29
  "dependencies": {
30
- "@d3ara1n/pi-model-roles": "^0.7.0"
30
+ "@d3ara1n/pi-model-roles": "*"
31
31
  },
32
32
  "pi": {
33
33
  "extensions": [
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Configuration loading regression tests.
3
3
  *
4
- * Uses isolated global/project settings roots via PI_AGENT_DIR.
4
+ * Uses isolated global/project settings roots via PI_CODING_AGENT_DIR.
5
5
  * node --test packages/pi-subagent/src/config.test.ts
6
6
  */
7
7
 
@@ -13,7 +13,7 @@ import * as path from "node:path";
13
13
  import { loadSubagentConfig } from "./config.ts";
14
14
  import { DEFAULT_CONFIG } from "./types.ts";
15
15
 
16
- const originalAgentDir = process.env.PI_AGENT_DIR;
16
+ const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
17
17
  const tempRoots: string[] = [];
18
18
 
19
19
  function makeRoot(): { agentDir: string; projectDir: string } {
@@ -23,7 +23,7 @@ function makeRoot(): { agentDir: string; projectDir: string } {
23
23
  const projectDir = path.join(root, "project");
24
24
  fs.mkdirSync(agentDir, { recursive: true });
25
25
  fs.mkdirSync(projectDir, { recursive: true });
26
- process.env.PI_AGENT_DIR = agentDir;
26
+ process.env.PI_CODING_AGENT_DIR = agentDir;
27
27
  return { agentDir, projectDir };
28
28
  }
29
29
 
@@ -37,8 +37,8 @@ function writeSettingsText(dir: string, content: string): void {
37
37
  }
38
38
 
39
39
  afterEach(() => {
40
- if (originalAgentDir === undefined) delete process.env.PI_AGENT_DIR;
41
- else process.env.PI_AGENT_DIR = originalAgentDir;
40
+ if (originalAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
41
+ else process.env.PI_CODING_AGENT_DIR = originalAgentDir;
42
42
  for (const root of tempRoots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
43
43
  });
44
44
 
package/src/config.ts CHANGED
@@ -5,19 +5,13 @@
5
5
  * project overrides global.
6
6
  */
7
7
 
8
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
8
9
  import * as fs from "node:fs";
9
- import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import type { SubagentConfig } from "./types.ts";
12
12
  import { DEFAULT_CONFIG } from "./types.ts";
13
13
  import { normalizeNonNegativeInteger, normalizeNonNegativeNumber } from "./utils.ts";
14
14
 
15
- function getAgentDir(): string {
16
- const envDir = process.env.PI_AGENT_DIR;
17
- if (envDir) return envDir;
18
- return path.join(os.homedir(), ".pi", "agent");
19
- }
20
-
21
15
  function readSettingsFile(filePath: string): any {
22
16
  try {
23
17
  const content = fs.readFileSync(filePath, "utf-8");
package/src/history.ts CHANGED
@@ -42,6 +42,7 @@ export function persistSubagentHistory(
42
42
  output: rawOutput ?? r.output,
43
43
  outputMethod: r.outputMethod,
44
44
  errorMessage: r.errorMessage,
45
+ fallbackFrom: r.fallbackFrom,
45
46
  usage: r.usage,
46
47
  activityLog: r.activityLog,
47
48
  };