Skip to main content
ChatCLI is a modular Go project organized into packages with well-defined responsibilities. Following a Phase 3 monolith decomposition effort, all major files have been broken down into focused, single-responsibility modules following SOLID principles. This page documents the internal architecture for contributors and advanced users.

Package Overview


Main Struct: ChatCLI

The ChatCLI struct in cli/cli.go is the heart of the system. Its most important fields are organized by responsibility:

LLM and Provider

History and Compaction

Execution Control

Subsystems

Auxiliary State


Startup Flow

The diagram below shows the complete boot sequence, from main.go to the interactive loop:

detectProjectDir()

Walks up from the current working directory to the filesystem root looking for project markers:
  1. .agent/ (explicit ChatCLI marker) β€” takes priority
  2. .git/ (common convention)
Returns the project root path, or "" if no marker is found.

Message Flow: Chat Mode

In normal interactive mode, each user message follows this pipeline:

Type-ahead (messageQueue)

Messages typed while the LLM is processing are stored in a FIFO queue (messageQueue). After each response, the system drains the queue and processes each message sequentially, eliminating the need to re-type. Coder mode has its own queue with a real-time visual indicator (β–Ό N msg queue) when messages are pending.

Input guard (anti-typeahead in security prompts)

Distinct from the β€œgood” typeahead above, when a security box appears mid-turn β€” three layers discard queued input to prevent consuming bytes as y/n:
  • TTY flush β€” TCIFLUSH (Linux), TIOCFLUSH (BSD/Darwin), FlushConsoleInputBuffer (Windows).
  • Drain channel β€” empties the non-blocking reader buffer.
  • 250ms debounce β€” discards input arriving in the first 250ms after the box renders.
Additionally, at the start of every agent turn, stty sane runs on the controlling /dev/tty to recover from a prior go-prompt teardown that may have left the terminal in raw mode.

Message Flow: Agent/Coder Mode

Entering agent mode uses a panic/recover mechanism to exit the go-prompt loop:

Anchor Reminder

At each turn, the agent injects a short reminder into the history to keep the LLM focused on the original task. This prevents drift in long conversations.

Tool Call Parsing

The parser in cli/agent/toolcall_parser.go uses a stateful scanner (not regex) for robustness:

Supported Formats

Scanner Algorithm

  1. Search for <tool_call case-insensitively
  2. Verify the next char is whitespace or > (not part of another tag like <tool_caller>)
  3. scanTagEnd(): advance forward respecting quotes (single and double)
    • Inside quotes, > is treated as literal text
    • Supports HTML entities (&gt;, &quot;, etc.)
  4. Extract name and args attributes regardless of order
  5. If self-closing fails, try paired tags with </tool_call>
  6. Fallback to JSON: try parseJSONToolCalls() in parallel

Why Not Regex?

Tool arguments frequently contain >, ", and nested JSON. Regex cannot distinguish > inside a quoted attribute from > that closes the tag. The stateful scanner solves this by tracking quote state.

Argument Sanitization Pipeline

After parsing, each tool call goes through a 7-step pipeline in agent_tool_sanitizer.go:

History Compaction (3 Levels)

The HistoryCompactor in cli/history_compactor.go manages history size through a progressive pipeline:

Agent Mode Trigger

In agent mode, compaction is checked at each turn of the ReAct loop. The trigger fires when token usage exceeds 60% of the model’s budget.

Checkpoint/Rewind System

rewind.go implements a history snapshot system:

Structure

Behavior

  • When saved: Before each LLM call (saveCheckpoint())
  • Limit: Maximum of 20 checkpoints (FIFO β€” oldest are discarded)
  • Deep copy: Each checkpoint contains a complete, independent copy of the history
  • Trigger: Press Esc+Esc (two Esc presses within 500ms) to open the rewind menu
  • Restore: The user selects a checkpoint, and the history is replaced with the saved copy

Provider Registry

Auto-registration Pattern

Each LLM provider auto-registers via init() in the llm/registry package:

ProviderInfo

Discovery Flow

This pattern eliminates switch/case blocks. To add a new provider, just create a package with register.go and implement the LLMClient interface.

Memory Worker (Background Process)

The memoryWorker in cli/memory_worker.go extracts memories from the conversation in the background:

Parameters

Triggers

  1. nudge(): Called after each LLM response. If there are >= 4 new messages and cooldown has expired, runs extraction
  2. 3-minute ticker: The background loop checks periodically (for long sessions where the user types infrequently)
  3. Compaction ticker (6h): Consolidates old facts with low scores
  4. Cleanup ticker (24h): Removes expired daily notes

Extraction Pipeline


Main Components

CLI and Modes

The ChatCLI struct in cli/cli.go is the central point (~923 lines after decomposition). The Start() method initiates interactive mode using Bubble Tea (Charmbracelet). Helper methods, history management, mode switching, output formatting, prompt building, session management, and tool handling have been extracted into dedicated files (cli_helpers.go, cli_history.go, cli_mode.go, cli_output.go, cli_prompt.go, cli_session.go, cli_tools.go). Mode switching uses a panic/recover mechanism to exit the go-prompt loop. ChatCLI uses a unified history (cli.history) shared across all modes. When switching modes, the full context is preserved. The /compact and /rewind commands operate directly on this single history.

Message Bus

The cli/bus package implements a typed message bus with:
  • Pub/sub with channel and type filters
  • Request-reply with correlation IDs
  • Atomic throughput metrics

Multi-Agent

The orchestration system in cli/agent/workers manages 12 specialist agents running in parallel goroutines with a configurable semaphore. Each worker has:
  • Isolated mini ReAct loop (observe -> reason -> act)
  • Own skills (accelerator and descriptive scripts)
  • File locks (mutex per-filepath)
  • Configurable timeout and max turns

Technologies


Design Patterns

  • Auto-registration via init() β€” Providers register themselves automatically
  • Interface-driven β€” LLMClient and ToolAwareClient for polymorphism
  • Fallback chain β€” Intelligent error classification + exponential cooldown
  • Stateful parser β€” XML parsing with escaped attributes (more robust than regex)
  • embed.FS β€” i18n files embedded in the binary (always uses /, never filepath.Join)
  • Panic/recover β€” Mode switching in go-prompt without restarting the process
  • Unified history β€” A single message array shared across chat, agent, and coder modes
  • Checkpoint/rewind β€” Deep copy of history before each LLM call, with selective restore
  • 3-level compaction β€” Near-lossless trimming, structured summarization, emergency truncation
  • Type-ahead queue β€” Messages typed during processing are queued and drained automatically
  • Workspace context injection β€” Bootstrap + memory automatically injected into every system prompt
  • Unified system prompt with caching β€” Attached contexts (/context attach) flow through the system prompt via SystemParts with CacheControl hints, enabling provider-level prompt caching (Anthropic cache_control: ephemeral, OpenAI automatic caching, Google context caching)
  • Background memory extraction β€” Worker goroutine extracts and consolidates memories from the conversation without blocking the main flow