OAR - programming interface for all agent harnesses
OAR (Open Agent Runtime) is a provider-independent programming interface for coding-agent runtimes: a solid foundation for building agent workspaces and other applications.
Delete harness logic and focus on outcomes and UX.
Supported runtimes
|
Antigravity |
Claude Code |
Codex |
Cursor |
Grok Build |
Kimi Code |
OpenCode |
Pi |
Qwen Code |
Qoder |
Pi Durable also runs through a host-owned Harness,
including in a browser via @botiverse/oar/pi-durable and the portable
core in @botiverse/oar/browser. The host installs its optional native peers.
Try the browser demo with pnpm durable-demo.
Library
One API drives every runtime: swap "claude" for "codex", "grok" or
"pi" (with a model it has) and the code stays the same. An option a
runtime cannot honor is refused before anything opens, never silently
dropped.
import { defaultRuntimes, promptAndWait } from "@botiverse/oar";
const claude = defaultRuntimes.require("claude");
const installation = await claude.installation?.();
if (installation?.kind !== "available") throw new Error("claude is not installed");
const session = await claude.session(installation, {
cwd: process.cwd(),
model: "sonnet",
effort: "high",
appendSystemPrompt: "Run the tests before you say you are done.",
mcpServers: [{ name: "fs", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."] }],
});
session.events((event) => {
// Every event says which agent it came from: [] is the root, a subagent has its own path.
const who = event.agentPath.length === 0 ? "" : [${event.agentPath.join(" > ")}] ;
switch (event.kind) {
case "text_delta": if (who === "") process.stdout.write(event.text); break;
case "tool_call_started": console.log(${who}${event.tool}); break;
case "turn_ended": console.log(${who}${event.outcome.kind}); break;
}
});
const run = promptAndWait(session, "Find and fix the flaky test", { timeoutMs: 600_000 });
// Meanwhile, from your UI: steers the running turn, or queues it where the runtime cannot steer.
await session.deliver("Leave the snapshots alone.");
console.log(await run);
console.log(session.status().value, session.usage().value.total, session.contextUsage().value);
await session.dispose();
// Later, even from another process: the same conversation, picked up where it stopped.
const resumed = await claude.session(installation, { cwd: process.cwd(), resume: session.id });
Hand work to other runtimes as subagents; each report wakes the session that is waiting for it:
import { createSubagents, formatReport, reportOrigin } from "@botiverse/oar/agents";
const crew = createSubagents({ maxRunning: 4 });
crew.onReport((report) => {
void resumed.deliver(formatReport(report), { origin: reportOrigin(report) });
});
await crew.spawn({ runtime: "codex", task: "Review the diff on this branch for race conditions" });
await crew.spawn({ runtime: "pi", task: "Write the changelog entry for this branch" });
What else a session gives you:
- Every fact, attributed.
events()is oneEventper fact (native user
seq and agentPath. { coalesceText: true } gives text in
blocks. rawEvents() and records() keep every native payload verbatim.
- Control with honest answers.
prompt,steer,queue,withdraw
abort each answer accepted or rejected with a typed code (busy,
no_active_turn, runtime_exited, …); where input landed is read from
the events, never guessed.
- Your own tools on any runtime.
serveMcpToolsserves tools your host
Runtime.mcpToolsListChanged says the running
session sees it (reference).
- Native goals where supported. Opt in with
goals: true, check
Session.goal, and observe native goal snapshots without a host continuation
loop (contract).
- Projections for UIs.
@botiverse/oar/observefolds records into a
reduceConversation, viewOf), status, tasks and stalls,
with no Node imports, so it runs in a browser.
- Recording and tests.
openVoyagewrites a session's records to a
@botiverse/oar/testing gives a scripted runtime with real Session
semantics and no binary or login.
The package README lists the public entry points.
defaultRuntimes holds the built-in Node runtimes; @botiverse/oar/runtimes/
loads one of them without the others. Cursor needs the SDK you install and hand
over: createRuntimeRegistry([...defaultRuntimes.list(), createCursorRuntime({ sdk: () => import("@cursor/sdk") })])
(why). Pi Durable needs
createPiDurableRuntime({ harness, models }) with the host's already-open Harness
and its Models.
Handy utilities, no session needed
OAR also offers a set of handy utilities you can use without running an agent at all: is it installed, is it signed in (and sign it in or out), which account and how much quota is left, which models it can run, is there an update (and install it), what skills and tools it has. One API covers every supported runtime, so a dashboard, a setup wizard or a quota monitor needs no per-runtime code.
import { defaultRuntimes } from "@botiverse/oar";
for (const runtime of defaultRuntimes.list()) {
const installation = await runtime.installation?.();
if (installation?.kind === "available") {
const [usage, models, update] = await Promise.all([
runtime.accountUsage?.(installation),
runtime.listModels?.(installation),
runtime.checkUpdate?.(installation),
]);
console.log(runtime.id, usage?.kind, models?.kind, update?.kind);
}
}
- Orientation:
orient(defaultRuntimes)combines installation and evidenced
depth: "account" for native credential/usage
I/O, or "models" for catalogs; usability(snapshot) projects readiness.
Partial failures retain their own status and time
(reference).
- Installation:
installation()finds the runtime on this machine and
shadowed lists the other copies on PATH after it, which never run.
- Install:
installPlan()says what installing a runtime would run (the
install() runs it for a runtime not found, judged by
installation() afterwards (claude, codex, grok, kimi, and opencode's
v1 or v2 line as the host chooses; reference).
- Login:
login()signs a runtime in through its own login without a
authStatus() says whether it is
signed in, and logout() signs it out through its own logout (claude,
codex, cursor; reference). oar never handles the
tokens.
- Account usage:
accountUsage()reads the plan and quota windows with
- Models:
listModels()lists the models the installation can run now
cwd), with their effort levels and service
tiers; not being logged in is its own answer, not an empty list.
- Setup forms:
@botiverse/oar/formsdescribes a runtime's settings as
- Updates:
checkUpdate()reports the version the runtime's own updater
upgrade() runs that updater and judges it by the
version afterwards (reference).
- Inventories:
skills(),commands(),mcpServers()andtools()read what the
- Session options:
refusedSessionOptionssays which session options a
session() rejects them with an
UnsupportedOptionError rather than drop them
(reference).
mcpServers attaches MCP servers to one session (except Cursor and
Pi Durable). name titles a session in the runtime's own list where it
has one. A resume the runtime declines for now (the conversation is
archived or open elsewhere) rejects with SessionUnavailableError.
The CLI exposes the same queries: oar installation, oar login
(--status only reports), oar logout, oar usage, oar models,
oar upgrade --check, and oar skills, oar commands, oar mcps and oar tools.
CLI
npx @botiverse/oar-cli list
oar run claude "What does this repo do?" --record run.jsonl
Every command: packages/cli (published as @botiverse/oar-cli).
Docs
- Design: why oar exists, its decisions and what comes next.
- Spec: the record stream and every contract built on it.
- Runtimes: what each runtime says natively and how oar maps it.
- Prior art: related projects compared feature by feature.
- Development: working in this repo.