feat: add permission modes and CLI orchestration layer
Permission Modes: - Add default/plan/auto_accept modes controlling tool access - Plan mode restricts Task agent to read-only tools only - Auto-accept mode bypasses approval prompts (with confirmation) Approval Scaffolding: - Add ApprovalRule/ApprovalRuleSet for granular tool control - Pattern-based matching on tool name and arguments - Default rules for common safe/dangerous patterns - Prep for future bidirectional approval flow CLI Refactor: - Default to Task agent (main orchestrator) - Add --mode flag and runtime mode switching - Integrate prompt_toolkit for better UX: - Persistent command history (~/.webber_history) - Tab completion for commands and file paths - Auto-suggest from history - Deprecate standalone 'explore' command Other: - Split CHANGELOG.md into per-package files - Update AGENTS.md release procedure for both packages Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
"""
|
||||
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)
|
||||
The Task agent has access to tools based on permission mode:
|
||||
- Plan mode: Read-only tools only
|
||||
- Default/auto_accept: All tools including write operations
|
||||
"""
|
||||
from pydantic_ai import Agent, RunContext
|
||||
|
||||
@@ -20,19 +18,8 @@ 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 ===
|
||||
|
||||
def _register_read_file(agent: Agent[AgentContext, str]) -> None:
|
||||
"""Register read_file tool."""
|
||||
@agent.tool
|
||||
async def read_file(
|
||||
ctx: RunContext[AgentContext],
|
||||
@@ -60,6 +47,9 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
|
||||
)
|
||||
return result.to_string()
|
||||
|
||||
|
||||
def _register_glob_files(agent: Agent[AgentContext, str]) -> None:
|
||||
"""Register glob_files tool."""
|
||||
@agent.tool
|
||||
async def glob_files(
|
||||
ctx: RunContext[AgentContext],
|
||||
@@ -91,6 +81,9 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
|
||||
)
|
||||
return result.to_string()
|
||||
|
||||
|
||||
def _register_grep_content(agent: Agent[AgentContext, str]) -> None:
|
||||
"""Register grep_content tool."""
|
||||
@agent.tool
|
||||
async def grep_content(
|
||||
ctx: RunContext[AgentContext],
|
||||
@@ -124,6 +117,9 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
|
||||
)
|
||||
return result.to_string()
|
||||
|
||||
|
||||
def _register_bash_readonly(agent: Agent[AgentContext, str]) -> None:
|
||||
"""Register bash_readonly tool."""
|
||||
@agent.tool
|
||||
async def bash_readonly(
|
||||
ctx: RunContext[AgentContext],
|
||||
@@ -159,6 +155,94 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
|
||||
)
|
||||
return result.to_string()
|
||||
|
||||
|
||||
def _register_spawn_agent(agent: Agent[AgentContext, str], readonly_only: bool = False) -> None:
|
||||
"""Register spawn_agent tool."""
|
||||
@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}"
|
||||
|
||||
|
||||
def register_readonly_tools(agent: Agent[AgentContext, str]) -> None:
|
||||
"""
|
||||
Register read-only tools with the agent.
|
||||
|
||||
Used in plan mode. Includes:
|
||||
- read_file, glob_files, grep_content, bash_readonly
|
||||
- spawn_agent (restricted to explore/plan)
|
||||
"""
|
||||
_register_read_file(agent)
|
||||
_register_glob_files(agent)
|
||||
_register_grep_content(agent)
|
||||
_register_bash_readonly(agent)
|
||||
_register_spawn_agent(agent, readonly_only=True)
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
# Register read-only tools via helpers
|
||||
_register_read_file(agent)
|
||||
_register_glob_files(agent)
|
||||
_register_grep_content(agent)
|
||||
_register_bash_readonly(agent)
|
||||
|
||||
# === Write tools ===
|
||||
|
||||
@agent.tool
|
||||
@@ -295,56 +379,4 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
|
||||
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}"
|
||||
_register_spawn_agent(agent, readonly_only=False)
|
||||
|
||||
Reference in New Issue
Block a user