@tintinweb/pi-subagents 0.9.0 → 0.10.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/CHANGELOG.md +30 -0
- package/README.md +47 -15
- package/dist/agent-runner.d.ts +49 -0
- package/dist/agent-runner.js +225 -35
- package/dist/agent-types.d.ts +8 -1
- package/dist/agent-types.js +15 -4
- package/dist/custom-agents.js +21 -1
- package/dist/index.js +22 -17
- package/dist/prompts.d.ts +6 -3
- package/dist/prompts.js +12 -4
- package/dist/status-note.d.ts +13 -0
- package/dist/status-note.js +24 -0
- package/dist/types.d.ts +3 -0
- package/dist/ui/agent-widget.d.ts +4 -4
- package/dist/ui/agent-widget.js +6 -6
- package/dist/ui/conversation-viewer.d.ts +9 -1
- package/dist/ui/conversation-viewer.js +35 -2
- package/package.json +2 -1
- package/src/agent-runner.ts +238 -34
- package/src/agent-types.ts +15 -4
- package/src/custom-agents.ts +23 -1
- package/src/index.ts +22 -18
- package/src/prompts.ts +12 -4
- package/src/status-note.ts +25 -0
- package/src/types.ts +3 -0
- package/src/ui/agent-widget.ts +6 -6
- package/src/ui/conversation-viewer.ts +32 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.0] - 2026-06-01
|
|
11
|
+
|
|
12
|
+
> **⚠️ Breaking: `extensions:` and `tools:` in agent frontmatter semantics changed.** The `extensions: [...]` array now selects which extensions *load*, not which tool names surface. Agents that previously used the array form will behave differently — see migration below. The `tools:` field also grew new `ext:` and `*` selector forms; existing `tools:` values without these selectors are unchanged.
|
|
13
|
+
> - `extensions: [...]` is now an **extension allowlist applied at load time**, not a tool-name substring filter. Each entry is an extension *name*, a *path* (absolute, `~/`-prefixed, or relative-to-cwd), or `"*"`. **Migration:** `extensions: ["mcp"]` previously loaded *every* extension and then surfaced only tools whose names contained `mcp`. To keep all extensions, use `extensions: true` or `extensions: "*"`. To narrow, name the extensions or point at their files. `"*"` composes: `extensions: "*, /abs/path/extra-ext.ts"` is all defaults plus one path-loaded.
|
|
14
|
+
> - `tools:` now accepts `ext:` selectors and `*`. **Gotcha:** a `tools:` value containing **only** `ext:` entries yields **zero built-in tools** — add `*` (e.g. `tools: "*, ext:foo"`) to keep the built-ins. And **any** `ext:` entry flips extension tools to an explicit allowlist (non-listed extensions stay loaded but expose no tools). A `tools:` with no `ext:` entries is unchanged.
|
|
15
|
+
> - **`extensions:` is the sole loading authority.** `ext:foo` only narrows tool *exposure* within the already-loaded set; it cannot pull an extension in. `extensions: false` + `tools: "ext:foo"` loads nothing and warns that `ext:foo` is orphaned. To expose one extension's tool from an otherwise-narrow agent, name the extension explicitly: `extensions: [foo]` + `tools: "ext:foo/bar"`.
|
|
16
|
+
|
|
17
|
+
> **⚠️ Heads-up — widget glyphs changed (visual only):** turn count now renders as `↻N` (was `⟳N`) and compaction count as `⇊N` (was `↻N`). Fix for [#84](https://github.com/tintinweb/pi-subagents/issues/84) — `⟳` overflowed its cell in common monospace fonts. **No API, behavior, or output-format changes — only the glyphs.** If you grep agent stats lines or pipe widget output through scripts, update your patterns: `⟳` → `↻` (turns), `↻` → `⇊` (compactions).
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`tools:` accepts `ext:` extension-tool selectors and a `*` built-in wildcard.** Entries in the `tools:` CSV are now partitioned: plain names are the built-in allowlist (unchanged); `*` expands to all built-ins (symmetric with `extensions: "*"`); `ext:foo` / `ext:foo/bar` select extension tools. **Any `ext:` entry flips extension tools to an explicit allowlist** — only tools named by an `ext:` selector reach the LLM, and extensions not named stay loaded (their `session_start` etc. handlers still fire) but expose no tools. `ext:foo` exposes all of `foo`'s tools; `ext:foo/bar` narrows `foo` to just `bar` (multiple `ext:foo/x` entries union; a bare `ext:foo` alongside `ext:foo/bar` lets narrowing win). `ext:` is **narrowing-only** — it does not load extensions. `extensions:` remains the sole loading authority; an `ext:foo` against an extension that `extensions:` excluded (including `extensions: false`) is orphaned and warns via `onToolActivity` (`extension-error:ext:foo …`). With no `ext:` entry present, extension-tool behaviour is unchanged. `ext:` is name-only (matched by canonical name, so it composes with path-loaded extensions); paths still go in `extensions:`. `isolated: true` ignores `ext:` selectors.
|
|
21
|
+
- **Stop a running agent from the conversation viewer.** In `/agents → Running agents`, select an agent and press `x` (then `x` again to confirm) to abort it. The two-press guard prevents an accidental kill; the footer shows `x stop` → `x again to STOP`. This works for **background** agents — which a global `Esc` can't unambiguously target — while `Esc` still stops a blocking foreground `Agent` call. Wires the existing `AgentManager.abort(id)` to the viewer (`onStop` callback); the affordance only appears while the agent is `running`/`queued`. Addresses the common "how do I stop a background subagent?" question ([#88](https://github.com/tintinweb/pi-subagents/issues/88)).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **BREAKING: `extensions: [...]` in agent frontmatter is now a loader-level extension allowlist, not a tool-name filter.** Previously a `string[]` value filtered exposed *tool names* by substring (`t.startsWith(e) || t.includes(e)`) while every discovered extension still loaded and ran its handlers. Now each entry selects an *extension*: a bare name keeps the matching default-discovered extension, a path (absolute, `~/`-prefixed, or relative-to-cwd) loads that extension fresh via `additionalExtensionPaths`, and `"*"` keeps all default-discovered extensions. Entries compose — `["*", "/abs/foo.ts"]` is all defaults plus foo, `["mcp", "/abs/foo.ts"]` is just those two. Excluded extensions no longer bind handlers or register tools (their factory still runs once during `reload()`). Directory extensions (`foo/index.ts`) match by the parent directory name. **Extension names match case-insensitively** (`extensions: [Mcp]` resolves the same as `[mcp]`); tool names within `ext:foo/bar` selectors remain case-sensitive (they're matched against pi-mono's registered identifiers). Unmatched names and failed paths warn via `onToolActivity` but do not abort the subagent (see the heads-up above for migration).
|
|
25
|
+
- **Non-normal subagent outcomes are now stated explicitly in the text delivered to the parent**, so the orchestrator can't mistake a stopped/incomplete agent for a completed one. The foreground `Agent` result, `get_subagent_result`, and the `<task-notification>` summary all append a clear note for `stopped` (user abort) → `(STOPPED BY THE USER before completion — output is partial; the task was NOT finished)`, `aborted` (turn limit) → `(aborted — hit the turn limit before completion; output may be incomplete)`, and `steered` → `(wrapped up at the turn limit — output may be partial)`. `stopped` (human intervention) is kept distinct from `aborted` (turn-budget cutoff); a clean `completed` adds no note. Extracted as `getStatusNote` in `src/status-note.ts`.
|
|
26
|
+
- **`BUILTIN_TOOL_NAMES` is derived from pi's tool factories** (`createCodingTools` + `createReadOnlyTools`) rather than a hardcoded list, so the built-in set tracks pi-mono automatically. Internal; no behavior change (the resolved set is the same seven names).
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Turn-count glyph in the agent widget no longer overflows its monospace cell** ([#84](https://github.com/tintinweb/pi-subagents/issues/84) — thanks [@linozen](https://github.com/linozen)). `formatTurns` used `⟳` (U+27F3 CLOCKWISE GAPPED CIRCLE ARROW) from the Miscellaneous Mathematical Symbols-A block, where common monospace fonts (Iosevka Nerd Font Mono, Menlo, SF Mono, JetBrains Mono) draw the glyph visually wider than one cell despite its Neutral East Asian Width — making the next character (the digit) overlap the glyph. Replaced with `↻` (U+21BB CLOCKWISE OPEN CIRCLE ARROW) from the standard Arrows block, which renders cleanly at one cell in those fonts. To avoid colliding with the existing compaction indicator (which previously also used `↻`), the compaction glyph moves to `⇊` (U+21CA DOWNWARDS PAIRED ARROWS) — same Arrows block, also single-cell, visually distinct. Widget vocabulary now reads: `↻5≤30` for turns, `⇊2` for compactions. Pi UI consumers / scripts grepping for the glyph in stats lines must update.
|
|
30
|
+
- **`tools: none` now actually yields zero built-in tools.** `getToolNamesForType` treated an explicit empty `builtinToolNames` (`[]`, produced by `tools: none`) as "unspecified" and fell back to all 7 built-ins. It now distinguishes an omitted field (`undefined` → all built-ins, for default agents) from an explicit empty list (`[]` → zero), consistent with `getConfig`. Same fix makes `tools:` values containing only `ext:` selectors yield zero built-ins as documented.
|
|
31
|
+
- **`tools:` typos no longer silently break tool-calling** ([#75](https://github.com/tintinweb/pi-subagents/issues/75)). Two parts: (a) `all` was previously parsed as a literal tool name, producing a one-element allowlist of the non-existent tool `"all"` — the model then returned an empty response or emitted raw XML tool calls, all with `status: completed` and no error. `parseToolsField` now treats `all` (case-insensitive) as an alias for the `*` wildcard, both standalone and inside a CSV. (b) Plain entries in `tools:` are expected to be built-in names (extension tools route through `ext:`), so an unknown name there is unambiguously a typo. `runAgent` now emits a `tools-error:tool "X" requested by agent "Y" is not a known built-in` event via `onToolActivity` for each unrecognized plain entry — same surfacing channel as the existing `extension-error:` warnings.
|
|
32
|
+
- **Subagents with `extensions: true` now actually expose extension-registered tools (MCP, etc.)** ([#47](https://github.com/tintinweb/pi-subagents/issues/47)). `runAgent` previously passed only the built-in tool names as the `tools:` allowlist to `createAgentSession`, so pi-mono's `allowedToolNames` gate rejected every extension-registered tool at registration — `extensions: true` agents silently got only the 7 built-ins. `runAgent` now enumerates extension tool names from the resource loader after `reload()` and builds the full master allowlist (built-ins + permitted extension tools), so pi-mono's gate admits them from the first instant of the session. `disallowedTools` and the internal `Agent`/`get_subagent_result`/`steer_subagent` exclusions are applied uniformly to built-in and extension tools at construction — no post-construction `setActiveToolsByName` narrowing.
|
|
33
|
+
- **Append-mode subagents no longer defeat the LLM's KV cache** ([#73](https://github.com/tintinweb/pi-subagents/pull/73) — reported by [@jeffutter](https://github.com/jeffutter)). The assembled child prompt placed the per-spawn-varying `<active_agent>` tag and `# Environment` block *before* the ~8k-token inherited parent prompt, and wrapped the parent prompt in `<inherited_system_prompt>` tags. Because KV caches key on a byte-identical prefix, every subagent spawn reprocessed all ~8k shared tokens from scratch (~40s on slower hardware). The parent prompt is now emitted **verbatim at the start** of the prompt (wrapper dropped), so it forms an identical, cacheable prefix with the parent session and across every spawn; the static `<sub_agent_context>` bridge follows, then the varying `<active_agent>` tag and env block. `replace` mode is unchanged (it inherits no parent prefix). The `<active_agent>` tag stays present and is parsed position-independently, so downstream permission resolution is unaffected. Mirrors the fix in [gotgenes/pi-packages#180](https://github.com/gotgenes/pi-packages/issues/180).
|
|
34
|
+
|
|
35
|
+
## [0.9.1] - 2026-05-30
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- **`Agent`, `get_subagent_result`, and `steer_subagent` now surface in pi's default system prompt** ([#87](https://github.com/tintinweb/pi-subagents/pull/87) — thanks [@that-yolanda](https://github.com/that-yolanda)). Adds `promptSnippet` to all three (a line in the prompt's `Available tools:` section) and `promptGuidelines` to `Agent` (bullets in `Guidelines:`). The tools were always callable via the tool-call API; this only adds system-prompt reinforcement for prompt-following models. No schema or tool-call changes.
|
|
39
|
+
|
|
10
40
|
## [0.9.0] - 2026-05-30
|
|
11
41
|
|
|
12
42
|
> **Heads-up — orchestrator behavior may shift.** This release substantially rewrites the `Agent` tool description and the three default-agent descriptions (`general-purpose`, `Explore`, `Plan`) to mirror Claude Code's upstream wording. No API, schema, or tool-call shape changes — purely a prompt-engineering shift, but a load-bearing one:
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
15
15
|
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
|
|
16
16
|
- **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and smart group join (consolidated notifications)
|
|
17
17
|
- **Live widget UI** — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons
|
|
18
|
-
- **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause)
|
|
18
|
+
- **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause). Stop a still-running agent from here by pressing `x` (then `x` again to confirm) — works for background agents too
|
|
19
19
|
- **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
|
|
20
20
|
- **Mid-run steering** — inject messages into running agents to redirect their work without restarting
|
|
21
21
|
- **Session resume** — pick up where an agent left off, preserving full conversation context
|
|
@@ -98,29 +98,29 @@ The extension renders a persistent widget above the editor showing all active ag
|
|
|
98
98
|
|
|
99
99
|
```
|
|
100
100
|
● Agents
|
|
101
|
-
├─ ⠹ Agent Refactor auth module ·
|
|
101
|
+
├─ ⠹ Agent Refactor auth module · ↻5≤30 · 5 tool uses · 33.8k token (62%) · 12.3s
|
|
102
102
|
│ ⎿ editing 2 files…
|
|
103
|
-
├─ ⠹ Explore Find auth files ·
|
|
103
|
+
├─ ⠹ Explore Find auth files · ↻3 · 3 tool uses · 12.4k token (8%) · 4.1s
|
|
104
104
|
│ ⎿ searching…
|
|
105
|
-
├─ ⠹ Agent Long-running task ·
|
|
105
|
+
├─ ⠹ Agent Long-running task · ↻42 · 38 tool uses · 91.0k token (84% · ⇊2) · 2m17s
|
|
106
106
|
│ ⎿ reading…
|
|
107
107
|
└─ 2 queued
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
The token field is annotated with two optional signals inside parens:
|
|
111
111
|
- **`NN%`** — context-window utilization (color-coded: <70% dim, 70–85% warning, ≥85% error). Omitted when the model has no declared `contextWindow`, or briefly right after compaction.
|
|
112
|
-
-
|
|
112
|
+
- **`⇊N`** — number of times the session has compacted, when > 0. Stays dim; the percent's color carries urgency.
|
|
113
113
|
|
|
114
114
|
Individual agent results render Claude Code-style in the conversation:
|
|
115
115
|
|
|
116
116
|
| State | Example |
|
|
117
117
|
|-------|---------|
|
|
118
|
-
| **Running** | `⠹
|
|
119
|
-
| **Completed** | `✓
|
|
120
|
-
| **Wrapped up** | `✓
|
|
121
|
-
| **Stopped** | `■
|
|
122
|
-
| **Error** | `✗
|
|
123
|
-
| **Aborted** | `✗
|
|
118
|
+
| **Running** | `⠹ ↻3≤30 · 3 tool uses · 12.4k token (8%)` / `⎿ searching, reading 3 files…` |
|
|
119
|
+
| **Completed** | `✓ ↻8 · 5 tool uses · 33.8k token (62%) · 12.3s` / `⎿ Done` |
|
|
120
|
+
| **Wrapped up** | `✓ ↻50≤50 · 50 tool uses · 89.1k token (84% · ⇊2) · 45.2s` / `⎿ Wrapped up (turn limit)` |
|
|
121
|
+
| **Stopped** | `■ ↻3 · 3 tool uses · 12.4k token (8%)` / `⎿ Stopped` |
|
|
122
|
+
| **Error** | `✗ ↻3 · 3 tool uses · 12.4k token (8%)` / `⎿ Error: timeout` |
|
|
123
|
+
| **Aborted** | `✗ ↻55≤50 · 55 tool uses · 102.3k token (95% · ⇊3)` / `⎿ Aborted (max turns exceeded)` |
|
|
124
124
|
|
|
125
125
|
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
|
|
126
126
|
|
|
@@ -128,7 +128,7 @@ Background agent completion notifications render as styled boxes:
|
|
|
128
128
|
|
|
129
129
|
```
|
|
130
130
|
✓ Find auth files completed
|
|
131
|
-
|
|
131
|
+
↻3 · 3 tool uses · 12.4k token · 4.1s
|
|
132
132
|
⎿ Found 5 files related to authentication...
|
|
133
133
|
transcript: .pi/output/agent-abc123.jsonl
|
|
134
134
|
```
|
|
@@ -194,8 +194,8 @@ All fields are optional — sensible defaults for everything.
|
|
|
194
194
|
|-------|---------|-------------|
|
|
195
195
|
| `description` | filename | Agent description shown in tool listings |
|
|
196
196
|
| `display_name` | — | Display name for UI (e.g. widget, agent list) |
|
|
197
|
-
| `tools` | all 7 |
|
|
198
|
-
| `extensions` | `true` |
|
|
197
|
+
| `tools` | all 7 | Which tools the agent can call. Built-in names (`read, grep, …`), `*` / `all` (all built-ins), `none`, and `ext:<extension>` / `ext:<extension>/<tool>` selectors for extension tools. See [Tool & extension scoping](#tool--extension-scoping) below |
|
|
198
|
+
| `extensions` | `true` | Which extensions to load for the agent. `true` (all defaults), `false` (none), or an explicit list: `[mcp, "/abs/path.ts", "*"]`. See [Tool & extension scoping](#tool--extension-scoping) below |
|
|
199
199
|
| `skills` | `true` | Inherit skills from parent. Can be a comma-separated list of skill names to preload (see [Skill Preloading](#skill-preloading) for discovery locations) |
|
|
200
200
|
| `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
|
|
201
201
|
| `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
|
|
@@ -206,11 +206,42 @@ All fields are optional — sensible defaults for everything.
|
|
|
206
206
|
| `prompt_mode` | `replace` | `replace`: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). `append`: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
|
|
207
207
|
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
208
208
|
| `run_in_background` | `false` | Run in background by default |
|
|
209
|
-
| `isolated` | `false` |
|
|
209
|
+
| `isolated` | `false` | Hermetic specialist mode: forces `extensions: false` + `skills: false` + drops `ext:` selectors. Only built-in tools. Distinct from `isolation: worktree` (filesystem) |
|
|
210
210
|
| `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
|
|
211
211
|
|
|
212
212
|
Frontmatter is authoritative. If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `run_in_background`, `isolated`, or `isolation`, those values are locked for that agent. `Agent` tool parameters only fill fields the agent config leaves unspecified.
|
|
213
213
|
|
|
214
|
+
### Tool & extension scoping
|
|
215
|
+
|
|
216
|
+
`extensions:` decides **which extensions load**, `tools:` decides **which tools surface to the LLM**. They compose:
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
# Default (both omitted): all extensions load, all 7 built-ins surface
|
|
220
|
+
|
|
221
|
+
tools: read, grep, find # narrow to listed built-ins; extensions still load
|
|
222
|
+
tools: "*" # all 7 built-ins (alias: `all`)
|
|
223
|
+
tools: none # zero built-ins (alias: `""`)
|
|
224
|
+
tools: "*, ext:mcp/search" # built-ins plus one extension tool
|
|
225
|
+
|
|
226
|
+
extensions: false # no extensions load
|
|
227
|
+
extensions: [mcp] # only mcp loads
|
|
228
|
+
extensions: ["*", "/abs/foo.ts"] # all defaults plus one path-loaded extension
|
|
229
|
+
|
|
230
|
+
# Specialist: load one extension, expose only one of its tools, keep built-ins
|
|
231
|
+
extensions: [mcp]
|
|
232
|
+
tools: "*, ext:mcp/search"
|
|
233
|
+
|
|
234
|
+
isolated: true # hermetic: built-ins only, no extensions/skills/context
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A few rules the examples don't make obvious:
|
|
238
|
+
|
|
239
|
+
- `extensions:` is the sole loading authority. `ext:foo` in `tools:` narrows what surfaces; it can't load `foo` on its own. Mismatches fire `extension-error:…` warnings.
|
|
240
|
+
- Any `ext:` entry flips extension tools to an explicit allowlist — unnamed extensions still load (handlers fire) but expose no tools. So `tools: "*, ext:mcp/search"` exposes only `search` from `mcp`, nothing from any other extension.
|
|
241
|
+
- Extension names match case-insensitively (`[Mcp]` = `[mcp]`); tool names in `ext:foo/bar` stay case-sensitive.
|
|
242
|
+
- Plain `tools:` typos fail loudly: `tools: reed, grep` fires `tools-error:…` instead of silently producing an under-tooled agent.
|
|
243
|
+
- Array and string forms are equivalent: `[a, b]` == `"a, b"`.
|
|
244
|
+
|
|
214
245
|
## Tools
|
|
215
246
|
|
|
216
247
|
### `Agent`
|
|
@@ -265,6 +296,7 @@ Create new agent ← manual wizard or AI-generated
|
|
|
265
296
|
Settings ← max concurrency, max turns, grace turns, join mode
|
|
266
297
|
```
|
|
267
298
|
|
|
299
|
+
- **Running agents** — select one to open its live conversation viewer. While it's still running, press `x` (then `x` again to confirm) to stop/abort it — including **background** agents, which a global Esc can't unambiguously target (Esc still stops a blocking foreground `Agent` call). A stopped agent reports its partial output flagged as incomplete, not as a completion.
|
|
268
300
|
- **Agent types** — unified list with source indicators: `•` (project), `◦` (global), `✕` (disabled). Select an agent to manage it:
|
|
269
301
|
- **Default agents** (no override): Eject (export as `.md`), Disable
|
|
270
302
|
- **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -5,6 +5,55 @@ import type { Model } from "@earendil-works/pi-ai";
|
|
|
5
5
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { type AgentSession, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Tool names registered by THIS extension. Single source of truth so the
|
|
10
|
+
* registration sites (index.ts) and the subagent exclusion list below can't
|
|
11
|
+
* drift apart. These are our own tools, not pi built-ins, so they can't be
|
|
12
|
+
* derived from pi — but they only need defining once.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SUBAGENT_TOOL_NAMES: {
|
|
15
|
+
readonly AGENT: "Agent";
|
|
16
|
+
readonly GET_RESULT: "get_subagent_result";
|
|
17
|
+
readonly STEER: "steer_subagent";
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Canonical name of an extension for `extensions: [...]` allowlist matching.
|
|
21
|
+
* Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
|
|
22
|
+
* resolves the same as `[mcp]`. Tool names within `ext:foo/bar` are not affected.
|
|
23
|
+
* Directory extensions (`foo/index.ts`) resolve to the parent directory name;
|
|
24
|
+
* single-file extensions to the basename minus `.ts`/`.js`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extensionCanonicalName(extPath: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Classify `extensions: string[]` frontmatter entries for the loader-level filter.
|
|
29
|
+
*
|
|
30
|
+
* An entry is a PATH iff it contains a path separator or starts with `~`; otherwise
|
|
31
|
+
* it is a NAME. `"*"` sets the wildcard flag (keep all default-discovered extensions).
|
|
32
|
+
*
|
|
33
|
+
* Path entries are resolved (`~` expanded, made absolute against `cwd`) into `paths`
|
|
34
|
+
* — and their canonical name is also added to `names`. The loader override matches
|
|
35
|
+
* everything by canonical name, so path-loaded extensions are matched via their name
|
|
36
|
+
* rather than their post-staging `Extension.path`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseExtensionsSpec(entries: string[], cwd: string): {
|
|
39
|
+
names: Set<string>;
|
|
40
|
+
paths: string[];
|
|
41
|
+
wildcard: boolean;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Parse raw `ext:` selector strings (from the `tools:` CSV) into the set of
|
|
45
|
+
* extension names to keep loaded and a per-extension tool-narrowing map.
|
|
46
|
+
*
|
|
47
|
+
* `ext:foo` → `extNames` has `foo`, no narrowing entry (all of foo's tools).
|
|
48
|
+
* `ext:foo/bar` → `extNames` has `foo`, `narrowing.foo` has `bar` (only `bar`).
|
|
49
|
+
* A name lands in `narrowing` only when a `/tool` form is seen, so a bare
|
|
50
|
+
* `ext:foo` alongside `ext:foo/bar` leaves narrowing in effect (narrowing wins).
|
|
51
|
+
* The split is on the first `/`; extension canonical names never contain `/`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseExtSelectors(entries: string[]): {
|
|
54
|
+
extNames: Set<string>;
|
|
55
|
+
narrowing: Map<string, Set<string>>;
|
|
56
|
+
};
|
|
8
57
|
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
9
58
|
export declare function normalizeMaxTurns(n: number | undefined): number | undefined;
|
|
10
59
|
/** Get the default max turns value. undefined = unlimited. */
|
package/dist/agent-runner.js
CHANGED
|
@@ -1,16 +1,119 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
3
|
*/
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
4
6
|
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
|
|
7
|
+
import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
|
|
6
8
|
import { buildParentContext, extractText } from "./context.js";
|
|
7
9
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
8
10
|
import { detectEnv } from "./env.js";
|
|
9
11
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
10
12
|
import { buildAgentPrompt } from "./prompts.js";
|
|
11
13
|
import { preloadSkills } from "./skill-loader.js";
|
|
14
|
+
/**
|
|
15
|
+
* Tool names registered by THIS extension. Single source of truth so the
|
|
16
|
+
* registration sites (index.ts) and the subagent exclusion list below can't
|
|
17
|
+
* drift apart. These are our own tools, not pi built-ins, so they can't be
|
|
18
|
+
* derived from pi — but they only need defining once.
|
|
19
|
+
*/
|
|
20
|
+
export const SUBAGENT_TOOL_NAMES = {
|
|
21
|
+
AGENT: "Agent",
|
|
22
|
+
GET_RESULT: "get_subagent_result",
|
|
23
|
+
STEER: "steer_subagent",
|
|
24
|
+
};
|
|
12
25
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
13
|
-
const EXCLUDED_TOOL_NAMES =
|
|
26
|
+
const EXCLUDED_TOOL_NAMES = Object.values(SUBAGENT_TOOL_NAMES);
|
|
27
|
+
/**
|
|
28
|
+
* Canonical name of an extension for `extensions: [...]` allowlist matching.
|
|
29
|
+
* Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
|
|
30
|
+
* resolves the same as `[mcp]`. Tool names within `ext:foo/bar` are not affected.
|
|
31
|
+
* Directory extensions (`foo/index.ts`) resolve to the parent directory name;
|
|
32
|
+
* single-file extensions to the basename minus `.ts`/`.js`.
|
|
33
|
+
*/
|
|
34
|
+
export function extensionCanonicalName(extPath) {
|
|
35
|
+
const base = basename(extPath);
|
|
36
|
+
const name = base === "index.ts" || base === "index.js"
|
|
37
|
+
? basename(dirname(extPath))
|
|
38
|
+
: base.replace(/\.(ts|js)$/, "");
|
|
39
|
+
return name.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Classify `extensions: string[]` frontmatter entries for the loader-level filter.
|
|
43
|
+
*
|
|
44
|
+
* An entry is a PATH iff it contains a path separator or starts with `~`; otherwise
|
|
45
|
+
* it is a NAME. `"*"` sets the wildcard flag (keep all default-discovered extensions).
|
|
46
|
+
*
|
|
47
|
+
* Path entries are resolved (`~` expanded, made absolute against `cwd`) into `paths`
|
|
48
|
+
* — and their canonical name is also added to `names`. The loader override matches
|
|
49
|
+
* everything by canonical name, so path-loaded extensions are matched via their name
|
|
50
|
+
* rather than their post-staging `Extension.path`.
|
|
51
|
+
*/
|
|
52
|
+
export function parseExtensionsSpec(entries, cwd) {
|
|
53
|
+
const names = new Set();
|
|
54
|
+
const paths = [];
|
|
55
|
+
let wildcard = false;
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry)
|
|
58
|
+
continue;
|
|
59
|
+
if (entry === "*") {
|
|
60
|
+
wildcard = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const isPathEntry = entry.includes("/") || entry.includes("\\") || entry.startsWith("~");
|
|
64
|
+
if (!isPathEntry) {
|
|
65
|
+
names.add(entry.toLowerCase());
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
let p = entry;
|
|
69
|
+
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
|
|
70
|
+
p = homedir() + p.slice(1);
|
|
71
|
+
}
|
|
72
|
+
const abs = isAbsolute(p) ? p : resolve(cwd, p);
|
|
73
|
+
paths.push(abs);
|
|
74
|
+
names.add(extensionCanonicalName(abs));
|
|
75
|
+
}
|
|
76
|
+
return { names, paths, wildcard };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse raw `ext:` selector strings (from the `tools:` CSV) into the set of
|
|
80
|
+
* extension names to keep loaded and a per-extension tool-narrowing map.
|
|
81
|
+
*
|
|
82
|
+
* `ext:foo` → `extNames` has `foo`, no narrowing entry (all of foo's tools).
|
|
83
|
+
* `ext:foo/bar` → `extNames` has `foo`, `narrowing.foo` has `bar` (only `bar`).
|
|
84
|
+
* A name lands in `narrowing` only when a `/tool` form is seen, so a bare
|
|
85
|
+
* `ext:foo` alongside `ext:foo/bar` leaves narrowing in effect (narrowing wins).
|
|
86
|
+
* The split is on the first `/`; extension canonical names never contain `/`.
|
|
87
|
+
*/
|
|
88
|
+
export function parseExtSelectors(entries) {
|
|
89
|
+
const extNames = new Set();
|
|
90
|
+
const narrowing = new Map();
|
|
91
|
+
for (const raw of entries) {
|
|
92
|
+
if (!raw)
|
|
93
|
+
continue;
|
|
94
|
+
const body = raw.slice("ext:".length);
|
|
95
|
+
const slash = body.indexOf("/");
|
|
96
|
+
// Extension name matches case-insensitively (matches the loader-side canonical
|
|
97
|
+
// name). Tool names are case-preserved — they're matched against pi-mono's
|
|
98
|
+
// registered identifiers, which are case-sensitive.
|
|
99
|
+
const name = (slash === -1 ? body : body.slice(0, slash)).trim().toLowerCase();
|
|
100
|
+
if (!name)
|
|
101
|
+
continue;
|
|
102
|
+
extNames.add(name);
|
|
103
|
+
if (slash === -1)
|
|
104
|
+
continue;
|
|
105
|
+
const tool = body.slice(slash + 1).trim();
|
|
106
|
+
if (!tool)
|
|
107
|
+
continue;
|
|
108
|
+
let set = narrowing.get(name);
|
|
109
|
+
if (!set) {
|
|
110
|
+
set = new Set();
|
|
111
|
+
narrowing.set(name, set);
|
|
112
|
+
}
|
|
113
|
+
set.add(tool);
|
|
114
|
+
}
|
|
115
|
+
return { extNames, narrowing };
|
|
116
|
+
}
|
|
14
117
|
/** Default max turns. undefined = unlimited (no turn limit). */
|
|
15
118
|
let defaultMaxTurns;
|
|
16
119
|
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
@@ -151,16 +254,46 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
151
254
|
// Still pass noSkills: true since we don't need the skill loader to load them again.
|
|
152
255
|
const noSkills = skills === false || Array.isArray(skills);
|
|
153
256
|
const agentDir = getAgentDir();
|
|
154
|
-
//
|
|
257
|
+
// Extension loading:
|
|
258
|
+
// - true → all default-discovered extensions
|
|
259
|
+
// - false → none (noExtensions)
|
|
260
|
+
// - string[] → loader-level allowlist. Bare names keep the matching
|
|
261
|
+
// default-discovered extension; path entries load that extension fresh;
|
|
262
|
+
// "*" keeps all default-discovered extensions. Excluded extensions never
|
|
263
|
+
// bind handlers or register tools (their factory still runs once).
|
|
264
|
+
//
|
|
155
265
|
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md — upstream's
|
|
156
266
|
// buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
|
|
157
267
|
// would defeat prompt_mode: replace and isolated: true. Parent context, if
|
|
158
268
|
// wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
|
|
159
269
|
// is embedded in systemPromptOverride) or inherit_context (conversation).
|
|
270
|
+
// `ext:` selectors from the `tools:` CSV narrow which extension tools surface to
|
|
271
|
+
// the LLM. They do NOT control loading — `extensions:` is the sole authority for
|
|
272
|
+
// which extensions load. `ext:foo` against an extension that `extensions:` excluded
|
|
273
|
+
// is an orphan and warns after reload. `isolated` means no extension tools at all.
|
|
274
|
+
const { extNames, narrowing } = parseExtSelectors(options.isolated ? [] : (agentConfig?.extSelectors ?? []));
|
|
275
|
+
const noExtensions = extensions === false;
|
|
276
|
+
const extensionsSpec = Array.isArray(extensions)
|
|
277
|
+
? parseExtensionsSpec(extensions, effectiveCwd)
|
|
278
|
+
: undefined;
|
|
279
|
+
const keepNames = extensionsSpec?.names ?? new Set();
|
|
280
|
+
// The override filters loaded extensions down to `keepNames`. It's only needed
|
|
281
|
+
// when we're neither loading everything (`extensions: true` or a `"*"` wildcard)
|
|
282
|
+
// nor nothing (`noExtensions`).
|
|
283
|
+
const loadAll = extensions === true || extensionsSpec?.wildcard === true;
|
|
284
|
+
const additionalExtensionPaths = extensionsSpec?.paths.length ? extensionsSpec.paths : undefined;
|
|
285
|
+
const extensionsOverride = loadAll || noExtensions
|
|
286
|
+
? undefined
|
|
287
|
+
: (base) => ({
|
|
288
|
+
...base,
|
|
289
|
+
extensions: base.extensions.filter((e) => keepNames.has(extensionCanonicalName(e.path))),
|
|
290
|
+
});
|
|
160
291
|
const loader = new DefaultResourceLoader({
|
|
161
292
|
cwd: effectiveCwd,
|
|
162
293
|
agentDir,
|
|
163
|
-
noExtensions
|
|
294
|
+
noExtensions,
|
|
295
|
+
additionalExtensionPaths,
|
|
296
|
+
extensionsOverride,
|
|
164
297
|
noSkills,
|
|
165
298
|
noPromptTemplates: true,
|
|
166
299
|
noThemes: true,
|
|
@@ -169,10 +302,94 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
169
302
|
appendSystemPromptOverride: () => [],
|
|
170
303
|
});
|
|
171
304
|
await loader.reload();
|
|
305
|
+
// Plain entries in `tools:` are expected to be built-in names (extension tools
|
|
306
|
+
// go through `ext:`), so an unknown name there is unambiguously a typo. Previously
|
|
307
|
+
// this produced a silently broken agent (#75) — pi-mono accepted the bogus name
|
|
308
|
+
// into the allowlist, then dropped it at registration with no signal back.
|
|
309
|
+
if (agentConfig?.builtinToolNames?.length) {
|
|
310
|
+
const knownBuiltins = new Set(BUILTIN_TOOL_NAMES);
|
|
311
|
+
for (const name of agentConfig.builtinToolNames) {
|
|
312
|
+
if (!knownBuiltins.has(name)) {
|
|
313
|
+
options.onToolActivity?.({
|
|
314
|
+
type: "end",
|
|
315
|
+
toolName: `tools-error:tool "${name}" requested by agent "${type}" is not a known built-in`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// A subagent spawns mid-task, so a bad `extensions:`/`ext:` entry warns rather
|
|
321
|
+
// than aborts. Two distinct misconfigurations to catch:
|
|
322
|
+
// - `extensions: [foo]` but no extension named foo was discovered (typo or
|
|
323
|
+
// path that failed to load — path entries fold their canonical name into
|
|
324
|
+
// `keepNames`, so this covers them too).
|
|
325
|
+
// - `tools: ext:foo` but foo isn't in the loaded set (because `extensions:`
|
|
326
|
+
// didn't include it). Since v0.9, `ext:` no longer pulls extensions in;
|
|
327
|
+
// loading is `extensions:`-authoritative.
|
|
328
|
+
if (keepNames.size > 0 || extNames.size > 0) {
|
|
329
|
+
const survivingNames = new Set(loader.getExtensions().extensions.map((e) => extensionCanonicalName(e.path)));
|
|
330
|
+
for (const name of keepNames) {
|
|
331
|
+
if (!survivingNames.has(name)) {
|
|
332
|
+
options.onToolActivity?.({
|
|
333
|
+
type: "end",
|
|
334
|
+
toolName: `extension-error:extension "${name}" requested by agent "${type}" was not loaded`,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const name of extNames) {
|
|
339
|
+
if (!survivingNames.has(name)) {
|
|
340
|
+
options.onToolActivity?.({
|
|
341
|
+
type: "end",
|
|
342
|
+
toolName: `extension-error:ext:${name} referenced by agent "${type}" but extension "${name}" is not loaded (add it to extensions:)`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
172
347
|
// Resolve model: explicit option > config.model > parent model
|
|
173
348
|
const model = options.model ?? resolveDefaultModel(ctx.model, ctx.modelRegistry, agentConfig?.model);
|
|
174
349
|
// Resolve thinking level: explicit option > agent config > undefined (inherit)
|
|
175
350
|
const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
|
|
351
|
+
const disallowedSet = agentConfig?.disallowedTools
|
|
352
|
+
? new Set(agentConfig.disallowedTools)
|
|
353
|
+
: undefined;
|
|
354
|
+
// Enumerate extension-registered tool names from the loaded resource loader.
|
|
355
|
+
// Extensions populate `extension.tools` during `loader.reload()` and the set
|
|
356
|
+
// is stable afterwards — `bindExtensions` does not register new tools.
|
|
357
|
+
//
|
|
358
|
+
// Opt-in flip: when any `ext:` selector is present, extension tools become an
|
|
359
|
+
// explicit allowlist — a loaded extension not named by a selector contributes
|
|
360
|
+
// no tools (its handlers still ran), and `ext:foo/bar` narrows `foo` to `bar`.
|
|
361
|
+
const extensionToolNames = [];
|
|
362
|
+
if (!noExtensions) {
|
|
363
|
+
const optInActive = extNames.size > 0;
|
|
364
|
+
for (const extension of loader.getExtensions().extensions) {
|
|
365
|
+
const canon = extensionCanonicalName(extension.path);
|
|
366
|
+
if (optInActive && !extNames.has(canon))
|
|
367
|
+
continue;
|
|
368
|
+
const narrowed = narrowing.get(canon);
|
|
369
|
+
for (const toolName of extension.tools.keys()) {
|
|
370
|
+
if (narrowed && !narrowed.has(toolName))
|
|
371
|
+
continue;
|
|
372
|
+
extensionToolNames.push(toolName);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Build the master tool allowlist applied at session construction.
|
|
377
|
+
// pi-mono's `allowedToolNames` gates BOTH registration and the initial active
|
|
378
|
+
// set, so listing the exact final set here means the session is correctly
|
|
379
|
+
// scoped from the first instant — no post-construction narrowing required.
|
|
380
|
+
const builtinToolNameSet = new Set(toolNames);
|
|
381
|
+
const allowedTools = [...toolNames, ...extensionToolNames].filter((t) => {
|
|
382
|
+
if (EXCLUDED_TOOL_NAMES.includes(t))
|
|
383
|
+
return false;
|
|
384
|
+
if (disallowedSet?.has(t))
|
|
385
|
+
return false;
|
|
386
|
+
if (builtinToolNameSet.has(t))
|
|
387
|
+
return true;
|
|
388
|
+
// Reached only for extension tools. The extension set was already filtered
|
|
389
|
+
// at the loader (extensionsOverride / noExtensions) and at enumeration
|
|
390
|
+
// (`ext:` opt-in flip), so any extension tool in `extensionToolNames` is allowed.
|
|
391
|
+
return !noExtensions;
|
|
392
|
+
});
|
|
176
393
|
const sessionOpts = {
|
|
177
394
|
cwd: effectiveCwd,
|
|
178
395
|
agentDir,
|
|
@@ -180,7 +397,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
180
397
|
settingsManager: SettingsManager.create(effectiveCwd, agentDir),
|
|
181
398
|
modelRegistry: ctx.modelRegistry,
|
|
182
399
|
model,
|
|
183
|
-
tools:
|
|
400
|
+
tools: allowedTools,
|
|
184
401
|
resourceLoader: loader,
|
|
185
402
|
};
|
|
186
403
|
if (thinkingLevel) {
|
|
@@ -189,37 +406,10 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
189
406
|
const { session } = await createAgentSession(sessionOpts);
|
|
190
407
|
const baseSessionName = agentConfig?.name ?? type;
|
|
191
408
|
session.setSessionName(options.agentId ? `${baseSessionName}#${options.agentId.slice(0, 8)}` : baseSessionName);
|
|
192
|
-
// Build disallowed tools set from agent config
|
|
193
|
-
const disallowedSet = agentConfig?.disallowedTools
|
|
194
|
-
? new Set(agentConfig.disallowedTools)
|
|
195
|
-
: undefined;
|
|
196
|
-
// Filter active tools: remove our own tools to prevent nesting,
|
|
197
|
-
// apply extension allowlist if specified, and apply disallowedTools denylist
|
|
198
|
-
if (extensions !== false) {
|
|
199
|
-
const builtinToolNameSet = new Set(toolNames);
|
|
200
|
-
const activeTools = session.getActiveToolNames().filter((t) => {
|
|
201
|
-
if (EXCLUDED_TOOL_NAMES.includes(t))
|
|
202
|
-
return false;
|
|
203
|
-
if (disallowedSet?.has(t))
|
|
204
|
-
return false;
|
|
205
|
-
if (builtinToolNameSet.has(t))
|
|
206
|
-
return true;
|
|
207
|
-
if (Array.isArray(extensions)) {
|
|
208
|
-
return extensions.some(ext => t.startsWith(ext) || t.includes(ext));
|
|
209
|
-
}
|
|
210
|
-
return true;
|
|
211
|
-
});
|
|
212
|
-
session.setActiveToolsByName(activeTools);
|
|
213
|
-
}
|
|
214
|
-
else if (disallowedSet) {
|
|
215
|
-
// Even with extensions disabled, apply denylist to built-in tools
|
|
216
|
-
const activeTools = session.getActiveToolNames().filter(t => !disallowedSet.has(t));
|
|
217
|
-
session.setActiveToolsByName(activeTools);
|
|
218
|
-
}
|
|
219
409
|
// Bind extensions so that session_start fires and extensions can initialize
|
|
220
|
-
// (e.g. loading credentials, setting up state).
|
|
221
|
-
//
|
|
222
|
-
//
|
|
410
|
+
// (e.g. loading credentials, setting up state). Tool gating already happened
|
|
411
|
+
// at session construction via the `tools:` allowlist above — no separate
|
|
412
|
+
// post-bind filter is needed. All ExtensionBindings fields are optional.
|
|
223
413
|
await session.bindExtensions({
|
|
224
414
|
onError: (err) => {
|
|
225
415
|
options.onToolActivity?.({
|
package/dist/agent-types.d.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentConfig } from "./types.js";
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* All known built-in tool names, derived from pi's own tool factories rather
|
|
10
|
+
* than hardcoded so the set tracks pi-mono if it adds/renames a built-in.
|
|
11
|
+
* `createCodingTools` → read/bash/edit/write; `createReadOnlyTools` →
|
|
12
|
+
* read/grep/find/ls; their de-duplicated union is the 7 built-ins
|
|
13
|
+
* (read, bash, edit, write, grep, find, ls). The `cwd` only binds tool
|
|
14
|
+
* operations we never invoke here — we read each tool's `.name` and discard it.
|
|
15
|
+
*/
|
|
9
16
|
export declare const BUILTIN_TOOL_NAMES: string[];
|
|
10
17
|
/**
|
|
11
18
|
* Register agents into the unified registry.
|
package/dist/agent-types.js
CHANGED
|
@@ -4,9 +4,19 @@
|
|
|
4
4
|
* Merges embedded default agents with user-defined agents from .pi/agents/*.md.
|
|
5
5
|
* User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
|
|
6
6
|
*/
|
|
7
|
+
import { createCodingTools, createReadOnlyTools } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
8
|
-
/**
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* All known built-in tool names, derived from pi's own tool factories rather
|
|
11
|
+
* than hardcoded so the set tracks pi-mono if it adds/renames a built-in.
|
|
12
|
+
* `createCodingTools` → read/bash/edit/write; `createReadOnlyTools` →
|
|
13
|
+
* read/grep/find/ls; their de-duplicated union is the 7 built-ins
|
|
14
|
+
* (read, bash, edit, write, grep, find, ls). The `cwd` only binds tool
|
|
15
|
+
* operations we never invoke here — we read each tool's `.name` and discard it.
|
|
16
|
+
*/
|
|
17
|
+
export const BUILTIN_TOOL_NAMES = [
|
|
18
|
+
...new Set([...createCodingTools("."), ...createReadOnlyTools(".")].map((t) => t.name)),
|
|
19
|
+
];
|
|
10
20
|
/** Unified runtime registry of all agents (defaults + user-defined). */
|
|
11
21
|
const agents = new Map();
|
|
12
22
|
/**
|
|
@@ -95,8 +105,9 @@ export function getToolNamesForType(type) {
|
|
|
95
105
|
const key = resolveKey(type);
|
|
96
106
|
const raw = key ? agents.get(key) : undefined;
|
|
97
107
|
const config = raw?.enabled !== false ? raw : undefined;
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
// `undefined` (definition omitted the field) → all built-ins; an explicit `[]`
|
|
109
|
+
// (`tools: none` or a `tools:` with only `ext:` entries) → zero built-ins.
|
|
110
|
+
return config?.builtinToolNames ?? [...BUILTIN_TOOL_NAMES];
|
|
100
111
|
}
|
|
101
112
|
/** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
|
|
102
113
|
export function getConfig(type) {
|
package/dist/custom-agents.js
CHANGED
|
@@ -43,11 +43,13 @@ function loadFromDir(dir, agents, source) {
|
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
45
|
const { frontmatter: fm, body } = parseFrontmatter(content);
|
|
46
|
+
const { builtinToolNames, extSelectors } = parseToolsField(fm.tools);
|
|
46
47
|
agents.set(name, {
|
|
47
48
|
name,
|
|
48
49
|
displayName: str(fm.display_name),
|
|
49
50
|
description: str(fm.description) ?? name,
|
|
50
|
-
builtinToolNames
|
|
51
|
+
builtinToolNames,
|
|
52
|
+
extSelectors,
|
|
51
53
|
disallowedTools: csvListOptional(fm.disallowed_tools),
|
|
52
54
|
extensions: inheritField(fm.extensions ?? fm.inherit_extensions),
|
|
53
55
|
skills: inheritField(fm.skills ?? fm.inherit_skills),
|
|
@@ -97,6 +99,24 @@ function csvList(val, defaults) {
|
|
|
97
99
|
return defaults;
|
|
98
100
|
return parseCsvField(val) ?? [];
|
|
99
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Partition the `tools:` CSV into the built-in tool allowlist and raw `ext:` selectors.
|
|
104
|
+
* `*` (and the case-insensitive alias `all`, for `tools: all`) expands to all
|
|
105
|
+
* built-ins; plain entries are built-in names; `ext:` entries are extension-tool
|
|
106
|
+
* selectors parsed later by the runner. omitted → all built-ins, no selectors.
|
|
107
|
+
* `tools:` present with only `ext:` entries → zero built-ins (use `*`).
|
|
108
|
+
*/
|
|
109
|
+
function parseToolsField(val) {
|
|
110
|
+
const entries = csvList(val, BUILTIN_TOOL_NAMES);
|
|
111
|
+
const isWildcard = (e) => e === "*" || e.toLowerCase() === "all";
|
|
112
|
+
const hasWildcard = entries.some(isWildcard);
|
|
113
|
+
const plain = entries.filter(e => !isWildcard(e) && !e.startsWith("ext:"));
|
|
114
|
+
const extEntries = entries.filter(e => e.startsWith("ext:"));
|
|
115
|
+
return {
|
|
116
|
+
builtinToolNames: hasWildcard ? [...new Set([...BUILTIN_TOOL_NAMES, ...plain])] : plain,
|
|
117
|
+
extSelectors: extEntries.length > 0 ? extEntries : undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
100
120
|
/**
|
|
101
121
|
* Parse an optional comma-separated list field.
|
|
102
122
|
* omitted → undefined; "none"/empty → undefined; csv → listed items.
|