Always return dict-shaped validated args for single-BaseModel tools#5137
Merged
Always return dict-shaped validated args for single-BaseModel tools#5137
Conversation
When a tool function has exactly one model-like parameter (BaseModel, dataclass,
TypedDict), the JSON schema sent to the model is the parameter's schema directly
(unwrapped) so the model generates its fields at the top level instead of in a
redundant wrapper. Pydantic validation of such args previously returned the
BaseModel instance itself rather than a `{param_name: instance}` dict, violating
the `ValidatedToolArgs` type contract and breaking every downstream consumer
that assumed a dict-shaped view: capability hooks (`after_tool_validate`,
`wrap_tool_validate`, `before/after/wrap_tool_execute`),
`AbstractToolset.call_tool`, and — most critically —
`args_validator_func(ctx, **args)` which fails at runtime on non-mapping `**`
unpacking.
Wrap the validator's core schema with `no_info_after_validator_function` that
produces `{single_arg_name: value}` — keeping the JSON schema sent to the model
unchanged while giving hooks and tool-invocation code a uniform dict view.
The now-redundant rewrap in `FunctionSchema._call_args` is removed.
# Conflicts: # tests/test_capabilities.py
Contributor
Docs Preview
|
Temporal's durable execution path serializes validated args out of the Agent
process, then re-validates them inside an activity using the same validator.
After the previous change the validator's output shape is `{name: value}`, but
its input shape was still the raw BaseModel fields — so re-validation of the
already-wrapped dict (`{"argument": {"city": "X"}}`) failed with a "field
required" error.
Switch from `no_info_after_validator_function` to
`no_info_wrap_validator_function` and fall back to unwrapping the single-keyed
`{name: value}` shape when the raw-shape validation fails. Raw input still wins
(and the BaseModel schema path still runs) for the first-pass LLM args.
Adds a regression test covering both input shapes.
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.
Problem
When a tool function has exactly one "model-like" parameter (
BaseModel, dataclass,TypedDict), the JSON schema sent to the model is the parameter's schema directly — so the model generates the model's fields at the top level instead of inside a redundant wrapper. However, Pydantic validation then returned theBaseModelinstance itself rather than a{param_name: instance}dict. This violated theValidatedToolArgs = dict[str, Any]type contract and broke every downstream consumer that relied on a dict-shaped view:after_tool_validate,wrap_tool_validate,before_tool_execute,after_tool_execute,wrap_tool_execute,on_tool_execute_error,on_tool_validate_errorValidatedToolCall.validated_args: dict[str, Any] | NoneAbstractToolset.call_tool(tool_args: dict[str, Any], ...)args_validator_func(ctx, **args)— silently broken at runtime, since**base_model_instanceisn't a valid mapping unpackFix
Wrap the validator's core schema with
core_schema.no_info_after_validator_functionthat produces{single_arg_name: value}. The JSON schema sent to the model is unchanged (pydantic's JSON schema generator delegates to the inner schema forfunction-after), so the model keeps receiving the BaseModel's fields at the top level — but the validator's output is uniformly dict-shaped, so every downstream consumer sees the same contract.The now-redundant rewrapping inside
FunctionSchema._call_argsis removed.Tests
test_args_validator_single_base_model_argconfirmsargs_validatornow works with single-BaseModel-arg tools (was silently broken)test_hooks_receive_dict_args_for_single_base_model_toolconfirmsafter_tool_validateandwrap_tool_executehooks both receive dict-shaped argsChecklist