@narumitw/pi-subagents 0.12.0 → 0.13.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/README.md +3 -2
- package/package.json +1 -1
- package/src/config-ui.ts +271 -0
- package/src/execution.ts +406 -0
- package/src/params.ts +59 -0
- package/src/render.ts +521 -0
- package/src/runner.ts +368 -0
- package/src/settings.ts +120 -0
- package/src/subagents.ts +23 -1679
package/src/runner.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
6
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
7
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type {
|
|
9
|
+
AgentConfig,
|
|
10
|
+
AgentScope,
|
|
11
|
+
AgentSource,
|
|
12
|
+
SubagentThinkingLevel,
|
|
13
|
+
} from "./agents.js";
|
|
14
|
+
|
|
15
|
+
const KILL_GRACE_MS = 5000;
|
|
16
|
+
|
|
17
|
+
export interface UsageStats {
|
|
18
|
+
input: number;
|
|
19
|
+
output: number;
|
|
20
|
+
cacheRead: number;
|
|
21
|
+
cacheWrite: number;
|
|
22
|
+
cost: number;
|
|
23
|
+
contextTokens: number;
|
|
24
|
+
turns: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SingleResult {
|
|
28
|
+
agent: string;
|
|
29
|
+
agentSource: AgentSource | "unknown";
|
|
30
|
+
task: string;
|
|
31
|
+
exitCode: number;
|
|
32
|
+
messages: Message[];
|
|
33
|
+
stderr: string;
|
|
34
|
+
usage: UsageStats;
|
|
35
|
+
model?: string;
|
|
36
|
+
thinkingLevel?: SubagentThinkingLevel;
|
|
37
|
+
stopReason?: string;
|
|
38
|
+
errorMessage?: string;
|
|
39
|
+
step?: number;
|
|
40
|
+
finalOutput?: string;
|
|
41
|
+
timedOut?: boolean;
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SubagentDetails {
|
|
46
|
+
mode: "single" | "parallel" | "chain";
|
|
47
|
+
agentScope: AgentScope;
|
|
48
|
+
projectAgentsDir: string | null;
|
|
49
|
+
results: SingleResult[];
|
|
50
|
+
aggregator?: SingleResult;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getFinalOutput(messages: Message[]): string {
|
|
54
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
55
|
+
const msg = messages[i];
|
|
56
|
+
if (msg.role === "assistant") {
|
|
57
|
+
for (const part of msg.content) {
|
|
58
|
+
if (part.type === "text") return part.text;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getResultFinalOutput(result: SingleResult): string {
|
|
66
|
+
return result.finalOutput ?? getFinalOutput(result.messages);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildFanInContext(results: SingleResult[]): string {
|
|
70
|
+
return results
|
|
71
|
+
.map((result, index) => {
|
|
72
|
+
const status = result.exitCode === 0 ? "completed" : result.exitCode === -1 ? "running" : "failed";
|
|
73
|
+
const output = getResultFinalOutput(result);
|
|
74
|
+
const error = result.errorMessage || result.stderr.trim();
|
|
75
|
+
return [
|
|
76
|
+
`## Result ${index + 1}: ${result.agent} (${status})`,
|
|
77
|
+
`Task: ${result.task}`,
|
|
78
|
+
output ? `Output:\n${output}` : error ? `Error:\n${error}` : "Output: (no output)",
|
|
79
|
+
].join("\n\n");
|
|
80
|
+
})
|
|
81
|
+
.join("\n\n---\n\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
85
|
+
items: TIn[],
|
|
86
|
+
concurrency: number,
|
|
87
|
+
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
88
|
+
): Promise<TOut[]> {
|
|
89
|
+
if (items.length === 0) return [];
|
|
90
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
91
|
+
const results: TOut[] = new Array(items.length);
|
|
92
|
+
let nextIndex = 0;
|
|
93
|
+
const workers = new Array(limit).fill(null).map(async () => {
|
|
94
|
+
while (true) {
|
|
95
|
+
const current = nextIndex++;
|
|
96
|
+
if (current >= items.length) return;
|
|
97
|
+
results[current] = await fn(items[current], current);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
await Promise.all(workers);
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function writePromptToTempFile(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> {
|
|
105
|
+
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
|
|
106
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
107
|
+
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
108
|
+
await withFileMutationQueue(filePath, async () => {
|
|
109
|
+
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
|
110
|
+
});
|
|
111
|
+
return { dir: tmpDir, filePath };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function buildPiArgs(options: {
|
|
115
|
+
model?: string;
|
|
116
|
+
thinkingLevel?: SubagentThinkingLevel;
|
|
117
|
+
tools?: string[];
|
|
118
|
+
systemPromptPath?: string;
|
|
119
|
+
task: string;
|
|
120
|
+
}): string[] {
|
|
121
|
+
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
122
|
+
if (options.model) args.push("--model", options.model);
|
|
123
|
+
if (options.thinkingLevel) args.push("--thinking", options.thinkingLevel);
|
|
124
|
+
if (Array.isArray(options.tools)) {
|
|
125
|
+
if (options.tools.length > 0) args.push("--tools", options.tools.join(","));
|
|
126
|
+
else args.push("--no-tools");
|
|
127
|
+
}
|
|
128
|
+
if (options.systemPromptPath) args.push("--append-system-prompt", options.systemPromptPath);
|
|
129
|
+
args.push(`Task: ${options.task}`);
|
|
130
|
+
return args;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
134
|
+
const currentScript = process.argv[1];
|
|
135
|
+
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
136
|
+
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
137
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
141
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
142
|
+
if (!isGenericRuntime) {
|
|
143
|
+
return { command: process.execPath, args };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { command: "pi", args };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function terminateProcess(proc: ReturnType<typeof spawn>) {
|
|
150
|
+
if (proc.killed) return;
|
|
151
|
+
if (process.platform !== "win32" && proc.pid) {
|
|
152
|
+
try {
|
|
153
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
154
|
+
} catch {
|
|
155
|
+
proc.kill("SIGTERM");
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
proc.kill("SIGTERM");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
if (proc.killed) return;
|
|
163
|
+
if (process.platform !== "win32" && proc.pid) {
|
|
164
|
+
try {
|
|
165
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
166
|
+
} catch {
|
|
167
|
+
proc.kill("SIGKILL");
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
proc.kill("SIGKILL");
|
|
171
|
+
}
|
|
172
|
+
}, KILL_GRACE_MS).unref();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
176
|
+
|
|
177
|
+
export async function runSingleAgent(
|
|
178
|
+
defaultCwd: string,
|
|
179
|
+
agents: AgentConfig[],
|
|
180
|
+
agentName: string,
|
|
181
|
+
task: string,
|
|
182
|
+
cwd: string | undefined,
|
|
183
|
+
step: number | undefined,
|
|
184
|
+
signal: AbortSignal | undefined,
|
|
185
|
+
thinkingLevel: SubagentThinkingLevel | undefined,
|
|
186
|
+
timeoutMs: number,
|
|
187
|
+
onUpdate: OnUpdateCallback | undefined,
|
|
188
|
+
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
189
|
+
): Promise<SingleResult> {
|
|
190
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
191
|
+
|
|
192
|
+
if (!agent) {
|
|
193
|
+
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
194
|
+
return {
|
|
195
|
+
agent: agentName,
|
|
196
|
+
agentSource: "unknown",
|
|
197
|
+
task,
|
|
198
|
+
exitCode: 1,
|
|
199
|
+
messages: [],
|
|
200
|
+
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
201
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
202
|
+
thinkingLevel,
|
|
203
|
+
step,
|
|
204
|
+
finalOutput: "",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let tmpPromptDir: string | null = null;
|
|
209
|
+
let tmpPromptPath: string | null = null;
|
|
210
|
+
|
|
211
|
+
const currentResult: SingleResult = {
|
|
212
|
+
agent: agentName,
|
|
213
|
+
agentSource: agent.source,
|
|
214
|
+
task,
|
|
215
|
+
exitCode: 0,
|
|
216
|
+
messages: [],
|
|
217
|
+
stderr: "",
|
|
218
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
219
|
+
model: agent.model ?? undefined,
|
|
220
|
+
thinkingLevel,
|
|
221
|
+
step,
|
|
222
|
+
timeoutMs,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const emitUpdate = () => {
|
|
226
|
+
currentResult.finalOutput = getFinalOutput(currentResult.messages);
|
|
227
|
+
if (onUpdate) {
|
|
228
|
+
onUpdate({
|
|
229
|
+
content: [{ type: "text", text: currentResult.finalOutput || "(running...)" }],
|
|
230
|
+
details: makeDetails([currentResult]),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
if (agent.systemPrompt.trim()) {
|
|
237
|
+
const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
238
|
+
tmpPromptDir = tmp.dir;
|
|
239
|
+
tmpPromptPath = tmp.filePath;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const args = buildPiArgs({
|
|
243
|
+
model: agent.model,
|
|
244
|
+
thinkingLevel,
|
|
245
|
+
tools: agent.tools,
|
|
246
|
+
systemPromptPath: tmpPromptPath ?? undefined,
|
|
247
|
+
task,
|
|
248
|
+
});
|
|
249
|
+
let wasAborted = false;
|
|
250
|
+
let timedOut = false;
|
|
251
|
+
|
|
252
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
253
|
+
const invocation = getPiInvocation(args);
|
|
254
|
+
let settled = false;
|
|
255
|
+
const finish = (code: number) => {
|
|
256
|
+
if (settled) return;
|
|
257
|
+
settled = true;
|
|
258
|
+
clearTimeout(timeout);
|
|
259
|
+
resolve(code);
|
|
260
|
+
};
|
|
261
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
262
|
+
cwd: cwd ?? defaultCwd,
|
|
263
|
+
detached: process.platform !== "win32",
|
|
264
|
+
shell: false,
|
|
265
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
266
|
+
});
|
|
267
|
+
let buffer = "";
|
|
268
|
+
const timeout = setTimeout(() => {
|
|
269
|
+
timedOut = true;
|
|
270
|
+
currentResult.timedOut = true;
|
|
271
|
+
currentResult.stopReason = "timeout";
|
|
272
|
+
currentResult.errorMessage = `Subagent timed out after ${timeoutMs}ms`;
|
|
273
|
+
currentResult.stderr += `${currentResult.stderr ? "\n" : ""}Subagent timed out after ${timeoutMs}ms.`;
|
|
274
|
+
emitUpdate();
|
|
275
|
+
terminateProcess(proc);
|
|
276
|
+
}, timeoutMs);
|
|
277
|
+
timeout.unref();
|
|
278
|
+
|
|
279
|
+
const processLine = (line: string) => {
|
|
280
|
+
if (!line.trim()) return;
|
|
281
|
+
let event: any;
|
|
282
|
+
try {
|
|
283
|
+
event = JSON.parse(line);
|
|
284
|
+
} catch {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (event.type === "message_end" && event.message) {
|
|
289
|
+
const msg = event.message as Message;
|
|
290
|
+
currentResult.messages.push(msg);
|
|
291
|
+
|
|
292
|
+
if (msg.role === "assistant") {
|
|
293
|
+
currentResult.usage.turns++;
|
|
294
|
+
const usage = msg.usage;
|
|
295
|
+
if (usage) {
|
|
296
|
+
currentResult.usage.input += usage.input || 0;
|
|
297
|
+
currentResult.usage.output += usage.output || 0;
|
|
298
|
+
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
299
|
+
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
300
|
+
currentResult.usage.cost += usage.cost?.total || 0;
|
|
301
|
+
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
302
|
+
}
|
|
303
|
+
if (!currentResult.model && msg.model) currentResult.model = msg.model;
|
|
304
|
+
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
305
|
+
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
|
306
|
+
}
|
|
307
|
+
emitUpdate();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (event.type === "tool_result_end" && event.message) {
|
|
311
|
+
currentResult.messages.push(event.message as Message);
|
|
312
|
+
emitUpdate();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
proc.stdout.on("data", (data) => {
|
|
317
|
+
buffer += data.toString();
|
|
318
|
+
const lines = buffer.split("\n");
|
|
319
|
+
buffer = lines.pop() || "";
|
|
320
|
+
for (const line of lines) processLine(line);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
proc.stderr.on("data", (data) => {
|
|
324
|
+
currentResult.stderr += data.toString();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
proc.on("close", (code) => {
|
|
328
|
+
if (buffer.trim()) processLine(buffer);
|
|
329
|
+
finish(timedOut ? 124 : (code ?? 0));
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
proc.on("error", (error) => {
|
|
333
|
+
currentResult.errorMessage = error.message;
|
|
334
|
+
currentResult.stderr += `${currentResult.stderr ? "\n" : ""}${error.message}`;
|
|
335
|
+
finish(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (signal) {
|
|
339
|
+
const killProc = () => {
|
|
340
|
+
wasAborted = true;
|
|
341
|
+
currentResult.stopReason = "aborted";
|
|
342
|
+
currentResult.errorMessage = "Subagent was aborted";
|
|
343
|
+
terminateProcess(proc);
|
|
344
|
+
};
|
|
345
|
+
if (signal.aborted) killProc();
|
|
346
|
+
else signal.addEventListener("abort", killProc, { once: true });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
currentResult.exitCode = exitCode;
|
|
351
|
+
currentResult.finalOutput = getFinalOutput(currentResult.messages);
|
|
352
|
+
if (wasAborted && !timedOut) throw new Error("Subagent was aborted");
|
|
353
|
+
return currentResult;
|
|
354
|
+
} finally {
|
|
355
|
+
if (tmpPromptPath)
|
|
356
|
+
try {
|
|
357
|
+
fs.unlinkSync(tmpPromptPath);
|
|
358
|
+
} catch {
|
|
359
|
+
/* ignore */
|
|
360
|
+
}
|
|
361
|
+
if (tmpPromptDir)
|
|
362
|
+
try {
|
|
363
|
+
fs.rmdirSync(tmpPromptDir);
|
|
364
|
+
} catch {
|
|
365
|
+
/* ignore */
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import {
|
|
5
|
+
type AgentConfig,
|
|
6
|
+
isThinkingLevel,
|
|
7
|
+
type SubagentAgentConfig,
|
|
8
|
+
type SubagentSettings,
|
|
9
|
+
type SubagentThinkingLevel,
|
|
10
|
+
} from "./agents.js";
|
|
11
|
+
|
|
12
|
+
export function hasOwn(obj: object, key: PropertyKey): boolean {
|
|
13
|
+
return Object.hasOwn(obj, key);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isStringArray(value: unknown): value is string[] {
|
|
21
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isPositiveNumber(value: unknown): value is number {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeAgentSettings(value: unknown): SubagentAgentConfig | undefined {
|
|
29
|
+
if (!isPlainObject(value)) return undefined;
|
|
30
|
+
|
|
31
|
+
const config: SubagentAgentConfig = {};
|
|
32
|
+
let hasKnownField = false;
|
|
33
|
+
|
|
34
|
+
if (hasOwn(value, "tools")) {
|
|
35
|
+
if (!isStringArray(value.tools)) return undefined;
|
|
36
|
+
config.tools = value.tools;
|
|
37
|
+
hasKnownField = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (hasOwn(value, "model")) {
|
|
41
|
+
if (value.model !== null && typeof value.model !== "string") return undefined;
|
|
42
|
+
config.model = value.model;
|
|
43
|
+
hasKnownField = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (hasOwn(value, "thinkingLevel")) {
|
|
47
|
+
if (value.thinkingLevel !== null && !isThinkingLevel(value.thinkingLevel)) return undefined;
|
|
48
|
+
config.thinkingLevel = value.thinkingLevel;
|
|
49
|
+
hasKnownField = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (hasOwn(value, "timeoutMs")) {
|
|
53
|
+
if (value.timeoutMs !== null && !isPositiveNumber(value.timeoutMs)) return undefined;
|
|
54
|
+
config.timeoutMs = value.timeoutMs;
|
|
55
|
+
hasKnownField = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return hasKnownField ? config : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeSubagentSettings(value: unknown): SubagentSettings | undefined {
|
|
62
|
+
if (!isPlainObject(value)) return undefined;
|
|
63
|
+
if (!hasOwn(value, "agents")) return {};
|
|
64
|
+
if (!isPlainObject(value.agents)) return undefined;
|
|
65
|
+
|
|
66
|
+
const agents: Record<string, SubagentAgentConfig> = {};
|
|
67
|
+
for (const [name, rawConfig] of Object.entries(value.agents)) {
|
|
68
|
+
const config = normalizeAgentSettings(rawConfig);
|
|
69
|
+
if (config) agents[name] = config;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Object.keys(agents).length > 0 ? { agents } : {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readSubagentSettings(): SubagentSettings | undefined {
|
|
76
|
+
const configPath = path.join(getAgentDir(), "pi-subagents-config.json");
|
|
77
|
+
if (!fs.existsSync(configPath)) return undefined;
|
|
78
|
+
try {
|
|
79
|
+
return normalizeSubagentSettings(JSON.parse(fs.readFileSync(configPath, "utf-8")));
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function saveSubagentConfig(settings: SubagentSettings): void {
|
|
86
|
+
const agentDir = getAgentDir();
|
|
87
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
88
|
+
|
|
89
|
+
const configPath = path.join(agentDir, "pi-subagents-config.json");
|
|
90
|
+
fs.writeFileSync(configPath, `${JSON.stringify(settings, null, "\t")}\n`, "utf-8");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function uniqueToolNames(tools: string[]): string[] {
|
|
94
|
+
return [...new Set(tools)];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function sameToolSet(left: string[], right: string[]): boolean {
|
|
98
|
+
const leftSet = new Set(left);
|
|
99
|
+
const rightSet = new Set(right);
|
|
100
|
+
if (leftSet.size !== rightSet.size) return false;
|
|
101
|
+
return [...leftSet].every((tool) => rightSet.has(tool));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function resolveSubagentThinkingLevel(
|
|
105
|
+
agents: readonly Pick<AgentConfig, "name" | "thinkingLevel">[],
|
|
106
|
+
agentName: string,
|
|
107
|
+
topLevelThinkingLevel?: SubagentThinkingLevel,
|
|
108
|
+
localThinkingLevel?: SubagentThinkingLevel,
|
|
109
|
+
): SubagentThinkingLevel | undefined {
|
|
110
|
+
return localThinkingLevel ?? topLevelThinkingLevel ?? agents.find((agent) => agent.name === agentName)?.thinkingLevel;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function hasAnyAgentOverride(config: SubagentAgentConfig): boolean {
|
|
114
|
+
return (
|
|
115
|
+
hasOwn(config, "tools") ||
|
|
116
|
+
hasOwn(config, "model") ||
|
|
117
|
+
hasOwn(config, "thinkingLevel") ||
|
|
118
|
+
hasOwn(config, "timeoutMs")
|
|
119
|
+
);
|
|
120
|
+
}
|