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:

Summary

  • RPC mode in oh-my-pi creates 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 RpcClient class 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →