TypeScript SDK Reference

Complete reference for the @agent-relay/sdk package

npm install @agent-relay/sdk

AgentRelay

The main entry point. Manages the broker lifecycle, spawns agents, and routes messages.

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

const relay = new AgentRelay(options?: AgentRelayOptions);

AgentRelayOptions

PropertyTypeDescriptionDefault
binaryPathstringPath to the broker binaryAuto-resolved
binaryArgsAgentRelayBrokerInitArgsTyped agent-relay-broker init flagsNone
brokerNamestringName for the broker instanceAuto-generated
channelsstring[]Default channels agents are joined to on spawn['general']
cwdstringWorking directory for the broker and spawned agentsprocess.cwd()
envNodeJS.ProcessEnvEnvironment variables passed to the brokerInherited
harnessesRecord<string, HarnessDefinition>Named harness configs registered with the broker{}
workspaceNamestringName for the auto-created Relaycast workspaceRandom
relaycastBaseUrlstringBase URL for the Relaycast APIhttps://api.relaycast.dev

Spawning Agents

relay.spawnAgent(config)

const codex = await relay.spawnAgent({
  cli: 'codex',
  model: Models.Codex.GPT_5_3_CODEX,
  channels: ['dev'],
  task: 'Implement the approved plan.',
});

const reviewer = await relay.spawnAgent({
  name: 'Reviewer',
  cli: 'opencode',
  runtime: 'headless',
  model: Models.Opencode.OPENAI_GPT_5_2,
  channels: ['reviews'],
  task: 'Review the branch and summarize risks.',
});

Spawn options:

PropertyTypeDescription
namestringAgent name (defaults from cli, e.g. Codex)
clistringCLI or named harness to spawn
runtime'pty' | 'headless'Runtime category for the agent (default: 'pty')
modelstringModel to use (see Models below)
taskstringInitial task / prompt
channelsstring[]Channels to join
argsstring[]Extra CLI arguments
cwdstringWorking directory override
onStartfunctionSync/async callback before spawn request is sent
onSuccessfunctionSync/async callback after spawn succeeds
onErrorfunctionSync/async callback when spawn fails

The returned Agent handle exposes readiness, idle, exit, output, messaging, release, and structured result helpers. To wait for the agent to become ready before continuing, call await agent.waitForReady(timeoutMs).

Omit runtime for terminal-backed CLIs such as Claude Code, Codex, and Gemini. Use runtime: 'headless' for app-server sessions such as OpenCode.


Agent

spawnAgent returns an Agent:

interface Agent {
  readonly name: string;
  readonly runtime: AgentRuntime;
  readonly channels: string[];
  readonly status: AgentStatus; // 'spawning' | 'ready' | 'idle' | 'exited'
  exitCode?: number;
  exitSignal?: string;
  exitReason?: string;

  sendMessage(input: {
    to: string;
    text: string;
    threadId?: string;
    priority?: number;
    data?: Record<string, unknown>;
  }): Promise<Message>;

  release(reasonOrOptions?: string | ReleaseOptions): Promise<void>;
  waitForReady(timeoutMs?: number): Promise<void>;
  waitForExit(timeoutMs?: number): Promise<'exited' | 'timeout' | 'released'>;
  waitForIdle(timeoutMs?: number): Promise<'idle' | 'timeout' | 'exited'>;
  waitForResult(timeoutMs?: number): Promise<AgentResult>;
  onOutput(callback: (chunk: string) => void): () => void; // returns unsubscribe
}

Structured Agent Results

Provide result when spawning an agent to expose a submit_result MCP tool inside that agent. The tool posts JSON back to the local broker, and the SDK validates it before resolving waiters or hooks.

structured-result.ts
import { AgentRelay } from '@agent-relay/sdk';
import { z } from 'zod';

const relay = new AgentRelay();

const Summary = z.object({
  status: z.enum(['pass', 'fail']),
  findings: z.array(z.string()),
});

