Compare commits

..

1 Commits

Author SHA1 Message Date
iris-clawd
516e4fdfc3 feat: add OpenSandbox tool for sandbox-based code execution 2026-05-08 22:47:35 +00:00
122 changed files with 5220 additions and 8359 deletions

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: ${{ matrix.python-version }}

View File

@@ -22,10 +22,10 @@ jobs:
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: tibdex/github-app-token@v2
with:
app-id: ${{ secrets.CREWAI_TOOL_SPECS_APP_ID }}
private-key: ${{ secrets.CREWAI_TOOL_SPECS_PRIVATE_KEY }}
app_id: ${{ secrets.CREWAI_TOOL_SPECS_APP_ID }}
private_key: ${{ secrets.CREWAI_TOOL_SPECS_PRIVATE_KEY }}
- name: Checkout code
uses: actions/checkout@v4
@@ -34,7 +34,7 @@ jobs:
token: ${{ steps.app-token.outputs.token }}
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: "3.12"

View File

@@ -13,7 +13,7 @@ jobs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
@@ -41,7 +41,7 @@ jobs:
uv-main-py3.11-
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: "3.11"

View File

@@ -44,7 +44,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: "3.12"
@@ -103,7 +103,7 @@ jobs:
contents: read
steps:
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: "3.12"

View File

@@ -10,7 +10,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: codelytv/pr-size-labeler@095a41fca88b8764fd9e008ad269bcdb82bb38b9 # v1
- uses: codelytv/pr-size-labeler@v1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
xs_label: "size/XS"

View File

@@ -12,7 +12,7 @@ jobs:
pr-title:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5
- uses: amannn/action-semantic-pull-request@v5
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -34,7 +34,7 @@ jobs:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
uses: astral-sh/setup-uv@v4
- name: Build packages
run: |
@@ -63,7 +63,7 @@ jobs:
ref: ${{ inputs.release_tag || github.ref }}
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: "3.12"
@@ -159,7 +159,7 @@ jobs:
- name: Notify Slack
if: success()
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0
uses: slackapi/slack-github-action@v2.1.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook

View File

@@ -13,7 +13,7 @@ jobs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
@@ -51,7 +51,7 @@ jobs:
uv-main-py${{ matrix.python-version }}-
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: ${{ matrix.python-version }}

View File

@@ -13,7 +13,7 @@ jobs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
@@ -48,7 +48,7 @@ jobs:
uv-main-py${{ matrix.python-version }}-
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: ${{ matrix.python-version }}

View File

@@ -38,7 +38,7 @@ jobs:
uv-main-py${{ matrix.python-version }}-
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: ${{ matrix.python-version }}

View File

@@ -31,7 +31,7 @@ jobs:
uv-main-py3.11-
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@v6
with:
version: "0.11.3"
python-version: "3.11"
@@ -46,39 +46,9 @@ jobs:
- name: Run pip-audit
run: |
uv run pip-audit --desc --aliases --skip-editable --format json --output pip-audit-report.json \
--ignore-vuln PYSEC-2024-277 \
--ignore-vuln PYSEC-2026-89 \
--ignore-vuln PYSEC-2026-97 \
--ignore-vuln PYSEC-2025-148 \
--ignore-vuln PYSEC-2025-183 \
--ignore-vuln PYSEC-2025-189 \
--ignore-vuln PYSEC-2025-190 \
--ignore-vuln PYSEC-2025-191 \
--ignore-vuln PYSEC-2025-192 \
--ignore-vuln PYSEC-2025-193 \
--ignore-vuln PYSEC-2025-194 \
--ignore-vuln PYSEC-2025-195 \
--ignore-vuln PYSEC-2025-196 \
--ignore-vuln PYSEC-2025-197 \
--ignore-vuln PYSEC-2025-210 \
--ignore-vuln PYSEC-2026-139 \
--ignore-vuln PYSEC-2025-211 \
--ignore-vuln PYSEC-2025-212 \
--ignore-vuln PYSEC-2025-213 \
--ignore-vuln PYSEC-2025-214 \
--ignore-vuln PYSEC-2025-215 \
--ignore-vuln PYSEC-2025-216 \
--ignore-vuln PYSEC-2025-217 \
--ignore-vuln PYSEC-2025-218
--ignore-vuln CVE-2026-3219
# Ignored CVEs:
# PYSEC-2024-277 - joblib 1.5.3: disputed; NumpyArrayWrapper only used with trusted caches
# PYSEC-2026-89 - markdown 3.10.2: DoS via malformed HTML; fix 3.8.1 — already past, advisory range is stale
# PYSEC-2026-97 - nltk 3.9.4: arbitrary file read in filestring(); no fix available
# PYSEC-2025-148 - onnx 1.21.0: path traversal in save_external_data; no fix available
# PYSEC-2025-183 - pyjwt 2.12.1: disputed weak-encryption claim; key length is application-chosen
# PYSEC-2025-189..197 - torch 2.11.0: memory-corruption/DoS in functions only reachable via untrusted models; no fix available
# PYSEC-2025-210, PYSEC-2026-139 - torch 2.11.0: profiler/deserialization issues; no fix available
# PYSEC-2025-211..218 - transformers 5.5.4: deserialization/code injection via malicious model checkpoints; no fix available
# CVE-2026-3219 - pip 26.0.1 (GHSA-58qw-9mgm-455v): no fix available, archive handling issue
continue-on-error: true
- name: Display results

View File

@@ -4,140 +4,6 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
<Update label="21 مايو 2026">
## v1.14.6a1
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.6a1)
## ما الذي تغير
### الميزات
- إضافة مستودع المهارات مع التسجيل، التخزين المؤقت، واجهة سطر الأوامر، وتكامل SDK
- توليد ملاحظات إصدار مصنفة للمؤسسات
### إصلاحات الأخطاء
- تعزيز تسلسل حالة وقت التشغيل عبر حقول الكيان
- تحديث idna إلى 3.15 لمعالجة مشكلة الأمان GHSA-65pc-fj4g-8rjx
- إزالة تعبيرات JSX `{" "}` التي تعطل عرض `<Steps>`
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.14.5
## المساهمون
@akaKuruma, @alex-clawd, @greysonlalonde
</Update>
<Update label="19 مايو 2026">
## v1.14.5
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5)
## ما الذي تغير
### الميزات
- إلغاء استخدام `CrewAgentExecutor`، وتعيين وكلاء الطاقم الافتراضيين إلى `AgentExecutor`
- تحسين أدوات صندوق الرمل Daytona
- إضافة معلمة بدء `restore_from_state_id`
- إضافة تسليط الضوء على `ExaSearchTool`، وإعادة تسميته من `EXASearchTool`
### إصلاحات الأخطاء
- إصلاح تسرب الذاكرة في `git.py` باستخدام `cached_property`
- عرض استدعاءات الأدوات المتدفقة عندما تكون `available_functions` غائبة
- ضمان تحميل أحداث `skills` للتتبع
- تصحيح مسار نقطة النهاية للحالة من `/{kickoff_id}/status` إلى `/status/{kickoff_id}`
- استعادة كتلة الشيفرة المفقودة في دليل التدفق الأول للغة البرتغالية (pt-BR)
- منع `result_as_answer` من إرجاع رسائل الخطأ أو الكتل المرتبطة كإجابة نهائية
- الحفاظ على مخرجات المهام عبر تفريغ الدفعات غير المتزامنة
- دائمًا استعادة `task.output_pydantic` في كتلة finally
- التعامل مع إدخال `BaseModel` في `convert_to_model`
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.14.5
- إضافة دليل ترقية OSS و انتقال الطاقم إلى التدفق
- توثيق متغيرات البيئة الإضافية لأدوات المطور
- إضافة وثائق لـ `TavilyGetResearch`
### إعادة الهيكلة
- استخراج واجهة سطر الأوامر إلى حزمة مستقلة `crewai-cli`
## المساهمون
@NIK-TIGER-BILL, @akaKuruma, @cgoeppinger, @github-actions[bot], @greysonlalonde, @heitorado, @irfaan101, @iris-clawd, @lorenzejay, @manisrinivasan2k1, @minasami-pr, @mislavivanda, @theCyberTech, @theishangoswami, @wishhyt
</Update>
<Update label="18 مايو 2026">
## v1.14.5a7
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a7)
## ما الذي تغير
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.14.5a6
### تغييرات كسرية
- إلغاء حقل function_calling_llm
## المساهمون
@greysonlalonde, @heitorado
</Update>
<Update label="15 مايو 2026">
## v1.14.5a6
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a6)
## ما الذي تغير
### إصلاحات الأخطاء
- إصلاح استدعاءات الأدوات المتدفقة عندما تكون available_functions غائبة
- رفع اعتماد langsmith إلى الإصدار >=0.8.0 لمعالجة GHSA-3644-q5cj-c5c7
- حل مشاكل الأماكن الشاغرة لكتل التعليمات البرمجية غير المترجمة في وثائق البرتغالية البرازيلية
### الوثائق
- إضافة وثائق لـ TavilyGetResearch
- تحديث سجل التغييرات والإصدار لـ v1.14.5a5
## المساهمون
@greysonlalonde, @heitorado, @iris-clawd, @lorenzejay, @manisrinivasan2k1
</Update>
<Update label="13 مايو 2026">
## v1.14.5a5
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a5)
## ما الذي تغير
### الميزات
- إلغاء استخدام CrewAgentExecutor، وتعيين وكلاء Crew الافتراضيين إلى AgentExecutor
- تحسين أدوات صندوق الرمل Daytona
### إصلاحات الأخطاء
- إصلاح كتلة الكود المفقودة في دليل التدفق الأول باللغة البرتغالية (pt-BR)
- تسجيل أخطاء المراجعة المسبقة والتقطير HITL، إضافة learn_strict
- تصحيح urllib3 للثغرات الأمنية
- تصحيح gitpython و langchain-core؛ تجاهل CVE paramiko غير المصححة
- تحديث جميع حزم مساحة العمل المنشورة على uv lock/sync
### الوثائق
- إضافة دليل ترحيل لـ `inputs.id` إلى `restoreFromStateId`
- إضافة دليل ترقية OSS ودليل ترحيل crew-to-flow
- تحديث سجل التغييرات والإصدار لـ v1.14.5a4
## المساهمون
@akaKuruma, @greysonlalonde, @iris-clawd, @lorenzejay, @mislavivanda
</Update>
<Update label="9 مايو 2026">
## v1.14.5a4

View File

@@ -29,7 +29,6 @@ from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"

View File

@@ -146,6 +146,7 @@ Crew Studio هو طريقة مبتكرة لإنشاء طواقم وكلاء ال
</Step>
{" "}
<Step title="الإجابة على الأسئلة">
أجب على أسئلة التوضيح من مساعد الطاقم لتنقيح
متطلباتك.
@@ -160,10 +161,12 @@ Crew Studio هو طريقة مبتكرة لإنشاء طواقم وكلاء ال
</Step>
{" "}
<Step title="الموافقة أو التعديل">
وافق على الخطة أو اطلب تغييرات إذا لزم الأمر.
</Step>
{" "}
<Step title="التنزيل أو النشر">
نزّل الكود للتخصيص أو انشر مباشرة على المنصة.
</Step>

View File

@@ -1,102 +0,0 @@
---
title: "الانتقال من inputs.id إلى restore_from_state_id"
description: "نقل تدفقات @persist من ترطيب inputs.id المهجور إلى حقل restore_from_state_id المدعوم"
icon: "arrow-right-arrow-left"
---
<Warning>
تمرير `id` داخل `inputs` لترطيب تدفق `@persist` هو **مهجور** ومقرر إزالته في إصدار مستقبلي. البديل، `restore_from_state_id`، متاح في CrewAI **v1.14.5 وما بعده** — الخطوات أدناه تنطبق بمجرد أن تقوم بالتحديث.
</Warning>
## نظرة عامة
الطريقة الموثقة لترطيب تدفق `@persist` من تنفيذ سابق هي تمرير UUID لذلك التنفيذ كـ `inputs.id`. الآن، تكشف CrewAI عن حقل مخصص، `restore_from_state_id`، الذي يقوم بنفس الترطيب دون تحميل حمولة `inputs` — ودون ربط مفتاح الترطيب بهوية التنفيذ الجديد.
## الانتقال
إذا كنت حالياً تبدأ تدفق `@persist` باستخدام `inputs={"id": ...}`:
```python
# مهجور
flow = CounterFlow()
flow.kickoff(inputs={"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv"})
```
انتقل إلى `restore_from_state_id`:
```python
# مدعوم
flow = CounterFlow()
flow.kickoff(restore_from_state_id="abcd1234-5678-90ef-ghij-klmnopqrstuv")
```
تتمتع الوضعيتان بمعاني سلالة مختلفة:
- `inputs={"id": <uuid>}` (مهجور) — **استئناف**: تكتب الكتابات تحت المعرف المقدم، مما يمدد نفس تاريخ `flow_uuid`.
- `restore_from_state_id=<uuid>` — **تفرع**: يترطب الحالة من اللقطة، ثم يكتب تحت `state.id` جديدة. يتم الحفاظ على تاريخ التدفق المصدر.
لأغلب سيناريوهات الإنتاج — إعادة تشغيل تدفق تم تهيئته من حالة سابقة — فإن التفرع هو ما تريده. راجع [إتقان حالة التدفق](/ar/guides/flows/mastering-flow-state) للحصول على النموذج الذهني الكامل.
إذا كنت تبدأ تدفقك عبر واجهة برمجة تطبيقات CrewAI AMP REST، راجع [AMP](#amp) أدناه لهجرة الحمولة المعادلة.
## لماذا نقوم بإهمال `inputs.id` لـ `@persist`
`inputs.id` هو حالياً الطريقة الموثقة لاستئناف تدفق `@persist` من تنفيذ سابق. المشكلة هي أن نفس UUID يقوم بوظيفتين في وقت واحد:
1. **يحدد أي لقطة يترطب منها `@persist`** — تحميل الحالة المحفوظة تحت ذلك UUID.
2. **يصبح معرف تنفيذ التدفق الجديد** (`state.id` في SDK؛ يظهر كـ `flow_id` في بعض السياقات) — كل كتابة `@persist` من هذه البداية أيضاً تقع تحت نفس UUID.
هذه الوظيفة المزدوجة هي السبب الجذري للمشاكل التي يصفها هذا الدليل. لأن UUID المقدم هو أيضاً معرف التنفيذ الجديد، فإن بدايتين تمرران نفس `inputs.id` ليست تنفيذين متميزين — إنهما تشتركان في معرف، وتشاركان في سجل الاستمرارية، و(على AMP) تشتركان في صف في قائمة التنفيذات. لا توجد طريقة للقول "ترطب من هذه اللقطة، ولكن سجل هذا التشغيل بشكل منفصل" دون تقسيم المسؤوليتين.
`restore_from_state_id` هو هذا الانقسام. إنه يخبر `@persist` من أي لقطة يترطب، بينما يترك التنفيذ الجديد حراً لاستلام `state.id` جديدة. لم يعد مصدر الترطيب والتشغيل المسجل نفس UUID — وهو ما تريده معظم سيناريوهات الإنتاج فعلياً.
## جدول إزالة
من المقرر إزالة `inputs.id` لترطيب `@persist` في إصدار مستقبلي من CrewAI. لا يوجد قطع صارم فوري — تظل التدفقات الحالية تعمل — ولكن بمجرد أن تقوم بالتحديث إلى v1.14.5 أو ما بعده، يجب أن يستخدم الكود الجديد `restore_from_state_id`، ويجب أن تهاجر التدفقات الحالية في الفرصة المناسبة التالية.
## AMP
إذا كنت تنشر تدفقك إلى CrewAI AMP، فإن الهجرة تمتد إلى الحمولة التي تبدأ بها المرسلة إلى طاقمك المنشور، وتظهر الأعراض المرئية لإعادة استخدام `inputs.id` على لوحة معلومات النشر. تغطي القسمان الفرعيان أدناه كلاهما.
### هجرة حمولة البداية
إذا كنت حالياً تبدأ تدفقاً منشوراً عن طريق تضمين `id` في `inputs`:
```bash
# مهجور
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{"inputs": {"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv", "topic": "AI Agent Frameworks"}}' \
https://your-crew-url.crewai.com/kickoff
```
نقل UUID إلى حقل `restoreFromStateId` في المستوى الأعلى:
```bash
# مدعوم
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{
"inputs": {"topic": "AI Agent Frameworks"},
"restoreFromStateId": "abcd1234-5678-90ef-ghij-klmnopqrstuv"
}' \
https://your-crew-url.crewai.com/kickoff
```
يجلس `restoreFromStateId` بجانب `inputs` في حمولة البداية، وليس داخلها. الآن، يحمل كائن `inputs` فقط القيم التي تستهلكها تدفقك فعلياً.
### ماذا يحدث عند إعادة استخدام `inputs.id`
عندما تتلقى AMP بداية لتدفق يتطابق `inputs.id` الخاص به مع تنفيذ موجود، فإنه يحل إلى السجل الموجود بدلاً من إنشاء سجل جديد. من لوحة معلومات النشر سترى:
- **حالة التنفيذ** — حالة التشغيل الجديد تحل محل حالة التشغيل السابق. يمكن أن تعود تنفيذات مكتملة إلى `جارية`، أو يمكن أن تتحول تشغيلات `مكتملة` إلى `خطأ` إذا فشلت البداية الجديدة — في كلتا الحالتين، لم تعد لوحة المعلومات تعكس التشغيل الأصلي.
- **التتبع** — تتراكم تتبعات OTel عبر البدايات لأنها تشترك في نفس معرف التنفيذ؛ تتبعات التشغيل السابق إما تُستبدل بـ، أو تُخلط مع، تشغيل الجديد. لم يعد إعادة التشغيل خطوة بخطوة يتوافق مع تنفيذ واحد.
- **قائمة التنفيذات** — البدايات التي يجب أن تظهر كصفوف منفصلة تتقلص إلى إدخال واحد، مما يخفي التاريخ.
تساعد الهجرة إلى `restoreFromStateId` في الحفاظ على كل بداية كتنفيذ خاص بها — مع حالتها الخاصة، وتتبعها، وصفها في القائمة — بينما لا تزال ترطب الحالة من تشغيل سابق.
<Card title="هل تحتاج مساعدة؟" icon="headset" href="mailto:support@crewai.com">
اتصل بفريق الدعم لدينا إذا لم تكن متأكداً من أي وضع يحتاجه تدفقك أو واجهت مشاكل أثناء الهجرة.
</Card>

View File

@@ -802,6 +802,7 @@ The tables below show a representative sample of current top-performing models a
Begin with well-established models like **GPT-4.1**, **Claude 3.7 Sonnet**, or **Gemini 2.0 Flash** that offer good performance across multiple dimensions and have extensive real-world validation.
</Step>
{" "}
<Step title="Identify Specialized Needs">
Determine if your crew has specific requirements (coding, reasoning, speed)
that would benefit from specialized models like **Claude 4 Sonnet** for
@@ -809,6 +810,7 @@ The tables below show a representative sample of current top-performing models a
consider fast inference providers like **Groq** alongside model selection.
</Step>
{" "}
<Step title="Implement Multi-Model Strategy">
Use different models for different agents based on their roles.
High-capability models for managers and complex tasks, efficient models for

File diff suppressed because it is too large Load Diff

View File

@@ -4,140 +4,6 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="May 21, 2026">
## v1.14.6a1
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.6a1)
## What's Changed
### Features
- Add Skills Repository with registry, cache, CLI, and SDK integration
- Generate categorized release notes for enterprise
### Bug Fixes
- Harden RuntimeState serialization across entity fields
- Bump idna to 3.15 to address security issue GHSA-65pc-fj4g-8rjx
- Remove `{" "}` JSX expressions breaking `<Steps>` render
### Documentation
- Update changelog and version for v1.14.5
## Contributors
@akaKuruma, @alex-clawd, @greysonlalonde
</Update>
<Update label="May 19, 2026">
## v1.14.5
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5)
## What's Changed
### Features
- Deprecate `CrewAgentExecutor`, default Crew agents to `AgentExecutor`
- Improve Daytona sandbox tools
- Add `restore_from_state_id` kickoff parameter
- Add highlights to `ExaSearchTool`, rename from `EXASearchTool`
### Bug Fixes
- Fix memory leak in `git.py` by using `cached_property`
- Surface streamed tool calls when `available_functions` is absent
- Ensure `skills` loading events for traces
- Correct status endpoint path from `/{kickoff_id}/status` to `/status/{kickoff_id}`
- Restore missing code block in pt-BR first-flow guide
- Prevent `result_as_answer` from returning hook-block or error messages as final answer
- Preserve task outputs across async batch flush
- Always restore `task.output_pydantic` in finally block
- Handle `BaseModel` input in `convert_to_model`
### Documentation
- Update changelog and version for v1.14.5
- Add OSS upgrade & crew-to-flow migration guide
- Document additional env vars for devtools
- Add docs for `TavilyGetResearch`
### Refactoring
- Extract CLI into standalone `crewai-cli` package
## Contributors
@NIK-TIGER-BILL, @akaKuruma, @cgoeppinger, @github-actions[bot], @greysonlalonde, @heitorado, @irfaan101, @iris-clawd, @lorenzejay, @manisrinivasan2k1, @minasami-pr, @mislavivanda, @theCyberTech, @theishangoswami, @wishhyt
</Update>
<Update label="May 18, 2026">
## v1.14.5a7
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a7)
## What's Changed
### Documentation
- Update changelog and version for v1.14.5a6
### Breaking Changes
- Deprecate function_calling_llm field
## Contributors
@greysonlalonde, @heitorado
</Update>
<Update label="May 15, 2026">
## v1.14.5a6
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a6)
## What's Changed
### Bug Fixes
- Fix streamed tool calls when available_functions is absent
- Bump langsmith dependency to version >=0.8.0 to address GHSA-3644-q5cj-c5c7
- Resolve untranslated code block placeholders in Brazilian Portuguese documentation
### Documentation
- Add documentation for TavilyGetResearch
- Update changelog and version for v1.14.5a5
## Contributors
@greysonlalonde, @heitorado, @iris-clawd, @lorenzejay, @manisrinivasan2k1
</Update>
<Update label="May 13, 2026">
## v1.14.5a5
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a5)
## What's Changed
### Features
- Deprecate CrewAgentExecutor, default Crew agents to AgentExecutor
- Improve Daytona sandbox tools
### Bug Fixes
- Fix missing code block in pt-BR first-flow guide
- Log HITL pre-review and distillation failures, add learn_strict
- Patch urllib3 for security vulnerabilities
- Patch gitpython and langchain-core; ignore unpatched paramiko CVE
- Refresh all published workspace packages on uv lock/sync
### Documentation
- Add migration guide for `inputs.id` to `restoreFromStateId`
- Add OSS upgrade and crew-to-flow migration guide
- Update changelog and version for v1.14.5a4
## Contributors
@akaKuruma, @greysonlalonde, @iris-clawd, @lorenzejay, @mislavivanda
</Update>
<Update label="May 09, 2026">
## v1.14.5a4

View File

@@ -29,7 +29,6 @@ from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"

View File

@@ -146,6 +146,7 @@ Here's a typical workflow for creating a crew with Crew Studio:
</Step>
{" "}
<Step title="Answer Questions">
Respond to clarifying questions from the Crew Assistant to refine your
requirements.
@@ -160,10 +161,12 @@ Here's a typical workflow for creating a crew with Crew Studio:
</Step>
{" "}
<Step title="Approve or Modify">
Approve the plan or request changes if necessary.
</Step>
{" "}
<Step title="Download or Deploy">
Download the code for customization or deploy directly to the platform.
</Step>

View File

@@ -1,143 +0,0 @@
---
title: "Migrating from inputs.id to restore_from_state_id"
description: "Move @persist flows off the deprecated inputs.id hydration onto the supported restore_from_state_id field"
icon: "arrow-right-arrow-left"
---
<Warning>
Passing `id` inside `inputs` to hydrate a `@persist` flow is **deprecated** and
scheduled for removal in a future release. The replacement, `restore_from_state_id`,
is available in CrewAI **v1.14.5 and later** — the steps below apply once you
upgrade.
</Warning>
## Overview
The documented way to hydrate a `@persist` flow from a previous execution is to pass
that execution's UUID as `inputs.id`. CrewAI now exposes a dedicated field,
`restore_from_state_id`, that performs the same hydration without overloading the
`inputs` payload — and without coupling the hydration key to the new execution's
identity.
## Migration
If you currently kickoff a `@persist` flow with `inputs={"id": ...}`:
```python
# Deprecated
flow = CounterFlow()
flow.kickoff(inputs={"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv"})
```
Switch to `restore_from_state_id`:
```python
# Supported
flow = CounterFlow()
flow.kickoff(restore_from_state_id="abcd1234-5678-90ef-ghij-klmnopqrstuv")
```
The two modes have different lineage semantics:
- `inputs={"id": <uuid>}` (deprecated) — **resume**: writes land under the supplied
id, extending the same `flow_uuid` history.
- `restore_from_state_id=<uuid>` — **fork**: hydrates state from the snapshot, then
writes under a fresh `state.id`. The source flow's history is preserved.
For most production scenarios — re-running a flow seeded from a previous state — fork
is what you want. See [Mastering Flow State](/en/guides/flows/mastering-flow-state)
for the full mental model.
If you kickoff your flow over the CrewAI AMP REST API, see [AMP](#amp) below for the
equivalent payload migration.
## Why we are deprecating `inputs.id` for `@persist`
`inputs.id` is currently the documented way to resume a `@persist` flow from a
previous execution. The problem is that the same UUID does two jobs at once:
1. **It selects which snapshot `@persist` hydrates from** — load the state saved
under that UUID.
2. **It becomes the new execution's Flow Execution ID** (`state.id` in the SDK;
surfaced as `flow_id` in some contexts) — every `@persist` write from this
kickoff also lands under that same UUID.
This dual role is the root cause of the issues this guide describes. Because the
supplied UUID is also the new execution's id, two kickoffs that pass the same
`inputs.id` are not two distinct executions — they share an id, share a persistence
record, and (on AMP) share a row in the executions list. There is no way to say
"hydrate from this snapshot, but record this run separately" without splitting the
two responsibilities.
`restore_from_state_id` is that split. It tells `@persist` which snapshot to hydrate
from, while leaving the new execution free to receive a fresh `state.id`. The
hydration source and the recorded run are no longer the same UUID — which is what
most production scenarios actually want.
## Removal timeline
`inputs.id` for `@persist` hydration is scheduled for removal in a future release of
CrewAI. There is no immediate hard cut-off — existing flows continue to work — but
once you upgrade to v1.14.5 or later, new code should use `restore_from_state_id`, and
existing flows should migrate at the next convenient opportunity.
## AMP
If you deploy your flow to CrewAI AMP, the migration extends to the kickoff payload
sent to your deployed crew, and the visible symptoms of reusing `inputs.id` show up
on the deployment dashboard. The two subsections below cover both.
### Migrating the kickoff payload
If you currently kickoff a deployed flow by embedding `id` in `inputs`:
```bash
# Deprecated
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{"inputs": {"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv", "topic": "AI Agent Frameworks"}}' \
https://your-crew-url.crewai.com/kickoff
```
Move the UUID to the top-level `restoreFromStateId` field:
```bash
# Supported
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{
"inputs": {"topic": "AI Agent Frameworks"},
"restoreFromStateId": "abcd1234-5678-90ef-ghij-klmnopqrstuv"
}' \
https://your-crew-url.crewai.com/kickoff
```
`restoreFromStateId` sits next to `inputs` in the kickoff payload, not inside it. The
`inputs` object now only carries values your flow actually consumes.
### What happens when `inputs.id` is reused
When AMP receives a kickoff for a flow whose `inputs.id` matches an existing
execution, it resolves to the existing record rather than creating a new one. From
the deployment dashboard you'll see:
- **Execution status** — the new run's status overwrites the previous run's. A
finished execution can flip back to `running`, or a `completed` run can flip to
`error` if the new kickoff fails — either way the dashboard no longer reflects
the original run.
- **Traces** — OTel traces stack across kickoffs because they share the same
execution id; the previous run's traces are either replaced by, or mixed with,
the new run's. A step-by-step replay no longer corresponds to a single execution.
- **Executions list** — kickoffs that should appear as separate rows collapse into
a single entry, hiding history.
Migrating to `restoreFromStateId` keeps every kickoff as its own execution — with
its own status, traces, and row in the list — while still hydrating state from a
previous run.
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
Contact our support team if you're unsure which mode your flow needs or hit issues
during the migration.
</Card>

View File

