Introduction
Welcome back! In our journey through Harness Engineering, we’ve already laid crucial groundwork. We’ve learned how to design systematic environments to ensure consistent agent execution and how to implement robust state management to maintain context and continuity across interactions. These are foundational for any reliable AI agent.
But here’s a critical question: once an agent has a clear environment and knows its current state, how do we ensure it takes the right actions? How do we prevent it from going off-script, misusing tools, or simply “hallucinating” an action that doesn’t make sense in its current context?
This is where Agent Control Systems come into play. In this chapter, we’ll dive deep into the mechanisms you can build to guide your agent’s behavior, manage its access to tools, and enforce desired operational boundaries. By the end, you’ll understand how to blend explicit rules with intelligent prompting to create agents that are not just capable, but also predictable and trustworthy.
Core Concepts: The Agent’s Guiding Hand
Imagine your AI agent isn’t just a brain, but a sophisticated robot with various tools at its disposal. A control system is like the robot’s operating manual and its internal safety protocols, ensuring it uses its tools correctly and stays focused on its mission.
What are Agent Control Systems?
Agent Control Systems are the frameworks and mechanisms you implement to steer an agent’s behavior, manage its decision-making process, and orchestrate its interaction with external tools and environments. They are vital for moving beyond basic prompt-response models to truly reliable, production-grade agentic systems.
Why do we need them? While Large Language Models (LLMs) are incredibly powerful, they are fundamentally predictive text generators. Without guardrails, they can be prone to:
- Drift: Slowly moving away from the intended goal.
- Hallucination: Inventing non-existent tools or actions.
- Misinterpretation: Using a tool incorrectly or at the wrong time.
- Security Risks: Accessing sensitive resources without proper authorization.
Control systems solve these problems by injecting structure and constraints, ensuring the agent adheres to its purpose, even in complex or ambiguous situations.
Types of Control
Effective control systems often combine different strategies, balancing rigidity with flexibility.
Explicit Control (Structured Guidance)
Explicit control involves defining clear, hardcoded rules, policies, or state machines that dictate an agent’s permissible actions. This is about setting non-negotiable boundaries.
- What it is: Code-based logic, whitelists, blacklists, input validation schemas, execution limits, and predefined workflows.
- Why it’s important: Provides strong guarantees for safety, security, and adherence to critical business logic. It’s ideal for scenarios where agent actions must be precise and predictable.
- When to use it: For critical paths, security-sensitive operations (like file writes, API calls), resource management (e.g., maximum retries), or enforcing compliance.
Prompt-Based Control (Soft Guidance)
Prompt-based control leverages the LLM’s natural language understanding to guide its behavior through carefully crafted instructions, examples, and contextual information within the prompt itself.
- What it is: System messages, few-shot examples, negative constraints (e.g., “do NOT use tool X”), role-playing instructions, and explicit goal definitions.
- Why it’s important: Offers flexibility and adaptability. It allows the agent to reason about situations and make choices within a defined scope, without requiring every permutation to be hardcoded.
- When to use it: For flexible tasks, creative problem-solving, adapting to novel situations, or guiding the agent’s internal thought process (e.g., “Think step-by-step”).
Tool Orchestration
Tools are the agent’s primary means of interacting with the world—reading files, calling APIs, running code, or interacting with a version control system. Tool orchestration is a specialized form of control focused on managing these interactions.
- What it is: Defining which tools an agent can access, providing clear descriptions and input schemas for each tool, and implementing mechanisms to call these tools safely and effectively.
- Why it’s important: Prevents tool misuse, ensures correct parameter passing, and protects external systems from unintended agent actions. Tools are powerful; their use must be controlled.
- Concepts:
- Tool Registry: A central place to define and store available tools.
- Tool Descriptors: Detailed descriptions (often in JSON Schema) of what a tool does, its parameters, and expected outputs. These are often included in the agent’s prompt.
- Execution Wrappers: Functions that validate inputs, handle errors, and execute the actual tool logic in a controlled environment.
The Agent Control Loop Pattern
Most agentic systems operate within a continuous loop. Control systems are integrated into various stages of this loop to ensure guidance is applied throughout the agent’s operation.
Here’s a simplified view of a common control loop:
In this loop:
- Observe: The agent gathers information from its environment and internal state.
- Plan: Based on observations and its goal, the agent formulates a plan, which might include calling a specific tool.
- Control System Checks: Before any action is taken, the control system intercepts the proposed action. It verifies if the action is allowed, if the tool exists, if parameters are valid, and if any other explicit policies are met.
- Act: If the action passes control checks, it’s executed safely.
- Reflect: The agent reviews the outcome of its action, updates its state, and potentially adjusts its plan. If the action was blocked by the control system, reflection helps the agent understand why and formulate an alternative.
Step-by-Step Implementation: Building a Basic Tool Control System
Let’s build a simple, explicit control system for an AI coding agent that can interact with files. We’ll focus on managing its access to read_file and write_file tools.
We’ll use Python for our examples, as it’s the de facto standard for AI agent development. Make sure you have a Python 3.9+ environment set up.
Step 1: Define Agent Tools
First, let’s define the core functions our agent can use. These are simple Python functions that simulate file operations. In a real-world scenario, these would interact with a sandboxed file system.
Create a file named agent_tools.py:
# agent_tools.py
import os
def read_file(file_path: str) -> str:
"""
Reads the content of a specified file.
Args:
file_path (str): The path to the file to read.
Returns:
str: The content of the file, or an error message if not found.
"""
try:
if not os.path.exists(file_path):
return f"Error: File not found at {file_path}"
with open(file_path, 'r') as f:
content = f.read()
return f"File '{file_path}' content:\n{content}"
except Exception as e:
return f"Error reading file '{file_path}': {str(e)}"
def write_file(file_path: str, content: str) -> str:
"""
Writes content to a specified file. Overwrites if the file exists.
Args:
file_path (str): The path to the file to write.
content (str): The content to write into the file.
Returns:
str: A success or error message.
"""
try:
with open(file_path, 'w') as f:
f.write(content)
return f"Successfully wrote to file '{file_path}'."
except Exception as e:
return f"Error writing to file '{file_path}': {str(e)}"
# In a real system, you'd also provide a schema for the LLM to understand these tools.
# For example, using Pydantic or a simple dict:
TOOL_METADATA = {
"read_file": {
"name": "read_file",
"description": "Reads the content of a specified file.",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "The path to the file."}
},
"required": ["file_path"]
}
},
"write_file": {
"name": "write_file",
"description": "Writes content to a specified file, overwriting if it exists.",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "The path to the file."},
"content": {"type": "string", "description": "The content to write."}
},
"required": ["file_path", "content"]
}
}
}Explanation:
- We define
read_fileandwrite_fileas standard Python functions. - They include basic error handling.
TOOL_METADATAprovides structured descriptions of each tool, including their names, descriptions, and expected parameters (using a simplified JSON Schema-like format). An LLM would use this metadata to understand how to call the tools.
Step 2: Implement a Tool Registry
The tool registry is our first explicit control mechanism. It acts as a whitelist of available functions that the agent can potentially call.
Create a new file agent_harness.py and add the following:
# agent_harness.py
import json
from typing import Callable, Dict, Any, List
from agent_tools import read_file, write_file, TOOL_METADATA
class AgentToolRegistry:
def __init__(self, tools: Dict[str, Callable]):
self._tools = tools
self._tool_metadata = TOOL_METADATA # In a larger system, this would be dynamic
def get_tool(self, tool_name: str) -> Callable | None:
"""Retrieves a tool function by its name."""
return self._tools.get(tool_name)
def get_tool_description(self, tool_name: str) -> Dict[str, Any] | None:
"""Retrieves the metadata description for a tool."""
return self._tool_metadata.get(tool_name)
# Initialize our registry with the defined tools
tool_registry = AgentToolRegistry({
"read_file": read_file,
"write_file": write_file,
})Explanation:
AgentToolRegistryholds a mapping of tool names (strings) to their corresponding Python functions.- It also stores the
TOOL_METADATAso the agent can understand how to describe tools to an LLM. - This registry provides a controlled way to access tool functions. If a tool isn’t in
_tools, the agent simply cannot retrieve it.
Step 3: Agent’s Decision Logic (Simplified)
In a real agent, an LLM would generate a response that includes a tool call. For this example, we’ll simulate that output as a JSON string.
Still in agent_harness.py, let’s add a function to simulate an LLM’s tool call output and a parser for it:
# agent_harness.py (continued)
# ... (previous code) ...
def simulate_llm_tool_call(prompt: str) -> str:
"""
Simulates an LLM's output containing a tool call in JSON format.
In a real system, this would be an actual LLM API call.
"""
if "read about" in prompt:
return json.dumps({
"tool_name": "read_file",
"parameters": {"file_path": "example.txt"}
})
elif "write a note" in prompt:
return json.dumps({
"tool_name": "write_file",
"parameters": {"file_path": "my_note.txt", "content": "Hello from the agent!"}
})
elif "delete file" in prompt: # This tool doesn't exist
return json.dumps({
"tool_name": "delete_file",
"parameters": {"file_path": "important.txt"}
})
else:
return json.dumps({"response": "I'm not sure how to help with that."})
def parse_llm_output(llm_output: str) -> Dict[str, Any] | None:
"""
Parses the LLM's output to extract tool call information.
"""
try:
data = json.loads(llm_output)
if "tool_name" in data and "parameters" in data:
return data
elif "response" in data:
print(f"Agent response: {data['response']}")
return None # Not a tool call
else:
print(f"Warning: Unexpected LLM output format: {llm_output}")
return None
except json.JSONDecodeError:
print(f"Error: LLM output is not valid JSON: {llm_output}")
return NoneExplanation:
simulate_llm_tool_callstands in for an actual LLM. It generates JSON strings that look like tool calls based on simple prompt keywords. Notice it can also generate a call fordelete_file, which is not a tool we defined.parse_llm_outputattempts to parse this JSON and extract thetool_nameandparameters.
Step 4: Controlled Tool Execution
This is the core of our explicit control system. We’ll create a function that takes the parsed LLM output and safely executes the requested tool.
Add execute_tool_safely to agent_harness.py:
# agent_harness.py (continued)
# ... (previous code) ...
def execute_tool_safely(tool_call_data: Dict[str, Any]) -> str:
"""
Executes a tool call only if it's registered and parameters are valid.
This is our control system's execution gate.
"""
tool_name = tool_call_data.get("tool_name")
parameters = tool_call_data.get("parameters", {})
# 1. Check if the tool is registered (Explicit Control)
tool_func = tool_registry.get_tool(tool_name)
if not tool_func:
return f"Error: Tool '{tool_name}' is not registered and cannot be executed."
# 2. Basic parameter validation (Explicit Control)
tool_meta = tool_registry.get_tool_description(tool_name)
if tool_meta and "parameters" in tool_meta:
required_params = tool_meta["parameters"].get("required", [])
for req_param in required_params:
if req_param not in parameters:
return f"Error: Tool '{tool_name}' requires parameter '{req_param}', but it was not provided."
# More advanced validation (e.g., type checking) would go here
print(f"Executing tool '{tool_name}' with parameters: {parameters}")
try:
result = tool_func(**parameters)
return result
except TypeError as e:
return f"Error: Invalid parameters for tool '{tool_name}': {e}. Check tool definition."
except Exception as e:
return f"Error during tool execution '{tool_name}': {str(e)}"Explanation:
execute_tool_safelyacts as a gatekeeper.- Registration Check: It first queries
tool_registryto see iftool_nameis even recognized. This is a fundamental explicit control. - Parameter Validation: It then performs a basic check for required parameters based on the
TOOL_METADATA. This prevents the agent from calling a function with missing arguments, which could lead to runtime errors. - Execution: Only if both checks pass, the tool function is called with the provided parameters. Error handling is included for robustness.
Let’s test it out! Add a main execution block to agent_harness.py:
# agent_harness.py (continued)
# ... (previous code including execute_tool_safely) ...
if __name__ == "__main__":
# Create a dummy file for reading
with open("example.txt", "w") as f:
f.write("This is some example content for the agent to read.")
print("--- Test Case 1: Valid Read File ---")
llm_output_read = simulate_llm_tool_call("Please read about the example file.")
parsed_read = parse_llm_output(llm_output_read)
if parsed_read:
print(execute_tool_safely(parsed_read))
print("-" * 30)
print("--- Test Case 2: Valid Write File ---")
llm_output_write = simulate_llm_tool_call("Please write a note for me.")
parsed_write = parse_llm_output(llm_output_write)
if parsed_write:
print(execute_tool_safely(parsed_write))
print("-" * 30)
print("--- Test Case 3: Non-existent Tool ---")
llm_output_delete = simulate_llm_tool_call("I want to delete file important.txt.")
parsed_delete = parse_llm_output(llm_delete)
if parsed_delete:
print(execute_tool_safely(parsed_delete))
print("-" * 30)
print("--- Test Case 4: Missing Required Parameter (Simulated) ---")
# Manually create a malformed tool call for demonstration
malformed_call = {
"tool_name": "write_file",
"parameters": {"file_path": "incomplete.txt"} # Missing 'content'
}
print(execute_tool_safely(malformed_call))
print("-" * 30)
print("--- Test Case 5: Agent response (not a tool call) ---")
llm_output_response = simulate_llm_tool_call("Tell me a joke.")
parse_llm_output(llm_output_response) # This will print the agent response directly
print("-" * 30)
# Clean up dummy files
if os.path.exists("example.txt"):
os.remove("example.txt")
if os.path.exists("my_note.txt"):
os.remove("my_note.txt")Run this script from your terminal:
python agent_harness.pyYou should see output similar to this:
--- Test Case 1: Valid Read File ---
Executing tool 'read_file' with parameters: {'file_path': 'example.txt'}
File 'example.txt' content:
This is some example content for the agent to read.
------------------------------
--- Test Case 2: Valid Write File ---
Executing tool 'write_file' with parameters: {'file_path': 'my_note.txt', 'content': 'Hello from the agent!'}
Successfully wrote to file 'my_note.txt'.
------------------------------
--- Test Case 3: Non-existent Tool ---
Error: Tool 'delete_file' is not registered and cannot be executed.
------------------------------
--- Test Case 4: Missing Required Parameter (Simulated) ---
Error: Tool 'write_file' requires parameter 'content', but it was not provided.
------------------------------
--- Test Case 5: Agent response (not a tool call) ---
Agent response: I'm not sure how to help with that.
------------------------------Notice how delete_file was blocked because it wasn’t in our tool_registry, and the write_file call with missing content was also caught by our validation. This demonstrates the power of explicit control!
Step 5: Adding a Whitelist (Explicit Policy)
What if we want to dynamically restrict which registered tools an agent can use in a specific context? Let’s add an explicit whitelist to execute_tool_safely.
Modify agent_harness.py to add an allowed_tools parameter to execute_tool_safely:
# agent_harness.py (modified)
# ... (previous imports and AgentToolRegistry class) ...
def execute_tool_safely(tool_call_data: Dict[str, Any], allowed_tools: List[str] | None = None) -> str:
"""
Executes a tool call only if it's registered, in the allowed_tools list,
and parameters are valid.
"""
tool_name = tool_call_data.get("tool_name")
parameters = tool_call_data.get("parameters", {})
# 1. Check if the tool is in the explicit whitelist (NEW Explicit Control)
if allowed_tools is not None and tool_name not in allowed_tools:
return f"Error: Tool '{tool_name}' is not allowed in this context."
# 2. Check if the tool is registered
tool_func = tool_registry.get_tool(tool_name)
if not tool_func:
return f"Error: Tool '{tool_name}' is not registered and cannot be executed."
# 3. Basic parameter validation
tool_meta = tool_registry.get_tool_description(tool_name)
if tool_meta and "parameters" in tool_meta:
required_params = tool_meta["parameters"].get("required", [])
for req_param in required_params:
if req_param not in parameters:
return f"Error: Tool '{tool_name}' requires parameter '{req_param}', but it was not provided."
print(f"Executing tool '{tool_name}' with parameters: {parameters}")
try:
result = tool_func(**parameters)
return result
except TypeError as e:
return f"Error: Invalid parameters for tool '{tool_name}': {e}. Check tool definition."
except Exception as e:
return f"Error during tool execution '{tool_name}': {str(e)}"
# ... (rest of the file, update the __main__ block) ...
if __name__ == "__main__":
# ... (previous setup and test cases) ...
print("--- Test Case 6: Whitelisted Tool (read_file only) ---")
# In this context, only 'read_file' is allowed.
llm_output_read_again = simulate_llm_tool_call("Please read about the example file.")
parsed_read_again = parse_llm_output(llm_output_read_again)
if parsed_read_again:
print(execute_tool_safely(parsed_read_again, allowed_tools=["read_file"]))
print("-" * 30)
print("--- Test Case 7: Whitelisted Tool (write_file blocked) ---")
# Even though write_file is registered, it's not in the allowed_tools for this context.
llm_output_write_blocked = simulate_llm_tool_call("Please write a note for me.")
parsed_write_blocked = parse_llm_output(llm_output_write_blocked)
if parsed_write_blocked:
print(execute_tool_safely(parsed_write_blocked, allowed_tools=["read_file"]))
print("-" * 30)
# Clean up dummy files
if os.path.exists("example.txt"):
os.remove("example.txt")
if os.path.exists("my_note.txt"):
os.remove("my_note.txt")Run python agent_harness.py again.
You should now see the new test cases:
... (previous output) ...
--- Test Case 6: Whitelisted Tool (read_file only) ---
Executing tool 'read_file' with parameters: {'file_path': 'example.txt'}
File 'example.txt' content:
This is some example content for the agent to read.
------------------------------
--- Test Case 7: Whitelisted Tool (write_file blocked) ---
Error: Tool 'write_file' is not allowed in this context.
------------------------------Explanation:
By adding the allowed_tools parameter, we’ve introduced another layer of explicit control. This allows us to define context-specific permissions for tools. An agent might have access to a broad set of tools, but a control system can restrict that access based on the current task, user permissions, or security policies.
Mini-Challenge: Enhancing Tool Control
You’ve successfully implemented basic tool registration, parameter validation, and a dynamic whitelist. Now, let’s add another common explicit control.
Challenge: Implement a max_execution_time constraint for any tool. Modify the execute_tool_safely function so that if a tool call takes longer than a specified duration (e.g., 5 seconds), it is interrupted and an error message is returned.
Hint: Python’s threading module can be used with a Thread and a join with a timeout argument, or for a simpler illustrative example, you could simulate a delay within a tool and check time.time() before and after the call. For a production system, consider libraries like joblib.Parallel or concurrent.futures for more robust timeouts.
What to observe/learn: How to add temporal constraints to agent actions, preventing long-running or unresponsive tool calls from blocking the agent’s progress or consuming excessive resources. This is crucial for managing agent performance and reliability in real-time systems.
Common Pitfalls & Troubleshooting
Building robust control systems is an art of balancing guidance and autonomy. Here are some common issues:
Over-constraining the Agent:
- What can go wrong: Too much explicit control, rigid rules, or overly strict whitelists can stifle the agent’s ability to reason, adapt, and solve novel problems. It becomes a glorified script rather than an intelligent agent.
- Troubleshooting: Start with soft, prompt-based guidance. Only introduce explicit controls for critical paths, safety, or non-negotiable business rules. Review your explicit rules regularly to ensure they are still necessary and not overly restrictive.
Ambiguous Prompt-Based Control:
- What can go wrong: Vague instructions, conflicting directives, or insufficient examples in the prompt can lead to unpredictable agent behavior, misinterpretations, or ignoring your guidance entirely.
- Troubleshooting: Be precise, concise, and explicit in your prompts. Use few-shot examples to demonstrate desired behavior. Leverage negative constraints (e.g., “Do NOT access the database directly, use the
query_datatool instead”). Test your prompts thoroughly across various scenarios.
Ignoring Tool Input Validation:
- What can go wrong: Even if an agent calls a valid tool, it might provide invalid parameters (e.g., a non-existent file path, incorrect data type, or malicious input). This can lead to runtime errors, unexpected behavior, or security vulnerabilities in the underlying tools.
- Troubleshooting: Always validate tool inputs against expected schemas (like we did with
TOOL_METADATA). Use libraries like Pydantic for robust schema validation. Ensure the tool itself handles invalid inputs gracefully.
Lack of Observability into Control Decisions:
- What can go wrong: If an agent behaves unexpectedly, and you don’t know why a particular tool was chosen (or blocked), debugging becomes incredibly difficult. You lose insight into the control system’s effectiveness.
- Troubleshooting: Log all control decisions. Record when a tool call is proposed, if it passes validation, if it’s blocked by a whitelist, and the outcome of its execution. This creates a traceable audit trail for understanding agent behavior.
Summary
In this chapter, we’ve explored the crucial role of Agent Control Systems in building reliable and predictable AI coding agents. We’ve learned that:
- Control systems are essential for guiding agent behavior, preventing drift, and ensuring safe tool usage.
- They blend explicit control (hardcoded rules, registries, validation) for reliability and safety with prompt-based control (careful instructions, examples) for flexibility and adaptability.
- Tool orchestration is a key component, managing what tools an agent can access, and ensuring their correct and secure execution.
- Implementing explicit checks like tool registration and parameter validation are fundamental steps toward building trustworthy agentic systems.
By systematically applying these control mechanisms, you move closer to agents that are not only intelligent but also dependable and aligned with your operational goals.
In our next chapter, we’ll delve into Observability for Agentic Systems. We’ll learn how to monitor, log, and analyze agent behavior to understand what they’re doing, why they’re doing it, and how to debug issues when they arise.
References
- Modern Agent Harness Blueprint 2026 - GitHub Gist
- RasaHQ/why-agents-fail: A self-paced course on harness engineering
- muratcankoylan/Agent-Skills-for-Context-Engineering - GitHub
- ai-boost/awesome-harness-engineering - GitHub
- Python
jsonmodule documentation - Python
osmodule documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.