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.
Implement run_agent(task, script, max_steps=5).
llm = make_llm(script) (provided). It's a scripted stand-in for a real model.messages as [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task}].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"}dispatch(call), and append {"role": "tool", "content": result}.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.
run_agent(task: str, script: list[str], max_steps: int = 5) -> dict