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

# Agent Worker API

> HTTP API for the agent-worker Node.js sidecar that executes LLM inference.

<Note>
  The agent-worker API is **internal** — runners call it, not user applications. The endpoints below apply to inbound server mode. In outbound client mode, the worker binds no port and calls the runner instead.
</Note>

## Transport Modes

Inbound server mode remains the default. When both `ORCA_RUNNER_URL` and `ORCA_WORKER_TOKEN` are set, the same process switches to a session-scoped outbound client. `ORCA_SESSION_ID` is then required.

The outbound client uses three runner endpoints, all authenticated with `Authorization: Bearer <ORCA_WORKER_TOKEN>`:

| Method and path                      | Purpose                                                                                                         |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `GET /worker/sessions/{id}/next-run` | Long-poll for `run`, `export_state`, or `shutdown` commands. `204` means no work.                               |
| `POST /worker/sessions/{id}/events`  | Upload the active run's NDJSON event stream. `X-Orca-Run-Id` identifies the run; closing the upload cancels it. |
| `POST /worker/sessions/{id}/state`   | Upload an opaque runtime state bundle with `Content-Type` and `X-Runtime-Session-Id`.                           |

The worker sends `X-Orca-Worker-Protocol: 1` when polling. It writes state to `ORCA_STATE_DIR` after each run and on `export_state`, uploads a durable copy to the runner, and restores the disk copy before its first poll. These state operations are best-effort so a corrupt or unavailable bundle does not prevent the worker from calling home.

## Base URL

In inbound server mode, sidecars run on configurable ports (default `7070`). In a typical setup:

| Sidecar           | Port |
| ----------------- | ---- |
| `MODE=claude`     | 7071 |
| `MODE=codex`      | 7072 |
| `MODE=general`    | 7073 |
| `MODE=all` (poly) | 7070 |

***

## Health Check

### GET /health

Returns sidecar health, the active runtime mode, and the identity and
self-reported footprint of this worker process. `/healthz` returns the same
body.

```bash theme={null}
curl http://sidecar:7071/health
```

```json theme={null}
{
  "ok": true,
  "runtime": "claude",
  "instanceId": "worker-a1b2c3d4",
  "pid": 42,
  "startedAt": "2026-04-25T10:00:00.000Z",
  "uptimeSeconds": 3600,
  "inflight": 1,
  "nodeVersion": "v24.9.0",
  "memory": {
    "rssBytes": 80000000,
    "heapUsedBytes": 30000000,
    "heapTotalBytes": 50000000,
    "externalBytes": 2000000
  },
  "cpu": { "userMicros": 1200000, "systemMicros": 300000 }
}
```

The response also includes the `X-Orca-Worker-Instance` header. The ID is
generated once per process and changes when the process restarts.

`POST /run` responses include the same `X-Orca-Worker-Instance` header and an
`X-Orca-Worker-Inflight` header containing the process's in-flight run count at
the start of the response. The runner uses the identity header for passive
instance observation, while it obtains memory and CPU details from `/health`
probes.

***

## Run Execution

### POST /run

Executes a task against the configured LLM provider. Returns a streaming NDJSON response of `RunEvent` objects.

**Request body:**

```json theme={null}
{
  "sessionId": "sess-a1b2c3d4-e5f6g7h8",
  "profile": {
    "name": "researcher",
    "runtime": "general",
    "systemPrompt": "You are a research assistant.",
    "tools": ["web_search", "time_now"],
    "model": "anthropic:claude-sonnet-4-6"
  },
  "subtask": {
    "title": "Research task",
    "prompt": "What are the top AI agent frameworks?",
    "files": []
  },
  "sessionMcpUrl": "http://runner:7070/runner/sessions/sess-.../mcp",
  "skills": [],
  "mcpServers": [
    {
      "name": "my-api",
      "transport": "http",
      "url": "https://api.example.com/mcp",
      "headers": { "Authorization": "Bearer resolved-secret" }
    }
  ]
}
```

**Request fields:**

| Field                  | Type      | Required | Description                                                          |
| ---------------------- | --------- | -------- | -------------------------------------------------------------------- |
| `sessionId`            | string    | ✓        | Session identifier                                                   |
| `profile.name`         | string    | ✓        | Profile name                                                         |
| `profile.runtime`      | string    | ✓        | `claude`, `codex`, or `general`                                      |
| `profile.systemPrompt` | string    | —        | System instructions                                                  |
| `profile.tools`        | string\[] | —        | Tool allowlist                                                       |
| `profile.model`        | string    | —        | LLM model identifier                                                 |
| `subtask.title`        | string    | ✓        | Short label                                                          |
| `subtask.prompt`       | string    | ✓        | Task description                                                     |
| `subtask.files`        | string\[] | —        | File references                                                      |
| `sessionMcpUrl`        | string    | ✓        | Runner's MCP endpoint for this session                               |
| `skills`               | object\[] | —        | Pre-resolved active skill bodies and resource manifests for this run |
| `mcpServers`           | object\[] | —        | Additional external MCP servers                                      |

**Response:** `application/x-ndjson` (streaming)

