@biroai/agent-runtime
v2026.513.0
Published
**P2.2b** — Biro-native abstraction layer over coding-agent runtimes.
Readme
@biroai/agent-runtime
P2.2b — Biro-native abstraction layer over coding-agent runtimes.
Why this exists
Biro runs coding agents (Claude Code, Codex, OpenClaw, and future runtimes) inside sandboxes. Without a shared interface, each consumer would write its own ad-hoc wiring — different event shapes, different session lifecycles, different error types.
This package defines a single AgentRuntime interface that every runtime provider implements. The shape deliberately mirrors the Rivet Sandbox Agent SDK (rivet-dev/sandbox-agent v0.4.x, per doc/design/sandbox-abstraction.md). This means a future @biroai/adapter-rivet-runtime package can implement the interface by proxying to a Rivet binary running inside any sandbox, giving Biro six coding-agent runtimes for one adapter's worth of work.
The interface
AgentRuntime is a transport-agnostic interface with five concerns:
- Session lifecycle —
createSession,getSession,listSessions,stopSession - Messaging —
postMessage(enqueue a user turn; does not block on the agent's response) - Event streaming —
streamEventsreturns anAsyncIterable<BiroAgentEvent>that drains queued events then delivers live events as they arrive - Processes —
processes.list,processes.killfor processes running inside the session's sandbox - Filesystem —
fs.read,fs.write,fs.list,fs.deletewithin the session's working directory
All event types are drawn from @biroai/agent-events (BiroAgentEvent discriminated union). streamEvents ends naturally after emitting agent.session.ended.
The mock provider
InMemoryAgentRuntime is an in-memory implementation for tests and local development. It synthesizes realistic BiroAgentEvent sequences without spawning any real process:
import { InMemoryAgentRuntime } from "@biroai/agent-runtime/mock";
const runtime = new InMemoryAgentRuntime();
const handle = await runtime.createSession({
kind: "mock",
cwd: "/workspace",
companyId: "acme",
agentId: "dev-1",
issueId: null,
});
await runtime.postMessage(handle.sessionId, { text: "Fix the tests." });
for await (const event of runtime.streamEvents(handle.sessionId)) {
console.log(event.type);
// agent.session.started -> agent.turn.completed
}
await runtime.stopSession(handle.sessionId);
// stream yields agent.session.ended, then completesThe mock intentionally does NOT simulate filesystems or process tables — fs.read and fs.write throw NotImplementedError. Only event-shape testing is in scope.
Providers
mock(in-memory, for tests) — already shipped. Import from"@biroai/agent-runtime/mock".claude-code— spawnsclaude -psubprocesses. Status: minimal implementation.createSession/streamEvents/stopSessionwork;postMessageis unsupported (one-shot model — start a new session for a follow-up turn, optionally passingresumeSessionIdto resume Claude's context).processes.*andfs.*throwNotImplementedError.
ClaudeCodeProvider usage
import { ClaudeCodeProvider } from "@biroai/agent-runtime";
const runtime = new ClaudeCodeProvider();
const handle = await runtime.createSession({
kind: "claude-code",
cwd: "/workspace",
prompt: "Fix the failing tests.", // required for claude-code
companyId: "acme",
agentId: "dev-1",
issueId: null,
});
for await (const event of runtime.streamEvents(handle.sessionId)) {
console.log(event.type);
// agent.session.started -> agent.turn.completed -> agent.session.ended
}ClaudeCodeProvider accepts a spawn option for dependency injection in tests — pass a factory that returns a ChildProcessLike to avoid spawning real subprocesses.
Status
Types + interface + mock + ClaudeCodeProvider. The ./mock subpath is kept separate from the root export intentionally — production code cannot accidentally import the test runtime.
Related
packages/agent-events/—BiroAgentEventschema andBiroRuntimeliteralsdoc/design/sandbox-abstraction.md— rationale for the Rivet-shaped interfaceROADMAP-NEXT.mdrow P2.2 — context for this and adjacent tasks
