Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
6627845
feat: pass RuntimeState through event bus, add .checkpoint(directory)
greysonlalonde Apr 2, 2026
cf241d8
feat: pass RuntimeState through event bus, add .checkpoint() and .fro…
greysonlalonde Apr 2, 2026
2e1f882
feat: convert executor/tools/prompts to BaseModel, enable checkpoint …
greysonlalonde Apr 3, 2026
743ebed
fix: preserve kickoff_event_id on resume, verbose already works
greysonlalonde Apr 3, 2026
9ab85e6
refactor: make CrewAgentExecutorMixin a proper base class with Fields…
greysonlalonde Apr 3, 2026
5179b41
Merge branch 'main' into chore/runtime-state-event-bus
greysonlalonde Apr 3, 2026
2c4914b
Merge branch 'main' into chore/runtime-state-event-bus
greysonlalonde Apr 3, 2026
6504e39
feat: type executor fields, auto-register entities in event bus, conv…
greysonlalonde Apr 3, 2026
78fbe45
fix: TokenCalcHandler hashability, test MinimalExecutor as instance
greysonlalonde Apr 3, 2026
de9f121
fix: type remaining Any fields on CrewAgentExecutor
greysonlalonde Apr 3, 2026
0b980db
fix: use spec= on test mocks for typed executor fields
greysonlalonde Apr 3, 2026
3a08e95
fix: replace object.__new__ and MinimalExecutor subclass with proper …
greysonlalonde Apr 3, 2026
caaccd7
fix: validate entity_type tag before auto-registering in emit()
greysonlalonde Apr 3, 2026
2e1525f
fix: mypy errors in streaming.py and core.py
greysonlalonde Apr 3, 2026
1ed6646
refactor: move RuntimeState to runtime_state.py, type _runtime_state …
greysonlalonde Apr 3, 2026
de93007
refactor: move RuntimeState to state/, add async checkpoint with prov…
greysonlalonde Apr 3, 2026
c653d41
feat: add EventRecord to RuntimeState checkpoints
greysonlalonde Apr 3, 2026
5ace0bf
fix: suppress duplicate lifecycle events on checkpoint resume
greysonlalonde Apr 3, 2026
6dc9f46
feat: mid-task checkpoint resume and executor refactor
greysonlalonde Apr 3, 2026
191053c
refactor: generic from_checkpoint with provider, full LLM serialization
greysonlalonde Apr 3, 2026
fb8b59d
fix: guard register_entity when RuntimeState is None
greysonlalonde Apr 3, 2026
f9d58d4
fix: add BaseAgentExecutor to model_rebuild chain
greysonlalonde Apr 3, 2026
fba5605
fix: use spec= on mocks for typed executor fields
greysonlalonde Apr 3, 2026
7f24d74
fix: use real pydantic instances in executor tests, preserve cache ha…
greysonlalonde Apr 3, 2026
a3d25c6
fix: register RuntimeState in Flow.from_checkpoint
greysonlalonde Apr 3, 2026
206259b
fix: add spec= to remaining mocks passed to pydantic models
greysonlalonde Apr 3, 2026
a94c2bf
fix: bypass pydantic validation for mocks in BaseAgentExecutor tests
greysonlalonde Apr 3, 2026
c1f9d92
fix: assign mocks post-init for remaining executor tests
greysonlalonde Apr 3, 2026
e72b08e
fix: resolve mypy errors from optional executor fields
greysonlalonde Apr 3, 2026
81e51f0
Merge branch 'main' into chore/runtime-state-event-bus
greysonlalonde Apr 3, 2026
b882988
fix: checkpoint resume bugs and handler signature caching
greysonlalonde Apr 3, 2026
dc2904d
Merge branch 'main' into chore/runtime-state-event-bus
greysonlalonde Apr 3, 2026
ebb58a2
fix: bump uv-pre-commit to 0.11.3, distinguish checkpoint resume
greysonlalonde Apr 4, 2026
d769469
fix: restore checkpoint_train flag during checkpoint resume
greysonlalonde Apr 4, 2026
88d9984
Merge branch 'chore/runtime-state-event-bus' of https://github.com/cr…
greysonlalonde Apr 4, 2026
c4bbb03
refactor: use lru_cache for handler param count
greysonlalonde Apr 4, 2026
70fc701
fix: bump litellm to ~=1.83.0 and openai to ~=2.30.0
greysonlalonde Apr 4, 2026
fac186a
fix: handle unhashable partial handlers in param count cache
greysonlalonde Apr 4, 2026
686cff6
fix: register entities in aemit like emit does
greysonlalonde Apr 4, 2026
0079c70
ci: bump uv from 0.8.4 to 0.11.3 in all workflows
greysonlalonde Apr 4, 2026
da5a890
fix: share event metadata setup between emit and aemit
greysonlalonde Apr 4, 2026
3f447f2
cleanup: remove redundant _registered_entity_ids class annotation
greysonlalonde Apr 4, 2026
0c228b4
fix: seed _registered_entity_ids from restored RuntimeState
greysonlalonde Apr 4, 2026
e0fc321
fix: return 0 instead of None when checkpoint resumes from first task
greysonlalonde Apr 4, 2026
6e7afb7
fix: avoid duplicating LLM hooks on checkpoint restore
greysonlalonde Apr 4, 2026
055d146
fix: resolve mypy errors from openai 2.x type changes
greysonlalonde Apr 4, 2026
5c243b7
fix: skip adding crew-owned agents as top-level RuntimeState entities
greysonlalonde Apr 4, 2026
b46e965
fix: return state messages by reference, not copy
greysonlalonde Apr 4, 2026
167b609
fix: restore event scope stack from checkpoint event record
greysonlalonde Apr 4, 2026
e1bbab7
fix: eliminate PydanticSerializationUnexpectedValue warnings
greysonlalonde Apr 6, 2026
97866c6
fix: pre-cache tiktoken encoding before VCR intercepts requests
greysonlalonde Apr 6, 2026
470af2f
feat: add llm_type and executor_type discriminators for checkpoint fi…
greysonlalonde Apr 6, 2026
ef7654d
fix: add RWLock to EventRecord for thread-safe concurrent access
greysonlalonde Apr 6, 2026
4e15d49
fix: wire executor back-references in BaseAgent.from_checkpoint
greysonlalonde Apr 6, 2026
408cd04
cleanup: remove dead _entity_discriminator and CheckpointPayload
greysonlalonde Apr 6, 2026
6aa6d27
Merge branch 'main' into chore/runtime-state-event-bus
greysonlalonde Apr 6, 2026
e5452d5
fix: use json mode in RuntimeState entity serialization
greysonlalonde Apr 6, 2026
b55e86a
fix: only restore crew-level scope on checkpoint resume
greysonlalonde Apr 6, 2026
61422be
fix: populate task_id on task events and match in scope restore
greysonlalonde Apr 6, 2026
9235edc
feat: preserve event subtypes through checkpoint serialization
greysonlalonde Apr 6, 2026
57cd9cf
fix: aemit skips ContextVar scope mutations, only records events
greysonlalonde Apr 6, 2026
594e1d4
fix: restore emission counter from checkpoint event record
greysonlalonde Apr 6, 2026
d2b092a
fix: clear error messages for missing llm_type/executor_type
greysonlalonde Apr 6, 2026
54897bb
fix: restore type[BaseModel] | None on CrewStructuredTool.args_schema
greysonlalonde Apr 6, 2026
07aa7c0
fix: skip create_agent_executor for resuming agents
greysonlalonde Apr 6, 2026
c9c3ac8
fix: serialize args_schema as JSON schema for checkpoint roundtrip
greysonlalonde Apr 6, 2026
3ceeda3
Merge branch 'main' into chore/runtime-state-event-bus
greysonlalonde Apr 6, 2026
14d5d5b
fix: accumulate all prior task outputs when skipping on checkpoint re…
greysonlalonde Apr 6, 2026
7c3b987
fix: make BaseTool survive checkpoint JSON round-trip
greysonlalonde Apr 6, 2026
ac0a4b2
fix: make BaseTool survive checkpoint JSON round-trip
greysonlalonde Apr 6, 2026
a9cea10
Merge branch 'main' into chore/runtime-state-event-bus
greysonlalonde Apr 6, 2026
3ced490
test: include required in expected init_params_schema keys
greysonlalonde Apr 6, 2026
4db05c3
fix: restore Flow execution state from checkpoint fields
greysonlalonde Apr 6, 2026
3be42f9
fix: restore Flow subclass on checkpoint resume
greysonlalonde Apr 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/crewai-tools/tests/test_generate_tool_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def test_extract_init_params_schema(mock_tool_extractor):
assert init_params_schema.keys() == {
"$defs",
"properties",
"required",
"title",
"type",
}
Expand Down
1 change: 1 addition & 0 deletions lib/crewai/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies = [
"uv~=0.9.13",
"aiosqlite~=0.21.0",
"pyyaml~=6.0",
"aiofiles~=24.1.0",
"lancedb>=0.29.2,<0.30.1",
]