@@ -313,9 +313,9 @@ flow1 = PersistentCounterFlow()
result1 = flow1.kickoff()
print(f"First run result: {result1}")
# Second run - pass the ID to load the persisted state
# Second run - state is automatically loaded
flow2 = PersistentCounterFlow()
result2 = flow2.kickoff(inputs={"id": flow1.state.id})
result2 = flow2.kickoff()
print(f"Second run result: {result2}") # Will be higher due to persisted state
```

View File

@@ -805,6 +805,7 @@ The tables below show a representative sample of current top-performing models a
Begin with well-established models like **GPT-4.1**, **Claude 3.7 Sonnet**, or **Gemini 2.0 Flash** that offer good performance across multiple dimensions and have extensive real-world validation.
</Step>
{" "}
<Step title="Identify Specialized Needs">
Determine if your crew has specific requirements (coding, reasoning, speed)
that would benefit from specialized models like **Claude 4 Sonnet** for
@@ -812,6 +813,7 @@ The tables below show a representative sample of current top-performing models a
consider fast inference providers like **Groq** alongside model selection.
</Step>
{" "}
<Step title="Implement Multi-Model Strategy">
Use different models for different agents based on their roles.
High-capability models for managers and complex tasks, efficient models for

View File

@@ -54,14 +54,6 @@ These tools enable your agents to search the web, research topics, and find info
Extract structured content from web pages using the Tavily API.
</Card>
<Card title="Tavily Research Tool" icon="flask" href="/en/tools/search-research/tavilyresearchtool">
Run multi-step research tasks and get cited reports using the Tavily Research API.
</Card>
<Card title="Tavily Get Research Tool" icon="clipboard-list" href="/en/tools/search-research/tavilygetresearchtool">
Retrieve the status and results of an existing Tavily research task.
</Card>
<Card title="Arxiv Paper Tool" icon="box-archive" href="/en/tools/search-research/arxivpapertool">
Search arXiv and optionally download PDFs.
</Card>
@@ -84,15 +76,7 @@ These tools enable your agents to search the web, research topics, and find info
- **Academic Research**: Find scholarly articles and technical papers
```python
from crewai_tools import (
GitHubSearchTool,
SerperDevTool,
TavilyExtractorTool,
TavilyGetResearchTool,
TavilyResearchTool,
TavilySearchTool,
YoutubeVideoSearchTool,
)
from crewai_tools import SerperDevTool, GitHubSearchTool, YoutubeVideoSearchTool, TavilySearchTool, TavilyExtractorTool
# Create research tools
web_search = SerperDevTool()
@@ -100,21 +84,11 @@ code_search = GitHubSearchTool()
video_research = YoutubeVideoSearchTool()
tavily_search = TavilySearchTool()
content_extractor = TavilyExtractorTool()
tavily_research = TavilyResearchTool()
tavily_get_research = TavilyGetResearchTool()
# Add to your agent
agent = Agent(
role="Research Analyst",
tools=[
web_search,
code_search,
video_research,
tavily_search,
content_extractor,
tavily_research,
tavily_get_research,
],
tools=[web_search, code_search, video_research, tavily_search, content_extractor],
goal="Gather comprehensive information on any topic"
)
```

View File

@@ -1,85 +0,0 @@
---
title: "Tavily Get Research Tool"
description: "Retrieve the status and results of an existing Tavily research task"
icon: "clipboard-list"
mode: "wide"
---
The `TavilyGetResearchTool` lets CrewAI agents check an existing Tavily research task by `request_id`. Use it when a research task was started earlier and you need to retrieve its current status or final results.
If you need to start a new research job, use the [Tavily Research Tool](/en/tools/search-research/tavilyresearchtool). This tool is specifically for looking up an existing Tavily research request after you already have its `request_id`.
## Installation
To use the `TavilyGetResearchTool`, install the `tavily-python` library alongside `crewai-tools`:
```shell
uv add 'crewai[tools]' tavily-python
```
## Environment Variables
Set your Tavily API key:
```bash
export TAVILY_API_KEY='your_tavily_api_key'
```
Get an API key at [https://app.tavily.com/](https://app.tavily.com/) (sign up, then create a key).
## Example Usage
```python
from crewai_tools import TavilyGetResearchTool
tavily_get_research_tool = TavilyGetResearchTool()
status_result = tavily_get_research_tool.run(
request_id="your-research-request-id"
)
print(status_result)
```
## Common Workflow
Use `TavilyGetResearchTool` when your application or another service has already created a Tavily research task and saved its `request_id`.
Typical cases include:
- Polling for completion after kicking off research in a background job.
- Looking up the latest status of a long-running research task.
- Fetching final research output from a previously created Tavily request.
## Configuration Options
The `TavilyGetResearchTool` accepts the following argument when calling the `run` method:
- `request_id` (str): **Required.** The existing Tavily research request ID to retrieve.
## Async Usage
Use `_arun` when your application is already running inside an async event loop:
```python
from crewai_tools import TavilyGetResearchTool
tavily_get_research_tool = TavilyGetResearchTool()
status_result = await tavily_get_research_tool._arun(
request_id="your-research-request-id"
)
```
## Features
- **Research status retrieval**: Fetch the current status of an existing Tavily research task.
- **Result retrieval**: Return available research output once Tavily has completed the task.
- **Sync and async**: Use either `_run`/`run` or `_arun` depending on your application's runtime.
- **JSON output**: Returns Tavily responses as formatted JSON strings.
## Response Format
The tool returns a JSON string containing the current research task status and any available results from Tavily. The exact response shape depends on the task state returned by Tavily, so incomplete tasks may return status information before the final research output is available.
Refer to the [Tavily API documentation](https://docs.tavily.com/) for full details on the Research API.

View File

@@ -4,140 +4,6 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
<Update label="2026년 5월 21일">
## v1.14.6a1
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.6a1)
## 변경 사항
### 기능
- 레지스트리, 캐시, CLI 및 SDK 통합이 포함된 기술 저장소 추가
- 기업용으로 분류된 릴리스 노트 생성
### 버그 수정
- 엔티티 필드 전반에 걸쳐 RuntimeState 직렬화 강화
- 보안 문제 GHSA-65pc-fj4g-8rjx를 해결하기 위해 idna를 3.15로 업데이트
- `<Steps>` 렌더링을 방해하는 `{" "}` JSX 표현식 제거
### 문서
- v1.14.5에 대한 변경 로그 및 버전 업데이트
## 기여자
@akaKuruma, @alex-clawd, @greysonlalonde
</Update>
<Update label="2026년 5월 19일">
## v1.14.5
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5)
## 변경 사항
### 기능
- `CrewAgentExecutor` 사용 중단, 기본 Crew 에이전트를 `AgentExecutor`로 설정
- Daytona 샌드박스 도구 개선
- `restore_from_state_id` 시작 매개변수 추가
- `ExaSearchTool`에 하이라이트 추가, 이름을 `EXASearchTool`에서 변경
### 버그 수정
- `git.py`에서 `cached_property`를 사용하여 메모리 누수 수정
- `available_functions`가 없을 때 스트리밍 도구 호출 표시
- 추적을 위한 `skills` 로딩 이벤트 보장
- 상태 엔드포인트 경로를 `/{kickoff_id}/status`에서 `/status/{kickoff_id}`로 수정
- pt-BR 첫 흐름 가이드에서 누락된 코드 블록 복원
- `result_as_answer`가 후크 블록이나 오류 메시지를 최종 답변으로 반환하지 않도록 방지
- 비동기 배치 플러시 간 작업 출력 보존
- 항상 finally 블록에서 `task.output_pydantic` 복원
- `convert_to_model`에서 `BaseModel` 입력 처리
### 문서화
- v1.14.5에 대한 변경 로그 및 버전 업데이트
- OSS 업그레이드 및 Crew-투-흐름 마이그레이션 가이드 추가
- 개발 도구를 위한 추가 환경 변수 문서화
- `TavilyGetResearch`에 대한 문서 추가
### 리팩토링
- CLI를 독립형 `crewai-cli` 패키지로 추출
## 기여자
@NIK-TIGER-BILL, @akaKuruma, @cgoeppinger, @github-actions[bot], @greysonlalonde, @heitorado, @irfaan101, @iris-clawd, @lorenzejay, @manisrinivasan2k1, @minasami-pr, @mislavivanda, @theCyberTech, @theishangoswami, @wishhyt
</Update>
<Update label="2026년 5월 18일">
## v1.14.5a7
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a7)
## 변경 사항
### 문서
- v1.14.5a6의 변경 로그 및 버전 업데이트
### 주요 변경 사항
- function_calling_llm 필드 사용 중단
## 기여자
@greysonlalonde, @heitorado
</Update>
<Update label="2026년 5월 15일">
## v1.14.5a6
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a6)
## 변경 사항
### 버그 수정
- available_functions가 없을 때 스트리밍 도구 호출 수정
- GHSA-3644-q5cj-c5c7 문제를 해결하기 위해 langsmith 의존성을 버전 >=0.8.0으로 업데이트
- 브라질 포르투갈어 문서에서 번역되지 않은 코드 블록 자리 표시자 해결
### 문서
- TavilyGetResearch에 대한 문서 추가
- v1.14.5a5에 대한 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde, @heitorado, @iris-clawd, @lorenzejay, @manisrinivasan2k1
</Update>
<Update label="2026년 5월 13일">
## v1.14.5a5
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a5)
## 변경 사항
### 기능
- CrewAgentExecutor 사용 중단, 기본 Crew 에이전트를 AgentExecutor로 설정
- Daytona 샌드박스 도구 개선
### 버그 수정
- pt-BR 첫 번째 흐름 가이드에서 누락된 코드 블록 수정
- HITL 사전 검토 및 증류 실패 로그 기록, learn_strict 추가
- 보안 취약점을 위한 urllib3 패치
- gitpython 및 langchain-core 패치; 패치되지 않은 paramiko CVE 무시
- uv 잠금/동기화 시 모든 게시된 작업공간 패키지 새로 고침
### 문서
- `inputs.id`에서 `restoreFromStateId`로의 마이그레이션 가이드 추가
- OSS 업그레이드 및 crew-to-flow 마이그레이션 가이드 추가
- v1.14.5a4의 변경 로그 및 버전 업데이트
## 기여자
@akaKuruma, @greysonlalonde, @iris-clawd, @lorenzejay, @mislavivanda
</Update>
<Update label="2026년 5월 9일">
## v1.14.5a4

View File

@@ -29,7 +29,6 @@ from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"

View File

@@ -145,6 +145,7 @@ LLM 연결과 기본 설정을 구성했다면 이제 Crew Studio 사용을 시
</Step>
{" "}
<Step title="질문에 답하기">
crew assistant가 요구 사항을 구체화할 수 있도록 하는 추가 질문에 답변하세요.
</Step>
@@ -158,10 +159,12 @@ LLM 연결과 기본 설정을 구성했다면 이제 Crew Studio 사용을 시
</Step>
{" "}
<Step title="승인 또는 수정">
계획을 승인하거나 필요하다면 변경을 요청하세요.
</Step>
{" "}
<Step title="다운로드 또는 배포">
사용자화를 위해 코드를 다운로드하거나 플랫폼에 직접 배포하세요.
</Step>

View File

@@ -1,125 +0,0 @@
---
title: "inputs.id에서 restore_from_state_id로 마이그레이션"
description: "더 이상 지원되지 않는 inputs.id 하이드레이션에서 지원되는 restore_from_state_id 필드로 @persist 흐름을 이동"
icon: "arrow-right-arrow-left"
---
<Warning>
`inputs` 내에서 `id`를 전달하여 `@persist` 흐름을 하이드레이트하는 것은 **더 이상 지원되지 않으며**
향후 릴리스에서 제거될 예정입니다. 대체품인 `restore_from_state_id`는 CrewAI **v1.14.5 이상**에서 사용할 수 있으며,
아래 단계는 업그레이드 후 적용됩니다.
</Warning>
## 개요
이전 실행에서 `@persist` 흐름을 하이드레이트하는 문서화된 방법은
해당 실행의 UUID를 `inputs.id`로 전달하는 것입니다. CrewAI는 이제
`inputs` 페이로드를 과부하하지 않고 동일한 하이드레이션을 수행하는 전용 필드인
`restore_from_state_id`를 제공합니다 — 그리고 하이드레이션 키를 새로운 실행의
정체성과 결합하지 않습니다.
## 마이그레이션
현재 `inputs={"id": ...}`로 `@persist` 흐름을 시작하는 경우:
```python
# 더 이상 지원되지 않음
flow = CounterFlow()
flow.kickoff(inputs={"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv"})
```
`restore_from_state_id`로 전환하십시오:
```python
# 지원됨
flow = CounterFlow()
flow.kickoff(restore_from_state_id="abcd1234-5678-90ef-ghij-klmnopqrstuv")
```
두 모드는 서로 다른 계보 의미론을 가지고 있습니다:
- `inputs={"id": <uuid>}` (더 이상 지원되지 않음) — **재개**: 제공된
id 아래에 기록이 작성되어 동일한 `flow_uuid` 이력이 확장됩니다.
- `restore_from_state_id=<uuid>` — **분기**: 스냅샷에서 상태를 하이드레이트한 후
새로운 `state.id` 아래에 기록합니다. 원본 흐름의 이력은 보존됩니다.
대부분의 프로덕션 시나리오에서는 — 이전 상태에서 시드된 흐름을 다시 실행하는 경우 — 분기가
필요합니다. 전체 정신 모델은 [Flow State 마스터링](/ko/guides/flows/mastering-flow-state)을 참조하십시오.
CrewAI AMP REST API를 통해 흐름을 시작하는 경우, 아래 [AMP](#amp)에서
동일한 페이로드 마이그레이션을 참조하십시오.
## 왜 `@persist`에 대해 `inputs.id`를 더 이상 지원하지 않습니까?
`inputs.id`는 현재 이전 실행에서 `@persist` 흐름을 재개하는 문서화된 방법입니다. 문제는
동일한 UUID가 두 가지 작업을 동시에 수행한다는 것입니다:
1. **어떤 스냅샷에서 `@persist`가 하이드레이트되는지를 선택합니다** — 해당 UUID 아래에 저장된 상태를 로드합니다.
2. **새 실행의 흐름 실행 ID가 됩니다** (`state.id`는 SDK에서; 일부 컨텍스트에서는 `flow_id`로 표시됨) — 이
시작에서의 모든 `@persist` 기록도 동일한 UUID 아래에 작성됩니다.
이 이중 역할이 이 가이드에서 설명하는 문제의 근본 원인입니다. 제공된 UUID가 새 실행의 id이기도 하므로,
동일한 `inputs.id`를 전달하는 두 번의 시작은 두 개의 별도 실행이 아닙니다 — 그들은 id를 공유하고,
지속성 기록을 공유하며, (AMP에서) 실행 목록에서 행을 공유합니다. "이 스냅샷에서 하이드레이트하지만,
이 실행을 별도로 기록하십시오"라고 말할 방법이 없습니다.
`restore_from_state_id`가 그 분리입니다. 이는 `@persist`에 어떤 스냅샷에서 하이드레이트할지를 알려주며,
새 실행이 새로운 `state.id`를 받을 수 있도록 합니다. 하이드레이션 소스와 기록된 실행은 더 이상 동일한 UUID가 아닙니다 — 이는 대부분의 프로덕션 시나리오에서 실제로 원하는 것입니다.
## 제거 일정
`@persist` 하이드레이션을 위한 `inputs.id`는 CrewAI의 향후 릴리스에서 제거될 예정입니다. 즉각적인 강제 종료는 없으며 — 기존 흐름은 계속 작동합니다 — 하지만 v1.14.5 이상으로 업그레이드하면,
새 코드에서는 `restore_from_state_id`를 사용해야 하며, 기존 흐름은 다음 편리한 기회에 마이그레이션해야 합니다.
## AMP
흐름을 CrewAI AMP에 배포하는 경우, 마이그레이션은 배포된 팀에 전송되는 시작 페이로드로 확장되며,
`inputs.id`를 재사용하는 가시적인 증상은 배포 대시보드에 나타납니다. 아래 두 개의 하위 섹션이 이를 다룹니다.
### 시작 페이로드 마이그레이션
현재 `inputs`에 `id`를 포함하여 배포된 흐름을 시작하는 경우:
```bash
# 더 이상 지원되지 않음
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{"inputs": {"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv", "topic": "AI Agent Frameworks"}}' \
https://your-crew-url.crewai.com/kickoff
```
UUID를 최상위 `restoreFromStateId` 필드로 이동하십시오:
```bash
# 지원됨
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{
"inputs": {"topic": "AI Agent Frameworks"},
"restoreFromStateId": "abcd1234-5678-90ef-ghij-klmnopqrstuv"
}' \
https://your-crew-url.crewai.com/kickoff
```
`restoreFromStateId`는 시작 페이로드에서 `inputs` 옆에 위치하며, 내부에 있지 않습니다.
`inputs` 객체는 이제 흐름이 실제로 소비하는 값만 포함합니다.
### `inputs.id`가 재사용될 때 발생하는 일
AMP가 기존 실행과 `inputs.id`가 일치하는 흐름의 시작을 수신하면,
새로운 기록을 생성하는 대신 기존 기록으로 해결됩니다. 배포 대시보드에서 다음을 확인할 수 있습니다:
- **실행 상태** — 새로운 실행의 상태가 이전 실행의 상태를 덮어씁니다. 완료된 실행은
다시 `실행 중`으로 전환되거나, `완료`된 실행은 새로운 시작이 실패할 경우 `오류`로 전환될 수 있습니다 — 어쨌든 대시보드는 더 이상
원래 실행을 반영하지 않습니다.
- **추적** — OTel 추적이 시작 간에 쌓이기 때문에 동일한 실행 id를 공유합니다; 이전 실행의 추적은
새로운 실행의 추적과 교체되거나 혼합됩니다. 단계별 재생은 더 이상 단일 실행에 해당하지 않습니다.
- **실행 목록** — 별도의 행으로 나타나야 할 시작이 단일 항목으로 축소되어 이력을 숨깁니다.
`restoreFromStateId`로 마이그레이션하면 모든 시작이 자체 실행으로 유지됩니다 — 각자의 상태, 추적 및 목록의 행을 가지며 — 여전히 이전 실행에서 상태를 하이드레이트합니다.
<Card title="도움이 필요하신가요?" icon="headset" href="mailto:support@crewai.com">
흐름이 어떤 모드가 필요한지 확실하지 않거나 마이그레이션 중 문제가 발생하면 지원 팀에 문의하십시오.
</Card>

View File

@@ -797,6 +797,7 @@ LLM 선택을 최적화하고자 하는 팀을 위해 **CrewAI AMP 플랫폼**
여러 차원에서 우수한 성능을 제공하며 실제 환경에서 광범위하게 검증된 **GPT-4.1**, **Claude 3.7 Sonnet**, **Gemini 2.0 Flash**와 같은 잘 알려진 모델부터 시작하십시오.
</Step>
{" "}
<Step title="특화된 요구 사항 식별">
crew에 코드 작성, reasoning, 속도 등 특정 요구가 있는지 확인하고, 이러한
요구에 부합하는 **Claude 4 Sonnet**(개발용) 또는 **o3**(복잡한 분석용)과 같은
@@ -804,6 +805,7 @@ LLM 선택을 최적화하고자 하는 팀을 위해 **CrewAI AMP 플랫폼**
더불어 **Groq**와 같은 빠른 추론 제공자를 고려할 수 있습니다.
</Step>
{" "}
<Step title="다중 모델 전략 구현">
각 에이전트의 역할에 따라 다양한 모델을 사용하세요. 관리자와 복잡한 작업에는
고성능 모델을, 일상적 운영에는 효율적인 모델을 적용합니다.

View File

@@ -4,140 +4,6 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="21 mai 2026">
## v1.14.6a1
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.6a1)
## O que Mudou
### Recursos
- Adicionar Repositório de Habilidades com registro, cache, CLI e integração SDK
- Gerar notas de versão categorizadas para empresas
### Correções de Bugs
- Fortalecer a serialização de RuntimeState entre os campos da entidade
- Atualizar idna para 3.15 para resolver problema de segurança GHSA-65pc-fj4g-8rjx
- Remover expressões JSX `{" "}` que quebram a renderização de `<Steps>`
### Documentação
- Atualizar changelog e versão para v1.14.5
## Contribuidores
@akaKuruma, @alex-clawd, @greysonlalonde
</Update>
<Update label="19 mai 2026">
## v1.14.5
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5)
## O que Mudou
### Recursos
- Deprecar `CrewAgentExecutor`, definir agentes Crew como `AgentExecutor`
- Melhorar ferramentas do sandbox Daytona
- Adicionar parâmetro de início `restore_from_state_id`
- Adicionar destaques ao `ExaSearchTool`, renomeando de `EXASearchTool`
### Correções de Bugs
- Corrigir vazamento de memória em `git.py` usando `cached_property`
- Exibir chamadas de ferramentas transmitidas quando `available_functions` está ausente
- Garantir eventos de carregamento de `skills` para rastros
- Corrigir caminho do endpoint de status de `/{kickoff_id}/status` para `/status/{kickoff_id}`
- Restaurar bloco de código ausente no guia de primeiro fluxo em pt-BR
- Impedir que `result_as_answer` retorne mensagens de bloqueio de hook ou de erro como resposta final
- Preservar saídas de tarefas durante o descarregamento assíncrono em lote
- Sempre restaurar `task.output_pydantic` no bloco finally
- Lidar com entrada de `BaseModel` em `convert_to_model`
### Documentação
- Atualizar changelog e versão para v1.14.5
- Adicionar guia de migração de atualização OSS & crew-to-flow
- Documentar variáveis de ambiente adicionais para devtools
- Adicionar documentação para `TavilyGetResearch`
### Refatoração
- Extrair CLI para o pacote autônomo `crewai-cli`
## Contribuidores
@NIK-TIGER-BILL, @akaKuruma, @cgoeppinger, @github-actions[bot], @greysonlalonde, @heitorado, @irfaan101, @iris-clawd, @lorenzejay, @manisrinivasan2k1, @minasami-pr, @mislavivanda, @theCyberTech, @theishangoswami, @wishhyt
</Update>
<Update label="18 mai 2026">
## v1.14.5a7
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a7)
## O que Mudou
### Documentação
- Atualizar changelog e versão para v1.14.5a6
### Mudanças Quebradoras
- Depreciar o campo function_calling_llm
## Contributors
@greysonlalonde, @heitorado
</Update>
<Update label="15 mai 2026">
## v1.14.5a6
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a6)
## O que mudou
### Correções de Bugs
- Corrigir chamadas de ferramentas transmitidas quando available_functions está ausente
- Atualizar a dependência langsmith para a versão >=0.8.0 para resolver GHSA-3644-q5cj-c5c7
- Resolver espaços reservados de blocos de código não traduzidos na documentação em português brasileiro
### Documentação
- Adicionar documentação para TavilyGetResearch
- Atualizar changelog e versão para v1.14.5a5
## Contributors
@greysonlalonde, @heitorado, @iris-clawd, @lorenzejay, @manisrinivasan2k1
</Update>
<Update label="13 mai 2026">
## v1.14.5a5
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a5)
## O Que Mudou
### Recursos
- Deprecar CrewAgentExecutor, definir agentes Crew como AgentExecutor
- Melhorar ferramentas de sandbox Daytona
### Correções de Bugs
- Corrigir bloco de código ausente no guia de primeiro fluxo em pt-BR
- Registrar falhas de pré-revisão e destilação HITL, adicionar learn_strict
- Corrigir urllib3 para vulnerabilidades de segurança
- Corrigir gitpython e langchain-core; ignorar CVE paramiko não corrigido
- Atualizar todos os pacotes de workspace publicados no bloqueio/sincronização uv
### Documentação
- Adicionar guia de migração de `inputs.id` para `restoreFromStateId`
- Adicionar guia de atualização OSS e migração de crew para flow
- Atualizar changelog e versão para v1.14.5a4
## Contribuidores
@akaKuruma, @greysonlalonde, @iris-clawd, @lorenzejay, @mislavivanda
</Update>
<Update label="09 mai 2026">
## v1.14.5a4

View File

@@ -24,63 +24,7 @@ Os flows permitem que você crie fluxos de trabalho estruturados e orientados po
Vamos criar um Flow simples no qual você usará a OpenAI para gerar uma cidade aleatória em uma tarefa e, em seguida, usará essa cidade para gerar uma curiosidade em outra tarefa.
```python Code
from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"
@start()
def generate_city(self):
print("Starting flow")
# Cada estado do flow recebe automaticamente um ID único
print(f"Flow State ID: {self.state['id']}")
response = completion(
model=self.model,
messages=[
{
"role": "user",
"content": "Return the name of a random city in the world.",
},
],
)
random_city = response["choices"][0]["message"]["content"]
# Armazena a cidade no nosso estado
self.state["city"] = random_city
print(f"Random City: {random_city}")
return random_city
@listen(generate_city)
def generate_fun_fact(self, random_city):
response = completion(
model=self.model,
messages=[
{
"role": "user",
"content": f"Tell me a fun fact about {random_city}",
},
],
)
fun_fact = response["choices"][0]["message"]["content"]
# Armazena a curiosidade no nosso estado
self.state["fun_fact"] = fun_fact
return fun_fact
flow = ExampleFlow()
flow.plot()
result = flow.kickoff()
print(f"Generated fun fact: {result}")
# (O código não é traduzido)
```
Na ilustração acima, criamos um Flow simples que gera uma cidade aleatória usando a OpenAI e depois cria uma curiosidade sobre essa cidade. O Flow consiste em duas tarefas: `generate_city` e `generate_fun_fact`. A tarefa `generate_city` é o ponto de início do Flow, enquanto a tarefa `generate_fun_fact` fica escutando o resultado da tarefa `generate_city`.
@@ -112,16 +56,12 @@ O decorador `@listen()` pode ser usado de várias formas:
1. **Escutando um Método pelo Nome**: Você pode passar o nome do método ao qual deseja escutar como string. Quando esse método concluir, o método ouvinte será chamado.
```python Code
@listen("generate_city")
def generate_fun_fact(self, random_city):
# Implementação
# (O código não é traduzido)
```
2. **Escutando um Método Diretamente**: Você pode passar o próprio método. Quando esse método concluir, o método ouvinte será chamado.
```python Code
@listen(generate_city)
def generate_fun_fact(self, random_city):
# Implementação
# (O código não é traduzido)
```
### Saída de um Flow
@@ -136,24 +76,7 @@ Veja como acessar a saída final:
<CodeGroup>
```python Code
from crewai.flow.flow import Flow, listen, start
class OutputExampleFlow(Flow):
@start()
def first_method(self):
return "Output from first_method"
@listen(first_method)
def second_method(self, first_output):
return f"Second method received: {first_output}"
flow = OutputExampleFlow()
flow.plot("my_flow_plot")
final_output = flow.kickoff()
print("---- Final Output ----")
print(final_output)
# (O código não é traduzido)
```
```text Output
@@ -174,34 +97,8 @@ Além de recuperar a saída final, você pode acessar e atualizar o estado dentr
Veja um exemplo de como atualizar e acessar o estado:
<CodeGroup>
```python Code
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
class ExampleState(BaseModel):
counter: int = 0
message: str = ""
class StateExampleFlow(Flow[ExampleState]):
@start()
def first_method(self):
self.state.message = "Hello from first_method"
self.state.counter += 1
@listen(first_method)
def second_method(self):
self.state.message += " - updated by second_method"
self.state.counter += 1
return self.state.message
flow = StateExampleFlow()
flow.plot("my_flow_plot")
final_output = flow.kickoff()
print(f"Final Output: {final_output}")
print("Final State:")
print(flow.state)
# (O código não é traduzido)
```
```text Output
@@ -231,33 +128,7 @@ Essa abordagem oferece flexibilidade, permitindo que o desenvolvedor adicione ou
Mesmo com estados não estruturados, os flows do CrewAI geram e mantêm automaticamente um identificador único (UUID) para cada instância de estado.
```python Code
from crewai.flow.flow import Flow, listen, start
class UnstructuredExampleFlow(Flow):
@start()
def first_method(self):
# O estado inclui automaticamente um campo 'id'
print(f"State ID: {self.state['id']}")
self.state['counter'] = 0
self.state['message'] = "Hello from structured flow"
@listen(first_method)
def second_method(self):
self.state['counter'] += 1
self.state['message'] += " - updated"
@listen(second_method)
def third_method(self):
self.state['counter'] += 1
self.state['message'] += " - updated again"
print(f"State after third_method: {self.state}")
flow = UnstructuredExampleFlow()
flow.plot("my_flow_plot")
flow.kickoff()
# (O código não é traduzido)
```
![Flow Visual image](/images/crewai-flow-3.png)
@@ -277,39 +148,7 @@ Ao usar modelos como o `BaseModel` da Pydantic, os desenvolvedores podem definir
Cada estado nos flows do CrewAI recebe automaticamente um identificador único (UUID) para ajudar no rastreamento e gerenciamento. Esse ID é gerado e mantido automaticamente pelo sistema de flows.
```python Code
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
class ExampleState(BaseModel):
# Nota: o campo 'id' é adicionado automaticamente a todos os estados
counter: int = 0
message: str = ""
class StructuredExampleFlow(Flow[ExampleState]):
@start()
def first_method(self):
# Acesse o ID gerado automaticamente, se necessário
print(f"State ID: {self.state.id}")
self.state.message = "Hello from structured flow"
@listen(first_method)
def second_method(self):
self.state.counter += 1
self.state.message += " - updated"
@listen(second_method)
def third_method(self):
self.state.counter += 1
self.state.message += " - updated again"
print(f"State after third_method: {self.state}")
flow = StructuredExampleFlow()
flow.kickoff()
# (O código não é traduzido)
```
![Flow Visual image](/images/crewai-flow-3.png)
@@ -343,19 +182,7 @@ O decorador @persist permite a persistência automática do estado nos flows do
Quando aplicado no nível da classe, o decorador @persist garante a persistência automática de todos os estados dos métodos do flow:
```python
@persist # Usa SQLiteFlowPersistence por padrão
class MyFlow(Flow[MyState]):
@start()
def initialize_flow(self):
# Este método terá seu estado persistido automaticamente
self.state.counter = 1
print("Initialized flow. State ID:", self.state.id)
@listen(initialize_flow)
def next_step(self):
# O estado (incluindo self.state.id) é recarregado automaticamente
self.state.counter += 1
print("Flow state is persisted. Counter:", self.state.counter)
# (O código não é traduzido)
```
### Persistência no Nível de Método
@@ -363,14 +190,7 @@ class MyFlow(Flow[MyState]):
Para um controle mais granular, você pode aplicar @persist em métodos específicos:
```python
class AnotherFlow(Flow[dict]):
@persist # Persiste apenas o estado deste método
@start()
def begin(self):
if "runs" not in self.state:
self.state["runs"] = 0
self.state["runs"] += 1
print("Method-level persisted runs:", self.state["runs"])
# (O código não é traduzido)
```
### Forking de Estado Persistido
@@ -462,29 +282,8 @@ A arquitetura de persistência enfatiza precisão técnica e opções de persona
A função `or_` nos flows permite escutar múltiplos métodos e acionar o método ouvinte quando qualquer um dos métodos especificados gerar uma saída.
<CodeGroup>
```python Code
from crewai.flow.flow import Flow, listen, or_, start
class OrExampleFlow(Flow):
@start()
def start_method(self):
return "Hello from the start method"
@listen(start_method)
def second_method(self):
return "Hello from the second method"
@listen(or_(start_method, second_method))
def logger(self, result):
print(f"Logger: {result}")
flow = OrExampleFlow()
flow.plot("my_flow_plot")
flow.kickoff()
# (O código não é traduzido)
```
```text Output
@@ -503,28 +302,8 @@ A função `or_` serve para escutar vários métodos e disparar o método ouvint
A função `and_` nos flows permite escutar múltiplos métodos e acionar o método ouvinte apenas quando todos os métodos especificados emitirem uma saída.
<CodeGroup>
```python Code
from crewai.flow.flow import Flow, and_, listen, start
class AndExampleFlow(Flow):
@start()
def start_method(self):
self.state["greeting"] = "Hello from the start method"
@listen(start_method)
def second_method(self):
self.state["joke"] = "What do computers eat? Microchips."
@listen(and_(start_method, second_method))
def logger(self):
print("---- Logger ----")
print(self.state)
flow = AndExampleFlow()
flow.plot()
flow.kickoff()
# (O código não é traduzido)
```
```text Output
@@ -544,42 +323,8 @@ O decorador `@router()` nos flows permite definir lógica de roteamento condicio
Você pode especificar diferentes rotas conforme a saída do método, permitindo controlar o fluxo de execução de forma dinâmica.
<CodeGroup>
```python Code
import random
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
class ExampleState(BaseModel):
success_flag: bool = False
class RouterFlow(Flow[ExampleState]):
@start()
def start_method(self):
print("Starting the structured flow")
random_boolean = random.choice([True, False])
self.state.success_flag = random_boolean
@router(start_method)
def second_method(self):
if self.state.success_flag:
return "success"
else:
return "failed"
@listen("success")
def third_method(self):
print("Third method running")
@listen("failed")
def fourth_method(self):
print("Fourth method running")
flow = RouterFlow()
flow.plot("my_flow_plot")
flow.kickoff()
# (O código não é traduzido)
```
```text Output
@@ -656,105 +401,7 @@ Para um guia completo sobre feedback humano em flows, incluindo feedback assínc
Os agentes podem ser integrados facilmente aos seus flows, oferecendo uma alternativa leve às crews completas quando você precisar executar tarefas simples e focadas. Veja um exemplo de como utilizar um agente em um flow para realizar uma pesquisa de mercado:
```python
import asyncio
from typing import Any, Dict, List
from crewai_tools import SerperDevTool
from pydantic import BaseModel, Field
from crewai.agent import Agent
from crewai.flow.flow import Flow, listen, start
# Define um formato de saída estruturado
class MarketAnalysis(BaseModel):
key_trends: List[str] = Field(description="List of identified market trends")
market_size: str = Field(description="Estimated market size")
competitors: List[str] = Field(description="Major competitors in the space")
# Define o estado do flow
class MarketResearchState(BaseModel):
product: str = ""
analysis: MarketAnalysis | None = None
# Cria uma classe de flow
class MarketResearchFlow(Flow[MarketResearchState]):
@start()
def initialize_research(self) -> Dict[str, Any]:
print(f"Starting market research for {self.state.product}")
return {"product": self.state.product}
@listen(initialize_research)
async def analyze_market(self) -> Dict[str, Any]:
# Cria um agente para pesquisa de mercado
analyst = Agent(
role="Market Research Analyst",
goal=f"Analyze the market for {self.state.product}",
backstory="You are an experienced market analyst with expertise in "
"identifying market trends and opportunities.",
tools=[SerperDevTool()],
verbose=True,
)
# Define a consulta de pesquisa
query = f"""
Research the market for {self.state.product}. Include:
1. Key market trends
2. Market size
3. Major competitors
Format your response according to the specified structure.
"""
# Executa a análise com formato de saída estruturado
result = await analyst.kickoff_async(query, response_format=MarketAnalysis)
if result.pydantic:
print("result", result.pydantic)
else:
print("result", result)
# Retorna a análise para atualizar o estado
return {"analysis": result.pydantic}
@listen(analyze_market)
def present_results(self, analysis) -> None:
print("\nMarket Analysis Results")
print("=====================")
if isinstance(analysis, dict):
# Se recebemos um dict com a chave 'analysis', extrai o objeto de análise real
market_analysis = analysis.get("analysis")
else:
market_analysis = analysis
if market_analysis and isinstance(market_analysis, MarketAnalysis):
print("\nKey Market Trends:")
for trend in market_analysis.key_trends:
print(f"- {trend}")
print(f"\nMarket Size: {market_analysis.market_size}")
print("\nMajor Competitors:")
for competitor in market_analysis.competitors:
print(f"- {competitor}")
else:
print("No structured analysis data available.")
print("Raw analysis:", analysis)
# Exemplo de uso
async def run_flow():
flow = MarketResearchFlow()
flow.plot("MarketResearchFlowPlot")
result = await flow.kickoff_async(inputs={"product": "AI-powered chatbots"})
return result
# Executa o flow
if __name__ == "__main__":
asyncio.run(run_flow())
# (O código não é traduzido)
```
![Flow Visual image](/images/crewai-flow-7.png)
@@ -816,50 +463,7 @@ No arquivo `main.py`, você cria seu flow e conecta as crews. É possível defin
Veja um exemplo de como conectar a `poem_crew` no arquivo `main.py`:
```python Code
#!/usr/bin/env python
from random import randint
from pydantic import BaseModel
from crewai.flow.flow import Flow, listen, start
from .crews.poem_crew.poem_crew import PoemCrew
class PoemState(BaseModel):
sentence_count: int = 1
poem: str = ""
class PoemFlow(Flow[PoemState]):
@start()
def generate_sentence_count(self):
print("Generating sentence count")
self.state.sentence_count = randint(1, 5)
@listen(generate_sentence_count)
def generate_poem(self):
print("Generating poem")
result = PoemCrew().crew().kickoff(inputs={"sentence_count": self.state.sentence_count})
print("Poem generated", result.raw)
self.state.poem = result.raw
@listen(generate_poem)
def save_poem(self):
print("Saving poem")
with open("poem.txt", "w") as f:
f.write(self.state.poem)
def kickoff():
poem_flow = PoemFlow()
poem_flow.kickoff()
def plot():
poem_flow = PoemFlow()
poem_flow.plot("PoemFlowPlot")
if __name__ == "__main__":
kickoff()
plot()
# (O código não é traduzido)
```
Neste exemplo, a classe `PoemFlow` define um fluxo que gera a quantidade de frases, usa a `PoemCrew` para gerar um poema e, depois, salva o poema em um arquivo. O flow inicia com o método `kickoff()`, e o gráfico é gerado pelo método `plot()`.
@@ -911,8 +515,7 @@ O CrewAI oferece duas formas práticas de gerar plots dos seus flows:
Se estiver trabalhando diretamente com uma instância do flow, basta chamar o método `plot()` do objeto. Isso criará um arquivo HTML com o plot interativo do seu flow.
```python Code
# Considerando que você já tem uma instância do flow
flow.plot("my_flow_plot")
# (O código não é traduzido)
```
Esse comando gera um arquivo chamado `my_flow_plot.html` no diretório atual. Abra esse arquivo em um navegador para visualizar o plot interativo.

