forked from EduCraft/curriculum-project-hub
refactor(hub): 手搓 agent loop 换成 Vercel AI SDK
将 provider/runner/tools 三件手搓轮子替换为 AI SDK v7 的 generateText + tool:
- 删 openrouter-provider.ts(翻译层消失)、provider.ts 自定义消息类型
- runner.ts 循环体改为 generateText({stopWhen: stepCountIs}),保留 RunRequest/RunResult 公共签名
- tools.ts ToolRegistry 改为 ToolFactory + build(ctx, names?) 按 role 白名单构建 tools record
- workspace/cph/feishu 工具改写为 tool() 定义,execute 闭包捕获 per-run ToolContext
- transcript.ts 改为 ModelMessage[] JSONL(0.0.0 cutover,无迁移)
- server.ts/trigger.ts 接 modelFactory + toolWhitelist
- 测试:helpers 写 MockLanguageModel implements LanguageModelV4;5 个测试改写 + 新增 runner.test.ts
- 移除未使用的 openai 依赖
- ADR-0017 Consequences 同步:loop 委托给 AI SDK,provider seam 是 LanguageModel
tsc 干净,54 tests 全过。
This commit is contained in:
+33
-56
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* `cph` subprocess tools — the Hub↔Courseware coupling point.
|
||||
* `cph` subprocess tools - the Hub<->Courseware coupling point.
|
||||
*
|
||||
* The Hub agent does not parse engineering-file models in-process; it calls the
|
||||
* stable `cph` CLI (ADR-0016 version contract). `cph check` validates a
|
||||
* project; `cph build` renders a target artifact. Version compatibility is the
|
||||
* CLI's responsibility — it refuses incompatible `.cph-version` files with a
|
||||
* CLI's responsibility - it refuses incompatible `.cph-version` files with a
|
||||
* `cphVersionMismatch` error diagnostic (ADR-0016); the Hub merely runs the
|
||||
* subprocess and returns its stdout/stderr/exit to the model.
|
||||
*
|
||||
@@ -14,7 +14,9 @@
|
||||
*/
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import type { RegisteredTool } from "./tools.js";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import type { ToolContext, ToolDefinition } from "./tools.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -53,30 +55,19 @@ async function runCph(args: readonly string[], workspaceDir: string): Promise<Ex
|
||||
}
|
||||
}
|
||||
|
||||
interface CphCheckArgs {
|
||||
readonly path?: string;
|
||||
}
|
||||
|
||||
/// `cph check <dir>` — validate a curriculum engineering file. Returns the
|
||||
/// `cph check <dir>` - validate a curriculum engineering file. Returns the
|
||||
/// checker's diagnostics (stdout) for the model to act on. Non-zero exit means
|
||||
/// the file has error diagnostics (ADR-0010); the output is still returned,
|
||||
/// not thrown — the model needs to see the diagnostics to fix them.
|
||||
export function cphCheckTool(): RegisteredTool {
|
||||
return {
|
||||
spec: {
|
||||
name: "cph_check",
|
||||
description:
|
||||
"Run `cph check` on the project's curriculum engineering file. Returns diagnostics (7 classes: structure/schema/compile/version…). Non-zero exit means error diagnostics present — read the output to fix them.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Path to the engineering file dir; defaults to the workspace root." },
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(args, ctx): Promise<string> {
|
||||
const a = (args as CphCheckArgs) ?? {};
|
||||
const target = a.path ?? ".";
|
||||
/// not thrown - the model needs to see the diagnostics to fix them.
|
||||
export function cphCheckTool(ctx: ToolContext): ToolDefinition {
|
||||
return tool({
|
||||
description:
|
||||
"Run `cph check` on the project's curriculum engineering file. Returns diagnostics (7 classes: structure/schema/compile/version...). Non-zero exit means error diagnostics present - read the output to fix them.",
|
||||
inputSchema: z.object({
|
||||
path: z.string().describe("Path to the engineering file dir; defaults to the workspace root.").optional(),
|
||||
}),
|
||||
execute: async (args): Promise<string> => {
|
||||
const target = args.path ?? ".";
|
||||
const res = await runCph(["check", target], ctx.workspaceDir);
|
||||
return JSON.stringify({
|
||||
exitCode: res.exitCode,
|
||||
@@ -84,39 +75,25 @@ export function cphCheckTool(): RegisteredTool {
|
||||
stderr: res.stderr,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
interface CphBuildArgs {
|
||||
readonly target: string;
|
||||
readonly path?: string;
|
||||
readonly output?: string;
|
||||
}
|
||||
|
||||
/// `cph build <dir> --target <T> -o <out>` — render a target artifact. The
|
||||
/// target (student/teacher/…) determines the build (ADR-0009). Output path
|
||||
/// `cph build <dir> --target <T> -o <out>` - render a target artifact. The
|
||||
/// target (student/teacher/...) determines the build (ADR-0009). Output path
|
||||
/// is relative to the workspace unless absolute.
|
||||
export function cphBuildTool(): RegisteredTool {
|
||||
return {
|
||||
spec: {
|
||||
name: "cph_build",
|
||||
description:
|
||||
"Run `cph build` to render a curriculum artifact (e.g. student/teacher PDF). Target selects the build; output names the result file.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
target: { type: "string", description: "Build target, e.g. 'student', 'teacher'." },
|
||||
path: { type: "string", description: "Path to the engineering file dir; defaults to the workspace root." },
|
||||
output: { type: "string", description: "Output file path (relative to workspace or absolute)." },
|
||||
},
|
||||
required: ["target"],
|
||||
},
|
||||
},
|
||||
async execute(args, ctx): Promise<string> {
|
||||
const a = args as CphBuildArgs;
|
||||
const target = a.path ?? ".";
|
||||
const out = a.output ?? `build/${a.target}.pdf`;
|
||||
const res = await runCph(["build", target, "--target", a.target, "-o", out], ctx.workspaceDir);
|
||||
export function cphBuildTool(ctx: ToolContext): ToolDefinition {
|
||||
return tool({
|
||||
description:
|
||||
"Run `cph build` to render a curriculum artifact (e.g. student/teacher PDF). Target selects the build; output names the result file.",
|
||||
inputSchema: z.object({
|
||||
target: z.string().describe("Build target, e.g. 'student', 'teacher'."),
|
||||
path: z.string().describe("Path to the engineering file dir; defaults to the workspace root.").optional(),
|
||||
output: z.string().describe("Output file path (relative to workspace or absolute).").optional(),
|
||||
}),
|
||||
execute: async (args): Promise<string> => {
|
||||
const target = args.path ?? ".";
|
||||
const out = args.output ?? `build/${args.target}.pdf`;
|
||||
const res = await runCph(["build", target, "--target", args.target, "-o", out], ctx.workspaceDir);
|
||||
return JSON.stringify({
|
||||
exitCode: res.exitCode,
|
||||
stdout: res.stdout,
|
||||
@@ -124,5 +101,5 @@ export function cphBuildTool(): RegisteredTool {
|
||||
output: out,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user