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

# How to Add a Custom Agent

Custom agents let you bring your own agent process to an Upstash Box. The box still provides the same sandbox, filesystem, shell, git, logs, streaming, and console experience, but your code decides how to call the model and how to produce output.

Use a custom agent when the built-in Claude Code, Codex, OpenCode, or Cursor agents do not fit your workflow.

## Create a Custom Agent Box

Create the box with `agent.harness: Agent.Custom` and provide a `customHarness` command. The command runs inside the box for every `box.agent.run()` or `box.agent.stream()` call.

```ts theme={"system"}
import { Agent, Box } from "@upstash/box"

const box = await Box.create({
  apiKey: process.env.UPSTASH_BOX_API_KEY!,
  runtime: "node",
  agent: {
    harness: Agent.Custom,
    model: "claude-haiku-4-5-20251001",
    customHarness: {
      command: "node",
      args: ["/workspace/home/custom-anthropic-agent.mjs"],
      protocol: "box-sse-v1",
    },
  },
  env: {
    ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!,
  },
})
```

## Agent Contract

For each run, Box executes your command and appends these arguments:

```bash theme={"system"}
-p "<prompt>" --model "<model>" --stream --session "<session-id>"
```

`--session` is only included when a prior session exists.

Your process must write Server-Sent Events to `stdout` using the `box-sse-v1` protocol:

```txt theme={"system"}
event: text
data: {"text":"Hello"}

event: done
data: {"output":"Hello","input_tokens":10,"output_tokens":4,"total_cost_usd":0.0001,"session_id":"session-1"}
```

Supported event names:

| Event         | Description                       |
| ------------- | --------------------------------- |
| `text`        | Adds text to the visible response |
| `thinking`    | Emits reasoning/thinking text     |
| `tool`        | Shows a tool call in logs         |
| `tool_result` | Shows a tool result in logs       |
| `done`        | Finishes the run successfully     |
| `error`       | Finishes the run with an error    |

## SDK Helper

If your custom agent process can import `@upstash/box`, use `runCustomHarness()` to parse Box arguments and emit the protocol events:

```ts theme={"system"}
import { runCustomHarness } from "@upstash/box"

await runCustomHarness(async ({ prompt, model, sessionId }, emit) => {
  emit.tool({ name: "my_agent", input: { model } })

  const output = `received: ${prompt}`
  emit.text(output)

  return {
    output,
    inputTokens: prompt.split(/\s+/).length,
    outputTokens: output.split(/\s+/).length,
    sessionId,
  }
})
```

## Minimal Anthropic Agent

This custom agent calls Anthropic directly and streams text back through Box.

```js title="custom-anthropic-agent.mjs" theme={"system"}
const args = process.argv.slice(2)

function readArg(name, fallback = "") {
  const index = args.indexOf(name)
  return index >= 0 ? args[index + 1] ?? fallback : fallback
}

function emit(event, data) {
  process.stdout.write(`event: ${event}\n`)
  process.stdout.write(`data: ${JSON.stringify(data)}\n\n`)
}

const prompt = readArg("-p")
const model = readArg("--model", "claude-haiku-4-5-20251001")
const sessionId = readArg("--session") || crypto.randomUUID()

try {
  emit("tool", {
    name: "anthropic_messages",
    input: { model },
  })

  const response = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-api-key": process.env.ANTHROPIC_API_KEY,
      "anthropic-version": "2023-06-01",
    },
    body: JSON.stringify({
      model,
      max_tokens: 1024,
      messages: [{ role: "user", content: prompt }],
    }),
  })

  const body = await response.json()

  if (!response.ok) {
    throw new Error(body.error?.message ?? `Anthropic request failed: ${response.status}`)
  }

  const output = body.content
    ?.filter((part) => part.type === "text")
    .map((part) => part.text)
    .join("") ?? ""

  emit("text", { text: output })
  emit("done", {
    output,
    input_tokens: body.usage?.input_tokens ?? 0,
    output_tokens: body.usage?.output_tokens ?? 0,
    session_id: sessionId,
  })
} catch (error) {
  emit("error", {
    error: error instanceof Error ? error.message : String(error),
    session_id: sessionId,
  })
  process.exitCode = 1
}
```

Write the custom agent into the box before the first run:

```ts theme={"system"}
await box.files.write({
  path: "custom-anthropic-agent.mjs",
  content: agentSource,
})

const result = await box.agent.run({
  prompt: "Say hello from my custom agent",
})

console.log(result.result)
```

## Update an Existing Custom Agent

You can update the custom agent command for an existing custom box:

```ts theme={"system"}
await box.configureCustomHarness({
  command: "node",
  args: ["/workspace/home/another-agent.mjs"],
  protocol: "box-sse-v1",
})
```

This only works for boxes created with `agent.harness: Agent.Custom`.

## Custom Harness Examples

Ready-to-run examples for popular open-source agents:

* [Pi](https://github.com/upstash/box/blob/main/packages/sdk/examples/custom-pi-agent.ts) — multi-provider coding agent
* [Gemini](https://github.com/upstash/box/blob/main/packages/sdk/examples/custom-gemini-agent.ts) — Google Gemini via `@google/genai`
* [Aider](https://github.com/upstash/box/blob/main/packages/sdk/examples/custom-aider-agent.ts) — git-aware code editor
* [Goose](https://github.com/upstash/box/blob/main/packages/sdk/examples/custom-goose-agent.ts) — autonomous coding agent

## Notes

* Custom agents do not use managed provider keys.
* Pass secrets through `env` on `Box.create()` or configure them inside the box.
* The command must be a binary name from `PATH` or an absolute path under `/workspace/home/` or `/home/boxuser/`.
* The command runs as `boxuser` inside the existing box sandbox.
