Projects/Weather Tool Agent/Part 3
JSON Action Loop and Retries
Parse {tool, args} from the policy, execute tools, retry bad JSON, and stop on finish or budget.
The loop is the product. Tools without a loop are a SDK. A loop without a parse contract is a chatbot with side effects. This part wires JSON actions, a transcript, and retries around the registry from part 2.
The action contract
Every policy response must parse as:
{"tool": "<name>", "args": { ... }}
Rules:
- Top level is an object, not a list, not a string.
toolis a string.argsis an object (possibly empty). Missingargsis a parse error, not "no arguments".- Extra keys are ignored or rejected — pick one and test it. This project rejects extra keys so the model cannot smuggle
execute: true.
When the policy is a real LLM, it will wrap JSON in markdown fences, add "Sure!", or emit trailing commas. Your parser should (1) strip a ``json fence if present, (2) json.loads, (3) validate keys. If that fails, you do not call a tool. You append a role: system note: Parse error. Reply with JSON only.` and ask again. That is a parse retry, counted separately from tool steps so a confused model cannot spend the whole budget on prose.
Fake model that actually sequences
A good fake model is a state machine over the transcript:
- If there is no geocode observation yet, emit
geocodewith a city extracted from the goal. - If geocode returned
unknown_city, emitfinishwith acannot:answer. - If geocode succeeded and there is no weather observation, emit
weatherwith lat/lon from the last geocode result. - If weather failed, refuse.
- If weather succeeded,
finishwith a sentence.
City extraction can be dumb: last capitalized word, or a regex, or a tiny list of known names found as substrings. Dumb is fine. The loop is the lesson.
Run to execute this in your browser. Nothing is sent to a server.
Step-by-step: the loop you must be able to recite
- Seed the transcript with the user goal as the first event. Everything the policy knows comes from this list.
- Call the policy with the full transcript. In production this is an API call; here it is
fake_model. - Parse. Dicts from a fake model still go through
parse_actionso the real LLM path is identical. If parse fails, append a system observation and continue without incrementing a successful tool step — but do count it towardmax_parse_retriesandmax_stepsso you cannot infinite-loop. - Dispatch. Registry lookup. Call with
**args. Catch nothing from the tool if you already return error dicts; still guardTypeErrorif the model omits a key (kw["city"]vskw.get). - Append a tool event with name, args, and result. Args belong in the trace even when they failed — that is how you debug.
- Stop on
finishwith afinalkey. Afinishthat itself errors is not success; keep looping or refuse. - Budget. If the
forends, returnmax_steps. Never silently pick the last weather dict.
Retries are not hope
Retry parse errors with a protocol reminder. Retry timeouts by calling the same tool again (part 5). Do not retry unknown_city — that is information, not a blip. Retrying Atlantis will not create coordinates.
A simple rule: retry if the error code is in {"timeout", "parse", "rate_limit"}. Otherwise finish or change the plan (re-geocode with a stripped city).
Transcript hygiene
Keep tool results small. If a future weather API returns 400 JSON keys, store a projection: temp, conditions, place. The policy's context window is a budget. Logging the full HTTP body can live in a side channel, not in the prompt.
Note:When you swap fake_model for an LLM, wrap the API so it still returns a dict or a string. The loop above should not change.Exercise
Break the fake model on purpose: make it return the string Sure! {"tool": "geocode", "args": {"city": "Paris"}} on the first call. Extend parse_action to find the first { and last } and parse that slice. Confirm Paris still works. Then confirm that a string with no braces hits parse_budget.
Check your understanding