Skip to content
Cavemandocs
Apache-2.0

Migrate

From another SDK in three depths: one line at fetch, an adapter, or a port.

You can attach Caveman to an agent running on another SDK at three depths: one argument where you construct the provider client, one object in your framework's config, or a port of the agent definition. They are independent, each is useful alone, and most agents stop at the first or the second.

Depth 1 is four lines wherever you build the provider client:

diff
 import { createAnthropic } from "@ai-sdk/anthropic";
+import { createCavemanTransport } from "@caveman-ai/agent/wire";
+
+const caveman = createCavemanTransport({ budget: { maxTokens: 2_000 } });

-const anthropic = createAnthropic();
+const anthropic = createAnthropic({ fetch: caveman.fetch });

Unreleased: this page documents @caveman-ai/agent 0.2.0 from source at commit 311be40 in caveman-ai/agent-sdk. Build it from the checkout as described on Install. The adapter packages named below install from <checkout>/packages/adapters/<lane>.

DepthWhat changesWhat you getWhat stays
1. The wireThe fetch your provider client usesSpend ceiling, exact provider usage, cache metadataEverything. The framework never learns about it
2. The adapterOne object in a framework configNormalised per-call usage, lifecycle events, one hook before the provider callYour runner, retries, tools, streams, error types
3. The portThe agent definitionReceipts, eval-gated builds, sandbox, durable runs, subagentsTool bodies, prompts, eval cases

Depth 1 works on any framework, including one nobody wrote an adapter for, because it attaches to fetch and therefore scales by provider. Depth 2 needs a lane for your framework. Depth 3 rewrites the definition, not your tools.

Depth 1: one argument#

typescript
import { createAnthropic } from "@ai-sdk/anthropic";
import { createCavemanTransport } from "@caveman-ai/agent/wire";

const caveman = createCavemanTransport({
budget: { maxTokens: 2_000 },
onModelUsage: (usage) => usageLog.push(usage),
});

const anthropic = createAnthropic({ fetch: caveman.fetch });

