Compare commits

..

2 Commits

Author SHA1 Message Date
Devin AI
e18477235b style: fix ruff formatting in browser_toolkit.py
Co-Authored-By: João <joao@crewai.com>
2026-03-26 14:00:52 +00:00
Devin AI
a4890e5626 fix: add Python 3.14 compatibility by replacing asyncio.get_event_loop() with get_running_loop()
Python 3.14 changed asyncio.get_event_loop() to raise RuntimeError when
no running event loop exists instead of silently creating one. This broke
all async code paths that relied on get_event_loop().

Changes:
- Replace asyncio.get_event_loop() with asyncio.get_running_loop() in all
  async contexts across crewai core and crewai-tools
- Update requires-python from '<3.14' to '<3.15' in all pyproject.toml files
- Add comprehensive tests for Python 3.14 async compatibility
- Regenerate uv.lock for the updated version constraint

Closes #5109

Co-Authored-By: João <joao@crewai.com>
2026-03-26 13:57:11 +00:00
32 changed files with 4910 additions and 5324 deletions

View File

@@ -23,7 +23,7 @@ jobs:
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
node-version: "latest"
- name: Install Mintlify CLI
run: npm i -g mintlify

View File

@@ -4,63 +4,6 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
<Update label="27 مارس 2026">
## v1.13.0rc1
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
## ما الذي تغير
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.13.0a2
## المساهمون
@greysonlalonde
</Update>
<Update label="27 مارس 2026">
## v1.13.0a2
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
## ما الذي تغير
### الميزات
- تحديث تلقائي لمستودع اختبار النشر أثناء الإصدار
- تحسين مرونة إصدار المؤسسات وتجربة المستخدم
### الوثائق
- تحديث سجل التغييرات والإصدار للإصدار v1.13.0a1
## المساهمون
@greysonlalonde
</Update>
<Update label="27 مارس 2026">
## v1.13.0a1
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
## ما الذي تغير
### إصلاحات الأخطاء
- إصلاح الروابط المعطلة في سير العمل الوثائقي عن طريق تثبيت Node على LTS 22
- مسح ذاكرة التخزين المؤقت لـ uv للحزم المنشورة حديثًا في الإصدار المؤسسي
### الوثائق
- إضافة مصفوفة شاملة لأذونات RBAC ودليل النشر
- تحديث سجل التغييرات والإصدار للإصدار v1.12.2
## المساهمون
@greysonlalonde, @iris-clawd, @joaomdmoura
</Update>
<Update label="25 مارس 2026">
## v1.12.2

View File

@@ -4,63 +4,6 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="Mar 27, 2026">
## v1.13.0rc1
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
## What's Changed
### Documentation
- Update changelog and version for v1.13.0a2
## Contributors
@greysonlalonde
</Update>
<Update label="Mar 27, 2026">
## v1.13.0a2
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
## What's Changed
### Features
- Auto-update deployment test repo during release
- Improve enterprise release resilience and UX
### Documentation
- Update changelog and version for v1.13.0a1
## Contributors
@greysonlalonde
</Update>
<Update label="Mar 27, 2026">
## v1.13.0a1
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
## What's Changed
### Bug Fixes
- Fix broken links in documentation workflow by pinning Node to LTS 22
- Bust the uv cache for freshly published packages in enterprise release
### Documentation
- Add comprehensive RBAC permissions matrix and deployment guide
- Update changelog and version for v1.12.2
## Contributors
@greysonlalonde, @iris-clawd, @joaomdmoura
</Update>
<Update label="Mar 25, 2026">
## v1.12.2

View File

