pi-fast-subagent 0.7.0 → 0.9.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 +41 -0
- package/agents/general.md +3 -0
- package/agents/scout.md +3 -0
- package/agents.ts +14 -1
- package/format.ts +119 -0
- package/index.ts +58 -885
- package/loader-pool.ts +175 -0
- package/package.json +11 -1
- package/render.ts +348 -0
- package/runner.ts +371 -0
- package/schemas.ts +59 -0
- package/types.ts +55 -0
package/README.md
CHANGED
|
@@ -14,6 +14,24 @@ Runs subagents with `createAgentSession()` in same process instead of spawning `
|
|
|
14
14
|
- User + project agent discovery
|
|
15
15
|
- Project agents override user agents
|
|
16
16
|
- Max nesting depth guard
|
|
17
|
+
- Chronological expanded view (Ctrl+O): subagent tool calls and response text interleaved in execution order
|
|
18
|
+
- Collapsed view shows response + trailing tool calls as an indented tree
|
|
19
|
+
|
|
20
|
+
## Settings
|
|
21
|
+
|
|
22
|
+
Configure preview sizes in `~/.pi/agent/settings.json` or `.pi/settings.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"fastSubagent": {
|
|
27
|
+
"previewLines": 12,
|
|
28
|
+
"promptPreviewLines": 12
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `previewLines` — response text preview lines in collapsed view (default 12)
|
|
34
|
+
- `promptPreviewLines` — task/prompt preview lines in collapsed view (default 12)
|
|
17
35
|
|
|
18
36
|
## Install
|
|
19
37
|
|
|
@@ -69,6 +87,7 @@ You are code exploration specialist. Read relevant files, trace data flow, summa
|
|
|
69
87
|
| `description` | yes | One-line description shown in `/fast-subagent:agent` |
|
|
70
88
|
| `model` | no | Model override, format `provider/model-id` (e.g. `anthropic/claude-haiku-4-5`) |
|
|
71
89
|
| `tools` | no | Tool allowlist (see below) |
|
|
90
|
+
| `maxDepth` | no | Nested subagent depth this agent may spawn. Default `0` means this agent cannot call `subagent`. |
|
|
72
91
|
|
|
73
92
|
### `tools:` field
|
|
74
93
|
|
|
@@ -125,6 +144,28 @@ tools: all
|
|
|
125
144
|
|
|
126
145
|
**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.
|
|
127
146
|
|
|
147
|
+
### `maxDepth:` field
|
|
148
|
+
|
|
149
|
+
Subagents cannot spawn other subagents by default, even when `tools` exposes the `subagent` tool.
|
|
150
|
+
|
|
151
|
+
```md
|
|
152
|
+
---
|
|
153
|
+
name: planner
|
|
154
|
+
description: Can delegate one level deeper
|
|
155
|
+
maxDepth: 1
|
|
156
|
+
---
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Depth counts nested generations from that agent:
|
|
160
|
+
|
|
161
|
+
| Value | Behavior |
|
|
162
|
+
|-------|----------|
|
|
163
|
+
| *(omitted)* / `0` | This agent cannot spawn subagents |
|
|
164
|
+
| `1` | This agent may spawn subagents, but those children cannot spawn again unless their own `maxDepth` allows it |
|
|
165
|
+
| `2` | Allows two nested generations, subject to each child agent's own `maxDepth` |
|
|
166
|
+
|
|
167
|
+
Aliases accepted: `max_depth`, `depth`, `subagentDepth`.
|
|
168
|
+
|
|
128
169
|
## Background Agents
|
|
129
170
|
|
|
130
171
|
Every foreground subagent can be moved to background at any time. Background jobs run concurrently while you continue chatting. When a job finishes, pi automatically posts the result as a follow-up message.
|
package/agents/general.md
CHANGED
|
@@ -11,6 +11,9 @@ model: anthropic/claude-haiku-4-5
|
|
|
11
11
|
# comma-separated list → explicit allowlist, e.g. `read, grep, web_search`
|
|
12
12
|
# General is meant to be a do-anything fallback, so it keeps everything explicit.
|
|
13
13
|
tools: all
|
|
14
|
+
|
|
15
|
+
# Subagents cannot spawn subagents by default. Set maxDepth: 1+ to opt in.
|
|
16
|
+
maxDepth: 0
|
|
14
17
|
---
|
|
15
18
|
|
|
16
19
|
You are general-purpose subagent.
|
package/agents/scout.md
CHANGED
|
@@ -11,6 +11,9 @@ model: anthropic/claude-haiku-4-5
|
|
|
11
11
|
# comma-separated list → explicit allowlist
|
|
12
12
|
# Scout is read-only: no `edit`, no `write`, no extension tools. Keeps the agent from mutating the codebase.
|
|
13
13
|
tools: read, bash, grep, find, ls
|
|
14
|
+
|
|
15
|
+
# Subagents cannot spawn subagents by default. Keep scout focused on exploration only.
|
|
16
|
+
maxDepth: 0
|
|
14
17
|
---
|
|
15
18
|
|
|
16
19
|
You are code exploration specialist.
|
package/agents.ts
CHANGED
|
@@ -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;
|
|
@@ -46,7 +48,7 @@ export function agentNeedsExtensions(tools: AgentTools): boolean {
|
|
|
46
48
|
|
|
47
49
|
// Default: all tools, matching pi-subagents behavior. Agents opt into lean mode
|
|
48
50
|
// with `tools: builtins` or explicit built-in allowlists.
|
|
49
|
-
function parseToolsField(raw: unknown): AgentTools {
|
|
51
|
+
export function parseToolsField(raw: unknown): AgentTools {
|
|
50
52
|
if (raw === undefined || raw === null) return "all";
|
|
51
53
|
const str = String(raw).trim();
|
|
52
54
|
if (!str) return "all";
|
|
@@ -58,6 +60,13 @@ function parseToolsField(raw: unknown): AgentTools {
|
|
|
58
60
|
return list.length ? list : "all";
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
export 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
|
+
|
|
61
70
|
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
62
71
|
if (!fs.existsSync(dir)) return [];
|
|
63
72
|
let entries: fs.Dirent[];
|
|
@@ -77,11 +86,15 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
77
86
|
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
78
87
|
if (!frontmatter?.name || !frontmatter?.description) continue;
|
|
79
88
|
const tools = parseToolsField(frontmatter.tools);
|
|
89
|
+
const maxDepth = parseMaxDepthField(
|
|
90
|
+
frontmatter.maxDepth ?? frontmatter.max_depth ?? frontmatter.depth ?? frontmatter.subagentDepth,
|
|
91
|
+
);
|
|
80
92
|
agents.push({
|
|
81
93
|
name: frontmatter.name,
|
|
82
94
|
description: frontmatter.description,
|
|
83
95
|
model: frontmatter.model,
|
|
84
96
|
tools,
|
|
97
|
+
maxDepth,
|
|
85
98
|
systemPrompt: body.trim(),
|
|
86
99
|
source,
|
|
87
100
|
filePath,
|
package/format.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure formatting helpers used by runner + render + command layers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentConfig } from "./agents.js";
|
|
6
|
+
import type { BackgroundSubagentJob } from "./background-types.js";
|
|
7
|
+
import type { RunResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export function formatTools(tools: AgentConfig["tools"]): string {
|
|
10
|
+
if (tools === "all") return "all";
|
|
11
|
+
if (tools === "builtins") return "builtins (default)";
|
|
12
|
+
if (tools === "none") return "none";
|
|
13
|
+
return tools.join(", ");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function shortPath(p: unknown): string {
|
|
17
|
+
if (typeof p !== "string") return "";
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
|
|
20
|
+
return p.replace(/^\/Users\/[^/]+\/[^/]+\//, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function summarizeToolArgs(toolName: unknown, toolInput: unknown): string {
|
|
24
|
+
const name = String(toolName ?? "");
|
|
25
|
+
const input =
|
|
26
|
+
toolInput && typeof toolInput === "object" ? (toolInput as Record<string, unknown>) : {};
|
|
27
|
+
const filePath = (): string => shortPath(input.path ?? input.file_path) || "";
|
|
28
|
+
switch (name) {
|
|
29
|
+
case "Read":
|
|
30
|
+
case "read":
|
|
31
|
+
case "Write":
|
|
32
|
+
case "write":
|
|
33
|
+
case "Edit":
|
|
34
|
+
case "edit":
|
|
35
|
+
return filePath();
|
|
36
|
+
case "Bash":
|
|
37
|
+
case "bash": {
|
|
38
|
+
const cmd = String(input.command ?? "");
|
|
39
|
+
return cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd;
|
|
40
|
+
}
|
|
41
|
+
case "Glob":
|
|
42
|
+
case "glob":
|
|
43
|
+
return String(input.pattern ?? "");
|
|
44
|
+
case "find": {
|
|
45
|
+
const pat = String(input.pattern ?? "");
|
|
46
|
+
const p = shortPath(input.path);
|
|
47
|
+
return p ? `${pat} in ${p}` : pat;
|
|
48
|
+
}
|
|
49
|
+
case "Grep":
|
|
50
|
+
case "grep": {
|
|
51
|
+
const pat = String(input.pattern ?? "");
|
|
52
|
+
const g = input.glob ? ` ${input.glob}` : "";
|
|
53
|
+
return `${pat}${g}`;
|
|
54
|
+
}
|
|
55
|
+
case "ls":
|
|
56
|
+
return shortPath(input.path) || "";
|
|
57
|
+
case "subagent": {
|
|
58
|
+
const agent = String(input.agent ?? "");
|
|
59
|
+
const t = String(input.task ?? "");
|
|
60
|
+
const summary = t.length > 50 ? t.slice(0, 47) + "..." : t;
|
|
61
|
+
return agent ? `${agent}: ${summary}` : summary;
|
|
62
|
+
}
|
|
63
|
+
default: {
|
|
64
|
+
for (const v of Object.values(input)) {
|
|
65
|
+
if (typeof v === "string" && v.length > 0)
|
|
66
|
+
return v.length > 60 ? v.slice(0, 57) + "..." : v;
|
|
67
|
+
}
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatDuration(ms: number): string {
|
|
74
|
+
const s = Math.max(0, Math.floor(ms / 1000));
|
|
75
|
+
const m = Math.floor(s / 60);
|
|
76
|
+
const rem = s % 60;
|
|
77
|
+
return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function summarizeTask(task: string, max = 60): string {
|
|
81
|
+
return task.length > max ? task.slice(0, max - 3) + "..." : task;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatTokens(n: number): string {
|
|
85
|
+
if (n < 1000) return String(n);
|
|
86
|
+
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
87
|
+
return `${Math.round(n / 1000)}k`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function formatUsage(usage: RunResult["usage"], model?: string): string {
|
|
91
|
+
const parts: string[] = [];
|
|
92
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
93
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
94
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
95
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
96
|
+
if (model) parts.push(model);
|
|
97
|
+
return parts.join(" ");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getFinalText(r: RunResult): string {
|
|
101
|
+
if (r.exitCode !== 0) return `Error: ${r.error ?? r.output ?? "(no output)"}`;
|
|
102
|
+
return r.output || "(no output)";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatBgJobSummary(job: BackgroundSubagentJob, now = Date.now()): string {
|
|
106
|
+
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
|
|
107
|
+
return `${job.id} [${job.status}] ${job.agentName} · ${dur} · ${summarizeTask(job.task)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): string {
|
|
111
|
+
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
|
|
112
|
+
const lines = [`${job.id} [${job.status}] ${job.agentName} · ${dur}`, `Task: ${job.task}`];
|
|
113
|
+
if (job.model) lines.push(`Model: ${job.model}`);
|
|
114
|
+
if (job.status === "completed") lines.push(`\nResult:\n${job.resultSummary ?? "(no output)"}`);
|
|
115
|
+
if (job.status === "failed") lines.push(`\nError: ${job.error ?? "(unknown)"}`);
|
|
116
|
+
if (job.status === "cancelled") lines.push("\nCancelled.");
|
|
117
|
+
if (job.status === "running") lines.push("\nStill running.");
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|