feat: add Plan Agent for implementation planning
- 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>
This commit is contained in:
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> Tracking progress towards Claude Code-like functionality
|
||||
|
||||
## Current Status: ~65% Complete
|
||||
## Current Status: ~70% Complete
|
||||
|
||||
Last updated: 2026-01-11
|
||||
|
||||
@@ -44,6 +44,20 @@ Last updated: 2026-01-11
|
||||
|
||||
**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 |
|
||||
@@ -94,7 +108,7 @@ Last updated: 2026-01-11
|
||||
|
||||
| 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 |
|
||||
| **Context summarization** | Infrastructure | Compress history at token limit | High |
|
||||
| **Conversation persistence** | CLI | Multi-turn memory in chat mode | Medium |
|
||||
@@ -103,6 +117,7 @@ Last updated: 2026-01-11
|
||||
|
||||
| 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 |
|
||||
@@ -129,6 +144,7 @@ Last updated: 2026-01-11
|
||||
|------|---------|--------|--------|
|
||||
| Tool unit tests | 109 | 109 | ✅ |
|
||||
| API tests | 11 | 11 | ✅ |
|
||||
| Plan agent tests | 15 | 15 | ✅ |
|
||||
| Security tests | 14 | 14 | ✅ |
|
||||
| Integration tests | 10 | 10 | ✅ Agent + real LLM |
|
||||
| E2E tests | 12 | 12 | ✅ Full API workflow |
|
||||
@@ -140,6 +156,7 @@ Last updated: 2026-01-11
|
||||
- Web search: 10 tests
|
||||
- Gitignore filtering: 10 tests
|
||||
- API endpoints: 11 tests
|
||||
- Plan agent: 15 tests
|
||||
- Security: 14 tests
|
||||
- Health checks: 2 tests
|
||||
- Integration (LLM): 10 tests
|
||||
@@ -205,6 +222,11 @@ 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" \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "webber-api"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
description = "Webber API - Multi-Agent AI Development Server"
|
||||
authors = [
|
||||
{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 src.domains.agents.explore # noqa: F401
|
||||
import src.domains.agents.plan # noqa: F401
|
||||
from src.domains.agents.schemas import (
|
||||
AgentRunRequest,
|
||||
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