pi-fast-subagent 0.5.1 → 0.6.1

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 CHANGED
@@ -61,6 +61,56 @@ model: anthropic/claude-haiku-4-5
61
61
  You are code exploration specialist. Read relevant files, trace data flow, summarize findings clearly.
62
62
  ```
63
63
 
64
+ ### Agent frontmatter
65
+
66
+ | Field | Required | Description |
67
+ |-------|----------|-------------|
68
+ | `name` | yes | Unique agent identifier used in `subagent({ agent: "..." })` |
69
+ | `description` | yes | One-line description shown in `/fast-subagent:agent` |
70
+ | `model` | no | Model override, format `provider/model-id` (e.g. `anthropic/claude-haiku-4-5`) |
71
+ | `tools` | no | Tool allowlist (see below) |
72
+
73
+ ### `tools:` field
74
+
75
+ Controls which tools the subagent has access to. Subagents inherit parent extensions (web_search, fetch_content, mcp, …) by default.
76
+
77
+ | Value | Behavior |
78
+ |-------|----------|
79
+ | *(omitted)* | Inherit everything — all builtins + all parent extensions (**default**) |
80
+ | `all` | Same as omitted — explicit "everything" |
81
+ | `none` | No tools at all — pure reasoning agent |
82
+ | comma list | Allowlist; extensions auto-load only if any listed tool is non-builtin |
83
+
84
+ Built-in tools: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`.
85
+
86
+ Examples:
87
+
88
+ ```md
89
+ ---
90
+ name: writer
91
+ description: Pure-reasoning writing assistant
92
+ tools: none
93
+ ---
94
+ ```
95
+
96
+ ```md
97
+ ---
98
+ name: coder
99
+ description: Lean code-editing agent, no extensions
100
+ tools: read, bash, edit, write, grep, find, ls
101
+ ---
102
+ ```
103
+
104
+ ```md
105
+ ---
106
+ name: researcher
107
+ description: Web research agent
108
+ tools: read, write, web_search, fetch_content
109
+ ---
110
+ ```
111
+
112
+ > **Performance note:** inheriting all extensions adds startup cost (extension init) and token cost (larger system prompt). For tight, focused agents, list tools explicitly — extensions are only loaded when the allowlist actually needs them.
113
+
64
114
  ## Background Agents
65
115
 
66
116
  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/scout.md CHANGED
@@ -2,6 +2,7 @@
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
+ tools: read, bash, edit, write, grep, find, ls
5
6
  ---
6
7
 
7
8
  You are code exploration specialist.
package/agents.ts CHANGED
@@ -8,16 +8,54 @@ import * as path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
10
10
 
11
+ /**
12
+ * tools frontmatter semantics:
13
+ * unset → inherit everything (builtins + extensions) — same as `all`
14
+ * `all` → all builtins + all extension tools (web_search, fetch_content, mcp, …)
15
+ * `none` → no tools at all
16
+ * 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
+ *
20
+ * Represented as:
21
+ * "all" → everything (default when frontmatter omits `tools`)
22
+ * "none" → no tools
23
+ * string[] → allowlist
24
+ */
25
+ export type AgentTools = "all" | "none" | string[];
26
+
27
+ export const BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
28
+
11
29
  export interface AgentConfig {
12
30
  name: string;
13
31
  description: string;
14
32
  model?: string;
15
- tools?: string[];
33
+ tools: AgentTools;
16
34
  systemPrompt: string;
17
35
  source: "user" | "project";
18
36
  filePath: string;
19
37
  }
20
38
 
39
+ const BUILTIN_TOOLS = new Set<string>(BUILTIN_TOOL_NAMES);
40
+
41
+ export function agentNeedsExtensions(tools: AgentTools): boolean {
42
+ if (tools === "all") return true;
43
+ if (tools === "none") return false;
44
+ return tools.some((t) => !BUILTIN_TOOLS.has(t));
45
+ }
46
+
47
+ // Default: everything. Agents list specific tools for lean / restricted mode.
48
+ function parseToolsField(raw: unknown): AgentTools {
49
+ if (raw === undefined || raw === null) return "all";
50
+ const str = String(raw).trim();
51
+ if (!str) return "all";
52
+ const lower = str.toLowerCase();
53
+ if (lower === "all") return "all";
54
+ if (lower === "none") return "none";
55
+ const list = str.split(",").map((t) => t.trim()).filter(Boolean);
56
+ return list.length ? list : "all";
57
+ }
58
+
21
59
  function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
22
60
  if (!fs.existsSync(dir)) return [];
23
61
  let entries: fs.Dirent[];
@@ -36,13 +74,12 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
36
74
  const content = fs.readFileSync(filePath, "utf-8");
37
75
  const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
38
76
  if (!frontmatter?.name || !frontmatter?.description) continue;