Expand Down
41 changes: 31 additions & 10 deletions lib/crewai/src/crewai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM
from crewai.process import Process
from crewai.runtime_state import _entity_discriminator
from crewai.task import Task
from crewai.tasks.llm_guardrail import LLMGuardrail
from crewai.tasks.task_output import TaskOutput
Expand Down Expand Up @@ -99,8 +98,8 @@ def __getattr__(name: str) -> Any:

try:
from crewai.agents.agent_builder.base_agent import BaseAgent as _BaseAgent
from crewai.agents.agent_builder.base_agent_executor_mixin import (
CrewAgentExecutorMixin as _CrewAgentExecutorMixin,
from crewai.agents.agent_builder.base_agent_executor import (
BaseAgentExecutor as _BaseAgentExecutor,
)
from crewai.agents.tools_handler import ToolsHandler as _ToolsHandler
from crewai.experimental.agent_executor import AgentExecutor as _AgentExecutor
Expand All @@ -118,10 +117,18 @@ def __getattr__(name: str) -> Any:
"Flow": Flow,
"BaseLLM": BaseLLM,
"Task": Task,
"CrewAgentExecutorMixin": _CrewAgentExecutorMixin,
"BaseAgentExecutor": _BaseAgentExecutor,
"ExecutionContext": ExecutionContext,
"StandardPromptResult": _StandardPromptResult,
"SystemPromptResult": _SystemPromptResult,
}

