oira666_pi-subagent 0.1.1 → 0.1.3
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 +59 -261
- package/package.json +1 -1
- package/render.ts +208 -463
- package/runner.ts +14 -0
- package/types.ts +34 -0
package/README.md
CHANGED
|
@@ -1,326 +1,124 @@
|
|
|
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:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
cd ~/.pi/agent/extensions
|
|
37
|
-
git clone https://github.com/gee666/pi-subagent.git
|
|
38
|
-
cd pi-subagent
|
|
39
|
-
npm install
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Configuration
|
|
43
|
-
|
|
44
|
-
### Delegation Guards (Depth + Cycle Prevention)
|
|
45
|
-
|
|
46
|
-
By default, this extension enforces two runtime guards:
|
|
47
|
-
|
|
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`)
|
|
55
|
-
|
|
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:
|
|
17
|
+
## Remove
|
|
76
18
|
|
|
77
19
|
```bash
|
|
78
|
-
|
|
79
|
-
pi
|
|
80
|
-
|
|
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
|
|
20
|
+
pi remove npm:oira666_pi-subagent
|
|
89
21
|
```
|
|
90
22
|
|
|
91
|
-
|
|
23
|
+
## How It Works
|
|
92
24
|
|
|
93
|
-
|
|
25
|
+
Each subagent runs as a **separate `pi` process** — isolated memory, its own model/tool loop.
|
|
94
26
|
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
97
29
|
|
|
98
|
-
|
|
30
|
+
The main agent receives only the **final text output** from subagents (no tool calls, no reasoning).
|
|
99
31
|
|
|
100
|
-
|
|
101
|
-
{ "tasks": [{ "agent": "code-writer", "task": "Implement the API change" }], "mode": "spawn" }
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Multi-task example:
|
|
32
|
+
## Tool Call Shape
|
|
105
33
|
|
|
106
34
|
```json
|
|
107
|
-
{ "tasks": [{ "agent": "code-writer", "task": "
|
|
35
|
+
{ "tasks": [{ "agent": "code-writer", "task": "Implement the API" }], "mode": "spawn" }
|
|
108
36
|
```
|
|
109
37
|
|
|
110
|
-
|
|
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`)
|
|
136
|
-
|
|
137
|
-
`subagent` supports a top-level `mode` switch:
|
|
138
|
-
|
|
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:
|
|
38
|
+
Multiple tasks run in parallel:
|
|
148
39
|
|
|
149
40
|
```json
|
|
150
|
-
{
|
|
41
|
+
{
|
|
42
|
+
"tasks": [
|
|
43
|
+
{ "agent": "code-writer", "task": "Draft the implementation" },
|
|
44
|
+
{ "agent": "code-reviwer", "task": "Review the plan" }
|
|
45
|
+
],
|
|
46
|
+
"mode": "fork"
|
|
47
|
+
}
|
|
151
48
|
```
|
|
152
49
|
|
|
153
|
-
|
|
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`
|
|
50
|
+
Each task supports `agent`, `task`, and optional `cwd`.
|
|
166
51
|
|
|
167
|
-
|
|
52
|
+
## Bundled Agents
|
|
168
53
|
|
|
169
|
-
|
|
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).
|
|
206
|
-
|
|
207
|
-
### Writing a Good Agent File
|
|
208
|
-
|
|
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.
|
|
212
|
-
|
|
213
|
-
### Available Built-in Tools
|
|
214
|
-
|
|
215
|
-
Available Tools (default: `read`, `bash`, `edit`, `write`):
|
|
216
|
-
|
|
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)
|
|
224
|
-
|
|
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
|
|
228
|
-
|
|
229
|
-
### The Isolation Model
|
|
230
|
-
|
|
231
|
-
Each subagent always runs in a **separate `pi` process**:
|
|
232
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
What it can see depends on `mode`:
|
|
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 |
|
|
239
88
|
|
|
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: ...`
|
|
89
|
+
Available tools: `read`, `bash`, `edit`, `write`.
|
|
245
90
|
|
|
246
|
-
|
|
91
|
+
The Markdown body becomes the agent's system prompt (appended to Pi's default, not replacing it).
|
|
247
92
|
|
|
248
|
-
|
|
93
|
+
## Delegation Guards
|
|
249
94
|
|
|
250
|
-
|
|
95
|
+
Depth and cycle guards prevent runaway recursive delegation.
|
|
251
96
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
No parent conversation history is included. In `spawn`, include all required context in `task`.
|
|
259
|
-
|
|
260
|
-
#### `fork` mode
|
|
261
|
-
|
|
262
|
-
`subagent({ tasks: [{ agent: "writer", task: "Document the API" }], mode: "fork" })` sends:
|
|
263
|
-
|
|
264
|
-
```
|
|
265
|
-
[Forked snapshot of current session context]
|
|
266
|
-
[System Prompt from ~/.pi/agent/agents/writer.md]
|
|
267
|
-
|
|
268
|
-
User: Task: Document the API
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
Note: `fork` copies session context, not transient runtime-only prompt mutations from the parent process.
|
|
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 |
|
|
272
101
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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 |
|
|
282
|
-
|
|
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.
|
|
284
|
-
|
|
285
|
-
### Parallel Mode Behavior
|
|
286
|
-
|
|
287
|
-
When running multiple agents in parallel:
|
|
288
|
-
|
|
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:
|
|
292
|
-
|
|
293
|
-
```
|
|
294
|
-
Parallel: 3/3 succeeded
|
|
295
|
-
|
|
296
|
-
[writer] completed: Full output text here...
|
|
297
|
-
[tester] completed: Full output text here...
|
|
298
|
-
[reviewer] completed: Full output text here...
|
|
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)
|
|
299
106
|
```
|
|
300
107
|
|
|
301
|
-
##
|
|
108
|
+
## Parallel Limits
|
|
302
109
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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.
|
|
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 |
|
|
310
114
|
|
|
311
|
-
##
|
|
115
|
+
## create-subagent Skill
|
|
312
116
|
|
|
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
|
-
```
|
|
117
|
+
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
118
|
|
|
321
119
|
## Attribution
|
|
322
120
|
|
|
323
|
-
Inspired by
|
|
121
|
+
Inspired by [vaayne/agent-kit](https://github.com/vaayne/agent-kit) and [mariozechner/pi-mono](https://github.com/badlogic/pi-mono).
|
|
324
122
|
|
|
325
123
|
## License
|
|
326
124
|
|
package/package.json
CHANGED
package/render.ts
CHANGED
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
* TUI rendering for subagent tool calls and results.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
|
7
|
-
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
5
|
+
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
8
6
|
import {
|
|
9
7
|
type DelegationMode,
|
|
10
|
-
type DisplayItem,
|
|
11
8
|
type NestedSubagentResult,
|
|
12
9
|
type SingleResult,
|
|
13
10
|
type SubagentDetails,
|
|
@@ -18,11 +15,36 @@ import {
|
|
|
18
15
|
getFinalOutput,
|
|
19
16
|
getNestedSubagentResults,
|
|
20
17
|
isResultError,
|
|
18
|
+
isSubagentDetails,
|
|
21
19
|
} from "./types.js";
|
|
22
20
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const OUTPUT_PREVIEW_LINE_COUNT = 6;
|
|
22
|
+
|
|
23
|
+
type ThemeFg = (color: string, text: string) => string;
|
|
24
|
+
type NodeStatus = "running" | "success" | "error";
|
|
25
|
+
|
|
26
|
+
interface TreeNode {
|
|
27
|
+
label: string;
|
|
28
|
+
status: NodeStatus;
|
|
29
|
+
meta?: string;
|
|
30
|
+
task?: string;
|
|
31
|
+
outputPreview?: string[];
|
|
32
|
+
children: TreeNode[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface TreeCounts {
|
|
36
|
+
total: number;
|
|
37
|
+
running: number;
|
|
38
|
+
success: number;
|
|
39
|
+
error: number;
|
|
40
|
+
finished: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PendingSubagentCall {
|
|
44
|
+
toolCallId: string;
|
|
45
|
+
mode: DelegationMode;
|
|
46
|
+
tasks: Array<{ agent: string; task?: string }>;
|
|
47
|
+
}
|
|
26
48
|
|
|
27
49
|
// ---------------------------------------------------------------------------
|
|
28
50
|
// Formatting helpers
|
|
@@ -43,243 +65,221 @@ function formatUsage(usage: Partial<UsageStats>, model?: string): string {
|
|
|
43
65
|
(usage.cacheRead || 0) +
|
|
44
66
|
(usage.cacheWrite || 0);
|
|
45
67
|
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
46
|
-
if (totalTokens > 0) parts.push(`
|
|
68
|
+
if (totalTokens > 0) parts.push(`tok:${formatTokens(totalTokens)}`);
|
|
47
69
|
if (usage.input) parts.push(`in:${formatTokens(usage.input)}`);
|
|
48
70
|
if (usage.output) parts.push(`out:${formatTokens(usage.output)}`);
|
|
49
71
|
if (usage.cacheRead) parts.push(`cacheR:${formatTokens(usage.cacheRead)}`);
|
|
50
72
|
if (usage.cacheWrite) parts.push(`cacheW:${formatTokens(usage.cacheWrite)}`);
|
|
51
|
-
if (usage.cost) parts.push(
|
|
73
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
52
74
|
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
53
75
|
if (model) parts.push(model);
|
|
54
|
-
return parts.join(" ");
|
|
76
|
+
return parts.join(" • ");
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
function truncate(text: string, maxLen: number): string {
|
|
58
80
|
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
function shortenPath(p: string): string {
|
|
62
|
-
const home = os.homedir();
|
|
63
|
-
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
83
|
function normalizeDelegationMode(raw: unknown): DelegationMode {
|
|
67
84
|
return raw === "fork" ? "fork" : DEFAULT_DELEGATION_MODE;
|
|
68
85
|
}
|
|
69
86
|
|
|
70
|
-
|
|
87
|
+
function splitOutputLines(text: string): string[] {
|
|
88
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
89
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
71
92
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
function lastNonEmptyLines(text: string, limit: number): string[] {
|
|
94
|
+
return splitOutputLines(text)
|
|
95
|
+
.map((line) => line.trimEnd())
|
|
96
|
+
.filter((line) => line.trim().length > 0)
|
|
97
|
+
.slice(-limit);
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
case "
|
|
84
|
-
|
|
85
|
-
const tasks = Array.isArray(args.tasks) ? args.tasks : [];
|
|
86
|
-
if (tasks.length > 1) {
|
|
87
|
-
const agents = tasks
|
|
88
|
-
.slice(0, 3)
|
|
89
|
-
.map((task) => typeof task?.agent === "string" ? task.agent : "...")
|
|
90
|
-
.join(", ");
|
|
91
|
-
const extra = tasks.length > 3 ? ` +${tasks.length - 3}` : "";
|
|
92
|
-
return fg("toolTitle", "subagent ") + fg("accent", `parallel (${tasks.length})`) + fg("muted", ` [${mode}]`) + fg("dim", agents ? ` ${agents}${extra}` : "");
|
|
93
|
-
}
|
|
94
|
-
const task = tasks[0] as { agent?: string; task?: string } | undefined;
|
|
95
|
-
return fg("toolTitle", "subagent ") + fg("accent", task?.agent || "...") + fg("muted", ` [${mode}]`) + fg("dim", task?.task ? ` ${truncate(task.task, 48)}` : "");
|
|
96
|
-
}
|
|
97
|
-
case "bash": {
|
|
98
|
-
const cmd = (args.command as string) || "...";
|
|
99
|
-
return fg("muted", "$ ") + fg("toolOutput", truncate(cmd, 60));
|
|
100
|
-
}
|
|
101
|
-
case "read": {
|
|
102
|
-
let text = fg("accent", shortenPath(pathArg));
|
|
103
|
-
const offset = args.offset as number | undefined;
|
|
104
|
-
const limit = args.limit as number | undefined;
|
|
105
|
-
if (offset !== undefined || limit !== undefined) {
|
|
106
|
-
const start = offset ?? 1;
|
|
107
|
-
const end = limit !== undefined ? start + limit - 1 : "";
|
|
108
|
-
text += fg("warning", `:${start}${end ? `-${end}` : ""}`);
|
|
109
|
-
}
|
|
110
|
-
return fg("muted", "read ") + text;
|
|
111
|
-
}
|
|
112
|
-
case "write": {
|
|
113
|
-
const lines = ((args.content || "") as string).split("\n").length;
|
|
114
|
-
let text = fg("muted", "write ") + fg("accent", shortenPath(pathArg));
|
|
115
|
-
if (lines > 1) text += fg("dim", ` (${lines} lines)`);
|
|
116
|
-
return text;
|
|
117
|
-
}
|
|
118
|
-
case "edit":
|
|
119
|
-
return fg("muted", "edit ") + fg("accent", shortenPath(pathArg));
|
|
120
|
-
case "ls":
|
|
121
|
-
return fg("muted", "ls ") + fg("accent", shortenPath((args.path || ".") as string));
|
|
122
|
-
case "find":
|
|
123
|
-
return fg("muted", "find ") + fg("accent", (args.pattern || "*") as string) + fg("dim", ` in ${shortenPath((args.path || ".") as string)}`);
|
|
124
|
-
case "grep":
|
|
125
|
-
return fg("muted", "grep ") + fg("accent", `/${(args.pattern || "") as string}/`) + fg("dim", ` in ${shortenPath((args.path || ".") as string)}`);
|
|
100
|
+
function statusEmoji(status: NodeStatus, theme: { fg: ThemeFg }): string {
|
|
101
|
+
switch (status) {
|
|
102
|
+
case "running":
|
|
103
|
+
return theme.fg("warning", "⏳");
|
|
104
|
+
case "error":
|
|
105
|
+
return theme.fg("error", "❌");
|
|
126
106
|
default:
|
|
127
|
-
return fg("
|
|
107
|
+
return theme.fg("success", "✅");
|
|
128
108
|
}
|
|
129
109
|
}
|
|
130
110
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
111
|
+
function statusFromResult(result: SingleResult): NodeStatus {
|
|
112
|
+
if (result.exitCode === -1) return "running";
|
|
113
|
+
return isResultError(result) ? "error" : "success";
|
|
114
|
+
}
|
|
134
115
|
|
|
135
|
-
function
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
116
|
+
function countNodes(nodes: TreeNode[]): TreeCounts {
|
|
117
|
+
const counts: TreeCounts = {
|
|
118
|
+
total: 0,
|
|
119
|
+
running: 0,
|
|
120
|
+
success: 0,
|
|
121
|
+
error: 0,
|
|
122
|
+
finished: 0,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const visit = (node: TreeNode) => {
|
|
126
|
+
counts.total++;
|
|
127
|
+
if (node.status === "running") counts.running++;
|
|
128
|
+
if (node.status === "success") counts.success++;
|
|
129
|
+
if (node.status === "error") counts.error++;
|
|
130
|
+
if (node.status !== "running") counts.finished++;
|
|
131
|
+
for (const child of node.children) visit(child);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
for (const node of nodes) visit(node);
|
|
135
|
+
return counts;
|
|
139
136
|
}
|
|
140
137
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
for (const item of items) {
|
|
144
|
-
count += item.type === "text" ? splitOutputLines(item.text).length : 1;
|
|
145
|
-
}
|
|
146
|
-
return count;
|
|
138
|
+
function hasNestedChildren(nodes: TreeNode[]): boolean {
|
|
139
|
+
return nodes.some((node) => node.children.length > 0 || hasNestedChildren(node.children));
|
|
147
140
|
}
|
|
148
141
|
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
142
|
+
function extractPendingSubagentCalls(messages: SingleResult["messages"]): PendingSubagentCall[] {
|
|
143
|
+
const calls: PendingSubagentCall[] = [];
|
|
144
|
+
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
145
|
+
const message = messages[messageIndex] as any;
|
|
146
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
147
|
+
for (let partIndex = 0; partIndex < message.content.length; partIndex++) {
|
|
148
|
+
const part = message.content[partIndex] as any;
|
|
149
|
+
if (part?.type !== "toolCall" || part?.name !== "subagent") continue;
|
|
150
|
+
const args = part.arguments && typeof part.arguments === "object" ? part.arguments : {};
|
|
151
|
+
const tasks = Array.isArray((args as any).tasks)
|
|
152
|
+
? (args as any).tasks
|
|
153
|
+
.filter((task: any) => task && typeof task.agent === "string")
|
|
154
|
+
.map((task: any) => ({
|
|
155
|
+
agent: task.agent,
|
|
156
|
+
task: typeof task.task === "string" ? task.task : undefined,
|
|
157
|
+
}))
|
|
158
|
+
: [];
|
|
159
|
+
calls.push({
|
|
160
|
+
toolCallId:
|
|
161
|
+
typeof part.toolCallId === "string"
|
|
162
|
+
? part.toolCallId
|
|
163
|
+
: typeof part.id === "string"
|
|
164
|
+
? part.id
|
|
165
|
+
: `${messageIndex}:${partIndex}`,
|
|
166
|
+
mode: normalizeDelegationMode((args as any).mode),
|
|
167
|
+
tasks,
|
|
168
|
+
});
|
|
163
169
|
}
|
|
164
170
|
}
|
|
171
|
+
return calls;
|
|
172
|
+
}
|
|
165
173
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
function buildPendingNodes(call: PendingSubagentCall): TreeNode[] {
|
|
175
|
+
return call.tasks.map((task) => ({
|
|
176
|
+
label: task.agent,
|
|
177
|
+
status: "running",
|
|
178
|
+
meta: call.mode === "fork" ? "fork" : "spawn",
|
|
179
|
+
task: task.task,
|
|
180
|
+
children: [],
|
|
181
|
+
}));
|
|
174
182
|
}
|
|
175
183
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
return isResultError(r) ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
184
|
+
function buildNodesFromNestedResult(nested: NestedSubagentResult): TreeNode[] {
|
|
185
|
+
return nested.details.results.map((result) => buildResultNode(result, nested.details.delegationMode));
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
function buildNestedChildren(result: SingleResult): TreeNode[] {
|
|
189
|
+
const completedByToolCallId = new Map<string, NestedSubagentResult>();
|
|
190
|
+
for (const nested of getNestedSubagentResults(result.messages)) {
|
|
191
|
+
completedByToolCallId.set(nested.toolCallId, nested);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const nodes: TreeNode[] = [];
|
|
195
|
+
for (const call of extractPendingSubagentCalls(result.messages)) {
|
|
196
|
+
const completed = completedByToolCallId.get(call.toolCallId);
|
|
197
|
+
if (completed && isSubagentDetails(completed.details)) {
|
|
198
|
+
nodes.push(...buildNodesFromNestedResult(completed));
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
nodes.push(...buildPendingNodes(call));
|
|
202
|
+
}
|
|
203
|
+
return nodes;
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
function
|
|
198
|
-
items
|
|
199
|
-
theme: { fg: ThemeFg },
|
|
200
|
-
expanded: boolean,
|
|
201
|
-
depth = 0,
|
|
202
|
-
): string[] {
|
|
206
|
+
function buildLeafPreview(result: SingleResult): string[] | undefined {
|
|
207
|
+
const items = getDisplayItems(result.messages);
|
|
203
208
|
const lines: string[] = [];
|
|
204
209
|
for (const item of items) {
|
|
205
|
-
|
|
210
|
+
if (item.type === "text") {
|
|
211
|
+
lines.push(...lastNonEmptyLines(item.text, OUTPUT_PREVIEW_LINE_COUNT));
|
|
212
|
+
}
|
|
206
213
|
}
|
|
207
|
-
|
|
214
|
+
const finalOutput = getFinalOutput(result.messages);
|
|
215
|
+
if (finalOutput) lines.push(...lastNonEmptyLines(finalOutput, OUTPUT_PREVIEW_LINE_COUNT));
|
|
216
|
+
const unique = lines.filter((line, index) => line && lines.indexOf(line) === index);
|
|
217
|
+
return unique.length > 0 ? unique.slice(-OUTPUT_PREVIEW_LINE_COUNT) : undefined;
|
|
208
218
|
}
|
|
209
219
|
|
|
210
|
-
function
|
|
211
|
-
|
|
220
|
+
function buildResultNode(result: SingleResult, delegationMode: DelegationMode): TreeNode {
|
|
221
|
+
const status = statusFromResult(result);
|
|
222
|
+
const usage = formatUsage(result.usage, result.model);
|
|
223
|
+
const metaParts: string[] = [result.agentSource, delegationMode];
|
|
224
|
+
if (usage) metaParts.push(usage);
|
|
225
|
+
if (status === "error") {
|
|
226
|
+
const errorText = result.errorMessage || result.stderr || result.stopReason;
|
|
227
|
+
if (errorText) metaParts.push(truncate(errorText.replace(/\s+/g, " "), 120));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const children = buildNestedChildren(result);
|
|
231
|
+
return {
|
|
232
|
+
label: result.agent,
|
|
233
|
+
status,
|
|
234
|
+
meta: metaParts.join(" • "),
|
|
235
|
+
task: result.task,
|
|
236
|
+
outputPreview: children.length === 0 ? buildLeafPreview(result) : undefined,
|
|
237
|
+
children,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildTopLevelNodes(details: SubagentDetails): TreeNode[] {
|
|
242
|
+
return details.results.map((result) => buildResultNode(result, details.delegationMode));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function renderTreeLines(
|
|
246
|
+
nodes: TreeNode[],
|
|
212
247
|
theme: { fg: ThemeFg },
|
|
213
|
-
|
|
248
|
+
showOutputPreview: boolean,
|
|
214
249
|
depth = 0,
|
|
215
250
|
): string[] {
|
|
216
|
-
const indent = " ".repeat(depth);
|
|
217
|
-
if (details.mode === "single") {
|
|
218
|
-
const result = details.results[0];
|
|
219
|
-
if (!result) return [];
|
|
220
|
-
|
|
221
|
-
const lines: string[] = [];
|
|
222
|
-
let header = `${indent}${theme.fg("muted", "↳ ")}${theme.fg("toolTitle", "subagent ")}${theme.fg("accent", result.agent)} ${statusIcon(result, theme)}${theme.fg("muted", ` [${details.delegationMode}]`)}`;
|
|
223
|
-
const usage = formatUsage(result.usage, result.model);
|
|
224
|
-
if (usage) header += ` ${theme.fg("dim", usage)}`;
|
|
225
|
-
lines.push(header);
|
|
226
|
-
|
|
227
|
-
if (!expanded) return lines;
|
|
228
|
-
|
|
229
|
-
lines.push(`${indent} ${theme.fg("muted", "task: ")}${theme.fg("dim", truncate(result.task, 96))}`);
|
|
230
|
-
const nested = getNestedSubagentResults(result.messages);
|
|
231
|
-
if (nested.length > 0) {
|
|
232
|
-
lines.push(...renderNestedDelegationLines(nested, theme, true, depth + 1));
|
|
233
|
-
}
|
|
234
|
-
const finalLine = firstNonEmptyLine(getFinalOutput(result.messages));
|
|
235
|
-
if (finalLine) {
|
|
236
|
-
lines.push(`${indent} ${theme.fg("muted", "final: ")}${theme.fg("toolOutput", truncate(finalLine, 96))}`);
|
|
237
|
-
}
|
|
238
|
-
return lines;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const { icon, status } = parallelStatus(details);
|
|
242
251
|
const lines: string[] = [];
|
|
243
|
-
let header = `${indent}${theme.fg("muted", "↳ ")}${theme.fg("toolTitle", "parallel delegation ")}${theme.fg("accent", status)} ${theme.fg("muted", `[${details.delegationMode}]`)} ${theme.fg(icon === "✓" ? "success" : icon === "⏳" ? "warning" : "warning", icon)}`;
|
|
244
|
-
const totalUsage = formatUsage(aggregateUsage(details.results));
|
|
245
|
-
if (totalUsage) header += ` ${theme.fg("dim", totalUsage)}`;
|
|
246
|
-
lines.push(header);
|
|
247
252
|
|
|
248
|
-
for (const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
253
|
+
for (const node of nodes) {
|
|
254
|
+
const indent = " ".repeat(depth);
|
|
255
|
+
let line = `${indent}${statusEmoji(node.status, theme)} ${theme.fg("accent", node.label)}`;
|
|
256
|
+
if (node.meta) line += ` ${theme.fg("dim", node.meta)}`;
|
|
252
257
|
lines.push(line);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
lines.push(...renderNestedDelegationLines(nested, theme, true, depth + 2));
|
|
258
|
-
}
|
|
259
|
-
const finalLine = firstNonEmptyLine(getFinalOutput(result.messages));
|
|
260
|
-
if (finalLine) {
|
|
261
|
-
lines.push(`${indent} ${theme.fg("muted", "final: ")}${theme.fg("toolOutput", truncate(finalLine, 88))}`);
|
|
258
|
+
|
|
259
|
+
if (showOutputPreview && node.outputPreview && node.outputPreview.length > 0) {
|
|
260
|
+
for (const outputLine of node.outputPreview) {
|
|
261
|
+
lines.push(`${indent} ${theme.fg("toolOutput", outputLine)}`);
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
+
|
|
265
|
+
if (node.children.length > 0) {
|
|
266
|
+
lines.push(...renderTreeLines(node.children, theme, false, depth + 1));
|
|
267
|
+
}
|
|
264
268
|
}
|
|
265
269
|
|
|
266
270
|
return lines;
|
|
267
271
|
}
|
|
268
272
|
|
|
269
|
-
function
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
return [
|
|
280
|
-
...lines.slice(0, NESTED_PREVIEW_LINE_COUNT),
|
|
281
|
-
theme.fg("muted", `... ${lines.length - NESTED_PREVIEW_LINE_COUNT} more delegation lines`),
|
|
282
|
-
].join("\n");
|
|
273
|
+
function topLevelSummary(details: SubagentDetails, counts: TreeCounts): string {
|
|
274
|
+
const totalUsage = formatUsage(aggregateUsage(details.results));
|
|
275
|
+
const parts = [
|
|
276
|
+
`${counts.running} running`,
|
|
277
|
+
`${counts.finished}/${counts.total} finished`,
|
|
278
|
+
`${counts.success} ok`,
|
|
279
|
+
`${counts.error} error`,
|
|
280
|
+
];
|
|
281
|
+
if (totalUsage) parts.push(totalUsage);
|
|
282
|
+
return parts.join(" • ");
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
// ---------------------------------------------------------------------------
|
|
@@ -288,39 +288,25 @@ function nestedDelegationText(
|
|
|
288
288
|
|
|
289
289
|
export function renderCall(args: Record<string, any>, theme: { fg: ThemeFg; bold: (s: string) => string }): Text {
|
|
290
290
|
const delegationMode = normalizeDelegationMode(args.mode);
|
|
291
|
-
const modeBadge = theme.fg("muted", ` [${delegationMode}]`);
|
|
292
291
|
const tasks = Array.isArray(args.tasks) ? args.tasks : [];
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
for (const t of tasks.slice(0, 3)) {
|
|
300
|
-
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${truncate(t.task, 40)}`)}`;
|
|
301
|
-
}
|
|
302
|
-
if (tasks.length > 3) text += `\n ${theme.fg("muted", `... +${tasks.length - 3} more`)}`;
|
|
303
|
-
return new Text(text, 0, 0);
|
|
292
|
+
const count = tasks.length;
|
|
293
|
+
let text = `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("muted", `[${delegationMode}]`)} ${theme.fg("accent", `${count} task${count === 1 ? "" : "s"}`)}`;
|
|
294
|
+
for (const task of tasks.slice(0, 6)) {
|
|
295
|
+
const agent = typeof task?.agent === "string" ? task.agent : "...";
|
|
296
|
+
const preview = typeof task?.task === "string" ? ` ${truncate(task.task, 56)}` : "";
|
|
297
|
+
text += `\n ${theme.fg("warning", "⏳")} ${theme.fg("accent", agent)}${theme.fg("dim", preview)}`;
|
|
304
298
|
}
|
|
305
|
-
|
|
306
|
-
const singleTask = tasks[0];
|
|
307
|
-
const agentName = singleTask?.agent || args.agent || "...";
|
|
308
|
-
const preview = singleTask?.task ? truncate(singleTask.task, 60) : args.task ? truncate(args.task, 60) : "...";
|
|
309
|
-
let text =
|
|
310
|
-
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
311
|
-
theme.fg("accent", agentName) +
|
|
312
|
-
modeBadge;
|
|
313
|
-
text += `\n ${theme.fg("dim", preview)}`;
|
|
299
|
+
if (tasks.length > 6) text += `\n ${theme.fg("muted", `... +${tasks.length - 6} more`)}`;
|
|
314
300
|
return new Text(text, 0, 0);
|
|
315
301
|
}
|
|
316
302
|
|
|
317
303
|
// ---------------------------------------------------------------------------
|
|
318
|
-
// renderResult — shown after the tool completes
|
|
304
|
+
// renderResult — shown after the tool completes / streams updates
|
|
319
305
|
// ---------------------------------------------------------------------------
|
|
320
306
|
|
|
321
307
|
export function renderResult(
|
|
322
308
|
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
323
|
-
|
|
309
|
+
_expanded: boolean,
|
|
324
310
|
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
325
311
|
): Container | Text {
|
|
326
312
|
const details = result.details as SubagentDetails | undefined;
|
|
@@ -329,267 +315,26 @@ export function renderResult(
|
|
|
329
315
|
return new Text(first?.type === "text" && first.text ? first.text : "(no output)", 0, 0);
|
|
330
316
|
}
|
|
331
317
|
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
return renderSingleResult(details.results[0], delegationMode, expanded, theme);
|
|
337
|
-
}
|
|
338
|
-
return renderParallelResult(details, delegationMode, expanded, theme);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// ---------------------------------------------------------------------------
|
|
342
|
-
// Single-mode result
|
|
343
|
-
// ---------------------------------------------------------------------------
|
|
344
|
-
|
|
345
|
-
function renderSingleResult(
|
|
346
|
-
r: SingleResult,
|
|
347
|
-
delegationMode: DelegationMode,
|
|
348
|
-
expanded: boolean,
|
|
349
|
-
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
350
|
-
): Container | Text {
|
|
351
|
-
const error = isResultError(r);
|
|
352
|
-
const icon = statusIcon(r, theme);
|
|
353
|
-
const displayItems = getDisplayItems(r.messages);
|
|
354
|
-
const finalOutput = getFinalOutput(r.messages);
|
|
355
|
-
const nestedDelegations = nestedDelegationText(r.messages, theme, expanded);
|
|
356
|
-
|
|
357
|
-
if (expanded) {
|
|
358
|
-
return renderSingleExpanded(
|
|
359
|
-
r,
|
|
360
|
-
delegationMode,
|
|
361
|
-
icon,
|
|
362
|
-
error,
|
|
363
|
-
displayItems,
|
|
364
|
-
finalOutput,
|
|
365
|
-
nestedDelegations,
|
|
366
|
-
theme,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
return renderSingleCollapsed(r, delegationMode, icon, error, displayItems, nestedDelegations, theme);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function renderSingleExpanded(
|
|
373
|
-
r: SingleResult,
|
|
374
|
-
delegationMode: DelegationMode,
|
|
375
|
-
icon: string,
|
|
376
|
-
error: boolean,
|
|
377
|
-
displayItems: DisplayItem[],
|
|
378
|
-
finalOutput: string,
|
|
379
|
-
nestedDelegations: string,
|
|
380
|
-
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
381
|
-
): Container {
|
|
382
|
-
const mdTheme = getMarkdownTheme();
|
|
383
|
-
const container = new Container();
|
|
384
|
-
|
|
385
|
-
// Header
|
|
386
|
-
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource}, ${delegationMode})`)}`;
|
|
387
|
-
if (error && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
388
|
-
container.addChild(new Text(header, 0, 0));
|
|
389
|
-
if (error && r.errorMessage) {
|
|
390
|
-
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Task
|
|
394
|
-
container.addChild(new Spacer(1));
|
|
395
|
-
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
396
|
-
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
397
|
-
|
|
398
|
-
if (nestedDelegations) {
|
|
399
|
-
container.addChild(new Spacer(1));
|
|
400
|
-
container.addChild(new Text(theme.fg("muted", "─── Delegation tree ───"), 0, 0));
|
|
401
|
-
container.addChild(new Text(nestedDelegations, 0, 0));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Output
|
|
405
|
-
container.addChild(new Spacer(1));
|
|
406
|
-
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
|
407
|
-
if (displayItems.length === 0 && !finalOutput) {
|
|
408
|
-
container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
|
409
|
-
} else {
|
|
410
|
-
for (const item of displayItems) {
|
|
411
|
-
if (item.type === "toolCall") {
|
|
412
|
-
container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (finalOutput) {
|
|
416
|
-
container.addChild(new Spacer(1));
|
|
417
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Usage
|
|
422
|
-
const usageStr = formatUsage(r.usage, r.model);
|
|
423
|
-
if (usageStr) {
|
|
424
|
-
container.addChild(new Spacer(1));
|
|
425
|
-
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return container;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function renderSingleCollapsed(
|
|
432
|
-
r: SingleResult,
|
|
433
|
-
delegationMode: DelegationMode,
|
|
434
|
-
icon: string,
|
|
435
|
-
error: boolean,
|
|
436
|
-
displayItems: DisplayItem[],
|
|
437
|
-
nestedDelegations: string,
|
|
438
|
-
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
439
|
-
): Text {
|
|
440
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource}, ${delegationMode})`)}`;
|
|
441
|
-
if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
442
|
-
|
|
443
|
-
if (error && r.errorMessage) {
|
|
444
|
-
text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
445
|
-
} else if (displayItems.length === 0) {
|
|
446
|
-
text += `\n${theme.fg("muted", "(no output)")}`;
|
|
447
|
-
} else {
|
|
448
|
-
text += `\n${renderDisplayItems(displayItems, false, theme, COLLAPSED_LINE_COUNT)}`;
|
|
449
|
-
if (countDisplayLines(displayItems) > COLLAPSED_LINE_COUNT) {
|
|
450
|
-
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (nestedDelegations) {
|
|
455
|
-
text += `\n\n${theme.fg("muted", "Delegation tree:")}`;
|
|
456
|
-
text += `\n${nestedDelegations}`;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const usageStr = formatUsage(r.usage, r.model);
|
|
460
|
-
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
461
|
-
return new Text(text, 0, 0);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// ---------------------------------------------------------------------------
|
|
465
|
-
// Parallel-mode result
|
|
466
|
-
// ---------------------------------------------------------------------------
|
|
467
|
-
|
|
468
|
-
function renderParallelResult(
|
|
469
|
-
details: SubagentDetails,
|
|
470
|
-
delegationMode: DelegationMode,
|
|
471
|
-
expanded: boolean,
|
|
472
|
-
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
473
|
-
): Container | Text {
|
|
474
|
-
const running = details.results.filter((r) => r.exitCode === -1).length;
|
|
475
|
-
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
476
|
-
const failCount = details.results.filter((r) => r.exitCode > 0).length;
|
|
477
|
-
const isRunning = running > 0;
|
|
478
|
-
|
|
479
|
-
const icon = isRunning
|
|
318
|
+
const nodes = buildTopLevelNodes(details);
|
|
319
|
+
const counts = countNodes(nodes);
|
|
320
|
+
const showOutputPreview = !hasNestedChildren(nodes);
|
|
321
|
+
const icon = counts.running > 0
|
|
480
322
|
? theme.fg("warning", "⏳")
|
|
481
|
-
:
|
|
482
|
-
? theme.fg("
|
|
483
|
-
: theme.fg("success", "
|
|
484
|
-
|
|
485
|
-
const status = isRunning
|
|
486
|
-
? `${successCount + failCount}/${details.results.length} done, ${running} running`
|
|
487
|
-
: `${successCount}/${details.results.length} tasks`;
|
|
488
|
-
|
|
489
|
-
if (expanded && !isRunning) {
|
|
490
|
-
return renderParallelExpanded(details, delegationMode, icon, status, theme);
|
|
491
|
-
}
|
|
492
|
-
return renderParallelCollapsed(
|
|
493
|
-
details,
|
|
494
|
-
delegationMode,
|
|
495
|
-
icon,
|
|
496
|
-
status,
|
|
497
|
-
isRunning,
|
|
498
|
-
expanded,
|
|
499
|
-
theme,
|
|
500
|
-
);
|
|
501
|
-
}
|
|
323
|
+
: counts.error > 0
|
|
324
|
+
? theme.fg("error", "❌")
|
|
325
|
+
: theme.fg("success", "✅");
|
|
502
326
|
|
|
503
|
-
function renderParallelExpanded(
|
|
504
|
-
details: SubagentDetails,
|
|
505
|
-
delegationMode: DelegationMode,
|
|
506
|
-
icon: string,
|
|
507
|
-
status: string,
|
|
508
|
-
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
509
|
-
): Container {
|
|
510
|
-
const mdTheme = getMarkdownTheme();
|
|
511
327
|
const container = new Container();
|
|
512
328
|
container.addChild(
|
|
513
329
|
new Text(
|
|
514
|
-
`${icon} ${theme.fg("toolTitle", theme.bold("
|
|
330
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("subagent tree "))}${theme.fg("muted", `[${details.delegationMode}]`)} ${theme.fg("dim", topLevelSummary(details, counts))}`,
|
|
515
331
|
0,
|
|
516
332
|
0,
|
|
517
333
|
),
|
|
518
334
|
);
|
|
519
335
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const displayItems = getDisplayItems(r.messages);
|
|
523
|
-
const finalOutput = getFinalOutput(r.messages);
|
|
524
|
-
const nestedDelegations = nestedDelegationText(r.messages, theme, true);
|
|
525
|
-
|
|
526
|
-
container.addChild(new Spacer(1));
|
|
527
|
-
container.addChild(new Text(`${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`, 0, 0));
|
|
528
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
529
|
-
|
|
530
|
-
if (nestedDelegations) {
|
|
531
|
-
container.addChild(new Text(theme.fg("muted", "Delegation tree:"), 0, 0));
|
|
532
|
-
container.addChild(new Text(nestedDelegations, 0, 0));
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
for (const item of displayItems) {
|
|
536
|
-
if (item.type === "toolCall") {
|
|
537
|
-
container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (finalOutput) {
|
|
542
|
-
container.addChild(new Spacer(1));
|
|
543
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const taskUsage = formatUsage(r.usage, r.model);
|
|
547
|
-
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const totalUsage = formatUsage(aggregateUsage(details.results));
|
|
551
|
-
if (totalUsage) {
|
|
552
|
-
container.addChild(new Spacer(1));
|
|
553
|
-
container.addChild(new Text(theme.fg("dim", `Total: ${totalUsage}`), 0, 0));
|
|
554
|
-
}
|
|
336
|
+
container.addChild(new Spacer(1));
|
|
337
|
+
container.addChild(new Text(renderTreeLines(nodes, theme, showOutputPreview).join("\n"), 0, 0));
|
|
555
338
|
|
|
556
339
|
return container;
|
|
557
340
|
}
|
|
558
|
-
|
|
559
|
-
function renderParallelCollapsed(
|
|
560
|
-
details: SubagentDetails,
|
|
561
|
-
delegationMode: DelegationMode,
|
|
562
|
-
icon: string,
|
|
563
|
-
status: string,
|
|
564
|
-
isRunning: boolean,
|
|
565
|
-
expanded: boolean,
|
|
566
|
-
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
567
|
-
): Text {
|
|
568
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}${theme.fg("muted", ` [${delegationMode}]`)}`;
|
|
569
|
-
|
|
570
|
-
for (const r of details.results) {
|
|
571
|
-
const rIcon = statusIcon(r, theme);
|
|
572
|
-
const displayItems = getDisplayItems(r.messages);
|
|
573
|
-
const nestedDelegations = nestedDelegationText(r.messages, theme, false);
|
|
574
|
-
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
575
|
-
if (displayItems.length === 0) {
|
|
576
|
-
text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
577
|
-
} else {
|
|
578
|
-
text += `\n${renderDisplayItems(displayItems, false, theme, COLLAPSED_PARALLEL_LINE_COUNT)}`;
|
|
579
|
-
}
|
|
580
|
-
if (nestedDelegations) {
|
|
581
|
-
text += `\n${theme.fg("muted", "Delegation tree:")}`;
|
|
582
|
-
text += `\n${nestedDelegations}`;
|
|
583
|
-
}
|
|
584
|
-
const taskUsage = formatUsage(r.usage, r.model);
|
|
585
|
-
if (taskUsage) text += `\n${theme.fg("dim", taskUsage)}`;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (!isRunning) {
|
|
589
|
-
const totalUsage = formatUsage(aggregateUsage(details.results));
|
|
590
|
-
if (totalUsage) text += `\n\n${theme.fg("dim", `Total: ${totalUsage}`)}`;
|
|
591
|
-
}
|
|
592
|
-
if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
593
|
-
|
|
594
|
-
return new Text(text, 0, 0);
|
|
595
|
-
}
|
package/runner.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type SubagentDetails,
|
|
18
18
|
emptyUsage,
|
|
19
19
|
getFinalOutput,
|
|
20
|
+
getNestedSubagentErrorSummary,
|
|
20
21
|
} from "./types.js";
|
|
21
22
|
|
|
22
23
|
const SIGKILL_TIMEOUT_MS = 5000;
|
|
@@ -291,6 +292,8 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
|
|
|
291
292
|
});
|
|
292
293
|
};
|
|
293
294
|
|
|
295
|
+
emitUpdate();
|
|
296
|
+
|
|
294
297
|
// Write system prompt to temp file if needed
|
|
295
298
|
let promptTmpDir: string | null = null;
|
|
296
299
|
let promptTmpPath: string | null = null;
|
|
@@ -382,6 +385,17 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
|
|
|
382
385
|
result.errorMessage = "Subagent was aborted.";
|
|
383
386
|
if (!result.stderr.trim()) result.stderr = "Subagent was aborted.";
|
|
384
387
|
}
|
|
388
|
+
|
|
389
|
+
if (result.exitCode === 0) {
|
|
390
|
+
const nestedErrorSummary = getNestedSubagentErrorSummary(result.messages);
|
|
391
|
+
if (nestedErrorSummary) {
|
|
392
|
+
result.exitCode = 1;
|
|
393
|
+
result.stopReason = "error";
|
|
394
|
+
result.errorMessage = nestedErrorSummary;
|
|
395
|
+
if (!result.stderr.trim()) result.stderr = nestedErrorSummary;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
385
399
|
return result;
|
|
386
400
|
} finally {
|
|
387
401
|
cleanupTempDir(promptTmpDir);
|
package/types.ts
CHANGED
|
@@ -133,3 +133,37 @@ export function getNestedSubagentResults(messages: Message[]): NestedSubagentRes
|
|
|
133
133
|
}
|
|
134
134
|
return results;
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
function collectSubagentErrorLinesFromDetails(
|
|
138
|
+
details: SubagentDetails,
|
|
139
|
+
lines: string[],
|
|
140
|
+
prefix = "",
|
|
141
|
+
): void {
|
|
142
|
+
for (const result of details.results) {
|
|
143
|
+
if (isResultError(result)) {
|
|
144
|
+
const reason = result.errorMessage || result.stderr || result.stopReason || "failed";
|
|
145
|
+
lines.push(`${prefix}${result.agent}: ${reason}`);
|
|
146
|
+
}
|
|
147
|
+
const nested = getNestedSubagentResults(result.messages);
|
|
148
|
+
for (const child of nested) {
|
|
149
|
+
if (child.isError) {
|
|
150
|
+
collectSubagentErrorLinesFromDetails(
|
|
151
|
+
child.details,
|
|
152
|
+
lines,
|
|
153
|
+
`${prefix}${result.agent} -> `,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Summarize nested subagent failures captured in a message history. */
|
|
161
|
+
export function getNestedSubagentErrorSummary(messages: Message[]): string | null {
|
|
162
|
+
const lines: string[] = [];
|
|
163
|
+
for (const nested of getNestedSubagentResults(messages)) {
|
|
164
|
+
if (!nested.isError) continue;
|
|
165
|
+
collectSubagentErrorLinesFromDetails(nested.details, lines);
|
|
166
|
+
}
|
|
167
|
+
if (lines.length === 0) return null;
|
|
168
|
+
return `Nested subagent failure: ${lines.join("; ")}`;
|
|
169
|
+
}
|