docs/architecture.md

Architecture

Runtime architecture, packages, and control flow.

🧱 Architecture

πŸ—ΊοΈ System Overview

                                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                         β”‚    πŸ“‘ Dashboard   β”‚
                                         β”‚  (single HTML)    β”‚
                                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                  β”‚ SSE / REST
                                                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  πŸ’» CLI  β”‚ ── HTTP ────────────────▢│                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚    πŸ—οΈ Gateway        β”‚
                                      β”‚                      β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚ πŸ’¬ Discordβ”‚ ── discord.js ─────────▢│  β”‚  πŸ“‘ Channels   β”‚  β”‚
β”‚          │◀─────────────────────────│  β”‚  - HTTP API    β”‚  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚  β”‚  - Discord     β”‚  β”‚
                                      β”‚  β”‚  - iMessage    β”‚  β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚  β”‚  - Telegram    β”‚  β”‚
β”‚ πŸ“± iMessage── BB webhook ──────────▢│  β”‚  - Slack       β”‚  β”‚
β”‚          │◀── BB REST ──────────────│  β”‚  - Email       β”‚  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚  β”‚  - WhatsApp    β”‚  β”‚
                                      β”‚  β”‚  - Signal      β”‚  β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚  β”‚  - Voice       β”‚  β”‚
β”‚ πŸŽ™οΈ Phone β”‚ ── LiveKit/SIP ────────▢│  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚          │◀── TTS ─────────────────│          β”‚           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                                      β”‚  β”‚  πŸ”€ EventBus   β”‚  β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚  β”‚  πŸ“‚ Job Persistβ”‚  β”‚
β”‚ πŸ’¬ Slack β”‚ ── @slack/bolt ─────────▢│  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                 β”‚
                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                         β–Ό                       β–Ό                       β–Ό
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚    🎯 Runner     β”‚    β”‚   ⏰ Scheduler   β”‚    β”‚   πŸ“Š Analytics   β”‚
                 β”‚  - Brain Session β”‚    β”‚  - Heartbeat     β”‚    β”‚  - Annotations   β”‚
                β”‚  - Streaming     β”‚    β”‚  - Cron          β”‚    β”‚  - Scoring       β”‚
                β”‚  - Adapters      β”‚    β”‚  - Hooks         β”‚    β”‚  - Recommend.    β”‚
                β”‚  - MCP Server    β”‚    β”‚  (webhooks)      β”‚    β”‚  - Feedback      β”‚
                 β”‚  - Session I/O   β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  - Categorizer   β”‚
                β”‚  - Browser Tool  β”‚                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
                    β”‚          β”‚
           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”˜          └────────────────────────┐
           β–Ό                                            β–Ό
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚  πŸ” Credentials  β”‚      β”‚    🧠 Memory     β”‚   β”‚   🌐 Mesh        β”‚
     β”‚ - .env parsing   β”‚      β”‚ - Meilisearch    β”‚   β”‚  - Registry      β”‚
     β”‚ - Allowlist      β”‚      β”‚ - Cross-agent    β”‚   β”‚  - Discovery     β”‚
     β”‚ - Inheritance    β”‚      β”‚ - Auto-inject    β”‚   β”‚  - Router        β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚ - Posse sharing  β”‚   β”‚  - Health Mon.   β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚   πŸŽ™οΈ Voice       β”‚
     β”‚  - VoiceEngine   β”‚
     β”‚  - STT/TTS       β”‚
     β”‚  - LiveKit       β”‚
     β”‚  - Twilio SIP    β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“¦ Packages

PackageNameRole
packages/core@randal/core🧩 Types, config schema (Zod), structured logger. Leaf dependency.
packages/control-plane@randal/control-planeπŸ—‚οΈ Published-artifact bootstrap support and legacy file services. Not part of the current Studio product surface.
packages/credentials@randal/credentialsπŸ” Parses .env files, filters by allowlist, inherits parent env vars.
packages/memory@randal/memory🧠 Meilisearch-backed memory, cross-agent sharing, auto-injection.
packages/runner@randal/runner🎯 Agent execution loop, adapter pattern, sentinel wrapping, struggle detection.
packages/scheduler@randal/scheduler⏰ Heartbeat, cron scheduling, webhook hooks. Autonomy primitives.
packages/gateway@randal/gatewayπŸ—οΈ HTTP server (Hono), EventBus, YAML job persistence, orchestration.
packages/harness@randal/harness🀝 createRandal() unified programmatic API. Boots the full engine.
packages/dashboard@randal/dashboardπŸ“‘ Single-page HTML dashboard with inline CSS/JS.
packages/clirandalπŸ’» CLI binary. Entry point for all commands.

