Harnesses

Define custom PTY and headless harness configs in the SDK.

A harness is the thing Relay controls for an agent. The broker only executes serializable HarnessConfig data. SDK "adapters" are helpers that return those configs before a spawn request is sent.

Relay supports two runtime categories:

RuntimeUse forBroker capabilities
ptyTerminal-backed CLIs such as Codex or Claude Codestream, input, resize, snapshot, delivery, release
headlessNon-terminal sessions such as OpenCode app-serverdelivery, release

When runtime is headless, driver defaults to app_server.

Naming

NameMeaning
Harness configConcrete pty or headless JSON the broker can validate and run
Harness adapterSDK helper that returns a harness config
Named harnessSDK-side shortcut in new AgentRelay({ harnesses })
harnessConfigSpawn field carrying the concrete config to the broker

The broker boundary is always explicit config data. Named harnesses are resolved by the SDK before spawn; Relaycast and multi-broker spawns should send harnessConfig directly.

Claude PTY Config

Use a named harness when the command is stable across spawns:

claude-harness.ts
import { AgentRelay } from '@agent-relay/sdk';

const relay = new AgentRelay({
  harnesses: {
    'company-claude': {
      runtime: 'pty',
      command: 'claude',
      args: [
        '--dangerously-skip-permissions',
        '--append-system-prompt',
        'Follow the company review rubric.',
        '{modelArgs}',
        '{args}',
      ],
      modelArgs: ['--model', '{model}'],
      env: {
        CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
      },
    },
  },
});

await relay.spawnAgent({
  name: 'ClaudeReviewer',
  cli: 'company-claude',
  task: 'Review the current diff.',
  model: 'opus',
  args: ['--verbose'],
});

The SDK turns company-claude into an inline harnessConfig for that spawn. The broker does not keep a named harness registry.

Codex Per-Spawn Config

Use direct harnessConfig when setup produces a different config for each spawn, such as creating or resuming a Codex session:

codex-harness.ts
import { AgentRelay, type ResolvedHarnessConfig } from '@agent-relay/sdk';

function codexResume(sessionId: string, cwd: string): ResolvedHarnessConfig {
  return {
    runtime: 'pty',
    command: 'codex',
    args: ['resume', sessionId],
    cwd,
    env: {
      PATH: process.env.PATH ?? '',
      CODEX_HOME: process.env.CODEX_HOME ?? '',
    },
    sessionId,
  };
}

const relay = new AgentRelay();
const cwd = process.cwd();
const task = 'Review the current diff.';
const sessionId = await createCodexSession({ cwd, task });

await relay.spawnAgent({
  name: 'CodexReviewer',
  cli: 'codex',
  task,
  cwd,
  harnessConfig: codexResume(sessionId, cwd),
});

Do not copy the whole process environment into env. Pass only the keys the harness needs.

OpenCode Headless Config

Use headless for an agent that already exists inside an app-server session. OpenCode serve is the first supported protocol:

opencode-harness.ts
import { AgentRelay, type ResolvedHarnessConfig } from '@agent-relay/sdk';

function opencodeSession(input: {
  endpoint: string;
  sessionId: string;
  pid?: number;
}): ResolvedHarnessConfig {
  return {
    runtime: 'headless',
    protocol: 'opencode',
    endpoint: input.endpoint,
    sessionId: input.sessionId,
    host: { ownership: 'attached', pid: input.pid },
    release: 'abort',
  };
}

const relay = new AgentRelay();

await relay.spawnAgent({
  name: 'OpenCodeWorker',
  cli: 'opencode',
  runtime: 'headless',
  task: 'Inspect the repo.',
  harnessConfig: opencodeSession({
    endpoint: 'http://127.0.0.1:4096',
    sessionId: 'ses_123',
    pid: 34567,
  }),
});

For OpenCode, Relay delivers messages to POST /session/:id/prompt_async. For now, app-server configs must use protocol: 'opencode', an http or https endpoint, a non-empty sessionId, and attached host ownership. broker-owned app-server hosts are reserved for later broker supervision.

Relaycast Spawns

Agent-crafted spawns should send the full config:

{
  "agent": {
    "name": "CodexReviewer",
    "cli": "codex",
    "task": "Review the current diff.",
    "harnessConfig": {
      "runtime": "pty",
      "command": "codex",
      "args": ["resume", "session_123"],
      "sessionId": "session_123"
    }
  }
}

Relay intentionally avoids a broker-local harness registry for now. A spawn request should be self-contained so behavior does not depend on hidden runtime state or which broker receives it.

See Also