Merge branch 'main' into fix/file-tools-path-allowlist

This commit is contained in:
Rip&Tear
2026-06-25 12:02:07 +08:00
committed by GitHub
91 changed files with 3558 additions and 1449 deletions

View File

@@ -13,6 +13,9 @@ on:
- "docs.json"
workflow_dispatch:
permissions:
contents: read
jobs:
check-links:
name: Check broken links

View File

@@ -134,17 +134,21 @@ def bedrock_host_matcher(r1: Request, r2: Request) -> bool: # type: ignore[no-a
)
def _patched_make_vcr_request(httpx_request: Any, **kwargs: Any) -> Any:
def _patched_make_vcr_request(
httpx_request: Any, real_request_body: Any = None, **kwargs: Any
) -> Any:
"""Patched version of VCR's _make_vcr_request that handles binary content.
The original implementation fails on binary request bodies (like file uploads)
because it assumes all content can be decoded as UTF-8.
"""
raw_body = httpx_request.read()
try:
body = raw_body.decode("utf-8")
except UnicodeDecodeError:
body = base64.b64encode(raw_body).decode("ascii")
raw_body = real_request_body if real_request_body is not None else httpx_request.read()
body: Any = raw_body
if isinstance(raw_body, bytes):
try:
body = raw_body.decode("utf-8")
except UnicodeDecodeError:
body = base64.b64encode(raw_body).decode("ascii")
uri = str(httpx_request.url)
headers = dict(httpx_request.headers)
return Request(httpx_request.method, uri, body, headers)

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,83 @@ 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
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a4)
## ما الذي تغير
### الميزات
- دعم تدفقات المحادثة في واجهة سطر الأوامر TUI.
### إصلاحات الأخطاء
- إصلاح مسار التوجيه الرمزي في استخراج أرشيف المهارات.
- التحقق من صحة مسارات تعريف التدفق الإعلاني.
### الوثائق
- تحديث اللقطة وسجل التغييرات للإصدار v1.14.8a3.
## المساهمون
@lorenzejay, @theCyberTech, @vinibrsl
</Update>
<Update label="23 يونيو 2026">
## v1.14.8a3
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a3)
## ما الذي تغير
### الميزات
- إضافة تحميل تدفق موحد إعلاني
- تحسين تجربة بدء تشغيل crewai run
- دمج `crewai run` و `crewai flow kickoff`
- الحفاظ على تقدم طريقة التدفق مرئيًا للفرق المتداخلة
- إضافة دعم واجهة سطر الأوامر الإعلانية للتدفق
- السماح باستخدام `@router()` كطريقة بدء لتدفق
- إضافة مخططات مخرجات مكتوبة لأدوات CrewAI
### إصلاحات الأخطاء
- تثبيت opentelemetry على ~=1.42.0
### الوثائق
- إضافة صفحة استوديو "بطاقة واحدة لكل خطوة"
## المساهمون
@jessemiller, @joaomdmoura, @lucasgomide, @vinibrsl
</Update>
<Update label="18 يونيو 2026">
## v1.14.8a2
@@ -64,7 +141,7 @@ mode: "wide"
- تنفيذ أدوات تشغيل تعريف التدفق بدون كود Python
- دفع التغذية الراجعة البشرية من تعريف التدفق
- توصيل التكوين والاستمرارية من FlowDefinition إلى وقت التشغيل
- إضافة `crewai run --definition` التجريبية للتدفقات
- إضافة `crewai run --definition` للتدفقات التصريحية
- دعم تراجع نشر ZIP وتشغيل مشاريع الطاقم بتنسيق JSON
- تقديم الطواقم بتنسيق JSON أولاً

View File

@@ -959,7 +959,7 @@ source .venv/bin/activate
بعد تفعيل البيئة الافتراضية، يمكنك تشغيل التدفق بتنفيذ أحد الأوامر التالية:
```bash
crewai flow kickoff
crewai run
```
أو
@@ -1160,10 +1160,4 @@ crewai run
يكتشف هذا الأمر تلقائيًا ما إذا كان مشروعك تدفقًا (بناءً على إعداد `type = "flow"` في pyproject.toml الخاص بك) ويشغّله وفقًا لذلك. هذه هي الطريقة الموصى بها لتشغيل التدفقات من سطر الأوامر.
للتوافق مع الإصدارات السابقة، يمكنك أيضًا استخدام:
```shell
crewai flow kickoff
```
ومع ذلك، فإن أمر `crewai run` هو الطريقة المفضلة الآن لأنه يعمل لكل من فرق Crew والتدفقات.
أمر `crewai flow kickoff` القديم deprecated. استخدم `crewai run` لكل من فرق Crew والتدفقات.

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

@@ -172,7 +172,7 @@ crewai install
## الخطوة 8: تشغيل Flow
```bash
crewai flow kickoff
crewai run
```
عند تشغيل هذا الأمر، ستشاهد Flow يعمل:

View File

@@ -4,6 +4,83 @@ 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 release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a4)
## What's Changed
### Features
- Support conversational flows in the CLI TUI.
### Bug Fixes
- Fix symlink path traversal in skill archive extraction.
- Validate declarative flow definition paths.
### Documentation
- Update snapshot and changelog for v1.14.8a3.
## Contributors
@lorenzejay, @theCyberTech, @vinibrsl
</Update>
<Update label="Jun 23, 2026">
## v1.14.8a3
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a3)
## What's Changed
### Features
- Add unified declarative flow loading
- Improve crewai run startup UX
- Consolidate `crewai run` and `crewai flow kickoff`
- Keep flow method progress visible for nested crews
- Add declarative Flow CLI support
- Allow `@router()` as start method of a flow
- Add typed output schemas for CrewAI tools
### Bug Fixes
- Pin opentelemetry to ~=1.42.0
### Documentation
- Add "One Card per Step" Studio page
## Contributors
@jessemiller, @joaomdmoura, @lucasgomide, @vinibrsl
</Update>
<Update label="Jun 18, 2026">
## v1.14.8a2
@@ -64,7 +141,7 @@ mode: "wide"
- Implement Flow definition run tools without Python code
- Drive human feedback from the flow definition
- Wire config and persistence from FlowDefinition into the runtime
- Add experimental `crewai run --definition` for flows
- Add `crewai run --definition` for declarative flows
- Support ZIP deployment fallback and JSON crew project env runs
- Introduce JSON first crews

View File

@@ -956,13 +956,13 @@ Once all of the dependencies are installed, you need to activate the virtual env
source .venv/bin/activate
```
After activating the virtual environment, you can run the flow by executing one of the following commands:
After activating the virtual environment, you can run the flow with the CrewAI CLI:
```bash
crewai flow kickoff
crewai run
```
or
You can also run the project script directly:
```bash
uv run kickoff
@@ -1160,10 +1160,4 @@ crewai run
This command automatically detects if your project is a flow (based on the `type = "flow"` setting in your pyproject.toml) and runs it accordingly. This is the recommended way to run flows from the command line.
For backward compatibility, you can also use:
```shell
crewai flow kickoff
```
However, the `crewai run` command is now the preferred method as it works for both crews and flows.
The legacy `crewai flow kickoff` command is deprecated. Use `crewai run` for both crews and flows.

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

@@ -395,7 +395,7 @@ crewai install
Now it's time to see your flow in action! Run it using the CrewAI CLI:
```bash
crewai flow kickoff
crewai run
```
When you run this command, you'll see your flow spring to life:

View File

@@ -4,6 +4,83 @@ 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
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a4)
## 변경 사항
### 기능
- CLI TUI에서 대화형 흐름 지원.
### 버그 수정
- 기술 아카이브 추출 시 심볼릭 링크 경로 탐색 문제 수정.
- 선언적 흐름 정의 경로 검증.
### 문서
- v1.14.8a3에 대한 스냅샷 및 변경 로그 업데이트.
## 기여자
@lorenzejay, @theCyberTech, @vinibrsl
</Update>
<Update label="2026년 6월 23일">
## v1.14.8a3
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a3)
## 변경 사항
### 기능
- 통합 선언적 흐름 로딩 추가
- crewai run 시작 UX 개선
- `crewai run`과 `crewai flow kickoff` 통합
- 중첩된 크루의 흐름 메서드 진행 상황 표시 유지
- 선언적 Flow CLI 지원 추가
- 흐름의 시작 메서드로 `@router()` 허용
- CrewAI 도구에 대한 타입이 지정된 출력 스키마 추가
### 버그 수정
- opentelemetry를 ~=1.42.0으로 고정
### 문서
- "단계당 한 카드" 스튜디오 페이지 추가
## 기여자
@jessemiller, @joaomdmoura, @lucasgomide, @vinibrsl
</Update>
<Update label="2026년 6월 18일">
## v1.14.8a2
@@ -64,7 +141,7 @@ mode: "wide"
- Python 코드 없이 Flow 정의 실행 도구 구현
- Flow 정의에서 인간 피드백 유도
- FlowDefinition의 구성 및 지속성을 런타임에 연결
- 흐름을 위한 실험적 `crewai run --definition` 추가
- 선언적 흐름을 위한 `crewai run --definition` 추가
- ZIP 배포 대체 및 JSON 크루 프로젝트 환경 실행 지원
- JSON 우선 크루 도입

View File

@@ -951,7 +951,7 @@ source .venv/bin/activate
가상 환경을 활성화한 후, 아래 명령어 중 하나를 실행하여 플로우를 실행할 수 있습니다:
```bash
crewai flow kickoff
crewai run
```
또는
@@ -1054,10 +1054,4 @@ crewai run
이 명령어는 프로젝트가 pyproject.toml의 `type = "flow"` 설정을 기반으로 flow인지 자동으로 감지하여 해당 방식으로 실행합니다. 명령줄에서 flow를 실행하는 권장 방법입니다.
하위 호환성을 위해 다음 명령어도 사용할 수 있습니다:
```shell
crewai flow kickoff
```
하지만 `crewai run` 명령어가 이제 crew와 flow 모두에 작동하므로 더욱 선호되는 방법입니다.
레거시 `crewai flow kickoff` 명령어는 deprecated되었습니다. crew와 flow 모두 `crewai run`을 사용하세요.

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

@@ -393,7 +393,7 @@ crewai install
이제 여러분의 flow가 실제로 작동하는 모습을 볼 차례입니다! CrewAI CLI를 사용하여 flow를 실행하세요:
```bash
crewai flow kickoff
crewai run
```
이 명령어를 실행하면 flow가 다음과 같이 작동하는 것을 확인할 수 있습니다:

View File

@@ -4,6 +4,83 @@ 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
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a4)
## O que Mudou
### Recursos
- Suporte a fluxos de conversa na TUI do CLI.
### Correções de Bugs
- Corrigir a travessia de caminho de symlink na extração de arquivo de habilidade.
- Validar os caminhos de definição de fluxo declarativo.
### Documentação
- Atualizar snapshot e changelog para v1.14.8a3.
## Contribuidores
@lorenzejay, @theCyberTech, @vinibrsl
</Update>
<Update label="23 jun 2026">
## v1.14.8a3
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a3)
## O que Mudou
### Recursos
- Adicionar carregamento de fluxo declarativo unificado
- Melhorar a experiência de inicialização do crewai run
- Consolidar `crewai run` e `crewai flow kickoff`
- Manter o progresso do método de fluxo visível para equipes aninhadas
- Adicionar suporte a Flow CLI declarativo
- Permitir `@router()` como método de início de um fluxo
- Adicionar esquemas de saída tipados para ferramentas CrewAI
### Correções de Bugs
- Fixar opentelemetry em ~=1.42.0
### Documentação
- Adicionar página "Uma Cartão por Etapa" no Studio
## Contribuidores
@jessemiller, @joaomdmoura, @lucasgomide, @vinibrsl
</Update>
<Update label="18 jun 2026">
## v1.14.8a2
@@ -64,7 +141,7 @@ mode: "wide"
- Implementar ferramentas de execução de definição de fluxo sem código Python
- Conduzir feedback humano a partir da definição de fluxo
- Conectar configuração e persistência do FlowDefinition ao tempo de execução
- Adicionar `crewai run --definition` experimental para fluxos
- Adicionar `crewai run --definition` para fluxos declarativos
- Suportar fallback de implantação ZIP e execuções de projeto de equipe em JSON
- Introduzir equipes em JSON primeiro

View File

@@ -948,7 +948,7 @@ source .venv/bin/activate
Com o ambiente ativado, execute o flow usando um dos comandos:
```bash
crewai flow kickoff
crewai run
```
ou
@@ -1052,10 +1052,4 @@ crewai run
O comando detecta automaticamente se seu projeto é um flow (com base na configuração `type = "flow"` no pyproject.toml) e executa conforme o esperado. Esse é o método recomendado para executar flows pelo terminal.
Por compatibilidade retroativa, também é possível usar:
```shell
crewai flow kickoff
```
No entanto, o comando `crewai run` é agora o preferido, pois funciona tanto para crews quanto para flows.
O comando legado `crewai flow kickoff` está deprecated. Use `crewai run` para crews e flows.

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

@@ -393,7 +393,7 @@ crewai install
Agora é hora de ver seu flow em ação! Execute-o usando a CLI do CrewAI:
```bash
crewai flow kickoff
crewai run
```
Quando você rodar esse comando, verá seu flow ganhando vida:

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.8a2",
"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.8a2"
__version__ = "1.14.8a5"

View File

