Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5b2346db5 | ||
|
|
617ff61347 | ||
|
|
8609181447 |
@@ -184,18 +184,28 @@ cat ../webber-sandbox/TASKS.md
|
|||||||
## Versioning & Releases
|
## Versioning & Releases
|
||||||
|
|
||||||
Uses prefixed tags:
|
Uses prefixed tags:
|
||||||
- `api/v0.3.0` → Triggers API Docker build
|
- `api/vX.Y.Z` → Triggers API Docker build
|
||||||
- `cli/v0.1.0` → Triggers CLI build (future)
|
- `cli/vX.Y.Z` → Triggers CLI build (future)
|
||||||
|
|
||||||
|
### MANDATORY Release Procedure
|
||||||
|
|
||||||
|
**NEVER push a tag before updating version files.** Follow this exact order:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# API release
|
# 1. Update version in pyproject.toml
|
||||||
cd webber-api
|
# 2. Update CHANGELOG.md with release notes
|
||||||
# Update version in pyproject.toml
|
# 3. Commit the version bump
|
||||||
git add -A && git commit -m "chore: release api v0.3.0"
|
git add -A && git commit -m "chore: release api vX.Y.Z"
|
||||||
git tag api/v0.3.0
|
|
||||||
|
# 4. Create the tag (AFTER the commit)
|
||||||
|
git tag api/vX.Y.Z
|
||||||
|
|
||||||
|
# 5. Push everything together
|
||||||
git push origin main --tags
|
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
|
## Troubleshooting
|
||||||
|
|||||||
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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
|
## [0.3.1] - 2026-01-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Tracking progress towards Claude Code-like functionality
|
> Tracking progress towards Claude Code-like functionality
|
||||||
|
|
||||||
## Current Status: ~65% Complete
|
## Current Status: ~70% Complete
|
||||||
|
|
||||||
Last updated: 2026-01-11
|
Last updated: 2026-01-11
|
||||||
|
|
||||||
@@ -44,6 +44,20 @@ Last updated: 2026-01-11
|
|||||||
|
|
||||||
**Gap:** Mistral Nemo sometimes hallucinates instead of using tool results.
|
**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
|
### Phase 3: CLI Foundation ✅ Complete
|
||||||
|
|
||||||
| Component | Status | Notes |
|
| Component | Status | Notes |
|
||||||
@@ -94,7 +108,7 @@ Last updated: 2026-01-11
|
|||||||
|
|
||||||
| Feature | Category | Description | Complexity |
|
| Feature | Category | Description | Complexity |
|
||||||
|---------|----------|-------------|------------|
|
|---------|----------|-------------|------------|
|
||||||
| **Plan Agent** | Agents | Design implementation approaches | High |
|
| ~~**Plan Agent**~~ | Agents | ✅ Design implementation approaches | High |
|
||||||
| **Task Agent** | Agents | Autonomous multi-step execution | High |
|
| **Task Agent** | Agents | Autonomous multi-step execution | High |
|
||||||
| **Context summarization** | Infrastructure | Compress history at token limit | High |
|
| **Context summarization** | Infrastructure | Compress history at token limit | High |
|
||||||
| **Conversation persistence** | CLI | Multi-turn memory in chat mode | Medium |
|
| **Conversation persistence** | CLI | Multi-turn memory in chat mode | Medium |
|
||||||
@@ -103,6 +117,7 @@ Last updated: 2026-01-11
|
|||||||
|
|
||||||
| Feature | Category | Description | Complexity |
|
| 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 |
|
| **Tool result caching** | Infrastructure | Cache file reads for performance | Low |
|
||||||
| **Session persistence** | CLI | Save/resume conversations | Medium |
|
| **Session persistence** | CLI | Save/resume conversations | Medium |
|
||||||
| **Todo tracking** | CLI | Built-in task list (`/todo`) | Medium |
|
| **Todo tracking** | CLI | Built-in task list (`/todo`) | Medium |
|
||||||
@@ -129,6 +144,7 @@ Last updated: 2026-01-11
|
|||||||
|------|---------|--------|--------|
|
|------|---------|--------|--------|
|
||||||
| Tool unit tests | 109 | 109 | ✅ |
|
| Tool unit tests | 109 | 109 | ✅ |
|
||||||
| API tests | 11 | 11 | ✅ |
|
| API tests | 11 | 11 | ✅ |
|
||||||
|
| Plan agent tests | 15 | 15 | ✅ |
|
||||||
| Security tests | 14 | 14 | ✅ |
|
| Security tests | 14 | 14 | ✅ |
|
||||||
| Integration tests | 10 | 10 | ✅ Agent + real LLM |
|
| Integration tests | 10 | 10 | ✅ Agent + real LLM |
|
||||||
| E2E tests | 12 | 12 | ✅ Full API workflow |
|
| E2E tests | 12 | 12 | ✅ Full API workflow |
|
||||||
@@ -140,6 +156,7 @@ Last updated: 2026-01-11
|
|||||||
- Web search: 10 tests
|
- Web search: 10 tests
|
||||||
- Gitignore filtering: 10 tests
|
- Gitignore filtering: 10 tests
|
||||||
- API endpoints: 11 tests
|
- API endpoints: 11 tests
|
||||||
|
- Plan agent: 15 tests
|
||||||
- Security: 14 tests
|
- Security: 14 tests
|
||||||
- Health checks: 2 tests
|
- Health checks: 2 tests
|
||||||
- Integration (LLM): 10 tests
|
- Integration (LLM): 10 tests
|
||||||
@@ -205,6 +222,11 @@ curl -X POST http://localhost:8095/agents/run \
|
|||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"agent_type":"explore","prompt":"list python files","working_dir":"."}'
|
-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
|
# Streaming endpoint
|
||||||
curl -N http://localhost:8095/agents/stream \
|
curl -N http://localhost:8095/agents/stream \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "webber-api"
|
name = "webber-api"
|
||||||
version = "0.3.1"
|
version = "0.3.3"
|
||||||
description = "Webber API - Multi-Agent AI Development Server"
|
description = "Webber API - Multi-Agent AI Development Server"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "jpmschweitzer"}
|
{name = "jpmschweitzer"}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
"""
|
||||||
@@ -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()
|
||||||
@@ -9,6 +9,7 @@ from src.domains.agents.base import get_agent, list_agents
|
|||||||
|
|
||||||
# Import agents to ensure they're registered
|
# Import agents to ensure they're registered
|
||||||
import src.domains.agents.explore # noqa: F401
|
import src.domains.agents.explore # noqa: F401
|
||||||
|
import src.domains.agents.plan # noqa: F401
|
||||||
from src.domains.agents.schemas import (
|
from src.domains.agents.schemas import (
|
||||||
AgentRunRequest,
|
AgentRunRequest,
|
||||||
AgentRunResponse,
|
AgentRunResponse,
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"""
|
||||||
|
Tests for the Plan agent.
|
||||||
|
|
||||||
|
Tests registration, API endpoints, and tool restrictions.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.domains.agents.base import get_agent, list_agents
|
||||||
|
from src.domains.agents.plan import plan_agent, PlanAgentImpl
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlanAgentRegistration:
|
||||||
|
"""Tests for Plan agent registration."""
|
||||||
|
|
||||||
|
def test_plan_agent_registered(self):
|
||||||
|
"""Test that plan agent is registered in registry."""
|
||||||
|
agent = get_agent("plan")
|
||||||
|
assert agent is not None
|
||||||
|
assert agent.name == "plan"
|
||||||
|
|
||||||
|
def test_plan_agent_in_list(self):
|
||||||
|
"""Test that plan agent appears in agent list."""
|
||||||
|
agents = list_agents()
|
||||||
|
names = [a["name"] for a in agents]
|
||||||
|
assert "plan" in names
|
||||||
|
|
||||||
|
def test_plan_agent_has_description(self):
|
||||||
|
"""Test that plan agent has a description."""
|
||||||
|
agent = get_agent("plan")
|
||||||
|
assert agent is not None
|
||||||
|
assert len(agent.description) > 0
|
||||||
|
assert "plan" in agent.description.lower() or "architect" in agent.description.lower()
|
||||||
|
|
||||||
|
def test_plan_agent_singleton(self):
|
||||||
|
"""Test that plan_agent is the registered instance."""
|
||||||
|
registered = get_agent("plan")
|
||||||
|
assert registered is plan_agent
|
||||||
|
|
||||||
|
def test_plan_agent_is_correct_type(self):
|
||||||
|
"""Test that plan agent is correct implementation type."""
|
||||||
|
assert isinstance(plan_agent, PlanAgentImpl)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlanAgentTools:
|
||||||
|
"""Tests for Plan agent tool restrictions."""
|
||||||
|
|
||||||
|
def test_plan_agent_has_read_only_tools(self):
|
||||||
|
"""Test that plan agent has read-only tools."""
|
||||||
|
# Access the underlying PydanticAI agent to check tools
|
||||||
|
agent = plan_agent.agent
|
||||||
|
tool_names = list(agent._function_toolset.tools.keys())
|
||||||
|
|
||||||
|
# Should have read-only tools
|
||||||
|
assert "read_file" in tool_names
|
||||||
|
assert "glob_files" in tool_names
|
||||||
|
assert "grep_content" in tool_names
|
||||||
|
assert "bash_readonly" in tool_names
|
||||||
|
|
||||||
|
def test_plan_agent_no_write_tools(self):
|
||||||
|
"""Test that plan agent does NOT have write tools."""
|
||||||
|
agent = plan_agent.agent
|
||||||
|
tool_names = list(agent._function_toolset.tools.keys())
|
||||||
|
|
||||||
|
# Should NOT have write tools
|
||||||
|
assert "edit_file" not in tool_names
|
||||||
|
assert "write_file" not in tool_names
|
||||||
|
assert "bash" not in tool_names
|
||||||
|
assert "web_search" not in tool_names
|
||||||
|
|
||||||
|
def test_plan_agent_tool_count(self):
|
||||||
|
"""Test that plan agent has exactly 4 tools."""
|
||||||
|
agent = plan_agent.agent
|
||||||
|
tool_count = len(agent._function_toolset.tools)
|
||||||
|
assert tool_count == 4
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlanAgentAPI:
|
||||||
|
"""Tests for Plan agent REST API."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_list_agents_includes_plan(self, auth_client):
|
||||||
|
"""Test that agent list includes plan agent."""
|
||||||
|
response = await auth_client.get("/agents/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
names = [a["name"] for a in data["agents"]]
|
||||||
|
assert "plan" in names
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_plan_agent_info(self, auth_client):
|
||||||
|
"""Test getting plan agent info."""
|
||||||
|
response = await auth_client.get("/agents/plan")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "plan"
|
||||||
|
assert "description" in data
|
||||||
|
assert len(data["description"]) > 0
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_run_plan_with_invalid_body(self, auth_client):
|
||||||
|
"""Test running plan agent with invalid request."""
|
||||||
|
response = await auth_client.post(
|
||||||
|
"/agents/run",
|
||||||
|
json={
|
||||||
|
"agent_type": "plan",
|
||||||
|
# Missing prompt
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_stream_plan_with_invalid_body(self, auth_client):
|
||||||
|
"""Test streaming plan agent with invalid request."""
|
||||||
|
response = await auth_client.post(
|
||||||
|
"/agents/stream",
|
||||||
|
json={
|
||||||
|
"agent_type": "plan",
|
||||||
|
# Missing prompt
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlanAgentProperties:
|
||||||
|
"""Tests for Plan agent properties and configuration."""
|
||||||
|
|
||||||
|
def test_plan_agent_name(self):
|
||||||
|
"""Test plan agent name property."""
|
||||||
|
assert plan_agent.name == "plan"
|
||||||
|
|
||||||
|
def test_plan_agent_description_not_empty(self):
|
||||||
|
"""Test plan agent description is not empty."""
|
||||||
|
assert plan_agent.description
|
||||||
|
assert len(plan_agent.description) > 10
|
||||||
|
|
||||||
|
def test_plan_agent_creates_agent_lazily(self):
|
||||||
|
"""Test that PydanticAI agent is created lazily."""
|
||||||
|
# Create a fresh instance
|
||||||
|
fresh_agent = PlanAgentImpl()
|
||||||
|
|
||||||
|
# _agent should be None before first access
|
||||||
|
assert fresh_agent._agent is None
|
||||||
|
|
||||||
|
# Access the agent property
|
||||||
|
_ = fresh_agent.agent
|
||||||
|
|
||||||
|
# Now _agent should be set
|
||||||
|
assert fresh_agent._agent is not None
|
||||||
Reference in New Issue
Block a user