Skip to content
echogeco
The harness, in code · Build it

Tool-dispatch loop against a stub model

Implement the agent loop so an unknown or failing tool becomes a result the model reads, and a turn cap ends the run with a status and the partial log.

codemediumExercise · 3 min · 69 of 77
On this page

The loop is twenty lines, and most production agents that fail badly fail in those twenty lines. A scripted stub stands in for the model, so every way the loop can go wrong is a test case rather than an incident.

The situation

A team's agent loop crashed in production when the model called a renamed tool, and separately ran for 400 turns on a task with no exit. The rename was get_order to fetch_order in Tuesday's deploy; an example in the system prompt still used the old name, and for two hours 6% of runs ended in a KeyError with nothing in the trace. The 400-turn run is the one the loop that ran for 400 turns takes apart; successful runs take 3 to 7 turns.

The loop wanted can do neither: unknown tools go back to the model as error results, and a turn cap ends the run with a status the trace can read. The team wants it proved before it ships, with no model in the test: the stub is a list of replies the loop plays in order, and each tool is a plain callable.

The question

Implement the loop against a scripted stub model.

def solve(script, tools, max_turns):
    """script: stub replies in order, ("call", name, args) or ("final", text).
    tools: name -> callable(**args). Returns (status, answer, log), where
    status is "done" or "budget" and log is [(name, result), ...]."""
TOOLS = {"add": lambda a, b: a + b, "upper": lambda s: s.upper()}

TESTS = [
    (([("call", "add", {"a": 2, "b": 3}), ("final", "5")], TOOLS, 5),
     ("done", "5", [("add", 5)])),
    (([("call", "nope", {}), ("final", "gave up")], TOOLS, 5),
     ("done", "gave up", [("nope", "error: unknown tool nope")])),
    (([("call", "upper", {"s": "a"})] * 3, TOOLS, 2),
     ("budget", None, [("upper", "A"), ("upper", "A")])),
]

A tool that raises is logged the same way, as its exception type and message:

def lookup(order_id):
    raise TimeoutError("orders service timed out after 5 s")

solve([("call", "lookup", {"order_id": "A-17"}), ("final", "orders is down")], dict(TOOLS, lookup=lookup), 5)
    -> ("done", "orders is down", [("lookup", "error: TimeoutError: orders service timed out after 5 s")])

A strong answer · Rubric · Where answers go wrong · Solution

Write your answer first, then open