oira666_pi-subagent 0.1.2 → 0.1.4

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
@@ -1,326 +1,427 @@
1
1
  # Pi Subagent
2
2
 
3
- **Delegate tasks to specialized subagents with configurable context modes (`spawn` / `fork`).**
4
-
5
- There are many subagent extensions for pi, this one is mine.
6
-
7
- ## Why Pi Subagent
8
-
9
- **Specialization** — Use tailored agents for specific tasks like refactoring, documentation, or research.
10
-
11
- **Context Control** — Choose `spawn` (fresh context) or `fork` (inherit current session context), depending on the task.
12
-
13
- **Parallel Execution** — Run multiple agents at once.
14
-
15
- **A Simpler Fork** — This extension intentionally keeps the surface area small and predictable compared to heavier implementations. It supports nested delegation with depth/cycle guards, but avoids broader scope-selection complexity. If you want the minimal, “just delegate” experience, this is it.
3
+ Delegate tasks to specialized subagents with configurable context modes (`spawn` / `fork`).
16
4
 
17
5
  ## Install
18
6
 
19
- ### Option 1: Install from npm (recommended)
20
-
21
7
  ```bash
22
8
  pi install npm:oira666_pi-subagent
23
9
  ```
24
10
 
25
- ### Option 2: Install via git
11
+ Or via git:
26
12
 
27
13
  ```bash
28
14
  pi install git:github.com/gee666/pi-subagent.git
29
15
  ```
30
16
 
31
- ### Option 3: Manual Installation
32
-
33
- Clone this repository to your Pi extensions directory:
17
+ ## Remove
34
18
 
35
19
  ```bash
36
- cd ~/.pi/agent/extensions
37
- git clone https://github.com/gee666/pi-subagent.git
38
- cd pi-subagent
39
- npm install
20
+ pi remove npm:oira666_pi-subagent
40
21
  ```
41
22
 
42
- ## Configuration
43
-
44
- ### Delegation Guards (Depth + Cycle Prevention)
23
+ ## How It Works
45
24
 
46
- By default, this extension enforces two runtime guards:
25
+ Each subagent runs as a **separate `pi` process** — isolated memory, its own model/tool loop.
47
26
 
48
- 1. **Depth guard** (`--subagent-max-depth`, default `3`)
49
- - Main agent starts at depth `0`
50
- - Delegation is allowed while `currentDepth < maxDepth`
51
- - With default depth `3`: depth `0`, `1`, and `2` can delegate; depth `3` cannot
52
- 2. **Cycle guard** (`--subagent-prevent-cycles`, default `true`)
53
- - Blocks delegating to any agent name already present in the current delegation stack
54
- - Prevents self-recursion (`writer -> writer`) and loops (`planner -> reviewer -> planner`)
27
+ **`spawn` (default)** Child receives only the task string. Best for isolated work, lower cost.
28
+ **`fork`** Child receives a snapshot of the current session context + task. Best for follow-up work.
55
29
 
56
- You can configure depth with either:
57
-
58
- - CLI flag: `--subagent-max-depth <n>`
59
- - Environment variable: `PI_SUBAGENT_MAX_DEPTH=<n>`
60
-
61
- `n` must be a non-negative integer.
62
-
63
- You can configure cycle prevention with either:
64
-
65
- - CLI flag: `--subagent-prevent-cycles` / `--no-subagent-prevent-cycles`
66
- - Environment variable: `PI_SUBAGENT_PREVENT_CYCLES=true|false`
67
-
68
- Internal env vars managed by the extension and propagated to child processes:
69
-
70
- - `PI_SUBAGENT_DEPTH`
71
- - `PI_SUBAGENT_MAX_DEPTH`
72
- - `PI_SUBAGENT_STACK` (JSON array of ancestor agent names, e.g. `["scout","planner"]`)
73
- - `PI_SUBAGENT_PREVENT_CYCLES`
74
-
75
- Examples:
76
-
77
- ```bash
78
- # Default behavior: depth 3 + cycle prevention enabled
79
- pi
30
+ The main agent receives only the **final text output** from subagents (no tool calls, no reasoning).
80
31
 
81
- # Restrict to one nested level (main -> child -> grandchild)
82
- pi --subagent-max-depth 2
83
-
84
- # Disable subagent delegation entirely
85
- pi --subagent-max-depth 0
86
-
87
- # Allow depth 3 but disable cycle prevention (not recommended)
88
- pi --subagent-max-depth 3 --no-subagent-prevent-cycles
89
- ```
90
-
91
- ### Tool Call Shape
92
-
93
- `subagent` always accepts a top-level `tasks` array:
94
-
95
- - One task = single-agent delegation
96
- - Multiple tasks = parallel delegation
97
-
98
- Single-task example:
32
+ ## Tool Call Shape
99
33
 
100
34
  ```json
101
- { "tasks": [{ "agent": "code-writer", "task": "Implement the API change" }], "mode": "spawn" }
35
+ { "tasks": [{ "agent": "code-writer", "task": "Implement the API" }], "mode": "spawn" }
102
36
  ```
103
37
 
104
- Multi-task example:
38
+ Multiple tasks run in parallel:
105
39
 
106
40
  ```json
107
- { "tasks": [{ "agent": "code-writer", "task": "Draft the implementation" }, { "agent": "code-reviwer", "task": "Review the plan" }], "mode": "fork" }
41
+ {
42
+ "tasks": [
43
+ { "agent": "code-writer", "task": "Draft the implementation" },
44
+ { "agent": "code-reviwer", "task": "Review the plan" }
45
+ ],
46
+ "mode": "fork"
47
+ }
108
48
  ```
109
49
 
110
- Each task item supports:
111
-
112
- - `agent` — subagent name
113
- - `task` — delegated task text
114
- - `cwd` — optional working directory override for that task
115
-
116
- ### Parallel Execution Limits
117
-
118
- For multi-task calls, two environment variables control fan-out:
119
-
120
- - `PI_SUBAGENT_MAX_PARALLEL_TASKS` — maximum number of tasks allowed in one call (default: `16`)
121
- - `PI_SUBAGENT_MAX_CONCURRENCY` — maximum number of subagents running at the same time inside that call (default: `8`)
122
-
123
- `PI_SUBAGENT_MAX_CONCURRENCY` is effectively clamped to at least `1`.
124
-
125
- ### Project-local Agent Confirmation
126
-
127
- Project-local agents from `.pi/agents/*.md` can be gated by `PI_SUBAGENT_CONFIRM_PROJECT_AGENTS`:
128
-
129
- - `true`, `ask`, or `once` (default) — prompt with **Yes once**, **Yes for this session**, or **No**
130
- - `false` or `never` — skip the prompt and allow project-local agents immediately
131
- - `session` — allow project-local agents for the rest of the current session without prompting
132
-
133
- If you choose **Yes for this session** in the UI, the choice is remembered and you will not be asked again in that session. In non-UI mode, `ask` blocks execution because the extension cannot prompt.
134
-
135
- ### Context Mode (`spawn` vs `fork`)
50
+ Each task supports `agent`, `task`, and optional `cwd`.
136
51
 
