mirror of
https://github.com/Narcooo/inkos
synced 2026-04-21 14:37:16 +00:00
Merge pull request #195 from SttFang/feat/sub-agent-params-alignment
Some checks are pending
CI / build-and-test (20, ubuntu-latest) (push) Waiting to run
CI / build-and-test (20, windows-latest) (push) Waiting to run
CI / build-and-test (22, ubuntu-latest) (push) Waiting to run
CI / build-and-test (22, windows-latest) (push) Waiting to run
CI / verify-pack (push) Blocked by required conditions
Some checks are pending
CI / build-and-test (20, ubuntu-latest) (push) Waiting to run
CI / build-and-test (20, windows-latest) (push) Waiting to run
CI / build-and-test (22, ubuntu-latest) (push) Waiting to run
CI / build-and-test (22, windows-latest) (push) Waiting to run
CI / verify-pack (push) Blocked by required conditions
feat: sub_agent tool params alignment with pipeline methods
This commit is contained in:
commit
52250539db
4 changed files with 222 additions and 44 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
150
packages/core/src/__tests__/agent-tools-params.test.ts
Normal file
150
packages/core/src/__tests__/agent-tools-params.test.ts
Normal file
|
|
@ -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<typeof vi.fn>;
|
||||
let tool: ReturnType<typeof createSubAgentTool>;
|
||||
|
||||
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<typeof vi.fn>;
|
||||
let tool: ReturnType<typeof createSubAgentTool>;
|
||||
|
||||
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<typeof vi.fn>;
|
||||
let tool: ReturnType<typeof createSubAgentTool>;
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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,21 +73,17 @@ 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 等)
|
||||
- **rename_entity** — 统一改角色/实体名
|
||||
- **patch_chapter_text** — 对已有章节做局部定点修补
|
||||
- **edit** — 通用精确字符串替换编辑
|
||||
- **write** — 新建或整文件覆盖写入
|
||||
- **edit** — 在设定文件里做精确字符串替换(章节正文请用 patch_chapter_text)
|
||||
- **write** — 新建文件,或者重写整个文件(已有内容会被覆盖;真相文件优先用 write_truth_file,章节修订用 revise_chapter)
|
||||
- **grep** — 搜索内容(如"哪一章提到了某个角色")
|
||||
- **ls** — 列出文件或章节
|
||||
|
||||
|
|
@ -124,21 +122,17 @@ 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/
|
||||
- **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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AgentToolResult<undefined>> {
|
||||
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}".`);
|
||||
|
|
@ -371,8 +393,9 @@ export function createEditTool(projectRoot: string): AgentTool<typeof EditParams
|
|||
return {
|
||||
name: "edit",
|
||||
description:
|
||||
"Edit a file using exact string replacement. " +
|
||||
"old_string must appear exactly once in the file. Path is relative to books/.",
|
||||
"Edit a file under books/ via exact string replacement. " +
|
||||
"old_string must appear exactly once in the file. " +
|
||||
"For chapter text use patch_chapter_text; for canonical truth files (story_bible/volume_outline/book_rules/current_focus) prefer write_truth_file.",
|
||||
label: "Edit File",
|
||||
parameters: EditParams,
|
||||
async execute(
|
||||
|
|
@ -414,8 +437,9 @@ export function createWriteFileTool(projectRoot: string): AgentTool<typeof Write
|
|||
return {
|
||||
name: "write",
|
||||
description:
|
||||
"Create a new file or overwrite an existing file. " +
|
||||
"Path is relative to books/. Parent directories are created automatically.",
|
||||
"Create a new file, or fully replace an existing file's content under books/. " +
|
||||
"Parent directories are created automatically. Existing content is overwritten silently — " +
|
||||
"for canonical truth files prefer write_truth_file; for chapter revisions use revise_chapter.",
|
||||
label: "Write File",
|
||||
parameters: WriteFileParams,
|
||||
async execute(
|
||||
|
|
|
|||
Loading…
Reference in a new issue