Skip to content

Chapter 4: Composition - Agents Having Tools vs Being Tools

Theoretical Foundations

The architectural distinction between an agent having tools versus an agent being a tool is the fundamental line separating active, goal-oriented systems from passive, functional components. In the context of AI data structures and complex systems modeling, this distinction dictates the flow of control, the encapsulation of state, and the scalability of the entire application.

In Book 1, we established the basics of Object-Oriented Programming (OOP), focusing on encapsulation and inheritance. We learned that inheritance creates an "is-a" relationship (e.g., a Dog is an Animal). However, as we model complex AI systems, inheritance often proves too rigid. We now turn to Composition, which defines a "has-a" or "uses-a" relationship.

Composition is the act of building complex objects by combining simpler, specialized objects. In our domain, this means an Agent is composed of Tools.

The "Has-A" Relationship: Agents Having Tools

An agent that has tools is an active entity. It possesses the agency to decide when and how to use these tools to achieve a broader goal. This is the Tool-Use Pattern.

Real-World Analogy: The General Contractor Imagine a General Contractor (GC) building a house.

  • The GC is the Agent.
  • The GC does not pour concrete, wire electricity, or install plumbing personally. Instead, the GC has a list of subcontractors: an Electrician, a Plumber, and a Carpenter.
  • These subcontractors are the Tools.
  • The GC analyzes the blueprint (the goal), determines the sequence of operations, and calls upon the specific subcontractor when their expertise is required.
  • Crucially, the GC remains the active controller. The GC's internal state (schedule, budget) changes based on the outcomes of the tool calls.

In C#, this is modeled by maintaining a collection of tool objects within the agent. The agent does not inherit from the tool; it holds a reference to it.

using System;
using System.Collections.Generic;

// The Tool (Subcontractor)
public class CalculatorTool
{
    public string Name => "Calculator";

    public double Add(double a, double b)
    {
        return a + b;
    }
}

// The Agent (General Contractor)
public class MathAgent
{
    // Composition: The Agent HAS-A Tool
    private CalculatorTool _calculator;

    public MathAgent()
    {
        _calculator = new CalculatorTool();
    }

    public void SolveComplexEquation()
    {
        // The agent decides when to use the tool
        double result = _calculator.Add(5, 10);
        Console.WriteLine($"Result from tool: {result}");
    }
}

The "Is-A" Relationship: Agents Being Tools

An agent that is a tool represents Passive Composition. It functions as a utility or a subroutine. It has no internal decision-making logic regarding when it is called; it simply performs a specific function when invoked by an external controller.

Real-World Analogy: The Hammer A hammer is a tool. It does not decide to hit the nail; the carpenter decides. The hammer has no autonomy; it is a functional extension of the carpenter's arm. If you ask a hammer to build a house, it cannot respond because it lacks the orchestration logic.

In C#, this is often modeled as a static method or a simple class with a single responsibility, lacking the state management required to track progress toward a complex goal.

// The Tool (The Hammer)
public static class UtilityTool
{
    // Passive: It sits there waiting to be called
    public static double CalculateArea(double width, double height)
    {
        return width * height;
    }
}

// The Controller (The Carpenter)
public class Architect
{
    public void Design()
    {
        // The architect uses the tool
        double area = UtilityTool.CalculateArea(10, 20);
    }
}

The Tool-Use Pattern: Invoking External APIs vs. Internal Method Calls

In AI applications, the distinction between internal and external tool invocation is critical for latency, cost, and reliability.

  1. Internal Method Calls: When an agent uses a tool that is part of the same process (like the CalculatorTool above), the invocation is synchronous and fast. The agent maintains direct control over memory and execution flow. This is ideal for deterministic logic (math, string manipulation, data validation).

  2. External API Invocations: When an agent uses a tool that calls an external service (e.g., a weather API, a vector database, or another LLM), the agent must handle asynchrony, network errors, and data serialization.

    • Implication: The agent's state management becomes complex. It must wait for the external tool to return before proceeding.
    • AI Context: An LLM-based agent often receives a natural language request, parses it to identify a need (e.g., "What is the weather?"), selects the appropriate tool (Weather API), invokes it, and then processes the result back into natural language.

The Adapter Pattern: Standardizing Heterogeneous Tools

AI systems often aggregate tools from different providers (e.g., OpenAI, Google, Local Llama). These tools have vastly different interfaces. One might expect a string prompt, another a List<Message>, and a third a Tensor.

