À 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 SDKanyclaude
ai (server core)anyclaude-sdk (query / runToolLoop)
@ai-sdk/react (useChat)anyclaude-react (useAgent)
tool with execute → serverdefineTool({ run }) / built-ins
tool without execute → clientrun-less defineTool / clientTools / clientWorkspaceTools
addToolResult() → resubmitclientToolResults + 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.