@@ -7,13 +7,11 @@ mode: "wide"
## Overview
RBAC in CrewAI AMP enables secure, scalable access management through two layers:
1. **Feature permissions** — control what each role can do across the platform (manage, read, or no access)
2. **Entity-level permissions** — fine-grained access on individual automations, environment variables, LLM connections, and Git repositories
RBAC in CrewAI AMP enables secure, scalable access management through a combination of organizationlevel roles and automationlevel visibility controls.
<Frame>
<img src="/images/enterprise/users_and_roles.png" alt="RBAC overview in CrewAI AMP" />
</Frame>
## Users and Roles
@@ -41,13 +39,6 @@ You can configure users and roles in Settings → Roles.
</Step>
</Steps>
### Predefined Roles
| Role | Description |
| :--------- | :-------------------------------------------------------------------------- |
| **Owner** | Full access to all features and settings. Cannot be restricted. |
| **Member** | Read access to most features, manage access to Studio projects. Cannot modify organization or default settings. |
### Configuration summary
| Area | Where to configure | Options |
@@ -55,80 +46,23 @@ You can configure users and roles in Settings → Roles.
| Users & Roles | Settings → Roles | Predefined: Owner, Member; Custom roles |
| Automation visibility | Automation → Settings → Visibility | Private; Whitelist users/roles |
---
## Automationlevel Access Control
## Feature Permissions Matrix
In addition to organizationwide roles, CrewAI Automations support finegrained visibility settings that let you restrict access to specific automations by user or role.
Every role has a permission level for each feature area. The three levels are:
- **Manage** — full read/write access (create, edit, delete)
- **Read** — view-only access
- **No access** — feature is hidden/inaccessible
| Feature | Owner | Member (default) | Description |
| :------------------------ | :------ | :--------------- | :-------------------------------------------------------------- |
| `usage_dashboards` | Manage | Read | View usage metrics and analytics |
| `crews_dashboards` | Manage | Read | View deployment dashboards, access automation details |
| `invitations` | Manage | Read | Invite new members to the organization |
| `training_ui` | Manage | Read | Access training/fine-tuning interfaces |
| `tools` | Manage | Read | Create and manage tools |
| `agents` | Manage | Read | Create and manage agents |
| `environment_variables` | Manage | Read | Create and manage environment variables |
| `llm_connections` | Manage | Read | Configure LLM provider connections |
| `default_settings` | Manage | No access | Modify organization-wide default settings |
| `organization_settings` | Manage | No access | Manage billing, plans, and organization configuration |
| `studio_projects` | Manage | Manage | Create and edit projects in Studio |
<Tip>
When creating a custom role, you can set each feature independently to **Manage**, **Read**, or **No access** to match your team's needs.
</Tip>
---
## Deploying from GitHub or Zip
One of the most common RBAC questions is: _"What permissions does a team member need to deploy?"_
### Deploy from GitHub
To deploy an automation from a GitHub repository, a user needs:
1. **`crews_dashboards`**: at least `Read` — required to access the automations dashboard where deployments are created
2. **Git repository access** (if entity-level RBAC for Git repositories is enabled): the user's role must be granted access to the specific Git repository via entity-level permissions
3. **`studio_projects`: `Manage`** — if building the crew in Studio before deploying
### Deploy from Zip
To deploy an automation from a Zip file upload, a user needs:
1. **`crews_dashboards`**: at least `Read` — required to access the automations dashboard
2. **Zip deployments enabled**: the organization must not have disabled zip deployments in organization settings
### Quick Reference: Minimum Permissions for Deployment
| Action | Required feature permissions | Additional requirements |
| :------------------- | :------------------------------------ | :----------------------------------------------- |
| Deploy from GitHub | `crews_dashboards: Read` | Git repo entity access (if Git RBAC is enabled) |
| Deploy from Zip | `crews_dashboards: Read` | Zip deployments must be enabled at the org level |
| Build in Studio | `studio_projects: Manage` | — |
| Configure LLM keys | `llm_connections: Manage` | — |
| Set environment vars | `environment_variables: Manage` | Entity-level access (if entity RBAC is enabled) |
---
## Automationlevel Access Control (Entity Permissions)
In addition to organizationwide roles, CrewAI supports finegrained entity-level permissions that restrict access to individual resources.
### Automation Visibility
Automations support visibility settings that restrict access by user or role. This is useful for:
This is useful for:
- Keeping sensitive or experimental automations private
- Managing visibility across large teams or external collaborators
- Testing automations in isolated contexts
Deployments can be configured as private, meaning only whitelisted users and roles will be able to interact with them.
Deployments can be configured as private, meaning only whitelisted users and roles will be able to:
- View the deployment
- Run it or interact with its API
- Access its logs, metrics, and settings
The organization owner always has access, regardless of visibility settings.
You can configure automationlevel access control in Automation → Settings → Visibility tab.
@@ -165,92 +99,9 @@ You can configure automationlevel access control in Automation → Settings
<Frame>
<img src="/images/enterprise/visibility.png" alt="Automation Visibility settings in CrewAI AMP" />
</Frame>
### Deployment Permission Types
When granting entity-level access to a specific automation, you can assign these permission types:
| Permission | What it allows |
| :------------------- | :-------------------------------------------------- |
| `run` | Execute the automation and use its API |
| `traces` | View execution traces and logs |
| `manage_settings` | Edit, redeploy, rollback, or delete the automation |
| `human_in_the_loop` | Respond to human-in-the-loop (HITL) requests |
| `full_access` | All of the above |
### Entity-level RBAC for Other Resources
When entity-level RBAC is enabled, access to these resources can also be controlled per user or role:
| Resource | Controlled by | Description |
| :--------------------- | :------------------------------- | :---------------------------------------------------- |
| Environment variables | Entity RBAC feature flag | Restrict which roles/users can view or manage specific env vars |
| LLM connections | Entity RBAC feature flag | Restrict access to specific LLM provider configurations |
| Git repositories | Git repositories RBAC org setting | Restrict which roles/users can access specific connected repos |
---
## Common Role Patterns
While CrewAI ships with Owner and Member roles, most teams benefit from creating custom roles. Here are common patterns:
### Developer Role
A role for team members who build and deploy automations but don't manage organization settings.
| Feature | Permission |
| :------------------------ | :--------- |
| `usage_dashboards` | Read |
| `crews_dashboards` | Manage |
| `invitations` | Read |
| `training_ui` | Read |
| `tools` | Manage |
| `agents` | Manage |
| `environment_variables` | Manage |
| `llm_connections` | Read |
| `default_settings` | No access |
| `organization_settings` | No access |
| `studio_projects` | Manage |
### Viewer / Stakeholder Role
A role for non-technical stakeholders who need to monitor automations and view results.
| Feature | Permission |
| :------------------------ | :--------- |
| `usage_dashboards` | Read |
| `crews_dashboards` | Read |
| `invitations` | No access |
| `training_ui` | Read |
| `tools` | Read |
| `agents` | Read |
| `environment_variables` | No access |
| `llm_connections` | No access |
| `default_settings` | No access |
| `organization_settings` | No access |
| `studio_projects` | Read |
### Ops / Platform Admin Role
A role for platform operators who manage infrastructure settings but may not build agents.
| Feature | Permission |
| :------------------------ | :--------- |
| `usage_dashboards` | Manage |
| `crews_dashboards` | Manage |
| `invitations` | Manage |
| `training_ui` | Read |
| `tools` | Read |
| `agents` | Read |
| `environment_variables` | Manage |
| `llm_connections` | Manage |
| `default_settings` | Manage |
| `organization_settings` | Read |
| `studio_projects` | Read |
---
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
Contact our support team for assistance with RBAC questions.
</Card>

View File

