Architecture

For contributors. This page covers the source layout, the agent loop, the event stream, storage, and how to build and test.

Source layout#

.
├── src/
│   ├── raggie.py               # entry point: subcommands, project detection, mode dispatch
│   ├── cli.py                  # argparse definitions
│   ├── interactive.py          # terminal chat loop, one-shot mode, @path:lines expansion
│   ├── io_backend.py           # I/O layer: terminal vs headless, confirmations, questions
│   ├── mcp_client.py           # MCP client manager (stdio + Streamable HTTP)
│   ├── raggie_dirs.py          # data dir name (.raggie / .raggie-dev) and DB paths
│   ├── db_migrations.py        # yoyo migration runner for the SQLite databases
│   ├── chat.py                 # watermelon-themed status messages
│   ├── Agent/
│   │   ├── agent.py            # Agent: completions, tool execution, handover, commit, health stats
│   │   ├── providers.py        # per-provider API behavior (reasoning pass-back, thinking toggles, headers)
│   │   ├── config.py           # loads ~/.config/raggie/, runs config migrations
│   │   ├── migrations.py       # config schema migrations (v2..v11)
│   │   ├── migrations/chat_db/ # chat database migrations
│   │   ├── chat_history_db.py  # chats, sessions, messages, todo lists, whitelist, history repair
│   │   ├── chat_lock.py        # cross-process per-chat file locks
│   │   ├── command.py          # CommandRegistry for in-chat commands
│   │   ├── tools.py            # ToolRegistry: name -> handler, argument injection
│   │   ├── thinking_modes.py   # thinking mode table and depth checks
│   │   └── git_manager.py      # snapshot repo in .raggie/git (dulwich)
│   ├── Commands/               # one module per in-chat command, registered in __init__.py
│   ├── Tools/                  # one module per tool, registered in __init__.py
│   │   ├── utils.py            # colors, cwd and ignore checks, path permission prompt, reindex hook
│   │   ├── shell.py            # Shell: sandbox, read-only detection, whitelist, cancellation
│   │   ├── dispatch_subagent.py# DispatchSubagent: create or resume child sessions, UI event bridge
│   │   ├── todo_list.py        # the nine todo list tools
│   │   └── ...
│   ├── skills/
│   │   ├── manager.py          # SkillManager: storage, Agent Skills import/export, auto-discovery
│   │   └── tool.py             # GetSkill and SetSkill handlers
│   ├── indexing/
│   │   ├── code_indexer.py     # indexing pipeline
│   │   ├── parse_worker.py     # multiprocessing parse worker
│   │   ├── extractors.py       # symbol extraction
│   │   ├── node_utils.py       # tree-sitter helpers, branch counting
│   │   ├── language_config.py  # languages, extensions, node types
│   │   ├── queries.py          # index queries, complexity and bloat scoring
│   │   ├── code_index_sdk.py   # CodeIndexSDK, the query facade used by the agent
│   │   ├── models.py, frontend_models.py
│   │   ├── frontend/           # HTML, CSS, JSX, Vue, Svelte extraction and resolution
│   │   ├── frontend_config.py  # .raggie/frontend_config.json
│   │   ├── migrations/index_db/# index database migrations
│   │   └── cli.py, export_to_json.py, sdk_examples.py
│   ├── RAG/
│   │   ├── find.py             # symbol lookup with fuzzy fallback
│   │   ├── graph.py            # file structure view and call tree walk
│   │   └── document.py         # symbol description storage
│   ├── acp_server/
│   │   ├── __init__.py         # run_stdio(), run_http(), run_web()
│   │   ├── agent.py            # RaggieACPAgent: sessions, event mapping, web extensions
│   │   ├── http_transport.py   # JSON-RPC over HTTP + SSE, static file serving
│   │   └── webui/              # bundled web UI build (generated by sync_webui.py)
│   └── config/                 # defaults copied to ~/.config/raggie on first use
│       ├── roles.json
│       ├── tools.json
│       ├── mcp_servers.json
│       └── coder_system_prompt.md
├── web-ui/                     # Svelte 5 + TypeScript + Vite frontend (pnpm)
├── vscode-extension/           # VS Code extension: sidebar chat, server manager, copy reference
├── tests/                      # pytest suite
├── docs/
├── sync_webui.py               # copies web-ui/dist into src/acp_server/webui
├── build_all.py                # full build: web UI, package data, extension .vsix
└── pyproject.toml

The agent loop#

Agent.start(prompt) in src/Agent/agent.py is a generator that yields events.

  1. Re-index. Incremental index refresh.
  2. Commands. The prompt is offered to the CommandRegistry. A matching command runs locally. Its printed output is captured and emitted as a notice. Most commands end the turn there. /use returns text that becomes the prompt.
  3. Snapshot. The main agent commits the working tree if it changed.
  4. Persist. The user message is appended to history and saved.
  5. Loop:
    1. Call the model through _create_completion, the single choke point. It repairs the message sequence, normalizes content, applies provider-specific message filtering and request parameters, and strips forbidden ones.
    2. Save the assistant message.
    3. No tool calls: hand over if the context is nearly full and continue, otherwise commit, emit health stats and return.
    4. Tool calls: execute each through the ToolRegistry, saving every result. Then estimate the tokens just added (characters / 3) and hand over before the next call if needed.

