"""Tool-call wire format. The agent emits a single tool call per turn as a JSON object wrapped in ``...`` tags:: Some optional reasoning text the model writes before the call. {"kind": "add_module", "name": "validators", "responsibility": "validation"} Why this format and not OpenAI / Qwen native tool-calling: * It's tokenizer-agnostic. We don't depend on any chat-template's tool-call hooks, so we can swap models freely. * It's easy for a 0.5B model to emit reliably with a few in-context examples. * It's easy to fail cleanly: malformed output produces a structured ``ParseFailure`` that maps to MALFORMED in the reward engine. If the model emits multiple ```` blocks we take the *last* one; this matches "the agent reasoned, then committed to one action" and avoids rewarding an early stutter. """ from __future__ import annotations import json import re from dataclasses import dataclass ACTION_OPEN = "" ACTION_CLOSE = "" _ACTION_RE = re.compile(r"\s*(.*?)\s*", re.DOTALL) @dataclass(frozen=True) class ParseSuccess: action: dict[str, object] raw: str # the JSON text we extracted, for debugging @dataclass(frozen=True) class ParseFailure: code: str message: str raw: str ParseResult = ParseSuccess | ParseFailure def parse_completion(text: str) -> ParseResult: """Extract a tool call from a model completion. On success, returns ``ParseSuccess`` whose ``action`` is a JSON dict suitable to forward to ``/step``. On any failure path returns a ``ParseFailure`` with a stable code: * ``no_action_tag`` — neither tag found * ``unclosed_tag`` — open tag without close * ``invalid_json`` — tags found but body wasn't JSON * ``not_an_object`` — JSON parsed but isn't a dict * ``missing_kind`` — dict is missing the ``kind`` field """ if ACTION_OPEN not in text: return ParseFailure("no_action_tag", "no tag found", raw=text) if ACTION_CLOSE not in text: return ParseFailure("unclosed_tag", " tag never closed", raw=text) matches = _ACTION_RE.findall(text) if not matches: return ParseFailure( "no_action_tag", " tags present but body could not be extracted", raw=text, ) body = matches[-1].strip() # take the last action emitted try: obj = json.loads(body) except json.JSONDecodeError as e: return ParseFailure("invalid_json", f"json error: {e.msg}", raw=body) if not isinstance(obj, dict): return ParseFailure( "not_an_object", f"action body must be a JSON object, got {type(obj).__name__}", raw=body, ) if "kind" not in obj: return ParseFailure("missing_kind", "action object lacks 'kind' field", raw=body) return ParseSuccess(action=obj, raw=body) def render_action(action: dict[str, object]) -> str: """Render an action dict in the on-the-wire format. Used by tests and by scripted policies.""" return f"{ACTION_OPEN}\n{json.dumps(action)}\n{ACTION_CLOSE}"