To allow an Agent to compose these tools effectively, we must standardize them. This is where the Adapter Pattern comes in. We previously discussed polymorphism in Book 1; the Adapter Pattern is the practical application of polymorphism to bridge incompatible interfaces.

We define a common interface (ITool) that all tools must implement. The Agent then interacts solely with ITool, unaware of the underlying complexity of the specific implementation.

The ITool Interface: This interface acts as a contract. It ensures that every tool the agent possesses can be invoked in a uniform way.

// The Common Interface (The Contract)
public interface ITool
{
    string Name { get; }
    string Description { get; }
    // Executes the tool and returns a result string
    string Execute(string input);
}

Concrete Implementations (The Adapters): Now we adapt different systems to this interface.

// Adapter for a Local Math Library
public class LocalMathAdapter : ITool
{
    public string Name => "LocalMath";
    public string Description => "Performs basic arithmetic.";

    public string Execute(string input)
    {
        // Parse input "5+5" -> calculate -> return "10"
        // This hides the complexity of the internal math engine
        return "Calculated Result";
    }
}

// Adapter for a Remote Weather API
public class WeatherApiAdapter : ITool
{
    public string Name => "WeatherAPI";
    public string Description => "Fetches current weather data.";

    public string Execute(string input)
    {
        // Logic to make an HTTP request
        // Handle JSON parsing
        return "Sunny, 72F";
    }
}

Why this matters for AI: In a complex system, an Agent might need to swap between a fast, local model (like a small BERT model) and a heavy, remote model (like GPT-4). By wrapping both in an ITool interface (or an IModel interface), the Agent's composition logic doesn't change. It simply holds a reference to ITool, and the specific implementation is injected at runtime.

Tensor-Based State Management in Tool-Calling Agents

As we move into advanced OOP and AI data structures, we must consider how state is represented. In simple applications, state might be integers or strings. In AI, state is often represented as Tensors (multidimensional arrays of numbers).

When an Agent composes tools, the output of one tool often becomes the input to another. In a tensor-based architecture, these inputs and outputs are not just raw strings; they are high-dimensional vectors representing semantic meaning.

The Architectural Implication:

  1. Tool Input: The Agent must convert the current tensor state into a format the tool understands (e.g., a text prompt or a specific numeric tensor).
  2. Tool Execution: The tool processes the input.
  3. Tool Output: The tool returns a result (text, a number, or a new tensor).
  4. State Update: The Agent must integrate this result back into its central tensor state.

Example Scenario: An Agent is tasked with "Summarize this document and find the sentiment."

  1. Tool 1 (Document Reader): Takes a file path, returns a tensor embedding of the text.
  2. Tool 2 (Summarizer): Takes the embedding tensor, returns a condensed text string.
  3. Tool 3 (Sentiment Analyzer): Takes the condensed text, returns a sentiment score (a scalar tensor).

The Agent orchestrates this flow. It holds the "state" (the document data, the summary, the score) in a structured way, passing it between tools.

Visualizing the Architecture

The following diagram illustrates the flow of control in a Tool-Use Pattern agent versus a passive tool structure.

The diagram contrasts a passive tool structure, where an LLM simply calls a tool and waits for a response, with a Tool-Use Pattern agent that actively manages a state object—containing document data, a summary, and a score—to coordinate the flow of control between tools.
Hold "Ctrl" to enable pan & zoom

The diagram contrasts a passive tool structure, where an LLM simply calls a tool and waits for a response, with a Tool-Use Pattern agent that actively manages a state object—containing document data, a summary, and a score—to coordinate the flow of control between tools.

Summary of Architectural Implications

  1. Autonomy vs. Utility:

    • Active Agents (Has-A): Are suitable for complex workflows where the sequence of operations is unknown or dynamic (e.g., planning a trip, debugging code).
    • Passive Tools (Is-A): Are suitable for deterministic, reusable logic (e.g., calculating a checksum, formatting a date).
  2. Scalability:

    • Active Agents: Scale by adding more tools to the composition. The agent's complexity grows, but the tools remain decoupled. This allows for modular updates (swapping the Weather API without changing the Agent's core logic).
    • Passive Tools: Scale by being called by many different agents. However, they lack context; they cannot adapt their behavior based on a long-running conversation history.
  3. Error Handling:

    • In an Active Agent, if a tool fails (e.g., API timeout), the agent must have logic to handle that failure—perhaps by trying a different tool or asking the user for clarification.
    • In a Passive Tool, error handling is usually immediate and localized (throw an exception). It has no context to decide on a recovery strategy.

By mastering the distinction between having tools and being a tool, and by utilizing patterns like Adapter and Composition, we build AI systems that are not just collections of functions, but coherent, autonomous entities capable of solving complex, multi-step problems.