const reviewer = await relay.spawnAgent({
  name: 'Reviewer',
  cli: 'claude',
  task: 'Review the branch and submit the final JSON result.',
  result: {
    schema: Summary,
    onResult: (summary) => {
      console.log(summary.status, summary.findings.length);
    },
  },
});

const result = await reviewer.waitForResult(120_000);
console.log(result.data.status);

You can also pass jsonSchema directly when you only want to describe the expected payload to the spawned agent. Use relay.addListener('agentResult', ...) to observe all structured results globally.

ReleaseOptions

agent.release(...) accepts either a reason string or a ReleaseOptions object:

PropertyTypeDescription
reasonstringOptional release reason sent to the broker
onStartfunctionSync/async callback before release request is sent
onSuccessfunctionSync/async callback after release succeeds
onErrorfunctionSync/async callback when release fails

Low-Level Broker Client

AgentRelay is the high-level orchestration API. Use AgentRelayClient when you need direct broker control over spawned workers, PTYs, inbound delivery mode, or the broker event stream.

import { AgentRelayClient } from '@agent-relay/sdk';

// Spawn a broker and connect to it.
const client = await AgentRelayClient.spawn({ cwd: '/my/project' });

// Or connect to an already-running broker by reading .agent-relay/connection.json.
// const client = AgentRelayClient.connect({ cwd: '/my/project' });

// Or connect directly when you already know the broker URL and API key.
// const client = new AgentRelayClient({
//   baseUrl: 'http://127.0.0.1:3888',
//   apiKey: process.env.RELAY_BROKER_API_KEY,
// });

Client Options

PropertyTypeDescriptionDefault
baseUrlstringBroker listen API base URL.Required
apiKeystringAPI key sent as X-API-Key.None
fetchtypeof globalThis.fetchCustom fetch implementation for tests/runtimes.globalThis.fetch
requestTimeoutMsnumberHTTP request timeout in milliseconds.30000

Lifecycle and Messaging

MethodBroker routeDescription
client.spawnPty(input)POST /api/spawnSpawn a PTY-backed worker.
client.spawnCli(input)POST /api/spawnSpawn by CLI and transport.
client.spawnClaude(input)POST /api/spawnSpawn a Claude worker.
client.spawnOpencode(input)POST /api/spawnSpawn an OpenCode worker.
client.release(name, reason?)DELETE /api/spawned/{name}Release a worker.
client.listAgents()GET /api/spawnedList running workers.
client.sendMessage(input)POST /api/sendInject a relay message.
client.subscribeChannels(name, channels)POST /api/spawned/{name}/subscribeSubscribe a worker to channels.
client.unsubscribeChannels(name, channels)POST /api/spawned/{name}/unsubscribeUnsubscribe a worker from channels.

PTY and Delivery Control

These methods are the programmatic primitives behind CLI attach flows such as drive, view, and passthrough.

MethodBroker routeDescription
client.sendInput(name, data)POST /api/input/{name}Write raw bytes to a worker's PTY stdin.
client.openInputStream(name, options?)GET /api/input/{name}/streamOpen a websocket input stream with ordered writes and bounded client-side buffering.
client.resizePty(name, rows, cols)POST /api/resize/{name}Resize a worker PTY.
client.snapshot(name, format?)GET /api/spawned/{name}/snapshotCapture the visible PTY screen as plain text or base64 ansi.
client.getInboundDeliveryMode(name)GET /api/spawned/{name}/delivery-modeRead manual_flush or auto_inject.
client.setInboundDeliveryMode(name, mode)PUT /api/spawned/{name}/delivery-modeSet inbound delivery mode and return { mode, flushed }.
client.getPending(name)GET /api/spawned/{name}/pendingRead queued relay messages for a worker in manual_flush mode.
client.flushPending(name)POST /api/spawned/{name}/flushDrain queued relay messages into the worker PTY.
client.subscribeWorkerStream(name, options?)GET /wsAsync iterator over filtered worker_stream chunks for one worker.

