How to Use RPC Mode for Process Isolation in omp (oh-my-pi)
omp's RPC mode runs the coding agent as a headless JSON subprocess that communicates exclusively through stdin and stdout, eliminating terminal side-effects and UI interference to create a fully isolated execution environment.
The oh-my-pi (omp) repository provides a specialized RPC mode designed for embedding the AI coding agent in external hosts while maintaining strict process boundaries. When launched with --mode rpc, the agent suppresses all TUI rendering and terminal control sequences, instead emitting structured JSON messages over stdio streams. This architecture enables safe integration into web servers, CI pipelines, and parent Node.js processes without console contamination or shared-state risks.
How RPC Mode Achieves Process Isolation
Headless Execution Without Terminal Contamination
In packages/coding-agent/src/modes/rpc/rpc-mode.ts, the RPC mode disables the interactive TUI and sets process.env.PI_NOTIFICATIONS = "off" to suppress all notification side-effects. The agent never writes raw terminal control sequences; instead, all output destined for the user interface is serialized as JSON objects written to process.stdout. This ensures that the child process cannot corrupt the parent terminal or intermix logging output with protocol messages, eliminating race conditions that would break JSON parsing.
Strict JSON-over-Stdio Protocol
The protocol defined in packages/coding-agent/src/modes/rpc/rpc-types.ts enforces a newline-delimited JSON (JSONL) format where every command sent to the agent must be a JSON object with an optional id field for correlation. Responses and events are similarly emitted as JSON objects prefixed with newlines. Because the RPC client owns the stdio streams exclusively, the host process maintains complete control over the communication channel without worrying about stdout pollution from third-party libraries or debug logs.
Graceful Shutdown Sequences
Isolation requires safe termination semantics. The RPC mode implements a dedicated shutdown flow where a shutdown request sets an internal shutdownState.requested flag in rpc-mode.ts. The agent only terminates after completing the current command execution, ensuring that no partial writes or corrupted state persists in the host process. This prevents abrupt process kills that could leave file handles or network connections dangling in the parent environment.
Implementing RPC Process Isolation
Spawning the Subprocess with Bun
You can launch an isolated omp instance from any Node.js or Bun script by spawning a child process with --mode rpc and wrapping the stdio pipes in JSONL handlers. The following example demonstrates raw subprocess integration using Bun's shell API:
import { $ } from "bun";
import { readJsonl } from "@oh-my-pi/pi-utils";
// Spawn the RPC subprocess (headless)
const child = await $`node ${process.argv[1]} --mode rpc`.quiet().nothrow().spawn({
stdout: "pipe",
stdin: "pipe",
});
// Helper to send a JSON command
function send(command: object) {
child.stdin.write(`${JSON.stringify(command)}\n`);
}
// Helper to read JSON responses/events
async function* readResponses() {
for await (const line of readJsonl(child.stdout)) {
yield line;
}
}
// Issue a prompt command
send({
type: "prompt",
id: "p1",
message: "What is your favorite fruit?",
});
// Consume the response
for await (const msg of readResponses()) {
if (msg.type === "response" && msg.id === "p1") {
console.log("Agent reply:", msg.success ? msg.data?.text : "error");
break;
}
}
// Request graceful shutdown
send({ type: "shutdown", id: "s1" });
Using the RpcClient Wrapper
For production applications, packages/coding-agent/src/modes/rpc/rpc-client.ts provides a typed RpcClient class that handles process spawning, message correlation, and lifecycle management automatically:
import { RpcClient } from "@oh-my-pi/pi-coding-agent/modes/rpc/rpc-client";
async function run() {
const client = new RpcClient({
args: ["--mode", "rpc"],
cwd: process.cwd(),
env: { ...process.env, PI_RPC_EMIT_TITLE: "1" },
});
// Wait for the child to be ready
await client.waitForReady();
// Send a command and await the typed response
const resp = await client.sendCommand({
type: "get_state",
id: "state-1",
});
console.log("Current session state:", resp.data);
// Graceful termination
await client.shutdown();
}
run();
CI/CD Pipeline Integration
RPC mode enables safe usage in CI environments where terminal UI libraries would fail or corrupt logs. The following GitHub Actions pattern demonstrates process-level isolation with bash stdio redirection:
steps:
- name: Run OMP in RPC mode
run: |
bun run src/main.ts --mode rpc \
> /tmp/omp-events.jsonl &
OMP_PID=$!
# Send a command to the background process
echo '{"type":"prompt","id":"ci-1","message":"CI health?"}' >> /proc/$OMP_PID/fd/0
# Wait for the correlated response
grep -m1 '"type":"response","id":"ci-1"' /tmp/omp-events.jsonl
# Clean up
kill $OMP_PID
Core RPC Implementation Files
The process isolation architecture is implemented across several key files in the oh-my-pi repository:
-
packages/coding-agent/src/modes/rpc/rpc-mode.ts– Contains the main execution loop that reads JSON fromstdin, writes JSON tostdout, and enforces the headless environment by disabling TUI rendering and notifications. -
packages/coding-agent/src/modes/rpc/rpc-client.ts– Provides theRpcClientwrapper class for spawning RPC children and managing typed command/response cycles with automatic correlation tracking. -
packages/coding-agent/src/modes/rpc/rpc-types.ts– Defines the TypeScript interfaces for JSON-RPC commands, responses, and UI requests, ensuring type-safe communication across the process boundary. -
packages/coding-agent/src/modes/rpc/host-tools.ts– Implements the bridge for host-provided tools, isolating tool execution behind the RPC protocol barrier so that filesystem or network operations cannot escape the sandbox. -
packages/coding-agent/src/modes/rpc/host-uris.ts– Manages URI scheme handlers through the RPC layer, maintaining isolation for resource access operations. -
packages/coding-agent/src/main.ts– Entry point that parses--mode rpc(or--mode rpc-uifor UI-enabled RPC) and dispatches to the appropriate mode handler.
Summary
- RPC mode in
oh-my-picreates a headless coding agent that communicates exclusively via JSON-over-stdio, eliminating terminal contamination risks. - The architecture relies on strict stdio ownership to prevent log interleaving and race conditions, with all TUI elements disabled via environment variables like
PI_NOTIFICATIONS=off. - Graceful shutdown semantics ensure the process terminates only after completing current work, preventing resource leaks in host applications.
- You can implement isolation using raw subprocess spawning with Bun/Node.js APIs or the built-in
RpcClientclass for typed, promise-based interactions. - The implementation files in
packages/coding-agent/src/modes/rpc/provide the complete isolation layer, from protocol definitions to host-tool bridges.
Frequently Asked Questions
What is the difference between --mode rpc and --mode rpc-ui?
The --mode rpc flag runs the agent in a completely headless state with all terminal UI disabled, suitable for pure server-side integration. The --mode rpc-ui variant enables UI rendering capabilities while still communicating over the JSON-RPC protocol, allowing hosts to forward display events to their own interfaces. Both modes maintain process isolation through stdio stream ownership, but rpc-ui emits additional UI-related JSON events that require handling by the host application.
How does RPC mode prevent stdout corruption from third-party libraries?
By disabling notifications via process.env.PI_NOTIFICATIONS = "off" and taking exclusive control of process.stdout in rpc-mode.ts, the RPC subprocess ensures that all output conforms to the JSONL protocol. Any library attempting to write raw strings to stdout would immediately corrupt the protocol, so the mode enforces strict buffer management where only structured JSON messages are emitted. Hosts can additionally spawn the process with piped stdio to physically isolate the streams from the parent's console.
Can multiple RPC agents run simultaneously without interference?
Yes. Because each RPC agent operates as an independent OS process with its own stdio pipes, multiple instances can run concurrently on the same machine or within the same parent application. Each instance requires its own RpcClient instance or subprocess handle, and the JSON correlation IDs ensure that responses from different agents never intermix. The lack of shared global state or terminal device contention makes this pattern safe for high-concurrency scenarios like web servers handling multiple user sessions.
What happens if the host process dies while the RPC agent is running?
The RPC agent monitors the parent process through the stdio pipe lifetime; if the host closes stdin or terminates without sending a shutdown command, the agent detects the stream closure and initiates an emergency teardown. However, best practice requires sending an explicit {"type": "shutdown"} command and awaiting the termination response to ensure graceful cleanup of active tool executions and file handles. The RpcClient class automatically handles this lifecycle management during garbage collection if properly disposed.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →