# Harness and sandbox

> **Note:** This page covers every SDK. Each sample is labelled with its language and where it runs: the client (your app, signed in as the user) or the server (with your API secret).

The harness sits between the caller and the models. It runs in the backend and handles:

- Hand-off to a subagent, a slower thinking model, while the fast model keeps talking
- Loading skills when they are used
- Compaction when the conversation nears the model's context window
- Starting the sandbox when code first runs

A sandbox lets the agent write and run code. The backend has a basic one built in. For anything more, run your own sandbox in your process and offer it as a tool.

## Standard agent setup

The harness is part of the agent, set in `agent.yaml`. A session cannot change it.

```yaml title="agents/support/agent.yaml"
name: support
llm: llm-fast
harness: default
subagent: llm-smart
sandbox: daytona
```

| Field      | Meaning                                                                                                                                                                                                                                                         |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `harness`  | Which harness runs the agent. `default` is the only one, and what an agent that leaves it out gets.                                                                                                                                                             |
| `subagent` | The thinking model that skills are handed to. When left out, the router defaults to `llm-thinking` when the deployment can route it, and the fast model answers everything itself when it cannot. Voice agents only: a text agent runs its skills on its `llm`. |
| `sandbox`  | Where the agent runs code it writes: the subagent on a voice agent, the agent itself on a text agent. `daytona` is the only provider. Leave it out for none.                                                                                                    |

### Hand-off

When a question needs more thought, the fast model says something like "let me check" and asks for a [skill](https://getstream.io/agents/docs/knowledge-and-skills/#skills). The subagent answers under that skill's instructions, and the answer comes back as a turn of its own, so the caller hears it without asking again. Work the caller has moved on from is cancelled.

The subagent can look things up in the [knowledge base](https://getstream.io/agents/docs/knowledge-and-skills/) and search the web when the agent can, the knowledge base first.

### Skills

The fast model sees only each skill's name and description. A skill's instructions are loaded when it is used, so an edited skill takes effect on its next use, even in a conversation already running.

### Compaction

Once a prompt fills 80% of the conversation model's context window, the subagent summarises the older part of the conversation and the summary replaces it. The latest turns stay word for word. Compaction needs a subagent; an agent without one keeps its full history.

### Sandbox

With `sandbox: daytona`, the agent is offered a `run_code` tool that runs short Python programs and reads back what they print. That is how it handles arithmetic, dates and parsing, and runs a skill's own scripts. On a voice agent only the subagent gets it, because running code takes seconds a call does not have. A text agent has no subagent, so the model holding the conversation runs code itself.

Code in the sandbox reaches nothing outside it by default. Under `sandbox_options`, `network: true` opens the internet, and `allowed_domains` (domains) or `network_allow_list` (CIDR ranges), one or the other, name the only places it may go, with or without `network`:

```yaml title="agents/support/agent.yaml"
sandbox: daytona
sandbox_options:
  allowed_domains:
    - api.github.com
```

To let code call a connector's API, such as GitHub's, with the connection's token, see [Sandbox](https://getstream.io/agents/docs/connectors/sandbox/).

`sandbox_options` also says how big the sandbox is, where it runs and what it is built from:

```yaml title="agents/support/agent.yaml"
sandbox: daytona
sandbox_options:
  snapshot: daytona-large
  region: eu
  timeout: 5m
```

| Option                                             | Meaning                                                                                                                                                                                                                                                                                                                  |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `snapshot`                                         | One of Daytona's default sandboxes: `daytona-small` (1 vCPU, 1 GiB memory, 3 GiB disk), `daytona-medium` (2, 4, 8), `daytona-large` (4, 8, 10), or a Linux VM of the same size, `daytona-vm-small`, `daytona-vm-medium` or `daytona-vm-large`. Nothing is built, so it starts quickly. Other snapshot names are refused. |
| `region`                                           | Where the sandbox runs: `us` or `eu`. Left out, Daytona's default region.                                                                                                                                                                                                                                                |
| `image`, `setup`                                   | Build a sandbox of your own instead of a snapshot: a container `image` with Python to start from, and shell commands run on it once, such as `pip install`. Daytona keeps the build, so only the first sandbox from a setup waits for it.                                                                                |
| `cpu`, `memory_gb`, `disk_gb`                      | The size of a built sandbox. A snapshot has its own size, so it takes none of these, nor `image` or `setup`.                                                                                                                                                                                                             |
| `timeout`                                          | How long one run of code may take, such as `5m`, at most `30m`. Left out, 30 seconds.                                                                                                                                                                                                                                    |
| `network`, `allowed_domains`, `network_allow_list` | What code in the sandbox may reach, as above.                                                                                                                                                                                                                                                                            |

The sandbox is created the first time code runs and released when the session ends. On a voice agent, a skill may use its tools up to four rounds, within its deadline. The router needs `DAYTONA_API_KEY`.

## Custom sandbox

To give the agent a sandbox of your own (a VM holding your repositories, say, or tools the built-in one lacks), keep it in your process and offer it as a [tool](https://getstream.io/agents/docs/tool-calling-and-dispatch/). The backend never sees the VM: it offers the tool to the model, sends the call over the session socket, and waits for your answer.

**Go (server)**

```go label="Go"
type RunInVM struct {
	Command string `json:"command" schema:"the shell command to run in the project's VM"`
}

vm, err := StartVM(ctx) // your own: Daytona, Firecracker, a container
if err != nil {
	return err
}
defer vm.Close()

runInVM := func(ctx context.Context, args RunInVM) (any, error) {
	return vm.Exec(ctx, args.Command)
}

err = tools.Register(agent.Tools(), "run_in_vm", "Run a shell command in a VM holding our repositories", runInVM)
if err != nil {
	return err
}
```

- Start the VM before the first call and let a call that arrives early wait for it, since the model cannot try again later.
- The tool timeout bounds how long the backend waits. Raise the session's `tool_timeout_ms` (`ToolTimeout` on Go's `stream.Config`) to give slow work room.
- Leave `sandbox` out of `agent.yaml`, so the agent is not handed a second one.

>
> **Warning:** Sessions opened from a client cannot reach your VM.
>

A [dispatch worker](https://getstream.io/agents/docs/tool-calling-and-dispatch/#hosting-tools) can host the agent's tools for every session of that agent, wherever it was opened. In Go:

**Go (server)**

```go label="Go"
support := api.Agent("support")
err = tools.Register(support.Tools(), "run_in_vm", "Run a shell command in a VM holding our repositories", runInVM)
if err != nil {
	return err
}

dispatch, err := agents.NewDispatch(agents.DispatchOptions{})
if err != nil {
	return err
}
dispatch.Host(support, 2*time.Minute)
return dispatch.Run(ctx)
```

---

For the most recent version of this documentation, visit [https://getstream.io/agents/docs/harness-and-sandbox/](https://getstream.io/agents/docs/harness-and-sandbox/).