How mouadse/pi-lite works
1. Overall shape
pi-lite is a single-process C++20 CLI agent. The center of the design is a small agent loop that alternates between model calls and tool execution. There is no server, no UI framework, and no plugin system; everything runs in one binary built with CMake.
2. Entry point and orchestration
src/main.cpp is the top-level entry. It wires together configuration, input handling, the agent loop, status output, and optional self-test mode. src/agent.cpp / agent.hpp appear to hold the main conversation orchestration: prompt assembly, message history, tool-call handling, context compaction, and the “call model → execute tools → feed results back” cycle described in the README.
3. Configuration boundary
src/config.* is the configuration layer. It likely merges CLI flags, real environment variables, and workspace .env values, then exposes runtime options such as model name, base URL, memory settings, shell permissions, and context-budget controls. This is an important boundary because it determines what external services the agent can talk to and which local capabilities are enabled.
4. Model client subsystem
src/llm_client.* is the OpenAI-compatible transport layer. It uses libcurl to call chat completions endpoints and consume SSE streaming responses. Architecturally, this module is the only component that knows about HTTP, streaming deltas, and provider-specific API details; everything else works with normalized assistant/tool messages.
5. Conversation data model
src/messages.hpp likely defines the shared message and tool-call structures used across the agent, tools, and memory layers. This is the glue that keeps user messages, assistant responses, tool requests, and tool results in a uniform format. The architecture depends on this shared model to move data cleanly between the LLM client and the tool executor.
6. Tooling subsystem
src/tool.* and src/tools.* form the model-facing tool registry and dispatcher. The README shows the exposed tools: file reads/writes, directory listing, grep, patch application, optional bash, and memory operations. This subsystem is a major architectural boundary because it translates model tool calls into constrained local actions and returns structured results back into the agent loop.
7. Workspace safety and secret protection
The file tools are intentionally sandboxed to the workspace. Secret-like files such as .env, credentials, and SSH keys are blocked from reads, writes, grep, and patching. That safety policy is part of the tool boundary, not the model layer, which keeps the LLM from directly accessing sensitive local state even when it asks for files.
8. Long-term memory subsystem
src/memory.* implements optional local memory backed by SQLite. The README indicates a fully local store with hashed embeddings and in-process cosine search, plus save/list/search/update/delete operations. Architecturally, memory is a separate persistence layer that can be queried before model calls and can also be written explicitly by tools or automatically through the opt-in capture flow.
9. Automatic memory capture path
Memory search and memory injection happen on the hot path when memory is enabled: relevant memories are retrieved and inserted into the system prompt as a short “Relevant long-term memory” section. Auto-capture is a second, separate path that extracts durable facts after interactions, filters noisy or unsafe content, deduplicates locally, and persists only the curated facts. That separation matters because memory is not just a log; it is a gated knowledge store.
10. Interactive terminal I/O
src/line_reader.* and src/status_indicator.* handle the human-facing terminal experience. The line reader likely manages prompt editing, autocomplete, history, and shortcuts like /help or !command. The status indicator handles “thinking”, elapsed-time, and tool progress badges, with stdout/stderr separation so streamed assistant output stays clean when piped.
11. Rendering layer
src/markdown_renderer.* is a lightweight terminal presentation layer for assistant output. It converts common Markdown shapes into ANSI-styled terminal text: headings, lists, emphasis, inline code, fenced code blocks, blockquotes, and simple tables. This sits between streamed model text and terminal output, and it degrades gracefully when output is not an interactive terminal or when NO_COLOR=1 is set.
12. Shared utilities and support code
src/util.* probably hosts cross-cutting helpers such as path handling, string utilities, token/cost estimation helpers, and other small functions needed across modules. src/linked_list.hpp suggests a small internal container or queue structure, likely used for message history or compaction bookkeeping where a lightweight custom structure is preferred over heavier abstractions.
13. Runtime data flow
The core data flow is: user input enters through stdin or the line reader, the agent builds a prompt from system instructions plus conversation history, optional memory is retrieved and injected, the LLM streams a response, tool calls are accumulated, tools execute locally, tool results are appended as messages, and the loop repeats until the assistant finishes without more tool calls. This is the main architecture to draw: user terminal ↔ agent ↔ LLM client ↔ tools/memory, with streamed text and tool results flowing back into the same conversation state.
14. External dependencies and persistence
The project depends on libcurl for HTTP streaming, SQLite for memory persistence, nlohmann-json for structured payloads, and the patch command for unified-diff application. The binary also reads .env for configuration but keeps that separate from file-tool access. Those dependencies matter architecturally because they define the only persistent store, the network boundary, and the patch-based code-editing capability.
Main entry points: agent.cpp Agent loop · tools.cpp Tool registry · main.cpp main · config.cpp Config
Components 16
Runtime
Top-level binary and agent orchestration.
- main · Program entry that wires configuration, agent loop, terminal I/O, and self-test mode.
src/main.cpp - Agent loop · Coordinates prompt assembly, model calls, tool execution, and context compaction.
src/agent.cpp - Config · Merges CLI, environment, and workspace settings into runtime options.
src/config.cpp
Model
LLM transport and conversation state.
- Messages · Shared message, tool-call, and tool-result structures passed across subsystems.
src/messages.hpp - LLM client · OpenAI-compatible chat/completions transport with streaming response handling.
src/llm_client.cpp
Tools
Local action execution and safety boundaries.
- Tool registry · Defines model-facing tools and routes tool calls to local executors.
src/tools.cpp - Tool API · Shared tool definitions and execution contracts.
src/tool.cpp - File tools · Workspace-scoped file and directory actions with secret-file protections.
- Shell tool · Optional constrained bash execution path exposed to the agent.
Memory
Optional persistent long-term memory.
- Memory core · Local persistent memory store with search and CRUD operations.
src/memory.cpp - Memory capture · Filters and persists durable facts from interactions, including deduplication and opt-in capture.
Terminal
Interactive input and output presentation.
- Line reader · Interactive prompt, history, shortcuts, and command entry handling.
src/line_reader.cpp - Status · Thinking, elapsed-time, and tool-progress display for terminal sessions.
src/status_indicator.cpp - Markdown render · Formats assistant output into ANSI-styled terminal text.
src/markdown_renderer.cpp
Support
Shared helpers and small internal data structures.
- Utilities · Cross-cutting helpers for strings, paths, budgeting, and other shared logic.
src/util.cpp - List queue · Lightweight linked-list structure used for internal history or bookkeeping.
src/linked_list.hpp
Connections 19
- main → Config: loads config · Startup reads runtime settings before constructing the agent.
- main → Line reader: reads input · Main process routes user interaction through the terminal reader.
- main → Agent loop: starts loop · Entry point hands control to the agent orchestrator.
- Agent loop → Messages: uses messages · Conversation state is represented with shared message structures.
- Agent loop → LLM client: calls model · Agent sends prompts and receives streamed assistant output.
- Agent loop → Tool registry: executes tools · Tool calls from the model are dispatched locally.
- Agent loop → Memory core: queries memory · Relevant memories are retrieved and injected when enabled.
- Agent loop → Memory capture: captures facts · Post-interaction durable facts can be extracted and stored.
- LLM client → Messages: emits replies · Normalized assistant and tool messages are produced from streamed responses.
- Tool registry → Tool API: implements · Registry uses the shared tool interface and execution contracts.
- Tool registry → File tools: file ops · Filesystem tools perform sandboxed workspace reads and writes.
- Tool registry → Shell tool: shell ops · Optional command execution is exposed through the tool layer.
- File tools → Config: enforces policy · Workspace and secret-access rules are derived from runtime configuration.
- Memory core → Config: uses settings · Memory behavior depends on enabled features and budget limits.
- Line reader → Status: updates status · Interactive input coordinates with progress display.
- Agent loop → Markdown render: renders output · Assistant text is formatted for terminal display.
- Markdown render → Utilities: uses helpers · Rendering relies on shared formatting and utility helpers.
- Agent loop → Utilities: uses helpers · Agent orchestration depends on shared utility functions.
- Agent loop → List queue: tracks history · Internal bookkeeping may use a lightweight linked structure.