from crewai.tools.base_tool import BaseTool as _BaseTool
from crewai.tools.structured_tool import CrewStructuredTool as _CrewStructuredTool

_base_namespace["BaseTool"] = _BaseTool
_base_namespace["CrewStructuredTool"] = _CrewStructuredTool

try:
from crewai.a2a.config import (
A2AClientConfig as _A2AClientConfig,
Expand Down Expand Up @@ -155,36 +162,49 @@ def __getattr__(name: str) -> Any:
**sys.modules[_BaseAgent.__module__].__dict__,
}

import crewai.state.runtime as _runtime_state_mod

for _mod_name in (
_BaseAgent.__module__,
Agent.__module__,
Crew.__module__,
Flow.__module__,
Task.__module__,
"crewai.agents.crew_agent_executor",
_runtime_state_mod.__name__,
_AgentExecutor.__module__,
):
sys.modules[_mod_name].__dict__.update(_resolve_namespace)

from crewai.agents.crew_agent_executor import (
CrewAgentExecutor as _CrewAgentExecutor,
)
from crewai.tasks.conditional_task import ConditionalTask as _ConditionalTask

_BaseAgentExecutor.model_rebuild(force=True, _types_namespace=_full_namespace)
_BaseAgent.model_rebuild(force=True, _types_namespace=_full_namespace)
Task.model_rebuild(force=True, _types_namespace=_full_namespace)
_ConditionalTask.model_rebuild(force=True, _types_namespace=_full_namespace)
_CrewAgentExecutor.model_rebuild(force=True, _types_namespace=_full_namespace)
Crew.model_rebuild(force=True, _types_namespace=_full_namespace)
Flow.model_rebuild(force=True, _types_namespace=_full_namespace)
_AgentExecutor.model_rebuild(force=True, _types_namespace=_full_namespace)

from typing import Annotated

from pydantic import Discriminator, RootModel, Tag
from pydantic import Field

from crewai.state.runtime import RuntimeState

Entity = Annotated[
Annotated[Flow, Tag("flow")] # type: ignore[type-arg]
| Annotated[Crew, Tag("crew")]
| Annotated[Agent, Tag("agent")],
Discriminator(_entity_discriminator),
Flow | Crew | Agent, # type: ignore[type-arg]
Field(discriminator="entity_type"),
]
RuntimeState = RootModel[list[Entity]]

RuntimeState.model_rebuild(
force=True,
_types_namespace={**_full_namespace, "Entity": Entity},
)

