Architecture
Acolyte runs as a headless daemon with typed RPC clients, explicit lifecycle phases, observable effects, scoped tools, and persistent memory.
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 for the renderer, Chat Presentation for the chat-state-to-terminal pipeline
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 → toolkit → registry
- budget — step-budget check inlined into tool execution
- toolkit — domain tool definitions
- registry — toolkit registration and agent-facing tool surface
- diff — every file and code edit reported through
git diff --no-indexover scratch copies of its before and after content - git —
git-ops.tsowns invocation;hermeticGitEnvpins the config Git reads,requireGitVersionrequires 2.14 or newer - 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 the terminal step, persist outputs, emit final response
-
model-host protocol — model completes with a native
end_turn(a step with no tool calls); that step’s text is the final response, backstopped by finish-reason classification (incomplete finishes reopen once then error; unrecoverable finishes error immediately) -
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) — 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) to access 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 (
server.login state dir) and SQLite (trace.dbin data dir); the CLI queries SQLite for indexed trace lookups. See Paths for platform-specific locations - 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