> ## Documentation Index
> Fetch the complete documentation index at: https://yutori.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Building agents with n2

> Step-by-step guide to build a Python agent loop that lets Navigator n2 control a Daytona sandbox

[Navigator n2](https://yutori.mintlify.site/reference/n2.md) is a computer-use model that looks at desktop screenshots and predicts tool calls — mouse and keyboard actions, bash commands, or file edits — to be executed in desktop environments.
[Daytona](https://www.daytona.io) provides sandboxes with full desktop environments where the predicted actions can be executed.

This guide walks you through how to set up a simple agent loop that wires the two together to set up a complete computer-use agent stack.

> [!NOTE]
>
> To drive your own Mac, install [Yutori MCP](https://github.com/yutori-ai/yutori-mcp) — it ships a local loop already wired up.

```text theme={null}
          ┌──────────────────────────────────────────────────┐
          │                    Agent Loop                    │
          │               (your code, Python)                │
          └──────────────────────────────────────────────────┘
                   │   ▲                         │   ▲
 task + screenshot │   │ next action     execute │   │ screenshot
                   ▼   │                         ▼   │
          ┌────────────────────┐        ┌────────────────────┐
          │    Navigator n2    │        │  Daytona Sandbox   │
          │    (Yutori API)    │        │  Linux desktop +   │
          │                    │        │  Computer Use API  │
          └────────────────────┘        └────────────────────┘
                 decides                   perceives + acts
```

## Components

| Component | Role | Your part |
| - | - | - |
| **Navigator n2** | Reads the screenshot and returns the next tool call(s) | A Yutori API key |
| **Daytona sandbox** | Runs those actions on a real Linux desktop | A Daytona API key |
| **Agent loop** | Sends the task and screenshot, executes the calls, sends the results back, repeats | [`navigator_n2_daytona.py`](https://github.com/yutori-ai/yutori-sdk-python/blob/main/examples/navigator_n2_daytona.py) |

The loop is `N2ComputerAgent` from the [Python SDK](https://github.com/yutori-ai/yutori-sdk-python): it owns the turns, the message history, the screenshot window, and the coordinate math. Everything specific to Daytona lives in one adapter class, `DaytonaComputer`. Both are in [`examples/navigator_n2_daytona.py`](https://github.com/yutori-ai/yutori-sdk-python/blob/main/examples/navigator_n2_daytona.py), along with a `main` that creates the sandbox, runs the agent, and deletes the sandbox.

n2 typically answers with one `computer_batch` call, which chains several actions against a single screenshot. It can also call `bash`, or one of the file tools (`read`, `write`, `edit`) that the example serves through the SDK's `ShellFileToolsMixin`. `N2ComputerAgent` runs whatever comes back and replies with a fresh screenshot for the batch, or the tool's own output otherwise.

## Prerequisites

* **[uv](https://docs.astral.sh/uv/)** — the script declares its dependencies inline (PEP 723), so `uv run` picks a Python 3.10+ interpreter and installs them into an isolated environment. Nothing to pin by hand.
* A **Yutori API key** — see [Authentication](https://yutori.mintlify.site/authentication.md).
* A **Daytona API key** from the [Daytona dashboard](https://app.daytona.io).

```bash theme={null}
uv tool install yutori       # provides `yutori auth login`; skip it if you export YUTORI_API_KEY

yutori auth login            # or: export YUTORI_API_KEY=...
export DAYTONA_API_KEY=...
```

The two are not interchangeable when both are present: the SDK resolves the key as
explicit argument, then `YUTORI_API_KEY`, then the stored login. A stale `YUTORI_API_KEY`
left in your shell silently shadows `yutori auth login` and the run fails with `401`.

## Run it

Run the script straight from the SDK repository with a task — `uv` fetches it, resolves its inline dependencies, and runs it:

```bash theme={null}
uv run https://raw.githubusercontent.com/yutori-ai/yutori-sdk-python/main/examples/navigator_n2_daytona.py \
  "Write 'hello from n2' to /tmp/demo.txt, then open a terminal and cat the file"
```

Each turn prints the model's own text and the call it made:

```text theme={null}
ACTION bash: {"command": "echo 'hello from n2' > /tmp/demo.txt"}
The file is written. Now I'll launch the Terminal Emulator.
ACTION computer_batch: {"actions": [{"name": "left_click", "arguments": {"coordinates": [56, 11]}}]}
```

The loop ends when n2 returns a message with no tool calls. The sandbox is deleted in a `finally` block, so it does not outlive the run even when the run fails.

Two flags are worth knowing: `--max-steps N` raises the turn budget from its default of 50,
and `--record` saves a screen recording of the desktop to `n2-daytona-run.mp4` when the run
ends, which is how you review a run after the sandbox is gone.

### Adapting it with a coding agent

Point your coding agent to the script, and describe what needs to change for your setup.

```text theme={null}
Start from https://github.com/yutori-ai/yutori-sdk-python/blob/main/examples/navigator_n2_daytona.py
— it is a working Navigator n2 + Daytona agent loop. Copy it in and adapt it for <what I'm building>.

Install: pip install 'yutori>=0.9.6' 'daytona==0.207.0'
Keys:    YUTORI_API_KEY and DAYTONA_API_KEY in the environment.

Leave alone unless I ask: the pinned tool_set, the typing chunker, the scroll conversion,
the bash working-directory sentinel, and deleting the sandbox in a finally block.
Each is there for a reason documented at https://docs.yutori.com/reference/n2-daytona.

Change: <the snapshot, the step cap, how the result gets back to my code>.
```

## Adapter details

`DaytonaComputer` is the class `N2ComputerAgent` calls to observe and act. Most methods pass through to Daytona unchanged. The exceptions are below:

* **Sandbox lifecycle** — wait for `computer_use.start()` to finish before interacting with the sandbox, and always delete it in a `finally` block to stop billing.
* **Coordinates** — n2 predicts them in a [normalized 1000×1000 space](https://yutori.mintlify.site/reference/n2.md#coordinate-system). `N2ComputerAgent` rescales them to the desktop's native pixel size before calling the adapter, so the adapter does not do any conversions.
* **Typing** — `keyboard.type` fails on control characters and silently truncates long strings, so send `\n` and `\t` as key presses and chunk the rest.
* **Scrolling** — Daytona wants wheel notches. The adapter declares a `model_action` parameter, so the loop hands it the model's own call, whose `direction` and `amount` are already notches. The pixel distance the loop passes otherwise — one notch of `amount` being a tenth of the screen — is converted back as the fallback.
* **`bash` results** — the command's output, with an `Exit code N` header prepended on failure. The SDK's `format_shell_output` truncates each result at 30,000 characters, and `N2ComputerAgent` holds a 256 KiB backstop above that, so a large `cat` cannot push the request over the [10 MB limit](https://yutori.mintlify.site/reference/n2.md#screenshots).

> [!NOTE]
>
> `bash` promises a working directory that persists across calls, but every `exec` is a fresh process, so a `cd` would be forgotten by the next command. The adapter honors the contract: each command prints its final `$PWD` on a sentinel line, and the next command starts there through `exec`'s `cwd`. `run_in_background` detaches the command with `nohup` and returns its pid and output file.

## Knowing when to stop

Without a limit, a loop will keep going on a task it cannot finish. `N2ComputerAgent`'s `max_steps` caps the number of model turns; the script defaults to 50 and exposes it as `--max-steps`. When that budget is spent, `agent.stopped_by` reads `"max_steps"`, and the script spends one last completion asking the model to summarize how far it got before exiting non-zero. For a stop condition of your own, a callback's `on_run_continue` runs before each turn and ends the run when it returns `False`.

Execution errors are not fatal to the run. A batch stops at the first action that fails, and the tool result tells the model which action failed along with a screenshot of the resulting state; a `bash` call the adapter cannot run at all comes back as an `[ERROR] bash failed: ...` result, and a nonzero exit code is simply part of the output. A timeout is deliberately not an error: the adapter reports it as an ordinary `Command timed out after 120s` result, because an expiry is something the model should react to rather than a failure of the harness. In every case the model sees what happened and routes around it.

For anything unattended on a machine that is not disposable, add an `action_confirmation_callback`. `N2ComputerAgent` calls it before every action except `screenshot`, `wait`, `mouse_move`, and `scroll`, and skips the call if it returns `False`. The sandbox here is disposable, so the script does not confirm anything. On a harness driving a real machine, keep shell confirmation mandatory.

## Going further

* **Pick a snapshot with the apps you need.** `daytonaio/sandbox:0.6.0` is a bare XFCE desktop at 1024×768. Build your own to give the model a browser, an editor, or a dataset.
* **Watch the run.** `(await sandbox.get_preview_link(6080)).url` is a noVNC view of the desktop, which is the fastest way to see what a failing run is doing.
* **Send the whole history.** `N2ComputerAgent` keeps every turn and strips only the images outside the [two newest image-bearing messages](https://yutori.mintlify.site/reference/n2.md#message-history), which is what the server would drop anyway. Trimming messages yourself risks the model repeating work it has already done. A run long enough to approach the context window compacts instead of truncating — `compactor="auto"` is the default, and each pass fires the `on_compaction` callback the script prints from.

## Related

> [card] [Navigator n2](https://yutori.mintlify.site/reference/n2.md)
>
> The model reference — tools, actions, coordinates, and request fields.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.