Basic Code Example

Problem Context: The Home Assistant Agent

Imagine you are building a smart home assistant. This assistant needs to interact with various devices, such as a smart light and a thermostat. The assistant acts as an Agent that has tools. It must decide which tool to use based on user requests.

We will model two scenarios:

  1. Active Composition (The Agent): A HomeAssistantAgent that possesses a collection of tools. It parses a request and dynamically selects the appropriate tool to execute.
  2. Passive Composition (The Tool): A Thermostat class that functions solely as a tool. It has no decision-making logic; it simply executes commands given to it.

This example highlights the architectural difference: the Agent is responsible for logic and orchestration, while the Tool is responsible for specific, isolated actions.

Common Pitfalls

A frequent mistake in designing these systems is tight coupling between the Agent and specific tool implementations. If the HomeAssistantAgent directly instantiates LightBulb or Thermostat inside its class, it becomes impossible to swap tools or add new ones without modifying the Agent's code. This violates the Open/Closed Principle. Always rely on abstraction (interfaces) and dependency injection to ensure the Agent works with any tool that adheres to the expected interface.

Code Example

Here is a basic implementation demonstrating the Agent-Tool relationship using only Advanced OOP principles (Interfaces, Polymorphism, Encapsulation) without Generics, Lambdas, or LINQ.

using System;
using System.Collections;

namespace SmartHomeSystem
{
    // 1. THE TOOL INTERFACE
    // Defines the contract that all tools must follow.
    // This allows the Agent to treat different devices uniformly.
    public interface ITool
    {
        string Name { get; }
        void Execute(string command);
    }

    // 2. CONCRETE TOOLS (Passive Composition)
    // These classes implement the logic for specific actions.
    // They are "dumb" and do not decide when to run.

    public class LightBulb : ITool
    {
        // Encapsulated state: the light is either on or off.
        private bool _isOn = false;

        public string Name
        {
            get { return "Light Bulb"; }
        }

        public void Execute(string command)
        {
            if (command == "Turn On")
            {
                _isOn = true;
                Console.WriteLine("The light is now ON.");
            }
            else if (command == "Turn Off")
            {
                _isOn = false;
                Console.WriteLine("The light is now OFF.");
            }
            else
            {
                Console.WriteLine("Light Bulb does not understand that command.");
            }
        }
    }

    public class Thermostat : ITool
    {
        // Encapsulated state: current temperature setting.
        private int _temperature = 72;

        public string Name
        {
            get { return "Thermostat"; }
        }

        public void Execute(string command)
        {
            if (command.StartsWith("Set Temp: "))
            {
                // Parsing the command string to extract the temperature.
                // Note: In a more advanced system, we might use structured data,
                // but here we rely on string parsing to keep it simple.
                string tempStr = command.Substring(10);
                int newTemp;
                if (int.TryParse(tempStr, out newTemp))
                {
                    _temperature = newTemp;
                    Console.WriteLine($"Thermostat set to {_temperature} degrees.");
                }
                else
                {
                    Console.WriteLine("Invalid temperature format.");
                }
            }
            else
            {
                Console.WriteLine("Thermostat does not understand that command.");
            }
        }
    }

    // 3. THE AGENT (Active Composition)
    // The Agent possesses tools and makes decisions.
    public class HomeAssistantAgent
    {
        // The Agent holds a collection of tools.
        // We use an ArrayList because Generics are forbidden in this section.
        // ArrayList stores objects of type 'object', so we must cast when retrieving.
        private ArrayList _tools;

        public HomeAssistantAgent()
        {
            _tools = new ArrayList();
        }

        // Method to dynamically compose the toolset.
        // This demonstrates dependency injection (passing dependencies in).
        public void AddTool(ITool tool)
        {
            _tools.Add(tool);
            Console.WriteLine($"Agent registered tool: {tool.Name}");
        }

        // The core logic: parsing input and delegating to the correct tool.
        public void ProcessRequest(string userRequest)
        {
            Console.WriteLine($"\n--- Processing Request: '{userRequest}' ---");

            ITool selectedTool = null;
            string command = "";

            // Simple parsing logic (Decision Making)
            if (userRequest.Contains("light"))
            {
                if (userRequest.Contains("on")) command = "Turn On";
                else if (userRequest.Contains("off")) command = "Turn Off";

                // Find the tool by iterating through the collection
                selectedTool = FindTool("Light Bulb");
            }
            else if (userRequest.Contains("temperature") || userRequest.Contains("thermostat"))
            {
                if (userRequest.Contains("75")) command = "Set Temp: 75";
                else if (userRequest.Contains("68")) command = "Set Temp: 68";

                selectedTool = FindTool("Thermostat");
            }

            // Execute the tool if found
            if (selectedTool != null && command != "")
            {
                Console.WriteLine($"Agent selecting tool: {selectedTool.Name}");
                selectedTool.Execute(command);
            }
            else
            {
                Console.WriteLine("Agent could not determine how to handle the request.");
            }
        }

