@trim21/personal-pi-extensions 0.1.678 → 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 +10 -4
- package/package.json +1 -1
- package/src/codemode/declarations.ts +28 -12
- package/src/codemode/prelude.ts +32 -16
- package/src/codemode/protocol.ts +5 -2
- package/src/codemode/tool.ts +10 -3
- package/src/codemode/worker.js +36 -29
- package/src/codemode/worker.ts +2 -10
- package/src/gh/base.ts +62 -12
- package/src/gh/schemas.ts +230 -0
- package/src/gh/tools/download-release-assets.ts +51 -44
- package/src/gh/tools/get-workflow-jobs.ts +29 -18
- package/src/gh/tools/read-ci-logs.ts +37 -23
- package/src/gh/tools/read-issue-comments.ts +31 -24
- package/src/gh/tools/read-issue.ts +30 -22
- package/src/gh/tools/read-pr-comments.ts +62 -58
- package/src/gh/tools/read-pr-status.ts +31 -20
- package/src/gh/tools/read-pr.ts +30 -22
- package/src/gh/tools/wait-commit-checks.ts +35 -31
- package/src/gh/tools/wait-pr-checks.ts +30 -23
- package/src/lib/tool-bus.ts +128 -8
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
|
-
或模块加载,脚本唯一的出口是 `
|
|
34
|
+
或模块加载,脚本唯一的出口是 `call(name, args)`。
|
|
35
35
|
|
|
36
|
-
- **嵌套调用经工具总线**:`
|
|
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
|
-
-
|
|
46
|
-
`
|
|
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,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
/**
|
|
80
|
-
function
|
|
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
|
-
|
|
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) => ({
|
|
98
|
+
return tools.map((tool) => ({
|
|
99
|
+
name: tool.name,
|
|
100
|
+
description: tool.description,
|
|
101
|
+
structuredSchema: tool.structuredSchema,
|
|
102
|
+
}));
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
/**
|
|
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
|
-
|
|
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;",
|
package/src/codemode/prelude.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* codemode 脚本侧的 prelude:在 QuickJS VM 里先于脚本求值,构建脚本能看到的全部
|
|
3
|
-
* 能力(`
|
|
4
|
-
*
|
|
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
|
|
101
|
+
const callers = new Map();
|
|
93
102
|
const allTools = [];
|
|
94
|
-
for (const { name,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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, "
|
|
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
|
|
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(
|
|
289
|
+
promise = fn();
|
|
274
290
|
} catch (error) {
|
|
275
291
|
done(false, describeError(error));
|
|
276
292
|
return;
|
package/src/codemode/protocol.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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 =
|
package/src/codemode/tool.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* codemode 工具:模型写一段 JavaScript,脚本在 QuickJS VM(worker 线程)里执行,脚本唯一
|
|
3
|
-
* 的能力是调用 `
|
|
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
|
|
77
|
-
" and a failing tool rejects with
|
|
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 };
|
package/src/codemode/worker.js
CHANGED
|
@@ -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
|
|
84
|
+
const callers = new Map();
|
|
78
85
|
const allTools = [];
|
|
79
|
-
for (const { name,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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, "
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
};
|
package/src/codemode/worker.ts
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
}
|