@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/dist/settings.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Persistence for pi-subagents operational settings.
|
|
2
|
+
// - Global: ~/.pi/agent/subagents.json (via getAgentDir()) — manual defaults, never written here
|
|
3
|
+
// - Project: <cwd>/.pi/subagents.json — written by /agents → Settings; overrides global on load
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
const VALID_JOIN_MODES = new Set(["async", "group", "smart"]);
|
|
8
|
+
// Sanity ceilings — prevent hand-edited configs from asking for values that
|
|
9
|
+
// make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
|
|
10
|
+
// that any realistic power-user setting passes through.
|
|
11
|
+
const MAX_CONCURRENT_CEILING = 1024;
|
|
12
|
+
const MAX_TURNS_CEILING = 10_000;
|
|
13
|
+
const GRACE_TURNS_CEILING = 1_000;
|
|
14
|
+
/** Drop fields that don't match the expected shape. Silent — garbage becomes absent. */
|
|
15
|
+
function sanitize(raw) {
|
|
16
|
+
if (!raw || typeof raw !== "object")
|
|
17
|
+
return {};
|
|
18
|
+
const r = raw;
|
|
19
|
+
const out = {};
|
|
20
|
+
if (Number.isInteger(r.maxConcurrent) &&
|
|
21
|
+
r.maxConcurrent >= 1 &&
|
|
22
|
+
r.maxConcurrent <= MAX_CONCURRENT_CEILING) {
|
|
23
|
+
out.maxConcurrent = r.maxConcurrent;
|
|
24
|
+
}
|
|
25
|
+
if (Number.isInteger(r.defaultMaxTurns) &&
|
|
26
|
+
r.defaultMaxTurns >= 0 &&
|
|
27
|
+
r.defaultMaxTurns <= MAX_TURNS_CEILING) {
|
|
28
|
+
out.defaultMaxTurns = r.defaultMaxTurns;
|
|
29
|
+
}
|
|
30
|
+
if (Number.isInteger(r.graceTurns) &&
|
|
31
|
+
r.graceTurns >= 1 &&
|
|
32
|
+
r.graceTurns <= GRACE_TURNS_CEILING) {
|
|
33
|
+
out.graceTurns = r.graceTurns;
|
|
34
|
+
}
|
|
35
|
+
if (typeof r.defaultJoinMode === "string" && VALID_JOIN_MODES.has(r.defaultJoinMode)) {
|
|
36
|
+
out.defaultJoinMode = r.defaultJoinMode;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function globalPath() {
|
|
41
|
+
return join(getAgentDir(), "subagents.json");
|
|
42
|
+
}
|
|
43
|
+
function projectPath(cwd) {
|
|
44
|
+
return join(cwd, ".pi", "subagents.json");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read a settings file. Missing file is silent (returns `{}`). A file that
|
|
48
|
+
* exists but can't be parsed emits a warning to stderr so users aren't
|
|
49
|
+
* silently reverted to defaults — and still returns `{}` so startup proceeds.
|
|
50
|
+
*/
|
|
51
|
+
function readSettingsFile(path) {
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
return {};
|
|
54
|
+
try {
|
|
55
|
+
return sanitize(JSON.parse(readFileSync(path, "utf-8")));
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
59
|
+
console.warn(`[pi-subagents] Ignoring malformed settings at ${path}: ${reason}`);
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Load merged settings: global provides defaults, project overrides. */
|
|
64
|
+
export function loadSettings(cwd = process.cwd()) {
|
|
65
|
+
return { ...readSettingsFile(globalPath()), ...readSettingsFile(projectPath(cwd)) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Write project-local settings. Global is never touched from code.
|
|
69
|
+
* Returns `true` on success, `false` if the write (or mkdir) failed so the
|
|
70
|
+
* caller can surface a warning — persistence isn't fatal but isn't silent.
|
|
71
|
+
*/
|
|
72
|
+
export function saveSettings(s, cwd = process.cwd()) {
|
|
73
|
+
const path = projectPath(cwd);
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
76
|
+
writeFileSync(path, JSON.stringify(s, null, 2), "utf-8");
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Apply persisted settings to the in-memory state via caller-supplied setters. */
|
|
84
|
+
export function applySettings(s, appliers) {
|
|
85
|
+
if (typeof s.maxConcurrent === "number")
|
|
86
|
+
appliers.setMaxConcurrent(s.maxConcurrent);
|
|
87
|
+
if (typeof s.defaultMaxTurns === "number")
|
|
88
|
+
appliers.setDefaultMaxTurns(s.defaultMaxTurns);
|
|
89
|
+
if (typeof s.graceTurns === "number")
|
|
90
|
+
appliers.setGraceTurns(s.graceTurns);
|
|
91
|
+
if (s.defaultJoinMode)
|
|
92
|
+
appliers.setDefaultJoinMode(s.defaultJoinMode);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Format the user-facing toast for a settings mutation. Pure function —
|
|
96
|
+
* routes the success/failure of `saveSettings` into the right message + level
|
|
97
|
+
* so the UI layer (index.ts) stays a thin wire between input and notification.
|
|
98
|
+
*/
|
|
99
|
+
export function persistToastFor(successMsg, persisted) {
|
|
100
|
+
return persisted
|
|
101
|
+
? { message: successMsg, level: "info" }
|
|
102
|
+
: { message: `${successMsg} (session only; failed to persist)`, level: "warning" };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Load merged settings, apply them to in-memory state, and emit the
|
|
106
|
+
* `subagents:settings_loaded` lifecycle event. Returns the loaded settings so
|
|
107
|
+
* callers can log/inspect. Extension init wires this once.
|
|
108
|
+
*/
|
|
109
|
+
export function applyAndEmitLoaded(appliers, emit, cwd = process.cwd()) {
|
|
110
|
+
const settings = loadSettings(cwd);
|
|
111
|
+
applySettings(settings, appliers);
|
|
112
|
+
emit("subagents:settings_loaded", { settings });
|
|
113
|
+
return settings;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Persist a settings snapshot, emit the `subagents:settings_changed` event
|
|
117
|
+
* (regardless of persist outcome so listeners see the in-memory change), and
|
|
118
|
+
* return the toast the UI should display. Event payload carries the `persisted`
|
|
119
|
+
* flag so listeners can react to write failures.
|
|
120
|
+
*/
|
|
121
|
+
export function saveAndEmitChanged(snapshot, successMsg, emit, cwd = process.cwd()) {
|
|
122
|
+
const persisted = saveSettings(snapshot, cwd);
|
|
123
|
+
emit("subagents:settings_changed", { settings: snapshot, persisted });
|
|
124
|
+
return persistToastFor(successMsg, persisted);
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tintinweb/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
|
|
5
5
|
"author": "tintinweb",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"autonomous"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@mariozechner/pi-ai": "^0.
|
|
25
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
26
|
-
"@mariozechner/pi-tui": "^0.
|
|
24
|
+
"@mariozechner/pi-ai": "^0.70.2",
|
|
25
|
+
"@mariozechner/pi-coding-agent": "^0.70.2",
|
|
26
|
+
"@mariozechner/pi-tui": "^0.70.2",
|
|
27
27
|
"@sinclair/typebox": "latest"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
package/src/agent-runner.ts
CHANGED
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
createAgentSession,
|
|
11
11
|
DefaultResourceLoader,
|
|
12
12
|
type ExtensionAPI,
|
|
13
|
+
getAgentDir,
|
|
13
14
|
SessionManager,
|
|
14
15
|
SettingsManager,
|
|
15
16
|
} from "@mariozechner/pi-coding-agent";
|
|
16
|
-
import { getAgentConfig, getConfig,
|
|
17
|
+
import { getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
|
|
17
18
|
import { buildParentContext, extractText } from "./context.js";
|
|
18
19
|
import { detectEnv } from "./env.js";
|
|
19
20
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
@@ -183,27 +184,25 @@ export async function runAgent(
|
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
let
|
|
187
|
+
let toolNames = getToolNamesForType(type);
|
|
187
188
|
|
|
188
189
|
// Persistent memory: detect write capability and branch accordingly.
|
|
189
190
|
// Account for disallowedTools — a tool in the base set but on the denylist is not truly available.
|
|
190
191
|
if (agentConfig?.memory) {
|
|
191
|
-
const existingNames = new Set(
|
|
192
|
+
const existingNames = new Set(toolNames);
|
|
192
193
|
const denied = agentConfig.disallowedTools ? new Set(agentConfig.disallowedTools) : undefined;
|
|
193
194
|
const effectivelyHas = (name: string) => existingNames.has(name) && !denied?.has(name);
|
|
194
195
|
const hasWriteTools = effectivelyHas("write") || effectivelyHas("edit");
|
|
195
196
|
|
|
196
197
|
if (hasWriteTools) {
|
|
197
|
-
// Read-write memory: add any missing memory
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
198
|
+
// Read-write memory: add any missing memory tool names (read/write/edit)
|
|
199
|
+
const extraNames = getMemoryToolNames(existingNames);
|
|
200
|
+
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
200
201
|
extras.memoryBlock = buildMemoryBlock(agentConfig.name, agentConfig.memory, effectiveCwd);
|
|
201
202
|
} else {
|
|
202
|
-
// Read-only memory: only add read tool, use read-only prompt
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (readTools.length > 0) tools = [...tools, ...readTools];
|
|
206
|
-
}
|
|
203
|
+
// Read-only memory: only add read tool name, use read-only prompt
|
|
204
|
+
const extraNames = getReadOnlyMemoryToolNames(existingNames);
|
|
205
|
+
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
207
206
|
extras.memoryBlock = buildReadOnlyMemoryBlock(agentConfig.name, agentConfig.memory, effectiveCwd);
|
|
208
207
|
}
|
|
209
208
|
}
|
|
@@ -232,14 +231,24 @@ export async function runAgent(
|
|
|
232
231
|
// Still pass noSkills: true since we don't need the skill loader to load them again.
|
|
233
232
|
const noSkills = skills === false || Array.isArray(skills);
|
|
234
233
|
|
|
235
|
-
|
|
234
|
+
const agentDir = getAgentDir();
|
|
235
|
+
|
|
236
|
+
// Load extensions/skills: true or string[] → load; false → don't.
|
|
237
|
+
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md — upstream's
|
|
238
|
+
// buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
|
|
239
|
+
// would defeat prompt_mode: replace and isolated: true. Parent context, if
|
|
240
|
+
// wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
|
|
241
|
+
// is embedded in systemPromptOverride) or inherit_context (conversation).
|
|
236
242
|
const loader = new DefaultResourceLoader({
|
|
237
243
|
cwd: effectiveCwd,
|
|
244
|
+
agentDir,
|
|
238
245
|
noExtensions: extensions === false,
|
|
239
246
|
noSkills,
|
|
240
247
|
noPromptTemplates: true,
|
|
241
248
|
noThemes: true,
|
|
249
|
+
noContextFiles: true,
|
|
242
250
|
systemPromptOverride: () => systemPrompt,
|
|
251
|
+
appendSystemPromptOverride: () => [],
|
|
243
252
|
});
|
|
244
253
|
await loader.reload();
|
|
245
254
|
|
|
@@ -251,21 +260,21 @@ export async function runAgent(
|
|
|
251
260
|
// Resolve thinking level: explicit option > agent config > undefined (inherit)
|
|
252
261
|
const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
|
|
253
262
|
|
|
254
|
-
const sessionOpts:
|
|
263
|
+
const sessionOpts: Parameters<typeof createAgentSession>[0] = {
|
|
255
264
|
cwd: effectiveCwd,
|
|
265
|
+
agentDir,
|
|
256
266
|
sessionManager: SessionManager.inMemory(effectiveCwd),
|
|
257
|
-
settingsManager: SettingsManager.create(),
|
|
267
|
+
settingsManager: SettingsManager.create(effectiveCwd, agentDir),
|
|
258
268
|
modelRegistry: ctx.modelRegistry,
|
|
259
269
|
model,
|
|
260
|
-
tools,
|
|
270
|
+
tools: toolNames,
|
|
261
271
|
resourceLoader: loader,
|
|
262
272
|
};
|
|
263
273
|
if (thinkingLevel) {
|
|
264
274
|
sessionOpts.thinkingLevel = thinkingLevel;
|
|
265
275
|
}
|
|
266
276
|
|
|
267
|
-
|
|
268
|
-
const { session } = await createAgentSession(sessionOpts as Parameters<typeof createAgentSession>[0]);
|
|
277
|
+
const { session } = await createAgentSession(sessionOpts);
|
|
269
278
|
|
|
270
279
|
// Build disallowed tools set from agent config
|
|
271
280
|
const disallowedSet = agentConfig?.disallowedTools
|
|
@@ -275,11 +284,11 @@ export async function runAgent(
|
|
|
275
284
|
// Filter active tools: remove our own tools to prevent nesting,
|
|
276
285
|
// apply extension allowlist if specified, and apply disallowedTools denylist
|
|
277
286
|
if (extensions !== false) {
|
|
278
|
-
const
|
|
287
|
+
const builtinToolNameSet = new Set(toolNames);
|
|
279
288
|
const activeTools = session.getActiveToolNames().filter((t) => {
|
|
280
289
|
if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
|
|
281
290
|
if (disallowedSet?.has(t)) return false;
|
|
282
|
-
if (
|
|
291
|
+
if (builtinToolNameSet.has(t)) return true;
|
|
283
292
|
if (Array.isArray(extensions)) {
|
|
284
293
|
return extensions.some(ext => t.startsWith(ext) || t.includes(ext));
|
|
285
294
|
}
|
package/src/agent-types.ts
CHANGED
|
@@ -5,33 +5,11 @@
|
|
|
5
5
|
* User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
|
9
|
-
import {
|
|
10
|
-
createBashTool,
|
|
11
|
-
createEditTool,
|
|
12
|
-
createFindTool,
|
|
13
|
-
createGrepTool,
|
|
14
|
-
createLsTool,
|
|
15
|
-
createReadTool,
|
|
16
|
-
createWriteTool,
|
|
17
|
-
} from "@mariozechner/pi-coding-agent";
|
|
18
8
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
19
9
|
import type { AgentConfig } from "./types.js";
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const TOOL_FACTORIES: Record<string, ToolFactory> = {
|
|
24
|
-
read: (cwd) => createReadTool(cwd),
|
|
25
|
-
bash: (cwd) => createBashTool(cwd),
|
|
26
|
-
edit: (cwd) => createEditTool(cwd),
|
|
27
|
-
write: (cwd) => createWriteTool(cwd),
|
|
28
|
-
grep: (cwd) => createGrepTool(cwd),
|
|
29
|
-
find: (cwd) => createFindTool(cwd),
|
|
30
|
-
ls: (cwd) => createLsTool(cwd),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/** All known built-in tool names, derived from the factory registry. */
|
|
34
|
-
export const BUILTIN_TOOL_NAMES = Object.keys(TOOL_FACTORIES);
|
|
11
|
+
/** All known built-in tool names. */
|
|
12
|
+
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
35
13
|
|
|
36
14
|
/** Unified runtime registry of all agents (defaults + user-defined). */
|
|
37
15
|
const agents = new Map<string, AgentConfig>();
|
|
@@ -113,35 +91,29 @@ export function isValidType(type: string): boolean {
|
|
|
113
91
|
const MEMORY_TOOL_NAMES = ["read", "write", "edit"];
|
|
114
92
|
|
|
115
93
|
/**
|
|
116
|
-
* Get
|
|
117
|
-
* Only returns tools that are NOT already in the provided set.
|
|
94
|
+
* Get memory tool names (read/write/edit) not already in the provided set.
|
|
118
95
|
*/
|
|
119
|
-
export function
|
|
120
|
-
return MEMORY_TOOL_NAMES
|
|
121
|
-
.filter(n => !existingToolNames.has(n) && n in TOOL_FACTORIES)
|
|
122
|
-
.map(n => TOOL_FACTORIES[n](cwd));
|
|
96
|
+
export function getMemoryToolNames(existingToolNames: Set<string>): string[] {
|
|
97
|
+
return MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
123
98
|
}
|
|
124
99
|
|
|
125
100
|
/** Tool names needed for read-only memory access. */
|
|
126
101
|
const READONLY_MEMORY_TOOL_NAMES = ["read"];
|
|
127
102
|
|
|
128
103
|
/**
|
|
129
|
-
* Get only
|
|
130
|
-
* Only returns tools that are NOT already in the provided set.
|
|
104
|
+
* Get read-only memory tool names not already in the provided set.
|
|
131
105
|
*/
|
|
132
|
-
export function
|
|
133
|
-
return READONLY_MEMORY_TOOL_NAMES
|
|
134
|
-
.filter(n => !existingToolNames.has(n) && n in TOOL_FACTORIES)
|
|
135
|
-
.map(n => TOOL_FACTORIES[n](cwd));
|
|
106
|
+
export function getReadOnlyMemoryToolNames(existingToolNames: Set<string>): string[] {
|
|
107
|
+
return READONLY_MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
136
108
|
}
|
|
137
109
|
|
|
138
|
-
/** Get built-in
|
|
139
|
-
export function
|
|
110
|
+
/** Get built-in tool names for a type (case-insensitive). */
|
|
111
|
+
export function getToolNamesForType(type: string): string[] {
|
|
140
112
|
const key = resolveKey(type);
|
|
141
113
|
const raw = key ? agents.get(key) : undefined;
|
|
142
114
|
const config = raw?.enabled !== false ? raw : undefined;
|
|
143
|
-
const
|
|
144
|
-
return
|
|
115
|
+
const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES];
|
|
116
|
+
return names;
|
|
145
117
|
}
|
|
146
118
|
|
|
147
119
|
/** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
|
package/src/custom-agents.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
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
|
|
|
5
5
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
6
|
import { basename, join } from "node:path";
|
|
8
|
-
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
9
8
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
10
9
|
import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js";
|
|
11
10
|
|
|
@@ -13,13 +12,13 @@ import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js";
|
|
|
13
12
|
* Scan for custom agent .md files from multiple locations.
|
|
14
13
|
* Discovery hierarchy (higher priority wins):
|
|
15
14
|
* 1. Project: <cwd>/.pi/agents/*.md
|
|
16
|
-
* 2. Global: ~/.pi/agent/agents/*.md
|
|
15
|
+
* 2. Global: $PI_CODING_AGENT_DIR/agents/*.md (default: ~/.pi/agent/agents/*.md)
|
|
17
16
|
*
|
|
18
17
|
* Project-level agents override global ones with the same name.
|
|
19
18
|
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
|
20
19
|
*/
|
|
21
20
|
export function loadCustomAgents(cwd: string): Map<string, AgentConfig> {
|
|
22
|
-
const globalDir = join(
|
|
21
|
+
const globalDir = join(getAgentDir(), "agents");
|
|
23
22
|
const projectDir = join(cwd, ".pi", "agents");
|
|
24
23
|
|
|
25
24
|
const agents = new Map<string, AgentConfig>();
|
package/src/index.ts
CHANGED
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
14
|
-
import { homedir } from "node:os";
|
|
15
14
|
import { join } from "node:path";
|
|
16
|
-
import type
|
|
15
|
+
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
17
16
|
import { Text } from "@mariozechner/pi-tui";
|
|
18
17
|
import { Type } from "@sinclair/typebox";
|
|
19
18
|
import { AgentManager } from "./agent-manager.js";
|
|
@@ -25,6 +24,7 @@ import { GroupJoinManager } from "./group-join.js";
|
|
|
25
24
|
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
26
25
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
27
26
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
27
|
+
import { applyAndEmitLoaded, type SubagentsSettings, saveAndEmitChanged } from "./settings.js";
|
|
28
28
|
import { type AgentConfig, type AgentRecord, type JoinMode, type NotificationDetails, type SubagentType } from "./types.js";
|
|
29
29
|
import {
|
|
30
30
|
type AgentActivity,
|
|
@@ -430,7 +430,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
430
430
|
manager.clearCompleted(); // preserve existing behavior
|
|
431
431
|
});
|
|
432
432
|
|
|
433
|
-
pi.on("
|
|
433
|
+
pi.on("session_before_switch", () => { manager.clearCompleted(); });
|
|
434
434
|
|
|
435
435
|
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
|
|
436
436
|
events: pi.events,
|
|
@@ -534,7 +534,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
534
534
|
...defaultDescs,
|
|
535
535
|
...(customDescs.length > 0 ? ["", "Custom agents:", ...customDescs] : []),
|
|
536
536
|
"",
|
|
537
|
-
|
|
537
|
+
`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.`,
|
|
538
538
|
].join("\n");
|
|
539
539
|
};
|
|
540
540
|
|
|
@@ -548,9 +548,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
548
548
|
|
|
549
549
|
const typeListText = buildTypeListText();
|
|
550
550
|
|
|
551
|
+
// Apply persisted settings on startup and emit `subagents:settings_loaded`.
|
|
552
|
+
// Global + project merged; missing → defaults; corrupt file emits a warning
|
|
553
|
+
// to stderr and falls back to defaults.
|
|
554
|
+
applyAndEmitLoaded(
|
|
555
|
+
{
|
|
556
|
+
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
557
|
+
setDefaultMaxTurns,
|
|
558
|
+
setGraceTurns,
|
|
559
|
+
setDefaultJoinMode,
|
|
560
|
+
},
|
|
561
|
+
(event, payload) => pi.events.emit(event, payload),
|
|
562
|
+
);
|
|
563
|
+
|
|
551
564
|
// ---- Agent tool ----
|
|
552
565
|
|
|
553
|
-
pi.registerTool
|
|
566
|
+
pi.registerTool(defineTool({
|
|
554
567
|
name: "Agent",
|
|
555
568
|
label: "Agent",
|
|
556
569
|
description: `Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
@@ -582,7 +595,7 @@ Guidelines:
|
|
|
582
595
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
583
596
|
}),
|
|
584
597
|
subagent_type: Type.String({
|
|
585
|
-
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or
|
|
598
|
+
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.`,
|
|
586
599
|
}),
|
|
587
600
|
model: Type.Optional(
|
|
588
601
|
Type.String({
|
|
@@ -964,11 +977,11 @@ Guidelines:
|
|
|
964
977
|
details,
|
|
965
978
|
);
|
|
966
979
|
},
|
|
967
|
-
});
|
|
980
|
+
}));
|
|
968
981
|
|
|
969
982
|
// ---- get_subagent_result tool ----
|
|
970
983
|
|
|
971
|
-
pi.registerTool({
|
|
984
|
+
pi.registerTool(defineTool({
|
|
972
985
|
name: "get_subagent_result",
|
|
973
986
|
label: "Get Agent Result",
|
|
974
987
|
description:
|
|
@@ -1038,11 +1051,11 @@ Guidelines:
|
|
|
1038
1051
|
|
|
1039
1052
|
return textResult(output);
|
|
1040
1053
|
},
|
|
1041
|
-
});
|
|
1054
|
+
}));
|
|
1042
1055
|
|
|
1043
1056
|
// ---- steer_subagent tool ----
|
|
1044
1057
|
|
|
1045
|
-
pi.registerTool({
|
|
1058
|
+
pi.registerTool(defineTool({
|
|
1046
1059
|
name: "steer_subagent",
|
|
1047
1060
|
label: "Steer Agent",
|
|
1048
1061
|
description:
|
|
@@ -1080,12 +1093,12 @@ Guidelines:
|
|
|
1080
1093
|
return textResult(`Failed to steer agent: ${err instanceof Error ? err.message : String(err)}`);
|
|
1081
1094
|
}
|
|
1082
1095
|
},
|
|
1083
|
-
});
|
|
1096
|
+
}));
|
|
1084
1097
|
|
|
1085
1098
|
// ---- /agents interactive menu ----
|
|
1086
1099
|
|
|
1087
1100
|
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
1088
|
-
const personalAgentsDir = () => join(
|
|
1101
|
+
const personalAgentsDir = () => join(getAgentDir(), "agents");
|
|
1089
1102
|
|
|
1090
1103
|
/** Find the file path of a custom agent by name (project first, then global). */
|
|
1091
1104
|
function findAgentFile(name: string): { path: string; location: "project" | "personal" } | undefined {
|
|
@@ -1324,7 +1337,7 @@ Guidelines:
|
|
|
1324
1337
|
async function ejectAgent(ctx: ExtensionCommandContext, name: string, cfg: AgentConfig) {
|
|
1325
1338
|
const location = await ctx.ui.select("Choose location", [
|
|
1326
1339
|
"Project (.pi/agents/)",
|
|
1327
|
-
|
|
1340
|
+
`Personal (${personalAgentsDir()})`,
|
|
1328
1341
|
]);
|
|
1329
1342
|
if (!location) return;
|
|
1330
1343
|
|
|
@@ -1386,7 +1399,7 @@ Guidelines:
|
|
|
1386
1399
|
// No file (built-in default) — create a stub
|
|
1387
1400
|
const location = await ctx.ui.select("Choose location", [
|
|
1388
1401
|
"Project (.pi/agents/)",
|
|
1389
|
-
|
|
1402
|
+
`Personal (${personalAgentsDir()})`,
|
|
1390
1403
|
]);
|
|
1391
1404
|
if (!location) return;
|
|
1392
1405
|
|
|
@@ -1424,7 +1437,7 @@ Guidelines:
|
|
|
1424
1437
|
async function showCreateWizard(ctx: ExtensionCommandContext) {
|
|
1425
1438
|
const location = await ctx.ui.select("Choose location", [
|
|
1426
1439
|
"Project (.pi/agents/)",
|
|
1427
|
-
|
|
1440
|
+
`Personal (${personalAgentsDir()})`,
|
|
1428
1441
|
]);
|
|
1429
1442
|
if (!location) return;
|
|
1430
1443
|
|
|
@@ -1605,6 +1618,17 @@ ${systemPrompt}
|
|
|
1605
1618
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
1606
1619
|
}
|
|
1607
1620
|
|
|
1621
|
+
function snapshotSettings(): SubagentsSettings {
|
|
1622
|
+
return {
|
|
1623
|
+
maxConcurrent: manager.getMaxConcurrent(),
|
|
1624
|
+
// 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
|
|
1625
|
+
// normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
|
|
1626
|
+
defaultMaxTurns: getDefaultMaxTurns() ?? 0,
|
|
1627
|
+
graceTurns: getGraceTurns(),
|
|
1628
|
+
defaultJoinMode: getDefaultJoinMode(),
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1608
1632
|
async function showSettings(ctx: ExtensionCommandContext) {
|
|
1609
1633
|
const choice = await ctx.ui.select("Settings", [
|
|
1610
1634
|
`Max concurrency (current: ${manager.getMaxConcurrent()})`,
|
|
@@ -1620,7 +1644,7 @@ ${systemPrompt}
|
|
|
1620
1644
|
const n = parseInt(val, 10);
|
|
1621
1645
|
if (n >= 1) {
|
|
1622
1646
|
manager.setMaxConcurrent(n);
|
|
1623
|
-
ctx
|
|
1647
|
+
notifyApplied(ctx, `Max concurrency set to ${n}`);
|
|
1624
1648
|
} else {
|
|
1625
1649
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1626
1650
|
}
|
|
@@ -1631,10 +1655,10 @@ ${systemPrompt}
|
|
|
1631
1655
|
const n = parseInt(val, 10);
|
|
1632
1656
|
if (n === 0) {
|
|
1633
1657
|
setDefaultMaxTurns(undefined);
|
|
1634
|
-
ctx
|
|
1658
|
+
notifyApplied(ctx, "Default max turns set to unlimited");
|
|
1635
1659
|
} else if (n >= 1) {
|
|
1636
1660
|
setDefaultMaxTurns(n);
|
|
1637
|
-
ctx
|
|
1661
|
+
notifyApplied(ctx, `Default max turns set to ${n}`);
|
|
1638
1662
|
} else {
|
|
1639
1663
|
ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
1640
1664
|
}
|
|
@@ -1645,7 +1669,7 @@ ${systemPrompt}
|
|
|
1645
1669
|
const n = parseInt(val, 10);
|
|
1646
1670
|
if (n >= 1) {
|
|
1647
1671
|
setGraceTurns(n);
|
|
1648
|
-
ctx
|
|
1672
|
+
notifyApplied(ctx, `Grace turns set to ${n}`);
|
|
1649
1673
|
} else {
|
|
1650
1674
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1651
1675
|
}
|
|
@@ -1659,11 +1683,24 @@ ${systemPrompt}
|
|
|
1659
1683
|
if (val) {
|
|
1660
1684
|
const mode = val.split(" ")[0] as JoinMode;
|
|
1661
1685
|
setDefaultJoinMode(mode);
|
|
1662
|
-
ctx
|
|
1686
|
+
notifyApplied(ctx, `Default join mode set to ${mode}`);
|
|
1663
1687
|
}
|
|
1664
1688
|
}
|
|
1665
1689
|
}
|
|
1666
1690
|
|
|
1691
|
+
// Persist the current snapshot, emit `subagents:settings_changed`, and surface
|
|
1692
|
+
// the right toast. Successful saves show info; persistence failures downgrade
|
|
1693
|
+
// to warning so users aren't silently reverted on restart. Event fires regardless
|
|
1694
|
+
// of outcome so listeners see the in-memory change.
|
|
1695
|
+
function notifyApplied(ctx: ExtensionCommandContext, successMsg: string) {
|
|
1696
|
+
const { message, level } = saveAndEmitChanged(
|
|
1697
|
+
snapshotSettings(),
|
|
1698
|
+
successMsg,
|
|
1699
|
+
(event, payload) => pi.events.emit(event, payload),
|
|
1700
|
+
);
|
|
1701
|
+
ctx.ui.notify(message, level);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1667
1704
|
pi.registerCommand("agents", {
|
|
1668
1705
|
description: "Manage agents",
|
|
1669
1706
|
handler: async (_args, ctx) => { await showAgentsMenu(ctx); },
|