Skip to content

fix(types): parse null and {} as empty dspy.ToolCalls - #10613

Open
isaacbmiller wants to merge 1 commit into
mainfrom
isaac/toolcalls-empty-values
Open

isaacbmiller wants to merge 1 commit into
mainfrom
isaac/toolcalls-empty-values

Conversation

@isaacbmiller

Copy link
Copy Markdown
Collaborator

Fixes #10612.

Summary

dspy.ToolCalls now parses null and {} as an empty ToolCalls, the same as []. JSON-mode models (seen with DeepSeek-V4-Flash + JSONAdapter on τ-bench retail) use both to mean "no tool calls". ReActV2 then rejected the turn as parse_error, and a forced submit with the same reply raised AdapterParseError from forward.

The fix goes in ToolCalls parsing rather than ReActV2's signature. Declaring tool_calls: dspy.ToolCalls | None would also accept null. But adapters detect the native tool-call field with field.annotation == ToolCalls, so an Optional field silently turns off native function calling for every LM that supports it. Parsing empty values in the type fixes every ToolCalls output field on the text path and leaves the native path unchanged. ToolCalls | None fields still get None for None.

With this change, null/{} behave exactly like []: the loop ends with empty_tool_calls, and the forced submit runs. If the model still never calls submit, ReActV2 raises "no submit call" as before. Text mode can't force a tool choice, which is a separate question.

Verification

  • test_react_v2_json_null_or_empty_tool_calls_mean_no_tool_calls (parametrized over None and {}) drives ReActV2 through JSONAdapter with a DummyLM. Both cases fail on main with AdapterParseError and pass with the fix.
  • Extended test_toolcalls_vague_match to cover None, {}, and []; it fails on main.
  • pytest tests/predict/test_react_v2.py tests/adapters/test_tool.py: 91 passed. pytest tests/adapters tests/predict tests/signatures: 1014 passed, 148 skipped.
  • ruff check passes on the changed files, and CI's ruff check --fix-only --exit-non-zero-on-fix is clean. pre-commit hooks pass.
  • Live on Azure DeepSeek-V4-Flash: a forced-submit reply of "tool_calls": null now ends as "after empty_tool_calls: no submit call" instead of AdapterParseError.

AI assistance

Investigated, reproduced, and implemented with Amp (Claude) in a maintainer-directed session. Prompt: "Open one issue and one fix PR for: ReActV2 fails when the model returns tool_calls: null (or {}) with DeepSeek-V4-Flash + JSONAdapter. Reproduce it, confirm the root cause in code, include a DummyLM repro, decide whether the fix belongs in ReActV2's signature or in how ToolCalls parses empty values, and explain the choice."

Models in JSON mode (e.g. DeepSeek-V4-Flash with JSONAdapter) often write
"tool_calls": null or {} to mean "no tool calls". ToolCalls accepted [] but
rejected both, so ReActV2 ended its loop with parse_error and the forced
submit raised AdapterParseError.

Treat None and {} like [] in ToolCalls.validate_input. Optional fields
(ToolCalls | None) still validate None as None.

Amp-Thread-ID: https://ampcode.com/threads/T-01a126ab-8118-77c7-a811-67f49e5f20cd
Co-authored-by: Isaac Miller <isaac@cmpnd.ai>
@greptile-apps

greptile-apps Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] Safe to merge within the verified scope: empty values are accepted without bypassing the requirement to call submit.

T-Rex evidence

Evidence from the check

  • The authored script executes both revisions’ actual parser source and asserts empty, populated, malformed, and optional-value contracts, making the comparison reproducible.

Command output from the check

  • Ran 43 contract checks against the base revision and captured results plus full validation errors, showing that direct None and empty dictionaries were rejected while the other tested contracts passed.

Command output from the check

  • Ran the same 43 contract checks against HEAD and captured results plus full validation errors, showing the intended empty-input acceptance without a regression in the tested parsing contracts.

Command output from the check

  • Executed the existing vague-match parser test and both JSON empty-input adapter cases on HEAD, showing all three tests passed.

Evidence from the check

  • This script was executed on base and HEAD to assert two-turn submission and repeated-empty rejection for null and {}, making the scoped checks reproducible.

Command output from the check

  • Ran the setup's focused ToolCalls and ReActV2 tests with the configured virtual environment, and all 3 tests passed.

Command output from the check

  • Ran six selected ReActV2 empty-call and forced-submission tests, including async submission filtering, and all 6 passed.

Command output from the check

  • Executed all four scripted cases against the base revision, showing successful forced submission with an actual submit and AdapterParseError for repeated empty values.

Command output from the check

  • Executed the same four cases against HEAD, showing two-turn forced submission with answers and the required no-submit ValueError when empty values repeat.

View artifacts

Summary

ToolCalls now accepts None and {} as empty call lists, matching [].

  • Tool-call replies can use null or {} to mean no calls.

No blocking issues were found in the changed behavior. This change is safe to merge within the verified scope.

Reviews (1) · Last reviewed commit: "fix(types): parse null and {} as empty d..." · Reviewed by Greptile

Copy link
Copy Markdown

AI-assisted offline review of 3350e369 against f1260ff1 on native Windows/Python 3.13.12.

The existing ReActV2 and Tool suites pass: 91 tests on head, 89 on base. An additional 61 checks importing the actual package all pass on head; base has 47 passes and 14 null/empty-object failures. Ruff passes on the three changed files.

Actual JSONAdapter/DummyLM sync and async flows accept an empty turn followed by submit, while repeated empty replies still require submit. Controls cover 12 malformed ToolCalls shapes, Optional None and native detection/hooks/schema handling. Both revisions already normalize non-dict args such as args=1 to {}; these checks do not claim otherwise. No live provider/Azure calls or full repository suite were run. I found no consequential defect in this scope.

reviewed and tested locally.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] ReActV2 fails when a JSON-mode model writes "tool_calls": null or {}

3 participants