client.openInputStream accepts PtyInputStreamOptions: highWaterMarkBytes defaults to 1 MiB of queued and in-flight UTF-8 input, and openTimeoutMs defaults to 10 seconds. Invalid non-finite or non-positive values are rejected before connecting.

import { AgentRelayClient, type InboundDeliveryMode } from '@agent-relay/sdk';

const client = AgentRelayClient.connect({ cwd: '/my/project' });
const name = 'Reviewer';

const previousMode: InboundDeliveryMode = await client.getInboundDeliveryMode(name);
await client.setInboundDeliveryMode(name, 'manual_flush');

try {
  const snapshot = await client.snapshot(name, 'plain');
  console.log(snapshot.screen);

  const pending = await client.getPending(name);
  console.log(`Queued messages: ${pending.length}`);

  const input = client.openInputStream(name);
  try {
    await input.waitUntilOpen();
    const result = await input.send('please continue\n');
    console.log(`Wrote ${result.bytes_written} bytes`);
  } catch (error) {
    console.error(`Input stream failed: ${error instanceof Error ? error.message : error}`);
  } finally {
    input.close();
  }

  await client.resizePty(name, 40, 120);
  await client.flushPending(name);
} finally {
  await client.setInboundDeliveryMode(name, previousMode);
}
for await (const chunk of client.subscribeWorkerStream('Reviewer', { stream: 'stdout' })) {
  process.stdout.write(chunk);
}

drive, view, and passthrough stay in the CLI because they manage raw terminal mode, keybindings, status lines, signals, and teardown. The SDK exposes the composable broker primitives those interactive surfaces use.


Human Handles

Send messages from a named human or system identity (not a spawned CLI agent):

// Named human
const human = relay.human({ name: 'Orchestrator' });
await human.sendMessage({ to: 'Worker', text: 'Start the task' });

// System identity (name: "system")
const sys = relay.system();
await sys.sendMessage({ to: 'Worker', text: 'Stop and report status' });

// Broadcast to all agents
await relay.broadcast('All hands: stand by for new task');

Event Hooks

Register listeners with addListener; keep the returned function to unsubscribe:

relay.addListener('messageReceived', (msg: Message) => { ... })
relay.addListener('messageSent', (msg: Message) => { ... })
relay.addListener('agentSpawned', (agent: Agent) => { ... })
relay.addListener('agentReleased', (agent: Agent) => { ... })
relay.addListener('agentExited', (agent: Agent) => { ... })
relay.addListener('agentReady', (agent: Agent) => { ... })
relay.addListener('agentIdle', ({ name, idleSecs }) => { ... })
relay.addListener('agentActivityChanged', ({ name, active, pendingDeliveries, reason }) => { ... })
relay.addListener('agentExitRequested', ({ name, reason }) => { ... })
relay.addListener('workerOutput', ({ name, stream, chunk }) => { ... })
relay.addListener('deliveryUpdate', (event: BrokerEvent) => { ... })
relay.addListener('agentResult', (result: AgentResult) => { ... })

Message type:

interface Message {
  eventId: string;
  from: string;
  to: string;
  text: string;
  threadId?: string;
  data?: Record<string, unknown>;
}

Other Methods

// List all known agents
const agents = await relay.listAgents(): Promise<Agent[]>

// Get broker status
const status = await relay.getStatus(): Promise<BrokerStatus>

// Read last N lines of an agent's log file
const logs = await relay.getLogs('Worker', { lines: 100 })
// logs.found: boolean, logs.content: string

// List agents that have log files
const names = await relay.listLoggedAgents(): Promise<string[]>

// Stream an agent's log file (returns handle with .unsubscribe())
const handle = relay.followLogs('Worker', {
  historyLines: 50,
  onEvent(event) {
    if (event.type === 'log') console.log(event.content);
  },
})
handle.unsubscribe();

