Skip to content

fix: validate inputs_from_state when a tool has no input parameters - #12813

Open
Lesereingrape wants to merge 1 commit into
deepset-ai:mainfrom
Lesereingrape:fix/inputs-from-state-empty-tool-validation
Open

Lesereingrape wants to merge 1 commit into
deepset-ai:mainfrom
Lesereingrape:fix/inputs-from-state-empty-tool-validation

Conversation

@Lesereingrape

@Lesereingrape Lesereingrape commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Related Issues

Proposed Changes:

Tool.__init__ validated inputs_from_state with if valid_inputs and param_name not in valid_inputs: (haystack/tools/tool.py:208). The valid_inputs and conjunct makes "this tool has no input parameters" indistinguishable from "we could not work out the parameters", so for a zero-argument function — or a ComponentTool around a component without input sockets — a mapping to a name that cannot exist was accepted at construction time. The user then met it at the worst possible moment: inside the agent loop, as ToolInvocationError: Failed to invoke Tool \clock` with parameters {'city': 'Berlin'}. Error: no_param_tool() got an unexpected keyword argument 'city', which never mentions inputs_from_state`.

The fix drops that one conjunct, so the check is if param_name not in valid_inputs:. An empty set now validates as strictly as a non-empty one, and the error reads ... Valid parameters are: set().

Why the empty set is safe to validate against, and why the guard was asymmetric in the first place:

  • _get_valid_inputs() is typed -> set[str] (tool.py:214) and always returns a set — the union of the callable's signature parameters and the properties declared in parameters. A Tool with no parameters has resolved that set and found it empty; that is knowledge, not missing information.
  • The sibling _get_valid_outputs() (tool.py:249) is typed -> set[str] | None precisely because function-based tools genuinely have no output schema, and its caller tests the sentinel with is not None (tool.py:148-149) rather than by truthiness. So outputs already distinguish "none" from "unknown"; inputs conflated the two.
  • Nothing in the library depends on the old skip. Tool, ComponentTool, AgentTool and PipelineTool all resolve a non-empty input set in normal use except for the no-input case this PR fixes; from_function builds the schema from the same signature, so the set it reports is the set the mapping must name.

Impact for users: a configuration error that previously produced a confusing runtime failure inside an agent run now fails at construction, with the valid names listed — matching what already happens for every tool that takes input.

How did you test it?

Two tests, one per affected construction path, plus the existing suites of both files.

test on main (b717d00, source reverted) on this branch
test_tool.py::TestTool::test_inputs_from_state_validation_with_no_valid_parameters fail (no ValueError) pass
test_component_tool.py::TestComponentTool::test_from_component_with_inputs_from_state_and_no_input_sockets fail (no ValueError) pass
pre-existing tests in both files (69 of them) pass pass

Both new tests carry an in-file control: the ComponentTool one first asserts tool.parameters == {"type": "object", "properties": {}}, i.e. that the empty schema really comes from a component with no input sockets rather than from a mistake in the test. The neighbouring test_inputs_from_state_validation_* tests (non-empty valid inputs, TypeError on non-string values) exercise the untouched branches and pass on both sides.

# this branch
pytest test/tools/test_tool.py test/tools/test_component_tool.py -q  ->  71 passed, 7 skipped

# same test files, haystack/tools/tool.py reverted to b717d00
pytest test/tools/test_tool.py test/tools/test_component_tool.py -q  ->  2 failed, 69 passed, 7 skipped

