From 31d170a9e07c246020952e1e301f3c6a053a85e1 Mon Sep 17 00:00:00 2001 From: fanghanjun <17737663888@163.com> Date: Thu, 16 Apr 2026 10:04:09 -0700 Subject: [PATCH 1/2] feat(core): align sub_agent tool params with pipeline runner methods Extend SubAgentParams schema with explicit architect params (genre, platform, language, targetChapters) so the architect agent stops hardcoding initBook config. Auditor now returns issue severity + description list. Reviser reads mode directly from the schema instead of regex-matching the instruction. System prompt and tests are updated in lockstep with the schema. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/__tests__/agent-system-prompt.test.ts | 10 ++ .../src/__tests__/agent-tools-params.test.ts | 150 ++++++++++++++++++ .../core/src/agent/agent-system-prompt.ts | 26 ++- packages/core/src/agent/agent-tools.ts | 62 +++++--- 4 files changed, 212 insertions(+), 36 deletions(-) create mode 100644 packages/core/src/__tests__/agent-tools-params.test.ts diff --git a/packages/core/src/__tests__/agent-system-prompt.test.ts b/packages/core/src/__tests__/agent-system-prompt.test.ts index e4b56b22..4ddf6f76 100644 --- a/packages/core/src/__tests__/agent-system-prompt.test.ts +++ b/packages/core/src/__tests__/agent-system-prompt.test.ts @@ -99,5 +99,15 @@ describe("buildAgentSystemPrompt", () => { // architect 不在可用工具列表里 expect(prompt).not.toMatch(/agent="architect"/); }); + + it("book-mode prompt documents all sub_agent params", () => { + const prompt = buildAgentSystemPrompt("test-book", "zh"); + expect(prompt).toContain("chapterWordCount"); + expect(prompt).toContain("chapterNumber"); + expect(prompt).toContain("mode"); + expect(prompt).toContain("anti-detect"); + expect(prompt).toContain("format"); + expect(prompt).toContain("approvedOnly"); + }); }); }); diff --git a/packages/core/src/__tests__/agent-tools-params.test.ts b/packages/core/src/__tests__/agent-tools-params.test.ts new file mode 100644 index 00000000..62277e59 --- /dev/null +++ b/packages/core/src/__tests__/agent-tools-params.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { createSubAgentTool } from "../agent/agent-tools.js"; + +describe("SubAgentParams schema", () => { + const mockPipeline = {} as any; + const tool = createSubAgentTool(mockPipeline, null); + const schema = tool.parameters; + const props = (schema as any).properties; + + it("has architect params: title, genre, platform, language, targetChapters", () => { + expect(props.title).toBeDefined(); + expect(props.genre).toBeDefined(); + expect(props.platform).toBeDefined(); + expect(props.language).toBeDefined(); + expect(props.targetChapters).toBeDefined(); + }); + + it("has writer/architect param: chapterWordCount", () => { + expect(props.chapterWordCount).toBeDefined(); + }); + + it("has reviser param: mode", () => { + expect(props.mode).toBeDefined(); + }); + + it("has exporter params: format, approvedOnly", () => { + expect(props.format).toBeDefined(); + expect(props.approvedOnly).toBeDefined(); + }); + + it("has existing params: agent, instruction, bookId, chapterNumber", () => { + expect(props.agent).toBeDefined(); + expect(props.instruction).toBeDefined(); + expect(props.bookId).toBeDefined(); + expect(props.chapterNumber).toBeDefined(); + }); + + it("all new params have description with agent scope", () => { + expect(props.title.description).toMatch(/architect/i); + expect(props.genre.description).toMatch(/architect/i); + expect(props.mode.description).toMatch(/reviser/i); + expect(props.format.description).toMatch(/exporter/i); + }); +}); + +describe("architect agent — BookConfig construction", () => { + let initBookMock: ReturnType; + let tool: ReturnType; + + beforeEach(() => { + initBookMock = vi.fn(async () => {}); + const mockPipeline = { initBook: initBookMock } as any; + tool = createSubAgentTool(mockPipeline, null); + }); + + it("passes complete BookConfig with schema params", async () => { + await tool.execute("tc1", { + agent: "architect", + instruction: "Create a xuanhuan novel", + title: "天道独行", + genre: "xuanhuan", + platform: "tomato", + language: "zh", + targetChapters: 100, + chapterWordCount: 4000, + }); + expect(initBookMock).toHaveBeenCalledOnce(); + const [bookConfig, options] = initBookMock.mock.calls[0]; + expect(bookConfig.title).toBe("天道独行"); + expect(bookConfig.genre).toBe("xuanhuan"); + expect(bookConfig.platform).toBe("tomato"); + expect(bookConfig.language).toBe("zh"); + expect(bookConfig.targetChapters).toBe(100); + expect(bookConfig.chapterWordCount).toBe(4000); + expect(bookConfig.status).toBe("outlining"); + expect(bookConfig.createdAt).toBeDefined(); + expect(options.externalContext).toBe("Create a xuanhuan novel"); + }); + + it("uses defaults when optional params are omitted", async () => { + await tool.execute("tc2", { agent: "architect", instruction: "Create a book", title: "Test Book" }); + const [bookConfig] = initBookMock.mock.calls[0]; + expect(bookConfig.genre).toBe("general"); + expect(bookConfig.platform).toBe("other"); + expect(bookConfig.language).toBe("zh"); + expect(bookConfig.targetChapters).toBe(200); + expect(bookConfig.chapterWordCount).toBe(3000); + }); +}); + +describe("writer agent — wordCount passthrough", () => { + let writeNextChapterMock: ReturnType; + let tool: ReturnType; + + beforeEach(() => { + writeNextChapterMock = vi.fn(async () => ({ wordCount: 3000 })); + const mockPipeline = { writeNextChapter: writeNextChapterMock } as any; + tool = createSubAgentTool(mockPipeline, "existing-book"); + }); + + it("passes chapterWordCount as wordCount", async () => { + await tool.execute("tc1", { agent: "writer", instruction: "Write", bookId: "my-book", chapterWordCount: 5000 }); + expect(writeNextChapterMock).toHaveBeenCalledWith("my-book", 5000); + }); + + it("passes undefined when chapterWordCount omitted", async () => { + await tool.execute("tc2", { agent: "writer", instruction: "Write", bookId: "my-book" }); + expect(writeNextChapterMock).toHaveBeenCalledWith("my-book", undefined); + }); +}); + +describe("auditor agent — rich return value", () => { + it("returns issue details with severity", async () => { + const auditDraftMock = vi.fn(async () => ({ + chapterNumber: 3, passed: false, + issues: [ + { severity: "warning", description: "Pacing too fast" }, + { severity: "critical", description: "Name inconsistency" }, + ], + })); + const tool = createSubAgentTool({ auditDraft: auditDraftMock } as any, "book"); + const result = await tool.execute("tc1", { agent: "auditor", instruction: "Audit", bookId: "my-book", chapterNumber: 3 }); + const text = (result.content[0] as { type: "text"; text: string }).text; + expect(text).toContain("FAILED"); + expect(text).toContain("2 issue(s)"); + expect(text).toContain("[warning]"); + expect(text).toContain("[critical]"); + expect(text).toContain("Pacing too fast"); + }); +}); + +describe("reviser agent — mode field", () => { + let reviseDraftMock: ReturnType; + let tool: ReturnType; + + beforeEach(() => { + reviseDraftMock = vi.fn(async () => ({})); + tool = createSubAgentTool({ reviseDraft: reviseDraftMock } as any, "book"); + }); + + it("uses mode param directly", async () => { + await tool.execute("tc1", { agent: "reviser", instruction: "Fix", bookId: "my-book", chapterNumber: 5, mode: "anti-detect" }); + expect(reviseDraftMock).toHaveBeenCalledWith("my-book", 5, "anti-detect"); + }); + + it("defaults to spot-fix", async () => { + await tool.execute("tc2", { agent: "reviser", instruction: "Fix", bookId: "my-book" }); + expect(reviseDraftMock).toHaveBeenCalledWith("my-book", undefined, "spot-fix"); + }); +}); diff --git a/packages/core/src/agent/agent-system-prompt.ts b/packages/core/src/agent/agent-system-prompt.ts index 88202774..ba76cbf0 100644 --- a/packages/core/src/agent/agent-system-prompt.ts +++ b/packages/core/src/agent/agent-system-prompt.ts @@ -17,6 +17,7 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string): 2. **确认建书**(调用阶段)— 当信息足够时,调用 sub_agent 工具委托 architect 子智能体建书: - 必须显式传入 "title" 参数,不能留空 + - 同时传入结构化参数:genre(题材)、platform(平台)、language(语言)、targetChapters(章数)、chapterWordCount(每章字数) - instruction 中包含收集到的所有信息(题材、世界观、主角、冲突等) - architect 会生成完整的 foundation(世界观设定、卷纲规划、叙事规则等) @@ -47,6 +48,7 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string): 2. **Create book** — When you have enough info, call the sub_agent tool with agent="architect": - Pass the explicit "title" parameter; do not leave it empty + - Pass structured params: genre, platform, language, targetChapters, chapterWordCount - Include all collected info in the instruction - The architect will generate the complete foundation @@ -71,14 +73,10 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string): ## 可用工具 - **sub_agent** — 委托子智能体执行重操作: - - agent="writer" 写下一章(支持 chapterWordCount) - - agent="auditor" 审计章节质量(可指定 chapterNumber) - - agent="reviser" 修订章节(可指定 chapterNumber 和 mode) - - agent="exporter" 导出书籍(可指定 format 和 approvedOnly) - - **chapterNumber 参数**:auditor 和 reviser 支持指定章节号,不指定则默认最新章节 - - **chapterWordCount 参数**:writer 可指定本章目标字数 - - **mode 参数**:reviser 可显式指定修订模式 - - **format / approvedOnly 参数**:exporter 可显式指定导出格式和是否仅导出已通过章节 + - agent="writer" 写下一章(参数:chapterWordCount 覆盖字数) + - agent="auditor" 审计章节质量(参数:chapterNumber 指定章节) + - agent="reviser" 修订章节(参数:chapterNumber, mode: spot-fix/polish/rewrite/rework/anti-detect) + - agent="exporter" 导出书籍(参数:format: txt/md/epub, approvedOnly: true/false) - **read** — 读取书籍的设定文件或章节内容 - **revise_chapter** — 对已有章节做精修/重写/返工 - **write_truth_file** — 整文件覆盖真相文件(story_bible、volume_outline、book_rules、current_focus 等) @@ -124,14 +122,10 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string): ## Available Tools - **sub_agent** — Delegate to sub-agents: - - agent="writer" for writing next chapter (supports chapterWordCount) - - agent="auditor" for chapter quality audit (supports chapterNumber) - - agent="reviser" for chapter revision (supports chapterNumber and explicit mode) - - agent="exporter" for book export (supports format and approvedOnly) - - **chapterNumber param**: auditor and reviser accept an explicit chapter number; omit for latest - - **chapterWordCount param**: writer can override the chapter target word count - - **mode param**: reviser can explicitly choose the revision mode - - **format / approvedOnly params**: exporter can explicitly choose the export format and approval filter + - agent="writer" write next chapter (params: chapterWordCount for word count override) + - agent="auditor" audit chapter quality (params: chapterNumber to target specific chapter) + - agent="reviser" revise chapter (params: chapterNumber, mode: spot-fix/polish/rewrite/rework/anti-detect) + - agent="exporter" export book (params: format: txt/md/epub, approvedOnly: true/false) - **read** — Read truth files or chapter content - **revise_chapter** — Rewrite or polish an existing chapter - **write_truth_file** — Replace a canonical truth file in story/ diff --git a/packages/core/src/agent/agent-tools.ts b/packages/core/src/agent/agent-tools.ts index 068ce3d5..d1a2a61f 100644 --- a/packages/core/src/agent/agent-tools.ts +++ b/packages/core/src/agent/agent-tools.ts @@ -57,24 +57,39 @@ const SubAgentParams = Type.Object({ Type.Literal("reviser"), Type.Literal("exporter"), ]), - instruction: Type.String({ description: "Natural language instruction from the main Agent" }), + instruction: Type.String({ description: "Natural language instruction for the sub-agent" }), bookId: Type.Optional(Type.String({ description: "Book ID — required for all agents except architect" })), - title: Type.Optional(Type.String({ description: "Architect only: explicit book title. Required when creating a book." })), - chapterNumber: Type.Optional(Type.Number({ description: "Target chapter number for auditor/reviser. Omit to use the latest chapter." })), - chapterWordCount: Type.Optional(Type.Number({ description: "Writer only: explicit chapterWordCount override." })), + chapterNumber: Type.Optional(Type.Number({ description: "auditor/reviser: target chapter number. Omit to use the latest chapter." })), + // -- architect params -- + title: Type.Optional(Type.String({ description: "architect only: explicit book title. Required when creating a book." })), + genre: Type.Optional(Type.String({ description: "architect only: genre (xuanhuan, urban, mystery, romance, scifi, fantasy, wuxia, general, etc.)" })), + platform: Type.Optional(Type.Union([ + Type.Literal("tomato"), + Type.Literal("qidian"), + Type.Literal("feilu"), + Type.Literal("other"), + ], { description: "architect only: target platform. Default: other" })), + language: Type.Optional(Type.Union([ + Type.Literal("zh"), + Type.Literal("en"), + ], { description: "architect only: writing language. Default: zh" })), + targetChapters: Type.Optional(Type.Number({ description: "architect only: total chapter count. Default: 200" })), + chapterWordCount: Type.Optional(Type.Number({ description: "architect/writer: words per chapter. Default: 3000" })), + // -- reviser params -- mode: Type.Optional(Type.Union([ Type.Literal("spot-fix"), Type.Literal("polish"), Type.Literal("rewrite"), Type.Literal("rework"), Type.Literal("anti-detect"), - ])), + ], { description: "reviser only: revision mode. Default: spot-fix" })), + // -- exporter params -- format: Type.Optional(Type.Union([ Type.Literal("txt"), Type.Literal("md"), Type.Literal("epub"), - ])), - approvedOnly: Type.Optional(Type.Boolean({ description: "Exporter only: export approved chapters only." })), + ], { description: "exporter only: export format. Default: txt" })), + approvedOnly: Type.Optional(Type.Boolean({ description: "exporter only: export only approved chapters. Default: false" })), }); function deriveBookIdFromTitle(title: string): string { @@ -105,7 +120,7 @@ export function createSubAgentTool( _signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback, ): Promise> { - const { agent, instruction, bookId, title, chapterNumber, chapterWordCount, mode, format, approvedOnly } = params; + const { agent, instruction, bookId, title, chapterNumber, genre, platform, language, targetChapters, chapterWordCount, mode, format, approvedOnly } = params; const progress = (msg: string) => { onUpdate?.(textResult(msg)); @@ -114,7 +129,6 @@ export function createSubAgentTool( try { switch (agent) { case "architect": { - // architect 只在没有书的时候可用(建书流程) if (activeBookId) { return textResult("当前已有书籍,不需要建书。如果你想创建新书,请先回到首页。"); } @@ -123,9 +137,21 @@ export function createSubAgentTool( return textResult('Error: title is required for the architect agent.'); } const id = bookId || deriveBookIdFromTitle(resolvedTitle) || `book-${Date.now().toString(36)}`; + const now = new Date().toISOString(); progress(`Starting architect for book "${id}"...`); await pipeline.initBook( - { id, genre: "general", title: resolvedTitle, language: "zh" } as any, + { + id, + title: resolvedTitle, + genre: genre ?? "general", + platform: (platform ?? "other") as any, + language: (language ?? "zh") as any, + status: "outlining" as any, + targetChapters: targetChapters ?? 200, + chapterWordCount: chapterWordCount ?? 3000, + createdAt: now, + updatedAt: now, + }, { externalContext: instruction }, ); progress(`Architect finished — book "${id}" foundation created.`); @@ -148,22 +174,18 @@ export function createSubAgentTool( progress(`Auditing chapter ${chapterNumber ?? "latest"} for "${bookId}"...`); const audit = await pipeline.auditDraft(bookId, chapterNumber); progress(`Audit complete for "${bookId}".`); - const issueCount = audit.issues?.length ?? 0; + const issueLines = (audit.issues ?? []) + .map((i: any) => `[${i.severity}] ${i.description}`) + .join("\n"); return textResult( - `Audit complete for "${bookId}": ${issueCount} issue(s) found. ` + - `Chapter ${audit.chapterNumber}.`, + `Audit chapter ${audit.chapterNumber}: ${audit.passed ? "PASSED" : "FAILED"}, ${(audit.issues ?? []).length} issue(s).` + + (issueLines ? `\n${issueLines}` : ""), ); } case "reviser": { if (!bookId) return textResult("Error: bookId is required for the reviser agent."); - const resolvedMode: ReviseMode = mode ?? (/rewrite|改写|重写/.test(instruction) - ? "rewrite" - : /polish|润色/.test(instruction) - ? "polish" - : /rework|返工/.test(instruction) - ? "rework" - : "spot-fix"); + const resolvedMode: ReviseMode = (mode as ReviseMode) ?? "spot-fix"; progress(`Revising "${bookId}" chapter ${chapterNumber ?? "latest"} in ${resolvedMode} mode...`); await pipeline.reviseDraft(bookId, chapterNumber, resolvedMode); progress(`Revision complete for "${bookId}".`); From 291f038e598a233d1758b9653aebca51d9fe92f8 Mon Sep 17 00:00:00 2001 From: fanghanjun <17737663888@163.com> Date: Thu, 16 Apr 2026 10:05:15 -0700 Subject: [PATCH 2/2] docs(core): clarify edit/write tool boundaries vs specialised tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generic edit and write tools were described abstractly. Spell out when to reach for specialised tools instead — patch_chapter_text for chapter text, write_truth_file for canonical truth files, revise_chapter for chapter revisions — so the agent stops overwriting chapters or truth files with the generic tools. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/core/src/agent/agent-system-prompt.ts | 8 ++++---- packages/core/src/agent/agent-tools.ts | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/core/src/agent/agent-system-prompt.ts b/packages/core/src/agent/agent-system-prompt.ts index ba76cbf0..a8469a34 100644 --- a/packages/core/src/agent/agent-system-prompt.ts +++ b/packages/core/src/agent/agent-system-prompt.ts @@ -82,8 +82,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** — 通用精确字符串替换编辑 -- **write** — 新建或整文件覆盖写入 +- **edit** — 在设定文件里做精确字符串替换(章节正文请用 patch_chapter_text) +- **write** — 新建文件,或者重写整个文件(已有内容会被覆盖;真相文件优先用 write_truth_file,章节修订用 revise_chapter) - **grep** — 搜索内容(如"哪一章提到了某个角色") - **ls** — 列出文件或章节 @@ -131,8 +131,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 -- **write** — Create or fully overwrite a file +- **edit** — Exact string replacement on setting files (use patch_chapter_text for chapter text) +- **write** — Create a new file, or fully replace an existing file's content (prefer write_truth_file for canonical truth files, revise_chapter for chapter revisions) - **grep** — Search content across chapters - **ls** — List files or chapters diff --git a/packages/core/src/agent/agent-tools.ts b/packages/core/src/agent/agent-tools.ts index d1a2a61f..9a061a67 100644 --- a/packages/core/src/agent/agent-tools.ts +++ b/packages/core/src/agent/agent-tools.ts @@ -393,8 +393,9 @@ export function createEditTool(projectRoot: string): AgentTool