diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index d077dc0a4..b012155ca 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -1030,6 +1030,10 @@ class CrewAgentExecutor(BaseAgentExecutor): color="red", ) + # An after_tool_call hook may have replaced the result with a + # FileArtifact; keep those bytes out of the message and events too. + result = store_if_artifact(result, scope_id) + if not error_event_emitted: crewai_event_bus.emit( self, diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 674a1a302..d3d266bec 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -1939,6 +1939,10 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): color="red", ) + # An after_tool_call hook may have replaced the result with a + # FileArtifact; keep those bytes out of the message and events too. + result = store_if_artifact(result, scope_id) + if not error_event_emitted: crewai_event_bus.emit( self, diff --git a/lib/crewai/src/crewai/utilities/agent_utils.py b/lib/crewai/src/crewai/utilities/agent_utils.py index aa330d927..95b92080d 100644 --- a/lib/crewai/src/crewai/utilities/agent_utils.py +++ b/lib/crewai/src/crewai/utilities/agent_utils.py @@ -1541,6 +1541,10 @@ def execute_single_native_tool_call( except Exception: # noqa: S110 pass + # An after_tool_call hook may have replaced the result with a FileArtifact; + # keep those bytes out of the message and events too. + result = store_if_artifact(result, scope_id) + if not error_event_emitted: crewai_event_bus.emit( event_source, diff --git a/lib/crewai/tests/tools/test_file_artifact.py b/lib/crewai/tests/tools/test_file_artifact.py index 0a1b9efc8..ff169e35c 100644 --- a/lib/crewai/tests/tools/test_file_artifact.py +++ b/lib/crewai/tests/tools/test_file_artifact.py @@ -314,6 +314,39 @@ class TestNativeExecutorWiring: assert base64.b64decode(captured["content"]) == payload +class TestAfterHookArtifact: + """An after_tool_call hook that returns a FileArtifact must still be stored.""" + + def test_hook_returned_artifact_is_replaced_by_handle(self) -> None: + from crewai.hooks.tool_hooks import ( + register_after_tool_call_hook, + unregister_after_tool_call_hook, + ) + from crewai.tools import BaseTool, FileArtifact + + payload = bytes(range(256)) * 50 + + class Echo(BaseTool): + name: str = "echo" + description: str = "Echo" + + def _run(self) -> str: + return "plain text" + + def hook(_context): + return FileArtifact(data=payload, filename="hook.bin") + + register_after_tool_call_hook(hook) + try: + run = _experimental_executor_runner([Echo()]) + result = run("echo", "{}")["result"] + finally: + unregister_after_tool_call_hook(hook) + + assert base64.b64encode(payload).decode() not in result + assert _HANDLE.search(result) is not None + + class TestTtlPrune: @staticmethod def _expire(handle: str) -> None: