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

# Navigator n2 (Computer Use)

**Navigator n2** is a computer-use model. It operates a full desktop. Given a task in natural language and a screenshot, it predicts the next batch of mouse and keyboard actions, and it can run shell commands directly. Use the API model id `n2` in the `model` field of your `chat.completions` requests.

n2 follows the [OpenAI `chat.completions` interface](/reference/navigator). It exposes five tools — `computer_batch` for the GUI, `edit` / `read` / `write` for files, and `bash` for the shell — and returns its actions as `tool_calls` for your client to execute against the desktop.

<Tip>
  Want to see n2 work before you write anything? Open the [Playground](https://platform.yutori.com/navigator/playground) — it runs n2 against a hosted desktop and streams the session live.

  Want to run n2 on your local Mac? Use [Yutori MCP](https://github.com/yutori-ai/yutori-mcp) for the local computer-use harness and setup instructions.
</Tip>

## Model Versions

| API model id | Description                                     |
| ------------ | ----------------------------------------------- |
| `n2`         | Points to the latest stable Navigator n2 model. |

## Supported Actions

The default tool set is `computer_use_tools-20260830`. It contains five tools.

### `computer_batch`

The one tool that drives the GUI. It runs a sequence of primitive actions in a single call and returns a screenshot after the last one, so it is both how the model acts and how it sees.

| Parameter | Type          | Description                                                         |
| --------- | ------------- | ------------------------------------------------------------------- |
| `actions` | array (min 1) | The ordered sequence of primitive GUI actions to run in this batch. |

Each item in `actions` is shaped `{"name": <action>, "arguments": {...}}`:

```json theme={null}
{
  "name": "computer_batch",
  "arguments": {
    "actions": [
      {"name": "left_click", "arguments": {"coordinates": [412, 380]}},
      {"name": "type", "arguments": {"text": "quarterly-report.csv"}},
      {"name": "key_press", "arguments": {"key": "enter"}}
    ]
  }
}
```

Three properties of a batch matter when you execute it:

* Actions run **sequentially**, in order.
* Execution **stops at the first error**. Remaining actions are skipped — do not continue past a failure, and report which action failed in the tool result.
* All coordinates in a batch refer to the screenshot taken **before the batch started**, not to the state after any earlier action in the same batch.

Return a **single** tool result for the whole batch, carrying one screenshot taken after the last action executed.

### Batch Actions

The current vocabulary has 15 primitive action types inside `computer_batch`. Coordinate-taking actions use the [normalized 1000×1000 space](#coordinate-system). The Python SDK's `N2ComputerAgent` accepts 1–20 actions in one batch.

| Action         | Description                            | Required Args                        | Optional Args |
| -------------- | -------------------------------------- | ------------------------------------ | ------------- |
| `left_click`   | Left mouse click                       | `coordinates`                        | `modifier`    |
| `double_click` | Double left click                      | `coordinates`                        | `modifier`    |
| `triple_click` | Triple left click                      | `coordinates`                        | `modifier`    |
| `middle_click` | Middle mouse click                     | `coordinates`                        | `modifier`    |
| `right_click`  | Right mouse click                      | `coordinates`                        | `modifier`    |
| `scroll`       | Scroll vertically, centered on a point | `coordinates`, `direction`, `amount` | `modifier`    |
| `type`         | Type text into the focused input       | `text`                               |               |
| `key_press`    | Press a key, combination, or sequence  | `key`                                |               |
| `drag`         | Drag from one point to another         | `start_coordinates`, `coordinates`   |               |
| `mouse_move`   | Move the mouse to a point              | `coordinates`                        |               |
| `mouse_down`   | Press and hold the left button         |                                      | `coordinates` |
| `mouse_up`     | Release the left button                |                                      | `coordinates` |
| `hold_key`     | Hold a key down for a duration         | `key`                                | `duration`    |
| `wait`         | Pause without interacting              |                                      | `duration`    |
| `screenshot`   | Capture the screen without acting      |                                      |               |

**Parameter notes:**

* `coordinates` and `start_coordinates` are `[x, y]` integers in the normalized `0–1000` space, origin top-left.
* `direction` for `scroll` is `up` or `down`. **Horizontal scrolling is not available.**
* `amount` for `scroll` is an integer from `1` to `50`, measured in wheel notches.
* `modifier` is a single modifier key held for the duration of the action: `ctrl`, `shift`, `alt`, `meta`, `command`, or `super`. Execute it as one gesture — press, act, release. A modified click that degrades to a plain click is worse than an error, because a `ctrl`-click meant to extend a selection instead activates the target.
* `duration` for `wait` and `hold_key` is in seconds.
* `mouse_down` and `mouse_up` bracket a manual drag: `mouse_move` → `mouse_down` → `mouse_move` → `mouse_up`. The button stays held across the members between them, so execute them against real press/release primitives rather than synthesizing a click. Release anything still held when the batch ends.
* `screenshot` takes no arguments and acts on nothing. A batch already answers with a screenshot taken after its last action, so a `screenshot` member is a no-op you can skip; it exists because it is the only way the model can ask to look **without** acting.

### `bash`

Runs a shell command on the desktop and returns combined stdout/stderr.

| Parameter           | Type    | Default | Description                                                                                                                                            |
| ------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `command`           | string  | —       | The command to execute. Required.                                                                                                                      |
| `timeout`           | number  | `120`   | Timeout in seconds. Maximum `600`.                                                                                                                     |
| `run_in_background` | boolean | `false` | Run detached. The result carries a task id, the output file path to read, the process id when available, and a cancel command. No trailing `&` needed. |

Each call is a **separate process**. The working directory persists across calls; environment variables and shell functions do not — chain dependent steps in one command with `&&` or `;`.

### `read`

Reads a text file and returns it in `cat -n` format, with the encoding detected from the byte-order mark.

| Parameter   | Type    | Default | Description                                                                       |
| ----------- | ------- | ------- | --------------------------------------------------------------------------------- |
| `file_path` | string  | —       | Path to read. Required.                                                           |
| `offset`    | integer | `1`     | First line to return, using 1-based line numbers for paging through a large file. |
| `limit`     | integer | `2000`  | Number of lines to return.                                                        |

Relative paths resolve against the shell's working directory. To look at an image, take a screenshot instead — the tool result channel is text.

### `write`

Writes a file, creating it or replacing its contents.

| Parameter   | Type   | Description                                                 |
| ----------- | ------ | ----------------------------------------------------------- |
| `file_path` | string | Path to write. Required.                                    |
| `content`   | string | Full file contents. Required. Capped at 256,000 characters. |

Prefer this over a `bash` heredoc for anything non-trivial: content rides as a JSON string, so it is never shell-escaped and a backtick or `$` in the payload cannot corrupt the file.

### `edit`

Replaces an exact string in an existing file.

| Parameter     | Type    | Default | Description                                                   |
| ------------- | ------- | ------- | ------------------------------------------------------------- |
| `file_path`   | string  | —       | Path to edit. Required.                                       |
| `old_string`  | string  | —       | Exact text to replace, including indentation. Required.       |
| `new_string`  | string  | —       | Replacement text. Required.                                   |
| `replace_all` | boolean | `false` | Replace every occurrence instead of requiring a unique match. |

The file has to be read (or written) earlier in the same session first — the model is expected to know the current bytes before changing them, and your executor should enforce that rather than editing blind.

### Key Space

n2 uses lowercase key names. Combinations are joined with `+`, and sequential presses are separated by spaces.

| Category        | Key Names                                                                                                                           |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Modifiers       | `ctrl`, `alt`, `shift`, `meta`, `command`, `super`                                                                                  |
| Common          | `enter`, `backspace`, `delete`, `tab`, `esc`, `space`                                                                               |
| Arrow keys      | `left`, `right`, `up`, `down`                                                                                                       |
| Page navigation | `pageup`, `pagedown`, `home`, `end`                                                                                                 |
| Function keys   | `f1` through `f12`                                                                                                                  |
| Punctuation     | `minus`, `plus`, `equal`, `comma`, `period`, `slash`, `backslash`, `semicolon`, `quote`, `backquote`, `bracketleft`, `bracketright` |

Examples: `ctrl+c`, `ctrl+shift+t`, `alt+tab`, `down down down enter`

Punctuation keys use their **word forms** — `key_press` with `slash`, not `/`. To enter punctuation as text, use `type` instead.

## Coordinate System

n2 outputs **normalized coordinates in a 1000×1000 space**, origin top-left. Scale them to your desktop's real pixel resolution before executing. The model is never told the resolution.

```python theme={null}
from yutori.navigator import denormalize_coordinates

pixel_x, pixel_y = denormalize_coordinates(
    coordinates=[412, 380],
    width=screen_width,
    height=screen_height,
)
```

See [`yutori.navigator.coordinates`](https://github.com/yutori-ai/yutori-sdk-python/blob/main/yutori/navigator/coordinates.py) for the inverse `normalize_coordinates` function.

## Screenshots

Capture the **whole screen**, including window borders, title bars, the taskbar, and any system UI. n2 clicks on all of it, so cropping any of it away removes targets the model needs to see.

n2 reports [normalized coordinates](#coordinate-system). **1920×1080**, **1280×720**, and **1280×800** are all in regular use. Grounding may degrade at extreme aspect ratios.

Requests are capped at **10 MB**, returning HTTP `413` (`request_too_large`) above that. Full-screen captures are large and a trajectory carries several, so compress them: **WebP at \~80% quality**, not PNG. A handful of uncompressed full-screen PNGs will exceed the cap on their own.

The [Python SDK](https://github.com/yutori-ai/yutori-sdk-python) handles encoding:

```bash theme={null}
pip install yutori
```

```python theme={null}
from yutori.navigator import screenshot_to_data_url

image_url = screenshot_to_data_url(screenshot_bytes)
```

See [`yutori.navigator.images`](https://github.com/yutori-ai/yutori-sdk-python/blob/main/yutori/navigator/images.py) for resolution and quality options.

## Request Fields

Supported alongside `model` and `messages`:

| Field                                                                  | Notes                                                                                                                                             |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `max_completion_tokens`                                                | Default `16384`. Shared by the reasoning trace and the tool call, so a low value truncates the call. `max_tokens` is rejected — use this instead. |
| `temperature`                                                          | Default `0.6`. See [Sampling](#sampling).                                                                                                         |
| `reasoning_effort`                                                     | `none` \| `low` \| `medium` \| `xhigh`. Default `medium`. See [Reasoning](#reasoning).                                                            |
| `top_p`, `repetition_penalty`, `presence_penalty`, `frequency_penalty` | Standard sampling parameters. The first three have server defaults — see [Sampling](#sampling).                                                   |
| `tool_set`                                                             | See [Tool Sets](#tool-sets). Defaults to the current desktop set.                                                                                 |
| `disable_tools`                                                        | Drop tools from the selected set. `bash`, `read`, `write`, `edit` only. See [Changing the tool set](#changing-the-tool-set).                      |
| `tools`                                                                | Serve your own tool definitions alongside the set. See [Changing the tool set](#changing-the-tool-set).                                           |
| `prev_request_id`                                                      | Echo the previous call's `request_id` to group a trajectory into one conversation for usage reporting.                                            |
| `parallel_tool_calls`                                                  | Accepted but ignored — pinned to `true` server-side.                                                                                              |

### Sampling

Applied whenever you send nothing for a field. An explicit `null` counts as sending nothing:

| Field                | Server default |
| -------------------- | -------------- |
| `temperature`        | `0.6`          |
| `top_p`              | `0.95`         |
| `presence_penalty`   | `0.0`          |
| `repetition_penalty` | `1.0`          |
| `top_k`              | `20`           |
| `min_p`              | `0.0`          |

`temperature: 0` is not recommended for a reasoning model.

**Rejected.** n2 returns an error for these rather than silently ignoring them:

| Field                  | Why                                                           |
| ---------------------- | ------------------------------------------------------------- |
| `json_schema`          | Structured output is not supported.                           |
| `response_format`      | Guided decoding is managed server-side.                       |
| `chat_template_kwargs` | Managed server-side. Use `reasoning_effort` instead.          |
| `stream`               | Streaming is not supported. Must be absent or `false`.        |
| `tool_choice`          | Must be absent or `"auto"`.                                   |
| `tool_set`             | Must name a registered tool set. See [Tool Sets](#tool-sets). |

Each returns HTTP `400` with an OpenAI-shaped error body:

```json theme={null}
{
  "error": {
    "message": "response_format is not supported by n2",
    "type": "invalid_request_error",
    "code": "invalid_parameter"
  }
}
```

`max_tokens` is also rejected, but as a schema violation (HTTP `422`) rather than a `400`.

<Note>
  A rejected field sent as an **empty** value is dropped instead of rejected — `"tools": []` and
  `"response_format": {}` are accepted as no-ops. Only a non-empty value returns a `400`, so an
  empty value is not a reliable way to probe whether a field is supported.
</Note>

`prompt_cache_key` is accepted and then dropped before the request reaches the model. Any field
outside the ones documented here is stripped rather than rejected — an unrecognized parameter will
not error, and will not take effect.

`parallel_tool_calls` is pinned to `true` server-side, so a value you send for it is ignored. In practice n2 answers with a single `computer_batch` call, which is how it chains actions against one screenshot — but it may return more than one call in a turn, and a loop has to handle that. The SDK's `N2ComputerAgent` executes the first call and answers the rest with an error result, because they were planned against a screenshot that the first call has already changed. Whatever the policy, every call needs its own tool result.

## Prompting

A system message you supply adds to n2's defaults rather than replacing them, so a custom system prompt cannot override them — and extra behavioral instructions generally degrade results.

Put task-specific instructions in the **first user message**, after the task description.

## Message History

Send the full conversation. Do not drop messages — the model needs its own action history to avoid repeating work.

n2 retains screenshots at the **message** level: the **last 2 image-bearing messages** keep their images, capped at **6 images within each**. Older images are dropped server-side with no marker; a short placeholder is inserted only when dropping them would leave a message with no content.

In practice it means n2 sees the current screen and the previous one, which is why the previous screenshot should be a real observation rather than a stale duplicate. Send the full history regardless — the text of every earlier turn is kept, and it is what the model uses to know what it has already tried.

A turn looks like this — assistant message with the batch call, then one `tool` result carrying the post-batch screenshot:

```python theme={null}
response = client.chat.completions.create(
    model="n2",
    messages=[
        # Task plus the first screenshot of the desktop
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "Open the Files app and rename report.csv to q3.csv."},
                {"type": "image_url", "image_url": {"url": f"data:image/webp;base64,{screenshot_1}"}},
            ],
        },
        # The model's batch of actions
        {
            "role": "assistant",
            "content": "I can see the desktop. I'll open the Files app from the dock.",
            "tool_calls": [
                {
                    "id": "chatcmpl-tool-123",
                    "type": "function",
                    "function": {
                        "name": "computer_batch",
                        "arguments": '{"actions": [{"name": "double_click", "arguments": {"coordinates": [62, 470]}}, {"name": "wait", "arguments": {"duration": 1}}]}',
                    },
                }
            ],
        },
        # One tool result for the whole batch, with the screenshot taken after the last action
        {
            "role": "tool",
            "tool_call_id": "chatcmpl-tool-123",
            "content": [
                {"type": "text", "text": "Executed 2 of 2 actions."},
                {"type": "image_url", "image_url": {"url": f"data:image/webp;base64,{screenshot_2}"}},
            ],
        },
    ],
)
```

If a batch stopped early, say so in the tool result text — which action failed, and why. For `edit`, `read`, `write`, and `bash`, return the tool's result text or the command output the same way.

When n2 is done with the task it returns `content` text and no `tool_calls` — the signal to end the loop. You can append a new user message to continue from the same history.

## Context and Compaction

The served context window is **128,000 tokens**. Nothing on this endpoint compacts a conversation for you: the server drops older *images* (see [Message History](#message-history)) but keeps the text of every turn, so a long trajectory grows until a request no longer fits.

n2 is trained for long-horizon work with a compaction step, and reproducing it is what keeps a long run in distribution:

| Property           | Value                                                                                                                 |
| ------------------ | --------------------------------------------------------------------------------------------------------------------- |
| Trained context    | 64,000 tokens                                                                                                         |
| Compaction trigger | 53,760 prompt tokens — 64,000 minus 10,240 of headroom                                                                |
| Retained           | The original user request, a model-written checkpoint standing in for older turns, and the most recent turns verbatim |

Once a response reports more than 53,760 prompt tokens, ask the model to write a checkpoint of the conversation so far, then replace the compacted turns with it and continue. A run that instead grows toward the full 128,000-token window is operating past the context the model was trained at.

The [Python SDK](https://github.com/yutori-ai/yutori-sdk-python) does this by default — `N2ComputerAgent` attaches an `N2InlineCompactor` unless you pass `compactor=None`. The compaction prompt and the reference implementation live in [`yutori/navigator/n2_compaction.py`](https://github.com/yutori-ai/yutori-sdk-python/blob/main/yutori/navigator/n2_compaction.py).

## Reasoning

Reasoning is **on by default**, at **medium** effort. Set `reasoning_effort` to change it:

| Value    | Effect                                        |
| -------- | --------------------------------------------- |
| `low`    | Shorter traces, fewer output tokens           |
| `medium` | Default                                       |
| `xhigh`  | Longest traces, best on hard multi-step tasks |
| `none`   | Turns reasoning off entirely                  |

```python theme={null}
response = client.chat.completions.create(
    model="n2",
    messages=[...],
    extra_body={"reasoning_effort": "xhigh"},
)
```

OpenAI's `high` and `minimal` are accepted as aliases for `xhigh` and `low`. Any other value falls back to `medium`. `chat_template_kwargs` is not accepted — `reasoning_effort` is the supported way to set this.

The trace comes back on the assistant message as `reasoning_content`, alongside `content` and `tool_calls`. **Echo it back on the next call** — append the assistant message unchanged, `reasoning_content` included — so the model keeps its own reasoning across turns:

```python theme={null}
messages.append(response.choices[0].message.model_dump(exclude_none=True))
```

Reasoning tokens bill at the output rate (see [Pricing](/pricing)) and count against `max_completion_tokens`.

## Tool Sets

Published dated tool sets are immutable: an id always denotes the same tool definitions, and a new surface gets a new date.

| Tool set                      | Contents                                                                                                                                   |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `computer_use_tools-20260830` | `computer_batch`, `edit`, `read`, `write`, `bash`                                                                                          |
| `computer_use_tools-20260825` | `computer_batch`, `edit`, `read`, `write`, `bash`                                                                                          |
| `computer_use_tools-20260822` | `computer_batch`, `screenshot`, `bash`                                                                                                     |
| `computer_use_tools-20260815` | `computer_batch`, `screenshot`, `bash` — the batch lacks `screenshot`, `mouse_down`, `mouse_up`, and `hold_key`                            |
| `computer_use_tools-20260812` | `computer_batch`, `screenshot`, `bash` — no click/scroll modifiers; the batch lacks `screenshot`, `mouse_down`, `mouse_up`, and `hold_key` |

The default is the current latest set (`computer_use_tools-20260830`), and you can select a set explicitly with the `tool_set` field:

```python theme={null}
response = client.chat.completions.create(
    model="n2",
    messages=[...],
    extra_body={
        "tool_set": "computer_use_tools-20260830",
    },
)
```

### Changing the tool set

The set is a fixed, immutable list, but you can drop tools from it and add your own. The two
compose: `disable_tools` is applied first, then your definitions in `tools` are appended
after the set's.

#### Disabling a tool

Drop tools you cannot support. A host with no shell or no filesystem should say so rather than
let the model call a tool that will always fail:

```python theme={null}
response = client.chat.completions.create(
    model="n2",
    messages=[...],
    extra_body={
        "tool_set": "computer_use_tools-20260830",
        "disable_tools": ["bash", "write", "edit"],
    },
)
```

Only `bash`, `read`, `write`, and `edit` can be disabled. `computer_batch` cannot — it is the GUI
surface, and without it there is no computer agent.

Unknown names are rejected rather than ignored, so a typo (`"shell"`, `"bash "`) fails the call
instead of quietly serving the full set.

#### Adding custom tools

`tools` serves your own definitions alongside the set, in the standard OpenAI tool shape. Use it
to expose capabilities the predefined tool sets do not cover:

```python theme={null}
response = client.chat.completions.create(
    model="n2",
    messages=[...],
    extra_body={
        "tools": [
            {
                "type": "function",
                "function": {
                    "name": "lookup_order",
                    "description": "Fetch an order by id.",
                    "parameters": {
                        "type": "object",
                        "properties": {"order_id": {"type": "string"}},
                        "required": ["order_id"],
                    },
                },
            }
        ],
    },
)
```

A tool from the `tool_set` can also be redefined by first disabling the tool name and then
providing your own definition in `tools`. Adding a definition in `tools` with a conflicting
name to an existing tool in the `tool_set` will return a `400` if the original tool is not
first disabled:

```json theme={null}
{
  "disable_tools": ["read"],
  "tools": [{"type": "function", "function": {"name": "read", ...}}]
}
```

`computer_batch` cannot be redefined at all, for the same reason it cannot be disabled. Duplicate
names *within* `tools` are rejected too.

## Agent Loop

One turn: send the task and the newest screenshot, receive a tool call (usually one; see [Request Fields](#request-fields) for the case of several), execute it against the desktop, and send back one tool result per call — a screenshot for `computer_batch`, text for `bash`, `read`, `write`, and `edit`. Repeat until the model returns text with no `tool_calls`.

A few tips:

1. **Unpack the batch.** Read `actions` and turn each `{"name", "arguments"}` item into one call against your input driver.
2. **Denormalize coordinates** against the screen resolution, per action, before executing it.
3. **Stop at the first error** and report which action failed. Do not run the rest of the batch.
4. **Return one tool result per tool call**, not one per action, with exactly one screenshot taken after the last executed action.
5. **Keep the payload under 10 MB** by compressing screenshots.
6. **Compact long runs** rather than growing to the full window — see [Context and Compaction](#context-and-compaction).
7. **End a capped run with a summary.** If you cap the number of turns, spend the last one on a user message asking the model to stop and summarize its progress; a run cut off mid-trajectory otherwise returns no final answer.

Some helpers in the [Python SDK](https://github.com/yutori-ai/yutori-sdk-python):

| Helper                                          | Use                                                                                                    |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `yutori.navigator.screenshot_to_data_url`       | Encode a screenshot as a compressed data URL                                                           |
| `yutori.navigator.denormalize_coordinates`      | Map the 1000×1000 space to screen pixels                                                               |
| `yutori.navigator.map_key_to_playwright`        | Translate n2 key names for drivers that expect Playwright names                                        |
| `yutori.navigator.estimate_messages_size_bytes` | Check a payload against the 10 MB cap before sending                                                   |
| `yutori.navigator.N2ComputerAgent`              | Run the whole loop — batching, coordinates, results, and compaction — against your own desktop adapter |

The SDK exports `TOOL_SET_COMPUTER_USE_LATEST` so you can pin the tool set without hardcoding a date string. It tracks the SDK release rather than the server default, so check the constant's value in the version you install. See [Tool Sets](#tool-sets).

### Reference Implementations

`N2ComputerAgent` owns the loop — turns, message history, batching, coordinate mapping, screenshot results, compaction. You supply an adapter that implements the system-dependent pieces: the single-action GUI primitives, `run_bash_command`, and the `read`/`write`/`edit` file operations. The SDK documents that surface and ships complete adapters to copy from:

| Reference                                                                                                                                             | What it covers                                                                                                       |
| ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| [Navigator n2 overview](https://github.com/yutori-ai/yutori-sdk-python#navigator-n2-computer-use)                                                     | The shortest complete `N2ComputerAgent` program, and the split between what you implement and what the SDK provides  |
| [The adapter contract (`N2Computer`)](https://github.com/yutori-ai/yutori-sdk-python/blob/main/api.md#the-adapter-contract-n2computer)                | Every method the loop calls on your adapter, with the result format each one must return                             |
| [`examples/navigator_n2/direct_x11_adapter.py`](https://github.com/yutori-ai/yutori-sdk-python/blob/main/examples/navigator_n2/direct_x11_adapter.py) | Start here when the adapter runs on the desktop host itself and reaches the display, shell, and filesystem directly  |
| [`examples/navigator_n2/cua_adapter.py`](https://github.com/yutori-ai/yutori-sdk-python/blob/main/examples/navigator_n2/cua_adapter.py)               | Start here when those operations cross an API boundary, to local sandbox software or a remote service                |
| [`yutori/navigator/macos/computer.py`](https://github.com/yutori-ai/yutori-sdk-python/blob/main/yutori/navigator/macos/computer.py)                   | The native macOS adapter that ships in the SDK, the one [Yutori MCP](https://github.com/yutori-ai/yutori-mcp) drives |

Both example adapters render `bash` and file-tool results in the exact format n2 expects — exit-code headers, `cat -n` line numbering, truncation markers, and tool errors returned as plain `ERROR: ...` results rather than raised failures — so they double as the reference for those output contracts. For a compact, hosted-VM variant of the same loop, see [Building agents with n2](/reference/n2-daytona).