If run dispatch fails after the streaming response has started, the sidecar
writes a terminal `error` event and closes the stream; the process remains
available for subsequent runs.

Each line is a `RunEvent` JSON object:

```
{"type":"progress","message":"Starting run...","ts":"..."}
{"type":"session","runtimeSessionId":"sdk-session-or-thread-id","ts":"..."}
{"type":"assistant","message":"I'll look this up...","ts":"..."}
{"type":"tool_call","toolCallId":"call_abc","toolName":"web_search","input":{"query":"..."},"ts":"..."}
{"type":"tool_result","toolCallId":"call_abc","toolName":"web_search","output":{"results":[...]},"isError":false,"ts":"..."}
{"type":"assistant","message":"Based on my research...","ts":"..."}
{"type":"result","message":"Final answer...","ts":"..."}
{"type":"usage","usage":{"inputTokens":1240,"outputTokens":380},"ts":"..."}
```

***

## RunEvent Schema

```typescript theme={null}
interface RunEvent {
  type: "progress" | "assistant" | "tool_call" | "tool_result" | "usage" | "session" | "result" | "error";
  message?: string;           // for progress, assistant, result, error
  runtimeSessionId?: string;  // for session; Claude session_id or Codex thread_id
  toolCallId?: string;        // for tool_call, tool_result
  toolName?: string;          // for tool_call, tool_result
  input?: unknown;            // tool input (tool_call)
  output?: unknown;           // tool output (tool_result)
  isError?: boolean;          // error flag (tool_result)
  usage?: {
    inputTokens: number;
    outputTokens: number;
    cacheReadTokens?: number;
    cacheCreateTokens?: number;
  };
  ts: string;                 // ISO 8601
}
```

***

## Session State

### GET /state/:sessionId

Exports an opaque state bundle for one sidecar session. Runners use this internal route when they need to persist runtime-local conversation state outside the sidecar process.

```bash theme={null}
curl http://sidecar:7071/state/sess-a1b2c3d4-e5f6g7h8
```

When `MODE=all`, pass the runtime explicitly:

```bash theme={null}
curl 'http://sidecar:7070/state/sess-a1b2c3d4-e5f6g7h8?runtime=claude'
```

**Response headers:**

| Header                 | Description                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| `Content-Type`         | `application/json` for `general`; `application/gzip` for `claude` and `codex`              |
| `X-Runtime-Session-Id` | Runtime-native conversation handle, such as a Claude SDK `session_id` or Codex `thread_id` |

Returns `400` for malformed percent-encoding in `sessionId` or, in `MODE=all`, a missing or unknown `runtime` query parameter. Returns `404` when the sidecar has no exportable state for the session.

Codex exports include only rollout files whose filename contains the exact requested thread ID at ID-token boundaries; a thread ID like `thread-1` does not also export `thread-12`.

### POST /state/:sessionId

Hydrates a sidecar session from a previously exported state bundle. The request body is treated as the exported body for that runtime: JSON for `general`, gzip state bundles for `claude` and `codex`.

```bash theme={null}
curl -X POST http://sidecar:7071/state/sess-a1b2c3d4-e5f6g7h8 \
  -H 'Content-Type: application/gzip' \
  -H 'X-Runtime-Session-Id: sdk-session-or-thread-id' \
  --data-binary @state-bundle.tgz
```

When `MODE=all`, pass the runtime query parameter. On success, the sidecar returns `204 No Content`; the next `POST /run` for that session can resume from the hydrated state.

Client-caused import failures, such as invalid JSON or corrupt gzip/tar data, return `400` and do not seed `X-Runtime-Session-Id` for the session. Unexpected dispatcher failures return `500`; the sidecar contains the error and continues serving subsequent requests.

***

## Mode Configuration

The sidecar's behavior is controlled by the `MODE` environment variable:

<Tabs>
  <Tab title="claude">
    Uses `@anthropic-ai/claude-agent-sdk`.

    ```bash theme={null}
    MODE=claude \
    PORT=7071 \
    ANTHROPIC_API_KEY=sk-ant-... \
    node src/server.js
    ```

    **Model format:** `claude-sonnet-4-6`, `claude-haiku-4-5`

    Host-executing Claude built-ins are disabled by default. Use runner MCP tools for filesystem, sandbox, and skill-script execution.
  </Tab>

  <Tab title="codex">
    Uses `@openai/codex-sdk`.

    ```bash theme={null}
    MODE=codex \
    PORT=7072 \
    OPENAI_API_KEY=sk-... \
    node src/server.js
    ```

    **Model format:** `codex-mini`, `o4-mini`

    Codex runs with `sandboxMode: "read-only"` by default. It does not receive host write/exec access unless the break-glass `AGENT_WORKER_ALLOW_HOST_TOOLS=1` is set.
  </Tab>

  <Tab title="general">
    Uses Vercel AI SDK with multi-provider support.

    ```bash theme={null}
    MODE=general \
    PORT=7073 \
    ANTHROPIC_API_KEY=sk-ant-... \
    OPENAI_API_KEY=sk-... \
    node src/server.js
    ```

    **Model format:** `anthropic:claude-sonnet-4-6`, `openai:gpt-5.2`, `google:gemini-2.5-pro`, `groq:llama-3.3-70b-versatile`
  </Tab>

  <Tab title="all (poly)">
    Dispatches to the appropriate mode based on `profile.runtime`.

    ```bash theme={null}
    MODE=all \
    PORT=7070 \
    ANTHROPIC_API_KEY=sk-ant-... \
    OPENAI_API_KEY=sk-... \
    node src/server.js
    ```

    One process handles all runtimes.
  </Tab>
