|
|
@@ -1,9 +1,11 @@
|
|
|
import { Type, type Static } from "@mariozechner/pi-ai";
|
|
|
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@mariozechner/pi-agent-core";
|
|
|
import type { PipelineRunner } from "../pipeline/runner.js";
|
|
|
-import type { ReviseMode } from "../agents/reviser.js";
|
|
|
+import { DEFAULT_REVISE_MODE, type ReviseMode } from "../agents/reviser.js";
|
|
|
import { readFile, writeFile, readdir, stat } from "node:fs/promises";
|
|
|
import { join, normalize, resolve } from "node:path";
|
|
|
+import { StateManager } from "../state/manager.js";
|
|
|
+import { createInteractionToolsFromDeps } from "../interaction/project-tools.js";
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// Helpers
|
|
|
@@ -25,6 +27,23 @@ function safeBooksPath(booksRoot: string, relativePath: string): string {
|
|
|
return resolved;
|
|
|
}
|
|
|
|
|
|
+function resolveToolBookId(
|
|
|
+ toolName: string,
|
|
|
+ paramsBookId: string | undefined,
|
|
|
+ activeBookId: string | null,
|
|
|
+): string {
|
|
|
+ const resolvedBookId = paramsBookId ?? activeBookId ?? undefined;
|
|
|
+ if (!resolvedBookId) {
|
|
|
+ throw new Error(`${toolName} requires bookId when there is no active book.`);
|
|
|
+ }
|
|
|
+ return resolvedBookId;
|
|
|
+}
|
|
|
+
|
|
|
+function createDeterministicInteractionTools(pipeline: PipelineRunner, projectRoot: string) {
|
|
|
+ const state = new StateManager(projectRoot);
|
|
|
+ return createInteractionToolsFromDeps(pipeline, state);
|
|
|
+}
|
|
|
+
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// 1. SubAgentTool (sub_agent)
|
|
|
// ---------------------------------------------------------------------------
|
|
|
@@ -133,6 +152,165 @@ export function createSubAgentTool(pipeline: PipelineRunner, activeBookId: strin
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// 1.5 Deterministic writing tools
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+const ReviseChapterParams = Type.Object({
|
|
|
+ bookId: Type.Optional(Type.String({ description: "Book ID. Defaults to the active book." })),
|
|
|
+ chapterNumber: Type.Optional(Type.Number({ description: "Chapter number. Defaults to the latest drafted chapter." })),
|
|
|
+ mode: Type.Optional(Type.Union([
|
|
|
+ Type.Literal("polish"),
|
|
|
+ Type.Literal("rewrite"),
|
|
|
+ Type.Literal("rework"),
|
|
|
+ Type.Literal("anti-detect"),
|
|
|
+ Type.Literal("spot-fix"),
|
|
|
+ ])),
|
|
|
+});
|
|
|
+
|
|
|
+export function createReviseChapterTool(
|
|
|
+ pipeline: PipelineRunner,
|
|
|
+ activeBookId: string | null,
|
|
|
+): AgentTool<typeof ReviseChapterParams> {
|
|
|
+ return {
|
|
|
+ name: "revise_chapter",
|
|
|
+ description:
|
|
|
+ "Rewrite or revise an existing chapter through the deterministic chapter revision pipeline. " +
|
|
|
+ "Use this for 精修、重写、返工, not the generic edit tool.",
|
|
|
+ label: "Revise Chapter",
|
|
|
+ parameters: ReviseChapterParams,
|
|
|
+ async execute(
|
|
|
+ _toolCallId: string,
|
|
|
+ params: Static<typeof ReviseChapterParams>,
|
|
|
+ ): Promise<AgentToolResult<undefined>> {
|
|
|
+ try {
|
|
|
+ const bookId = resolveToolBookId("revise_chapter", params.bookId, activeBookId);
|
|
|
+ const result = await pipeline.reviseDraft(
|
|
|
+ bookId,
|
|
|
+ params.chapterNumber as number,
|
|
|
+ (params.mode ?? DEFAULT_REVISE_MODE) as ReviseMode,
|
|
|
+ );
|
|
|
+ const responseText = `Chapter revision completed for "${bookId}"${params.chapterNumber ? ` chapter ${params.chapterNumber}` : ""}.`;
|
|
|
+ return textResult(responseText);
|
|
|
+ } catch (err: any) {
|
|
|
+ return textResult(`Failed to revise chapter: ${err?.message ?? String(err)}`);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+const WriteTruthFileParams = Type.Object({
|
|
|
+ bookId: Type.Optional(Type.String({ description: "Book ID. Defaults to the active book." })),
|
|
|
+ fileName: Type.String({ description: "Truth file name, e.g. story_bible.md or current_focus.md" }),
|
|
|
+ content: Type.String({ description: "Complete replacement content for the truth file" }),
|
|
|
+});
|
|
|
+
|
|
|
+export function createWriteTruthFileTool(
|
|
|
+ pipeline: PipelineRunner,
|
|
|
+ projectRoot: string,
|
|
|
+ activeBookId: string | null,
|
|
|
+): AgentTool<typeof WriteTruthFileParams> {
|
|
|
+ const interactionTools = createDeterministicInteractionTools(pipeline, projectRoot);
|
|
|
+
|
|
|
+ return {
|
|
|
+ name: "write_truth_file",
|
|
|
+ description:
|
|
|
+ "Replace a canonical truth file in story/. Use this for story_bible, volume outline, rules, focus, and other book facts.",
|
|
|
+ label: "Write Truth File",
|
|
|
+ parameters: WriteTruthFileParams,
|
|
|
+ async execute(
|
|
|
+ _toolCallId: string,
|
|
|
+ params: Static<typeof WriteTruthFileParams>,
|
|
|
+ ): Promise<AgentToolResult<undefined>> {
|
|
|
+ try {
|
|
|
+ const bookId = resolveToolBookId("write_truth_file", params.bookId, activeBookId);
|
|
|
+ await interactionTools.writeTruthFile(bookId, params.fileName, params.content);
|
|
|
+ return textResult(`Updated ${params.fileName} for "${bookId}".`);
|
|
|
+ } catch (err: any) {
|
|
|
+ return textResult(`Failed to write truth file: ${err?.message ?? String(err)}`);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+const RenameEntityParams = Type.Object({
|
|
|
+ bookId: Type.Optional(Type.String({ description: "Book ID. Defaults to the active book." })),
|
|
|
+ oldValue: Type.String({ description: "The old character/entity name" }),
|
|
|
+ newValue: Type.String({ description: "The new character/entity name" }),
|
|
|
+});
|
|
|
+
|
|
|
+export function createRenameEntityTool(
|
|
|
+ pipeline: PipelineRunner,
|
|
|
+ projectRoot: string,
|
|
|
+ activeBookId: string | null,
|
|
|
+): AgentTool<typeof RenameEntityParams> {
|
|
|
+ const interactionTools = createDeterministicInteractionTools(pipeline, projectRoot);
|
|
|
+
|
|
|
+ return {
|
|
|
+ name: "rename_entity",
|
|
|
+ description:
|
|
|
+ "Rename a character or entity across the current book through the deterministic edit controller.",
|
|
|
+ label: "Rename Entity",
|
|
|
+ parameters: RenameEntityParams,
|
|
|
+ async execute(
|
|
|
+ _toolCallId: string,
|
|
|
+ params: Static<typeof RenameEntityParams>,
|
|
|
+ ): Promise<AgentToolResult<undefined>> {
|
|
|
+ try {
|
|
|
+ const bookId = resolveToolBookId("rename_entity", params.bookId, activeBookId);
|
|
|
+ const result = await interactionTools.renameEntity(bookId, params.oldValue, params.newValue);
|
|
|
+ const responseText = (result as { __interaction?: { responseText?: string } }).__interaction?.responseText
|
|
|
+ ?? `Renamed ${params.oldValue} to ${params.newValue} in "${bookId}".`;
|
|
|
+ return textResult(responseText);
|
|
|
+ } catch (err: any) {
|
|
|
+ return textResult(`Failed to rename entity: ${err?.message ?? String(err)}`);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+const PatchChapterTextParams = Type.Object({
|
|
|
+ bookId: Type.Optional(Type.String({ description: "Book ID. Defaults to the active book." })),
|
|
|
+ chapterNumber: Type.Number({ description: "Chapter number to patch" }),
|
|
|
+ targetText: Type.String({ description: "Exact text to replace" }),
|
|
|
+ replacementText: Type.String({ description: "Replacement text" }),
|
|
|
+});
|
|
|
+
|
|
|
+export function createPatchChapterTextTool(
|
|
|
+ pipeline: PipelineRunner,
|
|
|
+ projectRoot: string,
|
|
|
+ activeBookId: string | null,
|
|
|
+): AgentTool<typeof PatchChapterTextParams> {
|
|
|
+ const interactionTools = createDeterministicInteractionTools(pipeline, projectRoot);
|
|
|
+
|
|
|
+ return {
|
|
|
+ name: "patch_chapter_text",
|
|
|
+ description:
|
|
|
+ "Apply a local deterministic patch to an existing chapter and mark it for review.",
|
|
|
+ label: "Patch Chapter Text",
|
|
|
+ parameters: PatchChapterTextParams,
|
|
|
+ async execute(
|
|
|
+ _toolCallId: string,
|
|
|
+ params: Static<typeof PatchChapterTextParams>,
|
|
|
+ ): Promise<AgentToolResult<undefined>> {
|
|
|
+ try {
|
|
|
+ const bookId = resolveToolBookId("patch_chapter_text", params.bookId, activeBookId);
|
|
|
+ const result = await interactionTools.patchChapterText(
|
|
|
+ bookId,
|
|
|
+ params.chapterNumber,
|
|
|
+ params.targetText,
|
|
|
+ params.replacementText,
|
|
|
+ );
|
|
|
+ const responseText = (result as { __interaction?: { responseText?: string } }).__interaction?.responseText
|
|
|
+ ?? `Patched chapter ${params.chapterNumber} in "${bookId}".`;
|
|
|
+ return textResult(responseText);
|
|
|
+ } catch (err: any) {
|
|
|
+ return textResult(`Failed to patch chapter text: ${err?.message ?? String(err)}`);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// 2. Read Tool
|
|
|
// ---------------------------------------------------------------------------
|