@trim21/personal-pi-extensions 0.1.674 → 0.1.679

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 CHANGED
@@ -31,9 +31,9 @@
31
31
 
32
32
  `codemode` 让模型写一段 JavaScript(作为 async 函数体,`await` 与 `return` 都可用),在
33
33
  进程内的 worker 线程里用 QuickJS wasm VM 执行。VM 里没有 node、文件系统、网络、timer
34
- 或模块加载,脚本唯一的出口是 `tools.<name>(args)`。
34
+ 或模块加载,脚本唯一的出口是 `call(name, args)`。
35
35
 
36
- - **嵌套调用经工具总线**:`tools.Read({ file_path })` 最终执行的是 `Read` 工具自己的
36
+ - **嵌套调用经工具总线**:`call("Read", { file_path })` 最终执行的是 `Read` 工具自己的
37
37
  `execute`,所以工具的审批照常生效(写工作区外会弹 write-guard,Bash 沙箱外执行会弹
38
38
  自己的提权确认)。codemode 不额外加确认层:脚本里连发十次写操作就是十次工具自己的
39
39
  审批(需要审批的那些)。
@@ -42,10 +42,16 @@
42
42
  子代理的工具白名单都同样约束脚本。
43
43
  - **只有脚本输出进上下文**:`text(value)` / `console.log(...)` 与 `return` 值进入工具结果,
44
44
  中间的工具调用与它们的返回内容不会(也不在会话记录里留下工具调用条目)。
45
- - **脚本接口**:`tools` / `ALL_TOOLS` / `text` / `image` / `exit` / `console.*` /
46
- `store.set` / `store.get` / `store.list`(会话内持久的键值表);首行可选
45
+ - **返回值**:声明了 `structuredSchema` 的工具(如 gh-readonly 的读类工具)把结果放在
46
+ `structuredResult` 里,`call()` 解包成对象给脚本;`{ ok: false, error }` 会 reject 成
47
+ `CallFailedError`(脚本可按 `instanceof CallFailedError` 区分工具失败与自身运行期错误)。
48
+ 没有声明输出结构的工具回退成工具输出的文本。
49
+ - **脚本接口**:`call` / `CallFailedError` / `ALL_TOOLS` / `text` / `image` / `exit` /
50
+ `console.*` / `store.set` / `store.get` / `store.list`(会话内持久的键值表);首行可选
47
51
  `// @options: {"max_output_tokens": 10000}`。
48
52
  脚本没有超时:死循环由调用方中止(Esc)结束,等嵌套调用返回(含用户审批弹窗)多久都不算超时。
53
+ - **工具描述**里给出每个可调用工具的 `declare function call(name, args): Promise<T>` 重载,
54
+ 参数与返回类型都取自工具自己的 schema,所以模型在写脚本前就知道返回值形状。
49
55
  - **store** 记在每次成功调用工具结果的 `details.store` 上(与 `src/lib/file-reads.ts` 的
50
56
  已读记账同一套做法),下一次调用从当前分支的 toolResult 重放;输出超过 `max_output_tokens`
51
57
  时头尾截断并把全文落到 `$TMPDIR/pi-codemode-*.txt`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.674",
3
+ "version": "0.1.679",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -83,7 +83,7 @@
83
83
  "prettier --write"
84
84
  ]
85
85
  },
86
- "packageManager": "pnpm@12.8.1",
86
+ "packageManager": "pnpm@11.28.3",
87
87
  "engines": {
88
88
  "node": ">=24.14"
89
89
  },
@@ -1,9 +1,9 @@
1
1
  /**
2
- * 把工具的参数 schema 渲染成脚本侧的 TypeScript 声明:写进 codemode 的工具描述里,
3
- * 模型据此知道脚本里能写 `await tools.Read({ file_path: "..." })`。
2
+ * 把工具声明渲染成 codemode 脚本侧的 TypeScript:写进 codemode 的工具描述里,模型据此
3
+ * 知道脚本里能写 `await call("Read", { file_path: "..." })`,以及每个工具返回什么。
4
4
  *
5
5
  * 只处理本仓库工具实际用到的形状(object / array / string / number / boolean /
6
- * union / enum),其余退化成 `unknown`——宁可少给类型,也不要编出错类型。
6
+ * const / union / enum),其余退化成 `unknown`——宁可少给类型,也不要编出错类型。
7
7
  */
