Agent loop recipe
The canonical tool-using loop that powers every Meridian agent.
The loop
Repeat until the agent emits a final answer or hits the iteration cap.
1. Think
The model receives the conversation history plus available tool definitions. It decides whether to respond directly or invoke a tool. If a tool is needed, it outputs a structured tool-call block with the function name and arguments.
2. Tool call
Your runtime intercepts the tool-call block, validates arguments against the schema, and executes the corresponding function. Timeouts are enforced — if a tool hangs, the runtime kills it and returns an error result.
3. Tool result
The return value is serialized and appended to the conversation as a tool-result message. The model sees this on the next turn and incorporates it into its reasoning.
4. Repeat
Control loops back to step 1. The agent may chain multiple tools before settling on a final answer. Set a hard limit (e.g. 10 iterations) to prevent infinite loops.
Python sample
A minimal implementation with two tools: get_weather and calculate.
import json, math
from typing import Any
MAX_ITERATIONS = 10
TOOLS = {
"get_weather": {
"description": "Get current weather for a city",
"parameters": {"city": "string"},
},
"calculate": {
"description": "Evaluate a math expression",
"parameters": {"expression": "string"},
},
}
def get_weather(city: str) -> dict[str, Any]:
# Stub — replace with real API call
return {"city": city, "temp_c": 22, "condition": "sunny"}
def calculate(expression: str) -> dict[str, Any]:
allowed = set("0123456789+-*/().%^ eEpi")
if not all(c in allowed for c in expression):
return {"error": "Disallowed characters in expression"}
try:
result = eval(expression, {"__builtins__": {}}, {"pi": math.pi, "e": math.e})
return {"expression": expression, "result": result}
except Exception as e:
return {"error": str(e)}
def run_tool(name: str, args: dict[str, Any]) -> dict[str, Any]:
if name == "get_weather":
return get_weather(args["city"])
if name == "calculate":
return calculate(args["expression"])
return {"error": f"Unknown tool: {name}"}
def agent_loop(messages: list[dict], model_call) -> str:
for _ in range(MAX_ITERATIONS):
response = model_call(messages, TOOLS)
if response.get("role") == "assistant" and "content" in response:
return response["content"] # final answer
tool_call = response.get("tool_call")
if not tool_call:
return "Agent produced no tool call or answer."
name = tool_call["name"]
args = tool_call.get("arguments", {})
result = run_tool(name, args)
messages.append({"role": "tool", "name": name, "content": json.dumps(result)})
return "Agent exceeded maximum iterations."Rules of thumb
- ▸Always validate tool arguments before execution. Never pass raw model output to
evalor shell. - ▸Enforce a per-tool timeout (e.g. 30s). Network calls can hang.
- ▸Cap total iterations. A confused model can loop forever.
- ▸Log every tool invocation with arguments and latency. Debugging blind loops is painful.
- ▸Return structured errors as tool results — let the model self-correct rather than crashing the loop.