@pi-archimedes/subagent 1.0.0 → 1.2.0
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/agent-manager.ts +49 -14
- package/src/agents.ts +38 -6
- package/src/index.ts +2 -2
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.
|
|
3
|
+
"version": "1.2.0",
|
|
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.
|
|
14
|
+
"@pi-archimedes/core": "1.2.0"
|
|
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/agent-manager.ts
CHANGED
|
@@ -54,8 +54,10 @@ interface ToolInfo {
|
|
|
54
54
|
interface ManagerState {
|
|
55
55
|
screen: "list" | "detail" | "edit" | "name-input" | "confirm-delete";
|
|
56
56
|
agents: AgentConfig[];
|
|
57
|
+
globalAgents: AgentConfig[];
|
|
57
58
|
userAgents: AgentConfig[];
|
|
58
59
|
projectAgents: AgentConfig[];
|
|
60
|
+
globalDir: string | null;
|
|
59
61
|
userDir: string;
|
|
60
62
|
projectDir: string | null;
|
|
61
63
|
|
|
@@ -86,7 +88,7 @@ interface ManagerState {
|
|
|
86
88
|
// Name input state
|
|
87
89
|
nameInputBuffer: string;
|
|
88
90
|
nameInputCursor: number;
|
|
89
|
-
nameInputScope: "user" | "project";
|
|
91
|
+
nameInputScope: "global" | "user" | "project";
|
|
90
92
|
nameInputMode: "new" | "clone";
|
|
91
93
|
nameInputSource: AgentConfig | null;
|
|
92
94
|
nameInputError: string | null;
|
|
@@ -188,7 +190,8 @@ function renderFooter(text: string, width: number, theme: Theme): string {
|
|
|
188
190
|
return theme.fg("dim", padEnd(text, width));
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
function scopeLabel(source: "user" | "project"): string {
|
|
193
|
+
function scopeLabel(source: "global" | "user" | "project"): string {
|
|
194
|
+
if (source === "global") return "home";
|
|
192
195
|
return source === "user" ? "user" : "proj";
|
|
193
196
|
}
|
|
194
197
|
|
|
@@ -1225,7 +1228,9 @@ function saveAgent(state: ManagerState, requestRender: () => void): void {
|
|
|
1225
1228
|
}
|
|
1226
1229
|
|
|
1227
1230
|
// Determine target directory
|
|
1228
|
-
const dir = agent.source === "
|
|
1231
|
+
const dir = agent.source === "global" ? state.globalDir
|
|
1232
|
+
: agent.source === "user" ? state.userDir
|
|
1233
|
+
: state.projectDir;
|
|
1229
1234
|
if (!dir) {
|
|
1230
1235
|
state.editError = "Target directory not available";
|
|
1231
1236
|
requestRender();
|
|
@@ -1259,9 +1264,11 @@ function saveAgent(state: ManagerState, requestRender: () => void): void {
|
|
|
1259
1264
|
// Refresh agents list
|
|
1260
1265
|
const cwd = process.cwd();
|
|
1261
1266
|
const discovery = discoverAgentsAll(cwd);
|
|
1267
|
+
state.globalAgents = discovery.global;
|
|
1262
1268
|
state.userAgents = discovery.user;
|
|
1263
1269
|
state.projectAgents = discovery.project;
|
|
1264
|
-
state.
|
|
1270
|
+
state.globalDir = discovery.globalDir;
|
|
1271
|
+
state.agents = [...discovery.global, ...discovery.user, ...discovery.project];
|
|
1265
1272
|
|
|
1266
1273
|
// Find the saved agent and switch to detail
|
|
1267
1274
|
const savedAgent = state.agents.find((a) => a.name === agent.name && a.source === agent.source);
|
|
@@ -1305,12 +1312,22 @@ function renderNameInput(state: ManagerState, width: number, theme: Theme): stri
|
|
|
1305
1312
|
const scopeText = `Scope: [${state.nameInputScope}] [tab] toggle`;
|
|
1306
1313
|
lines.push(padEnd(theme.fg("dim", scopeText), width));
|
|
1307
1314
|
|
|
1308
|
-
// Cross-scope collision warning
|
|
1309
|
-
const
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1315
|
+
// Cross-scope collision warning — check higher-precedence scopes
|
|
1316
|
+
const scopeOrder = ["global", "user", "project"];
|
|
1317
|
+
const currentIdx = scopeOrder.indexOf(state.nameInputScope);
|
|
1318
|
+
let collisionAgent: AgentConfig | undefined;
|
|
1319
|
+
let collisionScope: string | undefined;
|
|
1320
|
+
for (const scope of scopeOrder.slice(currentIdx + 1)) {
|
|
1321
|
+
const scopeAgents = scope === "global" ? state.globalAgents : scope === "user" ? state.userAgents : state.projectAgents;
|
|
1322
|
+
const found = scopeAgents.find((a) => a.name === state.nameInputBuffer.trim());
|
|
1323
|
+
if (found) {
|
|
1324
|
+
collisionAgent = found;
|
|
1325
|
+
collisionScope = scope;
|
|
1326
|
+
break;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1312
1329
|
if (collisionAgent) {
|
|
1313
|
-
lines.push(padEnd(theme.fg("warning", `Warning: a ${
|
|
1330
|
+
lines.push(padEnd(theme.fg("warning", `Warning: a ${collisionScope} agent "${collisionAgent.name}" exists and will take precedence`), width));
|
|
1314
1331
|
} else if (state.nameInputError) {
|
|
1315
1332
|
lines.push(padEnd(theme.fg("error", ` ${state.nameInputError}`), width));
|
|
1316
1333
|
} else {
|
|
@@ -1329,8 +1346,13 @@ function handleNameInput(
|
|
|
1329
1346
|
requestRender: () => void,
|
|
1330
1347
|
): void {
|
|
1331
1348
|
if (matchesKey(data, Key.tab)) {
|
|
1332
|
-
|
|
1333
|
-
|
|
1349
|
+
const scopes = ["global", "user", "project"];
|
|
1350
|
+
const currentIdx = scopes.indexOf(state.nameInputScope);
|
|
1351
|
+
const nextIdx = (currentIdx + 1) % scopes.length;
|
|
1352
|
+
state.nameInputScope = scopes[nextIdx] as "global" | "user" | "project";
|
|
1353
|
+
if (state.nameInputScope === "global" && !state.globalDir) {
|
|
1354
|
+
state.nameInputError = "No global agents directory found";
|
|
1355
|
+
} else if (state.nameInputScope === "project" && !state.projectDir) {
|
|
1334
1356
|
state.nameInputError = "No project agents directory found";
|
|
1335
1357
|
} else {
|
|
1336
1358
|
state.nameInputError = null;
|
|
@@ -1363,6 +1385,11 @@ function handleNameInput(
|
|
|
1363
1385
|
requestRender();
|
|
1364
1386
|
return;
|
|
1365
1387
|
}
|
|
1388
|
+
if (state.nameInputScope === "global" && !state.globalDir) {
|
|
1389
|
+
state.nameInputError = "No global agents directory found";
|
|
1390
|
+
requestRender();
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1366
1393
|
if (state.nameInputScope === "project" && !state.projectDir) {
|
|
1367
1394
|
state.nameInputError = "No project agents directory found";
|
|
1368
1395
|
requestRender();
|
|
@@ -1379,7 +1406,9 @@ function handleNameInput(
|
|
|
1379
1406
|
return;
|
|
1380
1407
|
}
|
|
1381
1408
|
|
|
1382
|
-
const dir = state.nameInputScope === "
|
|
1409
|
+
const dir = state.nameInputScope === "global" ? state.globalDir
|
|
1410
|
+
: state.nameInputScope === "user" ? state.userDir
|
|
1411
|
+
: state.projectDir;
|
|
1383
1412
|
if (!dir) {
|
|
1384
1413
|
state.nameInputError = "Target directory not available";
|
|
1385
1414
|
requestRender();
|
|
@@ -1486,9 +1515,11 @@ function handleConfirmDelete(
|
|
|
1486
1515
|
// Refresh agents list
|
|
1487
1516
|
const cwd = process.cwd();
|
|
1488
1517
|
const discovery = discoverAgentsAll(cwd);
|
|
1518
|
+
state.globalAgents = discovery.global;
|
|
1489
1519
|
state.userAgents = discovery.user;
|
|
1490
1520
|
state.projectAgents = discovery.project;
|
|
1491
|
-
state.
|
|
1521
|
+
state.globalDir = discovery.globalDir;
|
|
1522
|
+
state.agents = [...discovery.global, ...discovery.user, ...discovery.project];
|
|
1492
1523
|
|
|
1493
1524
|
state.listCursor = 0;
|
|
1494
1525
|
state.listScroll = 0;
|
|
@@ -1506,8 +1537,10 @@ function handleConfirmDelete(
|
|
|
1506
1537
|
// ── Main factory ────────────────────────────────────────────────────────────
|
|
1507
1538
|
|
|
1508
1539
|
export function createAgentManager(
|
|
1540
|
+
globalAgents: AgentConfig[],
|
|
1509
1541
|
userAgents: AgentConfig[],
|
|
1510
1542
|
projectAgents: AgentConfig[],
|
|
1543
|
+
globalDir: string | null,
|
|
1511
1544
|
userDir: string,
|
|
1512
1545
|
projectDir: string | null,
|
|
1513
1546
|
tui: TUIContext,
|
|
@@ -1518,9 +1551,11 @@ export function createAgentManager(
|
|
|
1518
1551
|
): Component {
|
|
1519
1552
|
const state: ManagerState = {
|
|
1520
1553
|
screen: "list",
|
|
1521
|
-
agents: [...userAgents, ...projectAgents],
|
|
1554
|
+
agents: [...globalAgents, ...userAgents, ...projectAgents],
|
|
1555
|
+
globalAgents,
|
|
1522
1556
|
userAgents,
|
|
1523
1557
|
projectAgents,
|
|
1558
|
+
globalDir,
|
|
1524
1559
|
userDir,
|
|
1525
1560
|
projectDir,
|
|
1526
1561
|
|
package/src/agents.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
|
|
@@ -14,13 +15,13 @@ export interface AgentConfig {
|
|
|
14
15
|
model?: string;
|
|
15
16
|
thinking?: string;
|
|
16
17
|
systemPrompt: string;
|
|
17
|
-
source: "user" | "project";
|
|
18
|
+
source: "global" | "user" | "project";
|
|
18
19
|
filePath: string;
|
|
19
20
|
// Extra fields preserved from frontmatter but not editable in TUI
|
|
20
21
|
extraFields?: Record<string, unknown>;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
24
|
+
function loadAgentsFromDir(dir: string, source: "global" | "user" | "project"): AgentConfig[] {
|
|
24
25
|
const agents: AgentConfig[] = [];
|
|
25
26
|
|
|
26
27
|
if (!fs.existsSync(dir)) return agents;
|
|
@@ -102,28 +103,55 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
function findNearestAgentsDir(cwd: string): string | null {
|
|
107
|
+
// Walk up looking for .agents/agents inside a git repo
|
|
108
|
+
let currentDir = cwd;
|
|
109
|
+
while (true) {
|
|
110
|
+
const gitPath = path.join(currentDir, ".git");
|
|
111
|
+
if (fs.existsSync(gitPath)) {
|
|
112
|
+
const projectAgentsDir = path.join(currentDir, ".agents", "agents");
|
|
113
|
+
if (fs.existsSync(projectAgentsDir)) return projectAgentsDir;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const parentDir = path.dirname(currentDir);
|
|
117
|
+
if (parentDir === currentDir) break;
|
|
118
|
+
currentDir = parentDir;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback: ~/.agents/agents
|
|
122
|
+
const homeAgentsDir = path.join(os.homedir(), ".agents", "agents");
|
|
123
|
+
if (fs.existsSync(homeAgentsDir)) return homeAgentsDir;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
105
127
|
/**
|
|
106
128
|
* Result of discovering agents — separated by source with directory paths.
|
|
107
129
|
*/
|
|
108
130
|
export interface AgentsDiscoveryResult {
|
|
131
|
+
global: AgentConfig[];
|
|
109
132
|
user: AgentConfig[];
|
|
110
133
|
project: AgentConfig[];
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
globalDir: string | null; // e.g., .agents/agents or null if not found
|
|
135
|
+
userDir: string; // e.g., ~/.pi/agent/agents
|
|
136
|
+
projectDir: string | null; // e.g., .pi/agents or null if not found
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
/**
|
|
116
|
-
* Discover all available agents from user and/or project directories.
|
|
140
|
+
* Discover all available agents from global, user and/or project directories.
|
|
141
|
+
* Precedence (highest last): global < user < project
|
|
117
142
|
*/
|
|
118
143
|
export function discoverAgents(cwd: string): AgentConfig[] {
|
|
144
|
+
const globalDir = findNearestAgentsDir(cwd);
|
|
119
145
|
const userDir = path.join(getAgentDir(), "agents");
|
|
120
146
|
const projectDir = findNearestProjectAgentsDir(cwd);
|
|
121
147
|
|
|
148
|
+
const globalAgents = globalDir ? loadAgentsFromDir(globalDir, "global") : [];
|
|
122
149
|
const userAgents = loadAgentsFromDir(userDir, "user");
|
|
123
150
|
const projectAgents = projectDir ? loadAgentsFromDir(projectDir, "project") : [];
|
|
124
151
|
|
|
125
|
-
//
|
|
152
|
+
// Later sources override earlier: global < user < project
|
|
126
153
|
const agentMap = new Map<string, AgentConfig>();
|
|
154
|
+
for (const agent of globalAgents) agentMap.set(agent.name, agent);
|
|
127
155
|
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
128
156
|
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
129
157
|
|
|
@@ -134,15 +162,19 @@ export function discoverAgents(cwd: string): AgentConfig[] {
|
|
|
134
162
|
* Discover agents and return them separated by source with directory paths.
|
|
135
163
|
*/
|
|
136
164
|
export function discoverAgentsAll(cwd: string): AgentsDiscoveryResult {
|
|
165
|
+
const globalDir = findNearestAgentsDir(cwd);
|
|
137
166
|
const userDir = path.join(getAgentDir(), "agents");
|
|
138
167
|
const projectDir = findNearestProjectAgentsDir(cwd);
|
|
139
168
|
|
|
169
|
+
const globalAgents = globalDir ? loadAgentsFromDir(globalDir, "global") : [];
|
|
140
170
|
const userAgents = loadAgentsFromDir(userDir, "user");
|
|
141
171
|
const projectAgents = projectDir ? loadAgentsFromDir(projectDir, "project") : [];
|
|
142
172
|
|
|
143
173
|
return {
|
|
174
|
+
global: globalAgents,
|
|
144
175
|
user: userAgents,
|
|
145
176
|
project: projectAgents,
|
|
177
|
+
globalDir,
|
|
146
178
|
userDir,
|
|
147
179
|
projectDir,
|
|
148
180
|
};
|
package/src/index.ts
CHANGED
|
@@ -266,7 +266,7 @@ export function registerAgentsCommand(pi: ExtensionAPI): void {
|
|
|
266
266
|
pi.registerCommand("agents", {
|
|
267
267
|
description: "Open the Agents Manager",
|
|
268
268
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
269
|
-
const { user, project, userDir, projectDir } = discoverAgentsAll(ctx.cwd);
|
|
269
|
+
const { global: globalAgents, user, project, globalDir, userDir, projectDir } = discoverAgentsAll(ctx.cwd);
|
|
270
270
|
|
|
271
271
|
const availableModels = ctx.modelRegistry.getAvailable().map((m) => ({
|
|
272
272
|
id: m.id,
|
|
@@ -281,7 +281,7 @@ export function registerAgentsCommand(pi: ExtensionAPI): void {
|
|
|
281
281
|
|
|
282
282
|
await ctx.ui.custom<void>(
|
|
283
283
|
(tui: TUI, theme: Theme, _keybindings, done: () => void) => {
|
|
284
|
-
return createAgentManager(user, project, userDir, projectDir, tui, theme, done, availableModels, availableTools);
|
|
284
|
+
return createAgentManager(globalAgents, user, project, globalDir, userDir, projectDir, tui, theme, done, availableModels, availableTools);
|
|
285
285
|
},
|
|
286
286
|
{ overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
|
|
287
287
|
);
|