Skip to content

[fix] keep falsy tool results (0, False, []) on the sync tool execution path - #9948

Merged
harshsinha03 merged 7 commits into
agno-agi:mainfrom
BlueX888:fix/sync-tool-result-falsy-values
Sep 16, 2026
Merged

harshsinha03 merged 7 commits into
agno-agi:mainfrom
BlueX888:fix/sync-tool-result-falsy-values

Conversation

@BlueX888

@BlueX888 BlueX888 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

On the synchronous tool-execution path, a tool whose return value is falsy but meaningful (0, 0.0, False, [], {}) is sent to the model as an empty tool message. Model.run_function_call stringified the result only when it was truthy:

function_call_output = str(function_execution_result.result) if function_execution_result.result else ""

while Model.arun_function_calls already used str(function_call.result). So agent.run() and agent.arun() gave the model different tool results for the same tool, and the model could not tell "zero items" apart from "the tool produced no output". The empty string also ends up in RunOutput.tools[i].result, so it is what gets persisted to the session.

This PR drops the truthiness check so the sync branch matches the async one. See the note below on None.

Issue number: #9947

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Improvement
  • Model update
  • Other:

Changes

  • libs/agno/agno/models/base.py: one-line change in run_function_call, str(function_execution_result.result) unconditionally, matching arun_function_calls.
  • libs/agno/tests/unit/models/test_tool_result_falsy_values.py: new regression test running a tool that returns 0, 0.0, False, [], {}, 1, "text" through both run_function_call and arun_function_calls with a stub Model (no network), asserting the tool message content on each path and that the two paths agree.

Note on None: with this change a tool returning None produces "None" on the sync path, which is what the async path has always produced (verified: both paths now give 'None'). Previously sync gave "". I kept the two paths identical rather than special-casing None. If you would rather have "" for None, the change is a one-liner (if result is not None else "") and I'm happy to apply it to both paths.

Testing

Commands actually run, in a uv venv (uv pip install -e "libs/agno[dev]" plus provider SDKs), on main @ 56eae14f:

  • Reproduction (Agent level, stub model that requests one tool call for a count_items() -> int tool returning 0):
    • Before: run() tool message content '', arun() '0'.
    • After: both '0'.
  • New test file: 18 passed with the fix. Sabotage check with the fix reverted and the test kept: 8 failed (all sync-path cases for 0, 0.0, False, [], {} and the sync/async equality cases), 10 passed. Restoring the fix: 18 passed.
  • pytest libs/agno/tests/unit/models libs/agno/tests/unit/agent libs/agno/tests/unit/team libs/agno/tests/unit/tools/test_functions.py libs/agno/tests/unit/tools/test_decorator.py libs/agno/tests/unit/tools/test_toolkit.py → 3121 passed, 12 skipped.
  • pytest libs/agno/tests/unit/workflow → 560 passed, 7 skipped.
  • ruff format --check and ruff check on the two changed files → clean.
  • mypy --config-file libs/agno/pyproject.toml libs/agno/agno/models/base.py → 0 errors.

I did not run the entire libs/agno/tests/unit suite: some tool modules need optional extras not installed here. CI will cover the rest.


Checklist

  • Code complies with style guidelines
  • Ran format/validation scripts (ruff format/check and mypy with the repo's pyproject.toml config, as ./scripts/validate.sh does)
  • Self-review completed
  • Documentation updated (comments, docstrings) — not applicable, no public API change
  • Examples and guides: Relevant cookbook examples have been included or updated (if applicable) — not applicable
  • Tested in clean environment
  • Tests added/updated (if applicable)

Duplicate and AI-Generated PR Check

  • I have searched existing open pull requests and confirmed that no other PR already addresses this issue
  • If a similar PR exists, I have explained below why this PR is a better approach
  • Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.)

Additional Notes

Related open PR: #6633 (for #6361) refactors both branches into a shared _format_non_generator_result helper and, in passing, changes this check to is not None. That PR is much broader (also touches event handling for generators and workflow events), has been conflicting since June, and does not call out the falsy-result behaviour or test it. This PR is the minimal fix for the specific bug in #9947 with a targeted regression test; if #6633 lands first this becomes redundant and can be closed.

AI disclosure: not entirely AI-generated. I found and reproduced the bug, decided on the fix, and ran every command listed above; an AI assistant helped draft the test file and this description, and I reviewed and understand every line.

…on path

run_function_call stringified the tool result only when it was truthy, so a
tool returning 0, 0.0, False, [] or {} reached the model as an empty tool
message. arun_function_calls already used str(result), so agent.run() and
agent.arun() gave the model different results for the same tool.

Fixes agno-agi#9947
@harshsinha03 harshsinha03 self-assigned this Sep 4, 2026

@VANDRANKI VANDRANKI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Community review, not a merge gate.

Traced this fully. The bug: libs/agno/agno/models/base.py's sync run_function_call had function_call_output = str(function_execution_result.result) if function_execution_result.result else "", which treats any falsy tool result (0, False, [], {}, 0.0) as "no result" and silently replaces it with an empty string before it reaches the model.

