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

  1. Core lifetime is a strict subset of Edge lifetime – mechanically enforced, not advisory.
  2. Mutual exclusion on side-effect tools – only one lane owns “the limbs” at a time.
  3. Injection is append-only – never mutates history, never resets budgets.
  4. True log is immutable and authoritative – the arbiter’s in-memory log is the runtime authority.
  5. No hidden recursion – explicit states, explicit transitions, no graph DSL.
  6. Core never sends raw chain-of-thought – only workflow summaries.
  7. Agent identity is unified – Edge and Core are the same agent to the user.
  8. Container is disposable – restart is cheap, workspace is non-authoritative, secrets are ephemeral.
  9. LLM proposes, control plane decides – all side effects serialized through the arbiter.
  10. Keep it boring – single host, single DB, minimal components, strong outer boundary.
  11. At most one active instance per agent – choird rejects agent start if 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.