8
8
 
9
9
  import type { ScriptTool } from "./protocol.js";
@@ -12,6 +12,7 @@ interface ToolLike {
12
12
  name: string;
13
13
  description?: string;
14
14
  parameters?: unknown;
15
+ structuredSchema?: unknown;
15
16
  }
16
17
 
17
18
  type JsonSchema = Record<string, unknown>;
@@ -24,7 +25,7 @@ function literal(value: unknown): string {
24
25
  return typeof value === "string" ? JSON.stringify(value) : String(value);
25
26
  }
26
27
 
27
- /** 工具名作为属性访问:非法标识符用引号形式。 */
28
+ /** 对象成员名:非法标识符用引号形式。 */
28
29
  function propertyName(name: string): string {
29
30
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
30
31
  }
@@ -33,6 +34,10 @@ function renderType(schema: unknown, indent: string): string {
33
34
  if (!isSchema(schema)) {
34
35
  return "unknown";
35
36
  }
37
+ // TypeBox 的 Type.Literal / StringEnum 产出 const,而不是 enum
38
+ if ("const" in schema) {
39
+ return literal(schema.const);
40
+ }
36
41
  if (Array.isArray(schema.enum) && schema.enum.length > 0) {
37
42
  return schema.enum.map((value) => literal(value)).join(" | ");
38
43
  }
@@ -76,24 +81,35 @@ function renderType(schema: unknown, indent: string): string {
76
81
  }
77
82
  }
78
83
 