βš™οΈ Primitives

🎯 Runner (Brain-Managed Sessions)

The runner is a session host, not an iterative summarizer. Its job is to launch a brain-managed session, stream output, and persist the durable surfaces that the rest of the system reads.

For each job:

  1. Create the harness job record (status: queued).
  2. Build a scoped environment via Credentials.
  3. Read and clear any injected human context (context.md) plus any structured task context packet or update files before the session starts.
  4. Assemble the minimal system prompt: structured task context + injected channel context + analytics feedback.
  5. Spawn a single brain session via Bun.spawn(["bash", "-c", wrappedCommand]).
  6. Stream stdout/stderr, parse protocol tags, update harness job state, and emit events.
  7. Mark the job complete, failed, or stopped based on the session result.

There is no runtime context compaction path. Resume behavior depends on the durable plan files and canonical loop-state.json, not on a summarized iteration transcript.

πŸ—‚οΈ Durable Session 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 for plan content; loop-state.json is the machine-readable index of session state.

Ownership rules:

  • OpenCode planning/building flows own build records keyed by plan slug.
  • OpenCode writes planning/build metadata such as plan_file, branch, worktree, status, phase, total_steps, completed_steps, current_step, task_id, timestamps, and cost fields.
  • The runner may only add or refresh fields it directly knows from the active brain session lifecycle. It must not invent a second schema, second keying strategy, or step-level truth it does not maintain.
  • The gateway and dashboard are read-only consumers of the canonical file and must read the job-specific worktree state rather than reconstructing parallel state elsewhere.
  • Corrupted or unreadable loop-state is an explicit failure condition, not a cue to silently reset durable state.

πŸ”Œ Agent Adapters

Adapters normalize the agent CLI behind a common interface:

AdapterBinaryNotes
opencodeopencodeopencode run [--model] <prompt>
mockbashFor testing. Reads from script files.

Each adapter implements: buildCommand(), parseUsage(), envOverrides().

🚦 Sentinel

Wraps agent commands with __START_<token> / __DONE_<token>:<exitcode> markers for reliable output boundary detection and exit code capture.

πŸ” Struggle Detection

Monitors iteration history for signs the agent is stuck:

  • πŸ”„ No file changes for N consecutive iterations
  • ❌ N consecutive non-zero exit codes
  • πŸ” Identical summaries across iterations
  • πŸ”₯ High token burn without observable progress

πŸ—οΈ Gateway

Orchestrates the daemon mode:

  1. Creates EventBus (pub/sub for SSE streaming to dashboard).
  2. Initializes MemoryManager (graceful fallback on failure).
  3. Creates Runner with an event handler that emits to EventBus and persists job state.
  4. Detects configured tools (checks which for each binary).
  5. Creates Hono HTTP app with REST endpoints + SSE.
  6. Starts messaging channel adapters (Discord, iMessage) from config.
  7. Starts Bun.serve.

πŸ“‘ Channel Adapters

Channel adapters provide inbound/outbound messaging for chat-based interaction. Each adapter implements the ChannelAdapter interface:

interface ChannelAdapter {
  readonly name: string;
  start(): Promise<void>;
  stop(): void;
}

All adapters share handleCommand() for parsing and executing commands, and formatEvent() for formatting job notifications. Channel-aware event routing uses JobOrigin β€” when a channel starts a job, it stamps the origin so notifications route back only to the originating channel/chat.

ChannelTransportPlatformAuth
HTTPREST + SSEAllBearer token
Discorddiscord.js WebSocketAllBot token
iMessageBlueBubbles REST + WebhookmacOS onlyServer password

Adding a new channel: Implement ChannelAdapter, add a config schema to config.ts, and add a case to the gateway startup loop. handleCommand() and formatEvent() are reusable.

πŸ” Credentials

Builds a clean, scoped environment for agent processes:

  • Parses .env file (handles quotes, comments, multiline).
  • Filters variables through an explicit allowlist.
  • Inherits specified vars from the parent process (default: PATH, HOME, SHELL, TERM).
  • Injects RANDAL_JOB_ID and RANDAL_ITERATION per iteration.

🧠 Memory

Persistent memory backed by Meilisearch. Full-text search, filterable by type/category/source/file, sorted by timestamp. Auto-installed on first randal serve.


