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 +141 -0
- package/agents/general.md +27 -0
- package/agents/scout.md +28 -0
- package/agents.ts +100 -0
- package/index.ts +602 -0
- package/package.json +23 -0
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
|
package/agents/scout.md
ADDED
|
@@ -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
|
+
}
|