79
- /** 单个工具在脚本里的签名(`tools` 的一个成员)。 */
80
- function renderToolMember(tool: ToolLike): string {
84
+ /**
85
+ * 单个工具在脚本里的调用重载:`declare function call(name: "X", args: T): Promise<R>;`。
86
+ * 返回类型取自工具声明的输出结构,未声明就是文本(`string`)。
87
+ */
88
+ function renderOverload(tool: ToolLike): string {
81
89
  const summary = tool.description?.split("\n", 1)[0]?.trim();
82
- const doc = summary ? `/** ${summary} */\n ` : "";
83
- return `${doc}${propertyName(tool.name)}(args: ${renderType(tool.parameters, " ")}): Promise<unknown>;`;
90
+ const doc = summary ? `/** ${summary} */\n` : "";
91
+ const output =
92
+ tool.structuredSchema === undefined ? "string" : renderType(tool.structuredSchema, "");
93
+ return `${doc}declare function call(name: ${JSON.stringify(tool.name)}, args: ${renderType(tool.parameters, "")}): Promise<${output}>;`;
84
94
  }
85
95
 
86
96
  /** 给脚本用的工具清单(名字 + 说明)。 */
87
97
  export function toScriptTools(tools: readonly ToolLike[]): ScriptTool[] {
88
- return tools.map((tool) => ({ name: tool.name, description: tool.description }));
98
+ return tools.map((tool) => ({
99
+ name: tool.name,
100
+ description: tool.description,
101
+ structuredSchema: tool.structuredSchema,
102
+ }));
89
103
  }
90
104
 
91
- /** 渲染脚本侧的声明:`declare const tools: {...}` 与全局辅助函数。 */
105
+ /** 渲染脚本侧的声明:每个工具一条 `call` 重载,外加 `CallFailedError` 与全局辅助函数。 */
92
106
  export function renderDeclarations(tools: readonly ToolLike[]): string {
93
- const members = tools.map((tool) => ` ${renderToolMember(tool)}`).join("\n");
94
107
  return [
95
- `declare const tools: {\n${members}\n};`,
108
+ ...tools.map((tool) => renderOverload(tool)),
109
+ // 动态名字的兜底重载,必须放最后
110
+ "declare function call(name: string, args?: unknown): Promise<unknown>;",
96
111
  "declare const ALL_TOOLS: Array<{ name: string; description?: string }>;",
112
+ 'declare class CallFailedError extends Error { readonly name: "CallFailedError"; }',
97
113
  "declare function text(value: unknown): void;",
98
114
  "declare function image(value: unknown): void;",
99
115
  "declare function exit(): void;",
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * codemode 脚本侧的 prelude:在 QuickJS VM 里先于脚本求值,构建脚本能看到的全部
3
- * 能力(`tools` / `ALL_TOOLS` / `text` / `image` / `exit` / `console` / `store`),
4
- * 并把宿主桥接封在闭包里——脚本拿不到 `bridge` 本身。
3
+ * 能力(`call` / `CallFailedError` / `ALL_TOOLS` / `text` / `image` / `exit` /
4
+ * `console` / `store`),并把宿主桥接封在闭包里——脚本拿不到 `bridge` 本身。
5
5
  *
6
6
  * 值与参数过桥时都是 JSON 文本,本侧负责 parse/stringify;异步调用用一个 pending
7
- * 表把 id 映射到 promise,由宿主在结果到达时 settle。
7
+ * 表把 id 映射到 promise,由宿主在结果到达时 settle。每次嵌套调用失败都由宿主以
8
+ * `ok: false` 回报,本侧统一 reject 成 `CallFailedError`(脚本可以按 instanceof 区分
9
+ * 「工具失败」与自己的运行期错误)。
8
10
  *
9
11
  * 求值结果是一个函数 `(bridge, toolsJson, storeJson) => { settle, run, stalled }`。
10
12
  * `bridge(kind, a, b, c)`:
@@ -28,6 +30,13 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
28
30
  const promiseThen = Promise.prototype.then;
29
31
  const ErrorCtor = Error;
30
32
  const TypeErrorCtor = TypeError;
33
+ // 嵌套调用失败统一用它 reject:脚本能按 instanceof 区分「工具失败」与自身运行期错误
34
+ class CallFailedError extends ErrorCtor {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = "CallFailedError";
38
+ }
39
+ }
31
40
  const pending = new Map();
32
41
  let nextId = 1;
33
42
  let finished = false;
@@ -89,20 +98,26 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
89
98
  });
90
99
  }
91
100
 
92
- const tools = Object.create(null);
101
+ const callers = new Map();
93
102
  const allTools = [];
94
- for (const { name, jsName, description } of parse(toolsJson)) {
95
- const fn = caller(name);
96
- // 两个名字归一化成同一个标识符时,第一个赢
97
- if (!(jsName in tools)) {
98
- tools[jsName] = fn;
99
- allTools.push(Object.freeze({ name: jsName, description }));
100
- }
101
- if (!(name in tools)) tools[name] = fn;
103
+ for (const { name, description } of parse(toolsJson)) {
104
+ if (callers.has(name)) continue;
105
+ callers.set(name, caller(name));
106
+ allTools.push(Object.freeze({ name, description }));
102
107
  }
103
- Object.freeze(tools);
104
108
  Object.freeze(allTools);
105
109
 
110
+ function call(name, args) {
111
+ const fn = callers.get(name);
112
+ if (fn === undefined) {
113
+ return Promise.reject(
114
+ new CallFailedError('Tool "' + String(name) + '" is not available in codemode.'),
115
+ );
116
+ }
117
+ return fn(args);
118
+ }
119
+ Object.freeze(call);
120
+
106
121
  // key -> JSON 文本;容量按 key 与 JSON 的字符数计
107
122
  const stored = new Map();
108
123
  const writes = new Map();
@@ -242,7 +257,8 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
242
257
  }
243
258
  Object.freeze(console);
244
259
 
