Просмотр исходного кода

feat(core): add generic write tool alongside edit

Ma 6 месяцев назад
Родитель
Сommit
5619adfa7e

+ 2 - 1
packages/core/src/__tests__/agent-system-prompt.test.ts

@@ -60,6 +60,7 @@ describe("buildAgentSystemPrompt", () => {
       expect(prompt).toContain("rename_entity");
       expect(prompt).toContain("patch_chapter_text");
       expect(prompt).toContain("edit");
+      expect(prompt).toContain("write");
       expect(prompt).toContain("grep");
       expect(prompt).toContain("ls");
     });
@@ -70,7 +71,7 @@ describe("buildAgentSystemPrompt", () => {
       expect(prompt).toContain("write_truth_file");
       expect(prompt).toContain("用户要求重写/精修已有章节");
       expect(prompt).toContain("revise_chapter");
-      expect(prompt).toContain("只有当上述专用工具都不适合");
+      expect(prompt).toContain("也可以直接使用 edit / write");
     });
 
     it("Chinese prompt warns NOT to call architect", () => {

+ 14 - 0
packages/core/src/__tests__/agent-tools.test.ts

@@ -8,6 +8,7 @@ import {
   createPatchChapterTextTool,
   createRenameEntityTool,
   createReviseChapterTool,
+  createWriteFileTool,
   createWriteTruthFileTool,
 } from "../agent/agent-tools.js";
 
@@ -220,4 +221,17 @@ describe("agent deterministic writing tools", () => {
       expect(result.content[0].text).toContain(".md");
     }
   });
+
+  it("creates nested files through the generic write tool", async () => {
+    const tool = createWriteFileTool(root);
+
+    const result = await tool.execute("tool-10", {
+      path: "harbor/story/runtime/notes.md",
+      content: "# Notes\n\nWatch the harbor ledger.\n",
+    });
+
+    expect(result.content[0]?.type).toBe("text");
+    await expect(readFile(join(state.bookDir("harbor"), "story", "runtime", "notes.md"), "utf-8"))
+      .resolves.toContain("Watch the harbor ledger");
+  });
 });

+ 2 - 0
packages/core/src/agent/agent-session.ts

@@ -11,6 +11,7 @@ import {
   createReviseChapterTool,
   createReadTool,
   createEditTool,
+  createWriteFileTool,
   createGrepTool,
   createLsTool,
   createWriteTruthFileTool,
@@ -241,6 +242,7 @@ export async function runAgentSession(
           createRenameEntityTool(pipeline, projectRoot, bookId),
           createPatchChapterTextTool(pipeline, projectRoot, bookId),
           createEditTool(projectRoot),
+          createWriteFileTool(projectRoot),
           createGrepTool(projectRoot),
           createLsTool(projectRoot),
         ],

+ 6 - 4
packages/core/src/agent/agent-system-prompt.ts

@@ -84,7 +84,8 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string):
 - **write_truth_file** — 整文件覆盖真相文件(story_bible、volume_outline、book_rules、current_focus 等)
 - **rename_entity** — 统一改角色/实体名
 - **patch_chapter_text** — 对已有章节做局部定点修补
-- **edit** — 通用精确字符串替换编辑(仅在专用 deterministic 工具都不适合时使用)
+- **edit** — 通用精确字符串替换编辑
+- **write** — 新建或整文件覆盖写入
 - **grep** — 搜索内容(如"哪一章提到了某个角色")
 - **ls** — 列出文件或章节
 
@@ -96,7 +97,7 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string):
 - 用户要求重写/精修已有章节 → 用 revise_chapter
 - 用户要求角色或实体改名 → 用 rename_entity
 - 用户要求对某一章做局部小修 → 用 patch_chapter_text
-- 只有当上述专用工具都不适合,而且你已精确读取并定位文本时,才使用 edit
+- 当你已经明确目标文件和内容时,也可以直接使用 edit / write
 - 其他情况 → 直接对话回答
 - **注意:不要调用 architect,当前已有书籍,不需要建书**
 - **不要在回复中添加表情符号**
@@ -136,7 +137,8 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string):
 - **write_truth_file** — Replace a canonical truth file in story/
 - **rename_entity** — Rename a character or entity across the book
 - **patch_chapter_text** — Apply a local deterministic patch to a chapter
-- **edit** — Generic exact string replacement editor (use only when the dedicated deterministic tools do not fit)
+- **edit** — Generic exact string replacement editor
+- **write** — Create or fully overwrite a file
 - **grep** — Search content across chapters
 - **ls** — List files or chapters
 
@@ -148,7 +150,7 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string):
 - Use revise_chapter for rewrite/polish/rework of existing chapters
 - Use rename_entity for character/entity renames
 - Use patch_chapter_text for local chapter fixes
-- Use edit only as a last-resort generic editor after you have read the exact file content
+- Use edit / write directly when you already know the exact target file and replacement content
 - Chat directly for other questions
 - **Do NOT call architect — a book already exists**
 - **Do NOT use emoji in your responses**

+ 38 - 1
packages/core/src/agent/agent-tools.ts

@@ -400,7 +400,44 @@ export function createEditTool(projectRoot: string): AgentTool<typeof EditParams
 }
 
 // ---------------------------------------------------------------------------
-// 4. Grep Tool
+// 4. Write Tool
+// ---------------------------------------------------------------------------
+
+const WriteFileParams = Type.Object({
+  path: Type.String({ description: "File path relative to books/" }),
+  content: Type.String({ description: "Full file content to write" }),
+});
+
+export function createWriteFileTool(projectRoot: string): AgentTool<typeof WriteFileParams> {
+  const booksRoot = join(projectRoot, "books");
+
+  return {
+    name: "write",
+    description:
+      "Create a new file or overwrite an existing file. " +
+      "Path is relative to books/. Parent directories are created automatically.",
+    label: "Write File",
+    parameters: WriteFileParams,
+    async execute(
+      _toolCallId: string,
+      params: Static<typeof WriteFileParams>,
+    ): Promise<AgentToolResult<undefined>> {
+      try {
+        const filePath = safeBooksPath(booksRoot, params.path);
+        const parentDir = resolve(filePath, "..");
+        const { mkdir } = await import("node:fs/promises");
+        await mkdir(parentDir, { recursive: true });
+        await writeFile(filePath, params.content, "utf-8");
+        return textResult(`File "${params.path}" written successfully.`);
+      } catch (err: any) {
+        return textResult(`Failed to write "${params.path}": ${err?.message ?? String(err)}`);
+      }
+    },
+  };
+}
+
+// ---------------------------------------------------------------------------
+// 5. Grep Tool
 // ---------------------------------------------------------------------------
 
 const GrepParams = Type.Object({

+ 1 - 0
packages/core/src/agent/index.ts

@@ -7,6 +7,7 @@ export {
   createRenameEntityTool,
   createPatchChapterTextTool,
   createEditTool,
+  createWriteFileTool,
   createGrepTool,
   createLsTool,
 } from "./agent-tools.js";