Compare commits

..

8 Commits

Author SHA1 Message Date
Devin AI
84a72c4350 fix: remove type: ignore comments that fail on older Python mypy
Use getattr/object.__setattr__ pattern to avoid version-dependent
type: ignore comments that cause unused-ignore errors on Python 3.10/3.11.

Co-Authored-By: João <joao@crewai.com>
2026-04-15 19:19:27 +00:00
Devin AI
1305bfc7ea fix: make _resolving_refs thread-safe via threading.local()
Address Bugbot review: replace module-level set with threading.local()
so concurrent schema conversions in ThreadPoolExecutor don't interfere.

Co-Authored-By: João <joao@crewai.com>
2026-04-15 19:12:39 +00:00
Devin AI
ae09793712 fix: handle circular $ref in MCP tool JSON schemas (#5474)
MCP servers exposing self-referential JSON schemas (e.g. ms-365-mcp-server
with >10 tools) triggered 'maximum recursion depth exceeded' because:

1. jsonref.replace_refs(proxies=False) infinitely inlines circular $refs
2. Downstream recursive visitors (force_additional_properties_false, etc.)
   loop on the resulting circular Python dicts
3. resolve_refs and _json_schema_to_pydantic_type had no cycle detection

Fix:
- Add _has_circular_refs() to detect circular $ref chains
- Add _break_circular_refs() to replace circular refs with {type: object} stubs
- Wrap jsonref.replace_refs in _safe_replace_refs() that breaks cycles first
- Add cycle detection to resolve_refs() using a resolving-set parameter
- Add cycle detection to _json_schema_to_pydantic_type() via _resolving_refs

Tests added for all new helpers and end-to-end circular schema scenarios.

Co-Authored-By: João <joao@crewai.com>
2026-04-15 19:03:12 +00:00
Greyson LaLonde
1c90d574ab docs: update changelog and version for v1.14.2a5
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
2026-04-15 22:45:15 +08:00
Greyson LaLonde
3a7c550512 feat: bump versions to 1.14.2a5 2026-04-15 22:40:48 +08:00
Greyson LaLonde
5b6f89fe64 docs: update changelog and version for v1.14.2a4
Some checks failed
Vulnerability Scan / pip-audit (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
2026-04-15 02:34:32 +08:00
Greyson LaLonde
ad5e66d1d0 feat: bump versions to 1.14.2a4 2026-04-15 02:29:06 +08:00
Greyson LaLonde
94e7d86df1 fix: stop forwarding strict mode to Bedrock Converse API
Forwarding strict and sanitizing tool schemas for strict mode causes
Bedrock Converse requests to hang until timeout. Drop strict forwarding
and schema sanitization from the Bedrock provider.
2026-04-15 02:22:50 +08:00
102 changed files with 933 additions and 429 deletions

View File

@@ -19,7 +19,7 @@ repos:
language: system
pass_filenames: true
types: [python]
exclude: ^(lib/crewai/src/crewai/cli/templates/|lib/crewai/tests/|lib/crewai-tools/tests/|lib/crewai-files/tests/|lib/crewai-a2a/tests/)
exclude: ^(lib/crewai/src/crewai/cli/templates/|lib/crewai/tests/|lib/crewai-tools/tests/|lib/crewai-files/tests/)
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.11.3
hooks:

View File

@@ -4,6 +4,46 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
<Update label="15 أبريل 2026">
## v1.14.2a5
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
## ما الذي تغير
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.14.2a4
## المساهمون
@greysonlalonde
</Update>
<Update label="15 أبريل 2026">
## v1.14.2a4
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
## ما الذي تغير
### الميزات
- إضافة تلميحات استئناف إلى إصدار أدوات المطورين عند الفشل
### إصلاحات الأخطاء
- إصلاح توجيه وضع الصرامة إلى واجهة برمجة تطبيقات Bedrock Converse
- إصلاح إصدار pytest إلى 9.0.3 لثغرة الأمان GHSA-6w46-j5rx-g56g
- رفع الحد الأدنى لـ OpenAI إلى >=2.0.0
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.14.2a3
## المساهمون
@greysonlalonde
</Update>
<Update label="13 أبريل 2026">
## v1.14.2a3

View File

@@ -4,6 +4,46 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="Apr 15, 2026">
## v1.14.2a5
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
## What's Changed
### Documentation
- Update changelog and version for v1.14.2a4
## Contributors
@greysonlalonde
</Update>
<Update label="Apr 15, 2026">
## v1.14.2a4
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
## What's Changed
### Features
- Add resume hints to devtools release on failure
### Bug Fixes
- Fix strict mode forwarding to Bedrock Converse API
- Fix pytest version to 9.0.3 for security vulnerability GHSA-6w46-j5rx-g56g
- Bump OpenAI lower bound to >=2.0.0
### Documentation
- Update changelog and version for v1.14.2a3
## Contributors
@greysonlalonde
</Update>
<Update label="Apr 13, 2026">
## v1.14.2a3

View File

@@ -4,6 +4,46 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
<Update label="2026년 4월 15일">
## v1.14.2a5
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
## 변경 사항
### 문서
- v1.14.2a4의 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde
</Update>
<Update label="2026년 4월 15일">
## v1.14.2a4
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
## 변경 사항
### 기능
- 실패 시 devtools 릴리스에 이력서 힌트 추가
### 버그 수정
- Bedrock Converse API로의 엄격 모드 포워딩 수정
- 보안 취약점 GHSA-6w46-j5rx-g56g에 대해 pytest 버전을 9.0.3으로 수정
- OpenAI 하한을 >=2.0.0으로 상향 조정
### 문서
- v1.14.2a3에 대한 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde
</Update>
<Update label="2026년 4월 13일">
## v1.14.2a3

View File

@@ -4,6 +4,46 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="15 abr 2026">
## v1.14.2a5
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
## O que Mudou
### Documentação
- Atualizar changelog e versão para v1.14.2a4
## Contribuidores
@greysonlalonde
</Update>
<Update label="15 abr 2026">
## v1.14.2a4
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
## O que Mudou
### Recursos
- Adicionar dicas de retomar ao release do devtools em caso de falha
### Correções de Bugs
- Corrigir o encaminhamento do modo estrito para a API Bedrock Converse
- Corrigir a versão do pytest para 9.0.3 devido à vulnerabilidade de segurança GHSA-6w46-j5rx-g56g
- Aumentar o limite inferior do OpenAI para >=2.0.0
### Documentação
- Atualizar o changelog e a versão para v1.14.2a3
## Contribuidores
@greysonlalonde
</Update>
<Update label="13 abr 2026">
## v1.14.2a3

View File

@@ -1,22 +0,0 @@
[project]
name = "crewai-a2a"
dynamic = ["version"]
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Greyson LaLonde", email = "greyson@crewai.com" }
]
requires-python = ">=3.10, <3.14"
dependencies = [
"a2a-sdk~=0.3.10",
"httpx-auth~=0.23.1",
"httpx-sse~=0.4.0",
"aiocache[redis,memcached]~=0.12.3",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.version]
path = "src/crewai_a2a/__init__.py"

View File

@@ -1,13 +0,0 @@
"""Agent-to-Agent (A2A) protocol communication module for CrewAI."""
__version__ = "1.14.2a3"
from crewai_a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
__all__ = [
"A2AClientConfig",
"A2AConfig",
"A2AServerConfig",
"__version__",
]

View File

@@ -1,71 +0,0 @@
"""Deprecated: Authentication schemes for A2A protocol agents.
This module is deprecated. Import from crewai_a2a.auth instead:
- crewai_a2a.auth.ClientAuthScheme (replaces AuthScheme)
- crewai_a2a.auth.BearerTokenAuth
- crewai_a2a.auth.HTTPBasicAuth
- crewai_a2a.auth.HTTPDigestAuth
- crewai_a2a.auth.APIKeyAuth
- crewai_a2a.auth.OAuth2ClientCredentials
- crewai_a2a.auth.OAuth2AuthorizationCode
"""
from __future__ import annotations
from typing_extensions import deprecated
from crewai_a2a.auth.client_schemes import (
APIKeyAuth as _APIKeyAuth,
BearerTokenAuth as _BearerTokenAuth,
ClientAuthScheme as _ClientAuthScheme,
HTTPBasicAuth as _HTTPBasicAuth,
HTTPDigestAuth as _HTTPDigestAuth,
OAuth2AuthorizationCode as _OAuth2AuthorizationCode,
OAuth2ClientCredentials as _OAuth2ClientCredentials,
)
@deprecated("Use ClientAuthScheme from crewai_a2a.auth instead", category=FutureWarning)
class AuthScheme(_ClientAuthScheme):
"""Deprecated: Use ClientAuthScheme from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class BearerTokenAuth(_BearerTokenAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class HTTPBasicAuth(_HTTPBasicAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class HTTPDigestAuth(_HTTPDigestAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class APIKeyAuth(_APIKeyAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class OAuth2ClientCredentials(_OAuth2ClientCredentials):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class OAuth2AuthorizationCode(_OAuth2AuthorizationCode):
"""Deprecated: Import from crewai_a2a.auth instead."""
__all__ = [
"APIKeyAuth",
"AuthScheme",
"BearerTokenAuth",
"HTTPBasicAuth",
"HTTPDigestAuth",
"OAuth2AuthorizationCode",
"OAuth2ClientCredentials",
]

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.14.2a3"
__version__ = "1.14.2a5"

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.2a3",
"crewai==1.14.2a5",
"tiktoken~=0.8.0",
"beautifulsoup4~=4.13.4",
"python-docx~=1.2.0",

View File

@@ -305,4 +305,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.14.2a3"
__version__ = "1.14.2a5"

View File

@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.14.2a3",
"crewai-tools==1.14.2a5",
]
embeddings = [
"tiktoken~=0.8.0"
@@ -99,7 +99,10 @@ anthropic = [
"anthropic~=0.73.0",
]
a2a = [
"crewai-a2a",
"a2a-sdk~=0.3.10",
"httpx-auth~=0.23.1",
"httpx-sse~=0.4.0",
"aiocache[redis,memcached]~=0.12.3",
]
file-processing = [
"crewai-files",
@@ -137,7 +140,6 @@ torchvision = [
{ index = "pytorch", marker = "python_version < '3.13'" },
]
crewai-files = { workspace = true }
crewai-a2a = { workspace = true }
[build-system]

View File

@@ -46,7 +46,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.14.2a3"
__version__ = "1.14.2a5"
_telemetry_submitted = False

View File

@@ -1,57 +1,10 @@
"""Compatibility shim: ``crewai.a2a`` re-exports :mod:`crewai_a2a`.
"""Agent-to-Agent (A2A) protocol communication module for CrewAI."""
The package lives in the ``crewai-a2a`` distribution (install via the
``crewai[a2a]`` extra). This module aliases the old import path so existing
code using ``crewai.a2a.*`` keeps working.
"""
from __future__ import annotations
from collections.abc import Sequence
import importlib
from importlib.abc import Loader, MetaPathFinder
from importlib.machinery import ModuleSpec
import sys
from types import ModuleType
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
try:
import crewai_a2a as _crewai_a2a
except ImportError as exc:
raise ImportError(
"crewai.a2a requires the 'crewai-a2a' package. "
"Install it with: pip install 'crewai[a2a]'"
) from exc
class _A2AAliasFinder(MetaPathFinder, Loader):
_SRC = "crewai.a2a"
_DST = "crewai_a2a"
def find_spec(
self,
fullname: str,
path: Sequence[str] | None = None,
target: ModuleType | None = None,
) -> ModuleSpec | None:
if fullname == self._SRC or fullname.startswith(self._SRC + "."):
return ModuleSpec(fullname, self)
return None
def create_module(self, spec: ModuleSpec) -> ModuleType:
target = self._DST + spec.name[len(self._SRC) :]
module = importlib.import_module(target)
sys.modules[spec.name] = module
return module
def exec_module(self, module: ModuleType) -> None:
return None
if not any(isinstance(f, _A2AAliasFinder) for f in sys.meta_path):
sys.meta_path.insert(0, _A2AAliasFinder())
for _attr in getattr(_crewai_a2a, "__all__", []):
globals()[_attr] = getattr(_crewai_a2a, _attr)
__all__ = list(getattr(_crewai_a2a, "__all__", []))
__all__ = [
"A2AClientConfig",
"A2AConfig",
"A2AServerConfig",
]

View File

@@ -1,6 +1,6 @@
"""A2A authentication schemas."""
from crewai_a2a.auth.client_schemes import (
from crewai.a2a.auth.client_schemes import (
APIKeyAuth,
AuthScheme,
BearerTokenAuth,
@@ -11,7 +11,7 @@ from crewai_a2a.auth.client_schemes import (
OAuth2ClientCredentials,
TLSConfig,
)
from crewai_a2a.auth.server_schemes import (
from crewai.a2a.auth.server_schemes import (
AuthenticatedUser,
EnterpriseTokenAuth,
OIDCAuth,

View File

@@ -0,0 +1,71 @@
"""Deprecated: Authentication schemes for A2A protocol agents.
This module is deprecated. Import from crewai.a2a.auth instead:
- crewai.a2a.auth.ClientAuthScheme (replaces AuthScheme)
- crewai.a2a.auth.BearerTokenAuth
- crewai.a2a.auth.HTTPBasicAuth
- crewai.a2a.auth.HTTPDigestAuth
- crewai.a2a.auth.APIKeyAuth
- crewai.a2a.auth.OAuth2ClientCredentials
- crewai.a2a.auth.OAuth2AuthorizationCode
"""
from __future__ import annotations
from typing_extensions import deprecated
from crewai.a2a.auth.client_schemes import (
APIKeyAuth as _APIKeyAuth,
BearerTokenAuth as _BearerTokenAuth,
ClientAuthScheme as _ClientAuthScheme,
HTTPBasicAuth as _HTTPBasicAuth,
HTTPDigestAuth as _HTTPDigestAuth,
OAuth2AuthorizationCode as _OAuth2AuthorizationCode,
OAuth2ClientCredentials as _OAuth2ClientCredentials,
)
@deprecated("Use ClientAuthScheme from crewai.a2a.auth instead", category=FutureWarning)
class AuthScheme(_ClientAuthScheme):
"""Deprecated: Use ClientAuthScheme from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class BearerTokenAuth(_BearerTokenAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class HTTPBasicAuth(_HTTPBasicAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class HTTPDigestAuth(_HTTPDigestAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class APIKeyAuth(_APIKeyAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class OAuth2ClientCredentials(_OAuth2ClientCredentials):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class OAuth2AuthorizationCode(_OAuth2AuthorizationCode):
"""Deprecated: Import from crewai.a2a.auth instead."""
__all__ = [
"APIKeyAuth",
"AuthScheme",
"BearerTokenAuth",
"HTTPBasicAuth",
"HTTPDigestAuth",
"OAuth2AuthorizationCode",
"OAuth2ClientCredentials",
]

View File

@@ -20,7 +20,7 @@ from a2a.types import (
)
from httpx import AsyncClient, Response
from crewai_a2a.auth.client_schemes import (
from crewai.a2a.auth.client_schemes import (
APIKeyAuth,
BearerTokenAuth,
ClientAuthScheme,

View File

@@ -20,10 +20,10 @@ from pydantic import (
)
from typing_extensions import Self, deprecated
from crewai_a2a.auth.client_schemes import ClientAuthScheme
from crewai_a2a.auth.server_schemes import ServerAuthScheme
from crewai_a2a.extensions.base import ValidatedA2AExtension
from crewai_a2a.types import ProtocolVersion, TransportType, Url
from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai.a2a.auth.server_schemes import ServerAuthScheme
from crewai.a2a.extensions.base import ValidatedA2AExtension
from crewai.a2a.types import ProtocolVersion, TransportType, Url
try:
@@ -36,8 +36,8 @@ try:
SecurityScheme,
)
from crewai_a2a.extensions.server import ServerExtension
from crewai_a2a.updates import UpdateConfig
from crewai.a2a.extensions.server import ServerExtension
from crewai.a2a.updates import UpdateConfig
except ImportError:
UpdateConfig: Any = Any # type: ignore[no-redef]
AgentCapabilities: Any = Any # type: ignore[no-redef]
@@ -50,7 +50,7 @@ except ImportError:
def _get_default_update_config() -> UpdateConfig:
from crewai_a2a.updates import StreamingConfig
from crewai.a2a.updates import StreamingConfig
return StreamingConfig()
@@ -360,8 +360,8 @@ class ClientTransportConfig(BaseModel):
@deprecated(
"""
`crewai_a2a.config.A2AConfig` is deprecated and will be removed in v2.0.0,
use `crewai_a2a.config.A2AClientConfig` or `crewai_a2a.config.A2AServerConfig` instead.
`crewai.a2a.config.A2AConfig` is deprecated and will be removed in v2.0.0,
use `crewai.a2a.config.A2AClientConfig` or `crewai.a2a.config.A2AServerConfig` instead.
""",
category=FutureWarning,
)

View File

@@ -13,13 +13,13 @@ via the X-A2A-Extensions header.
See: https://a2a-protocol.org/latest/topics/extensions/
"""
from crewai_a2a.extensions.base import (
from crewai.a2a.extensions.base import (
A2AExtension,
ConversationState,
ExtensionRegistry,
ValidatedA2AExtension,
)
from crewai_a2a.extensions.server import (
from crewai.a2a.extensions.server import (
ExtensionContext,
ServerExtension,
ServerExtensionRegistry,

View File

@@ -1,6 +1,6 @@
"""A2UI (Agent to UI) declarative UI protocol support for CrewAI."""
from crewai_a2a.extensions.a2ui.catalog import (
from crewai.a2a.extensions.a2ui.catalog import (
AudioPlayer,
Button,
Card,
@@ -20,8 +20,8 @@ from crewai_a2a.extensions.a2ui.catalog import (
TextField,
Video,
)
from crewai_a2a.extensions.a2ui.client_extension import A2UIClientExtension
from crewai_a2a.extensions.a2ui.models import (
from crewai.a2a.extensions.a2ui.client_extension import A2UIClientExtension
from crewai.a2a.extensions.a2ui.models import (
A2UIEvent,
A2UIMessage,
A2UIResponse,
@@ -31,13 +31,13 @@ from crewai_a2a.extensions.a2ui.models import (
SurfaceUpdate,
UserAction,
)
from crewai_a2a.extensions.a2ui.server_extension import (
from crewai.a2a.extensions.a2ui.server_extension import (
A2UI_STANDARD_CATALOG_ID,
A2UI_V09_BASIC_CATALOG_ID,
A2UI_V09_EXTENSION_URI,
A2UIServerExtension,
)
from crewai_a2a.extensions.a2ui.v0_9 import (
from crewai.a2a.extensions.a2ui.v0_9 import (
A2UIEventV09,
A2UIMessageV09,
ActionEvent,
@@ -68,7 +68,7 @@ from crewai_a2a.extensions.a2ui.v0_9 import (
UpdateDataModel,
VideoV09,
)
from crewai_a2a.extensions.a2ui.validator import (
from crewai.a2a.extensions.a2ui.validator import (
validate_a2ui_event,
validate_a2ui_event_v09,
validate_a2ui_message,

View File

@@ -10,18 +10,18 @@ from pydantic import Field
from pydantic.dataclasses import dataclass
from typing_extensions import TypeIs, TypedDict
from crewai_a2a.extensions.a2ui.models import extract_a2ui_json_objects
from crewai_a2a.extensions.a2ui.prompt import (
from crewai.a2a.extensions.a2ui.models import extract_a2ui_json_objects
from crewai.a2a.extensions.a2ui.prompt import (
build_a2ui_system_prompt,
build_a2ui_v09_system_prompt,
)
from crewai_a2a.extensions.a2ui.server_extension import (
from crewai.a2a.extensions.a2ui.server_extension import (
A2UI_MIME_TYPE,
A2UI_STANDARD_CATALOG_ID,
A2UI_V09_BASIC_CATALOG_ID,
)
from crewai_a2a.extensions.a2ui.v0_9 import extract_a2ui_v09_json_objects
from crewai_a2a.extensions.a2ui.validator import (
from crewai.a2a.extensions.a2ui.v0_9 import extract_a2ui_v09_json_objects
from crewai.a2a.extensions.a2ui.validator import (
A2UIValidationError,
validate_a2ui_message,
validate_a2ui_message_v09,
@@ -30,6 +30,7 @@ from crewai_a2a.extensions.a2ui.validator import (
if TYPE_CHECKING:
from a2a.types import Message
from crewai.agent.core import Agent

View File

@@ -4,13 +4,13 @@ from __future__ import annotations
import json
from crewai_a2a.extensions.a2ui.catalog import STANDARD_CATALOG_COMPONENTS
from crewai_a2a.extensions.a2ui.schema import load_schema
from crewai_a2a.extensions.a2ui.server_extension import (
from crewai.a2a.extensions.a2ui.catalog import STANDARD_CATALOG_COMPONENTS
from crewai.a2a.extensions.a2ui.schema import load_schema
from crewai.a2a.extensions.a2ui.server_extension import (
A2UI_EXTENSION_URI,
A2UI_V09_BASIC_CATALOG_ID,
)
from crewai_a2a.extensions.a2ui.v0_9 import (
from crewai.a2a.extensions.a2ui.v0_9 import (
BASIC_CATALOG_COMPONENTS as V09_CATALOG_COMPONENTS,
BASIC_CATALOG_FUNCTIONS,
)

View File

@@ -5,16 +5,16 @@ from __future__ import annotations
import logging
from typing import Any
from crewai_a2a.extensions.a2ui.models import A2UIResponse, extract_a2ui_json_objects
from crewai_a2a.extensions.a2ui.v0_9 import (
from crewai.a2a.extensions.a2ui.models import A2UIResponse, extract_a2ui_json_objects
from crewai.a2a.extensions.a2ui.v0_9 import (
extract_a2ui_v09_json_objects,
)
from crewai_a2a.extensions.a2ui.validator import (
from crewai.a2a.extensions.a2ui.validator import (
A2UIValidationError,
validate_a2ui_message,
validate_a2ui_message_v09,
)
from crewai_a2a.extensions.server import ExtensionContext, ServerExtension
from crewai.a2a.extensions.server import ExtensionContext, ServerExtension
logger = logging.getLogger(__name__)

View File

@@ -6,7 +6,7 @@ from typing import Any
from pydantic import BaseModel, ValidationError
from crewai_a2a.extensions.a2ui.catalog import (
from crewai.a2a.extensions.a2ui.catalog import (
AudioPlayer,
Button,
Card,
@@ -26,8 +26,8 @@ from crewai_a2a.extensions.a2ui.catalog import (
TextField,
Video,
)
from crewai_a2a.extensions.a2ui.models import A2UIEvent, A2UIMessage
from crewai_a2a.extensions.a2ui.v0_9 import (
from crewai.a2a.extensions.a2ui.models import A2UIEvent, A2UIMessage
from crewai.a2a.extensions.a2ui.v0_9 import (
A2UIEventV09,
A2UIMessageV09,
AudioPlayerV09,

View File

@@ -19,6 +19,7 @@ from pydantic import BeforeValidator
if TYPE_CHECKING:
from a2a.types import Message
from crewai.agent.core import Agent

View File

@@ -18,8 +18,8 @@ from a2a.extensions.common import (
)
from a2a.types import AgentCard, AgentExtension
from crewai_a2a.config import A2AClientConfig, A2AConfig
from crewai_a2a.extensions.base import ExtensionRegistry
from crewai.a2a.config import A2AClientConfig, A2AConfig
from crewai.a2a.extensions.base import ExtensionRegistry
def get_extensions_from_config(

View File

@@ -18,12 +18,13 @@ from a2a.types import (
TaskStatusUpdateEvent,
TextPart,
)
from typing_extensions import NotRequired, TypedDict
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
A2AResponseReceivedEvent,
)
from typing_extensions import NotRequired, TypedDict
if TYPE_CHECKING:

View File

@@ -15,7 +15,7 @@ from typing_extensions import NotRequired, TypedDict
try:
from crewai_a2a.updates import (
from crewai.a2a.updates import (
PollingConfig,
PollingHandler,
PushNotificationConfig,

View File

@@ -1,6 +1,6 @@
"""A2A update mechanism configuration types."""
from crewai_a2a.updates.base import (
from crewai.a2a.updates.base import (
BaseHandlerKwargs,
PollingHandlerKwargs,
PushNotificationHandlerKwargs,
@@ -8,12 +8,12 @@ from crewai_a2a.updates.base import (
StreamingHandlerKwargs,
UpdateHandler,
)
from crewai_a2a.updates.polling.config import PollingConfig
from crewai_a2a.updates.polling.handler import PollingHandler
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
from crewai_a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai_a2a.updates.streaming.config import StreamingConfig
from crewai_a2a.updates.streaming.handler import StreamingHandler
from crewai.a2a.updates.polling.config import PollingConfig
from crewai.a2a.updates.polling.handler import PollingHandler
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai.a2a.updates.streaming.config import StreamingConfig
from crewai.a2a.updates.streaming.handler import StreamingHandler
UpdateConfig = PollingConfig | StreamingConfig | PushNotificationConfig

View File

@@ -29,8 +29,8 @@ if TYPE_CHECKING:
from a2a.client import Client
from a2a.types import AgentCard, Message, Task
from crewai_a2a.task_helpers import TaskStateResult
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.task_helpers import TaskStateResult
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
class BaseHandlerKwargs(TypedDict, total=False):

View File

@@ -18,6 +18,17 @@ from a2a.types import (
TaskState,
TextPart,
)
from typing_extensions import Unpack
from crewai.a2a.errors import A2APollingTimeoutError
from crewai.a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai.a2a.updates.base import PollingHandlerKwargs
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
@@ -25,17 +36,6 @@ from crewai.events.types.a2a_events import (
A2APollingStatusEvent,
A2AResponseReceivedEvent,
)
from typing_extensions import Unpack
from crewai_a2a.errors import A2APollingTimeoutError
from crewai_a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai_a2a.updates.base import PollingHandlerKwargs
if TYPE_CHECKING:

View File

@@ -7,8 +7,8 @@ from typing import Annotated
from a2a.types import PushNotificationAuthenticationInfo
from pydantic import AnyHttpUrl, BaseModel, BeforeValidator, Field
from crewai_a2a.updates.base import PushNotificationResultStore
from crewai_a2a.updates.push_notifications.signature import WebhookSignatureConfig
from crewai.a2a.updates.base import PushNotificationResultStore
from crewai.a2a.updates.push_notifications.signature import WebhookSignatureConfig
def _coerce_signature(

View File

@@ -16,6 +16,19 @@ from a2a.types import (
TaskState,
TextPart,
)
from typing_extensions import Unpack
from crewai.a2a.task_helpers import (
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai.a2a.updates.base import (
CommonParams,
PushNotificationHandlerKwargs,
PushNotificationResultStore,
extract_common_params,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
@@ -23,19 +36,6 @@ from crewai.events.types.a2a_events import (
A2APushNotificationTimeoutEvent,
A2AResponseReceivedEvent,
)
from typing_extensions import Unpack
from crewai_a2a.task_helpers import (
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai_a2a.updates.base import (
CommonParams,
PushNotificationHandlerKwargs,
PushNotificationResultStore,
extract_common_params,
)
if TYPE_CHECKING:

View File

@@ -22,6 +22,18 @@ from a2a.types import (
TaskStatusUpdateEvent,
TextPart,
)
from typing_extensions import Unpack
from crewai.a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
)
from crewai.a2a.updates.base import StreamingHandlerKwargs, extract_common_params
from crewai.a2a.updates.streaming.params import (
process_status_update,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AArtifactReceivedEvent,
@@ -30,18 +42,6 @@ from crewai.events.types.a2a_events import (
A2AStreamingChunkEvent,
A2AStreamingStartedEvent,
)
from typing_extensions import Unpack
from crewai_a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
)
from crewai_a2a.updates.base import StreamingHandlerKwargs, extract_common_params
from crewai_a2a.updates.streaming.params import (
process_status_update,
)
logger = logging.getLogger(__name__)

View File

@@ -16,6 +16,15 @@ from a2a.client.errors import A2AClientHTTPError
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
from aiocache import cached # type: ignore[import-untyped]
from aiocache.serializers import PickleSerializer # type: ignore[import-untyped]
import httpx
from crewai.a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai.a2a.auth.utils import (
_auth_store,
configure_auth_client,
retry_on_401,
)
from crewai.a2a.config import A2AServerConfig
from crewai.crew import Crew
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
@@ -23,23 +32,13 @@ from crewai.events.types.a2a_events import (
A2AAuthenticationFailedEvent,
A2AConnectionErrorEvent,
)
import httpx
from crewai_a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai_a2a.auth.utils import (
_auth_store,
configure_auth_client,
retry_on_401,
)
from crewai_a2a.config import A2AServerConfig
if TYPE_CHECKING:
from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai.agent import Agent
from crewai.task import Task
from crewai_a2a.auth.client_schemes import ClientAuthScheme
def _get_tls_verify(auth: ClientAuthScheme | None) -> ssl.SSLContext | bool | str:
"""Get TLS verify parameter from auth scheme.
@@ -496,7 +495,7 @@ def _agent_to_agent_card(agent: Agent, url: str) -> AgentCard:
Returns:
AgentCard describing the agent's capabilities.
"""
from crewai_a2a.utils.agent_card_signing import sign_agent_card
from crewai.a2a.utils.agent_card_signing import sign_agent_card
server_config = _get_server_config(agent) or A2AServerConfig()
@@ -530,7 +529,7 @@ def _agent_to_agent_card(agent: Agent, url: str) -> AgentCard:
capabilities = server_config.capabilities
if server_config.server_extensions:
from crewai_a2a.extensions.server import ServerExtensionRegistry
from crewai.a2a.extensions.server import ServerExtensionRegistry
registry = ServerExtensionRegistry(server_config.server_extensions)
ext_list = registry.get_agent_extensions()

View File

@@ -5,7 +5,7 @@ JSON Web Signatures (JWS) as per RFC 7515. Signed agent cards allow clients
to verify the authenticity and integrity of agent card information.
Example:
>>> from crewai_a2a.utils.agent_card_signing import sign_agent_card
>>> from crewai.a2a.utils.agent_card_signing import sign_agent_card
>>> signature = sign_agent_card(agent_card, private_key_pem, key_id="key-1")
>>> card_with_sig = card.model_copy(update={"signatures": [signature]})
"""

View File

@@ -10,6 +10,7 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, Final, Literal, cast
from a2a.types import Part
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import A2AContentTypeNegotiatedEvent

View File

@@ -23,55 +23,55 @@ from a2a.types import (
Role,
TextPart,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConversationStartedEvent,
A2ADelegationCompletedEvent,
A2ADelegationStartedEvent,
A2AMessageSentEvent,
)
import httpx
from pydantic import BaseModel
from crewai_a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai_a2a.auth.utils import (
from crewai.a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai.a2a.auth.utils import (
_auth_store,
configure_auth_client,
validate_auth_against_agent_card,
)
from crewai_a2a.config import ClientTransportConfig, GRPCClientConfig
from crewai_a2a.extensions.registry import (
from crewai.a2a.config import ClientTransportConfig, GRPCClientConfig
from crewai.a2a.extensions.registry import (
ExtensionsMiddleware,
validate_required_extensions,
)
from crewai_a2a.task_helpers import TaskStateResult
from crewai_a2a.types import (
from crewai.a2a.task_helpers import TaskStateResult
from crewai.a2a.types import (
HANDLER_REGISTRY,
HandlerType,
PartsDict,
PartsMetadataDict,
TransportType,
)
from crewai_a2a.updates import (
from crewai.a2a.updates import (
PollingConfig,
PushNotificationConfig,
StreamingHandler,
UpdateConfig,
)
from crewai_a2a.utils.agent_card import (
from crewai.a2a.utils.agent_card import (
_afetch_agent_card_cached,
_get_tls_verify,
_prepare_auth_headers,
)
from crewai_a2a.utils.content_type import (
from crewai.a2a.utils.content_type import (
DEFAULT_CLIENT_OUTPUT_MODES,
negotiate_content_types,
)
from crewai_a2a.utils.transport import (
from crewai.a2a.utils.transport import (
NegotiatedTransport,
TransportNegotiationError,
negotiate_transport,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConversationStartedEvent,
A2ADelegationCompletedEvent,
A2ADelegationStartedEvent,
A2AMessageSentEvent,
)
logger = logging.getLogger(__name__)
@@ -80,7 +80,7 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from a2a.types import Message
from crewai_a2a.auth.client_schemes import ClientAuthScheme
from crewai.a2a.auth.client_schemes import ClientAuthScheme
_DEFAULT_TRANSPORT: Final[TransportType] = "JSONRPC"
@@ -771,7 +771,7 @@ def _create_grpc_channel_factory(
auth_metadata: list[tuple[str, str]] = []
if auth is not None:
from crewai_a2a.auth.client_schemes import (
from crewai.a2a.auth.client_schemes import (
APIKeyAuth,
BearerTokenAuth,
HTTPBasicAuth,

View File

@@ -103,7 +103,7 @@ class LogContext:
_log_context.reset(self._token)
def configure_json_logging(logger_name: str = "crewai_a2a") -> None:
def configure_json_logging(logger_name: str = "crewai.a2a") -> None:
"""Configure JSON logging for the A2A module.
Args:

View File

@@ -4,10 +4,10 @@ from __future__ import annotations
from typing import TypeAlias
from crewai.types.utils import create_literals_from_strings
from pydantic import BaseModel, Field, create_model
from crewai_a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
from crewai.types.utils import create_literals_from_strings
A2AConfigTypes: TypeAlias = A2AConfig | A2AServerConfig | A2AClientConfig

View File

@@ -37,6 +37,11 @@ from a2a.utils import (
)
from a2a.utils.errors import ServerError
from aiocache import SimpleMemoryCache, caches # type: ignore[import-untyped]
from pydantic import BaseModel
from typing_extensions import TypedDict
from crewai.a2a.utils.agent_card import _get_server_config
from crewai.a2a.utils.content_type import validate_message_parts
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AServerTaskCanceledEvent,
@@ -46,18 +51,12 @@ from crewai.events.types.a2a_events import (
)
from crewai.task import Task
from crewai.utilities.pydantic_schema_utils import create_model_from_schema
from pydantic import BaseModel
from typing_extensions import TypedDict
from crewai_a2a.utils.agent_card import _get_server_config
from crewai_a2a.utils.content_type import validate_message_parts
if TYPE_CHECKING:
from crewai.a2a.extensions.server import ExtensionContext, ServerExtensionRegistry
from crewai.agent import Agent
from crewai_a2a.extensions.server import ExtensionContext, ServerExtensionRegistry
logger = logging.getLogger(__name__)

View File

@@ -11,6 +11,7 @@ from dataclasses import dataclass
from typing import Final, Literal
from a2a.types import AgentCard, AgentInterface
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import A2ATransportNegotiatedEvent

View File

@@ -15,6 +15,33 @@ from types import MethodType
from typing import TYPE_CHECKING, Any, NamedTuple
from a2a.types import Role, TaskState
from pydantic import BaseModel, ValidationError
from crewai.a2a.config import A2AClientConfig, A2AConfig
from crewai.a2a.extensions.base import (
A2AExtension,
ConversationState,
ExtensionRegistry,
)
from crewai.a2a.task_helpers import TaskStateResult
from crewai.a2a.templates import (
AVAILABLE_AGENTS_TEMPLATE,
CONVERSATION_TURN_INFO_TEMPLATE,
PREVIOUS_A2A_CONVERSATION_TEMPLATE,
REMOTE_AGENT_RESPONSE_NOTICE,
UNAVAILABLE_AGENTS_NOTICE_TEMPLATE,
)
from crewai.a2a.types import AgentResponseProtocol
from crewai.a2a.utils.agent_card import (
afetch_agent_card,
fetch_agent_card,
inject_a2a_server_methods,
)
from crewai.a2a.utils.delegation import (
aexecute_a2a_delegation,
execute_a2a_delegation,
)
from crewai.a2a.utils.response_model import get_a2a_agents_and_response_model
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConversationCompletedEvent,
@@ -22,37 +49,11 @@ from crewai.events.types.a2a_events import (
)
from crewai.lite_agent_output import LiteAgentOutput
from crewai.task import Task
from pydantic import BaseModel, ValidationError
from crewai_a2a.config import A2AClientConfig, A2AConfig
from crewai_a2a.extensions.base import (
A2AExtension,
ConversationState,
ExtensionRegistry,
)
from crewai_a2a.task_helpers import TaskStateResult
from crewai_a2a.templates import (
AVAILABLE_AGENTS_TEMPLATE,
CONVERSATION_TURN_INFO_TEMPLATE,
PREVIOUS_A2A_CONVERSATION_TEMPLATE,
REMOTE_AGENT_RESPONSE_NOTICE,
UNAVAILABLE_AGENTS_NOTICE_TEMPLATE,
)
from crewai_a2a.types import AgentResponseProtocol
from crewai_a2a.utils.agent_card import (
afetch_agent_card,
fetch_agent_card,
inject_a2a_server_methods,
)
from crewai_a2a.utils.delegation import (
aexecute_a2a_delegation,
execute_a2a_delegation,
)
from crewai_a2a.utils.response_model import get_a2a_agents_and_response_model
if TYPE_CHECKING:
from a2a.types import AgentCard, Message
from crewai.agent.core import Agent
from crewai.tools.base_tool import BaseTool

View File

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

View File

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

View File

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

View File

@@ -17,10 +17,7 @@ from crewai.utilities.agent_utils import is_context_length_exceeded
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
)
from crewai.utilities.pydantic_schema_utils import (
generate_model_description,
sanitize_tool_params_for_bedrock_strict,
)
from crewai.utilities.pydantic_schema_utils import generate_model_description
from crewai.utilities.types import LLMMessage
@@ -173,7 +170,6 @@ class ToolSpec(TypedDict, total=False):
name: Required[str]
description: Required[str]
inputSchema: ToolInputSchema
strict: bool
class ConverseToolTypeDef(TypedDict):
@@ -1988,21 +1984,10 @@ class BedrockCompletion(BaseLLM):
"description": description,
}
func_info = tool.get("function", {})
strict_enabled = bool(func_info.get("strict"))
if parameters and isinstance(parameters, dict):
schema_params = (
sanitize_tool_params_for_bedrock_strict(parameters)
if strict_enabled
else parameters
)
input_schema: ToolInputSchema = {"json": schema_params}
input_schema: ToolInputSchema = {"json": parameters}
tool_spec["inputSchema"] = input_schema
if strict_enabled:
tool_spec["strict"] = True
converse_tool: ConverseToolTypeDef = {"toolSpec": tool_spec}
converse_tools.append(converse_tool)

View File

@@ -19,6 +19,7 @@ from collections.abc import Callable
from copy import deepcopy
import datetime
import logging
import threading
from typing import TYPE_CHECKING, Annotated, Any, Final, Literal, TypedDict, Union, cast
import uuid
@@ -91,6 +92,9 @@ def resolve_refs(schema: dict[str, Any]) -> dict[str, Any]:
This is needed because Pydantic generates $ref-based schemas that
some consumers (e.g. LLMs, tool frameworks) don't handle well.
Circular references are detected and replaced with a plain
``{"type": "object"}`` stub to prevent infinite recursion.
Args:
schema: JSON Schema dict that may contain "$refs" and "$defs".
@@ -100,18 +104,23 @@ def resolve_refs(schema: dict[str, Any]) -> dict[str, Any]:
defs = schema.get("$defs", {})
schema_copy = deepcopy(schema)
def _resolve(node: Any) -> Any:
def _resolve(node: Any, resolving: frozenset[str] = frozenset()) -> Any:
if isinstance(node, dict):
ref = node.get("$ref")
if isinstance(ref, str) and ref.startswith("#/$defs/"):
def_name = ref.replace("#/$defs/", "")
if def_name in resolving:
return {"type": "object"}
if def_name in defs:
return _resolve(deepcopy(defs[def_name]))
return _resolve(
deepcopy(defs[def_name]),
resolving | {def_name},
)
raise KeyError(f"Definition '{def_name}' not found in $defs.")
return {k: _resolve(v) for k, v in node.items()}
return {k: _resolve(v, resolving) for k, v in node.items()}
if isinstance(node, list):
return [_resolve(i) for i in node]
return [_resolve(i, resolving) for i in node]
return node
@@ -658,6 +667,104 @@ def build_rich_field_description(prop_schema: dict[str, Any]) -> str:
return ". ".join(parts) if parts else ""
# Thread-local storage tracking which ``$ref`` paths are currently being
# resolved. Used by ``_json_schema_to_pydantic_type`` to detect circular
# ``$ref`` chains and break the recursion with a ``dict`` fallback.
# Each thread gets its own independent set so concurrent schema conversions
# (e.g. via ThreadPoolExecutor in MCP tool resolution) don't interfere.
_resolving_refs_local = threading.local()
def _get_resolving_refs() -> set[str]:
"""Return the per-thread resolving-refs set, creating it on first access."""
refs: set[str] | None = getattr(_resolving_refs_local, "refs", None)
if refs is None:
refs = set()
object.__setattr__(_resolving_refs_local, "refs", refs)
return refs
def _safe_replace_refs(json_schema: dict[str, Any]) -> dict[str, Any]:
"""Resolve ``$ref`` pointers in *json_schema*, tolerating circular refs.
``jsonref.replace_refs(proxies=False)`` performs eager, recursive
inlining. When a definition refers back to itself (directly or
transitively) this blows the Python call stack and also produces
Python dicts with circular object references that break all
downstream recursive visitors.
Strategy: always break circular ``$ref`` chains *before* handing the
schema to ``jsonref`` so the library never encounters a cycle.
"""
schema_copy = deepcopy(json_schema)
defs = schema_copy.get("$defs", {})
if defs and _has_circular_refs(schema_copy, defs):
_break_circular_refs(schema_copy, defs, set())
try:
return dict(jsonref.replace_refs(schema_copy, proxies=False))
except RecursionError:
# Last resort - return the manually patched copy as-is.
return schema_copy
def _has_circular_refs(
node: Any,
defs: dict[str, Any],
visiting: set[str] | None = None,
) -> bool:
"""Return ``True`` if *node* contains any circular ``$ref`` chain."""
if visiting is None:
visiting = set()
if isinstance(node, dict):
ref = node.get("$ref")
if isinstance(ref, str) and ref.startswith("#/$defs/"):
def_name = ref.removeprefix("#/$defs/")
if def_name in visiting:
return True
if def_name in defs:
visiting.add(def_name)
if _has_circular_refs(defs[def_name], defs, visiting):
return True
visiting.discard(def_name)
for value in node.values():
if _has_circular_refs(value, defs, visiting):
return True
elif isinstance(node, list):
for item in node:
if _has_circular_refs(item, defs, visiting):
return True
return False
def _break_circular_refs(
node: Any,
defs: dict[str, Any],
visiting: set[str],
) -> None:
"""Walk *node* in-place and replace circular ``$ref`` pointers with stubs."""
if isinstance(node, dict):
ref = node.get("$ref")
if isinstance(ref, str) and ref.startswith("#/$defs/"):
def_name = ref.removeprefix("#/$defs/")
if def_name in visiting:
# Circular - replace the *whole* node content with a stub.
node.clear()
node["type"] = "object"
return
if def_name in defs:
visiting.add(def_name)
_break_circular_refs(defs[def_name], defs, visiting)
visiting.discard(def_name)
for value in node.values():
_break_circular_refs(value, defs, visiting)
elif isinstance(node, list):
for item in node:
_break_circular_refs(item, defs, visiting)
def create_model_from_schema( # type: ignore[no-any-unimported]
json_schema: dict[str, Any],
*,
@@ -677,6 +784,10 @@ def create_model_from_schema( # type: ignore[no-any-unimported]
as nested objects, referenced definitions ($ref), arrays with typed items,
union types (anyOf/oneOf), and string formats.
Circular ``$ref`` chains (common in complex MCP tool schemas) are detected
and broken automatically so that deeply-nested or self-referential schemas
never trigger a ``RecursionError``.
Args:
json_schema: A dictionary representing the JSON schema.
root_schema: The root schema containing $defs. If not provided, the
@@ -712,7 +823,7 @@ def create_model_from_schema( # type: ignore[no-any-unimported]
>>> person.name
'John'
"""
json_schema = dict(jsonref.replace_refs(json_schema, proxies=False))
json_schema = _safe_replace_refs(json_schema)
effective_root = root_schema or json_schema
@@ -920,13 +1031,22 @@ def _json_schema_to_pydantic_type(
"""
ref = json_schema.get("$ref")
if ref:
ref_schema = _resolve_ref(ref, root_schema)
return _json_schema_to_pydantic_type(
ref_schema,
root_schema,
name_=name_,
enrich_descriptions=enrich_descriptions,
)
# Detect circular $ref chains - if we are already resolving this
# ref higher up the call stack, break the cycle by returning dict.
resolving = _get_resolving_refs()
if ref in resolving:
return dict
resolving.add(ref)
try:
ref_schema = _resolve_ref(ref, root_schema)
return _json_schema_to_pydantic_type(
ref_schema,
root_schema,
name_=name_,
enrich_descriptions=enrich_descriptions,
)
finally:
resolving.discard(ref)
enum_values = json_schema.get("enum")
if enum_values:

View File

@@ -9,11 +9,12 @@ from __future__ import annotations
from typing import Any
import jsonschema
import pytest
from crewai.a2a.extensions.a2ui import catalog
from crewai.a2a.extensions.a2ui.models import A2UIEvent, A2UIMessage
from crewai.a2a.extensions.a2ui.schema import load_schema
import jsonschema
import pytest
SERVER_SCHEMA = load_schema("server_to_client")
@@ -205,10 +206,7 @@ VALID_COMPONENTS: dict[str, dict[str, Any]] = {
"Divider": {"axis": "horizontal"},
"Modal": {"entryPointChild": "trigger", "contentChild": "body"},
"Button": {"child": "label", "action": {"name": "go"}},
"CheckBox": {
"label": {"literalString": "Accept"},
"value": {"literalBoolean": False},
},
"CheckBox": {"label": {"literalString": "Accept"}, "value": {"literalBoolean": False}},
"TextField": {"label": {"literalString": "Name"}},
"DateTimeInput": {"value": {"path": "/date"}},
"MultipleChoice": {

View File

@@ -3,13 +3,15 @@ from __future__ import annotations
import os
import uuid
from a2a.client import ClientFactory
from a2a.types import AgentCard, Message, Part, Role, Task, TaskState, TextPart
from crewai.a2a.updates.polling.handler import PollingHandler
from crewai.a2a.updates.streaming.handler import StreamingHandler
import pytest
import pytest_asyncio
from a2a.client import ClientFactory
from a2a.types import AgentCard, Message, Part, Role, TaskState, TextPart
from crewai.a2a.updates.polling.handler import PollingHandler
from crewai.a2a.updates.streaming.handler import StreamingHandler
A2A_TEST_ENDPOINT = os.getenv("A2A_TEST_ENDPOINT", "http://localhost:9999")
@@ -160,7 +162,7 @@ class TestA2APushNotificationHandler:
)
@pytest.fixture
def mock_task(self) -> Task:
def mock_task(self) -> "Task":
"""Create a minimal valid task for testing."""
from a2a.types import Task, TaskStatus
@@ -180,12 +182,11 @@ class TestA2APushNotificationHandler:
from unittest.mock import AsyncMock, MagicMock
from a2a.types import Task, TaskStatus
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import (
PushNotificationHandler,
)
from pydantic import AnyHttpUrl
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
completed_task = Task(
id="task-123",
context_id="ctx-123",
@@ -245,12 +246,11 @@ class TestA2APushNotificationHandler:
from unittest.mock import AsyncMock, MagicMock
from a2a.types import Task, TaskStatus
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import (
PushNotificationHandler,
)
from pydantic import AnyHttpUrl
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
mock_store = MagicMock()
mock_store.wait_for_result = AsyncMock(return_value=None)
@@ -303,9 +303,7 @@ class TestA2APushNotificationHandler:
"""Test that push handler fails gracefully without config."""
from unittest.mock import MagicMock
from crewai.a2a.updates.push_notifications.handler import (
PushNotificationHandler,
)
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
mock_client = MagicMock()

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from a2a.types import AgentCard, AgentSkill
from crewai import Agent
from crewai.a2a.config import A2AClientConfig, A2AServerConfig
from crewai.a2a.utils.agent_card import inject_a2a_server_methods

View File

@@ -6,12 +6,13 @@ import asyncio
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import pytest_asyncio
from a2a.server.agent_execution import RequestContext
from a2a.server.events import EventQueue
from a2a.types import Message, Task as A2ATask, TaskState, TaskStatus
from crewai.a2a.utils.task import cancel, cancellable, execute
import pytest
import pytest_asyncio
@pytest.fixture
@@ -84,11 +85,8 @@ class TestCancellableDecorator:
assert call_count == 1
@pytest.mark.asyncio
async def test_executes_function_with_context(
self, mock_context: MagicMock
) -> None:
async def test_executes_function_with_context(self, mock_context: MagicMock) -> None:
"""Function executes normally with RequestContext when not cancelled."""
@cancellable
async def my_func(context: RequestContext) -> str:
await asyncio.sleep(0.01)
@@ -136,7 +134,6 @@ class TestCancellableDecorator:
@pytest.mark.asyncio
async def test_extracts_context_from_kwargs(self, mock_context: MagicMock) -> None:
"""Context can be passed as keyword argument."""
@cancellable
async def my_func(value: int, context: RequestContext | None = None) -> int:
return value + 1
@@ -357,7 +354,6 @@ class TestExecuteAndCancelIntegration:
mock_task: MagicMock,
) -> None:
"""Calling cancel stops a running execute."""
async def slow_task(**kwargs: Any) -> str:
await asyncio.sleep(2.0)
return "should not complete"
@@ -376,4 +372,4 @@ class TestExecuteAndCancelIntegration:
await cancel(mock_context, mock_event_queue)
with pytest.raises(asyncio.CancelledError):
await execute_task
await execute_task

View File

@@ -2,9 +2,9 @@
from unittest.mock import MagicMock, patch
from crewai.a2a.config import A2AConfig
import pytest
from crewai.a2a.config import A2AConfig
try:
from a2a.types import Message, Role
@@ -27,8 +27,9 @@ def _create_mock_agent_card(name: str = "Test", url: str = "http://test-endpoint
@pytest.mark.skipif(not A2A_SDK_INSTALLED, reason="Requires a2a-sdk to be installed")
def test_trust_remote_completion_status_true_returns_directly():
"""When trust_remote_completion_status=True and A2A returns completed, return result directly."""
from crewai import Agent, Task
from crewai.a2a.wrapper import _delegate_to_a2a
from crewai.a2a.types import AgentResponseProtocol
from crewai import Agent, Task
a2a_config = A2AConfig(
endpoint="http://test-endpoint.com",
@@ -82,8 +83,8 @@ def test_trust_remote_completion_status_true_returns_directly():
@pytest.mark.skipif(not A2A_SDK_INSTALLED, reason="Requires a2a-sdk to be installed")
def test_trust_remote_completion_status_false_continues_conversation():
"""When trust_remote_completion_status=False and A2A returns completed, ask server agent."""
from crewai import Agent, Task
from crewai.a2a.wrapper import _delegate_to_a2a
from crewai import Agent, Task
a2a_config = A2AConfig(
endpoint="http://test-endpoint.com",
@@ -151,4 +152,4 @@ def test_default_trust_remote_completion_status_is_false():
endpoint="http://test-endpoint.com",
)
assert a2a_config.trust_remote_completion_status is False
assert a2a_config.trust_remote_completion_status is False

View File

@@ -4,9 +4,10 @@ from __future__ import annotations
import os
import pytest
from crewai import Agent
from crewai.a2a.config import A2AClientConfig
import pytest
A2A_TEST_ENDPOINT = os.getenv(
@@ -49,7 +50,9 @@ class TestAgentA2AKickoff:
@pytest.mark.skip(reason="VCR cassette matching issue with agent card caching")
@pytest.mark.vcr()
def test_agent_kickoff_with_calculator_skill(self, researcher_agent: Agent) -> None:
def test_agent_kickoff_with_calculator_skill(
self, researcher_agent: Agent
) -> None:
"""Test that agent can delegate calculation to A2A server."""
result = researcher_agent.kickoff(
"Ask the remote A2A agent to calculate 25 times 17."
@@ -146,7 +149,9 @@ class TestAgentA2AKickoff:
@pytest.mark.skip(reason="VCR cassette matching issue with agent card caching")
@pytest.mark.vcr()
def test_agent_kickoff_with_list_messages(self, researcher_agent: Agent) -> None:
def test_agent_kickoff_with_list_messages(
self, researcher_agent: Agent
) -> None:
"""Test that agent.kickoff() works with list of messages."""
messages = [
{

View File

@@ -1,12 +1,14 @@
"""Test A2A wrapper is only applied when a2a is passed to Agent."""
from unittest.mock import patch
import pytest
from crewai import Agent
from crewai.a2a.config import A2AConfig
import pytest
try:
import a2a
import a2a # noqa: F401
A2A_SDK_INSTALLED = True
except ImportError:
@@ -104,9 +106,6 @@ def test_wrapper_is_applied_differently_per_instance():
a2a=a2a_config,
)
assert (
agent_without_a2a.execute_task.__func__
is not agent_with_a2a.execute_task.__func__
)
assert agent_without_a2a.execute_task.__func__ is not agent_with_a2a.execute_task.__func__
assert not hasattr(agent_without_a2a.execute_task, "__wrapped__")
assert hasattr(agent_with_a2a.execute_task, "__wrapped__")

View File

@@ -19,6 +19,9 @@ import pytest
from pydantic import BaseModel
from crewai.utilities.pydantic_schema_utils import (
_break_circular_refs,
_has_circular_refs,
_safe_replace_refs,
build_rich_field_description,
convert_oneof_to_anyof,
create_model_from_schema,
@@ -882,3 +885,333 @@ class TestEndToEndMCPSchema:
)
assert obj.filters.date_from == datetime.date(2025, 1, 1)
assert obj.filters.categories == ["news", "tech"]
# ---------------------------------------------------------------------------
# Circular $ref handling (issue #5474)
# ---------------------------------------------------------------------------
class TestCircularRefDetection:
"""Tests for _has_circular_refs helper."""
def test_detects_direct_self_reference(self) -> None:
schema: dict[str, Any] = {
"type": "object",
"properties": {"child": {"$ref": "#/$defs/Node"}},
"$defs": {
"Node": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {"$ref": "#/$defs/Node"},
},
},
},
},
}
assert _has_circular_refs(schema, schema["$defs"]) is True
def test_detects_indirect_circular_reference(self) -> None:
schema: dict[str, Any] = {
"type": "object",
"properties": {"a": {"$ref": "#/$defs/A"}},
"$defs": {
"A": {
"type": "object",
"properties": {"b": {"$ref": "#/$defs/B"}},
},
"B": {
"type": "object",
"properties": {"a": {"$ref": "#/$defs/A"}},
},
},
}
assert _has_circular_refs(schema, schema["$defs"]) is True
def test_no_circular_ref(self) -> None:
schema: dict[str, Any] = {
"type": "object",
"properties": {"item": {"$ref": "#/$defs/Item"}},
"$defs": {
"Item": {
"type": "object",
"properties": {"name": {"type": "string"}},
},
},
}
assert _has_circular_refs(schema, schema["$defs"]) is False
class TestBreakCircularRefs:
"""Tests for _break_circular_refs helper."""
def test_breaks_direct_self_reference(self) -> None:
schema: dict[str, Any] = {
"type": "object",
"properties": {"child": {"$ref": "#/$defs/Node"}},
"$defs": {
"Node": {
"type": "object",
"properties": {
"name": {"type": "string"},
"children": {
"type": "array",
"items": {"$ref": "#/$defs/Node"},
},
},
},
},
}
_break_circular_refs(schema, schema["$defs"], set())
# The self-referential $ref inside Node's items should be replaced
items = schema["$defs"]["Node"]["properties"]["children"]["items"]
assert items == {"type": "object"}
assert "$ref" not in items
def test_preserves_non_circular_refs(self) -> None:
schema: dict[str, Any] = {
"type": "object",
"properties": {"item": {"$ref": "#/$defs/Item"}},
"$defs": {
"Item": {
"type": "object",
"properties": {"name": {"type": "string"}},
},
},
}
original = deepcopy(schema)
_break_circular_refs(schema, schema["$defs"], set())
# Non-circular schema should be unchanged
assert schema == original
class TestSafeReplaceRefs:
"""Tests for _safe_replace_refs."""
def test_resolves_non_circular_schema(self) -> None:
schema: dict[str, Any] = {
"type": "object",
"properties": {"item": {"$ref": "#/$defs/Item"}},
"$defs": {
"Item": {
"type": "object",
"properties": {"id": {"type": "integer"}},
},
},
}
result = _safe_replace_refs(schema)
assert "$ref" not in result.get("properties", {}).get("item", {})
assert result["properties"]["item"]["type"] == "object"
def test_handles_circular_schema_without_recursion_error(self) -> None:
schema: dict[str, Any] = {
"type": "object",
"properties": {"root": {"$ref": "#/$defs/TreeNode"}},
"$defs": {
"TreeNode": {
"type": "object",
"properties": {
"name": {"type": "string"},
"children": {
"type": "array",
"items": {"$ref": "#/$defs/TreeNode"},
},
},
},
},
}
# Must not raise RecursionError
result = _safe_replace_refs(schema)
assert isinstance(result, dict)
class TestResolveRefsCircular:
"""Tests that resolve_refs handles circular references."""
def test_circular_ref_does_not_recurse(self) -> None:
schema: dict[str, Any] = {
"type": "object",
"properties": {"root": {"$ref": "#/$defs/Node"}},
"$defs": {
"Node": {
"type": "object",
"properties": {
"child": {"$ref": "#/$defs/Node"},
},
},
},
}
resolved = resolve_refs(schema)
# The circular ref should become {"type": "object"} stub
child = resolved["properties"]["root"]["properties"]["child"]
assert child == {"type": "object"}
def test_indirect_circular_ref(self) -> None:
schema: dict[str, Any] = {
"type": "object",
"properties": {"a": {"$ref": "#/$defs/A"}},
"$defs": {
"A": {
"type": "object",
"properties": {"b": {"$ref": "#/$defs/B"}},
},
"B": {
"type": "object",
"properties": {"a": {"$ref": "#/$defs/A"}},
},
},
}
resolved = resolve_refs(schema)
# A -> B -> A(cycle) => the second A should be a stub
b_schema = resolved["properties"]["a"]["properties"]["b"]
assert b_schema["properties"]["a"] == {"type": "object"}
class TestCreateModelCircularRef:
"""End-to-end tests for create_model_from_schema with circular $ref schemas.
Regression tests for GitHub issue #5474: MCP servers with >10 tools
that expose self-referential JSON schemas caused
``RecursionError: maximum recursion depth exceeded``.
"""
def test_direct_self_referential_schema(self) -> None:
"""A type that references itself (tree-like structure)."""
schema: dict[str, Any] = {
"type": "object",
"properties": {
"name": {"type": "string"},
"children": {
"type": "array",
"items": {"$ref": "#/$defs/TreeNode"},
},
},
"required": ["name"],
"$defs": {
"TreeNode": {
"type": "object",
"properties": {
"name": {"type": "string"},
"children": {
"type": "array",
"items": {"$ref": "#/$defs/TreeNode"},
},
},
"required": ["name"],
},
},
}
Model = create_model_from_schema(schema, model_name="TreeSchema")
assert Model.__name__ == "TreeSchema"
obj = Model(name="root")
assert obj.name == "root"
def test_indirect_circular_reference(self) -> None:
"""Two types that reference each other (A -> B -> A)."""
schema: dict[str, Any] = {
"type": "object",
"properties": {"node": {"$ref": "#/$defs/NodeA"}},
"required": ["node"],
"$defs": {
"NodeA": {
"type": "object",
"properties": {
"name": {"type": "string"},
"linked": {"$ref": "#/$defs/NodeB"},
},
"required": ["name"],
},
"NodeB": {
"type": "object",
"properties": {
"value": {"type": "integer"},
"back": {"$ref": "#/$defs/NodeA"},
},
"required": ["value"],
},
},
}
Model = create_model_from_schema(schema, model_name="MutualRef")
obj = Model(node={"name": "hello", "linked": {"value": 42}})
assert obj.node.name == "hello"
def test_many_tools_with_complex_schemas(self) -> None:
"""Simulate an MCP server exposing >10 tools (issue #5474 trigger)."""
for i in range(15):
tool_schema: dict[str, Any] = {
"type": "object",
"properties": {
"query": {"type": "string"},
"options": {
"type": "object",
"properties": {
"limit": {"type": "integer"},
"filter": {"type": "string"},
},
},
},
"required": ["query"],
}
Model = create_model_from_schema(
tool_schema, model_name=f"Tool{i}Schema"
)
obj = Model(query=f"test_{i}")
assert obj.query == f"test_{i}"
def test_circular_ref_with_enrich_descriptions(self) -> None:
"""Circular schema + enrich_descriptions should not blow up."""
schema: dict[str, Any] = {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Node name"},
"child": {"$ref": "#/$defs/Recursive"},
},
"required": ["name"],
"$defs": {
"Recursive": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name"},
"child": {"$ref": "#/$defs/Recursive"},
},
},
},
}
Model = create_model_from_schema(
schema,
model_name="EnrichedCircular",
enrich_descriptions=True,
)
assert Model.__name__ == "EnrichedCircular"
obj = Model(name="top")
assert obj.name == "top"
def test_deeply_nested_non_circular_still_works(self) -> None:
"""A deep but non-circular chain of $refs should still resolve."""
schema: dict[str, Any] = {
"type": "object",
"properties": {"l1": {"$ref": "#/$defs/Level1"}},
"required": ["l1"],
"$defs": {
"Level1": {
"type": "object",
"properties": {"l2": {"$ref": "#/$defs/Level2"}},
"required": ["l2"],
},
"Level2": {
"type": "object",
"properties": {"l3": {"$ref": "#/$defs/Level3"}},
"required": ["l3"],
},
"Level3": {
"type": "object",
"properties": {"value": {"type": "string"}},
"required": ["value"],
},
},
}
Model = create_model_from_schema(schema, model_name="DeepChain")
obj = Model(l1={"l2": {"l3": {"value": "deep"}}})
assert obj.l1.l2.l3.value == "deep"

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.14.2a3"
__version__ = "1.14.2a5"

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