π§± 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
| Package | Name | Role |
|---|---|---|
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/cli | randal | π» 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:
- Create the harness job record (status:
queued). - Build a scoped environment via Credentials.
- Read and clear any injected human context (
context.md) plus any structured task context packet or update files before the session starts. - Assemble the minimal system prompt: structured task context + injected channel context + analytics feedback.
- Spawn a single brain session via
Bun.spawn(["bash", "-c", wrappedCommand]). - Stream stdout/stderr, parse protocol tags, update harness job state, and emit events.
- 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:
| Adapter | Binary | Notes |
|---|---|---|
opencode | opencode | opencode run [--model] <prompt> |
mock | bash | For 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:
- Creates EventBus (pub/sub for SSE streaming to dashboard).
- Initializes MemoryManager (graceful fallback on failure).
- Creates Runner with an event handler that emits to EventBus and persists job state.
- Detects configured tools (checks
whichfor each binary). - Creates Hono HTTP app with REST endpoints + SSE.
- Starts messaging channel adapters (Discord, iMessage) from config.
- 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.
| Channel | Transport | Platform | Auth |
|---|---|---|---|
| HTTP | REST + SSE | All | Bearer token |
| Discord | discord.js WebSocket | All | Bot token |
| iMessage | BlueBubbles REST + Webhook | macOS only | Server 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
.envfile (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_IDandRANDAL_ITERATIONper 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
/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
Job Execution (Daemon Mode)
Client Gateway Runner Agent
| | | |
|ββ POST /job ββββββββΆ| | |
| |ββ execute(req) ββββΆ| |
| | |ββ spawn ββββββββββΆ|
| | |βββ stdout/exit βββ|
| |βββ event ββββββββββ| |
|βββ SSE event βββββββ| | |
| |ββ saveJob(yaml) βββΆ| |
| | | |
- Client submits job via
POST /job(orrandal send). - Gateway passes request to Runner.
- Runner hosts a brain-managed session, collects output, and emits events.
- Gateway forwards events to EventBus (SSE) and persists job state to
~/.randal/jobs/. - 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" ββ| | |
- User sends a message via Discord DM or iMessage text.
- Channel adapter parses the command (or treats as implicit
run:). handleCommand()executes against Runner/Memory/Jobs with aJobOriginstamp.- Job events route back to the originating channel/chat only (no cross-channel spam).
- 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.yamlor 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
possemembership (top-levelpossefield). - Memory sharing: Instances in the same posse publish learnings to a shared Meilisearch index and read from each other's indexes.
- Instance discovery: The
/instanceendpoint exposesname,posse, andcapabilities, 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.