Environment note, stated plainly: hatch is not available on my machine, so I did not run hatch run test:unit or hatch run test:types. I ran against an editable install of this checkout (haystack 3.2.0-rc0, import haystack resolving to the clone, Python 3.13.5) with pytest directly, after installing the test environment's pytest plugins (pytest-bdd, pytest-asyncio, pytest-rerunfailures, pytest-cov, flaky) so that both files collect. The 7 skips are pre-existing (integration-marked) and unchanged between the two runs. For lint I ran the repository's pinned ruff (v0.16.0, the rev in .pre-commit-config.yaml) on the changed files — All checks passed! / 3 files already formatted — and scripts/release_note_backticks.py on the new release note (exit 0). I also ran mypy over the three changed files with --ignore-missing-imports --follow-imports=silent: no errors — but that is a looser configuration than the repo's [tool.mypy] pass over test/tools/, so consider CI the first authoritative type check. Pre-commit hooks are not installed in my clone, so the individual tools above were run directly rather than through pre-commit run --all-files; codespell was not run.

Notes for the reviewer

  • Scope is one conjunct at haystack/tools/tool.py:208; tests in test/tools/test_tool.py and test/tools/test_component_tool.py; release note at releasenotes/notes/tool-empty-inputs-from-state-validation-bc8460226991e1ba.yaml.
  • This is a validation-tightening change: code that today constructs a Tool/ComponentTool with a bogus inputs_from_state mapping and never invokes it will start raising at construction. That is the bug being reported, and the tool would have failed at invocation anyway — but it is still a behavior change for such callers, and I want it visible in review rather than buried.
  • Deliberately not changed: _get_valid_inputs() stays set[str]. Making it set[str] | None to mirror outputs would reintroduce the same silent skip for tools whose signature cannot be introspected (inspect.signature raising ValueError/TypeError, tool.py:230), and today those tools still yield a usable set from parameters["properties"]. If you would rather have inputs and outputs share the None sentinel, I can rework it that way.
  • I checked for overlap before opening: no open PR in this repo touches inputs_from_state validation (searched title/body across open PRs). The only matching issue is inputs_from_state typos pass construction when a tool takes no input #12811, which I filed for this report. My own fix(auth): deserialize listed secrets when recursive is enabled #12810 touches haystack/utils/auth.py and does not overlap with this change.
  • This contribution was made with an AI coding assistant: it located the asymmetry, wrote the reproduction and the tests, and measured the before/after numbers quoted above. It was opened during an unattended contribution run, so the account owner has not read the diff yet — please review it with that in mind, and I will amend or close it on request.

Checklist

  • I have read the contributors guidelines and the code of conduct.
  • I have updated the related issue with new insights and changes.
  • I have added unit tests and updated the docstrings. (Tests added; no docstring change was needed — the inputs_from_state parameter docs already state that it maps state keys to the tool's input parameters.)
  • I've used one of the conventional commit types for my PR title: fix:, feat:, build:, chore:, ci:, docs:, style:, refactor:, perf:, test: and added ! in case the PR includes breaking changes.
  • I have documented my code.
  • I have added a release note file, following the contributors guidelines.
  • I ran the tools behind the pre-commit hooks that apply to my files (repo-pinned ruff 0.16.0 check + format, release_note_backticks.py) and they are clean; I could not run hatch or the full hook suite locally, and my mypy was run with a looser configuration than CI's — see "How did you test it?" for the exact scope.

The check was gated on `valid_inputs` being truthy, which conflated "this tool
takes no input" with "we could not resolve the inputs". For a zero-argument
function, or a ComponentTool around a component without input sockets, a mapping
to a name that cannot exist was accepted at construction time and only surfaced
later as a ToolInvocationError that never mentions inputs_from_state.

_get_valid_inputs() is typed `-> set[str]` and always returns a resolved set, so
an empty set is knowledge rather than missing information; the sibling output
check already distinguishes the two with a `set[str] | None` sentinel.
@Lesereingrape
Lesereingrape requested a review from a team as a code owner September 19, 2026 10:32
@Lesereingrape
Lesereingrape requested review from julian-risch and removed request for a team September 19, 2026 10:32
@vercel

vercel Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

@Lesereingrape is attempting to deploy a commit to the deepset Team on Vercel.

A member of the Team first needs to authorize it.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

inputs_from_state typos pass construction when a tool takes no input

1 participant