123 lines
5.1 KiB
Plaintext
123 lines
5.1 KiB
Plaintext
---
|
||
title: "Code execution sandbox"
|
||
sidebarTitle: "Code sandbox"
|
||
description: "Warm an isolated sandbox on each chat turn, run an AI SDK executeCode tool, and tear down right before the run suspends — using chat.agent hooks and chat.local."
|
||
---
|
||
|
||
Use a **hosted code sandbox** (for example [E2B](https://e2b.dev)) when the model should run short scripts to analyze tool output (PostHog queries, CSV-like data, math) without executing arbitrary code on the Trigger worker host.
|
||
|
||
This page describes a **durable chat** pattern that fits `chat.agent()`:
|
||
|
||
- **Warm** the sandbox at the start of each turn (**non-blocking**).
|
||
- **Reuse** it for every `executeCode` tool call during that turn (and across turns in the same run if you keep the handle).
|
||
- **Dispose** it **right before the run suspends** waiting for the next user message — using the **`onChatSuspend`** hook, not `onTurnComplete`.
|
||
|
||
|
||
## Why not tear down in `onTurnComplete`?
|
||
|
||
After a turn finishes, the chat runtime still goes through an **idle** window and only then suspends. During that window the run is still executing — useful for `chat.defer()` work — and the run hasn't suspended yet.
|
||
|
||
The boundary you want for “turn done, about to sleep” is **`onChatSuspend`**, which fires right before the run transitions from idle to suspended. It provides the `phase` (`”preload”` or `”turn”`) and full chat context. See [onChatSuspend / onChatResume](/ai-chat/lifecycle-hooks#onchatsuspend--onchatresume).
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant TurnStart as onTurnStart
|
||
participant Run as run / streamText
|
||
participant TurnDone as onTurnComplete
|
||
participant Idle as Idle window
|
||
participant Suspend as onChatSuspend
|
||
participant Sleep as suspended
|
||
|
||
TurnStart->>Run: warm sandbox (async)
|
||
Run->>TurnDone: persist / inject / etc.
|
||
TurnDone->>Idle: still running
|
||
Idle->>Suspend: dispose sandbox
|
||
Suspend->>Sleep: waiting for next message
|
||
```
|
||
|
||
## Recommended provider: E2B
|
||
|
||
- **API key** auth — works from any Trigger.dev worker; no Vercel-only OIDC.
|
||
- **Code Interpreter** SDK (`@e2b/code-interpreter`): long-lived sandbox, `runCode()`, `kill()`.
|
||
|
||
Alternatives (Modal, Daytona, raw Docker) are fine but more DIY. Vercel’s sandbox + AI SDK helpers are a better fit when execution stays **on Vercel**, not on the Trigger worker.
|
||
|
||
## Implementation sketch
|
||
|
||
### 1. Run-scoped sandbox map
|
||
|
||
Keep a `Map<runId, Promise<Sandbox>>` (or similar) in a **task-only module** so your Next.js app never imports it.
|
||
|
||
### 2. `onTurnStart` — warm without blocking
|
||
|
||
```ts
|
||
onTurnStart: async ({ runId, ctx, ...rest }) => {
|
||
warmCodeSandbox(runId); // fire-and-forget Sandbox.create()
|
||
// ...persist messages, writer, etc.
|
||
},
|
||
```
|
||
|
||
### 3. `chat.local` — run id for tools
|
||
|
||
Tool `execute` functions do not receive hook payloads. Use [`chat.local()`](/ai-chat/chat-local) to store the current run id for the sandbox key, **initialized from `onTurnStart`** (same `runId` as the map):
|
||
|
||
```ts
|
||
// In the same task module as your tools
|
||
import { chat } from "@trigger.dev/sdk/ai";
|
||
|
||
export const codeSandboxRun = chat.local<{ runId: string }>({ id: "codeSandboxRun" });
|
||
|
||
export function warmCodeSandbox(runId: string) {
|
||
codeSandboxRun.init({ runId });
|
||
// ...start Sandbox.create(), store promise in Map by runId
|
||
}
|
||
```
|
||
|
||
The **`executeCode`** tool reads `codeSandboxRun.runId` and awaits the sandbox promise before `runCode`.
|
||
|
||
### 4. `onChatSuspend` / `onComplete` — teardown
|
||
|
||
Use **`onChatSuspend`** to dispose the sandbox right before the run suspends, and **`onComplete`** as a safety net when the run ends entirely.
|
||
|
||
```ts
|
||
export const aiChat = chat.agent({
|
||
id: "ai-chat",
|
||
// ...
|
||
onChatSuspend: async ({ phase, ctx }) => {
|
||
await disposeCodeSandboxForRun(ctx.run.id);
|
||
},
|
||
onComplete: async ({ ctx }) => {
|
||
await disposeCodeSandboxForRun(ctx.run.id);
|
||
},
|
||
});
|
||
```
|
||
|
||
Unlike `onWait` (which fires for all wait types), `onChatSuspend` only fires at chat suspension points — no need to filter on `wait.type`. The `phase` discriminator tells you if this is a preload or post-turn suspension.
|
||
|
||
Optional **`onChatResume`**: log or reset flags; a fresh sandbox can be warmed again on the next **`onTurnStart`**.
|
||
|
||
### 5. AI SDK tool
|
||
|
||
Wrap the provider in a normal AI SDK `tool({ inputSchema, execute })` (same pattern as `webFetch`). Keep tool definitions in **task code**, not in the Next.js server bundle.
|
||
|
||
### 6. Environment
|
||
|
||
Set **`E2B_API_KEY`** (or your provider’s secret) on the **Trigger environment** for the worker — not in public client env.
|
||
|
||
## Typing `ctx`
|
||
|
||
Every `chat.agent` lifecycle event and the `run` payload include **`ctx`**: the same **[`TaskRunContext`](/ai-chat/reference#task-context-ctx)** shape as `task({ run: (payload, { ctx }) => ... })`.
|
||
|
||
```ts
|
||
import type { TaskRunContext } from "@trigger.dev/sdk";
|
||
```
|
||
|
||
The alias **`Context`** is also exported from `@trigger.dev/sdk` and is the same type.
|
||
|
||
## See also
|
||
|
||
- [Database persistence for chat](/ai-chat/patterns/database-persistence) — conversation + session rows, hooks, token renewal
|
||
- [Lifecycle hooks](/ai-chat/lifecycle-hooks)
|
||
- [API Reference — `ctx` on events](/ai-chat/reference#task-context-ctx)
|
||
- [Per-run data with `chat.local`](/ai-chat/chat-local)
|