View File

@@ -146,6 +146,7 @@ Veja um fluxo de trabalho típico para criação de um crew com o Crew Studio:
</Step>
{" "}
<Step title="Responder Perguntas">
Responda às perguntas de esclarecimento do Crew Assistant para refinar seus
requisitos.
@@ -160,10 +161,12 @@ Veja um fluxo de trabalho típico para criação de um crew com o Crew Studio:
</Step>
{" "}
<Step title="Aprovar ou Modificar">
Aprove o plano ou solicite alterações, se necessário.
</Step>
{" "}
<Step title="Baixar ou Fazer Deploy">
Baixe o código para personalização ou faça o deploy diretamente na plataforma.
</Step>

View File

@@ -266,165 +266,7 @@ Nosso flow irá:
Vamos criar nosso flow no arquivo `main.py`:
```python
#!/usr/bin/env python
import json
import os
from typing import List, Dict
from pydantic import BaseModel, Field
from crewai import LLM
from crewai.flow.flow import Flow, listen, start
from guide_creator_flow.crews.content_crew.content_crew import ContentCrew
# Definir nossos modelos para dados estruturados
class Section(BaseModel):
title: str = Field(description="Title of the section")
description: str = Field(description="Brief description of what the section should cover")
class GuideOutline(BaseModel):
title: str = Field(description="Title of the guide")
introduction: str = Field(description="Introduction to the topic")
target_audience: str = Field(description="Description of the target audience")
sections: List[Section] = Field(description="List of sections in the guide")
conclusion: str = Field(description="Conclusion or summary of the guide")
# Definir o estado do nosso flow
class GuideCreatorState(BaseModel):
topic: str = ""
audience_level: str = ""
guide_outline: GuideOutline = None
sections_content: Dict[str, str] = {}
class GuideCreatorFlow(Flow[GuideCreatorState]):
"""Flow para criar um guia abrangente sobre qualquer tópico"""
@start()
def get_user_input(self):
"""Obter entrada do usuário sobre o tópico e público do guia"""
print("\n=== Create Your Comprehensive Guide ===\n")
# Obter entrada do usuário
self.state.topic = input("What topic would you like to create a guide for? ")
# Obter nível do público com validação
while True:
audience = input("Who is your target audience? (beginner/intermediate/advanced) ").lower()
if audience in ["beginner", "intermediate", "advanced"]:
self.state.audience_level = audience
break
print("Please enter 'beginner', 'intermediate', or 'advanced'")
print(f"\nCreating a guide on {self.state.topic} for {self.state.audience_level} audience...\n")
return self.state
@listen(get_user_input)
def create_guide_outline(self, state):
"""Criar um esboço estruturado para o guia usando uma chamada direta ao LLM"""
print("Creating guide outline...")
# Inicializar o LLM
llm = LLM(model="openai/gpt-4o-mini", response_format=GuideOutline)
# Criar as mensagens para o esboço
messages = [
{"role": "system", "content": "You are a helpful assistant designed to output JSON."},
{"role": "user", "content": f"""
Create a detailed outline for a comprehensive guide on "{state.topic}" for {state.audience_level} level learners.
The outline should include:
1. A compelling title for the guide
2. An introduction to the topic
3. 4-6 main sections that cover the most important aspects of the topic
4. A conclusion or summary
For each section, provide a clear title and a brief description of what it should cover.
"""}
]
# Fazer a chamada ao LLM com formato de resposta JSON
response = llm.call(messages=messages)
# Analisar a resposta JSON
outline_dict = json.loads(response)
self.state.guide_outline = GuideOutline(**outline_dict)
# Garantir que o diretório de saída exista antes de salvar
os.makedirs("output", exist_ok=True)
# Salvar o esboço em um arquivo
with open("output/guide_outline.json", "w") as f:
json.dump(outline_dict, f, indent=2)
print(f"Guide outline created with {len(self.state.guide_outline.sections)} sections")
return self.state.guide_outline
@listen(create_guide_outline)
def write_and_compile_guide(self, outline):
"""Escrever todas as seções e compilar o guia"""
print("Writing guide sections and compiling...")
completed_sections = []
# Processar seções uma por uma para manter o fluxo de contexto
for section in outline.sections:
print(f"Processing section: {section.title}")
# Construir contexto a partir das seções anteriores
previous_sections_text = ""
if completed_sections:
previous_sections_text = "# Previously Written Sections\n\n"
for title in completed_sections:
previous_sections_text += f"## {title}\n\n"
previous_sections_text += self.state.sections_content.get(title, "") + "\n\n"
else:
previous_sections_text = "No previous sections written yet."
# Executar a crew de conteúdo para esta seção
result = ContentCrew().crew().kickoff(inputs={
"section_title": section.title,
"section_description": section.description,
"audience_level": self.state.audience_level,
"previous_sections": previous_sections_text,
"draft_content": ""
})
# Armazenar o conteúdo
self.state.sections_content[section.title] = result.raw
completed_sections.append(section.title)
print(f"Section completed: {section.title}")
# Compilar o guia final
guide_content = f"# {outline.title}\n\n"
guide_content += f"## Introduction\n\n{outline.introduction}\n\n"
# Adicionar cada seção em ordem
for section in outline.sections:
section_content = self.state.sections_content.get(section.title, "")
guide_content += f"\n\n{section_content}\n\n"
# Adicionar conclusão
guide_content += f"## Conclusion\n\n{outline.conclusion}\n\n"
# Salvar o guia
with open("output/complete_guide.md", "w") as f:
f.write(guide_content)
print("\nComplete guide compiled and saved to output/complete_guide.md")
return "Guide creation completed successfully"
def kickoff():
"""Executar o flow criador de guias"""
GuideCreatorFlow().kickoff()
print("\n=== Flow Complete ===")
print("Your comprehensive guide is ready in the output directory.")
print("Open output/complete_guide.md to view it.")
def plot():
"""Gerar uma visualização do flow"""
flow = GuideCreatorFlow()
flow.plot("guide_creator_flow")
print("Flow visualization saved to guide_creator_flow.html")
if __name__ == "__main__":
kickoff()
# [CÓDIGO NÃO TRADUZIDO, MANTER COMO ESTÁ]
```
Vamos analisar o que está acontecendo neste flow:

View File