245
- Object.defineProperty(globalThis, "tools", { value: tools, enumerable: true });
260
+ Object.defineProperty(globalThis, "call", { value: call, enumerable: true });
261
+ Object.defineProperty(globalThis, "CallFailedError", { value: CallFailedError, enumerable: true });
246
262
  Object.defineProperty(globalThis, "ALL_TOOLS", { value: allTools, enumerable: true });
247
263
  Object.defineProperty(globalThis, "console", { value: console, enumerable: true });
248
264
  Object.defineProperty(globalThis, "text", { value: text, enumerable: true });
@@ -255,7 +271,7 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
255
271
  if (!entry) return;
256
272
  pending.delete(id);
257
273
  if (!ok) {
258
- entry.reject(new ErrorCtor(payload));
274
+ entry.reject(new CallFailedError(payload));
259
275
  return;
260
276
  }
261
277
  let value;
@@ -270,7 +286,7 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
270
286
  run(fn) {
271
287
  let promise;
272
288
  try {
273
- promise = fn(tools, console);
289
+ promise = fn();
274
290
  } catch (error) {
275
291
  done(false, describeError(error));
276
292
  return;
@@ -23,6 +23,8 @@ const outputItemSchema = Type.Union([
23
23
  const toolDeclSchema = Type.Object({
24
24
  name: Type.String(),
25
25
  description: Type.Optional(Type.String()),
26
+ /** 工具的 structuredSchema(TypeBox schema):脚本侧据此渲染 call() 的返回类型。 */
27
+ structuredSchema: Type.Optional(Type.Unknown()),
26
28
  });
27
29
 
28
30
  const startSchema = Type.Object({
@@ -54,7 +56,7 @@ const callSchema = Type.Object({
54
56
  args: Type.Unknown(),
55
57
  });
56
58
 
57
- const outputSchema = Type.Object({
59
+ const outputFrameSchema = Type.Object({
58
60
  t: Type.Literal("output"),
59
61
  items: Type.Array(outputItemSchema),
60
62
  });
@@ -87,7 +89,7 @@ const doneSchema = Type.Union([
87
89
  ]);
88
90
 
89
91
  const hostMessageSchema = Type.Union([startSchema, resultSchema]);
90
- const workerMessageSchema = Type.Union([callSchema, outputSchema, doneSchema]);
92
+ const workerMessageSchema = Type.Union([callSchema, outputFrameSchema, doneSchema]);
91
93
 
92
94
  /** worker 启动数据:注册时编译好的 wasm 模块(结构化克隆可以带它跨线程)。 */
93
95
  export interface WorkerBootstrap {
@@ -117,6 +119,7 @@ export interface StoreWrites {
117
119
  export interface ScriptTool {
118
120
  name: string;
119
121
  description?: string;
122
+ structuredSchema?: unknown;
120
123
  }
121
124
 
122
125
  export type HostMessage =
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * codemode 工具:模型写一段 JavaScript,脚本在 QuickJS VM(worker 线程)里执行,脚本唯一
3
- * 的能力是调用 `tools.*`——每个嵌套调用都由主线程经本仓库的工具总线执行,因此工具实现
3
+ * 的能力是调用 `call(name, args)`——每个嵌套调用都由主线程经本仓库的工具总线执行,因此工具实现
4
4
  * 内部的审批(工作区外写入、Bash 沙箱提权等)照常生效;codemode 不再加自己的确认层。
5
5
  *
6
6
  * 可调用集合:总线上实际注册的工具减去 codemode 自身与 spawn-agent,执行时再与 active
@@ -45,6 +45,7 @@ interface CallableTool {
45
45
  name: string;
46
46
  description?: string;
47
47
  parameters?: unknown;
48
+ structuredSchema?: unknown;
48
49
  }
49
50
 
50
51
  /** 只在 pi 有 active 工具概念时才求交(子代理、`--tools` 等场景)。 */
@@ -66,6 +67,7 @@ function collectTools(bus: ToolBus, allowed: Set<string> | undefined): CallableT
66
67
  name: definition.name,
67
68
  description: definition.description,
68
69
  parameters: definition.parameters,
70
+ structuredSchema: definition.structuredSchema,
69
71
  }));
70
72
  }
71
73
 
@@ -73,8 +75,8 @@ function buildDescription(tools: readonly CallableTool[]): string {
73
75
  return [
74
76
  "Run JavaScript code that orchestrates tool calls in a QuickJS sandbox.",
75
77
  "- The code is the body of an async function: top-level `await` and `return` both work.",
76
- "- Call tools with `await tools.<name>(args)`. Arguments and results make a JSON round trip,",
77
- " and a failing tool rejects with an `Error` you can catch.",
78
+ "- Call tools with `await call(name, args)`. Arguments and results make a JSON round trip,",
79
+ " and a failing tool rejects with a `CallFailedError` you can catch.",
78
80
  "- Only what the script passes to `text(value)` / `console.log(...)` and its `return` value enter",
79
81
  " this conversation; nested calls and their results stay out of it.",
80
82
  "- `store.set(key, value)`, `store.get(key)` and `store.list()` are a small key/value store that",
@@ -297,6 +299,11 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
297
299
  return { ok: false, error: `Tool "${name}" is not available in codemode.` };
298
300
  }
299
301
  const result = await bus.executeTool(name, args, { ctx, signal });
302
+ if (result.structuredResult) {
303
+ return result.structuredResult.ok
304
+ ? { ok: true, value: result.structuredResult.value }
305
+ : { ok: false, error: result.structuredResult.error };
306
+ }
300
307
  const text = toolResultText(result);
301
308
  if (result.isError) {
302
309
  return { ok: false, error: text };
@@ -13,6 +13,13 @@ var PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson) {
13
13
  const promiseThen = Promise.prototype.then;
14
14
  const ErrorCtor = Error;
15
15
  const TypeErrorCtor = TypeError;
16
+ // 嵌套调用失败统一用它 reject:脚本能按 instanceof 区分「工具失败」与自身运行期错误
17
+ class CallFailedError extends ErrorCtor {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "CallFailedError";
21
+ }
22
+ }
16
23
  const pending = new Map();
17
24
  let nextId = 1;
18
25
  let finished = false;
@@ -74,20 +81,26 @@ var PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson) {
74
81
  });
75
82
  }
76
83
 
77
- const tools = Object.create(null);
84
+ const callers = new Map();
78
85
  const allTools = [];
79
- for (const { name, jsName, description } of parse(toolsJson)) {
80
- const fn = caller(name);
81
- // 两个名字归一化成同一个标识符时,第一个赢
82
- if (!(jsName in tools)) {
83
- tools[jsName] = fn;
84
- allTools.push(Object.freeze({ name: jsName, description }));
85
- }
86
- if (!(name in tools)) tools[name] = fn;
86
+ for (const { name, description } of parse(toolsJson)) {
87
+ if (callers.has(name)) continue;
88
+ callers.set(name, caller(name));
89
+ allTools.push(Object.freeze({ name, description }));
87
90
  }
88
- Object.freeze(tools);
89
91
  Object.freeze(allTools);
90
92
 
93
+ function call(name, args) {
94
+ const fn = callers.get(name);
95
+ if (fn === undefined) {
96
+ return Promise.reject(
97
+ new CallFailedError('Tool "' + String(name) + '" is not available in codemode.'),
98
+ );
99
+ }
100
+ return fn(args);
101
+ }
102
+ Object.freeze(call);
103
+
91
104
  // key -> JSON 文本;容量按 key 与 JSON 的字符数计
92
105
  const stored = new Map();
93
106
  const writes = new Map();
@@ -227,7 +240,8 @@ var PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson) {
227
240
  }
228
241
  Object.freeze(console);
229
242
 
230
- Object.defineProperty(globalThis, "tools", { value: tools, enumerable: true });
243
+ Object.defineProperty(globalThis, "call", { value: call, enumerable: true });
244
+ Object.defineProperty(globalThis, "CallFailedError", { value: CallFailedError, enumerable: true });
231
245
  Object.defineProperty(globalThis, "ALL_TOOLS", { value: allTools, enumerable: true });
232
246
  Object.defineProperty(globalThis, "console", { value: console, enumerable: true });
233
247
  Object.defineProperty(globalThis, "text", { value: text, enumerable: true });
@@ -240,7 +254,7 @@ var PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson) {
240
254
  if (!entry) return;
241
255
  pending.delete(id);
242
256
  if (!ok) {
243
- entry.reject(new ErrorCtor(payload));
257
+ entry.reject(new CallFailedError(payload));
244
258
  return;
245
259
  }
246
260
  let value;
@@ -255,7 +269,7 @@ var PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson) {
255
269
  run(fn) {
256
270
  let promise;
257
271
  try {
258
- promise = fn(tools, console);
272
+ promise = fn();
259
273
  } catch (error) {
260
274
  done(false, describeError(error));
261
275
  return;
@@ -301,7 +315,9 @@ var outputItemSchema = Type.Union([
301
315
  ]);
302
316
  var toolDeclSchema = Type.Object({
303
317
  name: Type.String(),
304
- description: Type.Optional(Type.String())
318
+ description: Type.Optional(Type.String()),
319
+ /** 工具的 structuredSchema(TypeBox schema):脚本侧据此渲染 call() 的返回类型。 */
320
+ structuredSchema: Type.Optional(Type.Unknown())
305
321
  });
306
322
  var startSchema = Type.Object({
307
323
  t: Type.Literal("start"),
@@ -329,7 +345,7 @@ var callSchema = Type.Object({
329
345
  name: Type.String(),
330
346
  args: Type.Unknown()
331
347
  });
332
- var outputSchema = Type.Object({
348
+ var outputFrameSchema = Type.Object({
333
349
  t: Type.Literal("output"),
334
350
  items: Type.Array(outputItemSchema)
335
351
  });
@@ -358,7 +374,7 @@ var doneSchema = Type.Union([
358
374
  })
359
375
  ]);
360
376
  var hostMessageSchema = Type.Union([startSchema, resultSchema]);
361
- var workerMessageSchema = Type.Union([callSchema, outputSchema, doneSchema]);
377
+ var workerMessageSchema = Type.Union([callSchema, outputFrameSchema, doneSchema]);
362
378
  function decode(schema, value) {
363
379
  if (!Value.Check(schema, value)) {
364
380
  const preview = JSON.stringify(value).slice(0, 200);
@@ -372,9 +388,6 @@ function decodeHostMessage(value) {
372
388
 
373
389
  // src/codemode/worker.ts
374
390
  var MEMORY_LIMIT_BYTES = 512 * 1024 * 1024;
375
- function toScriptIdentifier(name) {
376
- return name.replaceAll(/[^A-Za-z0-9_$]/g, "_").replaceAll(/^\d/g, "_");
377
- }
378
391
  function discardOutput(memory) {
379
392
  return {
380
393
  fd_write(_fd, iovsPtr, iovsLen, nwrittenPtr) {
@@ -493,8 +506,8 @@ async function runScript(wasm, start) {
493
506
  JSON.stringify(
494
507
  start.tools.map((tool) => ({
495
508
  name: tool.name,
496
- jsName: toScriptIdentifier(tool.name),
497
- description: tool.description
509
+ description: tool.description,
510
+ structuredSchema: tool.structuredSchema
498
511
  }))
499
512
  )
500
513
  ),
@@ -530,11 +543,8 @@ async function runScript(wasm, start) {
530
543
  drain();
531
544
  });
532
545
  try {
533
- const fn = vm.evalCode(
534
- `(async (tools, console) => {${start.code}
535
- })`,
536
- "codemode.js"
537
- );
546
+ const fn = vm.evalCode(`(async () => {${start.code}
547
+ })`, "codemode.js");
538
548
  vm.callFunction(run, api, fn).dispose();
539
549
  fn.dispose();
540
550
  drain();
@@ -580,6 +590,3 @@ function main() {
580
590
  });
581
591
  }
582
592
  main();
583
- export {
584
- toScriptIdentifier
585
- };
@@ -26,11 +26,6 @@ import {
26
26
  /** QuickJS VM 的堆上限:超量分配在脚本里变成 InternalError,而不是拖垮宿主。 */
27
27
  const MEMORY_LIMIT_BYTES = 512 * 1024 * 1024;
28
28
 
29
- /** 工具名到脚本标识符的归一化:非法字符换成 `_`,数字开头补 `_`。 */
30
- export function toScriptIdentifier(name: string): string {
31
- return name.replaceAll(/[^A-Za-z0-9_$]/g, "_").replaceAll(/^\d/g, "_");
32
- }
33
-
34
29
  /**
35
30
  * QuickJS 把引擎诊断写到 fd 1 / 2,那会直接进 pi 的 TUI;按写入长度回报并丢弃内容,
36
31
  * 避免 libc 重试。
@@ -178,8 +173,8 @@ async function runScript(wasm: object, start: Extract<HostMessage, { t: "start"
178
173
  JSON.stringify(
179
174
  start.tools.map((tool) => ({
180
175
  name: tool.name,
181
- jsName: toScriptIdentifier(tool.name),
182
176
  description: tool.description,
177
+ structuredSchema: tool.structuredSchema,
183
178
  })),
184
179
  ),
185
180
  ),
@@ -225,10 +220,7 @@ async function runScript(wasm: object, start: Extract<HostMessage, { t: "start"
225
220
 
226
221
  try {
227
222
  // 前缀与脚本首行共用一行,报错行号与用户写的脚本一致
228
- const fn: JSValueHandle = vm.evalCode(
229
- `(async (tools, console) => {${start.code}\n})`,
230
- "codemode.js",
231
- );
223
+ const fn: JSValueHandle = vm.evalCode(`(async () => {${start.code}\n})`, "codemode.js");
232
224
  vm.callFunction(run, api, fn).dispose();
233
225
  fn.dispose();
234
226
  drain();
package/src/gh/base.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import { existsSync } from "node:fs";
10
10
  import { delimiter, join } from "node:path";
11
11
 
12
- import { Type } from "typebox";
12
+ import { type Static, type TSchema, Type } from "typebox";
13
13
  import { Value } from "typebox/value";
14
14
 
15
15
  import { egress } from "../lib/egress.js";
@@ -25,7 +25,10 @@ import {
25
25
  type GithubSearch,
26
26
  renderHits,
27
27
  } from "../lib/github.js";
28
+ import { parseWithSchema } from "../lib/parse-with-schema.js";
28
29
  import { type ToolPendant } from "../lib/pendant.js";
30
+ import { type StructuredResult } from "../lib/tool-bus.js";
31
+ import type { checksVerdictSchema } from "./schemas.js";
29
32
 
30
33
  /** A tool result: what the model sees plus the structured details payload. */
31
34
  export interface ToolResult {
@@ -33,6 +36,24 @@ export interface ToolResult {
33
36
  details: Record<string, unknown>;
34
37
  }
35
38
 
39
+ /**
40
+ * 带结构化结果的结果:`structuredResult` 的成功支类型由 `T` 决定。声明了 structuredSchema
41
+ * 的工具用它,交给 codemode 脚本按名字调用时解包。它不改变 `content` 与 `details`。
42
+ */
43
+ export type WithStructured<T> = ToolResult & { structuredResult: StructuredResult<T> };
44
+
45
+ /** 某个 `structuredSchema` 对应的工具结果:成功支与 schema 的静态类型对齐。 */
46
+ export type StructuredResultOf<TSchema_ extends TSchema> = WithStructured<Static<TSchema_>>;
47
+
48
+ /** 结构化失败(Result 的失败支):与具体的输出类型无关,任何结构化工具都能用。 */
49
+ export interface StructuredFailure {
50
+ ok: false;
51
+ error: string;
52
+ }
53
+
54
+ /** 结构化失败的结果:不必抛异常就能把「没找到」这类结果告诉脚本。 */
55
+ export type StructuredFailureResult = ToolResult & { structuredResult: StructuredFailure };
56
+
36
57
  /** What a toolcall handler receives from the framework. */
37
58
  export interface ToolCall<Params> {
38
59
  params: Params;
@@ -239,6 +260,32 @@ export function toToolResultJson(json: string, input?: unknown): ToolResult {
239
260
  };
240
261
  }
241
262
 
263
+ /** 给结果附上结构化结果(成功支):`value` 就是脚本解包后拿到的东西。 */
264
+ export function withStructuredResult<T>(result: ToolResult, value: T): WithStructured<T> {
265
+ return { ...result, structuredResult: { ok: true, value } };
266
+ }
267
+
268
+ /**
269
+ * 透传型工具的成功结果:`gh --json` 的原始输出既是给模型的文本(整段 JSON,不经行
270
+ * 截断),也按 `schema` 解析成结构化结果。解析失败会抛错——schema 与 GitHub 实际返回
271
+ * 漂移时必须显式失败,不能悄悄少给字段。
272
+ */
273
+ export function toStructuredJsonResult<T extends TSchema>(
274
+ json: string,
275
+ input: unknown,
276
+ schema: T,
277
+ ): StructuredResultOf<T> {
278
+ return withStructuredResult(
279
+ toToolResultJson(json, input),
280
+ parseWithSchema(schema, JSON.parse(json)),
281
+ );
282
+ }
283
+
284
+ /** 结构化失败(Result 的失败支):不必抛异常就能把「没找到」这类结果告诉脚本。 */
285
+ export function structuredFailure(error: string): StructuredFailure {
286
+ return { ok: false, error };
287
+ }
288
+
242
289
  /**
243
290
  * Pendant subtitle for a tool result: `repo=x/y` (when provided) plus the
244
291
  * tool's id parameter, e.g. `repo=x/y number=123`. Returns undefined when
@@ -747,7 +794,7 @@ export async function waitChecksReport(options: {
747
794
  onUpdate: ((msg: ToolResult) => void) | undefined;
748
795
  params: unknown;
749
796
  pendant?: ToolPendant;
750
- }): Promise<ToolResult> {
797
+ }): Promise<StructuredResultOf<typeof checksVerdictSchema>> {
751
798
  const { subject, owner, repo, headSha, failFast, event, signal, onUpdate, params, pendant } =
752
799
  options;
753
800
 
@@ -779,15 +826,18 @@ export async function waitChecksReport(options: {
779
826
  }
780
827
 
781
828
  const verdict = renderChecksVerdict({ subject, poll, actionJobs, enrichmentError });
782
- return {
783
- content: [{ type: "text", text: verdict.text }],
784
- details: {
785
- status: verdict.status,
786
- totalChecks: poll.checks.length,
787
- checks: poll.checks,
788
- failedJobs: verdict.failedJobs,
789
- input: params,
790
- ...(pendant && { pendant }),
791
- },
829
+ const payload = {
830
+ status: verdict.status,
831
+ totalChecks: poll.checks.length,
832
+ // 复制成可变数组:结构化结果的类型要与 structuredSchema 的静态类型一致
833
+ checks: [...poll.checks],
834
+ failedJobs: [...verdict.failedJobs],
792
835
  };
836
+ return withStructuredResult(
837
+ {
838
+ content: [{ type: "text", text: verdict.text }],
839
+ details: { ...payload, input: params, ...(pendant && { pendant }) },
840
+ },
841
+ payload,
842
+ );
793
843
  }