Page
Architecture
Choir is an LLM agent orchestration system built on the principle of deterministic outer control plane + stochastic inner cognition: the LLM proposes, the control plane decides.
Components
| Component | Location | Role |
|---|---|---|
| choird | Host daemon | Control plane: Telegram gateway (multi-bot, multi-DM routing), container lifecycle, tool/skill registry authority, policy enforcement, session leasing, approval pipeline, memory/embeddings module, Postgres connection pooling. |
| choirctl | Host CLI | Stateless admin interface to choird. Approve proposals, manage sessions, inspect logs, add tools/skills, control policies. |
| choir-agent | Docker container | Unified cognition + syscall runtime. Single Go binary, single process. Manages state machines, edge/core lanes, skills, LLM requests, tool execution, lock manager, secret store, choird RPC client. |
.choir.d/ |
Host filesystem | Choird state directory. Contains config.json (static resource config), secrets.json (authoritative secrets), local clone cache (repos/), logs, and user-local sockets (socks/). |
choird Internal Modules
choird/
control_plane/ # lifecycle, policy, approvals
agent_lifecycle/ # container management, leasing
memory/ # Postgres/pgvector memory subsystem
embeddings/ # embedding pipeline, batch processing
approvals/ # approval queue and resolution
gateway/ # multi-bot Telegram routing
Memory is folded into choird (not a separate service) because on a single host with a single operator, an extra service adds complexity without adding meaningful isolation.
choir-agent Internal Structure
type Agent struct {
arbiter *Arbiter // serializes all committed side effects
llm *LLMEngine
skills *SkillEngine
tools *ToolExecutor
locks *LockManager
secrets *SecretStore
rpc *ChoirdClient
}
The agent is a single binary with logical separation. The container boundary is the sandbox; internal process-level separation adds complexity without meaningful security gain.
Data Flow
User(s)
| (multiple Telegram bots, multiple DMs)
v
choird (host)
|-- gateway (multi-bot Telegram, DM routing, admin/regular permissions)
|-- control plane (lifecycle, policy, approvals)
|-- memory module -> Postgres (+pgvector, per-agent schema)
|-- embedding client -> embedding API
|-- browser worker (Playwright, host-side, per-agent contexts)
|-- search client (Brave API)
|-- log manager -> .choir.d/logs/
|
|-- [transport: UDS ~/.choir.d/socks/choird.sock OR HTTP :9400]
|
v
Docker container
|-- choir-agent (single Go binary, single process)
|-- Edge lane (goroutine, fast model, user-facing)
|-- Core pool (multiple named core jobs, each a goroutine)
|-- Arbiter (serializes all committed side effects)
|-- Lock manager
|-- Secret store (in-memory only)
|-- Tool executor
|-- choird RPC client (UDS or HTTP, selected at startup)
|
|-- /choir (read-only, image-baked identity + tools)
|-- /workspace (writable, bind-mounted persistent directory)
| +-- .choirtmp/send/ (agent -> choird file staging)
| +-- .choirtmp/recv/ (choird -> agent file staging)
Message Flow (Telegram to Agent)
Telegram -> Bot API (long-poll) -> choird gateway module
-> identify bot instance + user ID -> look up bound DM + agent
-> route to edge lane of bound agent (queued)
-> edge response -> choird gateway -> Telegram reply to DM
RPC Flow (Agent to Host)
choir-agent communicates with choird over 8 RPC verbs via UDS (default) or HTTP:
| Verb | Direction | Purpose |
|---|---|---|
INIT_HELLO |
agent -> choird | Identity handshake, receive resource bindings and recovery snapshot |
GET_SECRETS |
agent -> choird | Request secret values for bound resources |
HEARTBEAT |
agent -> choird | Replicate committed events, sync config version |
REQUEST_APPROVAL |
agent -> choird | Submit proposals requiring human approval |
REPORT_STATUS |
agent -> choird | Lane state, skill progress, budget remaining |
TERMINATE_SELF |
agent -> choird | Graceful self-termination |
FETCH_DYNAMIC_CONFIG |
agent -> choird | Pull updated hot-reloadable config |
EXECUTE_HOST_TOOL |
agent -> choird | Dispatch tools that require host-side execution |
Trust Boundaries
Three real trust boundaries enforce security:
1. Host <-> Container (Strong)
The container runs with namespaced root. Prevented: --privileged, Docker socket mount, --pid=host, --network=host, device passthrough. Only /workspace and the control socket are bind-mounted. The container cannot cause host-level side effects without explicit human approval.
2. Choird <-> Agent RPC (Policy)
Every RPC call carries a lease-scoped authentication token, generated by choird at container creation and scoped to a single session. Rate limits and payload size caps are enforced. No RPC verb can directly perform host-destructive operations.
3. Tool Lock System (Serialization)
Resource-level mutual exclusion ensures only one lane owns side-effect tools at a time. Lock states: FREE, S_LOCKED(count) (shared/read), X_LOCKED(owner) (exclusive/write). All locks in a tool’s lockset are acquired atomically (all-or-nothing).
Design Invariants
- Core lifetime is a strict subset of Edge lifetime – mechanically enforced, not advisory.
- Mutual exclusion on side-effect tools – only one lane owns “the limbs” at a time.
- Injection is append-only – never mutates history, never resets budgets.
- True log is immutable and authoritative – the arbiter’s in-memory log is the runtime authority.
- No hidden recursion – explicit states, explicit transitions, no graph DSL.
- Core never sends raw chain-of-thought – only workflow summaries.
- Agent identity is unified – Edge and Core are the same agent to the user.
- Container is disposable – restart is cheap, workspace is non-authoritative, secrets are ephemeral.
- LLM proposes, control plane decides – all side effects serialized through the arbiter.
- Keep it boring – single host, single DB, minimal components, strong outer boundary.
- At most one active instance per agent – choird rejects
agent startif already running.
.choir.d/ Directory Structure
.choir.d/
config.json # static resource configuration (source of truth)
secrets.json # authoritative secret values (strict JSON, mode 0600)
repos/ # choird-managed local clone cache
global/ # clone of global tools/skills/identity repo
agents/
<agent-id>/ # clone of per-agent repo
socks/ # user-local unix sockets
choird.sock # default choird UDS socket
logs/ # structured log files
choird.log # active choird log
<agent-id>.log # active per-agent log
archive/ # compressed archived logs
For the full design specification, see DESIGN.md.