try:
Agent.model_rebuild(force=True, _types_namespace=_full_namespace)
Expand All @@ -205,6 +225,7 @@ def __getattr__(name: str) -> Any:
"BaseLLM",
"Crew",
"CrewOutput",
"Entity",
"ExecutionContext",
"Flow",
"Knowledge",
Expand Down
18 changes: 9 additions & 9 deletions lib/crewai/src/crewai/agent/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
BeforeValidator,
ConfigDict,
Field,
InstanceOf,
PrivateAttr,
model_validator,
)
Expand Down Expand Up @@ -195,12 +194,12 @@ class Agent(BaseAgent):
llm: Annotated[
str | BaseLLM | None,
BeforeValidator(_validate_llm_ref),
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
PlainSerializer(_serialize_llm_ref, return_type=dict | None, when_used="json"),
] = Field(description="Language model that will run the agent.", default=None)
function_calling_llm: Annotated[
str | BaseLLM | None,
BeforeValidator(_validate_llm_ref),
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
PlainSerializer(_serialize_llm_ref, return_type=dict | None, when_used="json"),
] = Field(description="Language model that will run the agent.", default=None)
system_template: str | None = Field(
default=None, description="System format for the agent."
Expand Down Expand Up @@ -297,8 +296,8 @@ class Agent(BaseAgent):
Can be a single A2AConfig/A2AClientConfig/A2AServerConfig, or a list of any number of A2AConfig/A2AClientConfig with a single A2AServerConfig.
""",
)
agent_executor: InstanceOf[CrewAgentExecutor] | InstanceOf[AgentExecutor] | None = (
Field(default=None, description="An instance of the CrewAgentExecutor class.")
agent_executor: CrewAgentExecutor | AgentExecutor | None = Field(
default=None, description="An instance of the CrewAgentExecutor class."
)
executor_class: Annotated[
type[CrewAgentExecutor] | type[AgentExecutor],
Expand Down Expand Up @@ -1011,10 +1010,10 @@ def create_agent_executor(
)
self.agent_executor = self.executor_class(
llm=self.llm,
task=task, # type: ignore[arg-type]
task=task,
i18n=self.i18n,
agent=self,
crew=self.crew, # type: ignore[arg-type]
crew=self.crew,
tools=parsed_tools,
prompt=prompt,
original_tools=raw_tools,
Expand Down Expand Up @@ -1057,7 +1056,8 @@ def _update_executor_parameters(
if self.agent_executor is None:
raise RuntimeError("Agent executor is not initialized.")

self.agent_executor.task = task
if task is not None:
self.agent_executor.task = task
self.agent_executor.tools = tools
self.agent_executor.original_tools = raw_tools
self.agent_executor.prompt = prompt
Expand All @@ -1076,7 +1076,7 @@ def _update_executor_parameters(
self.agent_executor.tools_handler = self.tools_handler
self.agent_executor.request_within_rpm_limit = rpm_limit_fn

if self.agent_executor.llm:
if isinstance(self.agent_executor.llm, BaseLLM):
existing_stop = getattr(self.agent_executor.llm, "stop", [])
self.agent_executor.llm.stop = list(
set(
Expand Down
94 changes: 87 additions & 7 deletions lib/crewai/src/crewai/agents/agent_builder/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
BaseModel,
BeforeValidator,
Field,
InstanceOf,
PrivateAttr,
SerializeAsAny,
field_validator,
model_validator,
)
Expand All @@ -24,7 +24,7 @@
from typing_extensions import Self

from crewai.agent.internal.meta import AgentMeta
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
from crewai.agents.agent_builder.base_agent_executor import BaseAgentExecutor
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
from crewai.agents.cache.cache_handler import CacheHandler
from crewai.agents.tools_handler import ToolsHandler
Expand All @@ -51,6 +51,7 @@
if TYPE_CHECKING:
from crewai.context import ExecutionContext
from crewai.crew import Crew
from crewai.state.provider.core import BaseProvider


def _validate_crew_ref(value: Any) -> Any:
Expand All @@ -63,7 +64,31 @@ def _serialize_crew_ref(value: Any) -> str | None:
return str(value.id) if hasattr(value, "id") else str(value)


_LLM_TYPE_REGISTRY: dict[str, str] = {
"base": "crewai.llms.base_llm.BaseLLM",
"litellm": "crewai.llm.LLM",
"openai": "crewai.llms.providers.openai.completion.OpenAICompletion",
"anthropic": "crewai.llms.providers.anthropic.completion.AnthropicCompletion",
"azure": "crewai.llms.providers.azure.completion.AzureCompletion",
"bedrock": "crewai.llms.providers.bedrock.completion.BedrockCompletion",
"gemini": "crewai.llms.providers.gemini.completion.GeminiCompletion",
}


def _validate_llm_ref(value: Any) -> Any:
if isinstance(value, dict):
import importlib

llm_type = value.get("llm_type")
if not llm_type or llm_type not in _LLM_TYPE_REGISTRY:
raise ValueError(
f"Unknown or missing llm_type: {llm_type!r}. "
f"Expected one of {list(_LLM_TYPE_REGISTRY)}"
)
dotted = _LLM_TYPE_REGISTRY[llm_type]
mod_path, cls_name = dotted.rsplit(".", 1)
cls = getattr(importlib.import_module(mod_path), cls_name)
return cls(**value)
return value


Expand All @@ -75,12 +100,37 @@ def _resolve_agent(value: Any, info: Any) -> Any:
return Agent.model_validate(value, context=getattr(info, "context", None))


def _serialize_llm_ref(value: Any) -> str | None:
_EXECUTOR_TYPE_REGISTRY: dict[str, str] = {
"base": "crewai.agents.agent_builder.base_agent_executor.BaseAgentExecutor",
"crew": "crewai.agents.crew_agent_executor.CrewAgentExecutor",
"experimental": "crewai.experimental.agent_executor.AgentExecutor",
}


def _validate_executor_ref(value: Any) -> Any:
if isinstance(value, dict):
import importlib

executor_type = value.get("executor_type")
if not executor_type or executor_type not in _EXECUTOR_TYPE_REGISTRY:
raise ValueError(
f"Unknown or missing executor_type: {executor_type!r}. "
f"Expected one of {list(_EXECUTOR_TYPE_REGISTRY)}"
)
dotted = _EXECUTOR_TYPE_REGISTRY[executor_type]
mod_path, cls_name = dotted.rsplit(".", 1)
cls = getattr(importlib.import_module(mod_path), cls_name)
return cls.model_validate(value)
return value
Comment thread
cursor[bot] marked this conversation as resolved.


def _serialize_llm_ref(value: Any) -> dict[str, Any] | None:
if value is None:
return None
if isinstance(value, str):
return value
return getattr(value, "model", str(value))
return {"model": value}
Comment thread
cursor[bot] marked this conversation as resolved.
result: dict[str, Any] = value.model_dump()
return result
Comment thread
cursor[bot] marked this conversation as resolved.


_SLUG_RE: Final[re.Pattern[str]] = re.compile(
Expand Down Expand Up @@ -197,13 +247,19 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
max_iter: int = Field(
default=25, description="Maximum iterations for an agent to execute a task"
)
agent_executor: InstanceOf[CrewAgentExecutorMixin] | None = Field(
agent_executor: SerializeAsAny[BaseAgentExecutor] | None = Field(
default=None, description="An instance of the CrewAgentExecutor class."
)

@field_validator("agent_executor", mode="before")
@classmethod
def _validate_agent_executor(cls, v: Any) -> Any:
return _validate_executor_ref(v)

llm: Annotated[
str | BaseLLM | None,
BeforeValidator(_validate_llm_ref),
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
PlainSerializer(_serialize_llm_ref, return_type=dict | None, when_used="json"),
] = Field(default=None, description="Language model that will run the agent.")
crew: Annotated[
Crew | str | None,
Expand Down Expand Up @@ -276,6 +332,30 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
)
execution_context: ExecutionContext | None = Field(default=None)

@classmethod
def from_checkpoint(
cls, path: str, *, provider: BaseProvider | None = None
) -> Self:
"""Restore an Agent from a checkpoint file."""
from crewai.context import apply_execution_context
from crewai.state.provider.json_provider import JsonProvider
from crewai.state.runtime import RuntimeState

state = RuntimeState.from_checkpoint(
path,
provider=provider or JsonProvider(),
context={"from_checkpoint": True},
)
for entity in state.root:
if isinstance(entity, cls):
if entity.execution_context is not None:
apply_execution_context(entity.execution_context)
if entity.agent_executor is not None:
entity.agent_executor.agent = entity
entity.agent_executor._resuming = True
return entity
raise ValueError(f"No {cls.__name__} found in checkpoint: {path}")
Comment thread
greysonlalonde marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

@model_validator(mode="before")
@classmethod
def process_model_config(cls, values: Any) -> dict[str, Any]:
Expand Down
Loading
Loading