</Tabs>

***

## Environment Variables

| Variable                        | Default           | Description                                                                                                                                                                       |
| ------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PORT`                          | `7070`            | HTTP listen port in inbound server mode                                                                                                                                           |
| `MODE`                          | `general`         | Runtime mode                                                                                                                                                                      |
| `ORCA_RUNNER_URL`               | —                 | Runner base URL. Together with `ORCA_WORKER_TOKEN`, enables outbound client mode.                                                                                                 |
| `ORCA_WORKER_TOKEN`             | —                 | Session-scoped bearer token required for outbound client mode.                                                                                                                    |
| `ORCA_SESSION_ID`               | —                 | Orca session ID; required when outbound client mode is enabled.                                                                                                                   |
| `ORCA_WORKER_PROTOCOL`          | `1`               | Outbound worker protocol major. The worker rejects unsupported values at startup.                                                                                                 |
| `ORCA_STATE_DIR`                | `/var/orca/state` | Sandbox-disk checkpoint directory for outbound runtime state.                                                                                                                     |
| `ANTHROPIC_API_KEY`             | —                 | Required for `claude` and Anthropic-backed `general`                                                                                                                              |
| `OPENAI_API_KEY`                | —                 | Required for `codex` and OpenAI-backed `general`                                                                                                                                  |
| `GOOGLE_GENERATIVE_AI_API_KEY`  | —                 | Google Gemini models in `general` mode                                                                                                                                            |
| `GROQ_API_KEY`                  | —                 | Groq models in `general` mode                                                                                                                                                     |
| `TAVILY_API_KEY`                | —                 | Optional for sidecar-native modes; runner platform web tools need this on the runner                                                                                              |
| `AGENT_WORKER_STEP_COUNT`       | `24`              | Maximum Vercel AI SDK tool-call steps per request in `general` mode                                                                                                               |
| `AGENT_WORKER_CHILD_ENV_ALLOW`  | —                 | Comma-separated extra environment variable names to pass to spawned `claude` or `codex` children beyond the built-in allowlist                                                    |
| `AGENT_WORKER_ALLOW_HOST_TOOLS` | —                 | Break-glass only. Set to `1` to re-enable Claude host built-ins and Codex `danger-full-access`; unset keeps host tools disabled and routes execution through runner sandbox tools |

***

## MCP Bridge (general mode)

In `general` mode, the sidecar connects an MCP client to the runner's session MCP endpoint:

```mermaid theme={null}
flowchart TB
  worker["agent-worker<br/>general mode"]
  sdk["Vercel AI SDK<br/>streamText"]
  mcp["MCP client<br/>@ai-sdk/mcp"]
  endpoint["sessionMcpUrl"]
  list["tools/list"]
  call["tools/call"]

  worker --> sdk
  worker --> mcp
  sdk -->|"uses MCP tools"| endpoint
  mcp -->|"connects to"| endpoint
  endpoint -->|"discovers platform tools"| list
  endpoint -->|"invokes platform tools"| call
```

The sidecar also opens any external MCP servers from the profile and merges them with the runner platform tools. External tool names are prefixed separately from runner tool names.

When a connected MCP catalog is large enough for deferred loading, `general` mode exposes `search_tools` and `call_tool` meta-tools instead of sending every underlying tool schema to the model on each turn. The deferred catalog stores plain JSON schemas only; Vercel AI SDK schema wrappers are unwrapped before search results are returned, and degenerate wrappers with no usable JSON schema fall back to `{}` so replayed history does not contain Symbol-keyed or function-valued schema metadata.

The `general` mode sidecar keeps bounded in-memory session history for replay. If the upstream model stream errors after a turn has started, the sidecar commits the user prompt and any accumulated assistant/tool context once before emitting the terminal `error` event, so the next turn can replay the prior context. Replay also repairs malformed or partial tool-result history before handing messages back to the AI SDK, including defensive JSON sanitization that drops function-valued properties and replaces circular references with `"[circular]"`.

***

## Custom Sidecar

You can implement a custom sidecar that conforms to this API. Requirements:

1. `GET /health` → `{ ok: true, runtime: string }`
2. `POST /run` → accepts the run envelope body, responds with NDJSON `RunEvent` stream
3. Must stream events as newline-delimited JSON
4. Must emit a `result` or `error` event to signal completion
5. For resumable sessions, implement `GET /state/:sessionId` and `POST /state/:sessionId` with opaque state-bundle bytes

This lets you integrate any LLM provider or custom inference engine with Orca's orchestration layer.