137
- `subagent` supports a top-level `mode` switch:
52
+ ## Bundled Agents
138
53
 
139
- - `spawn` (default) Child receives only the task string (`Task: ...`). Best for isolated, reproducible work; typically lower token/cost and less context leakage.
140
- - `fork` — Child receives a forked snapshot of the current session context **plus** the task string. Best for follow-up work that depends on prior context; typically higher token/cost and may include sensitive context.
141
-
142
- Quick rule of thumb:
143
-
144
- - Start with `spawn` for one-off tasks.
145
- - Use `fork` when the delegated task depends on the current session's prior discussion, reads, or decisions.
146
-
147
- Examples:
148
-
149
- ```json
150
- { "tasks": [{ "agent": "code-writer", "task": "Implement the migration" }], "mode": "spawn" }
151
- ```
152
-
153
- ```json
154
- { "tasks": [{ "agent": "code-reviwer", "task": "Double-check this migration" }], "mode": "fork" }
155
- ```
156
-
157
- If omitted, mode defaults to `spawn`.
158
-
159
- ### Subagent Definitions
160
-
161
- Subagents are defined as Markdown files with YAML frontmatter.
162
-
163
- **User Agents:** `~/.pi/agent/agents/*.md`
164
- **Project Agents:** `.pi/agents/*.md`
165
- **Bundled Fallback Agents:** `agents/code-writer.md`, `agents/code-reviwer.md`, `agents/code-architect.md`
166
-
167
- The extension always loads user and project agents first. If a project agent shares a name with a user agent, the project agent wins. The bundled fallback agents are only discovered when no user or project agents are found at all. If you have any user or project agents configured, the bundled defaults are hidden and not discoverable. When project agents are requested, Pi can prompt for confirmation before running them, depending on `PI_SUBAGENT_CONFIRM_PROJECT_AGENTS`.
168
-
169
- If nothing is configured yet, these fallback agents are available by default:
54
+ Three fallback agents ship with the extension (used when no user/project agents are configured):
170
55
 
171
56
  - `code-writer` — implementation and refactoring
172
57
  - `code-reviwer` — code review and risk finding
173
58
  - `code-architect` — technical design and approach selection
174
59
 
175
- Example agent (`~/.pi/agent/agents/writer.md`):
60
+ ## Defining Agents
61
+
62
+ Create Markdown files with YAML frontmatter:
63
+
64
+ - **User agents:** `~/.pi/agent/agents/*.md`
65
+ - **Project agents:** `.pi/agents/*.md` *(may prompt for confirmation — see `PI_SUBAGENT_CONFIRM_PROJECT_AGENTS`)*
176
66
 
177
67
  ```markdown
178
68
  ---
179
69
  name: writer
180
- description: Expert technical writer and editor
70
+ description: Expert technical writer
181
71
  model: anthropic/claude-3-5-sonnet
182
- tools: read, write
72
+ thinking: low
73
+ tools: read,write
183
74
  ---
184
75
 
185
- You are an expert technical writer. Your task is to improve the clarity and conciseness of the provided text.
76
+ You are an expert technical writer focused on clarity and conciseness.
186
77
  ```
187
78
 
188
- Note: this repository includes bundled fallback agents in `agents/code-writer.md`, `agents/code-reviwer.md`, and `agents/code-architect.md`.
189
-
190
79
  ### Frontmatter Fields
191
80
 
192
- | Field | Required | Default | Description |
193
- | ------------- | -------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
194
- | `name` | Yes | — | Agent identifier used in tool calls (must match exactly) |
195
- | `description` | Yes | — | What the agent does (shown to the main agent) |
196
- | `model` | No | Uses the default pi model | Overrides the model for this agent. You can include a provider prefix (e.g. `anthropic/claude-3-5-sonnet` or `openrouter/claude-3.5-sonnet`) to force a specific provider. |
197
- | `thinking` | No | Uses Pi's default thinking level | Sets the thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`). Equivalent to `--thinking`. |
198
- | `tools` | No | `read,bash,edit,write` | Comma-separated list of **built-in** tools to enable for this agent. If omitted, defaults apply. |
199
-
200
- Notes:
201
-
202
- - `model` accepts `provider/model` syntax — this is a Pi feature. Use it when multiple providers offer the same model ID.
203
- - `thinking` uses the same values as Pi's `--thinking` flag; it's recommended to set it explicitly since thinking support varies by model.
204
- - `tools` only controls built-in tools. Extension tools remain available unless extensions are disabled.
205
- - The Markdown body below the frontmatter becomes the agent's system prompt and is **appended** to Pi's default system prompt (it does **not** replace it).
81
+ | Field | Required | Default | Description |
82
+ | ------------- | -------- | -------------------- | -------------------------------------------------------- |
83
+ | `name` | Yes | — | Agent identifier used in tool calls |
84
+ | `description` | Yes | — | What the agent does (shown to the main agent) |
85
+ | `model` | No | Pi default | Override model, e.g. `anthropic/claude-3-5-sonnet` |
86
+ | `thinking` | No | Pi default | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
87
+ | `tools` | No | `read,bash,edit,write` | Comma-separated built-in tools |
206
88
 
207
- ### Writing a Good Agent File
89
+ Available tools: `read`, `bash`, `edit`, `write`.
208
90
 
209
- - **Description matters** the main agent uses the `description` to decide which subagent to call, so be specific about what the agent is good at.
210
- - **Tool scope is optional but helpful** — reducing tools can keep the agent focused, but you can leave defaults if unsure.
211
- - **Model + thinking is the power combo** — selecting the right model and thinking level is often the biggest quality boost.
91
+ The Markdown body becomes the agent's system prompt (appended to Pi's default, not replacing it).
212
92
 