// Wait for the first of many agents to exit
const { agent, result } = await AgentRelay.waitForAny([agent1, agent2], 60000)

// Shut down all agents and the broker
await relay.shutdown()

Broker tracing logs

The broker writes its tracing logs to a platform-standard directory (~/Library/Logs/agent-relay/ on macOS, ~/.local/state/agent-relay/logs/ on Linux, %LOCALAPPDATA%\agent-relay\Logs\ on Windows) with daily rotation. These helpers inspect and prune those files from Node without spawning the broker.

import {
  getBrokerLogDir,
  listBrokerLogs,
  tailBrokerLog,
  pruneBrokerLogs,
  clearBrokerLogs,
} from '@agent-relay/sdk';

// Resolve the directory for the current platform.
const dir = getBrokerLogDir();

// List every {brokerId}.log[.YYYY-MM-DD] file, newest first.
const files = await listBrokerLogs();
// files: Array<{ path, name, brokerId, date, size, mtime }>

// Tail the most recent log file for a broker.
const tail = await tailBrokerLog('myproject', { lines: 500 });
// tail?.path / tail?.content — null if no log file exists

// Retention: remove rotated files older than 7 days. The active
// un-suffixed file is never touched.
const { removed, kept } = await pruneBrokerLogs({ keepDays: 7 });

// Wipe one broker's logs (or pass nothing to wipe everything).
await clearBrokerLogs({ brokerId: 'myproject' });

These wrap the same logic that powers agent-relay log {path,list,view,rotate,clear}.


Complete Example

import { AgentRelay, Models } from '@agent-relay/sdk';

const relay = new AgentRelay();

relay.addListener('messageReceived', (msg) => {
  console.log(`${msg.from} → ${msg.to}: ${msg.text}`);
});

relay.addListener('agentSpawned', (agent) => {
  console.log(`Spawned: ${agent.name}`);
});

// Spawn agents
const planner = await relay.spawnAgent({
  name: 'Planner',
  cli: 'claude',
  model: Models.Claude.OPUS,
  task: 'Plan the feature implementation',
});

const coder = await relay.spawnAgent({
  name: 'Coder',
  cli: 'codex',
  model: Models.Codex.GPT_5_3_CODEX,
  task: 'Implement the plan',
});

// Wait for both to be ready
await planner.waitForReady();
await coder.waitForReady();

// Send a message
await planner.sendMessage({ to: 'Coder', text: 'Start implementing the auth module' });

// Wait for coder to finish
await coder.waitForExit(300_000);

await relay.shutdown();

Models

import { Models } from '@agent-relay/sdk';

// Claude
Models.Claude.OPUS; // 'opus'
Models.Claude.SONNET; // 'sonnet'
Models.Claude.HAIKU; // 'haiku'

// Codex
Models.Codex.GPT_5_4; // 'gpt-5.4' (default)
Models.Codex.GPT_5_3_CODEX; // 'gpt-5.3-codex'

// Gemini
Models.Gemini.GEMINI_3_1_PRO_PREVIEW; // 'gemini-3.1-pro-preview' (default)
Models.Gemini.GEMINI_2_5_PRO; // 'gemini-2.5-pro'

// OpenCode
Models.Opencode.OPENAI_GPT_5_2; // 'openai/gpt-5.2' (default)
Models.Opencode.OPENCODE_GPT_5_NANO; // 'opencode/gpt-5-nano'

Error Types

import { AgentRelayProtocolError, AgentRelayProcessError } from '@agent-relay/sdk';

try {
  await relay.spawnAgent({ cli: 'claude' });
} catch (err) {
  if (err instanceof AgentRelayProtocolError) {
    // Broker returned an error response (err.code available)
  }
  if (err instanceof AgentRelayProcessError) {
    // Broker process failed to start or crashed
  }
}

See Also