That is the whole change, and it is the same line whichever framework sits above it. Every @ai-sdk/* provider takes fetch at construction, as do the OpenAI, Anthropic and LangChain clients underneath the other frameworks; a loop you wrote yourself passes caveman.fetch directly.

One call through it, with the sent body and the meter printed beside usageLog, against a stub upstream so the numbers are fixed:

terminal
body max_tokens sent: 1875
meter settled: 908

The request declared max_tokens: 4000; the meter could fund 1875 and clamped the body to that before it left, then settled by the measured 908. The eleven fields of the usage record each call hands onModelUsage, and which hosts and paths the transport recognises, are on Wire transport.

Verify: onModelUsage fires once per completed call, and meter.settled moves by the measured figure rather than the reserve. Roll back: delete the fetch argument.

Depth 2: an adapter#

model and prompt below are your framework's own, unchanged by this step:

typescript
import { createVercelAISDKAdapter } from "@caveman-ai/adapter-vercel-ai-sdk";
import { createModelBoundary } from "@caveman-ai/agent/model-boundary";
import { ToolLoopAgent, wrapLanguageModel } from "ai";

const boundary = createModelBoundary([{
id: "deterministic-request",
prepare: ({ request }) => ({ ...request, temperature: 0 }),
}]);

const adapter = createVercelAISDKAdapter({
modelBoundary: boundary,
onLifecycleEvent: (event) => lifecycle.push(event),
onModelUsage: (record) => usage.push(record),
});

const agent = new ToolLoopAgent({
model: wrapLanguageModel({ model, middleware: adapter.middleware }),
instructions: "Return one evidence-backed recommendation as JSON.",
...adapter.composeAgentCallbacks(),
});

Vercel keeps its loop, its model transport, its retries, its streams, its aborts and its tool execution. The adapter adds a normalised usage record, a lifecycle event stream, and the model boundary: middleware that runs immediately before the framework's own provider call, on the request the framework built, in your process. That is where prompt trimming, redaction and routing belong, because the boundary hands the rewritten request back to the framework instead of sending anything behind its back.

Middleware receives the request and returns the request, so the framework stays the one thing that talks to the provider. Nine lanes ship with exact upstream pins: Adapters.

Depth 1 and depth 2 compose. The transport can carry the budget and the cache metadata while the boundary carries the request rewrite. Compaction and model routing belong on neither of them, for the reason Wire transport gives.

Depth 3: port the definition#

Do this for the receipt, the eval-gated build, the sandbox, or durable runs.

typescript
import { agent, auto, output, run, schema, tool } from "@caveman-ai/agent";

const lookupOrder = tool({
name: "lookup_order",
description: "Read one order.",
input: schema.object({ id: schema.string() }),
effect: "read",
async execute({ id }) { return JSON.stringify(orders[id] ?? null); },
});

const support = agent({
id: "support",
instructions: "Answer from order data. Never invent a status.",
model: auto(),
tools: [lookupOrder],
output: output({ maxTokens: 400, schema: schema.object({ status: schema.string() }) }),
});

const result = await run(support, "Where is order A-123?", { budget: { maxUsd: 0.05 } });
result.output;
result.receipt;

The loop is what you delete. effect is what you add: read, write, idempotent, or external, a declaration unique to this SDK that the sandbox, the tool policy and durable resume all read.

What you haveWhere it goesNote
System promptinstructions, or instructions.mdRaw markdown either way
Model id stringmodel: "provider/model", or auto()auto() reads CAVE_MODEL, then .caveman/provider.json, then the one credential present
Tools with a Zod or JSON schematool() in tools/Input takes Standard Schema v1. The filename is the tool name
Retrieved documents, playbooks, policy textcontext()Declare stability and safety; volatile data in a stable zone is rejected
Structured output schemaoutput({ maxTokens, schema })Validated before the value can enter model context
Handoffs, agent-as-tool, graph nodes that call a modelsubagent(), or subagents/<name>/Each with its own instructions and wallet
Long-lived memory store@caveman-ai/agent/memoryLocal, opt-in, one turn behind, out of the cached prefix
Max steps, turn caps, spend capsbudget and breakersEnforced per run, root and descendants
Lifecycle callbacks and hooksYour own code around run() or stream()There is no hook surface to port to
Eval suiteevals/*.eval.ts, then caveman-agent buildA failed eval produces no lock
Framework durable executiondurable: { runId }Journal identity includes the definition and budget digests
Channels, schedules, cron, webhooksStays where it isCall run() from it

Shape by shape: Vercel AI SDK, Mastra and Cloudflare Agents port almost literally, model id and tool bodies included. OpenAI Agents handoffs become subagent(), and so does the agent-as-tool pattern. LangGraph is the largest port, because a graph spreads across several definitions: a node that calls a model becomes a run or a subagent, a node that calls code stays your code, and the edges become ordinary control flow. If the graph is the product, stay at depth 1 or 2.

For the Claude Agent SDK and Pi, write the Caveman definition and run it on the runtime you already have through the runner lane rather than porting.

The move#

terminal
npm create @caveman-ai/agent@latest reference-bot
caveman-agent doctor
caveman-agent dev

doctor runs offline, so run it as often as you like: it names Node version, sandbox containment, config, Context IR and provider selection. dev ends its first turn with the receipt. Install covers both.

Keep the scaffold beside the real agent while you move instructions.md, tools/*.ts, and skills into .agents/skills/<name>/SKILL.md.

Verify each depth#

AfterCheckGood looks like
Depth 1onModelUsage, meter.settledOne record per call, null where the provider said nothing, settlement on the measured figure
Depth 1onCacheDecisionapplied on the grammar you expect, heldByScope where the gate held one
Depth 2The adapter's usage sink and lifecycle eventsDisjoint counts, and cost that stays unknown when they are incomplete
Depth 3caveman-agent doctorEvery check passes before a run costs anything
Depth 3The end-of-run receiptA list-price subtotal per call, per tool, per subagent
Depth 3caveman-agent buildA lock, or a named eval failure and no lock

Rolling back#

Depth 1: remove the fetch argument. Depth 2: remove the adapter object from the config; your framework keeps its native result and its native error. Depth 3: the old agent is untouched code in another directory, so run both against the same cases before deleting it.

Each of the three reverses on its own, and each stands alone. Every local figure any of them produces is inferred, defined on Numbers and limits.