OMP Docs
Reference

RPC mode

Run omp as a long-lived subprocess and control it with newline-delimited JSON over stdin and stdout.

Choose RPC when you own the host process

RPC mode is the lowest-common-denominator way to embed omp. Your program starts omp, sends one JSON object per line on stdin, and receives responses and streaming events as JSON lines on stdout. It works from any language with subprocess and JSON support.

SurfaceChoose it when…
Interactive ompA person is working in the terminal and wants omp's editor, dialogs, and tool views.
One-shot text or json modeA shell or CI job needs one prompt and then exits. json is an event stream, but it does not accept RPC commands.
SDKYour TypeScript application can run omp in-process and wants typed, direct APIs instead of a process boundary.
ACPYou are building an editor or client that already speaks the standardized Agent Client Protocol.
RPCYou need a long-lived omp session, streaming output, cancellation, state controls, or host callbacks from any language.

Start with headless RPC and an ephemeral session:

omp --mode rpc --no-session

Omit --no-session to save the conversation under omp's normal session store. Normal model, provider, working-directory, and configuration options still apply; see the CLI reference. RPC rejects @file command-line arguments—send file references in the prompt instead.

--mode rpc-ui uses the same wire protocol but marks the embedded session as UI-capable and connects UI-dependent tool and extension surfaces to the RPC UI bridge. It does not launch a TUI or add a separate “tool card” frame type. Use it only when your host implements those UI behaviors. Either mode can emit extension_ui_request frames for features such as extension dialogs or login, so a complete host should understand that sub-protocol.

Complete one prompt

Keep stdin open until the turn finishes. Read stdout continuously, keep stderr separate, and flush every command after its trailing newline.

This is a minimal valid exchange. Every displayed line is one complete JSON object; other events such as available_commands_update may be interleaved after ready.

{"type":"ready","protocolVersion":1,"supportedProtocolVersions":[1,2],"maxFrameBytes":1048576,"maxReassembledFrameBytes":67108864}
{"id":"p1","type":"prompt","message":"Reply with only the word ok"}
{"id":"p1","type":"response","command":"prompt","success":true}

The first line is stdout from omp, the second is stdin from your host, and the third is a possible acknowledgment on stdout. The acknowledgment means “accepted,” not “finished.” The turn then streams events and finishes at an agent_end whose isTerminal is not false.

Here is a complete client using only the Python standard library. It deliberately remains on protocol v1 because its prompt produces a small response; production clients should negotiate v2 as described below.

import json
import subprocess

