Tunacode Architecture Documentation
Overview
Tunacode is a TUI (Terminal User Interface) code agent built with a layered, component-based architecture. The system is designed with clear separation of concerns between the presentation layer (UI), the core agent logic, tool capabilities, and configuration management.
File: <repo_root>/src
Version: 0.1.20
Python Version: 3.11-3.13
1. Architectural Pattern
The project follows a pragmatic layered architecture with strong component-based design principles:
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (src/tunacode/ui/) │
│ Built with Textual framework - handles user interaction │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (src/tunacode/core/) │
│ Agent orchestration, request processing, state management │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Domain Layer │
│ (src/tunacode/tools/) │
│ Tool functions - bash, grep, read_file, etc. │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ (src/tunacode/configuration/, indexing/, lsp/, utils/) │
│ Configuration, indexing, LSP integration, utilities │
└─────────────────────────────────────────────────────────────┘
Key Architectural Principles
- UI/Core Decoupling: The core agent logic is completely independent of the Textual UI, enabling headless execution and testing
- Callback-based Communication: Layers communicate through callbacks rather than direct dependencies
- Centralized State Management: A
StateManagerholds all session state, avoiding scattered state management - Modular Tool System: Tools are self-contained functions with clear interfaces
- Composable System Prompts: Prompts are composed from smaller sections for maintainability
2. Component Relationships
Major Components
┌──────────────────────────────────────────────────────────────────┐
│ UI Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ TextualApp │ │ Screens │ │ Renderers │ │
│ │ (app.py) │ │ (setup, │ │ (tools, │ │
│ │ │ │ picker) │ │ panels) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│ │ │
│ requests │ renders │ callbacks
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Core Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Agent │ │ Request │ │ State │ │
│ │ Config │ │Orchestrator │ │ Manager │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│ │ │
│ configures │ orchestrates │ manages
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Tools Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Bash │ │ Grep │ │ Read File │ │
│ │ Tool │ │ Tool │ │ Tool │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Glob │ │ Tool │ │ Decorators │ │
│ │ Tool │ │ Utilities │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│ │ │
│ reads │ uses │ provides
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Configuration Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Models │ │ Defaults │ │ Settings │ │
│ │ Registry │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Dependency Graph
┌─────────────┐
│ UI │
└──────┬──────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Core │───▶│ Tools │ │ LSP │
└─────┬─────┘ └─────┬─────┘ └───────────┘
│ │
└────────┬───────┘
▼
┌──────────────┐
│Configuration │
└──────────────┘
Note: Dependency direction is one-way here:
coreimports tool functions for orchestration- tools do not import from
core
3. Data Flow
Complete Request Lifecycle
USER INPUT
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 1. INPUT CAPTURE (UI Layer) │
│ - User types in Editor widget (ui/app.py) │
│ - EditorSubmitRequested message posted │
│ - Message placed on request_queue │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 2. REQUEST PROCESSING (Core Layer) │
│ - _request_worker dequeues from request_queue │
│ - Calls process_request() (core/agents/main.py) │
│ - RequestOrchestrator instantiated │
│ - Agent created/retrieved from cache │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 3. AGENT EXECUTION (Core Layer) │
│ - System prompt composed from sections │
│ - Agent.iter() loop begins │
│ - Model request sent to LLM API │
│ - Response streamed back │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 4. TOOL SELECTION (Core Layer) │
│ - Node processor inspects response │
│ - Tool calls extracted from structured output │
│ - tool_callback invoked (ui/repl_support.py) │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 5. TOOL DISPATCH (Core Layer) │
│ - Tool calls registered and batched │
│ - Parallel execution scheduled │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 6. TOOL EXECUTION (Tools Layer) │
│ - Tool function executed (e.g., bash, grep) │
│ - Return value captured │
│ - Results recorded in the run message history │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 7. RESPONSE RENDERING (UI Layer) │
│ - streaming_callback updates streaming_output │
│ - tool_result_callback creates Panel for results │
│ - Final response written to RichLog │
│ - Token usage and cost calculated │
└───────────────────────────────────────────────────────────────┘
│
▼
STATE UPDATE
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 8. STATE MANAGEMENT (Core Layer) │
│ - SessionState messages synced from agent_run history │
│ - Token usage updated │
│ - Context window managed via compaction │
└───────────────────────────────────────────────────────────────┘
Callback Communication Flow
┌─────────────────┐ ┌─────────────────┐
│ UI │ │ Core │
│ (TextualApp) │ │ (Orchestrator) │
└────────┬────────┘ └────────┬────────┘
│ │
│ streaming_callback(text) │
│ tool_callback(tool_name) │
│ tool_result_callback(result) │
│ tool_start_callback() │
│------------------------------------>│
│ │
│ │ Execute tool
│ │ Stream response
│ │
│ Update UI widgets │
│ Show confirmation dialog │
│ Render tool output │
│<-------------------------------------│
│ │
4. Design Patterns
1. Factory & Singleton Pattern
Location: src/tunacode/core/agents/agent_components/agent_config.py
def get_or_create_agent(model: ModelName, state_manager: StateManager) -> PydanticAgent:
# Factory: Creates agent instances
# Singleton: Caches agents per model
if model not in _agent_cache:
agent = Agent(
model=model_instance,
system_prompt=system_prompt,
tools=tools_list,
)
_agent_cache[model] = agent
return _agent_cache[model]
Purpose: Avoid expensive re-initialization of agents while allowing dynamic creation.
2. Strategy Pattern
Location: src/tunacode/tools/grep.py
def grep(..., search_type: Literal["smart", "ripgrep", "python", "hybrid"] = "smart"):
# Dynamically selects search algorithm based on context
if search_type == "smart":
# Choose best algorithm automatically
elif search_type == "ripgrep":
# Use ripgrep
elif search_type == "python":
# Use pure Python
Purpose: Allow runtime selection of search algorithms based on file count and context.
3. Decorator Pattern
Location: src/tunacode/tools/decorators.py
@file_tool
def read_file(file_path: str) -> str:
# Decorator adds error handling and LSP diagnostics
...
@base_tool
def bash(command: str) -> str:
# Decorator adds error handling and retry logic
...
Purpose: Add cross-cutting concerns (error handling, retries) without cluttering tool logic.
4. Observer Pattern
Location: Throughout UI/Core communication
# Core notifies UI of state changes
async def process_request(
streaming_callback: Callable[[str], None],
tool_callback: Callable[[ToolCall], None],
tool_result_callback: Callable[[ToolResult], None],
...
):
# Core (Subject) notifies UI (Observer) via callbacks
await streaming_callback(token)
await tool_callback(tool_call)
await tool_result_callback(result)
Purpose: Enable UI to react to core events without tight coupling.
5. Composite Pattern
Location: src/tunacode/core/agents/delegation_tools.py
# A tool that is itself an agent
def research_codebase(query: str) -> str:
# Main agent delegates to specialized research agent
research_agent = get_or_create_agent(model, ...)
return await research_agent.run(query)
Purpose: Enable hierarchical agent organization where agents can delegate to specialized sub-agents.
6. State Management Pattern
Location: src/tunacode/core/state.py
class StateManager:
def __init__(self):
self.session = SessionState(
messages=[],
model=None,
token_usage=TokenUsage(),
...
)
# Centralized state store passed to all components
state_manager = StateManager()
app = TextualReplApp(state_manager=state_manager)
Purpose: Single source of truth for application state, avoiding scattered state management.
5. Module Dependencies
Dependency Hierarchy
Level 0 (Foundation):
├── constants
├── types
└── exceptions
Level 1 (Utilities):
├── configuration
│ ├── models (model registry, pricing)
│ ├── defaults (default config)
│ └── settings (API keys, user config)
├── utils
│ ├── config
│ ├── messaging
│ ├── parsing
│ └── system
├── lsp
├── prompting
│ └── sections
└── indexing
Level 2 (Core Logic):
├── core
│ ├── agents (agent orchestration)
│ ├── background (async task management)
│ ├── logging
│ ├── setup
│ └── token_usage
└── tools
├── grep_components
├── prompts
└── utils
Level 3 (Application):
├── ui
│ ├── screens (setup, picker, confirm)
│ ├── renderers (tools, panels)
│ ├── components
│ ├── widgets
│ └── styles
└── services (currently empty)
Import Flow Analysis
Tightly Coupled:
core->tools(one-way orchestration dependency)coreimports tool functions for agent configuration and dispatch
Loosely Coupled:
configuration- Standalone, only depends on foundation typesconstants,types,exceptions- No internal dependenciesindexing,lsp,prompting- Limited, specific dependencies
Highly Coupled to UI:
uidepends on nearly all modules (core, tools, configuration, indexing, lsp)
External Dependencies
| Package | Version | Purpose |
|---|---|---|
textual |
^4.0.0 | TUI framework |
pydantic-ai |
^1.18.0 | AI agent framework |
pydantic |
^2.12.4 | Data validation |
typer |
^0.15.0 | CLI framework |
rich |
^14.2.0 | Terminal formatting |
pathspec |
^0.12.1 | Gitignore pattern matching |
html2text |
^2024.2.26 | HTML to Markdown conversion |
python-Levenshtein |
^0.21.0 | String similarity |
ruff |
^0.14.0 | Linting and formatting |
textual-autocomplete |
^4.0.6 | Autocomplete widget |
defusedxml |
* | Secure XML parsing |
prompt_toolkit |
^3.0.52 | REPL prompt handling |
click |
^8.1.0 | CLI utilities |
6. Key Architectural Decisions
Decision 1: UI/Core Decoupling
Rationale: Enable headless execution, simplify testing, support future UI implementations
Implementation:
- Callback-based communication
StateManageras shared state object- Core logic has no Textual dependencies
Files:
<repo_root>/src/tunacode/ui/app.py<repo_root>/src/tunacode/core/agents/main.py
Decision 2: Modular System Prompts
Rationale: Make prompts maintainable, composable, and customizable per agent type
Implementation:
- Prompts composed from sections in
src/tunacode/core/prompting/sections/ SectionLoaderandcompose_promptfor assembly- Different agents use different prompt combinations
Files:
<repo_root>/src/tunacode/core/prompting/<repo_root>/src/tunacode/prompts/sections/
Decision 3: Agent as Iterator
Rationale: Provide clean, pull-based model for consuming agent execution steps
Implementation:
- Agent execution exposed as async iterator
- UI consumes steps as they arrive
- Avoids callback hell
Files:
<repo_root>/src/tunacode/core/agents/main.py
Decision 4: Centralized State Management
Rationale: Single source of truth, explicit state changes, easier debugging
Implementation:
SessionStatedataclass holds all session dataStateManagerprovides access and updates- State passed to key components
Files:
<repo_root>/src/tunacode/core/state.py
7. Separation of Concerns
UI Layer (src/tunacode/ui/)
Responsibility: Presentation and user interaction
Key Files:
app.py- Main Textual applicationscreens/- Setup, picker, and confirmation screensrenderers/- Tool output renderingcomponents/- Reusable UI componentswidgets/- Custom widgets
Does NOT:
- Execute tool logic
- Make agent decisions
- Manage business logic
Core Layer (src/tunacode/core/)
Responsibility: Agent orchestration and business logic
Key Files:
agents/main.py- Request processing entry pointagents/agent_config.py- Agent factoryagents/agent_components/node_processor.py- Response processingstate.py- State managementprompting/- Prompt composition
Does NOT:
- Render UI
- Execute file operations directly
- Handle Textual framework details
Tools Layer (src/tunacode/tools/)
Responsibility: Agent capabilities and system interactions
Key Files:
bash.py- Shell command executiongrep.py- Code searchread_file.py- File reading
Does NOT:
- Orchestrate agent flow
- Render output
- Manage UI state
Configuration Layer (src/tunacode/configuration/)
Responsibility: Settings and model data
Key Files:
models.py- Model registrydefaults.py- Default configurationsettings.py- User settings
Does NOT:
- Execute logic
- Manage state
- Interact with UI
8. Anti-Patterns and Technical Debt
Circular Dependency: Core ↔ Tools
Issue:
coreimports tool functions- Some tools import from
core
Impact:
- Difficult to test in isolation
- Complex initialization order
- Harder to reason about
Potential Solutions:
- Extract shared interfaces to a separate module
- Use dependency injection
- Move shared functionality to a utilities module
Current Locations
Core → Tools:
<repo_root>/src/tunacode/core/agents/agent_components/agent_config.py
Tools → Core:
<repo_root>/src/tunacode/tools/todo.py
9. Extension Points
Adding a New Tool
- Create tool function in
<repo_root>/src/tunacode/tools/ - Decorate with
@file_toolor@base_tool - Add to tools list in
agent_config.py - Optionally create custom renderer in
ui/renderers/tools/
Adding a New Agent Type
- Create prompt sections in
<repo_root>/src/tunacode/prompts/sections/ - Compose prompt using
compose_prompt() - Configure tools for agent type
- Add agent factory logic
Adding a New UI Screen
- Create screen class in
<repo_root>/src/tunacode/ui/screens/ - Integrate with
TextualReplApp - Add navigation logic
10. Testing Strategy
Current Test Coverage
Located in <repo_root>/tests/:
- Tool decorator tests
- Tool conformance tests
- Compaction tests
- Tool retry logic tests
Testing Challenges
- Circular Dependency: Makes unit testing difficult
- Async Code: Requires pytest-asyncio
- UI Code: Textual framework requires specialized testing
Recommended Testing Approach
- Unit Tests: Test tools in isolation
- Integration Tests: Test core orchestration with mock tools
- E2E Tests: Test full request flow with real tools
- UI Tests: Use textual-dev for screen testing
11. Performance Considerations
Token Management
- Context window managed via
prune_old_tool_outputs() - Token usage tracked per request and session
- Cost calculation based on model pricing
Caching
- Agent instances cached per model
- Code index cached
- Model registry lazy-loaded
Async Operations
- All I/O operations are async
- Background tasks for long-running operations
- Non-blocking UI updates
13. Security Considerations
Tool Safety
- Tool definitions enforce validation and limits
- Bash commands validated before execution
- File operations enforce size and path checks
Input Validation
- Pydantic models for all inputs
- Path sanitization for file operations
- Shell command validation
Dependency Security
defusedxmlfor secure XML parsing- Bandit security scanning configured
- Regular dependency updates
Conclusion
Tunacode demonstrates a well-structured, modular architecture with clear separation of concerns. The primary architectural strength is the UI/Core decoupling, enabling multiple UI implementations and headless operation. Core-to-tools dependencies are now one-way, keeping the tool layer isolated from core internals.
The callback-based communication pattern, centralized state management, and composable system prompts all contribute to a maintainable and extensible codebase.
Generated: 2026-01-04 Analysis Tool: Gemini MCP (gemini-2.5-pro) Codebase Version: 0.1.20