pi-fast-subagent 0.1.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,141 @@
1
+ # pi-fast-subagent
2
+
3
+ In-process subagent delegation for [pi](https://github.com/badlogic/pi-mono).
4
+
5
+ Runs subagents with `createAgentSession()` in same process instead of spawning `pi` subprocesses. This removes subprocess cold-start and reuses pi auth/model registry.
6
+
7
+ ## Features
8
+
9
+ - Single mode: `{ agent, task }`
10
+ - Parallel mode: `{ tasks: [...] }`
11
+ - Chain mode: `{ chain: [...] }`
12
+ - Per-call or per-step model override
13
+ - User + project agent discovery
14
+ - Project agents override user agents
15
+ - Max nesting depth guard
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pi install /absolute/path/to/pi-fast-subagent
21
+ ```
22
+
23
+ Or from npm after publish:
24
+
25
+ ```bash
26
+ pi install npm:pi-fast-subagent
27
+ ```
28
+
29
+ ## Package contents
30
+
31
+ This package exposes one pi extension:
32
+
33
+ - `./index.ts` — registers `subagent` tool
34
+
35
+ ## Included agents
36
+
37
+ This package bundles default agents:
38
+
39
+ - `scout` — code exploration specialist
40
+ - `general` — general-purpose helper
41
+
42
+ Discovery priority:
43
+
44
+ 1. bundled package agents
45
+ 2. `~/.pi/agent/agents/`
46
+ 3. nearest `.pi/agents/`
47
+ 4. nearest legacy `.agents/`
48
+
49
+ User and project agents override bundled agents with same name.
50
+
51
+ Example override agent file:
52
+
53
+ ```md
54
+ ---
55
+ name: scout
56
+ description: Explore codebases and summarize findings
57
+ model: anthropic/claude-haiku-4-5
58
+ ---
59
+
60
+ You are code exploration specialist. Read relevant files, trace data flow, summarize findings clearly.
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ ### List agents
66
+
67
+ ```js
68
+ subagent({ action: "list" })
69
+ ```
70
+
71
+ ### Single
72
+
73
+ ```js
74
+ subagent({
75
+ agent: "scout",
76
+ task: "Explore src and summarize architecture"
77
+ })
78
+ ```
79
+
80
+ ### General-purpose built-in agent
81
+
82
+ ```js
83
+ subagent({
84
+ agent: "general",
85
+ task: "Summarize open TODOs and propose next step"
86
+ })
87
+ ```
88
+
89
+ ### Override model
90
+
91
+ ```js
92
+ subagent({
93
+ agent: "scout",
94
+ task: "Explore src and summarize architecture",
95
+ model: "anthropic/claude-haiku-4-5"
96
+ })
97
+ ```
98
+
99
+ ### Parallel
100
+
101
+ ```js
102
+ subagent({
103
+ tasks: [
104
+ { agent: "scout", task: "Map auth flow" },
105
+ { agent: "scout", task: "Map navigation" }
106
+ ],
107
+ concurrency: 2
108
+ })
109
+ ```
110
+
111
+ ### Chain
112
+
113
+ ```js
114
+ subagent({
115
+ chain: [
116
+ { agent: "scout", task: "Explore app structure" },
117
+ { agent: "scout", task: "Based on this: {previous}\n\nExtract only auth flow." }
118
+ ]
119
+ })
120
+ ```
121
+
122
+ ## Notes
123
+
124
+ - Async/background isolation not supported in-process
125
+ - Git worktree isolation not supported
126
+ - Nested subagent depth limited to 2 by default
127
+
128
+ ## Publish
129
+
130
+ ```bash
131
+ cd ~/.pi/agent/extensions/fast-subagent
132
+ npm publish
133
+ ```
134
+
135
+ If package name is taken, rename `name` in `package.json` first, usually with your npm scope:
136
+
137
+ ```json
138
+ {
139
+ "name": "@your-scope/pi-fast-subagent"
140
+ }
141
+ ```
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: general
3
+ description: General-purpose helper for coding, analysis, writing, debugging, and task execution
4
+ model: anthropic/claude-haiku-4-5
5
+ ---
6
+
7
+ You are general-purpose subagent.
8
+
9
+ Use this agent for focused tasks that do not need specialized behavior.
10
+
11
+ Priorities:
12
+ - follow task exactly
13
+ - stay concise
14
+ - prefer direct answers over long essays
15
+ - use available tools when needed
16
+ - report concrete results, not narration
17
+
18
+ When task involves code:
19
+ - inspect relevant files
20
+ - explain root cause before fix when debugging
21
+ - preserve existing style
22
+ - mention changed files if edits are made
23
+
24
+ When task involves analysis:
25
+ - summarize key findings first
26
+ - list assumptions and unknowns briefly
27
+ - keep recommendations practical
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: scout
3
+ description: Explores codebases, maps structure, traces data flow, answers how things work across many files
4
+ model: anthropic/claude-haiku-4-5
5
+ ---
6
+
7
+ You are code exploration specialist.
8
+
9
+ Goals:
10
+ - understand unfamiliar codebases fast
11
+ - map structure, modules, ownership, and boundaries
12
+ - trace data flow, auth flow, navigation flow, state flow, and side effects
13
+ - summarize findings with concrete file paths and function/component names
14
+
15
+ How to work:
16
+ 1. Start broad. Find top-level structure first.
17
+ 2. Read only files needed to answer task well.
18
+ 3. Prefer facts from code over guesses.
19
+ 4. When tracing flow, name entry point, intermediate layers, and destination.
20
+ 5. Call out uncertainty clearly if code is incomplete.
21
+ 6. Keep output concise but information-dense.
22
+
23
+ Output style:
24
+ - use sections
25
+ - include file paths
26
+ - include short bullets
27
+ - mention notable patterns, risks, and coupling
28
+ - do not propose code changes unless asked
package/agents.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Agent discovery — reads .md files from user + project agent directories.
3
+ * Compatible with pi-subagents frontmatter format.
4
+ */
5
+
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
10
+
11
+ export interface AgentConfig {
12
+ name: string;
13
+ description: string;
14
+ model?: string;
15
+ tools?: string[];
16
+ systemPrompt: string;
17
+ source: "user" | "project";
18
+ filePath: string;
19
+ }
20
+
21
+ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
22
+ if (!fs.existsSync(dir)) return [];
23
+ let entries: fs.Dirent[];
24
+ try {
25
+ entries = fs.readdirSync(dir, { withFileTypes: true });
26
+ } catch {
27
+ return [];
28
+ }
29
+
30
+ const agents: AgentConfig[] = [];
31
+ for (const entry of entries) {
32
+ if (!entry.name.endsWith(".md")) continue;
33
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
34
+ const filePath = path.join(dir, entry.name);
35
+ try {
36
+ const content = fs.readFileSync(filePath, "utf-8");
37
+ const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
38
+ if (!frontmatter?.name || !frontmatter?.description) continue;
39
+ const rawTools = frontmatter.tools;
40
+ const tools = rawTools?.split(",").map((t: string) => t.trim()).filter(Boolean);
41
+ agents.push({
42
+ name: frontmatter.name,
43
+ description: frontmatter.description,
44
+ model: frontmatter.model,
45
+ tools: tools?.length ? tools : undefined,
46
+ systemPrompt: body.trim(),
47
+ source,
48
+ filePath,
49
+ });
50
+ } catch {
51
+ // skip malformed files
52
+ }
53
+ }
54
+ return agents;
55
+ }
56
+
57
+ function findNearestProjectAgentsDir(cwd: string): string | null {
58
+ let dir = cwd;
59
+ while (true) {
60
+ const candidate = path.join(dir, ".pi", "agents");
61
+ try {
62
+ if (fs.statSync(candidate).isDirectory()) return candidate;
63
+ } catch {}
64
+ // Also support legacy .agents/ dir
65
+ const legacy = path.join(dir, ".agents");
66
+ try {
67
+ if (fs.statSync(legacy).isDirectory()) return legacy;
68
+ } catch {}
69
+ const parent = path.dirname(dir);
70
+ if (parent === dir) return null;
71
+ dir = parent;
72
+ }
73
+ }
74
+
75
+ export function discoverAgents(cwd: string): AgentConfig[] {
76
+ const agentMap = new Map<string, AgentConfig>();
77
+
78
+ // Bundled package agents (lowest priority)
79
+ const here = path.dirname(fileURLToPath(import.meta.url));
80
+ const bundledDir = path.join(here, "agents");
81
+ for (const agent of loadAgentsFromDir(bundledDir, "user")) {
82
+ agentMap.set(agent.name, { ...agent, source: "user" });
83
+ }
84
+
85
+ // User agents override bundled agents
86
+ const userDir = path.join(getAgentDir(), "agents");
87
+ for (const agent of loadAgentsFromDir(userDir, "user")) {
88
+ agentMap.set(agent.name, agent);
89
+ }
90
+
91
+ // Project agents override user agents
92
+ const projectDir = findNearestProjectAgentsDir(cwd);
93
+ if (projectDir) {
94
+ for (const agent of loadAgentsFromDir(projectDir, "project")) {
95
+ agentMap.set(agent.name, agent);
96
+ }
97
+ }
98
+
99
+ return Array.from(agentMap.values());
100
+ }
package/index.ts ADDED
@@ -0,0 +1,602 @@
1
+ /**
2
+ * fast-subagent — In-process subagent delegation.
3
+ *
4
+ * Uses createAgentSession() to run subagents in the same process as pi —
5
+ * no subprocess spawn, no cold-start overhead.
6
+ *
7
+ * Drop-in replacement for pi-subagents subprocess mode.
8
+ * Supports: single, parallel, chain.
9
+ * Agent .md files are compatible with pi-subagents frontmatter format.
10
+ */
11
+
12
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
+ import { Text } from "@mariozechner/pi-tui";
14
+ import {
15
+ AuthStorage,
16
+ createAgentSession,
17
+ DefaultResourceLoader,
18
+ getAgentDir,
19
+ ModelRegistry,
20
+ SessionManager,
21
+ } from "@mariozechner/pi-coding-agent";
22
+ import { Type } from "@sinclair/typebox";
23
+ import { type AgentConfig, discoverAgents } from "./agents.js";
24
+
25
+ // ─── Shared auth (created once, reused across calls) ─────────────────────────
26
+
27
+ let _authStorage: ReturnType<typeof AuthStorage.create> | null = null;
28
+ let _modelRegistry: ReturnType<typeof ModelRegistry.create> | null = null;
29
+
30
+ function getAuth() {
31
+ if (!_authStorage) _authStorage = AuthStorage.create();
32
+ if (!_modelRegistry) _modelRegistry = ModelRegistry.create(_authStorage);
33
+ return { authStorage: _authStorage, modelRegistry: _modelRegistry };
34
+ }
35
+
36
+ // ─── In-process runner ───────────────────────────────────────────────────────
37
+
38
+ const MAX_DEPTH = 2;
39
+ const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
40
+
41
+ interface RunResult {
42
+ output: string;
43
+ exitCode: number;
44
+ error?: string;
45
+ model?: string;
46
+ usage: { input: number; output: number; cost: number; turns: number };
47
+ }
48
+
49
+ type OnUpdate = (partial: { content: [{ type: "text"; text: string }]; details: unknown }) => void;
50
+
51
+ function formatDuration(ms: number): string {
52
+ const s = Math.max(0, Math.floor(ms / 1000));
53
+ const m = Math.floor(s / 60);
54
+ const rem = s % 60;
55
+ return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
56
+ }
57
+
58
+ async function runAgent(
59
+ agent: AgentConfig,
60
+ task: string,
61
+ cwd: string,
62
+ modelOverride: string | undefined,
63
+ signal: AbortSignal | undefined,
64
+ onUpdate: OnUpdate | undefined,
65
+ ): Promise<RunResult> {
66
+ const depth = parseInt(process.env[DEPTH_ENV] ?? "0", 10);
67
+ if (depth >= MAX_DEPTH) {
68
+ return {
69
+ output: "",
70
+ exitCode: 1,
71
+ error: `Max subagent depth (${MAX_DEPTH}) exceeded. Increase PI_FAST_SUBAGENT_DEPTH env to allow deeper nesting.`,
72
+ usage: { input: 0, output: 0, cost: 0, turns: 0 },
73
+ };
74
+ }
75
+
76
+ const { authStorage, modelRegistry } = getAuth();
77
+ const agentDir = getAgentDir();
78
+
79
+ // Build resource loader — no extensions/context files to keep subagent lean
80
+ const loaderOptions: ConstructorParameters<typeof DefaultResourceLoader>[0] = {
81
+ cwd,
82
+ agentDir,
83
+ noExtensions: true,
84
+ noContextFiles: true,
85
+ noSkills: true,
86
+ };
87
+ if (agent.systemPrompt) {
88
+ // Replace pi's base system prompt with the agent's own prompt
89
+ loaderOptions.systemPromptOverride = () => agent.systemPrompt;
90
+ }
91
+
92
+ const loader = new DefaultResourceLoader(loaderOptions);
93
+ await loader.reload();
94
+
95
+ const { session } = await createAgentSession({
96
+ cwd,
97
+ agentDir,
98
+ sessionManager: SessionManager.inMemory(cwd),
99
+ authStorage,
100
+ modelRegistry,
101
+ resourceLoader: loader,
102
+ });
103
+
104
+ // Resolve and apply model
105
+ const modelStr = modelOverride ?? agent.model;
106
+ if (modelStr) {
107
+ const [provider, ...rest] = modelStr.split("/");
108
+ const modelId = rest.join("/");
109
+ if (provider && modelId) {
110
+ const model = modelRegistry.find(provider, modelId);
111
+ if (model) await session.setModel(model);
112
+ }
113
+ }
114
+
115
+ // Restrict tools if agent specifies them
116
+ if (agent.tools && agent.tools.length > 0) {
117
+ session.setActiveToolsByName(agent.tools);
118
+ }
119
+
120
+ // Track output and usage
121
+ const usage = { input: 0, output: 0, cost: 0, turns: 0 };
122
+ let lastOutput = "";
123
+ let currentDelta = "";
124
+ let detectedModel: string | undefined;
125
+ const startedAt = Date.now();
126
+ const configuredModel = modelOverride ?? agent.model;
127
+
128
+ onUpdate?.({
129
+ content: [{ type: "text", text: "Starting subagent..." }],
130
+ details: {
131
+ agent: agent.name,
132
+ usage,
133
+ running: true,
134
+ elapsedMs: 0,
135
+ model: configuredModel,
136
+ },
137
+ });
138
+
139
+ const heartbeat = setInterval(() => {
140
+ onUpdate?.({
141
+ content: [{ type: "text", text: currentDelta || lastOutput || "Running..." }],
142
+ details: {
143
+ agent: agent.name,
144
+ usage,
145
+ running: true,
146
+ elapsedMs: Date.now() - startedAt,
147
+ model: detectedModel ?? configuredModel,
148
+ },
149
+ });
150
+ }, 1000);
151
+
152
+ const unsubscribe = session.subscribe((event: any) => {
153
+ // Stream text deltas live to the UI
154
+ if (event.type === "message_update") {
155
+ const e = event.assistantMessageEvent;
156
+ if (e?.type === "text_delta" && e.delta) {
157
+ currentDelta += e.delta;
158
+ onUpdate?.({
159
+ content: [{ type: "text", text: currentDelta }],
160
+ details: {
161
+ agent: agent.name,
162
+ usage,
163
+ running: true,
164
+ elapsedMs: Date.now() - startedAt,
165
+ model: detectedModel ?? configuredModel,
166
+ },
167
+ });
168
+ }
169
+ return;
170
+ }
171
+
172
+ if (event.type !== "message_end" || !event.message) return;
173
+ const msg = event.message;
174
+ if (msg.role !== "assistant") return;
175
+
176
+ usage.turns++;
177
+ const u = msg.usage;
178
+ if (u) {
179
+ usage.input += u.input ?? 0;
180
+ usage.output += u.output ?? 0;
181
+ usage.cost += u.cost?.total ?? 0;
182
+ }
183
+ if (msg.model) detectedModel = msg.model;
184
+
185
+ // Extract last text content
186
+ for (const part of msg.content ?? []) {
187
+ if (part.type === "text") {
188
+ lastOutput = part.text;
189
+ break;
190
+ }
191
+ }
192
+ // Reset delta accumulator for next turn
193
+ currentDelta = "";
194
+
195
+ onUpdate?.({
196
+ content: [{ type: "text", text: lastOutput || "(running...)" }],
197
+ details: {
198
+ agent: agent.name,
199
+ usage,
200
+ running: true,
201
+ elapsedMs: Date.now() - startedAt,
202
+ model: detectedModel ?? configuredModel,
203
+ },
204
+ });
205
+ });
206
+
207
+ // Propagate depth to any nested fast-subagent calls
208
+ const prevDepth = process.env[DEPTH_ENV];
209
+ process.env[DEPTH_ENV] = String(depth + 1);
210
+
211
+ let exitCode = 0;
212
+ let error: string | undefined;
213
+
214
+ try {
215
+ if (signal?.aborted) throw new Error("Aborted");
216
+
217
+ const onAbort = () => void session.abort();
218
+ signal?.addEventListener("abort", onAbort, { once: true });
219
+ try {
220
+ await session.prompt(task);
221
+ } finally {
222
+ signal?.removeEventListener("abort", onAbort);
223
+ }
224
+ } catch (e) {
225
+ exitCode = 1;
226
+ error = signal?.aborted ? "Aborted" : e instanceof Error ? e.message : String(e);
227
+ } finally {
228
+ clearInterval(heartbeat);
229
+ unsubscribe();
230
+ session.dispose();
231
+ if (prevDepth === undefined) delete process.env[DEPTH_ENV];
232
+ else process.env[DEPTH_ENV] = prevDepth;
233
+ }
234
+
235
+ return { output: lastOutput, exitCode, error, model: detectedModel, usage };
236
+ }
237
+
238
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
239
+
240
+ async function mapConcurrent<TIn, TOut>(
241
+ items: TIn[],
242
+ concurrency: number,
243
+ fn: (item: TIn, i: number) => Promise<TOut>,
244
+ ): Promise<TOut[]> {
245
+ if (!items.length) return [];
246
+ const limit = Math.max(1, Math.min(concurrency, items.length));
247
+ const results: TOut[] = new Array(items.length);
248
+ let next = 0;
249
+ await Promise.all(
250
+ Array.from({ length: limit }, async () => {
251
+ while (true) {
252
+ const i = next++;
253
+ if (i >= items.length) return;
254
+ results[i] = await fn(items[i], i);
255
+ }
256
+ }),
257
+ );
258
+ return results;
259
+ }
260
+
261
+ function formatTokens(n: number): string {
262
+ if (n < 1000) return String(n);
263
+ if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
264
+ return `${Math.round(n / 1000)}k`;
265
+ }
266
+
267
+ function formatUsage(usage: RunResult["usage"], model?: string): string {
268
+ const parts: string[] = [];
269
+ if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
270
+ if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
271
+ if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
272
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
273
+ if (model) parts.push(model);
274
+ return parts.join(" ");
275
+ }
276
+
277
+ function getFinalText(r: RunResult): string {
278
+ if (r.exitCode !== 0) return `Error: ${r.error ?? r.output ?? "(no output)"}`;
279
+ return r.output || "(no output)";
280
+ }
281
+
282
+ // ─── Tool schemas ─────────────────────────────────────────────────────────────
283
+
284
+ const TaskItem = Type.Object({
285
+ agent: Type.String({ description: "Agent name" }),
286
+ task: Type.String({ description: "Task to delegate" }),
287
+ model: Type.Optional(Type.String({ description: "Model override (provider/model)" })),
288
+ cwd: Type.Optional(Type.String({ description: "Working directory" })),
289
+ count: Type.Optional(Type.Number({ description: "Repeat this task N times" })),
290
+ });
291
+
292
+ const ChainItem = Type.Object({
293
+ agent: Type.String({ description: "Agent name" }),
294
+ task: Type.Optional(
295
+ Type.String({
296
+ description:
297
+ "Task template. Supports {previous} (output from prior step) and {task} (first step task). " +
298
+ "Defaults to {previous} for steps 2+.",
299
+ }),
300
+ ),
301
+ model: Type.Optional(Type.String({ description: "Model override (provider/model)" })),
302
+ cwd: Type.Optional(Type.String({ description: "Working directory" })),
303
+ });
304
+
305
+ const SubagentParams = Type.Object({
306
+ // Single mode
307
+ agent: Type.Optional(Type.String({ description: "Agent name (single mode)" })),
308
+ task: Type.Optional(Type.String({ description: "Task (single mode)" })),
309
+ model: Type.Optional(Type.String({ description: "Model override (single mode)" })),
310
+ cwd: Type.Optional(Type.String({ description: "Working directory" })),
311
+
312
+ // Parallel mode
313
+ tasks: Type.Optional(
314
+ Type.Array(TaskItem, {
315
+ description: "Array of {agent, task} for parallel execution. Use count to repeat one task.",
316
+ }),
317
+ ),
318
+ concurrency: Type.Optional(
319
+ Type.Number({ description: "Max parallel concurrency (default: 4)", default: 4 }),
320
+ ),
321
+
322
+ // Chain mode
323
+ chain: Type.Optional(
324
+ Type.Array(ChainItem, {
325
+ description: "Sequential chain. Use {previous} in task to receive prior step output.",
326
+ }),
327
+ ),
328
+
329
+ // Management
330
+ action: Type.Optional(
331
+ Type.Union(
332
+ [
333
+ Type.Literal("list"),
334
+ Type.Literal("get"),
335
+ ],
336
+ { description: "'list' to discover agents, 'get' to inspect one agent" },
337
+ ),
338
+ ),
339
+ agentScope: Type.Optional(
340
+ Type.Union(
341
+ [Type.Literal("user"), Type.Literal("project"), Type.Literal("both")],
342
+ { description: "Agent scope filter", default: "both" },
343
+ ),
344
+ ),
345
+ });
346
+
347
+ // ─── Extension entry point ────────────────────────────────────────────────────
348
+
349
+ export default function (pi: ExtensionAPI) {
350
+ pi.registerTool({
351
+ name: "subagent",
352
+ label: "Subagent",
353
+ description: [
354
+ "Delegate tasks to specialized subagents. Runs IN-PROCESS — no subprocess cold-start overhead.",
355
+ "Modes: single ({ agent, task }), parallel ({ tasks: [...] }), chain ({ chain: [...] }).",
356
+ "Chain supports {task} (first step task) and {previous} (prior step output) template vars.",
357
+ "Agents defined as .md files in ~/.pi/agent/agents/ (user) or .pi/agents/ (project).",
358
+ "Use { action: 'list' } to discover available agents.",
359
+ ].join(" "),
360
+ parameters: SubagentParams,
361
+
362
+ renderResult(result, { isPartial }, theme) {
363
+ const text = result.content?.[0]?.type === "text" ? result.content[0].text : "";
364
+ const details = (result.details ?? {}) as {
365
+ usage?: RunResult["usage"];
366
+ running?: boolean;
367
+ elapsedMs?: number;
368
+ model?: string;
369
+ };
370
+
371
+ let status = "";
372
+ if (details.running) {
373
+ const statusParts: string[] = ["running"];
374
+ if (details.usage?.turns) statusParts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
375
+ if (details.elapsedMs !== undefined) statusParts.push(formatDuration(details.elapsedMs));
376
+ if (details.model) statusParts.push(details.model);
377
+ status = `\n${statusParts.join(" · ")}`;
378
+ } else if (details.usage) {
379
+ const usageStr = formatUsage(details.usage, details.model);
380
+ if (usageStr) status = `\n${usageStr}`;
381
+ }
382
+
383
+ if (isPartial) {
384
+ return new Text(theme.fg("dim", (text || "Running...") + status), 0, 0);
385
+ }
386
+ return new Text(text + status, 0, 0);
387
+ },
388
+
389
+ async execute(_id, params, signal, onUpdate, ctx) {
390
+ const cwd = params.cwd ?? ctx.cwd;
391
+ const agents = discoverAgents(cwd);
392
+
393
+ const findAgent = (name: string): { agent?: AgentConfig; error?: string } => {
394
+ const found = agents.find((a) => a.name === name);
395
+ if (!found) {
396
+ const list = agents.map((a) => `"${a.name}"`).join(", ") || "none";
397
+ return { error: `Unknown agent: "${name}". Available: ${list}` };
398
+ }
399
+ return { agent: found };
400
+ };
401
+
402
+ // ── Management: list ──────────────────────────────────────────────────────
403
+ if (params.action === "list" || (!params.agent && !params.tasks && !params.chain)) {
404
+ if (agents.length === 0) {
405
+ return {
406
+ content: [{
407
+ type: "text",
408
+ text: "No agents found. Add .md files to ~/.pi/agent/agents/ or .pi/agents/.",
409
+ }],
410
+ };
411
+ }
412
+ const lines = agents.map(
413
+ (a) => `${a.name} [${a.source}]${a.model ? ` · ${a.model}` : ""}: ${a.description}`,
414
+ );
415
+ return { content: [{ type: "text", text: `Agents (${agents.length}):\n${lines.join("\n")}` }] };
416
+ }
417
+
418
+ // ── Management: get ───────────────────────────────────────────────────────
419
+ if (params.action === "get" && params.agent) {
420
+ const { agent, error } = findAgent(params.agent);
421
+ if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
422
+ const info = [
423
+ `## ${agent.name} [${agent.source}]`,
424
+ `**Description:** ${agent.description}`,
425
+ agent.model ? `**Model:** ${agent.model}` : null,
426
+ agent.tools ? `**Tools:** ${agent.tools.join(", ")}` : null,
427
+ agent.systemPrompt ? `\n**System prompt:**\n${agent.systemPrompt}` : null,
428
+ ].filter(Boolean).join("\n");
429
+ return { content: [{ type: "text", text: info }] };
430
+ }
431
+
432
+ // ── Single mode ───────────────────────────────────────────────────────────
433
+ if (params.agent && params.task) {
434
+ const { agent, error } = findAgent(params.agent);
435
+ if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
436
+
437
+ const result = await runAgent(
438
+ agent,
439
+ params.task,
440
+ cwd,
441
+ params.model,
442
+ signal,
443
+ onUpdate,
444
+ );
445
+
446
+ return {
447
+ content: [{ type: "text", text: getFinalText(result) }],
448
+ details: {
449
+ usage: result.usage,
450
+ running: false,
451
+ elapsedMs: undefined,
452
+ model: result.model,
453
+ },
454
+ isError: result.exitCode !== 0,
455
+ };
456
+ }
457
+
458
+ // ── Parallel mode ─────────────────────────────────────────────────────────
459
+ if (params.tasks && params.tasks.length > 0) {
460
+ // Expand count shorthand
461
+ const expanded: Array<{ agent: string; task: string; model?: string; cwd?: string }> = [];
462
+ for (const t of params.tasks) {
463
+ const n = t.count ?? 1;
464
+ for (let i = 0; i < n; i++) expanded.push({ agent: t.agent, task: t.task, model: t.model, cwd: t.cwd });
465
+ }
466
+
467
+ const concurrency = params.concurrency ?? 4;
468
+ let doneCount = 0;
469
+
470
+ const allResults = await mapConcurrent(
471
+ expanded,
472
+ concurrency,
473
+ async (t, _i) => {
474
+ const { agent, error } = findAgent(t.agent);
475
+ if (error || !agent) {
476
+ return { agentName: t.agent, output: "", exitCode: 1, error, model: undefined, usage: { input: 0, output: 0, cost: 0, turns: 0 } };
477
+ }
478
+ const result = await runAgent(agent, t.task, t.cwd ?? cwd, t.model, signal, undefined);
479
+ doneCount++;
480
+ onUpdate?.({
481
+ content: [{ type: "text", text: `Parallel: ${doneCount}/${expanded.length} done...` }],
482
+ details: {},
483
+ });
484
+ return { ...result, agentName: t.agent };
485
+ },
486
+ );
487
+
488
+ const successCount = allResults.filter((r) => r.exitCode === 0).length;
489
+ const summaries = allResults.map((r) => {
490
+ const out = getFinalText(r);
491
+ const preview = out.length > 300 ? `${out.slice(0, 300)}...` : out;
492
+ return `**[${r.agentName}]** ${r.exitCode === 0 ? "✓" : "✗"}\n${preview}`;
493
+ });
494
+ const totalUsage = allResults.reduce(
495
+ (acc, r) => ({
496
+ input: acc.input + r.usage.input,
497
+ output: acc.output + r.usage.output,
498
+ cost: acc.cost + r.usage.cost,
499
+ turns: acc.turns + r.usage.turns,
500
+ }),
501
+ { input: 0, output: 0, cost: 0, turns: 0 },
502
+ );
503
+
504
+ return {
505
+ content: [{
506
+ type: "text",
507
+ text: [
508
+ `Parallel: ${successCount}/${allResults.length} succeeded`,
509
+ "",
510
+ summaries.join("\n\n"),
511
+ "",
512
+ formatUsage(totalUsage),
513
+ ].join("\n"),
514
+ }],
515
+ };
516
+ }
517
+
518
+ // ── Chain mode ────────────────────────────────────────────────────────────
519
+ if (params.chain && params.chain.length > 0) {
520
+ const firstTask = params.chain[0]?.task ?? "";
521
+ let previousOutput = "";
522
+
523
+ const stepResults: Array<RunResult & { agentName: string; step: number }> = [];
524
+
525
+ for (let i = 0; i < params.chain.length; i++) {
526
+ const step = params.chain[i];
527
+ const { agent, error } = findAgent(step.agent);
528
+ if (error || !agent) {
529
+ return {
530
+ content: [{ type: "text", text: `Chain stopped at step ${i + 1}: ${error ?? "Not found"}` }],
531
+ isError: true,
532
+ };
533
+ }
534
+
535
+ // Resolve task template
536
+ let task = step.task ?? (i === 0 ? firstTask : "{previous}");
537
+ task = task
538
+ .replace(/\{previous\}/g, previousOutput)
539
+ .replace(/\{task\}/g, firstTask);
540
+
541
+ if (onUpdate) {
542
+ onUpdate({
543
+ content: [{
544
+ type: "text",
545
+ text: `Chain step ${i + 1}/${params.chain.length}: ${step.agent}...`,
546
+ }],
547
+ details: {},
548
+ });
549
+ }
550
+
551
+ const result = await runAgent(
552
+ agent,
553
+ task,
554
+ step.cwd ?? cwd,
555
+ step.model,
556
+ signal,
557
+ onUpdate,
558
+ );
559
+
560
+ stepResults.push({ ...result, agentName: step.agent, step: i + 1 });
561
+
562
+ if (result.exitCode !== 0) {
563
+ return {
564
+ content: [{
565
+ type: "text",
566
+ text: `Chain failed at step ${i + 1} (${step.agent}): ${result.error ?? "(no output)"}`,
567
+ }],
568
+ isError: true,
569
+ };
570
+ }
571
+
572
+ previousOutput = result.output;
573
+ }
574
+
575
+ const last = stepResults[stepResults.length - 1];
576
+ const totalUsage = stepResults.reduce(
577
+ (acc, r) => ({
578
+ input: acc.input + r.usage.input,
579
+ output: acc.output + r.usage.output,
580
+ cost: acc.cost + r.usage.cost,
581
+ turns: acc.turns + r.usage.turns,
582
+ }),
583
+ { input: 0, output: 0, cost: 0, turns: 0 },
584
+ );
585
+
586
+ return {
587
+ content: [{
588
+ type: "text",
589
+ text: [
590
+ last.output,
591
+ "",
592
+ `Chain: ${stepResults.length} steps · ${formatUsage(totalUsage)}`,
593
+ ].join("\n"),
594
+ }],
595
+ };
596
+ }
597
+
598
+ // Shouldn't reach here
599
+ return { content: [{ type: "text", text: "Provide agent+task, tasks array, or chain array." }] };
600
+ },
601
+ });
602
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "pi-fast-subagent",
3
+ "version": "0.1.0",
4
+ "description": "In-process subagent delegation for pi with single, parallel, and chain modes",
5
+ "type": "module",
6
+ "keywords": ["pi-package", "pi", "subagent", "agents", "extension"],
7
+ "files": [
8
+ "index.ts",
9
+ "agents.ts",
10
+ "agents/*.md",
11
+ "README.md"
12
+ ],
13
+ "pi": {
14
+ "extensions": [
15
+ "./index.ts"
16
+ ]
17
+ },
18
+ "peerDependencies": {
19
+ "@mariozechner/pi-coding-agent": "*",
20
+ "@mariozechner/pi-tui": "*",
21
+ "@sinclair/typebox": "*"
22
+ }
23
+ }