Skip to content

Always return dict-shaped validated args for single-BaseModel tools#5137

Merged
DouweM merged 3 commits intomainfrom
validatedtoolargs
Apr 17, 2026
Merged

Always return dict-shaped validated args for single-BaseModel tools#5137
DouweM merged 3 commits intomainfrom
validatedtoolargs

Conversation

@DouweM
Copy link
Copy Markdown
Collaborator

@DouweM DouweM commented Apr 17, 2026

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 the BaseModel instance itself rather than a {param_name: instance} dict. This violated the ValidatedToolArgs = dict[str, Any] type contract and broke every downstream consumer that relied on a dict-shaped view:

  • Capability hooks: after_tool_validate, wrap_tool_validate, before_tool_execute, after_tool_execute, wrap_tool_execute, on_tool_execute_error, on_tool_validate_error
  • ValidatedToolCall.validated_args: dict[str, Any] | None
  • AbstractToolset.call_tool(tool_args: dict[str, Any], ...)
  • args_validator_func(ctx, **args) — silently broken at runtime, since **base_model_instance isn't a valid mapping unpack

Fix

Wrap the validator's core schema with core_schema.no_info_after_validator_function that produces {single_arg_name: value}. The JSON schema sent to the model is unchanged (pydantic's JSON schema generator delegates to the inner schema for function-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_args is removed.

Tests

  • test_args_validator_single_base_model_arg confirms args_validator now works with single-BaseModel-arg tools (was silently broken)
  • test_hooks_receive_dict_args_for_single_base_model_tool confirms after_tool_validate and wrap_tool_execute hooks both receive dict-shaped args

Checklist

  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • No breaking changes in accordance with the version policy.
  • PR title is fit for the release changelog.

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.
@github-actions github-actions Bot added size: S Small PR (≤100 weighted lines) bug Report that something isn't working, or PR implementing a fix labels Apr 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

devin-ai-integration[bot]

This comment was marked as resolved.

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.
@DouweM DouweM merged commit 531d6da into main Apr 17, 2026
54 checks passed
@DouweM DouweM deleted the validatedtoolargs branch April 17, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Report that something isn't working, or PR implementing a fix size: S Small PR (≤100 weighted lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Type ValidatedToolArgs missing pydantic BaseModel

1 participant