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.
-
Internal Method Calls: When an agent uses a tool that is part of the same process (like the
CalculatorToolabove), 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). -
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:
- 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).
- Tool Execution: The tool processes the input.
- Tool Output: The tool returns a result (text, a number, or a new tensor).
- 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."
- Tool 1 (Document Reader): Takes a file path, returns a tensor embedding of the text.
- Tool 2 (Summarizer): Takes the embedding tensor, returns a condensed text string.
- 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.
Summary of Architectural Implications
-
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).
-
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.
-
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:
- Active Composition (The Agent): A
HomeAssistantAgentthat possesses a collection of tools. It parses a request and dynamically selects the appropriate tool to execute. - Passive Composition (The Tool): A
Thermostatclass 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
-
Defining the Abstraction (
IToolInterface): We start by defining an interface namedITool. In object-oriented architecture, interfaces are contracts. They define what a class can do without dictating how it does it. Here,IToolrequires two members: aNameproperty (to identify the tool) and anExecutemethod (to perform an action). This allows the Agent to interact with any tool blindly, relying on polymorphism. -
Implementing Concrete Tools (
LightBulbandThermostat): We create two classes that implementITool.- Encapsulation: Both classes contain private state (
_isOnand_temperature) that cannot be accessed directly from outside the class. This protects the data integrity of the devices. - Logic: The
Executemethod contains the specific logic for that device. TheLightBulbsimply toggles a boolean, while theThermostatparses 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
Executemethod to be called.
- Encapsulation: Both classes contain private state (
-
The Agent Class (
HomeAssistantAgent): TheHomeAssistantAgentrepresents 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 implementITool. - 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
ProcessRequestmethod 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.
- Composition: It maintains a collection of tools. Because Generics are forbidden in this section, we use the non-generic
-
Type Casting and Safety: In the
FindToolmethod, we iterate through theArrayList. SinceArrayListstores items asobject, we must explicitly cast the retrieved item back toITool((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, anInvalidCastExceptionwill occur. In a production system, we would add error handling here, but for this example, we assume the agent is composed correctly.
- 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
-
Execution Flow: In the
Mainmethod, we see the lifecycle:- Agent creation.
- Tool instantiation and injection.
- 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.
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.