213
- ### Available Built-in Tools
93
+ ## Delegation Guards
214
94
 
215
- Available Tools (default: `read`, `bash`, `edit`, `write`):
95
+ Depth and cycle guards prevent runaway recursive delegation.
216
96
 
217
- - `read` Read file contents
218
- - `bash` Execute bash commands
219
- - `edit` Edit files with find/replace
220
- - `write` Write files (creates/overwrites)
221
- - `grep` — Search file contents (read-only, off by default)
222
- - `find` — Find files by glob pattern (read-only, off by default)
223
- - `ls` — List directory contents (read-only, off by default)
97
+ | Config | Default | Description |
98
+ | ------------------------------ | ------- | ------------------------------------------------ |
99
+ | `--subagent-max-depth` / `PI_SUBAGENT_MAX_DEPTH` | `3` | Max delegation depth (0 disables delegation) |
100
+ | `--subagent-prevent-cycles` / `PI_SUBAGENT_PREVENT_CYCLES` | `true` | Block same agent in delegation chain |
224
101
 
225
- Tip: for a read-only tool selection, use `read,find,ls,grep`. As soon as you include `edit`, `write`, or `bash`, the agent can practically go wild.
226
-
227
- ## How Communication Works
102
+ ```bash
103
+ pi --subagent-max-depth 2 # one nested level
104
+ pi --subagent-max-depth 0 # disable delegation entirely
105
+ pi --no-subagent-prevent-cycles # allow cycles (not recommended)
106
+ ```
228
107
 
229
- ### The Isolation Model
108
+ ## Parallel Limits
230
109
 
231
- Each subagent always runs in a **separate `pi` process**:
110
+ | Env Var | Default | Description |
111
+ | -------------------------------- | ------- | ---------------------------------------- |
112
+ | `PI_SUBAGENT_MAX_PARALLEL_TASKS` | `16` | Max tasks per single call |
113
+ | `PI_SUBAGENT_MAX_CONCURRENCY` | `8` | Max subagents running simultaneously |
232
114
 
233
- - No shared memory/state with the parent process
234
- - ❌ No visibility into sibling subagents
235
- - ✅ Its own model/tool/runtime loop
236
- - ✅ Started with `PI_OFFLINE=1` to skip startup network operations and reduce spawn latency
115
+ ## CLI Argument Proxying
237
116
 
238
- What it can see depends on `mode`:
117
+ All flags passed to the parent `pi` process are forwarded to subagent child processes, so they
118
+ inherit the same provider, API key, model, and other runtime settings. Flags the extension manages
119
+ itself are blocked from being forwarded.
239
120
 
240
- - `spawn` (default)
241
- - ✅ Receives: subagent system prompt + `Task: ...`
242
- - ❌ Does **not** receive parent session history
243
- - `fork`
244
- - ✅ Receives: forked snapshot of current parent session context + `Task: ...`
121
+ **Always forwarded verbatim:**
245
122
 
246
- ### What Gets Sent to Subagents
123
+ | Flag(s) | Purpose |
124
+ | --- | --- |
125
+ | `--provider` | AI provider |
126
+ | `--api-key` | API key |
127
+ | `--system-prompt` | Base system prompt override |
128
+ | `--session-dir` | Session storage directory |
129
+ | `--models` | Model cycling list |
130
+ | `--skill`, `--no-skills`/`-ns` | Skill loading |
131
+ | `--prompt-template`, `--no-prompt-templates`/`-np` | Prompt templates |
132
+ | `--theme`, `--no-themes` | Themes |
133
+ | `--verbose` | Verbose startup output |
134
+ | Unknown/custom flags | Forwarded with heuristic value detection |
247
135
 
248
- #### `spawn` mode (default)
136
+ **Forwarded as fallback** (agent frontmatter overrides if set):
249
137
 
250
- `subagent({ tasks: [{ agent: "writer", task: "Document the API" }] })` sends:
138
+ | Flag | Overridden by |
139
+ | --- | --- |
140
+ | `--model` | `model:` in agent frontmatter |
141
+ | `--thinking` | `thinking:` in agent frontmatter |
142
+ | `--tools` / `--no-tools` | `tools:` in agent frontmatter |
251
143
 
252
- ```
253
- [System Prompt from ~/.pi/agent/agents/writer.md]
144
+ **Never forwarded** (managed by the extension itself):
145
+ `--mode`, `-p`/`--print`, `--session`/`--no-session`, `--continue`, `--resume`,
146
+ `--append-system-prompt`, `--offline`, `--extension`/`-e`, `--no-extensions`/`-ne`,
147
+ `--subagent-max-depth`, `--subagent-prevent-cycles`, `--export`, `--list-models`,
148
+ `--help`, `--version`.
254
149
 
255
- User: Task: Document the API
256
- ```
150
+ ---
257
151
 
258
- No parent conversation history is included. In `spawn`, include all required context in `task`.
152
+ ## Programmatic Usage (JSON RPC)
259
153
 
260
- #### `fork` mode
154
+ When running `pi` programmatically with `--mode rpc` (or `--mode json`), the stream contains
155
+ `tool_result_end` events whenever the agent completes a `subagent` tool call. The `details` field
156
+ of these events carries the full stats for that delegation — including recursive usage and tool
157
+ call counts from all subagents in the tree.
261
158
 
262
- `subagent({ tasks: [{ agent: "writer", task: "Document the API" }], mode: "fork" })` sends:
159
+ ### Stream event shape
263
160
 
264
161
  ```
265
- [Forked snapshot of current session context]
266
- [System Prompt from ~/.pi/agent/agents/writer.md]
267
-
268
- User: Task: Document the API
162
+ tool_result_end
163
+ └── message
164
+ ├── role: "toolResult"
165
+ ├── toolName: "subagent"
166
+ ├── toolCallId: string
167
+ ├── isError: boolean
168
+ ├── content: [{ type: "text", text: "<final output>" }]
169
+ └── details: SubagentDetails
269
170
  ```
270
171
 
