Meridian ships built-in tool use helpers for AI agents. Stateless agents (serverless functions, Lambda, Cloudflare Workers) can read and write CRDTs via pure HTTP — no persistent WebSocket required.
getMeridianTools() returns Anthropic-compatible tool definitions by default. Use toOpenAITools() or toGeminiTools() to convert to the format expected by OpenAI or Gemini.
Install
# Anthropic
npm install meridian-sdk @anthropic-ai/sdk
# OpenAI
npm install meridian-sdk openai
# Gemini
npm install meridian-sdk @google/generative-ai
Quick start
import Anthropic from "@anthropic-ai/sdk";
import { getMeridianTools, executeMeridianTool } from "meridian-sdk";
import type { ToolUseBlock } from "meridian-sdk";
const config = {
baseUrl: "https://your-meridian-server.com",
token: process.env.MERIDIAN_TOKEN!,
namespace: "my-app",
};
const anthropic = new Anthropic();
// 1. Generate Anthropic-compatible tool definitions from your CRDT IDs
const tools = getMeridianTools(config, ["views", "notes", "tags"]);
// 2. Run the agentic loop
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "Increment the views counter and update the notes." },
];
while (true) {
const response = await anthropic.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
tools,
messages,
});
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
// 3. Execute each tool call against Meridian via HTTP
const result = await executeMeridianTool(config, block as ToolUseBlock);
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result });
}
}
messages.push({ role: "assistant", content: response.content });
if (toolResults.length === 0) break; // Claude is done
messages.push({ role: "user", content: toolResults });
}
OpenAI
import OpenAI from "openai";
import { getMeridianTools, toOpenAITools, executeOpenAITool } from "meridian-sdk";
const openai = new OpenAI();
const tools = toOpenAITools(getMeridianTools(config, ["views", "notes", "tags"]));
const response = await openai.chat.completions.create({
model: "gpt-4o",
tools,
messages: [{ role: "user", content: "Increment views and update notes." }],
});
const msg = response.choices[0].message;
const toolResults = [];
for (const call of msg.tool_calls ?? []) {
// Pass call directly — no wrapping needed
const result = await executeOpenAITool(config, call);
toolResults.push({ role: "tool", tool_call_id: call.id, content: result });
}
Gemini
import { GoogleGenerativeAI } from "@google/generative-ai";
import { getMeridianTools, toGeminiTools, executeGeminiTool } from "meridian-sdk";
const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genai.getGenerativeModel({
model: "gemini-1.5-pro",
tools: [toGeminiTools(getMeridianTools(config, ["views", "notes", "tags"]))],
});
const result = await model.generateContent("Increment views and update notes.");
const parts = result.response.candidates?.[0].content.parts ?? [];
for (const part of parts) {
if (part.functionCall) {
// Pass part.functionCall directly — no id hack, no wrapping
const toolResult = await executeGeminiTool(config, part.functionCall);
// pass toolResult back as functionResponse part
}
}
function getMeridianTools(
config: MeridianAgentConfig,
crdtIds: string[],
): Tool[]
Returns an Anthropic Tool[] array. Each CRDT ID exposes four tools:
| Tool | CRDT type | Description |
|---|
meridian_read_{id} | any | Read current JSON state |
meridian_increment_{id} | GCounter / PNCounter | Increment by amount |
meridian_set_{id} | LWW Register | Set value (string, number, object) |
meridian_add_{id} | ORSet | Add an element |
Special characters in CRDT IDs (:, /, -) are replaced with _ in tool names.
function executeMeridianTool(
config: MeridianAgentConfig,
toolUse: ToolUseBlock,
): Promise<string>
Executes a tool call returned by Claude and returns a JSON string suitable for the content field of a tool_result block.
Dispatches to the correct HTTP endpoint based on the tool name:
read → GET /v1/namespaces/:ns/crdts/:id
increment → POST /v1/namespaces/:ns/crdts/:id/ops (GCounter or PNCounter op)
set → POST /v1/namespaces/:ns/crdts/:id/ops (LwwRegister op)
add → POST /v1/namespaces/:ns/crdts/:id/ops (ORSet Add op)
function toOpenAITools(tools: Tool[]): OpenAITool[]
Converts Meridian tools to OpenAI Chat Completions format ({ type: "function", function: { name, description, parameters } }).
function toGeminiTools(tools: Tool[]): GeminiTool
Converts Meridian tools to Google Gemini format ({ functionDeclarations: [...] }). Returns a single GeminiTool object to pass directly in the tools array of model.generateContent().
function executeOpenAITool(
config: MeridianAgentConfig,
toolCall: OpenAIToolCall,
): Promise<string>
Executes a tool call from OpenAI. OpenAIToolCall structurally matches ChatCompletionMessageToolCall from the openai package — pass tool_calls[n] directly. Parses function.arguments (JSON string) internally.
function executeGeminiTool(
config: MeridianAgentConfig,
functionCall: GeminiFunctionCall,
): Promise<string>
Executes a tool call from Gemini. GeminiFunctionCall structurally matches FunctionCall from @google/generative-ai — pass part.functionCall directly.
MeridianAgentConfig
interface MeridianAgentConfig {
/** HTTP base URL of the Meridian server */
baseUrl: string;
/** Bearer token with read+write permissions for the namespace */
token: string;
/** Namespace scoping all CRDT operations */
namespace: string;
}
Persistent agent memory
Because Meridian CRDTs are durable, an agent can use them as long-term memory across runs — reading its previous state at startup and writing an updated summary before exiting.
// On startup — read previous memory
const lastRun = await fetch(`${baseUrl}/v1/namespaces/${ns}/crdts/memory-last-run`, {
headers: { Authorization: `Bearer ${token}` },
}).then(r => r.ok ? r.json() : null);
// Pass previous memory as context to Claude
const prompt = lastRun
? `Your last run summary: ${JSON.stringify(lastRun.value)}\n\nContinue from where you left off.`
: `This is your first run.`;
// ... agentic loop ...
// On exit — write updated memory
await executeMeridianTool(config, {
type: "tool_use", id: "1", name: "meridian_set_memory_last_run",
input: { value: JSON.stringify({ timestamp: new Date(), summary: "..." }), client_id: 1 },
});
A full working example is in examples/ai-agents/agents/memory.ts. It uses three CRDTs (namespace: ai-agents):
| CRDT | Type | Purpose |
|---|
memory-run-count | GCounter | Total runs ever |
memory-last-run | LWW Register | Summary of the last run |
memory-insights | ORSet | Observations accumulated across all runs |
CRDT IDs must use hyphens only — colons are not supported. getMeridianTools converts special characters to _ in tool names and executeMeridianTool converts them back to - when calling the API.
Multi-agent debate
Two agents can use CRDTs as a shared communication channel — each writing its argument to an LWW-Register and reading the opponent’s latest argument before composing a rebuttal.
A full working example is in examples/ai-agents/agents/debate.ts. It runs two Claude agents (FOR / AGAINST) in interleaved rounds, with each agent’s argument history accumulated in an ORSet. A React UI displays both sides live.
Live UI
The AI agents example ships a React dashboard with four tabs — Agents (multi-worker coordinator), Tool Use (single Claude agent), Memory (persistent memory across runs), and Debate (two-agent debate) — all subscribing to the same CRDTs via WebSocket and displaying live state as agents write.
SSE streaming
For agents that need to react to changes without polling, use the SSE endpoint:
GET /v1/namespaces/:ns/crdts/:id/events
Authorization: Bearer <token>
Accept: text/event-stream
Each event is a base64-encoded msgpack delta. The SDK HttpClient.syncCrdt() handles decoding automatically. Keep-alive pings are sent every 15 seconds to prevent proxy timeouts.
const es = new EventSource(
`${baseUrl}/v1/namespaces/${ns}/crdts/${id}/events`,
{ headers: { Authorization: `Bearer ${token}` } }
);
es.onmessage = (e) => {
const delta = decode(base64ToBytes(e.data)); // msgpack delta
// apply delta to local CRDT state
};
Token requirements
The token must have read and write permissions for all CRDT IDs the agent will access. Issue one via the admin API:
curl -X POST https://your-server.com/v1/tokens \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"namespace": "my-app",
"client_id": 42,
"permissions": { "read": ["*"], "write": ["*"], "admin": false }
}'
See Tokens for details.