Standard library only · runs in your browser
Put it all together: one loop that handles single and parallel tool calls, recovers from errors, detects loops, trims its context, enforces a real cost budget, and leaves a trace you could debug at 3am. This is the core of a real agent runtime, written by you.
Implement parse_reply(text) and run_agent_v2(task, script, config).
parse_reply extends stage 1. Return None if the text has no "tool substring. Extract the JSON the same way as stage 1. If it has a tool_calls list, each item is a call; otherwise the object itself is one call. Return a list of {"id", "tool", "arguments"} (id may be None). Raise ValueError on bad JSON or bad shape.
run_agent_v2. config keys, all optional: max_steps (8), max_consecutive_errors (3), max_tokens (None), context_limit (None). Each step:
context = trim_messages(messages, context_limit) if a limit is set, else messages. Its token cost is the sum of count_tokens over context. If max_tokens is set and the tokens already spent plus this cost would exceed it, return "token_budget" with steps = step - 1. Otherwise add the cost to the tokens spent, which accumulate across calls, and call llm(context).{"step", "action": "parse_error"} to trace.{"step", "action": "final_answer"}, then return.json.dumps([[tool, arguments], ...], sort_keys=True). If it's the same as the last two signatures, trace "loop_detected" and return. Otherwise run execute_tool_calls(calls), append every result message, add each tool name to tools_called, count the failed results as errors, and trace {"step", "action": "tools", "tools": [...], "failed": n}."too_many_errors" when they reach the limit.Return {"answer", "steps", "stop_reason", "tools_called", "errors", "trace"}.
run_agent_v2(task: str, script: list[str], config: dict) -> dict