@pi-archimedes/subagent 1.1.0 → 1.2.1
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 +96 -0
- package/package.json +4 -3
- package/src/compact.ts +22 -6
- package/src/execute.ts +51 -5
- package/src/index.ts +9 -7
- package/src/spawn.ts +12 -10
- package/src/stream.ts +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @pi-archimedes/subagent
|
|
2
|
+
|
|
3
|
+
Subagent dispatch with live TUI streaming and cost tracking for the [Pi coding agent](https://github.com/earendil-works/pi).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Single & parallel execution** — dispatch one task or fan out multiple tasks across different agents simultaneously
|
|
8
|
+
- **Live TUI streaming** — watch subagent progress in real-time with tool calls, token counts, and cost updates
|
|
9
|
+
- **Agent discovery** — auto-discovers agents from `.pi/agents/*.md` files at project, user, and global scope
|
|
10
|
+
- **Per-agent model override** — each subagent can use its own model, falling back to the parent's selection
|
|
11
|
+
- **Cost tracking** — detailed token usage (input, output, cache read/write) and cost per subagent, emitted through the core bus for the footer to consume
|
|
12
|
+
- **`/agents` command** — full CRUD TUI for managing agent definitions with model picker, tool picker, and cross-scope collision warnings (available via the meta package)
|
|
13
|
+
|
|
14
|
+
## Screenshots
|
|
15
|
+
|
|
16
|
+
### Main view — parallel execution
|
|
17
|
+
|
|
18
|
+
Live progress panel showing two parallel subagents with token stats, cost, and recent output:
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
### Agent details view
|
|
23
|
+
|
|
24
|
+
Browse and inspect agent configurations — name, model, tools, system prompt, and more:
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
### Model selection
|
|
29
|
+
|
|
30
|
+
Pick from all available models registered in Pi's model registry:
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
### Tool selection
|
|
35
|
+
|
|
36
|
+
Toggle which tools are available to an agent from Pi's full toolset:
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pi install @pi-archimedes/subagent
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or install the full [pi-archimedes](../..) meta package for the integrated experience (cost tracking in footer, shared chrome, etc.):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pi install pi-archimedes
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### As a tool
|
|
55
|
+
|
|
56
|
+
The `subagent` tool accepts either a single `task` or an array of `tasks` for parallel execution:
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
{
|
|
60
|
+
"agent": "reviewer", // optional, defaults to "general"
|
|
61
|
+
"task": "review the PR", // single task
|
|
62
|
+
"model": "openrouter/anthropic/claude-4", // optional override
|
|
63
|
+
"cwd": "/path/to/dir" // optional working directory
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Parallel mode:
|
|
68
|
+
|
|
69
|
+
```jsonc
|
|
70
|
+
{
|
|
71
|
+
"tasks": [
|
|
72
|
+
{ "agent": "researcher", "task": "find all usages of foo" },
|
|
73
|
+
{ "agent": "reviewer", "task": "review the implementation plan" }
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### As a command
|
|
79
|
+
|
|
80
|
+
Run `/agents` to open the interactive Agents Manager for creating, editing, and deleting agent definitions.
|
|
81
|
+
|
|
82
|
+
## Agent files
|
|
83
|
+
|
|
84
|
+
Agents are defined as `.md` files with YAML frontmatter, placed in one of:
|
|
85
|
+
|
|
86
|
+
- **Project scope:** `<cwd>/.pi/agents/` — available only in this project
|
|
87
|
+
- **User scope:** `~/.pi/agents/` — available across all projects
|
|
88
|
+
- **Global scope:** installed via packages or extensions
|
|
89
|
+
|
|
90
|
+
Frontmatter supports: `name`, `model`, `tools`, `thinking`, `inheritProjectContext`, `inheritSkills`, `systemPromptMode`, and `systemPrompt`.
|
|
91
|
+
|
|
92
|
+
## Integration
|
|
93
|
+
|
|
94
|
+
When installed via `pi-archimedes` (the meta package), subagent cost events flow through `@pi-archimedes/core/bus` and are consumed by `@pi-archimedes/footer`'s `CostAccumulator`. This merges subagent tokens and cost into the main status bar for a unified view.
|
|
95
|
+
|
|
96
|
+
The `/agents` command is also only registered by the meta package (not by standalone `@pi-archimedes/subagent`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/subagent",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"main": "./src/index.ts",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@pi-archimedes/core": "1.1
|
|
14
|
+
"@pi-archimedes/core": "1.2.1"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"pi": {
|
|
27
27
|
"extensions": [
|
|
28
28
|
"./src/index.ts"
|
|
29
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"image": "https://raw.githubusercontent.com/danielcherubini/pi-archimedes/main/docs/images/subagents-main-view.png"
|
|
30
31
|
}
|
|
31
32
|
}
|
package/src/compact.ts
CHANGED
|
@@ -25,6 +25,17 @@ export function buildActivityLine(
|
|
|
25
25
|
return theme.fg("error", "✗ " + truncLine(data.error, 80));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Finished subagents always show their final status — never a stale
|
|
29
|
+
// tool call from history. This keeps compact view consistent with
|
|
30
|
+
// expanded view ("✓ Done" / "✗ Failed" at the bottom).
|
|
31
|
+
if (data.status === "completed") {
|
|
32
|
+
return theme.fg("success", "✓ Done");
|
|
33
|
+
}
|
|
34
|
+
if (data.status === "failed") {
|
|
35
|
+
return theme.fg("error", "✗ Failed");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Running: show the current tool with live duration
|
|
28
39
|
if (data.currentTool) {
|
|
29
40
|
const arrow = theme.fg("muted", "↳ ");
|
|
30
41
|
const argsPreview = data.currentToolArgs
|
|
@@ -43,25 +54,24 @@ export function buildActivityLine(
|
|
|
43
54
|
return arrow + line;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
//
|
|
57
|
+
// Running, no active tool: show the most recently completed tool call
|
|
47
58
|
if (data.toolCalls && data.toolCalls.length > 0) {
|
|
48
59
|
const lastCall = data.toolCalls[data.toolCalls.length - 1];
|
|
49
60
|
if (lastCall) return theme.fg("muted", "↳ " + lastCall);
|
|
50
61
|
}
|
|
51
62
|
|
|
63
|
+
// Running, no tool history: show first line of streamed output if any
|
|
52
64
|
if (data.finalOutput) {
|
|
53
65
|
const firstLine = data.finalOutput.split("\n")[0] ?? "";
|
|
54
66
|
return theme.fg("muted", "↳ " + truncLine(firstLine, 80));
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
// Running
|
|
69
|
+
// Running, no info yet
|
|
58
70
|
if (data.status === "running") {
|
|
59
71
|
return theme.fg("muted", "↳ Starting...");
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
return
|
|
63
|
-
? theme.fg("success", "✓ Done")
|
|
64
|
-
: theme.fg("error", "✗ Failed");
|
|
74
|
+
return "";
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
// ── Status glyph ────────────────────────────────────────────────────────────
|
|
@@ -143,7 +153,13 @@ export function renderCompactParallel(
|
|
|
143
153
|
const agentName = result.agent ?? "agent-" + i;
|
|
144
154
|
const summary = result.progressSummary ?? { toolCount: 0, tokens: 0, durationMs: 0 };
|
|
145
155
|
const isRunning = progress?.status === "running";
|
|
146
|
-
|
|
156
|
+
// Prefer result.exitCode as the source of truth for completion status
|
|
157
|
+
// (matches expanded view). Only fall back to progress.status when the
|
|
158
|
+
// subagent is still actively running. This prevents stale or misaligned
|
|
159
|
+
// progress from showing the wrong status for a finished subagent.
|
|
160
|
+
const status: "running" | "completed" | "failed" = isRunning
|
|
161
|
+
? "running"
|
|
162
|
+
: result.exitCode === 0 ? "completed" : "failed";
|
|
147
163
|
|
|
148
164
|
const glyph = statusGlyph(isRunning, status);
|
|
149
165
|
const glyphColored = status === "completed"
|
package/src/execute.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface ExecuteOptions {
|
|
|
9
9
|
agentConfig: AgentConfig | undefined;
|
|
10
10
|
task: string;
|
|
11
11
|
model: string | undefined;
|
|
12
|
+
activeModel: string | undefined;
|
|
12
13
|
cwd: string | undefined;
|
|
13
14
|
signal: AbortSignal | undefined;
|
|
14
15
|
onUpdate: ((progress: SubagentProgress) => void) | undefined;
|
|
@@ -30,6 +31,7 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
30
31
|
const child = spawnSubagent({
|
|
31
32
|
task: options.task,
|
|
32
33
|
model: options.model,
|
|
34
|
+
activeModel: options.activeModel,
|
|
33
35
|
cwd: options.cwd,
|
|
34
36
|
signal: options.signal,
|
|
35
37
|
agent: options.agentConfig,
|
|
@@ -65,12 +67,34 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
65
67
|
task: options.task,
|
|
66
68
|
progress: result.progress
|
|
67
69
|
? { ...result.progress, agent: agentName, durationMs }
|
|
68
|
-
:
|
|
70
|
+
: // Defensive: streamEvents should always return a progress, but if not,
|
|
71
|
+
// synthesize one so the parallel renderer stays aligned with results.
|
|
72
|
+
{
|
|
73
|
+
agent: agentName,
|
|
74
|
+
status: result.exitCode === 0 ? "completed" : "failed",
|
|
75
|
+
task: options.task,
|
|
76
|
+
currentTool: undefined,
|
|
77
|
+
currentToolArgs: undefined,
|
|
78
|
+
currentToolStartedAt: undefined,
|
|
79
|
+
toolCount: 0,
|
|
80
|
+
inputTokens: 0,
|
|
81
|
+
outputTokens: 0,
|
|
82
|
+
tokens: 0,
|
|
83
|
+
cost: 0,
|
|
84
|
+
durationMs,
|
|
85
|
+
error: undefined,
|
|
86
|
+
output: undefined,
|
|
87
|
+
recentOutput: undefined,
|
|
88
|
+
toolCalls: undefined,
|
|
89
|
+
model: result.model,
|
|
90
|
+
},
|
|
69
91
|
progressSummary: result.progressSummary
|
|
70
92
|
? { ...result.progressSummary, durationMs }
|
|
71
93
|
: { toolCount: 0, tokens: 0, durationMs },
|
|
72
94
|
};
|
|
73
95
|
} catch (err) {
|
|
96
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
97
|
+
const durationMs = Date.now() - startTime;
|
|
74
98
|
return {
|
|
75
99
|
agent: agentName,
|
|
76
100
|
task: options.task,
|
|
@@ -85,9 +109,31 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
85
109
|
} as SubagentUsage,
|
|
86
110
|
model: undefined,
|
|
87
111
|
finalOutput: undefined,
|
|
88
|
-
error:
|
|
89
|
-
progress
|
|
90
|
-
|
|
112
|
+
error: errorMessage,
|
|
113
|
+
// Always return a valid progress object so the parallel renderer's
|
|
114
|
+
// `details.progress[i]` stays aligned with `details.results[i]`.
|
|
115
|
+
// Returning undefined here would be filtered out and cause index
|
|
116
|
+
// misalignment between results and progress in the parallel view.
|
|
117
|
+
progress: {
|
|
118
|
+
agent: agentName,
|
|
119
|
+
status: "failed",
|
|
120
|
+
task: options.task,
|
|
121
|
+
currentTool: undefined,
|
|
122
|
+
currentToolArgs: undefined,
|
|
123
|
+
currentToolStartedAt: undefined,
|
|
124
|
+
toolCount: 0,
|
|
125
|
+
inputTokens: 0,
|
|
126
|
+
outputTokens: 0,
|
|
127
|
+
tokens: 0,
|
|
128
|
+
cost: 0,
|
|
129
|
+
durationMs,
|
|
130
|
+
error: errorMessage,
|
|
131
|
+
output: undefined,
|
|
132
|
+
recentOutput: undefined,
|
|
133
|
+
toolCalls: undefined,
|
|
134
|
+
model: undefined,
|
|
135
|
+
},
|
|
136
|
+
progressSummary: { toolCount: 0, tokens: 0, durationMs },
|
|
91
137
|
};
|
|
92
138
|
}
|
|
93
139
|
}
|
|
@@ -96,7 +142,7 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
96
142
|
* Execute multiple subagents in parallel.
|
|
97
143
|
*/
|
|
98
144
|
export async function executeParallel(options: {
|
|
99
|
-
tasks: Array<{ agent: string | undefined; agentConfig: AgentConfig | undefined; task: string; model: string | undefined; cwd: string | undefined }>;
|
|
145
|
+
tasks: Array<{ agent: string | undefined; agentConfig: AgentConfig | undefined; task: string; model: string | undefined; activeModel: string | undefined; cwd: string | undefined }>;
|
|
100
146
|
signal: AbortSignal | undefined;
|
|
101
147
|
onUpdate: ((progress: SubagentProgress[]) => void) | undefined;
|
|
102
148
|
}): Promise<SubagentResult[]> {
|
package/src/index.ts
CHANGED
|
@@ -97,9 +97,8 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
97
97
|
agent: t.agent ?? undefined,
|
|
98
98
|
agentConfig: t.agent ? findAgent(agents, t.agent) : undefined,
|
|
99
99
|
task: t.task,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
model: t.model ?? ctx.model?.id,
|
|
100
|
+
model: t.model,
|
|
101
|
+
activeModel: ctx.model?.id,
|
|
103
102
|
cwd: t.cwd ?? undefined,
|
|
104
103
|
})),
|
|
105
104
|
signal: signal ?? undefined,
|
|
@@ -120,7 +119,11 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
120
119
|
details: {
|
|
121
120
|
mode: "parallel",
|
|
122
121
|
results,
|
|
123
|
-
progress
|
|
122
|
+
// progress is always defined for each result (executeSubagent synthesizes
|
|
123
|
+
// a failed-progress object in its catch block) so we keep alignment with
|
|
124
|
+
// results by index. Do NOT filter(Boolean) here — that would misalign
|
|
125
|
+
// details.progress[i] with details.results[i] in the renderer.
|
|
126
|
+
progress: results.map(r => r.progress) as SubagentProgress[],
|
|
124
127
|
},
|
|
125
128
|
};
|
|
126
129
|
}
|
|
@@ -144,9 +147,8 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
144
147
|
agent: params.agent ?? undefined,
|
|
145
148
|
agentConfig,
|
|
146
149
|
task: params.task,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
model: params.model ?? ctx.model?.id,
|
|
150
|
+
model: params.model,
|
|
151
|
+
activeModel: ctx.model?.id,
|
|
150
152
|
cwd: params.cwd ?? undefined,
|
|
151
153
|
signal: signal ?? undefined,
|
|
152
154
|
onUpdate: (progress: SubagentProgress) => {
|
package/src/spawn.ts
CHANGED
|
@@ -19,6 +19,7 @@ process.on("exit", () => {
|
|
|
19
19
|
export interface SpawnOptions {
|
|
20
20
|
task: string;
|
|
21
21
|
model: string | undefined;
|
|
22
|
+
activeModel: string | undefined;
|
|
22
23
|
cwd: string | undefined;
|
|
23
24
|
signal: AbortSignal | undefined;
|
|
24
25
|
agent: AgentConfig | undefined;
|
|
@@ -86,24 +87,25 @@ export function spawnSubagent(options: SpawnOptions): ChildProcess {
|
|
|
86
87
|
"--no-session",
|
|
87
88
|
];
|
|
88
89
|
|
|
89
|
-
//
|
|
90
|
+
// Resolve model with correct priority:
|
|
91
|
+
// 1. frontmatter model (agent.model)
|
|
92
|
+
// 2. explicit tool-call model (options.model)
|
|
93
|
+
// 3. currently active model (options.activeModel)
|
|
94
|
+
// 4. no --model flag → pi default
|
|
95
|
+
const modelToUse = options.agent?.model ?? options.model ?? options.activeModel;
|
|
96
|
+
if (modelToUse) {
|
|
97
|
+
args.push("--model", modelToUse);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Apply other agent config options
|
|
90
101
|
if (options.agent) {
|
|
91
102
|
const agent = options.agent;
|
|
92
|
-
// Agent's model takes precedence; tool-call model as fallback
|
|
93
|
-
const modelToUse = agent.model ?? options.model;
|
|
94
|
-
if (modelToUse) {
|
|
95
|
-
args.push("--model", modelToUse);
|
|
96
|
-
}
|
|
97
103
|
if (agent.thinking) {
|
|
98
104
|
args.push("--thinking", agent.thinking);
|
|
99
105
|
}
|
|
100
106
|
if (agent.tools && agent.tools.length > 0) {
|
|
101
107
|
args.push("--tools", agent.tools.join(","));
|
|
102
108
|
}
|
|
103
|
-
} else if (options.model) {
|
|
104
|
-
// Fallback: tool-call model (which itself falls back to the parent's
|
|
105
|
-
// currently-selected model via ctx.model?.id at the call site).
|
|
106
|
-
args.push("--model", options.model);
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
// Temp files for system prompt
|
package/src/stream.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import type { ChildProcess } from "node:child_process";
|
|
3
3
|
import type { StreamState, SubagentProgress, SubagentResult } from "./types.js";
|
|
4
|
+
import { getBus, Events } from "@pi-archimedes/core/bus";
|
|
4
5
|
import {
|
|
5
6
|
type JsonEvent,
|
|
6
7
|
handleToolStart,
|
|
@@ -124,6 +125,17 @@ export function streamEvents(
|
|
|
124
125
|
case "tool_execution_start": {
|
|
125
126
|
handleToolStart(state, event);
|
|
126
127
|
emitProgress();
|
|
128
|
+
// Forward manage_todo_list writes to the bus for parent widget
|
|
129
|
+
if (event.toolName === "manage_todo_list") {
|
|
130
|
+
const args = event.args as Record<string, unknown> | undefined;
|
|
131
|
+
const todoList = args?.todoList as Array<unknown> | undefined;
|
|
132
|
+
if (Array.isArray(todoList)) {
|
|
133
|
+
getBus().emit(Events.TODOS_UPDATE, {
|
|
134
|
+
source: `subagent:${callbacks.agent ?? "general"}`,
|
|
135
|
+
todos: todoList,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
127
139
|
break;
|
|
128
140
|
}
|
|
129
141
|
case "tool_execution_end": {
|
|
@@ -193,6 +205,11 @@ export function streamEvents(
|
|
|
193
205
|
// Final progress update
|
|
194
206
|
callbacks.onProgress?.(result.progress!);
|
|
195
207
|
|
|
208
|
+
// Clear subagent todos from the bus on process exit
|
|
209
|
+
getBus().emit(Events.TODOS_CLEAR, {
|
|
210
|
+
source: `subagent:${callbacks.agent ?? "general"}`,
|
|
211
|
+
});
|
|
212
|
+
|
|
196
213
|
resolve(result);
|
|
197
214
|
});
|
|
198
215
|
|