Repository navigation
feat(adapters/langgraph): bound an agent whose model will not produce structured output - #43
Merged
spichen merged 1 commit intoSep 24, 2026
Conversation
… structured output Backports StructuredOutputGuard from oracle#210. oi-fork took the single-string shortcut from that PR in 0242c9a but not the guard that pairs with it, so every agent that keeps response_format is unbounded here. Without it a model that never produces a structured response fails two ways: * no tools: the agent loops. create_agent sets recursion_limit=9999, so that is thousands of model calls before a GraphRecursionError naming neither the agent nor the schema. * with tools: routing treats a turn with no tool calls as done, so the agent exits with no structured_response and the declared outputs come back empty, with no error at all (langchain#36349). The guard bounds both. after_model counts consecutive turns ending in an AIMessage with no tool calls; after_agent catches the silent exit that budget never sees. Turns with tool calls do not count, since under ToolStrategy the structured response is itself a tool call. Attached only when a response model was built, so it composes with the output format gate added in f111e7a: an agent that declares TEXT never builds one and never sees the guard. Guard source is taken unchanged from oracle#210. The wiring, _types.py exports and this commit are the only local parts; oracle#210 also moves is_single_string_output to adapters/_utils.py, which is omitted here to keep the diff small. Refs OA-1681
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backports
StructuredOutputGuardfrom oracle#210.Why
oi-forktook the single-string shortcut from oracle#210 in0242c9a, but not the guard that pairs with it. So every agent here that keepsresponse_formatis unbounded.Without it, a model that never produces a structured response fails two ways:
create_agentsetsrecursion_limit=9999, so that is thousands of model calls before aGraphRecursionErrornaming neither the agent nor the schema.structured_responseand the declared outputs come back empty, with no error at all (langchain#36349).This became more pressing after
f111e7a: gating on the declared output format moves one-field JSON schemas out of the shortcut and into theresponse_formatpath, which is the unguarded one.What it does
after_modelcounts consecutive turns ending in anAIMessagewith no tool calls.after_agentcatches the silent exit that budget never sees. Turns with tool calls do not count, since underToolStrategythe structured response is itself a tool call.Attached only when a response model was built, so it composes with
f111e7a: an agent declaring TEXT never builds one and never sees the guard.Testing
Local kind stack, model
nvidia/Qwen3.8-27B-NVFP4:{"sentiment":"positive"}, 2 steps, guard firesafter_modelthenafter_agentStructuredOutputGuard.*eventsValueErrorThe TEXT case is the one that matters: the raw event stream shows the guard is never attached when no response model is built, so plain chat agents are untouched.
Not exercised: a model that actually refuses structured output — the registry here offers only one model and it complies. The guard's firing path is therefore covered by oracle#210's own tests rather than mine.
Scope
Guard source is taken unchanged from oracle#210. Only the wiring and the
_types.pyexports are local. oracle#210 also movesis_single_string_outputtoadapters/_utils.py; that is omitted here to keep the diff small.Refs OA-1681