@@ -4,63 +4,6 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
<Update label="2026년 3월 27일">
## v1.13.0rc1
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
## 변경 사항
### 문서
- v1.13.0a2의 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde
</Update>
<Update label="2026년 3월 27일">
## v1.13.0a2
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
## 변경 사항
### 기능
- 릴리스 중 자동 업데이트 배포 테스트 리포지토리
- 기업 릴리스의 복원력 및 사용자 경험 개선
### 문서
- v1.13.0a1에 대한 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde
</Update>
<Update label="2026년 3월 27일">
## v1.13.0a1
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
## 변경 사항
### 버그 수정
- Node를 LTS 22로 고정하여 문서 작업 흐름의 끊어진 링크 수정
- 기업 릴리스에서 새로 게시된 패키지의 uv 캐시 초기화
### 문서
- 포괄적인 RBAC 권한 매트릭스 및 배포 가이드 추가
- v1.12.2에 대한 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde, @iris-clawd, @joaomdmoura
</Update>
<Update label="2026년 3월 25일">
## v1.12.2

View File

@@ -4,63 +4,6 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="27 mar 2026">
## v1.13.0rc1
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
## O que Mudou
### Documentação
- Atualizar changelog e versão para v1.13.0a2
## Contribuidores
@greysonlalonde
</Update>
<Update label="27 mar 2026">
## v1.13.0a2
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
## O que Mudou
### Recursos
- Repositório de teste de implantação de autoatualização durante o lançamento
- Melhorar a resiliência e a experiência do usuário na versão empresarial
### Documentação
- Atualizar changelog e versão para v1.13.0a1
## Contribuidores
@greysonlalonde
</Update>
<Update label="27 mar 2026">
## v1.13.0a1
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
## O que Mudou
### Correções de Bugs
- Corrigir links quebrados no fluxo de documentação fixando o Node na LTS 22
- Limpar o cache uv para pacotes recém-publicados na versão empresarial
### Documentação
- Adicionar uma matriz abrangente de permissões RBAC e guia de implantação
- Atualizar o changelog e a versão para v1.12.2
## Contributors
@greysonlalonde, @iris-clawd, @joaomdmoura
</Update>
<Update label="25 mar 2026">
## v1.12.2

View File

