À la carte primitives & where tools run
anyclaude-sdk is a standalone agent core with a React client adapter — the same shape as Vercel AI SDK. Adopt it whole with query(), or drop in individual pieces: the loop engine, the tools, compaction, permissions, sessions, and — the key one — per-tool control over whether a tool runs on the server or in the browser.
One rule: import the browser-clean subpaths
The bare anyclaude-sdk root re-exports LocalSandbox (node:child_process) and the background worker (comlink), so a browser bundler will choke. In browser code, always import from a subpath. Each subpath below is browser-safe.
Where tools run — server vs. browser
The principle (same as Vercel AI SDK): a tool runs where its executor lives. A tool with a local run executes on the server; a tool with no local run — or one you name in clientTools — is delegated: the loop emits a client_tool_request, pauses, and the host executes it. This is how you run bash + file ops in the user's browser (a real WebContainer) while the agent loop and your LLM key stay on the server.
The one switch
Delegate all built-in file/shell tools to the host in one flag — the server then never runs them against its own (in-memory) filesystem:
// SERVER (e.g. a serverless route)
import { query } from 'anyclaude-sdk/query'
for await (const m of query({
prompt, llm, model, sessionId,
clientWorkspaceTools: true, // ⇐ bash + read/write/edit/list/glob/grep run on the HOST
resume: !!body.continueRun, continueRun: body.continueRun,
clientToolResults: body.clientToolResults, // host-executed results, fed back on resume
})) emit(m)
// (equivalent to: clientTools: WORKSPACE_TOOL_NAMES)The browser side (the adapter)
// BROWSER
import { useAgent, createWorkspaceClientTools } from 'anyclaude-react'
import { WebContainerWorkspace } from 'anyclaude-sdk/workspace'
import { DexieFileSystem } from 'anyclaude-sdk/fs'
// real shell + files in a WebContainer:
useAgent({ endpoint: '/agent', clientTools: createWorkspaceClientTools(new WebContainerWorkspace(wc, '/home/projects')) })
// …or the user's OWN IndexedDB (files, no shell):
useAgent({ endpoint: '/agent', clientTools: createWorkspaceClientTools(new DexieFileSystem('my-db')) })createWorkspaceClientTools(workspace) reuses the SDK's real tool implementations, so client-side behavior matches server-side exactly. It auto-executes each client_tool_request and resubmits results — the analog of Vercel's addToolResult().
Custom client tools — just omit run
import { defineTool } from 'anyclaude-sdk/tools'
// has run → executes SERVER-side
const search = defineTool({ name: 'search', description: '…', parameters: { properties: { q: { type: 'string' } }, required: ['q'] },
run: async ({ q }, ctx) => ({ content: await db.search(String(q)) }) })
// no run → auto-delegated to the host (the browser/connector executes it)
const deploy = defineTool({ name: 'deploy_app', description: '…', parameters: { properties: { provider: { type: 'string' } }, required: ['provider'] } })Why this matters: with clientWorkspaceTools on, file/shell tools cannot silently run against a phantom server filesystem — the server only emits the calls. That structurally eliminates the "agent edits an empty in-memory /work" class of bug. Two requirements: the server query({ cwd }) must match the browser container root, and pass your real system prompt.
Just the loop — runToolLoop
Want only the call → execute → append cycle (no sessions/MCP/survivor/sub-agents)? Use the standalone engine — it yields the same SDKMessages, so anyclaude-react renders it unchanged.
import { runToolLoop } from 'anyclaude-sdk/loop'
for await (const m of runToolLoop({
history, tools, llm, model,
ctx, // { fs, exec, cwd, readFiles: new Set() }
clientTools: ['bash'], // delegate inline (duplex / in-browser)
onClientTool: async (req) => ({ content: await runInBrowser(req) }),
})) render(m)Inline onClientTool suits duplex/in-browser consumers; for one-way serverless, use query()'s pause/resume (clientTools + clientToolResults).
Other drop-ins
import { estimateTokens, compactWithWindow } from 'anyclaude-sdk/compact'
import { rulesToCanUseTool, isDangerousBash } from 'anyclaude-sdk/permissions'
import { parseInlineToolCalls, hasInlineToolCalls } from 'anyclaude-sdk/llm'
import { MessageQueue } from 'anyclaude-sdk/queue'
import { defineSkill } from 'anyclaude-sdk/skills'
// keep recent turns verbatim, summarize the older prefix:
if (estimateTokens(history) > LIMIT) history = await compactWithWindow(history, llm, { keepRecent: 8 })
// gate tools without writing your own logic:
const canUseTool = rulesToCanUseTool({ deny: ['delete_file'], ask: ['bash'] }, { onAsk: confirm })
// recover tool calls a weak model emitted as TEXT instead of native function-calls:
if (hasInlineToolCalls(text)) { const { toolCalls } = parseInlineToolCalls(text) }Mental model vs. Vercel AI SDK
| Vercel AI SDK | anyclaude |
|---|---|
ai (server core) | anyclaude-sdk (query / runToolLoop) |
@ai-sdk/react (useChat) | anyclaude-react (useAgent) |
tool with execute → server | defineTool({ run }) / built-ins |
tool without execute → client | run-less defineTool / clientTools / clientWorkspaceTools |
addToolResult() → resubmit | clientToolResults + createWorkspaceClientTools (automatic) |
Same architecture — plus anyclaude ships the WebContainer / IndexedDB / OPFS executors out of the box. See the React UI kit and Deploy pages for end-to-end wiring.