Cancellation is checked before each completion, per stream chunk and before each tool call.

Agent construction#

Agent(role, chat_id, session_id=None) loads roles, tools and keys, resolves the provider, base URL and key, builds the system prompt (auto-discovering skills on the way), loads the session history, creates the CodeIndexSDK and GitManager, and indexes the codebase. Tool and command handlers are attached afterwards by setup_toolcalls and setup_commands.

Subagents are ordinary Agent instances bound to a child session. is_subagent turns off commits, handover and health stats for them.

Tool handlers#

A handler is handle(arguments, toolcall_id, ...) returning a tool message dict. ToolRegistry inspects the signature and injects optional parameters by name: agent_role, parent_session_id or session_id, code_indexer, cancel_check. Extra keys on the returned dict (exit_code, subagent_session_id, image_data) are internal and never sent to the API.

To add a tool: write the handler in src/Tools/, register it in src/Tools/__init__.py, add its schema to src/config/tools.json, and add its name to a role's tools list. Existing users have their own copies of the config files, so shipping a schema change to them needs a config migration in src/Agent/migrations.py.

Command handlers#

A handler is handle(args, agent). Return "" when the command is fully handled, or a string to use as the prompt. Register it in src/Commands/__init__.py with a description, an optional usage string and optional choices (a list, or a callable taking the role). UIs use those to build menus.

Events#

Event Payload Emitted
status / status_done text Around indexing and handover phases
notice text Captured output of an in-chat command
response text Full assistant text (non-streaming)
response_chunk text Streamed assistant text
reasoning / reasoning_chunk text Model reasoning, full or streamed
tool_call name, arguments JSON Before a tool runs
tool_result name, content, is_error After a tool runs
handover_doc text When a context handover happens
health_stats text After the main agent's final response
error text API or handover failure. Ends the turn

Consumers: interactive.py prints them, acp_server/agent.py maps them to ACP updates and web frames, and dispatch_subagent.py collects a child's events into a tool result.

The terminal uses non-streaming completions. ACP and web sessions set agent.stream = True.

I/O layer#

Agent, tool and command code never reads stdin or opens its own console. Everything goes through io_backend:

  • confirm(title, detail): yes or no
  • confirm_with_always(title, detail): yes, no or always
  • ask(message, options=..., allow_multiple=...): free-form or multiple choice

In terminal mode these prompt on the console. In ACP mode they are routed to per-thread handlers that the ACP layer installs, or fall back to the auto-approve policy. Full contract: io_backend specification.

Storage#

Per project: .raggie/#

Path Content
.raggie.chat SQLite. Chats, sessions, messages (with an FTS5 index), skills, todo lists and tasks, handovers, recorded changes and session files, shell command whitelist
.code_index.raggie SQLite. Files, symbols, imports, dependencies, frontend entities
git/ Snapshot repository
locks/ chat-<id>.lock anchor files
.redo_stack, .undoing, .redoing Undo and redo state
frontend_config.json Optional frontend indexing overrides

Both databases are migrated with yoyo. Migration files live in src/Agent/migrations/chat_db/ and src/indexing/migrations/index_db/ and are applied on open.

Sessions#

A chat has a tree of sessions.

  • The root session is the main conversation (depth 0).
  • A subagent gets a child session with parent_session_id, the toolcall_id that dispatched it, and depth + 1. The toolcall_id is what lets a crashed dispatch find and resume its child.
  • A handover creates a new session and sets redirect_session_id on the old one. resolve_session_id follows the chain to the active session.

Per user: ~/.config/raggie/#

See Configuration. Config schema changes go through MIGRATIONS in src/Agent/migrations.py, tracked by schema_version in _meta.json.

Building#

pip install -e .                 # editable install

cd web-ui && pnpm install && pnpm build   # web UI
python sync_webui.py             # bundle it into the package

python build_all.py              # everything, including the VS Code .vsix
python build_all.py --no-install # skip dependency installs

Use raggie ... --dev while developing so your work uses .raggie-dev/ and cannot damage real project data.

Tests#

python -m pytest tests/                         # Python suite
python -m pytest tests/test_handover.py -v      # one file
cd web-ui && pnpm test                          # web UI (vitest)
cd vscode-extension && npm test                 # extension (vitest)

Project rules#

From AGENTS.md:

  • Never use recursion. It has caused performance problems and memory leaks here. Use explicit stacks and loops.
  • Always use pnpm for the frontend.
  • The frontend is in web-ui/.

Deep dives#