@trim21/personal-pi-extensions 0.1.651 → 0.1.652
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 +67 -9
- package/package.json +3 -11
- package/src/aft/index.ts +40 -23
- package/src/aft/tools.ts +10 -9
- package/src/claude-code/files.ts +99 -54
- package/src/claude-code/glob.ts +8 -5
- package/src/claude-code/grep.ts +8 -5
- package/src/claude-code/index.ts +56 -10
- package/src/claude-code/search.ts +4 -3
- package/src/claude-code/session-tools.ts +4 -3
- package/src/claude-code/shell.ts +3 -8
- package/src/gh/index.ts +48 -25
- package/src/gh/tools/download-release-assets.ts +3 -3
- package/src/gh/tools/get-workflow-jobs.ts +3 -3
- package/src/gh/tools/list-issues.ts +3 -3
- package/src/gh/tools/list-prs.ts +3 -3
- package/src/gh/tools/list-releases.ts +3 -3
- package/src/gh/tools/list-workflow-runs.ts +3 -3
- package/src/gh/tools/read-ci-logs.ts +3 -4
- package/src/gh/tools/read-issue-comments.ts +3 -3
- package/src/gh/tools/read-issue.ts +3 -3
- package/src/gh/tools/read-pr-comments.ts +3 -3
- package/src/gh/tools/read-pr-diff.ts +3 -3
- package/src/gh/tools/read-pr-status.ts +3 -3
- package/src/gh/tools/read-pr.ts +3 -3
- package/src/gh/tools/read-release.ts +3 -3
- package/src/gh/tools/read-repo.ts +3 -3
- package/src/gh/tools/wait-commit-checks.ts +3 -3
- package/src/gh/tools/wait-pr-checks.ts +3 -3
- package/src/gh/tools/watch-run.ts +3 -3
- package/src/index.ts +120 -0
- package/src/lib/lsp/inspect-tool.ts +5 -5
- package/src/lib/lsp/rename-tool.ts +4 -7
- package/src/lib/tool-bus.ts +126 -0
- package/src/lib/tool-registration.ts +83 -0
- package/src/lib/tool-services.ts +65 -0
- package/src/lib/tool-units.ts +102 -0
- package/src/lib/tools-config.ts +277 -0
- package/src/opencode/bash.ts +7 -2
- package/src/opencode/files.ts +90 -43
- package/src/opencode/glob.ts +7 -1
- package/src/opencode/grep.ts +7 -1
- package/src/opencode/index.ts +66 -21
- package/src/opencode/question.ts +8 -2
- package/src/opencode/todo.ts +8 -2
- package/src/orphan-worker-cleaner.ts +38 -0
- package/src/spawn-agent.ts +160 -175
- package/src/talk/index.ts +92 -66
- package/src/vision-agent.ts +31 -7
- package/src/web/fetch.ts +89 -72
- package/src/web/search.ts +14 -2
- package/src/gh-readonly.ts +0 -10
package/src/opencode/todo.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { type ExtensionAPI, truncateToVisualLines } from "@earendil-works/pi-cod
|
|
|
27
27
|
import { type Static, Type } from "typebox";
|
|
28
28
|
|
|
29
29
|
import { type ToolPendant } from "../lib/pendant.js";
|
|
30
|
+
import type { ToolBus } from "../lib/tool-bus.js";
|
|
31
|
+
import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
|
|
30
32
|
|
|
31
33
|
// ── constants ────────────────────────────────────────────────────────────────
|
|
32
34
|
|
|
@@ -155,8 +157,12 @@ export function buildTodoWidgetLines(todos: readonly TodoInfo[]): string[] | und
|
|
|
155
157
|
|
|
156
158
|
// ── extension ────────────────────────────────────────────────────────────────
|
|
157
159
|
|
|
158
|
-
export default function todowrite(pi: ExtensionAPI) {
|
|
159
|
-
pi
|
|
160
|
+
export default function todowrite(pi: ExtensionAPI): void {
|
|
161
|
+
registerToolsOnSessionStart(pi, registerTodoTool);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function registerTodoTool(bus: ToolBus): void {
|
|
165
|
+
bus.register<typeof todowriteSchema, TodoDetails>({
|
|
160
166
|
name: TOOL_NAME,
|
|
161
167
|
label: "Todo Write",
|
|
162
168
|
description: TODOWRITE_DESCRIPTION,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orphan-worker-cleaner —— 宿主消失时让 pi SDK worker 自行退出
|
|
3
|
+
*
|
|
4
|
+
* VS Code 的 pi 扩展(Pendant)用 fork 启动 dist/pi-sdk-worker.cjs 承载 pi SDK。
|
|
5
|
+
* 宿主进程消失时 worker 不会退出:worker 侧只把 IPC 断连当成可重试的传输错误
|
|
6
|
+
* 上报,既不清理也不自杀,于是留下 ppid=1 的孤儿,连同它拉起的 language server
|
|
7
|
+
* (实测单个 worker 身上挂 4 GiB 上下)一起常驻。
|
|
8
|
+
*
|
|
9
|
+
* 这里只做一件事:定时看一眼自己的父进程还在不在(父进程退出后会被 init 收养,
|
|
10
|
+
* ppid 变成 1),不在就退出。worker 一退,它拉起的 language server 会因为 stdin
|
|
11
|
+
* 关闭自行退出,language server 退出时(process.on("exit") → shutdown())又会
|
|
12
|
+
* kill 掉自己 fork 的 tsserver,整棵树随之消失 —— 所以不需要遍历或清理别的进程。
|
|
13
|
+
*
|
|
14
|
+
* 只对 fork 出来的 SDK worker 生效(process.channel 存在):CLI 直跑、以及 pi
|
|
15
|
+
* 内部 spawn 出来的进程都没有 IPC channel,不在此列,不会被误伤。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
|
|
20
|
+
/** 检查父进程是否还在的间隔 */
|
|
21
|
+
export const DEFAULT_CHECK_INTERVAL_MS = 5_000;
|
|
22
|
+
|
|
23
|
+
export default function orphanWorkerCleaner(pi: ExtensionAPI): void {
|
|
24
|
+
if (process.channel === undefined) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const timer = setInterval(() => {
|
|
28
|
+
if (process.ppid === 1) {
|
|
29
|
+
// eslint-disable-next-line unicorn/no-process-exit -- 宿主已经没了,worker 必须整体退出
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
}, DEFAULT_CHECK_INTERVAL_MS);
|
|
33
|
+
timer.unref();
|
|
34
|
+
|
|
35
|
+
pi.on("session_shutdown", () => {
|
|
36
|
+
clearInterval(timer);
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/spawn-agent.ts
CHANGED
|
@@ -47,9 +47,7 @@
|
|
|
47
47
|
* inherit the user's sandbox.json, whose modes may be relaxed for interactive use.
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
-
import { existsSync } from "node:fs";
|
|
51
50
|
import { join } from "node:path";
|
|
52
|
-
import { fileURLToPath } from "node:url";
|
|
53
51
|
|
|
54
52
|
import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
55
53
|
import {
|
|
@@ -59,9 +57,9 @@ import {
|
|
|
59
57
|
createAgentSession,
|
|
60
58
|
DefaultResourceLoader,
|
|
61
59
|
type ExtensionAPI,
|
|
60
|
+
type ExtensionFactory,
|
|
62
61
|
type ExtensionUIContext,
|
|
63
62
|
getAgentDir,
|
|
64
|
-
type InlineExtension,
|
|
65
63
|
ModelRuntime,
|
|
66
64
|
type PromptOptions,
|
|
67
65
|
SessionManager,
|
|
@@ -72,11 +70,13 @@ import { throttle } from "lodash-es";
|
|
|
72
70
|
import { Type } from "typebox";
|
|
73
71
|
|
|
74
72
|
import { type BwrapConfig, completeBwrapConfig } from "./bwrap/core.js";
|
|
75
|
-
import {
|
|
76
|
-
import { registerShellTools } from "./claude-code/shell.js";
|
|
73
|
+
import { createClaudeCodeFileTools } from "./claude-code/files.js";
|
|
77
74
|
import { type ToolPendant } from "./lib/pendant.js";
|
|
78
|
-
import {
|
|
79
|
-
import
|
|
75
|
+
import type { ToolBus } from "./lib/tool-bus.js";
|
|
76
|
+
import { createToolRegistration, registerToolsOnSessionStart } from "./lib/tool-registration.js";
|
|
77
|
+
import { createToolServices } from "./lib/tool-services.js";
|
|
78
|
+
import { toolsetsForToolNames, unitsForToolNames } from "./lib/tool-units.js";
|
|
79
|
+
import { createOpencodeFileTools } from "./opencode/files.js";
|
|
80
80
|
import {
|
|
81
81
|
type AgentConfig,
|
|
82
82
|
applyAgentDefaults,
|
|
@@ -114,45 +114,16 @@ const SPAWN_AGENT_CONFIG_PATH = join(getAgentDir(), "spawn-agent.json");
|
|
|
114
114
|
const SETTINGS_PATH = join(getAgentDir(), "settings.json");
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* instead of the built-in one.
|
|
117
|
+
* 子代理的工具由 `subagentToolsExtension` 的 inline 工厂注册:它按 frontmatter
|
|
118
|
+
* 声明的工具名从 `TOOL_UNITS` 里挑出覆盖到的单元(见 src/lib/tool-units.ts),
|
|
119
|
+
* 与主入口共用同一张表,因此实现、审批与参数完全一致。
|
|
121
120
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* config — a path-based override has no channel to pass per-agent config
|
|
126
|
-
* (see createSubagentSession).
|
|
127
|
-
* (Workspace write protection is embedded in the opencode write/edit tools.)
|
|
121
|
+
* 不用 `-e` 路径加载的原因:路径加载是「一个模块一份模块图」,子代理里没有
|
|
122
|
+
* 任何东西能看到完整工具集(工具之间互相看不见),而且 per-agent 的沙箱配置
|
|
123
|
+
* 没有注入通道——inline 工厂的闭包是 SDK 提供的唯一注入点。
|
|
128
124
|
*
|
|
129
|
-
*
|
|
130
|
-
* files, so a subagent can enable exactly the tools it declares — e.g. `Grep`
|
|
131
|
-
* without `Glob`. The stateful file tools (`Read`/`Edit`/`Write`) share one
|
|
132
|
-
* implementation file (they share a read-snapshot state); the `tools`
|
|
133
|
-
* allowlist still exposes only the declared subset. The opencode file tools
|
|
134
|
-
* (read/edit/write) likewise share opencode/files.ts (they share the LSP
|
|
135
|
-
* service instance); that file also registers the shared `lsp-rename` and
|
|
136
|
-
* LSP inspect tools, which stay hidden unless a subagent declares them.
|
|
137
|
-
*
|
|
138
|
-
* The lowercase search tools map to the opencode implementations: `grep`
|
|
139
|
-
* overrides pi's built-in grep, and `glob` adds a tool pi has no built-in for
|
|
140
|
-
* (its `find` stays available). Each is a self-contained file registering one
|
|
141
|
-
* tool, so they load independently — `grep` without `glob`.
|
|
125
|
+
* 未声明 `tools` 时子 agent 用 DEFAULT_TOOLS(只读)。
|
|
142
126
|
*/
|
|
143
|
-
const TOOL_EXTENSION_OVERRIDES: Record<string, string> = {
|
|
144
|
-
read: "opencode/files.ts",
|
|
145
|
-
edit: "opencode/files.ts",
|
|
146
|
-
write: "opencode/files.ts",
|
|
147
|
-
grep: "opencode/grep.ts",
|
|
148
|
-
glob: "opencode/glob.ts",
|
|
149
|
-
Grep: "claude-code/grep.ts",
|
|
150
|
-
Glob: "claude-code/glob.ts",
|
|
151
|
-
Read: "claude-code/files.ts",
|
|
152
|
-
Edit: "claude-code/files.ts",
|
|
153
|
-
Write: "claude-code/files.ts",
|
|
154
|
-
};
|
|
155
|
-
|
|
156
127
|
// ── schema ───────────────────────────────────────────────────────────────────
|
|
157
128
|
|
|
158
129
|
const spawnAgentSchema = Type.Object({
|
|
@@ -236,59 +207,58 @@ function formatPendantMarkdown(task: string, response: string): string {
|
|
|
236
207
|
}
|
|
237
208
|
|
|
238
209
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
throw new Error(`Extension file not found: ${abs}`);
|
|
249
|
-
}
|
|
250
|
-
return abs;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Load the override extension for each declared tool (read/edit/write → opencode
|
|
255
|
-
* files.ts, bash → opencode bash.ts, ...), so the subagent uses the enhanced
|
|
256
|
-
* implementation instead of the built-in one. Several tool names can map to
|
|
257
|
-
* the same implementation file (e.g. cc Read/Edit/Write → claude-code/files.ts);
|
|
258
|
-
* loading a file twice would run its extension factory twice and create
|
|
259
|
-
* separate closure states, so each file is loaded at most once.
|
|
210
|
+
* 子代理的工具扩展:一个 inline 工厂里建一份注册上下文与共享服务,只注册被
|
|
211
|
+
* 声明工具覆盖到的单元。
|
|
212
|
+
*
|
|
213
|
+
* 为什么不用 `-e` 路径加载:路径加载是「一个模块一份模块图」,子代理里没有
|
|
214
|
+
* 任何东西能看到完整工具集(工具之间、脚本工具与工具之间互相看不见),而且
|
|
215
|
+
* per-agent 的沙箱配置没有注入通道。inline 工厂是 pi SDK 提供的唯一注入点。
|
|
216
|
+
*
|
|
217
|
+
* 沙箱配置随闭包携带该 agent 的完整配置(frontmatter `sandbox`,已在
|
|
218
|
+
* discoverAgents 补全成 BwrapConfig),未声明时用 SUBAGENT_DEFAULT_SANDBOX。
|
|
260
219
|
*/
|
|
261
|
-
export function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
220
|
+
export function subagentToolsExtension(
|
|
221
|
+
agent: AgentConfig,
|
|
222
|
+
tools: readonly string[],
|
|
223
|
+
): ExtensionFactory {
|
|
224
|
+
return (pi) => {
|
|
225
|
+
const registration = createToolRegistration(pi);
|
|
226
|
+
const services = createToolServices(pi, { sandbox: agent.sandbox ?? SUBAGENT_DEFAULT_SANDBOX });
|
|
227
|
+
const kind = toolsetsForToolNames(tools);
|
|
228
|
+
if (!kind) {
|
|
229
|
+
return;
|
|
268
230
|
}
|
|
269
231
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
232
|
+
const fileToolset =
|
|
233
|
+
kind === "claude-code"
|
|
234
|
+
? createClaudeCodeFileTools(pi, {
|
|
235
|
+
policy: services.policy,
|
|
236
|
+
bus: registration.bus,
|
|
237
|
+
manager: services.manager,
|
|
238
|
+
})
|
|
239
|
+
: createOpencodeFileTools(pi, {
|
|
240
|
+
policy: services.policy,
|
|
241
|
+
bus: registration.bus,
|
|
242
|
+
manager: services.manager,
|
|
243
|
+
});
|
|
244
|
+
services.setLspEnabledHandler((service) => fileToolset.onLspEnabled(registration.bus, service));
|
|
245
|
+
|
|
246
|
+
const units = unitsForToolNames(kind, tools);
|
|
247
|
+
// 工具在 session_start 里注册:禁用规则可以带 models,而子代理的模型在扩展
|
|
248
|
+
// 加载期还读不到。
|
|
249
|
+
registration.onSessionStart((bus, ctx) => {
|
|
250
|
+
fileToolset.restoreReads(ctx);
|
|
251
|
+
for (const unit of units) {
|
|
252
|
+
unit.register({
|
|
253
|
+
pi,
|
|
254
|
+
bus,
|
|
255
|
+
policy: services.policy,
|
|
256
|
+
runtime: services.runtime,
|
|
257
|
+
fileToolset,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
};
|
|
292
262
|
}
|
|
293
263
|
|
|
294
264
|
/**
|
|
@@ -344,8 +314,8 @@ export type SessionFactory = (
|
|
|
344
314
|
/**
|
|
345
315
|
* Create the subagent session via the pi SDK: an in-memory session (no disk
|
|
346
316
|
* session recovery or persistence, same as the old --no-session child), a
|
|
347
|
-
* resource loader
|
|
348
|
-
* (equivalent to --no-extensions
|
|
317
|
+
* resource loader with no extensions except our own inline tool factory
|
|
318
|
+
* (equivalent to --no-extensions), and the parent UI bound directly so
|
|
349
319
|
* subagent extensions show their dialogs in the parent without RPC.
|
|
350
320
|
*/
|
|
351
321
|
export async function createSubagentSession(
|
|
@@ -360,11 +330,7 @@ export async function createSubagentSession(
|
|
|
360
330
|
agentDir: getAgentDir(),
|
|
361
331
|
settingsManager,
|
|
362
332
|
noExtensions: true,
|
|
363
|
-
|
|
364
|
-
extensionFactories: [
|
|
365
|
-
...(tools.includes("bash") ? [subagentShellExtension(agent, opencodeBash)] : []),
|
|
366
|
-
...(tools.includes("Bash") ? [subagentShellExtension(agent, registerShellTools)] : []),
|
|
367
|
-
],
|
|
333
|
+
extensionFactories: [subagentToolsExtension(agent, tools)],
|
|
368
334
|
appendSystemPrompt: agent.systemPrompt ? [agent.systemPrompt] : undefined,
|
|
369
335
|
});
|
|
370
336
|
await loader.reload();
|
|
@@ -543,16 +509,8 @@ export function formatAgentListSection(agents: AgentConfig[]): string {
|
|
|
543
509
|
|
|
544
510
|
// ── extension ────────────────────────────────────────────────────────────────
|
|
545
511
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// bwrap 沙箱、信号处理),不做 Windows 适配。
|
|
549
|
-
if (process.platform === "win32") {
|
|
550
|
-
pi.on("session_start", (_event, ctx) => {
|
|
551
|
-
ctx.ui.notify("spawn-agent is disabled on Windows.", "warning");
|
|
552
|
-
});
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
|
|
512
|
+
/** spawn-agent 工具集:入口从它取注册函数。 */
|
|
513
|
+
export function createSpawnAgentTool(): { register(bus: ToolBus): void } {
|
|
556
514
|
// Discover the available subagent types once at extension startup. The
|
|
557
515
|
// extension owns this discovery: the model never has to guess agent names
|
|
558
516
|
// or read the agent directory itself. Editing ~/.pi/agent/agents/*.md or
|
|
@@ -563,74 +521,101 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
563
521
|
);
|
|
564
522
|
const agentListSection = agents.length > 0 ? formatAgentListSection(agents) : null;
|
|
565
523
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
{
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
524
|
+
return {
|
|
525
|
+
register(bus) {
|
|
526
|
+
bus.register<typeof spawnAgentSchema, SubagentDetails>({
|
|
527
|
+
name: "spawn-agent",
|
|
528
|
+
label: "spawn-agent",
|
|
529
|
+
description: [
|
|
530
|
+
"Delegate a task to a subagent that runs in an isolated session with its own context window, inside this pi process rather than a separate one.",
|
|
531
|
+
"The call blocks until the subagent finishes its turn; its final output comes back as the tool result.",
|
|
532
|
+
"The `agent` parameter must be one of the available subagent types listed in the system prompt.",
|
|
533
|
+
`Subagents run read-only (${DEFAULT_TOOLS.join(", ")}) unless the agent declares an explicit toolset.`,
|
|
534
|
+
].join(" "),
|
|
535
|
+
promptGuidelines: agentListSection ? [agentListSection] : undefined,
|
|
536
|
+
parameters: spawnAgentSchema,
|
|
537
|
+
|
|
538
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
539
|
+
const agent = agents.find((a) => a.name === params.agent);
|
|
540
|
+
if (!agent) {
|
|
541
|
+
return {
|
|
542
|
+
content: [
|
|
543
|
+
{
|
|
544
|
+
type: "text",
|
|
545
|
+
text: `Unknown agent "${params.agent}". Available agents: ${formatAgentList(agents)}`,
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
details: {},
|
|
549
|
+
isError: true,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
592
552
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
553
|
+
const result = await runAgent(
|
|
554
|
+
agent,
|
|
555
|
+
params.task,
|
|
556
|
+
ctx.cwd,
|
|
557
|
+
signal,
|
|
558
|
+
onUpdate,
|
|
559
|
+
ctx.hasUI ? ctx.ui : undefined,
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
const isError =
|
|
563
|
+
result.exitCode !== 0 ||
|
|
564
|
+
result.stopReason === "error" ||
|
|
565
|
+
result.stopReason === "aborted";
|
|
566
|
+
if (isError) {
|
|
567
|
+
const { reason, message } = formatSubagentError(result);
|
|
568
|
+
return {
|
|
569
|
+
content: [
|
|
570
|
+
{
|
|
571
|
+
type: "text",
|
|
572
|
+
text: `Subagent "${result.agent}" failed (${reason}):\n${message}`,
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
details: {
|
|
576
|
+
pendant: {
|
|
577
|
+
subtitle: result.agent,
|
|
578
|
+
markdown: formatPendantMarkdown(params.task, message),
|
|
579
|
+
} satisfies ToolPendant,
|
|
580
|
+
},
|
|
581
|
+
isError: true,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
619
584
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
585
|
+
const output = getFinalOutput(result.messages) || "(no output)";
|
|
586
|
+
const truncation = truncateTail(output, { maxBytes: MAX_OUTPUT_BYTES });
|
|
587
|
+
const text = truncation.truncated
|
|
588
|
+
? `${truncation.content}\n\n[Output truncated to ${formatTokens(truncation.content.length)} bytes.]`
|
|
589
|
+
: output;
|
|
590
|
+
return {
|
|
591
|
+
content: [{ type: "text", text }],
|
|
592
|
+
details: {
|
|
593
|
+
pendant: {
|
|
594
|
+
subtitle: result.agent,
|
|
595
|
+
markdown: formatPendantMarkdown(params.task, text),
|
|
596
|
+
} satisfies ToolPendant,
|
|
597
|
+
},
|
|
598
|
+
};
|
|
632
599
|
},
|
|
633
|
-
};
|
|
600
|
+
});
|
|
634
601
|
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export default function spawnAgent(pi: ExtensionAPI): void {
|
|
606
|
+
// Windows 上禁用:子代理的工具集依赖 POSIX 设施(opencode bash 的
|
|
607
|
+
// bwrap 沙箱、信号处理),不做 Windows 适配。
|
|
608
|
+
if (process.platform === "win32") {
|
|
609
|
+
pi.on("session_start", (_event, ctx) => {
|
|
610
|
+
ctx.ui.notify("spawn-agent is disabled on Windows.", "warning");
|
|
611
|
+
});
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const tools = createSpawnAgentTool();
|
|
616
|
+
// 工具在 session_start 里注册:禁用规则可以带 models,只有那时才知道本会话
|
|
617
|
+
// 的模型。
|
|
618
|
+
registerToolsOnSessionStart(pi, (bus) => {
|
|
619
|
+
tools.register(bus);
|
|
635
620
|
});
|
|
636
621
|
}
|
package/src/talk/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { type TObject, Type } from "typebox";
|
|
|
24
24
|
|
|
25
25
|
import { type CommandResult, type CommandSpec, parseCommand } from "../lib/cli.js";
|
|
26
26
|
import { resolveHomePath } from "../lib/path.js";
|
|
27
|
+
import type { ToolBus } from "../lib/tool-bus.js";
|
|
28
|
+
import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
|
|
27
29
|
import { restoreTalkAgentId, TALK_JOIN_ENTRY_TYPE, TalkCore } from "./core.js";
|
|
28
30
|
import { age, formatDelivery } from "./format.js";
|
|
29
31
|
import type { Letter } from "./mailbox.js";
|
|
@@ -97,7 +99,83 @@ function readTalkSettings(): { dbPath?: string; deliver?: "steer" | "queue" } {
|
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
/** talk 的三个工具:由扩展入口在 session_start 里注册。 */
|
|
103
|
+
function registerTalkTools(
|
|
104
|
+
bus: ToolBus,
|
|
105
|
+
core: TalkCore,
|
|
106
|
+
requireInit: () => string | undefined,
|
|
107
|
+
): void {
|
|
108
|
+
bus.register({
|
|
109
|
+
name: "talk-list-agents",
|
|
110
|
+
label: "List Talk Agents",
|
|
111
|
+
description: "List visible pi agents (id, status, work_dir, name).",
|
|
112
|
+
promptSnippet: "List other pi agents on this machine",
|
|
113
|
+
parameters: Type.Object({
|
|
114
|
+
cwd: Type.Optional(Type.String({ description: "Only list agents in this directory" })),
|
|
115
|
+
}),
|
|
116
|
+
async execute(_toolCallId, params) {
|
|
117
|
+
const initError = requireInit();
|
|
118
|
+
if (initError) {
|
|
119
|
+
return toolResult(initError);
|
|
120
|
+
}
|
|
121
|
+
return toolResult(params.cwd ? await core.listCwd(params.cwd) : await core.list());
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
bus.register({
|
|
126
|
+
name: "talk-ask",
|
|
127
|
+
label: "Ask Talk",
|
|
128
|
+
description:
|
|
129
|
+
"Ask another pi agent a question and block until it responds (or times out). Before asking, it checks whether that agent already sent you something; if so, you are told to read and respond first instead of asking.",
|
|
130
|
+
promptSnippet: "Ask another pi agent a question and wait for a response",
|
|
131
|
+
parameters: Type.Object({
|
|
132
|
+
to: Type.String({ description: "Target agent id (from talk-list-agents)" }),
|
|
133
|
+
message: Type.String({ description: "The question" }),
|
|
134
|
+
timeoutMs: Type.Optional(
|
|
135
|
+
Type.Number({ description: `Wait cap in ms; default ${ASK_TIMEOUT_MS}` }),
|
|
136
|
+
),
|
|
137
|
+
}),
|
|
138
|
+
async execute(_toolCallId, params, signal) {
|
|
139
|
+
const initError = requireInit();
|
|
140
|
+
if (initError) {
|
|
141
|
+
return toolResult(initError);
|
|
142
|
+
}
|
|
143
|
+
return toolResult(
|
|
144
|
+
await core.ask(
|
|
145
|
+
params.to,
|
|
146
|
+
params.message,
|
|
147
|
+
params.timeoutMs ?? ASK_TIMEOUT_MS,
|
|
148
|
+
signal ?? undefined,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
bus.register({
|
|
155
|
+
name: "talk-send",
|
|
156
|
+
label: "Send Talk Message",
|
|
157
|
+
description:
|
|
158
|
+
"Send a plain-text message to a single pi agent. Plain text only, ≤32KB — send a summary and a path, never file contents.",
|
|
159
|
+
promptSnippet: "Send a message to another pi agent",
|
|
160
|
+
parameters: Type.Object({
|
|
161
|
+
to: Type.String({ description: "Target agent id (from talk-list-agents)" }),
|
|
162
|
+
message: Type.String({ description: "Message body" }),
|
|
163
|
+
}),
|
|
164
|
+
async execute(_toolCallId, params) {
|
|
165
|
+
const initError = requireInit();
|
|
166
|
+
if (initError) {
|
|
167
|
+
return toolResult(initError);
|
|
168
|
+
}
|
|
169
|
+
return toolResult(await core.send(params.to, params.message));
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* talk 工具集:创建核心、注册会话接线与 /talk 命令,返回工具注册函数。
|
|
176
|
+
* 入口与独立默认导出都用它,因此两种加载方式行为一致。
|
|
177
|
+
*/
|
|
178
|
+
export function createTalkTools(pi: ExtensionAPI): { register(bus: ToolBus): void } {
|
|
101
179
|
const settings = readTalkSettings();
|
|
102
180
|
const configured = process.env.PI_TALK_DB ?? settings.dbPath;
|
|
103
181
|
const dbPath = configured
|
|
@@ -220,71 +298,8 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
220
298
|
});
|
|
221
299
|
|
|
222
300
|
// ── Tools ──────────────────────────────────────────────────────────────
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
name: "talk-list-agents",
|
|
226
|
-
label: "List Talk Agents",
|
|
227
|
-
description: "List visible pi agents (id, status, work_dir, name).",
|
|
228
|
-
promptSnippet: "List other pi agents on this machine",
|
|
229
|
-
parameters: Type.Object({
|
|
230
|
-
cwd: Type.Optional(Type.String({ description: "Only list agents in this directory" })),
|
|
231
|
-
}),
|
|
232
|
-
async execute(_toolCallId, params) {
|
|
233
|
-
const initError = requireInit();
|
|
234
|
-
if (initError) {
|
|
235
|
-
return toolResult(initError);
|
|
236
|
-
}
|
|
237
|
-
return toolResult(params.cwd ? await core.listCwd(params.cwd) : await core.list());
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
pi.registerTool({
|
|
242
|
-
name: "talk-ask",
|
|
243
|
-
label: "Ask Talk",
|
|
244
|
-
description:
|
|
245
|
-
"Ask another pi agent a question and block until it responds (or times out). Before asking, it checks whether that agent already sent you something; if so, you are told to read and respond first instead of asking.",
|
|
246
|
-
promptSnippet: "Ask another pi agent a question and wait for a response",
|
|
247
|
-
parameters: Type.Object({
|
|
248
|
-
to: Type.String({ description: "Target agent id (from talk-list-agents)" }),
|
|
249
|
-
message: Type.String({ description: "The question" }),
|
|
250
|
-
timeoutMs: Type.Optional(
|
|
251
|
-
Type.Number({ description: `Wait cap in ms; default ${ASK_TIMEOUT_MS}` }),
|
|
252
|
-
),
|
|
253
|
-
}),
|
|
254
|
-
async execute(_toolCallId, params, signal) {
|
|
255
|
-
const initError = requireInit();
|
|
256
|
-
if (initError) {
|
|
257
|
-
return toolResult(initError);
|
|
258
|
-
}
|
|
259
|
-
return toolResult(
|
|
260
|
-
await core.ask(
|
|
261
|
-
params.to,
|
|
262
|
-
params.message,
|
|
263
|
-
params.timeoutMs ?? ASK_TIMEOUT_MS,
|
|
264
|
-
signal ?? undefined,
|
|
265
|
-
),
|
|
266
|
-
);
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
pi.registerTool({
|
|
271
|
-
name: "talk-send",
|
|
272
|
-
label: "Send Talk Message",
|
|
273
|
-
description:
|
|
274
|
-
"Send a plain-text message to a single pi agent. Plain text only, ≤32KB — send a summary and a path, never file contents.",
|
|
275
|
-
promptSnippet: "Send a message to another pi agent",
|
|
276
|
-
parameters: Type.Object({
|
|
277
|
-
to: Type.String({ description: "Target agent id (from talk-list-agents)" }),
|
|
278
|
-
message: Type.String({ description: "Message body" }),
|
|
279
|
-
}),
|
|
280
|
-
async execute(_toolCallId, params) {
|
|
281
|
-
const initError = requireInit();
|
|
282
|
-
if (initError) {
|
|
283
|
-
return toolResult(initError);
|
|
284
|
-
}
|
|
285
|
-
return toolResult(await core.send(params.to, params.message));
|
|
286
|
-
},
|
|
287
|
-
});
|
|
301
|
+
// 工具在 session_start 里注册:禁用规则可以带 models,只有那时才知道本会话
|
|
302
|
+
// 的模型。
|
|
288
303
|
|
|
289
304
|
// ── /talk commands ────────────────────────────────────────────────────
|
|
290
305
|
|
|
@@ -526,4 +541,15 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
526
541
|
};
|
|
527
542
|
});
|
|
528
543
|
}
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
register(bus) {
|
|
547
|
+
registerTalkTools(bus, core, requireInit);
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export default function talk(pi: ExtensionAPI): void {
|
|
553
|
+
const tools = createTalkTools(pi);
|
|
554
|
+
registerToolsOnSessionStart(pi, (bus) => tools.register(bus));
|
|
529
555
|
}
|