@trim21/personal-pi-extensions 0.0.312 → 0.0.313
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/package.json +1 -1
- package/src/spawn-agent-agents.ts +117 -6
- package/src/spawn-agent.ts +66 -20
package/package.json
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* - grep
|
|
14
14
|
* - find
|
|
15
15
|
* - ls
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* provider: openai # optional; overrides the global default
|
|
17
|
+
* model: claude-haiku-4-5 # optional; overrides the global default
|
|
18
|
+
* thinkingLevel: high # optional; overrides the global default
|
|
18
19
|
* ---
|
|
19
20
|
* System prompt for the agent goes here.
|
|
20
21
|
*
|
|
@@ -22,22 +23,33 @@
|
|
|
22
23
|
* (missing name/description, wrong field types) are skipped. If `tools` is
|
|
23
24
|
* omitted, the subagent runs with the read-only default toolset from the
|
|
24
25
|
* spawn-agent config (read/grep/find/ls) unless overridden there.
|
|
26
|
+
*
|
|
27
|
+
* Global defaults for provider/model/thinkingLevel come from
|
|
28
|
+
* `~/.pi/agent/spawn-agent.json` (see loadSpawnAgentConfig); frontmatter
|
|
29
|
+
* fields take precedence over them, which in turn take precedence over the
|
|
30
|
+
* top-level defaultProvider/defaultModel/defaultThinkingLevel in pi's
|
|
31
|
+
* settings.json.
|
|
25
32
|
*/
|
|
26
33
|
|
|
27
34
|
import { readdirSync, readFileSync } from "node:fs";
|
|
28
35
|
import { join } from "node:path";
|
|
29
36
|
|
|
30
37
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
31
|
-
import { Type } from "typebox";
|
|
38
|
+
import { type Static, Type } from "typebox";
|
|
32
39
|
import { Value } from "typebox/value";
|
|
33
40
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Valid thinking levels, mirroring pi's CLI --thinking validation
|
|
43
|
+
* (VALID_THINKING_LEVELS). "off" disables thinking; "max" is deliberately
|
|
44
|
+
* excluded — pi's CLI layer does not accept it.
|
|
45
|
+
*/
|
|
46
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
36
47
|
|
|
37
48
|
const agentFrontmatterSchema = Type.Object({
|
|
38
49
|
name: Type.String({ minLength: 1 }),
|
|
39
50
|
description: Type.String({ minLength: 1 }),
|
|
40
51
|
tools: Type.Optional(Type.Array(Type.String())),
|
|
52
|
+
provider: Type.Optional(Type.String()),
|
|
41
53
|
model: Type.Optional(Type.String()),
|
|
42
54
|
thinkingLevel: Type.Optional(Type.Union(THINKING_LEVELS.map((level) => Type.Literal(level)))),
|
|
43
55
|
});
|
|
@@ -55,13 +67,25 @@ export interface AgentConfig {
|
|
|
55
67
|
description: string;
|
|
56
68
|
/** Toolset from the frontmatter; undefined means "use the config default". */
|
|
57
69
|
tools?: string[];
|
|
70
|
+
provider?: string;
|
|
58
71
|
model?: string;
|
|
59
|
-
/** Thinking level, applied
|
|
72
|
+
/** Thinking level, applied via --thinking. */
|
|
60
73
|
thinkingLevel?: (typeof THINKING_LEVELS)[number];
|
|
61
74
|
systemPrompt: string;
|
|
62
75
|
filePath: string;
|
|
63
76
|
}
|
|
64
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Global defaults resolved for subagents. Fields are optional: an absent
|
|
80
|
+
* field means "not configured", and applyAgentDefaults leaves the frontmatter
|
|
81
|
+
* value (or its absence) untouched.
|
|
82
|
+
*/
|
|
83
|
+
export interface SpawnAgentDefaults {
|
|
84
|
+
provider?: string;
|
|
85
|
+
model?: string;
|
|
86
|
+
thinkingLevel?: (typeof THINKING_LEVELS)[number];
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
export function discoverAgents(dir = join(getAgentDir(), "agents")): AgentConfig[] {
|
|
66
90
|
let entries;
|
|
67
91
|
try {
|
|
@@ -91,6 +115,7 @@ export function discoverAgents(dir = join(getAgentDir(), "agents")): AgentConfig
|
|
|
91
115
|
name: fm.name,
|
|
92
116
|
description: fm.description,
|
|
93
117
|
tools: fm.tools,
|
|
118
|
+
provider: fm.provider,
|
|
94
119
|
model: fm.model,
|
|
95
120
|
thinkingLevel: fm.thinkingLevel,
|
|
96
121
|
systemPrompt: body,
|
|
@@ -100,6 +125,92 @@ export function discoverAgents(dir = join(getAgentDir(), "agents")): AgentConfig
|
|
|
100
125
|
return agents;
|
|
101
126
|
}
|
|
102
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Load the global subagent defaults from `~/.pi/agent/spawn-agent.json`,
|
|
130
|
+
* falling back per-field to the top-level defaultProvider/defaultModel/
|
|
131
|
+
* defaultThinkingLevel in pi's settings.json (same pattern as the vision and
|
|
132
|
+
* session-name extensions). Returns undefined when the spawn-agent.json file
|
|
133
|
+
* is missing, broken, or empty — in that case applyAgentDefaults leaves agents
|
|
134
|
+
* untouched and the subagent inherits pi's own defaults.
|
|
135
|
+
*/
|
|
136
|
+
export function loadSpawnAgentConfig(
|
|
137
|
+
spawnAgentPath: string,
|
|
138
|
+
settingsPath: string,
|
|
139
|
+
): SpawnAgentDefaults | undefined {
|
|
140
|
+
let raw: unknown;
|
|
141
|
+
try {
|
|
142
|
+
raw = JSON.parse(readFileSync(spawnAgentPath, "utf8"));
|
|
143
|
+
} catch {
|
|
144
|
+
return undefined; // 文件缺失或 JSON 损坏
|
|
145
|
+
}
|
|
146
|
+
let config: SpawnAgentDefaults;
|
|
147
|
+
try {
|
|
148
|
+
config = Value.Parse(spawnAgentConfigSchema, raw);
|
|
149
|
+
} catch {
|
|
150
|
+
return undefined; // 结构不符(非 object / thinkingLevel 非法等)
|
|
151
|
+
}
|
|
152
|
+
if (!config.provider && !config.model && !config.thinkingLevel) return undefined;
|
|
153
|
+
|
|
154
|
+
let fallback: SpawnAgentSettingsFallback | undefined;
|
|
155
|
+
try {
|
|
156
|
+
fallback = Value.Parse(
|
|
157
|
+
spawnAgentSettingsFallbackSchema,
|
|
158
|
+
JSON.parse(readFileSync(settingsPath, "utf8")),
|
|
159
|
+
);
|
|
160
|
+
} catch {
|
|
161
|
+
// settings.json 缺失或损坏:只用 spawn-agent.json 自身的字段
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
provider: nonEmpty(config.provider) ?? nonEmpty(fallback?.defaultProvider),
|
|
165
|
+
model: nonEmpty(config.model) ?? nonEmpty(fallback?.defaultModel),
|
|
166
|
+
thinkingLevel:
|
|
167
|
+
config.thinkingLevel ??
|
|
168
|
+
(isThinkingLevel(fallback?.defaultThinkingLevel) ? fallback.defaultThinkingLevel : undefined),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** spawn-agent.json 的 schema;thinkingLevel 限定与 frontmatter 相同的合法集合 */
|
|
173
|
+
const spawnAgentConfigSchema = Type.Object({
|
|
174
|
+
provider: Type.Optional(Type.String()),
|
|
175
|
+
model: Type.Optional(Type.String()),
|
|
176
|
+
thinkingLevel: Type.Optional(Type.Union(THINKING_LEVELS.map((level) => Type.Literal(level)))),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/** pi settings.json 顶层的兜底字段;thinkingLevel 用宽松字符串,解析后再过滤 */
|
|
180
|
+
const spawnAgentSettingsFallbackSchema = Type.Object({
|
|
181
|
+
defaultProvider: Type.Optional(Type.String()),
|
|
182
|
+
defaultModel: Type.Optional(Type.String()),
|
|
183
|
+
defaultThinkingLevel: Type.Optional(Type.String()),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
type SpawnAgentSettingsFallback = Static<typeof spawnAgentSettingsFallbackSchema>;
|
|
187
|
+
|
|
188
|
+
function nonEmpty(value: string | undefined): string | undefined {
|
|
189
|
+
return value?.trim() || undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isThinkingLevel(value: string | undefined): value is (typeof THINKING_LEVELS)[number] {
|
|
193
|
+
return value !== undefined && (THINKING_LEVELS as readonly string[]).includes(value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Merge global defaults into discovered agents, field by field: a frontmatter
|
|
198
|
+
* value always wins; otherwise the global default is used. Returns the input
|
|
199
|
+
* array unchanged when there are no defaults.
|
|
200
|
+
*/
|
|
201
|
+
export function applyAgentDefaults(
|
|
202
|
+
agents: AgentConfig[],
|
|
203
|
+
defaults: SpawnAgentDefaults | undefined,
|
|
204
|
+
): AgentConfig[] {
|
|
205
|
+
if (!defaults) return agents;
|
|
206
|
+
return agents.map((agent) => ({
|
|
207
|
+
...agent,
|
|
208
|
+
provider: agent.provider ?? defaults.provider,
|
|
209
|
+
model: agent.model ?? defaults.model,
|
|
210
|
+
thinkingLevel: agent.thinkingLevel ?? defaults.thinkingLevel,
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
103
214
|
export function formatAgentList(agents: AgentConfig[]): string {
|
|
104
215
|
if (agents.length === 0) return "none";
|
|
105
216
|
return agents.map((a) => `${a.name}: ${a.description}`).join("; ");
|
package/src/spawn-agent.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type AgentSessionEvent,
|
|
35
35
|
type ExtensionAPI,
|
|
36
36
|
type ExtensionUIContext,
|
|
37
|
+
getAgentDir,
|
|
37
38
|
type RpcExtensionUIRequest,
|
|
38
39
|
type RpcExtensionUIResponse,
|
|
39
40
|
truncateTail,
|
|
@@ -43,7 +44,13 @@ import {
|
|
|
43
44
|
import { Type } from "typebox";
|
|
44
45
|
|
|
45
46
|
import { type ToolPendant } from "./lib/pendant.js";
|
|
46
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
type AgentConfig,
|
|
49
|
+
applyAgentDefaults,
|
|
50
|
+
discoverAgents,
|
|
51
|
+
formatAgentList,
|
|
52
|
+
loadSpawnAgentConfig,
|
|
53
|
+
} from "./spawn-agent-agents.js";
|
|
47
54
|
|
|
48
55
|
// ── constants ────────────────────────────────────────────────────────────────
|
|
49
56
|
|
|
@@ -55,6 +62,13 @@ const DEFAULT_TOOLS = ["read", "grep", "find", "ls"];
|
|
|
55
62
|
const MAX_PROGRESS_LINES = 5;
|
|
56
63
|
/** Progress line content (without the `tool:` / `text:` prefix) is capped at 21 chars; longer text is folded to the first/last 9 chars joined by ` … `. */
|
|
57
64
|
const MAX_PROGRESS_CHARS_PER_LINE = 21;
|
|
65
|
+
/** stderr 采集上限:防止子代理崩溃循环输出撑爆内存;保留尾部(错误信息通常在尾部)。 */
|
|
66
|
+
const MAX_STDERR_CAPTURE_BYTES = 64 * 1024;
|
|
67
|
+
/** 错误消息里 stderr 的展示上限。 */
|
|
68
|
+
const MAX_STDERR_ERROR_BYTES = 4 * 1024;
|
|
69
|
+
/** 全局默认配置:~/.pi/agent/spawn-agent.json,字段可被 frontmatter 覆盖。 */
|
|
70
|
+
const SPAWN_AGENT_CONFIG_PATH = join(getAgentDir(), "spawn-agent.json");
|
|
71
|
+
const SETTINGS_PATH = join(getAgentDir(), "settings.json");
|
|
58
72
|
|
|
59
73
|
/**
|
|
60
74
|
* Tool → extension override map: when a subagent's frontmatter enables a
|
|
@@ -133,6 +147,26 @@ function getFinalOutput(messages: AgentMessage[]): string {
|
|
|
133
147
|
return "";
|
|
134
148
|
}
|
|
135
149
|
|
|
150
|
+
/**
|
|
151
|
+
* 组装失败消息,按来源分行(error/stderr/output)让父模型能分辨信息出处;
|
|
152
|
+
* stderr 截断到尾部(错误信息通常在最后)。全空时保底 "(no output)",
|
|
153
|
+
* 避免只回一个 exit code。
|
|
154
|
+
*/
|
|
155
|
+
export function formatSubagentError(result: SubagentDetails): { reason: string; message: string } {
|
|
156
|
+
const reason =
|
|
157
|
+
result.stopReason ?? (result.exitCode === 0 ? "failed" : `exit ${result.exitCode}`);
|
|
158
|
+
const parts: string[] = [];
|
|
159
|
+
if (result.errorMessage) parts.push(`error: ${result.errorMessage}`);
|
|
160
|
+
const stderr = truncateTail(result.stderr, { maxBytes: MAX_STDERR_ERROR_BYTES });
|
|
161
|
+
if (stderr.content.trim()) {
|
|
162
|
+
const truncatedMark = stderr.truncated ? "\n[stderr truncated]" : "";
|
|
163
|
+
parts.push(`stderr: ${stderr.content.trim()}${truncatedMark}`);
|
|
164
|
+
}
|
|
165
|
+
const output = getFinalOutput(result.messages);
|
|
166
|
+
if (output) parts.push(`output: ${output}`);
|
|
167
|
+
return { reason, message: parts.length > 0 ? parts.join("\n") : "(no output)" };
|
|
168
|
+
}
|
|
169
|
+
|
|
136
170
|
/**
|
|
137
171
|
* Fold over-long progress line content: keep the first/last 9 chars joined by
|
|
138
172
|
* ` … ` (space, ellipsis, space), so the folded line never exceeds
|
|
@@ -219,13 +253,15 @@ export function buildSubagentArgs(
|
|
|
219
253
|
// per-tool overrides) run inside the subagent.
|
|
220
254
|
const args: string[] = ["--mode", "rpc", "--no-session", "--no-extensions"];
|
|
221
255
|
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (model) args.push("--model", model);
|
|
256
|
+
// provider/model/thinkingLevel 已由 applyAgentDefaults 合并进 agent。
|
|
257
|
+
// --provider 只在 model 不含 "/" 前缀时传:带前缀的 model(如
|
|
258
|
+
// "openai/gpt-4o")由 pi 自己解析 provider,显式传 provider 会冲突。
|
|
259
|
+
if (agent.model && !agent.model.includes("/") && agent.provider) {
|
|
260
|
+
args.push("--provider", agent.provider);
|
|
261
|
+
}
|
|
262
|
+
if (agent.model) args.push("--model", agent.model);
|
|
263
|
+
// --thinking 独立传参;pi 支持 "off" 显式关闭思考。
|
|
264
|
+
if (agent.thinkingLevel) args.push("--thinking", agent.thinkingLevel);
|
|
229
265
|
// Read-only default unless the agent explicitly declares a toolset.
|
|
230
266
|
const tools = agent.tools ?? DEFAULT_TOOLS;
|
|
231
267
|
// Load the opencode override for each built-in tool the agent declares
|
|
@@ -514,7 +550,7 @@ export async function runAgent(
|
|
|
514
550
|
});
|
|
515
551
|
|
|
516
552
|
proc.stderr.on("data", (data: Buffer) => {
|
|
517
|
-
result.stderr
|
|
553
|
+
result.stderr = (result.stderr + data.toString()).slice(-MAX_STDERR_CAPTURE_BYTES);
|
|
518
554
|
});
|
|
519
555
|
|
|
520
556
|
proc.stdin.on("error", (error) => {
|
|
@@ -524,13 +560,23 @@ export async function runAgent(
|
|
|
524
560
|
sendRpc({ type: "prompt", message: `Task: ${task}` });
|
|
525
561
|
|
|
526
562
|
const exitCode = await new Promise<number>((resolve) => {
|
|
527
|
-
proc.on("close", (code) => {
|
|
563
|
+
proc.on("close", (code, childSignal) => {
|
|
528
564
|
if (buffer.trim()) processLine(buffer);
|
|
529
|
-
|
|
565
|
+
// 被信号终止时 code 为 null,不能算 0——否则中断会被误判为成功
|
|
566
|
+
resolve(code ?? (childSignal ? 1 : 0));
|
|
567
|
+
});
|
|
568
|
+
proc.on("error", (error) => {
|
|
569
|
+
// spawn 失败(如 pi 命令不存在)时 error 先于 close 触发;
|
|
570
|
+
// 记录真实错误,而不是只留一个 exit code。
|
|
571
|
+
result.errorMessage = error.message;
|
|
572
|
+
result.stopReason ??= "error";
|
|
573
|
+
resolve(1);
|
|
530
574
|
});
|
|
531
|
-
proc.on("error", () => resolve(1));
|
|
532
575
|
|
|
533
576
|
const kill = () => {
|
|
577
|
+
// abort 可能发生在子代理产生任何结果之前;标记 aborted 让上层
|
|
578
|
+
// 识别中断(已有 stopReason 则保留,避免误报)。
|
|
579
|
+
result.stopReason ??= "aborted";
|
|
534
580
|
proc.kill("SIGTERM");
|
|
535
581
|
setTimeout(() => {
|
|
536
582
|
if (!proc.killed) proc.kill("SIGKILL");
|
|
@@ -599,9 +645,12 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
599
645
|
|
|
600
646
|
// Discover the available subagent types once at extension startup. The
|
|
601
647
|
// extension owns this discovery: the model never has to guess agent names
|
|
602
|
-
// or read the agent directory itself. Editing ~/.pi/agent/agents/*.md
|
|
603
|
-
// requires /reload to take effect.
|
|
604
|
-
const agents =
|
|
648
|
+
// or read the agent directory itself. Editing ~/.pi/agent/agents/*.md or
|
|
649
|
+
// ~/.pi/agent/spawn-agent.json requires /reload to take effect.
|
|
650
|
+
const agents = applyAgentDefaults(
|
|
651
|
+
discoverAgents(),
|
|
652
|
+
loadSpawnAgentConfig(SPAWN_AGENT_CONFIG_PATH, SETTINGS_PATH),
|
|
653
|
+
);
|
|
605
654
|
const agentListSection = agents.length > 0 ? formatAgentListSection(agents) : null;
|
|
606
655
|
|
|
607
656
|
if (agentListSection) {
|
|
@@ -661,13 +710,10 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
661
710
|
const isError =
|
|
662
711
|
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
663
712
|
if (isError) {
|
|
664
|
-
const reason =
|
|
665
|
-
result.stopReason ?? (result.exitCode === 0 ? "failed" : `exit ${result.exitCode}`);
|
|
666
|
-
const message =
|
|
667
|
-
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
713
|
+
const { reason, message } = formatSubagentError(result);
|
|
668
714
|
return {
|
|
669
715
|
content: [
|
|
670
|
-
{ type: "text", text: `Subagent "${result.agent}" failed (${reason})
|
|
716
|
+
{ type: "text", text: `Subagent "${result.agent}" failed (${reason}):\n${message}` },
|
|
671
717
|
],
|
|
672
718
|
details: {
|
|
673
719
|
...result,
|