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:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Session transcript — append-only JSONL file in the workspace.
|
||||
*
|
||||
* Each line is one {@link Message}, JSON-encoded. The transcript lives at
|
||||
* Each line is one AI SDK {@link ModelMessage}, JSON-encoded. The transcript lives at
|
||||
* `workspaceDir/.cph/sessions/<sessionId>.jsonl`, following the same "workspace
|
||||
* is a directory tree" principle as the engineering file itself (ADR-0007).
|
||||
*
|
||||
@@ -15,9 +15,9 @@
|
||||
* distinct; storing session messages does not conflict with "the Hub avoids
|
||||
* becoming a full chat history store."
|
||||
*/
|
||||
import { readFile, writeFile, mkdir, appendFile } from "node:fs/promises";
|
||||
import { readFile, mkdir, appendFile } from "node:fs/promises";
|
||||
import { join, dirname } from "node:path";
|
||||
import type { Message } from "./provider.js";
|
||||
import type { ModelMessage } from "ai";
|
||||
|
||||
/// Subdirectory within a workspace where session transcripts live.
|
||||
export const TRANSCRIPT_DIR = ".cph/sessions";
|
||||
@@ -33,19 +33,19 @@ export function transcriptPath(workspaceDir: string, sessionId: string): string
|
||||
* skipped — a partially-written last line (crash mid-append) won't break the
|
||||
* next run.
|
||||
*/
|
||||
export async function readTranscript(path: string): Promise<Message[]> {
|
||||
export async function readTranscript(path: string): Promise<ModelMessage[]> {
|
||||
let content: string;
|
||||
try {
|
||||
content = await readFile(path, "utf8");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const messages: Message[] = [];
|
||||
const messages: ModelMessage[] = [];
|
||||
for (const line of content.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === "") continue;
|
||||
try {
|
||||
messages.push(JSON.parse(trimmed) as Message);
|
||||
messages.push(JSON.parse(trimmed) as ModelMessage);
|
||||
} catch {
|
||||
// Skip corrupt line — likely a partial write from a crash.
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export async function readTranscript(path: string): Promise<Message[]> {
|
||||
* Append messages to a transcript file. Creates the parent directory if needed.
|
||||
* Each message is written as one JSON line.
|
||||
*/
|
||||
export async function appendTranscript(path: string, messages: readonly Message[]): Promise<void> {
|
||||
export async function appendTranscript(path: string, messages: readonly ModelMessage[]): Promise<void> {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
const lines = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
|
||||
await appendFile(path, lines, "utf8");
|
||||
|
||||
Reference in New Issue
Block a user