oira666_pi-subagent 0.1.3 → 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 +303 -0
- 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
|
@@ -112,6 +112,309 @@ pi --no-subagent-prevent-cycles # allow cycles (not recommended)
|
|
|
112
112
|
| `PI_SUBAGENT_MAX_PARALLEL_TASKS` | `16` | Max tasks per single call |
|
|
113
113
|
| `PI_SUBAGENT_MAX_CONCURRENCY` | `8` | Max subagents running simultaneously |
|
|
114
114
|
|
|
115
|
+
## CLI Argument Proxying
|
|
116
|
+
|
|
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.
|
|
120
|
+
|
|
121
|
+
**Always forwarded verbatim:**
|
|
122
|
+
|
|
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 |
|
|
135
|
+
|
|
136
|
+
**Forwarded as fallback** (agent frontmatter overrides if set):
|
|
137
|
+
|
|
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 |
|
|
143
|
+
|
|
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`.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Programmatic Usage (JSON RPC)
|
|
153
|
+
|
|
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.
|
|
158
|
+
|
|
159
|
+
### Stream event shape
|
|
160
|
+
|
|
161
|
+
```
|
|
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
|
|
170
|
+
```
|
|
171
|
+
|
|
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
|
+
```
|
|
229
|
+
|
|
230
|
+
### Important notes on stats
|
|
231
|
+
|
|
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.
|
|
241
|
+
|
|
242
|
+
### Annotated example JSON
|
|
243
|
+
|
|
244
|
+
The scenario below: main agent delegates to `code-writer`, which does some file work and then
|
|
245
|
+
delegates to `code-reviwer` before finishing.
|
|
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
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
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
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
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.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
115
418
|
## create-subagent Skill
|
|
116
419
|
|
|
117
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.
|
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";
|