271
- Note: `fork` copies session context, not transient runtime-only prompt mutations from the parent process.
272
-
273
- ### What Comes Back to the Main Agent
274
-
275
- | Data | Main Agent Sees | TUI Shows |
276
- | --------------------------- | ------------------------ | ---------------------- |
277
- | Final text output | Yes full, unbounded | ✅ Yes |
278
- | Tool calls made by subagent | No | Yes (expanded view) |
279
- | Token usage / cost | ❌ No | ✅ Yes |
280
- | Reasoning/thinking steps | No | No |
281
- | Error messages | ✅ Yes (on failure) | ✅ Yes |
172
+ ### `SubagentDetails` object
173
+
174
+ ```ts
175
+ interface SubagentDetails {
176
+ // Execution metadata
177
+ mode: "single" | "parallel"; // one task vs multiple parallel tasks
178
+ delegationMode: "spawn" | "fork"; // context mode used
179
+ projectAgentsDir: string | null; // path to .pi/agents/ dir if used
180
+
181
+ // Individual agent results (one per task)
182
+ results: SingleResult[];
183
+
184
+ // ── Stats summary (own + all descendants, recursively) ──────────────────
185
+ aggregatedUsage: UsageStats; // token counts and cost, full tree
186
+ aggregatedToolCalls: ToolCallCounts; // { toolName: callCount }, full tree
187
+
188
+ // ── Per-agent breakdown ──────────────────────────────────────────────────
189
+ usageTree: UsageTreeNode[]; // one root node per result
190
+ }
191
+
192
+ interface SingleResult {
193
+ agent: string; // agent name
194
+ agentSource: "user" | "project" | "builtin" | "unknown";
195
+ task: string; // task string passed to this agent
196
+ exitCode: number; // 0 = success, >0 = error, -1 = still running
197
+ messages: Message[]; // full conversation history of the subagent
198
+ stderr: string;
199
+ usage: UsageStats; // this agent's OWN token usage only
200
+ toolCalls: ToolCallCounts; // this agent's OWN tool calls only
201
+ model?: string;
202
+ stopReason?: string; // "end_turn" | "error" | "aborted" | ...
203
+ errorMessage?: string;
204
+ }
205
+
206
+ interface UsageStats {
207
+ input: number; // input tokens
208
+ output: number; // output tokens
209
+ cacheRead: number; // cache read tokens
210
+ cacheWrite: number; // cache write tokens
211
+ cost: number; // total cost in USD
212
+ contextTokens: number; // snapshot: last context window size (not summed in aggregates)
213
+ turns: number; // number of assistant turns
214
+ }
215
+
216
+ // toolName → call count, e.g. { "bash": 5, "read": 3, "subagent": 1 }
217
+ type ToolCallCounts = Record<string, number>;
218
+
219
+ interface UsageTreeNode {
220
+ agent: string;
221
+ task: string;
222
+ ownUsage: UsageStats; // only this agent's turns
223
+ ownToolCalls: ToolCallCounts; // only this agent's tool calls
224
+ aggregatedUsage: UsageStats; // ownUsage + all children recursively
225
+ aggregatedToolCalls: ToolCallCounts; // ownToolCalls + all children recursively
226
+ children: UsageTreeNode[]; // one node per nested subagent invocation
227
+ }
228
+ ```
282
229
 
283
- **Key point:** The main agent receives **only the final assistant text** from each subagent. Not the tool calls, not the reasoning, not the intermediate steps. This prevents context pollution while still giving you the results.
230
+ ### Important notes on stats
284
231
 
285
- ### Parallel Mode Behavior
232
+ - **`SingleResult.usage`** and **`SingleResult.toolCalls`** cover **only that one agent's own work** —
233
+ not its children. Children run in separate processes; their tokens never appear in the parent's usage.
234
+ - **`aggregatedUsage`** / **`aggregatedToolCalls`** on `SubagentDetails` (and on each `UsageTreeNode`)
235
+ are the correct totals to use when you want the cost or tool call count for an entire delegation
236
+ subtree.
237
+ - **`contextTokens`** is a point-in-time snapshot of the context window size at the last turn of that
238
+ agent. It is **not** summed in aggregated stats (it would be meaningless as a cross-process sum).
239
+ - **`toolCalls`** includes **all** tool calls an agent made, including the `"subagent"` call itself.
240
+ You can use the `"subagent"` count to see how many nested delegations an agent spawned.
286
241
 
287
- When running multiple agents in parallel:
242
+ ### Annotated example JSON
288
243
 
289
- - Subagents run concurrently up to `PI_SUBAGENT_MAX_CONCURRENCY` (default `8`)
290
- - The top-level `mode` applies to all tasks in that call
291
- - Main agent receives a combined result after all finish:
244
+ The scenario below: main agent delegates to `code-writer`, which does some file work and then
245
+ delegates to `code-reviwer` before finishing.
292
246
 
