Skip to main content

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-index over scratch copies of its before and after content
  • gitgit-ops.ts owns invocation; hermeticGitEnv pins the config Git reads, requireGitVersion requires 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 ToolkitInput object 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) read appConfig and 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.log in state dir) and SQLite (trace.db in 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