forked from EduCraft/curriculum-project-hub
feat(hub): 结构化运行历史(A-3) + lock 心跳(A-5)
A-3 结构化历史: - 新增 AgentMessage 表(每条消息的 queryable 投影,transcript 文件仍是 agent 读回记忆) - 新增 AgentFileChange 表(run 期间文件变更 before/after hash + operation) - runner.ts: generateText 后 persistAgentMessages 落库 result.responseMessages - workspace.ts writeFileTool: 写前算 beforeHash(SHA-256),写后算 afterHash,通过 ctx.onFileChange sink 记录 - runner.ts flushFileChanges: 收集 sink 的变更批量写入 AgentFileChange - ToolContext 加 onFileChange sink 字段 - trigger.ts 接线 runId/sessionId/prisma 到 RunRequest A-5 lock 心跳: - ProjectAgentLock 加 heartbeatAt 列 - runner.ts onStepFinish 回调:每步 model step 后更新 heartbeatAt(吞错,lock 可能被 force-release) 60 tests 全过(tsc 干净)。
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
* surface; the Hub agent is narrower than a general coding agent.
|
||||
*/
|
||||
import { readFile, writeFile, readdir, mkdir } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join, resolve, relative, isAbsolute } from "node:path";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
@@ -67,8 +68,39 @@ export function writeFileTool(ctx: ToolContext): ToolDefinition {
|
||||
execute: async (args): Promise<string> => {
|
||||
try {
|
||||
const full = confine(args.path, ctx.workspaceDir);
|
||||
// A-3 file-change capture: compute before/after hashes around the write.
|
||||
// A hashing failure must not block the write — skip the sink in that case.
|
||||
let beforeHash: string | null = null;
|
||||
let afterHash: string | null = null;
|
||||
let operation: "CREATE" | "UPDATE" | null = null;
|
||||
try {
|
||||
let oldContent: string | null;
|
||||
try {
|
||||
oldContent = await readFile(full, "utf8");
|
||||
} catch (e) {
|
||||
if (e !== null && typeof e === "object" && "code" in e && e.code === "ENOENT") {
|
||||
oldContent = null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
afterHash = createHash("sha256").update(args.content).digest("hex");
|
||||
beforeHash = oldContent === null ? null : createHash("sha256").update(oldContent).digest("hex");
|
||||
if (beforeHash === null) {
|
||||
operation = "CREATE";
|
||||
} else if (beforeHash === afterHash) {
|
||||
operation = null;
|
||||
} else {
|
||||
operation = "UPDATE";
|
||||
}
|
||||
} catch {
|
||||
operation = null;
|
||||
}
|
||||
await mkdir(join(full, ".."), { recursive: true });
|
||||
await writeFile(full, args.content, "utf8");
|
||||
if (operation !== null && ctx.onFileChange !== undefined) {
|
||||
ctx.onFileChange({ path: args.path, operation, beforeHash, afterHash, diff: null });
|
||||
}
|
||||
return JSON.stringify({ ok: true, path: args.path, bytes: args.content.length });
|
||||
} catch (e) {
|
||||
return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
|
||||
|
||||
Reference in New Issue
Block a user