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:
2026-07-07 20:07:02 +08:00
parent afaf5bee09
commit a2c8fa8eaf
17 changed files with 516 additions and 808 deletions
+8 -10
View File
@@ -10,23 +10,22 @@
* ADR-0003-authorized tool (chat must match binding).
* 6. On finish/fail/timeout: release the lock, post a status card.
*
* The agent provider/model come from the ModelRegistry (ADR-0017 role-based
* The agent model id comes from the ModelRegistry (ADR-0017 role-based
* routing, role-as-data). The trigger does not pick a model directly.
*/
import type { PrismaClient } from "@prisma/client";
import type { FastifyBaseLogger } from "fastify";
import { sendText, sendCard, buildRunStatusCard, type FeishuRuntime, type MessageReceiveEvent } from "./client.js";
import type { AgentProvider } from "../agent/provider.js";
import type { ToolRegistry } from "../agent/tools.js";
import type { ModelRegistry } from "../agent/models.js";
import { runAgent, type ProjectContext } from "../agent/runner.js";
import { runAgent, type ModelFactory, type ProjectContext } from "../agent/runner.js";
import { acquireLock, currentLockRunId, releaseLock } from "../lock.js";
import { canTriggerAgent, canTriggerRole } from "../permission.js";
import { transcriptPath } from "../agent/transcript.js";
interface TriggerDeps {
readonly prisma: PrismaClient;
readonly provider: AgentProvider;
readonly modelFactory: ModelFactory;
readonly tools: ToolRegistry;
readonly models: ModelRegistry;
readonly logger: FastifyBaseLogger;
@@ -150,13 +149,11 @@ export function makeTriggerHandler(deps: TriggerDeps) {
}
const role = deps.models.role(roleId);
const model = deps.models.resolve(undefined, roleId);
const providerId = deps.provider.id;
const providerId = "openrouter";
// Per-role bundle (ADR-0017): systemPrompt seeds persona; tools whitelist
// is enforced by constructing a per-run registry subset. `tools: undefined`
// means the full registered set (unrestricted role).
// is enforced inside runAgent when it builds the SDK tools record. `tools:
// undefined` means the full registered set (unrestricted role).
const systemPrompt = role?.systemPrompt;
const runTools =
role?.tools !== undefined ? deps.tools.subset(role.tools) : deps.tools;
// ADR-0017: provider+model-bound session. Reuse if one exists for this
// (project, provider, model) triple; otherwise create.
@@ -220,11 +217,12 @@ export function makeTriggerHandler(deps: TriggerDeps) {
// Run the agent — fire-and-forget from the ws handler's perspective.
// Per-run tools + systemPrompt come from the role bundle (ADR-0017).
runAgent(deps.provider, runTools, {
runAgent(deps.modelFactory, deps.tools, {
prompt: cleanPrompt,
model,
project: projectCtx,
systemPrompt,
toolWhitelist: role?.tools,
transcriptPath: transcriptPath(project.workspaceDir, session.id),
})
.then(async (result) => {