fix(langgraph): keep JSON Schema semantics for object values and validate node I/O - #256
Open
fede-kamel wants to merge 1 commit into
Open
fix(langgraph): keep JSON Schema semantics for object values and validate node I/O#256fede-kamel wants to merge 1 commit into
fede-kamel wants to merge 1 commit into
Conversation
…date node I/O The pydantic models generated from Agent Spec object schemas lost the semantics of the schemas they came from, and node executors never checked values against the declared schemas: - a bare object schema (or a typed dictionary) became an empty model that silently stripped every key (oracle#220); - additional properties were dropped even when the schema allowed them (oracle#239); - declared defaults of omitted nested properties were replaced by None (oracle#241, LangGraph part); - tools received pydantic model instances for object arguments instead of the plain dictionaries used by the other runtimes, and those instances leaked into flow outputs and client-tool interrupt payloads; - StartNode inputs and EndNode outputs violating their nested schema (missing required property, wrong type, unexpected key) were accepted (oracle#231, oracle#233); - a ToolNode without declared outputs raised "Unsupported multi-output mapping" for any tool return value that was not a dict or tuple. Generated object models now derive from AgentSpecObjectModel: bare object schemas map to Dict[str, Any] (typed when additionalProperties is a schema), extra keys are kept unless additionalProperties is false, declared defaults are applied and unset optional fields are omitted when converting back to JSON with to_json_value(). Tool callables are wrapped so they receive JSON values, and structured agent outputs are converted the same way. Node executors apply nested defaults and validate every input/output value against its JSON schema with the jsonschema package, reporting each violation with the node and property names. The zero-output ToolNode branch now short-circuits the mapping. One existing test declared an object output but expected the scalar 1 to come out of it; it now declares an untyped output. Fixes oracle#220, oracle#231, oracle#233, oracle#239. Fixes the LangGraph part of oracle#241. Signed-off-by: Federico Kamelhar <federico.kamelhar@oracle.com>
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.
Fixes #220, #231, #233, #239, #251, #252. Fixes the LangGraph part of #241 (the Wayflow part lives in wayflowcore).
Root cause
Values flowing through the LangGraph adapter lost the JSON Schema semantics of their Agent Spec properties, and nothing checked them against the declared schemas:
_build_type_from_schema(adapters/_utils.py) turned a bare{"type": "object"}into an empty pydantic model, which silently stripped every key (Bare object schemas in tool parameters silently strip all keys (empty create_model + pydantic extra='ignore') #220); dropped additional properties even when the schema allowed them (LangGraph does not preserve additional object fields during Flow state propagation #239); and replaced omitted nested properties withNoneinstead of their declared default (Inconsistent handling of default values for optional fields inside ObjectProperty inputs #241).args_schemaand passesgetattr(result, field)to the callable, so tools received pydantic model instances for object inputs, and those instances leaked into flow outputs and client-tool interrupt payloads (LangGraph adapter: tools receive pydantic model instances for object inputs, and the instances leak into flow outputs #252)._cast_values_and_add_defaultsonly cast top-level primitives, soStartNodeinputs andEndNodeoutputs violating a nested schema (missing required property, wrong type, unexpected key) were accepted (LangGraph runtime does not enforce nested schema validation on Flow/StartNode input #231, LangGraph runtime does not enforce EndNode output schema constraints #233), while Wayflow rejects them. The integer-cast branch also compared against a Python error message that never occurs, so a non-numeric string for an integer input crashed with a rawint()error.ToolNodeExecutor._format_tool_resultusedifinstead ofeliffor the "no declared outputs" case, so any non-dict/tuple tool return value raisedUnsupported multi-output mapping(LangGraph adapter: ToolNode without declared outputs fails with "Unsupported multi-output mapping" #251).Changes
adapters/_utils.py: generated object models derive from a newAgentSpecObjectModel. Bare object schemas and typed dictionaries map toDict[str, ...]; extra keys are kept unlessadditionalPropertiesisfalse(JSON Schema default); declared defaults are applied;to_json_value()converts models back to plain JSON, omitting optional fields that were neither provided nor defaulted. New helpersapply_json_schema_defaultsandget_json_schema_validation_errors(uses thejsonschemapackage that is already a core dependency).langgraph/_langgraphconverter.py: tool callables (server, remote, client, andBaseToolfunc/coroutine) are wrapped with_with_json_argumentsso they receive JSON values; user-providedStructuredTools with their ownargs_schemaare untouched (to_json_valueonly converts models generated from Agent Spec schemas).langgraph/_node_execution.py: node inputs and outputs are converted to JSON values, nested defaults are applied, and each value is validated against its property's JSON schema with an error naming the node, the property and every violation. Structured agent outputs go throughto_json_value. The zero-outputToolNodebranch short-circuits.tests/adapters/test_schema_models.py(unit, no runtime needed),tests/adapters/langgraph/flows/test_schema_fidelity.py(the scenarios of the issues end to end, plus the zero-output ToolNode),tests/adapters/langgraph/test_tool_json_arguments.py(sync, async and client-tool interrupt payloads). One existing test declared an object output but expected the scalar1to come out of it; it now declares an untyped output.Behaviour changes to be aware of
additionalPropertiesnow keep extra keys instead of silently dropping them (JSON Schema default).create_pydantic_model_from_properties, so its nested object models gain the same defaults/extras behaviour. Its tests were not run (CrewAI is not installable alongside the other adapters).Verification
SKIP_LLM_TESTS=1), up from 1179 onmain.tests/run_tests.shwith core dependencies only) reproduced locally on Python 3.10 through 3.14.Notes for reviewers
extract_outputs_from_invoke_result, which the fix for LangGraph AgentNode returns tool-output field names instead of values #224 also edits; whichever lands second needs a trivial rebase._build_type_from_schema; no textual conflict.