EngineeringApril 29, 20264 min read

Agent toolkit for DOCX documents

Use @docx-editor.dev/agents to let an agent read, comment on, and suggest edits to .docx documents in the editor, in Node, or over MCP.

0.x postThis post uses the 0.x package names and APIs. For the current release see the 2.x docs.

@docx-editor.dev/agents integrates AI agents with .docx documents. It provides 14 tools for reading, finding, commenting, suggesting, formatting, and scrolling. The tools use OpenAI function-calling format. They use the stable Word paraId, which keeps paragraph references valid across multiple tool calls. The catalog supports three transports: a React <DocxEditor>, a parsed buffer in Node, and the Model Context Protocol.

This post covers the supported integration shapes, the paraId addressing model, and the framework adapters.

Common integrations

The integration surface is the same for each agent role. The deployment controls the available tools and the system prompt.

  • Contract review: Use read_document, add_comment, and suggest_change. An attorney accepts or rejects the tracked changes in Word, Google Docs, or the embedded editor.
  • Compliance scans: Use read-only tools, such as include: ['read_document', 'find_text', 'read_comments']. Comments can mark personally identifiable information (PII), policy violations, or missing clauses.
  • Document assistants: Use the complete catalog. Use include or exclude to limit the tools available through a chat panel.
  • Word add-in alternative: The toolkit provides web equivalents for Range.insertComment, comment.reply, body.search, and range.scrollIntoView. This mapping reduces changes to Office.js call sites.

The tools

The package exports 14 tools as raw definitions and OpenAI function-calling schemas. Anthropic tool use, the Vercel AI SDK, and other compatible clients can use these schemas without conversion.

GroupToolsPurpose
Locateread_document, read_selection, read_page, read_pages, find_text, read_comments, read_changesReturn paragraphs tagged with paraId.
Mutateadd_comment, suggest_change, apply_formatting, set_paragraph_style, reply_comment, resolve_commentTake a paraId and an optional search phrase.
NavigatescrollLive transport only.

Locate tools return paragraphs keyed by Word paraId; mutate tools take that paraId. This gives the agent a stable paragraph anchor across multi-step tool calls instead of relying on raw character offsets.

Building a custom agent

A custom agent uses the required catalog subset and any domain-specific tools. The application also defines the system prompt. The integration uses one React hook and a streaming chat client.

The following Roastmaster demo reads the document and selects three to five passages. It adds a comment to each passage. It uses only read and comment tools, so it cannot edit document text.

Step 1: limit the tools. Roastmaster does not edit text. The useDocxAgentTools call uses an include allowlist that excludes every mutation tool except add_comment:

import { useDocxAgentTools } from "@docx-editor.dev/agents/react";
 
const { tools, executeToolCall, getContext } = useDocxAgentTools({
  editorRef,
  author: "Roastmaster",
  include: [
    "read_document", "read_selection", "find_text",
    "read_comments", "read_changes", "scroll",
    "add_comment", "reply_comment", "resolve_comment",
  ],
});

The same include mechanism can expose a read-only compliance scanner, the complete catalog, or a review agent. A review agent can use read tools, add_comment, and suggest_change. Pass custom tools through tools to add them to the catalog.

Step 2: write the system prompt. The prompt defines the agent behavior. Roastmaster's prompt limits each turn to five comments. It also requires each comment to use a unique phrase from its paragraph and defines the review tone. A complete example is in examples/agent-chat-demo.

Step 3: run the loop. useChat from @ai-sdk/react handles streaming. executeToolCall runs each tool call in the client against the live editor. The server route sends schemas and receives chat and tool-call text. It does not receive the DOCX buffer.

const chat = useChat({
  transport: new DefaultChatTransport({
    api: "/api/agent-chat",
    prepareSendMessagesRequest: ({ messages }) => ({
      body: { messages, context: getContext() },
    }),
  }),
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
  onToolCall: ({ toolCall }) => {
    const result = executeToolCall(toolCall.toolName, toolCall.input);
    chatRef.current?.addToolResult({
      tool: toolCall.toolName,
      toolCallId: toolCall.toolCallId,
      output: result.success ? String(result.data) : result.error ?? "",
    });
  },
});

getContext() returns the user's selection and page. Pass it through prepareSendMessagesRequest to provide this context without another tool call. sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls continues the loop until the model returns a reply without a tool call.

The Live editor docs page contains the full source for the following example and its server route.

"use client";
 
import { useMemo, useRef, useState } from "react";
import { DocxEditor, AgentChatLog, AgentComposer } from "@eigenpal/docx-js-editor";
import { useDocxAgentTools, getToolDisplayName } from "@docx-editor.dev/agents/react";
import { toAgentMessages } from "@docx-editor.dev/agents/ai-sdk/react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from "ai";
 
export function EditorWithAgent({ buffer }: { buffer: ArrayBuffer }) {
  const editorRef = useRef(null);
  const { executeToolCall, getContext } = useDocxAgentTools({ editorRef, author: "Agent" });
  const chatRef = useRef(null);
 
  const chat = useChat({
    transport: new DefaultChatTransport({
      api: "/api/chat",
      prepareSendMessagesRequest: ({ messages }) => ({
        body: { messages, context: getContext() },
      }),
    }),
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
    onToolCall: ({ toolCall }) => {
      const result = executeToolCall(toolCall.toolName, toolCall.input);
      void chatRef.current?.addToolResult({
        tool: toolCall.toolName,
        toolCallId: toolCall.toolCallId,
        output: result.success ? String(result.data) : (result.error ?? ""),
      });
    },
  });
  chatRef.current = chat;
 
  const messages = useMemo(() => toAgentMessages(chat.messages, chat.status), [chat]);
  const [input, setInput] = useState("");
 
  return (
    <DocxEditor
      ref={editorRef}
      documentBuffer={buffer}
      agentPanel={{
        title: "Agent",
        render: () => (
          <>
            <AgentChatLog messages={messages} humanizeToolName={getToolDisplayName} />
            <AgentComposer
              value={input}
              onChange={setInput}
              onSubmit={() => {
                chat.sendMessage({ text: input });
                setInput("");
              }}
            />
          </>
        ),
      }}
    />
  );
}

The matching server route uses getAiSdkTools() from /ai-sdk/server and Vercel AI SDK's streamText({ tools }). The Live editor docs page contains the full source.

Try it

The following demo uses the same component on this site. The Roastmaster agent has only read and comment tools, so it cannot edit document text. Select a suggestion or enter a prompt to run it on the sample document.

Scope

  • Tracked-change acceptance requires a person. The catalog has no accept_change or reject_change tools. A person decides whether to accept each revision.
  • Formatting operations. apply_formatting supports bold, italic, underline, strike, color, highlight, font size, and font family. The toolkit does not support paragraph alignment or spacing operations.

Get started

The source is on GitHub. You can report issues or submit pull requests.

For related information, see Track Changes in a React DOCX Editor and Real-time DOCX collaboration with React, Vue 3, and Yjs.