Skip to content

Agent API

The core agent implementation for PatchPal, providing the main interface for interacting with LLMs and executing tools.

Creating an Agent

patchpal.agent.create_agent(model_id='anthropic/claude-sonnet-4-5', custom_tools=None, litellm_kwargs=None)

Create and return a PatchPal agent.

Parameters:

Name Type Description Default
model_id str

LiteLLM model identifier (default: anthropic/claude-sonnet-4-5)

'anthropic/claude-sonnet-4-5'
custom_tools Optional[List[Callable]]

Optional list of Python functions to use as custom tools. Each function should have type hints and a docstring.

None
litellm_kwargs Optional[Dict[str, Any]]

Optional dict of extra parameters to pass to litellm.completion() (e.g., {"reasoning_effort": "high"} for reasoning models)

None

Returns:

Type Description
PatchPalAgent

A configured PatchPalAgent instance

Example

def calculator(x: int, y: int) -> str: '''Add two numbers.

Args:
    x: First number
    y: Second number
'''
return str(x + y)

agent = create_agent(custom_tools=[calculator]) response = agent.run("What's 5 + 3?")

With reasoning model

agent = create_agent( model_id="ollama_chat/gpt-oss:20b", litellm_kwargs={"reasoning_effort": "high"} )

Source code in patchpal/agent.py
def create_agent(
    model_id: str = "anthropic/claude-sonnet-4-5",
    custom_tools: Optional[List[Callable]] = None,
    litellm_kwargs: Optional[Dict[str, Any]] = None,
) -> PatchPalAgent:
    """Create and return a PatchPal agent.

    Args:
        model_id: LiteLLM model identifier (default: anthropic/claude-sonnet-4-5)
        custom_tools: Optional list of Python functions to use as custom tools.
                     Each function should have type hints and a docstring.
        litellm_kwargs: Optional dict of extra parameters to pass to litellm.completion()
                       (e.g., {"reasoning_effort": "high"} for reasoning models)

    Returns:
        A configured PatchPalAgent instance

    Example:
        def calculator(x: int, y: int) -> str:
            '''Add two numbers.

            Args:
                x: First number
                y: Second number
            '''
            return str(x + y)

        agent = create_agent(custom_tools=[calculator])
        response = agent.run("What's 5 + 3?")

        # With reasoning model
        agent = create_agent(
            model_id="ollama_chat/gpt-oss:20b",
            litellm_kwargs={"reasoning_effort": "high"}
        )
    """
    # Reset session todos for new session
    from patchpal.tools import reset_session_todos

    reset_session_todos()

    return PatchPalAgent(
        model_id=model_id, custom_tools=custom_tools, litellm_kwargs=litellm_kwargs
    )

Agent Class

patchpal.agent.PatchPalAgent(model_id='anthropic/claude-sonnet-4-5', custom_tools=None, litellm_kwargs=None)

Simple agent that uses LiteLLM for tool calling.

Initialize the agent.

Parameters:

Name Type Description Default
model_id str

LiteLLM model identifier

'anthropic/claude-sonnet-4-5'
custom_tools Optional[List[Callable]]

Optional list of Python functions to add as tools

None
litellm_kwargs Optional[Dict[str, Any]]

Optional dict of extra parameters to pass to litellm.completion() (e.g., {"reasoning_effort": "high"} for reasoning models)

