From 867df0f633fbc65563ab4706587457b3f5879322 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Mon, 25 May 2026 19:24:02 -0700 Subject: [PATCH] fix(checkpoint): drop unroundtrippable callbacks and adapter state - callable_to_string returns None for lambdas/closures instead of an unresolvable dotted path; Crew filters Nones out of restored callback lists. - EventNode.event serializer honors info.mode so mode='json' calls cascade properly into nested event payloads. - RagTool.adapter serializes to None (post-validator rebuilds from config); concrete adapters hold runtime state that can't be round-tripped. --- .../src/crewai_tools/tools/rag/rag_tool.py | 32 ++- lib/crewai-tools/tool.specs.json | 212 ++++++------------ lib/crewai/src/crewai/crew.py | 9 + lib/crewai/src/crewai/state/event_record.py | 6 +- lib/crewai/src/crewai/types/callback.py | 13 +- lib/crewai/tests/test_callback.py | 15 +- 6 files changed, 130 insertions(+), 157 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py b/lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py index 8099443e2..97edfcb5b 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod import os -from typing import Any, Literal, cast +from typing import Annotated, Any, Literal, cast from crewai.rag.core.base_embeddings_callable import EmbeddingFunction from crewai.rag.embeddings.factory import build_embedder @@ -8,10 +8,13 @@ from crewai.rag.embeddings.types import ProviderSpec from crewai.tools import BaseTool from pydantic import ( BaseModel, + BeforeValidator, ConfigDict, Field, + PlainSerializer, TypeAdapter, ValidationError, + WithJsonSchema, field_validator, model_validator, ) @@ -100,6 +103,26 @@ class Adapter(BaseModel, ABC): """Add content to the knowledge base.""" +def _resolve_adapter(value: Any) -> Any: + """Validate the ``adapter`` field, returning a placeholder for dict/None input. + + Adapter state is not round-tripped; the ``_ensure_adapter`` post-validator + rebuilds a fresh adapter from the tool's ``config``. + """ + if isinstance(value, Adapter): + return value + if value is None or isinstance(value, dict): + return RagTool._AdapterPlaceholder() + return value + + +def _serialize_adapter(adapter: Any, info: Any) -> Any: + """Serialize the ``adapter`` field, dropping runtime state from the payload.""" + if not isinstance(adapter, Adapter): + return adapter + return None + + class RagTool(BaseTool): class _AdapterPlaceholder(Adapter): def query( @@ -123,7 +146,12 @@ class RagTool(BaseTool): similarity_threshold: float = 0.6 limit: int = 5 collection_name: str = "rag_tool_collection" - adapter: Adapter = Field(default_factory=_AdapterPlaceholder) + adapter: Annotated[ + Adapter, + BeforeValidator(_resolve_adapter), + PlainSerializer(_serialize_adapter, when_used="json"), + WithJsonSchema({"type": ["object", "null"]}), + ] = Field(default_factory=_AdapterPlaceholder) config: RagToolConfig = Field( default_factory=RagToolConfig, description="Configuration format accepted by RagTool.", diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index 6afe92ca7..b957a3afc 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -2912,12 +2912,6 @@ "humanized_name": "Search a CSV's content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -3903,7 +3897,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -3964,12 +3961,6 @@ "humanized_name": "Search a Code Docs content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -4955,7 +4946,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -5641,12 +5635,6 @@ "humanized_name": "Search a DOCX's content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -6632,7 +6620,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -7926,12 +7917,6 @@ "humanized_name": "Search a directory's content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -8917,7 +8902,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -10762,12 +10750,6 @@ "humanized_name": "Search a github repo's content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -11753,7 +11735,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -12041,12 +12026,6 @@ "humanized_name": "Search a JSON's content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -13032,7 +13011,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -13316,12 +13298,6 @@ "humanized_name": "Search a MDX's content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -14307,7 +14283,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -14774,12 +14753,6 @@ "humanized_name": "Search a database's table content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -15765,7 +15738,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -15967,21 +15943,6 @@ "title": "EnvVar", "type": "object" }, - "JsonResponseFormat": { - "description": "Response format requesting raw JSON output (e.g. ``{\"type\": \"json_object\"}``).", - "properties": { - "type": { - "const": "json_object", - "title": "Type", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "JsonResponseFormat", - "type": "object" - }, "LLM": { "properties": { "additional_params": { @@ -16210,16 +16171,6 @@ "title": "Reasoning Effort" }, "response_format": { - "anyOf": [ - { - "$ref": "#/$defs/JsonResponseFormat" - }, - {}, - { - "type": "null" - } - ], - "default": null, "title": "Response Format" }, "seed": { @@ -17207,12 +17158,6 @@ "humanized_name": "Search a PDF's content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -18198,7 +18143,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -18906,12 +18854,6 @@ "humanized_name": "Knowledge base", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -19897,7 +19839,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -20994,12 +20939,6 @@ "humanized_name": "Job Search", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -21985,7 +21924,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -22462,12 +22404,6 @@ "humanized_name": "Webpage to Markdown", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -23453,7 +23389,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -24307,12 +24246,6 @@ "humanized_name": "Search a txt's content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -25298,7 +25231,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -26227,12 +26163,6 @@ "humanized_name": "Search in a specific website", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -27218,7 +27148,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -27279,12 +27212,6 @@ "humanized_name": "Search a XML's content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -28270,7 +28197,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -28331,12 +28261,6 @@ "humanized_name": "Search a Youtube Channels content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -29322,7 +29246,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", @@ -29383,12 +29310,6 @@ "humanized_name": "Search a Youtube Video content", "init_params_schema": { "$defs": { - "Adapter": { - "description": "Abstract base class for RAG adapters.", - "properties": {}, - "title": "Adapter", - "type": "object" - }, "AzureProviderConfig": { "description": "Configuration for Azure provider.", "properties": { @@ -30374,7 +30295,10 @@ }, "properties": { "adapter": { - "$ref": "#/$defs/Adapter" + "type": [ + "object", + "null" + ] }, "collection_name": { "default": "rag_tool_collection", diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index 0ffec4888..870049179 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -382,6 +382,15 @@ class Crew(FlowTrackable, BaseModel): checkpoint_train: bool | None = Field(default=None) checkpoint_kickoff_event_id: str | None = Field(default=None) + @field_validator( + "before_kickoff_callbacks", "after_kickoff_callbacks", mode="before" + ) + @classmethod + def _drop_unresolvable_callbacks(cls, value: Any) -> Any: + if isinstance(value, list): + return [v for v in value if v is not None] + return value + @classmethod def from_checkpoint(cls, config: CheckpointConfig) -> Crew: """Restore a Crew from a checkpoint, ready to resume via kickoff(). diff --git a/lib/crewai/src/crewai/state/event_record.py b/lib/crewai/src/crewai/state/event_record.py index f0b15b48f..2ca78a396 100644 --- a/lib/crewai/src/crewai/state/event_record.py +++ b/lib/crewai/src/crewai/state/event_record.py @@ -67,7 +67,11 @@ class EventNode(BaseModel): event: Annotated[ BaseEvent, BeforeValidator(_resolve_event), - PlainSerializer(lambda v: v.model_dump()), + PlainSerializer( + lambda v, info: ( + v.model_dump(mode="json") if info.mode == "json" else v.model_dump() + ), + ), ] edges: dict[EdgeType, list[str]] = Field(default_factory=dict) diff --git a/lib/crewai/src/crewai/types/callback.py b/lib/crewai/src/crewai/types/callback.py index ea89effdb..a6fb2d101 100644 --- a/lib/crewai/src/crewai/types/callback.py +++ b/lib/crewai/src/crewai/types/callback.py @@ -130,18 +130,15 @@ def _resolve_dotted_path(path: str) -> Callable[..., Any]: raise ValueError(f"Cannot resolve callback {path!r}") -def callable_to_string(fn: Callable[..., Any]) -> str: - """Serialize a callable to its dotted-path string representation. - - Uses ``fn.__module__`` and ``fn.__qualname__`` to produce a string such - as ``"builtins.print"``. Lambdas and closures produce paths that contain - ```` and cannot be round-tripped via :func:`string_to_callable`. +def callable_to_string(fn: Callable[..., Any]) -> str | None: + """Serialize a module-level callable as a ``"module.qualname"`` string. Args: fn: The callable to serialize. Returns: - A dotted string of the form ``"module.qualname"``. + The dotted path, or ``None`` for lambdas and closures (not + resolvable by :func:`string_to_callable`). """ module = getattr(fn, "__module__", None) qualname = getattr(fn, "__qualname__", None) @@ -150,6 +147,8 @@ def callable_to_string(fn: Callable[..., Any]) -> str: f"Cannot serialize {fn!r}: missing __module__ or __qualname__. " "Use a module-level named function for checkpointable callbacks." ) + if "" in qualname or qualname == "": + return None return f"{module}.{qualname}" diff --git a/lib/crewai/tests/test_callback.py b/lib/crewai/tests/test_callback.py index 417c74d98..43d2ed0f7 100644 --- a/lib/crewai/tests/test_callback.py +++ b/lib/crewai/tests/test_callback.py @@ -4,6 +4,7 @@ from __future__ import annotations import functools import os +from collections.abc import Callable from typing import Any import pytest from pydantic import BaseModel, ValidationError @@ -93,10 +94,18 @@ class TestCallableToString: result = callable_to_string(print) assert result == "builtins.print" - def test_lambda_produces_locals_path(self) -> None: + def test_lambda_returns_none(self) -> None: fn = lambda: None # noqa: E731 - result = callable_to_string(fn) - assert "" in result + assert callable_to_string(fn) is None + + def test_closure_returns_none(self) -> None: + def outer() -> Callable[[], None]: + def inner() -> None: + return None + + return inner + + assert callable_to_string(outer()) is None def test_missing_qualname_raises(self) -> None: obj = type("NoQual", (), {"__module__": "test"})()