Tools for your agent
Hand the document to whatever model you already run.
These docs are for version 2.3.x. See the latest version.
The SDK exposes the document tools from Revise's own agent. Each tool has a JSON schema, runs without React state or browser focus, and works on document structure rather than screen positions.
Tool loop#
const definitions = editor.tools.getDefinitions();
const response = await yourModel.createMessage({
messages,
tools: definitions.map((tool) => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
})),
});
for (const call of response.toolCalls) {
const result = await editor.tools.executeDynamic(call.name, call.input);
messages.push({
role: "tool",
tool_call_id: call.id,
content: result.ok
? [
result.value.message ?? JSON.stringify(result.value.data),
// Read and search results: the document itself, with block IDs.
result.value.context?.html,
]
.filter(Boolean)
.join("\n\n")
: `${result.error.code}: ${result.error.message}`,
});
}Send the schemas to the model and return the results. For reads and searches,
message is only a summary. The text the model edits is in context.html, so
send both. The definitions come from the same source as Revise's production
agent, with the same descriptions and constraints.
Results#
This API and @reviseio/editor/server return the same result type, so the same handling
code works in the browser and in Node:
type ReviseToolResult<Name> =
| { ok: true; value: ReviseToolResponse<Name> }
| { ok: false; error: ReviseToolFailure<Name> }; // { callId, tool, code, message }
interface ReviseToolResponse<Name> {
callId: string;
tool: Name;
message: string | null; // summary for the model
data: ReviseToolData<Name>; // typed metadata, per tool
context: { format: "revise-html"; html: string } | null;
suggestionIds: string[] | null; // tracked changes this call created
}A rejected edit returns ok: false. It does not throw. Check ok and send
error.message back to the model. The message explains why the call failed
in terms the model can act on. tools.call() is the throwing variant for
application code and raises a typed ReviseToolError.
Read and search tools return context.html. This HTML keeps block IDs, inline
formatting, tables, and notes. Mutation tools target those block IDs. Mutations
in suggesting mode list the tracked changes they created in suggestionIds.
Pass them to review.acceptSuggestions().
Working with blocks#
The model reads part of the document, then edits by block ID:
const read = await editor.tools.call("read_blocks_from_index", {
index: 0,
context_notes: "Looking for the liability clause",
});
await editor.tools.call("replace", {
id: "b12",
replacements: [
{
find: "capped at the fees paid",
replace: "capped at two years of fees paid",
occurrence: "unique",
},
],
});context_notes is how a model carries what it learned from one read into the
next, and the schemas require it. When your own code calls a tool through
execute() or call(), you can leave it out; it defaults to "N/A". Calls
through executeDynamic() are passed on exactly as the model sent them.
replace works inside one block. Edits in two blocks take two calls. A
single call with finds in more than one block fails without applying any of
them.
Edit every match#
Searching the active document returns a search_result_id for the full match
set, across all pages. Pass it to replace, replace_block, style_blocks,
or remove_blocks instead of an id. The edit applies to each matched block
separately, in one call:
const search = await editor.tools.call("search_document", {
queries: ["Acme Corp."],
page: 0,
context_notes: "Renaming the counterparty throughout",
});
await editor.tools.call("replace", {
search_result_id: search.data.search_result_id,
replacements: [
{ find: "Acme Corp.", replace: "Acme Holdings Ltd.", occurrence: "all" },
],
});A search result ID works only in the editor session that created it. If the
matched blocks change, the call is rejected and nothing is applied. Run
search_document again and use the new ID.
Routing to a document#
Every tool schema has an optional document_id. Omit it to target the active
document, or pass the ID of any ready document in the same editor:
await editor.tools.call("set_title", { title: "Exhibit A" }, {
documentId: "exhibit-a",
});
// or bind a controller once
const exhibit = editor.tools.forDocument("exhibit-a");
await exhibit.execute("measure_document", {});Suggestions or direct edits#
Tool mutations are tracked changes by default, whatever mode the editor is in
for typing. Pass directMode to apply them directly:
await editor.tools.call("style_blocks", input, { directMode: true });See tracked changes for when to use it.
What is not a tool#
Review navigation, comment panel state, focus, viewport, zoom, and canvas
rendering belong to the host. They are not tools in this catalog. The agent
makes document edits, and your UI presents them. A delegated Revise agent can
inspect the rendered canvas with its internal render_document_pages tool. See
architecture and delegation.
The full catalog is in the tool reference.
Server-side editing#
@reviseio/editor/server binds the same tool definitions and executor to a Y.Doc that
you own. No model or React mount is involved. The backend
reference covers conversion, room lifecycle, and the
full session API.
A session:
- is bound to one document, so its schemas have no
document_id - keeps search result IDs across calls
- runs every call in arrival order
- must be disposed. Disposing removes Revise's observers and leaves your
Y.Docintact.
Literal tool names infer their exact input and output types. Use call() in
application code: it returns the response and throws ReviseToolError when the
tool rejects the call. Use execute() for a result object instead, or
executeDynamic() for tool names and JSON from a model.
Switch modes with document.setSuggestingMode() and
document.setEditingMode(). Each call uses the mode that was set when it was
submitted, so switching is safe with calls still queued.
import { readFile, writeFile } from "node:fs/promises";
import {
createServerDocumentSession,
decodeYDoc,
encodeYDoc,
fileToYDoc,
ydocToDocx,
} from "@reviseio/editor/server";
const ydoc = await fileToYDoc(
await readFile("agreement.docx"),
"agreement.docx",
);
const document = await createServerDocumentSession(ydoc, {
documentId: "agreement-1",
mode: "suggesting",
});
const read = await document.tools.call("read_blocks_from_index", {
index: 0,
context_notes: "Locating the liability clause",
});
console.log(read.context?.html); // Revise semantic HTML with block IDs
const search = await document.tools.call("search_document", {
queries: ["fees paid"],
page: 0,
context_notes: "Liability cap",
});
const searchResultId = search.data.search_result_id;
if (!searchResultId) throw new Error("The search returned no editable matches");
// Exact-text replacement needs no HTML. Apply this one directly.
document.setEditingMode();
await document.tools.call(
"replace",
{
search_result_id: searchResultId,
replacements: [{ find: "fees paid", replace: "fees paid or payable" }],
},
);
// Switch back to produce tracked changes.
document.setSuggestingMode();
const governingLaw = await document.tools.call("search_document", {
queries: ["Delaware"],
page: 0,
context_notes: "Suggesting a governing-law change",
});
const governingLawId = governingLaw.data.search_result_id;
if (!governingLawId) throw new Error("The governing-law text was not found");
const edit = await document.tools.call("replace", {
search_result_id: governingLawId,
replacements: [{ find: "Delaware", replace: "New York" }],
});
// Resolve by ID. Mutation results list the tracked changes they created.
// Store these IDs with your review task.
const created = edit.suggestionIds ?? [];
const decision = document.acceptSuggestions(created);
// decision.resolved: settled now. decision.missing: stale or unknown IDs.
// Or list everything pending. Each record has its author, so you can act on
// your agent's suggestions and leave collaborators' suggestions alone:
const mine = document
.listSuggestions()
.filter((record) => record.authorType === "ai");
// document.rejectSuggestions(mine.map((record) => record.id));
// These also settle collaborators' pending suggestions. Use them only when
// the host owns the document:
// document.acceptAllSuggestions(); // or document.rejectAllSuggestions()
await writeFile("agreement.yjs", encodeYDoc(ydoc));
const restored = decodeYDoc(await readFile("agreement.yjs"));
await writeFile("agreement-amended.docx", await ydocToDocx(restored));
document.dispose();In a model loop, return expected failures to the model as text:
const result = await document.tools.executeDynamic(call.name, call.input);
const toolMessage = result.ok
? (result.value.message ?? JSON.stringify(result.value.data))
: `${result.error.code}: ${result.error.message}`;Revise semantic HTML#
Read and search tools return response.context.html in Revise semantic
HTML: document markup with stable block IDs for paragraphs, formatted runs,
tables, comments, and notes where supported. Typed metadata is in
response.data. A short summary for the model is in response.message.
Structural insertion and whole-block replacement accept the same HTML. Use it
instead of building DocRoot or OOXML. It is not general browser HTML and CSS,
and it does not represent Word layout that Revise does not support. The
Yjs-backed Revise model is the source of truth, and DOCX is the import and
export format. Running a tool does not reparse unchanged content through HTML.
Do not mutate DocRoot directly. Some conversion calls still return that type
for compatibility. The supported server mutation API is
(await createServerDocumentSession(ydoc, ...)).tools.