Soba Docs

Reference

The event stream

Five event types that make Claude Code, Codex and a raw provider SDK look identical to the app consuming them.

TypeScript
type AgentEventType = "delta" | "thinking" | "status" | "done" | "error";

The five type values are stable

New information arrives as optional fields on the existing types, never as a new type, so a client written against this list stays correct.

The envelope is deliberately tiny. Anything specific to one runtime is normalised away before it reaches you.

AgentEvent#

TypeScript
interface AgentEvent {
  type: AgentEventType;
  text?: string;      // `delta`: the new text. `thinking`: the RUNNING TOTAL
  message?: string;   // `status` / `error`: human-readable, safe to show a user
  tool?: string;      // on a `status` raised by a tool: a stable slug
  usage?: RunUsage;   // on `done`
  tier?: CostClass;   // on the opening `status`
}
Type Carries Notes
delta text The new text only. Append it
thinking text The running total of reasoning text. Replace it
status message, tool?, tier? Progress, safe to display
done usage? Terminal
error message Terminal

The delta / thinking asymmetry is the one thing to get right in a client: delta appends, thinking replaces.

TypeScript
function isTerminal(e: AgentEvent): boolean  // done | error

The opening status carries the tier#

JSON
{"type":"status","message":"Starting","tier":"user-subscription"}

Announced before any output. A cheaper tier covering for a sleeping laptop will visibly underperform, and a visible downgrade beats a silent one. See Cost classes.

Tool slugs#

Adapters map their runtime's native tool names onto these, so the app never needs to know whether it is talking to Claude Code or Codex:

TypeScript
const TOOL_SLUGS = [
  "search", "fetch", "read", "write", "edit", "bash",
  "files", "agent", "todo", "think", "sources", "tool",
] as const;

Present on a status raised by a tool; absent on non-tool statuses. Map them to icons.

RunUsage#

TypeScript
interface RunUsage {
  costClass?: CostClass;  // who pays for this run
  costMicros?: number;    // absent when nothing was spent
  model: string;          // provider-neutral alias, e.g. "sonnet", "gpt-5-codex"
  provider?: string;      // needed to price it, since aliases collide
  inputTokens: number;
  outputTokens: number;
  cacheReadTokens: number;
  cacheWriteTokens: number;
  webSearches: number;
}

usage is absent on done when the runtime cannot report it. A subscription-backed CLI often can't, which is exactly why those runs are free.

costClass is present so a usage frame is self-describing: a meter should never have to remember what it routed to in order to know whose money was spent.

A whole run#

JSON
{"type":"event","runId":"r_01","event":{"type":"status","message":"Starting","tier":"user-subscription"}}
{"type":"event","runId":"r_01","event":{"type":"status","message":"Reading README.md","tool":"read"}}
{"type":"event","runId":"r_01","event":{"type":"delta","text":"Soba routes agent runs "}}
{"type":"event","runId":"r_01","event":{"type":"delta","text":"to compute the user owns."}}
{"type":"event","runId":"r_01","event":{"type":"done","usage":{"model":"sonnet","costClass":"user-subscription","inputTokens":1840,"outputTokens":210,"cacheReadTokens":0,"cacheWriteTokens":0,"webSearches":0}}}
© 2026 Soba resolved = machine grant ∩ broker request