@d3ara1n/pi-subagent 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -7
- package/package.json +1 -1
- package/src/config.ts +1 -0
- package/src/index.ts +199 -31
- package/src/roles.ts +72 -20
- package/src/spawn.ts +132 -26
- package/src/types.ts +17 -0
package/README.md
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Role-based subagent orchestration for [pi](https://github.com/earendil-works/pi).
|
|
4
4
|
|
|
5
|
-
Provides a `delegate` tool that lets the main model
|
|
5
|
+
Provides a `delegate` tool that lets the main model offload tasks to specialized pi child processes with configurable model roles, real-time TUI progress, and AI-generated summaries.
|
|
6
|
+
|
|
7
|
+
## Design Philosophy
|
|
8
|
+
|
|
9
|
+
**The main model is the decision maker; subagents are executors.**
|
|
10
|
+
|
|
11
|
+
Your primary AI has the most complete context — it knows the full conversation history, project structure, and task at hand. Subagents are spawned with **clean, isolated contexts** to handle specific, well-defined tasks without polluting the main model's context window.
|
|
12
|
+
|
|
13
|
+
This means:
|
|
14
|
+
- **Subagents don't plan** — the main model decides what needs to be done and provides a clear task description
|
|
15
|
+
- **Subagents don't orchestrate** — if a task requires multiple steps, the main model examines each result and decides the next move
|
|
16
|
+
- **Subagents don't inherit history** — they don't need the full conversation; just a precise task description
|
|
17
|
+
- **Multiple subagents can run in parallel** — emit multiple `delegate` calls in one turn; pi executes them concurrently
|
|
18
|
+
- **Subagents can nest subagents** — a `worker` can delegate exploration to `explorer` without returning to the main model
|
|
19
|
+
|
|
20
|
+
> This design intentionally excludes chain pipelines and context-forking — those patterns are better suited when subagents act as advisors (planner, oracle), not executors.
|
|
6
21
|
|
|
7
22
|
## How it works
|
|
8
23
|
|
|
@@ -15,12 +30,16 @@ Provides a `delegate` tool that lets the main model delegate tasks to specialize
|
|
|
15
30
|
|
|
16
31
|
## Built-in Roles
|
|
17
32
|
|
|
18
|
-
| Role | Model Role | Tools | Description |
|
|
19
|
-
|
|
20
|
-
| `explorer` | fast | read,
|
|
21
|
-
| `reviewer` | heavy | read, bash, grep, glob | Deep code review (read-only) |
|
|
22
|
-
| `worker` | default | read, bash, edit, write, grep, glob | Implementation
|
|
23
|
-
| `researcher` | fast | web_search, fetch_content, read | Web research
|
|
33
|
+
| Role | Model Role | Tools | Can Delegate To | Description |
|
|
34
|
+
|------|-----------|-------|-----------------|-------------|
|
|
35
|
+
| `explorer` | fast | read, find, grep, glob | — | Fast code search (read-only, no bash) |
|
|
36
|
+
| `reviewer` | heavy | read, bash, grep, glob | — | Deep code review (read-only, bash for git/log) |
|
|
37
|
+
| `worker` | default | read, bash, edit, write, grep, glob, delegate | explorer, researcher | Implementation — the only role that can modify files |
|
|
38
|
+
| `researcher` | fast | web_search, fetch_content, read, bash, delegate | explorer | Web research + GitHub repo analysis |
|
|
39
|
+
|
|
40
|
+
**Nested delegation**: `worker` and `researcher` can spawn their own subagents. This keeps the main model's context clean — a worker can explore unfamiliar code via an `explorer` subagent without returning intermediate results to the main model.
|
|
41
|
+
|
|
42
|
+
**Parallel execution**: To run multiple subagents concurrently, emit multiple `delegate` calls in a single turn. Pi's framework executes them in parallel automatically, with each subagent getting its own TUI progress display.
|
|
24
43
|
|
|
25
44
|
## TUI Display
|
|
26
45
|
|
|
@@ -62,8 +81,51 @@ Edit `~/.pi/agent/settings.json`:
|
|
|
62
81
|
|
|
63
82
|
All fields are optional. Defaults: `timeoutMs: 300000`, `summary.role: "utility"`, `summary.enabled: true`.
|
|
64
83
|
|
|
84
|
+
### Agent Overrides
|
|
85
|
+
|
|
86
|
+
Override, disable, or add subagent roles via `agentOverrides`. Built-in and custom roles are treated equally — all descriptions, examples, and decision triggers feed into the LLM's prompt dynamically.
|
|
87
|
+
|
|
88
|
+
```jsonc
|
|
89
|
+
{
|
|
90
|
+
"subagent": {
|
|
91
|
+
"agentOverrides": {
|
|
92
|
+
// ── Override a built-in role (only specify changed fields) ──
|
|
93
|
+
"worker": {
|
|
94
|
+
"role": "heavy" // use a stronger model
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// ── Disable a built-in role ──
|
|
98
|
+
"reviewer": {
|
|
99
|
+
"disabled": true
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// ── Add a custom role (all required fields must be provided) ──
|
|
103
|
+
"tester": {
|
|
104
|
+
"role": "default",
|
|
105
|
+
"description": "Test automation & QA — write and run tests, validate fixes. Tools: read, bash, edit, write, grep. Can delegate to explorer.",
|
|
106
|
+
"examples": [
|
|
107
|
+
"Write unit tests for the auth module",
|
|
108
|
+
"Run the test suite and fix failing tests"
|
|
109
|
+
],
|
|
110
|
+
"decisionTrigger": "Task writes or runs tests?",
|
|
111
|
+
"tools": ["read", "bash", "edit", "write", "grep"],
|
|
112
|
+
"systemPrompt": "QA engineer. Write tests, run them, fix failures. After each change, re-run affected tests."
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Required fields for custom roles:** `role`, `description`, `examples`, `decisionTrigger`, `tools`, `systemPrompt`.
|
|
120
|
+
|
|
121
|
+
**Optional fields:** `subagentRoles` (roles this role can spawn via delegate), `fallbackRole` (backup pi-model-roles role on provider errors).
|
|
122
|
+
|
|
123
|
+
Invalid custom roles (missing required fields) are silently skipped with an error notification at session start.
|
|
124
|
+
|
|
65
125
|
## Usage (by the main model)
|
|
66
126
|
|
|
127
|
+
Delegate tasks that would generate many tool calls or verbose output to keep your own context clean:
|
|
128
|
+
|
|
67
129
|
```json
|
|
68
130
|
{
|
|
69
131
|
"role": "explorer",
|
|
@@ -71,6 +133,24 @@ All fields are optional. Defaults: `timeoutMs: 300000`, `summary.role: "utility"
|
|
|
71
133
|
}
|
|
72
134
|
```
|
|
73
135
|
|
|
136
|
+
**Role-specific examples:**
|
|
137
|
+
|
|
138
|
+
| Role | Example task | Why delegate? |
|
|
139
|
+
|------|-------------|---------------|
|
|
140
|
+
| `explorer` | `"Map the routing structure of src/api/"` | You only need the conclusion, not every grep result |
|
|
141
|
+
| `reviewer` | `"Review error handling in auth.ts for security issues"` | Review output is longform; keep it isolated |
|
|
142
|
+
| `worker` | `"Rename all snake_case fields to camelCase in src/models/"` | Your context stays focused on high-level intent |
|
|
143
|
+
| `researcher` | `"Find the React 19 migration guide and summarize breaking changes"` | Search results are noisy; get a clean summary |
|
|
144
|
+
|
|
145
|
+
**Parallel usage:** emit multiple `delegate` calls in a single turn:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
[
|
|
149
|
+
{ "role": "explorer", "task": "Map the repository structure" },
|
|
150
|
+
{ "role": "researcher", "task": "Find latest docs on the library used here" }
|
|
151
|
+
]
|
|
152
|
+
```
|
|
153
|
+
|
|
74
154
|
## License
|
|
75
155
|
|
|
76
156
|
MIT
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -15,11 +15,11 @@ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
|
15
15
|
import { Type } from "typebox";
|
|
16
16
|
import type { ModelRolesAPI } from "@d3ara1n/pi-model-roles";
|
|
17
17
|
import { getModelRolesAPI } from "@d3ara1n/pi-model-roles";
|
|
18
|
-
import type { SubagentConfig, SubagentDetails, SubagentResult } from "./types.ts";
|
|
18
|
+
import type { SubagentConfig, SubagentDetails, SubagentResult, SubagentRole } from "./types.ts";
|
|
19
19
|
import { DEFAULT_CONFIG } from "./types.ts";
|
|
20
20
|
import { loadSubagentConfig } from "./config.ts";
|
|
21
21
|
import { BUILTIN_ROLES } from "./roles.ts";
|
|
22
|
-
import { spawnSubagent } from "./spawn.ts";
|
|
22
|
+
import { spawnSubagent, getPiInvocation } from "./spawn.ts";
|
|
23
23
|
import * as os from "node:os";
|
|
24
24
|
|
|
25
25
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
@@ -63,6 +63,9 @@ function getDisplayItems(messages: SubagentResult["messages"]): DisplayItem[] {
|
|
|
63
63
|
|
|
64
64
|
function shortenPath(p: string): string {
|
|
65
65
|
const home = os.homedir();
|
|
66
|
+
if (process.platform === "win32") {
|
|
67
|
+
return p.toLowerCase().startsWith(home.toLowerCase()) ? `~${p.slice(home.length)}` : p;
|
|
68
|
+
}
|
|
66
69
|
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -172,12 +175,20 @@ async function generateSummary(
|
|
|
172
175
|
const resolved = await rolesApi.resolveRoleAsync(summaryConfig.role);
|
|
173
176
|
if (!resolved.model) return undefined;
|
|
174
177
|
|
|
178
|
+
// Truncate large outputs to avoid wasting summary tokens (keep head + tail)
|
|
179
|
+
const SUMMARY_MAX_INPUT = 4000;
|
|
180
|
+
let summaryInput = outputText;
|
|
181
|
+
if (summaryInput.length > SUMMARY_MAX_INPUT) {
|
|
182
|
+
const half = Math.floor(SUMMARY_MAX_INPUT / 2);
|
|
183
|
+
summaryInput = summaryInput.slice(0, half) + "\n\n... [truncated for summary] ...\n\n" + summaryInput.slice(-half);
|
|
184
|
+
}
|
|
185
|
+
|
|
175
186
|
const result = await complete(
|
|
176
187
|
resolved.model,
|
|
177
188
|
{
|
|
178
189
|
systemPrompt:
|
|
179
190
|
"Summarize the following agent output in one concise Chinese sentence (max 60 characters). Focus on what was accomplished, not how. Output only the summary, no preamble.",
|
|
180
|
-
messages: [{ role: "user", content:
|
|
191
|
+
messages: [{ role: "user", content: summaryInput }],
|
|
181
192
|
},
|
|
182
193
|
{
|
|
183
194
|
maxTokens: 100,
|
|
@@ -194,7 +205,12 @@ async function generateSummary(
|
|
|
194
205
|
|
|
195
206
|
return text || undefined;
|
|
196
207
|
} catch {
|
|
197
|
-
|
|
208
|
+
// Fall back to manual truncation: use first line of output as summary
|
|
209
|
+
const trimmed = outputText.trim();
|
|
210
|
+
if (!trimmed) return undefined;
|
|
211
|
+
const firstLine = trimmed.split("\n")[0];
|
|
212
|
+
if (firstLine.length <= 65) return firstLine;
|
|
213
|
+
return firstLine.slice(0, 62) + "...";
|
|
198
214
|
}
|
|
199
215
|
}
|
|
200
216
|
|
|
@@ -203,49 +219,123 @@ async function generateSummary(
|
|
|
203
219
|
export default function subagentExtension(pi: ExtensionAPI) {
|
|
204
220
|
let config: SubagentConfig = DEFAULT_CONFIG;
|
|
205
221
|
|
|
222
|
+
// If spawned as a child by a parent subagent, PI_SUBAGENT_ALLOWED restricts
|
|
223
|
+
// which roles are available. Filter before any tool description sees them.
|
|
224
|
+
const ALLOWLIST: string[] | undefined = (() => {
|
|
225
|
+
const raw = process.env.PI_SUBAGENT_ALLOWED;
|
|
226
|
+
if (!raw) return undefined;
|
|
227
|
+
const list = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
228
|
+
return list.length > 0 ? list : undefined;
|
|
229
|
+
})();
|
|
230
|
+
|
|
231
|
+
const availableRoles: Record<string, SubagentRole> = {};
|
|
232
|
+
for (const [name, role] of Object.entries(BUILTIN_ROLES)) {
|
|
233
|
+
if (!ALLOWLIST || ALLOWLIST.includes(name)) {
|
|
234
|
+
availableRoles[name] = role;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Mutable guidelines array — rebuilt in session_start to reflect agentOverrides
|
|
239
|
+
const guidelines: string[] = [];
|
|
240
|
+
|
|
241
|
+
function rebuildGuidelines(roles: Record<string, SubagentRole>): void {
|
|
242
|
+
const entries = Object.entries(roles);
|
|
243
|
+
const exampleLines: string[] = [];
|
|
244
|
+
const decisionLines: string[] = [];
|
|
245
|
+
|
|
246
|
+
for (const [name, role] of entries) {
|
|
247
|
+
// Decision flow
|
|
248
|
+
decisionLines.push(` ${role.decisionTrigger} → delegate(${name})`);
|
|
249
|
+
|
|
250
|
+
// Concrete examples — one line per role with comma-separated examples
|
|
251
|
+
const quotedExamples = role.examples.map((e) => `"${e}"`).join(", ");
|
|
252
|
+
exampleLines.push(` delegate(${name}): ${quotedExamples}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
guidelines.length = 0;
|
|
256
|
+
guidelines.push(
|
|
257
|
+
"WHEN TO DELEGATE — offload substantial work when you only need the result:",
|
|
258
|
+
"",
|
|
259
|
+
"- Delegate ONLY when a task involves significant work (heavy analysis, multi-step investigation, large-scope changes) AND you only care about the conclusion, not intermediate steps.",
|
|
260
|
+
"- DO NOT delegate simple tasks: a single read, a one-line edit, a basic grep. Just do them yourself.",
|
|
261
|
+
"- DO NOT delegate straightforward file modifications touching 1-2 files. Use edit/write directly.",
|
|
262
|
+
"- Delegation has overhead (spawning a child process). Reserve it for tasks that would genuinely clutter your context with 3+ turns of raw tool output.",
|
|
263
|
+
"",
|
|
264
|
+
"AVAILABLE ROLES:",
|
|
265
|
+
...entries.map(([name, role]) => ` - ${name}: ${role.description}`),
|
|
266
|
+
"",
|
|
267
|
+
"DECISION FLOW (which role for what):",
|
|
268
|
+
"",
|
|
269
|
+
...decisionLines,
|
|
270
|
+
"",
|
|
271
|
+
"CONCRETE EXAMPLES of good delegation targets:",
|
|
272
|
+
"",
|
|
273
|
+
...exampleLines,
|
|
274
|
+
"",
|
|
275
|
+
"For multiple independent substantial tasks, emit multiple delegate calls in one turn — they run in parallel.",
|
|
276
|
+
"Include ALL necessary context — subagents have no access to this conversation.",
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Apply agent overrides on top of built-in roles
|
|
281
|
+
function applyAgentOverrides(roles: Record<string, SubagentRole>, overrides: Record<string, any>): void {
|
|
282
|
+
for (const [name, override] of Object.entries(overrides)) {
|
|
283
|
+
if (override.disabled) {
|
|
284
|
+
delete roles[name];
|
|
285
|
+
} else if (roles[name]) {
|
|
286
|
+
roles[name] = { ...roles[name], ...override };
|
|
287
|
+
} else {
|
|
288
|
+
// Custom role — must provide all required fields (validated in session_start)
|
|
289
|
+
roles[name] = override as SubagentRole;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Initial guidelines from built-in roles
|
|
295
|
+
rebuildGuidelines(availableRoles);
|
|
296
|
+
|
|
206
297
|
pi.on("session_start", async (_event, ctx) => {
|
|
207
298
|
config = loadSubagentConfig(ctx.cwd);
|
|
299
|
+
applyAgentOverrides(availableRoles, config.agentOverrides);
|
|
300
|
+
|
|
301
|
+
// Validate custom roles (skip built-in roles — they already have all fields)
|
|
302
|
+
const REQUIRED_FIELDS = ["role", "description", "examples", "decisionTrigger", "tools", "systemPrompt"] as const;
|
|
303
|
+
for (const [name, role] of Object.entries(availableRoles)) {
|
|
304
|
+
if (name in BUILTIN_ROLES) continue;
|
|
305
|
+
const missing = REQUIRED_FIELDS.filter((f) => !(f in (role as any)));
|
|
306
|
+
if (missing.length > 0) {
|
|
307
|
+
delete availableRoles[name];
|
|
308
|
+
ctx.ui.notify(
|
|
309
|
+
`[pi-subagent] Custom role "${name}" skipped — missing: ${missing.join(", ")}. Required: ${REQUIRED_FIELDS.join(", ")}.`,
|
|
310
|
+
"error",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
rebuildGuidelines(availableRoles);
|
|
208
316
|
});
|
|
209
317
|
|
|
210
318
|
pi.registerTool({
|
|
211
319
|
name: "delegate",
|
|
212
320
|
label: "Delegate to subagent",
|
|
213
|
-
description:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
" - explorer: fast code search and navigation (read-only)",
|
|
217
|
-
" - reviewer: deep code review with evidence (read-only)",
|
|
218
|
-
" - worker: implementation with file editing capabilities",
|
|
219
|
-
" - researcher: web research and documentation lookup",
|
|
220
|
-
"",
|
|
221
|
-
"Progress is shown in real-time via TUI (tool calls, turns, elapsed time).",
|
|
222
|
-
"Use Ctrl+O on a completed result to see full details.",
|
|
223
|
-
"",
|
|
224
|
-
"Note: Subagents only have built-in tools (read, bash, edit, write, grep, glob, find, web_search, fetch_content). They do NOT have access to MCP tools or custom tools from the main session.",
|
|
225
|
-
].join("\n"),
|
|
321
|
+
description: "Offload work to a specialized subagent to keep your own context clean and focused. Prefer this over doing work yourself when a task would generate many tool calls or verbose output. Subagents have isolated context — include all necessary info in the task description.",
|
|
322
|
+
promptSnippet: "Delegate tasks to specialized subagents",
|
|
323
|
+
promptGuidelines: guidelines,
|
|
226
324
|
|
|
227
325
|
parameters: Type.Object({
|
|
228
|
-
role: Type.
|
|
229
|
-
[
|
|
230
|
-
Type.Literal("explorer"),
|
|
231
|
-
Type.Literal("reviewer"),
|
|
232
|
-
Type.Literal("worker"),
|
|
233
|
-
Type.Literal("researcher"),
|
|
234
|
-
],
|
|
235
|
-
{ description: "Subagent role to use" },
|
|
236
|
-
),
|
|
326
|
+
role: Type.String({ description: "Subagent role to use" }),
|
|
237
327
|
task: Type.String({ description: "Specific task for the subagent" }),
|
|
238
328
|
cwd: Type.Optional(Type.String({ description: "Working directory (defaults to current)" })),
|
|
239
329
|
}),
|
|
240
330
|
|
|
241
331
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
242
|
-
const roleDef =
|
|
332
|
+
const roleDef = availableRoles[params.role];
|
|
243
333
|
if (!roleDef) {
|
|
244
334
|
return {
|
|
245
335
|
content: [
|
|
246
336
|
{
|
|
247
337
|
type: "text",
|
|
248
|
-
text: `Unknown subagent role: ${params.role}. Available: ${Object.keys(
|
|
338
|
+
text: `Unknown subagent role: ${params.role}. Available: ${Object.keys(availableRoles).join(", ")}`,
|
|
249
339
|
},
|
|
250
340
|
],
|
|
251
341
|
};
|
|
@@ -289,10 +379,11 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
289
379
|
}
|
|
290
380
|
|
|
291
381
|
try {
|
|
292
|
-
|
|
382
|
+
let result = await spawnSubagent(modelRef, params.task, {
|
|
293
383
|
cwd: params.cwd ?? ctx.cwd,
|
|
294
384
|
tools: roleDef.tools,
|
|
295
385
|
systemPrompt: roleDef.systemPrompt,
|
|
386
|
+
subagentRoles: roleDef.subagentRoles,
|
|
296
387
|
timeoutMs: config.timeoutMs,
|
|
297
388
|
signal,
|
|
298
389
|
onProgress: (partial) => {
|
|
@@ -325,6 +416,27 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
325
416
|
},
|
|
326
417
|
});
|
|
327
418
|
|
|
419
|
+
// Retry with fallback role on provider errors (quota, auth, timeout, etc.)
|
|
420
|
+
if ((result.exitCode !== 0 || result.errorMessage) && roleDef.fallbackRole) {
|
|
421
|
+
const isProviderError = /429|quota|rate.?limit|auth|timeout|exhausted|unavailable/i.test(
|
|
422
|
+
(result.stderr || "") + (result.errorMessage || ""),
|
|
423
|
+
);
|
|
424
|
+
if (isProviderError) {
|
|
425
|
+
const fallback = await rolesApi.resolveRoleAsync(roleDef.fallbackRole);
|
|
426
|
+
if (fallback.model) {
|
|
427
|
+
const fbRef = `${fallback.model.provider}/${fallback.model.id}`;
|
|
428
|
+
result = await spawnSubagent(fbRef, params.task, {
|
|
429
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
430
|
+
tools: roleDef.tools,
|
|
431
|
+
systemPrompt: roleDef.systemPrompt,
|
|
432
|
+
subagentRoles: roleDef.subagentRoles,
|
|
433
|
+
timeoutMs: config.timeoutMs,
|
|
434
|
+
signal,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
328
440
|
// Generate summary for TUI display
|
|
329
441
|
if (config.summary.enabled && result.output.trim()) {
|
|
330
442
|
result.summary = await generateSummary(rolesApi, result.output, config.summary);
|
|
@@ -469,8 +581,9 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
469
581
|
if (r.summary) {
|
|
470
582
|
text += ` ${theme.fg("dim", "\u00b7")} ${theme.fg("text", r.summary)}`;
|
|
471
583
|
}
|
|
472
|
-
if (isError
|
|
473
|
-
|
|
584
|
+
if (isError) {
|
|
585
|
+
const errMsg = r.errorMessage || (r.stderr ? r.stderr.trim().split("\n")[0].slice(0, 80) : r.stopReason);
|
|
586
|
+
if (errMsg) text += `\n${theme.fg("error", `Error: ${errMsg}`)}`;
|
|
474
587
|
}
|
|
475
588
|
const usageStr = formatUsageStats(r.usage, r.model);
|
|
476
589
|
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
@@ -478,4 +591,59 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
478
591
|
return new Text(text, 0, 0);
|
|
479
592
|
},
|
|
480
593
|
});
|
|
594
|
+
|
|
595
|
+
pi.registerCommand("subagent:doctor", {
|
|
596
|
+
description: "Diagnose pi-subagent configuration and dependencies",
|
|
597
|
+
handler: async (_args, ctx) => {
|
|
598
|
+
const lines: string[] = [];
|
|
599
|
+
let allOk = true;
|
|
600
|
+
|
|
601
|
+
// 1. pi executable
|
|
602
|
+
const inv = getPiInvocation(["--version"]);
|
|
603
|
+
lines.push(`[\u2713] pi invocation: ${inv.command} ${inv.args.slice(0, 1).join(" ")}`);
|
|
604
|
+
|
|
605
|
+
// 2. pi-model-roles
|
|
606
|
+
try {
|
|
607
|
+
const api = getModelRolesAPI();
|
|
608
|
+
lines.push("[\u2713] pi-model-roles: loaded");
|
|
609
|
+
|
|
610
|
+
// 3. config
|
|
611
|
+
try {
|
|
612
|
+
const cfg = loadSubagentConfig(ctx.cwd);
|
|
613
|
+
lines.push(`[\u2713] config: timeout=${cfg.timeoutMs}ms summary=${cfg.summary.enabled ? cfg.summary.role : "off"}`);
|
|
614
|
+
} catch {
|
|
615
|
+
lines.push("[\u2717] config: failed to load");
|
|
616
|
+
allOk = false;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 4. roles
|
|
620
|
+
for (const [name, role] of Object.entries(availableRoles)) {
|
|
621
|
+
try {
|
|
622
|
+
const resolved = await api.resolveRoleAsync(role.role);
|
|
623
|
+
if (resolved.model) {
|
|
624
|
+
lines.push(`[\u2713] role ${name}: \u2192 ${resolved.model.provider}/${resolved.model.id}`);
|
|
625
|
+
} else {
|
|
626
|
+
lines.push(`[\u2717] role ${name}: model not resolved (role config: ${role.role})`);
|
|
627
|
+
allOk = false;
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
lines.push(`[\u2717] role ${name}: resolution failed`);
|
|
631
|
+
allOk = false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
} catch {
|
|
635
|
+
lines.push("[\u2717] pi-model-roles: not initialized");
|
|
636
|
+
allOk = false;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// 5. ALLOWLIST
|
|
640
|
+
const allowed = process.env.PI_SUBAGENT_ALLOWED;
|
|
641
|
+
if (allowed) {
|
|
642
|
+
lines.push(`[i] PI_SUBAGENT_ALLOWED: ${allowed}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const summary = allOk ? "All checks passed" : "Some checks failed";
|
|
646
|
+
ctx.ui.notify(`${summary}\n\n${lines.join("\n")}`, "info");
|
|
647
|
+
},
|
|
648
|
+
});
|
|
481
649
|
}
|
package/src/roles.ts
CHANGED
|
@@ -11,47 +11,99 @@ import type { SubagentRole } from "./types.ts";
|
|
|
11
11
|
export const BUILTIN_ROLES: Record<string, SubagentRole> = {
|
|
12
12
|
explorer: {
|
|
13
13
|
role: "fast",
|
|
14
|
-
|
|
14
|
+
fallbackRole: "default",
|
|
15
|
+
description: "READ-ONLY codebase exploration — locate files, grep symbols, trace imports, explain structures. Tools: read, find, grep, glob. NO bash, NO edits, NO web access.",
|
|
16
|
+
examples: [
|
|
17
|
+
"Find where auth middleware is implemented",
|
|
18
|
+
"Map the routing structure",
|
|
19
|
+
],
|
|
20
|
+
decisionTrigger: "Task finds or maps code without touch?",
|
|
21
|
+
tools: ["read", "find", "grep", "glob"],
|
|
15
22
|
systemPrompt: [
|
|
16
|
-
"
|
|
17
|
-
"
|
|
23
|
+
"Fast code explorer. You have READ-ONLY tools only — no commands, no edits.",
|
|
24
|
+
"Grep/find to locate → read key sections only → identify types, interfaces, functions.",
|
|
25
|
+
"Never read entire files. Target specific line ranges.",
|
|
18
26
|
"",
|
|
19
|
-
"Output
|
|
20
|
-
"
|
|
27
|
+
"Output format (keep each section brief):",
|
|
28
|
+
"## Files: file paths with line ranges and one-line descriptions",
|
|
29
|
+
"## Findings: key types/functions with minimal code snippets",
|
|
30
|
+
"## Summary: direct answer to the task question",
|
|
21
31
|
].join("\n"),
|
|
22
32
|
},
|
|
23
33
|
reviewer: {
|
|
24
34
|
role: "heavy",
|
|
35
|
+
fallbackRole: "default",
|
|
36
|
+
description: "READ-ONLY code review & analysis — audit code, assess architecture, review diffs. Tools: read, bash, grep, glob. Has bash (git diff/log, test runs). NO edits, NO web access.",
|
|
37
|
+
examples: [
|
|
38
|
+
"Review the error handling in src/api/ for security issues",
|
|
39
|
+
"Audit this PR diff for performance regressions",
|
|
40
|
+
],
|
|
41
|
+
decisionTrigger: "Task audits or reviews code quality?",
|
|
25
42
|
tools: ["read", "bash", "grep", "glob"],
|
|
26
43
|
systemPrompt: [
|
|
27
|
-
"
|
|
28
|
-
"
|
|
44
|
+
"Senior code reviewer. READ-ONLY — you must NOT modify any file.",
|
|
45
|
+
"bash is for read-only commands only (git diff/log/show, test runs). Never use sed, tee, echo >, or any write command.",
|
|
46
|
+
"Provide evidence-backed findings with file:line references.",
|
|
29
47
|
"",
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
"",
|
|
33
|
-
"
|
|
48
|
+
"Output format (prioritize critical issues first):",
|
|
49
|
+
"## Issues: severity + file:line + description + suggested fix",
|
|
50
|
+
"## Observations: notable patterns or design concerns",
|
|
51
|
+
"## Summary: overall assessment in 1-2 sentences",
|
|
34
52
|
].join("\n"),
|
|
35
53
|
},
|
|
36
54
|
worker: {
|
|
37
55
|
role: "default",
|
|
38
|
-
|
|
56
|
+
description: "the ONLY role that can MODIFY files — edit, write, refactor, fix, implement. Tools: read, bash, edit, write, grep, glob, delegate. Can delegate to explorer/researcher.",
|
|
57
|
+
examples: [
|
|
58
|
+
"Rename all snake_case fields to camelCase",
|
|
59
|
+
"Add input validation to POST /login",
|
|
60
|
+
],
|
|
61
|
+
decisionTrigger: "Task modifies files?",
|
|
62
|
+
tools: ["read", "bash", "edit", "write", "grep", "glob", "delegate"],
|
|
63
|
+
subagentRoles: ["explorer", "researcher"],
|
|
39
64
|
systemPrompt: [
|
|
40
|
-
"
|
|
41
|
-
"
|
|
65
|
+
"Implementation worker. Work autonomously — all context is in the task description.",
|
|
66
|
+
"Always read a file before editing it. Make minimal, focused changes.",
|
|
67
|
+
"After each change, validate: run tests, check syntax, verify behavior.",
|
|
68
|
+
"",
|
|
69
|
+
"## Protecting your context",
|
|
70
|
+
"You have a `delegate` tool. Use it to offload exploration and research:",
|
|
71
|
+
"- delegate(role=explorer) when you need to map unfamiliar code before editing",
|
|
72
|
+
"- delegate(role=researcher) when you need external docs or library references",
|
|
73
|
+
"Don't delegate tasks you can do with a single read or grep.",
|
|
42
74
|
"",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
75
|
+
"Output format (be brief — summarize, don't paste full diffs):",
|
|
76
|
+
"## Changes: list each file touched and what changed",
|
|
77
|
+
"## Verification: what you ran to confirm correctness",
|
|
45
78
|
].join("\n"),
|
|
46
79
|
},
|
|
47
80
|
researcher: {
|
|
48
81
|
role: "fast",
|
|
49
|
-
|
|
82
|
+
fallbackRole: "default",
|
|
83
|
+
description: "the ONLY role with WEB ACCESS — search docs, fetch pages, analyze GitHub repos. Tools: web_search, fetch_content, read, bash, delegate. Can clone repos & delegate to explorer.",
|
|
84
|
+
examples: [
|
|
85
|
+
"Find the React 19 migration guide",
|
|
86
|
+
"Check GitHub issue #1234 for context",
|
|
87
|
+
],
|
|
88
|
+
decisionTrigger: "Task searches web or GitHub?",
|
|
89
|
+
tools: ["web_search", "fetch_content", "read", "bash", "delegate"],
|
|
90
|
+
subagentRoles: ["explorer"],
|
|
50
91
|
systemPrompt: [
|
|
51
|
-
"
|
|
92
|
+
"Web researcher. Search with varied angles, prefer official docs over blogs.",
|
|
93
|
+
"If first results are insufficient, refine queries and search again.",
|
|
94
|
+
"",
|
|
95
|
+
"## GitHub repo analysis",
|
|
96
|
+
"When the task requires analyzing a GitHub repo:",
|
|
97
|
+
"1. git clone the repo into PI_SUBAGENT_TMPDIR (must exist)",
|
|
98
|
+
"2. Use `delegate` with role=explorer to investigate the cloned codebase — pass the repo path and the research question",
|
|
99
|
+
"3. Combine explorer findings with any web search results",
|
|
100
|
+
"",
|
|
101
|
+
"bash is for git clone and read-only commands only. Never modify files.",
|
|
52
102
|
"",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
103
|
+
"Output format:",
|
|
104
|
+
"## Answer: direct answer to the question (2-3 sentences)",
|
|
105
|
+
"## Sources: list of URLs used",
|
|
106
|
+
"## Gaps: what could not be answered",
|
|
55
107
|
].join("\n"),
|
|
56
108
|
},
|
|
57
109
|
};
|
package/src/spawn.ts
CHANGED
|
@@ -10,28 +10,106 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
13
14
|
import type { SubagentMessage, SubagentResult } from "./types.ts";
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
+
/** Maximum task length before writing to a temp file (avoids CLI arg limits). */
|
|
17
|
+
const TASK_CHAR_LIMIT = 8000;
|
|
18
|
+
|
|
19
|
+
/** Maximum output characters returned to the main model. Larger outputs are truncated. */
|
|
20
|
+
const MAX_OUTPUT_CHARS = 50_000;
|
|
21
|
+
|
|
22
|
+
const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
23
|
+
|
|
24
|
+
function isRunnableScript(filePath: string): boolean {
|
|
25
|
+
try {
|
|
26
|
+
if (!fs.existsSync(filePath)) return false;
|
|
27
|
+
return /\.(?:mjs|cjs|js)$/i.test(filePath);
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findPiPackageRootFromEntry(entryPoint: string): string | undefined {
|
|
34
|
+
let dir = path.dirname(entryPoint);
|
|
35
|
+
while (dir !== path.dirname(dir)) {
|
|
36
|
+
const pkgPath = path.join(dir, "package.json");
|
|
37
|
+
if (fs.existsSync(pkgPath)) {
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { name?: unknown };
|
|
40
|
+
if (pkg.name === PI_CODING_AGENT_PACKAGE) return dir;
|
|
41
|
+
} catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
dir = path.dirname(dir);
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveWindowsPiCliScript(args: string[]): { command: string; args: string[] } | undefined {
|
|
51
|
+
// Strategy 1: Use process.argv[1] if it's a runnable script
|
|
52
|
+
// (works when pi is run via `bun pi` or `bunx pi` — argv[1] is the real CLI path)
|
|
53
|
+
const argv1 = process.argv[1];
|
|
54
|
+
if (argv1) {
|
|
55
|
+
const argvPath = path.isAbsolute(argv1) ? argv1 : path.resolve(argv1);
|
|
56
|
+
if (isRunnableScript(argvPath)) {
|
|
57
|
+
return { command: process.execPath, args: [argvPath, ...args] };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Strategy 2: Resolve pi-coding-agent package via import.meta.resolve,
|
|
62
|
+
// then read the bin field from its package.json
|
|
63
|
+
try {
|
|
64
|
+
const resolved = fileURLToPath(import.meta.resolve(PI_CODING_AGENT_PACKAGE));
|
|
65
|
+
const root = findPiPackageRootFromEntry(resolved);
|
|
66
|
+
if (root) {
|
|
67
|
+
const pkgPath = path.join(root, "package.json");
|
|
68
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as {
|
|
69
|
+
bin?: string | Record<string, string>;
|
|
70
|
+
};
|
|
71
|
+
const binField = pkg.bin;
|
|
72
|
+
const binPath =
|
|
73
|
+
typeof binField === "string"
|
|
74
|
+
? binField
|
|
75
|
+
: binField?.pi ?? Object.values(binField ?? {})[0];
|
|
76
|
+
if (binPath) {
|
|
77
|
+
const candidate = path.resolve(root, binPath);
|
|
78
|
+
if (isRunnableScript(candidate)) {
|
|
79
|
+
return { command: process.execPath, args: [candidate, ...args] };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
/* fall through */
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Determine how to invoke pi.
|
|
92
|
+
*
|
|
93
|
+
* On Windows, attempts to find the pi CLI script via:
|
|
94
|
+
* 1. process.argv[1] (when run via `bun pi` or `bunx pi`)
|
|
95
|
+
* 2. import.meta.resolve of @earendil-works/pi-coding-agent → bin field
|
|
96
|
+
* If found, spawns process.execPath (bun) with the script path.
|
|
97
|
+
* Falls back to `pi` from PATH if neither works.
|
|
98
|
+
*
|
|
99
|
+
* On non-Windows, always uses the `pi` CLI command from PATH.
|
|
16
100
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* diagnostic commands. Using the `pi` command from PATH avoids this.
|
|
101
|
+
* This avoids the standalone compiled pi.exe's process.execPath
|
|
102
|
+
* (virtual Bun path like B:/~BUN/root/pi.exe) ever being passed
|
|
103
|
+
* to the child process, while still working when `pi` is not in PATH.
|
|
21
104
|
*/
|
|
22
|
-
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
105
|
+
export function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
106
|
+
if (process.platform === "win32") {
|
|
107
|
+
const winResult = resolveWindowsPiCliScript(args);
|
|
108
|
+
if (winResult) return winResult;
|
|
109
|
+
}
|
|
23
110
|
return { command: "pi", args };
|
|
24
111
|
}
|
|
25
112
|
|
|
26
|
-
/** Write a system prompt to a temp file for --append-system-prompt. */
|
|
27
|
-
async function writeTempPromptFile(prefix: string, content: string): Promise<{ dir: string; filePath: string }> {
|
|
28
|
-
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
|
|
29
|
-
const safeName = prefix.replace(/[^\w.-]+/g, "_");
|
|
30
|
-
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
31
|
-
await fs.promises.writeFile(filePath, content, { encoding: "utf-8", mode: 0o600 });
|
|
32
|
-
return { dir: tmpDir, filePath };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
113
|
/**
|
|
36
114
|
* Spawn a pi child process with the given model and configuration.
|
|
37
115
|
* Fires onProgress on each JSON event for streaming TUI updates.
|
|
@@ -48,6 +126,7 @@ export async function spawnSubagent(
|
|
|
48
126
|
cwd?: string;
|
|
49
127
|
tools?: string[];
|
|
50
128
|
systemPrompt?: string;
|
|
129
|
+
subagentRoles?: string[];
|
|
51
130
|
timeoutMs?: number;
|
|
52
131
|
signal?: AbortSignal;
|
|
53
132
|
onProgress?: (update: Partial<SubagentResult>) => void;
|
|
@@ -74,14 +153,23 @@ export async function spawnSubagent(
|
|
|
74
153
|
args.push("--tools", options.tools.join(","));
|
|
75
154
|
}
|
|
76
155
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
tmpDir = tmp.dir;
|
|
80
|
-
tmpFile = tmp.filePath;
|
|
81
|
-
args.push("--append-system-prompt", tmpFile);
|
|
82
|
-
}
|
|
156
|
+
// Always create temp dir — used for prompt file, long task file, and as PI_SUBAGENT_TMPDIR for subagent work (e.g. git clone)
|
|
157
|
+
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
|
|
83
158
|
|
|
84
|
-
|
|
159
|
+
const promptContent = options.systemPrompt?.trim()
|
|
160
|
+
? options.systemPrompt + `\n\nPI_SUBAGENT_TMPDIR=${tmpDir}`
|
|
161
|
+
: `PI_SUBAGENT_TMPDIR=${tmpDir}`;
|
|
162
|
+
tmpFile = path.join(tmpDir, "prompt.md");
|
|
163
|
+
await fs.promises.writeFile(tmpFile, promptContent, { encoding: "utf-8", mode: 0o600 });
|
|
164
|
+
args.push("--append-system-prompt", tmpFile);
|
|
165
|
+
|
|
166
|
+
if (task.length > TASK_CHAR_LIMIT) {
|
|
167
|
+
const taskPath = path.join(tmpDir, "task.md");
|
|
168
|
+
await fs.promises.writeFile(taskPath, task, { encoding: "utf-8", mode: 0o600 });
|
|
169
|
+
args.push(`@${taskPath}`);
|
|
170
|
+
} else {
|
|
171
|
+
args.push(`Task: ${task}`);
|
|
172
|
+
}
|
|
85
173
|
|
|
86
174
|
// Spawn process
|
|
87
175
|
const invocation = getPiInvocation(args);
|
|
@@ -143,9 +231,20 @@ export async function spawnSubagent(
|
|
|
143
231
|
}
|
|
144
232
|
};
|
|
145
233
|
|
|
234
|
+
// Build env with optional subagent allowlist and tmpdir for researcher role
|
|
235
|
+
const childEnv: NodeJS.ProcessEnv = { ...process.env };
|
|
236
|
+
if (options.subagentRoles && options.subagentRoles.length > 0) {
|
|
237
|
+
childEnv.PI_SUBAGENT_ALLOWED = options.subagentRoles.join(",");
|
|
238
|
+
}
|
|
239
|
+
// Expose tmpdir as env var so subagent bash commands (e.g. git clone) can use it
|
|
240
|
+
childEnv.PI_SUBAGENT_TMPDIR = tmpDir;
|
|
241
|
+
|
|
242
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
243
|
+
|
|
146
244
|
const exitCode = await new Promise<number>((resolve) => {
|
|
147
245
|
const proc = spawn(invocation.command, invocation.args, {
|
|
148
246
|
cwd: options.cwd,
|
|
247
|
+
env: childEnv,
|
|
149
248
|
shell: false,
|
|
150
249
|
stdio: ["ignore", "pipe", "pipe"],
|
|
151
250
|
});
|
|
@@ -162,6 +261,7 @@ export async function spawnSubagent(
|
|
|
162
261
|
});
|
|
163
262
|
|
|
164
263
|
proc.on("close", (code) => {
|
|
264
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
165
265
|
if (buffer.trim()) processLine(buffer);
|
|
166
266
|
resolve(code ?? 0);
|
|
167
267
|
});
|
|
@@ -185,7 +285,7 @@ export async function spawnSubagent(
|
|
|
185
285
|
|
|
186
286
|
// Handle timeout
|
|
187
287
|
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
188
|
-
setTimeout(() => {
|
|
288
|
+
timeoutHandle = setTimeout(() => {
|
|
189
289
|
if (!proc.killed) {
|
|
190
290
|
proc.kill("SIGTERM");
|
|
191
291
|
setTimeout(() => {
|
|
@@ -198,10 +298,16 @@ export async function spawnSubagent(
|
|
|
198
298
|
|
|
199
299
|
result.exitCode = exitCode;
|
|
200
300
|
if (wasAborted) throw new Error("Subagent was aborted");
|
|
301
|
+
|
|
302
|
+
// Truncate large outputs: keep head (findings) + tail (summary), drop middle
|
|
303
|
+
if (result.output.length > MAX_OUTPUT_CHARS) {
|
|
304
|
+
const head = result.output.slice(0, 30_000);
|
|
305
|
+
const tail = result.output.slice(-(MAX_OUTPUT_CHARS - 30_050));
|
|
306
|
+
result.output = `[Output truncated — ${result.output.length} chars total]\n\n${head}\n\n... [truncated] ...\n\n${tail}`;
|
|
307
|
+
}
|
|
201
308
|
} finally {
|
|
202
|
-
// Cleanup temp
|
|
203
|
-
if (
|
|
204
|
-
if (tmpDir) try { fs.rmdirSync(tmpDir); } catch { /* ignore */ }
|
|
309
|
+
// Cleanup temp directory and all contents
|
|
310
|
+
if (tmpDir) try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
205
311
|
}
|
|
206
312
|
|
|
207
313
|
return result;
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
export interface SubagentConfig {
|
|
7
7
|
timeoutMs: number;
|
|
8
8
|
summary: SubagentSummaryConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Per-role overrides from settings.json. Keyed by role name.
|
|
11
|
+
* - Override built-in roles: provide fields to merge.
|
|
12
|
+
* - Disable built-in roles: set `disabled: true`.
|
|
13
|
+
*/
|
|
14
|
+
agentOverrides: Record<string, Partial<SubagentRole> & { disabled?: boolean }>;
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export interface SubagentSummaryConfig {
|
|
@@ -16,16 +22,27 @@ export interface SubagentSummaryConfig {
|
|
|
16
22
|
export const DEFAULT_CONFIG: SubagentConfig = {
|
|
17
23
|
timeoutMs: 300_000,
|
|
18
24
|
summary: { role: "utility", enabled: true },
|
|
25
|
+
agentOverrides: {},
|
|
19
26
|
};
|
|
20
27
|
|
|
21
28
|
/** A built-in subagent role definition. */
|
|
22
29
|
export interface SubagentRole {
|
|
23
30
|
/** pi-model-roles role name to use for this subagent */
|
|
24
31
|
role: string;
|
|
32
|
+
/** One-line description for the LLM prompt — what this role does and what tools it has */
|
|
33
|
+
description: string;
|
|
34
|
+
/** Example tasks to show in CONCRETE EXAMPLES section */
|
|
35
|
+
examples: string[];
|
|
36
|
+
/** Decision flow trigger phrase, e.g. "Task modifies files?" */
|
|
37
|
+
decisionTrigger: string;
|
|
25
38
|
/** System prompt for the subagent */
|
|
26
39
|
systemPrompt: string;
|
|
27
40
|
/** Tools available to this subagent */
|
|
28
41
|
tools: string[];
|
|
42
|
+
/** If this role has `delegate`, restrict which roles it may spawn. undefined = no restriction. */
|
|
43
|
+
subagentRoles?: string[];
|
|
44
|
+
/** Fallback pi-model-roles role name when this role's model is unavailable (provider error). Defaults to "default". */
|
|
45
|
+
fallbackRole?: string;
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
/** Usage statistics from a subagent execution. */
|