@@ -1,142 +0,0 @@
---
title: "Migrando de inputs.id para restore_from_state_id"
description: "Mover fluxos @persist da hidratação obsoleta inputs.id para o campo suportado restore_from_state_id"
icon: "arrow-right-arrow-left"
---
<Warning>
Passar `id` dentro de `inputs` para hidratar um fluxo `@persist` é **obsoleto** e
programado para remoção em uma versão futura. A substituição, `restore_from_state_id`,
está disponível no CrewAI **v1.14.5 e posterior** — os passos abaixo se aplicam uma vez que você
faça a atualização.
</Warning>
## Visão Geral
A maneira documentada de hidratar um fluxo `@persist` de uma execução anterior é passar
o UUID dessa execução como `inputs.id`. O CrewAI agora expõe um campo dedicado,
`restore_from_state_id`, que realiza a mesma hidratação sem sobrecarregar a
carga útil de `inputs` — e sem acoplar a chave de hidratação à identidade da nova execução.
## Migração
Se você atualmente inicia um fluxo `@persist` com `inputs={"id": ...}`:
```python
# Obsoleto
flow = CounterFlow()
flow.kickoff(inputs={"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv"})
```
Mude para `restore_from_state_id`:
```python
# Suportado
flow = CounterFlow()
flow.kickoff(restore_from_state_id="abcd1234-5678-90ef-ghij-klmnopqrstuv")
```
Os dois modos têm semânticas de linhagem diferentes:
- `inputs={"id": <uuid>}` (obsoleto) — **retomar**: as gravações são feitas sob o id fornecido,
estendendo a mesma história de `flow_uuid`.
- `restore_from_state_id=<uuid>` — **dividir**: hidrata o estado a partir de um snapshot, então
grava sob um novo `state.id`. A história do fluxo de origem é preservada.
Para a maioria dos cenários de produção — reexecutar um fluxo hidratado de um estado anterior — criar um fork
é o que você deseja. Veja [Dominando o Estado do Fluxo](/pt-BR/guides/flows/mastering-flow-state)
para o modelo mental completo.
Se você iniciar seu fluxo pela API REST do CrewAI AMP, veja [AMP](#amp) abaixo para a
migração equivalente da carga útil.
## Por que estamos descontinuando `inputs.id` para `@persist`?
`inputs.id` é atualmente a maneira documentada de retomar um fluxo `@persist` de uma
execução anterior. O problema é que o mesmo UUID faz duas funções ao mesmo tempo:
1. **Seleciona qual snapshot o `@persist` usa para hidratar** — carrega o estado salvo
sob aquele UUID.
2. **Torna-se o ID de Execução do Fluxo da nova execução** (`state.id` no SDK;
apresentado como `flow_id` em alguns contextos) — cada gravação `@persist` a partir desta
inicialização também cai sob aquele mesmo UUID.
Esse papel duplo é a causa raiz dos problemas que este guia descreve. Como o
UUID fornecido também é o id da nova execução, duas inicializações que passam o mesmo
`inputs.id` não são duas execuções distintas — elas compartilham um id, compartilham um registro
de persistência e (no AMP) compartilham uma linha na lista de execuções. Não há como dizer
"hidratar a partir deste snapshot, mas registrar esta execução separadamente" sem dividir as
duas responsabilidades.
`restore_from_state_id` é essa divisão. Ele informa ao `@persist` de qual snapshot hidratar,
enquanto deixa a nova execução livre para receber um novo `state.id`. A
fonte de hidratação e a execução registrada não são mais o mesmo UUID — que é o que
a maioria dos cenários de produção realmente deseja.
## Cronograma de remoção
`inputs.id` para hidratação `@persist` está programado para remoção em uma versão futura do
CrewAI. Não há um corte imediato — fluxos existentes continuam a funcionar — mas
uma vez que você atualize para v1.14.5 ou posterior, novo código deve usar `restore_from_state_id`, e
fluxos existentes devem migrar na próxima oportunidade conveniente.
## AMP
Se você implantar seu fluxo no CrewAI AMP, a migração se estende à carga útil de inicialização
enviada para sua Crew implantada, e os sintomas visíveis de reutilização de `inputs.id` aparecem
no painel de controle de implantação. As duas subseções abaixo cobrem ambos.
### Migrando a carga útil de inicialização
Se você atualmente inicia um fluxo implantado incorporando `id` em `inputs`:
```bash
# Obsoleto
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{"inputs": {"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv", "topic": "AI Agent Frameworks"}}' \
https://your-crew-url.crewai.com/kickoff
```
Mova o UUID para o campo `restoreFromStateId` de nível superior:
```bash
# Suportado
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{
"inputs": {"topic": "AI Agent Frameworks"},
"restoreFromStateId": "abcd1234-5678-90ef-ghij-klmnopqrstuv"
}' \
https://your-crew-url.crewai.com/kickoff
```
`restoreFromStateId` fica ao lado de `inputs` na carga útil de inicialização, não dentro dela. O
objeto `inputs` agora carrega apenas valores que seu fluxo realmente consome.
### O que acontece quando `inputs.id` é reutilizado
Quando o AMP recebe um kickoff para um fluxo cujo `inputs.id` corresponde a uma execução
existente, ele resolve para o registro existente em vez de criar um novo. A partir
do painel de controle de implantação, você verá:
- **Status da execução** — o status da nova execução sobrescreve o status da execução anterior. Uma
execução finalizada pode voltar para `running`, ou uma execução `completed` pode mudar para
`error` se a nova inicialização falhar — de qualquer forma, o painel não reflete mais
a execução original.
- **Rastros** — Os OTel traces se acumulam entre as inicializações porque compartilham o mesmo
id de execução; os traces da execução anterior são substituídos ou misturados
com os da nova execução. Uma reprodução passo a passo não corresponde mais a uma única execução.
- **Lista de execuções** — kickoffs que deveriam aparecer como linhas separadas colapsam em
uma única entrada, ocultando o histórico.
Migrar para `restoreFromStateId` mantém cada kickoff como sua própria execução — com
seu próprio status, traces e entrada na lista — enquanto ainda hidrata o estado de uma
execução anterior.
<Card title="Precisa de Ajuda?" icon="headset" href="mailto:support@crewai.com">
Entre em contato com nossa equipe de suporte se você não tiver certeza de qual modo seu fluxo precisa ou se encontrar problemas
durante a migração.
</Card>

View File

@@ -63,60 +63,7 @@ Com estado não estruturado:
Veja um exemplo simples de gerenciamento de estado não estruturado:
```python
from crewai.flow.flow import Flow, listen, start
class UnstructuredStateFlow(Flow):
@start()
def initialize_data(self):
print("Initializing flow data")
# Adiciona pares chave-valor ao estado
self.state["user_name"] = "Alex"
self.state["preferences"] = {
"theme": "dark",
"language": "English"
}
self.state["items"] = []
# O estado do flow recebe automaticamente um ID único
print(f"Flow ID: {self.state['id']}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Previous step returned: {previous_result}")
# Acessa e modifica o estado
user = self.state["user_name"]
print(f"Processing data for {user}")
# Adiciona itens a uma lista no estado
self.state["items"].append("item1")
self.state["items"].append("item2")
# Adiciona um novo par chave-valor
self.state["processed"] = True
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Acessa múltiplos valores do estado
user = self.state["user_name"]
theme = self.state["preferences"]["theme"]
items = self.state["items"]
processed = self.state.get("processed", False)
summary = f"User {user} has {len(items)} items with {theme} theme. "
summary += "Data is processed." if processed else "Data is not processed."
return summary
# Executa o flow
flow = UnstructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
# código não traduzido
```
### Quando Usar Estado Não Estruturado
@@ -147,63 +94,7 @@ Ao utilizar estado estruturado:
Veja como implementar o gerenciamento de estado estruturado:
```python
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel, Field
from typing import List, Dict, Optional
# Define o modelo de estado
class UserPreferences(BaseModel):
theme: str = "light"
language: str = "English"
class AppState(BaseModel):
user_name: str = ""
preferences: UserPreferences = UserPreferences()
items: List[str] = []
processed: bool = False
completion_percentage: float = 0.0
# Cria um flow com estado tipado
class StructuredStateFlow(Flow[AppState]):
@start()
def initialize_data(self):
print("Initializing flow data")
# Define valores do estado (com checagem de tipo)
self.state.user_name = "Taylor"
self.state.preferences.theme = "dark"
# O campo ID está disponível automaticamente
print(f"Flow ID: {self.state.id}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Processing data for {self.state.user_name}")
# Modifica o estado (com checagem de tipo)
self.state.items.append("item1")
self.state.items.append("item2")
self.state.processed = True
self.state.completion_percentage = 50.0
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Acessa o estado (com autocompletar)
summary = f"User {self.state.user_name} has {len(self.state.items)} items "
summary += f"with {self.state.preferences.theme} theme. "
summary += "Data is processed." if self.state.processed else "Data is not processed."
summary += f" Completion: {self.state.completion_percentage}%"
return summary
# Executa o flow
flow = StructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
# código não traduzido
```
### Benefícios do Estado Estruturado
@@ -247,29 +138,7 @@ Independente de você usar estado estruturado ou não estruturado, é possível
Métodos do flow podem retornar valores que serão passados como argumento para métodos listeners:
```python
from crewai.flow.flow import Flow, listen, start
class DataPassingFlow(Flow):
@start()
def generate_data(self):
# Este valor de retorno será passado para os métodos listeners
return "Generated data"
@listen(generate_data)
def process_data(self, data_from_previous_step):
print(f"Received: {data_from_previous_step}")
# Você pode modificar os dados e repassá-los adiante
processed_data = f"{data_from_previous_step} - processed"
# Também atualiza o estado
self.state["last_processed"] = processed_data
return processed_data
@listen(process_data)
def finalize_data(self, processed_data):
print(f"Received processed data: {processed_data}")
# Acessa tanto os dados passados quanto o estado
last_processed = self.state.get("last_processed", "")
return f"Final: {processed_data} (from state: {last_processed})"
# código não traduzido
```
Esse padrão permite combinar passagem de dados direta com atualizações de estado para obter máxima flexibilidade.
@@ -287,36 +156,7 @@ O decorador `@persist()` automatiza a persistência de estado, salvando o estado
Ao aplicar em nível de classe, `@persist()` salva o estado após cada execução de método:
```python
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
from pydantic import BaseModel
class CounterState(BaseModel):
value: int = 0
@persist() # Aplica à classe inteira do flow
class PersistentCounterFlow(Flow[CounterState]):
@start()
def increment(self):
self.state.value += 1
print(f"Incremented to {self.state.value}")
return self.state.value
@listen(increment)
def double(self, value):
self.state.value = value * 2
print(f"Doubled to {self.state.value}")
return self.state.value
# Primeira execução
flow1 = PersistentCounterFlow()
result1 = flow1.kickoff()
print(f"First run result: {result1}")
# Segunda execução - passa o ID para carregar o estado persistido
flow2 = PersistentCounterFlow()
result2 = flow2.kickoff(inputs={"id": flow1.state.id})
print(f"Second run result: {result2}") # Será maior devido ao estado persistido
# código não traduzido
```
#### Persistência em Nível de Método
@@ -324,26 +164,7 @@ print(f"Second run result: {result2}") # Será maior devido ao estado persistid
Para mais controle, você pode aplicar `@persist()` em métodos específicos:
```python
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
class SelectivePersistFlow(Flow):
@start()
def first_step(self):
self.state["count"] = 1
return "First step"
@persist() # Persiste apenas após este método
@listen(first_step)
def important_step(self, prev_result):
self.state["count"] += 1
self.state["important_data"] = "This will be persisted"
return "Important step completed"
@listen(important_step)
def final_step(self, prev_result):
self.state["count"] += 1
return f"Complete with count {self.state['count']}"
# código não traduzido
```
#### Forking de Estado Persistido
@@ -395,45 +216,7 @@ Notas sobre o comportamento:
Você pode usar o estado para implementar lógicas condicionais complexas em seus flows:
```python
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
class PaymentState(BaseModel):
amount: float = 0.0
is_approved: bool = False
retry_count: int = 0
class PaymentFlow(Flow[PaymentState]):
@start()
def process_payment(self):
# Simula o processamento do pagamento
self.state.amount = 100.0
self.state.is_approved = self.state.amount < 1000
return "Payment processed"
@router(process_payment)
def check_approval(self, previous_result):
if self.state.is_approved:
return "approved"
elif self.state.retry_count < 3:
return "retry"
else:
return "rejected"
@listen("approved")
def handle_approval(self):
return f"Payment of ${self.state.amount} approved!"
@listen("retry")
def handle_retry(self):
self.state.retry_count += 1
print(f"Retrying payment (attempt {self.state.retry_count})...")
# Aqui poderia ser implementada a lógica de retry
return "Retry initiated"
@listen("rejected")
def handle_rejection(self):
return f"Payment of ${self.state.amount} rejected after {self.state.retry_count} retries."
# código não traduzido
```
### Manipulações Complexas de Estado
@@ -441,60 +224,7 @@ class PaymentFlow(Flow[PaymentState]):
Para transformar estados complexos, você pode criar métodos dedicados:
```python
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
from typing import List, Dict
class UserData(BaseModel):
name: str
active: bool = True
login_count: int = 0
class ComplexState(BaseModel):
users: Dict[str, UserData] = {}
active_user_count: int = 0
class TransformationFlow(Flow[ComplexState]):
@start()
def initialize(self):
# Adiciona alguns usuários
self.add_user("alice", "Alice")
self.add_user("bob", "Bob")
self.add_user("charlie", "Charlie")
return "Initialized"
@listen(initialize)
def process_users(self, _):
# Incrementa contagens de login
for user_id in self.state.users:
self.increment_login(user_id)
# Desativa um usuário
self.deactivate_user("bob")
# Atualiza a contagem de ativos
self.update_active_count()
return f"Processed {len(self.state.users)} users"
# Métodos auxiliares para transformações de estado
def add_user(self, user_id: str, name: str):
self.state.users[user_id] = UserData(name=name)
self.update_active_count()
def increment_login(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].login_count += 1
def deactivate_user(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].active = False
self.update_active_count()
def update_active_count(self):
self.state.active_user_count = sum(
1 for user in self.state.users.values() if user.active
)
# código não traduzido
```
Esse padrão de criar métodos auxiliares mantém seus métodos de flow limpos, enquanto permite manipulações complexas de estado.
@@ -508,71 +238,7 @@ Um dos padrões mais poderosos na CrewAI é combinar o gerenciamento de estado d
Você pode usar o estado do flow para parametrizar crews:
```python
from crewai.flow.flow import Flow, listen, start
from crewai import Agent, Crew, Process, Task
from pydantic import BaseModel
class ResearchState(BaseModel):
topic: str = ""
depth: str = "medium"
results: str = ""
class ResearchFlow(Flow[ResearchState]):
@start()
def get_parameters(self):
# Em uma aplicação real, isso pode vir da entrada do usuário
self.state.topic = "Artificial Intelligence Ethics"
self.state.depth = "deep"
return "Parameters set"
@listen(get_parameters)
def execute_research(self, _):
# Cria os agentes
researcher = Agent(
role="Research Specialist",
goal=f"Research {self.state.topic} in {self.state.depth} detail",
backstory="You are an expert researcher with a talent for finding accurate information."
)
writer = Agent(
role="Content Writer",
goal="Transform research into clear, engaging content",
backstory="You excel at communicating complex ideas clearly and concisely."
)
# Cria as tarefas
research_task = Task(
description=f"Research {self.state.topic} with {self.state.depth} analysis",
expected_output="Comprehensive research notes in markdown format",
agent=researcher
)
writing_task = Task(
description=f"Create a summary on {self.state.topic} based on the research",
expected_output="Well-written article in markdown format",
agent=writer,
context=[research_task]
)
# Cria e executa a crew
research_crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process=Process.sequential,
verbose=True
)
# Executa a crew e armazena o resultado no estado
result = research_crew.kickoff()
self.state.results = result.raw
return "Research completed"
@listen(execute_research)
def summarize_results(self, _):
# Acessa os resultados armazenados
result_length = len(self.state.results)
return f"Research on {self.state.topic} completed with {result_length} characters of results."
# código não traduzido
```
### Manipulando Saídas de Crews no Estado
@@ -580,21 +246,7 @@ class ResearchFlow(Flow[ResearchState]):
Quando um crew finaliza, é possível processar sua saída e armazená-la no estado do flow:
```python
@listen(execute_crew)
def process_crew_results(self, _):
# Faz parsing dos resultados brutos (assumindo saída em JSON)
import json
try:
results_dict = json.loads(self.state.raw_results)
self.state.processed_results = {
"title": results_dict.get("title", ""),
"main_points": results_dict.get("main_points", []),
"conclusion": results_dict.get("conclusion", "")
}
return "Results processed successfully"
except json.JSONDecodeError:
self.state.error = "Failed to parse crew results as JSON"
return "Error processing results"
# código não traduzido
```
## Boas Práticas para Gerenciamento de Estado
@@ -604,19 +256,7 @@ def process_crew_results(self, _):
Projete seu estado para conter somente o necessário:
```python
# Abrangente demais
class BloatedState(BaseModel):
user_data: Dict = {}
system_settings: Dict = {}
temporary_calculations: List = []
debug_info: Dict = {}
# ...muitos outros campos
# Melhor: estado focado
class FocusedState(BaseModel):
user_id: str
preferences: Dict[str, str]
completion_status: Dict[str, bool]
# Exemplo não traduzido
```
### 2. Use Estado Estruturado em Flows Complexos
@@ -624,23 +264,7 @@ class FocusedState(BaseModel):
À medida que seus flows evoluem em complexidade, o estado estruturado se torna cada vez mais valioso:
```python
# Flow simples pode usar estado não estruturado
class SimpleGreetingFlow(Flow):
@start()
def greet(self):
self.state["name"] = "World"
return f"Hello, {self.state['name']}!"
# Flow complexo se beneficia de estado estruturado
class UserRegistrationState(BaseModel):
username: str
email: str
verification_status: bool = False
registration_date: datetime = Field(default_factory=datetime.now)
last_login: Optional[datetime] = None
class RegistrationFlow(Flow[UserRegistrationState]):
# Métodos com acesso ao estado fortemente tipado
# Exemplo não traduzido
```
### 3. Documente Transições de Estado
@@ -648,18 +272,7 @@ class RegistrationFlow(Flow[UserRegistrationState]):
Para flows complexos, documente como o estado muda ao longo da execução:
```python
@start()
def initialize_order(self):
"""
Initialize order state with empty values.
State before: {}
State after: {order_id: str, items: [], status: 'new'}
"""
self.state.order_id = str(uuid.uuid4())
self.state.items = []
self.state.status = "new"
return "Order initialized"
# Exemplo não traduzido
```
### 4. Trate Erros de Estado de Forma Elegante
@@ -667,18 +280,7 @@ def initialize_order(self):
Implemente tratamento de erros ao acessar o estado:
```python
@listen(previous_step)
def process_data(self, _):
try:
# Tenta acessar um valor que pode não existir
user_preference = self.state.preferences.get("theme", "default")
except (AttributeError, KeyError):
# Trata o erro de forma elegante
self.state.errors = self.state.get("errors", [])
self.state.errors.append("Failed to access preferences")
user_preference = "default"
return f"Used preference: {user_preference}"
# Exemplo não traduzido
```
### 5. Use o Estado Para Acompanhar o Progresso
@@ -686,30 +288,7 @@ def process_data(self, _):
Aproveite o estado para monitorar o progresso em flows de longa duração:
```python
class ProgressTrackingFlow(Flow):
@start()
def initialize(self):
self.state["total_steps"] = 3
self.state["current_step"] = 0
self.state["progress"] = 0.0
self.update_progress()
return "Initialized"
def update_progress(self):
"""Helper method to calculate and update progress"""
if self.state.get("total_steps", 0) > 0:
self.state["progress"] = (self.state.get("current_step", 0) /
self.state["total_steps"]) * 100
print(f"Progress: {self.state['progress']:.1f}%")
@listen(initialize)
def step_one(self, _):
# Realiza o trabalho...
self.state["current_step"] = 1
self.update_progress()
return "Step 1 complete"
# Etapas adicionais...
# Exemplo não traduzido
```
### 6. Prefira Operações Imutáveis Quando Possível
@@ -717,22 +296,7 @@ class ProgressTrackingFlow(Flow):
Especialmente com estado estruturado, prefira operações imutáveis para maior clareza:
```python
# Em vez de modificar listas no local:
self.state.items.append(new_item) # Operação mutável
# Considere criar um novo estado:
from pydantic import BaseModel
from typing import List
class ItemState(BaseModel):
items: List[str] = []
class ImmutableFlow(Flow[ItemState]):
@start()
def add_item(self):
# Cria uma nova lista com o item adicionado
self.state.items = [*self.state.items, "new item"]
return "Item added"
# Exemplo não traduzido
```
## Depurando o Estado do Flow
@@ -742,24 +306,7 @@ class ImmutableFlow(Flow[ItemState]):
Ao desenvolver, adicione logs para acompanhar mudanças no estado:
```python
import logging
logging.basicConfig(level=logging.INFO)
class LoggingFlow(Flow):
def log_state(self, step_name):
logging.info(f"State after {step_name}: {self.state}")
@start()
def initialize(self):
self.state["counter"] = 0
self.log_state("initialize")
return "Initialized"
@listen(initialize)
def increment(self, _):
self.state["counter"] += 1
self.log_state("increment")
return f"Incremented to {self.state['counter']}"
# Exemplo não traduzido
```
### Visualizando o Estado
@@ -767,30 +314,7 @@ class LoggingFlow(Flow):
Você pode adicionar métodos para visualizar seu estado durante o debug:
```python
def visualize_state(self):
"""Create a simple visualization of the current state"""
import json
from rich.console import Console
from rich.panel import Panel
console = Console()
if hasattr(self.state, "model_dump"):
# Pydantic v2
state_dict = self.state.model_dump()
elif hasattr(self.state, "dict"):
# Pydantic v1
state_dict = self.state.dict()
else:
# Estado não estruturado
state_dict = dict(self.state)
# Remove o id para uma saída mais limpa
if "id" in state_dict:
state_dict.pop("id")
state_json = json.dumps(state_dict, indent=2, default=str)
console.print(Panel(state_json, title="Current Flow State"))
# Exemplo não traduzido
```
## Conclusão

View File

@@ -797,6 +797,7 @@ As tabelas abaixo mostram uma amostra dos modelos de maior destaque em cada cate
Inicie com opções consagradas como **GPT-4.1**, **Claude 3.7 Sonnet** ou **Gemini 2.0 Flash**, que oferecem bom desempenho e ampla validação.
</Step>
{" "}
<Step title="Identifique Demandas Especializadas">
Descubra se sua crew possui requisitos específicos (código, raciocínio,
velocidade) que justifiquem modelos como **Claude 4 Sonnet** para
@@ -804,6 +805,7 @@ As tabelas abaixo mostram uma amostra dos modelos de maior destaque em cada cate
velocidade, considere Groq aliado à seleção do modelo.
</Step>
{" "}
<Step title="Implemente Estratégia Multi-Modelo">
Use modelos diferentes para agentes distintos conforme o papel. Modelos de
alta capacidade para managers e tarefas complexas, eficientes para rotinas.

View File

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

View File

@@ -1 +1 @@
__version__ = "1.14.6a1"
__version__ = "1.14.5a4"

View File

@@ -26,7 +26,6 @@ from crewai_cli.replay_from_task import replay_task_command
from crewai_cli.reset_memories_command import reset_memories_command
from crewai_cli.run_crew import run_crew
from crewai_cli.settings.main import SettingsCommand
from crewai_cli.skills.main import SkillCommand
from crewai_cli.task_outputs import load_task_outputs
from crewai_cli.tools.main import ToolCommand
from crewai_cli.train_crew import train_crew
@@ -547,56 +546,6 @@ def tool_publish(is_public: bool, force: bool) -> None:
tool_cmd.publish(is_public, force)
@crewai.group()
def skill() -> None:
"""Skill Repository related commands."""
@skill.command(name="create")
@click.argument("name")
@click.option(
"--no-project",
"in_project",
is_flag=True,
default=True,
flag_value=False,
help="Create skill in current dir instead of ./skills/",
)
def skill_create(name: str, in_project: bool) -> None:
skill_cmd = SkillCommand()
skill_cmd.create(name, in_project=in_project)
@skill.command(name="install")
@click.argument("ref")
def skill_install(ref: str) -> None:
skill_cmd = SkillCommand()
skill_cmd.install(ref)
@skill.command(name="publish")
@click.option(
"--force",
is_flag=True,
default=False,
show_default=True,
help="Skip git-state validation.",
)
@click.option("--public", "is_public", flag_value=True, default=False)
@click.option("--private", "is_public", flag_value=False)
@click.option("--org", default=None, help="Organisation slug (overrides settings).")
def skill_publish(is_public: bool, org: str | None, force: bool) -> None:
skill_cmd = SkillCommand()
skill_cmd.publish(is_public, org=org, force=force)
@skill.command(name="list")
def skill_list() -> None:
"""List locally installed skills."""
skill_cmd = SkillCommand()
skill_cmd.list_cached()
@crewai.group()
def template() -> None:
"""Browse and install project templates."""

View File

@@ -1,6 +1,5 @@
from typing import Any
from crewai_core.plus_api import CreateCrewPayload
from rich.console import Console
from crewai_cli import git
@@ -162,7 +161,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
self,
env_vars: dict[str, str],
remote_repo_url: str,
) -> CreateCrewPayload:
) -> dict[str, Any]:
"""
Create the payload for crew creation.
@@ -173,8 +172,6 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
Returns:
Dict[str, Any]: The payload for crew creation.
"""
if not self.project_name:
raise ValueError("project_name is required to create a deployment payload")
return {
"deploy": {
"name": self.project_name,

View File

@@ -1,4 +1,4 @@
from functools import cached_property
from functools import lru_cache
import subprocess
@@ -9,7 +9,7 @@ class Repository:
if not self.is_git_installed():
raise ValueError("Git is not installed or not found in your PATH.")
if not self.is_git_repo:
if not self.is_git_repo():
raise ValueError(f"{self.path} is not a Git repository.")
self.fetch()
@@ -40,9 +40,13 @@ class Repository:
encoding="utf-8",
).strip()
@cached_property
@lru_cache(maxsize=None) # noqa: B019
def is_git_repo(self) -> bool:
"""Check if the current directory is a git repository."""
"""Check if the current directory is a git repository.
Notes:
- TODO: This method is cached to avoid redundant checks, but using lru_cache on methods can lead to memory leaks
"""
try:
subprocess.check_output(
["git", "rev-parse", "--is-inside-work-tree"], # noqa: S607

View File

@@ -1,415 +0,0 @@
"""Skill Repository CLI commands for CrewAI."""
from __future__ import annotations
import base64
import io
import json
import os
from pathlib import Path
import tarfile
import zipfile
from rich.console import Console
from rich.table import Table
from crewai_cli.command import BaseCommand, PlusAPIMixin
from crewai_cli.config import Settings
from crewai_cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
console = Console()
_SKILL_MD_TEMPLATE = """\
---
name: {name}
version: 0.1.0
description: |
A short description of what this skill does.
---
## Instructions
Describe the skill behaviour here. This section is shown to the agent at activation time.
"""
class SkillCommand(BaseCommand, PlusAPIMixin):
"""Skill Repository related operations for CrewAI projects."""
def __init__(self) -> None:
BaseCommand.__init__(self)
PlusAPIMixin.__init__(self, telemetry=self._telemetry)
# ------------------------------------------------------------------
# create
# ------------------------------------------------------------------
def create(self, name: str, in_project: bool = True) -> None:
"""Scaffold a new skill directory.
If pyproject.toml is present (crew project), creates ./skills/{name}/.
Otherwise creates ./{name}/.
"""
if in_project and os.path.isfile("pyproject.toml"):
skill_dir = Path("skills") / name
else:
skill_dir = Path(name)
if skill_dir.exists():
console.print(f"[red]Directory {skill_dir} already exists.[/red]")
raise SystemExit(1)
skill_dir.mkdir(parents=True)
(skill_dir / "scripts").mkdir()
(skill_dir / "references").mkdir()
(skill_dir / "assets").mkdir()
skill_md = skill_dir / "SKILL.md"
skill_md.write_text(_SKILL_MD_TEMPLATE.format(name=name))
console.print(
f"[green]Created skill [bold]{name}[/bold] at [bold]{skill_dir}[/bold].[/green]"
)
console.print(f"Edit [bold]{skill_md}[/bold] to define the skill instructions.")
# ------------------------------------------------------------------
# install
# ------------------------------------------------------------------
def install(self, ref: str) -> None:
"""Download and install a registry skill.
Format: @org/name
Inside a crew project (pyproject.toml present): installs to ./skills/{name}/
Outside a project: installs to ~/.crewai/skills/{org}/{name}/
"""
if not ref.startswith("@"):
console.print(
"[red]Invalid skill reference. Use the format @org/name.[/red]"
)
raise SystemExit(1)
without_at = ref[1:]
if without_at.count("/") != 1:
console.print(
"[red]Invalid skill reference. Use the format @org/name.[/red]"
)
raise SystemExit(1)
org, name = without_at.split("/", 1)
if (
not org
or not name
or org.startswith(".")
or name.startswith(".")
or len(Path(org).parts) != 1
or len(Path(name).parts) != 1
):
console.print(
"[red]Invalid skill reference: org and name must be single, "
"non-empty path segments (no slashes, no '..').[/red]"
)
raise SystemExit(1)
self._print_current_organization()
console.print(f"[bold blue]Downloading skill {ref}...[/bold blue]")
get_response = self.plus_api_client.get_skill(org, name)
if get_response.status_code == 404:
console.print(
f"[red]Skill {ref} not found. Ensure it has been published and you have access.[/red]"
)
raise SystemExit(1)
if get_response.status_code != 200:
console.print(
f"[red]Failed to download skill {ref}: {get_response.status_code}[/red]"
)
raise SystemExit(1)
data = get_response.json()
version = data.get("latest_version") or data.get("version")
download_url = data.get("download_url")
if download_url:
import httpx
dl_response = httpx.get(download_url, follow_redirects=True)
dl_response.raise_for_status()
archive_bytes = dl_response.content
else:
encoded = data.get("file", "")
if "," in encoded:
encoded = encoded.split(",", 1)[1]
archive_bytes = base64.b64decode(encoded)
in_project = os.path.isfile("pyproject.toml")
if in_project:
dest = Path("skills") / name
dest.mkdir(parents=True, exist_ok=True)
self._unpack_archive(archive_bytes, dest)
console.print(
f"[green]Installed [bold]{ref}[/bold]{' (' + version + ')' if version else ''} to [bold]{dest}[/bold].[/green]"
)
else:
try:
from crewai.skills.cache import SkillCacheManager
cache = SkillCacheManager()
cache.store(org, name, version, archive_bytes)
except ImportError:
# Fallback if SDK not installed — write directly
cache_dir = Path.home() / ".crewai" / "skills" / org / name
if cache_dir.exists():
import shutil
shutil.rmtree(cache_dir)
cache_dir.mkdir(parents=True, exist_ok=True)
self._unpack_archive(archive_bytes, cache_dir)
# Write metadata so `crewai skill list` can discover it
from datetime import datetime, timezone
meta = {
"org": org,
"name": name,
"version": version,
"installed_at": datetime.now(tz=timezone.utc).isoformat(),
}
(cache_dir / ".crewai_meta.json").write_text(json.dumps(meta, indent=2))
console.print(
f"[green]Installed [bold]{ref}[/bold]{' (' + version + ')' if version else ''} to global cache.[/green]"
)
# ------------------------------------------------------------------
# publish
# ------------------------------------------------------------------
def publish(self, is_public: bool, org: str | None, force: bool = False) -> None:
"""Publish the skill in the current directory to the registry."""
skill_md = Path("SKILL.md")
if not skill_md.exists():
console.print(
"[red]No SKILL.md found in current directory. "
"Run this command from inside a skill directory.[/red]"
)
raise SystemExit(1)
# Parse frontmatter to extract name + version
try:
frontmatter = self._parse_frontmatter(skill_md.read_text())
except ValueError as exc:
console.print(f"[red]Failed to parse SKILL.md frontmatter: {exc}[/red]")
raise SystemExit(1) from exc
name = frontmatter.get("name")
version = frontmatter.get("version")
description = frontmatter.get("description")
if not name:
console.print(
"[red]SKILL.md frontmatter must include a 'name' field.[/red]"
)
raise SystemExit(1)
if not version:
console.print(
"[red]SKILL.md frontmatter must include a 'version' field before publishing.[/red]"
)
raise SystemExit(1)
settings = Settings()
effective_org = org or settings.org_name
if not effective_org:
console.print(
"[red]No organisation set. Run `crewai org switch <org_id>` first, "
"or pass --org.[/red]"
)
raise SystemExit(1)
self._print_current_organization()
console.print(
f"[bold blue]Publishing skill [bold]{name}[/bold] v{version} to {effective_org}...[/bold blue]"
)
archive_bytes = self._build_skill_tarball()
encoded_file = "data:application/x-gzip;base64," + base64.b64encode(
archive_bytes
).decode("utf-8")
response = self.plus_api_client.publish_skill(
org=effective_org,
name=name,
version=version,
is_public=is_public,
description=description,
encoded_file=encoded_file,
)
self._validate_response(response)
base_url = settings.enterprise_base_url or DEFAULT_CREWAI_ENTERPRISE_URL
console.print(
f"[green]Published [bold]{effective_org}/{name}[/bold] v{version}.\n\n"
"Security checks are running in the background. "
"Your skill will be available once checks complete.\n"
f"Monitor status at: {base_url}/crewai_plus/skills/{effective_org}/{name}[/green]"
)
# ------------------------------------------------------------------
# list_cached
# ------------------------------------------------------------------
def list_cached(self) -> None:
"""Show locally installed skills."""
table = Table(title="Installed Skills", show_lines=True)
table.add_column("Source", style="dim")
table.add_column("Ref")
table.add_column("Version")
table.add_column("Path")
# Project-local ./skills/
local_skills_dir = Path("skills")
if local_skills_dir.is_dir():
for skill_dir in sorted(local_skills_dir.iterdir()):
if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
version = self._read_version(skill_dir / "SKILL.md")
table.add_row(
"project",
skill_dir.name,
version or "-",
str(skill_dir),
)
# Global cache
cache_root = Path.home() / ".crewai" / "skills"
if cache_root.exists():
for org_dir in sorted(cache_root.iterdir()):
if not org_dir.is_dir():
continue
for skill_dir in sorted(org_dir.iterdir()):
meta_file = skill_dir / ".crewai_meta.json"
if meta_file.exists():
try:
meta = json.loads(meta_file.read_text())
table.add_row(
"cache",
f"@{meta['org']}/{meta['name']}",
meta.get("version") or "-",
str(skill_dir),
)
except (json.JSONDecodeError, KeyError):
console.print(
f"[yellow]Warning: skipping malformed cache entry at {meta_file}[/yellow]"
)
console.print(table)
# ------------------------------------------------------------------
# internal helpers
# ------------------------------------------------------------------
def _print_current_organization(self) -> None:
settings = Settings()
if settings.org_uuid:
console.print(
f"Current organization: {settings.org_name} ({settings.org_uuid})",
style="bold blue",
)
else:
console.print(
"No organization currently set. We recommend setting one before using: "
"`crewai org switch <org_id>` command.",
style="yellow",
)
def _unpack_archive(self, archive_bytes: bytes, dest: Path) -> None:
"""Unpack a .tar.gz or .zip archive into dest."""
# Try tar first, then zip
try:
with tarfile.open(fileobj=io.BytesIO(archive_bytes), mode="r:gz") as tf:
try:
tf.extractall(dest, filter="data")
except TypeError:
_safe_extractall(tf, dest)
return
except tarfile.TarError:
pass
# Fallback: zip
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as zf:
_safe_extract_zip(zf, dest)
def _build_skill_tarball(self) -> bytes:
"""Build an in-memory .tar.gz of SKILL.md + scripts/ + references/ + assets/."""
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tf:
tf.add("SKILL.md")
for folder in ("scripts", "references", "assets"):
folder_path = Path(folder)
if folder_path.is_dir():
for fpath in sorted(folder_path.rglob("*")):
if fpath.is_file():
tf.add(str(fpath))
return buf.getvalue()
def _parse_frontmatter(self, content: str) -> dict[str, str]:
"""Extract YAML frontmatter fields from a SKILL.md string.
Reuses crewai.skills.parser when available, with a minimal
fallback for environments where the full SDK isn't installed.
"""
try:
from crewai.skills.parser import parse_frontmatter
fm_dict, _ = parse_frontmatter(content)
return fm_dict
except ImportError:
pass
# Fallback: minimal YAML parsing without SDK dependency
import re
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not match:
raise ValueError("No YAML frontmatter block found")
try:
import yaml
return yaml.safe_load(match.group(1)) or {}
except ImportError:
result: dict[str, str] = {}
for line in match.group(1).splitlines():
if ":" in line:
key, _, value = line.partition(":")
result[key.strip()] = value.strip()
return result
def _read_version(self, skill_md: Path) -> str | None:
"""Read the version field from a SKILL.md file, or None."""
try:
fm = self._parse_frontmatter(skill_md.read_text())
return fm.get("version")
except Exception:
return None
def _safe_extractall(tf: tarfile.TarFile, dest: Path) -> None:
"""Path-traversal-safe extraction for Python < 3.12."""
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}")
tf.extractall(dest) # noqa: S202
def _safe_extract_zip(zf: zipfile.ZipFile, dest: Path) -> None:
"""Path-traversal-safe ZIP extraction."""
dest_resolved = dest.resolve()
for member in zf.namelist():
member_path = (dest / member).resolve()
if not member_path.is_relative_to(dest_resolved):
raise ValueError(f"Blocked path traversal attempt: {member!r}")
zf.extractall(dest) # noqa: S202

View File

@@ -1,205 +0,0 @@
"""Tests for SkillCommand CLI."""
from __future__ import annotations
import io
import os
import tempfile
import zipfile
from contextlib import contextmanager
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from crewai_cli.shared.token_manager import TokenManager
@contextmanager
def in_temp_dir():
original = os.getcwd()
with tempfile.TemporaryDirectory() as td:
os.chdir(td)
try:
yield td
finally:
os.chdir(original)
@pytest.fixture
def skill_command():
with tempfile.TemporaryDirectory() as temp_dir:
with patch.object(
TokenManager, "_get_secure_storage_path", return_value=Path(temp_dir)
):
TokenManager().save_tokens(
"test-token", (datetime.now() + timedelta(seconds=36000)).timestamp()
)
from crewai_cli.skills.main import SkillCommand
cmd = SkillCommand()
yield cmd
# ---------------------------------------------------------------------------
# create
# ---------------------------------------------------------------------------
class TestSkillCreate:
def test_create_in_project(self, skill_command, tmp_path):
with in_temp_dir():
# Simulate being inside a project
Path("pyproject.toml").write_text("[tool.poetry]\nname = 'test'\n")
skill_command.create("my-skill")
assert Path("skills/my-skill/SKILL.md").exists()
assert Path("skills/my-skill/scripts").is_dir()
assert Path("skills/my-skill/references").is_dir()
assert Path("skills/my-skill/assets").is_dir()
def test_create_outside_project(self, skill_command, tmp_path):
with in_temp_dir():
skill_command.create("standalone-skill", in_project=False)
assert Path("standalone-skill/SKILL.md").exists()
def test_create_adds_name_to_skill_md(self, skill_command):
with in_temp_dir():
skill_command.create("hello-world", in_project=False)
content = Path("hello-world/SKILL.md").read_text()
assert "name: hello-world" in content
assert "version: 0.1.0" in content
def test_create_fails_if_dir_exists(self, skill_command):
with in_temp_dir():
Path("existing-skill").mkdir()
with pytest.raises(SystemExit):
skill_command.create("existing-skill", in_project=False)
# ---------------------------------------------------------------------------
# install
# ---------------------------------------------------------------------------
class TestSkillInstall:
def _zip_skill(self, name: str) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("SKILL.md", f"---\nname: {name}\ndescription: Test.\n---\nInstructions.")
return buf.getvalue()
def test_install_invalid_ref_no_at(self, skill_command):
with pytest.raises(SystemExit):
skill_command.install("acme/my-skill")
def test_install_invalid_ref_no_slash(self, skill_command):
with pytest.raises(SystemExit):
skill_command.install("@acmeskill")
def test_install_404(self, skill_command):
mock_resp = MagicMock()
mock_resp.status_code = 404
skill_command.plus_api_client.get_skill = MagicMock(return_value=mock_resp)
with pytest.raises(SystemExit):
skill_command.install("@acme/ghost")
def test_install_in_project(self, skill_command):
import base64
archive = self._zip_skill("my-skill")
encoded = "data:application/zip;base64," + base64.b64encode(archive).decode()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"file": encoded, "version": "1.0.0"}
skill_command.plus_api_client.get_skill = MagicMock(return_value=mock_resp)
with in_temp_dir():
Path("pyproject.toml").write_text("[tool]\n")
skill_command.install("@acme/my-skill")
assert Path("skills/my-skill/SKILL.md").exists()
# ---------------------------------------------------------------------------
# publish
# ---------------------------------------------------------------------------
class TestSkillPublish:
def test_publish_no_skill_md(self, skill_command):
with in_temp_dir():
with pytest.raises(SystemExit):
skill_command.publish(is_public=True, org="acme")
def test_publish_missing_version(self, skill_command):
with in_temp_dir():
Path("SKILL.md").write_text(
"---\nname: my-skill\ndescription: Test.\n---\nInstructions."
)
with pytest.raises(SystemExit):
skill_command.publish(is_public=True, org="acme")
def test_publish_missing_name(self, skill_command):
with in_temp_dir():
Path("SKILL.md").write_text(
"---\ndescription: Test.\nversion: 1.0.0\n---\nInstructions."
)
with pytest.raises(SystemExit):
skill_command.publish(is_public=True, org="acme")
def test_publish_no_org(self, skill_command):
with in_temp_dir():
Path("SKILL.md").write_text(
"---\nname: my-skill\nversion: 1.0.0\ndescription: Test.\n---\nInstructions."
)
with patch.object(skill_command, "plus_api_client") as mock_client:
mock_resp = MagicMock()
mock_resp.is_success = True
mock_resp.status_code = 200
mock_resp.json.return_value = {}
mock_client.publish_skill.return_value = mock_resp
# No org set → should SystemExit (no org_name in settings)
with patch("crewai_cli.skills.main.Settings") as mock_settings_cls:
mock_settings_cls.return_value.org_name = None
mock_settings_cls.return_value.enterprise_base_url = None
with pytest.raises(SystemExit):
skill_command.publish(is_public=True, org=None)
def test_publish_calls_api(self, skill_command):
with in_temp_dir():
Path("SKILL.md").write_text(
"---\nname: my-skill\nversion: 1.0.0\ndescription: A test skill.\n---\nInstructions."
)
mock_resp = MagicMock()
mock_resp.is_success = True
mock_resp.status_code = 200
mock_resp.json.return_value = {}
skill_command.plus_api_client.publish_skill = MagicMock(return_value=mock_resp)
with patch("crewai_cli.skills.main.Settings") as mock_settings_cls:
mock_settings_cls.return_value.org_name = "acme"
mock_settings_cls.return_value.enterprise_base_url = None
skill_command.publish(is_public=False, org="acme")
skill_command.plus_api_client.publish_skill.assert_called_once()
call_kwargs = skill_command.plus_api_client.publish_skill.call_args
assert call_kwargs.kwargs["name"] == "my-skill"
assert call_kwargs.kwargs["version"] == "1.0.0"
# ---------------------------------------------------------------------------
# list_cached
# ---------------------------------------------------------------------------
class TestSkillListCached:
def test_list_cached_empty(self, skill_command, capsys):
with in_temp_dir():
skill_command.list_cached()
# Should not raise
def test_list_cached_shows_project_skills(self, skill_command, capsys):
with in_temp_dir():
skill_dir = Path("skills/my-skill")
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(
"---\nname: my-skill\nversion: 0.5.0\ndescription: A skill.\n---\nBody."
)
skill_command.list_cached()
# Should complete without error

