Compare commits

..

6 Commits

Author SHA1 Message Date
Vinicius Brasil
178c2d212c docs: snapshot and changelog for v1.14.8a5 (#6329)
Some checks failed
Check Documentation Broken Links / Check broken links (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
2026-06-24 17:31:32 -07:00
Vinicius Brasil
563b55f7ca feat: bump versions to 1.14.8a5 (#6328) 2026-06-24 17:25:08 -07:00
Vinicius Brasil
340d23ae5d Remove StateProxy from flow state access (#6327)
`StateProxy` looked like a thread-safety boundary, but it only protected
a small slice of state operations. Some examples of operations that were
not covered:

- `self.state.counter += 1`, `self.state["counter"] += 1` (increments)
- `self.state.user.profile.score += 1` (nested object mutations)
- `self.state.config["limits"]["max"] = 10` (mutation through model fields)
- `self.state.items[0].status = "done"` (list/container mutations)

This commit decided to remove it completely for simplicity and
performance:

- Simpler runtime code
- attr read: 24x faster, attr write: 27x faster, list append: 19x faster (local benchmark)
- Clearer concurrency contract (lifecycle locks remain, but arbitrary
  shared state mutation is not presented as thread-safe)
2026-06-24 16:37:51 -07:00
Vinicius Brasil
7738a1d30c Make declarative refs work across flows and crews (#6326)
Declarative flows already used `module:qualname` refs for runtime
objects, but crew JSON tools still had their own lookup path. That meant
examples like `project_tools:LookupTool` were treated as named
`crewai_tools` lookups and failed with guidance that only mentioned
`SerperDevTool` or `custom:<name>`. Invalid refs such as
`not_tools:NotATool` also missed the same BaseTool validation used by
flow tool actions.

Move ref resolution into a shared declarative helper, use it from flow
tool actions and crew JSON loading, and require tool refs to resolve to
`BaseTool` classes before instantiation. Validation still checks tool
refs structurally, so validating a crew does not import or execute
project code.
2026-06-24 15:11:59 -07:00
Vinicius Brasil
156b3500b4 Fix JSON schema flow state kickoff inputs (#6325)
Allow required JSON schema state fields to be supplied by kickoff inputs
instead of requiring every field to exist in state.default before
runtime.

Example: a flow with required lead_name and no state.default can now run
with kickoff inputs={"lead_name": "Ada Lovelace"}.
2026-06-24 13:55:38 -07:00
Jesse Miller
5827abbc17 docs: nest One Card per Step under Crew Studio and drop rollout banner (AGE-107) (#6317)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
The page itself already landed on main via #6247. This rebases onto main
and applies the two remaining changes:

- Nest crew-studio + merged-step-card into a collapsible "Crew Studio"
  nav group (pencil icon), across edge and v1.14.7 in en, pt-BR, ko, ar.
- Remove the temporary "Rolling out" Note banner (feature ships today).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:36:49 -04:00
39 changed files with 443 additions and 474 deletions

View File

@@ -397,8 +397,14 @@
"group": "Build",
"pages": [
"edge/en/enterprise/features/automations",
"edge/en/enterprise/features/crew-studio",
"edge/en/enterprise/features/merged-step-card",
{
"group": "Crew Studio",
"icon": "pencil",
"pages": [
"edge/en/enterprise/features/crew-studio",
"edge/en/enterprise/features/merged-step-card"
]
},
"edge/en/enterprise/features/marketplace",
"edge/en/enterprise/features/agent-repositories",
"edge/en/enterprise/features/tools-and-integrations",
@@ -922,8 +928,14 @@
"group": "Build",
"pages": [
"v1.14.7/en/enterprise/features/automations",
"v1.14.7/en/enterprise/features/crew-studio",
"v1.14.7/en/enterprise/features/merged-step-card",
{
"group": "Crew Studio",
"icon": "pencil",
"pages": [
"v1.14.7/en/enterprise/features/crew-studio",
"v1.14.7/en/enterprise/features/merged-step-card"
]
},
"v1.14.7/en/enterprise/features/marketplace",
"v1.14.7/en/enterprise/features/agent-repositories",
"v1.14.7/en/enterprise/features/tools-and-integrations",
@@ -8549,8 +8561,14 @@
"group": "Construir",
"pages": [
"edge/pt-BR/enterprise/features/automations",
"edge/pt-BR/enterprise/features/crew-studio",
"edge/pt-BR/enterprise/features/merged-step-card",
{
"group": "Crew Studio",
"icon": "pencil",
"pages": [
"edge/pt-BR/enterprise/features/crew-studio",
"edge/pt-BR/enterprise/features/merged-step-card"
]
},
"edge/pt-BR/enterprise/features/marketplace",
"edge/pt-BR/enterprise/features/agent-repositories",
"edge/pt-BR/enterprise/features/tools-and-integrations",
@@ -9051,8 +9069,14 @@
"group": "Construir",
"pages": [
"v1.14.7/pt-BR/enterprise/features/automations",
"v1.14.7/pt-BR/enterprise/features/crew-studio",
"v1.14.7/pt-BR/enterprise/features/merged-step-card",
{
"group": "Crew Studio",
"icon": "pencil",
"pages": [
"v1.14.7/pt-BR/enterprise/features/crew-studio",
"v1.14.7/pt-BR/enterprise/features/merged-step-card"
]
},
"v1.14.7/pt-BR/enterprise/features/marketplace",
"v1.14.7/pt-BR/enterprise/features/agent-repositories",
"v1.14.7/pt-BR/enterprise/features/tools-and-integrations",
@@ -16415,8 +16439,14 @@
"group": "빌드",
"pages": [
"edge/ko/enterprise/features/automations",
"edge/ko/enterprise/features/crew-studio",
"edge/ko/enterprise/features/merged-step-card",
{
"group": "Crew Studio",
"icon": "pencil",
"pages": [
"edge/ko/enterprise/features/crew-studio",
"edge/ko/enterprise/features/merged-step-card"
]
},
"edge/ko/enterprise/features/marketplace",
"edge/ko/enterprise/features/agent-repositories",
"edge/ko/enterprise/features/tools-and-integrations",
@@ -16929,8 +16959,14 @@
"group": "빌드",
"pages": [
"v1.14.7/ko/enterprise/features/automations",
"v1.14.7/ko/enterprise/features/crew-studio",
"v1.14.7/ko/enterprise/features/merged-step-card",
{
"group": "Crew Studio",
"icon": "pencil",
"pages": [
"v1.14.7/ko/enterprise/features/crew-studio",
"v1.14.7/ko/enterprise/features/merged-step-card"
]
},
"v1.14.7/ko/enterprise/features/marketplace",
"v1.14.7/ko/enterprise/features/agent-repositories",
"v1.14.7/ko/enterprise/features/tools-and-integrations",
@@ -24473,8 +24509,14 @@
"group": "البناء",
"pages": [
"edge/ar/enterprise/features/automations",
"edge/ar/enterprise/features/crew-studio",
"edge/ar/enterprise/features/merged-step-card",
{
"group": "استوديو الطاقم",
"icon": "pencil",
"pages": [
"edge/ar/enterprise/features/crew-studio",
"edge/ar/enterprise/features/merged-step-card"
]
},
"edge/ar/enterprise/features/marketplace",
"edge/ar/enterprise/features/agent-repositories",
"edge/ar/enterprise/features/tools-and-integrations",
@@ -24987,8 +25029,14 @@
"group": "البناء",
"pages": [
"v1.14.7/ar/enterprise/features/automations",
"v1.14.7/ar/enterprise/features/crew-studio",
"v1.14.7/ar/enterprise/features/merged-step-card",
{
"group": "استوديو الطاقم",
"icon": "pencil",
"pages": [
"v1.14.7/ar/enterprise/features/crew-studio",
"v1.14.7/ar/enterprise/features/merged-step-card"
]
},
"v1.14.7/ar/enterprise/features/marketplace",
"v1.14.7/ar/enterprise/features/agent-repositories",
"v1.14.7/ar/enterprise/features/tools-and-integrations",

View File

@@ -4,6 +4,32 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
<Update label="24 يونيو 2026">
## v1.14.8a5
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a5)
## ما الذي تغير
### الميزات
- جعل المراجع التصريحية تعمل عبر التدفقات والفرق (#6326)
### إصلاحات الأخطاء
- إصلاح مدخلات بدء حالة تدفق مخطط JSON (#6325)
### الوثائق
- وضع بطاقة واحدة لكل خطوة تحت استوديو الفريق وإزالة لافتة التوزيع (AGE-107) (#6317)
- تحديث اللقطة وسجل التغييرات للإصدار v1.14.8a4 (#6319)
### إعادة الهيكلة
- إزالة `StateProxy` من الوصول إلى حالة التدفق (#6327)
## المساهمون
@jessemiller, @vinibrsl
</Update>
<Update label="24 يونيو 2026">
## v1.14.8a4

View File

@@ -5,11 +5,6 @@ icon: "layer-group"
mode: "wide"
---
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
<Note>
**الإطلاق يوم الأربعاء 24 يونيو.** تنتقل لوحة Studio إلى بطاقة واحدة لكل خطوة بدلاً من عُقد منفصلة للمهمة والوكيل، وذلك لتبسيط اللوحة مع إضافتنا لوظائف جديدة قريبًا. تستمر أتمتتك الحالية في العمل دون أي تغييرات مطلوبة — تبقى جميع إعدادات المهمة والوكيل متاحة، ولكن منظّمة في بطاقة واحدة.
</Note>
## نظرة عامة
على لوحة Studio، تُمثَّل كل خطوة عمل بـ **بطاقة واحدة**. تجمع البطاقة بين عنصرين كانا في السابق في عُقد منفصلة:

View File

@@ -4,6 +4,32 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="Jun 24, 2026">
## v1.14.8a5
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a5)
## What's Changed
### Features
- Make declarative refs work across flows and crews (#6326)
### Bug Fixes
- Fix JSON schema flow state kickoff inputs (#6325)
### Documentation
- Nest One Card per Step under Crew Studio and drop rollout banner (AGE-107) (#6317)
- Update snapshot and changelog for v1.14.8a4 (#6319)
### Refactoring
- Remove `StateProxy` from flow state access (#6327)
## Contributors
@jessemiller, @vinibrsl
</Update>
<Update label="Jun 24, 2026">
## v1.14.8a4

View File

@@ -5,11 +5,6 @@ icon: "layer-group"
mode: "wide"
---
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
<Note>
**Rolling out Wednesday, June 24th.** The Studio canvas is moving to one card per step instead of separate task and agent nodes, to streamline the canvas as we add new functionality soon. Your existing automations keep working with no changes needed — every task and agent setting is still available, just organized onto a single card.
</Note>
## Overview
On the Studio canvas, each step of work is represented by a **single card**. The card combines two things that used to live in separate nodes:

View File

@@ -4,6 +4,32 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
<Update label="2026년 6월 24일">
## v1.14.8a5
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a5)
## 변경 사항
### 기능
- 선언적 참조가 흐름과 크루 간에 작동하도록 수정 (#6326)
### 버그 수정
- JSON 스키마 흐름 상태 시작 입력 수정 (#6325)
### 문서
- Crew Studio 아래에 단계별 One Card를 중첩하고 롤아웃 배너 제거 (AGE-107) (#6317)
- v1.14.8a4의 스냅샷 및 변경 로그 업데이트 (#6319)
### 리팩토링
- 흐름 상태 접근에서 `StateProxy` 제거 (#6327)
## 기여자
@jessemiller, @vinibrsl
</Update>
<Update label="2026년 6월 24일">
## v1.14.8a4

View File

@@ -5,11 +5,6 @@ icon: "layer-group"
mode: "wide"
---
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
<Note>
**6월 24일 수요일 출시.** Studio 캔버스가 작업과 에이전트를 별도의 노드로 표시하는 대신 단계당 하나의 카드로 전환됩니다. 곧 추가될 새로운 기능을 위해 캔버스를 간소화하기 위한 변경입니다. 기존 자동화는 아무런 변경 없이 그대로 동작하며, 모든 작업 및 에이전트 설정은 단일 카드에 정리되어 그대로 사용할 수 있습니다.
</Note>
## 개요
Studio 캔버스에서 각 작업 단계는 **하나의 카드**로 표현됩니다. 이 카드는 이전에 별도의 노드에 있던 두 가지를 결합합니다:

View File

@@ -4,6 +4,32 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="24 jun 2026">
## v1.14.8a5
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a5)
## O que mudou
### Recursos
- Fazer referências declarativas funcionarem em diferentes fluxos e equipes (#6326)
### Correções de Bugs
- Corrigir entradas de kickoff de estado de fluxo do esquema JSON (#6325)
### Documentação
- Aninhar One Card por Etapa sob Crew Studio e remover banner de rollout (AGE-107) (#6317)
- Atualizar snapshot e changelog para v1.14.8a4 (#6319)
### Refatoração
- Remover `StateProxy` do acesso ao estado de fluxo (#6327)
## Contribuidores
@jessemiller, @vinibrsl
</Update>
<Update label="24 jun 2026">
## v1.14.8a4

View File

@@ -5,11 +5,6 @@ icon: "layer-group"
mode: "wide"
---
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
<Note>
**Lançamento na quarta-feira, 24 de junho.** O canvas do Studio passa a exibir um card por etapa, em vez de nós separados para tarefa e agente, para simplificar o canvas à medida que adicionamos novas funcionalidades em breve. Suas automações existentes continuam funcionando sem nenhuma alteração necessária — cada configuração de tarefa e de agente continua disponível, apenas organizada em um único card.
</Note>
## Visão geral
No canvas do Studio, cada etapa de trabalho é representada por um **único card**. O card combina dois elementos que antes ficavam em nós separados:

View File

@@ -5,11 +5,6 @@ icon: "layer-group"
mode: "wide"
---
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
<Note>
**الإطلاق يوم الأربعاء 24 يونيو.** تنتقل لوحة Studio إلى بطاقة واحدة لكل خطوة بدلاً من عُقد منفصلة للمهمة والوكيل، وذلك لتبسيط اللوحة مع إضافتنا لوظائف جديدة قريبًا. تستمر أتمتتك الحالية في العمل دون أي تغييرات مطلوبة — تبقى جميع إعدادات المهمة والوكيل متاحة، ولكن منظّمة في بطاقة واحدة.
</Note>
## نظرة عامة
على لوحة Studio، تُمثَّل كل خطوة عمل بـ **بطاقة واحدة**. تجمع البطاقة بين عنصرين كانا في السابق في عُقد منفصلة:

View File

@@ -5,11 +5,6 @@ icon: "layer-group"
mode: "wide"
---
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
<Note>
**Rolling out Wednesday, June 24th.** The Studio canvas is moving to one card per step instead of separate task and agent nodes, to streamline the canvas as we add new functionality soon. Your existing automations keep working with no changes needed — every task and agent setting is still available, just organized onto a single card.
</Note>
## Overview
On the Studio canvas, each step of work is represented by a **single card**. The card combines two things that used to live in separate nodes:

View File

@@ -5,11 +5,6 @@ icon: "layer-group"
mode: "wide"
---
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
<Note>
**6월 24일 수요일 출시.** Studio 캔버스가 작업과 에이전트를 별도의 노드로 표시하는 대신 단계당 하나의 카드로 전환됩니다. 곧 추가될 새로운 기능을 위해 캔버스를 간소화하기 위한 변경입니다. 기존 자동화는 아무런 변경 없이 그대로 동작하며, 모든 작업 및 에이전트 설정은 단일 카드에 정리되어 그대로 사용할 수 있습니다.
</Note>
## 개요
Studio 캔버스에서 각 작업 단계는 **하나의 카드**로 표현됩니다. 이 카드는 이전에 별도의 노드에 있던 두 가지를 결합합니다:

View File

@@ -5,11 +5,6 @@ icon: "layer-group"
mode: "wide"
---
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
<Note>
**Lançamento na quarta-feira, 24 de junho.** O canvas do Studio passa a exibir um card por etapa, em vez de nós separados para tarefa e agente, para simplificar o canvas à medida que adicionamos novas funcionalidades em breve. Suas automações existentes continuam funcionando sem nenhuma alteração necessária — cada configuração de tarefa e de agente continua disponível, apenas organizada em um único card.
</Note>
## Visão geral
No canvas do Studio, cada etapa de trabalho é representada por um **único card**. O card combina dois elementos que antes ficavam em nós separados:

View File

@@ -8,7 +8,7 @@ authors = [
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai-core==1.14.8a4",
"crewai-core==1.14.8a5",
"click>=8.1.7,<9",
"pydantic>=2.11.9,<2.13",
"pydantic-settings~=2.10.1",

View File

@@ -1 +1 @@
__version__ = "1.14.8a4"
__version__ = "1.14.8a5"

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.14.8a4"
"crewai[tools]==1.14.8a5"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.14.8a4"
"crewai[tools]==1.14.8a5"
]
[build-system]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.14.8a4"
"crewai[tools]==1.14.8a5"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
readme = "README.md"
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.14.8a4"
"crewai[tools]==1.14.8a5"
]
[tool.crewai]

View File

@@ -1 +1 @@
__version__ = "1.14.8a4"
__version__ = "1.14.8a5"

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.14.8a4"
__version__ = "1.14.8a5"

View File

@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
dependencies = [
"pytube~=15.0.0",
"requests>=2.33.0,<3",
"crewai==1.14.8a4",
"crewai==1.14.8a5",
"tiktoken>=0.8.0,<0.13",
"beautifulsoup4~=4.13.4",
"python-docx~=1.2.0",

View File

@@ -330,4 +330,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.14.8a4"
__version__ = "1.14.8a5"

View File

@@ -8,8 +8,8 @@ authors = [
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai-core==1.14.8a4",
"crewai-cli==1.14.8a4",
"crewai-core==1.14.8a5",
"crewai-cli==1.14.8a5",
# Core Dependencies
"pydantic>=2.11.9,<2.13",
"openai>=2.30.0,<3",
@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.14.8a4",
"crewai-tools==1.14.8a5",
]
embeddings = [
"tiktoken>=0.8.0,<0.13"

View File

@@ -48,7 +48,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.14.8a4"
__version__ = "1.14.8a5"
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
"Memory": ("crewai.memory.unified_memory", "Memory"),

View File

@@ -54,7 +54,7 @@ from crewai.events.types.tool_usage_events import (
ToolUsageFinishedEvent,
ToolUsageStartedEvent,
)
from crewai.flow.flow import Flow, StateProxy, listen, or_, router, start
from crewai.flow.flow import Flow, listen, or_, router, start
from crewai.flow.types import FlowMethodName
from crewai.hooks.llm_hooks import (
get_after_llm_call_hooks,
@@ -276,11 +276,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
"""
return self.llm.supports_stop_words() if self.llm else False
@property
def state(self) -> AgentExecutorState:
"""Get thread-safe state proxy."""
return StateProxy(self._state, self._state_lock) # type: ignore[return-value]
@property # type: ignore[misc]
def iterations(self) -> int:
"""Compatibility property for mixin - returns state iterations."""

View File

@@ -24,9 +24,6 @@ from crewai.flow.runtime import (
Flow as RuntimeFlow,
FlowMeta,
FlowState,
LockedDictProxy,
LockedListProxy,
StateProxy,
)
@@ -42,9 +39,6 @@ __all__ = [
"Flow",
"FlowMeta",
"FlowState",
"LockedDictProxy",
"LockedListProxy",
"StateProxy",
"and_",
"listen",
"or_",

View File

@@ -1,8 +1,8 @@
"""Flow Runtime: the engine that executes a Flow.
Provides the ``Flow`` class (kickoff/resume/listener dispatch), the
``FlowMeta`` metaclass, and the thread-safe state proxies. Flows
authored with the Python DSL (see ``dsl``) are described by a Flow
Provides the ``Flow`` class (kickoff/resume/listener dispatch) and the
``FlowMeta`` metaclass. Flows authored with the Python DSL (see ``dsl``)
are described by a Flow
Structure (see ``flow_definition``) and executed here.
"""
@@ -11,12 +11,8 @@ from __future__ import annotations
import asyncio
from collections.abc import (
Callable,
ItemsView,
Iterable,
Iterator,
KeysView,
Sequence,
ValuesView,
)
from concurrent.futures import Future, ThreadPoolExecutor
import contextvars
@@ -35,10 +31,8 @@ from typing import (
Generic,
Literal,
ParamSpec,
SupportsIndex,
TypeVar,
cast,
overload,
)
from uuid import uuid4
@@ -123,7 +117,6 @@ from crewai.flow.human_feedback import (
from crewai.flow.input_provider import InputProvider
from crewai.flow.persistence.base import FlowPersistence
from crewai.flow.runtime._actions import FlowScriptExecutionDisabledError, build_action
from crewai.flow.runtime._refs import resolve_instance_ref, resolve_ref
from crewai.flow.types import (
FlowExecutionData,
FlowMethodName,
@@ -137,6 +130,7 @@ from crewai.state.checkpoint_config import (
_coerce_checkpoint,
apply_checkpoint,
)
from crewai.utilities.declarative_refs import InvalidRefError, resolve_ref
if TYPE_CHECKING:
@@ -227,7 +221,12 @@ def _build_definition_state_model(
pass
model_class = StateWithId
return model_class(**kwargs)
try:
return model_class(**kwargs)
except ValidationError as e:
if any(error.get("type") != "missing" for error in e.errors()):
raise
return model_class.model_construct(**kwargs)
def _iter_condition_events(condition: FlowDefinitionCondition) -> Iterator[str]:
@@ -284,6 +283,18 @@ def _resolve_persistence(value: Any) -> Any:
return value
def _resolve_instance_ref(ref: str, *, field: str) -> Any:
target = resolve_ref(ref, field=field)
if not inspect.isclass(target):
return target
try:
return target()
except Exception as e:
raise InvalidRefError(
f"cannot instantiate {field} ref {ref!r} without arguments: {e}"
) from e
def _serialize_persistence(value: Any) -> dict[str, Any] | None:
if value is None:
return None
@@ -299,7 +310,7 @@ def _validate_input_provider(value: Any) -> Any:
if value is None or isinstance(value, InputProvider):
return value
if isinstance(value, str) and ":" in value:
resolved = resolve_instance_ref(value, field="input_provider")
resolved = _resolve_instance_ref(value, field="input_provider")
else:
from crewai.types.callback import _dotted_path_to_instance
@@ -366,304 +377,6 @@ R = TypeVar("R")
F = TypeVar("F", bound=Callable[..., Any])
class LockedListProxy(list, Generic[T]): # type: ignore[type-arg]
"""Thread-safe proxy for list operations.
Subclasses ``list`` so that ``isinstance(proxy, list)`` returns True,
which is required by libraries like LanceDB and Pydantic that do strict
type checks. All mutations go through the lock; reads delegate to the
underlying list.
"""
def __init__(self, lst: list[T], lock: threading.Lock) -> None:
super().__init__() # empty builtin list; all access goes through self._list
self._list = lst
self._lock = lock
def append(self, item: T) -> None:
with self._lock:
self._list.append(item)
def extend(self, items: Iterable[T]) -> None:
with self._lock:
self._list.extend(items)
def insert(self, index: SupportsIndex, item: T) -> None:
with self._lock:
self._list.insert(index, item)
def remove(self, item: T) -> None:
with self._lock:
self._list.remove(item)
def pop(self, index: SupportsIndex = -1) -> T:
with self._lock:
return self._list.pop(index)
def clear(self) -> None:
with self._lock:
self._list.clear()
@overload
def __setitem__(self, index: SupportsIndex, value: T) -> None: ...
@overload
def __setitem__(self, index: slice, value: Iterable[T]) -> None: ...
def __setitem__(self, index: Any, value: Any) -> None:
with self._lock:
self._list[index] = value
def __delitem__(self, index: SupportsIndex | slice) -> None:
with self._lock:
del self._list[index]
@overload
def __getitem__(self, index: SupportsIndex) -> T: ...
@overload
def __getitem__(self, index: slice) -> list[T]: ...
def __getitem__(self, index: Any) -> Any:
return self._list[index]
def __len__(self) -> int:
return len(self._list)
def __iter__(self) -> Iterator[T]:
return iter(self._list)
def __contains__(self, item: object) -> bool:
return item in self._list
def __repr__(self) -> str:
return repr(self._list)
def __bool__(self) -> bool:
return bool(self._list)
def index(
self, value: T, start: SupportsIndex = 0, stop: SupportsIndex | None = None
) -> int:
if stop is None:
return self._list.index(value, start)
return self._list.index(value, start, stop)
def count(self, value: T) -> int:
return self._list.count(value)
def sort(self, *, key: Any = None, reverse: bool = False) -> None:
with self._lock:
self._list.sort(key=key, reverse=reverse)
def reverse(self) -> None:
with self._lock:
self._list.reverse()
def copy(self) -> list[T]:
return self._list.copy()
def __add__(self, other: list[T]) -> list[T]: # type: ignore[override]
return self._list + other
def __radd__(self, other: list[T]) -> list[T]:
return other + self._list
def __iadd__(self, other: Iterable[T]) -> LockedListProxy[T]: # type: ignore[override]
with self._lock:
self._list += list(other)
return self
def __mul__(self, n: SupportsIndex) -> list[T]:
return self._list * n
def __rmul__(self, n: SupportsIndex) -> list[T]:
return self._list * n
def __imul__(self, n: SupportsIndex) -> LockedListProxy[T]:
with self._lock:
self._list *= n
return self
def __reversed__(self) -> Iterator[T]:
return reversed(self._list)
def __eq__(self, other: object) -> bool:
"""Compare based on the underlying list contents."""
if isinstance(other, LockedListProxy):
# Avoid deadlocks by acquiring locks in a consistent order.
first, second = (self, other) if id(self) <= id(other) else (other, self)
with first._lock:
with second._lock:
return first._list == second._list
with self._lock:
return self._list == other
def __ne__(self, other: object) -> bool:
return not self.__eq__(other)
class LockedDictProxy(dict, Generic[T]): # type: ignore[type-arg]
"""Thread-safe proxy for dict operations.
Subclasses ``dict`` so that ``isinstance(proxy, dict)`` returns True,
which is required by libraries like Pydantic that do strict type checks.
All mutations go through the lock; reads delegate to the underlying dict.
"""
def __init__(self, d: dict[str, T], lock: threading.Lock) -> None:
super().__init__() # empty builtin dict; all access goes through self._dict
self._dict = d
self._lock = lock
def __setitem__(self, key: str, value: T) -> None:
with self._lock:
self._dict[key] = value
def __delitem__(self, key: str) -> None:
with self._lock:
del self._dict[key]
def pop(self, key: str, *default: T) -> T: # type: ignore[override]
with self._lock:
return self._dict.pop(key, *default)
def update(self, other: dict[str, T]) -> None: # type: ignore[override]
with self._lock:
self._dict.update(other)
def clear(self) -> None:
with self._lock:
self._dict.clear()
def setdefault(self, key: str, default: T) -> T: # type: ignore[override]
with self._lock:
return self._dict.setdefault(key, default)
def __getitem__(self, key: str) -> T:
return self._dict[key]
def __len__(self) -> int:
return len(self._dict)
def __iter__(self) -> Iterator[str]:
return iter(self._dict)
def __contains__(self, key: object) -> bool:
return key in self._dict
def keys(self) -> KeysView[str]: # type: ignore[override]
return self._dict.keys()
def values(self) -> ValuesView[T]: # type: ignore[override]
return self._dict.values()
def items(self) -> ItemsView[str, T]: # type: ignore[override]
return self._dict.items()
def get(self, key: str, default: T | None = None) -> T | None: # type: ignore[override]
return self._dict.get(key, default)
def __repr__(self) -> str:
return repr(self._dict)
def __bool__(self) -> bool:
return bool(self._dict)
def copy(self) -> dict[str, T]:
return self._dict.copy()
def __or__(self, other: dict[str, T]) -> dict[str, T]: # type: ignore[override]
return self._dict | other
def __ror__(self, other: dict[str, T]) -> dict[str, T]: # type: ignore[override]
return other | self._dict
def __ior__(self, other: dict[str, T]) -> LockedDictProxy[T]: # type: ignore[override]
with self._lock:
self._dict |= other
return self
def __reversed__(self) -> Iterator[str]:
return reversed(self._dict)
def __eq__(self, other: object) -> bool:
"""Compare based on the underlying dict contents."""
if isinstance(other, LockedDictProxy):
# Avoid deadlocks by acquiring locks in a consistent order.
first, second = (self, other) if id(self) <= id(other) else (other, self)
with first._lock:
with second._lock:
return first._dict == second._dict
with self._lock:
return self._dict == other
def __ne__(self, other: object) -> bool:
return not self.__eq__(other)
class StateProxy(Generic[T]):
"""Proxy that provides thread-safe access to flow state.
Wraps state objects (dict or BaseModel) and uses a lock for all write
operations to prevent race conditions when parallel listeners modify state.
"""
__slots__ = ("_proxy_lock", "_proxy_state")
def __init__(self, state: T, lock: threading.Lock) -> None:
object.__setattr__(self, "_proxy_state", state)
object.__setattr__(self, "_proxy_lock", lock)
def __getattr__(self, name: str) -> Any:
value = getattr(object.__getattribute__(self, "_proxy_state"), name)
lock = object.__getattribute__(self, "_proxy_lock")
if isinstance(value, list):
return LockedListProxy(value, lock)
if isinstance(value, dict):
return LockedDictProxy(value, lock)
return value
def __setattr__(self, name: str, value: Any) -> None:
if name in ("_proxy_state", "_proxy_lock"):
object.__setattr__(self, name, value)
else:
if isinstance(value, LockedListProxy):
value = value._list
elif isinstance(value, LockedDictProxy):
value = value._dict
with object.__getattribute__(self, "_proxy_lock"):
setattr(object.__getattribute__(self, "_proxy_state"), name, value)
def __getitem__(self, key: str) -> Any:
return object.__getattribute__(self, "_proxy_state")[key]
def __setitem__(self, key: str, value: Any) -> None:
with object.__getattribute__(self, "_proxy_lock"):
object.__getattribute__(self, "_proxy_state")[key] = value
def __delitem__(self, key: str) -> None:
with object.__getattribute__(self, "_proxy_lock"):
del object.__getattribute__(self, "_proxy_state")[key]
def __contains__(self, key: str) -> bool:
return key in object.__getattribute__(self, "_proxy_state")
def __repr__(self) -> str:
return repr(object.__getattribute__(self, "_proxy_state"))
def _unwrap(self) -> T:
"""Return the underlying state object."""
return cast(T, object.__getattribute__(self, "_proxy_state"))
def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
"""Return state as a dictionary.
Works for both dict and BaseModel underlying states.
"""
state = object.__getattribute__(self, "_proxy_state")
if isinstance(state, dict):
return state
result: dict[str, Any] = state.model_dump(*args, **kwargs)
return result
class FlowMeta(ModelMetaclass):
def __new__(
mcs,
@@ -1008,7 +721,6 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
)
_method_outputs: list[Any] = PrivateAttr(default_factory=list)
_definition: FlowDefinition = PrivateAttr()
_state_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
_or_listeners_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
_completed_methods: set[FlowMethodName] = PrivateAttr(default_factory=set)
_method_call_counts: dict[FlowMethodName, int] = PrivateAttr(default_factory=dict)
@@ -1930,7 +1642,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
@property
def state(self) -> T:
return StateProxy(self._state, self._state_lock) # type: ignore[return-value]
return cast(T, self._state)
@property
def method_outputs(self) -> list[Any]:
@@ -3600,7 +3312,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
) -> Any:
provider = feedback_definition.provider
if isinstance(provider, str):
provider = resolve_instance_ref(provider, field="human_feedback.provider")
provider = _resolve_instance_ref(provider, field="human_feedback.provider")
if provider is None:
from crewai.flow.flow_config import flow_config

View File

@@ -24,7 +24,11 @@ from crewai.flow.flow_definition import (
FlowToolActionDefinition,
)
from crewai.flow.runtime._outputs import outputs_by_name
from crewai.flow.runtime._refs import InvalidRefError, resolve_ref
from crewai.utilities.declarative_refs import (
InvalidRefError,
resolve_class_ref,
resolve_ref,
)
if TYPE_CHECKING:
@@ -103,16 +107,17 @@ class ToolAction:
)
def _build_tool(self) -> Any:
target = resolve_ref(self.definition.ref, field="do")
from crewai.tools import BaseTool
if not (inspect.isclass(target) and issubclass(target, BaseTool)):
raise InvalidRefError(
f"invalid tool ref {self.definition.ref!r}; expected a BaseTool class"
)
tool_cls = cast(
Callable[[], BaseTool],
resolve_class_ref(
self.definition.ref,
field="do",
base_class=BaseTool,
),
)
try:
tool_cls = cast(Callable[[], BaseTool], target)
return tool_cls()
except Exception as e:
raise InvalidRefError(

View File

@@ -1,38 +0,0 @@
"""Resolution of ``module:qualname`` refs into live Python objects."""
from __future__ import annotations
import importlib
import inspect
from operator import attrgetter
from typing import Any
class InvalidRefError(ValueError):
"""A definition ref that cannot be resolved to a live object."""
def resolve_ref(ref: str, *, field: str) -> Any:
"""Import the object a definition's `module:qualname` ref points to."""
module_name, _, qualname = ref.partition(":")
if "<" in ref or not module_name or not qualname:
raise InvalidRefError(
f"invalid {field} ref {ref!r}; expected 'module:qualname'"
)
try:
return attrgetter(qualname)(importlib.import_module(module_name))
except (ImportError, AttributeError) as e:
raise InvalidRefError(f"unresolvable {field} ref {ref!r}") from e
def resolve_instance_ref(ref: str, *, field: str) -> Any:
"""Resolve a ref, auto-instantiating a no-arg class into an instance."""
target = resolve_ref(ref, field=field)
if not inspect.isclass(target):
return target
try:
return target()
except Exception as e:
raise InvalidRefError(
f"cannot instantiate {field} ref {ref!r} without arguments: {e}"
) from e

View File

@@ -16,6 +16,8 @@ from urllib.parse import unquote, urlparse
from pydantic import BaseModel, ValidationError
from crewai.utilities.declarative_refs import InvalidRefError, resolve_class_ref
logger = logging.getLogger(__name__)
@@ -1820,6 +1822,9 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li
if tool_def.startswith("custom:"):
tools.append(_resolve_custom_tool(tool_def[7:], project_root=project_root))
continue
if ":" in tool_def:
tools.append(_instantiate_tool_import_ref(tool_def))
continue
try:
tool_cls = _find_tool_class(tool_def)
except Exception as e:
@@ -1827,8 +1832,10 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li
if tool_cls is None:
raise JSONProjectError(
f"Unknown tool '{tool_def}'. Tool names must match a class from "
f"the 'crewai_tools' package (e.g. 'SerperDevTool') or use the "
f"'custom:<name>' prefix for a tool defined in tools/<name>.py."
f"the 'crewai_tools' package (e.g. 'SerperDevTool'), use a "
f"'module:ClassName' import ref (e.g. 'crewai_tools:SerperDevTool'), "
f"or use the 'custom:<name>' prefix for a tool defined in "
f"tools/<name>.py."
)
try:
tools.append(tool_cls())
@@ -1839,6 +1846,32 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li
return tools
def _instantiate_tool_import_ref(ref: str) -> Any:
from crewai.tools import BaseTool
try:
tool_cls = cast(
Callable[[], BaseTool],
resolve_class_ref(ref, field="tool", base_class=BaseTool),
)
except InvalidRefError as e:
message = str(e)
if (
message.startswith("unresolvable ")
or "expected 'module:qualname'" in message
):
raise JSONProjectError(str(e)) from e
raise JSONProjectError(
f"invalid tool ref {ref!r}; expected a BaseTool class"
) from e
try:
return tool_cls()
except Exception as e:
raise JSONProjectError(
f"cannot instantiate tool ref {ref!r} without arguments: {e}"
) from e
_tool_class_cache: dict[str, type | None] = {}

View File

@@ -0,0 +1,69 @@
"""Resolve Python refs used in project definitions.
A ref must use this form: ``module:qualname``. ``module`` must name a Python
module we can import. ``qualname`` must name something inside that module. For
example, ``crewai_tools:SerperDevTool`` imports ``crewai_tools`` and returns
``SerperDevTool`` from it. Dots in ``qualname`` mean nested attributes.
Examples:
- ``crewai_tools:SerperDevTool`` imports ``crewai_tools`` and returns
``SerperDevTool``.
- ``my_app.tools:Factory.build`` imports ``my_app.tools``, gets ``Factory``,
then gets ``build`` from ``Factory``.
- ``crewai_tools`` is invalid because it has no ``:``.
- ``crewai_tools:`` is invalid because it has no ``qualname``.
These helpers are the shared contract for YAML/JSON definitions:
- ``resolve_ref()`` checks the ref, imports the module, and returns the symbol
as-is.
- ``resolve_class_ref()`` does the same work, then checks that the symbol is a
class. It can also check that the class extends a base class. It does not
create an object.
These helpers import user code. Code that must avoid that should check the raw
string shape instead.
"""
from __future__ import annotations
import importlib
import inspect
from operator import attrgetter
from typing import Any
class InvalidRefError(ValueError):
"""A definition ref that cannot be resolved to a live Python symbol."""
def resolve_ref(ref: str, *, field: str) -> Any:
"""Return the Python symbol named by a project definition field."""
module_name, _, qualname = ref.partition(":")
if "<" in ref or not module_name or not qualname:
raise InvalidRefError(
f"invalid {field} ref {ref!r}; expected 'module:qualname'"
)
try:
return attrgetter(qualname)(importlib.import_module(module_name))
except (ImportError, AttributeError) as e:
raise InvalidRefError(f"unresolvable {field} ref {ref!r}") from e
def resolve_class_ref(
ref: str,
*,
field: str,
base_class: type[Any] | None = None,
) -> type[Any]:
"""Return the named class, with an optional base class check."""
target = resolve_ref(ref, field=field)
if not inspect.isclass(target):
raise InvalidRefError(f"invalid {field} ref {ref!r}; expected a class")
if base_class is not None and not issubclass(target, base_class):
raise InvalidRefError(
f"invalid {field} ref {ref!r}; expected a subclass of "
f"{base_class.__module__}.{base_class.__name__}"
)
return target

View File

@@ -7,6 +7,7 @@ flow methods, routing logic, and error handling.
from __future__ import annotations
import asyncio
import threading
from types import SimpleNamespace
import time
from typing import Any
@@ -39,8 +40,6 @@ def _build_executor(**kwargs: Any) -> AgentExecutor:
executor._human_feedback_method_outputs = {}
executor._input_history = []
executor._is_execution_resuming = False
import threading
executor._state_lock = threading.Lock()
executor._or_listeners_lock = threading.Lock()
executor._execution_lock = threading.Lock()
executor._finalize_lock = threading.Lock()

View File

@@ -385,12 +385,52 @@ class TestLoadAgentFromDefinition:
class TestResolveTools:
def test_import_ref_tool_resolves(self, tmp_path, monkeypatch):
from crewai.project.json_loader import _resolve_tools
(tmp_path / "project_tools.py").write_text(
"from crewai.tools.base_tool import BaseTool\n"
"\n"
"class LookupTool(BaseTool):\n"
" name: str = 'lookup'\n"
" description: str = 'lookup input'\n"
"\n"
" def _run(self, text: str) -> str:\n"
" return text\n"
)
monkeypatch.syspath_prepend(str(tmp_path))
tools = _resolve_tools(["project_tools:LookupTool"])
assert len(tools) == 1
assert tools[0].name == "lookup"
def test_unknown_tool_raises_with_guidance(self):
from crewai.project.json_loader import JSONProjectError, _resolve_tools
with pytest.raises(JSONProjectError, match="Unknown tool 'NotARealToolXYZ'"):
_resolve_tools(["NotARealToolXYZ"])
def test_import_ref_tool_must_resolve_to_basetool_class(
self, tmp_path, monkeypatch
):
from crewai.project.json_loader import JSONProjectError, _resolve_tools
(tmp_path / "not_tools.py").write_text(
"class NotATool:\n"
" pass\n"
)
monkeypatch.syspath_prepend(str(tmp_path))
with pytest.raises(JSONProjectError, match="expected a BaseTool class"):
_resolve_tools(["not_tools:NotATool"])
def test_unresolvable_import_ref_tool_raises_guidance(self):
from crewai.project.json_loader import JSONProjectError, _resolve_tools
with pytest.raises(JSONProjectError, match="unresolvable tool ref"):
_resolve_tools(["not_a_real_module:MissingTool"])
def test_missing_custom_tool_raises(self, tmp_path, monkeypatch):
from crewai.project.json_loader import JSONProjectError, _resolve_tools
@@ -505,6 +545,30 @@ class TestValidationDoesNotExecuteTools:
assert not sentinel.exists(), "validation must not import Python refs"
def test_validate_does_not_import_tool_refs(
self, tmp_path, monkeypatch: pytest.MonkeyPatch
):
from crewai.project.json_loader import validate_crew_project
sentinel = tmp_path / "tool_ref_executed.txt"
(tmp_path / "project_tools.py").write_text(
"from pathlib import Path\n"
f"Path({str(sentinel)!r}).write_text('boom')\n"
"from crewai.tools.base_tool import BaseTool\n"
"class LookupTool(BaseTool):\n"
" name: str = 'lookup'\n"
" description: str = 'lookup input'\n"
" def _run(self, text: str) -> str:\n"
" return text\n"
)
monkeypatch.syspath_prepend(str(tmp_path))
sys.modules.pop("project_tools", None)
crew_path = self._write_project(tmp_path, tool_line='"project_tools:LookupTool"')
validate_crew_project(crew_path, tmp_path / "agents")
assert not sentinel.exists(), "validation must not import tool refs"
def test_validate_reports_missing_custom_tool_file(self, tmp_path):
from crewai.project.json_loader import (
JSONProjectValidationError,

View File

@@ -1510,42 +1510,36 @@ def test_conditional_router_events_exclusivity():
assert "handle_event_c" not in execution_order
def test_state_consistency_across_parallel_branches():
"""Test that state remains consistent when branches execute in parallel.
def test_and_join_waits_for_parallel_branches():
"""Test that sibling branches complete before a joined listener runs.
Note: Branches triggered by the same parent execute in parallel for efficiency.
Thread-safe state access via StateProxy ensures no race conditions.
We check the execution order to ensure the branches execute in parallel.
Branches triggered by the same parent execute in parallel for efficiency.
Shared state updates are not guaranteed to be atomic, so this test uses a
locked local recorder instead of branch state mutation.
"""
execution_order = []
execution_order_lock = threading.Lock()
def record(method_name: str) -> None:
with execution_order_lock:
execution_order.append(method_name)
class StateConsistencyFlow(Flow):
def __init__(self):
super().__init__()
self.state["counter"] = 0
self.state["branch_a_value"] = None
self.state["branch_b_value"] = None
@start()
def init(self):
execution_order.append("init")
self.state["counter"] = 10
record("init")
@listen(init)
def branch_a(self):
execution_order.append("branch_a")
self.state["branch_a_value"] = self.state["counter"]
self.state["counter"] += 1
record("branch_a")
@listen(init)
def branch_b(self):
execution_order.append("branch_b")
self.state["branch_b_value"] = self.state["counter"]
self.state["counter"] += 5
record("branch_b")
@listen(and_(branch_a, branch_b))
def verify_state(self):
execution_order.append("verify_state")
record("verify_state")
flow = StateConsistencyFlow()
flow.kickoff()
@@ -1554,10 +1548,8 @@ def test_state_consistency_across_parallel_branches():
assert "branch_b" in execution_order
assert "verify_state" in execution_order
assert flow.state["branch_a_value"] is not None
assert flow.state["branch_b_value"] is not None
assert flow.state["counter"] == 16
assert execution_order.index("branch_a") < execution_order.index("verify_state")
assert execution_order.index("branch_b") < execution_order.index("verify_state")
def test_deeply_nested_conditions():

View File

@@ -928,8 +928,6 @@ class TestConversationalFlow:
conversational = True
flow = BareChat()
# ``flow.state`` returns a ``StateProxy``; the underlying state is
# on ``flow._state``. Both views expose the same chat-shaped fields.
assert isinstance(flow._state, ConversationState)
assert flow.state.messages == []
assert flow.state.current_user_message is None

View File

@@ -357,6 +357,27 @@ methods:
listen: begin
"""
JSON_SCHEMA_REQUIRED_INPUT_STATE_YAML = """
schema: crewai.flow/v1
name: JsonSchemaRequiredInputStateFlow
state:
type: json_schema
json_schema:
title: LeadState
type: object
required:
- lead_name
properties:
lead_name:
type: string
methods:
begin:
start: true
do:
call: expression
expr: state.lead_name
"""
PYDANTIC_REF_WITH_SCHEMA_FALLBACK_YAML = f"""
schema: crewai.flow/v1
name: SchemaFallbackFlow
@@ -445,7 +466,8 @@ def _run_with_events(flow, inputs=None):
def _state_without_id(flow):
snapshot = dict(flow.state.model_dump())
state = flow.state
snapshot = dict(state if isinstance(state, dict) else state.model_dump())
snapshot.pop("id", None)
return snapshot
@@ -2298,6 +2320,18 @@ def test_json_schema_state_validates_inputs():
flow.kickoff(inputs={"count": "not-a-number"})
def test_json_schema_state_required_fields_can_come_from_kickoff_inputs():
flow = Flow.from_definition(
FlowDefinition.from_declaration(contents=JSON_SCHEMA_REQUIRED_INPUT_STATE_YAML)
)
result = flow.kickoff(inputs={"lead_name": "Ada Lovelace"})
assert result == "Ada Lovelace"
assert flow.state.lead_name == "Ada Lovelace"
assert flow.state.id
def test_pydantic_state_falls_back_to_json_schema_when_ref_unimportable():
flow = Flow.from_definition(
FlowDefinition.from_declaration(contents=PYDANTIC_REF_WITH_SCHEMA_FALLBACK_YAML)

View File

@@ -233,7 +233,7 @@ def test_persistence_with_base_model(tmp_path):
assert message.role == "user"
assert message.type == "text"
assert message.content == "Hello, World!"
assert isinstance(flow.state._unwrap(), State)
assert isinstance(flow.state, State)
def test_fork_with_restore_from_state_id(tmp_path):

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.14.8a4"
__version__ = "1.14.8a5"