Agentic Realtime
БесплатноПоддерживаетсяLive edits for people and agents on one document, with offline support.
Описание
Live edits for people and agents on one document, with offline support.
README
Live edits for people and agents on one document, with offline support.
Status: 0.6.0 preview. Most features below have an end-to-end test. Hibernation, the no-outbound-calls rule, and EU placement do not.
Install
npm install agentic-realtime
Features
- A sync server on Cloudflare Durable Objects. One object holds one document.
- The standard Yjs protocol (
y-websocket), so a stock Yjs client can connect. - Presence relay: peers see each other's awareness state. The server sets the
kindof each state from the grant, and thenamewhen the grant has one. A client cannot change either. The first socket that announces a client id owns it. Only that socket, or a new socket of the same principal, can update or remove it. - A headless Node client (
agentic-realtime/client) so an agent can join a document. - Browser offline support (
agentic-realtime/browser):openDocumentkeeps an IndexedDB copy, reconnects with backoff, and merges offline edits.onPeersandpeerLabelhelp any UI show peers, with agents marked as machine-made. - Version history:
client.snapshot(name),snapshots(), andrestore(name). A restore is a new edit by you. No stored update or snapshot changes. - A CLI and an MCP server, so an agent can join and edit with no code.
- Storage in the object. Updates append to a log. Every 50 updates (set
SYNC_COMPACT_EVERY, from 1 to 128) the log folds into one snapshot. - WebSocket hibernation. An idle document uses no compute.
- Roles. Owners and editors write. Commenters and viewers only read. The server drops their updates and answers with a permission-denied message.
- Author labels. Each update keeps the kind of principal that wrote it. The API lists the contributors and marks the machine-made ones.
- Who wrote what. A log keeps which principal sent which content, also after
compaction and inside each snapshot.
blameshows it as runs of text, with agents marked as machine-made. - Delete and export. The owner or the host can export the full stored history of a document, and delete all of its data.
- Live revocation. The host can revoke a principal or cap its role while it is connected. A timer asks the host again who each socket is.
- Data residency. Set
SYNC_JURISDICTION=euto pin documents to the EU. - No outbound calls. The server loads no CDN assets and sends no telemetry.
Live demo
A demo Worker runs on workers.dev at
https://agentic-sync-demo.hello-fd1.workers.dev.
GET /health answers 200. Every document and admin route needs a bearer token, so the
demo shows the sign-in rule and holds no public document. It runs
examples/host with documents pinned to the EU.
To run your own copy, or to redeploy this one, you need a Cloudflare account and a token that may edit Workers. Run in the repository root:
export CLOUDFLARE_API_TOKEN=... # keep it out of git and out of CI
npm ci
npm run deploy:demo
Then set the two secrets. HOST_TOKENS is described in the example host.
Until you set both, every route except /health answers 500.
npx wrangler secret put HOST_TOKENS --config examples/host/wrangler.demo.jsonc
npx wrangler secret put SYNC_ADMIN_SECRET --config examples/host/wrangler.demo.jsonc
The deploy is a manual step. No CI job holds a secret. The config turns wrangler telemetry off.
Use the Worker
You own sign-in. Pass an authorize hook. It returns a grant, or null to
refuse.
import { createSyncWorker } from "agentic-realtime/worker";
export { SyncRoom } from "agentic-realtime/worker";
export default createSyncWorker({
authorize: async (request, docId) => {
const user = await mySession(request); // your code
if (!user) return null;
return {
principal: { id: user.id, kind: "human" },
role: await myRoleFor(user, docId), // owner, editor, commenter, viewer
name: user.displayName, // optional. Shown in presence in place of the client's name. Use the safe set for names.
};
},
});
authorize also gets the Worker env as its third argument, for the secrets of your
sign-in. Copy wrangler.jsonc for the Durable Object binding and the migration.
The default Worker refuses every document. To see a working host, run
examples/host. It signs callers in with bearer tokens.
| Route | Use |
|---|---|
GET /api/v1/docs/:id/ws |
Upgrade to a WebSocket. Speaks the Yjs protocol. |
GET /api/v1/docs/:id |
Read the role, jurisdiction, update count, compaction count, and contributors. |
GET/POST /api/v1/docs/:id/snapshots |
List snapshots, or save one (editors only). |
GET /api/v1/docs/:id/snapshots/:name |
Read a snapshot as a Yjs update. |
GET /api/v1/docs/:id/attribution |
Read who sent which content. Any role may read it. |
GET /api/v1/docs/:id/snapshots/:name/attribution |
The same log, as of that snapshot. |
GET /api/v1/docs/:id/export |
Stream everything stored for the document as JSON lines. Owners only. |
DELETE /api/v1/docs/:id |
Delete all data of the document. Owners only. The body is {"confirm": "<id>"}. |
GET /api/v1/admin/docs/:id/export, DELETE /api/v1/admin/docs/:id |
The same two calls for the host, with the host secret. |
GET /health |
Check that the Worker runs. |
A document id matches [A-Za-z0-9_-]{1,128}. Ids are case sensitive.
An agent kind is one of agent_of_human, agent_of_agent, agent, or
user_token. Only human is not machine-made.
Revoke access while people are connected
A grant is checked when a socket opens. Two paths change it after that.
Host calls. Set SYNC_ADMIN_SECRET (at least 32 bytes) as a Worker secret.
The host then calls the admin routes with Authorization: Bearer <secret>. Keep
the secret in your backend. Never give it to an agent.
import { revokePrincipal, setPrincipalRole } from "agentic-realtime/sdk";
const host = { server: "https://sync.example.com", secret: env.SYNC_ADMIN_SECRET };
await setPrincipalRole(host, "notes", "person-1", "viewer"); // read-only at once
await revokePrincipal(host, "notes", "person-1"); // closes every socket, code 4003
await setPrincipalRole(host, "notes", "person-1", null); // lifts the cap or the revoke
A cap only narrows. The principal gets the narrower of the grant and the cap.
A revoked principal gets 403 ACCESS_REVOKED on every route, also for
snapshots, until the host lifts the revoke. The cap lives in the document's
storage. A cap is keyed by principal id, so keep ids unique across kinds. A
principal id matches the agentic-identity rule: 1 to 128 ASCII characters,
first an ASCII letter, digit, _ or -, then ASCII letters, digits, ., _
or -. One document holds up to 1000 role caps and up to 10000 bans, counted
apart, so a full table of role caps never stops a revoke. Past the ban limit, a
revoke still closes the sockets, but it answers
409 CAP_LIMIT because no ban is stored. A cap of owner narrows nothing,
so the server drops it. That also lifts a revoke, the same as null. Without a valid secret,
the admin routes answer 503 ADMIN_DISABLED. A Worker of your own can call the same
methods through the Realtime port, or by Durable Object RPC on SyncRoom.
Timer. Export a room class that knows how to ask your host:
import { createSyncRoom } from "agentic-realtime/worker";
export const SyncRoom = createSyncRoom({
// The principal's current role, or null for no access.
reauthorize: async (principal, docId) => myRoleFor(principal.id, docId),
});
Every 60 seconds (set SYNC_REAUTH_INTERVAL_SECONDS, 1 to 3600) the room asks
once for each connected principal, up to 20 calls at a time, each with a 5
second limit. The new role can only narrow the role granted at connect, so the
timer never widens access. It applies to all sockets of that principal. A null,
a throw, a timeout, or an unknown role closes them with code 4003. A host outage
therefore ends the sessions. It does not keep them open. A host that answers
slower than 5 seconds has the same effect, and every client then reconnects at
once, so size your host for that.
Join a document from Node
import { SyncClient } from "agentic-realtime/client";
const client = await SyncClient.connect({
url: "wss://sync.example.com/api/v1/docs/notes/ws",
headers: { authorization: `Bearer ${token}` },
presence: { name: "Summary bot" },
});
await client.edit((doc) => doc.getText("body").insert(0, "Hello"));
console.log(client.peers); // the other people and agents
await client.disconnect();
connect resolves when the document matches the server. edit and flush
resolve when the server has handled your changes. If your role is read-only,
the server drops the edit and client.denied holds its reason.
Your presence is { name, color, cursor }. The default color comes from your
name and has at least 3:1 contrast on a white page and on a dark page (#121212).
peerColor(name, "light" | "dark") gives a color with 4.5:1 text contrast on one
page. No single color has 4.5:1 on both. client.peers keeps a peer's color only
if it is #rrggbb with 3:1 on both pages. Otherwise it uses the peer's default. You do not set a kind. The server
adds the kind of your grant to your presence, so people always see an agent
as an agent. Each peer in client.peers carries that kind, and machine is
true for every kind except a person. A name has 1 to 40 characters. It starts
with a letter or a digit. It has only letters, digits, marks, single spaces, and
’ . _ ' ( ) -. It has no leading, trailing, or double space, and no run of more
than 3 combining marks. It has no bidi controls, zero-width characters, or other
invisible characters. The client throws for any other name (it first converts
a name to NFC). The server drops an unsafe name from the presence it relays,
and peers show Unknown. The socket stays open. The set does not stop
look-alike letters from other scripts. Show a name in an isolated run
(<bdi> or dir="auto"), because right-to-left letters are allowed. Check a name with isSafePeerName(name.normalize("NFC")). Use any Yjs document type on client.doc.
Pass away: { idleAfterSeconds: 120 } with presence to tell peers if you are
active, idle, or hidden. Call client.markActive() on input and
client.setHidden(true | false) when the page hides or shows. In a browser,
watchPageAway(client) does both, and openDocument calls it when you pass
away. A hidden page reads as hidden, not idle. A peer that reports no state,
such as an agent, has away: "unknown" in client.peers and in the SDK's
listPeers. The server drops a state that is not one of the three.
The client needs Node 22 or later. It reconnects only if you pass reconnect: {}.
Use the CLI
export AGENTIC_SYNC_URL=https://sync.example.com
export AGENTIC_SYNC_TOKEN=... # sent as a bearer token
agentic-realtime doc apply --doc notes --root body --text "Hello"
agentic-realtime doc get --doc notes
agentic-realtime doc export --doc notes # owners only; JSON array
agentic-realtime doc delete --doc notes --confirm notes # permanent
# Host operator environment: set AGENTIC_SYNC_ADMIN_SECRET.
agentic-realtime admin export --doc notes
agentic-realtime admin role --doc notes --principal person-1 --role viewer
agentic-realtime admin revoke --doc notes --principal person-1
agentic-realtime admin delete --doc notes --confirm notes
agentic-realtime doc snapshot --doc notes --name before-review
agentic-realtime doc restore --doc notes --name before-review
agentic-realtime doc presence --doc notes
agentic-realtime doc blame --doc notes --root body
Commands print JSON. Most commands join the document, do one job, and leave.
Export, delete, and host commands use HTTP without joining. doc snapshot
without --name lists the snapshots. A refused edit exits with 1 and prints
the reason. --name sets the name you show in presence, except for snapshot
and restore, where it is the snapshot name. The server takes
your role, your kind, and your author label from the token. The CLI has no
--kind flag. doc apply edits text roots and empty roots only. It refuses a
rich-text, map, or array root and exits with 1, so it never deletes a document.
Use the MCP server
agentic-realtime mcp serves these tools over stdio: doc_get, doc_export,
doc_delete, doc_apply, doc_snapshot, doc_snapshots, doc_restore,
doc_blame, and doc_presence. Set
AGENTIC_SYNC_URL, AGENTIC_SYNC_TOKEN, and optionally AGENTIC_SYNC_NAME in
the server environment. A tool call never carries a credential. doc_apply
returns an error for a root that is not text. The same operations are in agentic-realtime/sdk.
Export and delete a document
An owner, or the host with the host secret, can take a copy of a document and delete it.
import { deleteDocument, exportDocument } from "agentic-realtime/sdk";
const records = await exportDocument({ server, token: ownerToken }, "notes");
await deleteDocument({ server, token: ownerToken }, "notes"); // cannot be undone
exportDocumentAsHost and deleteDocumentAsHost take { server, secret } instead.
exportDocument returns every record at once, so it holds the whole export in memory.
For a large history, use exportDocumentStream or exportDocumentStreamAsHost. They
yield one record at a time, as the response arrives. The request starts when you ask for
the first record. A refusal is thrown then, not when you call the function:
import { exportDocumentStream } from "agentic-realtime/sdk";
for await (const record of exportDocumentStream({ server, token: ownerToken }, "notes")) {
// handle one record, then go on to the next
}
An export is JSON lines, one record each, sent as the room reads its storage:
type |
Holds |
|---|---|
meta |
format (agentic-realtime-export), version, exportedAt, compactions, and the base. |
state |
base64: the whole document as one Yjs update. |
roots |
Each root type as plain JSON. |
authors |
Everyone who wrote, with a machine label. |
caps |
The host's caps and bans: principalId and cap, a role or revoked. It can name principals who never wrote, and owners can read it. |
attribution |
The log of who sent which content. |
update |
One update that compaction has not folded yet: seq, at, author, base64. |
snapshot |
A named snapshot: name, at, bytes, author, base64, and its attribution. base64 and attribution are null when the stored state or log is missing. |
A delete closes every socket with code 4004. It removes the updates, the base, the
snapshots, the attribution logs, the caps and bans, and the timer. The document id
then starts empty, and a new writer starts a new history. A delete needs confirm
with the document id in its body, so a stray call does nothing.
The CLI doc export --doc <id> and MCP doc_export use the owner token from
AGENTIC_SYNC_TOKEN. Both return a JSON array and hold the full export in memory.
Other roles get OWNER_ONLY. The MCP tool has a read-only hint.
SyncClient has exportDocument() and deleteDocument() for owners. They use
the document URL and credentials from connect or open. Call flush() before
export to include local edits. Export does not join or write to the document.
The host backend can call revokePrincipal(secret, principalId) and
setPrincipalRole(secret, principalId, role). Pass null to lift a cap or ban.
The secret is passed per call and is not stored on the client.
Owners can run doc delete --doc notes --confirm notes or call MCP doc_delete
with { "doc": "notes", "confirm": "notes" }. A mismatch fails before the request.
The Worker checks the effective owner role for both export and delete.
Host operators set AGENTIC_SYNC_ADMIN_SECRET in the CLI environment. Then use:
agentic-realtime admin export --doc notes
agentic-realtime admin role --doc notes --principal person-1 --role viewer
agentic-realtime admin revoke --doc notes --principal person-1
agentic-realtime admin role --doc notes --principal person-1 --role null
agentic-realtime admin delete --doc notes --confirm notes
Each command prints JSON. Each subcommand accepts --help. Secrets have no CLI flag.
For MCP, the operator must also set AGENTIC_SYNC_ENABLE_ADMIN=1. This enables
admin_export, admin_delete, admin_revoke, and admin_role. Tool arguments use
doc, principalId, role (a known role or null), and confirm for delete.
The schemas check principal ids and roles. The Worker checks host authority.
An invalid or missing host secret fails closed. Normal MCP sessions omit these tools.
Run this mode only in a trusted host operator session. An agent must not hold the
host secret. Tool calls never carry credentials.
The SDK has typed bound clients. createDocumentClient({ server, token }) has
exportDocument, exportDocumentStream, and deleteDocument.
createAdminClient({ server, secret }) has the same methods plus
revokePrincipal(docId, principalId) and setPrincipalRole(docId, principalId, role).
These clients call the existing route functions. They add no authorization layer.
See who wrote what
Yjs names each piece of content by a client id and a clock. The room keeps a log of ranges: which principal (id and kind) sent each run of those names. It folds contiguous writes of one client into one range. It saves the log with every compaction, and each named snapshot keeps a copy.
import { blame } from "agentic-realtime/sdk";
const spans = await blame({ server, token }, "notes", "body");
// [{ text: "Hi ", author: { id: "p1", kind: "human", machine: false } },
// { text: "bot", author: { id: "a1", kind: "agent_of_human", machine: true } }]
await blame({ server, token }, "notes", "body", "before-review"); // as of a snapshot
author is null for text that no range names. getAttribution returns the raw
log: authors, ranges, dropped, and lost. The machine label comes from the kind, never from storage.
Work offline in the browser
import { openDocument, peerLabel } from "agentic-realtime/browser";
const { client } = await openDocument({
url: "wss://sync.example.com/api/v1/docs/notes/ws?ticket=...",
storageName: "notes",
presence: { name: "Ana" },
});
client.onPeers((peers) => render(peers.map(peerLabel)));
openDocument loads the IndexedDB copy first. It then connects and reconnects
after a drop, with delays that double from 500 ms up to 30 s. onPeers works in
any UI framework. peerLabel names an agent as machine-made.
Protocol
The WebSocket carries the y-websocket messages: sync (type 0), awareness (1),
auth (2), and query awareness (3). A read-only role gets an auth
permission-denied reply, and the server drops its update. The server answers in
order, so its reply to a new sync request proves it has all earlier updates.
flush relies on that. A restore is a normal update.
Swap the server
The Worker talks to the server through the Realtime interface. It has eleven
methods: connect, meta, createSnapshot, listSnapshots, getSnapshot,
revoke, updateRole, roleFor, erase, exportHistory, and attribution.
The Cloudflare adapter implements it with Durable Objects. Another server, such as
a Node Yjs server, could implement it. See the changelog for what 0.2.0 added. The wire
protocol helpers in agentic-realtime (readMessage, AwarenessRelay) are plain
TypeScript with no Cloudflare code.
Run on Firebase
RoomStorage is a small key/value port: get, list by prefix, put,
putAll, delete, and deleteAll. It matches the way the room uses Durable
Object storage. The package ships an in-memory adapter and
agentic-realtime/firestore. The Firestore adapter stores values of the same
shape as the room writes: updates, snapshots, authorship logs, and role caps
and bans. The room does not use the port yet, so nothing runs on Firebase
today.
import { createFirestoreStorage } from "agentic-realtime/firestore";
import { exportStorage } from "agentic-realtime";
const storage = createFirestoreStorage({
projectId: "my-project",
docId: "doc-1",
fetch: globalThis.fetch,
token: () => getAccessToken(), // your service account flow
});
await storage.put("cap:principal-1", "revoked");
const lines = exportStorage(storage); // a stream of JSON lines
await storage.deleteAll(); // erase the document
The adapter uses the Firestore REST API with fetch only. It has no Firebase
SDK and no secret. You pass the fetch and a token, or a function that returns
one. Leave the token out and set baseUrl to http://<FIRESTORE_EMULATOR_HOST>
for the emulator. An entry lives at agentic_realtime/<doc>/kv/<key>, where
<doc> and <key> are base64url text. So the adapter keeps the case of the
document id and of every key. It refuses an empty key and a key that is not
well-formed text.
How it is tested: one contract suite, runStorageContract, runs against the
in-memory adapter and the Firestore adapter. By default, the Firestore run uses
a fake of the REST calls in src/firestore/fake.ts. It accepts only the real
URL shapes. It follows Firestore for byte order, query cursors, atomic commits,
and the string-value and id size limits. It checks string values only, not field
names or the document name, and it does not model the 10 MiB request cap.
With FIRESTORE_EMULATOR_HOST set, the same suite runs against the real
Firestore emulator. The emulator run passed on the Firestore emulator 1.19.8.
Start it with firebase emulators:start --only firestore, or run the emulator
jar with Java. CI uses the fake, because it has no Java.
This port moves the storage only. The room still reads and writes Durable Object storage directly, and the WebSocket server still runs in a Durable Object. A Node server on Firebase must bring its own socket layer.
Limits of the adapter:
- A key is up to 1000 bytes of well-formed text. It is never empty.
- One value stays under 1 MB as JSON text. Bytes grow by a third as base64, so binary data stays under about 750 KB. A larger snapshot or authorship log, which the room allows, does not fit.
- One
putAlltakes up to 500 entries. This is the adapter's own limit. Firestore caps a commit at 10 MiB. - Update keys count up, and Firestore indexes
key. Firestore warns that ids or indexed values that rise in sequence can limit the write rate of a collection. A single-field index exemption forvaluesaves index cost.
Exports
Each entry point lists its exports below. A name marked (type) exists only at compile time.
agentic-realtime
| Name | What it is |
|---|---|
VERSION |
The package version. |
PRINCIPAL_KINDS, PrincipalKind (type) |
The principal kinds: human, agent_of_human, agent_of_agent, agent, user_token. |
ROLES, Role (type) |
The roles, from most to least rights: owner, editor, commenter, viewer. |
Principal (type), Grant (type), Authorize (type), Contributor (type), DocMeta (type), SnapshotInfo (type), Realtime (type) |
Types. Grant is what authorize returns. Realtime is the port that a server implements. |
DOC_ID_PATTERN, SNAPSHOT_NAME_PATTERN, MAX_SNAPSHOTS |
The rules for a document id, a snapshot name, and the 50-snapshot limit. |
canWrite(role) |
True for owner and editor. |
isMachineKind(kind) |
True for every kind except human. |
assertGrant(value) |
Checks a value from authorize and returns it as a Grant. Throws when it is malformed. |
MESSAGE_SYNC, MESSAGE_AWARENESS, MESSAGE_AUTH, MESSAGE_QUERY_AWARENESS |
The message types 0 to 3 of the wire protocol. |
syncStep1Message, updateMessage, awarenessMessage, permissionDeniedMessage |
Build one wire message as bytes. |
readMessage, Incoming (type) |
Read one client message against a Y.Doc. It returns the reply, a denial, an awareness update, or a query. It applies a writer's update to the doc, unless refuse names a reason. It throws on a malformed message. |
AwarenessRelay, AwarenessEntry (type) |
Keeps the presence state of a document and relays changes. It enforces the presence limits. |
encodeAwarenessUpdate, decodeAwarenessUpdate |
Convert presence entries to and from the wire format. |
AWAY_STATES, AwayState (type), isAwayState, readAwayState |
The states active, idle and hidden. readAwayState returns unknown for any other value. |
AwayTracker, AwayOptions (type) |
Tracks activity and sets idle after idleAfterSeconds. |
LIGHT_BACKGROUND, DARK_BACKGROUND, TEXT_CONTRAST, GRAPHIC_CONTRAST |
The two page colors, #ffffff and #121212, and the contrast minimums: 4.5 for text and 3 for graphics. |
contrastRatio(a, b), isHexColor(value), peerColor(key, mode?), safePeerColor(color, key), ColorMode (type) |
Color helpers. mode is light, dark or both. safePeerColor keeps a #rrggbb color that passes on both pages. Otherwise it uses peerColor. |
MAX_NAME_LENGTH, FALLBACK_NAME, isSafePeerName(name), assertSafePeerName(name), displayPeerName(name) |
The rules for a presence name. displayPeerName returns Unknown for an unsafe name. assertSafePeerName throws for one. |
restoreState(doc, state, origin?) |
Restores a state into a live document as a new edit. |
AttributionLog, AttributionRange (type), Attribution (type), BlameSpan (type), blameText, DEFAULT_MAX_RANGES |
The authorship log and its types. blameText splits a text root into runs with their author, as blame does. DEFAULT_MAX_RANGES is 4000. |
createMemoryStorage(), exportStorage(storage), RoomStorage (type) |
An in-memory RoomStorage for tests. exportStorage streams the storage as JSON lines. RoomStorage is the key/value port. |
agentic-realtime/sdk
Host and agent calls over HTTP. See the sections above for the flows.
| Name | What it is |
|---|---|
createDocumentClient, createAdminClient |
Typed bound owner and host clients. |
Target (type), AdminTarget (type) |
Types. Target is { server, token?, name?, headers? }. Export and delete forward headers; token takes precedence. AdminTarget is { server, secret }. |
EDIT_MODES, EditMode (type) |
The text edit modes: append, replace, insert. |
withDoc(target, docId, job) |
Joins a document, runs job with the connected client, and always leaves. getDoc, applyText, the snapshot calls, blame without a snapshot and listPeers use it. |
getDoc(target, docId) |
Reads the roots of a document as plain JSON. |
applyText(target, docId, edit) |
Writes text into a text root. edit has root, text, and optional mode and at. It refuses a root that is not text. |
createSnapshot, listSnapshots, restoreSnapshot |
Save a named snapshot, list them, and restore one. |
getAttribution(target, docId, snapshot?) |
Returns the authorship log, now or as of a snapshot. |
blame(target, docId, root, snapshot?) |
Returns the runs of a text root with their author. |
listPeers(target, docId, waitMs?) |
Joins the document, waits for the others to announce, and returns the peers. |
exportDocumentStream, exportDocument, exportDocumentStreamAsHost, exportDocumentAsHost, ExportRecord (type) |
Export the stored history. ExportRecord is one JSON line. The Stream forms yield one record at a time. |
deleteDocument, deleteDocumentAsHost |
Delete all data of a document. This cannot be undone. |
revokePrincipal(target, docId, principalId), setPrincipalRole(target, docId, principalId, role) |
Host calls that close, or cap, the access of a principal. |
agentic-realtime/client
| Name | What it is |
|---|---|
SyncClient |
The headless client. SyncClient.connect(options) resolves when the document matches the server. SyncClient.open(options) returns at once. With reconnect, it keeps trying while offline. |
ConnectOptions (type), Presence (type), Peer (type), Status (type) |
Types. Status is connecting, connected or disconnected. |
agentic-realtime/browser
| Name | What it is |
|---|---|
openDocument(options) |
Opens a document with an IndexedDB copy, connects, and reconnects. |
peerLabel(peer) |
A label for a peer. It marks every machine-made peer, which is every kind except human. |
watchPageAway(client, page?) |
Feeds page visibility and input into the away tracker. It returns a function that stops the watch. |
OpenedDocument (type), PageLike (type) |
Types. OpenedDocument holds the client, the persistence, and stopWatching. |
agentic-realtime/worker
| Name | What it is |
|---|---|
createSyncWorker(options) |
Makes the Worker. Pass authorize. The default export refuses every document. |
SyncEnv (type), SyncWorkerOptions (type) |
Types for the Worker bindings and options. |
SyncRoom, createSyncRoom({ reauthorize }) |
The Durable Object class. createSyncRoom adds the re-authorization timer. |
createDurableObjectRealtime(namespace, jurisdiction?) |
The Realtime port on Durable Objects. |
Reauthorize (type), Jurisdiction (type) |
Types. Jurisdiction is eu or fedramp. |
ROLES, assertGrant, Authorize (type), Grant (type), Principal (type), Role (type) |
The same names as the main entry, so a Worker can import them from one place. |
agentic-realtime/firestore
| Name | What it is |
|---|---|
createFirestoreStorage(options) |
A RoomStorage on Firestore over REST. See Run on Firebase. |
FirestoreStorageOptions (type), FirestoreError |
The options type, and the error that the adapter throws. |
MAX_VALUE_BYTES |
The limit of one stored value as JSON text, 1,000,000 bytes. Bytes go in as base64, so they take about a third more. |
prefixEnd(prefix) |
The first string after every string with that prefix, or null when there is none. |
Limits
- One snapshot or update must stay under 2 MB (Durable Object storage limit). The server closes a connection that sends a message over 1 MiB. It refuses an update that would take the document past 1.5 MiB, and answers with a permission-denied message.
- Presence limits: one socket owns up to 8 client ids, all sockets of one principal together own up to 16, and one document holds up to 256 presence entries. One state is up to 8192 characters. A socket that breaks a limit, sends a state that is not a JSON object, or uses an id that belongs to another principal gets closed with code 1008.
- A full document refuses new presence entries. The socket stays open and
document sync keeps working. The server sends a permission-denied message
with the reason
presence is full. Entries that already exist keep updating. - A new socket of the same principal can take over a client id. The server closes the old socket with code 4001. This lets a client reconnect before the server notices that the old connection is dead.
- A jurisdiction is part of the object id. Set it before the first write. Changing it later points at an empty document.
- A snapshot is a full copy of the state. A document keeps at most 50.
- Restore replaces each root type (text, map, array, XML) and keeps text formatting. The CLI and MCP tools edit text roots. Use the client for others.
- The IndexedDB tests run on
fake-indexeddbin Node, not in a real browser. - Presence lives in memory. After a hibernation the server forgets it until the clients send their next heartbeat, within about 15 seconds.
- The attribution log keeps up to 4000 ranges (set
SYNC_ATTRIBUTION_MAX_RANGES, 4 to 5000), and also stays under about 1.5 MB with the ids of its authors. Each new client id starts a range, so a client that reconnects often uses them fast. Past the limit the oldest ranges go,droppedcounts them, and their text shows as unattributed. The log is not a signed record. - A restore matches text by item in the Yjs history, never by letter. A piece that the live text and the snapshot both hold keeps its author, and a restore that only removes formatting changes no author. Text that a restore brings back is new text by the person who restored it. An undo that brings back deleted text does the same.
- Yjs holds an update that needs missing content, and applies it inside the next
update. The room refuses such an update and closes the socket with code 1008.
It also refuses an update that writes under the client id of another principal.
This check forgets a client id once the size limit drops its ranges. A client
that keeps its
Y.Docand signs in as another principal writes under the old client id, so the room refuses it. Make a newY.Docwhen the principal changes. lost: truein the log means the saved log was damaged. The document still loads. Text from before the damage has no author.- The log names who sent an insert. It does not name who deleted. It covers text roots. Content that a document held before this version has no range until someone rewrites it. A snapshot made before this version has no log, and its attribution answers 404.
- The author of a range is the principal that sent the update. A principal can send a range of ids that nobody has used yet. The first range recorded for an id wins, so a later sender cannot take over text that is already there.
- A delete removes the room's storage. It cannot remove copies that other
parties hold: an export, a snapshot a client downloaded, the offline copy in a
browser, your backups, and your host's logs. A client with an offline copy
writes it back when it reconnects, so make
authorizerefuse a deleted document, or revoke its principals. Record a delete in your own audit log. - An export is not a point in time. A write during the stream may or may not be in it. Compaction has folded older updates into the state, so an export lists only the updates since the last compaction. The snapshots and the attribution log keep who wrote what. An export holds the content in clear text, so treat the file as personal data.
- Revocation by the host call is immediate. Revocation by the timer takes up to
one interval. Without
createSyncRoom, there is no timer. A room with open sockets from before you deployedcreateSyncRoomstarts its timer at the next connection. - A client with
reconnectkeeps retrying after code 4003 and gets 403 each time, with growing delays. Stop it in your UI when you revoke. - The local
wrangler devcannot place objects in a jurisdiction. Test placement on a deployed Worker. - If you sign in with cookies, check the
Originheader inauthorize.
Develop
Run these on a machine you trust. The end-to-end test starts wrangler dev.
npm ci
npm run ci:local
License
MIT.
Установить Agentic Realtime в Claude Desktop, Claude Code, Cursor
unyly install agentic-realtimeСтавит в Claude Desktop, Claude Code, Cursor и VS Code — сам разбирается с npx, uvx и сборкой из исходников.
Впервые? Поставь CLI: curl -fsSL https://unyly.org/install | sh
Или настроить вручную
Выполни в терминале:
claude mcp add agentic-realtime --env AGENTIC_SYNC_TOKEN="" --env AGENTIC_SYNC_URL="" --env CLOUDFLARE_API_TOKEN="" -- npx -y agentic-realtimeПошаговые гайды: как установить Agentic Realtime
FAQ
Agentic Realtime MCP бесплатный?
Да, Agentic Realtime MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Agentic Realtime?
Да, требуются переменные окружения: AGENTIC_SYNC_TOKEN, AGENTIC_SYNC_URL, CLOUDFLARE_API_TOKEN. Unyly подставит их в конфиг при установке.
Agentic Realtime — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Agentic Realtime в Claude Desktop, Claude Code или Cursor?
Открой Agentic Realtime на 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 Realtime with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
