Handoff Boundaries: Agent vs Code

When Should Code Decide vs. the Agent

Not everything should be left to the agent. Sometimes code should decide.

Agent Decides

Let the agent decide when:

  • The decision depends on external information
  • Multiple valid paths are acceptable
  • The model should learn from context
  • The decision is complex or subtle

Example: Routing customer support

  • Customer asks: "I have a shipping question"
  • Agent should decide: Is this about a specific order or general shipping info?
  • Agent can understand the nuance

Code Decides

Let code decide when:

  • The rule is fixed and never changes
  • Speed is critical
  • Safety is critical
  • Cost matters
  • The decision is simple and deterministic

Example: Rate limiting

  • Rule: Each user gets 10 searches per hour
  • Code should enforce this, not the agent
  • Agent might try to bypass the limit

Examples

Good: Agent decides which tool

  • User: "Find information about renewable energy"
  • Agent can decide: search, then fetch, or search multiple sources, or check current date first
  • Flexible and smart

Good: Code decides whether to allow the action

# Code enforces safety
if user_role != "admin" and tool_name == "delete_database":
  return {"error": "Permission denied"}

result = tools[tool_name](**args)

Bad: Let agent decide everything

# Agent might ignore cost limits
while True:
  result = agent.step()
  # Agent keeps going even if cost is high

Good: Code enforces limits

if total_cost > max_cost:
  break  # Stop the agent

if iteration_count > max_iterations:
  break  # Stop the agent

Pattern: Guard Rails

Code proposes through the agent, code validates the action.

def guarded_agent_step():
  # Agent proposes an action
  tool_call = agent.propose_action()
  tool_name = tool_call["name"]
  tool_args = tool_call["args"]
  
  # Code validates the action
  if not is_allowed(tool_name, tool_args):
    return {"error": "Action not allowed"}
  
  if would_exceed_limits(tool_name, tool_args):
    return {"error": "Would exceed limits"}
  
  # Code executes the action
  return tools[tool_name](**tool_args)

def is_allowed(tool_name, args):
  # Allowlist: only certain tools allowed
  if tool_name not in ["search", "fetch", "summarize"]:
    return False
  
  # Safety check: no delete operations
  if "delete" in tool_name.lower():
    return False
  
  return True

def would_exceed_limits(tool_name, args):
  global total_cost
  global iteration_count
  
  if total_cost > max_cost:
    return True
  
  if iteration_count >= max_iterations:
    return True
  
  return False

Handoff Boundaries

Define clear boundaries. The agent owns some decisions, code owns others.

Agent responsibilities:
- Understand the user's intent
- Decide which tool to use
- Reason through multi-step tasks
- Synthesize information

Code responsibilities:
- Enforce cost limits
- Enforce iteration limits
- Validate tool inputs
- Check permissions
- Handle errors
- Ensure safety

Example: Complete Agent With Boundaries

class BoundedAgent:
  def __init__(self, max_iterations=15, max_cost=1.0):
    self.max_iterations = max_iterations
    self.max_cost = max_cost
    self.iteration_count = 0
    self.total_cost = 0.0
    self.allowed_tools = ["web_search", "fetch_webpage", "summarize"]
  
  def run(self, user_question):
    conversation = []
    
    while self.iteration_count < self.max_iterations:
      # Agent thinks
      response = model.think(conversation)
      
      # Agent proposes action
      if "<tool_use>" not in response:
        return response  # Final answer
      
      tool_call = parse_tool_call(response)
      tool_name = tool_call["name"]
      
      # CODE: Check if allowed
      if tool_name not in self.allowed_tools:
        error = {"error": f"Tool {tool_name} not allowed"}
        conversation.append({"role": "user", "content": str(error)})
        continue
      
      # CODE: Check cost
      cost = estimate_cost(tool_name)
      if self.total_cost + cost > self.max_cost:
        error = {"error": "Cost limit exceeded"}
        conversation.append({"role": "user", "content": str(error)})
        break
      
      # CODE: Execute
      result = tools[tool_name](**tool_call["args"])
      self.total_cost += cost
      self.iteration_count += 1
      
      # Agent observes
      conversation.append({"role": "assistant", "content": response})
      conversation.append({"role": "user", "content": str(result)})
    
    return "Max iterations reached"

Summary

  • Agent: Decides what to do based on context
  • Code: Enforces rules, limits, and safety

Don't let the agent make decisions that should be made by code. Don't have code make decisions that need agent reasoning.

Discussion

  • Loading…

← Back to Tutorials