I checked the async path (arun_function_calls, around line 3037-3050 in the current main branch) and it already does function_call_output = str(function_call.result) with no truthiness check at all. So this PR is making the sync path match an async behavior that already existed, not introducing new behavior. I also checked FunctionExecutionResult in libs/agno/agno/tools/function.py: result defaults to None and is set to whatever the user's tool entrypoint returns, including None for a void tool. Both before and after this PR, a None result stringifies differently on each path ("" on sync pre-fix, "None" on async both before and after) - this PR doesn't change that, it only aligns the falsy-but-non-None cases (0, False, [], {}) which is exactly what the new test file and its test_sync_and_async_tools_match parametrized case checks.

The new test file (test_tool_result_falsy_values.py) covers both sync and async paths with 0, 0.0, False, [], {}, 1, "text" and asserts the stringified content matches on both paths. I traced the fix, the async comparison, and the test cases by hand and didn't find a gap.

@harshsinha03 harshsinha03 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for contributing

@harshsinha03
harshsinha03 merged commit ccc22c8 into agno-agi:main Sep 16, 2026
11 checks passed
@BlueX888
BlueX888 deleted the fix/sync-tool-result-falsy-values branch September 16, 2026 12:16
harshsinha03 added a commit that referenced this pull request Sep 16, 2026
## Summary

Workflow step content checks used truthiness, so valid step results such
as a zero count or False were omitted when building downstream context.

Treat content as present unless it is None or blank text (`content is
not None and str(content).strip()`, the check `Parallel` already used
for its combined output) in:

- `StepInput.get_all_previous_content`
- `StepInput.get_step_content` for Parallel sub-steps and steps nested
inside them
- `Parallel._build_aggregated_content`
- `Step._get_deepest_content_from_step_output`, whose return type now
matches the non-string content it returns

Add regressions for zero, False, empty containers, blank text, absent
content, ordering and the Parallel paths.

No existing issue is linked; this behavior was reproduced from the
current main branch.

## Type of change

- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Improvement
- [ ] Model update
- [ ] Other

## Validation

New tests: 7 failed and 4 passed against the unchanged base; all 11
passed with the patch. Including existing StepInput serialization tests:
15 passed. No external services or model calls.

```text
python -m pytest libs/agno/tests/unit/workflow/test_previous_content_values.py libs/agno/tests/unit/workflow/test_step_input_serialization.py -q
```

Changed-file Ruff format/check and source-file mypy pass. On Windows,
the underlying checks from scripts/format.sh and scripts/validate.sh
were run directly: repository-wide Ruff lint/import checks and the
cookbook pattern check pass; mypy passes for all 1055 agno and 21
agnoctl source files. Repository-wide format checking reports one
unchanged baseline file,
libs/agno/tests/unit/vectordb/test_elasticsearch.py (also reproduced on
pristine main); changed files pass. Bash scripts themselves and the
complete test suite were not run. Python 3.12 on Windows; pytest 9.1.1,
pytest-asyncio 1.4.0, Ruff 0.15.20, mypy 2.1.0.

## Checklist

- [x] Code complies with style guidelines (changed files checked)
- [ ] Ran full format/validation scripts (`./scripts/format.sh` and
`./scripts/validate.sh`)
- [ ] Contributor self-review completed
- [ ] Documentation updated (not part of this patch)
- [ ] Examples and guides updated (not part of this patch)
- [ ] Tested in a clean, dependency-consistent environment
- [x] Tests added/updated

## Duplicate and AI-Generated PR Check

- [x] Searched existing open PR titles and relevant issue/PR keywords on
2026-09-16; no matching fix was found. Recheck immediately before
submission.
- [ ] If a similar PR exists, explain why this PR is a better approach
(related but different fixes discussed below)
- [x] This patch and PR draft were entirely AI-generated

## Additional Notes

Empty strings now retain the step heading; None continues to be omitted.
This semantic choice needs maintainer review. Related PR #9948 fixes
model tool-result formatting in models/base.py, not this workflow
helper. Other workflow content accessors are outside this patch.

An AI coding assistant discovered the behavior, drafted the patch/tests
and executed the reported local checks. The contributor approved
submission of this patch. The reported implementation and validation
work was performed by the assistant; no independent human implementation
claim is made.

---------

Co-authored-by: Harsh <[email protected]>
Co-authored-by: Harsh Sinha <[email protected]>
BlueX888 added a commit to BlueX888/BlueX888 that referenced this pull request Sep 17, 2026
- NOTES 补 bytedance/deer-flow#5509、agno-agi/agno#9948 两条中文说明(此前 cron 已把标题行写进贡献区但没有说明)
- 缺陷表:deer-flow #5473 → 社区 PR #5494 已修复;openai-agents-python #5041 → 维护者未接受并关闭(需真实 provider wire 证据)
- deer-flow star 数 82k → 83k
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.

3 participants