How the Tool‑Calling Agent Architecture Works in Open‑Code‑Review

The tool‑calling agent in open‑code‑review lets an LLM invoke external utilities during code review by constructing a schema‑driven prompt, parsing JSON function calls from the model, and dispatching execution to concrete tool implementations.

The open‑code‑review repository implements a tool‑calling agent that bridges large language model reasoning with external system capabilities. This architecture enables the agent to perform actions like running git commands, triggering CI pipelines, or querying issue trackers while maintaining a clean separation between LLM decision‑making and tool execution. The design centers on three layers: prompt construction with tool schemas, LLM response parsing, and dynamic tool dispatch.


Architecture Overview

The tool‑calling agent architecture consists of three tightly coupled layers that transform an LLM's intent into executable actions.

1. Prompt and Schema Generation

The agent begins by loading tool descriptors from toolsconfig/tools.json. Each descriptor defines:

  • Tool name and description
  • JSON Schema for parameters
  • Implementation class path

In open_code_review/agent.py, the ToolCallingAgent class constructs a system prompt that injects this tool catalog. The LLM receives explicit instructions on which functions it may call and what arguments each requires.

// toolsconfig/tools.json – example entry
{
  "name": "git_diff",
  "description": "Returns the diff between two git refs",
  "parameters": {
    "type": "object",
    "properties": {
      "repo_path": {"type": "string"},
      "base": {"type": "string"},
      "head": {"type": "string"}
    },
    "required": ["repo_path", "base", "head"]
  },
  "implementation": "open_code_review.tools.git_diff.GitDiffTool"
}

2. LLM Invocation and Function‑Call Parsing

The invoke() method in open_code_review/agent.py sends the constructed prompt to the configured LLM. The response is inspected for a function_call field containing JSON structured data.

When detected, the agent:

  1. Validates the JSON against the schema from toolsconfig/tools.json
  2. Instantiates a ToolCall object with the tool name and parsed arguments
  3. Proceeds to dispatch rather than returning raw text

# Simplified flow from agent.py

response = llm_client.chat(messages=messages, tools=tool_schemas)

if response.get("function_call"):
    tool_call = ToolCall(
        name=response["function_call"]["name"],
        arguments=json.loads(response["function_call"]["arguments"])
    )
    # Proceed to dispatch layer

else:
    # Return direct LLM text response

    return response["content"]

3. Tool Dispatch and Execution

The dispatch layer in open_code_review/tool_dispatcher.py resolves tool names to concrete implementations. Each tool inherits from BaseTool defined in open_code_review/tools/base_tool.py, which enforces a consistent run() interface.

The dispatcher:

  • Dynamically imports the class specified in implementation
  • Instantiates the tool with runtime configuration
  • Calls run() with validated arguments
  • Captures stdout/stderr and returns sanitized output

The tool output is then wrapped into a new message structure and sent back to the LLM, creating a feedback loop for multi‑turn reasoning.


Code Implementation Examples

Creating a Custom Tool

All tools inherit from BaseTool and implement the run() method with typed parameters matching their JSON schema.


# open_code_review/tools/my_tool.py

from .base_tool import BaseTool

class MyTool(BaseTool):
    """Echoes input text back to the agent."""
    
    def run(self, message: str) -> str:
        """
        Parameters must match the 'parameters' schema 
        in toolsconfig/tools.json exactly.
        """
        return f"Tool output: {message}"

Register in toolsconfig/tools.json:

{
  "name": "my_tool",
  "description": "Echoes the supplied string back",
  "parameters": {
    "type": "object",
    "properties": {
      "message": {
        "type": "string",
        "description": "Text to echo"
      }
    },
    "required": ["message"]
  },
  "implementation": "open_code_review.tools.my_tool.MyTool"
}

Using the Tool‑Calling Agent

from open_code_review.agent import ToolCallingAgent

# Initialize with model configuration

agent = ToolCallingAgent(model="gpt-4o-mini")

review_prompt = """
Review this pull request. If you need additional context 
about file changes, use the git_diff tool. Explain your reasoning.
"""

# Single call handles full loop: prompt → LLM → parse → dispatch → result

response = agent.invoke(review_prompt)

# Response contains final LLM answer after any tool calls complete

print(response.final_answer)

Manual Tool Dispatch (Testing and Debugging)

from open_code_review.tool_dispatcher import ToolDispatcher

dispatcher = ToolDispatcher()

# Execute tool directly without LLM involvement

result = dispatcher.run_tool(
    name="git_diff",
    args={
        "repo_path": "/path/to/repo",
        "base": "main",
        "head": "feature-branch"
    }
)

print(result)  # Raw diff output that would feed back to LLM

Core Source Files

File Purpose
open_code_review/agent.py ToolCallingAgent class — prompt building, LLM interaction, function‑call detection
open_code_review/tool_dispatcher.py ToolDispatcher class — dynamic tool resolution and execution
open_code_review/tools/base_tool.py BaseTool abstract class — interface contract for all tools
open_code_review/tools/git_diff.py Reference implementation showing git command execution
toolsconfig/tools.json Central registry of available tools with JSON schemas

Summary

  • Schema‑driven prompts make tool capabilities explicit to the LLM via toolsconfig/tools.json
  • JSON function‑call parsing extracts structured tool requests from model responses in agent.py
  • Dynamic dispatch through tool_dispatcher.py isolates execution logic from LLM reasoning
  • BaseTool abstraction in open_code_review/tools/base_tool.py enables consistent tool development
  • Feedback loop architecture allows multi‑turn tool use where previous results inform subsequent LLM reasoning

Frequently Asked Questions

How does the agent prevent invalid tool calls?

The ToolCallingAgent validates incoming function_call JSON against the schema defined in toolsconfig/tools.json before dispatch. Missing required parameters or type mismatches trigger an error response that the LLM receives as feedback, allowing it to correct and retry.

Can tools maintain state between calls?

Tool implementations in open_code_review/tools/ are designed as stateless functions. Any persistence (caching, session data) is handled externally through the ToolDispatcher configuration or passed explicitly via arguments in each call.

What LLM providers are supported?

The ToolCallingAgent in agent.py abstracts provider details through a client interface. The repository supports OpenAI‑compatible APIs and can extend to other providers by implementing the chat completion interface with function‑call response formats.

How do I add a tool that requires authentication?

Store credentials in environment variables or a secrets manager, then access them within your tool's run() method. Never expose secrets in toolsconfig/tools.json — that file should only contain schema and implementation metadata, not runtime configuration values.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →