File size: 5,230 Bytes
561e6f0 8fc8501 561e6f0 8fc8501 561e6f0 8fc8501 561e6f0 8fc8501 561e6f0 8fc8501 561e6f0 8fc8501 561e6f0 8fc8501 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | import { tool } from "ai";
import { z } from "zod";
import { buildTiptapTools } from "./tiptap-catalog.js";
/**
* Tools exposed to the LLM. These return instructions that the frontend
* will execute on the TipTap editor instance. The backend never touches
* the document directly - it only produces "edit commands" that are
* streamed back and applied client-side.
*
* All edits from a single agent turn are grouped into one Yjs undo step,
* so the user can revert an entire agent action with a single Cmd+Z.
*
* The editor commands come from a static catalog (`tiptap-catalog.ts`)
* that also drives the system prompt's "Selection State Machine" section.
* The domain-specific tools (applyDiff, replaceSelection, insertAtCursor,
* frontmatter) are defined inline here since they don't map 1:1 to a
* native Tiptap command.
*/
const customEditorTools = {
replaceSelection: tool({
description:
"Replace the currently selected text in the editor with new content. " +
"Use ONLY when text is selected and the user asks to rewrite, fix, or rephrase it.",
inputSchema: z.object({
newText: z
.string()
.describe("The replacement text (plain text or markdown)"),
}),
}),
insertAtCursor: tool({
description:
"Insert text at the current cursor position (does not replace anything). " +
"Use when the user asks to add or generate new content.",
inputSchema: z.object({
text: z.string().describe("The text to insert (plain text or markdown)"),
}),
}),
applyDiff: tool({
description:
"The PRIMARY tool for editing prose. Locates a piece of text in the document using " +
"context and replaces it. Provide surrounding context to disambiguate when the " +
"same text appears multiple times. For multiple edits, call this tool multiple " +
"times in a single response. Each call should be a minimal, surgical edit.",
inputSchema: z.object({
contextBefore: z
.string()
.describe(
"A few words of text that appear immediately BEFORE the content to delete. " +
"Used to locate the correct position. Can be empty if at the start of the document.",
),
contentToDelete: z
.string()
.describe(
"The exact verbatim text to remove from the document. Must match perfectly.",
),
contentToInsert: z
.string()
.describe(
"The new text to insert in place of the deleted text. " +
"Can be empty to just delete without replacing.",
),
contextAfter: z
.string()
.describe(
"A few words of text that appear immediately AFTER the content to delete. " +
"Used to confirm the correct position. Can be empty if at the end.",
),
}),
}),
};
const frontmatterTools = {
updateFrontmatter: tool({
description:
"Update article metadata (frontmatter). Use when the user asks to change the title, " +
"subtitle, authors, affiliations, publication date, DOI, template, or any article setting. " +
"Only include the fields you want to change - omitted fields are left unchanged.",
inputSchema: z.object({
title: z.string().optional().describe("Article title"),
subtitle: z.string().optional().describe("Subtitle"),
description: z.string().optional().describe("Short SEO description"),
published: z.string().optional().describe('Publication date, e.g. "Apr. 04, 2026"'),
template: z.enum(["article", "paper"]).optional().describe("Layout template"),
doi: z.string().optional().describe("DOI identifier"),
banner: z.string().optional().describe("Banner embed filename"),
showPdf: z.boolean().optional().describe("Show PDF download button"),
tableOfContentsAutoCollapse: z.boolean().optional().describe("Auto-collapse TOC"),
licence: z.string().optional().describe("Licence text (HTML allowed)"),
pdfProOnly: z.boolean().optional().describe("Gate PDF behind HF Pro"),
seoThumbImage: z.string().optional().describe("Custom OG image URL"),
}),
}),
addAuthor: tool({
description:
"Add an author to the article. Provide the author name and optionally a URL " +
"and affiliation indices (1-based). If the affiliation doesn't exist yet, " +
"provide newAffiliationName to create it.",
inputSchema: z.object({
name: z.string().describe("Author name"),
url: z.string().optional().describe("Author URL/homepage"),
affiliations: z
.array(z.number())
.optional()
.describe("1-based affiliation indices"),
newAffiliationName: z
.string()
.optional()
.describe("Name of a new affiliation to create (if needed)"),
newAffiliationUrl: z
.string()
.optional()
.describe("URL of the new affiliation"),
}),
}),
removeAuthor: tool({
description: "Remove an author by index (0-based).",
inputSchema: z.object({
index: z.number().describe("0-based index of the author to remove"),
}),
}),
};
export const editorTools = {
...buildTiptapTools(),
...customEditorTools,
...frontmatterTools,
};
|