pi-fast-subagent 0.6.1 → 0.8.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 +43 -6
- package/agents/general.md +12 -0
- package/agents/scout.md +12 -1
- package/agents.ts +23 -8
- package/index.ts +227 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,15 +69,17 @@ You are code exploration specialist. Read relevant files, trace data flow, summa
|
|
|
69
69
|
| `description` | yes | One-line description shown in `/fast-subagent:agent` |
|
|
70
70
|
| `model` | no | Model override, format `provider/model-id` (e.g. `anthropic/claude-haiku-4-5`) |
|
|
71
71
|
| `tools` | no | Tool allowlist (see below) |
|
|
72
|
+
| `maxDepth` | no | Nested subagent depth this agent may spawn. Default `0` means this agent cannot call `subagent`. |
|
|
72
73
|
|
|
73
74
|
### `tools:` field
|
|
74
75
|
|
|
75
|
-
Controls which tools the subagent has access to.
|
|
76
|
+
Controls which tools the subagent has access to. The default is **all tools** — builtins plus parent extensions (web_search, fetch_content, mcp, playwright, …). Agents opt into lean mode with `tools: builtins` or an explicit built-in allowlist.
|
|
76
77
|
|
|
77
78
|
| Value | Behavior |
|
|
78
79
|
|-------|----------|
|
|
79
|
-
| *(omitted)* |
|
|
80
|
+
| *(omitted)* | Builtins + every parent extension (**default**) |
|
|
80
81
|
| `all` | Same as omitted — explicit "everything" |
|
|
82
|
+
| `builtins` | Builtins only — `read, bash, edit, write, grep, find, ls` |
|
|
81
83
|
| `none` | No tools at all — pure reasoning agent |
|
|
82
84
|
| comma list | Allowlist; extensions auto-load only if any listed tool is non-builtin |
|
|
83
85
|
|
|
@@ -95,9 +97,10 @@ tools: none
|
|
|
95
97
|
|
|
96
98
|
```md
|
|
97
99
|
---
|
|
98
|
-
name:
|
|
99
|
-
description:
|
|
100
|
-
|
|
100
|
+
name: scout
|
|
101
|
+
description: Read-only code explorer
|
|
102
|
+
# drop `edit` and `write` so the agent cannot mutate the codebase
|
|
103
|
+
tools: read, bash, grep, find, ls
|
|
101
104
|
---
|
|
102
105
|
```
|
|
103
106
|
|
|
@@ -105,11 +108,45 @@ tools: read, bash, edit, write, grep, find, ls
|
|
|
105
108
|
---
|
|
106
109
|
name: researcher
|
|
107
110
|
description: Web research agent
|
|
111
|
+
# listing `web_search` triggers extension loading; `read` + `write` keep the rest local
|
|
108
112
|
tools: read, write, web_search, fetch_content
|
|
109
113
|
---
|
|
110
114
|
```
|
|
111
115
|
|
|
112
|
-
|
|
116
|
+
```md
|
|
117
|
+
---
|
|
118
|
+
name: general
|
|
119
|
+
description: Do-anything helper
|
|
120
|
+
# `tools` omitted means all tools; `tools: all` is equivalent
|
|
121
|
+
tools: all
|
|
122
|
+
---
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
> **Performance note:** omitted `tools` / `tools: all` loads every installed pi extension into the subagent session. That adds startup cost (extension init, possibly MCP server spawn, playwright runtime, …) and token cost (bigger system prompt). Use `tools: builtins` or list specific tools for tight, focused agents.
|
|
126
|
+
|
|
127
|
+
**YAML comments** (`# …`) are allowed inside the frontmatter — handy for documenting *why* a particular tool set was chosen. See `agents/general.md` and `agents/scout.md` for examples.
|
|
128
|
+
|
|
129
|
+
### `maxDepth:` field
|
|
130
|
+
|
|
131
|
+
Subagents cannot spawn other subagents by default, even when `tools` exposes the `subagent` tool.
|
|
132
|
+
|
|
133
|
+
```md
|
|
134
|
+
---
|
|
135
|
+
name: planner
|
|
136
|
+
description: Can delegate one level deeper
|
|
137
|
+
maxDepth: 1
|
|
138
|
+
---
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Depth counts nested generations from that agent:
|
|
142
|
+
|
|
143
|
+
| Value | Behavior |
|
|
144
|
+
|-------|----------|
|
|
145
|
+
| *(omitted)* / `0` | This agent cannot spawn subagents |
|
|
146
|
+
| `1` | This agent may spawn subagents, but those children cannot spawn again unless their own `maxDepth` allows it |
|
|
147
|
+
| `2` | Allows two nested generations, subject to each child agent's own `maxDepth` |
|
|
148
|
+
|
|
149
|
+
Aliases accepted: `max_depth`, `depth`, `subagentDepth`.
|
|
113
150
|
|
|
114
151
|
## Background Agents
|
|
115
152
|
|
package/agents/general.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
name: general
|
|
3
3
|
description: General-purpose helper for coding, analysis, writing, debugging, and task execution
|
|
4
4
|
model: anthropic/claude-haiku-4-5
|
|
5
|
+
|
|
6
|
+
# tools: which tools this agent can use.
|
|
7
|
+
# (omit) → all tools: builtins + every parent extension (default)
|
|
8
|
+
# all → same as omitted — explicit "everything"
|
|
9
|
+
# builtins → read, bash, edit, write, grep, find, ls only (fast startup)
|
|
10
|
+
# none → no tools — pure reasoning
|
|
11
|
+
# comma-separated list → explicit allowlist, e.g. `read, grep, web_search`
|
|
12
|
+
# General is meant to be a do-anything fallback, so it keeps everything explicit.
|
|
13
|
+
tools: all
|
|
14
|
+
|
|
15
|
+
# Subagents cannot spawn subagents by default. Set maxDepth: 1+ to opt in.
|
|
16
|
+
maxDepth: 0
|
|
5
17
|
---
|
|
6
18
|
|
|
7
19
|
You are general-purpose subagent.
|
package/agents/scout.md
CHANGED
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
name: scout
|
|
3
3
|
description: Explores codebases, maps structure, traces data flow, answers how things work across many files
|
|
4
4
|
model: anthropic/claude-haiku-4-5
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
# tools: which tools this agent can use.
|
|
7
|
+
# (omit) → all tools: builtins + every parent extension (default)
|
|
8
|
+
# all → same as omitted — explicit "everything"
|
|
9
|
+
# builtins → read, bash, edit, write, grep, find, ls only (fast startup)
|
|
10
|
+
# none → no tools — pure reasoning
|
|
11
|
+
# comma-separated list → explicit allowlist
|
|
12
|
+
# Scout is read-only: no `edit`, no `write`, no extension tools. Keeps the agent from mutating the codebase.
|
|
13
|
+
tools: read, bash, grep, find, ls
|
|
14
|
+
|
|
15
|
+
# Subagents cannot spawn subagents by default. Keep scout focused on exploration only.
|
|
16
|
+
maxDepth: 0
|
|
6
17
|
---
|
|
7
18
|
|
|
8
19
|
You are code exploration specialist.
|
package/agents.ts
CHANGED
|
@@ -10,19 +10,19 @@ import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* tools frontmatter semantics:
|
|
13
|
-
* unset →
|
|
14
|
-
* `all` → all builtins + all
|
|
13
|
+
* unset → all builtins + all parent extensions — DEFAULT
|
|
14
|
+
* `all` → all builtins + all parent extensions (web_search, fetch_content, mcp, …)
|
|
15
|
+
* `builtins` → built-in coding tools only (read, bash, edit, write, grep, find, ls)
|
|
15
16
|
* `none` → no tools at all
|
|
16
17
|
* comma list → allowlist; extensions auto-loaded if any listed tool is non-builtin
|
|
17
|
-
* for lean "builtins-only" mode, list them explicitly:
|
|
18
|
-
* tools: read, bash, edit, write, grep, find, ls
|
|
19
18
|
*
|
|
20
19
|
* Represented as:
|
|
21
|
-
* "
|
|
20
|
+
* "builtins" → only built-in coding tools
|
|
21
|
+
* "all" → everything (default)
|
|
22
22
|
* "none" → no tools
|
|
23
23
|
* string[] → allowlist
|
|
24
24
|
*/
|
|
25
|
-
export type AgentTools = "all" | "none" | string[];
|
|
25
|
+
export type AgentTools = "builtins" | "all" | "none" | string[];
|
|
26
26
|
|
|
27
27
|
export const BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
|
|
28
28
|
|
|
@@ -31,6 +31,8 @@ export interface AgentConfig {
|
|
|
31
31
|
description: string;
|
|
32
32
|
model?: string;
|
|
33
33
|
tools: AgentTools;
|
|
34
|
+
/** Number of nested subagent generations this agent may spawn. Default: 0. */
|
|
35
|
+
maxDepth: number;
|
|
34
36
|
systemPrompt: string;
|
|
35
37
|
source: "user" | "project";
|
|
36
38
|
filePath: string;
|
|
@@ -40,11 +42,12 @@ const BUILTIN_TOOLS = new Set<string>(BUILTIN_TOOL_NAMES);
|
|
|
40
42
|
|
|
41
43
|
export function agentNeedsExtensions(tools: AgentTools): boolean {
|
|
42
44
|
if (tools === "all") return true;
|
|
43
|
-
if (tools === "none") return false;
|
|
45
|
+
if (tools === "builtins" || tools === "none") return false;
|
|
44
46
|
return tools.some((t) => !BUILTIN_TOOLS.has(t));
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
// Default:
|
|
49
|
+
// Default: all tools, matching pi-subagents behavior. Agents opt into lean mode
|
|
50
|
+
// with `tools: builtins` or explicit built-in allowlists.
|
|
48
51
|
function parseToolsField(raw: unknown): AgentTools {
|
|
49
52
|
if (raw === undefined || raw === null) return "all";
|
|
50
53
|
const str = String(raw).trim();
|
|
@@ -52,10 +55,18 @@ function parseToolsField(raw: unknown): AgentTools {
|
|
|
52
55
|
const lower = str.toLowerCase();
|
|
53
56
|
if (lower === "all") return "all";
|
|
54
57
|
if (lower === "none") return "none";
|
|
58
|
+
if (lower === "builtins" || lower === "builtin") return "builtins";
|
|
55
59
|
const list = str.split(",").map((t) => t.trim()).filter(Boolean);
|
|
56
60
|
return list.length ? list : "all";
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
function parseMaxDepthField(raw: unknown): number {
|
|
64
|
+
if (raw === undefined || raw === null || raw === "") return 0;
|
|
65
|
+
const n = Number(raw);
|
|
66
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
67
|
+
return Math.floor(n);
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
60
71
|
if (!fs.existsSync(dir)) return [];
|
|
61
72
|
let entries: fs.Dirent[];
|
|
@@ -75,11 +86,15 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
75
86
|
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
76
87
|
if (!frontmatter?.name || !frontmatter?.description) continue;
|
|
77
88
|
const tools = parseToolsField(frontmatter.tools);
|
|
89
|
+
const maxDepth = parseMaxDepthField(
|
|
90
|
+
frontmatter.maxDepth ?? frontmatter.max_depth ?? frontmatter.depth ?? frontmatter.subagentDepth,
|
|
91
|
+
);
|
|
78
92
|
agents.push({
|
|
79
93
|
name: frontmatter.name,
|
|
80
94
|
description: frontmatter.description,
|
|
81
95
|
model: frontmatter.model,
|
|
82
96
|
tools,
|
|
97
|
+
maxDepth,
|
|
83
98
|
systemPrompt: body.trim(),
|
|
84
99
|
source,
|
|
85
100
|
filePath,
|
package/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
AgentToolUpdateCallback,
|
|
15
15
|
ExtensionAPI,
|
|
16
16
|
ExtensionContext,
|
|
17
|
+
ResourceLoader,
|
|
17
18
|
ToolRenderResultOptions,
|
|
18
19
|
} from "@mariozechner/pi-coding-agent";
|
|
19
20
|
import { BackgroundJobManager } from "./background-job-manager.js";
|
|
@@ -36,6 +37,7 @@ import { type AgentConfig, agentNeedsExtensions, discoverAgents } from "./agents
|
|
|
36
37
|
|
|
37
38
|
function formatTools(tools: AgentConfig["tools"]): string {
|
|
38
39
|
if (tools === "all") return "all";
|
|
40
|
+
if (tools === "builtins") return "builtins (default)";
|
|
39
41
|
if (tools === "none") return "none";
|
|
40
42
|
return tools.join(", ");
|
|
41
43
|
}
|
|
@@ -125,6 +127,125 @@ function refreshBgStatus(): void {
|
|
|
125
127
|
_setBgStatus?.(running.length > 0 ? `⧗ ${running.length} bg agent${running.length > 1 ? "s" : ""}` : undefined);
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
// ─── Resource loader pool ─────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
interface LoaderPoolEntry {
|
|
133
|
+
idle: DefaultResourceLoader[];
|
|
134
|
+
active: Set<DefaultResourceLoader>;
|
|
135
|
+
warming: Set<Promise<void>>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface LoaderLease {
|
|
139
|
+
loader: ResourceLoader;
|
|
140
|
+
release: () => void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const _loaderPool = new Map<string, LoaderPoolEntry>();
|
|
144
|
+
|
|
145
|
+
function loaderPoolKey(cwd: string, agentDir: string, noExtensions: boolean): string {
|
|
146
|
+
return `${cwd}\0${agentDir}\0${noExtensions ? "noext" : "ext"}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getLoaderPoolEntry(cwd: string, agentDir: string, noExtensions: boolean): LoaderPoolEntry {
|
|
150
|
+
const key = loaderPoolKey(cwd, agentDir, noExtensions);
|
|
151
|
+
let entry = _loaderPool.get(key);
|
|
152
|
+
if (!entry) {
|
|
153
|
+
entry = { idle: [], active: new Set(), warming: new Set() };
|
|
154
|
+
_loaderPool.set(key, entry);
|
|
155
|
+
}
|
|
156
|
+
return entry;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function makeLoaderOptions(cwd: string, agentDir: string, noExtensions: boolean): DefaultResourceLoaderOptions {
|
|
160
|
+
return {
|
|
161
|
+
cwd,
|
|
162
|
+
agentDir,
|
|
163
|
+
noExtensions,
|
|
164
|
+
noContextFiles: true,
|
|
165
|
+
noSkills: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
class AgentPromptResourceLoader implements ResourceLoader {
|
|
170
|
+
constructor(
|
|
171
|
+
private readonly base: ResourceLoader,
|
|
172
|
+
private readonly systemPromptOverride: string | undefined,
|
|
173
|
+
) {}
|
|
174
|
+
|
|
175
|
+
getExtensions() { return this.base.getExtensions(); }
|
|
176
|
+
getSkills() { return this.base.getSkills(); }
|
|
177
|
+
getPrompts() { return this.base.getPrompts(); }
|
|
178
|
+
getThemes() { return this.base.getThemes(); }
|
|
179
|
+
getAgentsFiles() { return this.base.getAgentsFiles(); }
|
|
180
|
+
getSystemPrompt() { return this.systemPromptOverride ?? this.base.getSystemPrompt(); }
|
|
181
|
+
getAppendSystemPrompt() { return this.base.getAppendSystemPrompt(); }
|
|
182
|
+
extendResources(paths: Parameters<ResourceLoader["extendResources"]>[0]): void { this.base.extendResources(paths); }
|
|
183
|
+
reload(): Promise<void> { return this.base.reload(); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isLoaderWarm(cwd: string, agentDir: string, noExtensions: boolean): boolean {
|
|
187
|
+
const entry = _loaderPool.get(loaderPoolKey(cwd, agentDir, noExtensions));
|
|
188
|
+
return !!entry && entry.idle.length > 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function allowUiPaint(coldLoader: boolean): Promise<void> {
|
|
192
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
193
|
+
if (!coldLoader) return;
|
|
194
|
+
// Give pi's TUI render timer a real timers-phase turn before CPU-heavy extension loading.
|
|
195
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 50));
|
|
196
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function acquireResourceLoader(
|
|
200
|
+
cwd: string,
|
|
201
|
+
agentDir: string,
|
|
202
|
+
noExtensions: boolean,
|
|
203
|
+
systemPromptOverride: string | undefined,
|
|
204
|
+
): Promise<LoaderLease> {
|
|
205
|
+
const entry = getLoaderPoolEntry(cwd, agentDir, noExtensions);
|
|
206
|
+
|
|
207
|
+
while (true) {
|
|
208
|
+
const cached = entry.idle.pop();
|
|
209
|
+
if (cached) {
|
|
210
|
+
entry.active.add(cached);
|
|
211
|
+
let released = false;
|
|
212
|
+
return {
|
|
213
|
+
loader: new AgentPromptResourceLoader(cached, systemPromptOverride),
|
|
214
|
+
release: () => {
|
|
215
|
+
if (released) return;
|
|
216
|
+
released = true;
|
|
217
|
+
entry.active.delete(cached);
|
|
218
|
+
entry.idle.push(cached);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const warming = entry.warming.values().next().value as Promise<void> | undefined;
|
|
224
|
+
if (warming) {
|
|
225
|
+
await warming;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const loader = new DefaultResourceLoader(makeLoaderOptions(cwd, agentDir, noExtensions));
|
|
230
|
+
const warmPromise = loader.reload()
|
|
231
|
+
.then(() => { entry.idle.push(loader); })
|
|
232
|
+
.finally(() => { entry.warming.delete(warmPromise); });
|
|
233
|
+
entry.warming.add(warmPromise);
|
|
234
|
+
await warmPromise;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function warmResourceLoader(cwd: string, agentDir: string, noExtensions: boolean): void {
|
|
239
|
+
const entry = getLoaderPoolEntry(cwd, agentDir, noExtensions);
|
|
240
|
+
if (entry.idle.length > 0 || entry.active.size > 0 || entry.warming.size > 0) return;
|
|
241
|
+
const loader = new DefaultResourceLoader(makeLoaderOptions(cwd, agentDir, noExtensions));
|
|
242
|
+
const warmPromise = loader.reload()
|
|
243
|
+
.then(() => { entry.idle.push(loader); })
|
|
244
|
+
.catch(() => { /* ignore warm failures; foreground call reports real error */ })
|
|
245
|
+
.finally(() => { entry.warming.delete(warmPromise); });
|
|
246
|
+
entry.warming.add(warmPromise);
|
|
247
|
+
}
|
|
248
|
+
|
|
128
249
|
// ─── Foreground detach registry ───────────────────────────────────────────────
|
|
129
250
|
|
|
130
251
|
interface ForegroundDetachEntry {
|
|
@@ -136,8 +257,9 @@ const _fgJobs = new Map<string, ForegroundDetachEntry>();
|
|
|
136
257
|
|
|
137
258
|
// ─── In-process runner ───────────────────────────────────────────────────────
|
|
138
259
|
|
|
139
|
-
const
|
|
260
|
+
const DEFAULT_MAX_DEPTH = 0;
|
|
140
261
|
const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
|
|
262
|
+
const MAX_DEPTH_ENV = "PI_FAST_SUBAGENT_MAX_DEPTH";
|
|
141
263
|
|
|
142
264
|
interface ToolCallEntry {
|
|
143
265
|
id: string;
|
|
@@ -168,6 +290,7 @@ interface AgentRowStatus {
|
|
|
168
290
|
|
|
169
291
|
interface SubagentDetails {
|
|
170
292
|
mode?: "single" | "parallel";
|
|
293
|
+
agentName?: string;
|
|
171
294
|
task?: string;
|
|
172
295
|
// parallel
|
|
173
296
|
parallelAgents?: AgentRowStatus[];
|
|
@@ -208,8 +331,9 @@ function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): strin
|
|
|
208
331
|
return lines.join("\n");
|
|
209
332
|
}
|
|
210
333
|
|
|
211
|
-
// Module-level depth
|
|
334
|
+
// Module-level depth counters for nested in-process subagent calls.
|
|
212
335
|
let _currentDepth = 0;
|
|
336
|
+
let _currentMaxDepth = DEFAULT_MAX_DEPTH;
|
|
213
337
|
|
|
214
338
|
async function runAgent(
|
|
215
339
|
agent: AgentConfig,
|
|
@@ -221,46 +345,70 @@ async function runAgent(
|
|
|
221
345
|
parentDepth?: number,
|
|
222
346
|
): Promise<RunResult> {
|
|
223
347
|
const depth = parentDepth ?? _currentDepth;
|
|
224
|
-
|
|
348
|
+
const isNestedCall = depth > 0;
|
|
349
|
+
if (isNestedCall && depth > _currentMaxDepth) {
|
|
225
350
|
return {
|
|
226
351
|
output: "",
|
|
227
352
|
exitCode: 1,
|
|
228
|
-
error: `
|
|
353
|
+
error: `Nested subagents are disabled by default. Set maxDepth: ${depth} (or higher) in the parent agent frontmatter to allow this call.`,
|
|
229
354
|
toolCalls: [],
|
|
230
355
|
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
231
356
|
};
|
|
232
357
|
}
|
|
233
358
|
|
|
359
|
+
const bootStartedAt = Date.now();
|
|
234
360
|
const { authStorage, modelRegistry } = getAuth();
|
|
235
361
|
const agentDir = getAgentDir();
|
|
362
|
+
const noExtensions = !agentNeedsExtensions(agent.tools);
|
|
363
|
+
const coldLoader = !isLoaderWarm(cwd, agentDir, noExtensions);
|
|
364
|
+
|
|
365
|
+
// Fire an immediate "running" emit so the UI draws the agent header + prompt
|
|
366
|
+
// before the (potentially slow) extension/session load. Without this, pi looks
|
|
367
|
+
// frozen while `loader.reload()` and `createAgentSession()` are in flight.
|
|
368
|
+
onUpdate?.({
|
|
369
|
+
content: [{ type: "text", text: "" }],
|
|
370
|
+
details: {
|
|
371
|
+
agentName: agent.name,
|
|
372
|
+
task,
|
|
373
|
+
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
374
|
+
running: true,
|
|
375
|
+
elapsedMs: 0,
|
|
376
|
+
model: modelOverride ?? agent.model,
|
|
377
|
+
toolCalls: [],
|
|
378
|
+
} satisfies SubagentDetails,
|
|
379
|
+
});
|
|
380
|
+
// Yield through timers when loader is cold so pi's render loop paints before
|
|
381
|
+
// CPU-heavy extension loading runs.
|
|
382
|
+
await allowUiPaint(coldLoader);
|
|
236
383
|
|
|
237
|
-
|
|
238
|
-
// Agents can opt in to extensions via `extensions: true` in frontmatter, which
|
|
239
|
-
// makes tools like web_search / fetch_content / mcp / etc. available to the
|
|
240
|
-
// subagent (subject to the optional `tools:` allowlist below).
|
|
241
|
-
const loaderOptions: DefaultResourceLoaderOptions = {
|
|
384
|
+
const loaderLease = await acquireResourceLoader(
|
|
242
385
|
cwd,
|
|
243
386
|
agentDir,
|
|
244
|
-
noExtensions
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
};
|
|
248
|
-
if (agent.systemPrompt) {
|
|
249
|
-
// Replace pi's base system prompt with the agent's own prompt
|
|
250
|
-
loaderOptions.systemPromptOverride = () => agent.systemPrompt;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const loader = new DefaultResourceLoader(loaderOptions);
|
|
254
|
-
await loader.reload();
|
|
387
|
+
noExtensions,
|
|
388
|
+
agent.systemPrompt || undefined,
|
|
389
|
+
);
|
|
255
390
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
391
|
+
let session: Awaited<ReturnType<typeof createAgentSession>>["session"];
|
|
392
|
+
try {
|
|
393
|
+
const created = await createAgentSession({
|
|
394
|
+
cwd,
|
|
395
|
+
agentDir,
|
|
396
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
397
|
+
authStorage,
|
|
398
|
+
modelRegistry,
|
|
399
|
+
resourceLoader: loaderLease.loader,
|
|
400
|
+
});
|
|
401
|
+
session = created.session;
|
|
402
|
+
} catch (e) {
|
|
403
|
+
loaderLease.release();
|
|
404
|
+
return {
|
|
405
|
+
output: "",
|
|
406
|
+
exitCode: 1,
|
|
407
|
+
error: e instanceof Error ? e.message : String(e),
|
|
408
|
+
toolCalls: [],
|
|
409
|
+
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
410
|
+
};
|
|
411
|
+
}
|
|
264
412
|
|
|
265
413
|
// Resolve and apply model
|
|
266
414
|
const modelStr = modelOverride ?? agent.model;
|
|
@@ -288,7 +436,7 @@ async function runAgent(
|
|
|
288
436
|
let lastOutput = "";
|
|
289
437
|
let currentDelta = "";
|
|
290
438
|
let detectedModel: string | undefined;
|
|
291
|
-
const startedAt =
|
|
439
|
+
const startedAt = bootStartedAt;
|
|
292
440
|
const configuredModel = modelOverride ?? agent.model;
|
|
293
441
|
const toolCalls: ToolCallEntry[] = [];
|
|
294
442
|
const toolStartTimes = new Map<string, number>();
|
|
@@ -300,6 +448,7 @@ async function runAgent(
|
|
|
300
448
|
onUpdate?.({
|
|
301
449
|
content: [{ type: "text", text: currentDelta || lastOutput || "" }],
|
|
302
450
|
details: {
|
|
451
|
+
agentName: agent.name,
|
|
303
452
|
task,
|
|
304
453
|
usage,
|
|
305
454
|
running: true,
|
|
@@ -397,10 +546,17 @@ async function runAgent(
|
|
|
397
546
|
});
|
|
398
547
|
});
|
|
399
548
|
|
|
400
|
-
// Propagate depth to nested calls
|
|
549
|
+
// Propagate depth to nested calls. `maxDepth` is per-agent and defaults to 0,
|
|
550
|
+
// so subagents cannot spawn subagents unless their frontmatter opts in.
|
|
401
551
|
const prevEnvDepth = process.env[DEPTH_ENV];
|
|
402
|
-
process.env[
|
|
552
|
+
const prevEnvMaxDepth = process.env[MAX_DEPTH_ENV];
|
|
553
|
+
const prevDepth = _currentDepth;
|
|
554
|
+
const prevMaxDepth = _currentMaxDepth;
|
|
555
|
+
const maxDepth = Math.max(DEFAULT_MAX_DEPTH, agent.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
403
556
|
_currentDepth = depth + 1;
|
|
557
|
+
_currentMaxDepth = depth + maxDepth;
|
|
558
|
+
process.env[DEPTH_ENV] = String(_currentDepth);
|
|
559
|
+
process.env[MAX_DEPTH_ENV] = String(_currentMaxDepth);
|
|
404
560
|
|
|
405
561
|
let exitCode = 0;
|
|
406
562
|
let error: string | undefined;
|
|
@@ -423,9 +579,13 @@ async function runAgent(
|
|
|
423
579
|
clearInterval(heartbeat);
|
|
424
580
|
unsubscribe();
|
|
425
581
|
session.dispose();
|
|
582
|
+
loaderLease.release();
|
|
426
583
|
if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
|
|
427
584
|
else process.env[DEPTH_ENV] = prevEnvDepth;
|
|
428
|
-
|
|
585
|
+
if (prevEnvMaxDepth === undefined) delete process.env[MAX_DEPTH_ENV];
|
|
586
|
+
else process.env[MAX_DEPTH_ENV] = prevEnvMaxDepth;
|
|
587
|
+
_currentDepth = prevDepth;
|
|
588
|
+
_currentMaxDepth = prevMaxDepth;
|
|
429
589
|
}
|
|
430
590
|
|
|
431
591
|
return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, usage };
|
|
@@ -558,12 +718,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
558
718
|
|
|
559
719
|
pi.on("session_start", async (_event, ctx) => {
|
|
560
720
|
_setBgStatus = (text) => ctx.ui.setStatus(BG_STATUS_KEY, text);
|
|
721
|
+
|
|
722
|
+
// Warm one extension-capable loader after startup. First `tools: all` subagent
|
|
723
|
+
// call can then reuse loaded extensions instead of blocking before first stream.
|
|
724
|
+
if (process.env.PI_FAST_SUBAGENT_WARM !== "0") {
|
|
725
|
+
const warmCwd = ctx.cwd;
|
|
726
|
+
const warmAgentDir = getAgentDir();
|
|
727
|
+
setTimeout(() => warmResourceLoader(warmCwd, warmAgentDir, false), 1000);
|
|
728
|
+
}
|
|
561
729
|
});
|
|
562
730
|
|
|
563
731
|
pi.on("session_shutdown", async () => {
|
|
564
732
|
getBgManager().shutdown();
|
|
565
733
|
_bgManager = null;
|
|
566
734
|
_setBgStatus = null;
|
|
735
|
+
_loaderPool.clear();
|
|
567
736
|
});
|
|
568
737
|
|
|
569
738
|
// ─── Ctrl+Shift+B — move foreground subagent to background ─────────────────────────
|
|
@@ -613,6 +782,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
613
782
|
`Description: ${agent.description}`,
|
|
614
783
|
agent.model ? `Model: ${agent.model}` : "",
|
|
615
784
|
`Tools: ${formatTools(agent.tools)}`,
|
|
785
|
+
`Max subagent depth: ${agent.maxDepth}`,
|
|
616
786
|
agent.systemPrompt ? `\nSystem prompt:\n${agent.systemPrompt}` : "",
|
|
617
787
|
].filter(Boolean).join("\n");
|
|
618
788
|
ctx.ui.notify(lines, "info");
|
|
@@ -625,7 +795,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
625
795
|
"Add .md files to:\n" +
|
|
626
796
|
" ~/.pi/agent/agents/ (user-level)\n" +
|
|
627
797
|
" .pi/agents/ (project-level)\n" +
|
|
628
|
-
"\nFrontmatter required: name, description. Optional: model, tools.",
|
|
798
|
+
"\nFrontmatter required: name, description. Optional: model, tools, maxDepth.",
|
|
629
799
|
"info"
|
|
630
800
|
);
|
|
631
801
|
return;
|
|
@@ -866,14 +1036,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
866
1036
|
|
|
867
1037
|
function statusLine(): string {
|
|
868
1038
|
if (details.backgroundJobId) return `moved to background · ${details.backgroundJobId}`;
|
|
1039
|
+
const prefix = details.agentName ? `${theme.fg("toolTitle", details.agentName)} · ` : "";
|
|
869
1040
|
if (details.running) {
|
|
870
1041
|
const parts: string[] = ["running"];
|
|
871
1042
|
if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
|
|
872
1043
|
if (details.elapsedMs != null) parts.push(formatDuration(details.elapsedMs));
|
|
873
1044
|
if (details.model) parts.push(details.model);
|
|
874
|
-
return parts.join(" · ");
|
|
1045
|
+
return prefix + parts.join(" · ");
|
|
875
1046
|
}
|
|
876
|
-
return formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
|
|
1047
|
+
return prefix + formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
|
|
877
1048
|
}
|
|
878
1049
|
|
|
879
1050
|
// Name(arg) ✓ 0.3s or Name(arg) (dim, still running)
|
|
@@ -904,6 +1075,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
904
1075
|
render(width: number): string[] {
|
|
905
1076
|
const out: string[] = [];
|
|
906
1077
|
const indent = " ";
|
|
1078
|
+
const ellipsisLine = (count: number) =>
|
|
1079
|
+
theme.fg("muted", `${indent}… (${count} more line${count === 1 ? "" : "s"})`);
|
|
907
1080
|
|
|
908
1081
|
// ── Prompt ────────────────────────────────────────────────────
|
|
909
1082
|
if (details.task) {
|
|
@@ -913,14 +1086,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
913
1086
|
for (const w of wrapLine(indent + line, width)) out.push(w);
|
|
914
1087
|
}
|
|
915
1088
|
} else {
|
|
916
|
-
// Up to 8 visual lines
|
|
1089
|
+
// Up to 8 visual lines from the HEAD of the prompt (keep opening, not tail).
|
|
917
1090
|
const PROMPT_PREVIEW_LINES = 8;
|
|
918
1091
|
if (cache.width !== width || cache.promptLines === undefined) {
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
1092
|
+
const innerWidth = Math.max(1, width - indent.length);
|
|
1093
|
+
const allVisual: string[] = [];
|
|
1094
|
+
for (const raw of details.task.split("\n")) {
|
|
1095
|
+
for (const w of wrapLine(raw, innerWidth)) allVisual.push(w);
|
|
1096
|
+
}
|
|
1097
|
+
const head = allVisual.slice(0, PROMPT_PREVIEW_LINES);
|
|
1098
|
+
cache.promptLines = head.map((l) => truncateToWidth(indent + l, width, "..."));
|
|
1099
|
+
cache.promptSkipped = Math.max(0, allVisual.length - head.length);
|
|
922
1100
|
}
|
|
923
1101
|
out.push(...cache.promptLines);
|
|
1102
|
+
if ((cache.promptSkipped ?? 0) > 0) {
|
|
1103
|
+
out.push(truncateToWidth(ellipsisLine(cache.promptSkipped!), width, "..."));
|
|
1104
|
+
}
|
|
924
1105
|
}
|
|
925
1106
|
}
|
|
926
1107
|
|
|
@@ -950,6 +1131,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
950
1131
|
cache.skipped = preview.skippedCount;
|
|
951
1132
|
cache.width = width;
|
|
952
1133
|
}
|
|
1134
|
+
// truncateToVisualLines keeps the tail — show ellipsis BEFORE the visible lines.
|
|
1135
|
+
if ((cache.skipped ?? 0) > 0) {
|
|
1136
|
+
out.push(truncateToWidth(ellipsisLine(cache.skipped!), width, "..."));
|
|
1137
|
+
}
|
|
953
1138
|
out.push(...(cache.responseLines ?? []));
|
|
954
1139
|
}
|
|
955
1140
|
}
|
|
@@ -1010,6 +1195,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1010
1195
|
`**Description:** ${agent.description}`,
|
|
1011
1196
|
agent.model ? `**Model:** ${agent.model}` : null,
|
|
1012
1197
|
`**Tools:** ${formatTools(agent.tools)}`,
|
|
1198
|
+
`**Max subagent depth:** ${agent.maxDepth}`,
|
|
1013
1199
|
agent.systemPrompt ? `\n**System prompt:**\n${agent.systemPrompt}` : null,
|
|
1014
1200
|
].filter(Boolean).join("\n");
|
|
1015
1201
|
return { content: [{ type: "text", text: info }] };
|
|
@@ -1129,6 +1315,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1129
1315
|
return {
|
|
1130
1316
|
content: [{ type: "text", text: `Moved to background: ${bgJobId}. Completion will be announced automatically.` }],
|
|
1131
1317
|
details: {
|
|
1318
|
+
agentName: params.agent,
|
|
1132
1319
|
task: params.task,
|
|
1133
1320
|
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
1134
1321
|
running: false,
|
|
@@ -1142,6 +1329,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1142
1329
|
return {
|
|
1143
1330
|
content: [{ type: "text", text: getFinalText(result) }],
|
|
1144
1331
|
details: {
|
|
1332
|
+
agentName: params.agent,
|
|
1145
1333
|
task: params.task,
|
|
1146
1334
|
usage: result.usage,
|
|
1147
1335
|
running: false,
|