@@ -40,14 +40,6 @@ def replay_task_command(*args: Any, **kwargs: Any) -> Any:
return _replay_task_command(*args, **kwargs)
def run_flow_definition(*args: Any, **kwargs: Any) -> Any:
from crewai_cli.run_flow_definition import (
run_flow_definition as _run_flow_definition,
)
return _run_flow_definition(*args, **kwargs)
def run_crew(*args: Any, **kwargs: Any) -> Any:
from crewai_cli.run_crew import run_crew as _run_crew
@@ -155,12 +147,18 @@ def uv(uv_args: tuple[str, ...]) -> None:
is_flag=True,
help="Use classic Python/YAML project structure instead of JSON",
)
@click.option(
"--declarative",
is_flag=True,
help="Create a declarative Flow project instead of a Python Flow project",
)
def create(
type: str | None,
name: str | None,
provider: str | None,
skip_provider: bool = False,
classic: bool = False,
declarative: bool = False,
) -> None:
"""Create a new crew, or flow."""
dmn_mode = is_dmn_mode_enabled()
@@ -194,6 +192,8 @@ def create(
if dmn_mode:
skip_provider = True
if type == "crew":
if declarative:
raise click.UsageError("--declarative can only be used with flow projects")
if classic:
from crewai_cli.create_crew import create_crew
@@ -205,7 +205,7 @@ def create(
elif type == "flow":
from crewai_cli.create_flow import create_flow
create_flow(name)
create_flow(name, declarative=declarative)
else:
click.secho("Error: Invalid type. Must be 'crew' or 'flow'.", fg="red")
@@ -468,7 +468,7 @@ def memory(
type=str,
default=None,
help=(
"Path to a trained-agents pickle (produced by `crewai train -f`). "
"Crew-only: path to a trained-agents pickle (produced by `crewai train -f`). "
"When set, agents load suggestions from this file instead of the "
"default trained_agents_data.pkl. Equivalent to setting "
"CREWAI_TRAINED_AGENTS_FILE."
@@ -512,16 +512,13 @@ def install(context: click.Context) -> None:
"--definition",
type=str,
default=None,
help=(
"Experimental: path to a Flow Definition YAML/JSON file, "
"or an inline YAML/JSON string."
),
help="Flow-only: path to a declarative flow definition.",
)
@click.option(
"--inputs",
type=str,
default=None,
help='Experimental: JSON object passed to flow.kickoff(), e.g. \'{"topic":"AI"}\'.',
help='Flow-only: JSON object passed to the declarative flow, e.g. \'{"topic":"AI"}\'.',
)
def run(
trained_agents_file: str | None,
@@ -531,16 +528,14 @@ def run(
"""Run the Crew or Flow."""
if inputs is not None and definition is None:
raise click.UsageError("--inputs requires --definition")
if trained_agents_file is not None and definition is not None:
raise click.UsageError("--filename can only be used when running crews")
if definition is not None:
click.secho(
"Warning: `crewai run --definition` is experimental and may change without notice.",
fg="yellow",
)
run_flow_definition(definition=definition, inputs=inputs)
return
run_crew(trained_agents_file=trained_agents_file)
run_crew(
trained_agents_file=trained_agents_file,
definition=definition,
inputs=inputs,
)
@crewai.command()
@@ -795,10 +790,11 @@ def flow() -> None:
@flow.command(name="kickoff")
def flow_run() -> None:
"""Kickoff the Flow."""
from crewai_cli.kickoff_flow import kickoff_flow
click.echo("Running the Flow")
kickoff_flow()
click.secho(
"The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead.",
fg="yellow",
)
run_crew(trained_agents_file=None, definition=None, inputs=None)
@flow.command(name="plot")

View File

@@ -5,7 +5,10 @@ import click
from crewai_core.telemetry import Telemetry
def create_flow(name: str) -> None:
DECLARATIVE_FLOW_FOLDERS = ("crews", "tools", "knowledge", "skills")
def create_flow(name: str, *, declarative: bool = False) -> None:
"""Create a new flow."""
folder_name = name.replace(" ", "_").replace("-", "_").lower()
class_name = name.replace("_", " ").replace("-", " ").title().replace(" ", "")
@@ -20,6 +23,17 @@ def create_flow(name: str) -> None:
telemetry = Telemetry()
telemetry.flow_creation_span(class_name)
if declarative:
_create_declarative_flow(name, class_name, folder_name, project_root)
else:
_create_python_flow(name, class_name, folder_name, project_root)
click.secho(f"Flow {name} created successfully!", fg="green", bold=True)
def _create_python_flow(
name: str, class_name: str, folder_name: str, project_root: Path
) -> None:
(project_root / "src" / folder_name).mkdir(parents=True)
(project_root / "src" / folder_name / "crews").mkdir(parents=True)
(project_root / "src" / folder_name / "tools").mkdir(parents=True)
@@ -92,4 +106,41 @@ def create_flow(name: str) -> None:
fg="yellow",
)
click.secho(f"Flow {name} created successfully!", fg="green", bold=True)
def _create_declarative_flow(
name: str, class_name: str, folder_name: str, project_root: Path
) -> None:
project_root.mkdir(parents=True)
package_root = project_root / "src" / folder_name
package_root.mkdir(parents=True)
for folder in DECLARATIVE_FLOW_FOLDERS:
(package_root / folder).mkdir()
package_dir = Path(__file__).parent
templates_dir = package_dir / "templates" / "declarative_flow"
agents_md_src = package_dir / "templates" / "AGENTS.md"
if agents_md_src.exists():
shutil.copy2(agents_md_src, project_root / "AGENTS.md")
for src_file in templates_dir.rglob("*"):
if not src_file.is_file():
continue
relative_path = src_file.relative_to(templates_dir)
dst_file = (
project_root / relative_path
if relative_path.name in {".gitignore", "README.md", "pyproject.toml"}
else package_root / relative_path
)
dst_file.parent.mkdir(parents=True, exist_ok=True)
content = src_file.read_text(encoding="utf-8")
content = content.replace("{{name}}", name)
content = content.replace("{{flow_name}}", class_name)
content = content.replace("{{folder_name}}", folder_name)
dst_file.write_text(content, encoding="utf-8")
(project_root / ".env").write_text("OPENAI_API_KEY=YOUR_API_KEY", encoding="utf-8")
(package_root / "__init__.py").write_text("", encoding="utf-8")
for folder in DECLARATIVE_FLOW_FOLDERS:
(package_root / folder / ".gitkeep").write_text("", encoding="utf-8")

View File

@@ -680,7 +680,7 @@ def _default_agents_and_tasks(
]
crew_settings = {
"process": "sequential",
"memory": False,
"memory": True,
"inputs": {},
}
return agents, tasks, crew_settings

View File

@@ -17,7 +17,7 @@ from textual.binding import Binding, BindingType
from textual.containers import Horizontal, Vertical, VerticalScroll
from textual.css.query import NoMatches
from textual.screen import ModalScreen
from textual.widgets import Button, Footer, Header, Static
from textual.widgets import Button, Footer, Header, Input, Static
_SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
@@ -382,6 +382,18 @@ Screen {
height: auto;
}
#conversation-input {
display: none;
height: 3;
border-top: hkey #333333;
background: #1c1c1c;
color: #e0e0e0;
}
#conversation-input:focus {
border-top: hkey #1F7982;
}
Header {
background: #1c1c1c;
color: #FF5A50;
@@ -483,6 +495,7 @@ FooterKey .footer-key--key {
total_tasks: int = 0,
agent_names: list[str] | None = None,
task_names: list[str] | None = None,
conversational: bool = False,
):
super().__init__()
self.title = f"CrewAI — {crew_name}"
@@ -544,6 +557,13 @@ FooterKey .footer-key--key {
self._event_handlers: list[tuple[type, Any]] = []
self._crew: Any = None
self._flow: Any = None
self._is_conversational = conversational
self._conversation_messages: list[tuple[str, str]] = []
self._conversation_turns = 0
self._conversation_turn_in_progress = False
self._conversation_previous_defer_trace_finalization: bool | None = None
self._conversation_exit_commands = {"exit", "quit"}
self._default_inputs: dict[str, Any] | None = None
self._crew_result: Any = None
self._crew_json_path: Any = None
@@ -566,6 +586,10 @@ FooterKey .footer-key--key {
yield Static(id="task-header")
with VerticalScroll(id="scroll-area"):
yield Static(id="main-content")
yield Input(
placeholder="Message the flow...",
id="conversation-input",
)
with VerticalScroll(id="log-panel"):
yield Static(id="log-content")
yield Footer()
@@ -574,7 +598,9 @@ FooterKey .footer-key--key {
self._start_time = time.time()
self._subscribe()
self._tick_timer = self.set_interval(1 / 8, self._tick)
if self._crew:
if self._is_conversational and self._flow:
self._start_conversational_session()
elif self._crew:
self._run_crew_worker()
elif self._crew_json_path:
self._load_and_run_worker()
@@ -725,6 +751,140 @@ FooterKey .footer-key--key {
self._tick_timer = self.set_interval(1 / 2, self._tick)
self._unsubscribe_if_no_running_memory_save(wait_for_queued=True)
# ── Conversational flow execution ───────────────────────
def _start_conversational_session(self) -> None:
from crewai.events.listeners.tracing.utils import (
set_suppress_tracing_messages,
set_tui_mode,
)
set_tui_mode(True)
set_suppress_tracing_messages(True)
with self._lock:
self._status = "chatting"
self._current_step = None
self._elapsed_frozen = None
self._conversation_previous_defer_trace_finalization = getattr(
self._flow, "defer_trace_finalization", False
)
self._flow.defer_trace_finalization = True
try:
input_widget = self.query_one("#conversation-input", Input)
input_widget.display = True
input_widget.focus()
except Exception: # noqa: S110
pass
def _finalize_conversational_session(self) -> None:
if not (self._is_conversational and self._flow):
return
try:
self._flow.finalize_session_traces()
except Exception: # noqa: S110
pass
previous = self._conversation_previous_defer_trace_finalization
if previous is not None:
try:
self._flow.defer_trace_finalization = previous
except Exception: # noqa: S110
pass
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id != "conversation-input":
return
if not self._is_conversational:
return
message = event.value.strip()
event.input.value = ""
if not message:
return
if message.lower() in self._conversation_exit_commands:
self._finalize_conversational_session()
self._unsubscribe()
self.exit(self._crew_result)
return
if self._conversation_turn_in_progress:
return
with self._lock:
self._conversation_messages.append(("user", message))
self._conversation_turn_in_progress = True
self._conversation_turns += 1
self._status = "working"
self._current_step = ("yellow", "Thinking…", "")
self._is_streaming = False
self._streaming_text = ""
self._task_full_output = ""
self._current_llm_text = ""
event.input.disabled = True
self._run_conversation_turn_worker(message)
@work(thread=True, exclusive=True, group="conversation")
def _run_conversation_turn_worker(self, message: str) -> None:
from crewai.events.listeners.tracing.utils import (
set_suppress_tracing_messages,
set_tui_mode,
)
set_tui_mode(True)
set_suppress_tracing_messages(True)
try:
result = self._flow.handle_turn(message)
if hasattr(result, "get_full_text") and hasattr(result, "result"):
for _chunk in result:
pass
result = result.result
self.call_from_thread(self._on_conversation_turn_done, result)
except Exception as e:
self.call_from_thread(self._on_conversation_turn_failed, str(e))
def _on_conversation_turn_done(self, result: Any) -> None:
with self._lock:
output = self._stringify_output(result)
self._conversation_messages.append(("assistant", output))
self._crew_result = result
self._conversation_turn_in_progress = False
self._status = "chatting"
self._is_streaming = False
self._streaming_text = ""
self._current_step = None
self._enable_conversation_input()
self._tick()
self._scroll_to_result()
def _on_conversation_turn_failed(self, error: str) -> None:
with self._lock:
self._status = "failed"
self._error = error
self._conversation_turn_in_progress = False
self._is_streaming = False
self._current_step = None
self._enable_conversation_input()
self._tick()
def _enable_conversation_input(self) -> None:
try:
input_widget = self.query_one("#conversation-input", Input)
input_widget.disabled = False
input_widget.focus()
except Exception: # noqa: S110
pass
def _stringify_output(self, result: Any) -> str:
raw_result = getattr(result, "raw", result)
if raw_result is None:
return ""
if isinstance(raw_result, str):
return raw_result
try:
return _json.dumps(raw_result, default=str, ensure_ascii=False)
except TypeError:
return str(raw_result)
# ── Actions ─────────────────────────────────────────────
def action_toggle_sidebar(self) -> None:
@@ -783,6 +943,7 @@ FooterKey .footer-key--key {
self._refresh_log_panel()
async def action_quit(self) -> None:
self._finalize_conversational_session()
self._unsubscribe()
self.exit(self._crew_result)
@@ -958,6 +1119,30 @@ FooterKey .footer-key--key {
t = Text()
sidebar_width = 30
if self._is_conversational:
t.append(" CONVERSATION\n", style=f"bold {_C_PRIMARY}")
t.append("\n")
if self._conversation_turn_in_progress:
t.append(f" {self._spinner()} ", style=_C_PRIMARY)
t.append("Working\n", style=f"bold {_C_TEXT}")
elif self._status == "failed":
t.append(" ✘ Failed\n", style=_C_RED)
else:
t.append(" ● Ready\n", style=_C_GREEN)
t.append(f" Turns {self._conversation_turns}\n", style=_C_DIM)
t.append("\n")
t.append(" TOKENS\n", style=f"bold {_C_PRIMARY}")
t.append("\n")
out = self._output_tokens + self._live_out_tokens
t.append(f"{self._input_tokens:,}\n", style=_C_DIM)
t.append(f"{out:,}\n", style=_C_DIM)
t.append("\n")
t.append(" COMMANDS\n", style=f"bold {_C_PRIMARY}")
t.append("\n")
t.append(" quit / exit\n", style=_C_DIM)
widget.update(t)
return
t.append(" TASKS\n", style=f"bold {_C_PRIMARY}")
t.append("\n")
@@ -1011,6 +1196,22 @@ FooterKey .footer-key--key {
widget = self.query_one("#task-header", Static)
t = Text()
if self._is_conversational:
if self._status == "failed":
t.append("", style=f"bold {_C_RED}")
t.append("Failed", style=f"bold {_C_RED}")
if self._error:
t.append(f"\n{self._error[:120]}", style=_C_RED)
elif self._conversation_turn_in_progress:
t.append(f"{self._spinner()} ", style=_C_PRIMARY)
t.append("Flow is responding", style=f"bold {_C_PRIMARY}")
else:
t.append("", style=f"bold {_C_GREEN}")
t.append("Conversational flow ready", style=f"bold {_C_GREEN}")
t.append(" Type a message below", style=_C_DIM)
widget.update(t)
return
if self._status == "completed":
elapsed = self._elapsed_frozen or (time.time() - self._start_time)
t.append("", style=f"bold {_C_GREEN}")
@@ -1062,6 +1263,41 @@ FooterKey .footer-key--key {
t = Text()
should_scroll = False
if self._is_conversational:
if not self._conversation_messages and not self._is_streaming:
t.append(" Start the conversation below.\n", style=_C_MUTED)
for role, content in self._conversation_messages:
if role == "user":
t.append("\n You\n", style=f"bold {_C_TEAL}")
else:
t.append("\n Assistant\n", style=f"bold {_C_PRIMARY}")
rendered = _format_json_in_text(_unescape_text(content))
for line in rendered.split("\n"):
style = _C_TEXT if role == "assistant" else _C_DIM
t.append(f" {line}\n", style=style)
if self._is_streaming and self._streaming_text:
text = _unescape_text(self._filtered_streaming_text())
if text.strip():
t.append("\n Assistant\n", style=f"bold {_C_PRIMARY}")
for line in text.rstrip().split("\n")[-40:]:
t.append(f" {line}\n", style=_C_TEXT)
should_scroll = True
if self._status == "failed" and self._error:
t.append("\n Error\n", style=f"bold {_C_RED}")
t.append(f" {self._error}\n", style=_C_RED)
widget.update(t)
if should_scroll:
try:
self.query_one("#scroll-area", VerticalScroll).scroll_end(
animate=False
)
except Exception: # noqa: S110
pass
return
# Plan section
if self._plan and self._plan.get("steps"):
plan_title = self._plan.get("plan", "Plan")

View File

@@ -1,15 +1,11 @@
from __future__ import annotations
from pathlib import Path
import re
import shutil
import tempfile
from typing import Any
import zipfile
from crewai_cli import git
from crewai_cli.deploy.validate import normalize_package_name
from crewai_cli.utils import parse_toml
_EXCLUDED_DIRS = {
@@ -38,8 +34,6 @@ _EXCLUDED_SUFFIXES = {
".pyc",
".pyo",
}
_SCRIPT_KEY_PATTERN = re.compile(r"^\s*(?P<key>[A-Za-z0-9_.-]+|\"[^\"]+\"|'[^']+')\s*=")
_SECTION_PATTERN = re.compile(r"^\s*\[[^\]]+\]\s*(?:#.*)?$")
def create_project_zip(
@@ -143,267 +137,7 @@ def _stage_project(root: Path, files: list[Path]) -> Path:
destination = staging_root / relative_path
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, destination)
if _is_json_crew_project(staging_root):
_add_json_crew_deploy_wrapper(staging_root)
except Exception:
shutil.rmtree(staging_root, ignore_errors=True)
raise
return staging_root
def _is_json_crew_project(root: Path) -> bool:
"""Return True for JSON crew projects that need a Python deploy wrapper."""
if not ((root / "crew.jsonc").is_file() or (root / "crew.json").is_file()):
return False
project = _read_pyproject(root)
tool_config = project.get("tool") or {}
crewai_config = tool_config.get("crewai") if isinstance(tool_config, dict) else None
declared_type = (
crewai_config.get("type") if isinstance(crewai_config, dict) else None
)
if declared_type == "flow":
return False
package_name = _package_name(root)
if package_name is None:
raise ValueError(
"Could not derive a valid Python package name from [project].name."
)
return not (root / "src" / package_name / "crew.py").is_file()
def _read_pyproject(root: Path) -> dict[str, Any]:
"""Read pyproject.toml, returning an empty mapping on missing or invalid data."""
pyproject_path = root / "pyproject.toml"
if not pyproject_path.is_file():
return {}
try:
pyproject = parse_toml(pyproject_path.read_text())
except Exception:
return {}
return pyproject if isinstance(pyproject, dict) else {}
def _package_name(root: Path) -> str | None:
"""Return the normalized Python package name for the project."""
project = _read_pyproject(root).get("project")
if not isinstance(project, dict):
return None
name = project.get("name")
if not isinstance(name, str) or not name.strip():
return None
package_name = normalize_package_name(name)
return package_name or None
def _class_name(package_name: str) -> str:
"""Return the generated wrapper class name for a package."""
parts = [part for part in re.split(r"[^a-zA-Z0-9]+", package_name) if part]
class_name = "".join(part[:1].upper() + part[1:] for part in parts)
if not class_name:
return "JsonCrew"
if class_name[0].isdigit():
return f"Crew{class_name}"
return class_name
def _add_json_crew_deploy_wrapper(root: Path) -> None:
"""Add Python wrapper files required to deploy a JSON crew project."""
package_name = _package_name(root)
if package_name is None:
raise ValueError(
"Could not derive a valid Python package name from [project].name."
)
package_dir = root / "src" / package_name
config_dir = package_dir / "config"
config_dir.mkdir(parents=True, exist_ok=True)
class_name = _class_name(package_name)
crew_filename = "crew.jsonc" if (root / "crew.jsonc").is_file() else "crew.json"
(package_dir / "__init__.py").write_text("", encoding="utf-8")
(config_dir / "agents.yaml").write_text("{}\n", encoding="utf-8")
(config_dir / "tasks.yaml").write_text("{}\n", encoding="utf-8")
(package_dir / "crew.py").write_text(
_json_crew_py(class_name, crew_filename),
encoding="utf-8",
)
(package_dir / "main.py").write_text(
_json_main_py(package_name, class_name),
encoding="utf-8",
)
_ensure_project_scripts(root, package_name)
def _json_crew_py(class_name: str, crew_filename: str) -> str:
"""Render the generated crew.py module for a JSON crew."""
return f'''from pathlib import Path
from crewai import Crew
from crewai.project import CrewBase, crew
from crewai.project.crew_loader import load_crew
def _crew_path() -> Path:
return Path(__file__).resolve().parents[2] / "{crew_filename}"
@CrewBase
class {class_name}:
"""Compatibility wrapper for a JSON-defined CrewAI project."""
@crew
def crew(self) -> Crew:
crew_instance, default_inputs = load_crew(_crew_path())
self.default_inputs = default_inputs
return crew_instance
'''
def _json_main_py(package_name: str, class_name: str) -> str:
"""Render the generated main.py entrypoints for a JSON crew."""
return f"""#!/usr/bin/env python
import json
import sys
from {package_name}.crew import {class_name}
def _load():
wrapper = {class_name}()
crew = wrapper.crew()
return crew, getattr(wrapper, "default_inputs", {{}})
def run():
crew, inputs = _load()
return crew.kickoff(inputs=inputs)
def train():
crew, inputs = _load()
return crew.train(
n_iterations=int(sys.argv[1]),
filename=sys.argv[2],
inputs=inputs,
)
def replay():
crew, _ = _load()
return crew.replay(task_id=sys.argv[1])
def test():
crew, inputs = _load()
return crew.test(
n_iterations=int(sys.argv[1]),
eval_llm=sys.argv[2],
inputs=inputs,
)
def run_with_trigger():
if len(sys.argv) < 2:
raise ValueError("No trigger payload provided.")
crew, inputs = _load()
trigger_payload = json.loads(sys.argv[1])
return crew.kickoff(
inputs={{**inputs, "crewai_trigger_payload": trigger_payload}}
)
"""
def _ensure_project_scripts(root: Path, package_name: str) -> None:
"""Ensure generated wrappers have project script entrypoints."""
pyproject_path = root / "pyproject.toml"
if not pyproject_path.is_file():
return
content = pyproject_path.read_text(encoding="utf-8")
entries = _project_script_entries(package_name)
pyproject_path.write_text(
_update_project_scripts(content, entries),
encoding="utf-8",
)
def _project_script_entries(package_name: str) -> dict[str, str]:
"""Return script entrypoints required by the generated JSON wrapper."""
return {
package_name: f"{package_name}.main:run",
"run_crew": f"{package_name}.main:run",
"train": f"{package_name}.main:train",
"replay": f"{package_name}.main:replay",
"test": f"{package_name}.main:test",
"run_with_trigger": f"{package_name}.main:run_with_trigger",
}
def _update_project_scripts(content: str, entries: dict[str, str]) -> str:
"""Add or replace generated script entries in pyproject.toml content."""
lines = content.rstrip().splitlines()
header_index = _project_scripts_header_index(lines)
if header_index is None:
return content.rstrip() + _project_scripts_block(entries)
end_index = _section_end_index(lines, header_index + 1)
seen: set[str] = set()
for index in range(header_index + 1, end_index):
key = _script_key(lines[index])
if key in entries:
lines[index] = _script_line(key, entries[key])
seen.add(key)
missing_lines = [
_script_line(key, value) for key, value in entries.items() if key not in seen
]
lines[end_index:end_index] = missing_lines
return "\n".join(lines).rstrip() + "\n"
def _project_scripts_header_index(lines: list[str]) -> int | None:
"""Return the line index of the project scripts table, if present."""
for index, line in enumerate(lines):
if line.strip() == "[project.scripts]":
return index
return None
def _section_end_index(lines: list[str], start_index: int) -> int:
"""Return the exclusive end index for a TOML table section."""
for index in range(start_index, len(lines)):
if _SECTION_PATTERN.match(lines[index]):
return index
return len(lines)
def _script_key(line: str) -> str | None:
"""Return the script key for a pyproject script line."""
match = _SCRIPT_KEY_PATTERN.match(line)
if not match:
return None
key = match.group("key")
if key.startswith(("'", '"')) and key.endswith(("'", '"')):
return key[1:-1]
return key
def _script_line(key: str, value: str) -> str:
"""Render a project script TOML entry."""
return f'{key} = "{value}"'
def _project_scripts_block(entries: dict[str, str]) -> str:
"""Render a project scripts TOML table."""
lines = ["", "", "[project.scripts]"]
lines.extend(_script_line(key, value) for key, value in entries.items())
return "\n".join(lines) + "\n"

View File

@@ -212,8 +212,16 @@ class DeployValidator:
if crew_path is None:
return self.results
agents_dir = self.project_root / "agents"
self._check_pyproject()
self._check_lockfile()
agents_dir_ok = self._check_json_agents_dir(agents_dir)
project = None
try:
project = validate_crew_project(crew_path, self.project_root / "agents")
if agents_dir_ok:
project = validate_crew_project(crew_path, agents_dir)
except JSONProjectValidationError as e:
self._add(
Severity.ERROR,
@@ -232,15 +240,27 @@ class DeployValidator:
)
return self.results
agents_dir = self.project_root / "agents"
self._check_pyproject()
self._check_lockfile()
self._check_env_vars_json(crew_path, agents_dir, project.agent_names)
if project is not None:
self._check_env_vars_json(crew_path, agents_dir, project.agent_names)
self._check_version_vs_lockfile()
return self.results
def _check_json_agents_dir(self, agents_dir: Path) -> bool:
if agents_dir.is_dir():
return True
self._add(
Severity.ERROR,
"missing_agents_dir",
"Cannot find agents/ directory",
detail=(
"JSON crew projects load agent definitions from "
f"{agents_dir.relative_to(self.project_root)}/*.jsonc or *.json."
),
hint="Create agents/ and add one JSON or JSONC file per agent.",
)
return False
def _check_env_vars_json(
self, crew_path: Path, agents_dir: Path, agent_names: list[str]
) -> None:

View File

@@ -378,12 +378,40 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
def _safe_extractall(tf: tarfile.TarFile, dest: Path) -> None:
"""Path-traversal-safe extraction for Python < 3.12."""
"""Path-traversal-safe extraction for Python versions without tar filters.
Validates both the member's own path and, for symlink/hardlink members,
the link target. Without the link-target check a malicious archive can
plant a symlink that escapes ``dest`` (e.g. ``link -> /home/user/.ssh``)
followed by a regular member written *through* that link
(``link/authorized_keys``), escaping ``dest`` even though every member
name resolves inside it. This mirrors the protection that
``tarfile.extractall(..., filter="data")`` provides when available.
"""
dest_resolved = dest.resolve()
for member in tf.getmembers():
member_path = (dest / member.name).resolve()
if not member_path.is_relative_to(dest_resolved):
raise ValueError(f"Blocked path traversal attempt: {member.name!r}")
if not (member.isfile() or member.isdir() or member.issym() or member.islnk()):
raise ValueError(f"Blocked unsupported tar member: {member.name!r}")
if member.issym() or member.islnk():
link_target = member.linkname
# Absolute link targets always escape the destination.
if os.path.isabs(link_target):
raise ValueError(
f"Blocked link target escaping destination: "
f"{member.name!r} -> {link_target!r}"
)
# Hardlink names are relative to the archive root; symlink
# targets are relative to the member's own directory.
anchor = dest if member.islnk() else (dest / member.name).parent
resolved_target = (anchor / link_target).resolve()
if not resolved_target.is_relative_to(dest_resolved):
raise ValueError(
f"Blocked link target escaping destination: "
f"{member.name!r} -> {link_target!r}"
)
tf.extractall(dest) # noqa: S202

View File

@@ -1,12 +1,94 @@
from __future__ import annotations
import importlib
import inspect
from pathlib import Path
import subprocess
import sys
from typing import Any
import click
def _project_script_target(script_name: str) -> str | None:
try:
from crewai_cli.utils import read_toml
pyproject = read_toml()
except Exception:
return None
target = pyproject.get("project", {}).get("scripts", {}).get(script_name)
return target if isinstance(target, str) else None
def _prepare_project_import_path() -> None:
cwd = Path.cwd()
for path in (cwd / "src", cwd):
path_str = str(path)
if path.exists() and path_str not in sys.path:
sys.path.insert(0, path_str)
def _load_conversational_flow_from_kickoff_script() -> Any | None:
target = _project_script_target("kickoff")
if not target or ":" not in target:
return None
module_name, _callable_name = target.split(":", 1)
_prepare_project_import_path()
try:
module = importlib.import_module(module_name)
from crewai.flow.flow import Flow
except Exception:
return None
for value in vars(module).values():
if (
inspect.isclass(value)
and value is not Flow
and issubclass(value, Flow)
and getattr(value, "conversational", False)
):
return value()
for value in vars(module).values():
if (
isinstance(value, Flow)
and getattr(value, "conversational", False)
and callable(getattr(value, "handle_turn", None))
):
return value
return None
def _run_conversational_flow_tui(flow: Any) -> Any:
from crewai_cli.crew_run_tui import CrewRunApp
app = CrewRunApp(
crew_name=getattr(flow, "name", None) or type(flow).__name__,
conversational=True,
)
app._flow = flow
app.run()
if app._status == "failed":
raise SystemExit(1)
return app._crew_result
def kickoff_flow() -> None:
"""
Kickoff the flow by running a command in the UV environment.
"""
flow = _load_conversational_flow_from_kickoff_script()
if flow is not None:
_run_conversational_flow_tui(flow)
return
command = ["uv", "run", "kickoff"]
try:

View File

@@ -5,19 +5,27 @@ import click
def plot_flow() -> None:
"""
Plot the flow by running a command in the UV environment.
Plot the flow from declarative config or the Python UV entrypoint.
"""
command = ["uv", "run", "plot"]
from crewai_cli.run_declarative_flow import (
configured_project_declarative_flow,
plot_declarative_flow_in_project_env,
)
try:
result = subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603
if definition := configured_project_declarative_flow():
plot_declarative_flow_in_project_env(definition)
else:
command = ["uv", "run", "plot"]
if result.stderr:
click.echo(result.stderr, err=True)
try:
subprocess.run( # noqa: S603
command, capture_output=False, text=True, check=True
)
except subprocess.CalledProcessError as e:
click.echo(f"An error occurred while plotting the flow: {e}", err=True)
click.echo(e.output, err=True)
except subprocess.CalledProcessError as e:
click.echo(f"An error occurred while plotting the flow: {e}", err=True)
raise SystemExit(1) from e
except Exception as e:
click.echo(f"An unexpected error occurred: {e}", err=True)
except Exception as e:
click.echo(f"An unexpected error occurred: {e}", err=True)
raise SystemExit(1) from e

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
from collections.abc import Callable
from contextlib import AbstractContextManager, nullcontext
from enum import Enum
import os
from pathlib import Path
import re
@@ -27,11 +26,6 @@ if TYPE_CHECKING:
from crewai_cli.crew_run_tui import CrewRunApp
class CrewType(Enum):
STANDARD = "standard"
FLOW = "flow"
# Must accept the same names as the kickoff interpolation pattern in
# crewai.utilities.string_utils (_VARIABLE_PATTERN), including hyphens —
# otherwise placeholders are interpolated at runtime but never prompted for.
@@ -537,7 +531,11 @@ def _print_post_tui_summary(app: CrewRunApp) -> None:
)
def run_crew(trained_agents_file: str | None = None) -> None:
def run_crew(
trained_agents_file: str | None = None,
definition: str | None = None,
inputs: str | None = None,
) -> None:
"""Run the crew or flow.
Args:
@@ -545,15 +543,98 @@ def run_crew(trained_agents_file: str | None = None) -> None:
by ``crewai train -f``. When set, exported as
``CREWAI_TRAINED_AGENTS_FILE`` so agents load suggestions from this
file instead of the default ``trained_agents_data.pkl``.
definition: Optional path to a declarative Flow definition.
inputs: Optional JSON object passed to a declarative Flow.
"""
# JSON crew projects take precedence
if inputs is not None and definition is None:
raise click.UsageError("--inputs requires --definition")
if definition is not None:
_run_explicit_declarative_flow(
definition=definition,
inputs=inputs,
trained_agents_file=trained_agents_file,
)
return
if _has_json_crew():
_run_json_crew_in_project_env(trained_agents_file=trained_agents_file)
return
pyproject_data = read_toml()
_warn_if_old_poetry_project(pyproject_data)
project_type = _get_project_type(pyproject_data)
if project_type == "flow":
_run_flow_project(
pyproject_data=pyproject_data,
trained_agents_file=trained_agents_file,
)
return
_run_classic_crew_project(
pyproject_data=pyproject_data,
trained_agents_file=trained_agents_file,
)
def _run_explicit_declarative_flow(
definition: str, inputs: str | None, trained_agents_file: str | None
) -> None:
if trained_agents_file is not None:
raise click.UsageError("--filename can only be used when running crews")
from crewai_cli.run_declarative_flow import run_declarative_flow
run_declarative_flow(definition=definition, inputs=inputs)
def _run_flow_project(
pyproject_data: dict[str, Any], trained_agents_file: str | None
) -> None:
if trained_agents_file is not None:
raise click.UsageError("--filename can only be used when running crews")
from crewai_cli.run_declarative_flow import (
configured_project_declarative_flow,
run_declarative_flow_in_project_env,
)
if definition := configured_project_declarative_flow(pyproject_data):
run_declarative_flow_in_project_env(definition=definition)
return
from crewai_cli.kickoff_flow import (
_load_conversational_flow_from_kickoff_script,
_run_conversational_flow_tui,
)
flow = _load_conversational_flow_from_kickoff_script()
if flow is not None:
_run_conversational_flow_tui(flow)
return
_execute_uv_script("kickoff", entity_type="flow")
def _run_classic_crew_project(
pyproject_data: dict[str, Any], trained_agents_file: str | None
) -> None:
_execute_uv_script(
"run_crew",
entity_type="crew",
trained_agents_file=trained_agents_file,
)
def _get_project_type(pyproject_data: dict[str, Any]) -> str | None:
project_type = pyproject_data.get("tool", {}).get("crewai", {}).get("type")
return project_type if isinstance(project_type, str) else None
def _warn_if_old_poetry_project(pyproject_data: dict[str, Any]) -> None:
crewai_version = get_crewai_version()
min_required_version = "0.71.0"
pyproject_data = read_toml()
if pyproject_data.get("tool", {}).get("poetry") and (
version.parse(crewai_version) < version.parse(min_required_version)
@@ -564,25 +645,22 @@ def run_crew(trained_agents_file: str | None = None) -> None:
fg="red",
)
is_flow = pyproject_data.get("tool", {}).get("crewai", {}).get("type") == "flow"
crew_type = CrewType.FLOW if is_flow else CrewType.STANDARD
click.echo(f"Running the {'Flow' if is_flow else 'Crew'}")
execute_command(crew_type, trained_agents_file=trained_agents_file)
def execute_command(
crew_type: CrewType, trained_agents_file: str | None = None
def _execute_uv_script(
script_name: str,
*,
entity_type: str,
trained_agents_file: str | None = None,
) -> None:
"""Execute the appropriate command based on crew type.
"""Execute a project script through uv.
Args:
crew_type: The type of crew to run.
script_name: The project script to run.
entity_type: The user-facing entity being run.
trained_agents_file: Optional trained-agents pickle path forwarded to
the subprocess via the ``CREWAI_TRAINED_AGENTS_FILE`` env var.
"""
command = ["uv", "run", "kickoff" if crew_type == CrewType.FLOW else "run_crew"]
command = ["uv", "run", script_name]
env = build_env_with_all_tool_credentials()
if trained_agents_file:
@@ -592,21 +670,20 @@ def execute_command(
subprocess.run(command, capture_output=False, text=True, check=True, env=env) # noqa: S603
except subprocess.CalledProcessError as e:
handle_error(e, crew_type)
_handle_run_error(e, entity_type)
except Exception as e:
click.echo(f"An unexpected error occurred: {e}", err=True)
def handle_error(error: subprocess.CalledProcessError, crew_type: CrewType) -> None:
def _handle_run_error(error: subprocess.CalledProcessError, entity_type: str) -> None:
"""
Handle subprocess errors with appropriate messaging.
Args:
error: The subprocess error that occurred
crew_type: The type of crew that was being run
entity_type: The type of entity that was being run
"""
entity_type = "flow" if crew_type == CrewType.FLOW else "crew"
click.echo(f"An error occurred while running the {entity_type}: {error}", err=True)
if error.output:

View File

@@ -0,0 +1,241 @@
from __future__ import annotations
import json
from pathlib import Path, PureWindowsPath
import subprocess
from typing import Any
import click
from pydantic import ValidationError
from crewai_cli.utils import build_env_with_all_tool_credentials
def run_declarative_flow_in_project_env(
definition: str | Path, inputs: str | None = None
) -> None:
"""Run a declarative flow inside the project's Python environment."""
if is_declarative_flow_project_env() or not _has_project_file():
run_declarative_flow(definition=definition, inputs=inputs)
return
if inputs is not None:
raise click.UsageError("--inputs is only supported with --definition")
_execute_declarative_flow_command(["uv", "run", "crewai", "run"])
def plot_declarative_flow_in_project_env(definition: str | Path) -> None:
"""Plot a declarative flow inside the project's Python environment."""
if is_declarative_flow_project_env() or not _has_project_file():
plot_declarative_flow(definition=definition)
return
_execute_declarative_flow_command(["uv", "run", "crewai", "flow", "plot"])
def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> None:
"""Run a declarative flow from a definition path."""
parsed_inputs = _parse_inputs(inputs)
try:
flow = load_declarative_flow(definition)
result = flow.kickoff(inputs=parsed_inputs)
except Exception as exc:
click.echo(
f"An error occurred while running the declarative flow: {exc}", err=True
)
raise SystemExit(1) from exc
click.echo(_format_result(result))
def plot_declarative_flow(definition: str | Path) -> None:
"""Plot a declarative flow from a definition path."""
try:
flow = load_declarative_flow(definition)
flow.plot()
except Exception as exc:
click.echo(
f"An error occurred while plotting the declarative flow: {exc}", err=True
)
raise SystemExit(1) from exc
def load_declarative_flow(definition: str | Path) -> Any:
"""Load a declarative Flow instance from a definition path."""
try:
from crewai.flow.flow import Flow
except ImportError as exc:
click.echo(
"Running declarative flows requires the full crewai package.",
err=True,
)
raise SystemExit(1) from exc
definition_path = Path(definition).expanduser()
try:
if not definition_path.is_file():
if definition_path.exists():
click.echo(
f"Invalid --definition path: {definition} is not a file.",
err=True,
)
raise SystemExit(1)
click.echo(
f"Invalid --definition path: {definition} does not exist.", err=True
)
raise SystemExit(1)
except OSError as exc:
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
raise SystemExit(1) from exc
try:
return Flow.from_declaration(path=definition_path)
except (OSError, UnicodeError, ValueError, ValidationError) as exc:
click.echo(
f"Unable to read --definition path {definition_path}: {exc}",
err=True,
)
raise SystemExit(1) from exc
def configured_project_declarative_flow(
pyproject_data: dict[str, Any] | None = None,
project_root: Path | None = None,
) -> Path | None:
"""Return the configured declarative flow source for flow projects."""
if pyproject_data is None:
try:
from crewai_cli.utils import read_toml
pyproject_data = read_toml()
except Exception:
return None
crewai_config = pyproject_data.get("tool", {}).get("crewai", {})
if crewai_config.get("type") != "flow":
return None
definition = crewai_config.get("definition")
if not isinstance(definition, str):
return None
definition = definition.strip()
if not definition:
return None
return _resolve_project_definition_path(
definition=definition,
project_root=project_root or Path.cwd(),
)
def _resolve_project_definition_path(definition: str, project_root: Path) -> Path:
definition_path = Path(definition)
windows_definition_path = PureWindowsPath(definition)
if definition.startswith("~"):
raise click.UsageError(
"[tool.crewai] definition must be a project-local path; "
f"got {definition!r}."
)
if definition_path.is_absolute() or windows_definition_path.is_absolute():
raise click.UsageError(
"[tool.crewai] definition must be relative to the project root; "
f"got {definition!r}."
)
try:
root = project_root.resolve(strict=True)
except OSError as exc:
raise click.UsageError(
f"Invalid project root for [tool.crewai] definition: {exc}"
) from exc
candidate = root / definition_path
try:
resolved_candidate = candidate.resolve(strict=False)
except OSError as exc:
raise click.UsageError(
f"Invalid [tool.crewai] definition path {definition!r}: {exc}"
) from exc
if not resolved_candidate.is_relative_to(root):
raise click.UsageError(
"[tool.crewai] definition must resolve inside the project root; "
f"got {definition!r}."
)
if not resolved_candidate.exists():
raise click.UsageError(
"[tool.crewai] definition must point to an existing file; "
f"got {definition!r}."
)
if not resolved_candidate.is_file():
raise click.UsageError(
"[tool.crewai] definition must point to a regular file; "
f"got {definition!r}."
)
return resolved_candidate
def _execute_declarative_flow_command(command: list[str]) -> None:
env = build_env_with_all_tool_credentials()
try:
subprocess.run( # noqa: S603
command,
capture_output=False,
text=True,
check=True,
env=env,
)
except subprocess.CalledProcessError as e:
raise SystemExit(e.returncode) from e
except Exception as e:
click.echo(
f"An unexpected error occurred while running the declarative flow: {e}",
err=True,
)
raise SystemExit(1) from e
def is_declarative_flow_project_env() -> bool:
import os
return os.environ.get("UV_RUN_RECURSION_DEPTH") is not None
def _has_project_file(project_root: Path | None = None) -> bool:
root = project_root or Path.cwd()
return (root / "pyproject.toml").is_file()
def _parse_inputs(inputs: str | None) -> dict[str, Any] | None:
if inputs is None:
return None
try:
parsed = json.loads(inputs)
except json.JSONDecodeError as exc:
click.echo(f"Invalid --inputs JSON: {exc}", err=True)
raise SystemExit(1) from exc
if not isinstance(parsed, dict):
click.echo("Invalid --inputs JSON: expected an object.", err=True)
raise SystemExit(1)
return parsed
def _format_result(result: Any) -> str:
raw_result = getattr(result, "raw", result)
if isinstance(raw_result, str):
return raw_result
try:
return json.dumps(raw_result, default=str)
except TypeError:
return str(raw_result)

View File

@@ -1,113 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import click
def run_flow_definition(definition: str, inputs: str | None = None) -> None:
"""Run a flow from a Flow Definition YAML/JSON string or file path."""
try:
from crewai.flow.flow import Flow
from crewai.flow.flow_definition import FlowDefinition
except ImportError as exc:
click.echo(
"Running flows from definitions requires the full crewai package.",
err=True,
)
raise SystemExit(1) from exc
parsed_inputs = _parse_inputs(inputs)
definition_source = _read_definition_source(definition)
try:
flow_definition = _parse_flow_definition(FlowDefinition, definition_source)
flow = Flow.from_definition(flow_definition)
result = flow.kickoff(inputs=parsed_inputs)
except Exception as exc:
click.echo(
f"An error occurred while running the flow definition: {exc}", err=True
)
raise SystemExit(1) from exc
click.echo(_format_result(result))
def _parse_inputs(inputs: str | None) -> dict[str, Any] | None:
if inputs is None:
return None
try:
parsed = json.loads(inputs)
except json.JSONDecodeError as exc:
click.echo(f"Invalid --inputs JSON: {exc}", err=True)
raise SystemExit(1) from exc
if not isinstance(parsed, dict):
click.echo("Invalid --inputs JSON: expected an object.", err=True)
raise SystemExit(1)
return parsed
def _read_definition_source(definition: str) -> str:
path = Path(definition).expanduser()
try:
is_file = path.is_file()
except OSError as exc:
if _looks_like_inline_definition(definition):
return definition
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
raise SystemExit(1) from exc
if is_file:
try:
return path.read_text(encoding="utf-8")
except (OSError, UnicodeError) as exc:
click.echo(
f"Unable to read --definition path {path}: {exc}",
err=True,
)
raise SystemExit(1) from exc
try:
if path.exists():
click.echo(
f"Invalid --definition path: {definition} is not a file.", err=True
)
raise SystemExit(1)
except OSError as exc:
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
raise SystemExit(1) from exc
return definition
def _looks_like_inline_definition(definition: str) -> bool:
stripped = definition.lstrip()
return "\n" in definition or stripped.startswith(("{", "---")) or ":" in stripped
def _parse_flow_definition(flow_definition_cls: type[Any], source: str) -> Any:
if _looks_like_json(source):
return flow_definition_cls.from_json(source)
return flow_definition_cls.from_yaml(source)
def _looks_like_json(source: str) -> bool:
stripped = source.lstrip()
return stripped.startswith("{")
def _format_result(result: Any) -> str:
raw_result = getattr(result, "raw", result)
if isinstance(raw_result, str):
return raw_result
try:
return json.dumps(raw_result, default=str)
except TypeError:
return str(raw_result)

View File

@@ -62,7 +62,7 @@ crewai create flow <name> --skip_provider # New flow project
# Running
crewai run # Run crew or flow (auto-detects from pyproject.toml)
crewai flow kickoff # Legacy flow execution
crewai flow kickoff # Deprecated compatibility alias for crewai run
# Testing & training
crewai test # Test crew (default: 2 iterations, gpt-4o-mini)

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.8a2"
"crewai[tools]==1.14.8a5"
]
[project.scripts]

View File

@@ -0,0 +1,5 @@
.env
.venv/
__pycache__/
.crewai/
output/

View File

@@ -0,0 +1,17 @@
# {{name}} Flow
This project defines a declarative CrewAI Flow in `src/{{folder_name}}/flow.yaml`.
## Install
```bash
crewai install
```
## Run
```bash
crewai run
```
Edit the declarative flow definition at `src/{{folder_name}}/flow.yaml` to change the flow. Add reusable crews under `src/{{folder_name}}/crews/`, custom Python tools under `src/{{folder_name}}/tools/`, and shared knowledge files under `src/{{folder_name}}/knowledge/`.

View File

@@ -0,0 +1,15 @@
schema: crewai.flow/v1
name: {{flow_name}}
description: A declarative CrewAI Flow.
state:
type: dict
default:
topic: AI agents
methods:
start:
start: true
do:
call: expression
expr: state.topic

View File

@@ -0,0 +1,20 @@
[project]
name = "{{folder_name}}"
version = "0.1.0"
description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.14.8a5"
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/{{folder_name}}"]
[tool.crewai]
type = "flow"
definition = "src/{{folder_name}}/flow.yaml"

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.8a2"
"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.8a2"
"crewai[tools]==1.14.8a5"
]
[tool.crewai]

View File

@@ -132,7 +132,7 @@ def test_create_project_zip_excludes_symlinked_files(tmp_path: Path):
assert names == {"pyproject.toml"}
def test_create_project_zip_adds_json_project_wrapper(tmp_path: Path):
def test_create_project_zip_preserves_json_project_shape(tmp_path: Path):
(tmp_path / "pyproject.toml").write_text(
"""
[project]
@@ -157,8 +157,6 @@ type = "crew"
try:
with zipfile.ZipFile(archive_path) as archive:
names = set(archive.namelist())
crew_py = archive.read("src/json_crew/crew.py").decode()
main_py = archive.read("src/json_crew/main.py").decode()
pyproject = archive.read("pyproject.toml").decode()
finally:
archive_path.unlink(missing_ok=True)
@@ -166,18 +164,50 @@ type = "crew"
assert "uv.lock" not in names
assert "crew.jsonc" in names
assert "agents/researcher.jsonc" in names
assert "src/json_crew/__init__.py" in names
assert "src/json_crew/crew.py" in names
assert "src/json_crew/main.py" in names
assert "src/json_crew/config/agents.yaml" in names
assert "src/json_crew/config/tasks.yaml" in names
assert "load_crew(_crew_path())" in crew_py
assert "JsonCrew" in crew_py
assert "from json_crew.crew import JsonCrew" in main_py
assert "run_crew = \"json_crew.main:run\"" in pyproject
assert all(not name.startswith("src/") for name in names)
assert "run_crew" not in pyproject
assert "json_crew =" not in pyproject
assert "[project.scripts]" not in pyproject
def test_create_project_zip_updates_existing_json_project_scripts(tmp_path: Path):
def test_create_project_zip_keeps_json_project_root_shape(tmp_path: Path):
(tmp_path / "pyproject.toml").write_text(
"""
[project]
name = "json_crew"
version = "0.1.0"
dependencies = ["crewai[tools]==1.14.8a1"]
[tool.crewai]
type = "crew"
""".strip()
+ "\n"
)
(tmp_path / "uv.lock").write_text("# lock\n")
(tmp_path / "agents").mkdir()
(tmp_path / "agents" / "foo.jsonc").write_text("{}\n")
(tmp_path / "crew.jsonc").write_text("{}\n")
archive_path = create_project_zip("json_crew", project_dir=tmp_path)
try:
with zipfile.ZipFile(archive_path) as archive:
names = set(archive.namelist())
pyproject = archive.read("pyproject.toml").decode()
finally:
archive_path.unlink(missing_ok=True)
assert names == {
"agents/foo.jsonc",
"crew.jsonc",
"pyproject.toml",
"uv.lock",
}
assert "run_crew" not in pyproject
assert "json_crew =" not in pyproject
assert "[project.scripts]" not in pyproject
def test_create_project_zip_does_not_rewrite_json_project_scripts(tmp_path: Path):
(tmp_path / "pyproject.toml").write_text(
"""
[project]
@@ -203,14 +233,10 @@ type = "crew"
finally:
archive_path.unlink(missing_ok=True)
assert 'json_crew = "json_crew.main:run"' in pyproject
assert 'run_crew = "json_crew.main:run"' in pyproject
assert 'train = "json_crew.main:train"' in pyproject
assert 'replay = "json_crew.main:replay"' in pyproject
assert 'test = "json_crew.main:test"' in pyproject
assert 'run_with_trigger = "json_crew.main:run_with_trigger"' in pyproject
assert 'json_crew = "old.module:run"' in pyproject
assert 'run_crew = "old.module:run"' in pyproject
assert 'custom = "custom.module:main"' in pyproject
assert "old.module:run" not in pyproject
assert pyproject.count("[project.scripts]") == 1
assert "[tool.crewai]" in pyproject
@@ -221,7 +247,7 @@ type = "crew"
'[tool]\ncrewai = "invalid"\n',
],
)
def test_create_project_zip_adds_json_wrapper_for_malformed_tool_config(
def test_create_project_zip_preserves_json_project_with_malformed_tool_config(
tmp_path: Path, tool_config: str
):
(tmp_path / "pyproject.toml").write_text(
@@ -244,12 +270,13 @@ version = "0.1.0"
finally:
archive_path.unlink(missing_ok=True)
assert "src/json_crew/crew.py" in names
assert "src/json_crew/main.py" in names
assert "run_crew = \"json_crew.main:run\"" in pyproject
assert names == {"crew.jsonc", "pyproject.toml"}
assert "run_crew" not in pyproject
assert "json_crew =" not in pyproject
assert "[project.scripts]" not in pyproject
def test_create_project_zip_rejects_empty_normalized_package_name(tmp_path: Path):
def test_create_project_zip_accepts_json_project_without_package_name(tmp_path: Path):
(tmp_path / "pyproject.toml").write_text(
"""
[project]
@@ -263,8 +290,15 @@ type = "crew"
)
(tmp_path / "crew.jsonc").write_text("{}\n")
with pytest.raises(
ValueError,
match=r"Could not derive a valid Python package name",
):
create_project_zip("invalid", project_dir=tmp_path)
archive_path = create_project_zip("invalid", project_dir=tmp_path)
try:
with zipfile.ZipFile(archive_path) as archive:
names = set(archive.namelist())
pyproject = archive.read("pyproject.toml").decode()
finally:
archive_path.unlink(missing_ok=True)
assert names == {"crew.jsonc", "pyproject.toml"}
assert "run_crew" not in pyproject
assert "json_crew =" not in pyproject
assert "[project.scripts]" not in pyproject

View File

@@ -200,6 +200,41 @@ def test_json_runtime_fields_are_deploy_errors(tmp_path: Path) -> None:
assert "runtime-only" in finding.detail
def test_json_crew_requires_agents_dir_without_classic_errors(tmp_path: Path) -> None:
_scaffold_json_crew(tmp_path)
for path in (tmp_path / "agents").iterdir():
path.unlink()
(tmp_path / "agents").rmdir()
v = DeployValidator(project_root=tmp_path)
v.run()
codes = _codes(v)
assert "missing_agents_dir" in codes
assert "missing_src_dir" not in codes
assert "missing_crew_py" not in codes
assert "missing_agents_yaml" not in codes
assert "missing_tasks_yaml" not in codes
def test_json_crew_reports_project_metadata_before_invalid_json(
tmp_path: Path,
) -> None:
_scaffold_json_crew(tmp_path)
(tmp_path / "pyproject.toml").unlink()
(tmp_path / "uv.lock").unlink()
(tmp_path / "crew.jsonc").write_text('{"agents": ["researcher"], "tasks": []}\n')
v = DeployValidator(project_root=tmp_path)
v.run()
codes = _codes(v)
assert "missing_pyproject" in codes
assert "missing_lockfile" in codes
assert "invalid_crew_json" in codes
assert "missing_src_dir" not in codes
def test_missing_pyproject_errors(tmp_path: Path) -> None:
v = _run_without_import_check(tmp_path)
assert "missing_pyproject" in _codes(v)

View File

@@ -0,0 +1,140 @@
"""Regression tests for path-traversal-safe archive extraction.
Guards against symlink/hardlink-based path traversal in the fallback used on
Python versions without tarfile extraction filters. The filtered path relies on
`tarfile.extractall(..., filter="data")`; the fallback must provide the same
protection by validating link targets, not just member names.
"""
from __future__ import annotations
import io
import tarfile
from pathlib import Path
import pytest
from crewai_cli.experimental.skills.main import _safe_extractall
def _tar_from_members(build) -> tarfile.TarFile:
"""Build an in-memory tar archive via `build(tf)` and return it for reading."""
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tf:
build(tf)
buf.seek(0)
return tarfile.open(fileobj=buf, mode="r")
def test_blocks_symlink_escaping_destination(tmp_path: Path) -> None:
"""A symlink whose target escapes dest, plus a file written through it,
must be rejected before anything is extracted."""
outside = tmp_path / "outside"
outside.mkdir()
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
link = tarfile.TarInfo("link")
link.type = tarfile.SYMTYPE
link.linkname = str(outside) # absolute path outside dest
tf.addfile(link)
payload = b"pwned"
info = tarfile.TarInfo("link/evil.txt")
info.size = len(payload)
tf.addfile(info, io.BytesIO(payload))
with _tar_from_members(build) as tf:
with pytest.raises(ValueError, match="escaping destination"):
_safe_extractall(tf, dest)
assert not (outside / "evil.txt").exists()
def test_blocks_relative_symlink_escaping_destination(tmp_path: Path) -> None:
"""A relative symlink (../..) that escapes dest is also rejected."""
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
link = tarfile.TarInfo("sub/link")
link.type = tarfile.SYMTYPE
link.linkname = "../../outside" # escapes dest from sub/
tf.addfile(link)
with _tar_from_members(build) as tf:
with pytest.raises(ValueError, match="escaping destination"):
_safe_extractall(tf, dest)
def test_blocks_hardlink_escaping_destination(tmp_path: Path) -> None:
"""A hardlink whose target escapes dest is rejected."""
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
link = tarfile.TarInfo("escape")
link.type = tarfile.LNKTYPE
link.linkname = "../outside.txt" # escapes archive root
tf.addfile(link)
with _tar_from_members(build) as tf:
with pytest.raises(ValueError, match="escaping destination"):
_safe_extractall(tf, dest)
def test_blocks_special_tar_member(tmp_path: Path) -> None:
"""Special tar members such as FIFOs are rejected."""
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
fifo = tarfile.TarInfo("pipe")
fifo.type = tarfile.FIFOTYPE
tf.addfile(fifo)
with _tar_from_members(build) as tf:
with pytest.raises(ValueError, match="unsupported tar member"):
_safe_extractall(tf, dest)
def test_allows_benign_relative_symlink(tmp_path: Path) -> None:
"""A symlink that stays within dest is permitted."""
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
payload = b"hi"
info = tarfile.TarInfo("real.txt")
info.size = len(payload)
tf.addfile(info, io.BytesIO(payload))
link = tarfile.TarInfo("alias.txt")
link.type = tarfile.SYMTYPE
link.linkname = "real.txt" # stays inside dest
tf.addfile(link)
with _tar_from_members(build) as tf:
_safe_extractall(tf, dest)
assert (dest / "real.txt").read_bytes() == b"hi"
assert (dest / "alias.txt").is_symlink()
assert (dest / "alias.txt").readlink() == Path("real.txt")
def test_allows_benign_archive(tmp_path: Path) -> None:
"""An ordinary archive of regular files extracts correctly."""
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
for name, body in (("SKILL.md", b"# skill"), ("scripts/run.py", b"print(1)")):
payload = body
info = tarfile.TarInfo(name)
info.size = len(payload)
tf.addfile(info, io.BytesIO(payload))
with _tar_from_members(build) as tf:
_safe_extractall(tf, dest)
assert (dest / "SKILL.md").read_bytes() == b"# skill"
assert (dest / "scripts" / "run.py").read_bytes() == b"print(1)"

View File

@@ -12,6 +12,7 @@ from crewai_cli.cli import (
deploy_remove,
deply_status,
flow_add_crew,
flow_run,
login,
reset_memories,
run,
@@ -126,38 +127,75 @@ def test_run_uses_project_runner_by_default(run_crew, runner):
result = runner.invoke(run)
assert result.exit_code == 0
run_crew.assert_called_once_with(trained_agents_file=None)
run_crew.assert_called_once_with(
trained_agents_file=None,
definition=None,
inputs=None,
)
assert "experimental" not in result.output.lower()
@mock.patch("crewai_cli.cli.run_flow_definition")
def test_run_with_definition_uses_definition_runner(run_flow_definition, runner):
@mock.patch("crewai_cli.cli.run_crew")
def test_run_with_definition_uses_project_runner(run_crew, runner):
result = runner.invoke(
run,
["--definition", "flow.yaml", "--inputs", '{"topic":"AI"}'],
)
assert result.exit_code == 0
assert (
"Warning: `crewai run --definition` is experimental and may change without notice."
in result.output
)
run_flow_definition.assert_called_once_with(
definition="flow.yaml", inputs='{"topic":"AI"}'
run_crew.assert_called_once_with(
trained_agents_file=None,
definition="flow.yaml",
inputs='{"topic":"AI"}',
)
@mock.patch("crewai_cli.cli.run_crew")
@mock.patch("crewai_cli.cli.run_flow_definition")
def test_run_rejects_inputs_without_definition(run_flow_definition, run_crew, runner):
def test_run_rejects_inputs_without_definition(run_crew, runner):
result = runner.invoke(run, ["--inputs", '{"topic":"AI"}'])
assert result.exit_code == 2
assert "Error: --inputs requires --definition" in result.output
run_flow_definition.assert_not_called()
run_crew.assert_not_called()
@mock.patch("crewai_cli.cli.run_crew")
def test_run_rejects_filename_with_definition(run_crew, runner):
result = runner.invoke(run, ["--definition", "flow.yaml", "--filename", "x.pkl"])
assert result.exit_code == 2
assert "Error: --filename can only be used when running crews" in result.output
run_crew.assert_not_called()
@mock.patch("crewai_cli.cli.run_crew")
def test_run_passes_filename_to_project_runner(run_crew, runner):
result = runner.invoke(run, ["--filename", "trained.pkl"])
assert result.exit_code == 0
run_crew.assert_called_once_with(
trained_agents_file="trained.pkl",
definition=None,
inputs=None,
)
@mock.patch("crewai_cli.cli.run_crew")
def test_flow_kickoff_is_deprecated_and_uses_run_path(run_crew, runner):
result = runner.invoke(flow_run)
assert result.exit_code == 0
run_crew.assert_called_once_with(
trained_agents_file=None,
definition=None,
inputs=None,
)
assert (
"The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead."
in result.output
)
@mock.patch("crewai_cli.create_json_crew.create_json_crew")
def test_create_crew_in_dmn_mode_skips_provider_prompts(create_json_crew, runner):
result = runner.invoke(create, ["crew", "DMN Crew"], env={"CREWAI_DMN": "True"})
@@ -166,6 +204,23 @@ def test_create_crew_in_dmn_mode_skips_provider_prompts(create_json_crew, runner
create_json_crew.assert_called_once_with("DMN Crew", None, True)
@mock.patch("crewai_cli.create_flow.create_flow")
def test_create_flow_declarative_uses_declarative_scaffold(create_flow, runner):
result = runner.invoke(create, ["flow", "My Flow", "--declarative"])
assert result.exit_code == 0
create_flow.assert_called_once_with("My Flow", declarative=True)
@mock.patch("crewai_cli.create_json_crew.create_json_crew")
def test_create_crew_rejects_declarative_flag(create_json_crew, runner):
result = runner.invoke(create, ["crew", "My Crew", "--declarative"])
assert result.exit_code == 2
assert "--declarative can only be used with flow projects" in result.output
create_json_crew.assert_not_called()
def test_create_requires_type_in_dmn_mode(runner):
result = runner.invoke(create, env={"CREWAI_DMN": "True"})

View File

@@ -712,8 +712,26 @@ def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch):
default_llm="openai/gpt-5.5",
)
assert (tmp_path / "json_crew" / "crew.jsonc").exists()
assert not (tmp_path / "json_crew" / "src").exists()
assert not (tmp_path / "json_crew" / "tests").exists()
assert not (tmp_path / "json_crew" / "config.jsonc").exists()
generated_paths = {
path.relative_to(tmp_path / "json_crew").as_posix()
for path in (tmp_path / "json_crew").rglob("*")
if path.is_file()
}
assert not any(
path.endswith("/crew.py") or path == "crew.py" for path in generated_paths
)
assert not any(
path.endswith("/agents.yaml") or path == "agents.yaml"
for path in generated_paths
)
assert not any(
path.endswith("/tasks.yaml") or path == "tasks.yaml"
for path in generated_paths
)
assert not any(path.startswith("src/") for path in generated_paths)
pyproject = tomli.loads((tmp_path / "json_crew" / "pyproject.toml").read_text())
dependency = pyproject["project"]["dependencies"][0]
@@ -849,7 +867,7 @@ def test_json_create_dmn_mode_uses_non_interactive_defaults(tmp_path, monkeypatc
crew_template = (project_root / "crew.jsonc").read_text()
agent_template = (project_root / "agents" / "researcher.jsonc").read_text()
assert '"memory": false' in crew_template
assert '"memory": true' in crew_template
assert '"description": "Research current AI trends and write a concise summary."' in (
crew_template
)

View File

@@ -0,0 +1,35 @@
from __future__ import annotations
from pathlib import Path
from click.testing import CliRunner
from pytest import MonkeyPatch
import tomli
from crewai_cli.cli import crewai
from crewai_cli.create_flow import create_flow
def test_create_flow_declarative_project_can_run(
tmp_path: Path, monkeypatch: MonkeyPatch
):
monkeypatch.chdir(tmp_path)
create_flow("Research Flow", declarative=True)
project_root = tmp_path / "research_flow"
assert project_root.is_dir()
pyproject = tomli.loads(
(project_root / "pyproject.toml").read_text(encoding="utf-8")
)
assert pyproject["project"]["name"] == "research_flow"
assert pyproject["project"]["requires-python"]
assert pyproject["project"]["dependencies"]
assert (project_root / pyproject["tool"]["crewai"]["definition"]).is_file()
monkeypatch.chdir(project_root)
result = CliRunner().invoke(crewai, ["run"], env={"UV_RUN_RECURSION_DEPTH": "1"})
assert result.exit_code == 0
assert "Running the Flow" not in result.output
assert "AI agents" in result.output

View File

@@ -126,6 +126,52 @@ def test_chain_deploy_does_not_login_for_deploy_exit(monkeypatch, capsys) -> Non
assert "Deploy failed with exit code 42" in capsys.readouterr().out
def test_conversation_turn_done_records_assistant_message() -> None:
class RawResult:
raw = "hello from the flow"
app = CrewRunApp(conversational=True)
app._conversation_turn_in_progress = True
app._enable_conversation_input = lambda: None # type: ignore[method-assign]
app._tick = lambda: None # type: ignore[method-assign]
app._scroll_to_result = lambda: None # type: ignore[method-assign]
app._on_conversation_turn_done(RawResult())
assert app._conversation_messages == [("assistant", "hello from the flow")]
assert app._conversation_turn_in_progress is False
assert app._status == "chatting"
assert isinstance(app._crew_result, RawResult)
@pytest.mark.asyncio
async def test_conversation_input_submits_turn() -> None:
class FakeFlow:
defer_trace_finalization = False
def handle_turn(self, message: str) -> str:
return f"reply: {message}"
def finalize_session_traces(self) -> None:
pass
app = CrewRunApp(crew_name="Demo", conversational=True)
app._flow = FakeFlow()
async with app.run_test() as pilot:
await pilot.click("#conversation-input")
await pilot.press("h", "i", "enter")
for _ in range(50):
await pilot.pause(0.05)
if app._conversation_messages[-1:] == [("assistant", "reply: hi")]:
break
assert app._conversation_messages == [
("user", "hi"),
("assistant", "reply: hi"),
]
def test_plan_step_status_updates_only_the_explicit_step() -> None:
app = _app_with_plan()

View File

@@ -0,0 +1,248 @@
from __future__ import annotations
from pathlib import Path
import subprocess
import click
import pytest
from click.testing import CliRunner
from crewai_cli.cli import flow_run
import crewai_cli.plot_flow as plot_flow_module
FLOW_YAML = """\
schema: crewai.flow/v1
name: TestFlow
config:
suppress_flow_events: true
methods:
begin:
start: true
do:
call: expression
expr: "'AI'"
"""
def _write_flow_project(project_root: Path) -> None:
(project_root / "flow.yaml").write_text(FLOW_YAML, encoding="utf-8")
(project_root / "pyproject.toml").write_text(
'[project]\nname = "demo"\n\n'
'[tool.crewai]\ntype = "flow"\ndefinition = "flow.yaml"\n',
encoding="utf-8",
)
def test_flow_kickoff_runs_configured_declarative_definition(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_write_flow_project(tmp_path)
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1")
result = CliRunner().invoke(flow_run)
assert result.exit_code == 0
assert (
"The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead."
in result.output
)
assert "AI\n" in result.output
assert "Running the Flow" not in result.output
def test_plot_flow_runs_configured_declarative_definition(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
_write_flow_project(tmp_path)
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1")
plot_flow_module.plot_flow()
def test_flow_kickoff_delegates_to_run_crew(
monkeypatch: pytest.MonkeyPatch,
) -> None:
calls = []
monkeypatch.setattr(
"crewai_cli.cli.run_crew",
lambda **kwargs: calls.append(kwargs),
)
result = CliRunner().invoke(flow_run)
assert result.exit_code == 0
assert calls == [
{"trained_agents_file": None, "definition": None, "inputs": None},
]
def test_plot_flow_keeps_python_entrypoint_without_definition(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
subprocess_calls = []
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(
subprocess,
"run",
lambda command, **kwargs: subprocess_calls.append((command, kwargs)),
)
plot_flow_module.plot_flow()
assert subprocess_calls == [
(
["uv", "run", "plot"],
{"capture_output": False, "text": True, "check": True},
)
]
def test_configured_project_declarative_flow(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
definition_path = tmp_path / "flow.yaml"
definition_path.write_text(FLOW_YAML, encoding="utf-8")
(tmp_path / "pyproject.toml").write_text(
'[tool.crewai]\ntype = "flow"\ndefinition = " flow.yaml "\n',
encoding="utf-8",
)
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
assert configured_project_declarative_flow() == definition_path.resolve()
@pytest.mark.parametrize(
("definition", "expected_error"),
[
("C:/tmp/flow.yaml", "must be relative to the project root"),
("~/flow.yaml", "must be a project-local path"),
("../flow.yaml", "must resolve inside the project root"),
],
)
def test_configured_project_declarative_flow_rejects_unsafe_paths(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
definition: str,
expected_error: str,
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "pyproject.toml").write_text(
f'[tool.crewai]\ntype = "flow"\ndefinition = "{definition}"\n',
encoding="utf-8",
)
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
with pytest.raises(click.UsageError) as exc_info:
configured_project_declarative_flow()
assert expected_error in exc_info.value.message
def test_configured_project_declarative_flow_allows_normalized_project_path(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.chdir(tmp_path)
definition_path = tmp_path / "flow.yaml"
definition_path.write_text(FLOW_YAML, encoding="utf-8")
(tmp_path / "src").mkdir()
(tmp_path / "pyproject.toml").write_text(
'[tool.crewai]\ntype = "flow"\ndefinition = "src/../flow.yaml"\n',
encoding="utf-8",
)
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
assert configured_project_declarative_flow() == definition_path.resolve()
def test_configured_project_declarative_flow_rejects_absolute_path(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.chdir(tmp_path)
definition = tmp_path / "flow.yaml"
(tmp_path / "pyproject.toml").write_text(
f'[tool.crewai]\ntype = "flow"\ndefinition = "{definition.as_posix()}"\n',
encoding="utf-8",
)
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
with pytest.raises(click.UsageError) as exc_info:
configured_project_declarative_flow()
assert "must be relative to the project root" in exc_info.value.message
def test_configured_project_declarative_flow_rejects_symlink_escape(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.chdir(tmp_path)
outside_definition = tmp_path.parent / "outside-flow.yaml"
outside_definition.write_text(FLOW_YAML, encoding="utf-8")
link = tmp_path / "flow.yaml"
try:
link.symlink_to(outside_definition)
except (NotImplementedError, OSError) as exc:
pytest.skip(f"symlinks unavailable: {exc}")
(tmp_path / "pyproject.toml").write_text(
'[tool.crewai]\ntype = "flow"\ndefinition = "flow.yaml"\n',
encoding="utf-8",
)
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
with pytest.raises(click.UsageError) as exc_info:
configured_project_declarative_flow()
assert "must resolve inside the project root" in exc_info.value.message
def test_configured_project_declarative_flow_rejects_missing_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "pyproject.toml").write_text(
'[tool.crewai]\ntype = "flow"\ndefinition = "missing-flow.yaml"\n',
encoding="utf-8",
)
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
with pytest.raises(click.UsageError) as exc_info:
configured_project_declarative_flow()
assert "must point to an existing file" in exc_info.value.message
def test_configured_project_declarative_flow_rejects_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "flow.yaml").mkdir()
(tmp_path / "pyproject.toml").write_text(
'[tool.crewai]\ntype = "flow"\ndefinition = "flow.yaml"\n',
encoding="utf-8",
)
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
with pytest.raises(click.UsageError) as exc_info:
configured_project_declarative_flow()
assert "must point to a regular file" in exc_info.value.message

View File

@@ -0,0 +1,63 @@
from __future__ import annotations
import sys
from crewai_cli import kickoff_flow
def test_loads_conversational_flow_from_kickoff_script(tmp_path, monkeypatch) -> None:
package_dir = tmp_path / "src" / "demo_chat"
package_dir.mkdir(parents=True)
(package_dir / "__init__.py").write_text("")
(package_dir / "main.py").write_text(
"\n".join(
[
"from crewai.flow import Flow",
"",
"class DemoChatFlow(Flow):",
" conversational = True",
]
)
)
(tmp_path / "pyproject.toml").write_text(
"\n".join(
[
"[project]",
'name = "demo-chat"',
"[project.scripts]",
'kickoff = "demo_chat.main:kickoff"',
]
)
)
monkeypatch.chdir(tmp_path)
sys.modules.pop("demo_chat.main", None)
sys.modules.pop("demo_chat", None)
flow = kickoff_flow._load_conversational_flow_from_kickoff_script()
assert flow is not None
assert type(flow).__name__ == "DemoChatFlow"
assert flow.conversational is True
def test_kickoff_flow_falls_back_to_uv_when_no_conversational_flow(
monkeypatch,
) -> None:
calls: list[list[str]] = []
def fake_run(command, capture_output, text, check):
calls.append(command)
class Result:
stderr = ""
return Result()
monkeypatch.setattr(
kickoff_flow, "_load_conversational_flow_from_kickoff_script", lambda: None
)
monkeypatch.setattr(kickoff_flow.subprocess, "run", fake_run)
kickoff_flow.kickoff_flow()
assert calls == [["uv", "run", "kickoff"]]

View File

@@ -568,3 +568,175 @@ def test_has_json_crew_true_without_pyproject(monkeypatch, tmp_path: Path):
(tmp_path / "crew.jsonc").write_text("{}")
assert run_crew_module._has_json_crew() is True
def test_run_crew_rejects_inputs_without_definition():
with pytest.raises(click.UsageError) as exc_info:
run_crew_module.run_crew(inputs='{"topic":"AI"}')
assert "--inputs requires --definition" in exc_info.value.message
def test_run_crew_rejects_filename_with_explicit_definition():
with pytest.raises(click.UsageError) as exc_info:
run_crew_module.run_crew(
trained_agents_file="trained.pkl",
definition="flow.yaml",
)
assert "--filename can only be used when running crews" in exc_info.value.message
def test_run_crew_runs_explicit_declarative_definition(monkeypatch, capsys):
calls = []
def fake_run_declarative_flow(definition: str, inputs: str | None = None):
calls.append((definition, inputs))
monkeypatch.setattr(
"crewai_cli.run_declarative_flow.run_declarative_flow",
fake_run_declarative_flow,
)
run_crew_module.run_crew(definition="flow.yaml", inputs='{"topic":"AI"}')
captured = capsys.readouterr()
assert "experimental" not in captured.out.lower()
assert calls == [("flow.yaml", '{"topic":"AI"}')]
def test_run_crew_runs_classic_crew_project(monkeypatch, capsys):
calls = []
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "crew"}}},
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda script_name, **kwargs: calls.append((script_name, kwargs)),
)
run_crew_module.run_crew(trained_agents_file="trained.pkl")
assert capsys.readouterr().out == ""
assert calls == [
(
"run_crew",
{"entity_type": "crew", "trained_agents_file": "trained.pkl"},
)
]
def test_run_crew_runs_python_flow_project(monkeypatch, capsys):
calls = []
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "flow"}}},
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda script_name, **kwargs: calls.append((script_name, kwargs)),
)
monkeypatch.setattr(
"crewai_cli.kickoff_flow._load_conversational_flow_from_kickoff_script",
lambda: None,
)
run_crew_module.run_crew()
assert capsys.readouterr().out == ""
assert calls == [("kickoff", {"entity_type": "flow"})]
def test_run_crew_runs_conversational_flow_tui(monkeypatch, capsys):
class Flow:
pass
flow = Flow()
calls = []
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "flow"}}},
)
monkeypatch.setattr(
"crewai_cli.kickoff_flow._load_conversational_flow_from_kickoff_script",
lambda: flow,
)
monkeypatch.setattr(
"crewai_cli.kickoff_flow._run_conversational_flow_tui",
lambda loaded_flow: calls.append(loaded_flow),
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda *_args, **_kwargs: pytest.fail(
"conversational flows must use the TUI"
),
)
run_crew_module.run_crew()
assert capsys.readouterr().out == ""
assert calls == [flow]
def test_run_crew_rejects_filename_for_flow_project(monkeypatch):
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "flow"}}},
)
with pytest.raises(click.UsageError) as exc_info:
run_crew_module.run_crew(trained_agents_file="trained.pkl")
assert "--filename can only be used when running crews" in exc_info.value.message
def test_run_crew_runs_configured_declarative_flow_project(
monkeypatch, tmp_path: Path, capsys
):
calls = []
monkeypatch.chdir(tmp_path)
definition_path = tmp_path / "flow.yaml"
definition_path.write_text("schema: crewai.flow/v1\n", encoding="utf-8")
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {
"tool": {
"crewai": {
"type": "flow",
"definition": "flow.yaml",
}
}
},
)
monkeypatch.setattr(
"crewai_cli.run_declarative_flow.run_declarative_flow_in_project_env",
lambda definition, inputs=None: calls.append((definition, inputs)),
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda *_args, **_kwargs: pytest.fail("declarative flows must not run kickoff"),
)
run_crew_module.run_crew()
assert capsys.readouterr().out == ""
assert calls == [(definition_path.resolve(), None)]

View File

@@ -0,0 +1,148 @@
from __future__ import annotations
from pathlib import Path
import pytest
import crewai_cli.run_declarative_flow as run_declarative_flow_module
FLOW_YAML = """\
schema: crewai.flow/v1
name: TestFlow
config:
suppress_flow_events: true
methods:
begin:
start: true
do:
call: expression
expr: state.topic
"""
def test_run_declarative_flow_reads_definition_file(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
definition_path = tmp_path / "flow.yaml"
definition_path.write_text(FLOW_YAML, encoding="utf-8")
run_declarative_flow_module.run_declarative_flow(
str(definition_path), '{"topic":"AI"}'
)
assert capsys.readouterr().out == "AI\n"
def test_run_declarative_flow_rejects_non_object_inputs(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
definition_path = tmp_path / "flow.yaml"
definition_path.write_text(FLOW_YAML, encoding="utf-8")
with pytest.raises(SystemExit):
run_declarative_flow_module.run_declarative_flow(
str(definition_path), '["not", "an", "object"]'
)
assert "Invalid --inputs JSON: expected an object." in capsys.readouterr().err
def test_run_declarative_flow_reports_missing_file(
capsys: pytest.CaptureFixture[str],
) -> None:
with pytest.raises(SystemExit):
run_declarative_flow_module.run_declarative_flow("missing-flow.yaml")
assert (
"Invalid --definition path: missing-flow.yaml does not exist."
in capsys.readouterr().err
)
def test_run_declarative_flow_reports_empty_file(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
definition_path = tmp_path / "flow.yaml"
definition_path.write_text(" \n", encoding="utf-8")
with pytest.raises(SystemExit):
run_declarative_flow_module.run_declarative_flow(str(definition_path))
assert "Flow declaration file is empty" in capsys.readouterr().err
@pytest.mark.parametrize(
"contents, expected_error",
[
("[]\n", "Flow declaration must contain a mapping"),
("schema: crewai.flow/v1\nmethods: {}\n", "Field required"),
],
)
def test_load_declarative_flow_reports_invalid_declarations(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
contents: str,
expected_error: str,
) -> None:
definition_path = tmp_path / "flow.yaml"
definition_path.write_text(contents, encoding="utf-8")
with pytest.raises(SystemExit) as exc_info:
run_declarative_flow_module.load_declarative_flow(str(definition_path))
assert exc_info.value.code == 1
stderr = capsys.readouterr().err
assert f"Unable to read --definition path {definition_path}:" in stderr
assert expected_error in stderr
def test_run_declarative_flow_in_project_env_uses_uv(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
subprocess_calls = []
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("UV_RUN_RECURSION_DEPTH", raising=False)
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
monkeypatch.setattr(
run_declarative_flow_module,
"build_env_with_all_tool_credentials",
lambda: {"EXISTING": "value"},
)
monkeypatch.setattr(
run_declarative_flow_module.subprocess,
"run",
lambda command, **kwargs: subprocess_calls.append((command, kwargs)),
)
run_declarative_flow_module.run_declarative_flow_in_project_env("flow.yaml")
assert subprocess_calls == [
(
["uv", "run", "crewai", "run"],
{
"capture_output": False,
"text": True,
"check": True,
"env": {"EXISTING": "value"},
},
)
]
def test_run_declarative_flow_in_process_inside_uv(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1")
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
(tmp_path / "flow.yaml").write_text(FLOW_YAML, encoding="utf-8")
run_declarative_flow_module.run_declarative_flow_in_project_env(
"flow.yaml", '{"topic":"AI"}'
)
assert capsys.readouterr().out == "AI\n"

View File

@@ -1,156 +0,0 @@
from __future__ import annotations
import json
import sys
import types
import pytest
import yaml
from crewai_cli.run_flow_definition import run_flow_definition
class _FakeFlow:
def __init__(self, definition):
self.definition = definition
def kickoff(self, inputs=None):
return {
"flow": self.definition["name"],
"inputs": inputs or {},
}
class _FakeFlowFactory:
@classmethod
def from_definition(cls, definition):
return _FakeFlow(definition)
class _FakeFlowDefinition:
@classmethod
def from_yaml(cls, source):
return yaml.safe_load(source)
@classmethod
def from_json(cls, source):
return json.loads(source)
@pytest.fixture
def fake_flow_runtime(monkeypatch):
crewai_module = types.ModuleType("crewai")
flow_package = types.ModuleType("crewai.flow")
flow_module = types.ModuleType("crewai.flow.flow")
flow_definition_module = types.ModuleType("crewai.flow.flow_definition")
flow_module.Flow = _FakeFlowFactory
flow_definition_module.FlowDefinition = _FakeFlowDefinition
monkeypatch.setitem(sys.modules, "crewai", crewai_module)
monkeypatch.setitem(sys.modules, "crewai.flow", flow_package)
monkeypatch.setitem(sys.modules, "crewai.flow.flow", flow_module)
monkeypatch.setitem(
sys.modules, "crewai.flow.flow_definition", flow_definition_module
)
def _captured_json(capsys):
return json.loads(capsys.readouterr().out)
def test_run_flow_definition_reads_definition_file(
tmp_path, capsys, fake_flow_runtime
):
definition_path = tmp_path / "flow.yaml"
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
run_flow_definition(str(definition_path), '{"topic":"AI"}')
assert _captured_json(capsys) == {
"flow": "TestFlow",
"inputs": {"topic": "AI"},
}
@pytest.mark.parametrize(
("definition_source", "expected_flow_name"),
[
pytest.param(
"schema: crewai.flow/v1\nname: InlineFlow\n",
"InlineFlow",
id="inline-yaml",
),
pytest.param(
'{"schema":"crewai.flow/v1","name":"InlineJsonFlow"}',
"InlineJsonFlow",
id="inline-json",
),
pytest.param(
'{"schema":"crewai.flow/v1","name":"' + ("JsonFlow" * 500) + '"}',
"JsonFlow" * 500,
id="large-inline-json",
),
],
)
def test_run_flow_definition_accepts_inline_definitions(
definition_source, expected_flow_name, capsys, fake_flow_runtime
):
run_flow_definition(definition_source)
assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}}
@pytest.mark.parametrize(
("filename", "definition_source", "expected_flow_name"),
[
pytest.param(
"flow.yaml",
"schema: crewai.flow/v1\nname: YamlFileFlow\n",
"YamlFileFlow",
id="yaml-file",
),
pytest.param(
"flow.json",
'{"schema":"crewai.flow/v1","name":"JsonFlow"}',
"JsonFlow",
id="json-file",
),
],
)
def test_run_flow_definition_accepts_definition_files(
filename, definition_source, expected_flow_name, tmp_path, capsys, fake_flow_runtime
):
definition_path = tmp_path / filename
definition_path.write_text(definition_source)
run_flow_definition(str(definition_path))
assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}}
def test_run_flow_definition_rejects_non_object_inputs(fake_flow_runtime, capsys):
with pytest.raises(SystemExit):
run_flow_definition("name: TestFlow", '["not", "an", "object"]')
assert "Invalid --inputs JSON: expected an object." in capsys.readouterr().err
def test_run_flow_definition_reports_unreadable_file(
monkeypatch, tmp_path, capsys, fake_flow_runtime
):
definition_path = tmp_path / "flow.yaml"
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
def raise_permission_error(self, *args, **kwargs):
raise PermissionError("no access")
monkeypatch.setattr("pathlib.Path.read_text", raise_permission_error)
with pytest.raises(SystemExit):
run_flow_definition(str(definition_path))
err = capsys.readouterr().err
assert "Unable to read --definition path" in err
assert str(definition_path) in err
assert "no access" in err

View File

@@ -16,9 +16,9 @@ dependencies = [
"pyjwt>=2.13.0,<3",
"pydantic>=2.11.9,<2.13",
"rich>=13.7.1",
"opentelemetry-api~=1.34.0",
"opentelemetry-sdk~=1.34.0",
"opentelemetry-exporter-otlp-proto-http~=1.34.0",
"opentelemetry-api~=1.42.0",
"opentelemetry-sdk~=1.42.0",
"opentelemetry-exporter-otlp-proto-http~=1.42.0",
"tomli~=2.0.2",
]

View File

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

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.14.8a2"
__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.8a2",
"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.8a2"
__version__ = "1.14.8a5"

View File

@@ -8,8 +8,8 @@ authors = [
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai-core==1.14.8a2",
"crewai-cli==1.14.8a2",
"crewai-core==1.14.8a5",
"crewai-cli==1.14.8a5",
# Core Dependencies
"pydantic>=2.11.9,<2.13",
"openai>=2.30.0,<3",
@@ -18,9 +18,9 @@ dependencies = [
"pdfplumber~=0.11.4",
"regex~=2026.1.15",
# Telemetry and Monitoring
"opentelemetry-api~=1.34.0",
"opentelemetry-sdk~=1.34.0",
"opentelemetry-exporter-otlp-proto-http~=1.34.0",
"opentelemetry-api~=1.42.0",
"opentelemetry-sdk~=1.42.0",
"opentelemetry-exporter-otlp-proto-http~=1.42.0",
# Data Handling
"chromadb~=1.1.0",
"tokenizers>=0.21,<1",
@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.14.8a2",
"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.8a2"
__version__ = "1.14.8a5"
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
"Memory": ("crewai.memory.unified_memory", "Memory"),

View File

@@ -373,9 +373,6 @@ To enable tracing, do any one of these:
status: str = "running",
) -> None:
"""Show method status panel."""
if not self.verbose:
return
if status == "running":
style = "yellow"
panel_title = "🔄 Flow Method Running"

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

@@ -9,6 +9,7 @@ from __future__ import annotations
from datetime import datetime, timezone
import json
import logging
import os
from pathlib import Path
import tarfile
from typing import TypedDict
@@ -127,12 +128,36 @@ class SkillCacheManager:
def _safe_extractall(tf: tarfile.TarFile, dest: Path) -> None:
"""Path-traversal-safe extraction for Python < 3.12."""
"""Path-traversal-safe extraction for Python versions without tar filters.
Validates both the member's own path and, for symlink/hardlink members,
the link target. Without the link-target check a malicious archive can
plant a symlink that escapes ``dest`` followed by a regular member written
through that link, escaping ``dest`` even though every member name resolves
inside it. This mirrors the protection that
``tarfile.extractall(..., filter="data")`` provides when available.
"""
dest_resolved = dest.resolve()
for member in tf.getmembers():
member_path = (dest / member.name).resolve()
if not member_path.is_relative_to(dest_resolved):
raise ValueError(f"Blocked path traversal attempt: {member.name!r}")
if not (member.isfile() or member.isdir() or member.issym() or member.islnk()):
raise ValueError(f"Blocked unsupported tar member: {member.name!r}")
if member.issym() or member.islnk():
link_target = member.linkname
if os.path.isabs(link_target):
raise ValueError(
f"Blocked link target escaping destination: "
f"{member.name!r} -> {link_target!r}"
)
anchor = dest if member.islnk() else (dest / member.name).parent
resolved_target = (anchor / link_target).resolve()
if not resolved_target.is_relative_to(dest_resolved):
raise ValueError(
f"Blocked link target escaping destination: "
f"{member.name!r} -> {link_target!r}"
)
tf.extractall(dest) # noqa: S202

View File

@@ -8,8 +8,8 @@ from crewai.flow.dsl._types import FlowMethodDecorator, FlowTrigger
from crewai.flow.dsl._utils import (
P,
R,
_merge_flow_method_definition,
_method_action,
_set_flow_method_definition,
)
from crewai.flow.flow_definition import FlowMethodDefinition
from crewai.flow.flow_wrappers import ListenMethod
@@ -45,7 +45,7 @@ def listen(condition: FlowTrigger) -> FlowMethodDecorator:
def decorator(func: Callable[P, R]) -> ListenMethod[P, R]:
wrapper = ListenMethod(func)
_set_flow_method_definition(
_merge_flow_method_definition(
wrapper,
FlowMethodDefinition(
do=_method_action(func),

View File

@@ -19,8 +19,8 @@ from crewai.flow.dsl._types import FlowMethodDecorator, FlowTrigger
from crewai.flow.dsl._utils import (
P,
R,
_merge_flow_method_definition,
_method_action,
_set_flow_method_definition,
)
from crewai.flow.flow_definition import FlowMethodDefinition
from crewai.flow.flow_wrappers import RouterMethod
@@ -95,7 +95,7 @@ def _normalize_router_emit(value: Sequence[Any] | str) -> list[str]:
def router(
condition: FlowTrigger,
condition: FlowTrigger | None = None,
*,
emit: Sequence[str] | str | None = None,
) -> FlowMethodDecorator:
@@ -107,6 +107,7 @@ def router(
Args:
condition: Specifies when the router should execute. Can be:
- None: no listen trigger, used when stacking with @start() or @listen()
- str: Route label or method name that triggers this router
- FlowCondition: Result from or_() or and_(), including nested conditions
- Flow method reference: A method whose completion triggers this router
@@ -146,14 +147,17 @@ def router(
else:
router_events = _get_router_return_events(func) or []
_set_flow_method_definition(
method_definition_kwargs: dict[str, Any] = {
"do": _method_action(func),
"router": True,
"emit": router_events or None,
}
if condition is not None:
method_definition_kwargs["listen"] = _to_definition_condition(condition)
_merge_flow_method_definition(
wrapper,
FlowMethodDefinition(
do=_method_action(func),
listen=_to_definition_condition(condition),
router=True,
emit=router_events or None,
),
FlowMethodDefinition(**method_definition_kwargs),
)
return wrapper

View File

@@ -8,8 +8,8 @@ from crewai.flow.dsl._types import FlowMethodDecorator, FlowTrigger
from crewai.flow.dsl._utils import (
P,
R,
_merge_flow_method_definition,
_method_action,
_set_flow_method_definition,
)
from crewai.flow.flow_definition import FlowMethodDefinition
from crewai.flow.flow_wrappers import StartMethod
@@ -54,7 +54,7 @@ def start(
def decorator(func: Callable[P, R]) -> StartMethod[P, R]:
wrapper = StartMethod(func)
_set_flow_method_definition(
_merge_flow_method_definition(
wrapper,
FlowMethodDefinition(
do=_method_action(func),

View File

@@ -106,6 +106,25 @@ def _get_flow_method_definition(method: Any) -> FlowMethodDefinition | None:
return None
def _merge_flow_method_definition(
wrapper: FlowMethod[P, R],
definition: FlowMethodDefinition,
) -> None:
existing = _get_flow_method_definition(wrapper)
if existing is None:
_set_flow_method_definition(wrapper, definition)
return
updates = {
field_name: getattr(definition, field_name)
for field_name in definition.model_fields_set
}
_set_flow_method_definition(
wrapper,
existing.model_copy(deep=True, update=updates),
)
def _is_json_serializable(value: Any) -> bool:
try:
json.dumps(value)

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,7 +1,7 @@
"""Flow Structure: the serializable, language-agnostic Flow contract.
"""Flow Definition: the serializable, declarative Flow contract.
Defines :class:`FlowDefinition` and its sub-models — a static, textual
(JSON/YAML) representation of a Flow: its methods, trigger conditions,
Defines :class:`FlowDefinition` and its sub-models — a static declarative
representation of a Flow: its methods, trigger conditions,
state, and configuration. It is independent of the Python authoring
layer that may have produced it and of the engine that runs it (see
``runtime``).
@@ -11,6 +11,7 @@ from __future__ import annotations
import json
import logging
from pathlib import Path
import re
from typing import Annotated, Any, Literal, TypeAlias, cast
@@ -18,6 +19,7 @@ from pydantic import (
BaseModel,
ConfigDict,
Field,
PrivateAttr,
field_serializer,
model_validator,
)
@@ -233,7 +235,7 @@ class FlowPersistenceDefinition(BaseModel):
``persistence`` may hold a live backend when the definition is built from
a decorated class — the engine then persists through the exact instance
the user configured; the JSON/YAML projection degrades it to its
the user configured; the declarative projection degrades it to its
serialized config.
"""
@@ -273,7 +275,7 @@ class FlowHumanFeedbackDefinition(BaseModel):
"""Static human feedback configuration.
``llm`` and ``provider`` may hold live Python objects when the definition
is built from a decorated class; the JSON/YAML projection degrades them to
is built from a decorated class; the declarative projection degrades them to
a serialized config (``llm``) or a ``module:qualname`` ref (``provider``).
"""
@@ -406,10 +408,19 @@ class FlowCrewActionDefinition(BaseModel):
)
call: Literal["crew"] = Field(
description="Action discriminator. Use crew to run an inline Crew definition.",
description=(
"Action discriminator. Use crew to run an inline or referenced Crew "
"definition."
),
examples=["crew"],
)
with_: CrewDefinition = Field(
from_declaration: str | None = Field(
default=None,
description="Path to a JSON/JSONC Crew declaration file or folder.",
examples=["crews/research_crew"],
)
with_: CrewDefinition | None = Field(
default=None,
alias="with",
description="Inline Crew definition to load and execute for this action.",
examples=[
@@ -430,10 +441,26 @@ class FlowCrewActionDefinition(BaseModel):
"agent": "researcher",
}
],
"inputs": {"topic": "${state.topic}"},
}
],
)
inputs: dict[str, ExpressionData] | None = Field(
default=None,
description=(
"Input overrides passed to the Crew. String values are evaluated as CEL "
"only when the trimmed value starts with ${ and ends with }; all other "
"values are literal."
),
examples=[{"topic": "${state.topic}"}],
)
@model_validator(mode="after")
def _validate_crew_source(self) -> FlowCrewActionDefinition:
if bool(self.from_declaration) == (self.with_ is not None):
raise ValueError(
"crew action requires exactly one of from_declaration or with"
)
return self
class FlowAgentActionDefinition(BaseModel):
@@ -684,10 +711,12 @@ class FlowDefinition(BaseModel):
arbitrary_types_allowed=True,
)
_source_path: Path | None = PrivateAttr(default=None)
schema_: Literal["crewai.flow/v1"] = Field(
default="crewai.flow/v1",
alias="schema",
description="Flow Definition schema identifier and version.",
description="Declarative Flow schema identifier and version.",
examples=["crewai.flow/v1"],
)
name: str = Field(
@@ -748,7 +777,7 @@ class FlowDefinition(BaseModel):
return self
def to_dict(self, *, exclude_none: bool = True) -> dict[str, Any]:
"""Serialize the definition to a JSON/YAML-ready dictionary."""
"""Serialize the definition to a declaration-ready dictionary."""
return self.model_dump(by_alias=True, exclude_none=exclude_none, mode="json")
def to_json(self, *, indent: int | None = 2, exclude_none: bool = True) -> str:
@@ -764,29 +793,66 @@ class FlowDefinition(BaseModel):
allow_unicode=True,
)
@property
def source_path(self) -> Path | None:
"""Original definition file path, when loaded from a file."""
return self._source_path
@property
def source_dir(self) -> Path | None:
"""Directory used to resolve relative paths in the definition."""
if self._source_path is None:
return None
return self._source_path.parent
@classmethod
def from_dict(cls, data: dict[str, Any]) -> FlowDefinition:
def from_dict(
cls, data: dict[str, Any], *, source_path: Path | None = None
) -> FlowDefinition:
"""Load a definition from a dictionary."""
definition = cls.model_validate(data)
if source_path is not None:
definition._source_path = source_path.expanduser().resolve()
log_flow_definition_issues(definition)
return definition
@classmethod
def from_json(cls, data: str) -> FlowDefinition:
"""Load a definition from JSON."""
return cls.from_dict(json.loads(data))
def from_declaration(
cls,
*,
contents: FlowDefinition | str | dict[str, Any] | None = None,
path: Path | str | None = None,
) -> FlowDefinition:
"""Load a declarative flow from contents or a file path."""
if isinstance(contents, cls):
return contents
@classmethod
def from_yaml(cls, data: str) -> FlowDefinition:
"""Load a definition from YAML."""
loaded = yaml.safe_load(data) or {}
source_path: Path | None = None
if contents is None:
if path is None:
raise ValueError("Provide contents or path")
source_path = Path(path)
contents = source_path.expanduser().read_text(encoding="utf-8")
if isinstance(contents, dict):
return cls.from_dict(contents)
if not isinstance(contents, str):
raise TypeError("Flow declaration contents must be a string or dictionary")
if not contents.strip():
if source_path is not None:
raise ValueError(f"Flow declaration file is empty: {source_path}")
raise ValueError("Flow declaration contents are empty")
loaded = yaml.safe_load(contents)
if not isinstance(loaded, dict):
raise ValueError("Flow definition YAML must contain a mapping")
return cls.from_dict(loaded)
raise ValueError("Flow declaration must contain a mapping")
return cls.from_dict(loaded, source_path=source_path)
@classmethod
def json_schema(cls) -> dict[str, Any]:
"""Return the JSON Schema for the Flow Definition contract."""
"""Return the JSON Schema for the declarative Flow contract."""
return cls.model_json_schema(by_alias=True)
@@ -826,10 +892,16 @@ def _validate_action_cel(
return
if isinstance(action, FlowCrewActionDefinition):
Expression(cast(ExpressionData, action.with_.inputs)).validate_template(
allowed_roots=allowed_roots,
source=f"{path}.with.inputs",
)
if action.with_ is not None:
Expression(cast(ExpressionData, action.with_.inputs)).validate_template(
allowed_roots=allowed_roots,
source=f"{path}.with.inputs",
)
if action.inputs is not None:
Expression(cast(ExpressionData, action.inputs)).validate_template(
allowed_roots=allowed_roots,
source=f"{path}.inputs",
)
return
if isinstance(action, FlowAgentActionDefinition):
@@ -870,14 +942,6 @@ def _validate_action_cel(
def log_flow_definition_issues(definition: FlowDefinition) -> None:
for method_name, method in definition.methods.items():
path = f"methods.{method_name}"
if method.router and not method.is_start and method.listen is None:
_log_flow_definition_issue(
definition.name,
code="router_without_trigger",
severity="error",
path=path,
message="router: true requires either start or listen",
)
if method.emit and not method.router:
_log_flow_definition_issue(
definition.name,

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
@@ -25,6 +21,7 @@ from datetime import datetime
import enum
import inspect
import logging
from pathlib import Path
import threading
from typing import (
TYPE_CHECKING,
@@ -34,10 +31,8 @@ from typing import (
Generic,
Literal,
ParamSpec,
SupportsIndex,
TypeVar,
cast,
overload,
)
from uuid import uuid4
@@ -122,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,
@@ -136,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:
@@ -226,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]:
@@ -283,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
@@ -298,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
@@ -365,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,
@@ -769,6 +483,21 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
@classmethod
def from_definition(cls, definition: FlowDefinition, **kwargs: Any) -> Flow[Any]:
"""Build a runnable Flow directly from a definition; no subclass required."""
return cls.from_declaration(contents=definition, **kwargs)
@classmethod
def from_declaration(
cls,
*,
contents: FlowDefinition | str | dict[str, Any] | None = None,
path: Path | str | None = None,
**kwargs: Any,
) -> Flow[Any]:
"""Build a runnable declarative flow from contents or a file path."""
definition = FlowDefinition.from_declaration(
contents=contents,
path=path,
)
return cls.model_validate(
{**definition.config.model_dump(), **kwargs},
context={"flow_definition": definition},
@@ -992,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)
@@ -1914,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]:
@@ -2455,11 +2183,6 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
object.__setattr__(
self, "_deferred_flow_started_event_id", started_event.event_id
)
if not self.suppress_flow_events:
self._log_flow_event(
f"Flow started with ID: {self.flow_id}", color="bold magenta"
)
# After FlowStarted: env events must not pre-empt trace batch init
# with implicit "crew" execution_type.
get_env_context()
@@ -3007,6 +2730,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
"""
# First, handle routers repeatedly until no router triggers anymore
router_results = []
router_result_payloads: dict[str, Any] = {}
router_result_to_feedback: dict[
str, Any
] = {} # Map outcome -> HumanFeedbackResult
@@ -3044,6 +2768,11 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
router_result_str = str(router_result)
router_result_event = FlowMethodName(router_result_str)
router_results.append(router_result_event)
router_result_payloads[router_result_str] = (
self.last_human_feedback
if self.last_human_feedback is not None
else router_result
)
if self.last_human_feedback is not None:
router_result_to_feedback[router_result_str] = (
@@ -3064,7 +2793,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
current_trigger, router_only=False
)
if listeners_triggered:
listener_result = router_result_to_feedback.get(
listener_result = router_result_payloads.get(
str(current_trigger), result
)
racing_group = self._get_racing_group_for_listeners(
@@ -3583,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

@@ -8,6 +8,7 @@ from collections.abc import Awaitable, Callable
import contextvars
import inspect
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Protocol, cast
from crewai.flow.expressions import Expression, ExpressionData
@@ -23,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:
@@ -102,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(
@@ -128,16 +134,34 @@ class CrewAction:
self.definition = definition
async def run(self, *_args: Any, **kwargs: Any) -> Any:
from crewai.project.crew_loader import load_crew_from_definition
from crewai.project.crew_loader import load_crew, load_crew_from_definition
local_context = _pop_local_context(kwargs)
crew_definition = self.definition.with_
if self.definition.from_declaration is not None:
crew, default_inputs = load_crew(
_resolve_crew_declaration(
self.definition.from_declaration,
base_dir=self.flow._definition.source_dir,
)
)
input_template = {**default_inputs, **(self.definition.inputs or {})}
else:
crew_definition = self.definition.with_
if crew_definition is None:
raise ValueError(
"crew action requires exactly one of from_declaration or with"
)
input_template = {
**crew_definition.inputs,
**(self.definition.inputs or {}),
}
crew, _ = load_crew_from_definition(crew_definition, source="crew action")
inputs = Expression.from_flow(
cast(ExpressionData, crew_definition.inputs),
cast(ExpressionData, input_template),
self.flow,
local_context=local_context,
).render_template()
crew, _ = load_crew_from_definition(crew_definition, source="crew action")
return await crew.kickoff_async(inputs=inputs)
@@ -359,3 +383,29 @@ def _pop_local_context(kwargs: dict[str, Any]) -> LocalContext | None:
if not isinstance(local_context, dict):
raise TypeError("flow definition local context must be a mapping")
return cast(LocalContext, local_context)
def _resolve_crew_declaration(
from_declaration: str, *, base_dir: Path | None = None
) -> Path:
path = Path(from_declaration).expanduser()
if base_dir is not None:
resolved_base_dir = base_dir.expanduser().resolve()
if not path.is_absolute():
path = resolved_base_dir / path
resolved_path = path.resolve()
if not resolved_path.is_relative_to(resolved_base_dir):
raise ValueError(
"crew declaration path must be within the flow definition directory"
)
path = resolved_path
if not path.is_dir():
return path
for name in ("crew.jsonc", "crew.json"):
candidate = path / name
if candidate.is_file():
return candidate
return path / "crew.jsonc"

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

@@ -11,11 +11,10 @@ import pytest
from crewai_cli.cli import run
from crewai_cli.run_crew import (
CrewType,
_execute_uv_script,
_load_json_crew_for_tui,
_missing_input_names,
_prompt_for_missing_inputs,
execute_command,
)
@@ -30,6 +29,8 @@ def test_run_passes_filename_to_run_crew(run_crew_mock: mock.Mock, runner: CliRu
run_crew_mock.assert_called_once_with(
trained_agents_file="my_custom_trained.pkl",
definition=None,
inputs=None,
)
assert result.exit_code == 0
@@ -38,7 +39,11 @@ def test_run_passes_filename_to_run_crew(run_crew_mock: mock.Mock, runner: CliRu
def test_run_without_filename_passes_none(run_crew_mock: mock.Mock, runner: CliRunner) -> None:
result = runner.invoke(run)
run_crew_mock.assert_called_once_with(trained_agents_file=None)
run_crew_mock.assert_called_once_with(
trained_agents_file=None,
definition=None,
inputs=None,
)
assert result.exit_code == 0
@@ -50,7 +55,11 @@ def test_run_without_filename_passes_none(run_crew_mock: mock.Mock, runner: CliR
def test_execute_command_sets_env_var_when_filename_provided(
_build_env: mock.Mock, subprocess_run: mock.Mock
) -> None:
execute_command(CrewType.STANDARD, trained_agents_file="my_custom_trained.pkl")
_execute_uv_script(
"run_crew",
entity_type="crew",
trained_agents_file="my_custom_trained.pkl",
)
_, kwargs = subprocess_run.call_args
assert kwargs["env"]["CREWAI_TRAINED_AGENTS_FILE"] == "my_custom_trained.pkl"
@@ -65,7 +74,7 @@ def test_execute_command_sets_env_var_when_filename_provided(
def test_execute_command_omits_env_var_when_filename_absent(
_build_env: mock.Mock, subprocess_run: mock.Mock
) -> None:
execute_command(CrewType.STANDARD)
_execute_uv_script("run_crew", entity_type="crew")
_, kwargs = subprocess_run.call_args
assert "CREWAI_TRAINED_AGENTS_FILE" not in kwargs["env"]

View File

@@ -8,7 +8,9 @@ import json
import tarfile
from pathlib import Path
from crewai.experimental.skills.cache import SkillCacheManager
import pytest
from crewai.experimental.skills.cache import SkillCacheManager, _safe_extractall
def _make_tar_gz(files: dict[str, str]) -> bytes:
@@ -35,6 +37,15 @@ def _make_tar_gz(files: dict[str, str]) -> bytes:
return out.getvalue()
def _tar_from_members(build) -> tarfile.TarFile:
"""Build an in-memory tar archive via `build(tf)` and return it for reading."""
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tf:
build(tf)
buf.seek(0)
return tarfile.open(fileobj=buf, mode="r")
class TestSkillCacheManager:
def test_get_cached_path_missing(self, tmp_path: Path) -> None:
cache = SkillCacheManager(cache_root=tmp_path)
@@ -113,3 +124,85 @@ class TestSkillCacheManager:
dest = cache.store("acme", "my-skill", None, archive)
meta = json.loads((dest / ".crewai_meta.json").read_text())
assert meta["version"] is None
def test_safe_extractall_blocks_symlink_escaping_cache_destination(
tmp_path: Path,
) -> None:
"""A symlink whose target escapes dest is rejected before extraction."""
outside = tmp_path / "outside"
outside.mkdir()
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
link = tarfile.TarInfo("link")
link.type = tarfile.SYMTYPE
link.linkname = str(outside)
tf.addfile(link)
payload = b"pwned"
info = tarfile.TarInfo("link/evil.txt")
info.size = len(payload)
tf.addfile(info, io.BytesIO(payload))
with _tar_from_members(build) as tf:
with pytest.raises(ValueError, match="escaping destination"):
_safe_extractall(tf, dest)
assert not (outside / "evil.txt").exists()
def test_safe_extractall_blocks_hardlink_escaping_cache_destination(
tmp_path: Path,
) -> None:
"""A hardlink whose target escapes dest is rejected."""
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
link = tarfile.TarInfo("escape")
link.type = tarfile.LNKTYPE
link.linkname = "../outside.txt"
tf.addfile(link)
with _tar_from_members(build) as tf:
with pytest.raises(ValueError, match="escaping destination"):
_safe_extractall(tf, dest)
def test_safe_extractall_blocks_special_cache_tar_member(tmp_path: Path) -> None:
"""Special tar members such as FIFOs are rejected."""
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
fifo = tarfile.TarInfo("pipe")
fifo.type = tarfile.FIFOTYPE
tf.addfile(fifo)
with _tar_from_members(build) as tf:
with pytest.raises(ValueError, match="unsupported tar member"):
_safe_extractall(tf, dest)
def test_safe_extractall_allows_benign_cache_symlink(tmp_path: Path) -> None:
"""A symlink that stays within dest is permitted."""
dest = tmp_path / "dest"
dest.mkdir()
def build(tf: tarfile.TarFile) -> None:
payload = b"hi"
info = tarfile.TarInfo("real.txt")
info.size = len(payload)
tf.addfile(info, io.BytesIO(payload))
link = tarfile.TarInfo("alias.txt")
link.type = tarfile.SYMTYPE
link.linkname = "real.txt"
tf.addfile(link)
with _tar_from_members(build) as tf:
_safe_extractall(tf, dest)
assert (dest / "real.txt").read_bytes() == b"hi"
assert (dest / "alias.txt").is_symlink()
assert (dest / "alias.txt").readlink() == Path("real.txt")

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

@@ -386,6 +386,54 @@ def test_router_runtime_uses_flow_definition_without_legacy_router_metadata():
assert execution_order == ["begin", "decide", "handle_left"]
def test_start_router_runtime_routes_public_dsl_return_value():
execution_order = []
class StartRouterFlow(Flow):
@start()
@router(emit=["continue"])
def decide(self):
execution_order.append("decide")
return "continue"
@listen("continue")
def handle_continue(self, result):
execution_order.append(f"handle_continue:{result}")
return "done"
assert StartRouterFlow().kickoff() == "done"
assert execution_order == ["decide", "handle_continue:continue"]
def test_start_router_runtime_chains_to_stacked_listener_router():
execution_order = []
class ChainedStartRouterFlow(Flow):
@start()
@router(emit=["approved", "not_approved"])
def first_router(self):
execution_order.append("first_router")
return "approved"
@listen("approved")
@router(emit=["second_approval", "not_approved"])
def second_router(self):
execution_order.append("second_router")
return "second_approval"
@listen("second_approval")
def handle_second_approval(self, result):
execution_order.append(f"handle_second_approval:{result}")
return "done"
assert ChainedStartRouterFlow().kickoff() == "done"
assert execution_order == [
"first_router",
"second_router",
"handle_second_approval:second_approval",
]
def test_router_falsy_result_emits_runtime_event():
execution_order = []
@@ -1462,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()
@@ -1506,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

@@ -565,7 +565,55 @@ def test_flow_definition_classifies_start_router_from_human_feedback_emit():
assert entry_point.emit is None
def test_flow_definition_round_trips_json_and_yaml():
def test_flow_definition_classifies_public_dsl_start_router():
class StartRouterFlow(Flow):
@start()
@router(emit=["continue", "stop"])
def entry_point(self):
return "continue"
@router(emit=["resume"])
@start()
def alternate_entry_point(self):
return "resume"
entry_point = StartRouterFlow.flow_definition().methods["entry_point"]
alternate_entry_point = StartRouterFlow.flow_definition().methods[
"alternate_entry_point"
]
assert entry_point.is_start is True
assert entry_point.router is True
assert entry_point.listen is None
assert entry_point.emit == ["continue", "stop"]
assert alternate_entry_point.is_start is True
assert alternate_entry_point.router is True
assert alternate_entry_point.listen is None
assert alternate_entry_point.emit == ["resume"]
def test_flow_definition_merges_stacked_listen_router():
class ChainedRouterFlow(Flow):
@start()
@router(emit=["approved", "not_approved"])
def first_router(self):
return "approved"
@listen("approved")
@router(emit=["second_approval", "not_approved"])
def second_router(self):
return "second_approval"
methods = ChainedRouterFlow.flow_definition().methods
assert methods["first_router"].is_start is True
assert methods["first_router"].listen is None
assert methods["second_router"].router is True
assert methods["second_router"].listen == "approved"
assert methods["second_router"].emit == ["second_approval", "not_approved"]
def test_flow_definition_round_trips_declaration_serialization():
class RoundTripFlow(Flow):
@start()
def begin(self):
@@ -581,16 +629,122 @@ def test_flow_definition_round_trips_json_and_yaml():
definition = RoundTripFlow.flow_definition()
json_round_trip = flow_definition.FlowDefinition.from_json(definition.to_json())
yaml_round_trip = flow_definition.FlowDefinition.from_yaml(definition.to_yaml())
round_trips = [
flow_definition.FlowDefinition.from_declaration(contents=definition.to_json()),
flow_definition.FlowDefinition.from_declaration(contents=definition.to_yaml()),
]
assert json_round_trip.to_dict() == definition.to_dict()
assert yaml_round_trip.to_dict() == definition.to_dict()
assert yaml_round_trip.methods["decide"].router is True
assert yaml_round_trip.methods["decide"].listen == "begin"
for round_trip in round_trips:
assert round_trip.to_dict() == definition.to_dict()
assert round_trip.methods["decide"].router is True
assert round_trip.methods["decide"].listen == "begin"
def test_each_action_round_trips_json_and_yaml():
def test_flow_definition_from_declaration_accepts_contents():
data = {
"schema": "crewai.flow/v1",
"name": "DeclarationFlow",
"methods": {
"begin": {
"start": True,
"do": {
"call": "expression",
"expr": "'started'",
},
},
},
}
definition = flow_definition.FlowDefinition.from_dict(data)
contents = [
definition,
data,
definition.to_json(),
definition.to_yaml(),
]
expected = definition.to_dict()
for content in contents:
loaded = flow_definition.FlowDefinition.from_declaration(contents=content)
assert loaded.to_dict() == expected
def test_flow_definition_from_declaration_rejects_empty_file(tmp_path: Path):
declaration_path = tmp_path / "flow.crewai"
declaration_path.write_text(" \n", encoding="utf-8")
with pytest.raises(ValueError, match="Flow declaration file is empty"):
flow_definition.FlowDefinition.from_declaration(path=declaration_path)
@pytest.mark.parametrize("contents", ["[]", "false", "0", "null", "~"])
def test_flow_definition_from_declaration_rejects_falsey_non_mapping_contents(
contents: str,
):
with pytest.raises(ValueError, match="Flow declaration must contain a mapping"):
flow_definition.FlowDefinition.from_declaration(contents=contents)
def test_flow_definition_from_declaration_accepts_paths(tmp_path: Path):
definition = flow_definition.FlowDefinition.from_dict(
{
"schema": "crewai.flow/v1",
"name": "DeclarationFlow",
"methods": {
"begin": {
"start": True,
"do": {
"call": "expression",
"expr": "'started'",
},
},
},
}
)
declaration_path = tmp_path / "flow.crewai"
declaration_path.write_text(definition.to_yaml(), encoding="utf-8")
path_inputs = [
declaration_path,
str(declaration_path),
]
for path_input in path_inputs:
loaded = flow_definition.FlowDefinition.from_declaration(path=path_input)
assert loaded.to_dict() == definition.to_dict()
assert loaded.source_path == declaration_path.resolve()
def test_flow_definition_from_declaration_requires_input():
with pytest.raises(ValueError, match="Provide contents or path"):
flow_definition.FlowDefinition.from_declaration()
def test_flow_definition_from_declaration_prefers_contents_over_path(
tmp_path: Path,
):
data = {
"schema": "crewai.flow/v1",
"name": "ContentsFlow",
"methods": {
"begin": {
"start": True,
"do": {"call": "expression", "expr": "'started'"},
},
},
}
declaration_path = tmp_path / "missing.crewai"
loaded = flow_definition.FlowDefinition.from_declaration(
contents=data,
path=declaration_path,
)
assert loaded.name == "ContentsFlow"
assert loaded.source_path is None
def test_each_action_round_trips_declaration_serialization():
definition = flow_definition.FlowDefinition.from_dict(
{
"schema": "crewai.flow/v1",
@@ -629,15 +783,17 @@ def test_each_action_round_trips_json_and_yaml():
}
)
json_round_trip = flow_definition.FlowDefinition.from_json(definition.to_json())
yaml_round_trip = flow_definition.FlowDefinition.from_yaml(definition.to_yaml())
round_trips = [
flow_definition.FlowDefinition.from_declaration(contents=definition.to_json()),
flow_definition.FlowDefinition.from_declaration(contents=definition.to_yaml()),
]
assert json_round_trip.to_dict() == definition.to_dict()
assert yaml_round_trip.to_dict() == definition.to_dict()
assert yaml_round_trip.methods["process_rows"].description == (
"Process every loaded row."
)
assert yaml_round_trip.methods["process_rows"].do.call == "each"
for round_trip in round_trips:
assert round_trip.to_dict() == definition.to_dict()
assert round_trip.methods["process_rows"].description == (
"Process every loaded row."
)
assert round_trip.methods["process_rows"].do.call == "each"
def test_flow_definition_rejects_invalid_method_names():
@@ -883,7 +1039,7 @@ def test_flow_definition_ignores_legacy_diagnostics_loaded_from_contract():
assert "diagnostics" not in definition.to_dict()
def test_router_start_false_without_listen_logs_missing_trigger(caplog):
def test_router_start_false_without_listen_is_allowed(caplog):
caplog.set_level(logging.ERROR, logger="crewai.flow.flow_definition")
flow_definition.FlowDefinition.from_dict(
@@ -901,12 +1057,7 @@ def test_router_start_false_without_listen_logs_missing_trigger(caplog):
}
)
assert any(
record.levelno == logging.ERROR
and "router_without_trigger" in record.message
and "methods.decision" in record.message
for record in caplog.records
)
assert not caplog.records
def test_router_human_feedback_preserves_existing_router_metadata():
@@ -1048,7 +1199,7 @@ def test_flow_definition_cache_is_not_reused_by_subclasses():
assert set(child_definition.methods) == {"child_step"}
def test_flow_definition_logs_validation_issues_when_loaded_from_contract(caplog):
def test_flow_definition_allows_router_without_trigger(caplog):
caplog.set_level(logging.WARNING, logger="crewai.flow.flow_definition")
flow_definition.FlowDefinition.from_dict(
@@ -1065,9 +1216,11 @@ def test_flow_definition_logs_validation_issues_when_loaded_from_contract(caplog
}
)
assert any(
record.levelno == logging.ERROR
and "LoadedFlow" in record.message
and "router_without_trigger" in record.message
for record in caplog.records
)
class StandaloneRouterFlow(Flow):
@router(emit=["continue"])
def decision(self):
return "continue"
StandaloneRouterFlow.flow_definition()
assert not caplog.records

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
@@ -454,7 +476,7 @@ def assert_parity(flow_cls, yaml_str, inputs=None, ordered=True):
class_flow = flow_cls()
class_result, class_events = _run_with_events(class_flow, inputs)
definition = FlowDefinition.from_yaml(yaml_str)
definition = FlowDefinition.from_declaration(contents=yaml_str)
definition_flow = Flow.from_definition(definition)
definition_result, definition_events = _run_with_events(definition_flow, inputs)
@@ -477,6 +499,21 @@ def test_simple_chain_parity():
assert flow.method_outputs == ["hello", "HELLO", "confirmed:True"]
def test_flow_from_declaration_builds_runnable_flow():
flow = Flow.from_declaration(contents=CHAIN_YAML)
assert flow.kickoff() == "confirmed:True"
assert flow.method_outputs == ["hello", "HELLO", "confirmed:True"]
def test_flow_from_declaration_accepts_flow_definition():
definition = FlowDefinition.from_declaration(contents=CHAIN_YAML)
flow = Flow.from_declaration(contents=definition)
assert flow.kickoff() == "confirmed:True"
assert flow.method_outputs == ["hello", "HELLO", "confirmed:True"]
def test_and_or_merge_parity():
flow, _ = assert_parity(MergeFlow, MERGE_YAML, ordered=False)
assert flow.state["joined"] is True
@@ -499,7 +536,7 @@ def test_cyclic_flow_parity():
def test_definition_flow_events_use_definition_name():
definition = FlowDefinition.from_yaml(CHAIN_YAML)
definition = FlowDefinition.from_declaration(contents=CHAIN_YAML)
flow = Flow.from_definition(definition)
_, events = _run_with_events(flow)
assert events
@@ -588,7 +625,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff() == "found:ai agents"
@@ -639,7 +676,7 @@ methods:
listen: begin
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"topic": "ai"}) == "found:ai agents"
@@ -758,7 +795,7 @@ methods:
listen: begin
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff() == "search:hello agents"
@@ -783,7 +820,7 @@ methods:
listen: build_query
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff() == "found:ai agents news"
@@ -803,7 +840,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert (
flow.kickoff(inputs={"limit": 2, "domains": ["crewai.com", "example.com"]})
@@ -836,7 +873,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"question": "What is CrewAI?"}) == {
"agent": "Analyst",
@@ -874,7 +911,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"questions": ["one", "two"]}) == [
"Analyst:one",
@@ -905,7 +942,7 @@ def test_agent_action_round_trips_with_inline_definition():
}
)
round_trip = FlowDefinition.from_yaml(definition.to_yaml())
round_trip = FlowDefinition.from_declaration(contents=definition.to_yaml())
action = round_trip.to_dict()["methods"]["answer"]["do"]
assert action["call"] == "agent"
@@ -968,7 +1005,7 @@ methods:
"""
with pytest.raises(ValidationError, match="invalid CEL expression"):
FlowDefinition.from_yaml(yaml_str)
FlowDefinition.from_declaration(contents=yaml_str)
def test_crew_action_runs_inline_yaml_definition(monkeypatch: pytest.MonkeyPatch):
@@ -1005,12 +1042,12 @@ methods:
description: Research {topic}
expected_output: Findings about {topic}
agent: researcher
inputs:
topic: "${state.topic}"
inputs:
topic: "${state.topic}"
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"topic": "AI"}) == {
"crew": "inline_research",
@@ -1020,6 +1057,180 @@ methods:
}
def test_crew_action_runs_crew_from_declaration(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
):
from crewai import Crew
project_root = tmp_path / "project"
crew_root = project_root / "crews" / "research_crew"
agents_root = crew_root / "agents"
agents_root.mkdir(parents=True)
(agents_root / "researcher.jsonc").write_text(
"""
{
"role": "Researcher",
"goal": "Research {topic}",
"backstory": "Knows things."
}
""",
encoding="utf-8",
)
(crew_root / "crew.jsonc").write_text(
"""
{
"name": "referenced_research",
"agents": ["researcher"],
"tasks": [
{
"name": "research_task",
"description": "Research {topic}",
"expected_output": "Findings about {topic}",
"agent": "researcher"
}
],
"inputs": {
"topic": "Default topic",
"audience": "developers"
}
}
""",
encoding="utf-8",
)
async def fake_kickoff_async(
self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any
) -> dict[str, Any]:
return {
"crew": self.name,
"tasks": [task.description for task in self.tasks],
"inputs": inputs,
}
monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async)
monkeypatch.chdir(project_root)
yaml_str = """
schema: crewai.flow/v1
name: CrewFlow
methods:
research:
do:
call: crew
from_declaration: crews/research_crew
inputs:
topic: "${state.topic}"
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"topic": "AI"}) == {
"crew": "referenced_research",
"tasks": ["Research {topic}"],
"inputs": {"topic": "AI", "audience": "developers"},
}
def test_crew_action_from_declaration_resolves_relative_to_flow_file(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
):
from crewai import Crew
project_root = tmp_path / "project"
crew_root = project_root / "crews" / "research_crew"
agents_root = crew_root / "agents"
agents_root.mkdir(parents=True)
(agents_root / "researcher.jsonc").write_text(
"""
{
"role": "Researcher",
"goal": "Research {topic}",
"backstory": "Knows things."
}
""",
encoding="utf-8",
)
(crew_root / "crew.jsonc").write_text(
"""
{
"name": "relative_research",
"agents": ["researcher"],
"tasks": [
{
"description": "Research {topic}",
"expected_output": "Findings about {topic}",
"agent": "researcher"
}
],
"inputs": {
"topic": "Default topic"
}
}
""",
encoding="utf-8",
)
async def fake_kickoff_async(
self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any
) -> dict[str, Any]:
return {"crew": self.name, "inputs": inputs}
monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async)
flow_path = project_root / "flow.yaml"
yaml_str = """
schema: crewai.flow/v1
name: CrewFlow
methods:
research:
do:
call: crew
from_declaration: crews/research_crew
inputs:
topic: "${state.topic}"
start: true
"""
flow_path.write_text(yaml_str, encoding="utf-8")
other_cwd = tmp_path / "other"
other_cwd.mkdir()
monkeypatch.chdir(other_cwd)
flow = Flow.from_definition(FlowDefinition.from_declaration(path=flow_path))
assert flow.kickoff(inputs={"topic": "AI"}) == {
"crew": "relative_research",
"inputs": {"topic": "AI"},
}
def test_crew_action_from_declaration_rejects_paths_outside_flow_file(
tmp_path: Path,
):
flow_path = tmp_path / "project" / "flow.yaml"
flow_path.parent.mkdir()
yaml_str = """
schema: crewai.flow/v1
name: CrewFlow
methods:
research:
do:
call: crew
from_declaration: ../outside/crew.jsonc
start: true
"""
flow_path.write_text(yaml_str, encoding="utf-8")
flow = Flow.from_definition(FlowDefinition.from_declaration(path=flow_path))
with pytest.raises(
ValueError,
match="crew declaration path must be within the flow definition directory",
):
flow.kickoff()
def test_crew_action_round_trips_with_inline_definition():
definition = FlowDefinition.from_dict(
{
@@ -1047,8 +1258,8 @@ def test_crew_action_round_trips_with_inline_definition():
"agent": "researcher",
}
],
"inputs": {"topic": "${state.topic}"},
},
"inputs": {"topic": "${state.topic}"},
},
}
},
@@ -1062,6 +1273,9 @@ def test_crew_action_round_trips_with_inline_definition():
]["role"]
== "Researcher"
)
assert definition.to_dict()["methods"]["research"]["do"]["inputs"] == {
"topic": "${state.topic}"
}
def test_crew_action_normalizes_named_agent_list_definition():
@@ -1162,7 +1376,7 @@ def test_crew_action_rejects_incomplete_inline_agent_definition():
)
def test_crew_action_rejects_ref():
def test_crew_action_rejects_python_ref_field():
with pytest.raises(ValidationError, match="ref"):
FlowDefinition.from_dict(
{
@@ -1174,7 +1388,6 @@ def test_crew_action_rejects_ref():
"do": {
"call": "crew",
"ref": "project.crew:build_crew",
"with": {"inputs": {"topic": "AI"}},
},
}
},
@@ -1232,7 +1445,7 @@ methods:
"""
with pytest.raises(ValidationError, match="invalid CEL expression"):
FlowDefinition.from_yaml(yaml_str)
FlowDefinition.from_declaration(contents=yaml_str)
def test_code_action_renders_keyword_inputs():
@@ -1250,7 +1463,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"name": "hello"}) == "hello!"
@@ -1269,7 +1482,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"value": "ok"}) == "callable:ok"
@@ -1293,7 +1506,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == [
"normalized:a",
@@ -1320,7 +1533,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
caller_thread_id = threading.get_ident()
assert flow.kickoff(inputs={"rows": ["a"]}) == ["process_rows:a"]
@@ -1347,7 +1560,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == ["async:a", "async:b"]
@@ -1369,7 +1582,7 @@ methods:
FlowScriptExecutionDisabledError,
match="CREWAI_ALLOW_FLOW_SCRIPT_EXECUTION=1",
) as exc_info:
Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert "methods with unresolvable actions" not in str(exc_info.value)
@@ -1393,7 +1606,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"raw_score": 3.2}) == "rounded:4"
assert flow.state["rounded"] == 4
@@ -1422,7 +1635,7 @@ methods:
listen: seed
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff() == "alpha:alpha"
assert flow.state["input_matches_output"] is True
@@ -1460,7 +1673,7 @@ methods:
listen: seed
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"rows": [" a ", " b "]}) == ["global:a", "global:b"]
@@ -1492,7 +1705,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == [
{"row": "a", "normalized": "saved:a"},
@@ -1521,7 +1734,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == ["a", "b"]
assert flow._method_outputs == [
@@ -1559,7 +1772,7 @@ methods:
listen: seed
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == [
"local:a",
@@ -1598,7 +1811,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(
inputs={
@@ -1632,7 +1845,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(inputs={"rows": [{"kind": "keep", "value": "a"}]}) == ["a"]
@@ -1659,7 +1872,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert flow.kickoff(
inputs={
@@ -1689,7 +1902,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
with pytest.raises(ValueError, match="if expression must evaluate to a boolean"):
flow.kickoff(inputs={"rows": [{"value": "truthy"}]})
@@ -1719,7 +1932,7 @@ methods:
listen: process_rows
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
events = []
with crewai_event_bus.scoped_handlers():
@@ -1890,7 +2103,7 @@ methods:
start: true
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
with pytest.raises(RuntimeError, match="bad row"):
flow.kickoff(inputs={"rows": ["ok", "bad"]})
@@ -2011,7 +2224,7 @@ methods:
listen: right
"""
definition = FlowDefinition.from_yaml(yaml_str)
definition = FlowDefinition.from_declaration(contents=yaml_str)
assert Flow.from_definition(definition).kickoff(
inputs={"direction": "left"}
@@ -2034,7 +2247,7 @@ methods:
"""
with pytest.raises(ValidationError, match="invalid CEL expression"):
FlowDefinition.from_yaml(yaml_str)
FlowDefinition.from_declaration(contents=yaml_str)
def test_expression_action_rejects_unknown_cel_root():
@@ -2050,7 +2263,7 @@ methods:
"""
with pytest.raises(ValidationError, match="unknown CEL root"):
FlowDefinition.from_yaml(yaml_str)
FlowDefinition.from_declaration(contents=yaml_str)
def test_tool_action_requires_module_qualname_ref():
@@ -2084,14 +2297,16 @@ def test_pydantic_state_from_ref_parity():
def test_pydantic_state_default_overlay():
flow = Flow.from_definition(FlowDefinition.from_yaml(PYDANTIC_STATE_OVERLAY_YAML))
flow = Flow.from_definition(
FlowDefinition.from_declaration(contents=PYDANTIC_STATE_OVERLAY_YAML)
)
result = flow.kickoff()
assert result == "count=6"
assert flow.state.count == 6
def test_json_schema_state():
flow = Flow.from_definition(FlowDefinition.from_yaml(JSON_SCHEMA_STATE_YAML))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=JSON_SCHEMA_STATE_YAML))
result = flow.kickoff()
assert result == "count=1"
assert flow.state.count == 1
@@ -2100,14 +2315,26 @@ def test_json_schema_state():
def test_json_schema_state_validates_inputs():
flow = Flow.from_definition(FlowDefinition.from_yaml(JSON_SCHEMA_STATE_YAML))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=JSON_SCHEMA_STATE_YAML))
with pytest.raises(ValueError, match="Invalid 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_yaml(PYDANTIC_REF_WITH_SCHEMA_FALLBACK_YAML)
FlowDefinition.from_declaration(contents=PYDANTIC_REF_WITH_SCHEMA_FALLBACK_YAML)
)
result = flow.kickoff()
assert result == "count=1"
@@ -2116,7 +2343,9 @@ def test_pydantic_state_falls_back_to_json_schema_when_ref_unimportable():
def test_pydantic_state_without_ref_or_schema_falls_back_to_dict(caplog):
with caplog.at_level("ERROR"):
flow = Flow.from_definition(FlowDefinition.from_yaml(UNRESOLVABLE_STATE_YAML))
flow = Flow.from_definition(
FlowDefinition.from_declaration(contents=UNRESOLVABLE_STATE_YAML)
)
assert "falling back to dict state" in caplog.text
result = flow.kickoff()
@@ -2126,7 +2355,7 @@ def test_pydantic_state_without_ref_or_schema_falls_back_to_dict(caplog):
def test_dict_state_is_a_copy_of_default_plus_id():
definition = FlowDefinition.from_yaml(DICT_STATE_YAML)
definition = FlowDefinition.from_declaration(contents=DICT_STATE_YAML)
flow = Flow.from_definition(definition)
assert flow.state["count"] == 5
@@ -2143,7 +2372,7 @@ def test_dict_state_is_a_copy_of_default_plus_id():
def test_unknown_state_type_falls_back_to_dict(caplog):
with caplog.at_level("WARNING"):
flow = Flow.from_definition(FlowDefinition.from_yaml(UNKNOWN_STATE_YAML))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=UNKNOWN_STATE_YAML))
assert "falling back to dict state" in caplog.text
result = flow.kickoff()
@@ -2216,7 +2445,7 @@ def _run_capturing_flow_lifecycle(yaml_str, event_types):
def capture(source, event):
events.append(event)
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
result = flow.kickoff()
return flow, result, events
@@ -2230,7 +2459,7 @@ _LIFECYCLE_EVENTS = [
]
def test_config_suppress_flow_events_from_yaml():
def test_config_suppress_flow_events_from_declaration():
twin_events = []
with crewai_event_bus.scoped_handlers():
for event_type in _LIFECYCLE_EVENTS:
@@ -2253,14 +2482,14 @@ def test_config_suppress_flow_events_from_yaml():
)
def test_config_max_method_calls_from_yaml():
flow = Flow.from_definition(FlowDefinition.from_yaml(CAPPED_LOOP_YAML))
def test_config_max_method_calls_from_declaration():
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=CAPPED_LOOP_YAML))
with pytest.raises(RecursionError, match="has been called 2 times"):
flow.kickoff()
def test_config_stream_from_yaml():
flow = Flow.from_definition(FlowDefinition.from_yaml(STREAMING_CHAIN_YAML))
def test_config_stream_from_declaration():
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=STREAMING_CHAIN_YAML))
streaming = flow.kickoff()
assert isinstance(streaming, FlowStreamingOutput)
for _ in streaming:
@@ -2269,7 +2498,7 @@ def test_config_stream_from_yaml():
assert flow.stream is True
def test_config_defer_trace_finalization_from_yaml():
def test_config_defer_trace_finalization_from_declaration():
_, _, baseline_events = _run_capturing_flow_lifecycle(
CHAIN_YAML, [FlowFinishedEvent]
)
@@ -2283,7 +2512,7 @@ def test_config_defer_trace_finalization_from_yaml():
assert deferred_events == []
def test_config_checkpoint_from_yaml(tmp_path):
def test_config_checkpoint_from_declaration(tmp_path):
yaml_str = (
CHAIN_YAML
+ f"""
@@ -2292,19 +2521,23 @@ config:
location: {tmp_path}
"""
)
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
assert isinstance(flow.checkpoint, CheckpointConfig)
assert flow.checkpoint.location == str(tmp_path)
def test_config_input_provider_from_yaml():
flow = Flow.from_definition(FlowDefinition.from_yaml(INPUT_PROVIDER_CHAIN_YAML))
def test_config_input_provider_from_declaration():
flow = Flow.from_definition(
FlowDefinition.from_declaration(contents=INPUT_PROVIDER_CHAIN_YAML)
)
assert isinstance(flow.input_provider, StubInputProvider)
def test_round_trip_config_equivalence():
class_flow = ConfiguredFlow()
definition = FlowDefinition.from_yaml(ConfiguredFlow.flow_definition().to_yaml())
definition = FlowDefinition.from_declaration(
contents=ConfiguredFlow.flow_definition().to_yaml()
)
definition_flow = Flow.from_definition(definition)
assert definition.config.suppress_flow_events is True
@@ -2474,9 +2707,9 @@ class MethodPersistedFlow(Flow):
return "two"
def test_flow_level_persist_from_yaml_saves_once_per_method():
def test_flow_level_persist_from_declaration_saves_once_per_method():
yaml_str = _flow_level_persist_yaml("yaml-flow-level")
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
result = flow.kickoff()
assert result == "two"
@@ -2486,9 +2719,9 @@ def test_flow_level_persist_from_yaml_saves_once_per_method():
assert final_save["id"] == flow.state["id"]
def test_method_level_persist_from_yaml_saves_only_that_method():
def test_method_level_persist_from_declaration_saves_only_that_method():
yaml_str = _method_level_persist_yaml("yaml-method-level")
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
flow.kickoff()
assert _saved_methods("yaml-method-level") == ["first"]
@@ -2517,20 +2750,20 @@ methods:
persist:
enabled: false
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
flow.kickoff()
assert _saved_methods("yaml-opt-out") == ["first"]
def test_persist_restore_by_id_from_yaml():
def test_persist_restore_by_id_from_declaration():
yaml_str = _flow_level_persist_yaml("yaml-restore")
flow1 = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow1 = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
flow1.kickoff()
assert flow1.state["count"] == 2
flow2 = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow2 = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
flow2.kickoff(inputs={"id": flow1.state["id"]})
assert flow2.state["count"] == 4
@@ -2550,7 +2783,9 @@ def test_method_level_persist_decorator_saves_only_that_method():
def test_round_trip_persist_equivalence():
definition = FlowDefinition.from_yaml(ClassPersistedFlow.flow_definition().to_yaml())
definition = FlowDefinition.from_declaration(
contents=ClassPersistedFlow.flow_definition().to_yaml()
)
before = len(DefinitionStoreBackend.saves["class-decorator"])
flow = Flow.from_definition(definition)
@@ -2559,7 +2794,7 @@ def test_round_trip_persist_equivalence():
assert _saved_methods("class-decorator")[before:] == ["first", "second"]
def test_method_persist_backend_overrides_flow_level_backend_from_yaml():
def test_method_persist_backend_overrides_flow_level_backend_from_declaration():
yaml_str = f"""
schema: crewai.flow/v1
name: PersistedFlow
@@ -2583,7 +2818,7 @@ methods:
persistence_type: DefinitionStoreBackend
store: yaml-mixed-method
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
flow.kickoff()
assert _saved_methods("yaml-mixed-flow") == ["first"]
@@ -2731,8 +2966,8 @@ methods:
"""
def test_human_feedback_from_yaml_default_outcome_routes():
flow = Flow.from_definition(FlowDefinition.from_yaml(REVIEW_YAML))
def test_human_feedback_from_declaration_default_outcome_routes():
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=REVIEW_YAML))
with patch.object(flow, "_request_human_feedback", return_value="") as request:
result = flow.kickoff()
@@ -2743,8 +2978,8 @@ def test_human_feedback_from_yaml_default_outcome_routes():
assert flow.last_human_feedback.output == "draft-content"
def test_human_feedback_from_yaml_collapses_and_routes():
flow = Flow.from_definition(FlowDefinition.from_yaml(REVIEW_YAML))
def test_human_feedback_from_declaration_collapses_and_routes():
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=REVIEW_YAML))
with (
patch.object(flow, "_request_human_feedback", return_value="ship it"),
@@ -2761,7 +2996,7 @@ def test_round_trip_human_feedback_equivalence():
with patch.object(class_flow, "_request_human_feedback", return_value=""):
class_result = class_flow.kickoff()
definition = FlowDefinition.from_yaml(ReviewFlow.flow_definition().to_yaml())
definition = FlowDefinition.from_declaration(contents=ReviewFlow.flow_definition().to_yaml())
twin = Flow.from_definition(definition)
with patch.object(twin, "_request_human_feedback", return_value=""):
twin_result = twin.kickoff()
@@ -2774,8 +3009,8 @@ def test_round_trip_human_feedback_equivalence():
)
def test_human_feedback_pending_and_resume_from_yaml():
definition = FlowDefinition.from_yaml(PENDING_REVIEW_YAML)
def test_human_feedback_pending_and_resume_from_declaration():
definition = FlowDefinition.from_declaration(contents=PENDING_REVIEW_YAML)
flow = Flow.from_definition(definition)
pending = flow.kickoff()
@@ -2796,7 +3031,7 @@ def test_human_feedback_pending_and_resume_from_yaml():
assert flow_id not in DefinitionStoreBackend.pending
def test_flow_config_provider_fallback_from_yaml():
def test_flow_config_provider_fallback_from_declaration():
yaml_str = f"""
schema: crewai.flow/v1
name: ConfigProviderFlow
@@ -2822,7 +3057,7 @@ methods:
return "from-config"
provider = RecordingProvider()
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
previous = flow_config.hitl_provider
flow_config.hitl_provider = provider
@@ -2925,7 +3160,7 @@ methods:
message: "Review:"
provider: {__name__}:_NeedsArgsProvider
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
with pytest.raises(
ValueError, match="cannot instantiate human_feedback.provider ref"
@@ -2946,7 +3181,7 @@ methods:
message: "Review:"
provider: missing_module_xyz:Provider
"""
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
with pytest.raises(
ValueError, match="unresolvable human_feedback.provider ref"
@@ -2958,7 +3193,7 @@ def _checkpoint_chain_flow(tmp_path):
from crewai.state.provider.json_provider import JsonProvider
from crewai.state.runtime import RuntimeState
definition = FlowDefinition.from_yaml(CHAIN_YAML)
definition = FlowDefinition.from_declaration(contents=CHAIN_YAML)
flow = Flow.from_definition(definition)
result = flow.kickoff()
assert result == "confirmed:True"
@@ -2998,7 +3233,7 @@ state:
methods: {}
"""
with pytest.raises(ValidationError, match="default"):
FlowDefinition.from_yaml(yaml_str)
FlowDefinition.from_declaration(contents=yaml_str)
def test_definition_method_missing_from_class_fails_loudly():

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

@@ -46,6 +46,16 @@ class TestConsoleFormatterPauseResume:
formatter.resume_live_updates()
def test_flow_method_status_ignores_formatter_verbose(self):
formatter = ConsoleFormatter(verbose=False)
with patch.object(formatter, "print_panel") as mock_print_panel:
formatter.handle_method_status("categorize_tickets")
mock_print_panel.assert_called_once()
_, kwargs = mock_print_panel.call_args
assert kwargs["is_flow"] is True
def test_streaming_after_pause_resume_creates_new_session(self):
"""Test that streaming after pause/resume creates new Live session."""
formatter = ConsoleFormatter()

View File

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

View File

@@ -15,7 +15,7 @@ dev = [
"pytest==9.0.3",
"pytest-asyncio==1.3.0",
"pytest-subprocess==1.5.3",
"vcrpy==7.0.0", # pinned, less versions break pytest-recording
"vcrpy==8.2.1", # pinned, lower versions break pytest-recording
"pytest-recording==0.13.4",
"pytest-randomly==4.0.1",
"pytest-timeout==2.4.0",
@@ -171,8 +171,8 @@ info = "Commits must follow Conventional Commits 1.0.0."
[tool.uv]
exclude-newer = "3 days"
# pypdf 6.13.3 is a security fix newer than the global supply-chain cutoff.
exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z" }
# These security fixes are newer than the global supply-chain cutoff.
exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z" }
# composio-core pins rich<14 but textual requires rich>=14.
# onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10.
@@ -188,7 +188,8 @@ exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z" }
# python-multipart <0.0.27 has GHSA-pp6c-gr5w-3c5g (DoS via unbounded multipart headers).
# gitpython <3.1.50 has GHSA-mv93-w799-cj2w (config_writer newline injection bypassing the 3.1.49 patch -> RCE via core.hooksPath).
# urllib3 <2.7.0 has GHSA-qccp-gfcp-xxvc (ProxyManager cross-origin redirect leaks Authorization/Cookie) and GHSA-mf9v-mfxr-j63j (streaming decompression-bomb bypass); force 2.7.0+.
# langsmith <0.8.0 has GHSA-3644-q5cj-c5c7 (public prompt manifest deserialization, SSRF/secret disclosure); force 0.8.0+.
# langsmith <0.8.18 has GHSA-3644-q5cj-c5c7 (public prompt manifest deserialization, SSRF/secret disclosure)
# and GHSA-f4xh-w4cj-qxq8; force 0.8.18+.
# authlib <1.6.12 has GHSA-jj8c-mmj3-mmgv (CSRF bypass in cache-based state storage) and PYSEC-2026-188.
# pip 26.1.1 has PYSEC-2026-196; force 26.1.2+.
# aiohttp <=3.13.x has GHSA-jg22-mg44-37j8, GHSA-hg6j-4rv6-33pg; fixed in 3.14.0; force 3.14.0+.
@@ -196,6 +197,8 @@ exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z" }
# pip <26.1.1 has GHSA-58qw-9mgm-455v (archive handling); OSV considers 26.1.1 unaffected.
# paramiko <5.0.0 has GHSA-r374-rxx8-8654 (SHA-1 in rsakey.py); OSV considers 5.0.0 unaffected. Transitive via composio-core.
# starlette <1.3.1 has PYSEC-2026-161, GHSA-jp82-jpqv-5vv3, and GHSA-82w8-qh3p-5jfq. Transitive via fastapi.
# msgpack <1.2.1 has GHSA-6v7p-g79w-8964; transitive via pip-audit[filecache].
# pydantic-settings <2.14.2 has GHSA-4xgf-cpjx-pc3j.
# Keep OpenAI on the SDK range required by CrewAI when transitive dependencies
# loosen or pin their own lower versions.
override-dependencies = [
@@ -212,16 +215,17 @@ override-dependencies = [
"uv>=0.11.15,<1",
"python-multipart>=0.0.27,<1",
"gitpython>=3.1.50,<4",
"langsmith>=0.8.0,<1",
"langsmith>=0.8.18,<1",
"authlib>=1.6.12",
"pip>=26.1.2",
"aiohttp>=3.14.0",
# [chunking] carried here because override-dependencies replace the whole
# requirement; without it the docling extra's chunking deps get stripped.
"docling-core[chunking]>=2.74.1",
"pydantic-settings>=2.14.0",
"paramiko>=5.0.0",
"starlette>=1.3.1",
"msgpack>=1.2.1",
"pydantic-settings>=2.14.2",
]
[tool.uv.workspace]

175
uv.lock generated
View File

@@ -13,11 +13,14 @@ resolution-markers = [
]
[options]
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer = "2026-06-20T16:46:21.117658Z"
exclude-newer-span = "P3D"
[options.exclude-newer-package]
msgpack = "2026-06-20T00:00:00Z"
langsmith = "2026-06-20T00:00:00Z"
pypdf = "2026-06-18T00:00:00Z"
pydantic-settings = "2026-06-20T00:00:00Z"
[manifest]
members = [
@@ -36,13 +39,14 @@ overrides = [
{ name = "gitpython", specifier = ">=3.1.50,<4" },
{ name = "langchain-core", specifier = ">=1.3.3,<2" },
{ name = "langchain-text-splitters", specifier = ">=1.1.2,<2" },
{ name = "langsmith", specifier = ">=0.8.0,<1" },
{ name = "langsmith", specifier = ">=0.8.18,<1" },
{ name = "msgpack", specifier = ">=1.2.1" },
{ name = "onnxruntime", marker = "python_full_version < '3.11'", specifier = "<1.24" },
{ name = "openai", specifier = ">=2.30.0,<3" },
{ name = "paramiko", specifier = ">=5.0.0" },
{ name = "pillow", specifier = ">=12.1.1" },
{ name = "pip", specifier = ">=26.1.2" },
{ name = "pydantic-settings", specifier = ">=2.14.0" },
{ name = "pydantic-settings", specifier = ">=2.14.2" },
{ name = "pypdf", specifier = ">=6.13.3,<7" },
{ name = "python-multipart", specifier = ">=0.0.27,<1" },
{ name = "rich", specifier = ">=13.7.1" },
@@ -77,7 +81,7 @@ dev = [
{ name = "types-redis", specifier = "~=4.6" },
{ name = "types-regex", specifier = "==2026.1.15.*" },
{ name = "types-requests", specifier = "~=2.31.0.6" },
{ name = "vcrpy", specifier = "==7.0.0" },
{ name = "vcrpy", specifier = "==8.2.1" },
]
[[package]]
@@ -1448,9 +1452,9 @@ requires-dist = [
{ name = "openai", specifier = ">=2.30.0,<3" },
{ name = "openpyxl", specifier = "~=3.1.5" },
{ name = "openpyxl", marker = "extra == 'openpyxl'", specifier = "~=3.1.5" },
{ name = "opentelemetry-api", specifier = "~=1.34.0" },
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.34.0" },
{ name = "opentelemetry-sdk", specifier = "~=1.34.0" },
{ name = "opentelemetry-api", specifier = "~=1.42.0" },
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.42.0" },
{ name = "opentelemetry-sdk", specifier = "~=1.42.0" },
{ name = "pandas", marker = "extra == 'pandas'", specifier = "~=2.2.3" },
{ name = "pdfplumber", specifier = "~=0.11.4" },
{ name = "portalocker", specifier = "~=2.7.0" },
@@ -1535,9 +1539,9 @@ requires-dist = [
{ name = "appdirs", specifier = "~=1.4.4" },
{ name = "cryptography", specifier = ">=42.0" },
{ name = "httpx", specifier = "~=0.28.1" },
{ name = "opentelemetry-api", specifier = "~=1.34.0" },
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.34.0" },
{ name = "opentelemetry-sdk", specifier = "~=1.34.0" },
{ name = "opentelemetry-api", specifier = "~=1.42.0" },
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.42.0" },
{ name = "opentelemetry-sdk", specifier = "~=1.42.0" },
{ name = "packaging", specifier = ">=23.0" },
{ name = "portalocker", specifier = "~=2.7.0" },
{ name = "pydantic", specifier = ">=2.11.9,<2.13" },
@@ -3946,7 +3950,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2
[[package]]
name = "langsmith"
version = "0.8.11"
version = "0.8.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -3960,9 +3964,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ea/0d/082410ece26ff9f3ed4f87b014a8675be47cbd7d65f06b922045dfc21c47/langsmith-0.8.11.tar.gz", hash = "sha256:d9b3496f8f7ca63f4f2d1dfd368afd6c527923fff2ce4026c82ce85f37db3965", size = 4495842, upload-time = "2026-06-08T22:54:44.395Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/a6681aa9847bbbc5ec21abe20a5e233b94e5edcfe39624db607ac7e8ccb4/langsmith-0.8.18.tar.gz", hash = "sha256:32dde9c0e67e053e0fb738921fc8ced768af7b8fa83d7a0e3fd63597cf8776dd", size = 4526988, upload-time = "2026-06-19T13:12:17.123Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/65/f9c9dc19b21a9076286fafdb0ab732c9019ddf71aa7e7d720a830a98fe2a/langsmith-0.8.11-py3-none-any.whl", hash = "sha256:08aa5e84b00703ecc11dbeafda78d84b92da4e8c6114e0be9b59df9e71afc59b", size = 478985, upload-time = "2026-06-08T22:54:42.349Z" },
{ url = "https://files.pythonhosted.org/packages/03/70/0e0cc80a3b064c8d6c8d697c3125ed86e39d5a7393ec6dc8b07cb1cf13c4/langsmith-0.8.18-py3-none-any.whl", hash = "sha256:3940183349993faef48e6c7d08e4822ee9cefd906b362d0e3c2d650314d2f282", size = 508108, upload-time = "2026-06-19T13:12:15.348Z" },
]
[[package]]
@@ -4702,45 +4706,53 @@ wheels = [
[[package]]
name = "msgpack"
version = "1.1.2"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" },
{ url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" },
{ url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" },
{ url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" },
{ url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" },
{ url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" },
{ url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" },
{ url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" },
{ url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
{ url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
{ url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
{ url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
{ url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
{ url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
{ url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
{ url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
{ url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
{ url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
{ url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
{ url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
{ url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
{ url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
{ url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
{ url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
{ url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
{ url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
{ url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
{ url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
{ url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
{ url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
{ url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
{ url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
{ url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
{ url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" },
{ url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" },
{ url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" },
{ url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" },
{ url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" },
{ url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" },
{ url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" },
{ url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" },
{ url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" },
{ url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" },
{ url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" },
{ url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" },
{ url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" },
{ url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" },
{ url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" },
{ url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" },
{ url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" },
{ url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" },
{ url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" },
{ url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" },
{ url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" },
{ url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" },
{ url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" },
{ url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" },
{ url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" },
{ url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" },
{ url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" },
{ url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" },
{ url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" },
{ url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" },
{ url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" },
{ url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" },
{ url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" },
{ url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" },
{ url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" },
{ url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" },
{ url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" },
]
[[package]]
@@ -5573,45 +5585,44 @@ wheels = [
[[package]]
name = "opentelemetry-api"
version = "1.34.1"
version = "1.42.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4d/5e/94a8cb759e4e409022229418294e098ca7feca00eb3c467bb20cbd329bda/opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3", size = 64987, upload-time = "2025-06-10T08:55:19.818Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/3a/2ba85557e8dc024c0842ad22c570418dc02c36cbd1ab4b832a93edf071b8/opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c", size = 65767, upload-time = "2025-06-10T08:54:56.717Z" },
{ url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp"
version = "1.34.1"
version = "1.42.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/ba/786b4de7e39d88043622d901b92c4485835f43e0be76c2824d2687911bc2/opentelemetry_exporter_otlp-1.34.1.tar.gz", hash = "sha256:71c9ad342d665d9e4235898d205db17c5764cd7a69acb8a5dcd6d5e04c4c9988", size = 6173, upload-time = "2025-06-10T08:55:21.595Z" }
sdist = { url = "https://files.pythonhosted.org/packages/08/94/8637919a5d01f81dacf510234bc0110b944f4687a6e96b0a02adf2f6bdce/opentelemetry_exporter_otlp-1.42.1.tar.gz", hash = "sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9", size = 6086, upload-time = "2026-05-21T16:32:51.963Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/c1/259b8d8391c968e8f005d8a0ccefcb41aeef64cf55905cd0c0db4e22aaee/opentelemetry_exporter_otlp-1.34.1-py3-none-any.whl", hash = "sha256:f4a453e9cde7f6362fd4a090d8acf7881d1dc585540c7b65cbd63e36644238d4", size = 7040, upload-time = "2025-06-10T08:54:59.655Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4d/c26080295a36fd22e201fefd7cb9c22cd203189b1af8cd73b158382b7ad8/opentelemetry_exporter_otlp-1.42.1-py3-none-any.whl", hash = "sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee", size = 6733, upload-time = "2026-05-21T16:32:31.261Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.34.1"
version = "1.42.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/f0/ff235936ee40db93360233b62da932d4fd9e8d103cd090c6bcb9afaf5f01/opentelemetry_exporter_otlp_proto_common-1.34.1.tar.gz", hash = "sha256:b59a20a927facd5eac06edaf87a07e49f9e4a13db487b7d8a52b37cb87710f8b", size = 20817, upload-time = "2025-06-10T08:55:22.55Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/e8/8b292a11cc8d8d87ec0c4089ae21b6a58af49ca2e51fa916435bc922fdc7/opentelemetry_exporter_otlp_proto_common-1.34.1-py3-none-any.whl", hash = "sha256:8e2019284bf24d3deebbb6c59c71e6eef3307cd88eff8c633e061abba33f7e87", size = 18834, upload-time = "2025-06-10T08:55:00.806Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-grpc"
version = "1.34.1"
version = "1.42.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
@@ -5622,14 +5633,14 @@ dependencies = [
{ name = "opentelemetry-sdk" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/41/f7/bb63837a3edb9ca857aaf5760796874e7cecddc88a2571b0992865a48fb6/opentelemetry_exporter_otlp_proto_grpc-1.34.1.tar.gz", hash = "sha256:7c841b90caa3aafcfc4fee58487a6c71743c34c6dc1787089d8b0578bbd794dd", size = 22566, upload-time = "2025-06-10T08:55:23.214Z" }
sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/42/0a4dd47e7ef54edf670c81fc06a83d68ea42727b82126a1df9dd0477695d/opentelemetry_exporter_otlp_proto_grpc-1.34.1-py3-none-any.whl", hash = "sha256:04bb8b732b02295be79f8a86a4ad28fae3d4ddb07307a98c7aa6f331de18cca6", size = 18615, upload-time = "2025-06-10T08:55:02.214Z" },
{ url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.34.1"
version = "1.42.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
@@ -5640,48 +5651,48 @@ dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/8f/954bc725961cbe425a749d55c0ba1df46832a5999eae764d1a7349ac1c29/opentelemetry_exporter_otlp_proto_http-1.34.1.tar.gz", hash = "sha256:aaac36fdce46a8191e604dcf632e1f9380c7d5b356b27b3e0edb5610d9be28ad", size = 15351, upload-time = "2025-06-10T08:55:24.657Z" }
sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/54/b05251c04e30c1ac70cf4a7c5653c085dfcf2c8b98af71661d6a252adc39/opentelemetry_exporter_otlp_proto_http-1.34.1-py3-none-any.whl", hash = "sha256:5251f00ca85872ce50d871f6d3cc89fe203b94c3c14c964bbdc3883366c705d8", size = 17744, upload-time = "2025-06-10T08:55:03.802Z" },
{ url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.34.1"
version = "1.42.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/b3/c3158dd012463bb7c0eb7304a85a6f63baeeb5b4c93a53845cf89f848c7e/opentelemetry_proto-1.34.1.tar.gz", hash = "sha256:16286214e405c211fc774187f3e4bbb1351290b8dfb88e8948af209ce85b719e", size = 34344, upload-time = "2025-06-10T08:55:32.25Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/ab/4591bfa54e946350ce8b3f28e5c658fe9785e7cd11e9c11b1671a867822b/opentelemetry_proto-1.34.1-py3-none-any.whl", hash = "sha256:eb4bb5ac27f2562df2d6857fc557b3a481b5e298bc04f94cc68041f00cebcbd2", size = 55692, upload-time = "2025-06-10T08:55:14.904Z" },
{ url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.34.1"
version = "1.42.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/41/fe20f9036433da8e0fcef568984da4c1d1c771fa072ecd1a4d98779dccdd/opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d", size = 159441, upload-time = "2025-06-10T08:55:33.028Z" }
sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/1b/def4fe6aa73f483cabf4c748f4c25070d5f7604dcc8b52e962983491b29e/opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e", size = 118477, upload-time = "2025-06-10T08:55:16.02Z" },
{ url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.55b1"
version = "0.63b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/f0/f33458486da911f47c4aa6db9bda308bb80f3236c111bf848bd870c16b16/opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3", size = 119829, upload-time = "2025-06-10T08:55:33.881Z" }
sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/89/267b0af1b1d0ba828f0e60642b6a5116ac1fd917cde7fc02821627029bd1/opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed", size = 196223, upload-time = "2025-06-10T08:55:17.638Z" },
{ url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" },
]
[[package]]
@@ -6989,16 +7000,16 @@ wheels = [
[[package]]
name = "pydantic-settings"
version = "2.14.1"
version = "2.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" },
{ url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
]
[[package]]
@@ -9698,17 +9709,15 @@ wheels = [
[[package]]
name = "vcrpy"
version = "7.0.0"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
{ name = "urllib3" },
{ name = "wrapt" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/25/d3/856e06184d4572aada1dd559ddec3bedc46df1f2edc5ab2c91121a2cccdb/vcrpy-7.0.0.tar.gz", hash = "sha256:176391ad0425edde1680c5b20738ea3dc7fb942520a48d2993448050986b3a50", size = 85502, upload-time = "2024-12-31T00:07:57.894Z" }
sdist = { url = "https://files.pythonhosted.org/packages/08/db/08183b845b0040bb877dad2bd7e4e0976fc232bb3476d7ee369c6c4f8b5a/vcrpy-8.2.1.tar.gz", hash = "sha256:d73a6e4eb6dae8148e659764b7a00e68cc51ba29ba9e6a85e1f0790ad96b97df", size = 90511, upload-time = "2026-06-16T13:20:52.906Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/5d/1f15b252890c968d42b348d1e9b0aa12d5bf3e776704178ec37cceccdb63/vcrpy-7.0.0-py2.py3-none-any.whl", hash = "sha256:55791e26c18daa363435054d8b35bd41a4ac441b6676167635d1b37a71dbe124", size = 42321, upload-time = "2024-12-31T00:07:55.277Z" },
{ url = "https://files.pythonhosted.org/packages/f8/7c/0e812ab83f5289404c674f3461ba783250b967d34b5ab034d361236ec042/vcrpy-8.2.1-py3-none-any.whl", hash = "sha256:7ce58c9e2792b246f79d6f4b3e9660676cc6f853be17e1547305b4437ab1ff85", size = 44925, upload-time = "2026-06-16T13:20:51.734Z" },
]
[[package]]