None
Source code in patchpal/agent.py
def __init__(
    self,
    model_id: str = "anthropic/claude-sonnet-4-5",
    custom_tools: Optional[List[Callable]] = None,
    litellm_kwargs: Optional[Dict[str, Any]] = None,
):
    """Initialize the agent.

    Args:
        model_id: LiteLLM model identifier
        custom_tools: Optional list of Python functions to add as tools
        litellm_kwargs: Optional dict of extra parameters to pass to litellm.completion()
                      (e.g., {"reasoning_effort": "high"} for reasoning models)
    """
    # Store custom tools
    self.custom_tools = custom_tools or []
    self.custom_tool_funcs = {func.__name__: func for func in self.custom_tools}

    # Convert ollama/ to ollama_chat/ for LiteLLM compatibility
    if model_id.startswith("ollama/"):
        model_id = model_id.replace("ollama/", "ollama_chat/", 1)

    self.model_id = _normalize_bedrock_model_id(model_id)

    # Register Ollama models as supporting native function calling
    # LiteLLM defaults to JSON mode if not explicitly registered
    if self.model_id.startswith("ollama_chat/"):
        # Suppress verbose output from register_model
        import sys
        from io import StringIO

        old_stdout = sys.stdout
        sys.stdout = StringIO()
        try:
            litellm.register_model(
                {"model_cost": {self.model_id: {"supports_function_calling": True}}}
            )
        finally:
            sys.stdout = old_stdout

    # Set up Bedrock environment if needed
    if self.model_id.startswith("bedrock/"):
        _setup_bedrock_env()

    # Conversation history (list of message dicts)
    self.messages: List[Dict[str, Any]] = []

    # Initialize context manager
    self.context_manager = ContextManager(self.model_id, SYSTEM_PROMPT)

    # Check if auto-compaction is enabled (default: True)
    self.enable_auto_compact = (
        os.getenv("PATCHPAL_DISABLE_AUTOCOMPACT", "false").lower() != "true"
    )

    # Track last compaction to prevent compaction loops
    self._last_compaction_message_count = 0

    # Track cumulative token usage across all LLM calls
    self.total_llm_calls = 0
    self.cumulative_input_tokens = 0
    self.cumulative_output_tokens = 0

    # Track cache-related tokens (for Anthropic/Bedrock models with prompt caching)
    self.cumulative_cache_creation_tokens = 0
    self.cumulative_cache_read_tokens = 0

    # Track OpenAI cache tokens (prompt_tokens_details.cached_tokens)
    self.cumulative_openai_cached_tokens = 0

    # Track cumulative costs across all LLM calls
    self.cumulative_cost = 0.0
    self.last_message_cost = 0.0

    # LiteLLM settings for models that need parameter dropping
    self.litellm_kwargs = {}
    if self.model_id.startswith("bedrock/"):
        self.litellm_kwargs["drop_params"] = True
        # Configure LiteLLM to handle Bedrock's strict message alternation requirement
        # This must be set globally, not as a completion parameter
        litellm.modify_params = True
    elif self.model_id.startswith("openai/") and os.getenv("OPENAI_API_BASE"):
        # Custom OpenAI-compatible servers (vLLM, etc.) often don't support all parameters
        self.litellm_kwargs["drop_params"] = True

    # Merge in any user-provided litellm_kwargs
    if litellm_kwargs:
        self.litellm_kwargs.update(litellm_kwargs)

    # Load MEMORY.md if it exists and has non-template content
    self._load_project_memory()

run(user_message, max_iterations=100)

Run the agent on a user message.

Parameters:

Name Type Description Default
user_message str

The user's request

required
max_iterations int

Maximum number of agent iterations (default: 100)

100

Returns:

Type Description
str

The agent's final response

Source code in patchpal/agent.py
def run(self, user_message: str, max_iterations: int = 100) -> str:
    """Run the agent on a user message.

    Args:
        user_message: The user's request
        max_iterations: Maximum number of agent iterations (default: 100)

    Returns:
        The agent's final response
    """
    # Add user message to history
    self.messages.append({"role": "user", "content": user_message})

    # Check for compaction BEFORE starting work
    # This ensures we never compact mid-execution and lose tool results
    if self.enable_auto_compact and self.context_manager.needs_compaction(self.messages):
        self._perform_auto_compaction()

    # Agent loop with interrupt handling
    try:
        return self._run_agent_loop(max_iterations)
    except KeyboardInterrupt:
        # Clean up conversation state if interrupted mid-execution
        self._cleanup_interrupted_state()
        raise  # Re-raise so CLI can handle it

Helper Functions

patchpal.agent._is_bedrock_arn(model_id)

Check if a model ID is a Bedrock ARN.

Source code in patchpal/agent.py
def _is_bedrock_arn(model_id: str) -> bool:
    """Check if a model ID is a Bedrock ARN."""
    return (
        model_id.startswith("arn:aws")
        and ":bedrock:" in model_id
        and ":inference-profile/" in model_id
    )

Usage Example

from patchpal.agent import create_agent

# Create agent with default model
agent = create_agent()

# Or specify a model
agent = create_agent(model_id="anthropic/claude-sonnet-4-5")

# Run a task
response = agent.run("List all Python files")
print(response)

# Check token usage
print(f"Total tokens: {agent.cumulative_input_tokens + agent.cumulative_output_tokens:,}")