@narumitw/pi-subagents 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/settings.ts +141 -7
- package/src/subagents.ts +12 -0
package/README.md
CHANGED
|
@@ -210,7 +210,7 @@ When `subagent_wait` starts while a turn is running, that wait consumes the turn
|
|
|
210
210
|
|
|
211
211
|
The default `subprocess` transport preserves compatibility: each turn starts a fresh isolated `pi --mode json -p --no-session` child and receives sanitized, bounded history. Set `transport` to `in-process` to retain one public Pi SDK `AgentSession` per stateful `agentId`, avoiding repeated process startup while preserving native child history in memory.
|
|
212
212
|
|
|
213
|
-
Configure the runtime in `~/.pi/agent/pi-subagents
|
|
213
|
+
Configure the runtime in `~/.pi/agent/pi-subagents.json`, then reload Pi:
|
|
214
214
|
|
|
215
215
|
```json
|
|
216
216
|
{
|
|
@@ -301,7 +301,9 @@ Built-in agents inherit the active/default Pi model instead of forcing a provide
|
|
|
301
301
|
## ⚙️ Configure agent tools
|
|
302
302
|
|
|
303
303
|
Run `/subagents:config` in an interactive Pi session to edit the tools each subagent may use.
|
|
304
|
-
The command stores settings in `~/.pi/agent/pi-subagents
|
|
304
|
+
The command stores settings in `~/.pi/agent/pi-subagents.json`.
|
|
305
|
+
|
|
306
|
+
Compatibility: a valid legacy `pi-subagents-config.json` is migrated automatically to `pi-subagents.json`. If both files exist, the new filename takes precedence.
|
|
305
307
|
|
|
306
308
|
- Select an agent, then press Enter or Space to toggle tools.
|
|
307
309
|
- Press `S` to save, or Esc to cancel and return to agent selection.
|
package/package.json
CHANGED
package/src/settings.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
@@ -118,22 +119,151 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
|
|
|
118
119
|
return settings;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
const SETTINGS_FILE = "pi-subagents.json";
|
|
123
|
+
const LEGACY_SETTINGS_FILE = "pi-subagents-config.json";
|
|
124
|
+
let pendingSettingsNotice: string | undefined;
|
|
125
|
+
|
|
121
126
|
export function readSubagentSettings(): SubagentSettings | undefined {
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
pendingSettingsNotice = undefined;
|
|
128
|
+
const canonicalPath = path.join(getAgentDir(), SETTINGS_FILE);
|
|
129
|
+
const legacyPath = path.join(getAgentDir(), LEGACY_SETTINGS_FILE);
|
|
130
|
+
if (fs.existsSync(canonicalPath)) {
|
|
131
|
+
const canonical = readSettingsFile(canonicalPath);
|
|
132
|
+
const notices: string[] = [];
|
|
133
|
+
if (!canonical) notices.push(`${SETTINGS_FILE} is invalid and was ignored.`);
|
|
134
|
+
if (fs.existsSync(legacyPath)) {
|
|
135
|
+
notices.push(`${LEGACY_SETTINGS_FILE} ignored because ${SETTINGS_FILE} takes precedence.`);
|
|
136
|
+
}
|
|
137
|
+
if (notices.length > 0) pendingSettingsNotice = notices.join("\n");
|
|
138
|
+
return canonical;
|
|
139
|
+
}
|
|
140
|
+
if (!fs.existsSync(legacyPath)) return undefined;
|
|
141
|
+
const legacySnapshot = readSettingsSnapshot(legacyPath);
|
|
142
|
+
const legacy = legacySnapshot.settings;
|
|
143
|
+
if (!legacy) {
|
|
144
|
+
pendingSettingsNotice = `${LEGACY_SETTINGS_FILE} is invalid and was ignored.`;
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
let installedIdentity: FileIdentity;
|
|
148
|
+
try {
|
|
149
|
+
installedIdentity = installFileExclusively(canonicalPath, legacySnapshot.contents ?? "");
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (fs.existsSync(canonicalPath)) {
|
|
152
|
+
const canonical = readSettingsFile(canonicalPath);
|
|
153
|
+
pendingSettingsNotice = [
|
|
154
|
+
...(!canonical ? [`${SETTINGS_FILE} is invalid and was ignored.`] : []),
|
|
155
|
+
`${LEGACY_SETTINGS_FILE} ignored because ${SETTINGS_FILE} was created concurrently.`,
|
|
156
|
+
].join("\n");
|
|
157
|
+
return canonical;
|
|
158
|
+
}
|
|
159
|
+
pendingSettingsNotice = `Subagent settings migration failed: ${formatError(error)}. The legacy file was used for this session.`;
|
|
160
|
+
return legacy;
|
|
161
|
+
}
|
|
162
|
+
if (!fileContentsEqual(legacyPath, legacySnapshot.contents ?? "")) {
|
|
163
|
+
pendingSettingsNotice = removeFileIfIdentityMatches(
|
|
164
|
+
canonicalPath,
|
|
165
|
+
installedIdentity,
|
|
166
|
+
legacySnapshot.contents ?? "",
|
|
167
|
+
)
|
|
168
|
+
? `${LEGACY_SETTINGS_FILE} changed during migration; the stale ${SETTINGS_FILE} snapshot was removed.`
|
|
169
|
+
: `${LEGACY_SETTINGS_FILE} changed during migration, but ${SETTINGS_FILE} was replaced concurrently and takes precedence on the next load.`;
|
|
170
|
+
return legacy;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
fs.rmSync(legacyPath);
|
|
174
|
+
pendingSettingsNotice = `Subagent settings migrated from ${LEGACY_SETTINGS_FILE} to ${SETTINGS_FILE}.`;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
pendingSettingsNotice = `Subagent settings migrated to ${SETTINGS_FILE}, but ${LEGACY_SETTINGS_FILE} could not be removed: ${formatError(error)}.`;
|
|
177
|
+
}
|
|
178
|
+
return legacy;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type FileIdentity = { dev: number; ino: number };
|
|
182
|
+
|
|
183
|
+
function installFileExclusively(filePath: string, contents: string): FileIdentity {
|
|
184
|
+
const tempFile = path.join(path.dirname(filePath), `.${SETTINGS_FILE}.${randomUUID()}.tmp`);
|
|
185
|
+
try {
|
|
186
|
+
fs.writeFileSync(tempFile, contents, { encoding: "utf8", flag: "wx" });
|
|
187
|
+
const identity = fs.lstatSync(tempFile);
|
|
188
|
+
fs.linkSync(tempFile, filePath);
|
|
189
|
+
return { dev: identity.dev, ino: identity.ino };
|
|
190
|
+
} finally {
|
|
191
|
+
try {
|
|
192
|
+
fs.rmSync(tempFile, { force: true });
|
|
193
|
+
} catch {
|
|
194
|
+
// Preserve the migration result if best-effort temp cleanup fails.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function removeFileIfIdentityMatches(
|
|
200
|
+
filePath: string,
|
|
201
|
+
expected: FileIdentity,
|
|
202
|
+
expectedContents: string,
|
|
203
|
+
) {
|
|
124
204
|
try {
|
|
125
|
-
|
|
205
|
+
const current = fs.lstatSync(filePath);
|
|
206
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino) return false;
|
|
207
|
+
if (fs.readFileSync(filePath, "utf8") !== expectedContents) return false;
|
|
208
|
+
fs.rmSync(filePath);
|
|
209
|
+
return true;
|
|
126
210
|
} catch {
|
|
127
|
-
return
|
|
211
|
+
return false;
|
|
128
212
|
}
|
|
129
213
|
}
|
|
130
214
|
|
|
215
|
+
function fileContentsEqual(filePath: string, expected: string) {
|
|
216
|
+
try {
|
|
217
|
+
return fs.readFileSync(filePath, "utf8") === expected;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function consumeSubagentSettingsNotice() {
|
|
224
|
+
const notice = pendingSettingsNotice;
|
|
225
|
+
pendingSettingsNotice = undefined;
|
|
226
|
+
return notice;
|
|
227
|
+
}
|
|
228
|
+
|
|
131
229
|
export function saveSubagentConfig(settings: SubagentSettings): void {
|
|
132
230
|
const agentDir = getAgentDir();
|
|
133
231
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
232
|
+
const configPath = path.join(agentDir, SETTINGS_FILE);
|
|
233
|
+
const tempFile = path.join(agentDir, `.${SETTINGS_FILE}.${randomUUID()}.tmp`);
|
|
234
|
+
try {
|
|
235
|
+
fs.writeFileSync(tempFile, `${JSON.stringify(settings, null, "\t")}\n`, {
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
flag: "wx",
|
|
238
|
+
});
|
|
239
|
+
fs.renameSync(tempFile, configPath);
|
|
240
|
+
} finally {
|
|
241
|
+
try {
|
|
242
|
+
fs.rmSync(tempFile, { force: true });
|
|
243
|
+
} catch {
|
|
244
|
+
// Preserve the save result if best-effort temp cleanup fails.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function readSettingsFile(configPath: string): SubagentSettings | undefined {
|
|
250
|
+
return readSettingsSnapshot(configPath).settings;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function readSettingsSnapshot(configPath: string): {
|
|
254
|
+
settings?: SubagentSettings;
|
|
255
|
+
contents?: string;
|
|
256
|
+
} {
|
|
257
|
+
try {
|
|
258
|
+
const contents = fs.readFileSync(configPath, "utf8");
|
|
259
|
+
return { settings: normalizeSubagentSettings(JSON.parse(contents)), contents };
|
|
260
|
+
} catch {
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
134
264
|
|
|
135
|
-
|
|
136
|
-
|
|
265
|
+
function formatError(error: unknown) {
|
|
266
|
+
return error instanceof Error ? error.message : String(error);
|
|
137
267
|
}
|
|
138
268
|
|
|
139
269
|
export function uniqueToolNames(tools: string[]): string[] {
|
|
@@ -153,7 +283,11 @@ export function resolveSubagentThinkingLevel(
|
|
|
153
283
|
topLevelThinkingLevel?: SubagentThinkingLevel,
|
|
154
284
|
localThinkingLevel?: SubagentThinkingLevel,
|
|
155
285
|
): SubagentThinkingLevel | undefined {
|
|
156
|
-
return
|
|
286
|
+
return (
|
|
287
|
+
localThinkingLevel ??
|
|
288
|
+
topLevelThinkingLevel ??
|
|
289
|
+
agents.find((agent) => agent.name === agentName)?.thinkingLevel
|
|
290
|
+
);
|
|
157
291
|
}
|
|
158
292
|
|
|
159
293
|
export function hasAnyAgentOverride(config: SubagentAgentConfig): boolean {
|
package/src/subagents.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { executeSubagent } from "./execution.js";
|
|
|
19
19
|
import { renderSubagentCall, renderSubagentResult } from "./render.js";
|
|
20
20
|
import type { SubagentDetails } from "./runner.js";
|
|
21
21
|
import { registerStatefulSubagents } from "./stateful.js";
|
|
22
|
+
import { consumeSubagentSettingsNotice, readSubagentSettings } from "./settings.js";
|
|
22
23
|
|
|
23
24
|
export default function (pi: ExtensionAPI) {
|
|
24
25
|
pi.registerTool<typeof SubagentParams, SubagentDetails>({
|
|
@@ -67,6 +68,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
67
68
|
if ((event.details as (SubagentDetails & { isError?: boolean }) | undefined)?.isError) return { isError: true };
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
pi.on("session_start", (_event, ctx) => {
|
|
72
|
+
let notice = consumeSubagentSettingsNotice();
|
|
73
|
+
if (!notice) {
|
|
74
|
+
readSubagentSettings();
|
|
75
|
+
notice = consumeSubagentSettingsNotice();
|
|
76
|
+
}
|
|
77
|
+
if (notice) ctx.ui.notify(notice, "warning");
|
|
78
|
+
});
|
|
79
|
+
|
|
70
80
|
registerSubagentConfigCommand(pi);
|
|
71
81
|
registerStatefulSubagents(pi);
|
|
72
82
|
}
|
|
@@ -75,7 +85,9 @@ export { buildPiArgs } from "./runner.js";
|
|
|
75
85
|
export {
|
|
76
86
|
normalizeAgentSettings,
|
|
77
87
|
normalizeSubagentSettings,
|
|
88
|
+
readSubagentSettings,
|
|
78
89
|
resolveSubagentThinkingLevel,
|
|
90
|
+
saveSubagentConfig,
|
|
79
91
|
sameToolSet,
|
|
80
92
|
uniqueToolNames,
|
|
81
93
|
} from "./settings.js";
|