JJoeven

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.
  • tool is a string.
  • args is an object (possibly empty). Missing args is 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 geocode with a city extracted from the goal.
  • If geocode returned unknown_city, emit finish with a cannot: answer.
  • If geocode succeeded and there is no weather observation, emit weather with lat/lon from the last geocode result.
  • If weather failed, refuse.
  • If weather succeeded, finish with 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.

Live Pythonpython
Output
Run to execute this in your browser. Nothing is sent to a server.

Step-by-step: the loop you must be able to recite

  1. Seed the transcript with the user goal as the first event. Everything the policy knows comes from this list.
  2. Call the policy with the full transcript. In production this is an API call; here it is fake_model.
  3. Parse. Dicts from a fake model still go through parse_action so 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 toward max_parse_retries and max_steps so you cannot infinite-loop.
  4. Dispatch. Registry lookup. Call with **args. Catch nothing from the tool if you already return error dicts; still guard TypeError if the model omits a key (kw["city"] vs kw.get).
  5. Append a tool event with name, args, and result. Args belong in the trace even when they failed — that is how you debug.
  6. Stop on finish with a final key. A finish that itself errors is not success; keep looping or refuse.
  7. Budget. If the for ends, return max_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

What should happen when JSON parsing fails?