πŸ”„ Data Flow

Config Studio And Runtime Split

Config Studio browser        Gateway / Studio API               Runtime Dashboard
        |                           |                                   |
        |-- load config ----------->|                                   |
        |<-- file-backed state -----|                                   |
        |-- generate draft -------->|                                   |
        |-- validate / preview ---->|                                   |
        |-- write to disk --------->|                                   |
        |                           |                                   |
        |                     same config file                          |
        |                           |-------------------------------> runtime reads it

Boundary rules:

  • dashboard at / is runtime-only
  • Config Studio at /studio is file-authoring only
  • Studio draft state is secondary until the file write succeeds
  • runtime worker orchestration remains runtime-owned and separate from config authoring

Job Execution (Daemon Mode)

Client                Gateway              Runner              Agent
  |                     |                    |                   |
  |── POST /job ───────▢|                    |                   |
  |                     |── execute(req) ───▢|                   |
  |                     |                    |── spawn ─────────▢|
  |                     |                    |◀── stdout/exit ───|
  |                     |◀── event ──────────|                   |
  |◀── SSE event ───────|                    |                   |
  |                     |── saveJob(yaml) ──▢|                   |
  |                     |                    |                   |
  1. Client submits job via POST /job (or randal send).
  2. Gateway passes request to Runner.
  3. Runner hosts a brain-managed session, collects output, and emits events.
  4. Gateway forwards events to EventBus (SSE) and persists job state to ~/.randal/jobs/.
  5. Dashboard receives events via SSE and updates in real time.

Chat Channel Flow (Discord / iMessage)

User (Discord/iMessage)    Gateway              Runner              Agent
  |                          |                    |                   |
  |── "refactor auth" ──────▢|                    |                   |
  |                          |── parseCommand ──▢ |                   |
  |◀── "Job abc1 started" ──|── execute(req) ───▢|                   |
  |                          |                    |── spawn ─────────▢|
  |                          |                    |◀── stdout/exit ───|
  |                          |◀── event ──────────|                   |
  |◀── "Job abc1 complete" ──|                    |                   |
  1. User sends a message via Discord DM or iMessage text.
  2. Channel adapter parses the command (or treats as implicit run:).
  3. handleCommand() executes against Runner/Memory/Jobs with a JobOrigin stamp.
  4. Job events route back to the originating channel/chat only (no cross-channel spam).
  5. Other channels pick up context via shared memory search.

Memory Flow

Agent saves memory via memory API
        β”‚
        β–Ό
Index to Meilisearch (with contentHash dedup)
        β”‚
        β–Ό
Next iteration: search memory for relevant context
        β”‚
        β–Ό
Auto-inject into system prompt as "## Relevant Memory"

If cross-agent sharing is configured:

  • Learnings are also published to a shared Meilisearch index (sharing.publishTo).
  • Before each iteration, results from shared indexes (sharing.readFrom) are merged into context.

πŸ—οΈ Gateway-Runner Decoupling

The Runner is a pure execution engine. It:

  • Takes a config and a callback function.
  • Executes jobs and emits events through the callback.
  • Has no knowledge of HTTP, persistence, SSE, or the gateway.

The Gateway is an orchestrator. It:

  • Creates and owns the Runner instance.
  • Wires the Runner's event callback to the EventBus and job persistence.

Managed Vs Local Runtime

Randal now has two startup sources, but only one default:

  • Local-first mode: load local randal.config.yaml or explicit config path.
  • Managed mode: optionally load a published YAML artifact through managed bootstrap env vars.

Managed mode is opt-in and does not alter the default local startup behavior.

  • Exposes the HTTP API and serves the dashboard.

This separation means randal run can use the Runner directly with a simple console-logging callback β€” no gateway, no server, no persistence. The same Runner code powers both modes.


🀝 Posse Readiness

A posse is a named group of Randal instances that coordinate as a team. The architecture supports this through:

  • Config: Each instance declares its posse membership (top-level posse field).
  • Memory sharing: Instances in the same posse publish learnings to a shared Meilisearch index and read from each other's indexes.
  • Instance discovery: The /instance endpoint exposes name, posse, and capabilities, enabling future service discovery.
  • Identity: Each instance has its own persona, rules, and knowledge, allowing role specialization within a posse.

The current implementation provides the memory-sharing and identity primitives. Full posse orchestration (task routing, delegation, consensus) is a future layer that builds on these foundations.