Skip to documentation
Documentation

Turn harnesses

Choose where your agent's turn loop runs: on Keiki, or on a host you control.

Every agent runs a loop. It sends the conversation to a model, calls tools, and repeats until it has an answer. The turn harness is where that loop runs. Keiki hosts it by default. You can also run it on your own machine with Hermes, or inside your own app with the external harness.

You choose per agent. Moving one agent to your own host leaves the rest on Keiki. Each connection belongs to the agent it was set up on, so two agents can point at different hosts.

Hermes cannot receive steered or Live Test turns, so steer a Hermes agent from one of its real channels instead. The external harness supports steered and Live Test turns; Keiki marks them held back so the app's answer is captured without reaching the contact.

Keiki-hosted (default)

Keiki runs the loop for you. There is nothing to set up.

TypeScript
const kit = AgentKit.create({
  // ...
  harness: { type: 'flue' },
})

Leave harness out and you get the same thing. If you register an SDK webhook with kit.serve(), that webhook still takes precedence.

Keiki VM (Maritime)

Keiki gives the agent its own micro-VM on Maritime and runs the loop there. Everything else stays on Keiki: channels, history, prompt, tools, steering, guards, traces, billing and delivery. Steered and silent turns and Live Test work exactly as they do on the Keiki-hosted harness.

It is a platform setting, not a choice on the agent: with ENABLE_MARITIME=true, every agent created from then on (dashboard, template, deployment or developer API) gets its own VM. Existing agents keep their harness, and SDK agents, whose loop runs on the developer's server, are not affected. Keiki uploads its runner, creates the VM and shows its state on the agent page: provisioning, deploying, running or asleep. The VM sleeps between turns and wakes on the next message in a few seconds. If a build fails or the VM is lost, the page shows Maritime's reason and a Redeploy VM button, which replaces the VM with a fresh one built from the current runner. Switching the agent to another harness, or deleting it, deletes its VM.

The VM holds no Keiki or model credentials. Keiki drives each turn over short long-polls (each under Maritime's 30-second request limit), which also keep the VM awake while the turn runs. The loop's model calls, tool calls and trace spans come back over those polls and run on Keiki, so the VM never calls Keiki or OpenRouter itself. The only secret on the VM is a token that opens its own turn routes.

The platform needs ENABLE_MARITIME, MARITIME_API_KEY and the runner bundle built with bun run build:maritime-runner (part of bun run build).

Run the loop yourself

You run the loop on a machine you control. Keiki still exposes the agent's tools and records what happens in the same turn trace.

Keiki sends each inbound turn with a stable session ID per contact, and your host keeps the conversation's context. Keiki still stores the messages for history and audit.

To set it up, create or edit an agent, select Hermes, then copy the prompt shown there and hand it to a coding agent on the host machine. It runs:

Shell
curl -fsSL https://<your-keiki-host>/api/v1/harness/install.sh \
  | KEIKI_API_KEY=sk_live_… bash

The installer installs Hermes, turns on its OpenAI-compatible API server, registers Keiki's MCP server and observability plugin, stores the local Hermes key as an encrypted secret, and points the agent at the host. You can run it again safely. It takes its options through CONNECTIVITY.

Set HERMES_BIN if the Hermes binary is not on PATH. If you run the installer inside the gateway, set HERMES_INSIDE_GATEWAY=1 so it skips the restart and lets the gateway pick up the new settings when it next starts.

External (your app)

For an application that runs its own model loop, choose the external harness and set dispatchUrl in push mode, or use relay mode. Keiki sends your app a signed asynchronous turn request; your app responds 202 as soon as it accepts the work, then calls the turn-scoped /api/v1/turns/:turnId/messages, /api/v1/turns/:turnId/stream, /api/v1/turns/:turnId/typing, and /api/v1/turns/:turnId/complete endpoints as needed. Copy the signing secret returned once by POST /api/v1/developer/agents/:agentId/harness/signing-secret into your app's secret manager. Keiki still handles channel delivery, exposes the agent's tools, and records the turn trace. See the external harness contract for the full request and response details.

With the SDK, configure the external harness like this:

TypeScript
harness: {
  type: 'external',
  connectivity: 'push',
  dispatchUrl: 'https://agent.example.com/keiki/turn',
  timeoutMs: '5 minutes',
}

For relay mode, set connectivity: 'relay' and omit dispatchUrl. For an outside agent that polls when available, set connectivity: 'inbox' and omit dispatchUrl; the agent uses a harness API key to claim and complete queued turns. A poller must be running for test and steer turns.

The Eve integration mounts POST /keiki/v1/dispatch; see the Eve extension guide for setup and the full route URL.

The Flue integration mounts POST /channels/keiki/dispatch; see the Flue integration guide for Node and Cloudflare setup.

For AI SDK 7 apps, see the AI SDK integration guide, including its prepareStep helper for pulling durable arrivals between model steps.

External turns follow the agent's midTurnStrategy. With append, eligible push-mode turns receive signed arrival requests in the active app run; the app reports consumed sequence numbers at completion, and any unconsumed or rejected batch is returned to the queue. Relay mode does not take arrivals, so those messages are answered by a later turn. With interrupt, Keiki cancels the active turn and starts a fresh one with the correction. With queue, the message waits for the active turn to finish and is answered by a later turn.

Hermes connectivity

ModeCONNECTIVITYWhat it does
RelayrelayDefault. A small daemon on your host polls Keiki for turns, so you do not need to open a port.
Public URLpublic-urlYou already have an HTTPS address that forwards to the host. Set HERMES_PUBLIC_URL to it.
Cloudflare quick tunnelcloudflaredUses the agent-scoped supervisor served at /api/v1/harness/tunnel.sh.
ngrokngrokUses the same agent-scoped supervisor. Set NGROK_AUTHTOKEN or configure ngrok first.
Shell
curl -fsSL https://<your-keiki-host>/api/v1/harness/install.sh \
  | KEIKI_API_KEY=sk_live_… CONNECTIVITY=relay bash
Shell
curl -fsSL https://<your-keiki-host>/api/v1/harness/install.sh \
  | KEIKI_API_KEY=sk_live_… CONNECTIVITY=public-url \
    HERMES_PUBLIC_URL=https://hermes.example.com bash

Traces and billing

Every turn gets a Keiki trace, and the tool calls from your host show up in the same trace as the rest. Keiki marks which activity came from your host.

You pay for message delivery through the usual counters. Model tokens your own provider serves are between you and that provider.

Security

  • Turns travel with short-lived credentials tied to the key, the conversation, the chat and the trace.
  • Hermes keys and external signing secrets are stored encrypted, never in plain config.
  • Keiki will not push turns to private networks, redirects, URLs with credentials, or plain HTTP unless a self-hosted deployment opts in.