Agents: Vercel AI SDK
vercelAiTools(box, ...) returns a Record<string, Tool> ready to spread into generateText or streamText. The Vercel runtime handles the multi-turn loop: you don't write the conversation harness yourself.
Install
pnpm add ai @ai-sdk/anthropic @tangle-network/sandbox
# or @ai-sdk/openai, @ai-sdk/google, etc.Minimal example
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { Sandbox } from "@tangle-network/sandbox";
import { vercelAiTools } from "@tangle-network/sandbox/agent";
const sandbox = new Sandbox({
apiKey: process.env.TANGLE_API_KEY!,
baseUrl: "https://sandbox.tangle.tools",
});
const box = await sandbox.create({ publicTemplateId: "python-data-science" });
const tools = await vercelAiTools(box, { sessionId: "s1" });
const { text } = await generateText({
model: anthropic("claude-opus-4-7"),
tools,
prompt: "Plot sin(x) for x in [0, 2π].",
maxSteps: 6,
});
console.log(text);vercelAiTools is async because the ai package is loaded via dynamic import: the Tangle SDK doesn't bundle it.
Streaming
streamText works the same way. Tool calls execute as the model emits them; their results re-enter the conversation transparently.
const { textStream } = streamText({
model: anthropic("claude-opus-4-7"),
tools,
prompt: "Train a linear model on the iris dataset, then plot residuals.",
maxSteps: 12,
});
for await (const delta of textStream) process.stdout.write(delta);Restricting tools
const tools = await vercelAiTools(box, {
sessionId: "s1",
allow: ["sandbox_run_code", "sandbox_read"],
});How input schemas work
We use ai.jsonSchema(...) to keep input descriptions framework-agnostic. The Vercel AI SDK accepts both Zod and JSON Schema; we use the latter so the Tangle SDK has no Zod runtime dependency.
Durability & resume
Don't bolt a Durable Object onto this. The SDK already buffers every event for every session server-side; the primitives below are how you survive a reload, a Worker eviction, or a deploy mid-stream.
The sessionId passed to vercelAiTools(box, { sessionId }) scopes the code-execution kernel: variables persist across runCode calls. To make the outer generateText/streamText invocation survive a Worker restart, browser reload, or driver crash, wrap it in box.dispatchPrompt + box.session(id).events({ since }). Pass a stable turnId alongside sessionId: a retry from a fresh process then hits the completed-turn cache and the tool loop is never re-billed. sessionId on its own only stops a second agent while the first is still in flight.
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
// Dispatch first to claim the sessionId. The turnId is what makes a retry
// safe: a completed turn is a cache lookup, not a second agent run.
const turnId = req.headers.get("x-turn-id") ?? crypto.randomUUID();
const sessionId = turnId;
await box.dispatchPrompt(req.body.prompt, { sessionId, turnId });
// Pass the same sessionId into the tool pack so the code-exec kernel is shared.
const tools = await vercelAiTools(box, { sessionId });
const { text } = await generateText({
model: anthropic("claude-opus-4-7"),
tools,
prompt: req.body.prompt,
maxSteps: 12,
});
// Reconnect path: a different process / fresh Worker can replay the same
// session without re-executing tool calls.
for await (const event of box.session(sessionId).events({
since: req.headers.get("last-event-id") ?? undefined,
})) {
res.write(`id: ${event.id}\ndata: ${JSON.stringify(event)}\n\n`);
}For browser-direct streaming, use SessionGatewayClient from @tangle-network/sandbox/session-gateway.
It reconnects automatically, replays sequence gaps, and persists lastEventId.
See the SDK's resumed-session guide.
Idempotency & retry
The tool pack's underlying box.runCode accepts an idempotencyKey per call. The agent's tool-execute callback doesn't surface this option directly; if you need per-call dedup, pass idempotencyKey when invoking box.runCode outside the tool pack, or use dispatchPrompt to dedup the entire generateText invocation.
await box.dispatchPrompt(prompt, { sessionId, turnId: deterministicKey });
const tools = await vercelAiTools(box, { sessionId });
const { text } = await generateText({ model, tools, prompt, maxSteps: 12 });The dispatchPrompt layer collapses the whole multi-step loop on retry; the per-call idempotencyKey is for direct runCode callers.