39
- const rawTools = frontmatter.tools;
40
- const tools = rawTools?.split(",").map((t: string) => t.trim()).filter(Boolean);
77
+ const tools = parseToolsField(frontmatter.tools);
41
78
  agents.push({
42
79
  name: frontmatter.name,
43
80
  description: frontmatter.description,
44
81
  model: frontmatter.model,
45
- tools: tools?.length ? tools : undefined,
82
+ tools,
46
83
  systemPrompt: body.trim(),
47
84
  source,
48
85
  filePath,
package/index.ts CHANGED
@@ -32,7 +32,13 @@ import {
32
32
 
33
33
  type DefaultResourceLoaderOptions = ConstructorParameters<typeof DefaultResourceLoader>[0];
34
34
  import { Type } from "@sinclair/typebox";
35
- import { type AgentConfig, discoverAgents } from "./agents.js";
35
+ import { type AgentConfig, agentNeedsExtensions, discoverAgents } from "./agents.js";
36
+
37
+ function formatTools(tools: AgentConfig["tools"]): string {
38
+ if (tools === "all") return "all";
39
+ if (tools === "none") return "none";
40
+ return tools.join(", ");
41
+ }
36
42
 
37
43
  // ─── Tool arg summarizer (compact one-liner per tool call) ─────────────────────
38
44
 
@@ -228,11 +234,14 @@ async function runAgent(
228
234
  const { authStorage, modelRegistry } = getAuth();
229
235
  const agentDir = getAgentDir();
230
236
 
231
- // Build resource loader — no extensions/context files to keep subagent lean
237
+ // Build resource loader — no extensions/context files to keep subagent lean.
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).
232
241
  const loaderOptions: DefaultResourceLoaderOptions = {
233
242
  cwd,
234
243
  agentDir,
235
- noExtensions: true,
244
+ noExtensions: !agentNeedsExtensions(agent.tools),
236
245
  noContextFiles: true,
237
246
  noSkills: true,
238
247
  };
@@ -264,8 +273,13 @@ async function runAgent(
264
273
  }
265
274
  }
266
275
 
267
- // Restrict tools if agent specifies them
268
- if (agent.tools && agent.tools.length > 0) {
276
+ // Apply tools allowlist.
277
+ // "all" → no restriction (everything registered stays active)
278
+ // "none" → disable every tool
279
+ // string[] → explicit allowlist
280
+ if (agent.tools === "none") {
281
+ session.setActiveToolsByName([]);
282
+ } else if (Array.isArray(agent.tools) && agent.tools.length > 0) {
269
283
  session.setActiveToolsByName(agent.tools);
270
284
  }
271
285
 
@@ -598,7 +612,7 @@ export default function (pi: ExtensionAPI) {
598
612
  `File: ${agent.filePath}`,
599
613
  `Description: ${agent.description}`,
600
614
  agent.model ? `Model: ${agent.model}` : "",
601
- agent.tools ? `Tools: ${agent.tools.join(", ")}` : "",
615
+ `Tools: ${formatTools(agent.tools)}`,
602
616
  agent.systemPrompt ? `\nSystem prompt:\n${agent.systemPrompt}` : "",
603
617
  ].filter(Boolean).join("\n");
604
618
  ctx.ui.notify(lines, "info");
@@ -877,7 +891,13 @@ export default function (pi: ExtensionAPI) {
877
891
  try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
878
892
  }
879
893
 
880
- const cache: { width?: number; responseLines?: string[]; skipped?: number } = {};
894
+ const cache: {
895
+ width?: number;
896
+ promptLines?: string[];
897
+ promptSkipped?: number;
898
+ responseLines?: string[];
899
+ skipped?: number;
900
+ } = {};
881
901
 
882
902
  return {
883
903
  invalidate() { cache.width = undefined; },
@@ -888,15 +908,19 @@ export default function (pi: ExtensionAPI) {
888
908
  // ── Prompt ────────────────────────────────────────────────────
889
909
  if (details.task) {
890
910
  out.push("Prompt:");
891
- const taskLines = details.task.split("\n");
892
911
  if (expanded) {
893
- for (const line of taskLines) {
912
+ for (const line of details.task.split("\n")) {
894
913
  for (const w of wrapLine(indent + line, width)) out.push(w);
895
914
  }
896
915
  } else {
897
- // Single truncated line in collapsed
898
- const oneLiner = taskLines[0] ?? "";
899
- out.push(truncateToWidth(indent + oneLiner, width, "..."));
916
+ // Up to 8 visual lines in collapsed mode
917
+ const PROMPT_PREVIEW_LINES = 8;
918
+ if (cache.width !== width || cache.promptLines === undefined) {
919
+ const preview = truncateToVisualLines(details.task, PROMPT_PREVIEW_LINES, width - indent.length);
920
+ cache.promptLines = preview.visualLines.map((l) => truncateToWidth(indent + l, width, "..."));
921
+ cache.promptSkipped = preview.skippedCount;
922
+ }
923
+ out.push(...cache.promptLines);
900
924
  }
901
925
  }
902
926
 
@@ -932,8 +956,9 @@ export default function (pi: ExtensionAPI) {
932
956
 
933
957
  // ── Status ───────────────────────────────────────────────
934
958
  const status = statusLine();
935
- const expandHint = !expanded && (cache.skipped ?? 0) > 0
936
- ? keyHint("app.tools.expand", `expand · ${cache.skipped} lines hidden`)
959
+ const totalSkipped = (cache.skipped ?? 0) + (cache.promptSkipped ?? 0);
960
+ const expandHint = !expanded && totalSkipped > 0
961
+ ? keyHint("app.tools.expand", `expand · ${totalSkipped} lines hidden`)
937
962
  : !expanded && toolCalls.some((t) => t.result !== undefined)
938
963
  ? keyHint("app.tools.expand", "expand for tool outputs")
939
964
  : "";
@@ -984,7 +1009,7 @@ export default function (pi: ExtensionAPI) {
984
1009
  `## ${agent.name} [${agent.source}]`,
985
1010
  `**Description:** ${agent.description}`,
986
1011
  agent.model ? `**Model:** ${agent.model}` : null,
987
- agent.tools ? `**Tools:** ${agent.tools.join(", ")}` : null,
1012
+ `**Tools:** ${formatTools(agent.tools)}`,
988
1013
  agent.systemPrompt ? `\n**System prompt:**\n${agent.systemPrompt}` : null,
989
1014
  ].filter(Boolean).join("\n");
990
1015
  return { content: [{ type: "text", text: info }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-subagent",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "In-process subagent delegation for pi with single, parallel, and background modes",
5
5
  "type": "module",
6
6
  "keywords": [