Architecture
Randal has one first-class user surface: Randal Console. Run randal serve, open the Console, chat with Randal, steer active work, and watch operational status in one local app. Anything outside that built-in surface should connect as a custom relay through the gateway.
System Overview
┌──────────────────┐ ┌──────────────────────┐
│ Randal Console │── sessions/SSE ─▶│ │
│ chat/control │◀─ events/status ─│ Gateway │
│ status │ │ │
└──────────────────┘ │ ┌────────────────┐ │
│ │ HTTP API │ │
┌──────────┐ │ │ - /api/sessions│ │
│ CLI │── jobs/status/context ─▶│ │ - /job, /jobs │ │
└──────────┘ │ │ - /events │ │
│ │ - /messages │ │
┌──────────┐ │ └───────┬────────┘ │
│ Custom │── sessions/SSE ────────▶│ │ │
│ Relay │◀─ rendered events ──────│ ┌───────┴────────┐ │
└──────────┘ │ │ EventBus │ │
│ │ Job persistence│ │
┌──────────┐ │ │ Message history│ │
│ Voice │── voice routes ────────▶│ └───────┬────────┘ │
└──────────┘ └──────────┼───────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Runner │ │ Scheduler │ │ Analytics │
│ - Brain session │ │ - Heartbeat │ │ - Annotations │
│ - Streaming │ │ - Cron │ │ - Scoring │
│ - Adapters │ │ - Hooks │ │ - Feedback │
│ - Session I/O │ └──────────────────┘ └──────────────────┘
└───┬──────────┬───┘
│ │
┌────────┘ └────────────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Credentials │ │ Memory │ │ Mesh │
│ - .env parsing │ │ - Postgres DAG │ │ - Registry │
│ - Allowlist │ │ - Cross-agent │ │ - Discovery │
│ - Inheritance │ │ - Auto-inject │ │ - Router │
└──────────────────┘ │ - Posse sharing │ └──────────────────┘
└──────────────────┘
Packages
| Package | Name | Role |
|---|---|---|
packages/core | @randal/core | Types, config schema, and structured logger. |
packages/control-plane | @randal/control-plane | Published-artifact bootstrap support and legacy file services. Not the current Console surface. |
packages/credentials | @randal/credentials | .env parsing, allowlist filtering, and inherited parent env vars. |
packages/memory | @randal/memory | Postgres-backed memory, messages, DAG recall, cross-agent sharing, and skill indexes. |
packages/runner | @randal/runner | Agent execution loop, adapter pattern, sentinel wrapping, streaming, and struggle detection. |
packages/scheduler | @randal/scheduler | Heartbeat, cron scheduling, and webhook hooks. |
packages/gateway | @randal/gateway | Hono HTTP server, session facade, EventBus, job persistence, and orchestration. |
packages/harness | @randal/harness | createRandal() unified programmatic API. |
packages/dashboard | @randal/dashboard | Single-page Randal Console with inline HTML/CSS/JS. |
packages/cli | randal | CLI binary and command entry point. |
Core Primitives
Runner
The runner is a brain-session host. It creates a job record, builds a scoped environment, starts the configured agent CLI, streams output, emits runner events, and marks the job complete, failed, or stopped.
Gateway
The gateway is the product boundary. It serves the Console, exposes the HTTP/session API, streams SSE events, persists jobs, connects memory/scheduler/analytics/mesh/voice status, and protects authenticated routes with the configured HTTP token.
Console Sessions
The Console talks in sessions, not raw jobs. A session is the user-facing conversation/control unit. It maps to a durable threadId and the current/latest runner jobId.
| Endpoint | Purpose |
|---|---|
POST /api/sessions | Create or reuse a Console/custom-relay session. Empty body is allowed; malformed JSON returns 400. |
GET /api/sessions | List known sessions with status and latest job summary. |
GET /api/sessions/:id | Fetch one session plus recent message history when message storage is configured. |
POST /api/sessions/:id/messages | Record a user message and submit a runner job stamped with origin.channel="console", replyTo=session.id, and sessionId=session.id. |
GET /api/sessions/:id/events | Stream existing runner events filtered to that session's known job IDs. |
POST /api/sessions/:id/context | Inject context into the active job, or return a conflict when no active job exists. |
POST /api/sessions/:id/stop | Stop the active job, or return a conflict when nothing is running. |
This is a facade. It does not create a second runner, second event system, or broad new persistence model.
Custom Relays
Custom relays let any external product talk to Randal without becoming an official adapter. The relay owns product auth, inbound webhooks, outbound formatting, retries, and user/thread mapping. Randal owns sessions, jobs, memory, scheduling, and events.
// Inbound product webhook
const sessionId = mapExternalThreadToSessionId(webhook.threadId);
await gateway.post("/api/sessions", {
threadId: sessionId,
title: webhook.threadTitle,
});
await gateway.post(`/api/sessions/${sessionId}/messages`, {
content: webhook.text,
});
// Outbound session stream
for await (const event of gateway.sse(`/api/sessions/${sessionId}/events`)) {
await product.send(webhook.threadId, renderForProduct(event));
}
Older internal channel adapter implementations may remain in the codebase for compatibility or historical use. The recommended extension path for new products is a custom relay over the gateway session contract.
Durable Build State
.opencode/loop-state.json is the canonical durable state surface for planning/build visibility and resume metadata. Plan markdown files remain the human-readable source of truth. The gateway and Console consume that state; they do not invent a second build-state schema.
Data Flow
Config Studio And Console Runtime Split
Config Studio browser Gateway / Studio API Randal Console
| | |
|-- load config ----------->| |
|<-- file-backed state -----| |
|-- generate draft -------->| |
|-- validate / preview ---->| |
|-- write to disk --------->| |
| | |
| same config file |
| |------------------------------> runtime reads it
Boundary rules:
- Console at
/is runtime-only. - Config Studio at
/studiois file-authoring only. - Studio draft state is secondary until the file write succeeds.
- Runtime worker orchestration remains runtime-owned and separate from config authoring.
Console Session Execution
Console/Relay Gateway Runner Agent
| | | |
|-- POST /api/sessions ->| | |
|-- POST /messages ----->| | |
| |-- submit(req) ---->| |
| | |-- spawn --------->|
| | |<- stdout/exit ----|
| |<- event -----------| |
|<- session SSE event ---| | |
| |-- persist job/event/checkpoint rows --->|
- Console or a custom relay creates/reuses a session.
- Message submission records the user text and starts a runner job.
- Runner hosts a brain-managed session, collects output, and emits events.
- Gateway forwards events to EventBus and persists job records, events, tool invocation summaries, checkpoints, artifacts, and audit timelines to Postgres.
- Console/custom relay receives session-filtered events and renders output, tools, progress, errors, and completion.
Lower-level job routes such as POST /job, GET /jobs, POST /job/:id/context, and DELETE /job/:id remain available for operators and CLI compatibility. New interactive products should prefer /api/sessions/*.
Memory Flow
Agent saves memory via memory API
|
v
Write to Postgres memory tables with contentHash dedup and graph projection
|
v
Future prompts and Console searches retrieve relevant context through FTS/vector/trigram/graph ranking
Postgres is the durable source of truth for memory, chat, jobs, mesh registry, checkpoints, annotations, delegation audit, and graph/DAG relationships. If Postgres readiness fails in hosted mode, /health should fail closed when RANDAL_REQUIRE_MEMORY=true; local operators can inspect randal db status --write-check for connectivity, migration, extension, and writeability details.