247
+ ```json
248
+ {
249
+ "type": "tool_result_end",
250
+ "message": {
251
+ "role": "toolResult",
252
+ "toolName": "subagent",
253
+ "toolCallId": "toolu_01XYZ",
254
+ "isError": false,
255
+ "content": [
256
+ {
257
+ "type": "text",
258
+ "text": "Feature implemented and reviewed. Added validation logic in auth.ts and updated the test suite."
259
+ }
260
+ ],
261
+ "details": {
262
+ "mode": "single",
263
+ "delegationMode": "spawn",
264
+ "projectAgentsDir": null,
265
+
266
+ "aggregatedUsage": {
267
+ "input": 2180,
268
+ "output": 615,
269
+ "cacheRead": 940,
270
+ "cacheWrite": 120,
271
+ "cost": 0.0079,
272
+ "contextTokens": 0,
273
+ "turns": 3
274
+ },
275
+ "aggregatedToolCalls": {
276
+ "read": 3,
277
+ "bash": 2,
278
+ "edit": 1,
279
+ "subagent": 1
280
+ },
281
+
282
+ "usageTree": [
283
+ {
284
+ "agent": "code-writer",
285
+ "task": "Implement the auth feature and have it reviewed",
286
+ "ownUsage": {
287
+ "input": 1380,
288
+ "output": 365,
289
+ "cacheRead": 540,
290
+ "cacheWrite": 120,
291
+ "cost": 0.0058,
292
+ "contextTokens": 2840,
293
+ "turns": 2
294
+ },
295
+ "ownToolCalls": {
296
+ "read": 1,
297
+ "bash": 1,
298
+ "edit": 1,
299
+ "subagent": 1
300
+ },
301
+ "aggregatedUsage": {
302
+ "input": 2180,
303
+ "output": 615,
304
+ "cacheRead": 940,
305
+ "cacheWrite": 120,
306
+ "cost": 0.0079,
307
+ "contextTokens": 0,
308
+ "turns": 3
309
+ },
310
+ "aggregatedToolCalls": {
311
+ "read": 3,
312
+ "bash": 2,
313
+ "edit": 1,
314
+ "subagent": 1
315
+ },
316
+ "children": [
317
+ {
318
+ "agent": "code-reviwer",
319
+ "task": "Review the auth implementation in auth.ts",
320
+ "ownUsage": {
321
+ "input": 800,
322
+ "output": 250,
323
+ "cacheRead": 400,
324
+ "cacheWrite": 0,
325
+ "cost": 0.0021,
326
+ "contextTokens": 1450,
327
+ "turns": 1
328
+ },
329
+ "ownToolCalls": {
330
+ "read": 2,
331
+ "bash": 1
332
+ },
333
+ "aggregatedUsage": {
334
+ "input": 800,
335
+ "output": 250,
336
+ "cacheRead": 400,
337
+ "cacheWrite": 0,
338
+ "cost": 0.0021,
339
+ "contextTokens": 0,
340
+ "turns": 1
341
+ },
342
+ "aggregatedToolCalls": {
343
+ "read": 2,
344
+ "bash": 1
345
+ },
346
+ "children": []
347
+ }
348
+ ]
349
+ }
350
+ ],
351
+
352
+ "results": [
353
+ {
354
+ "agent": "code-writer",
355
+ "agentSource": "builtin",
356
+ "task": "Implement the auth feature and have it reviewed",
357
+ "exitCode": 0,
358
+ "stopReason": "end_turn",
359
+ "model": "claude-opus-4-5",
360
+ "stderr": "",
361
+ "usage": {
362
+ "input": 1380,
363
+ "output": 365,
364
+ "cacheRead": 540,
365
+ "cacheWrite": 120,
366
+ "cost": 0.0058,
367
+ "contextTokens": 2840,
368
+ "turns": 2
369
+ },
370
+ "toolCalls": {
371
+ "read": 1,
372
+ "bash": 1,
373
+ "edit": 1,
374
+ "subagent": 1
375
+ },
376
+ "messages": [
377
+ "... full conversation history of code-writer (includes the nested subagent tool_result) ..."
378
+ ]
379
+ }
380
+ ]
381
+ }
382
+ }
383
+ }
293
384
  ```
294
- Parallel: 3/3 succeeded
295
385
 
296
- [writer] completed: Full output text here...
297
- [tester] completed: Full output text here...
298
- [reviewer] completed: Full output text here...
386
+ ### Collecting stats across an entire session
387
+
388
+ If you are consuming the JSON stream programmatically and want to track the total cost and tool
389
+ usage across all subagent work in a session, listen for every `tool_result_end` event where
390
+ `message.toolName === "subagent"` and sum `message.details.aggregatedUsage` across them.
391
+
392
+ ```js
393
+ let totalCost = 0;
394
+ const totalToolCalls = {};
395
+
396
+ for await (const line of jsonLines) {
397
+ const event = JSON.parse(line);
398
+ if (
399
+ event.type === "tool_result_end" &&
400
+ event.message?.toolName === "subagent" &&
401
+ event.message?.details
402
+ ) {
403
+ const { aggregatedUsage, aggregatedToolCalls } = event.message.details;
404
+ totalCost += aggregatedUsage.cost;
405
+ for (const [tool, count] of Object.entries(aggregatedToolCalls)) {
406
+ totalToolCalls[tool] = (totalToolCalls[tool] ?? 0) + count;
407
+ }
408
+ }
409
+ }
299
410
  ```
300
411
 
301
- ## Features
412
+ Note: if you also track the main agent's own usage from `message_end` events, make sure **not** to
413
+ double-count the subagent costs there — the main agent's own token usage (from its own `message_end`
414
+ events) does not include subagent work; they are always separate processes.
302
415
 
303
- - **Auto-Discovery** — Agents are found at startup and their descriptions are injected into the main agent's system prompt.
304
- - **Context Mode Switch** — `spawn` (fresh context) and `fork` (session snapshot + task) per call.
305
- - **Depth + Cycle Guards** — Depth limiting and ancestry-cycle checks prevent runaway recursive delegation by default.
306
- - **Streaming Updates** — Watch subagent progress in real-time as tool calls and outputs stream in.
307
- - **Nested Delegation** — Subagents can call `subagent` again, subject to depth and cycle guards.
308
- - **Rich TUI Rendering** — Collapsed/expanded views with usage stats, nested delegation trees, tool call previews, and markdown output.
309
- - **Security Confirmation** — Project-local agents can require explicit user approval, with one-time and session-wide approval options.
416
+ ---
310
417
 
311
- ## Project Structure
418
+ ## create-subagent Skill
312
419
 
