Architecture
The agent follows a three-category architecture designed for clarity and extensibility.
The Three Categories
| Category | Role | Has a shape? | User-extensible? | Examples |
|---|---|---|---|---|
| Core | Wireframe that connects everything | No | No | loop, registry, config, session |
| Services | Fixed branches the core depends on | Service type | No (built-in) | api, storage, admin, tokens, guardrails, persona |
| Plugins | Swappable components plugging into core at defined attachment points | Shape per type (Tool, Gateway, Hook, MemoryPlugin) | Yes (built-in + user ~/.agent/) | tools/, gateways/, hooks/, memory/ |
Core
The lightweight wireframe — it routes work outward but doesn't do the work itself. Pure orchestration: the event loop, the registry, config loading, session type definition.
Services
Fixed branches the core depends on. Each conforms to a Service shape:
pub type Service {
Service(
name: String,
supervised: Bool,
start: fn() -> Result(Nil, String),
stop: fn() -> Result(Nil, String),
health: fn() -> Result(String, String),
)
}The supervised: Bool flag self-declares whether the service needs OTP process management. Services do actual work (HTTP calls, SQLite, shell execution) but aren't user-swappable.
Plugins
Swappable, shape-conforming, user-extensible. Built-in and user-provided plugins follow the exact same shapes. Users add plugins in ~/.agent/tools/, ~/.agent/hooks/, etc.
Each plugin has a top-level Plugin shape plus a sub-type shape:
pub type Plugin {
Plugin(
name: String,
description: String,
plugin_type: PluginType,
supervised: Bool,
start: fn() -> Result(Nil, String),
stop: fn() -> Result(Nil, String),
health: fn() -> Result(String, String),
)
}Plugin types:
- Tool — things the agent can do (bash, web fetch, browser, memory, cron)
- Gateway — channels the agent communicates through (Telegram, CLI/TUI)
- Hook — lifecycle callbacks that fire at specific points (context compression, reflection, tool guardrails)
- MemoryPlugin — persistence backends for agent memory (file-based, SQLite)
Project Structure
src/
├── core/ — Wireframe (loop, registry, config, session, tool shape)
├── services/ — Fixed branches, Service shape, OTP-supervised
│ ├── api/openai/ — OpenAI completions client
│ ├── storage/ — SQLite persistence
│ ├── admin/admin.gleam — Inspection and management
│ ├── tokens/tokens.gleam — Heuristic token estimation (CJK-aware)
│ ├── guardrails/guardrails.gleam — Shell command safety layer
│ ├── persona/persona.gleam — Persona/SOUL.md file loader
│ ├── context/context.gleam — System prompt builder
│ ├── titler/titler.gleam — Auto-titling
│ ├── pulse/pulse.gleam — Time-driven periodic task execution
│ ├── cron/ — Traditional cron scheduler
│ ├── harness/harness.gleam — Deterministic validation gating
│ └── notifications/ — Notification delivery + DND coordination
├── plugins/ — Pluggable, shape-conforming, user-extensible
│ ├── shapes.gleam — Plugin shape definition
│ ├── tools/ — bash/, browser/, code/, cron/, memory/, session_search/, web/
│ ├── gateways/ — telegram/, tui/
│ ├── hooks/ — context_compressor/, reflection/, tool_guardrails/
│ └── memory/ — file_memory/
├── agent.gleam — CLI REPL entry point
├── agent_app.gleam — Daemon entry point
├── agent_admin.gleam — Admin CLI
└── agent_supervisor.gleam — Root supervisorOTP Supervision
The agent uses a structured OTP supervisor tree:
- Root supervisor (
agent_supervisor.gleam) coordinates service and gateway supervisors - Service supervisor manages services that declare
supervised: true - Gateway supervisor manages gateway processes (Telegram polling, admin TCP listener)
Each Service and Plugin self-declares whether it needs supervision via the supervised flag. The supervisor starts and monitors only those that opt in.
Request Flow
- A message arrives via a Gateway (Telegram, CLI)
- The Loop loads the session, builds the system prompt (via context service), and sends the conversation to the API service
- The API response may include tool calls — the loop dispatches them through the Tool Registry
- Tool results feed back into the conversation for the next API call
- Hooks fire at defined points: reflection after each turn, context compression when the window fills up, tool guardrails on every tool execution
- The loop continues until the model responds with a final message or the tool round limit is reached
Extending the Agent
Add custom tools by creating a module in ~/.agent/tools/ that conforms to the Tool shape. The plugin registry auto-discovers it on startup. See the Plugins page for details.