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

docs: add sub_agent params alignment implementation plan (8 tasks)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fanghanjun 6 месяцев назад
Родитель
Сommit
7ce5a0465f
1 измененных файлов с 713 добавлено и 0 удалено
  1. 713 0
      docs/plans/2026-04-15-sub-agent-params-alignment-plan.md

+ 713 - 0
docs/plans/2026-04-15-sub-agent-params-alignment-plan.md

@@ -0,0 +1,713 @@
+# sub_agent 参数对齐实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 让 sub_agent 的 5 个子智能体参数与 pipeline runner 方法完全对齐,消除硬编码和参数丢弃。
+
+**Architecture:** 在现有 `SubAgentParams` schema 上扁平扩展新字段(TypeBox description 标注适用 agent),修改每个 agent case 的调用代码,更新系统提示词。
+
+**Tech Stack:** TypeBox (schema), pi-agent-core (tool interface), vitest (tests)
+
+---
+
+### Task 1: 扩展 SubAgentParams schema
+
+**Files:**
+- Modify: `packages/core/src/agent/agent-tools.ts:32-43`
+- Test: `packages/core/src/__tests__/agent-tools-params.test.ts` (create)
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+// packages/core/src/__tests__/agent-tools-params.test.ts
+import { describe, it, expect } from "vitest";
+import { Type } from "@mariozechner/pi-ai";
+
+// We test the schema shape by importing and inspecting the compiled schema
+// Since SubAgentParams is not exported, we test via the tool's parameters property
+import { createSubAgentTool } from "../agent/agent-tools.js";
+
+describe("SubAgentParams schema", () => {
+  // Use a minimal mock pipeline
+  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);
+  });
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `pnpm --filter @actalk/inkos-core test -- --run agent-tools-params`
+Expected: FAIL — `props.title` is undefined (field doesn't exist yet)
+
+- [ ] **Step 3: Add new fields to SubAgentParams**
+
+In `packages/core/src/agent/agent-tools.ts`, replace the `SubAgentParams` definition:
+
+```ts
+const SubAgentParams = Type.Object({
+  agent: Type.Union([
+    Type.Literal("architect"),
+    Type.Literal("writer"),
+    Type.Literal("auditor"),
+    Type.Literal("reviser"),
+    Type.Literal("exporter"),
+  ]),
+  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" })),
+  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: book title" })),
+  genre: Type.Optional(Type.String({ description: "architect only: genre (xuanhuan, urban, mystery, romance, scifi, fantasy, wuxia, general, etc.)" })),
+  platform: Type.Optional(Type.String({ description: "architect only: target platform (tomato, qidian, feilu, other). Default: other" })),
+  language: Type.Optional(Type.String({ description: "architect only: writing language (zh/en). 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.String({ description: "reviser only: revision mode (spot-fix, polish, rewrite, rework, anti-detect). Default: spot-fix" })),
+  // -- exporter params --
+  format: Type.Optional(Type.String({ description: "exporter only: export format (txt, md, epub). Default: txt" })),
+  approvedOnly: Type.Optional(Type.Boolean({ description: "exporter only: export only approved chapters. Default: false" })),
+});
+```
+
+Update the destructuring in `execute`:
+
+```ts
+const { agent, instruction, bookId, chapterNumber, title, genre, platform, language, targetChapters, chapterWordCount, mode, format, approvedOnly } = params;
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `pnpm --filter @actalk/inkos-core test -- --run agent-tools-params`
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
+git commit -m "feat(core): extend SubAgentParams with architect/writer/reviser/exporter fields"
+```
+
+---
+
+### Task 2: Fix architect — 构建完整 BookConfig
+
+**Files:**
+- Modify: `packages/core/src/agent/agent-tools.ts:67-81` (architect case)
+- Test: `packages/core/src/__tests__/agent-tools-params.test.ts` (extend)
+
+- [ ] **Step 1: Write the failing test**
+
+Append to `agent-tools-params.test.ts`:
+
+```ts
+import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import { vi, beforeEach, afterEach } from "vitest";
+
+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 instead of hardcoded values", 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(bookConfig.updatedAt).toBeDefined();
+    expect(options.externalContext).toBe("Create a xuanhuan novel");
+  });
+
+  it("uses defaults when optional architect params are omitted", async () => {
+    await tool.execute("tc2", {
+      agent: "architect",
+      instruction: "Create a 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);
+  });
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `pnpm --filter @actalk/inkos-core test -- --run agent-tools-params`
+Expected: FAIL — `bookConfig.title` is `""`, `bookConfig.genre` is `"general"` (hardcoded)
+
+- [ ] **Step 3: Rewrite architect case**
+
+Replace the architect case in `agent-tools.ts`:
+
+```ts
+case "architect": {
+  if (activeBookId) {
+    return textResult("当前已有书籍,不需要建书。如果你想创建新书,请先回到首页。");
+  }
+  const id = bookId || `book-${Date.now().toString(36)}`;
+  const now = new Date().toISOString();
+  progress(`Starting architect for book "${id}"...`);
+  await pipeline.initBook(
+    {
+      id,
+      title: title ?? "",
+      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.`);
+  return textResult(`Book "${id}" initialised successfully. Foundation files are ready.`);
+}
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `pnpm --filter @actalk/inkos-core test -- --run agent-tools-params`
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
+git commit -m "fix(core): architect builds complete BookConfig from schema params"
+```
+
+---
+
+### Task 3: Fix writer — 透传 wordCount 和 instruction
+
+**Files:**
+- Modify: `packages/core/src/agent/agent-tools.ts:83-91` (writer case)
+- Test: `packages/core/src/__tests__/agent-tools-params.test.ts` (extend)
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+describe("writer agent — wordCount and instruction 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 to writeNextChapter", async () => {
+    await tool.execute("tc1", {
+      agent: "writer",
+      instruction: "Write an exciting chapter",
+      bookId: "my-book",
+      chapterWordCount: 5000,
+    });
+
+    expect(writeNextChapterMock).toHaveBeenCalledWith("my-book", 5000);
+  });
+
+  it("passes undefined wordCount when chapterWordCount is omitted", async () => {
+    await tool.execute("tc2", {
+      agent: "writer",
+      instruction: "Write next chapter",
+      bookId: "my-book",
+    });
+
+    expect(writeNextChapterMock).toHaveBeenCalledWith("my-book", undefined);
+  });
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Expected: FAIL — `writeNextChapter` called with only 1 argument
+
+- [ ] **Step 3: Update writer case**
+
+```ts
+case "writer": {
+  if (!bookId) return textResult("Error: bookId is required for the writer agent.");
+  progress(`Writing next chapter for "${bookId}"...`);
+  const result = await pipeline.writeNextChapter(bookId, chapterWordCount);
+  progress(`Writer finished chapter for "${bookId}".`);
+  return textResult(
+    `Chapter written for "${bookId}". ` +
+    `Word count: ${(result as any).wordCount ?? "unknown"}.`,
+  );
+}
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `pnpm --filter @actalk/inkos-core test -- --run agent-tools-params`
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
+git commit -m "fix(core): writer passes chapterWordCount to writeNextChapter"
+```
+
+---
+
+### Task 4: Fix auditor — 丰富返回值
+
+**Files:**
+- Modify: `packages/core/src/agent/agent-tools.ts:94-103` (auditor case)
+- Test: `packages/core/src/__tests__/agent-tools-params.test.ts` (extend)
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+describe("auditor agent — rich return value", () => {
+  let auditDraftMock: ReturnType<typeof vi.fn>;
+  let tool: ReturnType<typeof createSubAgentTool>;
+
+  beforeEach(() => {
+    auditDraftMock = vi.fn(async () => ({
+      chapterNumber: 3,
+      passed: false,
+      issues: [
+        { severity: "warning", description: "Pacing too fast in mid section" },
+        { severity: "critical", description: "Character name inconsistency" },
+      ],
+    }));
+    const mockPipeline = { auditDraft: auditDraftMock } as any;
+    tool = createSubAgentTool(mockPipeline, "existing-book");
+  });
+
+  it("returns issue details with severity, not just count", async () => {
+    const result = await tool.execute("tc1", {
+      agent: "auditor",
+      instruction: "Audit chapter 3",
+      bookId: "my-book",
+      chapterNumber: 3,
+    });
+
+    const text = result.content[0].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");
+    expect(text).toContain("Character name inconsistency");
+  });
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Expected: FAIL — current response only has `"2 issue(s) found"`, no `[warning]` or `[critical]`
+
+- [ ] **Step 3: Update auditor case**
+
+```ts
+case "auditor": {
+  if (!bookId) return textResult("Error: bookId is required for the auditor agent.");
+  progress(`Auditing chapter ${chapterNumber ?? "latest"} for "${bookId}"...`);
+  const audit = await pipeline.auditDraft(bookId, chapterNumber);
+  progress(`Audit complete for "${bookId}".`);
+  const issueLines = (audit.issues ?? [])
+    .map((i: any) => `[${i.severity}] ${i.description}`)
+    .join("\n");
+  return textResult(
+    `Audit chapter ${audit.chapterNumber}: ${audit.passed ? "PASSED" : "FAILED"}, ${(audit.issues ?? []).length} issue(s).` +
+    (issueLines ? `\n${issueLines}` : ""),
+  );
+}
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
+git commit -m "fix(core): auditor returns full issue details with severity"
+```
+
+---
+
+### Task 5: Fix reviser — mode 字段化 + instruction 透传
+
+**Files:**
+- Modify: `packages/core/src/agent/agent-tools.ts:106-119` (reviser case)
+- Test: `packages/core/src/__tests__/agent-tools-params.test.ts` (extend)
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+describe("reviser agent — mode field and instruction passthrough", () => {
+  let reviseDraftMock: ReturnType<typeof vi.fn>;
+  let tool: ReturnType<typeof createSubAgentTool>;
+
+  beforeEach(() => {
+    reviseDraftMock = vi.fn(async () => ({}));
+    const mockPipeline = { reviseDraft: reviseDraftMock } as any;
+    tool = createSubAgentTool(mockPipeline, "existing-book");
+  });
+
+  it("uses mode param directly instead of regex extraction", async () => {
+    await tool.execute("tc1", {
+      agent: "reviser",
+      instruction: "Fix this chapter",
+      bookId: "my-book",
+      chapterNumber: 5,
+      mode: "anti-detect",
+    });
+
+    expect(reviseDraftMock).toHaveBeenCalledWith("my-book", 5, "anti-detect");
+  });
+
+  it("defaults to spot-fix when mode is omitted", async () => {
+    await tool.execute("tc2", {
+      agent: "reviser",
+      instruction: "Fix chapter",
+      bookId: "my-book",
+    });
+
+    expect(reviseDraftMock).toHaveBeenCalledWith("my-book", undefined, "spot-fix");
+  });
+
+  it("anti-detect mode is reachable (was unreachable with regex)", async () => {
+    await tool.execute("tc3", {
+      agent: "reviser",
+      instruction: "Process chapter",
+      bookId: "my-book",
+      mode: "anti-detect",
+    });
+
+    expect(reviseDraftMock.mock.calls[0][2]).toBe("anti-detect");
+  });
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Expected: FAIL — `anti-detect` falls through to `spot-fix` via regex
+
+- [ ] **Step 3: Update reviser case**
+
+```ts
+case "reviser": {
+  if (!bookId) return textResult("Error: bookId is required for the reviser agent.");
+  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}".`);
+  return textResult(`Revision (${resolvedMode}) complete for "${bookId}" chapter ${chapterNumber ?? "latest"}.`);
+}
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
+git commit -m "fix(core): reviser uses mode param directly, anti-detect now reachable"
+```
+
+---
+
+### Task 6: Implement exporter — 调通导出
+
+**Files:**
+- Modify: `packages/core/src/agent/agent-tools.ts:122-124` (exporter case)
+- Test: `packages/core/src/__tests__/agent-tools-params.test.ts` (extend)
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+describe("exporter agent — export implementation", () => {
+  let loadChapterIndexMock: ReturnType<typeof vi.fn>;
+  let loadBookConfigMock: ReturnType<typeof vi.fn>;
+  let tool: ReturnType<typeof createSubAgentTool>;
+  let tmpRoot: string;
+
+  beforeEach(async () => {
+    tmpRoot = await mkdtemp(join(tmpdir(), "inkos-export-"));
+    const bookDir = join(tmpRoot, "books", "my-book");
+    const chaptersDir = join(bookDir, "chapters");
+    await mkdir(chaptersDir, { recursive: true });
+    await writeFile(join(chaptersDir, "0001_First.md"), "# 第一章\n\n内容内容内容");
+    await writeFile(join(chaptersDir, "0002_Second.md"), "# 第二章\n\n更多内容");
+
+    loadChapterIndexMock = vi.fn(async () => [
+      { number: 1, title: "First", status: "approved", wordCount: 100 },
+      { number: 2, title: "Second", status: "drafted", wordCount: 200 },
+    ]);
+    loadBookConfigMock = vi.fn(async () => ({ title: "Test Book" }));
+    const mockPipeline = {} as any;
+    const mockState = {
+      bookDir: (id: string) => join(tmpRoot, "books", id),
+      loadChapterIndex: loadChapterIndexMock,
+      loadBookConfig: loadBookConfigMock,
+    };
+    // exporter needs access to state — passed via projectRoot
+    tool = createSubAgentTool(mockPipeline, "my-book", tmpRoot);
+  });
+
+  afterEach(async () => {
+    await rm(tmpRoot, { recursive: true, force: true });
+  });
+
+  it("exports all chapters in txt format by default", async () => {
+    const result = await tool.execute("tc1", {
+      agent: "exporter",
+      instruction: "Export the book",
+      bookId: "my-book",
+    });
+
+    const text = result.content[0].text;
+    expect(text).not.toContain("not yet implemented");
+    expect(text).toContain("export");
+  });
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Expected: FAIL — returns "Export is not yet implemented"
+
+- [ ] **Step 3: Implement exporter case**
+
+Replace the exporter case. Since `exportBookToPath` in `project-tools.ts` is not directly importable (it's a local function), inline the core logic:
+
+```ts
+case "exporter": {
+  if (!bookId) return textResult("Error: bookId is required for the exporter agent.");
+  const exportFormat = (format ?? "txt") as "txt" | "md" | "epub";
+  const onlyApproved = approvedOnly ?? false;
+  progress(`Exporting "${bookId}" as ${exportFormat}...`);
+
+  const booksRoot = join(projectRoot, "books");
+  const bookDir = join(booksRoot, bookId);
+  const chaptersDir = join(bookDir, "chapters");
+
+  // Load index and book config
+  const indexRaw = await readFile(join(chaptersDir, "index.json"), "utf-8");
+  const index: Array<{ number: number; title: string; status: string; wordCount: number }> = JSON.parse(indexRaw);
+  const bookRaw = await readFile(join(bookDir, "book.json"), "utf-8");
+  const bookConfig = JSON.parse(bookRaw);
+
+  const chapters = onlyApproved ? index.filter((ch) => ch.status === "approved") : index;
+  if (chapters.length === 0) {
+    return textResult(`No chapters to export${onlyApproved ? " (approved only filter)" : ""}.`);
+  }
+
+  // Build chapter file lookup
+  const files = await readdir(chaptersDir);
+  const chapterFiles = new Map<number, string>();
+  for (const f of files.filter((f) => /^\d{4}_.*\.md$/.test(f)).sort()) {
+    chapterFiles.set(parseInt(f.slice(0, 4), 10), f);
+  }
+
+  const parts: string[] = [];
+  parts.push(exportFormat === "md" ? `# ${bookConfig.title ?? bookId}\n\n---\n` : `${bookConfig.title ?? bookId}\n\n`);
+  for (const ch of chapters) {
+    const file = chapterFiles.get(ch.number);
+    if (!file) continue;
+    parts.push(await readFile(join(chaptersDir, file), "utf-8"));
+    parts.push("\n\n");
+  }
+
+  const outputPath = join(booksRoot, "..", `${bookId}_export.${exportFormat}`);
+  await writeFile(outputPath, parts.join(exportFormat === "md" ? "\n---\n\n" : "\n"), "utf-8");
+
+  const totalWords = chapters.reduce((s, ch) => s + ch.wordCount, 0);
+  progress(`Export complete for "${bookId}".`);
+  return textResult(
+    `Exported "${bookId}": ${chapters.length} chapters, ${totalWords} words → ${outputPath}`,
+  );
+}
+```
+
+Note: `projectRoot` is already available in the closure from `createSubAgentTool`'s third parameter (added in a prior commit).
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
+git commit -m "feat(core): implement exporter agent with format and approvedOnly support"
+```
+
+---
+
+### Task 7: 更新系统提示词
+
+**Files:**
+- Modify: `packages/core/src/agent/agent-system-prompt.ts`
+
+- [ ] **Step 1: Write the failing test**
+
+Append to `packages/core/src/__tests__/agent-system-prompt.test.ts`:
+
+```ts
+it("book-mode prompt documents all sub_agent params", () => {
+  const prompt = buildAgentSystemPrompt("test-book", "zh");
+  // architect params
+  expect(prompt).toContain("title");
+  expect(prompt).toContain("genre");
+  expect(prompt).toContain("platform");
+  expect(prompt).toContain("chapterWordCount");
+  // reviser params
+  expect(prompt).toContain("mode");
+  expect(prompt).toContain("anti-detect");
+  // exporter params
+  expect(prompt).toContain("format");
+  expect(prompt).toContain("approvedOnly");
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `pnpm --filter @actalk/inkos-core test -- --run agent-system-prompt`
+Expected: FAIL — prompt doesn't mention `format`, `approvedOnly`, `anti-detect` etc.
+
+- [ ] **Step 3: Update both zh and en prompts**
+
+Update the Chinese book-mode prompt's tool section:
+
+```ts
+- **sub_agent** — 委托子智能体执行重操作:
+  - agent="architect" 从零建书(参数:title, genre, platform, language, targetChapters, chapterWordCount)
+  - 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)
+```
+
+Update the English book-mode prompt similarly.
+
+Also update the Chinese book-create prompt to mention architect params:
+
+```ts
+2. **确认建书**(调用阶段)— 当信息足够时,调用 sub_agent 工具委托 architect 子智能体建书:
+   - instruction 中包含收集到的所有信息
+   - 同时传入结构化参数:title(书名)、genre(题材)、platform(平台)、language(语言)、targetChapters(章数)、chapterWordCount(每章字数)
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/core/src/agent/agent-system-prompt.ts packages/core/src/__tests__/agent-system-prompt.test.ts
+git commit -m "docs(core): update system prompts with complete sub_agent param documentation"
+```
+
+---
+
+### Task 8: 全量测试验证
+
+**Files:** None (verification only)
+
+- [ ] **Step 1: Run all core tests**
+
+```bash
+pnpm --filter @actalk/inkos-core test -- --run
+```
+
+Expected: All pass
+
+- [ ] **Step 2: Run all studio tests**
+
+```bash
+pnpm --filter @actalk/inkos-studio test -- --run
+```
+
+Expected: All pass
+
+- [ ] **Step 3: Run all CLI tests**
+
+```bash
+pnpm --filter @actalk/inkos test -- --run
+```
+
+Expected: All pass (or only pre-existing Windows tar failures)
+
+- [ ] **Step 4: Commit any fixes needed**
+
+If any existing tests broke due to schema changes, fix them.