Files
Benjamin Taylor 5ca110e29c docs: gate 20 route snippets in CI and migrate the Claude SDK quickstarts
Two things this PR was missing, both now closed.

## The Claude SDK quickstarts are unblocked

#6618 put `showcase/integrations/claude-sdk-{python,typescript}` on
`createCopilotRuntimeHandler` with `mode: "single-route"`, at the **plain**
`route.ts` path. That dissolves the coupling that forced these two pages to be
reverted earlier: `verify-shell-docs.ts` asserts each page claims a starter file
at `src/app/api/copilotkit/route.ts` AND that the file exists in the extracted
starter. Single-route keeps that path, so the prose claims and
`requiredStarterFiles` are unchanged — only the fence bodies move to v2.

The two content assertions that pinned those pages to v1
(`ExperimentalEmptyAdapter`, `copilotRuntimeNextJSAppRouterEndpoint`) now
require `createCopilotRuntimeHandler`, the `/v2` entrypoint,
`mode: "single-route"` and a `POST` export. Mutation-checked: flipping the
fixture to `mode: "multi-route"` fails with
`app/api/copilotkit/route.ts missing single-route mode`.

## Snippet gating: 1 -> 20 route fences

I previously claimed the integration pages could not be doctested because of
path aliases and per-integration deps. **That was an assumption I never
checked, and it was wrong.** Of the 52 migrated route fences, 47 import nothing
project-relative; 36 are complete, self-standing routes. 27 pages were
eligible, 20 now hold a gated fence — each extracted and typechecked by
`tsc --noEmit` against real npm-installed packages in CI.

One fence per (page, title): `extract.ts` concatenates tagged blocks sharing a
title, so a second complete route on the same page would collide.

Mutation-checked on `snippets/integrations/langsmith/index.mdx`: restoring the
v1 import in the gated fence turns the run red (20 passed, 1 failed). My first
attempt at this check was a no-op — the pattern missed because the fence is
JSX-indented — and it "passed" misleadingly. The real check asserts the mutation
reached the extracted snippet before trusting the result.

### Harness changes this needed

- `extract.ts` now finds the nearest `doctest.json` by walking up to the docs
  root, instead of looking only in the page's own directory. Otherwise gating
  20 pages means ~20 duplicated dependency lists that then drift. A shared list
  lives at `content/doctest.json`; `docs/integrations/langgraph/` keeps its own
  (Python deps) and now also carries the TS deps its page needs.
- `run.ts` installs each dependency set **once**, into
  `.doctest-output/.deps/<hash>`, and links it into every snippet sharing that
  set. Per-snippet installs took **7:58** for 21 snippets, uncomfortably close
  to the job's 15-minute timeout; shared installs take **0:45** cold. Different
  dep sets still get separate stores, so this is a dedupe, not a merge.

### `@ag-ui/*` versions have to be pinned to what the runtime expects

Unpinned, the gated fences failed with `HttpAgent is not assignable to
AbstractAgent — separate declarations of a private property '_debug'`: npm
installs a newer `@ag-ui/client` than `@copilotkit/runtime` depends on, so two
`AbstractAgent` declarations collide. The sidecar pins `@ag-ui/client@0.0.57` and
`@ag-ui/core@0.0.57` to match `@copilotkit/runtime@1.68.3`.

## Seven fences are deliberately NOT gated

Un-tagged with the reason, rather than left failing or quietly dropped:

- `docs/auth.mdx`, `docs/premium/connect-your-runtime.mdx` — illustrative
  fences referencing placeholders (`myAgent`, `verifyJwt`) that cannot compile
  standalone by design.
- the four langgraph-family pages and
  `snippets/self-hosting-copilot-runtime-langgraph-endpoint.mdx` — these hit
  `LangGraphAgent is not assignable to AbstractAgent — separate declarations of
  a private property '_debug'`, which pinning does not fix.

**That last one is a real pre-existing defect, not a migration regression.** I
reconstructed the v1 form of the langgraph quickstart snippet verbatim from
`origin/main` and typechecked it against the identical installed dependencies:
it fails with the same error. So these snippets have never typechecked against
published packages — worth filing separately. It is also what the ~220
`@ts-ignore` comments across `showcase/integrations` were papering over.

## Verified

    doc-tests (cold, no cache)          -> 21 passed, 0 failed in 0:45
    mutation check (real, verified)     -> 20 passed, 1 failed
    vitest extract + verify-shell-docs  -> 34 passed
    showcase/shell-docs typecheck       -> exit 0
    showcase/shell-docs build           -> exit 0
    structural audit                    -> 21/21 pages, fence + JSX identical to HEAD

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 09:25:19 -05:00

164 lines
6.0 KiB
Plaintext

