Architecture
On Sun, 04 Oct 2026, by @lucasdicioccio, 1409 words, 13 code snippets, 0 links, 1images.
Generated from documentation/architecture.md, the repository is the canonical source and may be ahead of this page.
Architecture

This document describes the runtime architecture and core components of the Agents framework.
Core Architecture
The framework is built around a layered architecture that separates concerns between agent definition, runtime execution, and user interfaces.
Layer Overview
┌────────────────────────────────────────────────────────────────┐
│ Interface Layer │
│ (CLI commands, TUI, MCP server, HTTP endpoints) │
│ The TUI and the HTTP server (agents-server) are both clients │
│ of an in-process SessionRunner (Host.Runner), driven through │
│ the same RunnerClient interface -- see tui.md#architecture. │
├────────────────────────────────────────────────────────────────┤
│ OS Model Layer │
│ (Entity-Component-System, Resource Management, │
│ Conversation Tracking, Concurrent Access) │
├────────────────────────────────────────────────────────────────┤
│ Agent Tree Layer │
│ (multi-agent hierarchy, reference validation, cycle detection) │
├────────────────────────────────────────────────────────────────┤
│ Foundation Layer │
│ (sessions, tools, LLM integration, file loading) │
└────────────────────────────────────────────────────────────────┘
OS Model Architecture
The OS Model provides a centralized, ECS-based architecture for managing agents, toolboxes, and resources.
Entity-Component-System (ECS) Pattern
┌─────────────────────────────────────────────────────────────────┐
│ World │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Component │ │ Component │ │ Component │ │
│ │ Store 1 │ │ Store 2 │ │ Store N │ │
│ │ (TVar Any) │ │ (TVar Any) │ │ (TVar Any) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ HashMap ComponentTypeId │
└──────────────────────────┬──────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Entity │ │ Entity │ │ Entity │
│ 1 │ │ 2 │ │ N │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Agent │ │Toolbox │ │ Conv │
│ Config │ │ Config │ │ Config │
│ Agent │ │Toolbox │ │ Conv │
│ State │ │ State │ │ State │
└─────────┘ └─────────┘ └─────────┘
Key Design Principles:
- Entities are just IDs: Lightweight identifiers with phantom types for type safety
- Components are pure data: Serializable, immutable data structures
- Systems are functions: Operate on entities with specific component combinations
- Storage is heterogeneous: Uses
TVar Anyfor type erasure with safe casting
Component Types
Agent Components
| Component | ID | Purpose |
|---|---|---|
AgentConfig | 1 | Static agent configuration (name, model, system prompt) |
AgentState | 2 | Runtime state (status, current conversation) |
Toolbox Components
| Component | ID | Purpose |
|---|---|---|
ToolboxConfig | 3 | Toolbox type and settings |
ToolboxState | 4 | Runtime state and resource reference |
ToolboxBinding | 5 | Agent-to-toolbox relationship |
Conversation Components
| Component | ID | Purpose |
|---|---|---|
ConversationConfig | 30 | Conversation metadata |
ConversationState | 31 | Runtime status and timestamps |
AgentConversation | 32 | Agent-conversation relationship |
TurnConfig | 33 | Turn structure (parent, conversation) |
TurnState | 34 | Turn execution state |
ToolCallConfig | 35 | Tool call specification |
ToolCallState | 36 | Tool call execution state |
Message | 38 | Chat messages |
Resource Lifecycle Flow
Program Startup
│
▼
┌─────────────┐
│ Initialize │
│ World │
└──────┬──────┘
│
▼
┌─────────────┐ ┌─────────────┐
│ Create │────>│ Register │
│ Agents │ │ Component │
└──────┬──────┘ │ Stores │
│ └─────────────┘
▼
┌─────────────┐ ┌─────────────┐
│ Create │────>│ Register │
│ Toolboxes │ │ Resources │
└──────┬──────┘ └─────────────┘
│
▼
┌─────────────┐
│ Bind │
│ Agents │
│ to Toolboxes│
└──────┬──────┘
│
▼
┌────────┐
│ RUNTIME │
└────┬───┘
│
┌────┴────┬──────────┬──────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ Agent │ │ Agent │ │Shared │ │ Shared│
│ 1 │ │ 2 │ │SQLite │ │ HTTP │
│ │ │ │ │ DB │ │ Pool │
└───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘
│ │ │ │
└─────────┴─────────┴─────────┘
│
▼
┌─────────────┐
│ Cleanup │
│ Scope │
│ (on destroy)│
└─────────────┘
Resource Scopes:
- Program Scope: Global resources (HTTP connection pools, shared caches)
- Agent Scope: Per-agent resources (sandbox directories, agent-specific state)
- Toolbox Scope: Per-toolbox resources (SQLite connections, MCP clients)
- Conversation Scope: Per-conversation resources (isolated Lua states, temp files)
- Turn Scope: Temporary resources (single turn execution context)
- ToolCall Scope: Single-use resources (tool call arguments, results)
Concurrent Access Patterns
┌──────────────────────────────────────────────────────────────┐
│ Access Patterns │
├──────────────────────────────────────────────────────────────┤
│ │
│ Exclusive Access (TMVar) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Lock │────>│ Execute │────>│ Release │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ Use: Lua interpreters, process handles │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ Read-Write Access (RWLock) │
│ ┌─────┐ ┌─────┐ ┌─────────┐ ┌─────┐ ┌─────┐ │
│ │Read │ │Read │──────>│ Data │<─────│Write│ │ │
│ │ 1 │ │ 2 │ │ │ │ │ │ │
│ └─────┘ └─────┘ └─────────┘ └─────┘ │ │
│ Use: SQLite databases (especially WAL mode) │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ Pool Access (TBQueue) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Pool: [Token] [Token] [Token] ... [Token] │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▲ │ ▲ │ │
│ │ └─────────┘ │ │
│ Acquire Release │
│ Use: HTTP connection pools, DB connection pools │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ Stateless Access (No Lock) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Access │ │ Access │ │ Access │ (Concurrent, no sync) │
│ └─────────┘ └─────────┘ └─────────┘ │
│ Use: Immutable data, thread-safe resources │
│ │
└──────────────────────────────────────────────────────────────┘
Conversation and Lineage Tracking
Conversation Tree Structure
┌─────────────────┐
│ Conversation 1 │
│ (Entity) │
└────────┬────────┘
│
┌────┴────┐
▼ ▼
┌───────┐ ┌───────┐
│ Turn 1│ │ Turn 2│
│(Entity)│ │(Fork) │
└───┬───┘ └───┬───┘
│ │
▼ ▼
┌───────┐ ┌───────┐
│ Call 1│ │ Call 1│
│ │ │ │
└───┬───┘ └───┬───┘
│ │
▼ ▼
┌───────┐ ┌───────┐
│ Call 2│ │ Call 2│
│(Nested)│ │(Nested)│
└───────┘ └───────┘
Lineage Stack
┌─────────────────────────────────────┐
│ LineageFrame │
│ ├─ frameType: ToolCallFrame │
│ ├─ frameEntityId: <tool-call-id> │
│ └─ frameTimestamp: <time> │
├─────────────────────────────────────┤
│ LineageFrame │
│ ├─ frameType: TurnFrame │
│ ├─ frameEntityId: <turn-id> │
│ └─ frameTimestamp: <time> │
├─────────────────────────────────────┤
│ LineageFrame │
│ ├─ frameType: ConversationFrame │
│ ├─ frameEntityId: <conversation-id> │
│ └─ frameTimestamp: <time> │
└─────────────────────────────────────┘
Lineage provides:
- Complete call chain for debugging
- Recursion depth tracking
- Audit trail for accounting
- Context for subagent calls
Persistence Layer
┌──────────────────────────────────────────────────────────────┐
│ Persistence Backends │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ In-Memory│ │ File │ │ SQLite │ │PostgreSQL│ │
│ │ (Dev/Test)│ │(Compat) │ │ (Local) │ │(Production) │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴──────┬──────┴─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Unified API │ │
│ │ (persist, load) │ │
│ └─────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
SQLite Schema (simplified)
┌──────────────────────────────────────────────────────────────┐
│ entities (id, entity_type, created_at) │
│ components (entity_id, component_type, data) │
│ events (id, timestamp, type, data, entity_id) │
│ messages (conversation_id, timestamp, role, content) │
│ tool_calls (turn_id, timestamp, name, input, output) │
└──────────────────────────────────────────────────────────────┘
Architecture Benefits
- Shared Resources: Multiple agents can share toolboxes (e.g., same SQLite database)
- Resource Pooling: HTTP connections pooled across all agents
- Better Lifecycle Management: Explicit scopes with predictable cleanup
- Foundation for Web API: Centralized state enables HTTP server interface
- Durable Persistence: Built-in persistence layer with multiple backends
- Thorough Lineage Tracking: Complete call chains for debugging
Core Types
Base Types (System.Agents.Base)
-- Unique identifiers
newtype AgentId = AgentId UUID
newtype ConversationId = ConversationId UUID
newtype StepId = StepId UUID
-- Agent configuration
data Agent = Agent
{ slug :: Text -- Unique identifier
, apiKeyId :: Text -- Reference to API key
, flavor :: Text -- LLM provider (openai, etc.)
, modelUrl :: Text -- API endpoint
, modelName :: Text -- Model identifier
, announce :: Text -- User-facing description
, systemPrompt :: [Text] -- System instructions
, toolDirectory :: FilePath -- Path to tools
, mcpServers :: Maybe [McpServerDescription]
, extraAgents :: Maybe [ExtraAgentRef]
, builtinToolboxes :: Maybe [BuiltinToolboxDescription]
}OS Types (System.Agents.OS.Core)
-- Phantom-typed entity IDs
newtype AgentId = AgentId EntityId
newtype ToolboxId = ToolboxId EntityId
newtype ConversationId = ConversationId EntityId
-- Agent components
data AgentConfig = AgentConfig
{ agentName :: Text
, agentModel :: ModelConfig
, agentSystemPrompt :: Text
, agentToolboxBindings :: [ToolboxBindingSpec]
}
data AgentState = AgentState
{ agentStatus :: AgentStatus
, agentCurrentConversation :: Maybe ConversationId
, agentCreatedAt :: UTCTime
}Agent Tree System
The AgentTree module manages multi-agent hierarchies and handles agent discovery, reference validation, and cycle detection.
Tree Structure
Agents form a directed graph where:
- Parent-child edges: Discovered from tool directory hierarchy
- Extra reference edges: Explicit references via
extraAgents
┌─────────────┐
│ root-agent │
└──────┬──────┘
│
┌───┴───┐
▼ ▼
┌──────┐ ┌──────┐
│tool-a│ │tool-b│
└──┬───┘ └──────┘
│
▼
┌──────┐
│sub-1 │
└──────┘
Subagent Wiring
The Agent Tree system supports dynamic tool registration via STM TVars:
-- OS-native agents use STM TVar for mutable tool storage
wireAgentTools :: Props -> AgentConfigGraph -> Map AgentSlug OSAgentNode -> IO ()Conversation Flow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │────>│ Session │────>│ LLM │
│ Input │ │ (turns) │ │ (tools) │
└─────────────┘ └──────┬──────┘ └──────┬──────┘
│ │
│ ┌─────────────┐ │
└───>│ Tool Call │<─┘
│ Execution │
└──────┬──────┘
│
┌───────────┴───────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Bash Tool │ │ MCP Tool │
└─────────────┘ └─────────────┘
Tool Registration
Tools are registered with the LLM via the ToolRegistration type:
data ToolRegistration = ToolRegistration
{ toolName :: Text
, toolDescription :: Text
, toolParameters :: Value -- JSON Schema
, toolExecutor :: Value -> IO ToolResult
}Tool Sources
- BashToolbox: Executable scripts in the tool directory
- McpToolbox: MCP servers providing dynamic tool lists
- OpenAPIToolbox: REST API operations from OpenAPI specs
- IOTools: Haskell functions embedded in the runtime
- SystemToolbox: Builtin system information tools
- Subagent Tools: Other agents exposed as callable tools
Libraries and executables
agents.cabal builds several components:
| Component | Source | Contents |
|---|---|---|
agents-lib (public library) | src/ | The core: agents, tools, sessions, storage, the OS layer, the MCP stdio server, CLI commands other than the TUI. No terminal-UI dependencies. |
agents-postgres (public library) | postgres/ | Session and continuation stores in Postgres, for withHostStores. Kept apart so that agents-lib does not need libpq. |
agents-tui (public library) | tui/ | The terminal UI (System.Agents.TUI.*, CLI.TUI, CLI.Config, CLI), on brick and vty. |
agents-exe | app/ | The command-line tool, on both libraries. |
agents-server-internal, agents-server | examples/agents-server/ | The HTTP server (wai, warp). See [agents-server.md](/docs-agents-server.html). |
agq, durable-workflow-demo | agq/, examples/durable-workflow-demo/ | Other executables. |
Programs that embed agents depend on agents-lib only.
Module Dependencies
Main
├── AgentTree
│ ├── Base
│ ├── FileLoader
│ └── OS.Core
├── CLI.*
│ └── AgentTree
├── TUI
│ ├── Host, Host.Runner, Host.Client (RunnerClient)
│ └── Session (pure views only: usage, signals, trajectory)
├── MCP.Server
│ └── AgentTree
└── ExportImport.*
OS Layer
├── OS.Core
│ ├── OS.Core.Types
│ └── OS.Core.World
├── OS.Resources
│ ├── OS.Resources.Types
│ ├── OS.Resources.Sqlite
│ ├── OS.Resources.Lua
│ └── OS.Resources.Http
├── OS.Concurrent
│ ├── OS.Concurrent.Types
│ └── OS.Concurrent.Locks
├── OS.Conversation
│ ├── OS.Conversation.Types
│ └── OS.Conversation.Lineage
└── OS.Agents
Key Design Decisions
- STM for Concurrency: Tool reloading and subagent wiring use STM for thread-safe updates
- ECS Pattern: Enables flexible composition and powerful queries
- Explicit Resource Management: Predictable cleanup with explicit scopes
- Phantom Types: Type safety for entity IDs without runtime overhead
- Type Erasure:
Anyfor heterogeneous storage with safe casting via Component typeclass - Tracer Pattern: All side effects are traced for observability
- Two-Phase Initialization: Agent shells created first, then wired together to support cycles
Tradeoffs
- ECS Complexity: Adds indirection but enables powerful queries and flexible composition
- STM Overhead: Slight performance cost for composability
- Storage Overhead: Component storage uses more memory than direct fields but enables dynamic extension
- Type Erasure: Using
Anyrequires careful casting but enables heterogeneous storage