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.
- Re-index. Incremental index refresh.
- Commands. The prompt is offered to the
CommandRegistry. A matching command runs locally. Its printed output is captured and emitted as anotice. Most commands end the turn there./usereturns text that becomes the prompt. - Snapshot. The main agent commits the working tree if it changed.
- Persist. The user message is appended to history and saved.
- Loop:
- 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. - Save the assistant message.
- No tool calls: hand over if the context is nearly full and continue, otherwise commit, emit health stats and return.
- 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.
- Call the model through
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 noconfirm_with_always(title, detail): yes, no or alwaysask(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 (
depth0). - A subagent gets a child session with
parent_session_id, thetoolcall_idthat dispatched it, anddepth+ 1. Thetoolcall_idis what lets a crashed dispatch find and resume its child. - A handover creates a new session and sets
redirect_session_idon the old one.resolve_session_idfollows 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/.