---
title: "Daytona"
description: "Give CopilotKit's Built-in Agent a tool that executes code in an isolated Daytona sandbox, then return the output to the chat."
icon: "custom/daytona"
showIcon: true
doc_type: how-to
---
[Daytona](https://www.daytona.io) gives AI agents isolated cloud **sandboxes** — full Linux runtimes with
their own filesystem, network, and CPU/RAM. This recipe adds a server tool to CopilotKit's
[Built-in Agent](/) that runs code inside one of those sandboxes and returns the result to
the chat, so the agent can execute code it writes without touching your host.
This guide assumes you already have a running Built-in Agent app. If you don't, follow the
[Quickstart](/quickstart) first — it takes a few minutes.
## Try it live
{/* Placeholder URL — update to the permanent demo deployment once it exists. */}
<iframe
src="https://showcase-daytona-runcode-production.up.railway.app"
title="Daytona runCode live demo"
className="w-full h-[480px] sm:h-[600px] block rounded-xl border border-[var(--border)]"
/>
## Prerequisites
- A running Built-in Agent app (see the [Quickstart](/quickstart)).
- A Daytona account and API key — create one in the [Daytona dashboard](https://app.daytona.io/dashboard/keys).
Add the key to your `.env`:
```plaintext title=".env"
DAYTONA_API_KEY=your_daytona_api_key
```
## Install the Daytona SDK
```npm
npm install @daytonaio/sdk
```
## Define the sandbox tool
Add a `runCode` [server tool](/server-tools) and register it on your `BuiltInAgent`. The
tool creates a fresh sandbox, runs the code, returns stdout, and cleans the sandbox up — so every call is
isolated.
```ts title="app/api/copilotkit/[[...slug]]/route.ts" doctest="component"
import {
CopilotRuntime,
createCopilotRuntimeHandler,
InMemoryAgentRunner,
} from "@copilotkit/runtime/v2";
import { BuiltInAgent, defineTool } from "@copilotkit/runtime/v2"; // [!code highlight]
import { Daytona } from "@daytonaio/sdk"; // [!code highlight]
import { z } from "zod";
const daytona = new Daytona(); // reads DAYTONA_API_KEY
const runCode = defineTool({ // [!code highlight:20]
name: "runCode",
description: "Execute code in an isolated Daytona sandbox and return its output.",
parameters: z.object({
code: z.string().describe("The code to run in the sandbox"),
language: z
.enum(["python", "typescript", "javascript"])
.default("python")
.describe("Language runtime for the code"),
}),
execute: async ({ code, language }) => {
const sandbox = await daytona.create({ language });
try {
const res = await sandbox.process.codeRun(code);
return { stdout: res.result, exitCode: res.exitCode };
} finally {
await sandbox.delete();
}
},
});
const builtInAgent = new BuiltInAgent({
model: "openai:gpt-5.4-mini",
tools: [runCode], // [!code highlight]
maxSteps: 2, // required so the agent can call the tool and then respond // [!code highlight]
});
const runtime = new CopilotRuntime({
agents: { default: builtInAgent },
runner: new InMemoryAgentRunner(),
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handler;
export const POST = handler;
```
<Callout type="info" title="Sandbox networking on the free tier">
Daytona Tier 1 & 2 organizations run sandboxes with restricted networking, but package registries
(npm, PyPI) and major AI APIs (Anthropic, OpenAI, Google) are whitelisted — so this recipe works on the
free tier. Code that needs to reach arbitrary external URLs requires Tier 3+. See
[Daytona's limits](https://app.daytona.io/dashboard/limits).
</Callout>
## Try it
Start your app and ask the agent to run something. The tool runs Python by default, and the agent can
pass `language` to run TypeScript or JavaScript instead:
```text
Run a Python snippet that prints the first 10 Fibonacci numbers.
```
```text
Run JavaScript that logs the current timestamp.
```
The agent writes the code, calls `runCode`, and reports back the sandbox's output.
## Going further
- **Reuse a sandbox** to avoid per-call startup cost: create one lazily and cache it instead of deleting it
each call. Pause it with `sandbox.stop()` when idle.
- **Run a server and preview it** — expose a port with `sandbox.getPreviewLink(port)` to hand back a live
URL for an app the agent builds.
- **Custom environments** — preinstall packages with a snapshot so sandboxes start ready to go.
- **Any other language** — `codeRun` covers Python, TypeScript, and JavaScript. For anything else (Go,
Rust, a shell pipeline), use `sandbox.process.executeCommand(...)` instead, optionally on a custom image
with the toolchain installed.
See the [Daytona docs](https://www.daytona.io/docs) for the full SDK.
## Get started with a coding agent
Paste this prompt into your coding agent (Cursor, Claude Code, etc.) to add the integration to an existing
Built-in Agent app:
```text
In my existing CopilotKit Built-in Agent app, add a Daytona-backed code execution tool:
1. Install the Daytona SDK: `npm install @daytonaio/sdk`.
2. Add DAYTONA_API_KEY to my .env (I'll supply the value).
3. In my CopilotKit runtime route, create a Daytona client with `new Daytona()` and define a
server tool with `defineTool` from "@copilotkit/runtime/v2":
- name: "runCode"
- description: "Execute code in an isolated Daytona sandbox and return its output."
- parameters: a Zod object with a `code` string field and a `language` enum
("python" | "typescript" | "javascript", default "python").
- execute: create a sandbox with `daytona.create({ language })`, run the code with
`const res = await sandbox.process.codeRun(code)` — note that `codeRun` returns an
`ExecuteResponse` where stdout lives on `res.result` (not `res.stdout`), so return
`{ stdout: res.result, exitCode: res.exitCode }`. Call `sandbox.delete()` in a finally
block so the sandbox is cleaned up even on error.
4. Register the tool on my BuiltInAgent (`tools: [runCode]`) and set `maxSteps: 2` so it can call the
tool and then respond.
Keep my existing model and runtime setup unchanged.
```