The agent system uses a Registry Pattern with auto-discovery to enable plugin-like extensibility. New agents can be added without modifying the dispatcher code, following the Open/Closed Principle.
-
BaseAgent (
src/pipe/core/agents/base.py)- Abstract base class defining the agent interface
- All agents must implement the
run()method
-
Registry (
src/pipe/core/agents/__init__.py)@register_agent(key)decorator for registrationget_agent_class(key)to retrieve agents- Auto-discovery using
pkgutil.walk_packages()
-
Dispatcher (
src/pipe/core/dispatcher.py)- Uses registry to dynamically load agents
- No hardcoded if/elif branches
- Create a new file
src/pipe/core/agents/claude_api.py:
from pipe.core.agents import register_agent
from pipe.core.agents.base import BaseAgent
from pipe.core.models.args import TaktArgs
from pipe.core.services.prompt_service import PromptService
from pipe.core.services.session_service import SessionService
@register_agent("claude-api")
class ClaudeApiAgent(BaseAgent):
"""Agent for Anthropic Claude API."""
def run(
self,
args: TaktArgs,
session_service: SessionService,
prompt_service: PromptService,
) -> tuple[str, int | None, list]:
"""Execute the Claude API agent.
Args:
args: Command line arguments
session_service: Service for session management
prompt_service: Service for prompt building
Returns:
Tuple of (response_text, token_count, turns_to_save)
"""
# Your implementation here
model_response_text = "Hello from Claude!"
token_count = 42
turns_to_save = []
return model_response_text, token_count, turns_to_save-
That's it! The agent is automatically registered and available.
-
Use it by setting
api_mode: claude-apiinsetting.yml:
api_mode: claude-api- Add new agents without modifying existing code
- Dispatcher doesn't need to know about specific agents
- Agents are self-contained modules
- No circular dependencies in dispatch logic
- Each agent file focuses on a single responsibility
- Easy to locate and modify agent-specific code
- Auto-discovery eliminates manual registration
- LSP-friendly (autocomplete, go-to-definition work)
You can see all registered agents at runtime:
from pipe.core.agents import AGENT_REGISTRY
print(sorted(AGENT_REGISTRY.keys()))
# Output: ['gemini-api', 'gemini-cli']If an invalid api_mode is specified, the system provides a helpful error:
from pipe.core.agents import get_agent_class
try:
get_agent_class('unknown-mode')
except ValueError as e:
print(e)
# Unknown api_mode: 'unknown-mode'.
# Available agents: [gemini-api, gemini-cli]The __init__.py uses pkgutil.walk_packages() to automatically import all Python modules in the agents/ directory:
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
if module_name in ("base",): # Skip base module
continue
try:
importlib.import_module(f".{module_name}", __package__)
except Exception:
# Silently skip modules with missing dependencies
passThis triggers the @register_agent decorators, populating the AGENT_REGISTRY.
The dispatcher uses the registry polymorphically:
from pipe.core.agents import get_agent_class
# Get agent class from registry
AgentClass = get_agent_class(api_mode)
# Instantiate and execute (no branching needed)
agent_instance = AgentClass()
model_response_text, token_count, turns_to_save = agent_instance.run(
args, session_service, prompt_service
)When writing tests for agents, you can mock the agent directly:
@patch("pipe.core.agents.gemini_api.GeminiApiAgent.run",
return_value=("response", 100, []))
def test_my_feature(self, mock_agent_run):
# Your test code
passif api_mode == "gemini-api":
from .delegates import gemini_api_delegate
result = gemini_api_delegate.run(...)
elif api_mode == "gemini-cli":
from .delegates import gemini_cli_delegate
result = gemini_cli_delegate.run(...)
else:
raise ValueError(f"Unknown api_mode: {api_mode}")from pipe.core.agents import get_agent_class
AgentClass = get_agent_class(api_mode)
agent_instance = AgentClass()
result = agent_instance.run(args, session_service, prompt_service)-
Naming Convention: Use descriptive names like
{provider}_{interface}.py- Examples:
gemini_api.py,claude_api.py,openai_api.py
- Examples:
-
Error Handling: Agents should raise meaningful exceptions
- Use
RuntimeErrorfor API failures - Use
ValueErrorfor invalid configuration
- Use
-
Documentation: Include docstrings for the agent class and
run()method -
Dependencies: Handle optional dependencies gracefully
- The auto-discovery silently skips modules that fail to import
- Add dependency checks in the agent's
run()method if needed
-
Testing: Write unit tests for each agent
- Test the
run()method with various scenarios - Mock external API calls
- Test the
Potential improvements to the registry pattern:
- Agent Metadata: Store additional info (description, version, dependencies)
- Dynamic Loading: Load agents on-demand rather than at import time
- Agent Configuration: Per-agent settings validation
- Plugin System: Load agents from external packages