Agentic Documents
БесплатноПоддерживаетсяWrite documents with people and agents in real time.
Описание
Write documents with people and agents in real time.
README
Write documents with people and agents in real time.
Status: early, version 0.13.2. The document model, the text operations, the CLI, the MCP server, the Worker API, live editing in a room, and a small web UI work. See UI.
Install
npm install agentic-documents
Subpath exports
| Import | Use it for |
|---|---|
agentic-documents |
The document model, text operations, threads, suggestions, the service, the file store, export, the sharing API, and their types. See Quick start. |
agentic-documents/worker |
The HTTP API as a Hono app. See Worker subpath. |
agentic-documents/live |
LiveDoc, the client for a live room. See Live editing. |
agentic-documents/client |
A typed client for the Worker API. See SDK. |
agentic-documents/formats |
Import and export files. See File formats. |
agentic-documents/ui |
Mount the editor, or the whole app, in your page. See Embed the editor and Mount the whole app. |
agentic-documents/ui.css |
The base styles of the editor. Load it after agentic-design/styles.css and with agentic-documents/ui. See Embed the editor. |
agentic-documents/app.css |
The layout styles of the whole app. Load it after agentic-documents/ui.css. See Mount the whole app. |
agentic-documents/live-worker |
The HTTP API with live rooms, as a Worker. It exports SyncRoom. See Live editing. |
Quick start
import { AgenticDoc, Suggestions, Threads } from "agentic-documents";
const agent = { kind: "agent_of_human", name: "scribe" } as const;
const doc = AgenticDoc.fromMarkdown("# Plan\n\nShip on Friday.", agent);
const [plan] = doc.outline();
doc.replace({ text: "Friday" }, "Monday", agent);
doc.insert({ section: plan.id, at: "end" }, "- [ ] Write notes", agent);
console.log(doc.toMarkdown());
console.log(doc.edits()); // each edit keeps the author kind and name
const person = { kind: "human", name: "Ada" } as const;
const suggestions = new Suggestions(doc);
const id = suggestions.add({ text: "Monday" }, "Tuesday", agent);
suggestions.accept(id, person); // the text changes now
new Threads(doc).add({ text: "Tuesday" }, "Is this date firm?", agent);
UI
The Worker serves a small web app, built on agentic-design
and the typed client (agentic-documents/client). It has a document list, a full-screen rich-text editor, rename,
delete, a share dialog, the people and agents in the room, and a dotted underline on text that an agent wrote. The
editor toolbar has text style (paragraph, headings 1 to 3), bold, italic, strikethrough, inline code, link, bulleted,
numbered, and task lists, quote, code block, divider, table, image, undo, and redo. The Image tool inserts an image, or changes the selected one, with alt text or
the Decorative choice. It takes a web address, an embedded image, or a PNG, JPEG, GIF, or WebP file of up to about 180
KB, stored in the document. The page's content security policy blocks images from other hosts, so a web address shows
only in an app that allows it. A pasted image is kept only when its source passes the same check. A paste or a drop
whose images would take the document past the 1,000,000 byte limit of a live room is refused. The menu at the top end of a document has Rename,
Delete, the theme, and Sign out. The tests check each main view with axe (WCAG 2.2 AA rules) in the light and the dark theme: sign in, list, empty and error list, new-document dialog, editor, share dialog, link, rename and delete dialogs, the actions menu, and the read-only view. Axe finds only part of the faults, so it does not replace a test with a screen reader. Screenshots of
every view are in docs/screenshots.
Type / at the start of a line, or after a space, to add a block: a list box under the slash shows text, headings,
lists, quote, code block, divider, table, and image. A word after the slash filters it. The arrow keys move in it,
Enter or Tab picks, and Escape closes it. The text keeps the focus and names the active item for a screen reader.
Select text to get a bubble menu with bold, italic, strikethrough, inline code, and link. Alt+F10 moves the focus
into it, and Escape gives the focus back to the text. An empty document says how to start.
In a table, the Table tool opens the table changes: add a row above or below, add a column before or after, delete a
row or a column, and delete the table. Markdown tables always have a header row, so when the selection covers the
header row there is no Add row above and no Delete row. When the selection covers every column, Delete table removes
them. In a code block, a Code language tool sets the language of the fence. Twelve languages have syntax colors
(Shell, CSS, Go, HTML, JavaScript, JSON, Markdown, Python, Rust, SQL, TypeScript, YAML). Any other language shows as
plain text: it is never guessed. The colors are decorations, so the stored text has none. The document menu has
Download Markdown, which saves the text as a .md file named after the title, and Import Markdown, which replaces the
text for everyone in the room with a .md file of 512 KB or less, when the projected document state stays under
the room limit. The check counts old text that Undo keeps. Both
use the same conversion as doc_read and doc_new, so an import refuses what doc_new refuses (a footnote, for
example). An image that a paste would not keep is dropped. Undo takes an import back.
Run it on your machine:
npm ci
npm run build # writes dist/ and dist/ui
export WRANGLER_SEND_METRICS=false
TOKEN=$(openssl rand -hex 32); echo "$TOKEN"
npx wrangler dev --var AGENTIC_DOCS_REGION:unknown --var AGENTIC_DOCS_UNSAFE_REGION:1 \
--var AGENTIC_DOCS_UNSAFE_TOKENS:1 --var "AGENTIC_DOCS_TOKENS:{\"$TOKEN\":\"human:Ada\"}"
Open the address that wrangler prints, paste the token, and sign in. The token stays in that tab and goes
when you close it. A token for an agent, such as agent_of_human:Scribe, makes its edits show as agent text.
A viewer token opens documents read only. The region setting turns the residency check off, so use it for
local work only.
How it works:
- The list, rename, delete, and the share dialog use the HTTP API through the typed client.
- The editor joins the live room of the document. A browser WebSocket cannot send an
Authorizationheader, so the page offers the token as a subprotocol, which only the live-room route reads. - The page saves the room to the stored document 1.5 seconds after a change, and when you leave the document in the app (not when you close the tab). A command over HTTP then reads the new text. A save never makes a document: if someone deleted it, the page says so, stops saving, and turns read only.
- An agent's text is the text whose writer is a logged edit by a non-human author (see the
clientfield of the edit log). Typing in the editor does not add to the edit log. - The page sets a content security policy: scripts, styles, fonts, and images come from the Worker only, and a document image that points to another host does not load.
- Run the tests with
npm run e2e. They startwrangler devwith the built UI, drive Chromium with Playwright, and run an axe check (WCAG 2.2 AA) on each view in both themes. Run them once withUPDATE_SCREENSHOTS=1to write the screenshots again.
Serve the UI from the npm package
The npm package ships the built UI in dist/ui. You do not need a clone to serve it. Install the package in
your Worker project and point your wrangler.jsonc at the files in it:
npm install agentic-documents
{
"name": "my-docs",
"main": "node_modules/agentic-documents/dist/live-worker.js",
"compatibility_date": "2026-01-01",
"compatibility_flags": ["nodejs_compat"],
"assets": { "directory": "node_modules/agentic-documents/dist/ui" },
"durable_objects": { "bindings": [{ "name": "SYNC_ROOM", "class_name": "SyncRoom" }] },
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["SyncRoom"] }]
}
Add the D1 and R2 bindings from the sample wrangler.jsonc. The Worker signs in with agentic-identity keys from the D1 database (see Sign-in).
The Worker serves the files in dist/ui first. It answers /api, /health, and any other path. If you deploy from a clone
with the sample wrangler.jsonc, run npm run build before wrangler deploy. The build writes dist/ui,
and the sample file reads it.
CLI
Every change needs an author: --as <kind>:<name> or AGENTIC_DOCS_AS. An agent that may accept
suggestions also needs --scopes docs:accept (or AGENTIC_DOCS_SCOPES). Documents live in
--dir (or AGENTIC_DOCS_DIR, default .agentic-documents) as Yjs update files.
export AGENTIC_DOCS_AS=agent_of_human:scribe
printf '# Plan\n\nShip on Friday.\n' > plan.md
npx agentic-documents new plan --file plan.md
npx agentic-documents outline plan
npx agentic-documents replace plan --text Friday --content Monday
npx agentic-documents suggest plan --text Monday --replacement Tuesday
npx agentic-documents comment plan --text Monday --body "Is this date firm?"
npx agentic-documents export plan --format md --out plan.md
npx agentic-documents export plan --format odt --out plan.odt # OpenDocument text
Run npx agentic-documents --help for every command.
MCP
npx agentic-documents mcp --as agent_of_human:claude starts a stdio server. It has one tool
per command, named doc_<command> (doc_read, doc_replace, doc_suggest, and so on). The
author is fixed when the server starts. A tool call cannot pick another author.
HTTP API
A Cloudflare Worker (Hono) mirrors the CLI one to one under /api/v1/docs. A read command is a
GET (flags are query parameters). A write command is a POST (flags are the JSON body).
curl -X POST $URL/api/v1/docs -H "authorization: Bearer $TOKEN" -d '{"id":"plan","markdown":"# Plan"}'
curl "$URL/api/v1/docs/plan/outline" -H "authorization: Bearer $TOKEN"
curl -X POST $URL/api/v1/docs/plan/replace -H "authorization: Bearer $TOKEN" -d '{"text":"Plan","content":"Roadmap"}'
curl "$URL/api/v1/docs" -H "authorization: Bearer $TOKEN" # list: id, title, creator, last edit
curl -X POST $URL/api/v1/docs/plan/rename -H "authorization: Bearer $TOKEN" -d '{"title":"Roadmap"}'
curl "$URL/api/v1/docs/plan/export?format=html" -H "authorization: Bearer $TOKEN"
curl -X POST $URL/api/v1/docs/plan/grant -H "authorization: Bearer $TOKEN" -d '{"to":"human:kit","role":"editor"}' # owner only
# add &remoteImages=true to keep web images; the default shows only images stored inside the document
- Each key stands for one principal. See Sign-in. A body cannot name its own author.
- Errors are
{ "error": { "code", "message", "matches" } }. Status: 400 bad input, 401 no valid token, 403FORBIDDEN(for example an agent withoutdocs:acceptthat accepts), 404 not found, 409 a conflict (ambiguous match, stale anchor, already decided), 503LOCKED. listshows each document's title, creator kind, and last edit.renamesets the title and keeps the text.removedeletes the whole document. A person may remove. An agent needs thedocs:removescope, for example{"as":"agent_of_human:sweeper","scopes":["docs:remove"]}. A live room keeps its own copy of the text afterremove, so an open client can bring the document back with a save.- A browser cannot send an
Authorizationheader on a WebSocket. It offers two subprotocols instead:agentic-docsandagentic-docs.bearer.<token in base64url>. Only the live-room route reads it, and the server answers withagentic-docs, never the token. A header value can show in the logs of a proxy or a host, so keep access logs of that header off. - Each edit log entry holds
client, the Yjs client id of the replica that wrote it. A UI uses it to mark text an agent wrote. A client writes it, like the rest of the log. GET /healthneeds no token. It reveals nothing else.- Create an R2 bucket with
wrangler r2 bucket create agentic-documents-data --jurisdiction euand a D1 database withwrangler d1 create agentic-documents --jurisdiction eu. Put the database id inwrangler.jsonc. SetAGENTIC_DOCS_REGIONtoeuorus. It is a claim that you make: it has no default, and the Worker answers 500MISCONFIGUREDwithout it. Local D1 reports no region, so underwrangler devonly, add--var AGENTIC_DOCS_REGION:unknown --var AGENTIC_DOCS_UNSAFE_REGION:1. That turns the residency check off. - Run it locally with
npx wrangler dev. SetWRANGLER_SEND_METRICS=falseto turn off wrangler telemetry.
SDK
agentic-documents/client is a typed client for the Worker HTTP API. It uses only fetch, so it runs in
browsers, Workers, and Node 22. The token sets the author and the role.
import { createClient, DocsApiError } from "agentic-documents/client";
const docs = createClient({ baseUrl: "https://docs.example.com", token: process.env.AGENTIC_DOCS_TOKEN! });
await docs.create({ id: "plan", markdown: "# Plan" });
const [section] = await docs.outline("plan");
await docs.replace("plan", { section: section.id, content: "Ship it." });
console.log(await docs.read("plan"));
try {
await docs.read("missing", {}, { signal: AbortSignal.timeout(5000) });
} catch (error) {
if (error instanceof DocsApiError) console.log(error.code, error.status); // NO_DOCUMENT 404
}
- One method per route:
create,list,read,rename,remove,outline,edits,insert,replace,delete,append,suggest,suggestions,accept,reject,comment,reply,resolve,reopen,threads,grants,grant,revoke,invites,invite,link,unlink,export,room,me,health,state, andcheckpoint. The WebSocket route is not a method. UseLiveDocfromagentic-documents/livefor it. - Every method takes an optional
{ signal }last. An abort rejects with whatfetchthrows. - An API error throws
DocsApiErrorwithcode(the API error code),status(the HTTP status), andmatches. A reply that is not an API error has the codeHTTP_ERROR. A 2xx reply with the wrong content type or shape has the codeBAD_RESPONSE. - The client never retries. A write is not safe to send twice.
- Pass
fetchto use your own, for a Worker service binding or for tests.
Library API
The root import, agentic-documents, exports the document model and the helpers that the CLI, the MCP server, and the
Worker use. Each group below has a short example. The other subpaths have their own sections: SDK,
File formats, Worker subpath, Live editing, and UI.
| Export | Use it for |
|---|---|
createAssistantSuggestions, AssistantProposal, AssistantSuggestions |
Propose edits from a machine author. See Review and assistant proposals. |
AgenticDoc, DocOptions, Section, Target, TextTarget, SectionTarget, Anchor, AnchorTarget, EditEntry |
The document. See Quick start. |
DocError, DocErrorCode |
The error that a failed call throws. It has a code. |
parseAuthor, PRINCIPAL_KINDS, Principal, PrincipalKind |
Read a kind:name string. List the kinds. |
markdownToBlocks, blocksToMarkdown, PmNode, schema |
Convert Markdown to and from ProseMirror JSON. schema is the editor schema. |
Threads, Suggestions, Thread, Suggestion, Message |
Comments and suggested edits on text. |
commands, Command |
The command table. The CLI, the MCP server, and the HTTP API each read it. |
EXPORT_FORMATS, exportDoc, ExportFormat, ExportOptions, ExportResult |
Export a document as md, html, or odt. |
exportOdt |
Write blocks as an OpenDocument text file. It returns bytes. |
createFileStore, createFileBlobs, FileBlobsOptions |
The local file store. It keeps the catalog and the document bytes. |
DocsService, DocsPorts, DocInfo |
Create, read, change, and share documents over a store and blobs. |
ROLES, INVITE_ROLES, SHARE_SCOPE, parseSubject, contextOf, StoreSharing, DocSharing, GrantInput, InviteInput, LinkInput |
Roles, sharing helpers, and the sharing port. |
Document and author
import { AgenticDoc, parseAuthor, PRINCIPAL_KINDS } from "agentic-documents";
PRINCIPAL_KINDS; // "human", "agent_of_human", "agent_of_agent", "agent", "user_token"
parseAuthor("agent_of_human:scribe"); // { kind: "agent_of_human", name: "scribe" }
const doc = AgenticDoc.fromMarkdown("# Plan\n\nShip on Friday.", { kind: "agent_of_human", name: "scribe" });
doc.replace({ text: "Friday" }, "Monday", { kind: "agent_of_human", name: "scribe" });
parseAuthorreturnsundefinedfor no value. It does not check the kind. A change checks it and throwsDocErrorwithINVALID_AUTHOR.
Markdown and blocks
import { blocksToMarkdown, markdownToBlocks, schema } from "agentic-documents";
const blocks = markdownToBlocks("# Plan\n\nShip **Friday**.");
blocks[0].type; // "heading"
blocksToMarkdown(blocks); // "# Plan\n\nShip **Friday**.\n"
schema.nodeFromJSON(blocks[0]); // a ProseMirror node, for an editor you build yourself
Comments and suggestions
import { AgenticDoc, Suggestions, Threads } from "agentic-documents";
const person = { kind: "human", name: "ada" } as const;
const agent = { kind: "agent_of_human", name: "scribe" } as const;
const doc = AgenticDoc.fromMarkdown("# Plan\n\nShip on Friday.", person);
new Threads(doc).add({ text: "Friday" }, "Is this date firm?", agent); // returns the thread id
const change = new Suggestions(doc).add({ text: "Friday" }, "Monday", agent); // returns the suggestion id
new Suggestions(doc).accept(change, person);
Threadshasadd,reply,resolve,reopen, andlist.Suggestionshasadd,accept,reject, andlist.- A person (
humanoruser_token) accepts a suggestion that someone else made. An agent accepts one only when it holds thedocs:acceptscope. Nobody accepts their own suggestion. Otherwise the call throwsFORBIDDEN.
Commands
import { commands } from "agentic-documents";
// `service` is a DocsService, as in "Stores". `author` is a Principal.
commands.map((command) => command.name); // "new", "list", "read", "replace", "export", ...
const read = commands.find((command) => command.name === "read")!;
await read.run(service, { id: "plan" }, author); // the same call the CLI, MCP, and HTTP make
- A
Commandhasname,description,shape(a Zod shape for the input),readOnly, andrun(service, input, author, context?). - Use the table to list or call the commands in your own host. Each call still needs an author.
Export
import { exportDoc, exportOdt, markdownToBlocks } from "agentic-documents";
import { writeFile } from "node:fs/promises";
// `doc` is an AgenticDoc, as in "Document and author".
const md = exportDoc(doc, "md"); // { format, contentType, encoding: "utf8", data: "# Plan...", warnings: [] }
const odt = exportDoc(doc, "odt"); // encoding "base64": data is the .odt file in base64
await writeFile("plan.odt", exportOdt(markdownToBlocks("# Plan\n\nShip on Friday.")));
exportDoc(doc, format, { remoteImages? })returns anExportResult. Forodtit setsencodingtobase64. Formdandhtmlit setsutf8.exportOdt(blocks)returns the bytes of an.odtfile. It drops the warnings. UseexportDoc, orexportFilefrom File formats, to read them.
Stores
import { createFileBlobs, createFileStore, DocsService } from "agentic-documents";
import { join } from "node:path";
const dir = ".agentic-documents";
const service = new DocsService({
blobs: createFileBlobs(join(dir, "blobs")),
store: createFileStore(join(dir, "store")),
});
createFileStore(dir, options?)keeps the catalog in<dir>/store.json. Two stores on the same path share data across processes. A write that loses a race runs again, up to 64 times, then throwsStoreConflictError.createFileBlobs(dir, options?)keeps each document as a file under<dir>/blobs/. A conditional write that loses returnsnull. A lock wait that runs out (10 seconds) throwsLOCKED.- Both take
region.nowsets the clock, for tests.createFileBlobsalso takesurlBaseandurlSecret(32 bytes or more). With both set,signedUrlreturns a signed HTTP URL. - Release 0.9 and earlier kept blobs in
<dir>/<hash>. The first read or write of a key moves its old blob to the new layout. A delete removes it. - The CLI uses
<dir>/blobsand<dir>/store, as in the example above. So the CLI's blob files sit in<dir>/blobs/blobs/.
Service
const ada = { kind: "human", name: "ada" } as const;
await service.create("plan", "# Plan\n\nShip on Friday.", ada);
await service.write("plan", (doc) => doc.replace({ text: "Friday" }, "Monday", ada), ada);
await service.read("plan", (doc) => doc.toMarkdown()); // "# Plan\n\nShip on Monday.\n"
await service.list(); // [{ id: "plan", title: "plan", createdBy, createdAt, updatedAt, updatedBy }]
DocsServicetakes{ blobs, store }(theDocsPorts) and optionalDocOptions.service.info(id)returns aDocInfowithcreatedByandcreatedAt, ornullwhen the document has no catalog entry. Alistrow addsid,title,updatedAt, andupdatedBy.createthrowsDOCUMENT_EXISTSwhen the id is taken.writesaves only when nobody saved in between. If someone did, it loads the document and runs your callback again, up to 64 times, then throwsCONFLICT.readgives the document to a callback and returns the callback's result.service.sharingis aStoreSharing. See the next group.
Sharing helpers
import { contextOf, INVITE_ROLES, parseSubject, ROLES, SHARE_SCOPE } from "agentic-documents";
ROLES; // "viewer", "commenter", "editor", "owner"
INVITE_ROLES; // "viewer", "commenter", "editor"
SHARE_SCOPE; // "docs:admin"
parseSubject("agent:scribe"); // { kind: "agent", id: "scribe" }
const ada = { kind: "human", name: "ada" } as const;
await service.sharing.grant(contextOf(ada), "plan", parseSubject("human:grace"), "editor");
await service.sharing.grants(contextOf(ada), "plan");
contextOf(author)gives the sign-in context for a call that has no HTTP sign-in: the CLI, the MCP server, and the development token table. Ahumanholdsdocs:admin. Any other kind keeps only itsread,comment,write, andadminscopes.- Each sharing call needs an owner of the document. The creator of a document is its owner, unless the author cannot own one.
StoreSharingimplementsDocSharing. It takes the store and a lookup for documents.DocsServicebuilds it for you.GrantInput,InviteInput, andLinkInputare the input types.
Review and assistant proposals
ReviewPanel and CommentAnchor are public React components from agentic-documents/ui.
./ui also exports AgenticDoc, Anchor, Principal, createAssistantSuggestions,
AssistantProposal, and AssistantSuggestions for browser hosts without Node adapters.
Use the same AgenticDoc as the editor's host.doc (doc.ydoc). The host saves and syncs it.
The panel observes document updates. It has review and suggest modes, exact-text proposals,
comment anchors, and keyboard controls. Ambiguous text reports an error; use the engine's
occurrence or anchor targets for repeated text. Agent proposals show a Machine-made label.
import { AgenticDoc, createAssistantSuggestions, type AssistantProposal, type AssistantSuggestions,
ReviewPanel, CommentAnchor, type ReviewPanelOptions } from "agentic-documents/ui";
const doc = AgenticDoc.fromMarkdown("old text", { kind: "human", name: "Ada" });
const assistant: AssistantSuggestions = createAssistantSuggestions(doc, { kind: "agent", name: "Scribe" });
const proposal: AssistantProposal = { target: { text: "old text" }, replacement: "new text" };
assistant.propose(proposal); // Stores a pending suggestion. Keeps the text as it is.
const options: ReviewPanelOptions = {
doc, principal: { kind: "human", name: "Ada" }, writable: true,
onAnchor: (anchor) => revealInEditor(doc.resolve({ anchor })),
};
// Render beside DocumentEditor. The mode does not intercept the editor's direct edits.
<ReviewPanel {...options} mode="suggest" />;
<CommentAnchor doc={doc} anchor={doc.anchor("old text")} onAnchor={options.onAnchor} />;
AssistantSuggestions is the extension point for a host's assistant adapter. It exposes only
propose, bound to a machine author. It makes no provider calls. Targets support at for
insertions and an empty replacement for deletion. Invalid or stale targets throw.
The panel defaults to read only. Set writable from the host's permissions. Decision buttons
require a different principal from the author. Agents also need docs:accept. The engine
always refuses agent self-decisions, including through the SDK, CLI, MCP, and HTTP API.
The engine permits a person to withdraw their own suggestion; the panel uses different-principal
review for both decisions. A stale proposal cannot be accepted. It can be rejected.
These local controls do not replace server authorization. Only render trusted documents and
principals. onAnchor lets the host select or scroll to a range; stale anchors show as unavailable.
Embed the editor
agentic-documents/ui renders the editor inside your own app. You give it a document id, a client, the principal who
uses it, and an optional live room. With a room, people and agents edit together through agentic-realtime and see each
other. It needs react and react-dom 19 (peer dependencies).
import { createClient } from "agentic-documents/client";
import { mountEditor } from "agentic-documents/ui";
import "agentic-design/styles.css";
import "agentic-documents/ui.css";
const client = createClient({ baseUrl: "https://docs.example.com", token });
const principal = await client.me(); // { kind, name, role }
const editor = mountEditor(document.getElementById("editor")!, {
id: "plan",
client,
principal,
room: { url: "wss://docs.example.com/api/v1/docs/plan/ws", token },
});
// Later: editor.update({ ...options, id: "other" }), or await editor.unmount() to save this tab's changes and leave.
- In a React app, render
<DocumentEditor id client principal room onError onLeave />instead.onLeavegets the promise of the last save. clientneeds onlystateandcheckpoint. A client fromcreateClienthas them, or pass your own. Keep the same client object between renders: a new one opens the document again.principalneeds akind(one of the principal kinds) and aname. A missing or unknown kind throws aTypeError. It is never given a default. Only theroleeditorcan change the text. Any other role, or no role, opens it read only. The server still checks every write against the token. Use the principal thatclient.me()returns for the same token, so the editor labels the text with the kind that the server gives the token.- When an agent principal types, the editor writes one
liveentry to the edit log for its Yjs client. The editor and the web UI then mark that text as agent work for everyone who opens the document. room.urlis the WebSocket address of the document room. In a browser the token goes in a WebSocket subprotocol. Passroom.WebSocketfor a room that takes its token another way. Without a room, the editor saves through the client and shows no other people.- The editor saves 1.5 seconds after this tab changes the text, and again on
unmount. The promise fromunmountrejects when that last save fails. A save never makes a document, so a removed document stays removed. - The styles sit under the
.agd-docclass and use the agentic-design tokens, except the form rules of the link and image dialogs (.agd-form,.agd-form-actions), which apply page-wide because the dialogs render in a portal. Loadagentic-design/styles.cssfirst. - File import and export are in
agentic-documents/formats(see File formats). - Extension points for an app, such as comments or an assistant:
slashItems: more slash menu items, after the built-in ones. Each is{ title, description, keywords?, icon?, run(editor) }.bubbleActions: more tools in the bubble menu over a selection, after the built-in ones. Each is{ label, icon, run(editor) }.rungets the editor with the selection in place. They show where the built-in tools show: for a person who can edit, over text that is not code, an image, or a cell selection.extensions: more Tiptap extensions: plugins, decorations, and keyboard shortcuts. Keep this array stable. The editor reads it when it is built, including after a role or room change. Updating the array alone does not rebuild the editor. To apply new extensions at once, unmount the editor and mount it again. An extension that adds or changes a node, a mark, or an attribute stops the editor with a message that names it, since the stored document keeps only the nodes, marks, and attributes ofschema.editorMarkdown(editor)andsetEditorMarkdown(editor, markdown)read and replace the text as Markdown, with the same Markdown conversion as the server. Import drops images that a paste would refuse.setEditorMarkdownthrows if the editor is read only, has no sync document, or the projected Yjs state, including text kept for Undo, exceeds the room budget.- Invalid host options throw a
TypeErrorbefore the editor opens or updates. - The types
SlashItem,BubbleAction, andEditorExtrasare exported.
- The carets and the selections of the other people and agents in the room show in the text, each with the name that the server stamped for that peer. A person's selection is a tint. An agent's caret and selection are dashed, so its selection does not look like agent-written text.
onEditorgets the Tiptap editor when it is ready, andnullwhen it goes. Use it to read the selection or the JSON, or to insert content. The editor setsinjectCSS: false, andui.csshas the base rules, so a page whose content security policy forbids inline styles shows it right.
Use your own Yjs document
An app that keeps the document in its own Yjs room gives it as host. The editor then binds to that document. It does
not load, save, join a room, or close it, and it shows no save state. The app does all of that. client is not needed,
and room together with host throws a TypeError.
const editor = mountEditor(element, {
id: "plan",
principal,
host: {
doc, // a Y.Doc. The editor reads and writes its `default` XML fragment.
presence: sync, // optional: a SyncClient of agentic-realtime, or { awareness, peers, onPeers }
status: () => status, // optional: "connecting" | "connected" | "disconnected"
onStatus: (listener) => stop, // optional: tell the editor when the status changes
},
});
- Use the same
yjscopy as the editor. A document from another copy is refused with aTypeError. - With
presence, the editor shows the peers and their carets. It reads the name and the Agent tag frompeers, so give peers whosemachineflag the server sets.peersmust always give the current list, as the getter of aSyncClientdoes. A list thatonPeersupdates butpeersdoes not leaves the carets undrawn. - The editor binds again only when
docorpresencechanges. A newhostobject with the same two keeps it as is. So keep the samepresenceobject between renders: a new one rebuilds the editor, and the caret and the selection go. It readsonStatusonce each time it binds. - With
host,idonly names the editor: nothing loads by it. A newidinupdatestill builds a new editor. onEditoris read when the editor is made. That callback getsnullwhen the editor goes, after the editor is destroyed, so do not read from it then. A newonEditorinupdateis used for the next editor.- When an agent principal types, the editor writes one
liveentry to theeditsarray of the document, as above.
Mount the whole app
mount(element, options) of agentic-documents/ui renders the whole web UI inside your page: the document list, the
editor, rename, delete, and share. You give it a base URL and a callback that returns the token. Your page keeps
sign in and its own address. It needs react and react-dom 19 (peer dependencies).
import { mount } from "agentic-documents/ui";
import "agentic-design/styles.css";
import "agentic-documents/ui.css";
import "agentic-documents/app.css";
const app = mount(document.getElementById("app")!, {
baseUrl: "https://docs.example.com",
getToken: () => session.token(), // a string, or a promise of one
documentId: "plan", // optional: open this document first
onError: (error) => console.warn(error),
});
// Later: app.update({ ...options, documentId: "other" }), or app.unmount().
getTokenruns when the app opens, and again when the person picks Try again on the sign-in error. The app does not ask again when a token expires later: callapp.update({ ...options, getToken: newFunction })to sign in with a new token.onErroris called when sign in fails. The server sets the author and the role from the token. The app shows no Sign out button.- A link in the app moves inside the app. It does not change the address of your page.
baseUrlmust be a fullhttporhttpsaddress with no query, fragment, or credentials. It may have a path, such ashttps://example.com/docs.- The theme picker uses the agentic-design theme. It sets
data-themeon the page and saves the choice in local storage, so it changes the whole host page. unmountremoves the app. An open document saves its changes and leaves its room.app.cssholds the layout rules of the app. They use plain class names such as.page-headand.muted, so load them on a page that has its own styles with care. To embed one document only, usemountEditorabove.
File formats
agentic-documents/formats imports and exports files, so an app that embeds the editor needs no mapping
code of its own. ODT and DOCX go through the neutral text model of
agentic-formats. Markdown and HTML use the mapping of this package.
import { exportFile, importFile } from "agentic-documents/formats";
const { doc, title, warnings } = importFile(bytes, "docx", { kind: "human", name: "Ada" });
const odt = exportFile(doc, "odt", { title }); // { contentType, data (Uint8Array for odt), warnings }
importFile(input, format, author, { limits? })readsmd(text or UTF-8 bytes),odt, ordocx(bytes) into a newAgenticDoc. The author makes the onecreateedit, so a file that an agent imports stays labeled as agent work.exportFile(doc, format, { title?, remoteImages? })writesmdorhtml(text), orodtordocx(bytes).- Each result has
warnings: one line for each kind of content that the other side cannot hold. The text of that content stays. - A file that is not valid, or that goes past a limit in
limits(the agentic-formats defaults), fails withINVALID_INPUT. Formd, onlymaxInputBytesapplies. A link that is not a web, mail, or relative address is dropped, and its text stays. toTextDoc(blocks)andfromTextDoc(model)are the mapping itself, for an app that works with the model.
Sharing
A document has roles: viewer, commenter, editor, and owner. Grants, invites, and links come from
agentic-identity. Only an owner may use them. The person who creates a document owns it. Over the HTTP API, with
agentic-identity keys, an agent that creates a document for a person makes that person the owner. Over the HTTP API,
a caller also needs the docs:admin scope (or *:admin). That holds for a person and for an agent. In local mode, the
CLI and the MCP server give a person that scope, and an agent needs --scopes docs:admin. With --live, the token sets
the scopes. A development token gives a person with the editor role that scope too. Everyone else gets
FORBIDDEN (HTTP 403), and an agent cannot grant itself a role.
npx agentic-documents grant plan --to human:kit --role editor # --to is human:<id> or agent:<id>
npx agentic-documents grants plan # who holds a role
npx agentic-documents revoke plan --to human:kit
npx agentic-documents invite plan --role viewer --email [email protected] # or --handle <agent>
npx agentic-documents link plan --role viewer --expiresAt 1893456000000 --signedInOnly true # the token shows once
npx agentic-documents unlink plan --link <link id>
The same commands are MCP tools (doc_grant and so on), HTTP routes (POST /api/v1/docs/:id/grant), and SDK methods
(docs.grant("plan", { to: "human:kit", role: "editor" })). An invite list shows no email address. A document can have
no owner record. This happens to a document made before sharing. It also happens when its author had no docs:write
scope, or when its author name is not a valid grant name (1 to 128 letters, digits, -, and _). The creator claims
such a document with a write scope, when the root of the caller's delegation chain is that creator. A document with no
creator in the catalog, one whose author name is not a valid grant name, or one made by an agent that acts for a person,
cannot be claimed. A rename can record a creator for a document with no creator, so check again after a rename.
Features
- A Yjs document that a Tiptap editor can open. It uses the y-prosemirror layout.
- Headings, paragraphs, lists, task lists, tables, code, images, links, and marks.
- Markdown in and out.
- An outline. Each section has an id that stays the same when people edit around it. Blocks before the
first heading are the
preamblesection (level 0). It is listed when they exist, and it is always a valid target. A replace or delete on it has no heading to keep. - Share a document: roles, invites, and share links, for an owner (see Sharing).
- Insert, replace, and delete by section id or by exact text. Append to the end.
- Text anchors that follow their text when other replicas edit.
- Clear errors:
NO_MATCH,AMBIGUOUS_MATCH,STALE_ANCHOR,NO_SECTION,FORBIDDEN. - Comment threads on text, with replies and resolve. A thread follows its text.
- Suggested edits (insert, replace, delete). Nothing changes until a person accepts. An accepted edit is logged with the accepter as editor and the suggester as origin.
- Who may decide: a
humanor auser_tokenwho is not the suggester. An agent cannot accept unless its principal carries thedocs:acceptscope. Nobody accepts their own suggestion. A person can withdraw their own suggestion. An agent can never accept or reject its own. Other deciders getFORBIDDEN. Accepting an accepted suggestion changes nothing. Editor roles come withagentic-identity. - Two replicas can accept one suggestion before they meet. After the merge every replica keeps one application and undoes the other, so the text and the edit log hold it once. An accept beats a reject made at the same moment.
- Export to ODT (OpenDocument text): headings, paragraphs, bold, italic, strike, code, links, lists,
tables, and PNG, JPEG, and GIF images as embedded files.
agentic-formatswrites the file, with the mimetype stored first. The CLI and the API send the file bytes. A quote, a code block, a rule, a list start number, and a WebP image are not kept in full (see Limits). Each loss is inwarnings(SDK and MCP), on stderr (CLI), and in thex-export-warningsheader (API). - Export to Markdown and to self-contained HTML. The HTML loads nothing from other hosts. An image
stored in the document as a
data:URI (PNG, JPEG, GIF, or WebP) shows. Any other image becomes its alt text, because a remote image tells its host who opened the file.remoteImagesturns that back on. - Every edit, message, and suggestion needs an author. The edit log keeps the kind and the name, so
agent work stays labeled. The log holds no content. Over the CLI, MCP, and HTTP the server or
the operator sets the author. In a live room a client writes its own log entries, suggestion decisions, and
threads. A save merges them into the stored document. So after a live client saves, treat all three
as advisory. The room's own record of who sent updates in the room is at
GET /api/v1/docs/:id/room. It does not cover text written over HTTP, the CLI, or MCP before the room opened: the client that seeds the room appears there.
Worker subpath
agentic-documents/worker is the HTTP API as a Hono app. Use it to run the API in your own Worker. It exports:
| Export | Use it for |
|---|---|
default |
A Worker { fetch } over D1 and R2, with no live rooms. |
createApp(source, options?) |
The Hono app. source is a DocsService, or a function from the bindings to one. options.live makes the live-room server. options.auth makes the Auth port. |
serviceFor(env) |
The DocsService over the DB and DOCS bindings, after the region check. |
regionOf(env) |
The region claim from AGENTIC_DOCS_REGION. It throws MISCONFIGURED for a missing or unsafe claim. |
WorkerEnv, AppOptions, ServiceSource, DocRole |
Types. |
import { createApp, serviceFor, type WorkerEnv } from "agentic-documents/worker";
const app = createApp(serviceFor);
export default { fetch: (request: Request, env: WorkerEnv, ctx: unknown) => app.fetch(request, env, ctx as never) };
For live rooms, deploy agentic-documents/live-worker instead (see Live editing).
Sign-in
The Worker reads Bearer keys with createStoreAuth from agentic-identity. The principals and the keys live in
the principals and credentials collections of the D1 database that the Worker already binds as DB. Make them
with agentic-identity (its server, its CLI, or issueCredential) against that database. A key that is missing,
expired, or revoked gets 401. A revoked agent stops, and so does every key below it.
- The key sets the author. The principal id is the author name, and the principal kind is the author kind.
- The role comes from the scopes of the principal, after the delegation chain narrows them.
docs:write(ordocs:admin, or*:write, or*:admin) makes an editor. A principal with no write scope is aviewer: it reads and cannot write. docs:adminalso grants an agent thedocs:acceptanddocs:removescopes. A person needs no scope for either.- The Worker takes no sign-in from the request body, and it ignores
AGENTIC_DOCS_TOKENSunless you switch it on.
Development fallback: a static token table
For local work with wrangler dev, you can use a fixed table of tokens. It has no expiry and no revocation, so
never use it in production. Set both variables:
AGENTIC_DOCS_UNSAFE_TOKENS=1switches the table on. With any other value the table is ignored.AGENTIC_DOCS_TOKENSis a JSON object from token to author:{"<32 or more random bytes>":"agent_of_human:scribe"}. To give an agent the right to accept suggestions, use{"as":"agent_of_human:approver","scopes":["docs:accept"]}as the value. For a read-only token, use{"as":"human:Vic","role":"viewer"}. A token with no role is an editor. Make a token withopenssl rand -hex 32. A token holds onlyA-Z a-z 0-9 _ -(32 to 512 bytes), and an author name holds onlyA-Z a-z 0-9 . _ -(up to 128 characters, not starting with., no space or@).- A token under 32 bytes, a token with other characters (for example base64 with
+or/), a value that is not a valid author, or a value that is not a JSON object makes the Worker refuse every request (401). It logs the reason once, never the token.
Live demo
A demo Worker runs at https://agentic-documents-demo.hello-fd1.workers.dev. It is a staging build. Open the
address to see the web UI. GET /health answers 200 with no token. The UI asks for a token and the API refuses calls without one. The demo does
not publish a token, so run your own Worker to try edits (see Live editing). The demo may be reset at any
time.
The demo uses wrangler.demo.jsonc: a D1 database and an R2 bucket with the eu jurisdiction, live rooms in
Durable Objects, and AGENTIC_DOCS_REGION=eu. To redeploy, a maintainer with a Cloudflare token runs:
CLOUDFLARE_API_TOKEN=<token> npm run deploy:demo
The demo is a staging build, so it sets AGENTIC_DOCS_UNSAFE_TOKENS=1 and reads its static token table. The tokens live in a Worker secret, never in the repo or in CI. Set them with
WRANGLER_SEND_METRICS=false npx wrangler secret put AGENTIC_DOCS_TOKENS --config wrangler.demo.jsonc. To make your own demo, copy
wrangler.demo.jsonc, change the name, and put your own database id and bucket name in it. Make the database with
WRANGLER_SEND_METRICS=false npx wrangler d1 create <name> --jurisdiction eu, because the jurisdiction is fixed when you create it. Make the bucket with
WRANGLER_SEND_METRICS=false npx wrangler r2 bucket create <name> --jurisdiction eu.
Live editing
A person and an agent can edit one document at the same time. Each sees the other's changes. The Worker
hosts one room per document, on a Durable Object, through agentic-realtime.
Deploy agentic-documents/live-worker. The sample wrangler.jsonc binds SYNC_ROOM and sets the migration.
Your Worker's main module must export the SyncRoom class, as the example does.
import worker, { SyncRoom } from "agentic-documents/live-worker";
export { SyncRoom };
export default worker;
A person or an agent joins a room with LiveDoc:
import { LiveDoc } from "agentic-documents/live";
const live = await LiveDoc.join({ server: "https://docs.example.com", id: "plan", token });
await live.edit((doc, author) => doc.append("Ship on Friday.", author));
console.log(live.peers); // the other people and agents. `machine` is true for an agent.
await live.save(); // write the room state to the stored document
await live.leave();
The CLI and the MCP server join rooms too. Every command then runs in the room and saves:
export AGENTIC_DOCS_TOKEN=... # a bearer token. The server sets the author kind and the role.
export AGENTIC_DOCS_LIVE_URL=https://docs.example.com # or pass --live <url>
npx agentic-documents append plan --content "Ship on Friday." --live https://docs.example.com
npx agentic-documents mcp --live https://docs.example.com
- The server reads your author kind and role from the token (
GET /api/v1/me). An agent token writes agent edits.--as,--scopes, and--dirdo not apply with--live: the CLI refuses them and exits 2. This holds whenAGENTIC_DOCS_LIVE_URLis set too, so unset it for a local call. - A room starts from the stored document.
savemerges the room back into the stored document, soread,export, and the other commands see live edits. A save also creates a document that has none. The CLI and MCP server save after each change. If a save fails, the room keeps the change and the error says so: do not repeat it.newmakes the document in the store, and a room picks it up on the next join. A document over 1,000,000 bytes cannot open in a room or be saved from one. A save refuses an update with a wrong kind of content or a malformed thread, suggestion, or log entry. HTTP edits reach an open room only when a client joins and seeds it. - A
viewertoken joins and reads. The room drops its updates, andeditandsavefail withFORBIDDEN. A principal with nodocs:writescope is a viewer (see Sign-in). In the development token table, use{ "as": "human:Vic", "role": "viewer" }. GET /api/v1/docs/:id/roomlists who sent updates in the room, and whether each is machine-made.- Pass
reconnect: {}tojointo keep trying after a drop. Edits made offline merge on return. A room that restarts recovers its state from the Durable Object.
Limits of live editing:
- The edit log, the suggestion decisions, and the threads live in the document, and a client writes them.
A modified client can write a log entry with another author, or accept its own suggestion. The room's
list at
/roomis the record to trust for who sent updates. It does not cover text written before the room opened. - Until a client saves, a command over HTTP reads the stored copy, not the room.
- After the room refuses an edit, the local copy of
LiveDocstill holds it. Join again. - Durable Objects have no US jurisdiction. With the region
usa room sits near the first client that opens it. Witheuit stays in the EU. The localwrangler devcannot place rooms.
Rules for text matching
- A match is exact and case sensitive.
- A match stays inside one block. It cannot cross a paragraph, a cell, or a hard break.
- More than one match is an error, and matches may overlap:
anamatches twice inbanana. Passoccurrence(1 is the first start) to choose. - A replacement takes the marks of the first matched character.
Limits
- The UI has no accounts. You paste a token, and the token sets your role. The share dialog lists, changes, and removes roles, makes invites, and makes a share link, for an owner (see Sharing).
- The UI marks agent text from the edit log, which a client writes. A modified client can mark its own text as a person's, or a person's as an agent's. The last edit shown in the list is approximate when two writes race.
- The UI is English only. It needs a browser with
WebSocketand ES2022. - Sharing stores roles, invites, and link tokens. The document commands do not check them yet: a token's role
(
editororviewer) still decides what it may do. Nothing sends an invite email, and nothing accepts an invite or opens a link token in this package yet. The list of links is not available, so keep the id of a link that you make. The share dialog shows it next to the token. Removing a document erases its sharing data. - The Worker keeps each document in R2 and a catalog (who made it, and when) in D1. Both come from the
shared
agentic-identityports. A save is conditional on the version it read, so two writers cannot overwrite each other. The CLI and MCP server keep the same data in local files. - The CLI and MCP server keep files under
<dir>/blobsand<dir>/store. The catalog is<dir>/store/store.json, fromagentic-identity/file. The first write after an upgrade imports the catalog of release 0.8 once. Files from an older release that sit directly in<dir>are not read. The catalog store waits for a rival process with a short lock, runs a write again up to 64 times, then fails withStoreConflictError. The wait blocks the process. - A key's role comes from its scopes:
editorwith a scope that coversdocs:write,viewerwithout one. A development token (see Sign-in) sets its own role instead. A viewer cannot call a write command or write in a live room. The role is not per document: the Worker does not check the roles that sharing stores yet. - A save is a compare-and-set. The file store takes a lock file, checks the version, and renames a
uniquely named temp file into place. A writer that loses loads the document again and runs
its change again. A lock older than 10 seconds counts as dead and is taken over. A lock that
cannot be read, or that is dated in the future, counts as live. A writer that waits more than
5 seconds fails with
LOCKED. A writer that loses 64 times in a row fails withCONFLICT. The lock is for one machine. Two machines on a network file system need a store with its own conditional write. --asandAGENTIC_DOCS_ASare self-declared labels. The CLI does not prove who runs it, so--as human:Adalabels work as human on any machine that can run the command. Only the HTTP API (tokens) and an MCP server that a trusted host starts fix the author for the caller.- Suggestions work on text, not on whole sections.
- A scope on a principal is trusted as given. Nothing here proves who a caller is.
- The
exportcommand covers Markdown, HTML, and ODT. DOCX export, and ODT and DOCX import, are inagentic-documents/formatsonly. - File formats through
agentic-documents/formats: a quote becomes plain paragraphs, a code block becomes a paragraph of code text, a horizontal rule is left out, a numbered list starts at 1, and a task list becomes a list that starts each item with☐or☑. On import, underline, superscript, and subscript are left out, and a footnote becomes text in brackets, and code text keeps only its code mark. Each one is named inwarnings. - ODT export: a quote becomes plain paragraphs, a code block becomes a paragraph of code text, a rule is
left out, a numbered list starts at 1, a list item keeps one paragraph, and a WebP image becomes its alt text.
Comments and suggestions are not written. A code block loses its language. A table
cell span is not written (the model has none yet). A task list becomes a bullet list that starts each
item with
☐or☑. A remote image is not linked, because an office program would ask its host for it when the file opens. It becomes its alt text. - Markdown with footnotes, or any syntax the model has no place for, is refused with
INVALID_INPUT. Nothing is stored. Reference links and image references are resolved to their URL. An unused reference definition is dropped. - Markdown import drops link titles, table column alignment, and the part of a code fence line after the language.
- Markdown output trims whitespace at the edge of a block and skips empty paragraphs. A text
target that includes a trailing space can match in the document and not in
readoutput.
Run locally
bun install # or: npm ci
bun run dev # or: npm run dev
curl http://localhost:8787/health
The script builds the app, then runs wrangler dev with the region check off (AGENTIC_DOCS_REGION=unknown). /health needs no token. To sign in to the UI on your machine only, pass a static token: TOKEN=$(openssl rand -hex 32); echo "$TOKEN"; npm run dev -- --var AGENTIC_DOCS_UNSAFE_TOKENS:1 --var "AGENTIC_DOCS_TOKENS:{\"$TOKEN\":\"human:Ada\"}".
The scripts call npm and node (Node 22 or newer), so install both, even if you use bun. They need a POSIX shell.
Run npm run dev:smoke to check this path. It starts the dev server on port 8799, waits for GET /health to answer 200, and stops the server. CI runs it as the "dev smoke" step. Set DEV_SMOKE_PORT to use another port.
Develop
npm ci
npm run ci:local
Account export and erase
Use documentsAccount from agentic-documents/account with the identity account
registry that exposes AccountPackage, or call it directly. Identity 0.9 uses
the older AccountApp contract and needs a host adapter.
The subject is { id, kind }. Call once for each principal, including delegated
agents. Kinds keep people and agents distinct.
The host authenticates the subject and authorizes the account operation.
import { documentsAccount } from "agentic-documents/account";
const subject = { id: "ada", kind: "human" } as const;
const data = await documentsAccount.exportAccount(subject, { store, blobs });
const counts = await documentsAccount.eraseAccount(subject, { store, blobs });
The export returns plain JSON collections:
documentsholds owned catalog rows and Markdown.blobsholds their full Yjs state as base64. This includes history, comments, suggestions, inline images, and collaborators' content.grantsholds the subject's roles on documents and folders.sharingholds sharing rows for owned resources and invites the subject made.authoredholds the subject's own author records on other documents. It does not include document snapshots or other people's messages.
The package stores no separate attachment or version files. Image URLs refer to external data.
Erase deletes sole-owned documents and their sharing records.
It deletes sole-owned folders after their children. A folder with a child that
must stay stops the operation before any change.
Shared resources keep other owners and their content.
The contributor handles documents and their parent folders.
It also handles empty folders on which the subject holds a grant.
Other apps' resources stay outside the export and erase.
The subject's grants and invites go. A grant counted in grants is also counted
in sharing when it goes. Author names become (erased) in catalog rows,
edits, comments, suggestions, and acceptance records.
The kind stays, so machine-made work stays labeled.
Shared text stays, including personal data within text.
A second erase counts zero. A failed erase throws. Retry to finish it.
Damaged blobs, catalog rows, grants, and owner paths stop the operation.
Legacy grant keys with a valid kind work without migration.
Run migrateGrants first for older grants.
A grant with no kind on a document or its folder stops the operation.
Stop writes before export and keep them stopped until erase ends.
Pass saveRoom(id) to flush live rooms before reads.
Pass eraseRoom(id) to clear rooms before deletion or anonymization.
Without these hooks the host must stop and clear rooms itself.
Run this contributor before identity account erasure.
The host exports identity audit rows before erase.
This contributor deletes audit rows for each resource it deletes.
It does not export or count audit rows.
The host handles the other identity audit rows.
The contributor scans catalog and sharing rows. For legacy blobs with neither
row, pass documentIds: async () => ids from a complete storage inventory. The
Blobs port cannot list keys. A legacy document without an owner record uses its
catalog creator as owner only for a person or independent agent with a valid
grant name.
A delegated agent cannot claim it. A blob without either record can only be
matched by its author records.
Migrate old file blobs before account operations.
Backups and external image hosts need their own retention and erase steps.
Invites keep a creator id with no kind. Use unique ids across principal kinds. Invites with the subject's creator id are exported and deleted. Invite recipients cannot be matched to an account. Links have no creator record and leave with the document.
Review in a host editor
Import agentic-documents/review to bind agentic-comments to the editor's
Yjs document. The editor must use the default collaboration field. This entry
has no React runtime dependency. Use this entry's exported stamp to make actors;
it bundles the comments engine for browser use. It uses separate review maps from the document
SDK's Threads and Suggestions; it does not migrate those records.
import { anchorForQuote, bodyText, createReview, stamp } from "agentic-documents/review";
// Use the principal your host authenticated, not a name from request input.
const review = createReview(ydoc, stamp({ kind: "human", name: principalId }));
const machine = stamp({ kind: "agent", name: "Writer" });
const anchor = anchorForQuote(ydoc, "text to review");
if (anchor) {
review.comment(anchor, "Check this text.");
const proposal = review.suggest(anchor, "replacement text", machine);
review.decide(proposal.id, true); // false rejects without changing the text
}
const unsubscribe = review.subscribe(() => {
renderReview(review.threads, review.suggestions);
indexText(bodyText(ydoc));
});
// On host teardown:
unsubscribe();
review.close();
anchorOf(editor, ydoc) pins a selection or returns { problem }. It refuses
selections across blocks and over inline images or hard breaks.
resolveAnchor(ydoc, anchor) returns { text, from, to } or null. A resolved
range can contain changed text. Accept checks the original quote and refuses
stale suggestions against the local document state. If different, overlapping
suggestions are accepted on separate replicas before they sync, both replacements
remain after merge. Both decisions show as accepted; problems() does not report
this conflict. Hosts that need one winner must serialize these decisions.
Quotes must occur once within one text run. Quote search includes text after hard
breaks and inline images, but a quote cannot span an inline node. bodyText joins
paragraphs, headings and code blocks with newlines, including empty blocks. Hard breaks also add newlines. Accepted replacements keep
the marks of the first replaced character.
The controller also has reply, setResolved, and problems. Call close to
remove its Yjs listener. An agent needs comments:accept to decide another
principal's suggestion. An agent author cannot accept or reject its own proposal,
even if the decision uses another kind with the same name. Hosts must bind names
and scopes to authenticated identities. explainReview gives a safe error message;
labelOf preserves machine labels for a host's known machine names.
License
MIT. The editor is a port of gitmarkdown (MIT), and its page layout follows tinylittleoffice (MIT). See THIRD_PARTY_NOTICES.md.
Установить Agentic Documents в Claude Desktop, Claude Code, Cursor
unyly install agentic-documentsСтавит в Claude Desktop, Claude Code, Cursor и VS Code — сам разбирается с npx, uvx и сборкой из исходников.
Впервые? Поставь CLI: curl -fsSL https://unyly.org/install | sh
Или настроить вручную
Выполни в терминале:
claude mcp add agentic-documents --env AGENTIC_DOCS_AS="" --env AGENTIC_DOCS_LIVE_URL="" --env AGENTIC_DOCS_TOKEN="" --env CLOUDFLARE_API_TOKEN="" --env TOKEN="" --env WRANGLER_SEND_METRICS="" -- npx -y agentic-documentsПошаговые гайды: как установить Agentic Documents
FAQ
Agentic Documents MCP бесплатный?
Да, Agentic Documents MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Agentic Documents?
Да, требуются переменные окружения: AGENTIC_DOCS_AS, AGENTIC_DOCS_LIVE_URL, AGENTIC_DOCS_TOKEN, CLOUDFLARE_API_TOKEN, TOKEN, WRANGLER_SEND_METRICS. Unyly подставит их в конфиг при установке.
Agentic Documents — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Agentic Documents в Claude Desktop, Claude Code или Cursor?
Открой Agentic Documents на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Изменения
Версии и запрашиваемые доступы со временем.
- Новая версия опубликована
Похожие MCP
GitHub
PRs, issues, code search, CI status
автор: GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
автор: mcpdotdirectServer Sequential Thinking
MCP server for sequential thinking and problem solving
автор: GitHub ActionsUnyly Deploy
Deploy sites and MCP servers to Unyly straight from your AI agent
автор: UnylyAmap Maps Mcp Server
MCP server for using the AMap Maps API
автор: duxiaohuiPayMCP
Lightweight payments layer for MCP servers: turn tools into paid endpoints with a two-line decorator. [PyPI](https://pypi.org/project/paymcp/) · [npm](https://w
автор: blustAIServer For Power BI
Enables exploring Microsoft Fabric / Power BI workspaces and semantic models, and executing ad-hoc DAX queries.
автор: Mitsubishi-FusoGpt Image 2 Mcp
RunAPI GPT Image 2 MCP server for image generation: create tasks, poll results, and check pricing across 1 model from Claude Code, Codex, Cursor, and VS Code.
автор: runapi-aiCompare Agentic Documents with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