        // Helper method to locate a tool in the ArrayList.
        // Since ArrayList stores 'object', we must cast to ITool.
        private ITool FindTool(string toolName)
        {
            // Manual iteration is required because LINQ is forbidden.
            for (int i = 0; i < _tools.Count; i++)
            {
                object item = _tools[i];
                // Explicit cast to the interface
                ITool tool = (ITool)item;

                if (tool.Name == toolName)
                {
                    return tool;
                }
            }
            return null;
        }
    }

    // 4. USAGE EXAMPLE
    class Program
    {
        static void Main(string[] args)
        {
            // Instantiate the Agent
            HomeAssistantAgent agent = new HomeAssistantAgent();

            // Instantiate Tools
            ITool light = new LightBulb();
            ITool temp = new Thermostat();

            // Compose the Agent with Tools (Active Composition)
            agent.AddTool(light);
            agent.AddTool(temp);

            // Simulate User Requests
            agent.ProcessRequest("Can you turn the light on?");
            agent.ProcessRequest("Set the temperature to 75 degrees.");
            agent.ProcessRequest("Turn off the light");

            // Attempting a request with an unregistered tool or command
            agent.ProcessRequest("Play music"); 
        }
    }
}

Step-by-Step Explanation

  1. Defining the Abstraction (ITool Interface): We start by defining an interface named ITool. In object-oriented architecture, interfaces are contracts. They define what a class can do without dictating how it does it. Here, ITool requires two members: a Name property (to identify the tool) and an Execute method (to perform an action). This allows the Agent to interact with any tool blindly, relying on polymorphism.

  2. Implementing Concrete Tools (LightBulb and Thermostat): We create two classes that implement ITool.

    • Encapsulation: Both classes contain private state (_isOn and _temperature) that cannot be accessed directly from outside the class. This protects the data integrity of the devices.
    • Logic: The Execute method contains the specific logic for that device. The LightBulb simply toggles a boolean, while the Thermostat parses a string to update an integer.
    • Passive Nature: Notice that these classes have no logic to decide when to run. They simply wait for the Execute method to be called.
  3. The Agent Class (HomeAssistantAgent): The HomeAssistantAgent represents the active controller.

    • Composition: It maintains a collection of tools. Because Generics are forbidden in this section, we use the non-generic ArrayList. This collection allows the agent to hold a dynamic list of objects that implement ITool.
    • Dependency Injection: The AddTool(ITool tool) method allows us to inject dependencies into the agent at runtime. This is crucial for scalability; we can add new tools without changing the agent's internal code.
    • Decision Logic: The ProcessRequest method contains the "brain" of the agent. It parses the string input to determine intent (e.g., looking for keywords like "light" or "temperature"). Based on this intent, it selects the appropriate tool.
  4. Type Casting and Safety: In the FindTool method, we iterate through the ArrayList. Since ArrayList stores items as object, we must explicitly cast the retrieved item back to ITool ((ITool)item).

    • Architectural Implication: This casting is a necessary step in older C# styles or when avoiding Generics. It introduces a runtime risk: if the object in the list cannot be cast to ITool, an InvalidCastException will occur. In a production system, we would add error handling here, but for this example, we assume the agent is composed correctly.
  5. Execution Flow: In the Main method, we see the lifecycle:

    1. Agent creation.
    2. Tool instantiation and injection.
    3. Request processing. The Agent receives a request, consults its internal logic, selects the tool from its collection, and delegates the action. The tool executes the action and reports back to the console.

Visualizing the Architecture

The following diagram illustrates the relationship. The Agent holds a collection of Tools (Composition), while the Tools implement a common Interface.

In this architecture, the Agent composes a collection of Tools that implement a common Interface, allowing the Agent to interact with various Tools polymorphically.
Hold "Ctrl" to enable pan & zoom

In this architecture, the Agent composes a collection of Tools that implement a common Interface, allowing the Agent to interact with various Tools polymorphically.

The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon



Code License: All code examples are released under the MIT License. Github repo.

Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.

All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.