@@ -6,7 +6,7 @@ readme = "README.md"
authors = [
{ name = "Greyson LaLonde", email = "greyson@crewai.com" }
]
requires-python = ">=3.10, <3.14"
requires-python = ">=3.10, <3.15"
dependencies = [
"Pillow~=12.1.1",
"pypdf~=6.9.1",

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.13.0rc1"
__version__ = "1.12.2"

View File

@@ -6,12 +6,12 @@ readme = "README.md"
authors = [
{ name = "João Moura", email = "joaomdmoura@gmail.com" },
]
requires-python = ">=3.10, <3.14"
requires-python = ">=3.10, <3.15"
dependencies = [
"pytube~=15.0.0",
"requests~=2.32.5",
"docker~=7.1.0",
"crewai==1.13.0rc1",
"crewai==1.12.2",
"tiktoken~=0.8.0",
"beautifulsoup4~=4.13.4",
"python-docx~=1.2.0",

View File

@@ -309,4 +309,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.13.0rc1"
__version__ = "1.12.2"

View File

@@ -47,7 +47,7 @@ class BrowserSessionManager:
Returns:
An async browser instance specific to the thread
"""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
while True:
with self._lock:
if thread_id in self._async_sessions:

View File

@@ -94,11 +94,9 @@ class BrowserBaseTool(BaseTool):
try:
import nest_asyncio # type: ignore[import-untyped]
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
nest_asyncio.apply(loop)
result: str = asyncio.get_event_loop().run_until_complete(
self._arun(*args, **kwargs)
)
result: str = loop.run_until_complete(self._arun(*args, **kwargs))
return result
except Exception as e:
return f"Error in patched _run: {e!s}"
@@ -118,7 +116,7 @@ class BrowserBaseTool(BaseTool):
def _is_in_asyncio_loop(self) -> bool:
"""Check if we're currently in an asyncio event loop."""
try:
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
return loop.is_running()
except RuntimeError:
return False
@@ -544,14 +542,13 @@ class BrowserToolkit:
def _nest_current_loop(self) -> None:
"""Apply nest_asyncio if we're in an asyncio loop."""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
try:
import nest_asyncio
loop = asyncio.get_running_loop()
try:
import nest_asyncio
nest_asyncio.apply(loop)
except Exception as e:
logger.warning(f"Failed to apply nest_asyncio: {e!s}")
nest_asyncio.apply(loop)
except Exception as e:
logger.warning(f"Failed to apply nest_asyncio: {e!s}")
except RuntimeError:
pass

View File

@@ -168,7 +168,7 @@ class SnowflakeSearchTool(BaseTool):
with self._pool_lock:
if self._connection_pool:
return self._connection_pool.pop()
return await asyncio.get_event_loop().run_in_executor(
return await asyncio.get_running_loop().run_in_executor(
self._thread_pool, self._create_connection
)

View File

@@ -6,7 +6,7 @@ readme = "README.md"
authors = [
{ name = "Joao Moura", email = "joao@crewai.com" }
]
requires-python = ">=3.10, <3.14"
requires-python = ">=3.10, <3.15"
dependencies = [
# Core Dependencies
"pydantic~=2.11.9",
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.13.0rc1",
"crewai-tools==1.12.2",
]
embeddings = [
"tiktoken~=0.8.0"

View File

@@ -42,7 +42,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.13.0rc1"
__version__ = "1.12.2"
_telemetry_submitted = False

View File

@@ -362,7 +362,7 @@ class MemoryTUI(App[None]):
panel.loading = True
try:
scope = self._selected_scope if self._selected_scope != "/" else None
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
matches = await loop.run_in_executor(
None,
lambda: self._memory.recall(query, scope=scope, limit=10, depth="deep"),

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.13.0rc1"
"crewai[tools]==1.12.2"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.13.0rc1"
"crewai[tools]==1.12.2"
]
[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.13.0rc1"
"crewai[tools]==1.12.2"
]
[tool.crewai]

View File

@@ -57,7 +57,6 @@ class LLMCallCompletedEvent(LLMEventBase):
messages: str | list[dict[str, Any]] | None = None
response: Any
call_type: LLMCallType
stop_reason: str | None = None
class LLMCallFailedEvent(LLMEventBase):

View File

@@ -412,7 +412,6 @@ class BaseLLM(ABC):
from_task: Task | None = None,
from_agent: Agent | None = None,
messages: str | list[LLMMessage] | None = None,
stop_reason: str | None = None,
) -> None:
"""Emit LLM call completed event."""
from crewai.utilities.serialization import to_serializable
@@ -427,7 +426,6 @@ class BaseLLM(ABC):
from_agent=from_agent,
model=self.model,
call_id=get_current_call_id(),
stop_reason=stop_reason,
),
)

View File

@@ -676,26 +676,6 @@ class AnthropicCompletion(BaseLLM):
return converted
@staticmethod
def _extract_stop_reason(response: Message | BetaMessage) -> str | None:
"""Extract stop_reason from an Anthropic response, returning None if absent."""
raw = getattr(response, "stop_reason", None)
return raw if isinstance(raw, str) else None
def _warn_if_truncated(
self,
response: Message | BetaMessage,
from_agent: Any | None = None,
) -> None:
"""Log a warning if the response was truncated due to max_tokens."""
stop_reason = self._extract_stop_reason(response)
if stop_reason == "max_tokens":
agent_hint = f" [{from_agent.role}]" if from_agent else ""
logging.warning(
f"Truncated response{agent_hint}: stop_reason='max_tokens'. "
f"Consider increasing max_tokens (current: {self.max_tokens})."
)
def _format_messages_for_anthropic(
self, messages: str | list[LLMMessage]
) -> tuple[list[LLMMessage], str | None]:
@@ -878,9 +858,6 @@ class AnthropicCompletion(BaseLLM):
usage = self._extract_anthropic_token_usage(response)
self._track_token_usage_internal(usage)
stop_reason = self._extract_stop_reason(response)
self._warn_if_truncated(response, from_agent)
if _is_pydantic_model_class(response_model) and response.content:
if use_native_structured_output:
for block in response.content:
@@ -892,7 +869,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return structured_data
else:
@@ -908,7 +884,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return structured_data
@@ -931,7 +906,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return list(tool_uses)
@@ -963,7 +937,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
if usage.get("total_tokens", 0) > 0:
@@ -1104,9 +1077,6 @@ class AnthropicCompletion(BaseLLM):
usage = self._extract_anthropic_token_usage(final_message)
self._track_token_usage_internal(usage)
stop_reason = self._extract_stop_reason(final_message)
self._warn_if_truncated(final_message, from_agent)
if _is_pydantic_model_class(response_model):
if use_native_structured_output:
structured_data = response_model.model_validate_json(full_response)
@@ -1116,7 +1086,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return structured_data
for block in final_message.content:
@@ -1131,7 +1100,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return structured_data
@@ -1161,7 +1129,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return self._invoke_after_llm_call_hooks(
@@ -1308,9 +1275,6 @@ class AnthropicCompletion(BaseLLM):
follow_up_usage = self._extract_anthropic_token_usage(final_response)
self._track_token_usage_internal(follow_up_usage)
stop_reason = self._extract_stop_reason(final_response)
self._warn_if_truncated(final_response, from_agent)
final_content = ""
thinking_blocks: list[ThinkingBlock] = []
@@ -1335,7 +1299,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=follow_up_params["messages"],
stop_reason=stop_reason,
)
# Log combined token usage
@@ -1416,9 +1379,6 @@ class AnthropicCompletion(BaseLLM):
usage = self._extract_anthropic_token_usage(response)
self._track_token_usage_internal(usage)
stop_reason = self._extract_stop_reason(response)
self._warn_if_truncated(response, from_agent)
if _is_pydantic_model_class(response_model) and response.content:
if use_native_structured_output:
for block in response.content:
@@ -1430,7 +1390,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return structured_data
else:
@@ -1446,7 +1405,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return structured_data
@@ -1467,7 +1425,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return list(tool_uses)
@@ -1491,7 +1448,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
if usage.get("total_tokens", 0) > 0:
@@ -1620,9 +1576,6 @@ class AnthropicCompletion(BaseLLM):
usage = self._extract_anthropic_token_usage(final_message)
self._track_token_usage_internal(usage)
stop_reason = self._extract_stop_reason(final_message)
self._warn_if_truncated(final_message, from_agent)
if _is_pydantic_model_class(response_model):
if use_native_structured_output:
structured_data = response_model.model_validate_json(full_response)
@@ -1632,7 +1585,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return structured_data
for block in final_message.content:
@@ -1647,7 +1599,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return structured_data
@@ -1676,7 +1627,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=params["messages"],
stop_reason=stop_reason,
)
return full_response
@@ -1721,9 +1671,6 @@ class AnthropicCompletion(BaseLLM):
follow_up_usage = self._extract_anthropic_token_usage(final_response)
self._track_token_usage_internal(follow_up_usage)
stop_reason = self._extract_stop_reason(final_response)
self._warn_if_truncated(final_response, from_agent)
final_content = ""
if final_response.content:
for content_block in final_response.content:
@@ -1738,7 +1685,6 @@ class AnthropicCompletion(BaseLLM):
from_task=from_task,
from_agent=from_agent,
messages=follow_up_params["messages"],
stop_reason=stop_reason,
)
total_usage = {

View File

@@ -86,7 +86,7 @@ class ChromaDBClient(BaseClient):
yield
return
lock_cm = store_lock(self._lock_name)
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, lock_cm.__enter__)
try:
yield

