@trim21/personal-pi-extensions 0.1.678 → 0.1.680
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 +25 -12
- package/package.json +1 -1
- package/src/claude-code/files.ts +12 -2
- package/src/codemode/declarations.ts +35 -12
- package/src/codemode/fs.ts +138 -0
- package/src/codemode/prelude.ts +44 -17
- package/src/codemode/protocol.ts +17 -2
- package/src/codemode/tool.ts +69 -11
- package/src/codemode/worker.js +48 -30
- package/src/codemode/worker.ts +3 -13
- 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/index.ts +5 -2
- package/src/lib/tool-bus.ts +128 -8
- package/src/opencode/files.ts +12 -2
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
4
4
|
import {
|
|
5
5
|
type GhClient,
|
|
6
6
|
mergeChecks,
|
|
7
7
|
resolveRepo,
|
|
8
8
|
splitRepo,
|
|
9
|
+
type StructuredResultOf,
|
|
9
10
|
subtitlePendant,
|
|
10
11
|
type ToolCall,
|
|
11
|
-
type ToolResult,
|
|
12
12
|
toPositiveId,
|
|
13
|
+
withStructuredResult,
|
|
13
14
|
} from "../base.js";
|
|
15
|
+
import { prStatusSchema } from "../schemas.js";
|
|
14
16
|
|
|
15
17
|
interface PrStatusParams {
|
|
16
18
|
number: number | string;
|
|
@@ -22,7 +24,10 @@ interface PrStatusParams {
|
|
|
22
24
|
* path as the wait tools (octokit), but it never polls — pending checks come
|
|
23
25
|
* back as-is.
|
|
24
26
|
*/
|
|
25
|
-
export async function prStatus(
|
|
27
|
+
export async function prStatus(
|
|
28
|
+
gh: GhClient,
|
|
29
|
+
call: ToolCall<PrStatusParams>,
|
|
30
|
+
): Promise<StructuredResultOf<typeof prStatusSchema>> {
|
|
26
31
|
const { params, ctx, signal } = call;
|
|
27
32
|
const { number, repo } = params;
|
|
28
33
|
const pullNumber = toPositiveId(number, "number");
|
|
@@ -46,25 +51,31 @@ export async function prStatus(gh: GhClient, call: ToolCall<PrStatusParams>): Pr
|
|
|
46
51
|
}));
|
|
47
52
|
|
|
48
53
|
const payload = { pr: pullNumber, repo: effectiveRepo, head_sha: headSha, checks };
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
return withStructuredResult(
|
|
55
|
+
{
|
|
56
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
57
|
+
details: { ...payload, input: params, ...(pendant && { pendant }) },
|
|
58
|
+
},
|
|
59
|
+
payload,
|
|
60
|
+
);
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
export function addReadPrStatusTool(gh: GhClient, bus: ToolBus) {
|
|
56
|
-
bus.register(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
bus.register(
|
|
65
|
+
defineStructuredTool({
|
|
66
|
+
name: "read-github-pr-status",
|
|
67
|
+
label: "GitHub PR Status",
|
|
68
|
+
description:
|
|
69
|
+
"Get the current checks of a pull request's head commit as JSON {pr, repo, head_sha, checks:[{name, bucket, event, run_id, job_id, url}]}. `bucket` is pass / fail / pending / skipped; Actions checks carry the `run_id` and `job_id` behind them (null for other CI), which is what read-github-ci-logs and get-github-workflow-jobs take. Returns the snapshot immediately without waiting — use wait-github-pr-checks to block until the checks finish.",
|
|
70
|
+
promptSnippet: "Read GitHub PR status checks",
|
|
71
|
+
parameters: Type.Object({
|
|
72
|
+
number: Type.Union([Type.Number(), Type.String()], { description: "PR number" }),
|
|
73
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
|
|
74
|
+
}),
|
|
75
|
+
structuredSchema: prStatusSchema,
|
|
76
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
77
|
+
return prStatus(gh, { params, ctx, signal, onUpdate });
|
|
78
|
+
},
|
|
65
79
|
}),
|
|
66
|
-
|
|
67
|
-
return prStatus(gh, { params, ctx, signal, onUpdate });
|
|
68
|
-
},
|
|
69
|
-
});
|
|
80
|
+
);
|
|
70
81
|
}
|
package/src/gh/tools/read-pr.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
|
|
3
|
-
import type
|
|
4
|
-
import {
|
|
3
|
+
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
4
|
+
import {
|
|
5
|
+
type GhClient,
|
|
6
|
+
ghExec,
|
|
7
|
+
repoArgs,
|
|
8
|
+
subtitlePendant,
|
|
9
|
+
toStructuredJsonResult,
|
|
10
|
+
} from "../base.js";
|
|
11
|
+
import { prViewSchema } from "../schemas.js";
|
|
5
12
|
|
|
6
13
|
export function addReadPrTool(_gh: GhClient, bus: ToolBus) {
|
|
7
|
-
bus.register(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
bus.register(
|
|
15
|
+
defineStructuredTool({
|
|
16
|
+
name: "read-github-pr",
|
|
17
|
+
label: "GitHub PR",
|
|
18
|
+
description: "Get details of a GitHub pull request by number.",
|
|
19
|
+
promptSnippet: "Read a GitHub PR",
|
|
20
|
+
parameters: Type.Object({
|
|
21
|
+
number: Type.Union([Type.Number(), Type.String()], { description: "PR number" }),
|
|
22
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
23
|
+
}),
|
|
24
|
+
structuredSchema: prViewSchema,
|
|
25
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
26
|
+
const { number, repo } = params;
|
|
27
|
+
const out = await ghExec(
|
|
20
28
|
[
|
|
21
29
|
"pr",
|
|
22
30
|
"view",
|
|
@@ -26,11 +34,11 @@ export function addReadPrTool(_gh: GhClient, bus: ToolBus) {
|
|
|
26
34
|
"title,state,body,author,createdAt,updatedAt,mergedAt,mergedBy,headRefName,baseRefName,url,additions,deletions,changedFiles,labels,assignees,reviewRequests,reviews,comments,number",
|
|
27
35
|
],
|
|
28
36
|
{ cwd: ctx.cwd, signal, input: params },
|
|
29
|
-
)
|
|
30
|
-
params,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
|
|
37
|
+
);
|
|
38
|
+
const result = toStructuredJsonResult(out, params, prViewSchema);
|
|
39
|
+
result.details.pendant = subtitlePendant(params, "number");
|
|
40
|
+
return result;
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
36
44
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
4
4
|
import {
|
|
5
5
|
type GhClient,
|
|
6
6
|
resolveRepo,
|
|
7
7
|
splitRepo,
|
|
8
|
+
type StructuredResultOf,
|
|
8
9
|
subtitlePendant,
|
|
9
10
|
type ToolCall,
|
|
10
|
-
type ToolResult,
|
|
11
11
|
waitChecksReport,
|
|
12
12
|
} from "../base.js";
|
|
13
|
+
import { checksVerdictSchema } from "../schemas.js";
|
|
13
14
|
|
|
14
15
|
interface CommitChecksWaitParams {
|
|
15
16
|
commit: number | string;
|
|
@@ -22,7 +23,7 @@ interface CommitChecksWaitParams {
|
|
|
22
23
|
async function waitCommitChecks(
|
|
23
24
|
gh: GhClient,
|
|
24
25
|
call: ToolCall<CommitChecksWaitParams>,
|
|
25
|
-
): Promise<
|
|
26
|
+
): Promise<StructuredResultOf<typeof checksVerdictSchema>> {
|
|
26
27
|
const { params, ctx, signal, onUpdate } = call;
|
|
27
28
|
const { commit, repo, event, fail_fast } = params;
|
|
28
29
|
|
|
@@ -54,35 +55,38 @@ async function waitCommitChecks(
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export function addWaitCommitChecksTool(gh: GhClient, bus: ToolBus) {
|
|
57
|
-
bus.register(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"Commit to wait for: full or partial SHA, branch name, or tag name (resolved to the commit's SHA)",
|
|
72
|
-
}),
|
|
73
|
-
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
74
|
-
event: Type.Optional(
|
|
75
|
-
Type.String({
|
|
58
|
+
bus.register(
|
|
59
|
+
defineStructuredTool({
|
|
60
|
+
name: "wait-github-commit-checks",
|
|
61
|
+
label: "Watch GitHub Commit Checks",
|
|
62
|
+
description:
|
|
63
|
+
"Watch CI status checks for a commit until they complete — no pull request required. " +
|
|
64
|
+
"Same semantics as wait-github-pr-checks: returns when any check fails (immediately under fail_fast) " +
|
|
65
|
+
"or all checks pass/skip; on timeout the still-in-flight snapshot is returned. " +
|
|
66
|
+
"With `event`, only check runs triggered by that workflow event (e.g. push) are judged; " +
|
|
67
|
+
"commit statuses have an unknown trigger event and are excluded under a filter. " +
|
|
68
|
+
"Use this to wait for the runs a commit's push triggered, or for checks on an arbitrary ref.",
|
|
69
|
+
promptSnippet: "Watch and wait for GitHub commit CI checks to complete",
|
|
70
|
+
parameters: Type.Object({
|
|
71
|
+
commit: Type.Union([Type.Number(), Type.String()], {
|
|
76
72
|
description:
|
|
77
|
-
"
|
|
73
|
+
"Commit to wait for: full or partial SHA, branch name, or tag name (resolved to the commit's SHA)",
|
|
78
74
|
}),
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
76
|
+
event: Type.Optional(
|
|
77
|
+
Type.String({
|
|
78
|
+
description:
|
|
79
|
+
"Only judge check runs triggered by this workflow event (e.g. push, pull_request)",
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
fail_fast: Type.Optional(
|
|
83
|
+
Type.Boolean({ description: "Exit immediately when any check fails (default: false)" }),
|
|
84
|
+
),
|
|
85
|
+
}),
|
|
86
|
+
structuredSchema: checksVerdictSchema,
|
|
87
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
88
|
+
return waitCommitChecks(gh, { params, ctx, signal, onUpdate });
|
|
89
|
+
},
|
|
83
90
|
}),
|
|
84
|
-
|
|
85
|
-
return waitCommitChecks(gh, { params, ctx, signal, onUpdate });
|
|
86
|
-
},
|
|
87
|
-
});
|
|
91
|
+
);
|
|
88
92
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
|
|
4
|
-
import type
|
|
4
|
+
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
5
5
|
import {
|
|
6
6
|
type GhClient,
|
|
7
7
|
ghExec,
|
|
8
8
|
resolveRepo,
|
|
9
9
|
splitRepo,
|
|
10
|
+
type StructuredResultOf,
|
|
10
11
|
subtitlePendant,
|
|
11
12
|
type ToolCall,
|
|
12
|
-
type ToolResult,
|
|
13
13
|
waitChecksReport,
|
|
14
14
|
} from "../base.js";
|
|
15
|
+
import { checksVerdictSchema } from "../schemas.js";
|
|
15
16
|
|
|
16
17
|
interface PrChecksWaitParams {
|
|
17
18
|
number: number | string;
|
|
@@ -22,7 +23,10 @@ interface PrChecksWaitParams {
|
|
|
22
23
|
const prHeadSchema = Type.Object({ headRefOid: Type.String() });
|
|
23
24
|
|
|
24
25
|
/** The toolcall handler behind `wait-github-pr-checks`. */
|
|
25
|
-
async function waitPrChecks(
|
|
26
|
+
async function waitPrChecks(
|
|
27
|
+
gh: GhClient,
|
|
28
|
+
call: ToolCall<PrChecksWaitParams>,
|
|
29
|
+
): Promise<StructuredResultOf<typeof checksVerdictSchema>> {
|
|
26
30
|
const { params, ctx, signal, onUpdate } = call;
|
|
27
31
|
const { number, repo, fail_fast } = params;
|
|
28
32
|
|
|
@@ -56,25 +60,28 @@ async function waitPrChecks(gh: GhClient, call: ToolCall<PrChecksWaitParams>): P
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
export function addWaitPrChecksTool(gh: GhClient, bus: ToolBus) {
|
|
59
|
-
bus.register(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Type.
|
|
74
|
-
|
|
63
|
+
bus.register(
|
|
64
|
+
defineStructuredTool({
|
|
65
|
+
name: "wait-github-pr-checks",
|
|
66
|
+
label: "Watch GitHub PR Checks",
|
|
67
|
+
description:
|
|
68
|
+
"Watch CI status checks for a PR until they complete. Blocks until all checks pass (or are skipped) or one fails. " +
|
|
69
|
+
"Covers both commit statuses (Azure DevOps, Jenkins, ...) and GitHub Actions check runs. " +
|
|
70
|
+
"Each polling round streams a compact bullet list of the checks still in flight via onUpdate; " +
|
|
71
|
+
"on timeout the still-in-flight snapshot is returned instead of a verdict. " +
|
|
72
|
+
"Use this when you need to wait for CI to complete and see the final result.",
|
|
73
|
+
promptSnippet: "Watch and wait for GitHub PR CI checks to complete",
|
|
74
|
+
parameters: Type.Object({
|
|
75
|
+
number: Type.Union([Type.Number(), Type.String()], { description: "PR number" }),
|
|
76
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
77
|
+
fail_fast: Type.Optional(
|
|
78
|
+
Type.Boolean({ description: "Exit immediately when any check fails (default: false)" }),
|
|
79
|
+
),
|
|
80
|
+
}),
|
|
81
|
+
structuredSchema: checksVerdictSchema,
|
|
82
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
83
|
+
return waitPrChecks(gh, { params, ctx, signal, onUpdate });
|
|
84
|
+
},
|
|
75
85
|
}),
|
|
76
|
-
|
|
77
|
-
return waitPrChecks(gh, { params, ctx, signal, onUpdate });
|
|
78
|
-
},
|
|
79
|
-
});
|
|
86
|
+
);
|
|
80
87
|
}
|
package/src/index.ts
CHANGED
|
@@ -120,8 +120,11 @@ export default function personalExtensions(pi: ExtensionAPI): void {
|
|
|
120
120
|
for (const { name, module } of modules) {
|
|
121
121
|
runModule(name, () => module.register(bus));
|
|
122
122
|
}
|
|
123
|
-
// codemode 最后注册:它把总线上已有的工具写进自己的描述,并在注册时编译 wasm
|
|
124
|
-
|
|
123
|
+
// codemode 最后注册:它把总线上已有的工具写进自己的描述,并在注册时编译 wasm。
|
|
124
|
+
// 脚本的 fs 原语与文件工具共用请求策略与已读记账,所以这里把工具集那两份注入过去。
|
|
125
|
+
await runModuleAsync("codemode", () =>
|
|
126
|
+
codemode.register(bus, { policy: services.policy, reads: fileToolset.reads }),
|
|
127
|
+
);
|
|
125
128
|
|
|
126
129
|
for (const warning of warnings) {
|
|
127
130
|
ctx.ui.notify(warning, "warning");
|
package/src/lib/tool-bus.ts
CHANGED
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
* - `executeTool`:先用工具自身的参数 schema 校验参数,再调用定义的 `execute`;
|
|
8
8
|
* 参数校验失败与工具抛出的异常都归一化成错误结果返回,不向调用方抛异常。
|
|
9
9
|
*
|
|
10
|
+
* 结构化结果:工具可以在定义上声明 `structuredSchema`(TypeBox),并在结果里带
|
|
11
|
+
* `structuredResult: StructuredResult<Static<typeof structuredSchema>>`。成功支的载荷类型
|
|
12
|
+
* 由 schema 在编译期约束,执行时再由总线 `Value.Parse` 复核;`ok: false` 让工具不必抛异常
|
|
13
|
+
* 也能结构化地报告失败(它不改变结果的 `isError`)。
|
|
14
|
+
*
|
|
15
|
+
* 这两个名字是本仓库自己的约定,刻意不占用宿主字段:pi-agent-core >= 0.99 自带
|
|
16
|
+
* `outputSchema` / `structuredContent`,语义是「裸载荷 + 用 `isError` 表失败」,与本仓库的
|
|
17
|
+
* Result 信封不同,将来接宿主原生管线时需要显式换算。
|
|
18
|
+
*
|
|
10
19
|
* 状态由闭包持有(工厂 + 闭包),不使用模块级可变状态。
|
|
11
20
|
*/
|
|
12
21
|
|
|
@@ -20,8 +29,52 @@ import type { Static, TSchema } from "typebox";
|
|
|
20
29
|
|
|
21
30
|
import { parseWithSchema } from "./parse-with-schema.js";
|
|
22
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 结构化结果:成功带 `value`,失败带可直接展示的错误说明。它只嵌在工具结果的
|
|
34
|
+
* `structuredResult` 一个属性里,结果对象本身保持单一类型(两个互斥字段会让整个
|
|
35
|
+
* 结果类型分配成对象联合)。
|
|
36
|
+
*/
|
|
37
|
+
export type StructuredResult<T> = { ok: true; value: T } | { ok: false; error: string };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 声明了输出结构的工具定义:`structuredResult` 成功支的载荷类型由 `structuredSchema` 在编译
|
|
41
|
+
* 期绑定,因此工具无法返回与声明不符的结果。
|
|
42
|
+
*/
|
|
43
|
+
export interface StructuredToolDefinition<
|
|
44
|
+
TParams extends TSchema = TSchema,
|
|
45
|
+
TOutput extends TSchema = TSchema,
|
|
46
|
+
TDetails = unknown,
|
|
47
|
+
> extends Omit<ToolDefinition<TParams, TDetails, unknown>, "execute"> {
|
|
48
|
+
structuredSchema: TOutput;
|
|
49
|
+
execute(
|
|
50
|
+
toolCallId: string,
|
|
51
|
+
params: Static<TParams>,
|
|
52
|
+
signal: AbortSignal | undefined,
|
|
53
|
+
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
|
|
54
|
+
ctx: ExtensionContext,
|
|
55
|
+
): Promise<AgentToolResult<TDetails> & { structuredResult: StructuredResult<Static<TOutput>> }>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 结构化工具必须经它定义:`structuredSchema` 与 `execute` 返回类型之间的绑定只有在
|
|
60
|
+
* generic 函数的参数位置才建立得起来(与 pi 的 `defineTool` 同理)。
|
|
61
|
+
*/
|
|
62
|
+
export function defineStructuredTool<
|
|
63
|
+
TParams extends TSchema,
|
|
64
|
+
TOutput extends TSchema,
|
|
65
|
+
TDetails = unknown,
|
|
66
|
+
>(
|
|
67
|
+
definition: StructuredToolDefinition<TParams, TOutput, TDetails>,
|
|
68
|
+
): StructuredToolDefinition<TParams, TOutput, TDetails> {
|
|
69
|
+
return definition;
|
|
70
|
+
}
|
|
71
|
+
|
|
23
72
|
/** 一次工具执行的结果:`isError` 标出失败(工具不存在、参数不合法、工具抛错)。 */
|
|
24
|
-
export type ToolExecutionResult = AgentToolResult<unknown> & {
|
|
73
|
+
export type ToolExecutionResult = AgentToolResult<unknown> & {
|
|
74
|
+
isError: boolean;
|
|
75
|
+
/** 工具声明的结构化结果,原样交给 codemode 脚本。 */
|
|
76
|
+
structuredResult?: StructuredResult<unknown>;
|
|
77
|
+
};
|
|
25
78
|
|
|
26
79
|
export interface ExecuteToolOptions {
|
|
27
80
|
/** 工具 `execute` 需要的宿主上下文:调用方把自己拿到的那个 ctx 传进来。 */
|
|
@@ -32,16 +85,21 @@ export interface ExecuteToolOptions {
|
|
|
32
85
|
toolCallId?: string;
|
|
33
86
|
}
|
|
34
87
|
|
|
88
|
+
/** 注册后的定义:多出的 `structuredSchema` 供 codemode 渲染脚本侧返回类型。 */
|
|
89
|
+
export type RegisteredToolDefinition = ToolDefinition<TSchema, unknown, unknown> & {
|
|
90
|
+
structuredSchema?: TSchema;
|
|
91
|
+
};
|
|
92
|
+
|
|
35
93
|
export interface ToolBus {
|
|
36
94
|
/** 注册一个工具;被配置禁用时跳过并返回 false。 */
|
|
37
95
|
register<TParams extends TSchema, TDetails>(
|
|
38
96
|
definition: ToolDefinition<TParams, TDetails, unknown>,
|
|
39
97
|
): boolean;
|
|
40
98
|
/** 本次实际注册的工具定义。 */
|
|
41
|
-
list(): readonly
|
|
99
|
+
list(): readonly RegisteredToolDefinition[];
|
|
42
100
|
/** 所有尝试注册过的工具名(含被禁用而跳过的),用于校验配置里的模式是否有效。 */
|
|
43
101
|
declaredNames(): readonly string[];
|
|
44
|
-
get(name: string):
|
|
102
|
+
get(name: string): RegisteredToolDefinition | undefined;
|
|
45
103
|
/** 按名执行一个已注册工具;任何失败都以 `isError` 结果返回,不抛异常。 */
|
|
46
104
|
executeTool(
|
|
47
105
|
name: string,
|
|
@@ -55,8 +113,6 @@ export interface ToolBusOptions {
|
|
|
55
113
|
isDisabled?: (name: string) => boolean;
|
|
56
114
|
}
|
|
57
115
|
|
|
58
|
-
type AnyToolDefinition = ToolDefinition<TSchema, unknown, unknown>;
|
|
59
|
-
|
|
60
116
|
function errorResult(text: string): ToolExecutionResult {
|
|
61
117
|
return { content: [{ type: "text", text }], details: { error: text }, isError: true };
|
|
62
118
|
}
|
|
@@ -65,8 +121,64 @@ function errorMessage(error: unknown): string {
|
|
|
65
121
|
return error instanceof Error ? error.message : String(error);
|
|
66
122
|
}
|
|
67
123
|
|
|
124
|
+
type StructuredCheck =
|
|
125
|
+
{ ok: true; result?: StructuredResult<unknown> } | { ok: false; error: string };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 复核工具返回的 `structuredResult`:声明了 `structuredSchema` 的工具必须返回它,成功支的
|
|
129
|
+
* 载荷必须过 schema(外部数据用 typebox 校验是本仓库约定),失败支的错误必须是字符串。
|
|
130
|
+
*/
|
|
131
|
+
function checkStructuredResult(
|
|
132
|
+
name: string,
|
|
133
|
+
structuredSchema: TSchema | undefined,
|
|
134
|
+
result: AgentToolResult<unknown>,
|
|
135
|
+
): StructuredCheck {
|
|
136
|
+
const structured = (result as { structuredResult?: unknown }).structuredResult;
|
|
137
|
+
if (structured === undefined) {
|
|
138
|
+
return structuredSchema === undefined
|
|
139
|
+
? { ok: true }
|
|
140
|
+
: {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: `Tool "${name}" declared a structuredSchema but returned no structuredResult.`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (structuredSchema === undefined) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: `Tool "${name}" returned a structuredResult without declaring a structuredSchema.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (typeof structured !== "object" || structured === null) {
|
|
152
|
+
return { ok: false, error: `Tool "${name}" returned a malformed structuredResult.` };
|
|
153
|
+
}
|
|
154
|
+
const candidate = structured as { ok?: unknown; value?: unknown; error?: unknown };
|
|
155
|
+
if (candidate.ok === false) {
|
|
156
|
+
if (typeof candidate.error !== "string") {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
error: `Tool "${name}" returned a structuredResult whose error is not a string.`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, result: { ok: false, error: candidate.error } };
|
|
163
|
+
}
|
|
164
|
+
if (candidate.ok !== true) {
|
|
165
|
+
return { ok: false, error: `Tool "${name}" returned a malformed structuredResult.` };
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
result: { ok: true, value: parseWithSchema(structuredSchema, candidate.value) },
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
error: `Tool "${name}" returned a structuredResult value that does not match its structuredSchema: ${errorMessage(error)}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
68
180
|
export function createToolBus(pi: ExtensionAPI, options: ToolBusOptions = {}): ToolBus {
|
|
69
|
-
const registered = new Map<string,
|
|
181
|
+
const registered = new Map<string, RegisteredToolDefinition>();
|
|
70
182
|
const declared = new Set<string>();
|
|
71
183
|
let callCounter = 0;
|
|
72
184
|
|
|
@@ -76,7 +188,7 @@ export function createToolBus(pi: ExtensionAPI, options: ToolBusOptions = {}): T
|
|
|
76
188
|
if (options.isDisabled?.(definition.name) === true) {
|
|
77
189
|
return false;
|
|
78
190
|
}
|
|
79
|
-
const stored = definition as unknown as
|
|
191
|
+
const stored = definition as unknown as RegisteredToolDefinition;
|
|
80
192
|
pi.registerTool(stored);
|
|
81
193
|
registered.set(stored.name, stored);
|
|
82
194
|
return true;
|
|
@@ -117,7 +229,15 @@ export function createToolBus(pi: ExtensionAPI, options: ToolBusOptions = {}): T
|
|
|
117
229
|
options.onUpdate,
|
|
118
230
|
options.ctx,
|
|
119
231
|
);
|
|
120
|
-
|
|
232
|
+
const checked = checkStructuredResult(name, definition.structuredSchema, result);
|
|
233
|
+
if (!checked.ok) {
|
|
234
|
+
return errorResult(checked.error);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
...result,
|
|
238
|
+
isError: false,
|
|
239
|
+
...(checked.result !== undefined && { structuredResult: checked.result }),
|
|
240
|
+
};
|
|
121
241
|
} catch (error) {
|
|
122
242
|
return errorResult(`Tool "${name}" failed: ${errorMessage(error)}`);
|
|
123
243
|
}
|
package/src/opencode/files.ts
CHANGED
|
@@ -407,8 +407,14 @@ async function formatDirectoryEntries(dirPath: string): Promise<string[]> {
|
|
|
407
407
|
|
|
408
408
|
// ── read-before-edit 记账 ────────────────────────────────────────────────────
|
|
409
409
|
|
|
410
|
-
/**
|
|
411
|
-
|
|
410
|
+
/**
|
|
411
|
+
* 会更新 reads 记账(src/lib/file-reads.ts)并随 details 持久化快照的工具名。
|
|
412
|
+
*
|
|
413
|
+
* 含 `codemode`:脚本的 `fs.read` / `fs.write`(src/codemode/fs.ts)与本工具集共用
|
|
414
|
+
* 同一个 ReadsState,脚本记下的已读落在 codemode 自己的工具结果里,重放分支时要一起
|
|
415
|
+
* 收回来(见 `restoreReads`)。
|
|
416
|
+
*/
|
|
417
|
+
const READS_TOOL_NAMES = new Set(["read", "edit", "write", "lsp-rename", "codemode"]);
|
|
412
418
|
|
|
413
419
|
function registerReadTool(bus: ToolBus, getService: () => LspService, state: ReadsState): void {
|
|
414
420
|
bus.register({
|
|
@@ -897,6 +903,8 @@ export interface OpencodeFileToolset {
|
|
|
897
903
|
register(bus: ToolBus): void;
|
|
898
904
|
onLspEnabled(bus: ToolBus, service: LspService): void;
|
|
899
905
|
restoreReads(ctx: ExtensionContext): void;
|
|
906
|
+
/** 与工具共用的已读记账:codemode 的 fs 原语拿它做 stale 保护(两边互通)。 */
|
|
907
|
+
readonly reads: ReadsState;
|
|
900
908
|
}
|
|
901
909
|
|
|
902
910
|
/**
|
|
@@ -939,6 +947,8 @@ export function createOpencodeFileTools(
|
|
|
939
947
|
restoreReads(ctx) {
|
|
940
948
|
restoreReads(state, ctx.sessionManager, READS_TOOL_NAMES);
|
|
941
949
|
},
|
|
950
|
+
|
|
951
|
+
reads: state,
|
|
942
952
|
};
|
|
943
953
|
|
|
944
954
|
const manager =
|