Roj is a TypeScript SDK for building multi-agent LLM applications. You describe agents (system prompt, model, tools, sub-agents) as a preset, run it on a server, and consume it from a browser with ready-made React hooks and chat components.
The runtime is event-sourced and plugin-driven: every state change is a domain event, and almost all behavior — filesystem access, shell, services, todos, mailbox, compaction — lives in plugins you compose per preset and per agent.
Status: early. Packages are published as
0.1.xand the API still moves between releases.
| Package | Description |
|---|---|
@roj-ai/sdk |
Agent runtime — LLM providers, sessions, agents, event store, plugin system, built-in tools |
@roj-ai/transport |
WebSocket transport + RPC protocol (browser and Bun adapters) |
@roj-ai/shared |
Shared types, projections, RPC schemas |
@roj-ai/client |
Vanilla RPC client + platform REST client |
@roj-ai/client-react |
React hooks (useChat, usePreviewUrl, session stores) and chat components |
@roj-ai/debug |
Debug UI components (event timeline, agent tree, LLM calls) |
@roj-ai/standalone-server |
Single-instance local runtime with a platform-compatible REST + WS surface |
@roj-ai/sandbox-runtime |
Bun agent host for E2B sandboxes |
@roj-ai/cli |
REPL + CLI for inspecting sessions, agents, events and LLM calls |
@roj-ai/platform-cli |
roj build / upload / deploy for agent bundles |
@roj-ai/demo |
App Builder demo + e2e test (not published) |
Roj is also the OSS core of a hosted platform. The same client packages talk to either the standalone server or the Cloudflare-hosted platform — only the URL and auth differ.
The built-in git-status plugin normally compares the session's HEAD with the
detected local default branch. To display unpublished work, configure the
remote-tracking ref in the preset's plugins:
import { gitStatusPlugin } from '@roj-ai/sdk'
gitStatusPlugin.configure({ baseBranch: 'origin/main' })This works with both native Git and platform.git. The host owns fetching the
remote ref. If the configured ref is missing or unreadable, refresh returns no
snapshot and sends no notification; it does not substitute local main or zero.
For an initial publication where the remote branch may not exist yet, set
missingBase: 'all' alongside baseBranch. All HEAD commits then count as
unpublished until the ref appears. An unreadable existing ref still produces no
snapshot. A platform.git adapter must implement the corresponding
GitCountAheadOptions.missingBase policy.
- Bun (the monorepo, the server runtime and the test runner)
- An LLM API key —
ANTHROPIC_API_KEYorOPENROUTER_API_KEY
The demo runs the whole stack locally: an App Builder agent that writes plain HTML/CSS/JS, serves it from a dev service, and streams the result into a React SPA with a live preview iframe.
bun install
export ANTHROPIC_API_KEY=sk-ant-...
# terminal 1 — API server on :2486
bun run --filter @roj-ai/demo dev:server
# terminal 2 — SPA on :2487
bun run --filter @roj-ai/demo dev:spaOpen http://localhost:2487. See packages/demo/README.md
for the layout and the snapshot-based e2e test.
1. Define a preset. An agent is a system prompt, a model, plugins and optional sub-agents. Services (dev servers, watchers) start alongside it.
// roj.config.ts
import { ModelId, createOrchestrator, createPreset, defineAgent, defineConfig } from '@roj-ai/sdk'
import { filesystemPlugin } from '@roj-ai/sdk/tools/filesystem'
import { shellPlugin } from '@roj-ai/sdk/tools/shell'
const builder = defineAgent({
name: 'builder',
system: 'You build small web apps from a description.',
model: ModelId('anthropic/claude-haiku-4.5'),
services: [{
type: 'dev',
description: 'Preview server',
command: ({ port }) => `bunx serve -l ${port} .`,
autoStart: true,
readyPattern: 'Accepting connections',
}],
plugins: [filesystemPlugin.configureAgent({ directoryListing: { maxDepth: 3 } })],
tools: [],
agents: [],
})
export default defineConfig({
presets: [createPreset({
id: 'app-builder',
name: 'App Builder',
workspaceDir: '/tmp/roj/sessions/{sessionId}',
plugins: [shellPlugin.configure({ cwd: '/tmp/roj/sessions' })],
orchestrator: createOrchestrator({ ...builder, agents: [] }),
})],
})2. Run it. The standalone server hosts the preset and exposes the platform
REST + WebSocket shape on :2486.
bunx roj-standalone roj.config.ts3. Consume it. useChat handles the WebSocket connection, message stream,
agent questions, attachments and service readiness.
import { MessageInput, MessageList, useChat } from '@roj-ai/client-react'
function Chat({ instanceId, sessionId }: { instanceId: string; sessionId: string }) {
const chat = useChat({
platformUrl: 'http://localhost:2486',
instanceId,
sessionId,
token: '', // standalone has no auth — bind to localhost
services: ['dev'],
})
return (
<>
<MessageList messages={chat.messages} isAgentTyping={chat.isAgentTyping} />
<MessageInput disabled={!chat.isConnected} />
</>
)
}Instances and sessions are created with createRojClient from
@roj-ai/client/platform — see packages/demo/spa/App.tsx.
bun run packages/cli/src/main.ts # REPL against http://localhost:2486
bun run packages/cli/src/main.ts agents <sessionId> # agent tree
bun run packages/cli/src/main.ts events <sessionId> # domain events
bun run packages/cli/src/main.ts llm-calls <sessionId> # LLM call logThe server reads these environment variables (see
packages/sdk/src/config.ts):
| Variable | Default | Meaning |
|---|---|---|
PORT |
2486 |
Listen port |
HOST |
0.0.0.0 |
Bind address |
DATA_PATH |
./data |
Event store, workspaces, local registry |
PERSISTENCE |
file |
file or memory |
ANTHROPIC_API_KEY |
— | Anthropic provider |
OPENROUTER_API_KEY |
— | OpenRouter provider (fallback) |
DEFAULT_MODEL |
anthropic/claude-haiku-4.5 |
Model when a preset does not set one |
SESSION_IDLE_TIMEOUT_MS |
600000 (standalone) |
Evict a session runtime after this long idle; 0 disables. Unset means no eviction for embedders that call loadConfig() directly |
LOG_LEVEL / LOG_FORMAT |
info / console |
Logging |
Per-preset and per-agent settings (plugins, tools, workspace, services) live in
roj.config.ts, not in the environment.
bun install
bun run ts:build # type-check + build all packages
bun run ts:watch # incremental
bun run lint # Biome
bun run lint:fix
bun test # package testsConventions: ESM only, export type for type-only exports, Biome for linting.
packages/sdk is excluded from Biome and keeps its own conventions.
Tag a commit on clean main — publish.yml
publishes every public package to npm.
git tag vX.Y.Z && git push origin vX.Y.ZCLAUDE.md— repo map and conventionspackages/sdk/CLAUDE.md— plugin system, event sourcing, transportpackages/standalone-server/CLAUDE.md— REST surface, git layout, local registrypackages/demo/CLAUDE.md— demo and snapshot e2e test