fix: after_llm_call hooks no longer break native tool execution#6531
Conversation
Registering any `after_llm_call` hook broke native tool execution: the executor invokes `_setup_after_llm_call_hooks` on the intermediate response that carries the model's tool calls, the non-str payload was stringified for the response-rewrite pass, and the executor then treated that string as a final answer instead of executing the tools. Structured payloads (neither `str` nor `BaseModel`) now pass through untouched, mirroring the isinstance guard `_invoke_after_llm_call_hooks` already applies on the direct-call path; hooks still fire on the follow-up textual response. Fixes #6529
📝 WalkthroughWalkthroughChangesTool-call hook preservation
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/crewai/src/crewai/utilities/agent_utils.py (1)
1754-1760: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winGuard logic is correct; type annotations need widening.
The early-return guard correctly prevents stringification of structured tool-call payloads. However, the function signature at lines 1736 and 1739 still types
answerasstr | BaseModeland the return asstr | BaseModel, even though the guard now allows arbitrary objects (e.g.,list[Mock()]in the test) to pass through unchanged. Update the annotations to reflect reality.♻️ Proposed type annotation update
def _setup_after_llm_call_hooks( executor_context: CrewAgentExecutor | AgentExecutor | LiteAgent | None, - answer: str | BaseModel, + answer: str | BaseModel | Any, printer: Printer, verbose: bool = True, -) -> str | BaseModel: +) -> str | BaseModel | Any:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/utilities/agent_utils.py` around lines 1754 - 1760, Widen the answer parameter and return annotations of the function containing the new isinstance guard to include arbitrary object values, matching its passthrough behavior for non-str/BaseModel responses. Keep the existing guard and textual/BaseModel handling unchanged, and use the project’s established broad object type annotation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@lib/crewai/src/crewai/utilities/agent_utils.py`:
- Around line 1754-1760: Widen the answer parameter and return annotations of
the function containing the new isinstance guard to include arbitrary object
values, matching its passthrough behavior for non-str/BaseModel responses. Keep
the existing guard and textual/BaseModel handling unchanged, and use the
project’s established broad object type annotation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bf19ddef-bd25-401f-af1d-d582a0e8367e
📒 Files selected for processing (2)
lib/crewai/src/crewai/utilities/agent_utils.pylib/crewai/tests/hooks/test_llm_hooks.py
Registering any
after_llm_callhook broke native tool execution: the executor runs_setup_after_llm_call_hookson the intermediate response that carries the model's tool calls, the structured payload got stringified for the response-rewrite pass, and the executor then treated that string as the final answer instead of executing the tools. Structured payloads (neitherstrnorBaseModel) now pass through untouched, matching the guard the direct-call path already applies in_invoke_after_llm_call_hooks; hooks still fire on the follow-up textual response.Fixes #6529
Note
Low Risk
Small, targeted guard in hook setup with a regression test; behavior change only affects the tool-call intermediate response when after hooks are registered.
Overview
Fixes a regression where any registered
after_llm_callhook broke native tool execution:_setup_after_llm_call_hooksran on the LLM’s intermediate tool-call payload, stringified it for the hook rewrite path, and the executor then treated that string as the final answer instead of running tools (#6529)._setup_after_llm_call_hooksnow returns structured responses unchanged when the answer is neitherstrnorBaseModel(e.g. native tool-call lists). Hooks still run on normal text and Pydantic responses. This mirrors the guard the directllm.call()path already uses via_invoke_after_llm_call_hooks.Adds a regression test ensuring tool-call payloads pass through untouched while hooks still observe textual responses.
Reviewed by Cursor Bugbot for commit 12356b4. Bugbot is set up for automated code reviews on this repo. Configure here.