Split out of the #17 consolidation, where it was noticed but deliberately left alone to keep that work behaviour-preserving.
BaseAgent.parse_json() is annotated -> dict, but it returns whatever the response parsed to. A model emitting a top-level array — fenced, or as the only JSON in the response — yields a list.
This predates the #17 work. Dict-preference in extract_json() narrows it (an object anywhere in the response now wins) but does not close it: a response containing only an array still produces a list.
Options
- Raise
ValueError when the parsed result is not a dict. Breaking for anyone relying on lists today, but makes the annotation true and gives callers a clear failure instead of a surprising type.
- Widen the annotation to
dict | list. Honest about today's behaviour, but pushes an isinstance check onto every caller.
Needs a decision on which contract we want.
Related, same decision
extract_json() on [{"a": 1}, {"b": 2}] returns the first object and silently drops the sibling. That is pre-existing behaviour — the old brace-only scan did the same — and it is preserved deliberately by stage 5 of iter_json_spans(). Whether a model returning a list of results should be silently truncated to its first element is the same contract question.
Split out of the #17 consolidation, where it was noticed but deliberately left alone to keep that work behaviour-preserving.
BaseAgent.parse_json()is annotated-> dict, but it returns whatever the response parsed to. A model emitting a top-level array — fenced, or as the only JSON in the response — yields alist.This predates the #17 work. Dict-preference in
extract_json()narrows it (an object anywhere in the response now wins) but does not close it: a response containing only an array still produces a list.Options
ValueErrorwhen the parsed result is not a dict. Breaking for anyone relying on lists today, but makes the annotation true and gives callers a clear failure instead of a surprising type.dict | list. Honest about today's behaviour, but pushes an isinstance check onto every caller.Needs a decision on which contract we want.
Related, same decision
extract_json()on[{"a": 1}, {"b": 2}]returns the first object and silently drops the sibling. That is pre-existing behaviour — the old brace-only scan did the same — and it is preserved deliberately by stage 5 ofiter_json_spans(). Whether a model returning a list of results should be silently truncated to its first element is the same contract question.