@tintinweb/pi-subagents 0.5.2 → 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/CHANGELOG.md +24 -0
- package/README.md +31 -4
- package/dist/agent-runner.js +27 -20
- package/dist/agent-types.d.ts +7 -10
- package/dist/agent-types.js +12 -28
- package/dist/custom-agents.d.ts +2 -2
- package/dist/custom-agents.js +4 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +47 -19
- package/dist/settings.d.ts +56 -0
- package/dist/settings.js +125 -0
- package/package.json +4 -4
- package/src/agent-runner.ts +28 -19
- package/src/agent-types.ts +12 -40
- package/src/custom-agents.ts +4 -5
- package/src/index.ts +57 -20
- package/src/settings.ts +172 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.1] - 2026-04-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Persistent `/agents` → Settings** ([#24](https://github.com/tintinweb/pi-subagents/issues/24)) — the four runtime tuning values (`maxConcurrent`, `defaultMaxTurns`, `graceTurns`, `defaultJoinMode`) now survive pi restarts via a two-file dual-scope model mirroring pi's own `SettingsManager`. Global `~/.pi/agent/subagents.json` provides machine-wide defaults (edit by hand; the menu never writes here); project `<cwd>/.pi/subagents.json` holds per-project overrides (written by `/agents` → Settings). Load merges both with project winning on conflicts. Invalid fields are silently dropped per field; malformed JSON emits a warning to stderr and falls back to defaults so startup always proceeds; write failures downgrade the settings toast to a warning with `(session only; failed to persist)` so changes aren't silently reverted on next restart.
|
|
14
|
+
- **New lifecycle events** — `subagents:settings_loaded` (emitted once at extension init with the merged settings) and `subagents:settings_changed` (emitted on each `/agents` → Settings mutation with the new snapshot and a `persisted: boolean` flag so listeners can react to write failures).
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **`AGENTS.md` / `CLAUDE.md` / `APPEND_SYSTEM.md` no longer leak into sub-agent prompts** ([#26](https://github.com/tintinweb/pi-subagents/pull/26) — thanks [@mikeyobrien](https://github.com/mikeyobrien) for the diagnosis). Upstream `buildSystemPrompt()` re-appends `contextFiles` and `appendSystemPrompt` *after* our `systemPromptOverride` runs, which silently defeated `prompt_mode: replace` and `isolated: true` — parent project context (e.g. autoresearch-mode blocks) was bleeding into fresh `Explore` / custom sub-agents regardless of frontmatter. Fix uses upstream's `noContextFiles: true` flag (skips the load entirely, introduced in pi 0.68) plus `appendSystemPromptOverride: () => []` (no flag equivalent for append sources). **Behavior change:** subagents no longer implicitly inherit parent `AGENTS.md`/`CLAUDE.md`/`APPEND_SYSTEM.md`. To get parent project context into a subagent, use `prompt_mode: append` (parent's already-built system prompt flows in via `systemPromptOverride`), or `inherit_context: true` (parent conversation), or inline the content into the agent's own frontmatter.
|
|
18
|
+
- **Custom agent discovery respects `PI_CODING_AGENT_DIR`** ([#35](https://github.com/tintinweb/pi-subagents/pull/35), closes [#23](https://github.com/tintinweb/pi-subagents/issues/23) — thanks [@Amolith](https://github.com/Amolith) for the diagnosis). Two remaining hardcoded `~/.pi/agent/agents/` paths in `custom-agents.ts` and `index.ts` bypassed the env var, so users who relocated their agent directory (e.g. via `PI_CODING_AGENT_DIR`) still had global agents loaded from the default location and help text referencing the wrong path. Both now use upstream `getAgentDir()`, consistent with `agent-runner.ts` and `settings.ts`; tilde expansion is handled by upstream.
|
|
19
|
+
|
|
20
|
+
## [0.6.0] - 2026-04-24
|
|
21
|
+
|
|
22
|
+
> **⚠️ Breaking: drops support for `pi` < 0.68.** The upstream `pi-coding-agent` package shipped breaking API changes in v0.68 (and further ones in v0.70). This release migrates to `^0.70.2` and is **not** backward-compatible with hosts on `pi` 0.62–0.67. Users on those versions must upgrade their `pi` installation (`npm install -g @mariozechner/pi-coding-agent@latest`) before updating this extension.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Bumped peer `@mariozechner/pi-coding-agent` to `^0.70.2`** ([#28](https://github.com/tintinweb/pi-subagents/pull/28)) — crosses the v0.68 breaking-change line upstream. Specifically: tools are now passed as `string[]` (was `Tool[]`); `cwd`/`agentDir` are mandatory on `SettingsManager.create()` and `DefaultResourceLoader`; `session_switch` event renamed to `session_before_switch`; `ToolDefinition.params` widens to `unknown` under contextual typing, requiring `defineTool(...)`.
|
|
26
|
+
- **Tool registrations wrapped with `defineTool(...)`** — preserves `TParams` inference so `execute` handlers get properly-typed `params` instead of `unknown`. Applies to the `Agent`, `get_subagent_result`, and `steer_subagent` tools.
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
- **Cwd-bound tool factory registry** — the internal `TOOL_FACTORIES` closure table and `create{Bash,Edit,Read,Write,Grep,Find,Ls}Tool` imports are gone. Exported helpers renamed: `getToolsForType(type, cwd)` → `getToolNamesForType(type)`, `getMemoryTools(cwd, set)` → `getMemoryToolNames(set)`, `getReadOnlyMemoryTools(cwd, set)` → `getReadOnlyMemoryToolNames(set)` — all returning `string[]` instead of `Tool[]`. The host binds cwd when resolving tool names, so the extension no longer instantiates tools directly.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- **Subagent `SettingsManager` read wrong project settings in worktree mode** ([#30](https://github.com/tintinweb/pi-subagents/pull/30)) — `SettingsManager.create()` was called without arguments, defaulting `cwd` to `process.cwd()`. When the subagent's effective cwd differed (worktree isolation or explicit `cwd` override), its settings manager read `.pi/settings.json` from the parent's cwd rather than its own, diverging from the loader and session manager. Now passes `effectiveCwd` and `agentDir` explicitly, keeping all three managers consistent.
|
|
33
|
+
|
|
10
34
|
## [0.5.2] - 2026-03-26
|
|
11
35
|
|
|
12
36
|
### Fixed
|
package/README.md
CHANGED
|
@@ -116,9 +116,9 @@ Agents are discovered from two locations (higher priority wins):
|
|
|
116
116
|
| Priority | Location | Scope |
|
|
117
117
|
|----------|----------|-------|
|
|
118
118
|
| 1 (highest) | `.pi/agents/<name>.md` | Project — per-repo agents |
|
|
119
|
-
| 2 | `~/.pi/agent/agents/<name>.md` | Global — available everywhere |
|
|
119
|
+
| 2 | `$PI_CODING_AGENT_DIR/agents/<name>.md` (default `~/.pi/agent/agents/<name>.md`) | Global — available everywhere |
|
|
120
120
|
|
|
121
|
-
Project-level agents override global ones with the same name, so you can customize a global agent for a specific project.
|
|
121
|
+
Project-level agents override global ones with the same name, so you can customize a global agent for a specific project. The global location follows the upstream `PI_CODING_AGENT_DIR` env var — set it to relocate all pi-coding-agent state (agents, skills, settings) to a custom directory.
|
|
122
122
|
|
|
123
123
|
### Example: `.pi/agents/auditor.md`
|
|
124
124
|
|
|
@@ -163,7 +163,7 @@ All fields are optional — sensible defaults for everything.
|
|
|
163
163
|
| `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
|
|
164
164
|
| `thinking` | inherit | off, minimal, low, medium, high, xhigh |
|
|
165
165
|
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
|
|
166
|
-
| `prompt_mode` | `replace` | `replace`: body is the full system prompt. `append`: body appended to parent's prompt (agent acts as a "parent twin"
|
|
166
|
+
| `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) |
|
|
167
167
|
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
168
168
|
| `run_in_background` | `false` | Run in background by default |
|
|
169
169
|
| `isolation` | — | `worktree`: run in a temporary git worktree for full repo isolation |
|
|
@@ -272,6 +272,31 @@ When background agents complete, they notify the main agent. The **join mode** c
|
|
|
272
272
|
**Configuration:**
|
|
273
273
|
- Configure join mode in `/agents` → Settings → Join mode
|
|
274
274
|
|
|
275
|
+
## Persistent Settings
|
|
276
|
+
|
|
277
|
+
Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, default join mode) persist across pi restarts. Two files, merged on load:
|
|
278
|
+
|
|
279
|
+
- **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
|
|
280
|
+
- **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
|
|
281
|
+
|
|
282
|
+
**Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`, join mode `smart`).
|
|
283
|
+
|
|
284
|
+
**Example — global defaults for a beefy machine:**
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
mkdir -p ~/.pi/agent
|
|
288
|
+
cat > ~/.pi/agent/subagents.json <<'EOF'
|
|
289
|
+
{
|
|
290
|
+
"maxConcurrent": 16,
|
|
291
|
+
"graceTurns": 10
|
|
292
|
+
}
|
|
293
|
+
EOF
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Every project now starts with concurrency 16 and grace 10, without ever touching the menu. Individual projects can still override via `/agents` → Settings.
|
|
297
|
+
|
|
298
|
+
**Failure behavior:** missing file is silent; malformed JSON logs a `[pi-subagents] Ignoring malformed settings at …` warning to stderr; invalid/out-of-range field values are dropped per-field; write failures downgrade the `/agents` toast to a warning with `(session only; failed to persist)`.
|
|
299
|
+
|
|
275
300
|
## Events
|
|
276
301
|
|
|
277
302
|
Agent lifecycle events are emitted via `pi.events.emit()` so other extensions can react:
|
|
@@ -284,6 +309,8 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
|
|
|
284
309
|
| `subagents:failed` | Agent errored, stopped, or aborted | same as completed + `error`, `status` |
|
|
285
310
|
| `subagents:steered` | Steering message sent | `id`, `message` |
|
|
286
311
|
| `subagents:ready` | Extension loaded and RPC handlers registered | — |
|
|
312
|
+
| `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
|
|
313
|
+
| `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
|
|
287
314
|
|
|
288
315
|
## Cross-Extension RPC
|
|
289
316
|
|
|
@@ -417,7 +444,7 @@ src/
|
|
|
417
444
|
index.ts # Extension entry: tool/command registration, rendering
|
|
418
445
|
types.ts # Type definitions (AgentConfig, AgentRecord, etc.)
|
|
419
446
|
default-agents.ts # Embedded default agent configs (general-purpose, Explore, Plan)
|
|
420
|
-
agent-types.ts # Unified agent registry (defaults + user), tool
|
|
447
|
+
agent-types.ts # Unified agent registry (defaults + user), tool name resolution
|
|
421
448
|
agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
|
|
422
449
|
agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
|
|
423
450
|
cross-extension-rpc.ts # RPC handlers for cross-extension spawn/ping via pi.events
|
package/dist/agent-runner.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
3
|
*/
|
|
4
|
-
import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import { getAgentConfig, getConfig,
|
|
4
|
+
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
|
|
6
6
|
import { buildParentContext, extractText } from "./context.js";
|
|
7
7
|
import { detectEnv } from "./env.js";
|
|
8
8
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
@@ -110,28 +110,26 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
110
110
|
extras.skillBlocks = loaded;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
let
|
|
113
|
+
let toolNames = getToolNamesForType(type);
|
|
114
114
|
// Persistent memory: detect write capability and branch accordingly.
|
|
115
115
|
// Account for disallowedTools — a tool in the base set but on the denylist is not truly available.
|
|
116
116
|
if (agentConfig?.memory) {
|
|
117
|
-
const existingNames = new Set(
|
|
117
|
+
const existingNames = new Set(toolNames);
|
|
118
118
|
const denied = agentConfig.disallowedTools ? new Set(agentConfig.disallowedTools) : undefined;
|
|
119
119
|
const effectivelyHas = (name) => existingNames.has(name) && !denied?.has(name);
|
|
120
120
|
const hasWriteTools = effectivelyHas("write") || effectivelyHas("edit");
|
|
121
121
|
if (hasWriteTools) {
|
|
122
|
-
// Read-write memory: add any missing memory
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
122
|
+
// Read-write memory: add any missing memory tool names (read/write/edit)
|
|
123
|
+
const extraNames = getMemoryToolNames(existingNames);
|
|
124
|
+
if (extraNames.length > 0)
|
|
125
|
+
toolNames = [...toolNames, ...extraNames];
|
|
126
126
|
extras.memoryBlock = buildMemoryBlock(agentConfig.name, agentConfig.memory, effectiveCwd);
|
|
127
127
|
}
|
|
128
128
|
else {
|
|
129
|
-
// Read-only memory: only add read tool, use read-only prompt
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
tools = [...tools, ...readTools];
|
|
134
|
-
}
|
|
129
|
+
// Read-only memory: only add read tool name, use read-only prompt
|
|
130
|
+
const extraNames = getReadOnlyMemoryToolNames(existingNames);
|
|
131
|
+
if (extraNames.length > 0)
|
|
132
|
+
toolNames = [...toolNames, ...extraNames];
|
|
135
133
|
extras.memoryBlock = buildReadOnlyMemoryBlock(agentConfig.name, agentConfig.memory, effectiveCwd);
|
|
136
134
|
}
|
|
137
135
|
}
|
|
@@ -158,14 +156,23 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
158
156
|
// When skills is string[], we've already preloaded them into the prompt.
|
|
159
157
|
// Still pass noSkills: true since we don't need the skill loader to load them again.
|
|
160
158
|
const noSkills = skills === false || Array.isArray(skills);
|
|
161
|
-
|
|
159
|
+
const agentDir = getAgentDir();
|
|
160
|
+
// Load extensions/skills: true or string[] → load; false → don't.
|
|
161
|
+
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md — upstream's
|
|
162
|
+
// buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
|
|
163
|
+
// would defeat prompt_mode: replace and isolated: true. Parent context, if
|
|
164
|
+
// wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
|
|
165
|
+
// is embedded in systemPromptOverride) or inherit_context (conversation).
|
|
162
166
|
const loader = new DefaultResourceLoader({
|
|
163
167
|
cwd: effectiveCwd,
|
|
168
|
+
agentDir,
|
|
164
169
|
noExtensions: extensions === false,
|
|
165
170
|
noSkills,
|
|
166
171
|
noPromptTemplates: true,
|
|
167
172
|
noThemes: true,
|
|
173
|
+
noContextFiles: true,
|
|
168
174
|
systemPromptOverride: () => systemPrompt,
|
|
175
|
+
appendSystemPromptOverride: () => [],
|
|
169
176
|
});
|
|
170
177
|
await loader.reload();
|
|
171
178
|
// Resolve model: explicit option > config.model > parent model
|
|
@@ -174,17 +181,17 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
174
181
|
const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
|
|
175
182
|
const sessionOpts = {
|
|
176
183
|
cwd: effectiveCwd,
|
|
184
|
+
agentDir,
|
|
177
185
|
sessionManager: SessionManager.inMemory(effectiveCwd),
|
|
178
|
-
settingsManager: SettingsManager.create(),
|
|
186
|
+
settingsManager: SettingsManager.create(effectiveCwd, agentDir),
|
|
179
187
|
modelRegistry: ctx.modelRegistry,
|
|
180
188
|
model,
|
|
181
|
-
tools,
|
|
189
|
+
tools: toolNames,
|
|
182
190
|
resourceLoader: loader,
|
|
183
191
|
};
|
|
184
192
|
if (thinkingLevel) {
|
|
185
193
|
sessionOpts.thinkingLevel = thinkingLevel;
|
|
186
194
|
}
|
|
187
|
-
// createAgentSession's type signature may not include thinkingLevel yet
|
|
188
195
|
const { session } = await createAgentSession(sessionOpts);
|
|
189
196
|
// Build disallowed tools set from agent config
|
|
190
197
|
const disallowedSet = agentConfig?.disallowedTools
|
|
@@ -193,13 +200,13 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
193
200
|
// Filter active tools: remove our own tools to prevent nesting,
|
|
194
201
|
// apply extension allowlist if specified, and apply disallowedTools denylist
|
|
195
202
|
if (extensions !== false) {
|
|
196
|
-
const
|
|
203
|
+
const builtinToolNameSet = new Set(toolNames);
|
|
197
204
|
const activeTools = session.getActiveToolNames().filter((t) => {
|
|
198
205
|
if (EXCLUDED_TOOL_NAMES.includes(t))
|
|
199
206
|
return false;
|
|
200
207
|
if (disallowedSet?.has(t))
|
|
201
208
|
return false;
|
|
202
|
-
if (
|
|
209
|
+
if (builtinToolNameSet.has(t))
|
|
203
210
|
return true;
|
|
204
211
|
if (Array.isArray(extensions)) {
|
|
205
212
|
return extensions.some(ext => t.startsWith(ext) || t.includes(ext));
|
package/dist/agent-types.d.ts
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
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 type { AgentTool } from "@mariozechner/pi-agent-core";
|
|
8
7
|
import type { AgentConfig } from "./types.js";
|
|
9
|
-
/** All known built-in tool names
|
|
8
|
+
/** All known built-in tool names. */
|
|
10
9
|
export declare const BUILTIN_TOOL_NAMES: string[];
|
|
11
10
|
/**
|
|
12
11
|
* Register agents into the unified registry.
|
|
@@ -29,17 +28,15 @@ export declare function getUserAgentNames(): string[];
|
|
|
29
28
|
/** Check if a type is valid and enabled (case-insensitive). */
|
|
30
29
|
export declare function isValidType(type: string): boolean;
|
|
31
30
|
/**
|
|
32
|
-
* Get
|
|
33
|
-
* Only returns tools that are NOT already in the provided set.
|
|
31
|
+
* Get memory tool names (read/write/edit) not already in the provided set.
|
|
34
32
|
*/
|
|
35
|
-
export declare function
|
|
33
|
+
export declare function getMemoryToolNames(existingToolNames: Set<string>): string[];
|
|
36
34
|
/**
|
|
37
|
-
* Get only
|
|
38
|
-
* Only returns tools that are NOT already in the provided set.
|
|
35
|
+
* Get read-only memory tool names not already in the provided set.
|
|
39
36
|
*/
|
|
40
|
-
export declare function
|
|
41
|
-
/** Get built-in
|
|
42
|
-
export declare function
|
|
37
|
+
export declare function getReadOnlyMemoryToolNames(existingToolNames: Set<string>): string[];
|
|
38
|
+
/** Get built-in tool names for a type (case-insensitive). */
|
|
39
|
+
export declare function getToolNamesForType(type: string): string[];
|
|
43
40
|
/** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
|
|
44
41
|
export declare function getConfig(type: string): {
|
|
45
42
|
displayName: string;
|
package/dist/agent-types.js
CHANGED
|
@@ -4,19 +4,9 @@
|
|
|
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 { createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, } from "@mariozechner/pi-coding-agent";
|
|
8
7
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
bash: (cwd) => createBashTool(cwd),
|
|
12
|
-
edit: (cwd) => createEditTool(cwd),
|
|
13
|
-
write: (cwd) => createWriteTool(cwd),
|
|
14
|
-
grep: (cwd) => createGrepTool(cwd),
|
|
15
|
-
find: (cwd) => createFindTool(cwd),
|
|
16
|
-
ls: (cwd) => createLsTool(cwd),
|
|
17
|
-
};
|
|
18
|
-
/** All known built-in tool names, derived from the factory registry. */
|
|
19
|
-
export const BUILTIN_TOOL_NAMES = Object.keys(TOOL_FACTORIES);
|
|
8
|
+
/** All known built-in tool names. */
|
|
9
|
+
export const BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
20
10
|
/** Unified runtime registry of all agents (defaults + user-defined). */
|
|
21
11
|
const agents = new Map();
|
|
22
12
|
/**
|
|
@@ -87,32 +77,26 @@ export function isValidType(type) {
|
|
|
87
77
|
/** Tool names required for memory management. */
|
|
88
78
|
const MEMORY_TOOL_NAMES = ["read", "write", "edit"];
|
|
89
79
|
/**
|
|
90
|
-
* Get
|
|
91
|
-
* Only returns tools that are NOT already in the provided set.
|
|
80
|
+
* Get memory tool names (read/write/edit) not already in the provided set.
|
|
92
81
|
*/
|
|
93
|
-
export function
|
|
94
|
-
return MEMORY_TOOL_NAMES
|
|
95
|
-
.filter(n => !existingToolNames.has(n) && n in TOOL_FACTORIES)
|
|
96
|
-
.map(n => TOOL_FACTORIES[n](cwd));
|
|
82
|
+
export function getMemoryToolNames(existingToolNames) {
|
|
83
|
+
return MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
97
84
|
}
|
|
98
85
|
/** Tool names needed for read-only memory access. */
|
|
99
86
|
const READONLY_MEMORY_TOOL_NAMES = ["read"];
|
|
100
87
|
/**
|
|
101
|
-
* Get only
|
|
102
|
-
* Only returns tools that are NOT already in the provided set.
|
|
88
|
+
* Get read-only memory tool names not already in the provided set.
|
|
103
89
|
*/
|
|
104
|
-
export function
|
|
105
|
-
return READONLY_MEMORY_TOOL_NAMES
|
|
106
|
-
.filter(n => !existingToolNames.has(n) && n in TOOL_FACTORIES)
|
|
107
|
-
.map(n => TOOL_FACTORIES[n](cwd));
|
|
90
|
+
export function getReadOnlyMemoryToolNames(existingToolNames) {
|
|
91
|
+
return READONLY_MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
108
92
|
}
|
|
109
|
-
/** Get built-in
|
|
110
|
-
export function
|
|
93
|
+
/** Get built-in tool names for a type (case-insensitive). */
|
|
94
|
+
export function getToolNamesForType(type) {
|
|
111
95
|
const key = resolveKey(type);
|
|
112
96
|
const raw = key ? agents.get(key) : undefined;
|
|
113
97
|
const config = raw?.enabled !== false ? raw : undefined;
|
|
114
|
-
const
|
|
115
|
-
return
|
|
98
|
+
const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES];
|
|
99
|
+
return names;
|
|
116
100
|
}
|
|
117
101
|
/** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
|
|
118
102
|
export function getConfig(type) {
|
package/dist/custom-agents.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global (~/.pi/agent/agents/) locations.
|
|
2
|
+
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global ($PI_CODING_AGENT_DIR/agents/, default ~/.pi/agent/agents/) locations.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentConfig } from "./types.js";
|
|
5
5
|
/**
|
|
6
6
|
* Scan for custom agent .md files from multiple locations.
|
|
7
7
|
* Discovery hierarchy (higher priority wins):
|
|
8
8
|
* 1. Project: <cwd>/.pi/agents/*.md
|
|
9
|
-
* 2. Global: ~/.pi/agent/agents/*.md
|
|
9
|
+
* 2. Global: $PI_CODING_AGENT_DIR/agents/*.md (default: ~/.pi/agent/agents/*.md)
|
|
10
10
|
*
|
|
11
11
|
* Project-level agents override global ones with the same name.
|
|
12
12
|
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
package/dist/custom-agents.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global (~/.pi/agent/agents/) locations.
|
|
2
|
+
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global ($PI_CODING_AGENT_DIR/agents/, default ~/.pi/agent/agents/) locations.
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
5
|
import { basename, join } from "node:path";
|
|
7
|
-
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
8
7
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
9
8
|
/**
|
|
10
9
|
* Scan for custom agent .md files from multiple locations.
|
|
11
10
|
* Discovery hierarchy (higher priority wins):
|
|
12
11
|
* 1. Project: <cwd>/.pi/agents/*.md
|
|
13
|
-
* 2. Global: ~/.pi/agent/agents/*.md
|
|
12
|
+
* 2. Global: $PI_CODING_AGENT_DIR/agents/*.md (default: ~/.pi/agent/agents/*.md)
|
|
14
13
|
*
|
|
15
14
|
* Project-level agents override global ones with the same name.
|
|
16
15
|
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
|
17
16
|
*/
|
|
18
17
|
export function loadCustomAgents(cwd) {
|
|
19
|
-
const globalDir = join(
|
|
18
|
+
const globalDir = join(getAgentDir(), "agents");
|
|
20
19
|
const projectDir = join(cwd, ".pi", "agents");
|
|
21
20
|
const agents = new Map();
|
|
22
21
|
loadFromDir(globalDir, agents, "global"); // lower priority
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* /agents — Interactive agent management menu
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
13
|
-
import { homedir } from "node:os";
|
|
14
13
|
import { join } from "node:path";
|
|
14
|
+
import { defineTool, getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
15
15
|
import { Text } from "@mariozechner/pi-tui";
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
17
|
import { AgentManager } from "./agent-manager.js";
|
|
@@ -23,6 +23,7 @@ import { GroupJoinManager } from "./group-join.js";
|
|
|
23
23
|
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
24
24
|
import { resolveModel } from "./model-resolver.js";
|
|
25
25
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
26
|
+
import { applyAndEmitLoaded, saveAndEmitChanged } from "./settings.js";
|
|
26
27
|
import { AgentWidget, describeActivity, formatDuration, formatMs, formatTokens, formatTurns, getDisplayName, getPromptModeLabel, SPINNER, } from "./ui/agent-widget.js";
|
|
27
28
|
// ---- Shared helpers ----
|
|
28
29
|
/** Tool execute return value for a text response. */
|
|
@@ -383,7 +384,7 @@ export default function (pi) {
|
|
|
383
384
|
currentCtx = ctx;
|
|
384
385
|
manager.clearCompleted(); // preserve existing behavior
|
|
385
386
|
});
|
|
386
|
-
pi.on("
|
|
387
|
+
pi.on("session_before_switch", () => { manager.clearCompleted(); });
|
|
387
388
|
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
|
|
388
389
|
events: pi.events,
|
|
389
390
|
pi,
|
|
@@ -477,7 +478,7 @@ export default function (pi) {
|
|
|
477
478
|
...defaultDescs,
|
|
478
479
|
...(customDescs.length > 0 ? ["", "Custom agents:", ...customDescs] : []),
|
|
479
480
|
"",
|
|
480
|
-
|
|
481
|
+
`Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.`,
|
|
481
482
|
].join("\n");
|
|
482
483
|
};
|
|
483
484
|
/** Derive a short model label from a model string. */
|
|
@@ -488,8 +489,17 @@ export default function (pi) {
|
|
|
488
489
|
return name.replace(/-\d{8}$/, "");
|
|
489
490
|
}
|
|
490
491
|
const typeListText = buildTypeListText();
|
|
492
|
+
// Apply persisted settings on startup and emit `subagents:settings_loaded`.
|
|
493
|
+
// Global + project merged; missing → defaults; corrupt file emits a warning
|
|
494
|
+
// to stderr and falls back to defaults.
|
|
495
|
+
applyAndEmitLoaded({
|
|
496
|
+
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
497
|
+
setDefaultMaxTurns,
|
|
498
|
+
setGraceTurns,
|
|
499
|
+
setDefaultJoinMode,
|
|
500
|
+
}, (event, payload) => pi.events.emit(event, payload));
|
|
491
501
|
// ---- Agent tool ----
|
|
492
|
-
pi.registerTool({
|
|
502
|
+
pi.registerTool(defineTool({
|
|
493
503
|
name: "Agent",
|
|
494
504
|
label: "Agent",
|
|
495
505
|
description: `Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
@@ -521,7 +531,7 @@ Guidelines:
|
|
|
521
531
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
522
532
|
}),
|
|
523
533
|
subagent_type: Type.String({
|
|
524
|
-
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or
|
|
534
|
+
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`,
|
|
525
535
|
}),
|
|
526
536
|
model: Type.Optional(Type.String({
|
|
527
537
|
description: 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
|
|
@@ -847,9 +857,9 @@ Guidelines:
|
|
|
847
857
|
return textResult(`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
|
|
848
858
|
(record.result?.trim() || "No output."), details);
|
|
849
859
|
},
|
|
850
|
-
});
|
|
860
|
+
}));
|
|
851
861
|
// ---- get_subagent_result tool ----
|
|
852
|
-
pi.registerTool({
|
|
862
|
+
pi.registerTool(defineTool({
|
|
853
863
|
name: "get_subagent_result",
|
|
854
864
|
label: "Get Agent Result",
|
|
855
865
|
description: "Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background.",
|
|
@@ -908,9 +918,9 @@ Guidelines:
|
|
|
908
918
|
}
|
|
909
919
|
return textResult(output);
|
|
910
920
|
},
|
|
911
|
-
});
|
|
921
|
+
}));
|
|
912
922
|
// ---- steer_subagent tool ----
|
|
913
|
-
pi.registerTool({
|
|
923
|
+
pi.registerTool(defineTool({
|
|
914
924
|
name: "steer_subagent",
|
|
915
925
|
label: "Steer Agent",
|
|
916
926
|
description: "Send a steering message to a running agent. The message will interrupt the agent after its current tool execution " +
|
|
@@ -948,10 +958,10 @@ Guidelines:
|
|
|
948
958
|
return textResult(`Failed to steer agent: ${err instanceof Error ? err.message : String(err)}`);
|
|
949
959
|
}
|
|
950
960
|
},
|
|
951
|
-
});
|
|
961
|
+
}));
|
|
952
962
|
// ---- /agents interactive menu ----
|
|
953
963
|
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
954
|
-
const personalAgentsDir = () => join(
|
|
964
|
+
const personalAgentsDir = () => join(getAgentDir(), "agents");
|
|
955
965
|
/** Find the file path of a custom agent by name (project first, then global). */
|
|
956
966
|
function findAgentFile(name) {
|
|
957
967
|
const projectPath = join(projectAgentsDir(), `${name}.md`);
|
|
@@ -1179,7 +1189,7 @@ Guidelines:
|
|
|
1179
1189
|
async function ejectAgent(ctx, name, cfg) {
|
|
1180
1190
|
const location = await ctx.ui.select("Choose location", [
|
|
1181
1191
|
"Project (.pi/agents/)",
|
|
1182
|
-
|
|
1192
|
+
`Personal (${personalAgentsDir()})`,
|
|
1183
1193
|
]);
|
|
1184
1194
|
if (!location)
|
|
1185
1195
|
return;
|
|
@@ -1250,7 +1260,7 @@ Guidelines:
|
|
|
1250
1260
|
// No file (built-in default) — create a stub
|
|
1251
1261
|
const location = await ctx.ui.select("Choose location", [
|
|
1252
1262
|
"Project (.pi/agents/)",
|
|
1253
|
-
|
|
1263
|
+
`Personal (${personalAgentsDir()})`,
|
|
1254
1264
|
]);
|
|
1255
1265
|
if (!location)
|
|
1256
1266
|
return;
|
|
@@ -1285,7 +1295,7 @@ Guidelines:
|
|
|
1285
1295
|
async function showCreateWizard(ctx) {
|
|
1286
1296
|
const location = await ctx.ui.select("Choose location", [
|
|
1287
1297
|
"Project (.pi/agents/)",
|
|
1288
|
-
|
|
1298
|
+
`Personal (${personalAgentsDir()})`,
|
|
1289
1299
|
]);
|
|
1290
1300
|
if (!location)
|
|
1291
1301
|
return;
|
|
@@ -1462,6 +1472,16 @@ ${systemPrompt}
|
|
|
1462
1472
|
reloadCustomAgents();
|
|
1463
1473
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
1464
1474
|
}
|
|
1475
|
+
function snapshotSettings() {
|
|
1476
|
+
return {
|
|
1477
|
+
maxConcurrent: manager.getMaxConcurrent(),
|
|
1478
|
+
// 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
|
|
1479
|
+
// normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
|
|
1480
|
+
defaultMaxTurns: getDefaultMaxTurns() ?? 0,
|
|
1481
|
+
graceTurns: getGraceTurns(),
|
|
1482
|
+
defaultJoinMode: getDefaultJoinMode(),
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1465
1485
|
async function showSettings(ctx) {
|
|
1466
1486
|
const choice = await ctx.ui.select("Settings", [
|
|
1467
1487
|
`Max concurrency (current: ${manager.getMaxConcurrent()})`,
|
|
@@ -1477,7 +1497,7 @@ ${systemPrompt}
|
|
|
1477
1497
|
const n = parseInt(val, 10);
|
|
1478
1498
|
if (n >= 1) {
|
|
1479
1499
|
manager.setMaxConcurrent(n);
|
|
1480
|
-
ctx
|
|
1500
|
+
notifyApplied(ctx, `Max concurrency set to ${n}`);
|
|
1481
1501
|
}
|
|
1482
1502
|
else {
|
|
1483
1503
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
@@ -1490,11 +1510,11 @@ ${systemPrompt}
|
|
|
1490
1510
|
const n = parseInt(val, 10);
|
|
1491
1511
|
if (n === 0) {
|
|
1492
1512
|
setDefaultMaxTurns(undefined);
|
|
1493
|
-
ctx
|
|
1513
|
+
notifyApplied(ctx, "Default max turns set to unlimited");
|
|
1494
1514
|
}
|
|
1495
1515
|
else if (n >= 1) {
|
|
1496
1516
|
setDefaultMaxTurns(n);
|
|
1497
|
-
ctx
|
|
1517
|
+
notifyApplied(ctx, `Default max turns set to ${n}`);
|
|
1498
1518
|
}
|
|
1499
1519
|
else {
|
|
1500
1520
|
ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
@@ -1507,7 +1527,7 @@ ${systemPrompt}
|
|
|
1507
1527
|
const n = parseInt(val, 10);
|
|
1508
1528
|
if (n >= 1) {
|
|
1509
1529
|
setGraceTurns(n);
|
|
1510
|
-
ctx
|
|
1530
|
+
notifyApplied(ctx, `Grace turns set to ${n}`);
|
|
1511
1531
|
}
|
|
1512
1532
|
else {
|
|
1513
1533
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
@@ -1523,10 +1543,18 @@ ${systemPrompt}
|
|
|
1523
1543
|
if (val) {
|
|
1524
1544
|
const mode = val.split(" ")[0];
|
|
1525
1545
|
setDefaultJoinMode(mode);
|
|
1526
|
-
ctx
|
|
1546
|
+
notifyApplied(ctx, `Default join mode set to ${mode}`);
|
|
1527
1547
|
}
|
|
1528
1548
|
}
|
|
1529
1549
|
}
|
|
1550
|
+
// Persist the current snapshot, emit `subagents:settings_changed`, and surface
|
|
1551
|
+
// the right toast. Successful saves show info; persistence failures downgrade
|
|
1552
|
+
// to warning so users aren't silently reverted on restart. Event fires regardless
|
|
1553
|
+
// of outcome so listeners see the in-memory change.
|
|
1554
|
+
function notifyApplied(ctx, successMsg) {
|
|
1555
|
+
const { message, level } = saveAndEmitChanged(snapshotSettings(), successMsg, (event, payload) => pi.events.emit(event, payload));
|
|
1556
|
+
ctx.ui.notify(message, level);
|
|
1557
|
+
}
|
|
1530
1558
|
pi.registerCommand("agents", {
|
|
1531
1559
|
description: "Manage agents",
|
|
1532
1560
|
handler: async (_args, ctx) => { await showAgentsMenu(ctx); },
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { JoinMode } from "./types.js";
|
|
2
|
+
export interface SubagentsSettings {
|
|
3
|
+
maxConcurrent?: number;
|
|
4
|
+
/**
|
|
5
|
+
* 0 = unlimited — the extension's single source of truth for that convention:
|
|
6
|
+
* `normalizeMaxTurns()` in agent-runner.ts treats 0 → `undefined`, and the
|
|
7
|
+
* `/agents` → Settings input prompt explicitly says "0 = unlimited".
|
|
8
|
+
*/
|
|
9
|
+
defaultMaxTurns?: number;
|
|
10
|
+
graceTurns?: number;
|
|
11
|
+
defaultJoinMode?: JoinMode;
|
|
12
|
+
}
|
|
13
|
+
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
14
|
+
export interface SettingsAppliers {
|
|
15
|
+
setMaxConcurrent: (n: number) => void;
|
|
16
|
+
setDefaultMaxTurns: (n: number) => void;
|
|
17
|
+
setGraceTurns: (n: number) => void;
|
|
18
|
+
setDefaultJoinMode: (mode: JoinMode) => void;
|
|
19
|
+
}
|
|
20
|
+
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
21
|
+
export type SettingsEmit = (event: string, payload: unknown) => void;
|
|
22
|
+
/** Load merged settings: global provides defaults, project overrides. */
|
|
23
|
+
export declare function loadSettings(cwd?: string): SubagentsSettings;
|
|
24
|
+
/**
|
|
25
|
+
* Write project-local settings. Global is never touched from code.
|
|
26
|
+
* Returns `true` on success, `false` if the write (or mkdir) failed so the
|
|
27
|
+
* caller can surface a warning — persistence isn't fatal but isn't silent.
|
|
28
|
+
*/
|
|
29
|
+
export declare function saveSettings(s: SubagentsSettings, cwd?: string): boolean;
|
|
30
|
+
/** Apply persisted settings to the in-memory state via caller-supplied setters. */
|
|
31
|
+
export declare function applySettings(s: SubagentsSettings, appliers: SettingsAppliers): void;
|
|
32
|
+
/**
|
|
33
|
+
* Format the user-facing toast for a settings mutation. Pure function —
|
|
34
|
+
* routes the success/failure of `saveSettings` into the right message + level
|
|
35
|
+
* so the UI layer (index.ts) stays a thin wire between input and notification.
|
|
36
|
+
*/
|
|
37
|
+
export declare function persistToastFor(successMsg: string, persisted: boolean): {
|
|
38
|
+
message: string;
|
|
39
|
+
level: "info" | "warning";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Load merged settings, apply them to in-memory state, and emit the
|
|
43
|
+
* `subagents:settings_loaded` lifecycle event. Returns the loaded settings so
|
|
44
|
+
* callers can log/inspect. Extension init wires this once.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyAndEmitLoaded(appliers: SettingsAppliers, emit: SettingsEmit, cwd?: string): SubagentsSettings;
|
|
47
|
+
/**
|
|
48
|
+
* Persist a settings snapshot, emit the `subagents:settings_changed` event
|
|
49
|
+
* (regardless of persist outcome so listeners see the in-memory change), and
|
|
50
|
+
* return the toast the UI should display. Event payload carries the `persisted`
|
|
51
|
+
* flag so listeners can react to write failures.
|
|
52
|
+
*/
|
|
53
|
+
export declare function saveAndEmitChanged(snapshot: SubagentsSettings, successMsg: string, emit: SettingsEmit, cwd?: string): {
|
|
54
|
+
message: string;
|
|
55
|
+
level: "info" | "warning";
|
|
56
|
+
};
|