pi-subagents-lite 0.2.0 → 0.3.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 CHANGED
@@ -3,74 +3,260 @@
3
3
  [![npm version](https://img.shields.io/npm/v/pi-subagents-lite)](https://www.npmjs.com/package/pi-subagents-lite)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- Lightweight sub-agents for [pi](https://pi.dev). A focused fork of [pi-subagents](https://github.com/tintinweb/pi-subagents) with reduced surface area — spawn specialized agents with isolated sessions, tools, and models.
6
+ **Sub-agents for [pi](https://pi.dev) schema-first, zero-fluff.**
7
+
8
+ Spawn specialized agents with isolated sessions, custom tools, and per-type models — all at minimal token cost.
9
+
10
+ ## Schema-First Design
11
+
12
+ Every tool the LLM sees costs tokens — in the system prompt, and in every turn's context. Most extensions add description text, prompt snippets, and usage guidelines that compound across the session. This extension takes a **schema-first** approach: the tool name and parameter names **are** the schema. No bloated descriptions, no prose.
13
+
14
+ | Standard | Schema-first |
15
+ |---|---|
16
+ | `description: "Spawn a sub-agent"` | `description: "."` |
17
+ | `promptSnippet` with usage examples | _(none)_ |
18
+ | `promptGuidelines` with rules | _(none)_ |
19
+ | Parameters with `.description()` | Bare `Type.String()` |
20
+
21
+ Tool names like `Agent` and `StopAgent`, and parameter names like `prompt`, `description`, `run_in_background` are self-documenting. The LLM infers usage from the schema — no verbose descriptions needed. Tool results reinforce correct usage with clear success/error messages.
22
+
23
+ **Result:** foreground and background agents, custom agent types, per-model concurrency, cost tracking, steering, resume, model overrides — all with minimal token overhead.
7
24
 
8
25
  ## Features
9
26
 
10
- - **Agent tool** — spawn foreground or background sub-agents with `Agent({ prompt, description, agent, run_in_background, ... })`
11
- - **Auto-delivered results** — background agents notify you on completion, no polling needed
12
- - **steer_subagent** — inject messages into running agents mid-execution
13
- - **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter
14
- - **Turn limits** — soft limit with wrap-up warning, then hard abort
15
- - **Per-model concurrency** — configurable slot limits per model
16
- - **Stealth tools** — minimal prompt footprint (`.description`), no promptSnippet/guidelines
27
+ - **Two tools** — `Agent` (spawn) and `StopAgent` (kill)
28
+ - **Foreground & background** — block or fire-and-forget with auto-delivered results
29
+ - **Custom agent types** — define via `.md` files with YAML frontmatter (tools, model, thinking, turn limits)
30
+ - **Smart model resolution** — 6-level precedence: session config frontmatter → parent. Set once, forget
31
+ - **Concurrency control** — per-model and per-provider slot limits with automatic queuing
32
+ - **Cost tracking** — input/output/cache tokens and dollar cost per agent
33
+ - **Live widget** — persistent status bar above the editor showing running/completed agents
34
+ - **Result viewer** — fullscreen markdown viewer with stats
35
+ - **Steer & resume** — inject mid-execution guidance or continue a previous conversation
36
+ - **Output logs** — human-readable, `tail -f` friendly
17
37
 
18
38
  ## Install
19
39
 
20
40
  ```bash
21
- # Global
22
41
  pi install npm:pi-subagents-lite
42
+ pi install -l npm:pi-subagents-lite # project-local
43
+ pi -e npm:pi-subagents-lite # try without installing
44
+ ```
23
45
 
24
- # Project-local
25
- pi install -l npm:pi-subagents-lite
46
+ ## Quick Start
26
47
 
27
- # Try without installing
28
- pi -e npm:pi-subagents-lite
48
+ The LLM calls the `Agent` tool like any other tool. A foreground agent returns its result inline with stats; a background agent acknowledges immediately and auto-delivers the result when done.
29
49
 
30
- # From git
31
- pi install git:github.com/AlexParamonov/pi-subagents-lite
32
50
  ```
51
+ ⠹ Working...
33
52
 
34
- ## Quick Start
53
+ Agents
54
+ ├─ ⠙ Agent Write model precedence unit tests 6🛠 ·3⟳ ·8.1k(6%)·12s
55
+ │ │ tail -f /tmp/pi-agent-outputs/bb3382a9-1f7e-474.log
56
+ │ └ The file already exists but is ~175 lines. The user wants a …
57
+ ├─ ⠙ Agent Code review of agent-runner.ts 4🛠 ·2⟳ ·8.7k(4%)·12s
58
+ │ │ tail -f /tmp/pi-agent-outputs/23689696-3cd3-400.log
59
+ │ └ Now let me check the types and related files for context on …
60
+ └─ ⠙ Explore Explore codebase architecture 13🛠 ·4⟳ ·19.0k(15%)·12s
61
+ │ tail -f /tmp/pi-agent-outputs/4f6b0f08-7a9a-419.log
62
+ └ ## Architecture Summary: pi-subagents-lite
63
+ ```
64
+ Then you are notified like this for async (background) invocation:
65
+
66
+ ```
67
+ Subagent Result
68
+
69
+ ✓ Explore (model-name)·13🛠 ·5⟳ ·30.8k(15%)·21s
70
+ Explore codebase architecture
71
+ tail -f /tmp/pi-agent-outputs/4f6b0f08-7a9a-419.log
72
+ ```
73
+
74
+ or inline:
35
75
 
36
- ```ts
37
- // Spawn a foreground agent
38
- Agent({
39
- agent: "Explore",
40
- prompt: "Find all files that handle authentication",
41
- description: "Find auth files",
42
- })
43
-
44
- // Spawn a background agent (result auto-delivered)
45
- Agent({
46
- agent: "Explore",
47
- prompt: "Find all files that handle authentication",
48
- description: "Find auth files",
49
- run_in_background: true,
50
- })
51
76
  ```
77
+ ▸ Explore
78
+ ✓ 31🛠 ·6⟳ ·57.3k(28%)·39s
79
+ Explore project directory structure
80
+ ```
81
+
82
+ Stop a running agent at any time via /agents command
83
+
84
+ ```
85
+ ○ Agents
86
+ └─ ■ Agent Code review of agent-runner.ts 12🛠 ·10⟳ ·39.0k(8%)·52s stopped
87
+ tail -f /tmp/pi-agent-outputs/23689696-3cd3-400.log
88
+ ```
89
+
90
+ ### Agent Tool Parameters
91
+
92
+ | Parameter | Required | Description |
93
+ |---|---|---|
94
+ | `prompt` | ✅ | The task for the sub-agent |
95
+ | `description` | ✅ | Brief description for the LLM caller |
96
+ | `agent` | | Type name — `general-purpose`, `Explore`, or any custom type you define (see [Custom Agent Types](#custom-agent-types)). The available values are **auto-populated** from `.md` files in your agent directories — drop a file, it appears in the enum. Set `enabled: false` in frontmatter to remove a type from this list. |
97
+ | `run_in_background` | | Fire-and-forget; result delivered automatically when done |
98
+ | `resume` | | Agent ID to continue a previous conversation |
99
+
100
+ > `model`, `max_turns`, `isolated`, and `thinking` are **not visible to the LLM** through tool introspection — the extension injects them at call time from agent config and frontmatter. `model` is resolved via the [Model Resolution](#model-resolution) chain; `max_turns`/`isolated`/`thinking` come from the agent's config. See [Custom Agent Types](#custom-agent-types) to set them.
52
101
 
53
102
  ## Custom Agent Types
54
103
 
55
- Define agents in `.pi/agents/<name>.md` with YAML frontmatter:
104
+ Drop a `.md` file into `.pi/agents/` (project) or `~/.pi/agent/agents/` (global). The frontmatter configures the agent; the body is its system prompt.
105
+
106
+ The file's `name` frontmatter field (or the filename without extension) becomes the agent type name and **automatically populates the `agent` parameter's enum** in the tool schema. No registration step needed — the extension scans these directories at session start and makes every discovered agent available to the LLM.
107
+
108
+ Built-in types (`general-purpose`, `Explore`) are always present. User agents override built-ins with the same name; project agents override user agents (see [Merge precedence](#merge-precedence)).
56
109
 
57
110
  ```markdown
58
111
  ---
112
+ name: security-review
113
+ display_name: Security Review
59
114
  description: Review code for security issues
60
- tools: [read, bash, grep, find]
115
+ tools: [read, bash, grep, find, ls]
61
116
  extensions: false
62
117
  skills: false
63
- max_turns: 5
118
+ model: anthropic/claude-sonnet-4-5-20250514
119
+ thinking: high
120
+ max_turns: 10
64
121
  ---
65
122
 
66
123
  You are a security review specialist. Analyze code for vulnerabilities,
67
124
  focusing on injection flaws, auth bypasses, and insecure defaults.
68
125
  ```
69
126
 
127
+ **Frontmatter quick reference:**
128
+
129
+ | Field | Type | Description |
130
+ |---|---|---|
131
+ | `name` | string | Agent type name. Used as the enum value in the `agent` parameter (defaults to filename). |
132
+ | `display_name` | string | Human-readable label shown in the UI. |
133
+ | `description` | string | Short description — displayed in the `/agents` type list and tool rendering. |
134
+ | `tools` | string[] | Built-in tool allowlist: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`. If omitted, inherits all. |
135
+ | `disallowed_tools` | string[] | Tool denylist — removes these from the agent's toolset even if allowlisted by `tools` or extensions. |
136
+ | `extensions` | bool \| string[] | `false` = no extension tools; `true` = inherit all; `["ext-a"]` = allowlist. |
137
+ | `skills` | bool \| string[] | `false` = no skill prompts; `true` = inherit all; `["skill-a"]` = only these. |
138
+ | `model` | string | Default model as `"provider/model-id"`. Override via `/agents` or `subagents-lite.json`. |
139
+ | `thinking` | string | Default thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`. |
140
+ | `max_turns` | number | Turn limit (soft stop with steer). |
141
+ | `enabled` | bool | `false` removes the agent type from the tool schema's enum (LLM can't see or invoke it). Running agents unaffected. Default: `true`. |
142
+ | `isolated` | bool | Shorthand for `extensions: false` + `skills: false`. Reduces token footprint per turn. |
143
+
144
+ ### Token-Saving Frontmatter Settings
145
+
146
+ Every tool schema and every skill snippet you inject costs tokens — in every turn. These frontmatter fields are your main levers:
147
+
148
+ | Setting | What it controls | Token impact |
149
+ |---|---|---|
150
+ | `tools: [a, b, c]` | Which built-in tools the LLM sees | High — each tool has a schema (name, params, description) injected every turn. Fewer tools = fewer tokens. |
151
+ | `extensions: false` | Disables all extension-provided tools | Medium — extensions can register many tools (linters, browsers, etc.). Each adds schema tokens. |
152
+ | `extensions: ["my-ext"]` | Allowlist only specific extensions | Medium — pick only what the agent needs. |
153
+ | `skills: false` | Prevents skill content from being injected into the system prompt | **Highest** — skill prompts are prose, not schemas. A verbose skill can be 10-50x the token cost of a tool schema. |
154
+ | `skills: ["skill-a"]` | Inject only listed skills (preloaded inline) | Medium — you control exactly which skills appear. |
155
+ | `isolated: true` | Shorthand for `extensions: false` + `skills: false` | High — zero extension tools, zero skill prompts. Useful for fast, focused agents. |
156
+
157
+ **Practical example:** An `Explore` agent that only reads files doesn't need write tools, browser extensions, or git skills. Setting `tools: [read, bash, grep, find, ls]` + `extensions: false` + `skills: false` saves thousands of tokens per turn compared to inheriting everything.
158
+
159
+ ### Merge precedence
160
+
161
+ Project agents override user agents, which override built-ins (`general-purpose`, `Explore`). Agent types discovered from `.md` files automatically appear in the `agent` parameter's dropdown — no registration required.
162
+
163
+ ## Model Resolution
164
+
165
+ The extension picks the right model automatically. Precedence (highest first):
166
+
167
+ 1. **Session per-type override** — `/agents > Model settings`, lasts the session
168
+ 2. **Session global default** — temporary
169
+ 3. **Config per-type override** — `~/.pi/agent/subagents-lite.json`
170
+ 4. **Config global default**
171
+ 5. **Agent frontmatter** — `model` in `.md` file
172
+ 6. **Parent model** — inherit from the calling agent
173
+
174
+ The LLM never passes `model` — it's injected at call time. Set it once in config or frontmatter and forget about it.
175
+
70
176
  ## Commands
71
177
 
72
- - `/agents` — Management menu: model settings, concurrency, running agents, agent types, agent briefing
73
- - `/steer` — Steer a running agent
178
+ ### `/agents`
179
+
180
+ Management menu with four sections:
181
+
182
+ - **Model settings** — global default, per-type overrides, force background mode
183
+ - **Concurrency** — default limit, per-provider and per-model slots
184
+ - **Running agents** — list, steer, stop, view snapshot, view result
185
+ - **Debug** — agent types, agent briefing (sends capabilities to the LLM)
186
+
187
+ ## Interface
188
+
189
+ ### Live Widget
190
+
191
+ Persistent bar above the editor showing running and completed agents. Updates live during execution.
192
+
193
+ - Running agents show a spinner, current tool activity, turn count, token usage (with optional context-fill percent), and elapsed time
194
+ - Completed agents show a check mark with final stats
195
+ - Click `tail -f` path to follow output logs in real time
196
+
197
+ Format (tree structure with branch connectors):
198
+ ```
199
+ ├─ ⠙ Explore description 3🛠 ·5≤30⟳ ·12.0k(45%)·1h 2m 3s
200
+ │ └ thinking…
201
+ ```
202
+
203
+ Turn format uses `≤` and `⟳` glyphs (`5≤30⟳` = 5 of 30 turns). Token count uses compact notation (`12.0k`) with optional context-fill percent in parentheses. No "tokens" label — the glyphs are self-explanatory.
204
+
205
+ ### Result Viewer
206
+
207
+ Fullscreen markdown viewer for agent results. Opens automatically when viewing a completed agent's result from the `/agents` menu.
208
+
209
+ Key bindings: `↑↓` navigate · `PgUp/PgDn` · `g`/`G` top/bottom · `f` toggle fullscreen · `r` refresh · `q`/`Esc` close
210
+
211
+ Stats line: ` ↑12.0k · ↓8.0k · W3.0k · $0.024 · 15 turns · 47s`
212
+
213
+ ## Configuration
214
+
215
+ `~/.pi/agent/subagents-lite.json` — managed via `/agents` menu, or edit directly:
216
+
217
+ ```json
218
+ {
219
+ "agent": {
220
+ "default": null,
221
+ "forceBackground": false,
222
+ "Explore": "anthropic/claude-haiku-4-5-20251001"
223
+ },
224
+ "concurrency": {
225
+ "default": 4,
226
+ "providers": { "ollama": 2 },
227
+ "models": {
228
+ "anthropic/claude-sonnet-4-5-20250514": 3
229
+ }
230
+ }
231
+ }
232
+ ```
233
+
234
+ > **Note:** `agent.default` (global fallback), `agent.forceBackground` (flag), and per-type overrides like `"Explore"` are peers in the same object. Agent type names become dynamic keys alongside the special fields.
235
+
236
+ ## StopAgent Tool
237
+
238
+ Stop a running agent by ID. Returns a success message or an error if the agent isn't found.
239
+
240
+ | Parameter | Required | Description |
241
+ |---|---|---|
242
+ | `agent_id` | ✅ | The agent ID returned by the `Agent` tool when the agent was spawned |
243
+
244
+ Agent IDs can be discovered from:
245
+ - The `Agent` tool's result (shown on spawn)
246
+ - The `StopAgent` error message, which lists all running agent IDs
247
+ - The `/agents` menu's **Running agents** section
248
+
249
+ ## Output Logs
250
+
251
+ `/tmp/pi-agent-outputs/<agentId>.log` — append-only, human-readable, `tail -f` friendly. Every line is prefixed with an ISO 8601 timestamp:
252
+
253
+ ```
254
+ 2026-05-27T12:00:00.000Z [USER] Find all authentication files
255
+ 2026-05-27T12:00:02.000Z [TOOL] read("src/auth/index.ts")
256
+ 2026-05-27T12:00:02.000Z [TOOL_RESULT] read: 234 chars
257
+ 2026-05-27T12:00:15.000Z [ASSISTANT] I found the authentication module...
258
+ 2026-05-27T12:00:45.000Z [DONE] 5 turns, 12 tool uses, 12.3k tokens, $0.024
259
+ ```
74
260
 
75
261
  ## Requirements
76
262
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents-lite",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Lightweight sub-agents for pi — spawn specialized agents with isolated sessions, tools, and models.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -11,7 +11,9 @@
11
11
  ],
12
12
  "author": "AlexParamonov",
13
13
  "license": "MIT",
14
+ "packageManager": "bun@1",
14
15
  "engines": {
16
+ "bun": ">=1.0.0",
15
17
  "node": ">=18"
16
18
  },
17
19
  "repository": {
@@ -14,20 +14,7 @@
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
16
  import type { AgentConfig, ThinkingLevel } from "./types.js";
17
-
18
- /* ------------------------------------------------------------------ */
19
- /* Validation helpers */
20
- /* ------------------------------------------------------------------ */
21
-
22
- const VALID_THINKING_LEVELS: readonly ThinkingLevel[] = [
23
- "off", "minimal", "low", "medium", "high", "xhigh",
24
- ] as const;
25
-
26
- /** Validate and narrow a raw thinking value to ThinkingLevel. */
27
- function validateThinking(raw: string | undefined): ThinkingLevel | undefined {
28
- if (raw === undefined) return undefined;
29
- return VALID_THINKING_LEVELS.includes(raw as ThinkingLevel) ? (raw as ThinkingLevel) : undefined;
30
- }
17
+ import { parseThinkingLevel } from "./utils.js";
31
18
 
32
19
  /* ------------------------------------------------------------------ */
33
20
  /* Types */
@@ -46,6 +33,7 @@ export interface AgentConfigFromMd {
46
33
  max_turns?: number;
47
34
  disallowed_tools?: string[];
48
35
  enabled?: boolean;
36
+ isolated?: boolean;
49
37
  systemPrompt: string;
50
38
  source: "user" | "project";
51
39
  }
@@ -180,7 +168,7 @@ export function parseExtensions(
180
168
  /* ------------------------------------------------------------------ */
181
169
 
182
170
  /** Extract a non-empty string value from frontmatter. */
183
- export function parseString(
171
+ function parseString(
184
172
  frontmatter: Record<string, unknown>,
185
173
  key: string,
186
174
  ): string | undefined {
@@ -189,7 +177,7 @@ export function parseString(
189
177
  }
190
178
 
191
179
  /** Extract a string array from frontmatter (array or comma-separated string). */
192
- export function parseStringArray(
180
+ function parseStringArray(
193
181
  frontmatter: Record<string, unknown>,
194
182
  key: string,
195
183
  ): string[] | undefined {
@@ -203,6 +191,31 @@ export function parseStringArray(
203
191
  return undefined;
204
192
  }
205
193
 
194
+ /** Extract a boolean from frontmatter (true/false or "true"/"false"). */
195
+ function parseBoolean(
196
+ frontmatter: Record<string, unknown>,
197
+ key: string,
198
+ ): boolean | undefined {
199
+ const v = frontmatter[key];
200
+ if (v === true || v === "true") return true;
201
+ if (v === false || v === "false") return false;
202
+ return undefined;
203
+ }
204
+
205
+ /** Extract a number from frontmatter (number or numeric string). */
206
+ function parseNumber(
207
+ frontmatter: Record<string, unknown>,
208
+ key: string,
209
+ ): number | undefined {
210
+ const v = frontmatter[key];
211
+ if (typeof v === "number") return v;
212
+ if (typeof v === "string" && v.length > 0) {
213
+ const n = Number(v);
214
+ if (!Number.isNaN(n)) return n;
215
+ }
216
+ return undefined;
217
+ }
218
+
206
219
  /**
207
220
  * Build an object containing only the entries whose value is not undefined.
208
221
  * Used to transform AgentConfigFromMd fields into a Partial<AgentConfig>
@@ -233,42 +246,19 @@ export function parseAgentFile(
233
246
  ): AgentConfigFromMd {
234
247
  const { frontmatter, body } = parseFrontmatter(content);
235
248
 
236
- const extensions = parseExtensions(frontmatter.extensions);
237
- const skills = parseExtensions(frontmatter.skills);
238
-
239
- // enabled field
240
- const enabledRaw = frontmatter.enabled;
241
- let enabled: boolean | undefined;
242
- if (enabledRaw === "false" || enabledRaw === false) {
243
- enabled = false;
244
- } else if (enabledRaw === "true" || enabledRaw === true) {
245
- enabled = true;
246
- }
247
-
248
- // max_turns field
249
- const maxTurnsRaw = frontmatter.max_turns;
250
- let maxTurns: number | undefined;
251
- if (typeof maxTurnsRaw === "number") {
252
- maxTurns = maxTurnsRaw;
253
- } else if (typeof maxTurnsRaw === "string" && maxTurnsRaw.length > 0) {
254
- const parsed = Number(maxTurnsRaw);
255
- if (!Number.isNaN(parsed)) {
256
- maxTurns = parsed;
257
- }
258
- }
259
-
260
249
  return {
261
250
  name: parseString(frontmatter, "name"),
262
251
  display_name: parseString(frontmatter, "display_name"),
263
252
  description: parseString(frontmatter, "description"),
264
253
  tools: parseStringArray(frontmatter, "tools"),
265
- extensions,
266
- skills,
254
+ extensions: parseExtensions(frontmatter.extensions),
255
+ skills: parseExtensions(frontmatter.skills),
267
256
  model: parseString(frontmatter, "model"),
268
- thinking: validateThinking(parseString(frontmatter, "thinking")),
269
- max_turns: maxTurns,
257
+ thinking: parseThinkingLevel(parseString(frontmatter, "thinking")),
258
+ max_turns: parseNumber(frontmatter, "max_turns"),
270
259
  disallowed_tools: parseStringArray(frontmatter, "disallowed_tools"),
271
- enabled,
260
+ enabled: parseBoolean(frontmatter, "enabled"),
261
+ isolated: parseBoolean(frontmatter, "isolated"),
272
262
  systemPrompt: body,
273
263
  source: source,
274
264
  };
@@ -392,6 +382,7 @@ function fromMd(md: AgentConfigFromMd): Partial<AgentConfig> {
392
382
  maxTurns: md.max_turns,
393
383
  disallowedTools: md.disallowed_tools,
394
384
  enabled: md.enabled,
385
+ isolated: md.isolated,
395
386
  systemPrompt: md.systemPrompt,
396
387
  source: md.source === "project" ? "project" : "global",
397
388
  };