Neural Mastery
← Practice
The Agent Loopmedium

The Agent Loop

medium Build an Agent 15–20 min

Standard library only · runs in your browser

This is the loop behind ReAct, Claude Code and every 'agent' framework: ask the model, run the tool it picks, append the result, ask again, and stop when it answers. It's only about 15 lines, and the details are what separate a working agent from one that answers with stale or invented data.

Task

Implement run_agent(task, script, max_steps=5).

  1. Create the model with llm = make_llm(script) (provided). It's a scripted stand-in for a real model.
  2. Start messages as [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task}].
  3. Each step: reply = llm(messages), then append {"role": "assistant", "content": reply}.
    • parse_tool_call(reply) is None: return {"answer": reply, "steps": step, "tools_called": [...], "stop_reason": "final_answer"}
    • Otherwise record the tool name, run dispatch(call), and append {"role": "tool", "content": result}.
  4. If max_steps model calls go by without a final answer, return {"answer": None, "steps": max_steps, "tools_called": [...], "stop_reason": "max_steps"}.

Steps count from 1. In this stage you can assume every tool call parses.

Function Signature

run_agent(task: str, script: list[str], max_steps: int = 5) -> dict

Examples

Example 1: Model answers without tools
Input: task = 'What is the capital of France?' script = ['Paris is the capital of France.']
Output: {'answer': 'Paris is the capital of France.', 'steps': 1, 'tools_called': [], 'stop_reason': 'final_answer'}
Example 2: One tool call, then an answer that uses its result
Input: task = 'Weather in Paris?' script = ['{"tool": "get_weather", "arguments": {"city": "Paris"}}', 'Right now Paris is {last_tool_result}.']
Output: {'answer': 'Right now Paris is 18C, light rain.', 'steps': 2, 'tools_called': ['get_weather'], 'stop_reason': 'final_answer'}
Example 3: Model never answers: stop at max_steps
Input: task = 'Weather?' script = ['{"tool": "get_weather", "arguments": {"city": "Paris"}}', '{"tool": "get_weather", "arguments": {"city": "Tokyo"}}', '{"tool": "get_weather", "arguments": {"city": "Lima"}}'] max_steps = 2
Output: {'answer': None, 'steps': 2, 'tools_called': ['get_weather', 'get_weather'], 'stop_reason': 'max_steps'}
Example 4: Tool errors are observations too
Input: task = 'Weather in Atlantis?' script = ['{"tool": "get_weather", "arguments": {"city": "Atlantis"}}', 'Sorry: {last_tool_result}']
Output: {'answer': 'Sorry: error: get_weather raised ValueError: no weather data for Atlantis', 'steps': 2, 'tools_called': ['get_weather'], 'stop_reason': 'final_answer'}

Constraints

  • Feed every tool result back as a `tool` message. The scripted model fills `{last_tool_result}` from what you actually sent it.
  • Call the model exactly once per step. It raises if you call it more often than the script allows.
  • `tools_called` lists tool names in call order, including calls that returned errors.
Python3Saved ✓
import json
import math
import re


def run_agent(task, script, max_steps=5):
"""Run the model/tool loop until a final answer or max_steps."""
llm = make_llm(script)
messages = [{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": task}]
tools_called = []
# Your loop here
pass


# =============================================================================
# PROVIDED BELOW -- the sandbox and your earlier stages. Read it, don't edit it.
# =============================================================================
# ---------------------------------------------------------------------------
# The sandbox world your agent lives in (provided -- you don't edit this).
# Four deterministic tools, each with a JSON-Schema-style argument spec,
# exactly the shape you'd hand a real model's tool-calling API.
# ---------------------------------------------------------------------------
WEATHER = {"paris": "18C, light rain", "tokyo": "24C, clear", "lima": "16C, overcast"}
DOCS = {
"refund-policy": "Refunds are accepted within 30 days with a receipt.",
"shipping": "Standard shipping takes 3-5 business days.",
}


def add(a, b):
return a + b


def divide(a, b):
return a / b
Case 1: Model answers without tools
Input: task = 'What is the capital of France?' script = ['Paris is the capital of France.']
Expected: {'answer': 'Paris is the capital of France.', 'steps': 1, 'tools_called': [], 'stop_reason': 'final_answer'}
Case 2: One tool call, then an answer that uses its result
Input: task = 'Weather in Paris?' script = ['{"tool": "get_weather", "arguments": {"city": "Paris"}}', 'Right now Paris is {last_tool_result}.']
Expected: {'answer': 'Right now Paris is 18C, light rain.', 'steps': 2, 'tools_called': ['get_weather'], 'stop_reason': 'final_answer'}
Case 3: Model never answers: stop at max_steps
Input: task = 'Weather?' script = ['{"tool": "get_weather", "arguments": {"city": "Paris"}}', '{"tool": "get_weather", "arguments": {"city": "Tokyo"}}', '{"tool": "get_weather", "arguments": {"city": "Lima"}}'] max_steps = 2
Expected: {'answer': None, 'steps': 2, 'tools_called': ['get_weather', 'get_weather'], 'stop_reason': 'max_steps'}
Case 4: Tool errors are observations too
Input: task = 'Weather in Atlantis?' script = ['{"tool": "get_weather", "arguments": {"city": "Atlantis"}}', 'Sorry: {last_tool_result}']
Expected: {'answer': 'Sorry: error: get_weather raised ValueError: no weather data for Atlantis', 'steps': 2, 'tools_called': ['get_weather'], 'stop_reason': 'final_answer'}