Repository navigation
fix(plugins): return None, not the input, when a built-in plugin leaves a value unchanged - #7390
Open
AtulJoshi1206 wants to merge 2 commits into
Open
AtulJoshi1206 wants to merge 2 commits into
AtulJoshi1206 wants to merge 2 commits into
Conversation
…rt results For a tool result that is not a Part or a list of Parts, after_tool_callback returned the result itself. The plugin manager stops at the first non-None plugin result, and the agent's after_tool_callbacks only run when the plugin result is None, so registering this plugin silently disabled every agent after_tool_callback, and every later plugin, for ordinary dict results. Returning None is the BasePlugin contract for leaving a result unchanged.
AtulJoshi1206
force-pushed
the
fix/multimodal-plugin-after-tool-none
branch
from
October 17, 2026 03:58
4ebb7ff to
1b24c99
Compare
… plugins ReflectAndRetryModelPlugin and SaveFilesAsArtifactsPlugin had the same pattern as MultimodalToolResultsPlugin, returning their input to mean "unchanged". The plugin manager stops at the first non-None result, so: - ReflectAndRetryModelPlugin, once the retry limit was exceeded with throw_exception_if_retry_exceeded=False, returned the error response itself, and the flow then skipped the remaining plugins and the agent's canonical after_model_callbacks for it. - SaveFilesAsArtifactsPlugin, with no artifact service configured, returned the user message itself, which stopped every later plugin's on_user_message_callback. Both now return None, the BasePlugin contract for leaving a value unchanged.
AtulJoshi1206
force-pushed
the
fix/multimodal-plugin-after-tool-none
branch
from
October 17, 2026 04:01
1b24c99 to
aaeeab9
Compare
Contributor
Author
|
Rebased on latest main. While re-checking this I found the same "return the input to mean unchanged" pattern in two more built-in plugins: |
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.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
No existing issue; described below following the bug-report structure.
Describe the Bug:
MultimodalToolResultsPlugin.after_tool_callbackreturns the tool result itself when the result is not aPartor a list ofParts:PluginManagerstops at the first non-Noneplugin result. Inflows/llm_flows/tools/_caller.py, the agent'scanonical_after_tool_callbacksonly run when the plugin result isNone. Under theBasePlugincontract,Nonemeans "leave the result unchanged".Impact:
Registering this built-in plugin silently turns off every agent-level
after_tool_callback(redaction, rewriting, state updates) for every tool that returns an ordinary dict. It also turns off any plugin registered after it.Steps to Reproduce:
An
LlmAgentwith a dict-returning tool, anafter_tool_callbackthat rewrites the response, andplugins=[MultimodalToolResultsPlugin()].Observed Behavior: the callback is never called, and the original dict is sent to the model.
Expected Behavior: the callback runs and its return value replaces the response, the same as without the plugin.
Solution:
Return
Nonefor non-part results. The other return paths of this callback already returnNone.Testing Plan
Unit Tests:
I have added or updated unit tests for my change.
All unit tests pass locally.
Added
test_agent_after_tool_callback_runs_for_non_part_results, which runs an agent end to end throughInMemoryRunnerwith the plugin.Updated
test_tool_returning_non_list_of_parts_is_unchangedto assertresult is None. That matches its stated intent ("has this result unchanged") under the plugin contract.Both fail on
main.Checklist
🤖 Generated with Claude Code