22 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.5 2523db4da7 feat: add conversation persistence and context management layer
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Failing after 34s
- SQLAlchemy async database layer (SQLite dev, PostgreSQL prod)
- Conversation and Message models with UUID primary keys
- Token counting utilities using litellm
- Context summarization at 80% token threshold
- REST API endpoints for multi-turn conversations
- 19 conversation tests, 6 token tests (176 total passing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:42:38 +01:00
jpmschweitzer 470b7448ac chore: release api v0.3.4
Build and Push API / release (push) Successful in 4s
Build and Push API / build (push) Successful in 1m18s
2026-01-11 20:21:50 +01:00
jpmschweitzerandClaude Opus 4.5 b5b2346db5 feat: add Plan Agent for implementation planning
Build and Push API / release (push) Successful in 5s
Build and Push API / build (push) Successful in 1m15s
- Add PlanAgentImpl with read-only tools only
- System prompts optimized for architecture planning
- Outputs step-by-step implementation plans with critical files
- 15 unit tests for registration, tools, and API
- Update COVERAGE.md to ~70% complete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:57:30 +01:00
jpmschweitzerandClaude Opus 4.5 617ff61347 chore: release api v0.3.2
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Successful in 1m15s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:43:08 +01:00
jpmschweitzerandClaude Opus 4.5 8609181447 docs: add mandatory release procedure to AGENTS.md
Document the correct order for creating releases:
1. Update pyproject.toml version
2. Update CHANGELOG.md
3. Commit version bump
4. Create tag
5. Push with --tags

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:37:20 +01:00
jpmschweitzerandClaude Opus 4.5 1f3b241485 chore: release api v0.3.1
Build and Push API / release (push) Failing after 3s
Build and Push API / build (push) Has been skipped
- Update changelog with v0.3.0 and v0.3.1 changes
- Bump version in pyproject.toml to 0.3.1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:36:24 +01:00
jpmschweitzerandClaude Opus 4.5 c839e263f9 test: add integration and E2E test infrastructure
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Has been cancelled
- Add pytest markers (integration, e2e, slow) with skip logic
- Add command line options (--run-integration, --run-e2e)
- Create sample_project and sample_project_with_bug fixtures
- Add test_integration.py with 10 LLM tests
- Add test_e2e.py with 12 API server tests
- Update COVERAGE.md to reflect ~65% complete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:33:21 +01:00
jpmschweitzerandClaude Opus 4.5 ef69d9c945 docs: update coverage, READMEs, and add security tests
- Update COVERAGE.md to reflect completed features (now ~60%)
- Update main README with features and tools list
- Update CLI README with streaming options
- Expand API tests from 5 to 11 (add stream endpoint tests)
- Add 14 security tests for path traversal, command injection
- Total tests: 109 (up from 88)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:53:36 +01:00
jpmschweitzerandClaude Opus 4.5 82a816a5b5 feat: add web search tool using SearXNG
Add WebSearchTool that queries the self-hosted SearXNG metasearch engine
for current information, documentation, and facts beyond training data.

- Add SEARXNG_URL and SEARXNG_TIMEOUT config settings
- Create WebSearchTool with query, num_results, categories params
- Register web_search tool with explore agent
- Add 10 tests for search functionality

Usage: Agents can now use web_search(query="...") to find current info.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 14:09:21 +01:00
jpmschweitzerandClaude Opus 4.5 f6256363a2 feat: add streaming responses to API and CLI
Add real-time streaming support for agent responses using Server-Sent
Events (SSE). Responses now appear as they're generated instead of
waiting for completion.

- Add run_stream method to BaseAgent and ExploreAgentImpl
- Add /agents/stream SSE endpoint to API router
- Add run_agent_stream method to CLI client
- Add --stream flag to chat and explore commands (enabled by default)
- Use --no-stream for batch mode with spinner

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 12:58:54 +01:00
jpmschweitzerandClaude Opus 4.5 d0fa5b38a7 feat: add coding tools (edit_file, write_file, bash)
New tools for code modification:
- EditFileTool: find-and-replace with safety checks (unique match required)
- WriteFileTool: create/overwrite files with path validation
- BashTool: full bash with controlled write access

Security controls on BashTool:
- Allowed: mkdir, touch, cp, mv, rm (single files), git, pip, pytest
- Forbidden: sudo, curl, wget, ssh, rm -rf, chmod 777

Includes 39 new tests (78 total now passing).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 11:16:05 +01:00
jpmschweitzerandClaude Opus 4.5 3b58fa4f8b refactor: reorganize into monorepo with separate subprojects
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Successful in 2m27s
Structure webber into three independent subprojects:
- webber-api/: FastAPI backend server with all agent code
- webber-cli/: Standalone CLI client (renamed from cli/ to webber_cli/)
- webber-sandbox/: Test project for functional testing

Key changes:
- Each subproject has its own .venv (Python 3.12+)
- Added sandbox.sh for managing test project templates
- Created sandbox-templates/ with calculator-cli and empty starter
- Updated CI/CD for prefixed tags (api/v*, cli/v*)
- Added comprehensive AGENTS.md with operational instructions
- Added gitignore filtering to glob and grep tools
- Created pyproject.toml for each subproject

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 10:37:47 +01:00
jpmschweitzerandClaude Opus 4.5 f4e8552298 feat: add Explore agent with PydanticAI tool calling
Implements the first Claude-like agent for codebase exploration:

Core Features:
- Explore agent with glob, grep, read, and bash tools
- Native PydanticAI tool calling with Ollama/Mistral Nemo
- Sanitized Ollama provider (fixes content:null issue)
- REST API endpoints for agent execution

Tool Infrastructure:
- BaseTool abstract class with ToolResult dataclass
- ReadFileTool, GlobFilesTool, GrepContentTool, BashReadOnlyTool
- Path validation and sandboxing support

CLI Client (separate package for future extraction):
- webber-cli command with chat, explore, status commands
- Communicates with Webber API backend
- Rich console output with theming

Configuration:
- Dev server on port 8095 (production uses 8086)
- Mistral Nemo optimizations (temp 0.3, tool_choice required)

Tests: 24 tests covering tools and API endpoints

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 01:26:30 +01:00
jpmschweitzerandClaude Opus 4.5 667e2ca8e4 chore: add ruff linter, fix mypy errors, and write README
- Add ruff linter configuration to pyproject.toml with modern Python 3.12 rules
- Add ruff~=0.9.4 to dev dependencies
- Fix all mypy type errors (Optional[] hints, Token types, Any returns)
- Auto-fix 54 ruff issues (import sorting, Optional -> X | None syntax)
- Create ProjectMeta dataclass for single source of truth from pyproject.toml
- Write comprehensive README.md with setup, config, and development docs
- Update main.py to use settings.app_description from pyproject.toml

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:58:24 +01:00
jpmschweitzerandClaude Opus 4.5 34b68621c6 docs: add architecture.md and remove implementation plan
Replace temporary implementation plan with permanent architecture
documentation covering project structure, core patterns, and
deployment details.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:05:16 +01:00
jpmschweitzerandClaude Opus 4.5 33b7e72b18 feat: add Docker healthcheck for container monitoring
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 1m14s
Helps Watchtower and Docker detect unhealthy containers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 21:28:39 +01:00
jpmschweitzerandClaude Opus 4.5 d046831903 fix: handle empty environment variables for list fields
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 1m12s
- Use Optional[list[str]] with None default for allowed_paths
- Add env_parse_none_str="" to treat empty strings as None
- Add effective_allowed_paths property for safe access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:38:30 +01:00
jpmschweitzerandClaude Opus 4.5 cbf75986fb chore: release v0.2.1 - test CI/CD pipeline
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m23s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:22:07 +01:00
jpmschweitzerandClaude Opus 4.5 098367ab85 chore: release v0.2.0
Build and Push / release (push) Failing after 3s
Build and Push / build (push) Has been skipped
Added reference prompts and expanded documentation for agents and tools.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:07:04 +01:00
jpmschweitzerandClaude Opus 4.5 47eff4ecd1 docs: expand agent and tool documentation with detailed descriptions
agents/README.md:
- Added detailed descriptions for Explore, Plan, and Task agents
- Documented capabilities, when to use, and tools available
- Added utilities/ section for shared prompts

tools/README.md:
- Added detailed descriptions for file, shell, and search tools
- Documented key parameters and behaviors
- Added security notes for bash/sandbox

agents/utilities/:
- todowrite-prompt.md - Task list management
- askuserquestion-prompt.md - User clarification
- conversation-summarization-prompt.md - Context compaction
- session-title-prompt.md - Title/branch generation
- security-review-prompt.md - Security analysis

Source: gitea:library/claude-code-system-prompts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:05:23 +01:00
jpmschweitzerandClaude Opus 4.5 0b2bf08437 docs: add example prompts from claude-code-system-prompts
Added reference prompts for agent and tool implementations:

agents/explore/ - Codebase exploration agent prompt
agents/plan/    - Plan mode with system reminders
agents/task/    - Task execution agent prompt
agents/         - Main system prompt reference

tools/file/     - Read, Edit, Write, Glob tool descriptions
tools/shell/    - Bash with git commit/PR instructions
tools/search/   - Grep, WebSearch, WebFetch descriptions

Source: gitea:library/claude-code-system-prompts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 19:44:41 +01:00
jpmschweitzerandClaude Opus 4.5 ccfa16089a chore: add CI/CD pipeline for Gitea deployment
- Add .gitea/workflows/build.yml for tag-triggered builds
- Add Dockerfile for containerized deployment (port 8086)
- Add .dockerignore to keep image lean
- Add CHANGELOG.md following Keep a Changelog format
- Update AGENTS.md with deployment verification URL

Build pipeline:
1. Create Gitea release on v* tag
2. Build and push Docker image to git.schweitz.internal
3. Trigger Watchtower for automatic deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 19:10:52 +01:00
164 changed files with 13978 additions and 1019 deletions
+43
View File
@@ -0,0 +1,43 @@
# Git
.git
.gitignore
# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
.venv
venv
ENV
env
# Testing
.pytest_cache
.coverage
htmlcov
tests/
# IDE
.idea
.vscode
*.swp
*.swo
# Logs
logs/
*.log
# Environment
.env
.env.local
.env.*.local
# Development files
*.md
!README.md
requirements-dev.txt
implementation-plan.md
fastapi-best-practices.md
wakeup.sh
+64
View File
@@ -0,0 +1,64 @@
name: Build and Push API
on:
push:
tags:
- 'api/v*'
env:
IMAGE_NAME: git.schweitz.internal/jpmschweitzer/webber-api
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: version
run: |
# Extract version from api/v0.3.0 -> v0.3.0
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#api/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create Gitea Release
run: |
curl -sf -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"tag_name": "${{ github.ref_name }}", "name": "API Release ${{ steps.version.outputs.version }}", "body": "Automated release for webber-api ${{ steps.version.outputs.version }}"}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases"
build:
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v4
- name: Extract version from tag
id: version
run: |
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#api/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.schweitz.internal
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./webber-api
push: true
tags: |
${{ env.IMAGE_NAME }}:latest
${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
- name: Trigger Watchtower update
if: success()
run: |
curl -sf -H "Authorization: Bearer ${{ secrets.WATCHTOWER_TOKEN }}" \
http://watchtower:8080/v1/update
+25
View File
@@ -0,0 +1,25 @@
name: Build and Release CLI
on:
push:
tags:
- 'cli/v*'
# TODO: Implement CLI installer build
# This workflow will be implemented when CLI distribution is ready.
# Possible targets:
# - PyPI package
# - Standalone binary (PyInstaller)
# - Platform-specific installers
jobs:
placeholder:
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: version
run: |
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#cli/}"
echo "CLI release triggered for version: $VERSION"
echo "TODO: Implement CLI build and distribution"
+11
View File
@@ -64,3 +64,14 @@ Thumbs.db
# Project specific
*.db
*.sqlite3
# Monorepo - subproject venvs (explicit for clarity)
webber-api/.venv/
webber-cli/.venv/
webber-sandbox/.venv/
# Sandbox marker file
webber-sandbox/.current_template
# Ruff cache
.ruff_cache/
+221 -81
View File
@@ -1,115 +1,255 @@
# AGENTS.md
# Webber Monorepo - Agent Instructions
> **Start every session by reading this file.**
> This file outlines the operational protocols, coding standards, and architectural decisions for this FastAPI project.
> This file contains everything you need to work with this codebase efficiently.
## 1. Agent Operational Protocols
## Quick Reference
### 🧠 Work Patterns (Plan-Act-Reflect)
* **Plan:** Before writing code, briefly outline your plan. Identify which files you will touch and what the side effects might be.
* **Act:** Execute the changes in small, atomic steps.
* **Reflect:** After coding, verify your work. Did you break existing tests? Did you add new tests?
### 🛡️ Git Discipline
* **ALWAYS add the relevant tests for the added code** Make sure to keep the test coverage up as we go, and run tests before commiting.
* **NEVER commit to `main` or `master` directly.** Always create a feature branch: `feature/your-feature-name` or `fix/issue-description`.
* **Commit Messages:** Use the [Conventional Commits](https://www.conventionalcommits.org/) format.
* `feat: add user login endpoint`
* `fix: resolve database connection timeout`
* `refactor: split monolith dependency file`
* **Atomic Commits:** Keep commits small. One logical change = one commit.
### 📝 Changelog Maintenance
* **Update `CHANGELOG.md`** with every user-facing change.
* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`.
### 🚀 Release Flow
When changes are ready for deployment:
1. **Ask user if deploy cycle is desired **
2. **Update version** in `pyproject.toml`:
- Bug fixes: bump patch version (1.8.3 → 1.8.4)
- New features: bump minor version (1.8.4 → 1.9.0)
3. **Update CHANGELOG.md**:
- Move items from `[Unreleased]` to new version section
- Add release date: `## [1.8.4] - 2025-12-16`
4. **Commit and tag**:
```bash
git add -A
git commit -m "fix: description of changes"
git tag v1.8.4
git push origin main --tags
```
5. **CI/CD triggers automatically**:
- Gitea CI builds Docker image on new version tag (starts with "v")
- Watchtower pulls and deploys to production
| Action | Command |
|--------|---------|
| Start API server | `cd webber-api && ./wakeup.sh` |
| View API logs | `tail -f webber-api/logs/server.log` |
| Run API tests | `cd webber-api && .venv/bin/python -m pytest tests/ -v` |
| Check CLI status | `cd webber-cli && .venv/bin/webber-cli status` |
| Load sandbox | `./sandbox.sh load calculator-cli` |
| Explore sandbox | `cd webber-cli && .venv/bin/webber-cli explore "query" -d ../webber-sandbox` |
---
### 🧪 Local Development Setup
## Repository Structure
* **Always test locally first** before committing and deploying. The build-deploy loop is slow.
* **Only deploy** when a phase or feature is complete and tested locally
* **Environment**: Copy `.env.example` to `.env` and configure for your local setup
```
webber/
├── webber-api/ # FastAPI backend server
│ ├── src/ # API source code
│ ├── tests/ # API tests (pytest)
│ ├── docs/ # Architecture docs, COVERAGE.md
│ ├── logs/ # Runtime logs (server.log)
│ ├── .venv/ # API virtual environment
│ ├── wakeup.sh # Dev server startup script
│ └── AGENTS.md # API-specific development guide
├── webber-cli/ # CLI client
│ ├── webber_cli/ # Python package (underscore!)
│ ├── .venv/ # CLI virtual environment
│ └── README.md # CLI usage guide
├── webber-sandbox/ # Active test project (contents swappable)
│ ├── src/ # Current project source
│ ├── tests/ # Current project tests
│ ├── .venv/ # Sandbox virtual environment
│ └── TASKS.md # Tasks for Webber to complete
├── sandbox-templates/ # Template storage
│ ├── calculator-cli/ # Simple CLI with intentional bugs
│ └── empty/ # Blank starter project
├── sandbox.sh # Sandbox management script
└── AGENTS.md # THIS FILE
```
#### ⚠️ CRITICAL: Starting the Local Server
---
**ALWAYS use `./wakeup.sh` to start the local server. NEVER use raw uvicorn commands.**
## Development Workflow
### 1. Start the API Server
```bash
cd webber-api
./wakeup.sh
```
The wakeup script provides:
- **Port conflict detection** - Warns if port 8086 is already in use
- **Virtual environment activation** - Ensures correct Python environment
- **Centralized logging** - All logs written to `logs/server.log` for easy tailing
- **Auto-reload** - Code changes picked up automatically (except requirements.txt changes)
- **Consistent configuration** - Same startup every time
- **Port:** 8095 (dev), 8086 (production Docker)
- **Logs:** `webber-api/logs/server.log`
- **Health check:** `curl http://localhost:8095/health`
- **API docs:** http://localhost:8095/docs
To stop: `Ctrl+C` or `pkill -f "uvicorn src.main:app"`
### 2. Run Tests
To monitor logs in another terminal:
```bash
tail -f logs/server.log
# API tests (39 tests)
cd webber-api
.venv/bin/python -m pytest tests/ -v
# With coverage
.venv/bin/python -m pytest tests/ --cov=src
# Single test file
.venv/bin/python -m pytest tests/test_tools.py -v
```
To stop the server: Press `Ctrl+C`
### 3. Use the CLI
To kill a stuck server:
```bash
pkill -f "uvicorn src.main:app"
# or
kill $(lsof -t -i:8086)
cd webber-cli
# Check API connection
.venv/bin/webber-cli status
# Explore a directory
.venv/bin/webber-cli explore "find all python files" -d ../webber-sandbox
# Interactive chat mode
.venv/bin/webber-cli chat -d ../webber-sandbox
```
#### Testing
**Note:** The API server must be running for CLI commands to work.
---
## Sandbox Management
The sandbox is a swappable test project for functional testing.
### Available Templates
| Template | Description |
|----------|-------------|
| `calculator-cli` | Python CLI with intentional bugs (div-by-zero, missing tests) |
| `empty` | Blank starter project |
### Commands
**Test REST endpoints** against `http://localhost:8086`:
```bash
curl http://localhost:8086/health
curl http://localhost:8086/
curl http://localhost:8086/docs # Swagger UI
# List available templates
./sandbox.sh list
# Load a template (clears sandbox, preserves .venv)
./sandbox.sh load calculator-cli
# Reset to last loaded template
./sandbox.sh reset
# Save current sandbox as new template
./sandbox.sh save my-template
# Check current status
./sandbox.sh status
```
**Running tests**: Always use the venv explicitly to avoid environment mismatches:
### After Loading a Template
```bash
.venv/bin/python -m pytest tests/ # All tests
.venv/bin/python -m pytest tests/ -v # Verbose output
.venv/bin/python -m pytest tests/ --cov # With coverage
cd webber-sandbox
source .venv/bin/activate # Create .venv first if missing
pip install -r requirements.txt
# Read the tasks
cat TASKS.md
# Run the project's tests
pytest tests/ -v
```
---
## 2. FastAPI Architecture & Best Practices
*Reference: [FastAPI Best Practices](https://github.com/zhanymkanov/fastapi-best-practices)*
## Testing Webber's Capabilities
### 📂 Project Structure (Directory-based, NOT File-type based)
Do **not** group files by type (e.g., one huge `routers` folder). Group by **domain/module** inside a `src/` directory.
### Scenario: Find bugs in calculator-cli
**Correct Structure:**
```text
to be determined
```bash
# 1. Load the template
./sandbox.sh load calculator-cli
# 2. Have Webber explore it
cd webber-cli
.venv/bin/webber-cli explore "find all bugs in the code" -d ../webber-sandbox
# 3. Check TASKS.md for expected bugs
cat ../webber-sandbox/TASKS.md
```
### Known bugs in calculator-cli:
- Division by zero not handled (`operations.py:divide`)
- Invalid operation causes KeyError (`main.py:get_operation`)
- Power function broken for fractional exponents
- Missing tests for divide and power functions
---
## Key Files for Debugging
| File | Purpose |
|------|---------|
| `webber-api/logs/server.log` | API server logs |
| `webber-api/src/domains/agents/explore/prompts.py` | Explore agent system prompts |
| `webber-api/src/domains/agents/explore/agent.py` | Explore agent implementation |
| `webber-api/src/ollama/provider.py` | Ollama integration (sanitizes content:null) |
| `webber-api/docs/COVERAGE.md` | Feature coverage and known issues |
---
## Versioning & Releases
Uses prefixed tags:
- `api/vX.Y.Z` → Triggers API Docker build
- `cli/vX.Y.Z` → Triggers CLI build (future)
### MANDATORY Release Procedure
**NEVER push a tag before updating version files.** Follow this exact order:
```bash
# 1. Update version in pyproject.toml
# 2. Update CHANGELOG.md with release notes
# 3. Commit the version bump
git add -A && git commit -m "chore: release api vX.Y.Z"
# 4. Create the tag (AFTER the commit)
git tag api/vX.Y.Z
# 5. Push everything together
git push origin main --tags
```
**Why this matters:** Pushing a tag before the version commit requires deleting and recreating the tag, which can trigger CI/CD pipelines prematurely and cause deployment issues.
---
## Troubleshooting
### API server won't start
```bash
# Check if port is in use
lsof -i :8095
# Kill stuck process
pkill -f "uvicorn src.main:app"
```
### CLI can't connect
```bash
# Check API is running
curl http://localhost:8095/health
# Check CLI config
echo $WEBBER_API_URL # Should be http://localhost:8095
```
### Ollama errors
```bash
# Check Ollama is running
curl http://192.168.86.149:11434/api/tags
# Check model is available
curl http://192.168.86.149:11434/api/tags | grep mistral-nemo
```
### Tests failing
```bash
# Run with verbose output
cd webber-api
.venv/bin/python -m pytest tests/ -v --tb=short
```
---
## Known Limitations
1. **Model hallucination** - Mistral Nemo sometimes makes up file contents instead of using tool results
2. **No conversation memory** - CLI chat mode doesn't persist between sessions
3. **No streaming** - Responses appear all at once
See `webber-api/docs/COVERAGE.md` for full feature coverage status.
+135
View File
@@ -0,0 +1,135 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.4.0] - 2026-01-11
### Added
- Conversation persistence layer with SQLAlchemy async
- Database models: `Conversation`, `Message` with UUID primary keys
- SQLite (dev) and PostgreSQL (prod) support via async engines
- Lazy database initialization pattern
- Context management infrastructure
- Token counting utilities using `litellm`
- Context summarization at 80% token threshold
- XML-tagged context prompt building for agent injection
- REST API for multi-turn conversations
- `POST /conversations/` - Create new conversation
- `GET /conversations/` - List conversations
- `GET /conversations/{id}` - Get conversation with history
- `POST /conversations/{id}/messages` - Add message (triggers agent)
- `DELETE /conversations/{id}` - Delete conversation
- New dependencies: `sqlalchemy[asyncio]~=2.0.36`, `aiosqlite~=0.21.0`, `litellm~=1.57.0`
- Config settings: `database_url`, `summarization_threshold`, `keep_recent_messages`
- 19 conversation tests, 6 token counting tests (176 total tests passing)
### Changed
- Updated COVERAGE.md to ~80% complete
- Quieter pytest output (`-q --tb=short` instead of `-v`)
## [0.3.4] - 2026-01-11
### Added
- Task Agent - Full orchestrator for autonomous multi-step task execution
- Has ALL tools: read, write, edit, bash (full), web_search
- New `spawn_agent` tool to launch sub-agents (Explore, Plan) for focused work
- Recursion prevention: cannot spawn nested Task agents
- 22 unit tests for registration, tools, spawn_agent, and API
- Complete agent hierarchy: Explore (read-only) → Plan (read-only) → Task (orchestrator)
## [0.3.3] - 2026-01-11
### Added
- Plan Agent - READ-ONLY software architect that designs implementation strategies
- Uses only read-only tools: `read_file`, `glob_files`, `grep_content`, `bash_readonly`
- Creates step-by-step implementation plans with critical files list
- 15 unit tests for registration, tools, and API
- Web search summarizer added to roadmap (future feature)
### Changed
- Updated COVERAGE.md to ~70% complete
## [0.3.2] - 2026-01-11
### Added
- Mandatory release procedure documentation in AGENTS.md
## [0.3.1] - 2026-01-11
### Added
- Integration test infrastructure with pytest markers (integration, e2e, slow)
- 10 LLM integration tests (requires Ollama)
- 12 E2E API tests (requires running server)
- Command line options: `--run-integration`, `--run-e2e`, `--ollama-url`, `--api-url`
- Sample project fixtures for testing
- 14 security tests (path traversal, command injection, input validation)
- Helper functions: `assert_contains_any`, `assert_contains_all`
### Changed
- Updated COVERAGE.md to ~65% complete
## [0.3.0] - 2026-01-10
### Added
- Explore agent with PydanticAI tool calling and Mistral Nemo
- Coding tools: `edit_file`, `write_file`, `bash` (full)
- Web search tool using SearXNG integration
- Streaming responses via SSE for API and CLI
- CLI commands: `explore`, `chat`, `status`
- Sanitized Ollama provider (fixes `content: null` issue)
### Changed
- Reorganized into monorepo structure (webber-api/, webber-cli/, webber-sandbox/)
- Added ruff linter and fixed mypy errors
## [0.2.3] - 2026-01-09
### Added
- Docker healthcheck for container health monitoring
## [0.2.2] - 2026-01-09
### Fixed
- Config parsing for empty environment variables (allowed_paths, cors_*)
- Use `env_parse_none_str=""` to treat empty strings as None
## [0.2.1] - 2026-01-09
### Fixed
- CI/CD pipeline credentials configured
## [0.2.0] - 2026-01-09
### Added
- Reference prompts from claude-code-system-prompts for all agent types
- Detailed documentation for Explore, Plan, and Task agents
- Detailed documentation for File, Shell, and Search tools
- Utility prompts (TodoWrite, AskUserQuestion, conversation summarization, etc.)
- Security review prompt for code analysis
### Changed
- Expanded agents/README.md with capabilities and use cases
- Expanded tools/README.md with parameter details and behaviors
## [0.1.0] - 2026-01-09
### Added
- Initial FastAPI boilerplate setup
- Domain-based project structure (src/domains/, src/shared/)
- BaseController pattern with lazy router instantiation
- Pydantic Settings configuration with env file support
- Logger decorator with temporal benchmarking and trace IDs
- UserProvider singleton for request-scoped context
- Custom exception hierarchy
- Health endpoints (/, /health)
- Placeholder domains for agents (explore, plan, task)
- Placeholder domains for tools (file, shell, search)
- Placeholder domain for auth (tatlock integration)
- CI/CD workflow for Gitea with Docker build and Watchtower deployment
- Dockerfile for containerized deployment
- CVE-checked dependencies (2026-01-09)
+100
View File
@@ -0,0 +1,100 @@
# Webber - Multi-Agent AI Development System
A Claude Code-inspired development assistant powered by local LLMs via Ollama.
## Features
- **Explore Agent** - Search, read, and understand codebases
- **8 Tools** - File read/write, glob, grep, bash, web search
- **Streaming** - Real-time response display
- **Self-hosted** - Runs on your own hardware with Ollama
## Structure
This is a monorepo containing three subprojects:
| Directory | Description |
|-----------|-------------|
| `webber-api/` | FastAPI backend server with agent orchestration |
| `webber-cli/` | Command-line client for interacting with the API |
| `webber-sandbox/` | Test project for functional testing |
### Additional Directories
| Directory | Description |
|-----------|-------------|
| `sandbox-templates/` | Reusable project templates for the sandbox |
| `.gitea/workflows/` | CI/CD workflows for releases |
## Quick Start
### 1. Start the API Server
```bash
cd webber-api
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
./wakeup.sh
```
### 2. Set Up the CLI
```bash
cd webber-cli
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e .
# Test connection
webber-cli status
```
### 3. Explore with Webber
```bash
# One-shot exploration
webber-cli explore "find all bugs in the code" -d /path/to/project
# Interactive chat
webber-cli chat -d /path/to/project
```
## Available Tools
| Tool | Description |
|------|-------------|
| `read_file` | Read file contents with line numbers |
| `glob_files` | Find files by pattern |
| `grep_content` | Search file contents with regex |
| `bash_readonly` | Safe bash commands (ls, git status, etc.) |
| `edit_file` | Find-and-replace editing |
| `write_file` | Create/overwrite files |
| `bash` | Full bash with safety controls |
| `web_search` | Search web via SearXNG |
## Versioning
This project uses prefixed tags for independent release cycles:
- `api/v0.3.0` - Triggers API Docker build and deployment
- `cli/v0.1.0` - Triggers CLI installer build (future)
## Requirements
- Python 3.12+
- Ollama running with `mistral-nemo:latest` model
- Docker (for production deployment)
- SearXNG (optional, for web search)
## Documentation
- `webber-api/AGENTS.md` - API development guidelines
- `webber-api/docs/COVERAGE.md` - Feature coverage and roadmap
- `webber-api/docs/architecture.md` - System architecture
- `webber-cli/README.md` - CLI usage guide
## License
MIT
-628
View File
@@ -1,628 +0,0 @@
# Webber FastAPI Boilerplate Plan
## Overview
Set up FastAPI boilerplate for "Webber" - a multi-agent AI development system (similar to Claude Code, but local with different models). Follows core-api patterns with defensive coding practices.
**Key Decision: PydanticAI Framework**
After research, [PydanticAI](https://ai.pydantic.dev/) is the recommended agent coordination framework:
- Model-agnostic: supports Ollama, OpenAI, Anthropic, and 20+ providers
- Type-safe with Pydantic validation (same ecosystem as FastAPI)
- Built-in tool/function calling with automatic schema generation
- Multi-agent support for complex workflows
- Maintained by Pydantic team (285M+ monthly downloads)
**Port: 8086** (next available slot after Headscale 8085 per CONTAINERS.md)
**Default Models (always hot in VRAM on tower-of-joy):**
- Agent reasoning: `mistral-nemo-large:latest`
- Embeddings: `nomic-embed-text:latest`
**Target Clients:**
- **Tatlock Butler**: External advisor integration for coding/software guidance
- **CLI Interface**: TBD - command-line interface for local development
**Multi-tenancy:** API key authentication integrated with tatlock-ui/core-api user management
---
## 1. Directory Structure
```
webber/
├── AGENTS.md # Expanded with defensive LLM guidelines
├── README.md # Project overview
├── CHANGELOG.md # Version history
├── pyproject.toml # Package metadata
├── requirements.txt # Production dependencies only (~= pinned)
├── requirements-dev.txt # Dev/test dependencies (pytest, pip-audit, etc.)
├── .env.example # Environment template
├── wakeup.sh # Dev startup (update port to 8086)
├── src/
│ ├── __init__.py
│ ├── main.py # FastAPI app, lifespan, user provider init
│ │ # NO routes here - delegates to domain routers
│ │
│ ├── shared/ # Cross-cutting concerns
│ │ ├── __init__.py
│ │ ├── base.py # BaseController, BaseSchema
│ │ ├── config.py # Pydantic BaseSettings
│ │ ├── logging.py # Logger decorator + centralized setup
│ │ ├── exceptions.py # Custom exception hierarchy
│ │ ├── auth.py # API key validation, multi-tenant support
│ │ └── context.py # UserProvider singleton, request context
│ │
│ └── domains/ # Feature domains (each with router.py)
│ ├── __init__.py
│ ├── router.py # Root router - includes all domain routers
│ │
│ ├── health/ # Health endpoints
│ │ ├── __init__.py
│ │ ├── router.py # Health routes
│ │ └── controller.py # Health logic
│ │
│ ├── auth/ # Authentication domain
│ │ ├── __init__.py
│ │ ├── router.py # Auth routes (API key mgmt)
│ │ ├── controller.py
│ │ └── schemas.py
│ │
│ │── agents/ # Agent domain container
│ │ ├── __init__.py
│ │ ├── router.py # Agent routes (lists agents, runs them)
│ │ ├── controller.py # Agent orchestration logic
│ │ ├── schemas.py
│ │ │
│ │ ├── explore/ # Explore agent (codebase navigation)
│ │ │ ├── __init__.py
│ │ │ ├── agent.py # PydanticAI agent definition
│ │ │ └── prompts.py # System prompts
│ │ │
│ │ ├── plan/ # Plan agent (implementation design)
│ │ │ ├── __init__.py
│ │ │ ├── agent.py
│ │ │ └── prompts.py
│ │ │
│ │ └── task/ # Task agent (execution)
│ │ ├── __init__.py
│ │ ├── agent.py
│ │ └── prompts.py
│ │
│ └── tools/ # Tool domain container
│ ├── __init__.py
│ ├── router.py # Tool routes (list tools, execute)
│ ├── controller.py # Tool orchestration
│ ├── schemas.py
│ │
│ ├── file/ # File operation tools
│ │ ├── __init__.py
│ │ ├── read.py
│ │ ├── write.py
│ │ └── glob.py
│ │
│ ├── shell/ # Shell execution tools
│ │ ├── __init__.py
│ │ └── bash.py
│ │
│ └── search/ # Search tools
│ ├── __init__.py
│ ├── grep.py
│ └── web.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── test_health.py
└── docs/
└── architecture.md
```
### Key Architectural Decisions
1. **Clean main.py**: Only app creation, lifespan, and UserProvider init. All routes in domain routers.
2. **Domain routers**: Each domain has `router.py` that defines routes. Root `domains/router.py` composes them.
3. **Separate agent domains**: Each agent type (explore, plan, task) in its own subdir under `agents/`.
4. **Separate tool domains**: Each tool category (file, shell, search) in its own subdir under `tools/`.
5. **UserProvider singleton**: Set once in main.py lifespan, accessible everywhere via `shared/context.py`.
6. **Multi-tenant auth**: API key validation in `shared/auth.py`, integrates with tatlock-ui/core-api.
---
## 2. Key Files to Create
### Phase 1: Foundation (fully implemented)
| File | Purpose |
|------|---------|
| `src/shared/base.py` | BaseController, BaseSchema |
| `src/shared/config.py` | Settings via Pydantic BaseSettings |
| `src/shared/logging.py` | Logger decorator + centralized setup |
| `src/shared/exceptions.py` | Custom exception hierarchy |
| `src/shared/auth.py` | API key validation, tatlock integration stub |
| `src/shared/context.py` | UserProvider singleton pattern |
| `src/main.py` | FastAPI app, lifespan, UserProvider init (no routes!) |
| `src/domains/router.py` | Root router composing all domain routers |
| `src/domains/health/router.py` | Health routes |
| `src/domains/health/controller.py` | Health logic |
| `pyproject.toml` | Package metadata, pytest config |
| `requirements.txt` | Production deps (~= pinned) |
| `requirements-dev.txt` | Dev/test deps (pytest, pip-audit) |
| `.env.example` | Environment variable template |
| `tests/conftest.py` | Pytest fixtures |
| `tests/test_health.py` | Basic endpoint tests |
### Phase 2: Placeholders (structure + README docs)
| Directory | Purpose |
|-----------|---------|
| `src/domains/auth/` | API key management (stub) |
| `src/domains/agents/` | Agent container with explore/plan/task subdirs |
| `src/domains/tools/` | Tool container with file/shell/search subdirs |
| `docs/architecture.md` | System design documentation |
---
## 3. Dependency Management
### requirements.txt (Production - baked into Docker)
```
# Webber Production Dependencies
# Minor version pinning (~=) for security patches
# CVE check date: 2026-01-09
# CVE check sources: PyPI, GitHub Advisories, Snyk, NVD
# Core FastAPI
fastapi~=0.115.0
starlette~=0.45.0
uvicorn[standard]~=0.34.0
pydantic~=2.11.0
pydantic-settings~=2.7.0
# Agent Framework
pydantic-ai~=0.0.39 # Multi-agent LLM orchestration
# HTTP
httpx~=0.28.0
aiofiles~=24.1.0
# Utilities
python-multipart~=0.0.18
python-dotenv~=1.0.0
```
### requirements-dev.txt (Dev/Test only - NOT in Docker)
```
# Webber Development Dependencies
# Install with: pip install -r requirements-dev.txt
-r requirements.txt # Include production deps
# Testing
pytest~=8.3.0
pytest-asyncio~=0.24.0
pytest-cov~=6.0.0
# Security auditing
pip-audit~=2.7.0 # Run before releases: pip-audit
# Type checking
mypy~=1.13.0
# Code formatting (optional)
# ruff~=0.8.0
```
---
## 4. AGENTS.md Additions
Add these new sections:
### Section 3: Defensive LLM Coding Practices
- Input validation requirements
- Output parsing guidelines (expect malformed responses)
- Timeout and retry policies
- Security: no secrets in prompts, sandbox execution
### Section 4: Pattern Reuse Requirements
- Search existing code before writing new
- Check `src/shared/` for base classes
- Follow domain structure template
- Code review checklist
### Section 5: CVE Check Process
- Check PyPI, GitHub Advisories, Snyk, NVD before adding deps
- Document CVE decisions in requirements.txt
- Run `pip-audit` before releases
### Section 6: Mandatory Documentation
- Required reading before work: AGENTS.md, docs/architecture.md, src/shared/base.py
- Changelog and docstring requirements
### Section 7: Project Structure Reference
- Directory tree with explanations
- Domain structure template
---
## 5. Configuration (Settings)
Environment variables for:
- **App**: DEBUG, LOG_LEVEL
- **Server**: HOST, PORT (default **8086** per CONTAINERS.md allocation)
- **CORS**: origins, methods, headers
- **LLM Models** (hot in VRAM on tower-of-joy):
- OLLAMA_URL (default: http://192.168.86.149:11434)
- OLLAMA_AGENT_MODEL (default: mistral-nemo-large:latest)
- OLLAMA_EMBED_MODEL (default: nomic-embed-text:latest)
- **Auth**:
- TATLOCK_API_URL (default: http://192.168.86.149:8000)
- Internal API key for tatlock user validation
- **Tools**: TOOL_TIMEOUT_SECONDS, SANDBOX_ENABLED, ALLOWED_PATHS
- **Sessions**: SESSION_TTL_HOURS, MAX_CONTEXT_TOKENS
---
## 6. Core Patterns
### Logger Decorator with Temporal Benchmarking (shared/logging.py)
```python
import functools
import asyncio
import time
import logging
from typing import Callable, Optional
from contextvars import ContextVar
from dataclasses import dataclass, field
from uuid import uuid4
# Trace context for nested timing
@dataclass
class TraceSpan:
name: str
trace_id: str
parent_id: Optional[str] = None
span_id: str = field(default_factory=lambda: uuid4().hex[:8])
start_time: float = field(default_factory=time.perf_counter)
end_time: Optional[float] = None
@property
def duration_ms(self) -> float:
if self.end_time is None:
return (time.perf_counter() - self.start_time) * 1000
return (self.end_time - self.start_time) * 1000
# Context variable for trace propagation
_current_span: ContextVar[Optional[TraceSpan]] = ContextVar('current_span', default=None)
_trace_id: ContextVar[Optional[str]] = ContextVar('trace_id', default=None)
def get_current_trace_id() -> Optional[str]:
"""Get current trace ID for correlation."""
return _trace_id.get()
def logged(
logger: logging.Logger = None,
slow_threshold_ms: float = 100.0,
warn_threshold_ms: float = 500.0,
include_args: bool = False,
):
"""
Decorator for automatic function logging with temporal benchmarking.
Args:
logger: Logger instance (defaults to module logger)
slow_threshold_ms: Log INFO if execution exceeds this (default 100ms)
warn_threshold_ms: Log WARNING if execution exceeds this (default 500ms)
include_args: Include function arguments in log (careful with sensitive data)
Usage:
@logged()
async def my_function(): ...
@logged(slow_threshold_ms=50, warn_threshold_ms=200)
def critical_path(): ...
"""
def decorator(func: Callable):
nonlocal logger
if logger is None:
logger = logging.getLogger(func.__module__)
func_name = f"{func.__module__}.{func.__qualname__}"
def _create_span() -> TraceSpan:
parent = _current_span.get()
trace_id = _trace_id.get() or uuid4().hex[:16]
if _trace_id.get() is None:
_trace_id.set(trace_id)
return TraceSpan(
name=func_name,
trace_id=trace_id,
parent_id=parent.span_id if parent else None,
)
def _log_completion(span: TraceSpan, error: Exception = None):
span.end_time = time.perf_counter()
duration = span.duration_ms
# Build log context
ctx = {
"trace_id": span.trace_id,
"span_id": span.span_id,
"duration_ms": round(duration, 2),
"func": func_name,
}
if span.parent_id:
ctx["parent_id"] = span.parent_id
if error:
logger.error(
f"[{span.trace_id[:8]}] {func_name} FAILED after {duration:.2f}ms: {error}",
extra=ctx,
exc_info=True
)
elif duration >= warn_threshold_ms:
logger.warning(
f"[{span.trace_id[:8]}] {func_name} SLOW: {duration:.2f}ms (threshold: {warn_threshold_ms}ms)",
extra=ctx
)
elif duration >= slow_threshold_ms:
logger.info(
f"[{span.trace_id[:8]}] {func_name} completed in {duration:.2f}ms",
extra=ctx
)
else:
logger.debug(
f"[{span.trace_id[:8]}] {func_name} completed in {duration:.2f}ms",
extra=ctx
)
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
span = _create_span()
token = _current_span.set(span)
if include_args:
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}({args}, {kwargs})")
else:
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}")
try:
result = await func(*args, **kwargs)
_log_completion(span)
return result
except Exception as e:
_log_completion(span, error=e)
raise
finally:
_current_span.reset(token)
@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
span = _create_span()
token = _current_span.set(span)
if include_args:
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}({args}, {kwargs})")
else:
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}")
try:
result = func(*args, **kwargs)
_log_completion(span)
return result
except Exception as e:
_log_completion(span, error=e)
raise
finally:
_current_span.reset(token)
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
return decorator
# Convenience for manual span creation (context manager)
class trace_span:
"""
Context manager for manual span creation.
Usage:
with trace_span("database_query"):
result = await db.execute(query)
async with trace_span("llm_call"):
response = await agent.run(prompt)
"""
def __init__(self, name: str, logger: logging.Logger = None):
self.name = name
self.logger = logger or logging.getLogger(__name__)
self.span: Optional[TraceSpan] = None
self.token = None
def __enter__(self):
parent = _current_span.get()
trace_id = _trace_id.get() or uuid4().hex[:16]
if _trace_id.get() is None:
_trace_id.set(trace_id)
self.span = TraceSpan(
name=self.name,
trace_id=trace_id,
parent_id=parent.span_id if parent else None,
)
self.token = _current_span.set(self.span)
self.logger.debug(f"[{self.span.trace_id[:8]}] -> {self.name}")
return self.span
def __exit__(self, exc_type, exc_val, exc_tb):
if self.span:
self.span.end_time = time.perf_counter()
duration = self.span.duration_ms
if exc_val:
self.logger.error(f"[{self.span.trace_id[:8]}] {self.name} FAILED: {duration:.2f}ms")
else:
self.logger.debug(f"[{self.span.trace_id[:8]}] {self.name}: {duration:.2f}ms")
if self.token:
_current_span.reset(self.token)
return False
async def __aenter__(self):
return self.__enter__()
async def __aexit__(self, exc_type, exc_val, exc_tb):
return self.__exit__(exc_type, exc_val, exc_tb)
```
**Example output:**
```
DEBUG [a1b2c3d4] -> src.domains.agents.controller.run_agent
DEBUG [a1b2c3d4] -> src.domains.llm.service.call_ollama
DEBUG [a1b2c3d4] src.domains.llm.service.call_ollama: 45.23ms
INFO [a1b2c3d4] src.domains.agents.controller.run_agent completed in 156.78ms
WARN [a1b2c3d4] src.domains.tools.file.read.read_file SLOW: 523.45ms (threshold: 500ms)
```
**Features:**
- **Trace IDs**: Correlate logs across nested calls
- **Parent/child spans**: Track call hierarchy
- **Configurable thresholds**: `slow_threshold_ms` (INFO), `warn_threshold_ms` (WARNING)
- **Context manager**: `trace_span()` for manual instrumentation of code blocks
- **Zero overhead path**: Fast path for sub-threshold calls (DEBUG only)
### UserProvider Singleton (shared/context.py)
```python
from dataclasses import dataclass
from typing import Optional
from contextvars import ContextVar
@dataclass
class User:
id: str
email: str
api_key: str
tenant_id: Optional[str] = None
# Context variable for request-scoped user
_current_user: ContextVar[Optional[User]] = ContextVar('current_user', default=None)
class UserProvider:
"""Singleton for user context management."""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def set_user(self, user: User) -> None:
_current_user.set(user)
def get_user(self) -> Optional[User]:
return _current_user.get()
def clear_user(self) -> None:
_current_user.set(None)
# Global singleton
user_provider = UserProvider()
```
### BaseController (from core-api)
```python
class BaseController(ABC):
def __init__(self, prefix: str, tags: list[str]):
self.prefix = prefix
self.tags = tags
self._router = None
@abstractmethod
def create_router(self) -> APIRouter: pass
@property
def router(self) -> APIRouter:
if self._router is None:
self._router = self.create_router()
return self._router
```
### PydanticAI Agent Pattern (placeholder for future)
```python
from pydantic_ai import Agent
from pydantic_ai.models.ollama import OllamaModel
# Use the hot model from VRAM
agent = Agent(
OllamaModel('mistral-nemo-large:latest'),
system_prompt='You are a helpful assistant.',
)
@agent.tool
async def search_files(ctx, pattern: str) -> str:
"""Search for files matching pattern."""
pass # Implementation in tools/search/
```
---
## 7. Implementation Order
1. **Create directory structure** (`src/`, `src/shared/`, `src/domains/`)
2. **Implement shared modules** (base.py, config.py, logging.py, exceptions.py)
3. **Create main.py** with FastAPI app and lifespan
4. **Add health domain** as working example
5. **Set up tests** (conftest.py, test_health.py)
6. **Create placeholder domains** (llm, agents, tools - structure only)
7. **Update AGENTS.md** with new sections
8. **Create supporting files** (pyproject.toml, requirements.txt, .env.example)
9. **Add docs/architecture.md**
---
## 8. Verification
After implementation:
1. `./wakeup.sh` starts server without errors
2. `curl http://localhost:8086/health` returns healthy
3. `http://localhost:8086/docs` shows API documentation
4. `.venv/bin/python -m pytest tests/ -v` passes
5. Code follows patterns in AGENTS.md
---
## 9. Critical Reference Files
- `/mnt/media/Projects/core-api/src/shared/base.py` - BaseController pattern
- `/mnt/media/Projects/core-api/src/shared/config.py` - Settings pattern
- `/mnt/media/Projects/core-api/src/domains/health/controller.py` - Controller example
- https://ai.pydantic.dev/ - PydanticAI documentation
---
## Summary
**What will be created:**
- Complete FastAPI project structure following core-api patterns
- Working health endpoint at `http://localhost:8086/health`
- **Clean main.py** - no routes, just app init and UserProvider setup
- **Domain routers** - each domain has router.py, composed by root router
- **Logger decorator** - centralized logging via `@logged` decorator
- **UserProvider singleton** - request-scoped user context, no parameter passing
- **Multi-tenant auth stub** - API key validation ready for tatlock integration
- Separate **requirements.txt** (prod) and **requirements-dev.txt** (dev/test)
- Placeholder domains with agent/tool subdirectories (explore, plan, task / file, shell, search)
- Comprehensive AGENTS.md with defensive LLM coding practices, CVE checks, pattern reuse
- Test infrastructure with pytest
- docs/architecture.md explaining the system design
**What will NOT be created (deferred):**
- Database layer (add when needed)
- Full agent/tool implementations (PydanticAI patterns documented for future work)
- Docker/deployment configuration (can add later)
- CLI interface (TBD)
**Key decisions:**
- Port: **8086**
- Agent framework: **PydanticAI**
- Default model: **mistral-nemo-large:latest** (hot in VRAM)
- Embeddings: **nomic-embed-text:latest** (hot in VRAM)
- No database initially
- Separate prod/dev requirements
- UserProvider singleton pattern for multi-tenancy
@@ -0,0 +1,25 @@
# Python
__pycache__/
*.py[cod]
*.so
.Python
build/
dist/
*.egg-info/
# Virtual environments
.venv/
venv/
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDE
.idea/
.vscode/
*.swp
# Project
.current_template
+64
View File
@@ -0,0 +1,64 @@
# Calculator CLI - Tasks for Webber
A simple calculator with intentional bugs and missing features for testing Webber's capabilities.
## Bugs to Fix
### High Priority
- [ ] **Division by zero** - `operations.py:divide()` crashes when dividing by zero instead of returning an error
- [ ] **Invalid operation name** - `main.py:get_operation()` raises KeyError for unknown operations instead of helpful error message
### Medium Priority
- [ ] **Power function broken** - `operations.py:power()` doesn't handle negative exponents or fractional exponents correctly
- [ ] **No input validation** - `main.py` doesn't validate that command-line arguments are valid numbers
## Missing Tests
- [ ] Add `TestDivide` class with tests for:
- Normal division
- Division by zero (should test error handling once bug is fixed)
- Division with negative numbers
- [ ] Add `TestPower` class with tests for:
- Positive integer exponents
- Zero exponent (should return 1)
- Negative exponents
- [ ] Complete existing test classes:
- `test_add_zero`
- `test_add_floats`
- `test_subtract_negative`
- `test_multiply_by_zero`
## Features to Add
- [ ] **Expose power operation** - Add 'pow' to the operations dictionary in `main.py`
- [ ] **Add modulo operation** - Implement `modulo(a, b)` in operations.py
- [ ] **Add --verbose flag** - Show step-by-step calculation
- [ ] **Add history command** - Track and display recent calculations
- [ ] **Add REPL mode** - Interactive calculator loop
## Code Quality
- [ ] Add type hints to all functions
- [ ] Add docstrings following Google style
- [ ] Fix any linting errors (run `ruff check src/`)
## How to Run
```bash
# Setup
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Run calculator
python -m calculator.main 10 5 add
python -m calculator.main 10 5 div
# Run tests
pytest tests/ -v
# See failing tests (division by zero)
python -m calculator.main 10 0 div
```
@@ -0,0 +1,17 @@
[project]
name = "calculator"
version = "0.1.0"
description = "A simple calculator CLI with some bugs"
requires-python = ">=3.12"
[build-system]
requires = ["setuptools>=75.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-v"
@@ -0,0 +1,2 @@
# Calculator CLI dependencies
pytest>=8.0.0
@@ -0,0 +1,3 @@
"""Calculator CLI - A simple calculator with some bugs for testing."""
__version__ = "0.1.0"
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""
Calculator CLI - A simple command-line calculator.
NOTE: This file contains intentional bugs for testing purposes.
Usage:
python -m calculator.main 10 5 add
python -m calculator.main 10 5 sub
python -m calculator.main 10 5 mul
python -m calculator.main 10 5 div
"""
import sys
from calculator.operations import add, subtract, multiply, divide
def get_operation(op_name: str):
"""
Get the operation function by name.
BUG: No validation - invalid operation names cause KeyError!
"""
operations = {
"add": add,
"sub": subtract,
"mul": multiply,
"div": divide,
# BUG: 'power' is implemented in operations.py but not exposed here
}
# BUG: Should handle KeyError gracefully
return operations[op_name]
def main():
"""Main entry point."""
if len(sys.argv) != 4:
print("Usage: python -m calculator.main <a> <b> <operation>")
print("Operations: add, sub, mul, div")
sys.exit(1)
# BUG: No validation that a and b are valid numbers
a = float(sys.argv[1])
b = float(sys.argv[2])
op_name = sys.argv[3]
# BUG: This will crash with KeyError for invalid operation
operation = get_operation(op_name)
result = operation(a, b)
print(f"Result: {result}")
if __name__ == "__main__":
main()
@@ -0,0 +1,43 @@
"""
Math operations for the calculator.
NOTE: This file contains intentional bugs for testing purposes.
"""
def add(a: float, b: float) -> float:
"""Add two numbers."""
return a + b
def subtract(a: float, b: float) -> float:
"""Subtract b from a."""
return a - b
def multiply(a: float, b: float) -> float:
"""Multiply two numbers."""
return a * b
def divide(a: float, b: float) -> float:
"""
Divide a by b.
BUG: Does not handle division by zero!
"""
# BUG: No check for b == 0
return a / b
def power(a: float, b: float) -> float:
"""
Raise a to the power of b.
BUG: Negative exponents not handled correctly for some cases.
"""
# BUG: This naive implementation has issues with negative bases and fractional exponents
result = 1
for _ in range(int(b)):
result *= a
return result
@@ -0,0 +1 @@
"""Calculator tests."""
@@ -0,0 +1,46 @@
"""
Tests for calculator operations.
NOTE: Test coverage is intentionally incomplete for testing purposes.
"""
import pytest
from calculator.operations import add, subtract, multiply
class TestAdd:
"""Tests for add operation."""
def test_add_positive_numbers(self):
assert add(2, 3) == 5
def test_add_negative_numbers(self):
assert add(-2, -3) == -5
# MISSING: test_add_zero, test_add_floats
class TestSubtract:
"""Tests for subtract operation."""
def test_subtract_positive(self):
assert subtract(5, 3) == 2
# MISSING: test_subtract_negative, test_subtract_resulting_negative
class TestMultiply:
"""Tests for multiply operation."""
def test_multiply_positive(self):
assert multiply(3, 4) == 12
# MISSING: test_multiply_by_zero, test_multiply_negative
# MISSING: TestDivide class entirely!
# - test_divide_positive
# - test_divide_by_zero (should test error handling)
# - test_divide_negative
# MISSING: TestPower class entirely!
+25
View File
@@ -0,0 +1,25 @@
# Python
__pycache__/
*.py[cod]
*.so
.Python
build/
dist/
*.egg-info/
# Virtual environments
.venv/
venv/
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDE
.idea/
.vscode/
*.swp
# Project
.current_template
+18
View File
@@ -0,0 +1,18 @@
# My Project - Tasks
A blank starter template. Define your own tasks here.
## Setup
```bash
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
## Tasks
- [ ] Define your project goals
- [ ] Add source files to `src/myproject/`
- [ ] Add tests to `tests/`
- [ ] Update `requirements.txt` with dependencies
+17
View File
@@ -0,0 +1,17 @@
[project]
name = "myproject"
version = "0.1.0"
description = "A blank starter project"
requires-python = ">=3.12"
[build-system]
requires = ["setuptools>=75.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-v"
+2
View File
@@ -0,0 +1,2 @@
# Add your dependencies here
pytest>=8.0.0
@@ -0,0 +1,3 @@
"""My Project - A blank starter template."""
__version__ = "0.1.0"
@@ -0,0 +1 @@
"""Tests for myproject."""
Executable
+193
View File
@@ -0,0 +1,193 @@
#!/bin/bash
# Sandbox management script for Webber testing
#
# Usage:
# ./sandbox.sh list - List available templates
# ./sandbox.sh load <template> - Load a template into sandbox
# ./sandbox.sh reset - Reset sandbox to last loaded template
# ./sandbox.sh save <name> - Save current sandbox as new template
# ./sandbox.sh status - Show current sandbox status
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SANDBOX_DIR="$SCRIPT_DIR/webber-sandbox"
TEMPLATES_DIR="$SCRIPT_DIR/sandbox-templates"
MARKER_FILE="$SANDBOX_DIR/.current_template"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
usage() {
echo "Webber Sandbox Manager"
echo ""
echo "Usage: ./sandbox.sh <command> [template]"
echo ""
echo "Commands:"
echo " list List available templates"
echo " load <template> Load a template into sandbox (preserves .venv)"
echo " reset Reset sandbox to last loaded template"
echo " save <name> Save current sandbox as new template"
echo " status Show current sandbox status"
echo ""
echo "Available templates:"
ls -1 "$TEMPLATES_DIR" 2>/dev/null || echo " (none)"
}
list_templates() {
echo "Available templates:"
echo ""
for dir in "$TEMPLATES_DIR"/*/; do
if [ -d "$dir" ]; then
name=$(basename "$dir")
desc=""
if [ -f "$dir/TASKS.md" ]; then
desc=$(head -1 "$dir/TASKS.md" | sed 's/^#\s*//')
fi
printf " %-20s %s\n" "$name" "$desc"
fi
done
}
load_template() {
local template="$1"
if [ -z "$template" ]; then
echo -e "${RED}Error: Template name required${NC}"
echo "Usage: ./sandbox.sh load <template>"
exit 1
fi
if [ ! -d "$TEMPLATES_DIR/$template" ]; then
echo -e "${RED}Error: Template '$template' not found${NC}"
echo "Available templates:"
ls -1 "$TEMPLATES_DIR"
exit 1
fi
echo -e "${YELLOW}Loading template: $template${NC}"
# Create sandbox dir if needed
mkdir -p "$SANDBOX_DIR"
# Clear sandbox contents (except .venv and .git)
find "$SANDBOX_DIR" -mindepth 1 -maxdepth 1 ! -name '.venv' ! -name '.git' -exec rm -rf {} +
# Copy template contents (including hidden files)
cp -r "$TEMPLATES_DIR/$template/." "$SANDBOX_DIR/"
# Mark which template was loaded
echo "$template" > "$MARKER_FILE"
echo -e "${GREEN}Loaded template: $template${NC}"
echo ""
echo "Next steps:"
echo " cd webber-sandbox"
if [ ! -d "$SANDBOX_DIR/.venv" ]; then
echo " python3.12 -m venv .venv"
fi
echo " source .venv/bin/activate"
echo " pip install -r requirements.txt"
echo ""
if [ -f "$SANDBOX_DIR/TASKS.md" ]; then
echo "Tasks available in TASKS.md"
fi
}
reset_template() {
if [ ! -f "$MARKER_FILE" ]; then
echo -e "${RED}Error: No template loaded yet${NC}"
echo "Use './sandbox.sh load <template>' first"
exit 1
fi
local template=$(cat "$MARKER_FILE")
echo "Resetting to template: $template"
load_template "$template"
}
save_template() {
local name="$1"
if [ -z "$name" ]; then
echo -e "${RED}Error: Template name required${NC}"
echo "Usage: ./sandbox.sh save <name>"
exit 1
fi
if [ -d "$TEMPLATES_DIR/$name" ]; then
echo -e "${YELLOW}Warning: Template '$name' already exists${NC}"
read -p "Overwrite? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Cancelled"
exit 0
fi
rm -rf "$TEMPLATES_DIR/$name"
fi
mkdir -p "$TEMPLATES_DIR/$name"
# Copy sandbox contents (except .venv, .git, __pycache__)
rsync -a --exclude='.venv' --exclude='.git' --exclude='__pycache__' \
--exclude='*.pyc' --exclude='.pytest_cache' --exclude='.mypy_cache' \
"$SANDBOX_DIR/" "$TEMPLATES_DIR/$name/"
echo -e "${GREEN}Saved template: $name${NC}"
}
show_status() {
echo "Sandbox Status"
echo "=============="
echo ""
echo "Sandbox directory: $SANDBOX_DIR"
if [ -f "$MARKER_FILE" ]; then
echo "Current template: $(cat "$MARKER_FILE")"
else
echo "Current template: (none loaded)"
fi
if [ -d "$SANDBOX_DIR/.venv" ]; then
echo "Virtual env: exists"
else
echo "Virtual env: not created"
fi
echo ""
echo "Contents:"
if [ -d "$SANDBOX_DIR" ]; then
ls -la "$SANDBOX_DIR" 2>/dev/null | tail -n +4
else
echo " (sandbox not initialized)"
fi
}
# Main command dispatch
case "${1:-}" in
list)
list_templates
;;
load)
load_template "$2"
;;
reset)
reset_template
;;
save)
save_template "$2"
;;
status)
show_status
;;
-h|--help|"")
usage
;;
*)
echo -e "${RED}Unknown command: $1${NC}"
usage
exit 1
;;
esac
-53
View File
@@ -1,53 +0,0 @@
# Agents Domain
This domain contains PydanticAI agent definitions and orchestration.
## Structure
```
agents/
├── router.py # Agent routes (list, run)
├── controller.py # Agent orchestration logic
├── schemas.py # Request/response models
├── explore/ # Explore agent - codebase navigation
│ ├── agent.py # PydanticAI agent definition
│ └── prompts.py # System prompts
├── plan/ # Plan agent - implementation design
│ ├── agent.py
│ └── prompts.py
└── task/ # Task agent - execution
├── agent.py
└── prompts.py
```
## PydanticAI Pattern
```python
from pydantic_ai import Agent
from pydantic_ai.models.ollama import OllamaModel
from src.shared.config import get_settings
settings = get_settings()
explore_agent = Agent(
OllamaModel(settings.ollama_agent_model, base_url=settings.ollama_url),
system_prompt='You are a code exploration assistant...',
)
@explore_agent.tool
async def search_files(ctx, pattern: str) -> str:
"""Search for files matching pattern."""
# Implementation uses tools from src/domains/tools/
pass
```
## Adding a New Agent
1. Create a new directory under `agents/` (e.g., `agents/review/`)
2. Create `agent.py` with PydanticAI Agent definition
3. Create `prompts.py` with system prompts
4. Register in `controller.py`
5. Add tests in `tests/domains/test_agents/`
View File
View File
-70
View File
@@ -1,70 +0,0 @@
# Tools Domain
This domain contains tool implementations for agent use.
## Structure
```
tools/
├── router.py # Tool routes (list, execute)
├── controller.py # Tool orchestration
├── schemas.py # Tool request/response models
├── file/ # File operation tools
│ ├── read.py # Read file contents
│ ├── write.py # Write file contents
│ └── glob.py # Find files by pattern
├── shell/ # Shell execution tools
│ └── bash.py # Execute bash commands
└── search/ # Search tools
├── grep.py # Search file contents
└── web.py # Web search
```
## Tool Pattern
Tools are registered with PydanticAI agents via the `@agent.tool` decorator.
Each tool should:
1. Have clear input/output types
2. Include a docstring (used by LLM)
3. Handle errors gracefully
4. Respect sandbox settings
```python
from src.shared.config import get_settings
from src.shared.logging import logged
settings = get_settings()
@logged()
async def read_file(file_path: str, limit: int = 2000) -> str:
"""
Read contents of a file.
Args:
file_path: Absolute path to the file
limit: Maximum lines to read
Returns:
File contents as string
"""
# Check path is allowed
if settings.sandbox_enabled:
# Validate against allowed_paths
pass
# Read and return
pass
```
## Adding a New Tool
1. Create a new file in appropriate category (file/, shell/, search/)
2. Implement the tool function with proper types and docstring
3. Add `@logged()` decorator for timing
4. Handle sandbox restrictions
5. Register with agents that need it
6. Add tests
View File
View File
View File
View File
-76
View File
@@ -1,76 +0,0 @@
"""
Application configuration via Pydantic Settings.
All settings loaded from environment variables or .env file.
"""
import tomllib
from pathlib import Path
from functools import lru_cache
from typing import Optional
from pydantic_settings import BaseSettings, SettingsConfigDict
def _get_version() -> str:
"""Load version from pyproject.toml."""
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
try:
with open(pyproject_path, "rb") as f:
return tomllib.load(f).get("project", {}).get("version", "0.0.0")
except FileNotFoundError:
return "0.0.0"
__version__ = _get_version()
class Settings(BaseSettings):
"""Application settings loaded from environment."""
# Application
app_name: str = "Webber"
app_version: str = __version__
debug: bool = False
# Server
host: str = "0.0.0.0"
port: int = 8086
# Logging
log_level: str = "INFO"
# CORS
cors_origins: list[str] = ["http://localhost:3000", "http://localhost:8080"]
cors_credentials: bool = True
cors_methods: list[str] = ["*"]
cors_headers: list[str] = ["*"]
# LLM - Ollama (always hot in VRAM on tower-of-joy)
ollama_url: str = "http://192.168.86.149:11434"
ollama_agent_model: str = "mistral-nemo-large:latest"
ollama_embed_model: str = "nomic-embed-text:latest"
# Auth - Tatlock integration
tatlock_api_url: Optional[str] = "http://192.168.86.149:8000"
internal_api_key: Optional[str] = None
# Tool execution
tool_timeout_seconds: int = 120
sandbox_enabled: bool = True
allowed_paths: list[str] = []
# Sessions
session_ttl_hours: int = 24
max_context_tokens: int = 128000
model_config = SettingsConfigDict(
env_file=".env",
case_sensitive=False,
extra="ignore",
)
@lru_cache()
def get_settings() -> Settings:
"""Cached settings singleton."""
return Settings()
View File
-34
View File
@@ -1,34 +0,0 @@
"""
Pytest configuration and fixtures.
"""
import pytest
from httpx import AsyncClient, ASGITransport
from src.main import app
@pytest.fixture
def anyio_backend():
"""Use asyncio for async tests."""
return "asyncio"
@pytest.fixture
async def client():
"""Async HTTP client for testing."""
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test"
) as ac:
yield ac
@pytest.fixture
async def auth_client():
"""Async HTTP client with API key for authenticated requests."""
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
headers={"X-API-Key": "test-api-key"}
) as ac:
yield ac
+128
View File
@@ -0,0 +1,128 @@
# AGENTS.md
> **Start every session by reading this file.**
> This file outlines the operational protocols, coding standards, and architectural decisions for this FastAPI project.
## 1. Agent Operational Protocols
### 🧠 Work Patterns (Plan-Act-Reflect)
* **Plan:** Before writing code, briefly outline your plan. Identify which files you will touch and what the side effects might be.
* **Act:** Execute the changes in small, atomic steps.
* **Reflect:** After coding, verify your work. Did you break existing tests? Did you add new tests?
### 🛡️ Git Discipline
* **ALWAYS add the relevant tests for the added code** Make sure to keep the test coverage up as we go, and run tests before commiting.
* **NEVER commit to `main` or `master` directly.** Always create a feature branch: `feature/your-feature-name` or `fix/issue-description`.
* **Commit Messages:** Use the [Conventional Commits](https://www.conventionalcommits.org/) format.
* `feat: add user login endpoint`
* `fix: resolve database connection timeout`
* `refactor: split monolith dependency file`
* **Atomic Commits:** Keep commits small. One logical change = one commit.
### 📝 Changelog Maintenance
* **Update `CHANGELOG.md`** with every user-facing change.
* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`.
### 🚀 Release Flow
When changes are ready for deployment:
1. **Ask user if deploy cycle is desired **
2. **Update version** in `pyproject.toml`:
- Bug fixes: bump patch version (1.8.3 → 1.8.4)
- New features: bump minor version (1.8.4 → 1.9.0)
3. **Update CHANGELOG.md**:
- Move items from `[Unreleased]` to new version section
- Add release date: `## [1.8.4] - 2025-12-16`
4. **Commit and tag**:
```bash
git add -A
git commit -m "fix: description of changes"
git tag v1.8.4
git push origin main --tags
```
5. **CI/CD triggers automatically**:
- Gitea CI builds Docker image on new version tag (starts with "v")
- Watchtower pulls and deploys to production
- Verify deployment: `curl http://192.168.86.149:8086/health`
---
### 🧪 Local Development Setup
* **Always test locally first** before committing and deploying. The build-deploy loop is slow.
* **Only deploy** when a phase or feature is complete and tested locally
* **Environment**: Copy `.env.example` to `.env` and configure for your local setup
#### ⚠️ CRITICAL: Starting the Local Server
**ALWAYS use `./wakeup.sh` to start the local server. NEVER use raw uvicorn commands.**
```bash
./wakeup.sh
```
The wakeup script provides:
- **Port conflict detection** - Warns if port 8086 is already in use
- **Virtual environment activation** - Ensures correct Python environment
- **Centralized logging** - All logs written to `logs/server.log` for easy tailing
- **Auto-reload** - Code changes picked up automatically (except requirements.txt changes)
- **Consistent configuration** - Same startup every time
To monitor logs in another terminal:
```bash
tail -f logs/server.log
```
To stop the server: Press `Ctrl+C`
To kill a stuck server:
```bash
pkill -f "uvicorn src.main:app"
# or
kill $(lsof -t -i:8086)
```
#### Testing
**Test REST endpoints** against `http://localhost:8086`:
```bash
curl http://localhost:8086/health
curl http://localhost:8086/
curl http://localhost:8086/docs # Swagger UI
```
**Running tests**: Always use the venv explicitly to avoid environment mismatches:
```bash
.venv/bin/python -m pytest tests/ # All tests
.venv/bin/python -m pytest tests/ -v # Verbose output
.venv/bin/python -m pytest tests/ --cov # With coverage
```
---
## 1.5 Known Issues & Future Improvements
### Explore Agent
- **Model Hallucination**: Mistral Nemo sometimes hallucinates file contents instead of using actual tool results. Consider using a more capable model (codestral, qwen2.5-coder) or adding response validation.
- **Ollama Provider**: We use a custom `WebberOllamaProvider` (ported from tatlock) that sanitizes `content: null` to `content: ""` for assistant messages with tool calls. This works around an Ollama API limitation.
- **Gitignore Support**: ✅ Fixed - The filesystem tools now honor `.gitignore` patterns and default ignores (`.venv/`, `__pycache__/`, `node_modules/`, etc.).
---
## 2. FastAPI Architecture & Best Practices
*Reference: [FastAPI Best Practices](https://github.com/zhanymkanov/fastapi-best-practices)*
### 📂 Project Structure (Directory-based, NOT File-type based)
Do **not** group files by type (e.g., one huge `routers` folder). Group by **domain/module** inside a `src/` directory.
**Correct Structure:**
```text
to be determined
+25
View File
@@ -0,0 +1,25 @@
FROM python:3.12-slim
WORKDIR /app
# Install system dependencies (curl for healthcheck)
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY pyproject.toml .
COPY src/ ./src/
ENV PYTHONPATH=/app
EXPOSE 8086
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8086/health || exit 1
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8086", "--workers", "1"]
+271
View File
@@ -0,0 +1,271 @@
# Webber Feature Coverage
> Tracking progress towards Claude Code-like functionality
## Current Status: ~80% Complete
Last updated: 2026-01-11
---
## Phase 1-6: Foundation (Original Plan)
### Phase 1: Tool Infrastructure ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| `BaseTool` abstract class | ✅ | `src/domains/tools/base.py` |
| `ToolResult` dataclass | ✅ | Consistent success/error/truncated handling |
| `ReadFileTool` | ✅ | With line numbers, offset/limit support |
| `GlobFilesTool` | ✅ | Pattern matching, sorted by mtime |
| `GrepContentTool` | ✅ | Regex search with context lines |
| `BashReadOnlyTool` | ✅ | Allowlist-based command filtering |
| `EditFileTool` | ✅ | Find-and-replace with unique match validation |
| `WriteFileTool` | ✅ | Create/overwrite files with size limits |
| `BashTool` (full) | ✅ | Write-enabled shell with safety controls |
| `WebSearchTool` | ✅ | SearXNG integration for web search |
| Path validation | ✅ | `allowed_paths` restriction |
**Status:** Tools honor `.gitignore` patterns and default ignores (`.venv/`, `__pycache__/`, etc.)
### Phase 2: Explore Agent ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| `BaseAgent` abstract class | ✅ | `src/domains/agents/base.py` |
| Agent registry | ✅ | `register_agent()`, `get_agent()`, `list_agents()` |
| `ExploreAgentImpl` | ✅ | PydanticAI-based implementation |
| System prompts | ✅ | Mistral-optimized with tool examples |
| Tool registration | ✅ | `@agent.tool` decorator pattern |
| Sanitized Ollama provider | ✅ | Fixes `content: null` issue |
| Streaming support | ✅ | `run_stream()` method with SSE |
**Available tools:** `read_file`, `glob_files`, `grep_content`, `bash_readonly`, `edit_file`, `write_file`, `bash`, `web_search`
**Gap:** Mistral Nemo sometimes hallucinates instead of using tool results.
### Phase 2b: Plan Agent ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| `PlanAgentImpl` | ✅ | READ-ONLY software architect agent |
| System prompts | ✅ | Architecture-focused with tool examples |
| Tool registration | ✅ | Only read-only tools (4 tools) |
| Streaming support | ✅ | `run_stream()` method with SSE |
| Unit tests | ✅ | 15 tests for registration, tools, API |
**Available tools:** `read_file`, `glob_files`, `grep_content`, `bash_readonly` (read-only only)
**Purpose:** Design implementation strategies before coding - explores codebase and creates step-by-step plans.
### Phase 3: CLI Foundation ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| Typer + Rich setup | ✅ | Standalone `webber-cli/` package |
| `webber-cli --version` | ✅ | Shows version from pyproject.toml |
| Console theming | ✅ | Centralized color palette |
| Markdown rendering | ✅ | Rich markdown output |
| Streaming display | ✅ | Real-time token output with `--stream` flag |
### Phase 4: Agentic Loop ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| `webber-cli chat` command | ✅ | Interactive mode with streaming |
| `webber-cli explore` command | ✅ | One-shot query with streaming |
| `SessionState` dataclass | ✅ | Basic context tracking |
| `AgenticLoop` class | ✅ | Basic implementation |
| Conversation persistence | ✅ | SQLAlchemy async with SQLite/PostgreSQL |
| Context summarization | ✅ | Token counting (litellm) + auto-summarization |
| Conversation API | ✅ | `/conversations/` REST endpoints |
**Database:** SQLite (dev) or PostgreSQL (prod), async via SQLAlchemy 2.0
### Phase 5: REST API ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| `POST /agents/run` | ✅ | Execute agent with prompt |
| `POST /agents/stream` | ✅ | SSE streaming responses |
| `GET /agents/` | ✅ | List available agents |
| `GET /agents/{name}` | ✅ | Get agent info |
| Request/response schemas | ✅ | Pydantic models |
### Phase 6: Polish & Tests ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| Tool unit tests | ✅ | 109 tests total |
| API endpoint tests | ✅ | 11 tests for agent routes |
| Health check tests | ✅ | 2 tests |
| Security tests | ✅ | 14 tests for path traversal, injection |
| Integration tests | ✅ | 10 tests with real LLM (requires Ollama) |
| E2E tests | ✅ | 12 tests against running API server |
---
## Future Work: Remaining Features
### High Priority
| Feature | Category | Description | Complexity |
|---------|----------|-------------|------------|
| ~~**Plan Agent**~~ | Agents | ✅ Design implementation approaches | High |
| ~~**Task Agent**~~ | Agents | ✅ Autonomous multi-step execution | High |
| ~~**Context summarization**~~ | Infrastructure | ✅ Token counting + auto-summarization | High |
| ~~**Conversation persistence**~~ | Infrastructure | ✅ SQLAlchemy async database layer | Medium |
### Medium Priority
| Feature | Category | Description | Complexity |
|---------|----------|-------------|------------|
| **Web search summarizer** | Tools | Agent to extract core content from web pages (remove nav, footers, etc.) and preserve relevant links for nested fetching | Medium |
| **Tool result caching** | Infrastructure | Cache file reads for performance | Low |
| **Session persistence** | CLI | Save/resume conversations | Medium |
| **Todo tracking** | CLI | Built-in task list (`/todo`) | Medium |
| **Git integration** | CLI | Auto-commit, branch management | Medium |
| **Agent handoff** | Orchestration | Explore → Plan → Task workflow | High |
| **Retry logic** | Infrastructure | Auto-retry on tool failures | Low |
### Low Priority
| Feature | Category | Description | Complexity |
|---------|----------|-------------|------------|
| **Notebook editing** | Tools | Jupyter cell manipulation | Medium |
| **MCP support** | Infrastructure | Model Context Protocol | High |
| **Config file** | CLI | `~/.webber/config.toml` | Low |
| **IDE integration** | CLI | VS Code extension | High |
| **Parallel agents** | Orchestration | Concurrent agent execution | High |
| **Agent memory** | Orchestration | Shared context between agents | Medium |
---
## Testing Coverage
| Area | Current | Target | Status |
|------|---------|--------|--------|
| Tool unit tests | 109 | 109 | ✅ |
| API tests | 11 | 11 | ✅ |
| Plan agent tests | 15 | 15 | ✅ |
| Task agent tests | 15 | 15 | ✅ |
| Conversation tests | 19 | 19 | ✅ |
| Token tests | 6 | 6 | ✅ |
| Security tests | 14 | 14 | ✅ |
| Integration tests | 10 | 10 | ✅ Agent + real LLM |
| E2E tests | 12 | 12 | ✅ Full API workflow |
**Total: 176 tests passing**
**Test breakdown:**
- Read/Glob/Grep tools: 17 tests
- Edit/Write tools: 22 tests
- Bash tools: 22 tests
- Web search: 10 tests
- Gitignore filtering: 10 tests
- API endpoints: 11 tests
- Plan agent: 15 tests
- Task agent: 15 tests
- Conversations: 19 tests
- Tokens: 6 tests
- Security: 14 tests
- Health checks: 2 tests
- Integration (LLM): 10 tests
- E2E (API): 12 tests
**Running tests:**
```bash
# Unit tests only (default)
pytest tests/
# Include integration tests (requires Ollama)
pytest tests/ --run-integration
# Include E2E tests (requires running API server)
pytest tests/ --run-e2e
# All tests
pytest tests/ --run-integration --run-e2e
```
---
## Known Issues
1. **Model hallucination** - Mistral Nemo sometimes makes up file contents instead of using actual tool results.
2. **Temperature setting** - Changed from 0.0 to 0.3 for Mistral Nemo compatibility, may affect determinism.
3. **SQLAlchemy deprecation** - `datetime.utcnow()` deprecation warning from SQLAlchemy.
---
## Architecture Decisions Made
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Monorepo structure | `webber-api/`, `webber-cli/` | Separate packages, shared root |
| Sanitized Ollama provider | Custom wrapper | Fixes PydanticAI + Ollama `content: null` bug |
| Dev port 8095 | Separate from prod 8086 | Avoid conflicts with Docker deployment |
| Tool choice "required" | Force tool use | Mistral Nemo needs explicit instruction |
| Temperature 0.3 | Mistral recommendation | 0.0 caused issues with Nemo |
| SearXNG for search | Self-hosted | Privacy, no API keys needed |
| SSE for streaming | Server-Sent Events | Simple, well-supported |
---
## Quick Reference: What Works Now
```bash
# Start dev server
cd webber-api && ./wakeup.sh
# CLI commands (from webber-cli/)
.venv/bin/webber-cli status # Check API connection
.venv/bin/webber-cli explore "find tests" # One-shot exploration
.venv/bin/webber-cli explore "query" --no-stream # Batch mode
.venv/bin/webber-cli chat # Interactive mode
# API endpoints
curl http://localhost:8095/health
curl http://localhost:8095/agents/
curl -X POST http://localhost:8095/agents/run \
-H "Content-Type: application/json" \
-d '{"agent_type":"explore","prompt":"list python files","working_dir":"."}'
# Plan agent (read-only, creates implementation plans)
curl -X POST http://localhost:8095/agents/run \
-H "Content-Type: application/json" \
-d '{"agent_type":"plan","prompt":"plan how to add user auth","working_dir":"."}'
# Streaming endpoint
curl -N http://localhost:8095/agents/stream \
-H "Content-Type: application/json" \
-d '{"agent_type":"explore","prompt":"find config files","working_dir":"."}'
# Conversation API (stateful multi-turn)
curl -X POST http://localhost:8095/conversations/ \
-H "Content-Type: application/json" \
-H "X-API-Key: dev-key" \
-d '{"agent_type":"explore","working_dir":"."}'
curl -X POST http://localhost:8095/conversations/{id}/messages \
-H "Content-Type: application/json" \
-H "X-API-Key: dev-key" \
-d '{"content":"find all Python files"}'
```
---
## Tools Available
| Tool | Type | Description |
|------|------|-------------|
| `read_file` | Read | Read file contents with line numbers |
| `glob_files` | Read | Find files by pattern |
| `grep_content` | Read | Search file contents with regex |
| `bash_readonly` | Read | Safe bash commands (ls, git status, etc.) |
| `edit_file` | Write | Find-and-replace editing |
| `write_file` | Write | Create/overwrite files |
| `bash` | Write | Full bash with safety controls |
| `web_search` | External | Search web via SearXNG |
+349
View File
@@ -0,0 +1,349 @@
# Webber Architecture
Multi-Agent AI Development System - similar to Claude Code but running locally with configurable models.
## Overview
Webber is a FastAPI-based agent orchestration service that provides:
- Multi-agent execution (Explore, Plan, Task)
- Tool capabilities (file operations, shell, search)
- Multi-tenant authentication via Tatlock integration
- PydanticAI framework for LLM orchestration
**Port:** 8086
**Runtime:** Python 3.12, FastAPI, Uvicorn
**Agent Framework:** PydanticAI
**Default LLM:** Ollama with mistral-nemo-large:latest
---
## Directory Structure
```
webber/
├── src/
│ ├── main.py # App entry point (NO routes)
│ │
│ ├── shared/ # Cross-cutting concerns
│ │ ├── base.py # BaseController, BaseSchema
│ │ ├── config.py # Pydantic Settings
│ │ ├── logging.py # @logged decorator, trace_span
│ │ ├── exceptions.py # Custom exception hierarchy
│ │ ├── auth.py # API key validation
│ │ └── context.py # UserProvider singleton
│ │
│ └── domains/ # Feature domains
│ ├── router.py # Root router (composes all)
│ ├── health/ # Health endpoints
│ ├── auth/ # Authentication
│ ├── agents/ # Agent orchestration
│ │ ├── explore/ # Codebase navigation
│ │ ├── plan/ # Implementation design
│ │ └── task/ # Execution
│ └── tools/ # Tool execution
│ ├── file/ # Read, write, glob
│ ├── shell/ # Bash execution
│ └── search/ # Grep, web search
├── tests/
├── docs/
└── logs/
```
---
## Core Patterns
### 1. Clean main.py
The entry point contains ONLY:
- FastAPI app creation with lifespan
- CORS middleware
- Auth middleware (sets UserProvider)
- Exception handlers
- Single router include (`root_router`)
All routes live in domain routers. This keeps main.py focused on app initialization.
### 2. Domain-Based Structure
Each feature domain has its own directory:
```
domains/
├── router.py # Root router composing all domains
├── health/
│ ├── router.py # Domain routes
│ └── controller.py # Business logic
├── agents/
│ ├── router.py # Agent routes
│ ├── controller.py # Orchestration logic
│ ├── schemas.py # Request/response models
│ └── explore/ # Agent implementation
│ ├── agent.py # PydanticAI agent
│ └── prompts.py # System prompts
```
### 3. BaseController Pattern
Controllers use lazy router instantiation:
```python
from src.shared.base import BaseController
class MyController(BaseController):
def __init__(self):
super().__init__(prefix="/my", tags=["My"])
def create_router(self) -> APIRouter:
router = APIRouter(prefix=self.prefix, tags=self.tags)
@router.get("/")
async def list_items():
return []
return router
my_controller = MyController()
# Use: my_controller.router
```
### 4. UserProvider Singleton
Request-scoped user context without parameter passing:
```python
# In middleware (main.py):
user = await validate_api_key(api_key)
user_provider.set_user(user)
try:
response = await call_next(request)
finally:
user_provider.clear_user()
# Anywhere in code:
from src.shared.context import get_current_user, require_user
user = get_current_user() # Returns None if not authenticated
user = require_user() # Raises if not authenticated
```
Uses Python's `contextvars` for async-safe request isolation.
### 5. Logger with Temporal Benchmarking
The `@logged()` decorator automatically tracks execution time:
```python
from src.shared.logging import logged, trace_span, get_logger
logger = get_logger(__name__)
@logged()
async def my_function():
# Automatically logs entry, exit, and duration
pass
@logged(slow_threshold_ms=50, warn_threshold_ms=200)
def critical_path():
# Custom thresholds for performance-critical code
pass
async def complex_operation():
async with trace_span("llm_call"):
# Manual span for specific sections
result = await agent.run(prompt)
```
Features:
- Trace ID correlation across nested calls
- Configurable slow/warn thresholds
- DEBUG: all calls logged with timing
- INFO: slow calls (>100ms default)
- WARNING: very slow calls (>500ms default)
- ERROR: failed calls with stack trace
### 6. Exception Hierarchy
```python
from src.shared.exceptions import (
AppException,
NotFoundError,
ValidationError,
AuthenticationError,
AuthorizationError,
)
# Raise with context:
raise NotFoundError("User", user_id)
raise ValidationError("email", "Invalid format")
# Automatic JSON response via exception handlers in main.py
```
---
## Configuration
All settings via environment variables or `.env`:
| Variable | Default | Description |
|----------|---------|-------------|
| DEBUG | false | Enable debug mode |
| LOG_LEVEL | INFO | Logging level |
| HOST | 0.0.0.0 | Server host |
| PORT | 8086 | Server port |
| OLLAMA_URL | http://192.168.86.149:11434 | Ollama API URL |
| OLLAMA_AGENT_MODEL | mistral-nemo-large:latest | Agent reasoning model |
| OLLAMA_EMBED_MODEL | nomic-embed-text:latest | Embedding model |
| TATLOCK_API_URL | http://192.168.86.149:8000 | Tatlock auth service |
| TOOL_TIMEOUT_SECONDS | 120 | Tool execution timeout |
| SANDBOX_ENABLED | true | Enable sandboxed execution |
| ALLOWED_PATHS | [] | Paths accessible to tools |
| SESSION_TTL_HOURS | 24 | Session expiry |
| MAX_CONTEXT_TOKENS | 128000 | Max context window |
---
## Agent Architecture
Webber uses PydanticAI for agent orchestration. Each agent type is purpose-built:
### Explore Agent
Fast codebase exploration for:
- Finding files by pattern
- Searching code for keywords
- Answering questions about structure
### Plan Agent
Implementation design for:
- Analyzing requirements
- Creating step-by-step plans
- Identifying files to modify
- Considering trade-offs
### Task Agent
Autonomous execution for:
- Multi-step implementations
- Tool orchestration
- Code generation and modification
---
## Tool Architecture
Tools are sandboxed operations agents can invoke:
### File Tools
- **Read**: Read file contents with line limits
- **Write**: Create or overwrite files
- **Edit**: String replacement in files
- **Glob**: Pattern-based file search
### Shell Tools
- **Bash**: Command execution with timeout
- Sandboxed to allowed paths
- Captures stdout/stderr
### Search Tools
- **Grep**: Regex content search via ripgrep
- **WebSearch**: Web search integration (optional)
---
## Authentication Flow
1. Client sends `X-API-Key` header
2. Auth middleware calls `validate_api_key()`
3. Tatlock validates key and returns user info
4. UserProvider stores user in request context
5. Routes access via `get_current_user()` or `require_user()`
6. Middleware clears user in `finally` block
---
## Deployment
### Docker
```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY pyproject.toml .
COPY src/ ./src/
ENV PYTHONPATH=/app
EXPOSE 8086
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8086/health || exit 1
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8086"]
```
### CI/CD
Gitea Actions workflow:
1. Push tag `v*` triggers build
2. Creates Gitea release
3. Builds and pushes Docker image to registry
4. Watchtower auto-deploys to production
### Production Stack
Deployed in Portainer `agents` stack alongside Tatlock:
- Network: `docker-dataplane`
- Registry: `git.schweitz.internal/jpmschweitzer/webber`
- Auto-update: Watchtower with label `com.centurylinklabs.watchtower.enable=true`
---
## Adding New Domains
1. Create domain directory under `src/domains/`
2. Add `router.py` with routes
3. Add `controller.py` with business logic
4. Add `schemas.py` for request/response models
5. Import and include router in `src/domains/router.py`
6. Add tests in `tests/test_<domain>.py`
---
## Adding New Agents
1. Create agent directory under `src/domains/agents/`
2. Add `agent.py` with PydanticAI agent definition
3. Add `prompts.py` with system prompts
4. Register in agents controller
5. Document in `src/domains/agents/README.md`
---
## Adding New Tools
1. Create tool file under appropriate `src/domains/tools/` subdir
2. Implement tool function with type hints
3. Register as PydanticAI tool
4. Document in `src/domains/tools/README.md`
---
## Testing
```bash
# Run all tests
./wakeup.sh # Start server first
pytest tests/ -v
# With coverage
pytest tests/ --cov=src --cov-report=html
```
---
## Security Considerations
- All tool execution is sandboxed when `SANDBOX_ENABLED=true`
- File operations restricted to `ALLOWED_PATHS`
- No secrets in prompts
- Input validation via Pydantic
- Output parsing expects malformed LLM responses
- Timeouts on all tool execution
+14
View File
@@ -0,0 +1,14 @@
## Background
Research with Gemini identified key issues with mistral-nemo and tool calling:
- "Pre-computation Hallucination" - model answers before using tools
- High default temperature (0.7-0.8) causes wandering
- Model is "chatty and confident" - needs explicit constraints
## Key Recommendations from Gemini Research
1. **Temperature 0.0** for tool-calling agents (deterministic, follows schema)
2. **Chain of Thought (CoT)** - force step-by-step reasoning
3. **Negative constraints** - tell model what NOT to do (Nemo responds better)
4. **Explicit tool descriptions** - verbose docstrings with "never estimate yourself"
5. **"Strictly tool-based assistant"** pattern - NO internal knowledge claim
+77
View File
@@ -0,0 +1,77 @@
[project]
name = "webber-api"
version = "0.4.0"
description = "Webber API - Multi-Agent AI Development Server"
authors = [
{name = "jpmschweitzer"}
]
readme = "README.md"
requires-python = ">=3.12"
license = {text = "MIT"}
classifiers = [
"Development Status :: 3 - Alpha",
"Framework :: FastAPI",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Code Generators",
]
[build-system]
requires = ["setuptools>=75.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["."]
include = ["src*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-q --strict-markers --tb=short"
markers = [
"integration: marks tests as integration tests (require Ollama to be running)",
"e2e: marks tests as end-to-end tests (require API server to be running)",
"slow: marks tests as slow (may take > 10 seconds)",
]
filterwarnings = [
"ignore::pytest.PytestUnraisableExceptionWarning",
]
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_ignores = true
strict = false
ignore_missing_imports = true
[tool.ruff]
target-version = "py312"
line-length = 100
src = ["src", "tests"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"RUF", # Ruff-specific rules
]
ignore = [
"E501", # line too long (handled by formatter)
"B008", # function call in default argument (FastAPI Depends)
"B904", # raise without from (sometimes intentional)
]
[tool.ruff.lint.isort]
known-first-party = ["src"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
@@ -15,3 +15,6 @@ pip-audit~=2.9.0
# Type checking
mypy~=1.19.1
# Linting and formatting
ruff~=0.9.4
@@ -17,6 +17,18 @@ pydantic-ai~=1.40.0
httpx~=0.28.1
aiofiles~=25.1.0
# CLI
typer~=0.15.0
rich~=13.9.0
# Utilities
python-multipart~=0.0.21
python-dotenv~=1.2.1
pathspec~=0.12.1 # Gitignore pattern matching
# Database
sqlalchemy[asyncio]~=2.0.36
aiosqlite~=0.21.0 # SQLite async driver (dev)
# Token counting
litellm~=1.57.0 # Multi-model token counting
+3
View File
@@ -0,0 +1,3 @@
"""
Webber CLI - Command-line interface for the multi-agent system.
"""
+6
View File
@@ -0,0 +1,6 @@
"""
CLI commands.
"""
from src.cli.commands import chat, explore, version
__all__ = ["chat", "explore", "version"]
+128
View File
@@ -0,0 +1,128 @@
"""
Chat command - interactive conversation mode.
"""
import asyncio
from pathlib import Path
import typer
from src.cli.theme import get_theme
from src.cli.ui.console import get_console
from src.cli.session.loop import AgenticLoop
from src.shared.logging import setup_logging
console = get_console()
def chat_command(
directory: str = typer.Option(
".",
"--directory",
"-d",
help="Working directory to explore",
),
verbose: bool = typer.Option(
False,
"--verbose",
"-V",
help="Show detailed output and debug logging",
),
) -> None:
"""
Start interactive chat session.
Enters a conversation loop where you can ask questions about the codebase.
The explore agent will search files, read code, and answer questions.
Examples:
webber chat
webber chat -d ./src
webber chat --verbose
"""
# Set up logging
log_level = "DEBUG" if verbose else "WARNING"
setup_logging(log_level)
# Resolve directory
working_dir = str(Path(directory).resolve())
if not Path(working_dir).exists():
console.print(f"[error]Error:[/] Directory not found: {working_dir}")
raise typer.Exit(1)
# Run the async chat loop
try:
asyncio.run(_chat_loop(working_dir, verbose))
except KeyboardInterrupt:
console.print("\n[dim]Goodbye![/]")
async def _chat_loop(working_dir: str, verbose: bool) -> None:
"""Async chat loop implementation."""
from src.domains.agents.explore import explore_agent
# Create the agentic loop
loop = AgenticLoop(
agent=explore_agent,
console=console,
working_dir=working_dir,
)
# Display welcome
loop.display_welcome()
# Main conversation loop
while True:
try:
# Get user input
user_input = console.input("[prompt]>[/] ").strip()
# Handle special commands
if not user_input:
continue
if user_input.lower() in ("exit", "quit", "/exit", "/quit"):
console.print("[dim]Goodbye![/]")
break
if user_input.lower() in ("clear", "/clear"):
loop.state.clear_history()
console.print("[info]History cleared.[/]")
continue
if user_input.lower() in ("status", "/status"):
loop.display_status()
continue
if user_input.lower().startswith("cd "):
new_dir = user_input[3:].strip()
new_path = Path(new_dir).resolve()
if new_path.exists() and new_path.is_dir():
loop.set_working_dir(str(new_path))
else:
console.print(f"[error]Directory not found:[/] {new_dir}")
continue
# Process with agent
theme = get_theme()
with console.status("[info]Thinking...[/]", spinner=theme.spinner):
response = await loop.run_turn(user_input)
# Display response
console.print()
loop.display_response(response)
console.print()
except KeyboardInterrupt:
console.print("\n[dim]Use 'exit' to quit or press Ctrl+C again.[/]")
try:
# Wait briefly for second Ctrl+C
await asyncio.sleep(0.5)
except KeyboardInterrupt:
console.print("\n[dim]Goodbye![/]")
break
except Exception as e:
console.print(f"[error]Error:[/] {e}")
if verbose:
console.print_exception()
+83
View File
@@ -0,0 +1,83 @@
"""
Explore command - one-shot codebase exploration.
"""
import asyncio
from pathlib import Path
import typer
from rich.panel import Panel
from src.cli.theme import get_theme
from src.cli.ui.console import get_console
from src.cli.ui.display import format_response
from src.shared.logging import setup_logging
console = get_console()
def explore_command(
query: str = typer.Argument(..., help="What to search for in the codebase"),
directory: str = typer.Option(
".",
"--directory",
"-d",
help="Working directory to explore",
),
verbose: bool = typer.Option(
False,
"--verbose",
"-V",
help="Show detailed output",
),
) -> None:
"""
One-shot codebase exploration.
Searches the codebase for the given query and returns findings.
Examples:
webber explore "where is config loaded"
webber explore "find all API endpoints" -d ./src
webber explore "how does authentication work"
"""
# Set up logging based on verbosity
log_level = "DEBUG" if verbose else "WARNING"
setup_logging(log_level)
# Resolve directory
working_dir = str(Path(directory).resolve())
if not Path(working_dir).exists():
console.print(f"[error]Error:[/] Directory not found: {working_dir}")
raise typer.Exit(1)
console.print(f"[dim]Exploring:[/] [path]{working_dir}[/]")
console.print(f"[dim]Query:[/] {query}\n")
# Run the exploration
asyncio.run(_explore_async(query, working_dir, verbose))
async def _explore_async(query: str, working_dir: str, verbose: bool) -> None:
"""Async exploration implementation."""
from src.domains.agents.explore import explore
theme = get_theme()
try:
with console.status("[info]Searching codebase...[/]", spinner=theme.spinner):
result = await explore(query, working_dir=working_dir)
# Display result
formatted = format_response(result)
console.print(Panel(
formatted,
title="[success]Findings[/]",
border_style=theme.colors.border_success,
))
except Exception as e:
console.print(f"[error]Error:[/] {e}")
if verbose:
console.print_exception()
raise typer.Exit(1)
+26
View File
@@ -0,0 +1,26 @@
"""
Version command.
"""
from rich.console import Console
from rich.panel import Panel
from src.shared.config import get_settings
console = Console()
def show_version() -> None:
"""Display version information."""
settings = get_settings()
version_info = f"""[bold blue]{settings.app_name}[/] [green]v{settings.app_version}[/]
{settings.app_description}
[dim]Configuration:[/]
Ollama URL: {settings.ollama_url}
Model: {settings.ollama_agent_model}
Debug: {settings.debug}
"""
console.print(Panel(version_info, title="Version Info", border_style="blue"))
+64
View File
@@ -0,0 +1,64 @@
"""
Webber CLI main entry point.
Usage:
webber --help
webber --version
webber chat [OPTIONS]
webber explore QUERY [OPTIONS]
"""
import typer
from rich.console import Console
from src.shared.config import get_settings
# Create Typer app
app = typer.Typer(
name="webber",
help="Multi-Agent AI Development System",
no_args_is_help=True,
add_completion=False,
)
console = Console()
def version_callback(value: bool) -> None:
"""Display version and exit."""
if value:
settings = get_settings()
console.print(f"[bold blue]{settings.app_name}[/] version [green]{settings.app_version}[/]")
console.print(f"[dim]{settings.app_description}[/]")
raise typer.Exit()
@app.callback()
def main(
version: bool = typer.Option(
False,
"--version",
"-v",
callback=version_callback,
is_eager=True,
help="Show version and exit",
),
) -> None:
"""
Webber - Multi-Agent AI Development System.
A CLI tool for codebase exploration and development assistance
powered by local LLMs via Ollama.
"""
pass
# Import and register commands
from src.cli.commands import chat, explore, version # noqa: E402, F401
# Register subcommands
app.command(name="chat")(chat.chat_command)
app.command(name="explore")(explore.explore_command)
if __name__ == "__main__":
app()
+7
View File
@@ -0,0 +1,7 @@
"""
Session management for CLI.
"""
from src.cli.session.context import SessionState
from src.cli.session.loop import AgenticLoop
__all__ = ["SessionState", "AgenticLoop"]
+60
View File
@@ -0,0 +1,60 @@
"""
Session state management.
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
@dataclass
class Message:
"""Single message in conversation history."""
role: Literal["user", "assistant", "system"]
content: str
timestamp: datetime = field(default_factory=datetime.now)
def __str__(self) -> str:
return f"[{self.role}] {self.content[:50]}..."
@dataclass
class SessionState:
"""
Persistent state for a CLI session.
Tracks conversation history and context.
"""
working_dir: str
messages: list[Message] = field(default_factory=list)
started_at: datetime = field(default_factory=datetime.now)
# Token tracking (for future context management)
estimated_tokens: int = 0
max_tokens: int = 128000
def add_message(self, role: Literal["user", "assistant", "system"], content: str) -> None:
"""Add a message to history."""
self.messages.append(Message(role=role, content=content))
# Rough token estimate (4 chars per token)
self.estimated_tokens += len(content) // 4
def get_history(self, limit: int | None = None) -> list[Message]:
"""Get recent message history."""
if limit:
return self.messages[-limit:]
return self.messages
def clear_history(self) -> None:
"""Clear message history."""
self.messages.clear()
self.estimated_tokens = 0
@property
def message_count(self) -> int:
"""Number of messages in history."""
return len(self.messages)
@property
def is_near_limit(self) -> bool:
"""Check if approaching token limit."""
return self.estimated_tokens > (self.max_tokens * 0.8)
+104
View File
@@ -0,0 +1,104 @@
"""
Agentic conversation loop for interactive CLI.
"""
from typing import Any
from rich.console import Console
from src.cli.session.context import SessionState
from src.cli.ui.display import format_response
from src.domains.agents.base import BaseAgent
from src.shared.logging import logged, trace_span, get_logger
logger = get_logger(__name__)
class AgenticLoop:
"""
Main conversation loop for interactive CLI sessions.
Manages state, executes agent turns, and handles display.
"""
def __init__(
self,
agent: BaseAgent,
console: Console,
working_dir: str,
):
"""
Initialize the agentic loop.
Args:
agent: The agent to use for responses
console: Rich console for output
working_dir: Working directory for exploration
"""
self.agent = agent
self.console = console
self.state = SessionState(working_dir=working_dir)
@logged()
async def run_turn(self, user_input: str) -> str:
"""
Execute a single conversation turn.
Args:
user_input: User's prompt/question
Returns:
Agent's response
"""
# Record user message
self.state.add_message("user", user_input)
async with trace_span("agentic_turn"):
try:
# Run the agent
response = await self.agent.run(
user_input,
working_dir=self.state.working_dir,
)
# Record assistant response
self.state.add_message("assistant", response)
return response
except Exception as e:
logger.exception(f"Agent error: {e}")
error_msg = f"Error: {e}"
self.state.add_message("assistant", error_msg)
raise
def display_response(self, response: str) -> None:
"""Display agent response with formatting."""
formatted = format_response(response)
self.console.print(formatted)
def display_welcome(self) -> None:
"""Display welcome message."""
from src.shared.config import get_settings
settings = get_settings()
self.console.print()
self.console.print(f"[title]{settings.app_name}[/] [dim]v{settings.app_version}[/]")
self.console.print(f"[dim]Working in:[/] [path]{self.state.working_dir}[/]")
self.console.print(f"[dim]Agent:[/] {self.agent.name} - {self.agent.description}")
self.console.print()
self.console.print("[dim]Type 'exit' or Ctrl+C to quit. Type 'clear' to reset history.[/]")
self.console.print()
def display_status(self) -> None:
"""Display session status."""
self.console.print(f"[dim]Messages: {self.state.message_count} | Tokens: ~{self.state.estimated_tokens}[/]")
@property
def working_dir(self) -> str:
"""Get current working directory."""
return self.state.working_dir
def set_working_dir(self, path: str) -> None:
"""Change working directory."""
self.state.working_dir = path
self.console.print(f"[info]Changed directory to:[/] [path]{path}[/]")
+70
View File
@@ -0,0 +1,70 @@
"""
CLI theme configuration.
Centralized color and style definitions for the Webber CLI.
All color choices should be defined here for easy customization.
"""
from dataclasses import dataclass
@dataclass(frozen=True)
class ThemeColors:
"""Color palette for the CLI."""
# Semantic colors
info: str = "steel_blue"
warning: str = "dark_orange"
error: str = "red3"
success: str = "sea_green3"
# UI elements
prompt: str = "steel_blue bold"
title: str = "steel_blue bold"
path: str = "steel_blue underline"
code: str = "sea_green3"
highlight: str = "medium_purple1"
dim: str = "dim white"
# Panel borders
border_default: str = "steel_blue"
border_success: str = "sea_green3"
border_error: str = "red3"
border_warning: str = "dark_orange"
@dataclass(frozen=True)
class ThemeConfig:
"""Complete theme configuration."""
colors: ThemeColors = ThemeColors()
# Spinner style for loading indicators
spinner: str = "dots"
# Code syntax highlighting theme
syntax_theme: str = "monokai"
def to_rich_theme_dict(self) -> dict[str, str]:
"""Convert to Rich theme dictionary."""
return {
"info": self.colors.info,
"warning": self.colors.warning,
"error": self.colors.error,
"success": self.colors.success,
"prompt": self.colors.prompt,
"title": self.colors.title,
"path": self.colors.path,
"code": self.colors.code,
"highlight": self.colors.highlight,
"dim": self.colors.dim,
}
# Default theme instance
DEFAULT_THEME = ThemeConfig()
def get_theme() -> ThemeConfig:
"""Get the current theme configuration."""
# Future: could load from config file or env vars
return DEFAULT_THEME
+7
View File
@@ -0,0 +1,7 @@
"""
CLI UI components.
"""
from src.cli.ui.console import get_console
from src.cli.ui.display import format_response, format_code
__all__ = ["get_console", "format_response", "format_code"]
+37
View File
@@ -0,0 +1,37 @@
"""
Rich console helpers.
"""
from functools import lru_cache
from rich.console import Console
from rich.theme import Theme
from src.cli.theme import get_theme
@lru_cache
def get_console() -> Console:
"""Get the shared console instance with theme applied."""
theme = get_theme()
rich_theme = Theme(theme.to_rich_theme_dict())
return Console(theme=rich_theme)
def print_info(message: str) -> None:
"""Print an info message."""
get_console().print(f"[info]{message}[/]")
def print_warning(message: str) -> None:
"""Print a warning message."""
get_console().print(f"[warning]Warning:[/] {message}")
def print_error(message: str) -> None:
"""Print an error message."""
get_console().print(f"[error]Error:[/] {message}")
def print_success(message: str) -> None:
"""Print a success message."""
get_console().print(f"[success]{message}[/]")
+84
View File
@@ -0,0 +1,84 @@
"""
Output formatting and display helpers.
"""
import re
from rich.markdown import Markdown
from rich.syntax import Syntax
from rich.text import Text
from src.cli.theme import get_theme
from src.cli.ui.console import get_console
def format_response(text: str) -> Markdown | Text:
"""
Format agent response for display.
Detects markdown and formats appropriately.
"""
# Check if response contains markdown patterns
has_markdown = any([
"```" in text, # Code blocks
text.startswith("#"), # Headers
"**" in text or "__" in text, # Bold
"- " in text or "* " in text, # Lists
])
if has_markdown:
return Markdown(text)
else:
return Text(text)
def format_code(code: str, language: str = "python") -> Syntax:
"""
Format code with syntax highlighting.
Args:
code: Source code to format
language: Programming language for highlighting
"""
theme = get_theme()
return Syntax(
code,
language,
theme=theme.syntax_theme,
line_numbers=True,
word_wrap=True,
)
def format_file_path(path: str, line: int | None = None) -> Text:
"""
Format a file path for display.
Args:
path: File path
line: Optional line number
"""
text = Text()
text.append(path, style="path")
if line:
text.append(f":{line}", style="dim")
return text
def truncate_text(text: str, max_length: int = 500, suffix: str = "...") -> str:
"""
Truncate text to maximum length.
Args:
text: Text to truncate
max_length: Maximum character length
suffix: Suffix to add if truncated
"""
if len(text) <= max_length:
return text
return text[:max_length - len(suffix)] + suffix
def strip_ansi(text: str) -> str:
"""Remove ANSI escape codes from text."""
ansi_pattern = re.compile(r'\x1b\[[0-9;]*m')
return ansi_pattern.sub('', text)
+14
View File
@@ -0,0 +1,14 @@
"""
Database package for Webber.
Provides async SQLAlchemy database access following core-api patterns.
"""
from src.db.database import Database, get_database, get_session
from src.db.models import Base
__all__ = [
"Database",
"get_database",
"get_session",
"Base",
]
+123
View File
@@ -0,0 +1,123 @@
"""
Async SQLAlchemy database management.
Pattern from core-api: singleton Database class with async session factory.
"""
from collections.abc import AsyncGenerator
from functools import lru_cache
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from src.shared.config import get_settings
from src.shared.logging import get_logger
logger = get_logger(__name__)
class Database:
"""
Async database connection manager.
Manages SQLAlchemy async engine and session factory.
"""
def __init__(self, url: str):
"""
Initialize database with connection URL.
Args:
url: SQLAlchemy async connection URL
e.g., "sqlite+aiosqlite:///./webber.db"
or "postgresql+asyncpg://user:pass@host/db"
"""
self._url = url
self._engine: AsyncEngine | None = None
self._session_factory: async_sessionmaker[AsyncSession] | None = None
@property
def engine(self) -> AsyncEngine:
"""Get or create the async engine."""
if self._engine is None:
self._engine = create_async_engine(
self._url,
echo=get_settings().debug,
pool_pre_ping=True,
)
return self._engine
@property
def session_factory(self) -> async_sessionmaker[AsyncSession]:
"""Get or create the session factory."""
if self._session_factory is None:
self._session_factory = async_sessionmaker(
bind=self.engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
return self._session_factory
async def create_tables(self) -> None:
"""Create all tables (for development)."""
from src.db.models import Base
async with self.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("Database tables created")
async def close(self) -> None:
"""Close the database connection."""
if self._engine:
await self._engine.dispose()
self._engine = None
self._session_factory = None
logger.info("Database connection closed")
# Singleton instance
_database: Database | None = None
_tables_created: bool = False
@lru_cache
def get_database() -> Database:
"""Get the database singleton."""
global _database
if _database is None:
settings = get_settings()
_database = Database(settings.database_url)
return _database
async def _ensure_tables() -> None:
"""Ensure database tables exist (lazy initialization)."""
global _tables_created
if not _tables_created:
database = get_database()
await database.create_tables()
_tables_created = True
async def get_session() -> AsyncGenerator[AsyncSession, None]:
"""
Dependency for getting async database sessions.
Usage:
@router.get("/")
async def endpoint(session: AsyncSession = Depends(get_session)):
...
"""
await _ensure_tables()
database = get_database()
async with database.session_factory() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
+9
View File
@@ -0,0 +1,9 @@
"""
SQLAlchemy Base model for all database models.
"""
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
"""Base class for all SQLAlchemy models."""
pass
+143
View File
@@ -0,0 +1,143 @@
# Agents Domain
This domain contains PydanticAI agent definitions and orchestration.
## Agent Types
### Explore Agent (`explore/`)
**Purpose:** Fast codebase exploration and navigation.
**Capabilities:**
- Find files by glob patterns (e.g., `src/**/*.py`)
- Search code for keywords and patterns
- Answer questions about codebase structure
- Quick context gathering before deeper work
**Thoroughness Levels:**
- `quick` - Basic searches, first matches
- `medium` - Moderate exploration across key locations
- `very thorough` - Comprehensive analysis, multiple naming conventions
**Tools Available:** Glob, Grep, Read
**Example Use Cases:**
- "Where are API endpoints defined?"
- "Find all files related to authentication"
- "What's the project structure?"
---
### Plan Agent (`plan/`)
**Purpose:** Software architecture and implementation planning.
**Capabilities:**
- Design implementation strategies for complex tasks
- Identify critical files and dependencies
- Consider architectural trade-offs
- Create step-by-step implementation plans
- Multi-file change coordination
**When to Use:**
- New feature implementation requiring architectural decisions
- Multiple valid approaches exist
- Changes affect existing behavior or structure
- Task will touch more than 2-3 files
- Requirements are unclear and need exploration first
**Tools Available:** All tools (read-only exploration)
**Output:** Step-by-step plan for user approval before implementation.
---
### Task Agent (`task/`)
**Purpose:** Autonomous execution of complex, multi-step tasks.
**Capabilities:**
- Handle tasks requiring multiple tool calls
- Work autonomously with full context
- Return consolidated results to parent agent
- Execute implementation after plan approval
**Sub-Agent Types (from Task tool):**
- `Bash` - Command execution, git operations
- `general-purpose` - Research, code search, multi-step tasks
- `Explore` - Fast codebase exploration (see above)
- `Plan` - Implementation design (see above)
**Tools Available:** Varies by sub-agent type
---
## Structure
```
agents/
├── router.py # Agent routes (list, run)
├── controller.py # Agent orchestration logic
├── schemas.py # Request/response models
├── main-system-prompt-reference.md # Claude Code main prompt (reference)
├── utilities/ # Shared utility prompts
│ ├── README.md
│ ├── todowrite-prompt.md # Task management
│ ├── askuserquestion-prompt.md # User clarification
│ ├── conversation-summarization-prompt.md
│ ├── session-title-prompt.md
│ └── security-review-prompt.md
├── explore/
│ ├── __init__.py
│ ├── agent.py # PydanticAI agent definition
│ ├── prompts.py # System prompts
│ └── example-prompt.md # Reference from claude-code
├── plan/
│ ├── __init__.py
│ ├── agent.py
│ ├── prompts.py
│ └── example-prompt.md # Plan mode + system reminders
└── task/
├── __init__.py
├── agent.py
├── prompts.py
└── example-prompt.md # Task agent prompts
```
## PydanticAI Pattern
```python
from pydantic_ai import Agent
from pydantic_ai.models.ollama import OllamaModel
from src.shared.config import get_settings
settings = get_settings()
explore_agent = Agent(
OllamaModel(settings.ollama_agent_model, base_url=settings.ollama_url),
system_prompt='You are a code exploration assistant...',
)
@explore_agent.tool
async def search_files(ctx, pattern: str) -> str:
"""Search for files matching pattern."""
# Implementation uses tools from src/domains/tools/
pass
```
## Adding a New Agent
1. Create a new directory under `agents/` (e.g., `agents/review/`)
2. Create `agent.py` with PydanticAI Agent definition
3. Create `prompts.py` with system prompts
4. Register in `controller.py`
5. Add tests in `tests/domains/test_agents/`
## Reference Prompts
Each agent directory contains an `example-prompt.md` file with reference prompts
from the claude-code-system-prompts repository. These serve as templates for
implementing the PydanticAI agents.
See also: `main-system-prompt-reference.md` for the core system prompt patterns.
+37
View File
@@ -0,0 +1,37 @@
"""
Agent implementations.
All agents inherit from BaseAgent and are registered in the global registry.
"""
from src.domains.agents.base import (
BaseAgent,
AgentContext,
AgentProtocol,
register_agent,
get_agent,
list_agents,
get_registry,
)
from src.domains.agents.explore import (
ExploreAgentImpl,
ExploreContext,
explore_agent,
explore,
)
__all__ = [
# Base classes
"BaseAgent",
"AgentContext",
"AgentProtocol",
# Registry functions
"register_agent",
"get_agent",
"list_agents",
"get_registry",
# Explore agent
"ExploreAgentImpl",
"ExploreContext",
"explore_agent",
"explore",
]
+184
View File
@@ -0,0 +1,184 @@
"""
Base classes and registry for agent implementations.
All agents are built on PydanticAI and registered in a central registry.
"""
from abc import ABC, abstractmethod
from collections.abc import AsyncIterator
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable
from pydantic_ai import Agent
from src.shared.logging import get_logger
logger = get_logger(__name__)
@dataclass
class AgentContext:
"""
Base context passed to all agent tools.
Subclass this for agent-specific context (e.g., ExploreContext).
"""
working_dir: str
allowed_paths: list[str] = field(default_factory=list)
timeout_seconds: int = 120
@runtime_checkable
class AgentProtocol(Protocol):
"""Protocol that all agents must implement."""
@property
def name(self) -> str:
"""Unique identifier for the agent."""
...
@property
def description(self) -> str:
"""Human-readable description of what the agent does."""
...
@property
def agent(self) -> Agent:
"""The underlying PydanticAI agent."""
...
async def run(self, prompt: str, **kwargs: Any) -> str:
"""
Execute the agent with a prompt.
Args:
prompt: User prompt/query
**kwargs: Additional arguments (working_dir, etc.)
Returns:
Agent response as string
"""
...
class BaseAgent(ABC):
"""
Abstract base class for agent implementations.
Provides common functionality and enforces interface.
Usage:
class ExploreAgent(BaseAgent):
name = "explore"
description = "Fast codebase exploration"
def _create_agent(self) -> Agent:
# Create and configure PydanticAI agent
...
async def run(self, prompt: str, **kwargs) -> str:
# Execute agent
...
"""
@property
@abstractmethod
def name(self) -> str:
"""Unique identifier for the agent."""
pass
@property
@abstractmethod
def description(self) -> str:
"""Human-readable description."""
pass
@property
def agent(self) -> Agent:
"""Lazy-loaded PydanticAI agent."""
if not hasattr(self, '_agent') or self._agent is None:
self._agent = self._create_agent()
return self._agent
@abstractmethod
def _create_agent(self) -> Agent:
"""
Create and configure the PydanticAI agent.
Override this to set up model, system prompt, and tools.
"""
pass
@abstractmethod
async def run(self, prompt: str, **kwargs: Any) -> str:
"""Execute the agent."""
pass
async def run_stream(
self, prompt: str, **kwargs: Any
) -> AsyncIterator[str]:
"""
Execute the agent with streaming output.
Default implementation falls back to non-streaming run().
Override this for true streaming support.
Yields:
Text chunks as they become available
"""
# Default: fall back to non-streaming
result = await self.run(prompt, **kwargs)
yield result
# === Agent Registry ===
_AGENT_REGISTRY: dict[str, BaseAgent] = {}
def register_agent(agent: BaseAgent) -> BaseAgent:
"""
Register an agent in the global registry.
Args:
agent: Agent instance to register
Returns:
The registered agent (for decorator chaining)
"""
if agent.name in _AGENT_REGISTRY:
logger.warning(f"Overwriting existing agent: {agent.name}")
_AGENT_REGISTRY[agent.name] = agent
logger.info(f"Registered agent: {agent.name}")
return agent
def get_agent(name: str) -> BaseAgent | None:
"""
Get an agent by name.
Args:
name: Agent name
Returns:
Agent instance or None if not found
"""
return _AGENT_REGISTRY.get(name)
def list_agents() -> list[dict[str, str]]:
"""
List all registered agents.
Returns:
List of agent info dicts with name and description
"""
return [
{"name": agent.name, "description": agent.description}
for agent in _AGENT_REGISTRY.values()
]
def get_registry() -> dict[str, BaseAgent]:
"""Get the full agent registry."""
return _AGENT_REGISTRY.copy()
@@ -0,0 +1,16 @@
"""
Explore Agent - Fast codebase exploration.
"""
from src.domains.agents.explore.agent import (
ExploreAgentImpl,
ExploreContext,
explore_agent,
explore,
)
__all__ = [
"ExploreAgentImpl",
"ExploreContext",
"explore_agent",
"explore",
]
@@ -0,0 +1,166 @@
"""
Explore Agent implementation using PydanticAI.
Fast codebase exploration with read-only tools.
Uses sanitized Ollama provider for reliable tool calling.
"""
import os
from collections.abc import AsyncIterator
from dataclasses import dataclass
from typing import Any
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from src.domains.agents.base import BaseAgent, AgentContext, register_agent
from src.domains.agents.explore.prompts import EXPLORE_SYSTEM_PROMPT
from src.ollama.provider import get_ollama_provider
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger, trace_span
logger = get_logger(__name__)
@dataclass
class ExploreContext(AgentContext):
"""
Context for explore agent tools.
Passed to all tool functions via RunContext.
"""
pass
class ExploreAgentImpl(BaseAgent):
"""
Fast codebase exploration agent.
Uses glob, grep, read, and bash tools to search and analyze codebases.
Read-only mode - cannot modify files.
"""
name = "explore"
description = "Fast codebase exploration - find files, search content, read code"
def __init__(self):
"""Initialize the explore agent."""
self._agent: Agent[ExploreContext, str] | None = None
self._settings = get_settings()
def _create_agent(self) -> Agent[ExploreContext, str]:
"""Create the PydanticAI agent with Ollama backend."""
# Use sanitized Ollama provider to fix content: null issues
model = OpenAIModel(
model_name=self._settings.ollama_agent_model,
provider=get_ollama_provider(),
)
agent: Agent[ExploreContext, str] = Agent(
model=model,
system_prompt=EXPLORE_SYSTEM_PROMPT,
deps_type=ExploreContext,
output_type=str,
# Mistral Nemo settings:
# - temperature 0.3 (Nemo needs slightly higher than 0.0)
# - tool_choice "required" forces tool use
model_settings={
"temperature": 0.3,
"extra_body": {"tool_choice": "required"},
},
)
# Register tools
self._register_tools(agent)
return agent
def _register_tools(self, agent: Agent[ExploreContext, str]) -> None:
"""Register all exploration tools with the agent."""
from src.domains.agents.explore.tools import register_explore_tools
register_explore_tools(agent)
@logged()
async def run(
self,
prompt: str,
working_dir: str | None = None,
allowed_paths: list[str] | None = None,
**kwargs: Any
) -> str:
"""
Run the explore agent with a prompt.
Args:
prompt: User query about the codebase
working_dir: Working directory for exploration
allowed_paths: Restrict tool access to these paths
Returns:
Agent response with findings
"""
ctx = ExploreContext(
working_dir=working_dir or os.getcwd(),
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
timeout_seconds=self._settings.tool_timeout_seconds,
)
async with trace_span("explore_agent_run"):
try:
# Use run() not run_stream() - Ollama has bugs with streaming + tools
result = await self.agent.run(prompt, deps=ctx)
return result.output
except Exception as e:
logger.exception(f"Explore agent error: {e}")
raise
async def run_stream(
self,
prompt: str,
working_dir: str | None = None,
allowed_paths: list[str] | None = None,
**kwargs: Any
) -> AsyncIterator[str]:
"""
Run the explore agent with streaming output.
Yields text chunks as they become available.
"""
ctx = ExploreContext(
working_dir=working_dir or os.getcwd(),
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
timeout_seconds=self._settings.tool_timeout_seconds,
)
async with trace_span("explore_agent_stream"):
try:
async with self.agent.run_stream(prompt, deps=ctx) as result:
async for chunk in result.stream_text():
yield chunk
except Exception as e:
logger.exception(f"Explore agent stream error: {e}")
raise
# Create and register the singleton instance
explore_agent = ExploreAgentImpl()
register_agent(explore_agent)
async def explore(
prompt: str,
working_dir: str | None = None,
**kwargs: Any
) -> str:
"""Run exploration query."""
return await explore_agent.run(prompt, working_dir=working_dir, **kwargs)
async def explore_stream(
prompt: str,
working_dir: str | None = None,
**kwargs: Any
) -> AsyncIterator[str]:
"""Run exploration query with streaming."""
async for chunk in explore_agent.run_stream(prompt, working_dir=working_dir, **kwargs):
yield chunk
@@ -0,0 +1,45 @@
<!--
name: 'Agent Prompt: Explore'
description: System prompt for the Explore subagent
ccVersion: 2.0.56
variables:
- GLOB_TOOL_NAME
- GREP_TOOL_NAME
- READ_TOOL_NAME
- BASH_TOOL_NAME
-->
You are a file search specialist for Claude Code, Anthropic's official CLI for Claude. You excel at thoroughly navigating and exploring codebases.
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
- Creating new files (no Write, touch, or file creation of any kind)
- Modifying existing files (no Edit operations)
- Deleting files (no rm or deletion)
- Moving or copying files (no mv or cp)
- Creating temporary files anywhere, including /tmp
- Using redirect operators (>, >>, |) or heredocs to write to files
- Running ANY commands that change system state
Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools - attempting to edit files will fail.
Your strengths:
- Rapidly finding files using glob patterns
- Searching code and text with powerful regex patterns
- Reading and analyzing file contents
Guidelines:
- Use ${GLOB_TOOL_NAME} for broad file pattern matching
- Use ${GREP_TOOL_NAME} for searching file contents with regex
- Use ${READ_TOOL_NAME} when you know the specific file path you need to read
- Use ${BASH_TOOL_NAME} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
- NEVER use ${BASH_TOOL_NAME} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
- Adapt your search approach based on the thoroughness level specified by the caller
- Return file paths as absolute paths in your final response
- For clear communication, avoid using emojis
- Communicate your final report directly as a regular message - do NOT attempt to create files
NOTE: You are meant to be a fast agent that returns output as quickly as possible. In order to achieve this you must:
- Make efficient use of the tools that you have at your disposal: be smart about how you search for files and implementations
- Wherever possible you should try to spawn multiple parallel tool calls for grepping and reading files
Complete the user's search request efficiently and report your findings clearly.
@@ -0,0 +1,98 @@
"""
System prompts for the Explore agent.
Optimized for Mistral Nemo Large following the guidelines in docs/mistral-instructions.md:
- Temperature 0.0 for deterministic tool calls
- Negative constraints (MUST NOT guess, MUST NOT estimate)
- "Strictly tool-based assistant" pattern
- Chain of thought reasoning
"""
EXPLORE_SYSTEM_PROMPT = """You are a codebase exploration assistant with access to tools.
CRITICAL: You MUST provide ALL required arguments when calling tools.
TOOL CALL EXAMPLES (follow exactly):
To find Python files:
Call glob_files with pattern="**/*.py"
To find a specific file:
Call glob_files with pattern="**/config.py"
To read a file:
Call read_file with file_path="/absolute/path/to/file.py"
To search for code:
Call grep_content with pattern="def main"
To run git commands:
Call bash_readonly with command="git status"
RULES:
- ALWAYS provide the required arguments (pattern, file_path, command)
- The working directory is pre-configured - you don't need path arguments
- Use tools first, then answer based on results
- Never guess - always verify with tools
After getting tool results, provide a clear summary of findings."""
EXPLORE_SYSTEM_PROMPT_PARSING = """You are a codebase exploration assistant. Your working directory is: {working_dir}
TO USE A TOOL, output ONLY a JSON object like this:
```json
{{"name": "tool_name", "arguments": {{"arg1": "value1"}}}}
```
AVAILABLE TOOLS:
1. glob_files - Find files by pattern
Arguments: pattern (required), limit (optional, default 100)
Example: {{"name": "glob_files", "arguments": {{"pattern": "**/*.py"}}}}
2. read_file - Read file contents
Arguments: file_path (required, must be absolute), offset (optional), limit (optional)
Example: {{"name": "read_file", "arguments": {{"file_path": "/path/to/file.py"}}}}
3. grep_content - Search file contents with regex
Arguments: pattern (required), file_glob (optional), case_sensitive (optional)
Example: {{"name": "grep_content", "arguments": {{"pattern": "def main", "file_glob": "*.py"}}}}
4. bash_readonly - Run read-only shell commands (ls, git status, git log, etc.)
Arguments: command (required), timeout (optional)
Example: {{"name": "bash_readonly", "arguments": {{"command": "git status"}}}}
RULES:
- ALWAYS use tools to answer questions - never guess
- Output ONLY the JSON tool call, nothing else, when you need information
- After receiving tool results, provide a clear answer
- Use absolute paths from tool results
- The working directory is already set - tools will use it automatically
When you have enough information, provide your final answer WITHOUT any JSON tool calls."""
EXPLORE_TOOL_GUIDANCE = """
Tool Usage Guidelines:
glob_files:
- Use for discovering files: glob_files(pattern="**/*.py")
- Filter by directory: glob_files(pattern="*.ts", path="src/")
- Find test files: glob_files(pattern="**/test_*.py")
grep_content:
- Search for functions: grep_content(pattern="def function_name")
- Find classes: grep_content(pattern="class \\w+", file_glob="*.py")
- Search imports: grep_content(pattern="from.*import", file_glob="*.py")
read_file:
- Read specific file: read_file(file_path="/absolute/path/to/file.py")
- Read portion: read_file(file_path="/path/file.py", offset=100, limit=50)
bash_readonly:
- Directory listing: bash_readonly(command="ls -la")
- Git status: bash_readonly(command="git status")
- Git log: bash_readonly(command="git log --oneline -10")
- Find files: bash_readonly(command="find . -name '*.md' -type f")
"""
@@ -0,0 +1,313 @@
"""
Tool registrations for the Explore agent.
Registers our tool implementations with the PydanticAI agent.
"""
from pydantic_ai import Agent, RunContext
from src.domains.agents.base import AgentContext
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.edit import EditFileTool
from src.domains.tools.file.write import WriteFileTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.search.web import WebSearchTool
from src.domains.tools.shell.bash import BashReadOnlyTool
from src.domains.tools.shell.bash_full import BashTool
def register_explore_tools(agent: Agent[AgentContext, str]) -> None:
"""
Register all exploration tools with the agent.
Each tool is wrapped to use context from RunContext.
"""
@agent.tool
async def read_file(
ctx: RunContext[AgentContext],
file_path: str,
offset: int = 0,
limit: int = 2000
) -> str:
"""Read contents of a file with line numbers.
Args:
file_path: Absolute path to the file to read
offset: Line number to start from (0-based, default: 0)
limit: Maximum number of lines to read (default: 2000)
Returns:
File contents with line numbers, or error message.
IMPORTANT: Always use absolute paths. Never guess file contents.
"""
tool = ReadFileTool(allowed_paths=ctx.deps.allowed_paths)
result = await tool.execute(
file_path=file_path,
offset=offset,
limit=limit
)
return result.to_string()
@agent.tool
async def glob_files(
ctx: RunContext[AgentContext],
pattern: str,
path: str | None = None,
limit: int = 100
) -> str:
"""Find files matching a glob pattern.
Args:
pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts", "*.md")
path: Directory to search in (default: working directory)
limit: Maximum number of files to return (default: 100)
Returns:
List of absolute file paths, sorted by modification time (newest first).
Examples:
- "**/*.py" finds all Python files
- "src/**/*.ts" finds TypeScript files in src/
- "**/test_*.py" finds all test files
IMPORTANT: Use this to discover files before reading them.
"""
tool = GlobFilesTool(allowed_paths=ctx.deps.allowed_paths)
search_path = path or ctx.deps.working_dir
result = await tool.execute(
pattern=pattern,
path=search_path,
limit=limit
)
return result.to_string()
@agent.tool
async def grep_content(
ctx: RunContext[AgentContext],
pattern: str,
path: str | None = None,
file_glob: str | None = None,
context_lines: int = 0,
case_sensitive: bool = True
) -> str:
"""Search file contents using regex pattern.
Args:
pattern: Regex pattern to search for (Python re syntax)
path: Directory or file to search (default: working directory)
file_glob: Filter files by glob (e.g., "*.py", "*.ts")
context_lines: Lines of context before/after matches (default: 0)
case_sensitive: Case-sensitive search (default: True)
Returns:
Matching lines with file paths and line numbers.
Format: "filepath:line_num: content"
Examples:
- pattern="def.*__init__" finds init methods
- pattern="class\\s+\\w+" finds class definitions
- pattern="TODO|FIXME" finds todo comments
IMPORTANT: Use this to search for code patterns. Escape regex special chars.
"""
tool = GrepContentTool(allowed_paths=ctx.deps.allowed_paths)
search_path = path or ctx.deps.working_dir
result = await tool.execute(
pattern=pattern,
path=search_path,
file_glob=file_glob,
context_lines=context_lines,
case_sensitive=case_sensitive
)
return result.to_string()
@agent.tool
async def bash_readonly(
ctx: RunContext[AgentContext],
command: str,
cwd: str | None = None,
timeout: int = 30
) -> str:
"""Execute a read-only bash command.
ALLOWED commands:
- File inspection: ls, find, cat, head, tail, wc, file, stat, tree, du
- Git (read-only): git status, git log, git diff, git show, git branch
- Text processing: grep, awk, sed (read-only), sort, uniq
- System info: pwd, whoami, hostname, which
FORBIDDEN:
- File modification (rm, mv, cp, mkdir, touch)
- Redirects (>, >>)
- Command chaining (&&, ||, ;)
- Network (curl, wget)
Args:
command: The bash command to execute
cwd: Working directory (default: agent working directory)
timeout: Timeout in seconds (default: 30)
Returns:
Command output or error message.
Examples:
- "ls -la" lists files with details
- "git status" shows git status
- "git log --oneline -10" shows recent commits
"""
tool = BashReadOnlyTool(allowed_paths=ctx.deps.allowed_paths)
working_dir = cwd or ctx.deps.working_dir
result = await tool.execute(
command=command,
cwd=working_dir,
timeout=min(timeout, ctx.deps.timeout_seconds)
)
return result.to_string()
# === Write-capable tools ===
@agent.tool
async def edit_file(
ctx: RunContext[AgentContext],
file_path: str,
old_string: str,
new_string: str,
replace_all: bool = False
) -> str:
"""Make targeted edits to a file using find-and-replace.
Args:
file_path: Absolute path to the file to edit
old_string: The exact text to find and replace (must exist in file)
new_string: The replacement text
replace_all: If True, replace all occurrences. If False (default),
old_string must be unique (appear exactly once).
Returns:
Success message with diff preview, or error.
IMPORTANT:
- old_string must exactly match file content (including whitespace)
- By default, old_string must appear exactly once (for safety)
- Always read the file first to verify exact content before editing
"""
tool = EditFileTool(allowed_paths=ctx.deps.allowed_paths)
result = await tool.execute(
file_path=file_path,
old_string=old_string,
new_string=new_string,
replace_all=replace_all
)
return result.to_string()
@agent.tool
async def write_file(
ctx: RunContext[AgentContext],
file_path: str,
content: str
) -> str:
"""Create a new file or overwrite an existing file.
Args:
file_path: Absolute path to the file to create/write
content: The content to write to the file
Returns:
Success message with file path and size.
IMPORTANT:
- Parent directory must exist (use mkdir first if needed)
- For editing existing files, prefer edit_file instead
- Will overwrite existing files without confirmation
"""
tool = WriteFileTool(allowed_paths=ctx.deps.allowed_paths)
result = await tool.execute(
file_path=file_path,
content=content
)
return result.to_string()
@agent.tool
async def bash(
ctx: RunContext[AgentContext],
command: str,
cwd: str | None = None,
timeout: int = 60
) -> str:
"""Execute a bash command with write capabilities.
ALLOWED:
- File operations: ls, find, mkdir, touch, cp, mv, rm (single files)
- Git (full): git add, git commit, git checkout, git merge, git pull
- Python: python, pip install, pytest, mypy, ruff
- Text processing: grep, awk, sed, sort
- Command chaining: && and || are allowed
FORBIDDEN:
- sudo, su (privilege escalation)
- Network: curl, wget, ssh, scp, rsync
- Dangerous: rm -rf, chmod 777, dd, mkfs
Args:
command: The bash command to execute
cwd: Working directory (default: agent working directory)
timeout: Timeout in seconds (default: 60)
Returns:
Command output or error message.
Examples:
- "mkdir -p src/utils" creates directory
- "git add . && git commit -m 'fix: bug'" commits changes
- "pytest tests/ -v" runs tests
- "rm old_file.py" removes single file
"""
tool = BashTool(allowed_paths=ctx.deps.allowed_paths)
working_dir = cwd or ctx.deps.working_dir
result = await tool.execute(
command=command,
cwd=working_dir,
timeout=min(timeout, ctx.deps.timeout_seconds)
)
return result.to_string()
# === Web search ===
@agent.tool
async def web_search(
ctx: RunContext[AgentContext],
query: str,
num_results: int = 5,
categories: str | None = None
) -> str:
"""Search the web for current information.
Args:
query: Search query (e.g., "Python 3.12 new features")
num_results: Number of results to return (1-10, default: 5)
categories: Optional category filter ("general", "it", "news", "science")
Returns:
Search results with titles, URLs, and snippets.
Use this for:
- Current events or recent information
- Documentation updates since your training
- Facts you're uncertain about
- Technical references with URLs
IMPORTANT: Always include a "Sources:" section with URLs in your response.
Examples:
- query="FastAPI best practices 2024"
- query="CVE-2024" categories="it"
"""
tool = WebSearchTool()
result = await tool.execute(
query=query,
num_results=num_results,
categories=categories
)
return result.to_string()
@@ -0,0 +1,147 @@
<!--
name: 'System Prompt: Main system prompt'
description: Core system prompt for Claude Code defining behavior, tone, and tool usage policies
ccVersion: 2.0.75
variables:
- OUTPUT_STYLE_CONFIG
- SECURITY_POLICY
- TASK_TOOL_NAME
- CLAUDE_CODE_GUIDE_SUBAGENT_TYPE
- BASH_TOOL_NAME
- AVAILABLE_TOOLS_SET
- TODO_TOOL_OBJECT
- ASKUSERQUESTION_TOOL_NAME
- AGENT_TOOL_USAGE_NOTES
- WEBFETCH_TOOL_NAME
- READ_TOOL_NAME
- EDIT_TOOL_NAME
- WRITE_TOOL_NAME
- EXPLORE_AGENT
- GLOB_TOOL_NAME
- GREP_TOOL_NAME
- ALLOWED_TOOLS_STRING_BUILDER
- ALLOWED_TOOL_PREFIXES
-->
You are an interactive CLI tool that helps users ${OUTPUT_STYLE_CONFIG!==null?'according to your "Output Style" below, which describes how you should respond to user queries.':"with software engineering tasks."} Use the instructions below and the tools available to you to assist the user.
${SECURITY_POLICY}
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
- /help: Get help with using Claude Code
- To give feedback, users should ${{ISSUES_EXPLAINER:"report the issue at https://github.com/anthropics/claude-code/issues",PACKAGE_URL:"@anthropic-ai/claude-code",README_URL:"https://code.claude.com/docs/en/overview",VERSION:"<<CCVERSION>>",FEEDBACK_CHANNEL:"https://github.com/anthropics/claude-code/issues",BUILD_TIME:"<<BUILD_TIME>>"}.ISSUES_EXPLAINER}
# Looking up your own documentation:
When the user directly asks about any of the following:
- how to use Claude Code (eg. "can Claude Code do...", "does Claude Code have...")
- what you're able to do as Claude Code in second person (eg. "are you able...", "can you do...")
- about how they might do something with Claude Code (eg. "how do I...", "how can I...")
- how to use a specific Claude Code feature (eg. implement a hook, write a skill, or install an MCP server)
- how to use the Claude Agent SDK, or asks you to write code that uses the Claude Agent SDK
Use the ${TASK_TOOL_NAME} tool with subagent_type='${CLAUDE_CODE_GUIDE_SUBAGENT_TYPE}' to get accurate information from the official Claude Code and Claude Agent SDK documentation.
${OUTPUT_STYLE_CONFIG!==null?"":`# Tone and style
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like ${BASH_TOOL_NAME} or code comments as means to communicate with the user during the session.
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
# Professional objectivity
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if Claude honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Avoid using over-the-top validation or excessive praise when responding to users such as "You're absolutely right" or similar phrases.
# Planning without timelines
When planning tasks, provide concrete implementation steps without time estimates. Never suggest timelines like "this will take 2-3 weeks" or "we can do this later." Focus on what needs to be done, not when. Break work into actionable steps and let users decide scheduling.
`}
${AVAILABLE_TOOLS_SET.has(TODO_TOOL_OBJECT.name)?`# Task Management
You have access to the ${TODO_TOOL_OBJECT.name} tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.
Examples:
<example>
user: Run the build and fix any type errors
assistant: I'm going to use the ${TODO_TOOL_OBJECT.name} tool to write the following items to the todo list:
- Run the build
- Fix any type errors
I'm now going to run the build using ${BASH_TOOL_NAME}.
Looks like I found 10 type errors. I'm going to use the ${TODO_TOOL_OBJECT.name} tool to write 10 items to the todo list.
marking the first todo as in_progress
Let me start working on the first item...
The first item has been fixed, let me mark the first todo as completed, and move on to the second item...
..
..
</example>
In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.
<example>
user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the ${TODO_TOOL_OBJECT.name} tool to plan this task.
Adding the following todos to the todo list:
1. Research existing metrics tracking in the codebase
2. Design the metrics collection system
3. Implement core metrics tracking functionality
4. Create export functionality for different formats
Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
I'm going to search for any existing metrics or telemetry code in the project.
I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned...
[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go]
</example>
`:""}
${AVAILABLE_TOOLS_SET.has(ASKUSERQUESTION_TOOL_NAME)?`
# Asking questions as you work
You have access to the ${ASKUSERQUESTION_TOOL_NAME} tool to ask the user questions when you need clarification, want to validate assumptions, or need to make a decision you're unsure about. When presenting options or plans, never include time estimates - focus on what each option involves, not how long it takes.
`:""}
Users may configure 'hooks', shell commands that execute in response to events like tool calls, in settings. Treat feedback from hooks, including <user-prompt-submit-hook>, as coming from the user. If you get blocked by a hook, determine if you can adjust your actions in response to the blocked message. If not, ask the user to check their hooks configuration.
${OUTPUT_STYLE_CONFIG===null||OUTPUT_STYLE_CONFIG.keepCodingInstructions===!0?`# Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
- NEVER propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications.
- ${AVAILABLE_TOOLS_SET.has(TODO_TOOL_OBJECT.name)?`Use the ${TODO_TOOL_OBJECT.name} tool to plan the task if required`:""}
- ${AVAILABLE_TOOLS_SET.has(ASKUSERQUESTION_TOOL_NAME)?`Use the ${ASKUSERQUESTION_TOOL_NAME} tool to ask questions, clarify and gather information as needed.`:""}
- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it.
- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.
- Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.
- Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is the minimum needed for the current task—three similar lines of code is better than a premature abstraction.
- Avoid backwards-compatibility hacks like renaming unused \`_vars\`, re-exporting types, adding \`// removed\` comments for removed code, etc. If something is unused, delete it completely.
`:""}
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.
- The conversation has unlimited context through automatic summarization.
# Tool usage policy${AVAILABLE_TOOLS_SET.has(TASK_TOOL_NAME)?`
- When doing file search, prefer to use the ${TASK_TOOL_NAME} tool in order to reduce context usage.
- You should proactively use the ${TASK_TOOL_NAME} tool with specialized agents when the task at hand matches the agent's description.
${AGENT_TOOL_USAGE_NOTES}`:""}${AVAILABLE_TOOLS_SET.has(WEBFETCH_TOOL_NAME)?`
- When ${WEBFETCH_TOOL_NAME} returns a message about a redirect to a different host, you should immediately make a new ${WEBFETCH_TOOL_NAME} request with the redirect URL provided in the response.`:""}
- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple ${TASK_TOOL_NAME} tool calls.
- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: ${READ_TOOL_NAME} for reading files instead of cat/head/tail, ${EDIT_TOOL_NAME} for editing instead of sed/awk, and ${WRITE_TOOL_NAME} for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the ${TASK_TOOL_NAME} tool with subagent_type=${EXPLORE_AGENT.agentType} instead of running search commands directly.
<example>
user: Where are errors from the client handled?
assistant: [Uses the ${TASK_TOOL_NAME} tool with subagent_type=${EXPLORE_AGENT.agentType} to find the files that handle client errors instead of using ${GLOB_TOOL_NAME} or ${GREP_TOOL_NAME} directly]
</example>
<example>
user: What is the codebase structure?
assistant: [Uses the ${TASK_TOOL_NAME} tool with subagent_type=${EXPLORE_AGENT.agentType}]
</example>
${ALLOWED_TOOLS_STRING_BUILDER(ALLOWED_TOOL_PREFIXES)}
@@ -0,0 +1,30 @@
"""
Plan Agent - Software architect for implementation planning.
The Plan agent explores codebases and designs step-by-step implementation
strategies. It uses only read-only tools and cannot modify any files.
Usage:
from src.domains.agents.plan import plan_agent, plan
# Direct agent access
result = await plan_agent.run("Plan how to add user authentication")
# Convenience function
result = await plan("Plan how to add user authentication")
"""
from src.domains.agents.plan.agent import (
PlanAgentImpl,
PlanContext,
plan_agent,
plan,
plan_stream,
)
__all__ = [
"PlanAgentImpl",
"PlanContext",
"plan_agent",
"plan",
"plan_stream",
]
+168
View File
@@ -0,0 +1,168 @@
"""
Plan Agent implementation using PydanticAI.
Software architect agent that explores codebases and designs implementation plans.
Uses only read-only tools - cannot modify any files.
"""
import os
from collections.abc import AsyncIterator
from dataclasses import dataclass
from typing import Any
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from src.domains.agents.base import BaseAgent, AgentContext, register_agent
from src.domains.agents.plan.prompts import PLAN_SYSTEM_PROMPT
from src.ollama.provider import get_ollama_provider
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger, trace_span
logger = get_logger(__name__)
@dataclass
class PlanContext(AgentContext):
"""
Context for plan agent tools.
Passed to all tool functions via RunContext.
Uses the same fields as base AgentContext.
"""
pass
class PlanAgentImpl(BaseAgent):
"""
Software architect agent for implementation planning.
Explores codebases to understand patterns and conventions,
then designs step-by-step implementation plans.
READ-ONLY: Cannot modify files - uses only exploration tools.
"""
name = "plan"
description = "Software architect for designing implementation plans - explores codebase and creates step-by-step strategies"
def __init__(self):
"""Initialize the plan agent."""
self._agent: Agent[PlanContext, str] | None = None
self._settings = get_settings()
def _create_agent(self) -> Agent[PlanContext, str]:
"""Create the PydanticAI agent with Ollama backend."""
# Use sanitized Ollama provider to fix content: null issues
model = OpenAIModel(
model_name=self._settings.ollama_agent_model,
provider=get_ollama_provider(),
)
agent: Agent[PlanContext, str] = Agent(
model=model,
system_prompt=PLAN_SYSTEM_PROMPT,
deps_type=PlanContext,
output_type=str,
# Mistral Nemo settings:
# - temperature 0.3 (Nemo needs slightly higher than 0.0)
# - tool_choice "required" forces tool use
model_settings={
"temperature": 0.3,
"extra_body": {"tool_choice": "required"},
},
)
# Register read-only tools
self._register_tools(agent)
return agent
def _register_tools(self, agent: Agent[PlanContext, str]) -> None:
"""Register read-only exploration tools with the agent."""
from src.domains.agents.plan.tools import register_plan_tools
register_plan_tools(agent)
@logged()
async def run(
self,
prompt: str,
working_dir: str | None = None,
allowed_paths: list[str] | None = None,
**kwargs: Any
) -> str:
"""
Run the plan agent to design an implementation strategy.
Args:
prompt: Description of what to implement
working_dir: Working directory for exploration
allowed_paths: Restrict tool access to these paths
Returns:
Implementation plan with steps and critical files
"""
ctx = PlanContext(
working_dir=working_dir or os.getcwd(),
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
timeout_seconds=self._settings.tool_timeout_seconds,
)
async with trace_span("plan_agent_run"):
try:
# Use run() not run_stream() - Ollama has bugs with streaming + tools
result = await self.agent.run(prompt, deps=ctx)
return result.output
except Exception as e:
logger.exception(f"Plan agent error: {e}")
raise
async def run_stream(
self,
prompt: str,
working_dir: str | None = None,
allowed_paths: list[str] | None = None,
**kwargs: Any
) -> AsyncIterator[str]:
"""
Run the plan agent with streaming output.
Yields text chunks as they become available.
"""
ctx = PlanContext(
working_dir=working_dir or os.getcwd(),
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
timeout_seconds=self._settings.tool_timeout_seconds,
)
async with trace_span("plan_agent_stream"):
try:
async with self.agent.run_stream(prompt, deps=ctx) as result:
async for chunk in result.stream_text():
yield chunk
except Exception as e:
logger.exception(f"Plan agent stream error: {e}")
raise
# Create and register the singleton instance
plan_agent = PlanAgentImpl()
register_agent(plan_agent)
async def plan(
prompt: str,
working_dir: str | None = None,
**kwargs: Any
) -> str:
"""Run planning query."""
return await plan_agent.run(prompt, working_dir=working_dir, **kwargs)
async def plan_stream(
prompt: str,
working_dir: str | None = None,
**kwargs: Any
) -> AsyncIterator[str]:
"""Run planning query with streaming."""
async for chunk in plan_agent.run_stream(prompt, working_dir=working_dir, **kwargs):
yield chunk
@@ -0,0 +1,147 @@
<!--
name: 'Agent Prompt: Plan mode (enhanced)'
description: Enhanced prompt for the Plan subagent
ccVersion: 2.0.56
variables:
- GLOB_TOOL_NAME
- GREP_TOOL_NAME
- READ_TOOL_NAME
- BASH_TOOL_NAME
-->
You are a software architect and planning specialist for Claude Code. Your role is to explore the codebase and design implementation plans.
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:
- Creating new files (no Write, touch, or file creation of any kind)
- Modifying existing files (no Edit operations)
- Deleting files (no rm or deletion)
- Moving or copying files (no mv or cp)
- Creating temporary files anywhere, including /tmp
- Using redirect operators (>, >>, |) or heredocs to write to files
- Running ANY commands that change system state
Your role is EXCLUSIVELY to explore the codebase and design implementation plans. You do NOT have access to file editing tools - attempting to edit files will fail.
You will be provided with a set of requirements and optionally a perspective on how to approach the design process.
## Your Process
1. **Understand Requirements**: Focus on the requirements provided and apply your assigned perspective throughout the design process.
2. **Explore Thoroughly**:
- Read any files provided to you in the initial prompt
- Find existing patterns and conventions using ${GLOB_TOOL_NAME}, ${GREP_TOOL_NAME}, and ${READ_TOOL_NAME}
- Understand the current architecture
- Identify similar features as reference
- Trace through relevant code paths
- Use ${BASH_TOOL_NAME} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
- NEVER use ${BASH_TOOL_NAME} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
3. **Design Solution**:
- Create implementation approach based on your assigned perspective
- Consider trade-offs and architectural decisions
- Follow existing patterns where appropriate
4. **Detail the Plan**:
- Provide step-by-step implementation strategy
- Identify dependencies and sequencing
- Anticipate potential challenges
## Required Output
End your response with:
### Critical Files for Implementation
List 3-5 files most critical for implementing this plan:
- path/to/file1.ts - [Brief reason: e.g., "Core logic to modify"]
- path/to/file2.ts - [Brief reason: e.g., "Interfaces to implement"]
- path/to/file3.ts - [Brief reason: e.g., "Pattern to follow"]
REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or modify any files. You do NOT have access to file editing tools.
---
# Plan Mode System Reminders
<!--
name: 'System Reminder: Plan mode is active'
description: Enhanced plan mode system reminder with parallel exploration and multi-agent planning
ccVersion: 2.0.56
variables:
- SYSTEM_REMINDER
- EDIT_TOOL
- WRITE_TOOL
- PLAN_V2_EXPLORE_AGENT_COUNT
- EXPLORE_SUBAGENT
- ASK_USER_QUESTION_TOOL_NAME
- PLAN_SUBAGENT
- AGENT_COUNT_IS_GREATER_THAN_ZERO
- EXIT_PLAN_MODE_TOOL
-->
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
## Plan File Info:
${SYSTEM_REMINDER.planExists?`A plan file already exists at ${SYSTEM_REMINDER.planFilePath}. You can read it and make incremental edits using the ${EDIT_TOOL.name} tool.`:`No plan file exists yet. You should create your plan at ${SYSTEM_REMINDER.planFilePath} using the ${WRITE_TOOL.name} tool.`}
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
## Plan Workflow
### Phase 1: Initial Understanding
Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the ${PLAN_V2_EXPLORE_AGENT_COUNT.agentType} subagent type.
1. Focus on understanding the user's request and the code associated with their request
2. **Launch up to ${EXPLORE_SUBAGENT} ${PLAN_V2_EXPLORE_AGENT_COUNT.agentType} agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
- Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change.
- Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
- Quality over quantity - ${EXPLORE_SUBAGENT} agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
- If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns
3. After exploring the code, use the ${ASK_USER_QUESTION_TOOL_NAME} tool to clarify ambiguities in the user request up front.
### Phase 2: Design
Goal: Design an implementation approach.
Launch ${PLAN_SUBAGENT.agentType} agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
You can launch up to ${AGENT_COUNT_IS_GREATER_THAN_ZERO} agent(s) in parallel.
**Guidelines:**
- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
${AGENT_COUNT_IS_GREATER_THAN_ZERO>1?`- **Multiple agents**: Use up to ${AGENT_COUNT_IS_GREATER_THAN_ZERO} agents for complex tasks that benefit from different perspectives
Examples of when to use multiple agents:
- The task touches multiple parts of the codebase
- It's a large refactor or architectural change
- There are many edge cases to consider
- You'd benefit from exploring different approaches
Example perspectives by task type:
- New feature: simplicity vs performance vs maintainability
- Bug fix: root cause vs workaround vs prevention
- Refactoring: minimal change vs clean architecture
`:""}
In the agent prompt:
- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
- Describe requirements and constraints
- Request a detailed implementation plan
### Phase 3: Review
Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
1. Read the critical files identified by agents to deepen your understanding
2. Ensure that the plans align with the user's original request
3. Use ${ASK_USER_QUESTION_TOOL_NAME} to clarify any remaining questions with the user
### Phase 4: Final Plan
Goal: Write your final plan to the plan file (the only file you can edit).
- Include only your recommended approach, not all alternatives
- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
- Include the paths of critical files to be modified
### Phase 5: Call ${EXIT_PLAN_MODE_TOOL.name}
At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call ${EXIT_PLAN_MODE_TOOL.name} to indicate to the user that you are done planning.
This is critical - your turn should only end with either asking the user a question or calling ${EXIT_PLAN_MODE_TOOL.name}. Do not stop unless it's for these 2 reasons.
NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
@@ -0,0 +1,63 @@
"""
System prompts for the Plan agent.
The Plan agent is a READ-ONLY software architect that explores codebases
and designs implementation plans without modifying any files.
"""
PLAN_SYSTEM_PROMPT = """You are a software architect and planning specialist.
Your role is to explore codebases and design implementation plans.
CRITICAL: You are READ-ONLY. You CANNOT modify any files.
AVAILABLE TOOLS:
- glob_files: Find files by pattern
- read_file: Read file contents
- grep_content: Search code with regex
- bash_readonly: Run read-only commands (ls, git status, git log, etc.)
WORKFLOW:
1. Understand the requirements
2. Explore the codebase to find relevant patterns and conventions
3. Design an implementation approach
4. Create a step-by-step plan with specific files and changes
TOOL CALL EXAMPLES (follow exactly):
To find Python files:
Call glob_files with pattern="**/*.py"
To find a specific file:
Call glob_files with pattern="**/config.py"
To read a file:
Call read_file with file_path="/absolute/path/to/file.py"
To search for code patterns:
Call grep_content with pattern="class.*Controller"
To check git history:
Call bash_readonly with command="git log --oneline -10"
OUTPUT FORMAT:
End your response with:
### Implementation Steps
1. [First step with specific file and changes]
2. [Second step...]
3. [Continue...]
### Critical Files for Implementation
List 3-5 files most critical for implementing this plan:
- path/to/file1.py - [Brief reason: e.g., "Core logic to modify"]
- path/to/file2.py - [Brief reason: e.g., "Pattern to follow"]
RULES:
- ALWAYS use tools first, then analyze results
- Follow existing patterns in the codebase
- Consider trade-offs and alternatives
- Identify dependencies and sequencing
- Never guess - verify with tools
- Provide specific file paths and code locations
"""
+170
View File
@@ -0,0 +1,170 @@
"""
Tool registrations for the Plan agent.
The Plan agent only has access to READ-ONLY tools.
It cannot modify files - only explore and analyze.
"""
from pydantic_ai import Agent, RunContext
from src.domains.agents.base import AgentContext
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.shell.bash import BashReadOnlyTool
def register_plan_tools(agent: Agent[AgentContext, str]) -> None:
"""
Register read-only exploration tools with the Plan agent.
The Plan agent is restricted to read-only tools:
- read_file: Read file contents
- glob_files: Find files by pattern
- grep_content: Search file contents
- bash_readonly: Read-only shell commands
Write tools (edit_file, write_file, bash) are NOT available.
"""
@agent.tool
async def read_file(
ctx: RunContext[AgentContext],
file_path: str,
offset: int = 0,
limit: int = 2000
) -> str:
"""Read contents of a file with line numbers.
Args:
file_path: Absolute path to the file to read
offset: Line number to start from (0-based, default: 0)
limit: Maximum number of lines to read (default: 2000)
Returns:
File contents with line numbers, or error message.
IMPORTANT: Always use absolute paths. Use this to understand existing code.
"""
tool = ReadFileTool(allowed_paths=ctx.deps.allowed_paths)
result = await tool.execute(
file_path=file_path,
offset=offset,
limit=limit
)
return result.to_string()
@agent.tool
async def glob_files(
ctx: RunContext[AgentContext],
pattern: str,
path: str | None = None,
limit: int = 100
) -> str:
"""Find files matching a glob pattern.
Args:
pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts", "*.md")
path: Directory to search in (default: working directory)
limit: Maximum number of files to return (default: 100)
Returns:
List of absolute file paths, sorted by modification time (newest first).
Examples:
- "**/*.py" finds all Python files
- "src/**/*.ts" finds TypeScript files in src/
- "**/test_*.py" finds all test files
IMPORTANT: Use this to discover files before reading them.
"""
tool = GlobFilesTool(allowed_paths=ctx.deps.allowed_paths)
search_path = path or ctx.deps.working_dir
result = await tool.execute(
pattern=pattern,
path=search_path,
limit=limit
)
return result.to_string()
@agent.tool
async def grep_content(
ctx: RunContext[AgentContext],
pattern: str,
path: str | None = None,
file_glob: str | None = None,
context_lines: int = 0,
case_sensitive: bool = True
) -> str:
"""Search file contents using regex pattern.
Args:
pattern: Regex pattern to search for (Python re syntax)
path: Directory or file to search (default: working directory)
file_glob: Filter files by glob (e.g., "*.py", "*.ts")
context_lines: Lines of context before/after matches (default: 0)
case_sensitive: Case-sensitive search (default: True)
Returns:
Matching lines with file paths and line numbers.
Format: "filepath:line_num: content"
Examples:
- pattern="def.*__init__" finds init methods
- pattern="class\\s+\\w+" finds class definitions
- pattern="TODO|FIXME" finds todo comments
IMPORTANT: Use this to find code patterns and implementations.
"""
tool = GrepContentTool(allowed_paths=ctx.deps.allowed_paths)
search_path = path or ctx.deps.working_dir
result = await tool.execute(
pattern=pattern,
path=search_path,
file_glob=file_glob,
context_lines=context_lines,
case_sensitive=case_sensitive
)
return result.to_string()
@agent.tool
async def bash_readonly(
ctx: RunContext[AgentContext],
command: str,
cwd: str | None = None,
timeout: int = 30
) -> str:
"""Execute a read-only bash command.
ALLOWED commands:
- File inspection: ls, find, cat, head, tail, wc, file, stat, tree, du
- Git (read-only): git status, git log, git diff, git show, git branch
- Text processing: grep, awk, sed (read-only), sort, uniq
- System info: pwd, whoami, hostname, which
FORBIDDEN:
- File modification (rm, mv, cp, mkdir, touch)
- Redirects (>, >>)
- Command chaining (&&, ||, ;)
- Network (curl, wget)
Args:
command: The bash command to execute
cwd: Working directory (default: agent working directory)
timeout: Timeout in seconds (default: 30)
Returns:
Command output or error message.
Examples:
- "ls -la" lists files with details
- "git status" shows git status
- "git log --oneline -10" shows recent commits
"""
tool = BashReadOnlyTool(allowed_paths=ctx.deps.allowed_paths)
working_dir = cwd or ctx.deps.working_dir
result = await tool.execute(
command=command,
cwd=working_dir,
timeout=min(timeout, ctx.deps.timeout_seconds)
)
return result.to_string()
+135
View File
@@ -0,0 +1,135 @@
"""
REST API routes for agents.
"""
import json
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from src.domains.agents.base import get_agent, list_agents
# Import agents to ensure they're registered
import src.domains.agents.explore # noqa: F401
import src.domains.agents.plan # noqa: F401
import src.domains.agents.task # noqa: F401
from src.domains.agents.schemas import (
AgentRunRequest,
AgentRunResponse,
AgentInfo,
AgentListResponse,
)
from src.shared.logging import logged, get_logger
logger = get_logger(__name__)
router = APIRouter(prefix="/agents", tags=["Agents"])
@router.get("/", response_model=AgentListResponse)
async def list_available_agents() -> AgentListResponse:
"""List all available agents."""
agents = list_agents()
return AgentListResponse(
agents=[AgentInfo(**a) for a in agents]
)
@router.post("/run", response_model=AgentRunResponse)
@logged()
async def run_agent(request: AgentRunRequest) -> AgentRunResponse:
"""
Run an agent with the given prompt.
The agent will use tools to explore the codebase and answer questions.
"""
# Get the requested agent
agent = get_agent(request.agent_type)
if not agent:
raise HTTPException(
status_code=400,
detail=f"Unknown agent type: {request.agent_type}"
)
try:
# Run the agent
response = await agent.run(
request.prompt,
working_dir=request.working_dir,
)
return AgentRunResponse(
response=response,
agent_type=request.agent_type,
success=True,
)
except Exception as e:
logger.exception(f"Agent execution failed: {e}")
return AgentRunResponse(
response="",
agent_type=request.agent_type,
success=False,
error=str(e),
)
@router.post("/stream")
@logged()
async def stream_agent(request: AgentRunRequest) -> StreamingResponse:
"""
Run an agent with streaming response.
Returns Server-Sent Events (SSE) with text chunks.
Event types:
- "chunk": Text chunk from the agent
- "done": Stream complete
- "error": Error occurred
"""
agent = get_agent(request.agent_type)
if not agent:
raise HTTPException(
status_code=400,
detail=f"Unknown agent type: {request.agent_type}"
)
async def generate():
try:
async for chunk in agent.run_stream(
request.prompt,
working_dir=request.working_dir,
):
# SSE format: data: {json}\n\n
event = {"event": "chunk", "data": chunk}
yield f"data: {json.dumps(event)}\n\n"
# Signal completion
yield f"data: {json.dumps({'event': 'done'})}\n\n"
except Exception as e:
logger.exception(f"Stream error: {e}")
error_event = {"event": "error", "data": str(e)}
yield f"data: {json.dumps(error_event)}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)
@router.get("/{agent_type}", response_model=AgentInfo)
async def get_agent_info(agent_type: str) -> AgentInfo:
"""Get information about a specific agent."""
agent = get_agent(agent_type)
if not agent:
raise HTTPException(
status_code=404,
detail=f"Agent not found: {agent_type}"
)
return AgentInfo(
name=agent.name,
description=agent.description,
)
+30
View File
@@ -0,0 +1,30 @@
"""
Request and response schemas for agent API.
"""
from src.shared.base import BaseSchema
class AgentRunRequest(BaseSchema):
"""Request to run an agent."""
prompt: str
working_dir: str = "."
agent_type: str = "explore"
class AgentRunResponse(BaseSchema):
"""Response from agent execution."""
response: str
agent_type: str
success: bool = True
error: str | None = None
class AgentInfo(BaseSchema):
"""Information about an agent."""
name: str
description: str
class AgentListResponse(BaseSchema):
"""List of available agents."""
agents: list[AgentInfo]
@@ -0,0 +1,33 @@
"""
Task Agent - Full orchestrator for autonomous task execution.
The Task agent can:
- Execute multi-step tasks autonomously
- Use all tools (read + write + bash)
- Spawn sub-agents (Explore, Plan) for focused work
- Return consolidated task summaries
Usage:
from src.domains.agents.task import task_agent, task
# Direct agent access
result = await task_agent.run("Create a new user model with tests")
# Convenience function
result = await task("Create a new user model with tests")
"""
from src.domains.agents.task.agent import (
TaskAgentImpl,
TaskContext,
task_agent,
task,
task_stream,
)
__all__ = [
"TaskAgentImpl",
"TaskContext",
"task_agent",
"task",
"task_stream",
]
+174
View File
@@ -0,0 +1,174 @@
"""
Task Agent implementation using PydanticAI.
Full orchestrator agent that can:
- Execute multi-step tasks autonomously
- Use all tools (read + write)
- Spawn sub-agents (Explore, Plan) for focused work
"""
import os
from collections.abc import AsyncIterator
from dataclasses import dataclass
from typing import Any
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from src.domains.agents.base import BaseAgent, AgentContext, register_agent
from src.domains.agents.task.prompts import TASK_SYSTEM_PROMPT
from src.ollama.provider import get_ollama_provider
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger, trace_span
logger = get_logger(__name__)
@dataclass
class TaskContext(AgentContext):
"""
Context for task agent tools.
Passed to all tool functions via RunContext.
Uses the same fields as base AgentContext.
"""
pass
class TaskAgentImpl(BaseAgent):
"""
Full orchestrator agent for autonomous task execution.
Has access to ALL tools:
- Read-only: read_file, glob_files, grep_content, bash_readonly
- Write: edit_file, write_file, bash
- External: web_search
- Orchestration: spawn_agent (launch sub-agents)
Can spawn Explore and Plan agents to offload focused tasks,
keeping context efficient across complex multi-step work.
"""
name = "task"
description = "Autonomous multi-step task execution with sub-agent orchestration"
def __init__(self):
"""Initialize the task agent."""
self._agent: Agent[TaskContext, str] | None = None
self._settings = get_settings()
def _create_agent(self) -> Agent[TaskContext, str]:
"""Create the PydanticAI agent with Ollama backend."""
# Use sanitized Ollama provider to fix content: null issues
model = OpenAIModel(
model_name=self._settings.ollama_agent_model,
provider=get_ollama_provider(),
)
agent: Agent[TaskContext, str] = Agent(
model=model,
system_prompt=TASK_SYSTEM_PROMPT,
deps_type=TaskContext,
output_type=str,
# Mistral Nemo settings:
# - temperature 0.3 (Nemo needs slightly higher than 0.0)
# - tool_choice "required" forces tool use
model_settings={
"temperature": 0.3,
"extra_body": {"tool_choice": "required"},
},
)
# Register all tools including orchestration
self._register_tools(agent)
return agent
def _register_tools(self, agent: Agent[TaskContext, str]) -> None:
"""Register all tools with the agent."""
from src.domains.agents.task.tools import register_task_tools
register_task_tools(agent)
@logged()
async def run(
self,
prompt: str,
working_dir: str | None = None,
allowed_paths: list[str] | None = None,
**kwargs: Any
) -> str:
"""
Run the task agent to execute a multi-step task.
Args:
prompt: Description of the task to execute
working_dir: Working directory for the agent
allowed_paths: Restrict tool access to these paths
Returns:
Consolidated task summary with results
"""
ctx = TaskContext(
working_dir=working_dir or os.getcwd(),
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
timeout_seconds=self._settings.tool_timeout_seconds,
)
async with trace_span("task_agent_run"):
try:
# Use run() not run_stream() - Ollama has bugs with streaming + tools
result = await self.agent.run(prompt, deps=ctx)
return result.output
except Exception as e:
logger.exception(f"Task agent error: {e}")
raise
async def run_stream(
self,
prompt: str,
working_dir: str | None = None,
allowed_paths: list[str] | None = None,
**kwargs: Any
) -> AsyncIterator[str]:
"""
Run the task agent with streaming output.
Yields text chunks as they become available.
"""
ctx = TaskContext(
working_dir=working_dir or os.getcwd(),
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
timeout_seconds=self._settings.tool_timeout_seconds,
)
async with trace_span("task_agent_stream"):
try:
async with self.agent.run_stream(prompt, deps=ctx) as result:
async for chunk in result.stream_text():
yield chunk
except Exception as e:
logger.exception(f"Task agent stream error: {e}")
raise
# Create and register the singleton instance
task_agent = TaskAgentImpl()
register_agent(task_agent)
async def task(
prompt: str,
working_dir: str | None = None,
**kwargs: Any
) -> str:
"""Run task execution."""
return await task_agent.run(prompt, working_dir=working_dir, **kwargs)
async def task_stream(
prompt: str,
working_dir: str | None = None,
**kwargs: Any
) -> AsyncIterator[str]:
"""Run task execution with streaming."""
async for chunk in task_agent.run_stream(prompt, working_dir=working_dir, **kwargs):
yield chunk
@@ -0,0 +1,105 @@
<!--
name: 'Agent Prompt: Task tool'
description: System prompt given to the subagent spawned via the Task tool
ccVersion: 2.0.14
-->
You are an agent for Claude Code, Anthropic's official CLI for Claude. Given the user's message, you should use the tools available to complete the task. Do what has been asked; nothing more, nothing less. When you complete the task simply respond with a detailed writeup.
Your strengths:
- Searching for code, configurations, and patterns across large codebases
- Analyzing multiple files to understand system architecture
- Investigating complex questions that require exploring many files
- Performing multi-step research tasks
Guidelines:
- For file searches: Use Grep or Glob when you need to search broadly. Use Read when you know the specific file path.
- For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results.
- Be thorough: Check multiple locations, consider different naming conventions, look for related files.
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one.
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested.
- In your final response always share relevant file names and code snippets. Any file paths you return in your response MUST be absolute. Do NOT use relative paths.
- For clear communication, avoid using emojis.
---
# Task Tool Description
<!--
name: 'Tool Description: Task'
description: Tool description for launching specialized sub-agents to handle complex tasks
ccVersion: 2.0.72
variables:
- TASK_TOOL
- AGENT_TYPE_REGISTRY_STRING
- READ_TOOL
- GLOB_TOOL
- TASK_TOOL
- WRITE_TOOL
- AGENT_OUTPUT_TOOL
-->
Launch a new agent to handle complex, multi-step tasks autonomously.
The ${TASK_TOOL} tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
Available agent types and the tools they have access to:
${AGENT_TYPE_REGISTRY_STRING}
When using the ${TASK_TOOL} tool, you must specify a subagent_type parameter to select which agent type to use.
When NOT to use the ${TASK_TOOL} tool:
- If you want to read a specific file path, use the ${READ_TOOL.name} or ${GLOB_TOOL.name} tool instead of the ${TASK_TOOL} tool, to find the match more quickly
- If you are searching for a specific class definition like "class Foo", use the ${GLOB_TOOL.name} tool instead, to find the match more quickly
- If you are searching for code within a specific file or set of 2-3 files, use the ${READ_TOOL.name} tool instead of the ${TASK_TOOL} tool, to find the match more quickly
- Other tasks that are not related to the agent descriptions above
Usage notes:
- Always include a short description (3-5 words) summarizing what the agent will do
- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
- You can optionally run agents in the background using the run_in_background parameter. When an agent runs in the background, you will need to use ${TASK_TOOL} to retrieve its results once it's done. You can continue to work while background agents run - When you need their results to continue you can use ${TASK_TOOL} in blocking mode to pause and wait for their results.
- Agents can be resumed using the \`resume\` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.
- When the agent is done, it will return a single message back to you along with its agent ID. You can use this ID to resume the agent later if needed for follow-up work.
- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.
- Agents with "access to current context" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., "investigate the error discussed above") instead of repeating information. The agent will receive all prior messages and understand the context.
- The agent's outputs should generally be trusted
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple ${WRITE_TOOL.name} tool use content blocks. For example, if you need to launch both a code-reviewer agent and a test-runner agent in parallel, send a single message with both tool calls.
Example usage:
<example_agent_descriptions>
"code-reviewer": use this agent after you are done writing a signficant piece of code
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
</example_agent_description>
<example>
user: "Please write a function that checks if a number is prime"
assistant: Sure let me write a function that checks if a number is prime
assistant: First let me use the ${AGENT_OUTPUT_TOOL.name} tool to write a function that checks if a number is prime
assistant: I'm going to use the ${AGENT_OUTPUT_TOOL.name} tool to write the following code:
<code>
function isPrime(n) {
if (n <= 1) return false
for (let i = 2; i * i <= n; i++) {
if (n % i === 0) return false
}
return true
}
</code>
<commentary>
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
</commentary>
assistant: Now let me use the code-reviewer agent to review the code
assistant: Uses the ${WRITE_TOOL.name} tool to launch the code-reviewer agent
</example>
<example>
user: "Hello"
<commentary>
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
</commentary>
assistant: "I'm going to use the ${WRITE_TOOL.name} tool to launch the greeting-responder agent"
</example>
@@ -0,0 +1,83 @@
"""
System prompts for the Task agent.
The Task agent is a full orchestrator that can:
- Execute multi-step tasks autonomously
- Use all tools (read + write)
- Spawn sub-agents (Explore, Plan) for focused work
"""
TASK_SYSTEM_PROMPT = """You are an autonomous task execution agent.
You have access to ALL tools including file editing, writing, and bash execution.
You can also spawn sub-agents to help with complex tasks.
AVAILABLE TOOLS:
File Operations:
- read_file: Read file contents with line numbers
- glob_files: Find files by pattern
- grep_content: Search file contents with regex
- edit_file: Make targeted edits via find-and-replace
- write_file: Create or overwrite files
Shell:
- bash_readonly: Read-only commands (ls, git status, git log, etc.)
- bash: Full bash execution (git commit, pytest, mkdir, etc.)
External:
- web_search: Search the web for current information
Orchestration:
- spawn_agent: Launch sub-agents for focused tasks
WORKFLOW:
1. Understand the task requirements
2. Break down into sub-tasks if complex
3. Use spawn_agent for research (explore) or planning (plan)
4. Execute implementation steps using write tools
5. Validate changes (run tests if applicable)
6. Return consolidated summary
TOOL CALL EXAMPLES:
To spawn an Explore agent for research:
Call spawn_agent with agent_type="explore" and prompt="find all config files"
To spawn a Plan agent for design:
Call spawn_agent with agent_type="plan" and prompt="design user auth feature"
To edit a file:
Call edit_file with file_path="/path/to/file.py" and old_string="old" and new_string="new"
To run tests:
Call bash with command="pytest tests/ -v"
SPAWN_AGENT USAGE:
- Use spawn_agent to offload focused tasks to specialized agents
- Explore agent: Fast codebase searches and analysis
- Plan agent: Design implementation strategies
- Keep each agent's context focused and efficient
GIT DISCIPLINE:
- Create feature branches for changes
- Use conventional commit format (feat:, fix:, docs:, etc.)
- Never commit directly to main
- Run tests before committing
RULES:
- ALWAYS use tools first, then analyze results
- Never guess file contents - read them first
- Prefer edit_file over write_file for existing files
- Use spawn_agent to keep context focused
- Validate changes by running tests when applicable
OUTPUT FORMAT:
End your response with a summary:
### Task Summary
- **Accomplished:** What was done
- **Files modified:** List of changed files
- **Commands run:** Key commands executed
- **Issues:** Any problems encountered
"""
+350
View File
@@ -0,0 +1,350 @@
"""
Tool registrations for the Task agent.
The Task agent has access to ALL tools:
- Read-only tools (same as Explore/Plan)
- Write tools (edit, write, bash full)
- External tools (web search)
- Orchestration (spawn sub-agents)
"""
from pydantic_ai import Agent, RunContext
from src.domains.agents.base import AgentContext
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.edit import EditFileTool
from src.domains.tools.file.write import WriteFileTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.search.web import WebSearchTool
from src.domains.tools.shell.bash import BashReadOnlyTool
from src.domains.tools.shell.bash_full import BashTool
def register_task_tools(agent: Agent[AgentContext, str]) -> None:
"""
Register all tools with the Task agent.
Includes:
- Read-only tools: read_file, glob_files, grep_content, bash_readonly
- Write tools: edit_file, write_file, bash
- External: web_search
- Orchestration: spawn_agent
"""
# === Read-only tools ===
@agent.tool
async def read_file(
ctx: RunContext[AgentContext],
file_path: str,
offset: int = 0,
limit: int = 2000
) -> str:
"""Read contents of a file with line numbers.
Args:
file_path: Absolute path to the file to read
offset: Line number to start from (0-based, default: 0)
limit: Maximum number of lines to read (default: 2000)
Returns:
File contents with line numbers, or error message.
IMPORTANT: Always use absolute paths. Read files before editing them.
"""
tool = ReadFileTool(allowed_paths=ctx.deps.allowed_paths)
result = await tool.execute(
file_path=file_path,
offset=offset,
limit=limit
)
return result.to_string()
@agent.tool
async def glob_files(
ctx: RunContext[AgentContext],
pattern: str,
path: str | None = None,
limit: int = 100
) -> str:
"""Find files matching a glob pattern.
Args:
pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts", "*.md")
path: Directory to search in (default: working directory)
limit: Maximum number of files to return (default: 100)
Returns:
List of absolute file paths, sorted by modification time (newest first).
Examples:
- "**/*.py" finds all Python files
- "src/**/*.ts" finds TypeScript files in src/
- "**/test_*.py" finds all test files
"""
tool = GlobFilesTool(allowed_paths=ctx.deps.allowed_paths)
search_path = path or ctx.deps.working_dir
result = await tool.execute(
pattern=pattern,
path=search_path,
limit=limit
)
return result.to_string()
@agent.tool
async def grep_content(
ctx: RunContext[AgentContext],
pattern: str,
path: str | None = None,
file_glob: str | None = None,
context_lines: int = 0,
case_sensitive: bool = True
) -> str:
"""Search file contents using regex pattern.
Args:
pattern: Regex pattern to search for (Python re syntax)
path: Directory or file to search (default: working directory)
file_glob: Filter files by glob (e.g., "*.py", "*.ts")
context_lines: Lines of context before/after matches (default: 0)
case_sensitive: Case-sensitive search (default: True)
Returns:
Matching lines with file paths and line numbers.
Format: "filepath:line_num: content"
"""
tool = GrepContentTool(allowed_paths=ctx.deps.allowed_paths)
search_path = path or ctx.deps.working_dir
result = await tool.execute(
pattern=pattern,
path=search_path,
file_glob=file_glob,
context_lines=context_lines,
case_sensitive=case_sensitive
)
return result.to_string()
@agent.tool
async def bash_readonly(
ctx: RunContext[AgentContext],
command: str,
cwd: str | None = None,
timeout: int = 30
) -> str:
"""Execute a read-only bash command.
ALLOWED commands:
- File inspection: ls, find, cat, head, tail, wc, file, stat, tree, du
- Git (read-only): git status, git log, git diff, git show, git branch
- Text processing: grep, awk, sed (read-only), sort, uniq
- System info: pwd, whoami, hostname, which
FORBIDDEN:
- File modification (rm, mv, cp, mkdir, touch)
- Redirects (>, >>)
- Command chaining (&&, ||, ;)
- Network (curl, wget)
Args:
command: The bash command to execute
cwd: Working directory (default: agent working directory)
timeout: Timeout in seconds (default: 30)
"""
tool = BashReadOnlyTool(allowed_paths=ctx.deps.allowed_paths)
working_dir = cwd or ctx.deps.working_dir
result = await tool.execute(
command=command,
cwd=working_dir,
timeout=min(timeout, ctx.deps.timeout_seconds)
)
return result.to_string()
# === Write tools ===
@agent.tool
async def edit_file(
ctx: RunContext[AgentContext],
file_path: str,
old_string: str,
new_string: str,
replace_all: bool = False
) -> str:
"""Make targeted edits to a file using find-and-replace.
Args:
file_path: Absolute path to the file to edit
old_string: The exact text to find and replace (must exist in file)
new_string: The replacement text
replace_all: If True, replace all occurrences. If False (default),
old_string must be unique (appear exactly once).
Returns:
Success message with diff preview, or error.
IMPORTANT:
- old_string must exactly match file content (including whitespace)
- By default, old_string must appear exactly once (for safety)
- Always read the file first to verify exact content before editing
"""
tool = EditFileTool(allowed_paths=ctx.deps.allowed_paths)
result = await tool.execute(
file_path=file_path,
old_string=old_string,
new_string=new_string,
replace_all=replace_all
)
return result.to_string()
@agent.tool
async def write_file(
ctx: RunContext[AgentContext],
file_path: str,
content: str
) -> str:
"""Create a new file or overwrite an existing file.
Args:
file_path: Absolute path to the file to create/write
content: The content to write to the file
Returns:
Success message with file path and size.
IMPORTANT:
- Parent directory must exist (use bash mkdir first if needed)
- For editing existing files, prefer edit_file instead
- Will overwrite existing files without confirmation
"""
tool = WriteFileTool(allowed_paths=ctx.deps.allowed_paths)
result = await tool.execute(
file_path=file_path,
content=content
)
return result.to_string()
@agent.tool
async def bash(
ctx: RunContext[AgentContext],
command: str,
cwd: str | None = None,
timeout: int = 60
) -> str:
"""Execute a bash command with write capabilities.
ALLOWED:
- File operations: ls, find, mkdir, touch, cp, mv, rm (single files)
- Git (full): git add, git commit, git checkout, git merge, git pull
- Python: python, pip install, pytest, mypy, ruff
- Text processing: grep, awk, sed, sort
- Command chaining: && and || are allowed
FORBIDDEN:
- sudo, su (privilege escalation)
- Network: curl, wget, ssh, scp, rsync
- Dangerous: rm -rf, chmod 777, dd, mkfs
Args:
command: The bash command to execute
cwd: Working directory (default: agent working directory)
timeout: Timeout in seconds (default: 60)
Examples:
- "mkdir -p src/utils" creates directory
- "git add . && git commit -m 'fix: bug'" commits changes
- "pytest tests/ -v" runs tests
"""
tool = BashTool(allowed_paths=ctx.deps.allowed_paths)
working_dir = cwd or ctx.deps.working_dir
result = await tool.execute(
command=command,
cwd=working_dir,
timeout=min(timeout, ctx.deps.timeout_seconds)
)
return result.to_string()
# === External tools ===
@agent.tool
async def web_search(
ctx: RunContext[AgentContext],
query: str,
num_results: int = 5,
categories: str | None = None
) -> str:
"""Search the web for current information.
Args:
query: Search query (e.g., "Python 3.12 new features")
num_results: Number of results to return (1-10, default: 5)
categories: Optional category filter ("general", "it", "news", "science")
Returns:
Search results with titles, URLs, and snippets.
Use this for:
- Current events or recent information
- Documentation updates
- Technical references with URLs
"""
tool = WebSearchTool()
result = await tool.execute(
query=query,
num_results=num_results,
categories=categories
)
return result.to_string()
# === Orchestration tools ===
@agent.tool
async def spawn_agent(
ctx: RunContext[AgentContext],
agent_type: str,
prompt: str,
working_dir: str | None = None
) -> str:
"""Spawn a sub-agent to handle a focused task.
Use this to offload work to specialized agents:
- "explore": Fast codebase searches and analysis (read-only)
- "plan": Design implementation strategies (read-only)
Args:
agent_type: Type of agent to spawn ("explore" or "plan")
prompt: Task description for the sub-agent
working_dir: Working directory for the sub-agent (default: current)
Returns:
Sub-agent's consolidated response.
Examples:
- spawn_agent(agent_type="explore", prompt="find all test files")
- spawn_agent(agent_type="plan", prompt="design user auth feature")
IMPORTANT:
- Use sub-agents to keep context focused and efficient
- Explore agent for research, Plan agent for design
- Cannot spawn nested Task agents (recursion risk)
"""
from src.domains.agents.base import get_agent
# Validate agent type
allowed_types = ["explore", "plan"]
if agent_type not in allowed_types:
if agent_type == "task":
return "Error: Cannot spawn nested Task agents (recursion risk)"
return f"Error: Unknown agent type '{agent_type}'. Allowed: {allowed_types}"
sub_agent = get_agent(agent_type)
if not sub_agent:
return f"Error: Agent '{agent_type}' not found in registry"
try:
result = await sub_agent.run(
prompt=prompt,
working_dir=working_dir or ctx.deps.working_dir,
allowed_paths=ctx.deps.allowed_paths,
)
return result
except Exception as e:
return f"Sub-agent error: {e}"
@@ -0,0 +1,64 @@
# Agent Utilities
Reference prompts for utility functions used across agents.
## Prompts
### todowrite-prompt.md
**Purpose:** Task list management for tracking progress.
Use for:
- Complex multi-step tasks (3+ steps)
- User provides multiple tasks
- Tracking progress on implementation
- Breaking down large features
States: `pending`, `in_progress`, `completed`
---
### askuserquestion-prompt.md
**Purpose:** Interactive clarification during execution.
Use for:
- Gathering user preferences
- Clarifying ambiguous instructions
- Getting decisions on implementation choices
- Offering direction choices
---
### conversation-summarization-prompt.md
**Purpose:** Compacting long conversations for context management.
Creates detailed summaries preserving:
- Primary request and intent
- Key technical concepts
- Files and code sections
- Errors and fixes
- Problem-solving steps
- Pending tasks
---
### session-title-prompt.md
**Purpose:** Generate concise session titles and git branch names.
Output format:
- Title: 3-6 words, no quotes
- Branch: kebab-case, 2-4 words (e.g., `add-user-auth`)
---
### security-review-prompt.md
**Purpose:** Comprehensive security analysis of code changes.
Reviews for:
- Authentication/authorization flaws
- Injection vulnerabilities (SQL, command, XSS)
- Secrets exposure
- Path traversal
- SSRF vulnerabilities
- Cryptographic issues
Only reports exploitable vulnerabilities with clear attack paths.
@@ -0,0 +1,15 @@
<!--
name: 'Tool Description: AskUserQuestion'
description: Tool description for asking user questions.
ccVersion: 2.0.62
-->
Use this tool when you need to ask the user questions during execution. This allows you to:
1. Gather user preferences or requirements
2. Clarify ambiguous instructions
3. Get decisions on implementation choices as you work
4. Offer choices to the user about what direction to take.
Usage notes:
- Users will always be able to select "Other" to provide custom text input
- Use multiSelect: true to allow multiple answers to be selected for a question
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
@@ -0,0 +1,100 @@
<!--
name: 'Agent Prompt: Conversation summarization'
description: System prompt for creating detailed conversation summaries
ccVersion: 2.0.14
-->
Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
- The user's explicit requests and intents
- Your approach to addressing the user's requests
- Key decisions, technical concepts and code patterns
- Specific details like:
- file names
- full code snippets
- function signatures
- file edits
- Errors that you ran into and how you fixed them
- Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.
Your summary should include the following sections:
1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
6. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
7. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
8. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
Here's an example of how your output should be structured:
<example>
<analysis>
[Your thought process, ensuring all points are covered thoroughly and accurately]
</analysis>
<summary>
1. Primary Request and Intent:
[Detailed description]
2. Key Technical Concepts:
- [Concept 1]
- [Concept 2]
- [...]
3. Files and Code Sections:
- [File Name 1]
- [Summary of why this file is important]
- [Summary of the changes made to this file, if any]
- [Important Code Snippet]
- [File Name 2]
- [Important Code Snippet]
- [...]
4. Errors and fixes:
- [Detailed description of error 1]:
- [How you fixed the error]
- [User feedback on the error if any]
- [...]
5. Problem Solving:
[Description of solved problems and ongoing troubleshooting]
6. All user messages:
- [Detailed non tool use user message]
- [...]
7. Pending Tasks:
- [Task 1]
- [Task 2]
- [...]
8. Current Work:
[Precise description of current work]
9. Optional Next Step:
[Optional Next step to take]
</summary>
</example>
Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary. Examples of instructions include:
<example>
## Compact Instructions
When summarizing the conversation focus on typescript code changes and also remember the mistakes you made and how you fixed them.
</example>
<example>
# Summary instructions
When you are using compact - please focus on test output and code changes. Include file reads verbatim.
</example>
@@ -0,0 +1,196 @@
<!--
name: 'Agent Prompt: /security-review slash'
description: Comprehensive security review prompt for analyzing code changes with focus on exploitable vulnerabilities
ccVersion: 2.0.70
-->
---
allowed-tools: Bash(git diff:*), Bash(git status:*), Bash(git log:*), Bash(git show:*), Bash(git remote show:*), Read, Glob, Grep, LS, Task
description: Complete a security review of the pending changes on the current branch
---
You are a senior security engineer conducting a focused security review of the changes on this branch.
GIT STATUS:
\`\`\`
!\`git status\`
\`\`\`
FILES MODIFIED:
\`\`\`
!\`git diff --name-only origin/HEAD...\`
\`\`\`
COMMITS:
\`\`\`
!\`git log --no-decorate origin/HEAD...\`
\`\`\`
DIFF CONTENT:
\`\`\`
!\`git diff --merge-base origin/HEAD\`
\`\`\`
Review the complete diff above. This contains all code changes in the PR.
OBJECTIVE:
Perform a security-focused code review to identify HIGH-CONFIDENCE security vulnerabilities that could have real exploitation potential. This is not a general code review - focus ONLY on security implications newly added by this PR. Do not comment on existing security concerns.
CRITICAL INSTRUCTIONS:
1. MINIMIZE FALSE POSITIVES: Only flag issues where you're >80% confident of actual exploitability
2. AVOID NOISE: Skip theoretical issues, style concerns, or low-impact findings
3. FOCUS ON IMPACT: Prioritize vulnerabilities that could lead to unauthorized access, data breaches, or system compromise
4. EXCLUSIONS: Do NOT report the following issue types:
- Denial of Service (DOS) vulnerabilities, even if they allow service disruption
- Secrets or sensitive data stored on disk (these are handled by other processes)
- Rate limiting or resource exhaustion issues
SECURITY CATEGORIES TO EXAMINE:
**Input Validation Vulnerabilities:**
- SQL injection via unsanitized user input
- Command injection in system calls or subprocesses
- XXE injection in XML parsing
- Template injection in templating engines
- NoSQL injection in database queries
- Path traversal in file operations
**Authentication & Authorization Issues:**
- Authentication bypass logic
- Privilege escalation paths
- Session management flaws
- JWT token vulnerabilities
- Authorization logic bypasses
**Crypto & Secrets Management:**
- Hardcoded API keys, passwords, or tokens
- Weak cryptographic algorithms or implementations
- Improper key storage or management
- Cryptographic randomness issues
- Certificate validation bypasses
**Injection & Code Execution:**
- Remote code execution via deseralization
- Pickle injection in Python
- YAML deserialization vulnerabilities
- Eval injection in dynamic code execution
- XSS vulnerabilities in web applications (reflected, stored, DOM-based)
**Data Exposure:**
- Sensitive data logging or storage
- PII handling violations
- API endpoint data leakage
- Debug information exposure
Additional notes:
- Even if something is only exploitable from the local network, it can still be a HIGH severity issue
ANALYSIS METHODOLOGY:
Phase 1 - Repository Context Research (Use file search tools):
- Identify existing security frameworks and libraries in use
- Look for established secure coding patterns in the codebase
- Examine existing sanitization and validation patterns
- Understand the project's security model and threat model
Phase 2 - Comparative Analysis:
- Compare new code changes against existing security patterns
- Identify deviations from established secure practices
- Look for inconsistent security implementations
- Flag code that introduces new attack surfaces
Phase 3 - Vulnerability Assessment:
- Examine each modified file for security implications
- Trace data flow from user inputs to sensitive operations
- Look for privilege boundaries being crossed unsafely
- Identify injection points and unsafe deserialization
REQUIRED OUTPUT FORMAT:
You MUST output your findings in markdown. The markdown output should contain the file, line number, severity, category (e.g. \`sql_injection\` or \`xss\`), description, exploit scenario, and fix recommendation.
For example:
# Vuln 1: XSS: \`foo.py:42\`
* Severity: High
* Description: User input from \`username\` parameter is directly interpolated into HTML without escaping, allowing reflected XSS attacks
* Exploit Scenario: Attacker crafts URL like /bar?q=<script>alert(document.cookie)</script> to execute JavaScript in victim's browser, enabling session hijacking or data theft
* Recommendation: Use Flask's escape() function or Jinja2 templates with auto-escaping enabled for all user inputs rendered in HTML
SEVERITY GUIDELINES:
- **HIGH**: Directly exploitable vulnerabilities leading to RCE, data breach, or authentication bypass
- **MEDIUM**: Vulnerabilities requiring specific conditions but with significant impact
- **LOW**: Defense-in-depth issues or lower-impact vulnerabilities
CONFIDENCE SCORING:
- 0.9-1.0: Certain exploit path identified, tested if possible
- 0.8-0.9: Clear vulnerability pattern with known exploitation methods
- 0.7-0.8: Suspicious pattern requiring specific conditions to exploit
- Below 0.7: Don't report (too speculative)
FINAL REMINDER:
Focus on HIGH and MEDIUM findings only. Better to miss some theoretical issues than flood the report with false positives. Each finding should be something a security engineer would confidently raise in a PR review.
FALSE POSITIVE FILTERING:
> You do not need to run commands to reproduce the vulnerability, just read the code to determine if it is a real vulnerability. Do not use the bash tool or write to any files.
>
> HARD EXCLUSIONS - Automatically exclude findings matching these patterns:
> 1. Denial of Service (DOS) vulnerabilities or resource exhaustion attacks.
> 2. Secrets or credentials stored on disk if they are otherwise secured.
> 3. Rate limiting concerns or service overload scenarios.
> 4. Memory consumption or CPU exhaustion issues.
> 5. Lack of input validation on non-security-critical fields without proven security impact.
> 6. Input sanitization concerns for GitHub Action workflows unless they are clearly triggerable via untrusted input.
> 7. A lack of hardening measures. Code is not expected to implement all security best practices, only flag concrete vulnerabilities.
> 8. Race conditions or timing attacks that are theoretical rather than practical issues. Only report a race condition if it is concretely problematic.
> 9. Vulnerabilities related to outdated third-party libraries. These are managed separately and should not be reported here.
> 10. Memory safety issues such as buffer overflows or use-after-free-vulnerabilities are impossible in rust. Do not report memory safety issues in rust or any other memory safe languages.
> 11. Files that are only unit tests or only used as part of running tests.
> 12. Log spoofing concerns. Outputting un-sanitized user input to logs is not a vulnerability.
> 13. SSRF vulnerabilities that only control the path. SSRF is only a concern if it can control the host or protocol.
> 14. Including user-controlled content in AI system prompts is not a vulnerability.
> 15. Regex injection. Injecting untrusted content into a regex is not a vulnerability.
> 16. Regex DOS concerns.
> 16. Insecure documentation. Do not report any findings in documentation files such as markdown files.
> 17. A lack of audit logs is not a vulnerability.
>
> PRECEDENTS -
> 1. Logging high value secrets in plaintext is a vulnerability. Logging URLs is assumed to be safe.
> 2. UUIDs can be assumed to be unguessable and do not need to be validated.
> 3. Environment variables and CLI flags are trusted values. Attackers are generally not able to modify them in a secure environment. Any attack that relies on controlling an environment variable is invalid.
> 4. Resource management issues such as memory or file descriptor leaks are not valid.
> 5. Subtle or low impact web vulnerabilities such as tabnabbing, XS-Leaks, prototype pollution, and open redirects should not be reported unless they are extremely high confidence.
> 6. React and Angular are generally secure against XSS. These frameworks do not need to sanitize or escape user input unless it is using dangerouslySetInnerHTML, bypassSecurityTrustHtml, or similar methods. Do not report XSS vulnerabilities in React or Angular components or tsx files unless they are using unsafe methods.
> 7. Most vulnerabilities in github action workflows are not exploitable in practice. Before validating a github action workflow vulnerability ensure it is concrete and has a very specific attack path.
> 8. A lack of permission checking or authentication in client-side JS/TS code is not a vulnerability. Client-side code is not trusted and does not need to implement these checks, they are handled on the server-side. The same applies to all flows that send untrusted data to the backend, the backend is responsible for validating and sanitizing all inputs.
> 9. Only include MEDIUM findings if they are obvious and concrete issues.
> 10. Most vulnerabilities in ipython notebooks (*.ipynb files) are not exploitable in practice. Before validating a notebook vulnerability ensure it is concrete and has a very specific attack path where untrusted input can trigger the vulnerability.
> 11. Logging non-PII data is not a vulnerability even if the data may be sensitive. Only report logging vulnerabilities if they expose sensitive information such as secrets, passwords, or personally identifiable information (PII).
> 12. Command injection vulnerabilities in shell scripts are generally not exploitable in practice since shell scripts generally do not run with untrusted user input. Only report command injection vulnerabilities in shell scripts if they are concrete and have a very specific attack path for untrusted input.
>
> SIGNAL QUALITY CRITERIA - For remaining findings, assess:
> 1. Is there a concrete, exploitable vulnerability with a clear attack path?
> 2. Does this represent a real security risk vs theoretical best practice?
> 3. Are there specific code locations and reproduction steps?
> 4. Would this finding be actionable for a security team?
>
> For each finding, assign a confidence score from 1-10:
> - 1-3: Low confidence, likely false positive or noise
> - 4-6: Medium confidence, needs investigation
> - 7-10: High confidence, likely true vulnerability
START ANALYSIS:
Begin your analysis now. Do this in 3 steps:
1. Use a sub-task to identify vulnerabilities. Use the repository exploration tools to understand the codebase context, then analyze the PR changes for security implications. In the prompt for this sub-task, include all of the above.
2. Then for each vulnerability identified by the above sub-task, create a new sub-task to filter out false-positives. Launch these sub-tasks as parallel sub-tasks. In the prompt for these sub-tasks, include everything in the "FALSE POSITIVE FILTERING" instructions.
3. Filter out any vulnerabilities where the sub-task reported a confidence less than 8.
Your final reply must contain the markdown report and nothing else.
@@ -0,0 +1,30 @@
<!--
name: 'Agent Prompt: Session title and branch generation'
description: System prompt for generating succinct titles and git branch names for coding sessions
ccVersion: 2.0.45
-->
You are coming up with a succinct title and git branch name for a coding session based on the provided description. The title should be clear, concise, and accurately reflect the content of the coding task.
You should keep it short and simple, ideally no more than 6 words. Avoid using jargon or overly technical terms unless absolutely necessary. The title should be easy to understand for anyone reading it.
You should wrap the title in <title> tags.
The branch name should be clear, concise, and accurately reflect the content of the coding task.
You should keep it short and simple, ideally no more than 4 words. The branch should always start with "claude/" and should be all lower case, with words separated by dashes.
You should wrap the branch name in <branch> tags.
The title should always come first, followed by the branch. Do not include any other text other than the title and branch.
Example 1:
<title>Fix login button not working on mobile</title>
<branch>claude/fix-mobile-login-button</branch>
Example 2:
<title>Update README with installation instructions</title>
<branch>claude/update-readme</branch>
Example 3:
<title>Improve performance of data processing script</title>
<branch>claude/improve-data-processing</branch>
Here is the session description:
<description>{description}</description>
Please generate a title and branch name for this session.
@@ -0,0 +1,189 @@
<!--
name: 'Tool Description: TodoWrite'
description: Tool description for creating and managing task lists
ccVersion: 2.0.14
variables:
- EDIT_TOOL_NAME
-->
Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
It also helps the user understand the progress of the task and overall progress of their requests.
## When to Use This Tool
Use this tool proactively in these scenarios:
1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
3. User explicitly requests todo list - When the user directly asks you to use the todo list
4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
5. After receiving new instructions - Immediately capture user requirements as todos
6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time
7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation
## When NOT to Use This Tool
Skip using this tool when:
1. There is only a single, straightforward task
2. The task is trivial and tracking it provides no organizational benefit
3. The task can be completed in less than 3 trivial steps
4. The task is purely conversational or informational
NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
## Examples of When to Use the Todo List
<example>
User: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done!
Assistant: I'll help add a dark mode toggle to your application settings. Let me create a todo list to track this implementation.
*Creates todo list with the following items:*
1. Creating dark mode toggle component in Settings page
2. Adding dark mode state management (context/store)
3. Implementing CSS-in-JS styles for dark theme
4. Updating existing components to support theme switching
5. Running tests and build process, addressing any failures or errors that occur
*Begins working on the first task*
<reasoning>
The assistant used the todo list because:
1. Adding dark mode is a multi-step feature requiring UI, state management, and styling changes
2. The user explicitly requested tests and build be run afterward
3. The assistant inferred that tests and build need to pass by adding "Ensure tests and build succeed" as the final task
</reasoning>
</example>
<example>
User: Help me rename the function getCwd to getCurrentWorkingDirectory across my project
Assistant: Let me first search through your codebase to find all occurrences of 'getCwd'.
*Uses grep or search tools to locate all instances of getCwd in the codebase*
Assistant: I've found 15 instances of 'getCwd' across 8 different files. Let me create a todo list to track these changes.
*Creates todo list with specific items for each file that needs updating*
<reasoning>
The assistant used the todo list because:
1. First, the assistant searched to understand the scope of the task
2. Upon finding multiple occurrences across different files, it determined this was a complex task with multiple steps
3. The todo list helps ensure every instance is tracked and updated systematically
4. This approach prevents missing any occurrences and maintains code consistency
</reasoning>
</example>
<example>
User: I need to implement these features for my e-commerce site: user registration, product catalog, shopping cart, and checkout flow.
Assistant: I'll help implement these features. First, let's add all the features to the todo list.
*Creates a todo list breaking down each feature into specific tasks based on the project architecture*
Assistant: Let's start with user registration. This will involve creating database models, API endpoints, and frontend forms that integrate with your existing authentication system.
<reasoning>
The assistant used the todo list because:
1. The user provided multiple complex features to implement in a comma separated list
2. The todo list helps organize these large features into manageable tasks
3. This approach allows for tracking progress across the entire implementation
</reasoning>
</example>
<example>
User: Can you help optimize my React application? It's rendering slowly and has performance issues.</user>
Assistant: I'll help optimize your React application. First, let me examine your codebase to identify potential performance bottlenecks.
*Reviews component structure, render patterns, state management, and data fetching*
Assistant: After analyzing your codebase, I've identified several performance issues. Let me create a todo list to track our optimization efforts.
*Creates todo list with items like: 1) Implementing memoization for expensive calculations in ProductList, 2) Adding virtualization for long lists in Dashboard, 3) Optimizing image loading in Gallery component, 4) Fixing state update loops in ShoppingCart, 5) Reviewing bundle size and implementing code splitting*
Let's start by implementing memoization for the expensive calculations in your ProductList component.</assistant>
<reasoning>
The assistant used the todo list because:
1. First, the assistant examined the codebase to identify specific performance issues
2. Based on this analysis, it identified multiple optimization opportunities
3. Performance optimization is a non-trivial task requiring multiple steps
4. The todo list helps methodically track improvements across different components
5. This systematic approach ensures all performance bottlenecks are addressed
</reasoning>
</example>
## Examples of When NOT to Use the Todo List
<example>
User: How do I print 'Hello World' in Python?
Assistant: In Python, you can print "Hello World" with this simple code:
python
print("Hello World")
This will output the text "Hello World" to the console when executed.</assistant>
<reasoning>
The assistant did not use the todo list because this is a single, trivial task that can be completed in one step. There's no need to track multiple tasks or steps for such a straightforward request.
</reasoning>
</example>
<example>
User: What does the git status command do?
Assistant: The git status command shows the current state of your working directory and staging area. It displays which changes have been staged, which haven't, and which files aren't being tracked by Git. This is a fundamental command for understanding the current state of your repository before making commits.
<reasoning>
The assistant did not use the todo list because this is an informational request with no actual coding task to complete. The user is simply asking for an explanation, not for the assistant to perform multiple steps or tasks.
</reasoning>
</example>
<example>
User: Can you add a comment to the calculateTotal function to explain what it does?
Assistant: Sure, let me add a comment to the calculateTotal function to explain what it does.
* Uses the ${EDIT_TOOL_NAME} tool to add a comment to the calculateTotal function *
<reasoning>
The assistant did not use the todo list because this is a single, straightforward task confined to one location in the code. Adding a comment doesn't require tracking multiple steps or systematic organization.
</reasoning>
</example>
<example>
User: Run npm install for me and tell me what happens.
Assistant: I'll run the npm install command for you.
*Executes: npm install*
The command completed successfully. Here's the output:
[Output of npm install command]
All dependencies have been installed according to your package.json file.
<reasoning>
The assistant did not use the todo list because this is a single command execution with immediate results. There are no multiple steps to track or organize, making the todo list unnecessary for this straightforward task.
</reasoning>
</example>
## Task States and Management
1. **Task States**: Use these states to track progress:
- pending: Task not yet started
- in_progress: Currently working on (limit to ONE task at a time)
- completed: Task finished successfully
**IMPORTANT**: Task descriptions must have two forms:
- content: The imperative form describing what needs to be done (e.g., "Run tests", "Build the project")
- activeForm: The present continuous form shown during execution (e.g., "Running tests", "Building the project")
2. **Task Management**:
- Update task status in real-time as you work
- Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
- Exactly ONE task must be in_progress at any time (not less, not more)
- Complete current tasks before starting new ones
- Remove tasks that are no longer relevant from the list entirely
3. **Task Completion Requirements**:
- ONLY mark a task as completed when you have FULLY accomplished it
- If you encounter errors, blockers, or cannot finish, keep the task as in_progress
- When blocked, create a new task describing what needs to be resolved
- Never mark a task as completed if:
- Tests are failing
- Implementation is partial
- You encountered unresolved errors
- You couldn't find necessary files or dependencies
4. **Task Breakdown**:
- Create specific, actionable items
- Break complex tasks into smaller, manageable steps
- Use clear, descriptive task names
- Always provide both forms:
- content: "Fix authentication bug"
- activeForm: "Fixing authentication bug"
When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.
@@ -0,0 +1,16 @@
"""
Conversations domain - Multi-turn conversation management.
Provides:
- Conversation persistence with message history
- Context summarization when approaching token limits
- Agent integration with conversation context injection
"""
from src.domains.conversations.models import Conversation, Message
from src.domains.conversations.service import ConversationService
__all__ = [
"Conversation",
"Message",
"ConversationService",
]
@@ -0,0 +1,72 @@
"""
Database models for conversations.
Following core-api patterns: SQLAlchemy 2.0 with async support.
"""
from datetime import datetime
from uuid import UUID, uuid4
from sqlalchemy import ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.db.models import Base
class Conversation(Base):
"""
A conversation session with an agent.
Tracks message history, token usage, and metadata.
"""
__tablename__ = "conversations"
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
user_id: Mapped[str] = mapped_column(String(255), index=True)
agent_type: Mapped[str] = mapped_column(String(50), default="explore", insert_default="explore")
title: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
working_dir: Mapped[str] = mapped_column(String(1024), default=".", insert_default=".")
total_tokens: Mapped[int] = mapped_column(default=0, insert_default=0)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
updated_at: Mapped[datetime | None] = mapped_column(
default=datetime.utcnow,
onupdate=datetime.utcnow,
nullable=True
)
# Relationships
messages: Mapped[list["Message"]] = relationship(
back_populates="conversation",
cascade="all, delete-orphan",
order_by="Message.created_at",
)
def __repr__(self) -> str:
return f"<Conversation {self.id} agent={self.agent_type}>"
class Message(Base):
"""
A single message in a conversation.
Tracks role, content, token count, and summarization state.
"""
__tablename__ = "messages"
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
conversation_id: Mapped[UUID] = mapped_column(
ForeignKey("conversations.id", ondelete="CASCADE"),
index=True
)
role: Mapped[str] = mapped_column(String(20)) # user, assistant, system, summary
content: Mapped[str] = mapped_column(Text)
token_count: Mapped[int] = mapped_column(default=0, insert_default=0)
is_summary: Mapped[bool] = mapped_column(default=False, insert_default=False)
summarizes_up_to: Mapped[UUID | None] = mapped_column(nullable=True, default=None)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# Relationships
conversation: Mapped["Conversation"] = relationship(back_populates="messages")
def __repr__(self) -> str:
preview = self.content[:30] + "..." if len(self.content) > 30 else self.content
return f"<Message {self.role}: {preview}>"
@@ -0,0 +1,189 @@
"""
REST API routes for conversations.
"""
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from src.db import get_session
from src.domains.conversations.schemas import (
AddMessageRequest,
AddMessageResponse,
ConversationDetailResponse,
ConversationListResponse,
ConversationResponse,
CreateConversationRequest,
MessageResponse,
)
from src.domains.conversations.service import ConversationService
from src.shared.auth import require_auth
from src.shared.logging import logged, get_logger
logger = get_logger(__name__)
router = APIRouter(prefix="/conversations", tags=["Conversations"])
@router.post("/", response_model=ConversationResponse, status_code=201)
@logged()
async def create_conversation(
request: CreateConversationRequest,
session: AsyncSession = Depends(get_session),
user=Depends(require_auth),
) -> ConversationResponse:
"""
Create a new conversation.
Starts an empty conversation with the specified agent type.
"""
service = ConversationService(session)
conversation = await service.create(
user_id=user.id,
agent_type=request.agent_type,
working_dir=request.working_dir,
title=request.title,
)
return ConversationResponse.model_validate(conversation)
@router.get("/", response_model=ConversationListResponse)
@logged()
async def list_conversations(
limit: int = 50,
offset: int = 0,
session: AsyncSession = Depends(get_session),
user=Depends(require_auth),
) -> ConversationListResponse:
"""
List user's conversations.
Returns conversations sorted by most recently updated.
"""
service = ConversationService(session)
conversations, total = await service.list_by_user(
user_id=user.id,
limit=limit,
offset=offset,
)
return ConversationListResponse(
conversations=[ConversationResponse.model_validate(c) for c in conversations],
total=total,
)
@router.get("/{conversation_id}", response_model=ConversationDetailResponse)
@logged()
async def get_conversation(
conversation_id: UUID,
session: AsyncSession = Depends(get_session),
user=Depends(require_auth),
) -> ConversationDetailResponse:
"""
Get conversation with all messages.
Returns conversation metadata and full message history.
"""
service = ConversationService(session)
conversation = await service.get_with_messages(conversation_id)
if not conversation:
raise HTTPException(status_code=404, detail="Conversation not found")
if conversation.user_id != user.id:
raise HTTPException(status_code=403, detail="Not authorized")
return ConversationDetailResponse.model_validate(conversation)
@router.delete("/{conversation_id}", status_code=204)
@logged()
async def delete_conversation(
conversation_id: UUID,
session: AsyncSession = Depends(get_session),
user=Depends(require_auth),
) -> None:
"""
Delete a conversation and all its messages.
"""
service = ConversationService(session)
conversation = await service.get(conversation_id)
if not conversation:
raise HTTPException(status_code=404, detail="Conversation not found")
if conversation.user_id != user.id:
raise HTTPException(status_code=403, detail="Not authorized")
await service.delete(conversation_id)
@router.post("/{conversation_id}/messages", response_model=AddMessageResponse)
@logged()
async def add_message(
conversation_id: UUID,
request: AddMessageRequest,
session: AsyncSession = Depends(get_session),
user=Depends(require_auth),
) -> AddMessageResponse:
"""
Add a message to a conversation and get agent response.
This is the main endpoint for continuing conversations.
It:
1. Adds the user message
2. Checks if summarization is needed
3. Builds context from conversation history
4. Gets agent response
5. Adds agent response to conversation
6. Returns both messages
"""
service = ConversationService(session)
# Verify conversation exists and user owns it
conversation = await service.get(conversation_id)
if not conversation:
raise HTTPException(status_code=404, detail="Conversation not found")
if conversation.user_id != user.id:
raise HTTPException(status_code=403, detail="Not authorized")
# Add user message
user_message = await service.add_message(
conversation_id=conversation_id,
role="user",
content=request.content,
)
# Check if summarization needed before getting response
summarized = await service.summarize_if_needed(conversation_id)
# Get agent response with context
try:
response_text = await service.get_agent_response(
conversation_id=conversation_id,
user_message=request.content,
)
except Exception as e:
logger.exception(f"Agent response failed: {e}")
raise HTTPException(
status_code=500,
detail=f"Agent error: {str(e)}"
)
# Add assistant message
assistant_message = await service.add_message(
conversation_id=conversation_id,
role="assistant",
content=response_text,
)
# Get updated conversation for total tokens
conversation = await service.get(conversation_id)
return AddMessageResponse(
user_message=MessageResponse.model_validate(user_message),
assistant_message=MessageResponse.model_validate(assistant_message),
total_tokens=conversation.total_tokens if conversation else 0,
summarized=summarized,
)
@@ -0,0 +1,79 @@
"""
Pydantic schemas for conversation API.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
# === Request Schemas ===
class CreateConversationRequest(BaseModel):
"""Request to create a new conversation."""
agent_type: str = Field(default="explore", description="Agent type to use")
working_dir: str = Field(default=".", description="Working directory for agent")
title: str | None = Field(default=None, description="Optional conversation title")
class AddMessageRequest(BaseModel):
"""Request to add a message to a conversation."""
content: str = Field(..., min_length=1, description="Message content")
# === Response Schemas ===
class MessageResponse(BaseModel):
"""Response for a single message."""
id: UUID
role: str
content: str
token_count: int
is_summary: bool
created_at: datetime
model_config = {"from_attributes": True}
class ConversationResponse(BaseModel):
"""Response for conversation metadata."""
id: UUID
agent_type: str
title: str | None
working_dir: str
total_tokens: int
created_at: datetime
updated_at: datetime | None
model_config = {"from_attributes": True}
class ConversationDetailResponse(BaseModel):
"""Response for conversation with messages."""
id: UUID
agent_type: str
title: str | None
working_dir: str
total_tokens: int
created_at: datetime
updated_at: datetime | None
messages: list[MessageResponse]
model_config = {"from_attributes": True}
class ConversationListResponse(BaseModel):
"""Response for listing conversations."""
conversations: list[ConversationResponse]
total: int
class AddMessageResponse(BaseModel):
"""Response after adding a message (includes agent response)."""
user_message: MessageResponse
assistant_message: MessageResponse
total_tokens: int
summarized: bool = Field(
default=False,
description="Whether context was summarized due to token limit"
)
@@ -0,0 +1,354 @@
"""
Conversation service - Business logic for conversation management.
Handles CRUD operations, context building, and summarization triggers.
"""
from uuid import UUID
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from src.domains.agents.base import get_agent
from src.domains.conversations.models import Conversation, Message
from src.domains.conversations.summarize import generate_summary
from src.shared.config import get_settings
from src.shared.logging import get_logger
from src.shared.tokens import count_tokens
logger = get_logger(__name__)
class ConversationService:
"""
Service for managing conversations and messages.
Handles:
- CRUD operations for conversations and messages
- Context building for agent prompts
- Automatic summarization when approaching token limits
"""
def __init__(self, session: AsyncSession):
"""
Initialize with database session.
Args:
session: Async SQLAlchemy session
"""
self.session = session
self.settings = get_settings()
# === Conversation CRUD ===
async def create(
self,
user_id: str,
agent_type: str = "explore",
working_dir: str = ".",
title: str | None = None,
) -> Conversation:
"""
Create a new conversation.
Args:
user_id: Owner's user ID
agent_type: Type of agent for this conversation
working_dir: Working directory for agent
title: Optional title (auto-generated from first message if None)
Returns:
Created Conversation object
"""
conversation = Conversation(
user_id=user_id,
agent_type=agent_type,
working_dir=working_dir,
title=title,
)
self.session.add(conversation)
await self.session.flush()
logger.info(f"Created conversation {conversation.id} for user {user_id}")
return conversation
async def get(self, conversation_id: UUID) -> Conversation | None:
"""Get conversation by ID without messages."""
result = await self.session.execute(
select(Conversation).where(Conversation.id == conversation_id)
)
return result.scalar_one_or_none()
async def get_with_messages(self, conversation_id: UUID) -> Conversation | None:
"""Get conversation by ID with messages loaded."""
result = await self.session.execute(
select(Conversation)
.options(selectinload(Conversation.messages))
.where(Conversation.id == conversation_id)
)
return result.scalar_one_or_none()
async def list_by_user(
self,
user_id: str,
limit: int = 50,
offset: int = 0,
) -> tuple[list[Conversation], int]:
"""
List conversations for a user.
Args:
user_id: User ID to filter by
limit: Maximum results to return
offset: Offset for pagination
Returns:
Tuple of (conversations, total_count)
"""
# Get total count
count_result = await self.session.execute(
select(func.count(Conversation.id))
.where(Conversation.user_id == user_id)
)
total = count_result.scalar() or 0
# Get conversations
result = await self.session.execute(
select(Conversation)
.where(Conversation.user_id == user_id)
.order_by(Conversation.updated_at.desc())
.limit(limit)
.offset(offset)
)
conversations = list(result.scalars().all())
return conversations, total
async def delete(self, conversation_id: UUID) -> bool:
"""Delete a conversation and all its messages."""
conversation = await self.get(conversation_id)
if conversation:
await self.session.delete(conversation)
logger.info(f"Deleted conversation {conversation_id}")
return True
return False
# === Message Operations ===
async def add_message(
self,
conversation_id: UUID,
role: str,
content: str,
) -> Message:
"""
Add a message to a conversation.
Args:
conversation_id: Conversation to add to
role: Message role (user, assistant, system, summary)
content: Message content
Returns:
Created Message object
"""
# Count tokens
token_count = count_tokens(content)
message = Message(
conversation_id=conversation_id,
role=role,
content=content,
token_count=token_count,
)
self.session.add(message)
# Update conversation total tokens
conversation = await self.get(conversation_id)
if conversation:
conversation.total_tokens += token_count
# Auto-generate title from first user message
if conversation.title is None and role == "user":
conversation.title = content[:100] + ("..." if len(content) > 100 else "")
await self.session.flush()
return message
# === Context Building ===
def build_context_prompt(
self,
messages: list[Message],
current_message: str,
) -> str:
"""
Build a prompt with conversation context.
Includes summary (if exists) and recent messages.
Args:
messages: All conversation messages
current_message: The current user message
Returns:
Formatted prompt with context
"""
parts = []
# Find most recent summary
summaries = [m for m in messages if m.is_summary]
if summaries:
latest_summary = summaries[-1]
parts.append(
f"<conversation_summary>\n{latest_summary.content}\n</conversation_summary>"
)
# Get recent non-summary messages
recent = [m for m in messages if not m.is_summary]
keep_count = self.settings.keep_recent_messages
recent = recent[-keep_count:] if len(recent) > keep_count else recent
if recent:
parts.append("<recent_conversation>")
for msg in recent:
role_label = msg.role.upper()
parts.append(f"{role_label}: {msg.content}")
parts.append("</recent_conversation>")
# Add current message
parts.append(f"<current_request>\n{current_message}\n</current_request>")
return "\n\n".join(parts)
# === Agent Integration ===
async def get_agent_response(
self,
conversation_id: UUID,
user_message: str,
) -> str:
"""
Get agent response with conversation context.
Args:
conversation_id: Conversation ID
user_message: Current user message
Returns:
Agent's response text
"""
conversation = await self.get_with_messages(conversation_id)
if not conversation:
raise ValueError(f"Conversation {conversation_id} not found")
agent = get_agent(conversation.agent_type)
if not agent:
raise ValueError(f"Unknown agent type: {conversation.agent_type}")
# Build context prompt
context_prompt = self.build_context_prompt(
conversation.messages,
user_message,
)
# Run agent
response = await agent.run(
context_prompt,
working_dir=conversation.working_dir,
)
return response
# === Summarization ===
async def should_summarize(self, conversation_id: UUID) -> bool:
"""
Check if conversation needs summarization.
Args:
conversation_id: Conversation to check
Returns:
True if summarization should be triggered
"""
conversation = await self.get(conversation_id)
if not conversation:
return False
threshold = self.settings.max_context_tokens * self.settings.summarization_threshold
return conversation.total_tokens > threshold
async def summarize_if_needed(self, conversation_id: UUID) -> bool:
"""
Summarize old messages if approaching token limit.
Args:
conversation_id: Conversation to check and potentially summarize
Returns:
True if summarization was performed
"""
if not await self.should_summarize(conversation_id):
return False
conversation = await self.get_with_messages(conversation_id)
if not conversation:
return False
messages = conversation.messages
keep_count = self.settings.keep_recent_messages
# Don't summarize if not enough messages
if len(messages) <= keep_count + 1:
return False
# Get messages to summarize (exclude recent and existing summaries)
non_summary_msgs = [m for m in messages if not m.is_summary]
to_summarize = non_summary_msgs[:-keep_count]
if not to_summarize:
return False
logger.info(
f"Summarizing {len(to_summarize)} messages in conversation {conversation_id}"
)
# Generate summary
summary_text = await generate_summary(
to_summarize,
working_dir=conversation.working_dir,
)
# Get ID of last summarized message
last_summarized_id = to_summarize[-1].id
# Calculate tokens being removed
removed_tokens = sum(m.token_count for m in to_summarize)
summary_tokens = count_tokens(summary_text)
# Add summary message
summary_message = Message(
conversation_id=conversation_id,
role="summary",
content=summary_text,
token_count=summary_tokens,
is_summary=True,
summarizes_up_to=last_summarized_id,
)
self.session.add(summary_message)
# Mark old messages as summarized (soft delete by excluding from context)
for msg in to_summarize:
msg.is_summary = True # Reuse flag to mark as "summarized away"
# Update conversation token count
conversation.total_tokens = conversation.total_tokens - removed_tokens + summary_tokens
await self.session.flush()
logger.info(
f"Summarization complete: removed {removed_tokens} tokens, "
f"added {summary_tokens} token summary"
)
return True
@@ -0,0 +1,103 @@
"""
Context summarization for conversations.
Compresses old messages when approaching token limits.
"""
from src.domains.conversations.models import Message
from src.shared.logging import get_logger
logger = get_logger(__name__)
SUMMARIZE_PROMPT = """Summarize this conversation history concisely for context preservation.
Focus on:
- Key decisions made and their rationale
- Important files, functions, or code discussed
- Current task state and progress
- Any unresolved questions or blockers
- Technical details that would be needed to continue the work
Keep the summary under 500 words. Be factual and technical, not conversational.
Preserve specific file paths, function names, and code references.
CONVERSATION HISTORY:
{history}
CONCISE SUMMARY:"""
def format_messages_for_summary(messages: list[Message]) -> str:
"""
Format messages into a string for summarization.
Args:
messages: List of Message objects to format
Returns:
Formatted conversation string
"""
parts = []
for msg in messages:
if msg.is_summary:
parts.append(f"[Previous Summary]: {msg.content}")
else:
role = msg.role.upper()
parts.append(f"{role}: {msg.content}")
return "\n\n".join(parts)
async def generate_summary(
messages: list[Message],
working_dir: str = "."
) -> str:
"""
Generate a summary of conversation messages using the Explore agent.
Args:
messages: Messages to summarize
working_dir: Working directory for agent context
Returns:
Summary text
"""
from src.domains.agents.explore import explore
history = format_messages_for_summary(messages)
prompt = SUMMARIZE_PROMPT.format(history=history)
logger.info(f"Generating summary for {len(messages)} messages")
try:
summary = await explore(prompt, working_dir=working_dir)
return summary.strip()
except Exception as e:
logger.error(f"Summary generation failed: {e}")
# Fallback: create a simple truncated summary
return _fallback_summary(messages)
def _fallback_summary(messages: list[Message]) -> str:
"""
Create a simple fallback summary if agent summarization fails.
Args:
messages: Messages to summarize
Returns:
Basic summary string
"""
# Take first and last few messages
if len(messages) <= 4:
return format_messages_for_summary(messages)
first_two = messages[:2]
last_two = messages[-2:]
parts = [
"Conversation started with:",
format_messages_for_summary(first_two),
f"\n[... {len(messages) - 4} messages omitted ...]\n",
"Most recent exchange:",
format_messages_for_summary(last_two),
]
return "\n".join(parts)

Some files were not shown because too many files have changed in this diff Show More