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 +338 -237
- package/index.ts +4 -6
- package/package.json +1 -1
- package/render.ts +5 -1
- package/runner.ts +176 -19
- package/types.ts +117 -0
package/README.md
CHANGED
|
@@ -1,326 +1,427 @@
|
|
|
1
1
|
# Pi Subagent
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
Clone this repository to your Pi extensions directory:
|
|
17
|
+
## Remove
|
|
34
18
|
|
|
35
19
|
```bash
|
|
36
|
-
|
|
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
|
-
##
|
|
43
|
-
|
|
44
|
-
### Delegation Guards (Depth + Cycle Prevention)
|
|
23
|
+
## How It Works
|
|
45
24
|
|
|
46
|
-
|
|
25
|
+
Each subagent runs as a **separate `pi` process** — isolated memory, its own model/tool loop.
|
|
47
26
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
35
|
+
{ "tasks": [{ "agent": "code-writer", "task": "Implement the API" }], "mode": "spawn" }
|
|
102
36
|
```
|
|
103
37
|
|
|
104
|
-
|
|
38
|
+
Multiple tasks run in parallel:
|
|
105
39
|
|
|
106
40
|
```json
|
|
107
|
-
{
|
|
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
|
|
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
|
-
|
|
52
|
+
## Bundled Agents
|
|
138
53
|
|
|
139
|
-
|
|
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
|
-
|
|
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
|
|
70
|
+
description: Expert technical writer
|
|
181
71
|
model: anthropic/claude-3-5-sonnet
|
|
182
|
-
|
|
72
|
+
thinking: low
|
|
73
|
+
tools: read,write
|
|
183
74
|
---
|
|
184
75
|
|
|
185
|
-
You are an expert technical writer
|
|
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
|
|
193
|
-
| ------------- | -------- |
|
|
194
|
-
| `name` | Yes | —
|
|
195
|
-
| `description` | Yes | —
|
|
196
|
-
| `model` | No |
|
|
197
|
-
| `thinking` | No |
|
|
198
|
-
| `tools` | No | `read,bash,edit,write`
|
|
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
|
-
|
|
89
|
+
Available tools: `read`, `bash`, `edit`, `write`.
|
|
208
90
|
|
|
209
|
-
|
|
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
|
-
|
|
93
|
+
## Delegation Guards
|
|
214
94
|
|
|
215
|
-
|
|
95
|
+
Depth and cycle guards prevent runaway recursive delegation.
|
|
216
96
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- `
|
|
220
|
-
- `
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
108
|
+
## Parallel Limits
|
|
230
109
|
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
**Forwarded as fallback** (agent frontmatter overrides if set):
|
|
249
137
|
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
```
|
|
150
|
+
---
|
|
257
151
|
|
|
258
|
-
|
|
152
|
+
## Programmatic Usage (JSON RPC)
|
|
259
153
|
|
|
260
|
-
|
|
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
|
-
|
|
159
|
+
### Stream event shape
|
|
263
160
|
|
|
264
161
|
```
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
230
|
+
### Important notes on stats
|
|
284
231
|
|
|
285
|
-
|
|
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
|
-
|
|
242
|
+
### Annotated example JSON
|
|
288
243
|
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
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
package/render.ts
CHANGED
|
@@ -271,7 +271,11 @@ function renderTreeLines(
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
function topLevelSummary(details: SubagentDetails, counts: TreeCounts): string {
|
|
274
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
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";
|