View File

@@ -266,7 +266,7 @@ class CrewStructuredTool:
# Run sync functions in a thread pool
import asyncio
return await asyncio.get_event_loop().run_in_executor(
return await asyncio.get_running_loop().run_in_executor(
None, lambda: self.func(**parsed_args, **kwargs)
)
except Exception:

View File

@@ -184,7 +184,7 @@ def create_streaming_state(
if use_async:
async_queue = asyncio.Queue()
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
handler = _create_stream_handler(current_task_info, sync_queue, async_queue, loop)
crewai_event_bus.register_handler(LLMStreamChunkEvent, handler)

View File

@@ -1463,217 +1463,3 @@ def test_tool_search_saves_input_tokens():
f"Expected tool_search ({usage_search.prompt_tokens}) to use fewer input tokens "
f"than no search ({usage_no_search.prompt_tokens})"
)
def test_anthropic_warns_on_max_tokens_truncation():
"""Test that a warning is logged when Anthropic response has stop_reason='max_tokens'."""
import logging
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
with patch.object(llm.client.messages, "create") as mock_create:
mock_response = MagicMock()
mock_response.content = [MagicMock(text="truncated response")]
mock_response.content[0].type = "text"
mock_response.stop_reason = "max_tokens"
mock_response.usage = MagicMock(
input_tokens=100, output_tokens=4096, cache_read_input_tokens=0
)
mock_create.return_value = mock_response
with patch("crewai.llms.providers.anthropic.completion.logging") as mock_logging:
result = llm.call("Tell me a very long story")
assert result == "truncated response"
mock_logging.warning.assert_called_once()
warning_msg = mock_logging.warning.call_args[0][0]
assert "stop_reason='max_tokens'" in warning_msg
assert "Consider increasing max_tokens" in warning_msg
def test_anthropic_no_warning_on_end_turn():
"""Test that no truncation warning is logged when stop_reason is 'end_turn'."""
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
with patch.object(llm.client.messages, "create") as mock_create:
mock_response = MagicMock()
mock_response.content = [MagicMock(text="complete response")]
mock_response.content[0].type = "text"
mock_response.stop_reason = "end_turn"
mock_response.usage = MagicMock(
input_tokens=50, output_tokens=25, cache_read_input_tokens=0
)
mock_create.return_value = mock_response
with patch("crewai.llms.providers.anthropic.completion.logging") as mock_logging:
result = llm.call("Hello")
assert result == "complete response"
mock_logging.warning.assert_not_called()
def test_anthropic_truncation_warning_includes_agent_role():
"""Test that the truncation warning includes the agent role when available."""
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
mock_agent = MagicMock()
mock_agent.role = "Research Analyst"
with patch.object(llm.client.messages, "create") as mock_create:
mock_response = MagicMock()
mock_response.content = [MagicMock(text="truncated")]
mock_response.content[0].type = "text"
mock_response.stop_reason = "max_tokens"
mock_response.usage = MagicMock(
input_tokens=100, output_tokens=4096, cache_read_input_tokens=0
)
mock_create.return_value = mock_response
with patch("crewai.llms.providers.anthropic.completion.logging") as mock_logging:
llm.call("Tell me everything", from_agent=mock_agent)
mock_logging.warning.assert_called_once()
warning_msg = mock_logging.warning.call_args[0][0]
assert "[Research Analyst]" in warning_msg
assert "stop_reason='max_tokens'" in warning_msg
def test_anthropic_stop_reason_in_completed_event():
"""Test that stop_reason is included in the LLMCallCompletedEvent."""
from crewai.events.types.llm_events import LLMCallCompletedEvent
from crewai.events import crewai_event_bus
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
captured_events = []
def capture_event(source, event):
if isinstance(event, LLMCallCompletedEvent):
captured_events.append(event)
crewai_event_bus.register_handler(LLMCallCompletedEvent, capture_event)
try:
with patch.object(llm.client.messages, "create") as mock_create:
mock_response = MagicMock()
mock_response.content = [MagicMock(text="truncated response")]
mock_response.content[0].type = "text"
mock_response.stop_reason = "max_tokens"
mock_response.usage = MagicMock(
input_tokens=100, output_tokens=4096, cache_read_input_tokens=0
)
mock_create.return_value = mock_response
llm.call("Tell me a long story")
crewai_event_bus.flush(timeout=5.0)
assert len(captured_events) >= 1
event = captured_events[-1]
assert event.stop_reason == "max_tokens"
finally:
crewai_event_bus.off(LLMCallCompletedEvent, capture_event)
def test_anthropic_stop_reason_none_when_normal_completion():
"""Test that stop_reason is None in event when response completes normally."""
from crewai.events.types.llm_events import LLMCallCompletedEvent
from crewai.events import crewai_event_bus
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
captured_events = []
def capture_event(source, event):
if isinstance(event, LLMCallCompletedEvent):
captured_events.append(event)
crewai_event_bus.register_handler(LLMCallCompletedEvent, capture_event)
try:
with patch.object(llm.client.messages, "create") as mock_create:
mock_response = MagicMock()
mock_response.content = [MagicMock(text="complete response")]
mock_response.content[0].type = "text"
mock_response.stop_reason = "end_turn"
mock_response.usage = MagicMock(
input_tokens=50, output_tokens=25, cache_read_input_tokens=0
)
mock_create.return_value = mock_response
llm.call("Hello")
crewai_event_bus.flush(timeout=5.0)
assert len(captured_events) >= 1
event = captured_events[-1]
assert event.stop_reason == "end_turn"
finally:
crewai_event_bus.off(LLMCallCompletedEvent, capture_event)
def test_anthropic_tool_use_conversation_warns_on_truncation():
"""Test that _handle_tool_use_conversation warns when the follow-up response is truncated."""
from anthropic.types import ToolUseBlock, TextBlock
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
# Mock initial response with tool use
mock_initial_response = MagicMock()
mock_tool_block = MagicMock(spec=ToolUseBlock)
mock_tool_block.type = "tool_use"
mock_tool_block.id = "tool_123"
mock_tool_block.name = "get_data"
mock_tool_block.input = {"query": "test"}
mock_initial_response.content = [mock_tool_block]
# Mock final response with max_tokens truncation
mock_final_response = MagicMock()
mock_text_block = MagicMock(spec=TextBlock)
mock_text_block.text = "truncated tool result"
mock_text_block.type = "text"
mock_final_response.content = [mock_text_block]
mock_final_response.stop_reason = "max_tokens"
mock_final_response.usage = MagicMock(
input_tokens=200, output_tokens=4096, cache_read_input_tokens=0
)
with patch.object(llm.client.messages, "create", return_value=mock_final_response):
with patch("crewai.llms.providers.anthropic.completion.logging") as mock_logging:
def mock_tool_fn(**kwargs):
return "tool output"
result = llm._handle_tool_use_conversation(
initial_response=mock_initial_response,
tool_uses=[mock_tool_block],
params={"messages": [{"role": "user", "content": "test"}], "model": "claude-3-5-sonnet-20241022", "max_tokens": 4096},
available_functions={"get_data": mock_tool_fn},
)
assert result == "truncated tool result"
mock_logging.warning.assert_called_once()
warning_msg = mock_logging.warning.call_args[0][0]
assert "stop_reason='max_tokens'" in warning_msg
def test_llm_call_completed_event_has_stop_reason_field():
"""Test that LLMCallCompletedEvent has the stop_reason field with correct default."""
from crewai.events.types.llm_events import LLMCallCompletedEvent, LLMCallType
# Default stop_reason should be None
event = LLMCallCompletedEvent(
response="test",
call_type=LLMCallType.LLM_CALL,
call_id="test-id",
)
assert event.stop_reason is None
# stop_reason can be set
event_with_reason = LLMCallCompletedEvent(
response="test",
call_type=LLMCallType.LLM_CALL,
call_id="test-id",
stop_reason="max_tokens",
)
assert event_with_reason.stop_reason == "max_tokens"

View File

@@ -0,0 +1,210 @@
"""Tests for Python 3.14 compatibility.
Python 3.14 changed asyncio.get_event_loop() to raise RuntimeError when no
running event loop exists instead of creating one. All async code paths must
use asyncio.get_running_loop() instead.
See: https://github.com/crewAIInc/crewAI/issues/5109
"""
import asyncio
from contextlib import asynccontextmanager
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from crewai.tools.structured_tool import CrewStructuredTool
from crewai.utilities.streaming import create_streaming_state
class TestStructuredToolAsyncCompat:
"""Test that CrewStructuredTool.ainvoke uses get_running_loop correctly."""
@pytest.mark.asyncio
async def test_ainvoke_sync_func_uses_running_loop(self) -> None:
"""ainvoke() with a sync function must use the running event loop."""
def sync_func(x: int) -> int:
"""A sync function."""
return x * 2
tool = CrewStructuredTool.from_function(
func=sync_func, name="double", description="Doubles a number"
)
result = await tool.ainvoke({"x": 5})
assert result == 10
@pytest.mark.asyncio
async def test_ainvoke_async_func(self) -> None:
"""ainvoke() with an async function should call it directly."""
async def async_func(x: int) -> int:
"""An async function."""
return x * 3
tool = CrewStructuredTool.from_function(
func=async_func, name="triple", description="Triples a number"
)
result = await tool.ainvoke({"x": 4})
assert result == 12
@pytest.mark.asyncio
async def test_ainvoke_sync_func_runs_in_executor(self) -> None:
"""Verify ainvoke offloads sync functions to an executor via the running loop."""
import threading
call_thread_ids: list[int] = []
def sync_func(x: int) -> int:
"""A sync function that records its thread."""
call_thread_ids.append(threading.current_thread().ident or 0)
return x + 1
tool = CrewStructuredTool.from_function(
func=sync_func, name="inc", description="Increment"
)
result = await tool.ainvoke({"x": 1})
assert result == 2
assert len(call_thread_ids) == 1
# Sync func should run in a different thread (executor)
assert call_thread_ids[0] != threading.current_thread().ident
class TestStreamingStateAsyncCompat:
"""Test that create_streaming_state uses get_running_loop correctly."""
@pytest.mark.asyncio
async def test_create_streaming_state_async_uses_running_loop(self) -> None:
"""create_streaming_state(use_async=True) must use the running loop."""
task_info = {
"index": 0,
"name": "test",
"id": "test-id",
"agent_role": "tester",
"agent_id": "agent-id",
}
state = create_streaming_state(
current_task_info=task_info,
result_holder=[],
use_async=True,
)
assert state.loop is not None
assert state.async_queue is not None
assert state.loop is asyncio.get_running_loop()
def test_create_streaming_state_sync_no_loop_needed(self) -> None:
"""create_streaming_state(use_async=False) should not require a loop."""
task_info = {
"index": 0,
"name": "test",
"id": "test-id",
"agent_role": "tester",
"agent_id": "agent-id",
}
state = create_streaming_state(
current_task_info=task_info,
result_holder=[],
use_async=False,
)
assert state.loop is None
assert state.async_queue is None
@pytest.mark.asyncio
async def test_create_streaming_state_async_uses_get_running_loop_not_get_event_loop(
self,
) -> None:
"""Verify create_streaming_state does not call asyncio.get_event_loop()."""
task_info = {
"index": 0,
"name": "test",
"id": "test-id",
"agent_role": "tester",
"agent_id": "agent-id",
}
with patch("crewai.utilities.streaming.asyncio") as mock_asyncio:
mock_asyncio.Queue = asyncio.Queue
mock_asyncio.get_running_loop.return_value = asyncio.get_running_loop()
create_streaming_state(
current_task_info=task_info,
result_holder=[],
use_async=True,
)
mock_asyncio.get_running_loop.assert_called_once()
mock_asyncio.get_event_loop.assert_not_called()
class TestChromaDBClientAsyncCompat:
"""Test that ChromaDBClient._alocked uses get_running_loop correctly."""
@pytest.mark.asyncio
async def test_alocked_without_lock_name(self) -> None:
"""_alocked should yield immediately when no lock name is set."""
from crewai.rag.chromadb.client import ChromaDBClient
mock_client = MagicMock()
mock_ef = MagicMock()
client = ChromaDBClient(
client=mock_client,
embedding_function=mock_ef,
lock_name=None,
)
async with client._alocked():
pass # Should not raise
@pytest.mark.asyncio
async def test_alocked_uses_get_running_loop_not_get_event_loop(self) -> None:
"""Verify _alocked does not call asyncio.get_event_loop()."""
from crewai.rag.chromadb.client import ChromaDBClient
mock_client = MagicMock()
mock_ef = MagicMock()
client = ChromaDBClient(
client=mock_client,
embedding_function=mock_ef,
lock_name="test-lock",
)
with patch("crewai.rag.chromadb.client.asyncio") as mock_asyncio:
loop = asyncio.get_running_loop()
mock_asyncio.get_running_loop.return_value = loop
mock_cm = MagicMock()
with patch("crewai.rag.chromadb.client.store_lock", return_value=mock_cm):
async with client._alocked():
pass
mock_asyncio.get_running_loop.assert_called()
mock_asyncio.get_event_loop.assert_not_called()
class TestGetRunningLoopInAsyncContext:
"""General tests ensuring get_running_loop works in async contexts."""
@pytest.mark.asyncio
async def test_get_running_loop_available_in_async_context(self) -> None:
"""asyncio.get_running_loop() should work in an async context."""
loop = asyncio.get_running_loop()
assert loop is not None
assert loop.is_running()
@pytest.mark.asyncio
async def test_run_in_executor_with_running_loop(self) -> None:
"""run_in_executor should work with get_running_loop()."""
loop = asyncio.get_running_loop()
def sync_work() -> str:
return "done"
result = await loop.run_in_executor(None, sync_work)
assert result == "done"
def test_get_running_loop_raises_outside_async(self) -> None:
"""get_running_loop() should raise RuntimeError outside async context."""
with pytest.raises(RuntimeError):
asyncio.get_running_loop()

View File

@@ -271,7 +271,7 @@ async def test_mixed_sync_async_handler_execution():
timeout=5
)
await asyncio.get_event_loop().run_in_executor(None, wait_for_completion)
await asyncio.get_running_loop().run_in_executor(None, wait_for_completion)
assert len(sync_executed) == 5
assert len(async_executed) == 5

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.13.0rc1"
__version__ = "1.12.2"