View File

@@ -1 +1 @@
__version__ = "1.14.6a1"
__version__ = "1.14.5a4"

View File

@@ -3,162 +3,36 @@
from __future__ import annotations
import os
from typing import Any, Final, Literal, TypedDict, cast
from typing import Any
from urllib.parse import urljoin
import httpx
from typing_extensions import NotRequired
from crewai_core.constants import DEFAULT_CREWAI_ENTERPRISE_URL
from crewai_core.settings import Settings
from crewai_core.version import get_crewai_version
HttpMethod = Literal["GET", "POST", "PATCH", "DELETE"]
class AvailableExport(TypedDict):
name: str
class EnvVarEntry(TypedDict):
name: str
description: str
required: bool
default: str | None
class ToolMetadata(TypedDict):
name: str
module: str
humanized_name: str
description: str
run_params_schema: dict[str, Any]
init_params_schema: dict[str, Any]
env_vars: list[EnvVarEntry]
class ToolsMetadataPayload(TypedDict):
package: str
tools: list[ToolMetadata] | None
class PublishToolPayload(TypedDict):
handle: str
public: bool
version: str
file: str
description: str | None
available_exports: list[AvailableExport] | None
tools_metadata: ToolsMetadataPayload | None
class CrewDeploymentSpec(TypedDict):
name: str
repo_clone_url: str
env: dict[str, str]
class CreateCrewPayload(TypedDict):
deploy: CrewDeploymentSpec
class _WithUserIdentifier(TypedDict):
user_identifier: NotRequired[str]
class LoginPayload(_WithUserIdentifier):
pass
class TraceExecutionContext(TypedDict):
crew_fingerprint: str | None
crew_name: str | None
flow_name: str | None
crewai_version: str
privacy_level: str
class TraceExecutionMetadata(TypedDict):
expected_duration_estimate: int
agent_count: int
task_count: int
flow_method_count: int
execution_started_at: str
class TraceBatchInitPayload(_WithUserIdentifier):
trace_id: str
execution_type: str
execution_context: TraceExecutionContext
execution_metadata: TraceExecutionMetadata
ephemeral_trace_id: NotRequired[str]
class TraceBatchMetadata(TypedDict):
events_count: int
batch_sequence: int
is_final_batch: bool
class TraceEventsPayload(TypedDict):
events: list[dict[str, Any]]
batch_metadata: TraceBatchMetadata
class TraceFinalizePayload(TypedDict):
status: Literal["completed"]
duration_ms: float | None
final_event_count: int
class TraceFailedPayload(TypedDict):
status: Literal["failed"]
failure_reason: str
Headers = TypedDict(
"Headers",
{
"Content-Type": str,
"User-Agent": str,
"X-Crewai-Version": str,
"Authorization": NotRequired[str],
"X-Crewai-Organization-Id": NotRequired[str],
},
)
class RequestKwargs(TypedDict):
headers: dict[str, str]
json: NotRequired[Any]
params: NotRequired[dict[str, str]]
timeout: NotRequired[float]
class PlusAPI:
"""Client for working with the CrewAI+ API."""
TOOLS_RESOURCE: Final = "/crewai_plus/api/v1/tools"
SKILLS_RESOURCE: Final = "/crewai_plus/api/v1/skills"
ORGANIZATIONS_RESOURCE: Final = "/crewai_plus/api/v1/me/organizations"
CREWS_RESOURCE: Final = "/crewai_plus/api/v1/crews"
AGENTS_RESOURCE: Final = "/crewai_plus/api/v1/agents"
TRACING_RESOURCE: Final = "/crewai_plus/api/v1/tracing"
EPHEMERAL_TRACING_RESOURCE: Final = "/crewai_plus/api/v1/tracing/ephemeral"
INTEGRATIONS_RESOURCE: Final = "/crewai_plus/api/v1/integrations"
TOOLS_RESOURCE = "/crewai_plus/api/v1/tools"
ORGANIZATIONS_RESOURCE = "/crewai_plus/api/v1/me/organizations"
CREWS_RESOURCE = "/crewai_plus/api/v1/crews"
AGENTS_RESOURCE = "/crewai_plus/api/v1/agents"
TRACING_RESOURCE = "/crewai_plus/api/v1/tracing"
EPHEMERAL_TRACING_RESOURCE = "/crewai_plus/api/v1/tracing/ephemeral"
INTEGRATIONS_RESOURCE = "/crewai_plus/api/v1/integrations"
def __init__(self, api_key: str | None = None) -> None:
version = get_crewai_version()
self.api_key = api_key
self.headers: Headers = {
self.headers = {
"Content-Type": "application/json",
"User-Agent": f"CrewAI-CLI/{version}",
"X-Crewai-Version": version,
"User-Agent": f"CrewAI-CLI/{get_crewai_version()}",
"X-Crewai-Version": get_crewai_version(),
}
if api_key:
self.headers["Authorization"] = f"Bearer {api_key}"
settings = Settings()
if settings.org_uuid:
self.headers["X-Crewai-Organization-Id"] = settings.org_uuid
@@ -170,30 +44,17 @@ class PlusAPI:
)
def _make_request(
self,
method: HttpMethod,
endpoint: str,
*,
json: Any = None,
params: dict[str, str] | None = None,
timeout: float | None = None,
verify: bool = True,
self, method: str, endpoint: str, **kwargs: Any
) -> httpx.Response:
url = urljoin(self.base_url, endpoint)
request_kwargs: RequestKwargs = {"headers": cast(dict[str, str], self.headers)}
if json is not None:
request_kwargs["json"] = json
if params is not None:
request_kwargs["params"] = params
if timeout is not None:
request_kwargs["timeout"] = timeout
verify = kwargs.pop("verify", True)
with httpx.Client(trust_env=False, verify=verify) as client:
return client.request(method, url, **request_kwargs)
return client.request(method, url, headers=self.headers, **kwargs)
def login_to_tool_repository(
self, user_identifier: str | None = None
) -> httpx.Response:
payload: LoginPayload = {}
payload = {}
if user_identifier:
payload["user_identifier"] = user_identifier
return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login", json=payload)
@@ -204,7 +65,7 @@ class PlusAPI:
async def get_agent(self, handle: str) -> httpx.Response:
url = urljoin(self.base_url, f"{self.AGENTS_RESOURCE}/{handle}")
async with httpx.AsyncClient() as client:
return await client.get(url, headers=cast(dict[str, str], self.headers))
return await client.get(url, headers=self.headers)
def publish_tool(
self,
@@ -213,10 +74,10 @@ class PlusAPI:
version: str,
description: str | None,
encoded_file: str,
available_exports: list[AvailableExport] | None = None,
tools_metadata: list[ToolMetadata] | None = None,
available_exports: list[dict[str, Any]] | None = None,
tools_metadata: list[dict[str, Any]] | None = None,
) -> httpx.Response:
params: PublishToolPayload = {
params = {
"handle": handle,
"public": is_public,
"version": version,
@@ -229,47 +90,6 @@ class PlusAPI:
}
return self._make_request("POST", f"{self.TOOLS_RESOURCE}", json=params)
def get_skill(
self, org: str, name: str, version: str | None = None
) -> httpx.Response:
params: dict[str, str] = {}
if version is not None:
params["version"] = version
return self._make_request(
"GET",
f"{self.SKILLS_RESOURCE}/{org}/{name}",
params=params or None,
)
def publish_skill(
self,
org: str,
name: str,
version: str,
is_public: bool,
description: str | None,
encoded_file: str,
) -> httpx.Response:
payload = {
"org": org,
"name": name,
"version": version,
"public": is_public,
"description": description,
"file": encoded_file,
}
return self._make_request("POST", self.SKILLS_RESOURCE, json=payload)
def list_skills(self, org: str | None = None) -> httpx.Response:
params: dict[str, str] = {}
if org is not None:
params["org"] = org
return self._make_request(
"GET",
self.SKILLS_RESOURCE,
params=params or None,
)
def deploy_by_name(self, project_name: str) -> httpx.Response:
return self._make_request(
"POST", f"{self.CREWS_RESOURCE}/by-name/{project_name}/deploy"
@@ -309,13 +129,13 @@ class PlusAPI:
def list_crews(self) -> httpx.Response:
return self._make_request("GET", self.CREWS_RESOURCE)
def create_crew(self, payload: CreateCrewPayload) -> httpx.Response:
def create_crew(self, payload: dict[str, Any]) -> httpx.Response:
return self._make_request("POST", self.CREWS_RESOURCE, json=payload)
def get_organizations(self) -> httpx.Response:
return self._make_request("GET", self.ORGANIZATIONS_RESOURCE)
def initialize_trace_batch(self, payload: TraceBatchInitPayload) -> httpx.Response:
def initialize_trace_batch(self, payload: dict[str, Any]) -> httpx.Response:
return self._make_request(
"POST",
f"{self.TRACING_RESOURCE}/batches",
@@ -324,7 +144,7 @@ class PlusAPI:
)
def initialize_ephemeral_trace_batch(
self, payload: TraceBatchInitPayload
self, payload: dict[str, Any]
) -> httpx.Response:
return self._make_request(
"POST",
@@ -333,7 +153,7 @@ class PlusAPI:
)
def send_trace_events(
self, trace_batch_id: str, payload: TraceEventsPayload
self, trace_batch_id: str, payload: dict[str, Any]
) -> httpx.Response:
return self._make_request(
"POST",
@@ -343,7 +163,7 @@ class PlusAPI:
)
def send_ephemeral_trace_events(
self, trace_batch_id: str, payload: TraceEventsPayload
self, trace_batch_id: str, payload: dict[str, Any]
) -> httpx.Response:
return self._make_request(
"POST",
@@ -353,7 +173,7 @@ class PlusAPI:
)
def finalize_trace_batch(
self, trace_batch_id: str, payload: TraceFinalizePayload
self, trace_batch_id: str, payload: dict[str, Any]
) -> httpx.Response:
return self._make_request(
"PATCH",
@@ -363,7 +183,7 @@ class PlusAPI:
)
def finalize_ephemeral_trace_batch(
self, trace_batch_id: str, payload: TraceFinalizePayload
self, trace_batch_id: str, payload: dict[str, Any]
) -> httpx.Response:
return self._make_request(
"PATCH",
@@ -375,28 +195,20 @@ class PlusAPI:
def mark_trace_batch_as_failed(
self, trace_batch_id: str, error_message: str
) -> httpx.Response:
payload: TraceFailedPayload = {
"status": "failed",
"failure_reason": error_message,
}
return self._make_request(
"PATCH",
f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}",
json=payload,
json={"status": "failed", "failure_reason": error_message},
timeout=30,
)
def mark_ephemeral_trace_batch_as_failed(
self, trace_batch_id: str, error_message: str
) -> httpx.Response:
payload: TraceFailedPayload = {
"status": "failed",
"failure_reason": error_message,
}
return self._make_request(
"PATCH",
f"{self.EPHEMERAL_TRACING_RESOURCE}/batches/{trace_batch_id}",
json=payload,
json={"status": "failed", "failure_reason": error_message},
timeout=30,
)

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.14.6a1"
__version__ = "1.14.5a4"

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.6a1",
"crewai==1.14.5a4",
"tiktoken>=0.8.0,<0.13",
"beautifulsoup4~=4.13.4",
"python-docx~=1.2.0",
@@ -107,7 +107,7 @@ stagehand = [
"stagehand>=0.4.1",
]
github = [
"gitpython>=3.1.50,<4",
"gitpython>=3.1.47,<4",
"PyGithub==1.59.1",
]
rag = [

View File

@@ -330,4 +330,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.14.6a1"
__version__ = "1.14.5a4"

View File

@@ -5,8 +5,8 @@ from builtins import type as type_
import logging
import posixpath
import shlex
from typing import Any, Literal
import uuid
from typing import Any, Literal
from pydantic import BaseModel, Field, model_validator
@@ -33,63 +33,7 @@ FileAction = Literal[
]
def _daytona_file_schema_extra(schema: dict[str, Any]) -> None:
schema["allOf"] = [
{
"if": {
"properties": {
"action": {
"enum": [
"read",
"write",
"append",
"list",
"delete",
"mkdir",
"info",
"exists",
"move",
"find",
"search",
"chmod",
]
}
}
},
"then": {"required": ["path"]},
},
{
"if": {"properties": {"action": {"const": "append"}}},
"then": {"required": ["content"]},
},
{
"if": {"properties": {"action": {"const": "move"}}},
"then": {"required": ["destination"]},
},
{
"if": {"properties": {"action": {"enum": ["find", "search"]}}},
"then": {"required": ["pattern"]},
},
{
"if": {"properties": {"action": {"const": "replace"}}},
"then": {"required": ["paths", "pattern", "replacement"]},
},
{
"if": {"properties": {"action": {"const": "chmod"}}},
"then": {
"anyOf": [
{"required": ["mode"]},
{"required": ["owner"]},
{"required": ["group"]},
]
},
},
]
class DaytonaFileToolSchema(BaseModel):
model_config = {"json_schema_extra": _daytona_file_schema_extra}
action: FileAction = Field(
...,
description=(

View File

@@ -103,7 +103,7 @@ class MongoDBVectorSearchTool(BaseTool):
),
]
)
package_dependencies: list[str] = Field(default_factory=lambda: ["pymongo"])
package_dependencies: list[str] = Field(default_factory=lambda: ["mongdb"])
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)

View File

@@ -7184,7 +7184,7 @@
}
},
{
"description": "Perform filesystem operations inside a Daytona sandbox: read, write, append, list, delete, mkdir, info, exists, move, find (content grep), search (filename glob), chmod (permissions/owner/group), and replace (bulk find-and-replace across files). For files larger than a few KB, create the file with action='write' and empty content, then send the body via multiple 'append' calls of ~4KB each to stay within tool-call payload limits.",
"description": "Perform filesystem operations inside a Daytona sandbox: read a file, write content to a path, append content to an existing file, list a directory, delete a path, make a directory, or fetch file metadata. For files larger than a few KB, create the file with action='write' and empty content, then send the body via multiple 'append' calls of ~4KB each to stay within tool-call payload limits.",
"env_vars": [
{
"default": null,
@@ -7334,127 +7334,9 @@
"daytona"
],
"run_params_schema": {
"allOf": [
{
"if": {
"properties": {
"action": {
"enum": [
"read",
"write",
"append",
"list",
"delete",
"mkdir",
"info",
"exists",
"move",
"find",
"search",
"chmod"
]
}
}
},
"then": {
"required": [
"path"
]
}
},
{
"if": {
"properties": {
"action": {
"const": "append"
}
}
},
"then": {
"required": [
"content"
]
}
},
{
"if": {
"properties": {
"action": {
"const": "move"
}
}
},
"then": {
"required": [
"destination"
]
}
},
{
"if": {
"properties": {
"action": {
"enum": [
"find",
"search"
]
}
}
},
"then": {
"required": [
"pattern"
]
}
},
{
"if": {
"properties": {
"action": {
"const": "replace"
}
}
},
"then": {
"required": [
"paths",
"pattern",
"replacement"
]
}
},
{
"if": {
"properties": {
"action": {
"const": "chmod"
}
}
},
"then": {
"anyOf": [
{
"required": [
"mode"
]
},
{
"required": [
"owner"
]
},
{
"required": [
"group"
]
}
]
}
}
],
"properties": {
"action": {
"description": "The filesystem action to perform: 'read' (returns file contents); 'write' (create or replace a file with content); 'append' (append content to an existing file \u2014 use this for writing large files in chunks to avoid hitting tool-call size limits); 'list' (lists a directory); 'delete' (removes a file/dir); 'mkdir' (creates a directory); 'info' (returns file metadata); 'exists' (returns whether a path exists); 'move' (rename or relocate a file/dir; requires 'destination'); 'find' (grep file CONTENTS recursively; requires 'pattern'); 'search' (find files by NAME pattern; requires 'pattern'); 'chmod' (change permissions/owner/group; pass at least one of 'mode', 'owner', 'group'); 'replace' (find-and-replace text across files; requires 'paths', 'pattern', and 'replacement').",
"description": "The filesystem action to perform: 'read' (returns file contents), 'write' (create or replace a file with content), 'append' (append content to an existing file \u2014 use this for writing large files in chunks to avoid hitting tool-call size limits), 'list' (lists a directory), 'delete' (removes a file/dir), 'mkdir' (creates a directory), 'info' (returns file metadata).",
"enum": [
"read",
"write",
@@ -7462,13 +7344,7 @@
"list",
"delete",
"mkdir",
"info",
"exists",
"move",
"find",
"search",
"chmod",
"replace"
"info"
],
"title": "Action",
"type": "string"
@@ -7492,122 +7368,27 @@
"description": "Content to write or append. If omitted for 'write', an empty file is created. For files larger than a few KB, prefer one 'write' with empty content followed by multiple 'append' calls of ~4KB each to stay within tool-call payload limits.",
"title": "Content"
},
"destination": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "For action='move': absolute destination path.",
"title": "Destination"
},
"group": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "For action='chmod': new file group.",
"title": "Group"
},
"mode": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Octal permission string. For 'mkdir' it sets the new directory permissions (defaults to '0755' if omitted). For 'chmod' it sets the target's mode (e.g. '755' to make a script executable). Ignored for other actions.",
"title": "Mode"
},
"owner": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "For action='chmod': new file owner (user name).",
"title": "Owner"
"default": "0755",
"description": "For action='mkdir': octal permission string (default 0755).",
"title": "Mode",
"type": "string"
},
"path": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Absolute path inside the sandbox. Required for all actions except 'replace' (which uses 'paths' instead).",
"title": "Path"
},
"paths": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "For action='replace': list of absolute file paths in which to replace 'pattern' with 'replacement'.",
"title": "Paths"
},
"pattern": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "For 'find': substring matched against file CONTENTS. For 'search': glob-style pattern matched against file NAMES (e.g. '*.py'). For 'replace': text to replace inside files.",
"title": "Pattern"
"description": "Absolute path inside the sandbox.",
"title": "Path",
"type": "string"
},
"recursive": {
"default": false,
"description": "For action='delete': remove directories recursively.",
"title": "Recursive",
"type": "boolean"
},
"replacement": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "For action='replace': replacement text for 'pattern'.",
"title": "Replacement"
}
},
"required": [
"action"
"action",
"path"
],
"title": "DaytonaFileToolSchema",
"type": "object"
@@ -14633,7 +14414,7 @@
},
"name": "MongoDBVectorSearchTool",
"package_dependencies": [
"pymongo"
"mongdb"
],
"run_params_schema": {
"description": "Input for MongoDBTool.",

View File

@@ -8,8 +8,8 @@ authors = [
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai-core==1.14.6a1",
"crewai-cli==1.14.6a1",
"crewai-core==1.14.5a4",
"crewai-cli==1.14.5a4",
# Core Dependencies
"pydantic>=2.11.9,<2.13",
"openai>=2.30.0,<3",
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.14.6a1",
"crewai-tools==1.14.5a4",
]
embeddings = [
"tiktoken>=0.8.0,<0.13"
@@ -105,7 +105,7 @@ a2a = [
"aiocache[redis,memcached]~=0.12.3",
]
file-processing = [
"crewai-files",
"crewai-files==1.14.5a4",
]
qdrant-edge = [
"qdrant-edge-py>=0.6.0",

View File

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

View File

@@ -7,7 +7,6 @@ from collections.abc import Callable, Coroutine, Sequence
import concurrent.futures
import contextvars
from datetime import datetime
import inspect
import json
import os
from pathlib import Path
@@ -36,11 +35,13 @@ from typing_extensions import Self, TypeIs
from crewai.agent.planning_config import PlanningConfig
from crewai.agent.utils import (
ahandle_knowledge_retrieval,
append_skill_context,
apply_training_data,
build_task_prompt_with_schema,
format_task_with_context,
get_knowledge_config,
handle_knowledge_retrieval,
handle_reasoning,
prepare_tools,
process_tool_results,
save_last_messages,
@@ -149,17 +150,7 @@ def _validate_executor_class(value: Any) -> Any:
cls = _EXECUTOR_CLASS_MAP.get(value)
if cls is None:
raise ValueError(f"Unknown executor class: {value}")
value = cls
import warnings
if value is CrewAgentExecutor:
warnings.warn(
"CrewAgentExecutor is deprecated and will be removed in a future release. "
"Agents inside Crews now use AgentExecutor by default. "
"Switch to crewai.experimental.AgentExecutor.",
DeprecationWarning,
stacklevel=3,
)
return cls
return value
@@ -220,11 +211,7 @@ class Agent(BaseAgent):
str | BaseLLM | None,
BeforeValidator(_validate_llm_ref),
PlainSerializer(_serialize_llm_ref, return_type=dict | None, when_used="json"),
] = Field(
description="Language model that will run the agent.",
default=None,
deprecated="function_calling_llm is deprecated and will be removed in a future release.",
)
] = Field(description="Language model that will run the agent.", default=None)
system_template: str | None = Field(
default=None, description="System format for the agent."
)
@@ -338,8 +325,8 @@ class Agent(BaseAgent):
BeforeValidator(_validate_executor_class),
PlainSerializer(_serialize_executor_class, return_type=str, when_used="json"),
] = Field(
default=AgentExecutor,
description="Class to use for the agent executor. Defaults to AgentExecutor, can optionally use CrewAgentExecutor.",
default=CrewAgentExecutor,
description="Class to use for the agent executor. Defaults to CrewAgentExecutor, can optionally use AgentExecutor.",
)
@model_validator(mode="before")
@@ -434,7 +421,7 @@ class Agent(BaseAgent):
from crewai.crew import Crew
if resolved_crew_skills is None:
crew_skills: list[Path | SkillModel | str] | None = (
crew_skills: list[Path | SkillModel] | None = (
self.crew.skills
if isinstance(self.crew, Crew) and isinstance(self.crew.skills, list)
else None
@@ -446,7 +433,7 @@ class Agent(BaseAgent):
return
needs_work = self.skills and any(
isinstance(s, (Path, str))
isinstance(s, Path)
or (isinstance(s, SkillModel) and s.disclosure_level < INSTRUCTIONS)
for s in self.skills
)
@@ -454,28 +441,14 @@ class Agent(BaseAgent):
return
seen: set[str] = set()
resolved: list[Path | SkillModel | str] = []
items: list[Path | SkillModel | str] = list(self.skills) if self.skills else []
resolved: list[Path | SkillModel] = []
items: list[Path | SkillModel] = list(self.skills) if self.skills else []
if crew_skills:
items.extend(crew_skills)
for item in items:
if isinstance(item, str):
from crewai.skills.registry import (
is_registry_ref,
parse_registry_ref,
resolve_registry_ref,
)
if is_registry_ref(item):
skill = resolve_registry_ref(item, source=self)
org, _ = parse_registry_ref(item)
dedup_key = f"{org}/{skill.name}"
if dedup_key not in seen:
seen.add(dedup_key)
resolved.append(skill)
elif isinstance(item, Path):
if isinstance(item, Path):
discovered = discover_skills(item, source=self)
for skill in discovered:
if skill.name not in seen:
@@ -539,6 +512,8 @@ class Agent(BaseAgent):
The task prompt after memory retrieval, ready for knowledge lookup.
"""
get_env_context()
if self.executor_class is not AgentExecutor:
handle_reasoning(self, task)
self._inject_date_to_task(task)
@@ -566,6 +541,7 @@ class Agent(BaseAgent):
Returns:
The fully prepared task prompt.
"""
task_prompt = append_skill_context(self, task_prompt)
prepare_tools(self, tools, task)
return apply_training_data(self, task_prompt)
@@ -867,22 +843,18 @@ class Agent(BaseAgent):
if not self.agent_executor:
raise RuntimeError("Agent executor is not initialized.")
invoke_result = self.agent_executor.invoke(
{
"input": task_prompt,
"tool_names": self.agent_executor.tools_names,
"tools": self.agent_executor.tools_description,
"ask_for_human_input": task.human_input,
}
result = cast(
dict[str, Any],
self.agent_executor.invoke(
{
"input": task_prompt,
"tool_names": self.agent_executor.tools_names,
"tools": self.agent_executor.tools_description,
"ask_for_human_input": task.human_input,
}
),
)
if inspect.isawaitable(invoke_result):
invoke_result.close()
raise RuntimeError(
"Agent execution was invoked synchronously from within a running "
"event loop. Use `agent.kickoff_async()` / `crew.kickoff_async()` "
"(or `await agent.aexecute_task(...)`) when calling from async code."
)
return invoke_result["output"]
return result["output"]
async def aexecute_task(
self,
@@ -1502,6 +1474,8 @@ class Agent(BaseAgent):
),
)
formatted_messages = append_skill_context(self, formatted_messages)
inputs: dict[str, Any] = {
"input": formatted_messages,
"tool_names": get_tool_names(parsed_tools),

View File

@@ -213,6 +213,30 @@ def _combine_knowledge_context(agent: Agent) -> str:
return agent_ctx + separator + crew_ctx
def append_skill_context(agent: Agent, task_prompt: str) -> str:
"""Append activated skill context sections to the task prompt.
Args:
agent: The agent with optional skills.
task_prompt: The current task prompt.
Returns:
The task prompt with skill context appended.
"""
if not agent.skills:
return task_prompt
from crewai.skills.loader import format_skill_context
from crewai.skills.models import Skill
skill_sections = [
format_skill_context(s) for s in agent.skills if isinstance(s, Skill)
]
if skill_sections:
task_prompt += "\n\n" + "\n\n".join(skill_sections)
return task_prompt
def apply_training_data(agent: Agent, task_prompt: str) -> str:
"""Apply training data to the task prompt.

View File

@@ -1,28 +1,13 @@
from typing import TYPE_CHECKING, Any
from crewai.agents.cache.cache_handler import CacheHandler
from crewai.agents.parser import AgentAction, AgentFinish, OutputParserError, parse
from crewai.agents.tools_handler import ToolsHandler
if TYPE_CHECKING:
from crewai.agents.crew_agent_executor import CrewAgentExecutor
__all__ = [
"AgentAction",
"AgentFinish",
"CacheHandler",
"CrewAgentExecutor",
"OutputParserError",
"ToolsHandler",
"parse",
]
def __getattr__(name: str) -> Any:
if name == "CrewAgentExecutor":
from crewai.agents.crew_agent_executor import CrewAgentExecutor
return CrewAgentExecutor
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View File

@@ -51,10 +51,7 @@ class LangGraphAgentAdapter(BaseAgentAdapter):
_graph: Any = PrivateAttr(default=None)
_memory: Any = PrivateAttr(default=None)
_max_iterations: int = PrivateAttr(default=10)
function_calling_llm: Any = Field(
default=None,
deprecated="function_calling_llm is deprecated and will be removed in a future release.",
)
function_calling_llm: Any = Field(default=None)
step_callback: SerializableCallable | None = Field(default=None)
model: str = Field(default="gpt-4o")

View File

@@ -60,10 +60,7 @@ class OpenAIAgentAdapter(BaseAgentAdapter):
_openai_agent: OpenAIAgentProtocol = PrivateAttr()
_logger: Logger = PrivateAttr(default_factory=Logger)
_active_thread: str | None = PrivateAttr(default=None)
function_calling_llm: Any = Field(
default=None,
deprecated="function_calling_llm is deprecated and will be removed in a future release.",
)
function_calling_llm: Any = Field(default=None)
step_callback: Any = Field(default=None)
_tool_adapter: OpenAIAgentToolAdapter = PrivateAttr()
_converter_adapter: OpenAIConverterAdapter = PrivateAttr()

View File

@@ -31,13 +31,13 @@ from crewai.agents.tools_handler import ToolsHandler
from crewai.events.base_events import set_emission_counter
from crewai.events.event_bus import crewai_event_bus
from crewai.events.event_context import restore_event_scope, set_last_event_id
from crewai.knowledge.knowledge import Knowledge, _resolve_knowledge_sources
from crewai.knowledge.knowledge import Knowledge
from crewai.knowledge.knowledge_config import KnowledgeConfig
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage
from crewai.llms.base_llm import BaseLLM
from crewai.mcp.config import MCPServerConfig
from crewai.memory.memory_scope import MemoryScope, MemorySlice, _ensure_memory_kind
from crewai.memory.memory_scope import MemoryScope, MemorySlice
from crewai.memory.unified_memory import Memory
from crewai.rag.embeddings.types import EmbedderConfig
from crewai.security.security_config import SecurityConfig
@@ -127,13 +127,6 @@ def _validate_executor_ref(value: Any) -> Any:
return value
def _serialize_executor_ref(value: Any) -> dict[str, Any] | None:
if value is None:
return None
result: dict[str, Any] = value.model_dump(mode="json")
return result
def _serialize_llm_ref(value: Any) -> dict[str, Any] | None:
if value is None:
return None
@@ -258,13 +251,14 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
max_iter: int = Field(
default=25, description="Maximum iterations for an agent to execute a task"
)
agent_executor: Annotated[
SerializeAsAny[BaseAgentExecutor] | None,
BeforeValidator(_validate_executor_ref),
PlainSerializer(
_serialize_executor_ref, return_type=dict | None, when_used="json"
),
] = Field(default=None, description="An instance of the CrewAgentExecutor class.")
agent_executor: SerializeAsAny[BaseAgentExecutor] | None = Field(
default=None, description="An instance of the CrewAgentExecutor class."
)
@field_validator("agent_executor", mode="before")
@classmethod
def _validate_agent_executor(cls, v: Any) -> Any:
return _validate_executor_ref(v)
llm: Annotated[
str | BaseLLM | None,
@@ -294,10 +288,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
knowledge: Knowledge | None = Field(
default=None, description="Knowledge for the agent."
)
knowledge_sources: Annotated[
list[BaseKnowledgeSource] | None,
BeforeValidator(_resolve_knowledge_sources),
] = Field(
knowledge_sources: list[BaseKnowledgeSource] | None = Field(
default=None,
description="Knowledge sources for the agent.",
)
@@ -335,14 +326,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
default=None,
description="List of MCP server references. Supports 'https://server.com/path' for external servers and bare slugs like 'notion' for connected MCP integrations. Use '#tool_name' suffix for specific tools.",
)
memory: Annotated[
bool
| Annotated[
Memory | MemoryScope | MemorySlice, Field(discriminator="memory_kind")
]
| None,
BeforeValidator(_ensure_memory_kind),
] = Field(
memory: bool | Memory | MemoryScope | MemorySlice | None = Field(
default=None,
description=(
"Enable agent memory. Pass True for default Memory(), "
@@ -350,9 +334,9 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
"If not set, falls back to crew memory."
),
)
skills: list[Path | Skill | str] | None = Field(
skills: list[Path | Skill] | None = Field(
default=None,
description="Agent Skills. Accepts paths for discovery, pre-loaded Skill objects, or '@org/name' registry refs.",
description="Agent Skills. Accepts paths for discovery or pre-loaded Skill objects.",
min_length=1,
)
execution_context: ExecutionContext | None = Field(default=None)
@@ -413,21 +397,8 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
self.agent_executor._resuming = True
if self.checkpoint_kickoff_event_id is not None:
self._kickoff_event_id = self.checkpoint_kickoff_event_id
self._rebind_memory_view()
self._restore_event_scope(state)
def _rebind_memory_view(self) -> None:
"""Reattach a fresh ``Memory`` to a restored ``MemoryScope``/``MemorySlice``.
Checkpoint JSON omits the live ``Memory`` dependency, so scoped
memory views raise ``RuntimeError`` on first use after restore.
"""
if (
isinstance(self.memory, MemoryScope | MemorySlice)
and self.memory._memory is None
):
self.memory.bind(Memory())
def _restore_event_scope(self, state: RuntimeState) -> None:
"""Rebuild the event scope stack from the checkpoint's event record.
@@ -458,20 +429,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
def process_model_config(cls, values: Any) -> dict[str, Any]:
return process_config(values, cls)
@field_validator("skills", mode="before")
@classmethod
def coerce_skill_strings(cls, skills: Any) -> Any:
"""Coerce plain path strings to Path objects; keep @-prefixed refs as str."""
if not isinstance(skills, list):
return skills
result = []
for item in skills:
if isinstance(item, str) and not item.startswith("@"):
result.append(Path(item))
else:
result.append(item)
return result
@field_validator("tools")
@classmethod
def validate_tools(cls, tools: list[Any]) -> list[BaseTool]:

View File

@@ -14,7 +14,6 @@ import contextvars
import inspect
import logging
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
import warnings
from crewai_core.printer import PRINTER
from pydantic import (
@@ -139,13 +138,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
warnings.warn(
"CrewAgentExecutor is deprecated and will be removed in a future release.\n"
"Agents inside Crews now use AgentExecutor (crewai.experimental.AgentExecutor) by default.\n"
"To suppress this warning, migrate to AgentExecutor.",
DeprecationWarning,
stacklevel=2,
)
if not self.before_llm_call_hooks:
self.before_llm_call_hooks.extend(get_before_llm_call_hooks())
if not self.after_llm_call_hooks:
@@ -174,8 +166,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
if provider.setup_messages(cast(ExecutorContext, cast(object, self))):
return
from crewai.llms.cache import mark_cache_breakpoint
if self.prompt is not None and "system" in self.prompt:
system_prompt = self._format_prompt(
cast(str, self.prompt.get("system", "")), inputs
@@ -183,22 +173,11 @@ class CrewAgentExecutor(BaseAgentExecutor):
user_prompt = self._format_prompt(
cast(str, self.prompt.get("user", "")), inputs
)
# Cache breakpoints: end-of-system caches the per-agent stable
# prefix; end-of-user caches the per-task stable prefix across
# ReAct-loop iterations.
self.messages.append(
mark_cache_breakpoint(
format_message_for_llm(system_prompt, role="system")
)
)
self.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
self.messages.append(format_message_for_llm(system_prompt, role="system"))
self.messages.append(format_message_for_llm(user_prompt))
elif self.prompt is not None:
user_prompt = self._format_prompt(self.prompt.get("prompt", ""), inputs)
self.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
self.messages.append(format_message_for_llm(user_prompt))
provider.post_setup_messages(cast(ExecutorContext, cast(object, self)))

View File

@@ -93,11 +93,11 @@ from crewai.events.types.crew_events import (
CrewTrainStartedEvent,
)
from crewai.flow.flow_trackable import FlowTrackable
from crewai.knowledge.knowledge import Knowledge, _resolve_knowledge_sources
from crewai.knowledge.knowledge import Knowledge
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM
from crewai.memory.memory_scope import MemoryScope, MemorySlice, _ensure_memory_kind
from crewai.memory.memory_scope import MemoryScope, MemorySlice
from crewai.memory.unified_memory import Memory
from crewai.process import Process
from crewai.rag.embeddings.types import EmbedderConfig
@@ -223,14 +223,7 @@ class Crew(FlowTrackable, BaseModel):
] = Field(default_factory=list)
process: Process = Field(default=Process.sequential)
verbose: bool = Field(default=False)
memory: Annotated[
bool
| Annotated[
Memory | MemoryScope | MemorySlice, Field(discriminator="memory_kind")
]
| None,
BeforeValidator(_ensure_memory_kind),
] = Field(
memory: bool | Memory | MemoryScope | MemorySlice | None = Field(
default=False,
description=(
"Enable crew memory. Pass True for default Memory(), "
@@ -258,11 +251,7 @@ class Crew(FlowTrackable, BaseModel):
str | LLM | None,
BeforeValidator(_validate_llm_ref),
PlainSerializer(_serialize_llm_ref, return_type=dict | None, when_used="json"),
] = Field(
description="Language model that will run the agent.",
default=None,
deprecated="function_calling_llm is deprecated and will be removed in a future release.",
)
] = Field(description="Language model that will run the agent.", default=None)
config: Json[dict[str, Any]] | dict[str, Any] | None = Field(default=None)
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
share_crew: bool | None = Field(default=False)
@@ -329,10 +318,7 @@ class Crew(FlowTrackable, BaseModel):
default_factory=list,
description="list of execution logs for tasks",
)
knowledge_sources: Annotated[
list[BaseKnowledgeSource] | None,
BeforeValidator(_resolve_knowledge_sources),
] = Field(
knowledge_sources: list[BaseKnowledgeSource] | None = Field(
default=None,
description=(
"Knowledge sources for the crew. Add knowledge sources to the "
@@ -351,9 +337,9 @@ class Crew(FlowTrackable, BaseModel):
default=None,
description="Knowledge for the crew.",
)
skills: list[Path | Skill | str] | None = Field(
skills: list[Path | Skill] | None = Field(
default=None,
description="Skill search paths, pre-loaded Skill objects, or '@org/name' registry refs applied to all agents in the crew.",
description="Skill search paths or pre-loaded Skill objects applied to all agents in the crew.",
)
security_config: SecurityConfig = Field(
@@ -487,42 +473,8 @@ class Crew(FlowTrackable, BaseModel):
if self.checkpoint_train is not None:
self._train = self.checkpoint_train
self._rebind_memory_views()
self._restore_event_scope()
def _rebind_memory_views(self) -> None:
"""Reattach a live ``Memory`` to restored ``MemoryScope``/``MemorySlice`` views.
Checkpoint JSON omits the live ``Memory`` dependency on scope/slice
views, so after restore they raise ``RuntimeError`` on first use.
Prefer the crew's restored ``Memory`` (from ``create_crew_memory``
or a ``Crew.memory=Memory(...)`` instance) so all views share one
backing store; fall back to a fresh ``Memory()`` only if nothing is
available.
"""
from crewai.memory.memory_scope import MemoryScope, MemorySlice
from crewai.memory.unified_memory import Memory
backing: Memory | None = None
if isinstance(self._memory, Memory):
backing = self._memory
elif isinstance(self.memory, Memory):
backing = self.memory
def _ensure(view: Any) -> None:
nonlocal backing
if not isinstance(view, MemoryScope | MemorySlice):
return
if view._memory is not None:
return
if backing is None:
backing = Memory()
view.bind(backing)
_ensure(self.memory)
for agent in self.agents:
_ensure(agent.memory)
def _restore_event_scope(self) -> None:
"""Rebuild the event scope stack from the checkpoint's event record."""
from crewai.events.base_events import set_emission_counter
@@ -570,20 +522,6 @@ class Crew(FlowTrackable, BaseModel):
if max_seq > 0:
set_emission_counter(max_seq)
@field_validator("skills", mode="before")
@classmethod
def coerce_skill_strings(cls, skills: Any) -> Any:
"""Coerce plain path strings to Path objects; keep @-prefixed refs as str."""
if not isinstance(skills, list):
return skills
result = []
for item in skills:
if isinstance(item, str) and not item.startswith("@"):
result.append(Path(item))
else:
result.append(item)
return result
@field_validator("id", mode="before")
@classmethod
def _deny_user_set_id(cls, v: UUID4 | None, info: Any) -> UUID4 | None:

View File

@@ -6,14 +6,6 @@ import time
from typing import Any
import uuid
from crewai_core.plus_api import (
TraceBatchInitPayload,
TraceBatchMetadata,
TraceEventsPayload,
TraceExecutionContext,
TraceExecutionMetadata,
TraceFinalizePayload,
)
from crewai_core.settings import Settings
from rich.console import Console
from rich.panel import Panel
@@ -131,27 +123,25 @@ class TraceBatchManager:
return None
try:
execution_context: TraceExecutionContext = {
"crew_fingerprint": execution_metadata.get("crew_fingerprint"),
"crew_name": execution_metadata.get("crew_name", None),
"flow_name": execution_metadata.get("flow_name", None),
"crewai_version": self.current_batch.version,
"privacy_level": user_context.get("privacy_level", "standard"),
}
execution_metadata_payload: TraceExecutionMetadata = {
"expected_duration_estimate": execution_metadata.get(
"expected_duration_estimate", 300
),
"agent_count": execution_metadata.get("agent_count", 0),
"task_count": execution_metadata.get("task_count", 0),
"flow_method_count": execution_metadata.get("flow_method_count", 0),
"execution_started_at": datetime.now(timezone.utc).isoformat(),
}
payload: TraceBatchInitPayload = {
payload = {
"trace_id": self.current_batch.batch_id,
"execution_type": execution_metadata.get("execution_type", "crew"),
"execution_context": execution_context,
"execution_metadata": execution_metadata_payload,
"execution_context": {
"crew_fingerprint": execution_metadata.get("crew_fingerprint"),
"crew_name": execution_metadata.get("crew_name", None),
"flow_name": execution_metadata.get("flow_name", None),
"crewai_version": self.current_batch.version,
"privacy_level": user_context.get("privacy_level", "standard"),
},
"execution_metadata": {
"expected_duration_estimate": execution_metadata.get(
"expected_duration_estimate", 300
),
"agent_count": execution_metadata.get("agent_count", 0),
"task_count": execution_metadata.get("task_count", 0),
"flow_method_count": execution_metadata.get("flow_method_count", 0),
"execution_started_at": datetime.now(timezone.utc).isoformat(),
},
}
if use_ephemeral:
payload["ephemeral_trace_id"] = self.current_batch.batch_id
@@ -274,14 +264,13 @@ class TraceBatchManager:
if not self.plus_api or not self.trace_batch_id or not self.event_buffer:
return 500
try:
batch_metadata: TraceBatchMetadata = {
"events_count": len(self.event_buffer),
"batch_sequence": 1,
"is_final_batch": False,
}
payload: TraceEventsPayload = {
payload = {
"events": [event.to_dict() for event in self.event_buffer],
"batch_metadata": batch_metadata,
"batch_metadata": {
"events_count": len(self.event_buffer),
"batch_sequence": 1,
"is_final_batch": False,
},
}
response = (
@@ -375,7 +364,7 @@ class TraceBatchManager:
return
try:
payload: TraceFinalizePayload = {
payload = {
"status": "completed",
"duration_ms": self.calculate_duration("execution"),
"final_event_count": events_count,

View File

@@ -60,20 +60,3 @@ class SkillLoadFailedEvent(SkillEvent):
type: Literal["skill_load_failed"] = "skill_load_failed"
error: str
class SkillDownloadStartedEvent(SkillEvent):
"""Event emitted when a registry skill download begins."""
type: Literal["skill_download_started"] = "skill_download_started"
registry_ref: str
version: str | None = None
class SkillDownloadCompletedEvent(SkillEvent):
"""Event emitted when a registry skill download completes."""
type: Literal["skill_download_completed"] = "skill_download_completed"
registry_ref: str
version: str | None = None
cache_path: Path | None = None

View File

@@ -1191,13 +1191,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
@router("force_final_answer")
def ensure_force_final_answer(self) -> Literal["agent_finished"]:
"""Force agent to provide final answer when max iterations exceeded."""
# The flow framework can route here more than once per execution when the
# "initialized" label is emitted by both initialize_reasoning and
# increment_and_continue in the same listener pass. Skip the extra LLM
# round-trip once we've already produced a forced final answer.
if self.state.is_finished:
return "agent_finished"
formatted_answer = handle_max_iterations_exceeded(
formatted_answer=None,
printer=PRINTER,
@@ -2586,26 +2579,16 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self._kickoff_input = inputs.get("input", "")
if "system" in self.prompt:
from crewai.llms.cache import mark_cache_breakpoint
prompt = cast("SystemPromptResult", self.prompt)
system_prompt = self._format_prompt(prompt["system"], inputs)
user_prompt = self._format_prompt(prompt["user"], inputs)
self.state.messages.append(
mark_cache_breakpoint(
format_message_for_llm(system_prompt, role="system")
)
)
self.state.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
format_message_for_llm(system_prompt, role="system")
)
self.state.messages.append(format_message_for_llm(user_prompt))
else:
from crewai.llms.cache import mark_cache_breakpoint
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
self.state.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
self.state.messages.append(format_message_for_llm(user_prompt))
self._inject_files_from_inputs(inputs)
@@ -2687,26 +2670,16 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self._kickoff_input = inputs.get("input", "")
if "system" in self.prompt:
from crewai.llms.cache import mark_cache_breakpoint
prompt = cast("SystemPromptResult", self.prompt)
system_prompt = self._format_prompt(prompt["system"], inputs)
user_prompt = self._format_prompt(prompt["user"], inputs)
self.state.messages.append(
mark_cache_breakpoint(
format_message_for_llm(system_prompt, role="system")
)
)
self.state.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
format_message_for_llm(system_prompt, role="system")
)
self.state.messages.append(format_message_for_llm(user_prompt))
else:
from crewai.llms.cache import mark_cache_breakpoint
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
self.state.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
self.state.messages.append(format_message_for_llm(user_prompt))
self._inject_files_from_inputs(inputs)

View File

@@ -113,7 +113,7 @@ from crewai.flow.utils import (
is_flow_method_name,
is_simple_flow_condition,
)
from crewai.memory.memory_scope import MemoryScope, MemorySlice, _ensure_memory_kind
from crewai.memory.memory_scope import MemoryScope, MemorySlice
from crewai.memory.unified_memory import Memory
from crewai.state.checkpoint_config import (
CheckpointConfig,
@@ -159,39 +159,6 @@ def _resolve_persistence(value: Any) -> Any:
return value
def _serialize_persistence(value: Any) -> dict[str, Any] | None:
if value is None:
return None
if isinstance(value, FlowPersistence):
return value.model_dump(mode="json")
raise TypeError(
f"Cannot serialize Flow.persistence of type {type(value).__name__}: "
"expected FlowPersistence or None."
)
def _validate_input_provider(value: Any) -> Any:
if value is None or isinstance(value, InputProvider):
return value
from crewai.types.callback import _dotted_path_to_instance
resolved = _dotted_path_to_instance(value)
if resolved is None or isinstance(resolved, InputProvider):
return resolved
raise ValueError(
f"Resolved input_provider {resolved!r} does not implement the "
"InputProvider protocol (missing request_input)."
)
def _serialize_input_provider(value: Any) -> str | None:
if value is None:
return None
from crewai.types.callback import _instance_to_dotted_path
return _instance_to_dotted_path(value)
_INITIAL_STATE_CLASS_MARKER = "__crewai_pydantic_class_schema__"
@@ -982,30 +949,15 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
name: str | None = Field(default=None)
tracing: bool | None = Field(default=None)
stream: bool = Field(default=False)
memory: Annotated[
Annotated[
Memory | MemoryScope | MemorySlice, Field(discriminator="memory_kind")
]
| None,
BeforeValidator(_ensure_memory_kind),
] = Field(default=None)
input_provider: Annotated[
InputProvider | None,
BeforeValidator(_validate_input_provider),
PlainSerializer(
_serialize_input_provider, return_type=str | None, when_used="json"
),
] = Field(default=None)
memory: Memory | MemoryScope | MemorySlice | None = Field(default=None)
input_provider: InputProvider | None = Field(default=None)
suppress_flow_events: bool = Field(default=False)
human_feedback_history: list[HumanFeedbackResult] = Field(default_factory=list)
last_human_feedback: HumanFeedbackResult | None = Field(default=None)
persistence: Annotated[
SerializeAsAny[FlowPersistence] | None,
SerializeAsAny[FlowPersistence] | Any,
BeforeValidator(lambda v, _: _resolve_persistence(v)),
PlainSerializer(
_serialize_persistence, return_type=dict | None, when_used="json"
),
] = Field(default=None)
max_method_calls: int = Field(default=100)
@@ -1098,11 +1050,6 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
}
if self.checkpoint_state is not None:
self._restore_state(self.checkpoint_state)
if (
isinstance(self.memory, MemoryScope | MemorySlice)
and self.memory._memory is None
):
self.memory.bind(Memory())
restore_event_scope(())
reset_last_event_id()

View File

@@ -60,7 +60,6 @@ from collections.abc import Callable, Sequence
from dataclasses import dataclass, field
from datetime import datetime
from functools import wraps
import logging
from typing import TYPE_CHECKING, Any, TypeVar
from pydantic import BaseModel, Field
@@ -74,8 +73,6 @@ if TYPE_CHECKING:
from crewai.llms.base_llm import BaseLLM
logger = logging.getLogger(__name__)
F = TypeVar("F", bound=Callable[..., Any])
@@ -191,7 +188,6 @@ class HumanFeedbackConfig:
provider: HumanFeedbackProvider | None = None
learn: bool = False
learn_source: str = "hitl"
learn_strict: bool = False
class HumanFeedbackMethod(FlowMethod[Any, Any]):
@@ -241,7 +237,6 @@ def human_feedback(
provider: HumanFeedbackProvider | None = None,
learn: bool = False,
learn_source: str = "hitl",
learn_strict: bool = False,
) -> Callable[[F], F]:
"""Decorator for Flow methods that require human feedback.
@@ -280,14 +275,6 @@ def human_feedback(
external systems like Slack, Teams, or webhooks. When the
provider raises HumanFeedbackPending, the flow pauses and
can be resumed later with Flow.resume().
learn: Enable HITL learning. Recall past lessons to pre-review
output before the human sees it, and distill new lessons
from feedback after.
learn_source: Memory source tag for stored/recalled lessons.
learn_strict: When True, re-raise exceptions from the pre-review
and distillation steps instead of falling back to raw output.
Default False preserves graceful degradation; failures are
always logged via ``logger.warning`` regardless of this flag.
Returns:
A decorator function that wraps the method with human feedback
@@ -417,19 +404,7 @@ def human_feedback(
reviewed = llm_inst.call(messages)
return reviewed if isinstance(reviewed, str) else str(reviewed)
except Exception:
if learn_strict:
logger.warning(
"HITL pre-review failed for %s; re-raising (learn_strict=True)",
func.__name__,
exc_info=True,
)
raise
logger.warning(
"HITL pre-review failed for %s; falling back to raw output",
func.__name__,
exc_info=True,
)
return method_output
return method_output # fallback to raw output on any failure
def _distill_and_store_lessons(
flow_instance: Flow[Any], method_output: Any, raw_feedback: str
@@ -471,19 +446,8 @@ def human_feedback(
if lessons:
mem.remember_many(lessons, source=learn_source) # type: ignore[union-attr]
except Exception:
if learn_strict:
logger.warning(
"HITL lesson distillation failed for %s; re-raising (learn_strict=True)",
func.__name__,
exc_info=True,
)
raise
logger.warning(
"HITL lesson distillation failed for %s; no lessons stored",
func.__name__,
exc_info=True,
)
except Exception: # noqa: S110
pass # non-critical: don't fail the flow because lesson storage failed
# -- Core feedback helpers ------------------------------------
@@ -690,7 +654,6 @@ def human_feedback(
provider=provider,
learn=learn,
learn_source=learn_source,
learn_strict=learn_strict,
)
wrapper.__is_flow_method__ = True

View File

@@ -1,89 +1,16 @@
import os
from typing import Annotated, Any
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, PlainSerializer
from pydantic import BaseModel, ConfigDict, Field
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
from crewai.knowledge.source.crew_docling_source import CrewDoclingSource
from crewai.knowledge.source.csv_knowledge_source import CSVKnowledgeSource
from crewai.knowledge.source.excel_knowledge_source import ExcelKnowledgeSource
from crewai.knowledge.source.json_knowledge_source import JSONKnowledgeSource
from crewai.knowledge.source.pdf_knowledge_source import PDFKnowledgeSource
from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource
from crewai.knowledge.source.text_file_knowledge_source import (
TextFileKnowledgeSource,
)
from crewai.knowledge.storage.knowledge_storage import KnowledgeStorage
from crewai.rag.core.base_embeddings_provider import BaseEmbeddingsProvider
from crewai.rag.embeddings.types import EmbedderConfig
from crewai.rag.types import SearchResult
_KNOWN_SOURCES: dict[str, type[BaseKnowledgeSource]] = {
"string": StringKnowledgeSource,
"docling": CrewDoclingSource,
"csv": CSVKnowledgeSource,
"excel": ExcelKnowledgeSource,
"json": JSONKnowledgeSource,
"pdf": PDFKnowledgeSource,
"text_file": TextFileKnowledgeSource,
}
def _resolve_knowledge_sources(value: Any) -> Any:
"""Coerce list of dicts into typed BaseKnowledgeSource subclasses via source_type.
Pass-through for anything else (existing instances, mocks).
"""
if not isinstance(value, list):
return value
resolved: list[Any] = []
for idx, item in enumerate(value):
if isinstance(item, dict):
tag = item.get("source_type")
if not isinstance(tag, str):
resolved.append(item)
continue
cls = _KNOWN_SOURCES.get(tag)
if cls is None:
raise ValueError(
f"Unknown source_type={tag!r} at index {idx}: "
f"expected one of {sorted(_KNOWN_SOURCES)}"
)
try:
resolved.append(cls.model_validate(item))
except Exception as exc:
raise ValueError(
f"Failed to validate knowledge source at index {idx} "
f"with source_type={tag!r}: {exc}"
) from exc
else:
resolved.append(item)
return resolved
os.environ["TOKENIZERS_PARALLELISM"] = "false" # removes logging from fastembed
def _serialize_embedder_spec(value: Any) -> dict[str, Any] | None:
if value is None:
return None
if isinstance(value, BaseEmbeddingsProvider):
return value.model_dump(mode="json")
if isinstance(value, dict):
return value
if isinstance(value, type) and issubclass(value, BaseEmbeddingsProvider):
raise TypeError(
f"Cannot checkpoint embedder class {value.__module__}.{value.__qualname__}: "
"build_embedder requires an instance or ProviderSpec dict, not a class. "
"Instantiate the provider before assigning it to Knowledge.embedder."
)
raise TypeError(
f"Cannot serialize embedder of type {type(value).__name__}: "
"expected ProviderSpec dict or BaseEmbeddingsProvider instance."
)
class Knowledge(BaseModel):
"""
Knowledge is a collection of sources and setup for the vector store to save and query relevant context.
@@ -93,18 +20,10 @@ class Knowledge(BaseModel):
embedder: EmbedderConfig | None = None
"""
sources: Annotated[
list[BaseKnowledgeSource],
BeforeValidator(_resolve_knowledge_sources),
] = Field(default_factory=list)
sources: list[BaseKnowledgeSource] = Field(default_factory=list)
model_config = ConfigDict(arbitrary_types_allowed=True)
storage: KnowledgeStorage | None = Field(default=None)
embedder: Annotated[
EmbedderConfig | None,
PlainSerializer(
_serialize_embedder_spec, return_type=dict | None, when_used="json"
),
] = None
embedder: EmbedderConfig | None = None
collection_name: str | None = None
def __init__(

View File

@@ -13,9 +13,7 @@ class BaseKnowledgeSource(BaseModel, ABC):
chunk_size: int = 4000
chunk_overlap: int = 200
chunks: list[str] = Field(default_factory=list)
chunk_embeddings: list[np.ndarray[Any, np.dtype[Any]]] = Field(
default_factory=list, exclude=True
)
chunk_embeddings: list[np.ndarray[Any, np.dtype[Any]]] = Field(default_factory=list)
model_config = ConfigDict(arbitrary_types_allowed=True)
storage: KnowledgeStorage | None = Field(default=None)

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse
@@ -45,7 +45,6 @@ class CrewDoclingSource(BaseKnowledgeSource):
_logger: Logger = Logger(verbose=True)
source_type: Literal["docling"] = "docling"
file_path: list[Path | str] | None = Field(default=None)
file_paths: list[Path | str] = Field(default_factory=list)
chunks: list[str] = Field(default_factory=list)

View File

@@ -1,6 +1,5 @@
import csv
from pathlib import Path
from typing import Literal
from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource
@@ -8,8 +7,6 @@ from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledge
class CSVKnowledgeSource(BaseFileKnowledgeSource):
"""A knowledge source that stores and queries CSV file content using embeddings."""
source_type: Literal["csv"] = "csv"
def load_content(self) -> dict[Path, str]:
"""Load and preprocess CSV file content."""
content_dict = {}

View File

@@ -1,6 +1,6 @@
from pathlib import Path
from types import ModuleType
from typing import Any, Literal
from typing import Any
from pydantic import Field, field_validator
@@ -16,7 +16,6 @@ class ExcelKnowledgeSource(BaseKnowledgeSource):
_logger: Logger = Logger(verbose=True)
source_type: Literal["excel"] = "excel"
file_path: Path | list[Path] | str | list[str] | None = Field(
default=None,
description="[Deprecated] The path to the file. Use file_paths instead.",

View File

@@ -1,6 +1,6 @@
import json
from pathlib import Path
from typing import Any, Literal
from typing import Any
from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource
@@ -8,8 +8,6 @@ from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledge
class JSONKnowledgeSource(BaseFileKnowledgeSource):
"""A knowledge source that stores and queries JSON file content using embeddings."""
source_type: Literal["json"] = "json"
def load_content(self) -> dict[Path, str]:
"""Load and preprocess JSON file content."""
content: dict[Path, str] = {}

View File

@@ -1,6 +1,5 @@
from pathlib import Path
from types import ModuleType
from typing import Literal
from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource
@@ -8,8 +7,6 @@ from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledge
class PDFKnowledgeSource(BaseFileKnowledgeSource):
"""A knowledge source that stores and queries PDF file content using embeddings."""
source_type: Literal["pdf"] = "pdf"
def load_content(self) -> dict[Path, str]:
"""Load and preprocess PDF file content."""
pdfplumber = self._import_pdfplumber()

View File

@@ -1,4 +1,4 @@
from typing import Any, Literal
from typing import Any
from pydantic import Field
@@ -8,7 +8,6 @@ from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
class StringKnowledgeSource(BaseKnowledgeSource):
"""A knowledge source that stores and queries plain text content using embeddings."""
source_type: Literal["string"] = "string"
content: str = Field(...)
collection_name: str | None = Field(default=None)

View File

@@ -1,5 +1,4 @@
from pathlib import Path
from typing import Literal
from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource
@@ -7,8 +6,6 @@ from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledge
class TextFileKnowledgeSource(BaseFileKnowledgeSource):
"""A knowledge source that stores and queries text file content using embeddings."""
source_type: Literal["text_file"] = "text_file"
def load_content(self) -> dict[Path, str]:
"""Load and preprocess text file content."""
content = {}

View File

@@ -111,12 +111,7 @@ if LITELLM_AVAILABLE:
MIN_CONTEXT: Final[int] = 1024
MAX_CONTEXT: Final[int] = 2097152 # Current max from gemini-1.5-pro
ANTHROPIC_PREFIXES: Final[tuple[str, ...]] = (
"anthropic/",
"anthropic.",
"claude-",
"claude/",
)
ANTHROPIC_PREFIXES: Final[tuple[str, str, str]] = ("anthropic/", "claude-", "claude/")
LLM_CONTEXT_WINDOW_SIZES: Final[dict[str, int]] = {
# openai
@@ -470,7 +465,9 @@ class LLM(BaseLLM):
)
if provider == "anthropic" or provider == "claude":
return "claude" in model_lower or model_lower.startswith("anthropic")
return any(
model_lower.startswith(prefix) for prefix in ["claude-", "anthropic."]
)
if provider == "gemini" or provider == "google":
return any(
@@ -577,19 +574,6 @@ class LLM(BaseLLM):
if model in AZURE_MODELS:
return "azure"
# Fallback to pattern matching for models not in constants
provider_order = [
"bedrock",
"openai",
"anthropic",
"gemini",
"deepseek",
"dashscope",
]
for provider in provider_order:
if cls._matches_provider_pattern(model, provider):
return provider
return "openai"
@classmethod
@@ -669,8 +653,8 @@ class LLM(BaseLLM):
Returns:
bool: True if the model is from Anthropic, False otherwise.
"""
anthropic_indicators = ("anthropic/", "anthropic.", "claude-", "claude/")
return any(indicator in model.lower() for indicator in anthropic_indicators)
anthropic_prefixes = ("anthropic/", "claude-", "claude/")
return any(prefix in model.lower() for prefix in anthropic_prefixes)
def _prepare_completion_params(
self,
@@ -956,21 +940,6 @@ class LLM(BaseLLM):
self._track_token_usage_internal(usage_info)
self._handle_streaming_callbacks(callbacks, usage_info, last_chunk)
if accumulated_tool_args and not available_functions:
tool_calls_list: list[ChatCompletionDeltaToolCall] = [
ChatCompletionDeltaToolCall(
index=idx,
function=Function(
name=tool_arg.function.name,
arguments=tool_arg.function.arguments,
),
)
for idx, tool_arg in sorted(accumulated_tool_args.items())
if tool_arg.function.name
]
if tool_calls_list:
return tool_calls_list
if not tool_calls or not available_functions:
if response_model and self.is_litellm:
instructor_instance = InternalInstructor(
@@ -1566,7 +1535,8 @@ class LLM(BaseLLM):
if usage_info:
self._track_token_usage_internal(usage_info)
if accumulated_tool_args:
if accumulated_tool_args and available_functions:
# Convert accumulated tool args to ChatCompletionDeltaToolCall objects
tool_calls_list: list[ChatCompletionDeltaToolCall] = [
ChatCompletionDeltaToolCall(
index=idx,
@@ -1575,24 +1545,21 @@ class LLM(BaseLLM):
arguments=tool_arg.function.arguments,
),
)
for idx, tool_arg in sorted(accumulated_tool_args.items())
for idx, tool_arg in accumulated_tool_args.items()
if tool_arg.function.name
]
if tool_calls_list:
if available_functions:
result = self._handle_streaming_tool_calls(
tool_calls=tool_calls_list,
accumulated_tool_args=accumulated_tool_args,
available_functions=available_functions,
from_task=from_task,
from_agent=from_agent,
response_id=response_id,
)
if result is not None:
return result
else:
return tool_calls_list
result = self._handle_streaming_tool_calls(
tool_calls=tool_calls_list,
accumulated_tool_args=accumulated_tool_args,
available_functions=available_functions,
from_task=from_task,
from_agent=from_agent,
response_id=response_id,
)
if result is not None:
return result
usage_dict = self._usage_to_dict(usage_info)
self._handle_emit_call_events(

View File

@@ -14,7 +14,7 @@ from datetime import datetime
import json
import logging
import re
from typing import TYPE_CHECKING, Any, Final, Literal, cast
from typing import TYPE_CHECKING, Any, Final, Literal
import uuid
from pydantic import (
@@ -703,19 +703,10 @@ class BaseLLM(BaseModel, ABC):
Raises:
ValueError: If message format is invalid
"""
from crewai.llms.cache import CACHE_BREAKPOINT_KEY
from crewai.utilities.types import LLMMessage as _LLMMessage
if isinstance(messages, str):
return [{"role": "user", "content": messages}]
# Validate then copy each message, dropping the cache-breakpoint
# flag in the copy only. The caller (e.g. CrewAgentExecutor,
# experimental.AgentExecutor) reuses its messages buffer across
# many LLM calls in the tool-use loop; mutating their dicts
# in place would erase the markers after the first call and
# break prompt caching for every subsequent iteration.
cleaned: list[LLMMessage] = []
# Validate message format
for i, msg in enumerate(messages):
if not isinstance(msg, dict):
raise ValueError(f"Message at index {i} must be a dictionary")
@@ -723,12 +714,8 @@ class BaseLLM(BaseModel, ABC):
raise ValueError(
f"Message at index {i} must have 'role' and 'content' keys"
)
copy: dict[str, Any] = {
k: v for k, v in msg.items() if k != CACHE_BREAKPOINT_KEY
}
cleaned.append(cast(_LLMMessage, copy))
return self._process_message_files(cleaned)
return self._process_message_files(messages)
def _process_message_files(self, messages: list[LLMMessage]) -> list[LLMMessage]:
"""Process files attached to messages and format for the provider.

View File

@@ -1,37 +0,0 @@
"""Provider-agnostic prompt-cache breakpoint marker.
Application code (prompt builders, agent executors) marks messages where a
stable prefix ends. Provider adapters then translate the marker into the
cache directive their API expects, or strip it for providers that cache
implicitly (OpenAI, Gemini) or do not cache at all.
Usage:
from crewai.llms.cache import mark_cache_breakpoint
messages = [
mark_cache_breakpoint({"role": "system", "content": stable_system}),
mark_cache_breakpoint({"role": "user", "content": stable_user_prefix}),
{"role": "user", "content": volatile_query},
]
"""
from __future__ import annotations
from typing import Any
CACHE_BREAKPOINT_KEY = "cache_breakpoint"
def mark_cache_breakpoint(message: dict[str, Any]) -> dict[str, Any]:
"""Return ``message`` with the cache-breakpoint flag set.
Returns a new dict so callers can safely pass literal dicts.
"""
return {**message, CACHE_BREAKPOINT_KEY: True}
def strip_cache_breakpoint(message: dict[str, Any]) -> None:
"""Remove the breakpoint flag from a message in place."""
message.pop(CACHE_BREAKPOINT_KEY, None)

View File

@@ -425,7 +425,7 @@ class AnthropicCompletion(BaseLLM):
def _prepare_completion_params(
self,
messages: list[LLMMessage],
system_message: str | list[dict[str, Any]] | None = None,
system_message: str | None = None,
tools: list[dict[str, Any]] | None = None,
available_functions: dict[str, Any] | None = None,
) -> dict[str, Any]:
@@ -665,7 +665,7 @@ class AnthropicCompletion(BaseLLM):
def _format_messages_for_anthropic(
self, messages: str | list[LLMMessage]
) -> tuple[list[LLMMessage], str | list[dict[str, Any]] | None]:
) -> tuple[list[LLMMessage], str | None]:
"""Format messages for Anthropic API.
Anthropic has specific requirements:
@@ -679,51 +679,8 @@ class AnthropicCompletion(BaseLLM):
messages: Input messages
Returns:
Tuple of (formatted_messages, system_message). `system_message` is
a list of content blocks (with cache_control stamped) when any
system message in the input carried a cache_breakpoint flag;
otherwise a plain string for backwards compatibility.
Tuple of (formatted_messages, system_message)
"""
from crewai.llms.cache import CACHE_BREAKPOINT_KEY
# Read cache_breakpoint flags from raw input BEFORE super strips them.
# We track the CONTENT of marked user/assistant messages so we can
# locate the corresponding block in formatted_messages — Anthropic
# rewrites tool results into user messages, so positional indices
# do not survive the conversion. We must stamp the original stable
# message (typically the initial task prompt), not whatever happens
# to be the trailing user-role block after tool_result expansion.
cache_system = False
cache_match_contents: list[str] = []
if not isinstance(messages, str):
for m in messages:
if not (isinstance(m, dict) and m.get(CACHE_BREAKPOINT_KEY)):
continue
role = m.get("role")
if role == "system":
cache_system = True
continue
if role != "user":
# Only user messages survive Anthropic's role-coalescing
# in a stable, addressable position. Markers on assistant
# or tool messages have no reliable stamp target after
# tool_result expansion, so we ignore them.
continue
raw_content = m.get("content")
if isinstance(raw_content, str) and raw_content:
cache_match_contents.append(raw_content)
continue
if isinstance(raw_content, list):
# Pull text from a single-text-block list so callers that
# pre-format content blocks still match cleanly.
text_blocks = [
b.get("text")
for b in raw_content
if isinstance(b, dict) and b.get("type") == "text"
]
if len(text_blocks) == 1 and isinstance(text_blocks[0], str):
cache_match_contents.append(text_blocks[0])
# Use base class formatting first
base_formatted = super()._format_messages(messages)
@@ -831,62 +788,7 @@ class AnthropicCompletion(BaseLLM):
# If first message is not from user, insert a user message at the beginning
formatted_messages.insert(0, {"role": "user", "content": "Hello"})
# Stamp cache_control on the message(s) whose original content was
# marked. We scan formatted_messages in order and stamp the first
# match per marked content — Anthropic permits up to 4 cache
# breakpoints per request, which is more than enough for our usage.
# Matching by content (rather than position) handles the ReAct
# case where tool_result blocks get expanded into trailing user
# messages: the stable initial-task prompt still maps cleanly.
for needle in cache_match_contents:
for fm in formatted_messages:
if fm.get("role") != "user":
continue
content = fm.get("content")
if isinstance(content, str) and content == needle:
self._stamp_cache_control_on_message(fm)
break
if isinstance(content, list):
fm_texts: list[str] = [
b.get("text", "")
for b in content
if isinstance(b, dict) and b.get("type") == "text"
]
if len(fm_texts) == 1 and fm_texts[0] == needle:
self._stamp_cache_control_on_message(fm)
break
# Convert system to content-block form when caching is requested.
system_payload: str | list[dict[str, Any]] | None = system_message
if system_message and cache_system:
system_payload = [
{
"type": "text",
"text": system_message,
"cache_control": {"type": "ephemeral"},
}
]
return formatted_messages, system_payload
@staticmethod
def _stamp_cache_control_on_message(message: LLMMessage) -> None:
"""Stamp cache_control on the last content block of an Anthropic message."""
msg = cast(dict[str, Any], message)
content = msg.get("content")
if isinstance(content, str):
msg["content"] = [
{
"type": "text",
"text": content,
"cache_control": {"type": "ephemeral"},
}
]
return
if isinstance(content, list) and content:
last = content[-1]
if isinstance(last, dict):
last["cache_control"] = {"type": "ephemeral"}
return formatted_messages, system_message
def _handle_completion(
self,

View File

@@ -6,7 +6,6 @@ from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
from typing_extensions import Self
from crewai.memory.types import (
_RECALL_OVERSAMPLE_FACTOR,
@@ -17,35 +16,15 @@ from crewai.memory.types import (
from crewai.memory.unified_memory import Memory
def _ensure_memory_kind(value: Any) -> Any:
"""Backfill ``memory_kind`` on legacy dicts that predate the discriminator.
Lets pre-1.14.6 configs/checkpoints flow into the discriminated
``Memory | MemoryScope | MemorySlice`` union without crashing. Inference:
``scopes`` key → ``slice``; ``root_path`` → ``scope``; else ``memory``.
Pass-through for non-dict values (instances, ``bool``, ``None``).
"""
if isinstance(value, dict) and "memory_kind" not in value:
if "scopes" in value:
value["memory_kind"] = "slice"
elif "root_path" in value:
value["memory_kind"] = "scope"
else:
value["memory_kind"] = "memory"
return value
class MemoryScope(BaseModel):
"""View of Memory restricted to a root path. All operations are scoped under that path."""
model_config = ConfigDict(arbitrary_types_allowed=True)
memory_kind: Literal["scope"] = "scope"
root_path: str = Field(default="/")
_memory: Memory | None = PrivateAttr(default=None)
_root: str = PrivateAttr(default="")
_memory: Memory = PrivateAttr()
_root: str = PrivateAttr()
@model_validator(mode="wrap")
@classmethod
@@ -55,38 +34,21 @@ class MemoryScope(BaseModel):
return data
if not isinstance(data, dict):
raise ValueError(f"Expected dict or MemoryScope, got {type(data).__name__}")
memory = data.pop("memory", None)
if "memory" not in data:
raise ValueError("MemoryScope requires a 'memory' key")
memory = data.pop("memory")
instance: MemoryScope = handler(data)
if memory is not None:
instance._memory = memory
instance._memory = memory
root = instance.root_path.rstrip("/") or ""
if root and not root.startswith("/"):
root = "/" + root
instance._root = root
return instance
def bind(self, memory: Memory) -> Self:
"""Rebind the runtime ``Memory`` dependency after restore.
Required after deserializing from a checkpoint, since the live
``Memory`` cannot be serialized.
"""
self._memory = memory
return self
def _require_memory(self) -> Memory:
"""Return the bound ``Memory`` or raise a clear error if missing."""
if self._memory is None:
raise RuntimeError(
"MemoryScope is not bound to a Memory; call .bind(memory) "
"after restore."
)
return self._memory
@property
def read_only(self) -> bool:
"""Whether the underlying memory is read-only."""
return self._require_memory().read_only
return self._memory.read_only
def _scope_path(self, scope: str | None) -> str:
if not scope or scope == "/":
@@ -111,7 +73,7 @@ class MemoryScope(BaseModel):
) -> MemoryRecord | None:
"""Remember content; scope is relative to this scope's root."""
path = self._scope_path(scope)
return self._require_memory().remember(
return self._memory.remember(
content,
scope=path,
categories=categories,
@@ -134,7 +96,7 @@ class MemoryScope(BaseModel):
) -> list[MemoryRecord]:
"""Remember multiple items; scope is relative to this scope's root."""
path = self._scope_path(scope)
return self._require_memory().remember_many(
return self._memory.remember_many(
contents,
scope=path,
categories=categories,
@@ -157,7 +119,7 @@ class MemoryScope(BaseModel):
) -> list[MemoryMatch]:
"""Recall within this scope (root path and below)."""
search_scope = self._scope_path(scope) if scope else (self._root or "/")
return self._require_memory().recall(
return self._memory.recall(
query,
scope=search_scope,
categories=categories,
@@ -169,7 +131,7 @@ class MemoryScope(BaseModel):
def extract_memories(self, content: str) -> list[str]:
"""Extract discrete memories from content; delegates to underlying Memory."""
return self._require_memory().extract_memories(content)
return self._memory.extract_memories(content)
def forget(
self,
@@ -181,7 +143,7 @@ class MemoryScope(BaseModel):
) -> int:
"""Forget within this scope."""
prefix = self._scope_path(scope) if scope else (self._root or "/")
return self._require_memory().forget(
return self._memory.forget(
scope=prefix,
categories=categories,
older_than=older_than,
@@ -192,27 +154,27 @@ class MemoryScope(BaseModel):
def list_scopes(self, path: str = "/") -> list[str]:
"""List child scopes under path (relative to this scope's root)."""
full = self._scope_path(path)
return self._require_memory().list_scopes(full)
return self._memory.list_scopes(full)
def info(self, path: str = "/") -> ScopeInfo:
"""Info for path under this scope."""
full = self._scope_path(path)
return self._require_memory().info(full)
return self._memory.info(full)
def tree(self, path: str = "/", max_depth: int = 3) -> str:
"""Tree under path within this scope."""
full = self._scope_path(path)
return self._require_memory().tree(full, max_depth=max_depth)
return self._memory.tree(full, max_depth=max_depth)
def list_categories(self, path: str | None = None) -> dict[str, int]:
"""Categories in this scope; path None means this scope root."""
full = self._scope_path(path) if path else (self._root or "/")
return self._require_memory().list_categories(full)
return self._memory.list_categories(full)
def reset(self, scope: str | None = None) -> None:
"""Reset within this scope."""
prefix = self._scope_path(scope) if scope else (self._root or "/")
self._require_memory().reset(scope=prefix)
self._memory.reset(scope=prefix)
def subscope(self, path: str) -> MemoryScope:
"""Return a narrower scope under this scope."""
@@ -229,13 +191,11 @@ class MemorySlice(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
memory_kind: Literal["slice"] = "slice"
scopes: list[str] = Field(default_factory=list)
categories: list[str] | None = Field(default=None)
read_only: bool = Field(default=True)
_memory: Memory | None = PrivateAttr(default=None)
_memory: Memory = PrivateAttr()
@model_validator(mode="wrap")
@classmethod
@@ -245,27 +205,14 @@ class MemorySlice(BaseModel):
return data
if not isinstance(data, dict):
raise ValueError(f"Expected dict or MemorySlice, got {type(data).__name__}")
memory = data.pop("memory", None)
if "memory" not in data:
raise ValueError("MemorySlice requires a 'memory' key")
memory = data.pop("memory")
data["scopes"] = [s.rstrip("/") or "/" for s in data.get("scopes", [])]
instance: MemorySlice = handler(data)
if memory is not None:
instance._memory = memory
instance._memory = memory
return instance
def bind(self, memory: Memory) -> Self:
"""Rebind the runtime ``Memory`` dependency after restore."""
self._memory = memory
return self
def _require_memory(self) -> Memory:
"""Return the bound ``Memory`` or raise a clear error if missing."""
if self._memory is None:
raise RuntimeError(
"MemorySlice is not bound to a Memory; call .bind(memory) "
"after restore."
)
return self._memory
def remember(
self,
content: str,
@@ -279,7 +226,7 @@ class MemorySlice(BaseModel):
"""Remember into an explicit scope. No-op when read_only=True."""
if self.read_only:
return None
return self._require_memory().remember(
return self._memory.remember(
content,
scope=scope,
categories=categories,
@@ -303,7 +250,7 @@ class MemorySlice(BaseModel):
cats = categories or self.categories
all_matches: list[MemoryMatch] = []
for sc in self.scopes:
matches = self._require_memory().recall(
matches = self._memory.recall(
query,
scope=sc,
categories=cats,
@@ -325,14 +272,14 @@ class MemorySlice(BaseModel):
def extract_memories(self, content: str) -> list[str]:
"""Extract discrete memories from content; delegates to underlying Memory."""
return self._require_memory().extract_memories(content)
return self._memory.extract_memories(content)
def list_scopes(self, path: str = "/") -> list[str]:
"""List scopes across all slice roots."""
out: list[str] = []
for sc in self.scopes:
full = f"{sc.rstrip('/')}{path}" if sc != "/" else path
out.extend(self._require_memory().list_scopes(full))
out.extend(self._memory.list_scopes(full))
return sorted(set(out))
def info(self, path: str = "/") -> ScopeInfo:
@@ -344,7 +291,7 @@ class MemorySlice(BaseModel):
children: list[str] = []
for sc in self.scopes:
full = f"{sc.rstrip('/')}{path}" if sc != "/" else path
inf = self._require_memory().info(full)
inf = self._memory.info(full)
total_records += inf.record_count
all_categories.update(inf.categories)
if inf.oldest_record:
@@ -374,6 +321,6 @@ class MemorySlice(BaseModel):
counts: dict[str, int] = {}
for sc in self.scopes:
full = (f"{sc.rstrip('/')}{path}" if sc != "/" else path) if path else sc
for k, v in self._require_memory().list_categories(full).items():
for k, v in self._memory.list_categories(full).items():
counts[k] = counts.get(k, 0) + v
return counts

View File

@@ -63,8 +63,6 @@ class Memory(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
memory_kind: Literal["memory"] = "memory"
llm: Annotated[BaseLLM | str, PlainValidator(_passthrough)] = Field(
default="gpt-4o-mini",
description="LLM for analysis (model name or BaseLLM instance).",

View File

@@ -3,20 +3,15 @@
Provides filesystem-based skill packaging with progressive disclosure.
"""
from crewai.skills.cache import SkillCacheManager
from crewai.skills.loader import activate_skill, discover_skills
from crewai.skills.models import Skill, SkillFrontmatter
from crewai.skills.parser import SkillParseError
from crewai.skills.registry import is_registry_ref, resolve_registry_ref
__all__ = [
"Skill",
"SkillCacheManager",
"SkillFrontmatter",
"SkillParseError",
"activate_skill",
"discover_skills",
"is_registry_ref",
"resolve_registry_ref",
]

View File

@@ -1,148 +0,0 @@
"""Cache manager for registry-downloaded skills.
Manages ~/.crewai/skills/{org}/{name}/ as the global skill cache.
One version is stored per skill (last install wins).
"""
from __future__ import annotations
from datetime import datetime, timezone
import json
import logging
from pathlib import Path
import tarfile
from typing import TypedDict
import zipfile
_logger = logging.getLogger(__name__)
_CACHE_ROOT = Path.home() / ".crewai" / "skills"
_META_FILENAME = ".crewai_meta.json"
class SkillMetadata(TypedDict):
org: str
name: str
version: str | None
installed_at: str
class SkillCacheManager:
"""Manages the global skill cache at ~/.crewai/skills/."""
def __init__(self, cache_root: Path | None = None) -> None:
self._root = cache_root or _CACHE_ROOT
def _skill_dir(self, org: str, name: str) -> Path:
return self._root / org / name
def get_cached_path(self, org: str, name: str) -> Path | None:
"""Return the cached skill directory path if it exists, else None."""
skill_dir = self._skill_dir(org, name)
meta_file = skill_dir / _META_FILENAME
if skill_dir.is_dir() and meta_file.exists():
return skill_dir
return None
def store(
self, org: str, name: str, version: str | None, archive_bytes: bytes
) -> Path:
"""Unpack an archive into the cache and write metadata.
Uses tarfile with filter='data' for path-traversal protection.
Args:
org: Organisation slug.
name: Skill name.
version: Semantic version string, or None if unknown.
archive_bytes: Raw bytes of a .tar.gz archive.
Returns:
Path to the stored skill directory.
"""
skill_dir = self._skill_dir(org, name)
# Wipe any previous version
if skill_dir.exists():
import shutil
shutil.rmtree(skill_dir)
skill_dir.mkdir(parents=True, exist_ok=True)
import io
# Try tar.gz first, fall back to zip
try:
with tarfile.open(fileobj=io.BytesIO(archive_bytes), mode="r:gz") as tf:
try:
tf.extractall(skill_dir, filter="data")
except TypeError:
_safe_extractall(tf, skill_dir)
except tarfile.TarError:
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as zf:
_safe_extract_zip(zf, skill_dir)
meta: SkillMetadata = {
"org": org,
"name": name,
"version": version,
"installed_at": datetime.now(tz=timezone.utc).isoformat(),
}
(skill_dir / _META_FILENAME).write_text(json.dumps(meta, indent=2))
return skill_dir
def list_cached(self) -> list[SkillMetadata]:
"""Return metadata for every cached skill."""
results: list[SkillMetadata] = []
if not self._root.exists():
return results
for org_dir in sorted(self._root.iterdir()):
if not org_dir.is_dir():
continue
for skill_dir in sorted(org_dir.iterdir()):
meta_file = skill_dir / _META_FILENAME
if meta_file.exists():
try:
results.append(json.loads(meta_file.read_text()))
except (json.JSONDecodeError, KeyError):
_logger.debug(
"Skipping malformed cache entry: %s",
meta_file,
exc_info=True,
)
return results
def invalidate(self, org: str, name: str) -> bool:
"""Remove a cached skill.
Returns:
True if the cache entry existed and was removed, False otherwise.
"""
skill_dir = self._skill_dir(org, name)
if skill_dir.exists():
import shutil
shutil.rmtree(skill_dir)
return True
return False
def _safe_extractall(tf: tarfile.TarFile, dest: Path) -> None:
"""Path-traversal-safe extraction for Python < 3.12."""
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}")
tf.extractall(dest) # noqa: S202
def _safe_extract_zip(zf: zipfile.ZipFile, dest: Path) -> None:
"""Path-traversal-safe ZIP extraction."""
dest_resolved = dest.resolve()
for member in zf.namelist():
member_path = (dest / member).resolve()
if not member_path.is_relative_to(dest_resolved):
raise ValueError(f"Blocked path traversal attempt: {member!r}")
zf.extractall(dest) # noqa: S202

View File

@@ -161,9 +161,6 @@ def format_skill_context(skill: Skill) -> str:
At METADATA level: returns name and description only.
At INSTRUCTIONS level or above: returns full SKILL.md body.
Output is wrapped in <skill name="..."> XML tags so the block can serve
as a stable cache anchor when injected into the system prompt.
Args:
skill: The skill to format.
@@ -172,7 +169,7 @@ def format_skill_context(skill: Skill) -> str:
"""
if skill.disclosure_level >= INSTRUCTIONS and skill.instructions:
parts = [
f'<skill name="{skill.name}">',
f"## Skill: {skill.name}",
skill.description,
"",
skill.instructions,
@@ -183,6 +180,5 @@ def format_skill_context(skill: Skill) -> str:
for dir_name, files in sorted(skill.resource_files.items()):
if files:
parts.append(f"- **{dir_name}/**: {', '.join(files)}")
parts.append("</skill>")
return "\n".join(parts)
return f'<skill name="{skill.name}">\n{skill.description}\n</skill>'
return f"## Skill: {skill.name}\n{skill.description}"

View File

@@ -78,10 +78,6 @@ class SkillFrontmatter(BaseModel):
alias="allowed-tools",
description="Pre-approved tool names the skill may use, parsed from a space-delimited string in frontmatter.",
)
version: str | None = Field(
default=None,
description="Semantic version of the skill, e.g. '1.0.0'. Optional for local skills.",
)
@model_validator(mode="before")
@classmethod

View File

@@ -1,223 +0,0 @@
"""Registry reference resolution for the Agent Skills standard.
Handles @org/skill-name references, local-first resolution, and downloads
via the CrewAI+ API with a global cache at ~/.crewai/skills/.
"""
from __future__ import annotations
import logging
from pathlib import Path
import sys
from typing import Any
from crewai.skills.cache import SkillCacheManager
_logger = logging.getLogger(__name__)
class SkillNotCachedError(Exception):
"""Raised when a registry skill is not cached and the environment is non-interactive."""
def __init__(self, ref: str) -> None:
super().__init__(
f"Skill {ref!r} is not cached locally. "
f"Run `crewai skill install {ref}` to install it first."
)
self.ref = ref
def is_registry_ref(value: Any) -> bool:
"""Return True if *value* looks like a registry reference (@org/name)."""
return isinstance(value, str) and value.startswith("@")
def parse_registry_ref(ref: str) -> tuple[str, str]:
"""Parse '@org/skill-name' into (org, name).
Args:
ref: A registry reference, e.g. '@acme/my-skill'.
Returns:
A (org, name) tuple.
Raises:
ValueError: If the reference format is invalid.
"""
if not ref.startswith("@"):
raise ValueError(f"Registry reference must start with '@', got: {ref!r}")
without_at = ref[1:]
if without_at.count("/") != 1:
raise ValueError(
f"Registry reference must be in '@org/name' format, got: {ref!r}"
)
org, name = without_at.split("/", 1)
if (
not org
or not name
or org.startswith(".")
or name.startswith(".")
or "/" in org
or "/" in name
):
raise ValueError(
f"Registry reference org and name must be single, non-empty path "
f"segments (no '..' or leading dots), got: {ref!r}"
)
return org, name
def _is_noninteractive() -> bool:
"""Return True in CI or explicitly non-interactive environments."""
import os
return (
os.environ.get("CI") == "1"
or os.environ.get("CREWAI_NONINTERACTIVE") == "1"
or not sys.stdin.isatty()
)
def resolve_registry_ref(
ref: str,
source: Any = None,
) -> Skill: # type: ignore[name-defined] # noqa: F821
"""Resolve a registry reference to a Skill object.
Resolution order:
1. ./skills/{name}/ in the current working directory (project-local)
2. ~/.crewai/skills/{org}/{name}/ (global cache)
3. Download from registry (interactive only; raises SkillNotCachedError in CI)
Args:
ref: A registry reference, e.g. '@acme/my-skill'.
source: Optional source object passed through to skill loaders (for events).
Returns:
A Skill loaded at INSTRUCTIONS disclosure level.
Raises:
SkillNotCachedError: When not cached and running in non-interactive mode.
"""
from crewai.skills.loader import activate_skill
from crewai.skills.parser import load_skill_metadata
org, name = parse_registry_ref(ref)
# 1. Project-local: ./skills/{name}/
local_path = Path.cwd() / "skills" / name
if local_path.is_dir() and (local_path / "SKILL.md").exists():
try:
skill = load_skill_metadata(local_path)
return activate_skill(skill, source=source)
except Exception:
_logger.debug("Failed to load local skill at %s", local_path, exc_info=True)
# 2. Global cache
cache = SkillCacheManager()
cached_path = cache.get_cached_path(org, name)
if cached_path is not None and (cached_path / "SKILL.md").exists():
try:
skill = load_skill_metadata(cached_path)
return activate_skill(skill, source=source)
except Exception:
_logger.debug(
"Failed to load cached skill at %s", cached_path, exc_info=True
)
# 3. Download
if _is_noninteractive():
raise SkillNotCachedError(ref)
return download_skill(org, name, source=source)
def download_skill(
org: str,
name: str,
source: Any = None,
) -> Skill: # type: ignore[name-defined] # noqa: F821
"""Download a skill from the registry and store it in the cache.
Args:
org: Organisation slug.
name: Skill name.
source: Optional source for event emission.
Returns:
The downloaded Skill at INSTRUCTIONS level.
"""
from crewai.skills.loader import activate_skill
from crewai.skills.parser import load_skill_metadata
ref = f"@{org}/{name}"
try:
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.skill_events import (
SkillDownloadCompletedEvent,
SkillDownloadStartedEvent,
)
_has_events = True
except ImportError:
_has_events = False
if _has_events:
crewai_event_bus.emit(
source,
event=SkillDownloadStartedEvent(
registry_ref=ref,
),
)
try:
from crewai_core.plus_api import PlusAPI
api = PlusAPI()
response = api.get_skill(org, name)
response.raise_for_status()
data = response.json()
except Exception as exc:
raise RuntimeError(
f"Failed to download skill {ref!r} from registry: {exc}"
) from exc
import base64
import httpx
version = data.get("latest_version") or data.get("version")
download_url = data.get("download_url")
if download_url:
dl_response = httpx.get(download_url, follow_redirects=True)
dl_response.raise_for_status()
archive_bytes = dl_response.content
else:
encoded = data.get("file", "")
# Strip data URI prefix if present
if "," in encoded:
encoded = encoded.split(",", 1)[1]
archive_bytes = base64.b64decode(encoded)
cache = SkillCacheManager()
skill_dir = cache.store(org, name, version, archive_bytes)
if _has_events:
crewai_event_bus.emit(
source,
event=SkillDownloadCompletedEvent(
registry_ref=ref,
version=version,
cache_path=skill_dir,
),
)
if not (skill_dir / "SKILL.md").exists():
raise RuntimeError(
f"Skill archive for {ref!r} downloaded but no SKILL.md found in {skill_dir}"
)
skill = load_skill_metadata(skill_dir)
return activate_skill(skill, source=source)

View File

@@ -113,68 +113,12 @@ def _migrate(data: dict[str, Any]) -> dict[str, Any]:
)
# --- migrations in version order ---
if stored < Version("1.14.6"):
for entity in data.get("entities") or []:
_backfill_discriminators(entity)
# if stored < Version("X.Y.Z"):
# data.setdefault("some_field", "default")
return data
def _backfill_memory_kind(value: Any) -> None:
"""Infer ``memory_kind`` from structural fields on legacy memory dicts."""
if not isinstance(value, dict) or "memory_kind" in value:
return
if "scopes" in value:
value["memory_kind"] = "slice"
elif "root_path" in value:
value["memory_kind"] = "scope"
else:
value["memory_kind"] = "memory"
def _backfill_source_type(source: Any) -> None:
"""Infer ``source_type`` for legacy knowledge source dicts when possible.
Only StringKnowledgeSource is reliably inferrable: it stores ``content``
as a plain string. File-based sources (CSV/PDF/Excel/JSON/docling) also
have a ``content`` field but populate it with dicts/lists, so we leave
those untagged and let downstream validation surface a clear error.
"""
if not isinstance(source, dict) or "source_type" in source:
return
if isinstance(source.get("content"), str):
source["source_type"] = "string"
return
raise ValueError(
"Legacy knowledge source is missing 'source_type' and could not be "
"inferred during migration. Re-checkpoint after upgrading to 1.14.6+."
)
def _backfill_sources_on(container: Any) -> None:
"""Apply source_type backfill to ``sources`` and ``knowledge_sources`` lists."""
if not isinstance(container, dict):
return
for key in ("sources", "knowledge_sources"):
for src in container.get(key) or []:
_backfill_source_type(src)
def _backfill_discriminators(entity: Any) -> None:
"""Walk an entity dict and backfill discriminator fields added in 1.14.6."""
if not isinstance(entity, dict):
return
_backfill_memory_kind(entity.get("memory"))
_backfill_sources_on(entity)
_backfill_sources_on(entity.get("knowledge"))
for agent in entity.get("agents") or []:
if not isinstance(agent, dict):
continue
_backfill_memory_kind(agent.get("memory"))
_backfill_sources_on(agent)
_backfill_sources_on(agent.get("knowledge"))
class RuntimeState(RootModel): # type: ignore[type-arg]
root: list[Entity]
_provider: BaseProvider = PrivateAttr(default_factory=JsonProvider)

View File

@@ -0,0 +1,206 @@
"""OpenSandbox tool for CrewAI agents.
OpenSandbox (https://open-sandbox.ai) is a self-hosted sandbox platform
for running shell commands and managing files inside isolated containers.
This tool exposes its core operations to CrewAI agents through a single
``OpenSandboxTool`` that lazily creates one sandbox per tool instance and
reuses it across calls until ``kill`` is invoked.
"""
from __future__ import annotations
import asyncio
import concurrent.futures
from datetime import timedelta
import os
from typing import Any, Literal
from pydantic import BaseModel, Field, PrivateAttr
from crewai.tools.base_tool import BaseTool, EnvVar
class OpenSandboxToolSchema(BaseModel):
"""Arguments accepted by ``OpenSandboxTool``."""
action: Literal["run_command", "read_file", "write_file", "kill"] = Field(
description=(
"Operation to perform: run_command (execute shell command), "
"read_file (read file contents), write_file (write file contents), "
"or kill (terminate the sandbox)."
),
)
command: str | None = Field(
default=None,
description="Shell command to execute. Required when action is 'run_command'.",
)
path: str | None = Field(
default=None,
description="Absolute file path. Required for 'read_file' and 'write_file'.",
)
content: str | None = Field(
default=None,
description="File content to write. Required when action is 'write_file'.",
)
class OpenSandboxTool(BaseTool):
"""Run shell commands and manage files inside an OpenSandbox sandbox."""
name: str = "OpenSandbox"
description: str = (
"Execute commands and manage files in an isolated OpenSandbox container. "
"Useful for running untrusted code, scripting, file I/O, or any work that "
"should be isolated from the host. The same sandbox is reused across "
"calls; invoke action='kill' to release it."
)
args_schema: type[BaseModel] = OpenSandboxToolSchema
env_vars: list[EnvVar] = Field(
default_factory=lambda: [
EnvVar(
name="OPENSANDBOX_DOMAIN",
description="Host:port of the OpenSandbox server (e.g. 'localhost:8080').",
required=True,
),
EnvVar(
name="OPENSANDBOX_PROTOCOL",
description="Protocol used to reach the server: 'http' or 'https'.",
required=False,
default="http",
),
EnvVar(
name="OPENSANDBOX_IMAGE",
description="Container image to launch (e.g. 'python:3.12').",
required=False,
default="python:3.12",
),
EnvVar(
name="OPENSANDBOX_TIMEOUT_MINUTES",
description="Sandbox idle timeout in minutes before auto-shutdown.",
required=False,
default="30",
),
EnvVar(
name="OPENSANDBOX_API_KEY",
description="Optional API key if the OpenSandbox server requires auth.",
required=False,
default=None,
),
]
)
_sandbox: Any = PrivateAttr(default=None)
def _run(self, **kwargs: Any) -> str:
action = kwargs.get("action")
command = kwargs.get("command")
path = kwargs.get("path")
content = kwargs.get("content")
if action == "kill":
return self._run_async(self._kill())
if action == "run_command":
if not command:
return "Error: 'command' is required when action='run_command'."
return self._run_async(self._run_command(command))
if action == "read_file":
if not path:
return "Error: 'path' is required when action='read_file'."
return self._run_async(self._read_file(path))
if action == "write_file":
if not path:
return "Error: 'path' is required when action='write_file'."
if content is None:
return "Error: 'content' is required when action='write_file'."
return self._run_async(self._write_file(path, content))
return f"Error: unknown action '{action}'."
@staticmethod
def _run_async(coro: Any) -> str:
"""Run ``coro`` to completion from a sync context, regardless of loop state."""
try:
asyncio.get_running_loop()
except RuntimeError:
return asyncio.run(coro)
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
return executor.submit(asyncio.run, coro).result()
def _build_connection_config(self) -> Any:
from opensandbox.config.connection import ConnectionConfig
domain = (os.getenv("OPENSANDBOX_DOMAIN") or "").strip()
if not domain:
raise ValueError(
"OPENSANDBOX_DOMAIN is not set. Configure it to point at your "
"OpenSandbox server (e.g. 'localhost:8080')."
)
protocol = (os.getenv("OPENSANDBOX_PROTOCOL") or "http").strip()
api_key = os.getenv("OPENSANDBOX_API_KEY") or None
return ConnectionConfig(domain=domain, protocol=protocol, api_key=api_key)
async def _ensure_sandbox(self) -> Any:
if self._sandbox is not None:
return self._sandbox
from opensandbox import Sandbox
image = (os.getenv("OPENSANDBOX_IMAGE") or "python:3.12").strip()
timeout_minutes = int(os.getenv("OPENSANDBOX_TIMEOUT_MINUTES") or "30")
connection_config = self._build_connection_config()
self._sandbox = await Sandbox.create(
image,
timeout=timedelta(minutes=timeout_minutes),
connection_config=connection_config,
)
return self._sandbox
async def _run_command(self, command: str) -> str:
try:
sandbox = await self._ensure_sandbox()
execution = await sandbox.commands.run(command)
except Exception as exc:
return f"OpenSandbox error running command: {exc}"
stdout = "".join(
getattr(item, "text", "") for item in (execution.logs.stdout or [])
)
stderr = "".join(
getattr(item, "text", "") for item in (execution.logs.stderr or [])
)
parts: list[str] = []
if stdout:
parts.append(stdout)
if stderr:
parts.append(f"stderr:\n{stderr}")
if getattr(execution, "error", None):
parts.append(f"error: {execution.error}")
return "\n".join(parts).strip() or "(no output)"
async def _read_file(self, path: str) -> str:
try:
sandbox = await self._ensure_sandbox()
return await sandbox.files.read_file(path)
except Exception as exc:
return f"OpenSandbox error reading {path}: {exc}"
async def _write_file(self, path: str, content: str) -> str:
from opensandbox.models import WriteEntry
try:
sandbox = await self._ensure_sandbox()
await sandbox.files.write_files(
[WriteEntry(path=path, data=content, mode=0o644)]
)
except Exception as exc:
return f"OpenSandbox error writing {path}: {exc}"
return f"Wrote {len(content)} bytes to {path}."
async def _kill(self) -> str:
if self._sandbox is None:
return "No sandbox to kill."
try:
await self._sandbox.kill()
except Exception as exc:
self._sandbox = None
return f"OpenSandbox error during kill: {exc}"
self._sandbox = None
return "Sandbox killed."

View File

@@ -19,15 +19,6 @@ from pydantic import BeforeValidator, WithJsonSchema
from pydantic.functional_serializers import PlainSerializer
_TRUSTED_DESERIALIZE_VALUES = frozenset({"1", "true", "yes"})
def _trusted_deserialize() -> bool:
"""Return True only if ``CREWAI_DESERIALIZE_CALLBACKS`` is an explicit yes."""
raw = os.environ.get("CREWAI_DESERIALIZE_CALLBACKS", "")
return raw.strip().lower() in _TRUSTED_DESERIALIZE_VALUES
def _is_non_roundtrippable(fn: object) -> bool:
"""Return ``True`` if *fn* cannot survive a serialize/deserialize round-trip.
@@ -85,7 +76,7 @@ def string_to_callable(value: Any) -> Callable[..., Any]:
raise ValueError(
f"Invalid callback path {value!r}: expected 'module.name' format"
)
if not _trusted_deserialize():
if not os.environ.get("CREWAI_DESERIALIZE_CALLBACKS"):
raise ValueError(
f"Refusing to resolve callback path {value!r}: "
"set CREWAI_DESERIALIZE_CALLBACKS=1 to allow. "
@@ -159,78 +150,3 @@ SerializableCallable = Annotated[
PlainSerializer(callable_to_string, return_type=str, when_used="json"),
WithJsonSchema({"type": "string"}),
]
def _instance_to_dotted_path(value: Any) -> str:
"""Serialize an instance to a dotted path naming its class."""
if inspect.isclass(value):
module = getattr(value, "__module__", "<unknown>")
qualname = getattr(
value, "__qualname__", getattr(value, "__name__", str(type(value)))
)
raise ValueError(f"Expected an instance, got class {module}.{qualname}.")
cls = type(value)
if cls.__module__ == "builtins":
raise ValueError(
f"Cannot serialize {value!r}: builtin values are not "
"checkpointable instances."
)
module = getattr(cls, "__module__", None)
qualname = getattr(cls, "__qualname__", None)
if module is None or qualname is None:
raise ValueError(
f"Cannot serialize {value!r}: class missing __module__ or __qualname__. "
"Use a module-level class for checkpointable instances."
)
if qualname.endswith("<lambda>") or "<locals>" in qualname:
raise ValueError(
f"Cannot serialize {value!r}: class defined in <locals>. "
"Use a module-level class for checkpointable instances."
)
return f"{module}.{qualname}"
def _dotted_path_to_instance(value: Any) -> Any:
"""Resolve a dotted path to a class and instantiate it with no args.
If *value* is already a non-string object it is returned as-is.
"""
if value is None:
return value
if not isinstance(value, str):
if inspect.isclass(value):
raise ValueError(
f"Expected an instance or dotted path string, got class "
f"{getattr(value, '__module__', '<unknown>')}."
f"{getattr(value, '__qualname__', getattr(value, '__name__', ''))}."
)
if type(value).__module__ == "builtins":
raise ValueError(
f"Expected an instance of a user-defined class or dotted "
f"path string, got builtin value {value!r}."
)
return value
if "." not in value:
raise ValueError(
f"Invalid provider path {value!r}: expected 'module.name' format"
)
if not _trusted_deserialize():
raise ValueError(
f"Refusing to resolve provider path {value!r}: "
"set CREWAI_DESERIALIZE_CALLBACKS=1 to allow. "
"Only enable this for trusted checkpoint data."
)
cls = _resolve_dotted_path(value)
if not inspect.isclass(cls):
raise ValueError(
f"Invalid provider path {value!r}: expected a class, got "
f"{type(cls).__name__}"
)
try:
return cls()
except TypeError as exc:
raise ValueError(
f"Cannot reinstantiate {value!r} with no arguments: {exc}. "
"Only no-arg constructors are checkpointable; rebuild the "
"instance manually and assign it after restore."
) from exc

View File

@@ -13,7 +13,6 @@ import sys
import types
from typing import Any, cast, get_type_hints
from crewai_core.plus_api import AvailableExport, EnvVarEntry, ToolMetadata
from crewai_core.project import (
get_project_description as get_project_description,
get_project_name as get_project_name,
@@ -280,7 +279,7 @@ def is_valid_tool(obj: Any) -> bool:
return isinstance(obj, Tool)
def extract_available_exports(dir_path: str = "src") -> list[AvailableExport]:
def extract_available_exports(dir_path: str = "src") -> list[dict[str, Any]]:
"""Extract available tool classes from the project's __init__.py files.
Only includes classes that inherit from BaseTool or functions decorated with @tool.
@@ -339,7 +338,7 @@ def _load_module_from_file(
sys.modules.pop(module_name, None)
def _load_tools_from_init(init_file: Path) -> list[AvailableExport]:
def _load_tools_from_init(init_file: Path) -> list[dict[str, Any]]:
"""Load and validate tools from a given __init__.py file."""
try:
with _load_module_from_file(init_file) as module:
@@ -393,7 +392,7 @@ def _print_no_tools_warning() -> None:
)
def extract_tools_metadata(dir_path: str = "src") -> list[ToolMetadata]:
def extract_tools_metadata(dir_path: str = "src") -> list[dict[str, Any]]:
"""
Extract rich metadata from tool classes in the project.
@@ -405,7 +404,7 @@ def extract_tools_metadata(dir_path: str = "src") -> list[ToolMetadata]:
- init_params_schema: JSON Schema for __init__ params (filtered)
- env_vars: List of environment variable dicts
"""
tools_metadata: list[ToolMetadata] = []
tools_metadata: list[dict[str, Any]] = []
for init_file in Path(dir_path).glob("**/__init__.py"):
tools = _extract_tool_metadata_from_init(init_file)
@@ -414,7 +413,7 @@ def extract_tools_metadata(dir_path: str = "src") -> list[ToolMetadata]:
return tools_metadata
def _extract_tool_metadata_from_init(init_file: Path) -> list[ToolMetadata]:
def _extract_tool_metadata_from_init(init_file: Path) -> list[dict[str, Any]]:
"""
Load module from init file and extract metadata from valid tool classes.
"""
@@ -429,7 +428,7 @@ def _extract_tool_metadata_from_init(init_file: Path) -> list[ToolMetadata]:
if not exported_names:
return []
tools_metadata: list[ToolMetadata] = []
tools_metadata = []
for name in exported_names:
obj = getattr(module, name, None)
if obj is None or not (
@@ -447,7 +446,7 @@ def _extract_tool_metadata_from_init(init_file: Path) -> list[ToolMetadata]:
return []
def _extract_single_tool_metadata(tool_class: type) -> ToolMetadata | None:
def _extract_single_tool_metadata(tool_class: type) -> dict[str, Any] | None:
"""
Extract metadata from a single tool class.
"""
@@ -471,17 +470,19 @@ def _extract_single_tool_metadata(tool_class: type) -> ToolMetadata | None:
except (TypeError, ValueError):
module = tool_class.__module__
return ToolMetadata(
name=tool_class.__name__,
module=module,
humanized_name=str(
_extract_field_default(fields.get("name"), fallback=tool_class.__name__)
return {
"name": tool_class.__name__,
"module": module,
"humanized_name": _extract_field_default(
fields.get("name"), fallback=tool_class.__name__
),
description=str(_extract_field_default(fields.get("description"))).strip(),
run_params_schema=_extract_run_params_schema(fields.get("args_schema")),
init_params_schema=_extract_init_params_schema(tool_class),
env_vars=_extract_env_vars(fields.get("env_vars")),
)
"description": str(
_extract_field_default(fields.get("description"))
).strip(),
"run_params_schema": _extract_run_params_schema(fields.get("args_schema")),
"init_params_schema": _extract_init_params_schema(tool_class),
"env_vars": _extract_env_vars(fields.get("env_vars")),
}
except Exception:
return None
@@ -596,7 +597,7 @@ def _extract_init_params_schema(tool_class: type) -> dict[str, Any]:
return {}
def _extract_env_vars(env_vars_field: dict[str, Any] | None) -> list[EnvVarEntry]:
def _extract_env_vars(env_vars_field: dict[str, Any] | None) -> list[dict[str, Any]]:
"""
Extract environment variable definitions from env_vars field.
"""

View File

@@ -86,7 +86,7 @@ class Prompts(BaseModel):
slices.append("tools")
else:
slices.append("no_tools")
system: str = self._build_prompt(slices) + self._build_skill_block()
system: str = self._build_prompt(slices)
# Determine which task slice to use:
task_slice: COMPONENTS
@@ -106,7 +106,7 @@ class Prompts(BaseModel):
return SystemPromptResult(
system=system,
user=self._build_prompt([task_slice]),
prompt=self._build_prompt(slices) + self._build_skill_block(),
prompt=self._build_prompt(slices),
)
return StandardPromptResult(
prompt=self._build_prompt(
@@ -115,27 +115,8 @@ class Prompts(BaseModel):
self.prompt_template,
self.response_template,
)
+ self._build_skill_block()
)
def _build_skill_block(self) -> str:
"""Render the agent's activated skills as a stable XML block.
Skills are agent-scoped (do not change per task), so they live in the
system prompt where prompt-cache prefixes can survive across calls.
"""
skills = getattr(self.agent, "skills", None)
if not skills:
return ""
from crewai.skills.loader import format_skill_context
from crewai.skills.models import Skill
sections = [format_skill_context(s) for s in skills if isinstance(s, Skill)]
if not sections:
return ""
return "\n\n<skills>\n" + "\n\n".join(sections) + "\n</skills>"
def _build_prompt(
self,
components: list[COMPONENTS],

View File

@@ -25,16 +25,10 @@ def _reset_flow_memory(flow: Flow[Any]) -> None:
try:
if hasattr(mem, "reset"):
mem.reset()
elif hasattr(mem, "_memory") and mem._memory is not None:
elif hasattr(mem, "_memory") and hasattr(mem._memory, "reset"):
mem._memory.reset()
except FileNotFoundError:
# Storage directory was never created — nothing to reset.
except (FileNotFoundError, OSError):
pass
except OSError as exc:
click.echo(f"Memory reset skipped: storage I/O error ({exc}).", err=True)
except RuntimeError as exc:
# Restored MemoryScope/MemorySlice without a rebound Memory.
click.echo(f"Memory reset skipped: {exc}", err=True)
def reset_memories_command(

View File

@@ -389,8 +389,10 @@ def test_agent_custom_max_iterations():
assert result is not None
assert isinstance(result, str)
assert len(result) > 0
# With max_iter=1, exactly two provider calls are expected:
# one inside the reasoning loop and one for the forced final answer.
assert call_count > 0
# With max_iter=1, expect 2 calls:
# - Call 1: iteration 0
# - Call 2: iteration 1 (max reached, handle_max_iterations_exceeded called, then loop breaks)
assert call_count == 2
@@ -700,7 +702,6 @@ def test_agent_definition_based_on_dict():
# test for human input
@pytest.mark.vcr()
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_agent_human_input():
from crewai.core.providers.human_input import SyncHumanInputProvider
@@ -709,7 +710,6 @@ def test_agent_human_input():
"role": "test role",
"goal": "test goal",
"backstory": "test backstory",
"executor_class": CrewAgentExecutor,
}
agent = Agent(**config)
@@ -839,9 +839,7 @@ Thought:<|eot_id|>
"""
from crewai.experimental.agent_executor import AgentExecutor
with patch.object(AgentExecutor, "_format_prompt") as mock_format_prompt:
with patch.object(CrewAgentExecutor, "_format_prompt") as mock_format_prompt:
mock_format_prompt.return_value = expected_prompt
# Trigger the _format_prompt method
@@ -1100,11 +1098,9 @@ def test_agent_max_retry_limit():
agent.create_agent_executor(task=task)
from crewai.experimental.agent_executor import AgentExecutor
error_message = "Error happening while sending prompt to model."
with patch.object(
AgentExecutor, "invoke", wraps=agent.agent_executor.invoke
CrewAgentExecutor, "invoke", wraps=agent.agent_executor.invoke
) as invoke_mock:
invoke_mock.side_effect = Exception(error_message)
@@ -1287,10 +1283,8 @@ def test_handle_context_length_exceeds_limit_cli_no():
agent.create_agent_executor(task=task)
from crewai.experimental.agent_executor import AgentExecutor
with patch.object(
AgentExecutor, "invoke", wraps=agent.agent_executor.invoke
CrewAgentExecutor, "invoke", wraps=agent.agent_executor.invoke
) as private_mock:
task = Task(
description="The final answer is 42. But don't give it yet, instead keep using the `get_final_answer` tool.",

View File

@@ -286,6 +286,8 @@ def test_agent_execute_task_with_planning():
assert result is not None
assert "20" in str(result)
# Planning should be appended to task description
assert "Planning:" in task.description
@pytest.mark.vcr()
@@ -340,3 +342,4 @@ def test_agent_execute_task_with_planning_refine():
assert result is not None
# Area = pi * r^2 = 3.14 * 25 = 78.5
assert "78" in str(result) or "79" in str(result)
assert "Planning:" in task.description

Some files were not shown because too many files have changed in this diff Show More