Architecture
System map of Acolyte’s runtime flow, module boundaries, and extension points.
First-class concepts
Every concept below is modeled as an explicit entity with typed contracts, its own module, and clear boundaries — not buried as implementation details.
- Sessions — persistent conversation context with history and state
- Tasks — state-machined units of work with stable IDs and per-task scoping
- Lifecycle phases — resolve, prepare, generate, finalize as separate modules
- Effects — lifecycle-owned side effects applied per-tool-result via callback
- Tools — typed definitions with categories, schemas, and output contracts
- Skills — declarative prompt extensions with metadata and tool restrictions
- Memory sources — pluggable memory tiers (session, project, user) with pipeline stages
- Protocol — typed RPC messages with request correlation and lifecycle envelopes
System flow
CLI → client → server → lifecycle → model + tools
- execution model: one active task per session, with ordered queued tasks
- yielding: lifecycle only yields at safe checkpoints (never mid-step)
TUI
React tree → reconciler → TUI DOM → serialize → terminal output
- custom React reconciler for terminal rendering
- details: see TUI
Daemon flow
client → rpc server → task queue → lifecycle worker
- rpc server: accepts requests, exposes task/status streams, and routes to queue/lifecycle
- task queue: enforces ordering, capacity, and cancellation boundaries
- lifecycle worker: executes accepted tasks through lifecycle phases
Session flow
- create or resume: resolve target session from ID prefix or active session
- lock: acquire session lock to prevent concurrent modification
- persist: save session state at checkpoints during chat
- details: see Sessions
Task flow
accept → queue → run → complete|fail|cancel
- accept: validate request and assign
task_id - queue: hold until runnable under queue policy
- run: execute lifecycle for active task
- complete|fail|cancel: emit terminal state and persist task outcome
- details: see Tasks
Tool layering
lifecycle → budget → cache → toolkit → registry
- budget: step-budget check inlined into tool execution
- cache: per-task reuse layer for read-only and search tool results
- toolkit: domain tool definitions (
file-toolkit,code-toolkit,git-toolkit,shell-toolkit,web-toolkit,checklist-toolkit,memory-toolkit,skill-toolkit) - registry: toolkit registration and agent-facing tool surface
- details: see Tooling
Lifecycle flow
resolve → prepare → generate → finalize
-
resolve: pick model and policy (sync, not a full phase)
-
prepare: build inputs, context, and tools
-
generate: run model + tool calls (one pass, effects applied per-tool-result)
-
finalize: accept lifecycle signal, persist outputs, emit final response
-
model-host protocol: model may explicitly signal
done/no_op/blocked; host validates against runtime state -
host/model boundary: host provides runtime structure; model decides how to complete the task
-
scheduling: yield checks happen between lifecycle decisions, never mid-step
-
details: see Lifecycle
Memory engine
Memory Engine
→ Memory Pipeline (ingest → normalize → commit)
→ Memory Toolkit (search, add, remove) — on-demand access
- Memory Engine composes source strategy, pipeline stages, and distill behavior to provide continuity across turns
- pipeline seams: normalization is strategy-injectable behind registry contracts
- on-demand access: the model uses memory toolkit tools (
memory-search,memory-add,memory-remove) to access and manage memory at runtime instead of upfront injection - integration: commit is best-effort background work at finalize; memory access is on-demand via toolkit
- details: see Memory
Dependency injection
- No container, no decorators — dependencies are passed as typed parameters with defaults from
appConfig. - Toolkit input — toolkit factory functions take a single
ToolkitInputobject containing per-request runtime data (e.g.createFileToolkit(input: ToolkitInput)). Output budgets are defined locally in each tool. - Defaults at the edge — library modules accept injected params; composition roots (
cli-command-registry,server-chat-runtime,cli-chat) readappConfigand pass values down. - Tests inject directly — tests pass config through the new params instead of mutating
appConfig.
Contracts
- error handling: tools emit failures/error codes; lifecycle surfaces them for the model to decide
- step budget: inlined into tool execution; blocks calls when budget is exhausted
- protocol: transport contract is transport-agnostic; see docs/protocol.md
Observability and state
- observability: lifecycle emits ordered debug events per request (calls, tool results, effect decisions, summaries, errors). Events are dual-written to logfmt (
~/.acolyte/daemons/server.log) and SQLite (~/.acolyte/trace.db); the CLI queries SQLite for indexed trace lookups - runtime config: loaded from user/project config
- state ownership: chat/session state and memory are persisted outside lifecycle and passed in as inputs
- task trace: RPC emits task-state transitions with stable
task_id:
accepted → queued → running → completed|failed|cancelled