313
- ```
314
- index.ts — Extension entry point: lifecycle hooks, tool registration, mode orchestration
315
- agents.ts — Agent discovery: reads and parses .md files from user/project directories
316
- runner.ts — Process runner: starts `pi` subprocesses in spawn/fork context modes and streams JSON events
317
- render.ts — TUI rendering: renderCall and renderResult for the subagent tool
318
- types.ts — Shared types and pure helper functions
319
- ```
420
+ If you want the agent to **create new subagent definition files** for itself, install the [`create-subagent` skill](https://github.com/gee666/pi-subagent/tree/main/create-subagent). Once installed, the agent will know how to scaffold new `.md` agent files in the right location with correct frontmatter.
320
421
 
321
422
  ## Attribution
322
423
 
323
- Inspired by implementations from [vaayne/agent-kit](https://github.com/vaayne/agent-kit) and [mariozechner/pi-mono](https://github.com/badlogic/pi-mono).
424
+ Inspired by [vaayne/agent-kit](https://github.com/vaayne/agent-kit) and [mariozechner/pi-mono](https://github.com/badlogic/pi-mono).
324
425
 
325
426
  ## License
326
427
 
package/index.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  type SingleResult,
24
24
  type SubagentDetails,
25
25
  DEFAULT_DELEGATION_MODE,
26
+ buildSubagentDetails,
26
27
  emptyUsage,
27
28
  getFinalOutput,
28
29
  isResultError,
@@ -332,12 +333,8 @@ function makeDetailsFactory(
332
333
  delegationMode: DelegationMode,
333
334
  ) {
334
335
  return (mode: "single" | "parallel") =>
335
- (results: SingleResult[]): SubagentDetails => ({
336
- mode,
337
- delegationMode,
338
- projectAgentsDir,
339
- results,
340
- });
336
+ (results: SingleResult[]): SubagentDetails =>
337
+ buildSubagentDetails(mode, delegationMode, projectAgentsDir, results);
341
338
  }
342
339
 
343
340
  function formatAgentNames(agents: AgentConfig[]): string {
@@ -785,6 +782,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
785
782
  messages: [],
786
783
  stderr: "",
787
784
  usage: emptyUsage(),
785
+ toolCalls: {},
788
786
  }));
789
787
 
790
788
  const emitProgress = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/render.ts CHANGED
@@ -271,7 +271,11 @@ function renderTreeLines(
271
271
  }
272
272
 
273
273
  function topLevelSummary(details: SubagentDetails, counts: TreeCounts): string {
274
- const totalUsage = formatUsage(aggregateUsage(details.results));
274
+ // aggregatedUsage includes own agents + all their nested descendants;
275
+ // fall back to summing only direct results for old serialised data lacking the field.
276
+ const totalUsage = formatUsage(
277
+ details.aggregatedUsage ?? aggregateUsage(details.results),
278
+ );
275
279
  const parts = [
276
280
  `${counts.running} running`,
277
281
  `${counts.finished}/${counts.total} finished`,
package/runner.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  type SingleResult,
17
17
  type SubagentDetails,
18
18
  emptyUsage,
19
+ extractToolCalls,
19
20
  getFinalOutput,
20
21
  getNestedSubagentErrorSummary,
21
22
  } from "./types.js";
@@ -74,32 +75,168 @@ function resolveExtensionArg(value: string): string {
74
75
  return fs.existsSync(resolved) ? resolved : value;
75
76
  }
76
77
 
77
- function getInheritedExtensionArgs(argv: string[]): string[] {
78
- const args: string[] = [];
79
- for (let i = 2; i < argv.length; i++) {
80
- const arg = argv[i];
78
+ interface InheritedCliArgs {
79
+ /** --extension/-e and --no-extensions/-ne args (with path resolution) */
80
+ extensionArgs: string[];
81
+ /** All other non-blocked flags to forward verbatim to every child */
82
+ alwaysProxy: string[];
83
+ /** Parent --model value; used only when agent config doesn't specify model */
84
+ fallbackModel: string | undefined;
85
+ /** Parent --thinking value; used only when agent config doesn't specify thinking */
86
+ fallbackThinking: string | undefined;
87
+ /** Parent --tools value; used only when agent config doesn't specify tools */
88
+ fallbackTools: string | undefined;
89
+ /** Parent passed --no-tools; used only when agent config doesn't specify tools */
90
+ fallbackNoTools: boolean;
91
+ }
92
+
93
+ /**
94
+ * Parse process.argv into categorised groups for child-process arg construction.
95
+ *
96
+ * Categories:
97
+ * - BLOCKED : flags the extension manages itself — never forwarded
98
+ * - extensionArgs : --extension/-e and --no-extensions/-ne (with path resolution)
99
+ * - alwaysProxy : all other non-blocked flags forwarded verbatim
100
+ * - fallback* : flags the agent config may override
101
+ *
102
+ * Handles both "--flag value" and "--flag=value" forms.
103
+ * Unknown flags use a heuristic: if the next token doesn't start with "-",
104
+ * it is treated as the flag's value.
105
+ */
106
+ function parseInheritedCliArgs(argv: string[]): InheritedCliArgs {
107
+ const extensionArgs: string[] = [];
108
+ const alwaysProxy: string[] = [];
109
+ let fallbackModel: string | undefined;
110
+ let fallbackThinking: string | undefined;
111
+ let fallbackTools: string | undefined;
112
+ let fallbackNoTools = false;
113
+
114
+ let i = 2; // skip "node" and "pi"
115
+ while (i < argv.length) {
116
+ const raw = argv[i];
117
+ // Positional args (prompt text, @file refs) — skip, not proxied to children
118
+ if (!raw.startsWith("-")) { i++; continue; }
119
+
120
+ // Normalise: detect --flag=value inline form
121
+ const eqIdx = raw.indexOf("=");
122
+ const flagName = eqIdx !== -1 ? raw.slice(0, eqIdx) : raw;
123
+ const inlineValue: string | undefined = eqIdx !== -1 ? raw.slice(eqIdx + 1) : undefined;
124
+
125
+ const nextToken = argv[i + 1];
126
+ const nextIsValue = nextToken !== undefined && !nextToken.startsWith("-");
127
+
128
+ // Returns [resolvedValue | undefined, tokensToConsume]
129
+ const getVal = (): [string | undefined, number] => {
130
+ if (inlineValue !== undefined) return [inlineValue, 1];
131
+ if (nextIsValue) return [nextToken, 2];
132
+ return [undefined, 1];
133
+ };
134
+
135
+ // ── BLOCKED: value flags ─────────────────────────────────────────────────
136
+ // Extension manages these; consume flag + value, never proxy.
137
+ if ([
138
+ "--mode", "--session", "--append-system-prompt",
139
+ "--export", "--subagent-max-depth",
140
+ ].includes(flagName)) {
141
+ const [, skip] = getVal();
142
+ i += skip; continue;
143
+ }
81
144
 
82
- if (arg === "--no-extensions" || arg === "-ne") {
83
- args.push("--no-extensions");
145
+ // --subagent-prevent-cycles takes an optional value
146
+ if (flagName === "--subagent-prevent-cycles") {
147
+ if (inlineValue !== undefined || nextIsValue) { i += inlineValue !== undefined ? 1 : 2; }
148
+ else { i++; }
84
149
  continue;
85
150
  }
86
151
 
87
- if (arg === "--extension" || arg === "-e") {
88
- const value = argv[i + 1];
89
- if (value !== undefined) {
90
- args.push("--extension", resolveExtensionArg(value));
91
- i++;
92
- }
152
+ // --list-models has an optional search term
153
+ if (flagName === "--list-models") {
154
+ if (inlineValue !== undefined || nextIsValue) { i += inlineValue !== undefined ? 1 : 2; }
155
+ else { i++; }
93
156
  continue;
94
157
  }
95
158
 
96
- if (arg.startsWith("--extension=")) {
97
- args.push("--extension", resolveExtensionArg(arg.slice("--extension=".length)));
159
+ // ── BLOCKED: boolean flags ────────────────────────────────────────────────
160
+ if ([
161
+ "--print", "-p", "--no-session",
162
+ "--continue", "-c", "--resume", "-r",
163
+ "--offline", "--help", "-h", "--version", "-v",
164
+ "--no-subagent-prevent-cycles",
165
+ ].includes(flagName)) {
166
+ i++; continue;
167
+ }
168
+
169
+ // ── EXTENSION FLAGS: handled separately with path resolution ─────────────
170
+ if (flagName === "--no-extensions" || flagName === "-ne") {
171
+ extensionArgs.push(flagName);
172
+ i++; continue;
173
+ }
174
+ if (flagName === "--extension" || flagName === "-e") {
175
+ const [value, skip] = getVal();
176
+ if (value !== undefined) extensionArgs.push(flagName, resolveExtensionArg(value));
177
+ i += skip; continue;
178
+ }
179
+
180
+ // ── ALWAYS-PROXY: known value flags ──────────────────────────────────────
181
+ if ([
182
+ "--provider", "--api-key", "--system-prompt", "--session-dir",
183
+ "--models", "--skill", "--prompt-template", "--theme",
184
+ ].includes(flagName)) {
185
+ const [value, skip] = getVal();
186
+ if (value !== undefined) alwaysProxy.push(flagName, value);
187
+ i += skip; continue;
188
+ }
189
+
190
+ // ── ALWAYS-PROXY: known boolean flags ────────────────────────────────────
191
+ if ([
192
+ "--no-skills", "-ns", "--no-prompt-templates", "-np",
193
+ "--no-themes", "--verbose",
194
+ ].includes(flagName)) {
195
+ alwaysProxy.push(flagName);
196
+ i++; continue;
98
197
  }
198
+
199
+ // ── FALLBACK: agent config may override ───────────────────────────────────
200
+ if (flagName === "--model") {
201
+ const [value, skip] = getVal();
202
+ if (value !== undefined) fallbackModel = value;
203
+ i += skip; continue;
204
+ }
205
+ if (flagName === "--thinking") {
206
+ const [value, skip] = getVal();
207
+ if (value !== undefined) fallbackThinking = value;
208
+ i += skip; continue;
209
+ }
210
+ if (flagName === "--tools") {
211
+ const [value, skip] = getVal();
212
+ if (value !== undefined) fallbackTools = value;
213
+ i += skip; continue;
214
+ }
215
+ if (flagName === "--no-tools") {
216
+ fallbackNoTools = true;
217
+ i++; continue;
218
+ }
219
+
220
+ // ── UNKNOWN: heuristic passthrough ───────────────────────────────────────
221
+ // Likely a custom extension flag. Forward with value if next token looks like one.
222
+ if (inlineValue !== undefined) {
223
+ alwaysProxy.push(flagName, inlineValue);
224
+ i++; continue;
225
+ }
226
+ if (nextIsValue) {
227
+ alwaysProxy.push(flagName, nextToken);
228
+ i += 2; continue;
229
+ }
230
+ alwaysProxy.push(flagName);
231
+ i++;
99
232
  }
100
- return args;
233
+
234
+ return { extensionArgs, alwaysProxy, fallbackModel, fallbackThinking, fallbackTools, fallbackNoTools };
101
235
  }
102
236
 
237
+ /** Cached once — process.argv is immutable at runtime */
238
+ const _inheritedCliArgs = parseInheritedCliArgs(process.argv);
239
+
103
240
  // ---------------------------------------------------------------------------
104
241
  // JSON-line stream processing
105
242
  // ---------------------------------------------------------------------------
@@ -158,7 +295,8 @@ function buildPiArgs(
158
295
  const args: string[] = [
159
296
  "--mode",
160
297
  "json",
161
- ...getInheritedExtensionArgs(process.argv),
298
+ ..._inheritedCliArgs.extensionArgs,
299
+ ..._inheritedCliArgs.alwaysProxy,
162
300
  "-p",
163
301
  ];
164
302
 
@@ -168,10 +306,25 @@ function buildPiArgs(
168
306
  args.push("--session", forkSessionPath);
169
307
  }
170
308
 
171
- if (agent.model) args.push("--model", agent.model);
172
- if (agent.thinking) args.push("--thinking", agent.thinking);
173
- if (agent.tools && agent.tools.length > 0)
309
+ // Agent config takes priority; fall back to parent CLI value
310
+ const model = agent.model ?? _inheritedCliArgs.fallbackModel;
311
+ if (model) args.push("--model", model);
312
+
313
+ const thinking = agent.thinking ?? _inheritedCliArgs.fallbackThinking;
314
+ if (thinking) args.push("--thinking", thinking);
315
+
316
+ // agent.tools is set only when the agent file specifies tools (length > 0)
317
+ if (agent.tools && agent.tools.length > 0) {
174
318
  args.push("--tools", agent.tools.join(","));
319
+ } else if (agent.tools === undefined) {
320
+ // Agent didn't restrict tools — inherit parent's preference
321
+ if (_inheritedCliArgs.fallbackTools !== undefined) {
322
+ args.push("--tools", _inheritedCliArgs.fallbackTools);
323
+ } else if (_inheritedCliArgs.fallbackNoTools) {
324
+ args.push("--no-tools");
325
+ }
326
+ }
327
+
175
328
  if (systemPromptPath) args.push("--append-system-prompt", systemPromptPath);
176
329
  args.push(`Task: ${task}`);
177
330
  return args;
@@ -246,6 +399,7 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
246
399
  messages: [],
247
400
  stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
248
401
  usage: emptyUsage(),
402
+ toolCalls: {},
249
403
  };
250
404
  }
251
405
 
@@ -262,6 +416,7 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
262
416
  stderr:
263
417
  "Cannot run in fork mode: missing parent session snapshot context.",
264
418
  usage: emptyUsage(),
419
+ toolCalls: {},
265
420
  model: agent.model,
266
421
  stopReason: "error",
267
422
  errorMessage:
@@ -277,6 +432,7 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
277
432
  messages: [],
278
433
  stderr: "",
279
434
  usage: emptyUsage(),
435
+ toolCalls: {},
280
436
  model: agent.model,
281
437
  };
282
438
 
@@ -379,6 +535,7 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
379
535
  });
380
536
 
381
537
  result.exitCode = exitCode;
538
+ result.toolCalls = extractToolCalls(result.messages); // populate from parsed messages
382
539
  if (wasAborted) {
383
540
  result.exitCode = 130;
384
541
  result.stopReason = "aborted";
package/types.ts CHANGED
@@ -21,6 +21,9 @@ export interface UsageStats {
21
21
  turns: number;
22
22
  }
23
23
 
24
+ /** Tool calls made by an agent: toolName → call count */
25
+ export type ToolCallCounts = Record<string, number>;
26
+
24
27
  /** Result of a single subagent invocation. */
25
28
  export interface SingleResult {
26
29
  agent: string;
@@ -30,17 +33,40 @@ export interface SingleResult {
30
33
  messages: Message[];
31
34
  stderr: string;
32
35
  usage: UsageStats;
36
+ toolCalls: ToolCallCounts;
33
37
  model?: string;
34
38
  stopReason?: string;
35
39
  errorMessage?: string;
36
40
  }
37
41
 
42
+ /** A node in the per-subagent usage tree (own stats + recursive children) */
43
+ export interface UsageTreeNode {
44
+ agent: string;
45
+ task: string;
46
+ /** Token/cost usage for this agent's own turns only */
47
+ ownUsage: UsageStats;
48
+ /** Tool calls this agent made directly (all tools, including "subagent") */
49
+ ownToolCalls: ToolCallCounts;
50
+ /** ownUsage summed with all descendants recursively */
51
+ aggregatedUsage: UsageStats;
52
+ /** ownToolCalls merged with all descendants recursively */
53
+ aggregatedToolCalls: ToolCallCounts;
54
+ /** Nested subagent invocations, recursively populated */
55
+ children: UsageTreeNode[];
56
+ }
57
+
38
58
  /** Metadata attached to every tool result for rendering. */
39
59
  export interface SubagentDetails {
40
60
  mode: "single" | "parallel";
41
61
  delegationMode: DelegationMode;
42
62
  projectAgentsDir: string | null;
43
63
  results: SingleResult[];
64
+ /** Usage summed across all results and all their nested descendants */
65
+ aggregatedUsage: UsageStats;
66
+ /** Tool calls merged across all results and all their nested descendants */
67
+ aggregatedToolCalls: ToolCallCounts;
68
+ /** Per-agent recursive usage breakdown */
69
+ usageTree: UsageTreeNode[];
44
70
  }
45
71
 
46
72
  /** Nested subagent tool result captured from a delegated run. */
@@ -74,6 +100,97 @@ export function aggregateUsage(results: SingleResult[]): UsageStats {
74
100
  return total;
75
101
  }
76
102
 
103
+ /** Add delta into total in-place (contextTokens is a snapshot—not summed, left as-is in total) */
104
+ export function addUsage(total: UsageStats, delta: UsageStats): void {
105
+ total.input += delta.input;
106
+ total.output += delta.output;
107
+ total.cacheRead += delta.cacheRead;
108
+ total.cacheWrite += delta.cacheWrite;
109
+ total.cost += delta.cost;
110
+ total.turns += delta.turns;
111
+ }
112
+
113
+ /** Merge tool call counts from `source` into `target` in-place */
114
+ export function mergeToolCalls(target: ToolCallCounts, source: ToolCallCounts): void {
115
+ for (const [name, count] of Object.entries(source)) {
116
+ target[name] = (target[name] ?? 0) + count;
117
+ }
118
+ }
119
+
120
+ /** Extract all tool calls made by assistant turns in a message list */
121
+ export function extractToolCalls(messages: Message[]): ToolCallCounts {
122
+ const counts: ToolCallCounts = {};
123
+ for (const msg of messages) {
124
+ if (msg.role !== "assistant") continue;
125
+ for (const part of (msg.content as any[]) ?? []) {
126
+ if ((part as any)?.type !== "toolCall") continue;
127
+ const name: string = typeof (part as any).name === "string" ? (part as any).name : "unknown";
128
+ counts[name] = (counts[name] ?? 0) + 1;
129
+ }
130
+ }
131
+ return counts;
132
+ }
133
+
134
+ /** Build a UsageTreeNode for one result, recursing into nested subagent tool results */
135
+ function buildUsageTreeNode(result: SingleResult): UsageTreeNode {
136
+ const children: UsageTreeNode[] = [];
137
+ for (const nested of getNestedSubagentResults(result.messages)) {
138
+ for (const nestedResult of nested.details.results) {
139
+ children.push(buildUsageTreeNode(nestedResult));
140
+ }
141
+ }
142
+
143
+ const ownUsage = result.usage;
144
+ const ownToolCalls: ToolCallCounts = result.toolCalls ?? extractToolCalls(result.messages);
145
+
146
+ const aggregatedUsage = emptyUsage();
147
+ addUsage(aggregatedUsage, ownUsage);
148
+ for (const child of children) addUsage(aggregatedUsage, child.aggregatedUsage);
149
+
150
+ const aggregatedToolCalls: ToolCallCounts = { ...ownToolCalls };
151
+ for (const child of children) mergeToolCalls(aggregatedToolCalls, child.aggregatedToolCalls);
152
+
153
+ return {
154
+ agent: result.agent,
155
+ task: result.task,
156
+ ownUsage,
157
+ ownToolCalls,
158
+ aggregatedUsage,
159
+ aggregatedToolCalls,
160
+ children,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Construct a complete SubagentDetails with aggregated stats.
166
+ * This replaces the plain object literal previously used by makeDetailsFactory.
167
+ */
168
+ export function buildSubagentDetails(
169
+ mode: "single" | "parallel",
170
+ delegationMode: DelegationMode,
171
+ projectAgentsDir: string | null,
172
+ results: SingleResult[],
173
+ ): SubagentDetails {
174
+ const usageTree = results.map(buildUsageTreeNode);
175
+
176
+ const aggregatedUsage = emptyUsage();
177
+ const aggregatedToolCalls: ToolCallCounts = {};
178
+ for (const node of usageTree) {
179
+ addUsage(aggregatedUsage, node.aggregatedUsage);
180
+ mergeToolCalls(aggregatedToolCalls, node.aggregatedToolCalls);
181
+ }
182
+
183
+ return {
184
+ mode,
185
+ delegationMode,
186
+ projectAgentsDir,
187
+ results,
188
+ aggregatedUsage,
189
+ aggregatedToolCalls,
190
+ usageTree,
191
+ };
192
+ }
193
+
77
194
  /** Whether a result represents an error. */
78
195
  export function isResultError(r: SingleResult): boolean {
79
196
  return r.exitCode > 0 || r.stopReason === "error" || r.stopReason === "aborted";