@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 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
+ ![subagents main view](../../docs/images/subagents-main-view.png)
21
+
22
+ ### Agent details view
23
+
24
+ Browse and inspect agent configurations — name, model, tools, system prompt, and more:
25
+
26
+ ![subagents agent view](../../docs/images/subagents-agent-view.png)
27
+
28
+ ### Model selection
29
+
30
+ Pick from all available models registered in Pi's model registry:
31
+
32
+ ![subagents model selection](../../docs/images/subagents-model-selection.png)
33
+
34
+ ### Tool selection
35
+
36
+ Toggle which tools are available to an agent from Pi's full toolset:
37
+
38
+ ![subagents tool selection](../../docs/images/subagents-tool-selection.png)
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.0.0",
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.0.0"
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
  }
@@ -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 === "user" ? state.userDir : state.projectDir;
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.agents = [...discovery.user, ...discovery.project];
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 otherScope = state.nameInputScope === "user" ? "project" : "user";
1310
- const otherScopeAgents = otherScope === "user" ? state.userAgents : state.projectAgents;
1311
- const collisionAgent = otherScopeAgents.find((a) => a.name === state.nameInputBuffer.trim());
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 ${otherScope} agent "${collisionAgent.name}" exists and will take precedence`), width));
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
- state.nameInputScope = state.nameInputScope === "user" ? "project" : "user";
1333
- if (state.nameInputScope === "project" && !state.projectDir) {
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 === "user" ? state.userDir : state.projectDir;
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.agents = [...discovery.user, ...discovery.project];
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
- userDir: string; // e.g., ~/.pi/agent/agents
112
- projectDir: string | null; // e.g., .pi/agents or null if not found
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
- // Project agents override user agents with the same name
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
  );