92 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 探索工具 server 的框架扩展(阶段 D)。
//
// 边界合同中的工具层:给创作智能体暴露只读探索工具。本扩展不自带任何查询逻辑,
// 工具登记表与实现都在 read_tools.py(Python,只读连接、未知工具拒绝、结果有界)。
// 加载时执行 `read_tools.py --list` 取登记表,按登记表动态注册工具,保证单一事实源;
// 每次工具执行把参数原样转给 `read_tools.py <工具名> '<json>'`,stdout 即结果。
//
// 派发器经环境变量定位实现:MUSE_READ_TOOLS_PYTHON(解释器)、MUSE_READ_TOOLS_SCRIPT(脚本)。
// 未经派发器注入这两个变量时不注册任何工具(静默跳过),隔离会话不受影响。
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
interface ToolSpec {
description: string;
tables: string[];
args: Record<string, "int" | "str">;
}
function runReadTools(python: string, script: string, argv: string[]): string {
const bun = (globalThis as { Bun?: any }).Bun;
if (bun?.spawnSync) {
const res = bun.spawnSync([python, script, ...argv], {
stdout: "pipe",
stderr: "pipe",
});
const stdout = new TextDecoder().decode(res.stdout);
if (res.exitCode !== 0) {
const stderr = new TextDecoder().decode(res.stderr).trim();
throw new Error(stderr || `read_tools 退出码 ${res.exitCode}`);
}
return stdout;
}
// Node 运行时的回退路径。
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { execFileSync } = require("node:child_process");
return execFileSync(python, [script, ...argv], {
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
});
}
export default function (pi: ExtensionAPI) {
const python = process.env.MUSE_READ_TOOLS_PYTHON;
const script = process.env.MUSE_READ_TOOLS_SCRIPT;
if (!python || !script) {
return; // 非工具 server 派发:不注册任何工具。
}
let registry: Record<string, ToolSpec> = {};
try {
registry = JSON.parse(runReadTools(python, script, ["--list"]));
} catch {
return; // 登记表不可读时保持无工具,不放行裸查询。
}
for (const [name, spec] of Object.entries(registry)) {
const props: Record<string, any> = {};
for (const [rawName, argType] of Object.entries(spec.args ?? {})) {
const optional = rawName.endsWith("?");
const argName = optional ? rawName.slice(0, -1) : rawName;
const base =
argType === "int"
? Type.Integer({ description: argName })
: Type.String({ description: argName });
props[argName] = optional ? Type.Optional(base) : base;
}
pi.registerTool({
name,
label: name,
description: `${spec.description}(只读;读取 ${spec.tables.join("、")})`,
promptGuidelines: [
`Use ${name} only for read-only exploration; it never modifies data.`,
],
parameters: Type.Object(props),
async execute(_toolCallId, params) {
try {
const stdout = runReadTools(python, script, [
name,
JSON.stringify(params ?? {}),
]);
return { content: [{ type: "text", text: stdout }] };
} catch (error) {
// 抛出错误使框架把该工具调用标记为失败,模型可据此调整探索。
throw new Error(
error instanceof Error ? error.message : String(error)
);
}
},
});
}
}