# Harness & Sandbox

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/php/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/php/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 access](https://getstream.io/agents/docs/php/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/php/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 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.

Sessions opened from a browser or a phone cannot reach your VM. In Go, a dispatch worker can host the agent's tools for every session of that agent, wherever it was opened:

```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/php/harness-and-sandbox/](https://getstream.io/agents/docs/php/harness-and-sandbox/).