proc = subprocess.Popen(
    ["omp", "--mode", "rpc", "--no-session"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    text=True,
    bufsize=1,
)

assert proc.stdin is not None
assert proc.stdout is not None

ready = json.loads(proc.stdout.readline())
if ready.get("type") != "ready":
    raise RuntimeError(f"expected ready, got {ready!r}")

request_id = "p1"
proc.stdin.write(json.dumps({
    "id": request_id,
    "type": "prompt",
    "message": "Reply with only the word ok",
}) + "\n")
proc.stdin.flush()

accepted = False
finished = False
text_parts = []

for line in proc.stdout:
    frame = json.loads(line)

    if frame.get("type") == "response" and frame.get("id") == request_id:
        if not frame.get("success"):
            raise RuntimeError(frame.get("error", "RPC command failed"))
        if frame.get("command") == "prompt":
            accepted = True
            if frame.get("data", {}).get("agentInvoked") is False:
                finished = True

    if frame.get("type") == "message_update":
        event = frame.get("assistantMessageEvent", {})
        if event.get("type") == "text_delta":
            text_parts.append(event["delta"])

    if (frame.get("type") == "prompt_result"
            and frame.get("id") == request_id
            and frame.get("agentInvoked") is False):
        finished = True

    if (frame.get("type") == "agent_end"
            and frame.get("isTerminal") is not False):
        finished = True

    if accepted and finished:
        break

proc.stdin.close()
if proc.wait() != 0:
    raise RuntimeError(f"omp exited with {proc.returncode}")

print("".join(text_parts))

Do not assume the response arrives before the events: some prompt paths can begin emitting events first. Match command responses by id, and independently track prompt completion.

Lifecycle, queues, and cancellation

Process lifecycle

Spawn the subprocess

Spawn omp --mode rpc with separate stdin, stdout, and stderr streams.

Await the ready event

Wait for ready before sending commands.

Read stdout continuously

Read stdout continuously. A host callback or UI request can arrive while a command is outstanding.

Send newline-delimited commands

Send commands as compact, unchunked JSON objects followed by \n.

Close stdin to disconnect

Close stdin to disconnect. omp rejects pending UI/tool/URI callbacks, drains accepted commands, disposes the session, and exits with code 0.

Isolate stdout and stderr

Stdout is exclusively the JSON protocol channel. Logs and diagnostics belong on stderr; never merge stderr into the JSON parser.

Prompt completion

prompt acknowledges after the message is admitted (queued, given an idle turn slot, or routed to an extension command), not before native input handlers or image preparation finish, and without waiting for the agent run. abort_and_prompt first awaits the abort, then acknowledges the same way. A failure before admission is the command's error response; a failure after admission can still emit a later error response with the same id.

frame.type == "agent_end" and frame.isTerminal != false

isTerminal: false means asynchronous delivery or maintenance will resume the session. An absent field is terminal-compatible.

A prompt may instead be handled locally—for example, by a slash command—and emit no agent_end. Detect that with either:

  • response.data.agentInvoked === false, or
  • a later { "type": "prompt_result", "id": …, "agentInvoked": false }.

Command acceptance is not run completion. A prompt completes via data.agentInvoked: false on its response or via its own prompt_result; a run completes on an agent_end frame where isTerminal !== false. Native input handlers run once, in submission order, before command, skill, or queue dispatch. Later input waits until the earlier submission is admitted, including an idle skill's vision description, and does not wait for its model turn. An abort cancels input received before it that is not yet admitted, even if that input is still in a hook. A successful new_session, switch_session, branch, fork or open_session does the same for input received before it; a vetoed one cancels nothing, and input sent after the session change runs in the new session.

Sending work while a turn is active

  • steer inserts a user message into the active run.
  • follow_up queues a message for after the active run.
  • A second prompt while streaming must include streamingBehavior: "steer" or "followUp" (camel-case followUp).
  • Steering and follow-up queues default to "one-at-a-time"; their set_*_mode commands can switch to "all".
  • Interrupt mode defaults to "immediate". "wait" defers steering until the turn finishes.

bash is the exception to normal command serialization: it runs concurrently so abort_bash can overtake it. Its response arrives when the command finishes, and may be out of order relative to other responses. Correlation by id is mandatory.

Cancel safely

  • Send abort to stop the active agent run. Wait for both the abort response and the terminal agent_end for the run.
  • Send abort_and_prompt to abort and schedule a replacement prompt. It does not provide data.agentInvoked or prompt_result; rely on lifecycle events or a same-id scheduling error.
  • Send abort_retry to stop an automatic retry delay.
  • Send abort_bash to stop the current RPC-started shell command.
  • Treat host_tool_cancel, host_uri_cancel, and UI method: "cancel" as cancellation of the referenced host-side operation.

Errors and recovery

Command failures are ordinary response frames and usually leave the process usable:

{"id":"m1","type":"response","command":"set_model","success":false,"error":"Model not found: provider/model"}

All failures have type: "response", success: false, a string command, and a human-readable error. code is optional; get_messages_page currently uses session_busy and stale_cursor.

Important edge cases:

Avoid callback deadlocks

A host that ignores extension_ui_request, host_tool_call, or host_uri_request can deadlock the operation that requested it. Dispatch these frames while other commands are pending.

Transport versions and large frames

Protocol v1 is active at startup. It emits one JSON object per physical line and caps that line, including its newline, at the advertised maxFrameBytes (currently 1 MiB). Inbound frames are always single, unchunked JSONL objects and should stay below that limit.

In v1, oversized responses fail with RPC response exceeded the transport limit; other frames may be compacted or replaced by rpc_frame_error. An oversized agent_end can omit messages already delivered through message_end and add messageCount. Never use agent_end.messages as the only transcript store: collect message events or page history with get_messages_page.

Public protocol reference

A ? below means optional. All commands accept id?: string. If supplied, a normal response echoes it.

Commands sent to stdin

Areatype and fieldsSuccessful data
Protocolnegotiate_protocol — protocolVersion: 2{ protocolVersion: 2 }
Promptprompt — message: string, images?: ImageContent[], streamingBehavior?: "steer" | "followUp"optional { agentInvoked: boolean }
Promptsteer — message, images?none
Promptfollow_up — message, images?none
Promptabortnone
Promptabort_and_prompt — message, images?none
Sessionnew_session — parentSession?: string{ cancelled: boolean }
Stateget_stateRpcSessionState below
Stateset_fast_mode — enabled: boolean{ enabled: boolean, active: boolean }
Stategoal — op: "get" | "create" | "resume" | "pause" | "drop", objective?: string, token_budget?: number{ goal: Goal | null, state: GoalModeState | null }
Stateset_todos — phases: TodoPhase[]{ todoPhases: TodoPhase[] }
Hostset_host_tools — tools: RpcHostToolDefinition[]{ toolNames: string[] }
Hostset_host_uri_schemes — schemes: RpcHostUriSchemeDefinition[]{ schemes: string[] }
Subagentsset_subagent_subscription — level: "off" | "progress" | "events"{ level }
Subagentsget_subagents{ subagents: RpcSubagentSnapshot[] }
Subagentsget_subagent_messages — subagentId?: string, sessionFile?: string, fromByte?: numberRpcSubagentMessagesResult
Modelset_model — provider: string, modelId: stringselected Model
Modelcycle_model{ model, thinkingLevel, isScoped } | null
Modelget_available_models{ models: Model[] }
Thinkingset_thinking_level — level: "inherit" | "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"none
Thinkingcycle_thinking_level{ level: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" } | null
Queueset_steering_mode — mode: "all" | "one-at-a-time"none
Queueset_follow_up_mode — same valuesnone
Queueset_interrupt_mode — mode: "immediate" | "wait"none
Compactioncompact — customInstructions?: stringCompactionResult
Compactionset_auto_compaction — enabled: booleannone
Retryset_auto_retry — enabled: booleannone
Retryabort_retrynone
Shellbash — command: stringBashResult
Shellabort_bashnone
Sessionget_session_statsSessionStats
Sessionexport_html — outputPath?: string{ path: string }
Sessionswitch_session — sessionPath: string{ cancelled: boolean }
Sessionbranch — entryId: string{ text: string, cancelled: boolean }
Sessionfork — entryId?: string{ cancelled: boolean }
Sessionget_branch_messages{ messages: Array<{ entryId: string, text: string }> }
Sessionget_last_assistant_text{ text: string | null }
Sessionset_session_name — name: stringnone
Sessionhandoff — customInstructions?: string{ savedPath?: string } | null
Messagesget_messages{ messages: AgentMessage[] }
Messagesget_messages_page — cursor?: string, limit?: number{ messages, nextCursor?, totalMessages }
Loginget_login_providers{ providers: Array<{ id, name, available, authenticated }> }
Loginlogin — providerId: string{ providerId: string }

get_messages_page defaults to 100 messages and accepts limits from 1 through 256. Follow the opaque nextCursor until absent. Paging is snapshot-based: restart from no cursor after stale_cursor; wait for streaming/compaction to stop after session_busy. fork moves the process onto a new session file and returns { cancelled }; read the new sessionFile/sessionId with get_state. With entryId (any message entry from get_entries, such as a user or assistant message), the new file holds the root-to-entry path including that entry plus the session's artifacts, so kept artifact:// references still resolve, and its header's parentSession is the old session file. When entryId sits inside an assistant tool-call batch (the assistant message itself, or one of its tool results), the cut extends through the batch's recorded tool results so the fork never ends on tool calls whose results were dropped. This runs session_before_branch/session_branch hooks with reason "fork".

Shared input objects

ImageContent:

FieldType
typeliteral "image"
database64-encoded image bytes
mimeTypeMIME string such as image/png
detail?"auto" | "low" | "high" | "original"
providerFile?{ provider: "openai" | "anthropic" | "google", id?: string, uri?: string, expiresAt?: number }
url?HTTPS mirror of exactly the bytes in data

TodoPhase is { name: string, tasks: TodoItem[] }. A TodoItem is { content: string, status: "pending" | "in_progress" | "completed" | "abandoned" | "blocked", blocker?: string }.

RpcHostToolDefinition is { name: string, label?: string, description: string, parameters: JSONSchemaObject, hidden?: boolean, loadMode?: "essential" | "discoverable" }. Sending set_host_tools replaces the previous host-tool set.

RpcHostUriSchemeDefinition is { scheme: string, description?: string, writable?: boolean, immutable?: boolean }. Supply a scheme without ://. Names are normalized to lowercase; security is reserved. Sending the command replaces the previous set.

Response envelope

Every command produces one of:

Success: { id?, type: "response", command: string, success: true, data?: any }
Failure: { id?, type: "response", command: string, success: false, error: string, code?: string }

Useful result objects have these exact fields:

  • CompactionResult: { summary, shortSummary?, firstKeptEntryId, tokensBefore, details?, preserveData? }.
  • BashResult: { output, exitCode: number | undefined, cancelled, timedOut?, truncated, totalLines, totalBytes, outputLines, outputBytes, artifactId?, workingDir? }. JSON serialization omits fields whose value is undefined.
  • SessionStats: { sessionFile?, sessionId, userMessages, assistantMessages, toolCalls, toolResults, totalMessages, tokens: { input, output, reasoning, cacheRead, cacheWrite, total }, premiumRequests, cost, contextUsage? }.
  • Model is the model catalog object. provider and id are the stable selection keys; preserve unknown additional model metadata for forward compatibility.

RpcSessionState contains:

FieldType
model?Model
thinkingLevelthinking-level string or omitted
isStreaming, isCompactingboolean
steeringMode, followUpMode"all" | "one-at-a-time"
interruptMode"immediate" | "wait"
sessionFile?, sessionId, sessionName?string
autoCompactionEnabledboolean
fastModeEnabled, fastModeActiveboolean
tokensPerSecondnumber or null
messageCount, queuedMessageCountnumber
todoPhasesTodoPhase[]
systemPrompt?string[]
dumpTools?array of { name, description, parameters, examples? }
contextUsage?{ tokens: number, contextWindow: number, percent: number }
goalGoal | null

RpcAvailableSlashCommand is { name, source, aliases?, description?, input?: { hint? }, subcommands?: Array<{ name, description?, usage? }> }. source is "builtin", "skill", "extension", "custom", "mcp_prompt", or "file".

Agent messages

Events and history responses carry AgentMessage objects. Consumers should switch on role and preserve unknown roles because extensions can add message variants.

roleCore fields
usercontent: string | (TextContent | ImageContent)[], timestamp, plus optional attribution/transport metadata
developersame content forms and timestamp
assistantcontent: ContentBlock[], api, provider, model, usage, stopReason, timestamp, plus optional error, timing, recovery, and provider metadata
toolResulttoolCallId, toolName, content, isError, timestamp, optional details
bashExecutioncommand, output, exitCode?, cancelled, truncated, timestamp, optional meta, excludeFromContext
pythonExecutioncode, output, exitCode?, cancelled, truncated, timestamp, optional meta, excludeFromContext
customcustomType, content, display, details?, timestamp
hookMessagelegacy shape matching custom
fileMentionfiles: Array<{ path, content, lineCount?, byteSize?, skippedReason?, image? }>, timestamp
branchSummarysummary, fromId, timestamp
compactionSummarysummary, shortSummary?, tokensBefore, tokensAfter?, method?, blocks?, images?, warning?, timestamp

The common content blocks are:

  • { type: "text", text: string, textSignature?: string }
  • { type: "thinking", thinking: string, thinkingSignature?: string, itemId?: string }
  • { type: "redactedThinking", data: string }
  • ImageContent
  • { type: "toolCall", id: string, name: string, arguments: object, intent?: string, …opaque replay metadata }
  • { type: "fallback", from: { model: string }, to: { model: string } }
  • { type: "anthropicServerTool", block: object } for provider-native server tool replay

Provider-specific assistant blocks can also appear. Ignore blocks you do not render, but retain them if you persist and replay protocol data. Assistant usage is { input, output, cacheRead, cacheWrite, totalTokens, contextTokens?, orchestration?, premiumRequests?, reasoningTokens?, cttl?, server?, cost: { input, output, cacheRead, cacheWrite, total } }. stopReason is "stop", "length", "toolUse", "error", or "aborted".

Event and notification schema

Agent lifecycle events

typeFields after type
agent_startnone
agent_endmessages: AgentMessage[], isTerminal?: boolean, telemetry?: object, coverage?: object; transport compaction may also add messageCount
turn_startnone
turn_endmessage: AgentMessage, toolResults: ToolResultMessage[]
message_startmessage: AgentMessage
message_updatemessage: AgentMessage, assistantMessageEvent: AssistantMessageEvent
message_endmessage: AgentMessage
tool_execution_starttoolCallId, toolName, args, intent?
tool_execution_updatetoolCallId, toolName, args, partialResult
tool_execution_endtoolCallId, toolName, result, isError?

AssistantMessageEvent is one of:

Event typeOther fields
startpartial: AssistantMessage
text_startcontentIndex, partial
text_deltacontentIndex, delta: string, partial
text_endcontentIndex, content: string, partial
thinking_startcontentIndex, partial
thinking_deltacontentIndex, delta: string, partial
thinking_endcontentIndex, content: string, partial
image_endcontentIndex, content: ImageContent, partial
toolcall_startcontentIndex, partial
toolcall_deltacontentIndex, delta: string, partial
toolcall_endcontentIndex, toolCall, partial
donereason: "stop" | "length" | "toolUse", message: AssistantMessage
errorreason: "aborted" | "error", error: AssistantMessage

The spelling is toolcall_*, without an underscore between tool and call.

Session events

typeFields after type
auto_compaction_startreason: "threshold" | "overflow" | "idle" | "incomplete", action: "context-full" | "remote" | "handoff" | "shake" | "snapcompact"
auto_compaction_endaction, result?: CompactionResult, aborted, willRetry, errorMessage?, skipped?
auto_retry_startattempt, maxAttempts, delayMs, errorMessage, errorId?
auto_retry_endsuccess, attempt, finalError?, retryErrors?
retry_fallback_appliedfrom, to, role
retry_fallback_succeededmodel, role
model_changednone; call get_state if you need the new model
thinking_level_changedthinkingLevel?, configured?, resolved?
ttsr_triggeredrules: Rule[]
todo_remindertodos: TodoItem[], attempt, maxAttempts
todo_auto_clearnone
irc_messagemessage: CustomMessage
noticelevel: "info" | "warning" | "error", message, source?
goal_updatedgoal: Goal | null, state?: GoalModeState

goal payload

goal manages goal mode with the same lifecycle as the interactive /goal command. Every op answers { goal: Goal | null, state: GoalModeState | null }; get_state carries the same state as goal. goal_updated events report every change, including those made by the agent's goal tool.

  • get only reads. It never starts a turn.
  • create needs goal.enabled, a non-empty objective, and no active or paused goal. It is refused in plan mode, and token_budget must be a positive integer. It adds the goal tool to the active tools.
  • resume resumes a paused goal (refused in plan mode). pause and drop restore the active tools from before the goal started.
  • Failures are ordinary success: false responses.

Goals do not continue on their own over RPC unless goal.continuationModes contains "rpc"; this covers both --mode rpc and --mode rpc-ui.

Other stdout frames

typeFields and action
readyprotocolVersion: 1, supportedProtocolVersions: [1,2], maxFrameBytes, maxReassembledFrameBytes
responsecommand result envelope described above
rpc_chunkchunkId, index, count, byteLength, data; reassemble before dispatch
rpc_frame_errorerror, originalType?; a logical output could not fit the active transport
prompt_resultid?, agentInvoked: boolean; currently emitted for local-only completion
available_commands_updatecommands: RpcAvailableSlashCommand[]; emitted at startup and when metadata changes
command_outputtext: string; output from a locally handled slash command
session_info_updatetitle?, sessionId; session metadata changed
config_updatemodel?, thinkingLevel?; a local command changed configuration
extension_errorextensionPath, event, error
extension_ui_requestUI sub-protocol below
host_tool_call, host_tool_cancelhost-tool sub-protocol below
host_uri_request, host_uri_cancelhost-URI sub-protocol below
subagent_lifecycle, subagent_progress, subagent_eventsubscription-gated subagent frames below
live_phase, live_levels, live_transcript, live_endlive-voice frames below; set_event_filter never drops them

Unknown event types should be ignored or logged, not treated as a fatal protocol error. New optional fields and event types can be added without changing the transport version.

Extension UI sub-protocol

All requests have type: "extension_ui_request" and an id. Only select, confirm, input, and editor expect a value response. Fire-and-forget presentation methods need no response.

methodRequest fields
selecttitle, options: string[], optionDetails?: Array<{ description?: string }>, timeout?
confirmtitle, message, timeout?
inputtitle, placeholder?, timeout?
editortitle, prefill?, promptStyle?
canceltargetId
notifymessage, notifyType?: "info" | "warning" | "error"
setStatusstatusKey, statusText?
setWidgetwidgetKey, widgetLines?: string[], widgetPlacement?: "aboveEditor" | "belowEditor"
setTitletitle; suppressed unless PI_RPC_EMIT_TITLE=1
set_editor_texttext
open_urlurl, launchUrl?, instructions?; prefer launchUrl as the copy/open target when supplied

Reply on stdin with the same id:

Text/select/editor: { type: "extension_ui_response", id, value: string }
Confirmation:       { type: "extension_ui_response", id, confirmed: boolean }
Cancel/timeout:      { type: "extension_ui_response", id, cancelled: true, timedOut?: boolean }

An unknown response id is ignored. When a request carries timeout, the host should enforce it and return the cancellation form with timedOut: true.

OAuth login emits open_url, may emit notification frames, and can then ask for a pasted redirect or code with input. Providers that require interactive input before producing a URL are rejected in RPC mode; log in through interactive omp instead.

Host tools and host URIs

After set_host_tools, omp can ask your process to execute a registered tool:

{"type":"host_tool_call","id":"host-1","toolCallId":"toolu-1","toolName":"lookup_ticket","arguments":{"id":42}}

You may stream zero or more updates, then must complete the same id:

{"type":"host_tool_update","id":"host-1","partialResult":{"content":[{"type":"text","text":"looking up ticket"}]}}
{"type":"host_tool_result","id":"host-1","result":{"content":[{"type":"text","text":"ticket 42 is open"}]}}

partialResult and result are { content: Array<TextContent | ImageContent>, details?: any, isError?: boolean, providerMetadata?: object, useless?: boolean }. Set top-level isError: true on host_tool_result to reject the pending call and surface the returned content as a tool error. host_tool_cancel is { type, id, targetId }; stop the operation identified by targetId.

After set_host_uri_schemes, omp routes reads and allowed writes of those schemes to your process:

Read:  { type: "host_uri_request", id, operation: "read", url }
Write: { type: "host_uri_request", id, operation: "write", url, content }
Cancel:{ type: "host_uri_cancel", id, targetId }

Reply with:

Success read:  { type: "host_uri_result", id, content, contentType?, notes?, immutable? }
Success write: { type: "host_uri_result", id }
Failure:       { type: "host_uri_result", id, isError: true, error?: string, content?: string }

contentType is "text/markdown", "application/json", or "text/plain" and defaults to plain text. Successful reads require content. A result-level immutable overrides the registered scheme for that read. The edit tool does not edit host URIs; register the scheme as writable to let full-replacement writes use the write path.

Subagent frames

Forwarding defaults to off:

  • progress emits subagent_lifecycle and subagent_progress.
  • events also emits every nested subagent_event.

subagent_lifecycle.payload is { id, agent, agentSource, description?, status: "started" | "completed" | "failed" | "aborted", sessionFile?, parentToolCallId?, index, detached? }.

subagent_progress.payload is { index, agent, agentSource, task, parentToolCallId?, assignment?, progress, sessionFile?, detached? }. progress includes identity/status, recent tool/output summaries, counts, tokens, cost, duration, model selection, and optional retry state. Treat newly added progress metrics as optional.

subagent_event.payload is { id, event: AgentSessionEvent }, where event uses the same lifecycle schema above.

RpcSubagentSnapshot returned by get_subagents is { id, index, agent, agentSource, description?, status, task?, assignment?, sessionFile?, lastUpdate, progress?, parentToolCallId? }.

RpcSubagentMessagesResult is { sessionFile, fromByte, nextByte, reset, entries, messages }. Select by subagentId or sessionFile; fromByte enables incremental reads. If the transcript shrank beneath fromByte, the server restarts at byte zero and sets reset: true.

Live voice

RPC hosts can run a GPT live voice session (the realtime surface behind the terminal's /live) bound to the RPC session. The realtime model talks to the user through the machine's microphone and speakers and delegates work into the RPC session as ordinary turns. At most one live session runs per RPC server.

  • { id?, type: "live_start", voice?: string, instructions?: string } returns data: { voice: string }. voice defaults to the live.voice setting; instructions replaces the bundled live prompt and is rendered as a Handlebars template with {{username}} and {{firstName}} of the local OS account. Starting while a session is connecting, active, or closing fails.
  • { id?, type: "live_stop" } responds after the session has stopped and succeeds when none is active.
  • { id?, type: "live_mute", muted?: boolean } sets the microphone mute, or toggles it when muted is omitted, and fails when no session is active.

live_start responds once the session is connected and recording, so it is dispatched concurrently like bash; live_stop sent meanwhile cancels the connection and the pending live_start then fails.

Live frames are not session events: set_event_filter never drops them.

  • { type: "live_phase", phase } on every phase change; phase is one of connecting, listening, working, speaking, muted, error.
  • { type: "live_levels", input: number, output: number } carries microphone and speaker RMS in [0, 1], at most one frame per 100 ms.
  • { type: "live_transcript", role: "user" | "assistant", turn: number, text: string, final: boolean } carries the accumulated text of one turn; later frames for the same role and turn replace earlier ones until final: true.
  • { type: "live_end", error?: string } arrives exactly once per session when it ends, carrying the failure when it ended on one (including a failed live_start).

Closing stdin stops an active live session before the process exits.

Host implementation checklist

A robust language-neutral client should follow these implementation steps:

Spawn and await ready

Spawn one process per independently controlled session and wait for ready.

Serialize stdin writes

Keep stdin writes serialized, newline-terminated, and flushed.

Read stdout continuously

Continuously read stdout while requests are pending; never wait for one response before servicing callbacks.

Correlate responses by ID

Generate unique string ids and correlate responses by id, not arrival order.

Track completion independently

Track prompt acceptance separately from terminal or local-only completion.

Negotiate protocol v2

Negotiate v2 and strictly reassemble chunks when large messages are possible.

Non-blocking callback dispatch

Dispatch extension UI, host tool, and host URI requests without blocking the reader.

Preserve forward-compatible fields

Preserve unknown fields, message roles, content blocks, and event types for forward compatibility.

Clean shutdown

Close stdin, then wait for process exit during orderly shutdown.


Source: omp.sh — MIT, Stencil Labs. Initially imported on 2026-10-03; updates via automatic sync.

OMP Docs· always in sync with upstream
oh-my-piomp.sh/docsContent MIT · Stencil Labs