@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 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.1.0",
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.0"
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
- // Show last completed tool call from history
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 with no tool info yet
69
+ // Running, no info yet
58
70
  if (data.status === "running") {
59
71
  return theme.fg("muted", "↳ Starting...");
60
72
  }
61
73
 
62
- return data.status === "completed"
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
- const status = progress?.status ?? (result.exitCode === 0 ? "completed" : "failed");
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
- : undefined,
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: err instanceof Error ? err.message : String(err),
89
- progress: undefined,
90
- progressSummary: { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime },
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
- // Fall back to the parent's currently-selected model when the caller
101
- // (and the agent frontmatter) didn't specify one.
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: results.map(r => r.progress).filter(Boolean) as SubagentProgress[] | undefined,
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
- // Fall back to the parent's currently-selected model when the caller
148
- // (and the agent frontmatter) didn't specify one.
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
- // Use agent config from frontmatter if available
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