View File

@@ -156,33 +156,6 @@ def update_version_in_file(file_path: Path, new_version: str) -> bool:
return False
def update_pyproject_version(file_path: Path, new_version: str) -> bool:
"""Update the [project] version field in a pyproject.toml file.
Args:
file_path: Path to pyproject.toml file.
new_version: New version string.
Returns:
True if version was updated, False otherwise.
"""
if not file_path.exists():
return False
content = file_path.read_text()
new_content = re.sub(
r'^(version\s*=\s*")[^"]+(")',
rf"\g<1>{new_version}\2",
content,
count=1,
flags=re.MULTILINE,
)
if new_content != content:
file_path.write_text(new_content)
return True
return False
_DEFAULT_WORKSPACE_PACKAGES: Final[list[str]] = [
"crewai",
"crewai-tools",
@@ -1072,84 +1045,10 @@ def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool:
return False
_DEPLOYMENT_TEST_REPO: Final[str] = "crewAIInc/crew_deployment_test"
_PYPI_POLL_INTERVAL: Final[int] = 15
_PYPI_POLL_TIMEOUT: Final[int] = 600
def _update_deployment_test_repo(version: str, is_prerelease: bool) -> None:
"""Update the deployment test repo to pin the new crewai version.
Clones the repo, updates the crewai[tools] pin in pyproject.toml,
regenerates the lockfile, commits, and pushes directly to main.
Args:
version: New crewai version string.
is_prerelease: Whether this is a pre-release version.
"""
console.print(
f"\n[bold cyan]Updating {_DEPLOYMENT_TEST_REPO} to {version}[/bold cyan]"
)
with tempfile.TemporaryDirectory() as tmp:
repo_dir = Path(tmp) / "crew_deployment_test"
run_command(["gh", "repo", "clone", _DEPLOYMENT_TEST_REPO, str(repo_dir)])
console.print(f"[green]✓[/green] Cloned {_DEPLOYMENT_TEST_REPO}")
pyproject = repo_dir / "pyproject.toml"
content = pyproject.read_text()
new_content = re.sub(
r'"crewai\[tools\]==[^"]+"',
f'"crewai[tools]=={version}"',
content,
)
if new_content == content:
console.print(
"[yellow]Warning:[/yellow] No crewai[tools] pin found to update"
)
return
pyproject.write_text(new_content)
console.print(f"[green]✓[/green] Updated crewai[tools] pin to {version}")
lock_cmd = [
"uv",
"lock",
"--refresh-package",
"crewai",
"--refresh-package",
"crewai-tools",
]
if is_prerelease:
lock_cmd.append("--prerelease=allow")
max_retries = 10
for attempt in range(1, max_retries + 1):
try:
run_command(lock_cmd, cwd=repo_dir)
break
except subprocess.CalledProcessError:
if attempt == max_retries:
console.print(
f"[red]Error:[/red] uv lock failed after {max_retries} attempts"
)
raise
console.print(
f"[yellow]uv lock failed (attempt {attempt}/{max_retries}),"
f" retrying in {_PYPI_POLL_INTERVAL}s...[/yellow]"
)
time.sleep(_PYPI_POLL_INTERVAL)
console.print("[green]✓[/green] Lockfile updated")
run_command(["git", "add", "pyproject.toml", "uv.lock"], cwd=repo_dir)
run_command(
["git", "commit", "-m", f"chore: bump crewai to {version}"],
cwd=repo_dir,
)
run_command(["git", "push"], cwd=repo_dir)
console.print(f"[green]✓[/green] Pushed to {_DEPLOYMENT_TEST_REPO}")
def _wait_for_pypi(package: str, version: str) -> None:
"""Poll PyPI until a specific package version is available.
@@ -1242,11 +1141,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
pyproject = pkg_dir / "pyproject.toml"
if pyproject.exists():
if update_pyproject_version(pyproject, version):
console.print(
f"[green]✓[/green] Updated version in: "
f"{pyproject.relative_to(repo_dir)}"
)
if update_pyproject_dependencies(
pyproject, version, extra_packages=list(_ENTERPRISE_EXTRA_PACKAGES)
):
@@ -1265,35 +1159,7 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
_wait_for_pypi("crewai", version)
console.print("\nSyncing workspace...")
sync_cmd = [
"uv",
"sync",
"--refresh-package",
"crewai",
"--refresh-package",
"crewai-tools",
"--refresh-package",
"crewai-files",
]
if is_prerelease:
sync_cmd.append("--prerelease=allow")
max_retries = 10
for attempt in range(1, max_retries + 1):
try:
run_command(sync_cmd, cwd=repo_dir)
break
except subprocess.CalledProcessError:
if attempt == max_retries:
console.print(
f"[red]Error:[/red] uv sync failed after {max_retries} attempts"
)
raise
console.print(
f"[yellow]uv sync failed (attempt {attempt}/{max_retries}),"
f" retrying in {_PYPI_POLL_INTERVAL}s...[/yellow]"
)
time.sleep(_PYPI_POLL_INTERVAL)
run_command(["uv", "sync"], cwd=repo_dir)
console.print("[green]✓[/green] Workspace synced")
# --- branch, commit, push, PR ---
@@ -1309,7 +1175,7 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
run_command(["git", "push", "-u", "origin", branch_name], cwd=repo_dir)
console.print("[green]✓[/green] Branch pushed")
pr_url = run_command(
run_command(
[
"gh",
"pr",
@@ -1326,7 +1192,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
cwd=repo_dir,
)
console.print("[green]✓[/green] Enterprise bump PR created")
console.print(f"[cyan]PR URL:[/cyan] {pr_url}")
_poll_pr_until_merged(branch_name, "enterprise bump PR", repo=enterprise_repo)
@@ -1693,18 +1558,7 @@ def tag(dry_run: bool, no_edit: bool) -> None:
is_flag=True,
help="Skip the enterprise release phase",
)
@click.option(
"--skip-to-enterprise",
is_flag=True,
help="Skip phases 1 & 2, run only the enterprise release phase",
)
def release(
version: str,
dry_run: bool,
no_edit: bool,
skip_enterprise: bool,
skip_to_enterprise: bool,
) -> None:
def release(version: str, dry_run: bool, no_edit: bool, skip_enterprise: bool) -> None:
"""Full release: bump versions, tag, and publish a GitHub release.
Combines bump and tag into a single workflow. Creates a version bump PR,
@@ -1717,19 +1571,11 @@ def release(
dry_run: Show what would be done without making changes.
no_edit: Skip editing release notes.
skip_enterprise: Skip the enterprise release phase.
skip_to_enterprise: Skip phases 1 & 2, run only the enterprise release phase.
"""
try:
check_gh_installed()
if skip_enterprise and skip_to_enterprise:
console.print(
"[red]Error:[/red] Cannot use both --skip-enterprise "
"and --skip-to-enterprise"
)
sys.exit(1)
if not skip_enterprise or skip_to_enterprise:
if not skip_enterprise:
missing: list[str] = []
if not _ENTERPRISE_REPO:
missing.append("ENTERPRISE_REPO")
@@ -1748,15 +1594,6 @@ def release(
cwd = Path.cwd()
lib_dir = cwd / "lib"
is_prerelease = _is_prerelease(version)
if skip_to_enterprise:
_release_enterprise(version, is_prerelease, dry_run)
console.print(
f"\n[green]✓[/green] Enterprise release [bold]{version}[/bold] complete!"
)
return
if not dry_run:
console.print("Checking git status...")
check_git_clean()
@@ -1850,8 +1687,7 @@ def release(
if not dry_run:
_create_tag_and_release(tag_name, release_notes, is_prerelease)
_trigger_pypi_publish(tag_name, wait=True)
_update_deployment_test_repo(version, is_prerelease)
_trigger_pypi_publish(tag_name, wait=not skip_enterprise)
if not skip_enterprise:
_release_enterprise(version, is_prerelease, dry_run)

View File

@@ -1,7 +1,7 @@
name = "crewai-workspace"
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
readme = "README.md"
requires-python = ">=3.10,<3.14"
requires-python = ">=3.10,<3.15"
authors = [
{ name = "Joao Moura", email = "joao@crewai.com" }
]

9113
uv.lock generated

File diff suppressed because it is too large Load Diff