Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

- name: Install dependencies
working-directory: sdk
run: poetry install
run: poetry install --all-extras

- name: Run ruff linter
working-directory: sdk
Expand Down Expand Up @@ -180,7 +180,7 @@ jobs:

- name: Install dependencies
working-directory: sdk
run: poetry install
run: poetry install --all-extras

- name: Run tests with coverage
working-directory: sdk
Expand Down
10 changes: 10 additions & 0 deletions sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- `setup_tracing` type errors: exporter selection is typed, `endpoint` is
passed explicitly, and the unused provider reassignment is gone.
- A2A adapter type errors: `A2AConfig` imports `SecurityScheme`
unconditionally, and the executor no longer falls back to enqueuing a raw
dict the event queue cannot model.
- CI lint and test jobs install all extras, so the tracing, a2a and mcp
modules are type-checked and tested.

## [0.5.0] - 2026-09-10

**Migration note for DLQ consumers.** The *shape of DLQ entries* changes in
Expand Down
7 changes: 1 addition & 6 deletions sdk/eggai/adapters/a2a/config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
"""A2A configuration for EggAI agents."""

from a2a.types import SecurityScheme
from pydantic import BaseModel, ConfigDict, Field

try:
from a2a.types import SecurityScheme
except ImportError:
# Fallback if a2a-sdk not installed
SecurityScheme = dict


class A2AConfig(BaseModel):
"""Simple A2A configuration for EggAI agents."""
Expand Down
34 changes: 9 additions & 25 deletions sdk/eggai/adapters/a2a/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,30 +189,14 @@

async def _send_agent_response(self, event_queue: EventQueue, data: dict):
"""Send agent response through event queue."""
try:
from uuid import uuid4

from a2a.types import DataPart, Message, Part, Role
from uuid import uuid4

# Create A2A message with response data
response_message = Message(
message_id=str(uuid4()),
role=Role.agent,
parts=[Part(root=DataPart(data=data))],
)
from a2a.types import DataPart, Message, Part, Role

# Enqueue the message as an event
await event_queue.enqueue_event(response_message)
logger.debug(f"Sent agent response: {data}")

except ImportError as e:
logger.error(f"A2A types not available: {e}")
raise
except (ValueError, TypeError) as e:
logger.exception(f"Failed to create A2A message from data: {e}")
# Fallback: try to send as simple event
try:
await event_queue.enqueue_event({"text": json.dumps(data)})
except (TypeError, ValueError) as json_err:
logger.error(f"Failed to serialize response data: {json_err}")
raise
response_message = Message(
message_id=str(uuid4()),
role=Role.agent,
parts=[Part(root=DataPart(data=data))],

Check notice on line 199 in sdk/eggai/adapters/a2a/executor.py

View workflow job for this annotation

GitHub Actions / QualOps Code Quality

LOW: security

Full agent response payload is logged at DEBUG level, risking exposure of PII or sensitive business data in log aggregation systems.
)
await event_queue.enqueue_event(response_message)

Check failure on line 201 in sdk/eggai/adapters/a2a/executor.py

View workflow job for this annotation

GitHub Actions / QualOps Code Quality

HIGH: bug

`event_queue.enqueue_event` is called with a raw `Message` object, but the A2A `EventQueue` API may expect an event wrapper (e.g. `SendMessageResponse` or a task-status event) rather than a bare `Message`.
logger.debug(f"Sent agent response: {data}")
33 changes: 19 additions & 14 deletions sdk/eggai/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import os
from collections.abc import Callable
from contextlib import contextmanager
from typing import Any
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from opentelemetry.sdk.trace.export import SpanExporter

Check notice on line 9 in sdk/eggai/tracing.py

View workflow job for this annotation

GitHub Actions / QualOps Code Quality

LOW: maintainability

The `SpanExporter` symbol is imported twice: once under `TYPE_CHECKING` (line 9, for static analysis) and once at runtime inside `setup_tracing` (line 119, for the `isinstance` check). This dual-import pattern is non-obvious and could confuse maintainers into thinking the `TYPE_CHECKING` import is sufficient for the runtime check.


class _NoOpSpan:
Expand Down Expand Up @@ -101,7 +104,7 @@


def setup_tracing(
exporter="otlp",
exporter: str | SpanExporter = "otlp",
*,
service_name: str | None = None,
endpoint: str | None = None,
Expand All @@ -110,39 +113,41 @@
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor,
ConsoleSpanExporter,
SpanExporter,
)

resource = Resource.create(
{"service.name": service_name or os.getenv("OTEL_SERVICE_NAME", "eggai")}
{"service.name": service_name or os.getenv("OTEL_SERVICE_NAME") or "eggai"}
)
provider = TracerProvider(resource=resource)

if exporter == "console":
span_exporter: SpanExporter
if isinstance(exporter, SpanExporter):
span_exporter = exporter
elif exporter == "console":
span_exporter = ConsoleSpanExporter()
elif exporter == "otlp-http":
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter,
OTLPSpanExporter as HttpSpanExporter,
)

span_exporter = OTLPSpanExporter(**({"endpoint": endpoint} if endpoint else {}))
elif hasattr(exporter, "export"):
span_exporter = exporter # accept pre-built exporter object (for testing)
span_exporter = HttpSpanExporter(endpoint=endpoint)
else: # "otlp" / "otlp-grpc" (default)

Check warning on line 138 in sdk/eggai/tracing.py

View workflow job for this annotation

GitHub Actions / QualOps Code Quality

MEDIUM: bug

OTLP exporter now receives explicit `endpoint=None` instead of having the keyword argument omitted, changing behaviour when no endpoint is configured.
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
OTLPSpanExporter as GrpcSpanExporter,
)

span_exporter = OTLPSpanExporter(**({"endpoint": endpoint} if endpoint else {}))
span_exporter = GrpcSpanExporter(endpoint=endpoint)

provider.add_span_processor(BatchSpanProcessor(span_exporter))

from opentelemetry.trace import ProxyTracerProvider

Check failure on line 148 in sdk/eggai/tracing.py

View workflow job for this annotation

GitHub Actions / QualOps Code Quality

HIGH: bug

Removing the `else` branch causes `setup_tracing` to silently create and discard a `TracerProvider`+`BatchSpanProcessor` (and its background worker thread) when a non-`ProxyTracerProvider` is already registered, leaking an export thread that runs indefinitely.
if isinstance(trace.get_tracer_provider(), ProxyTracerProvider):
trace.set_tracer_provider(provider)
else:
# Adopt the already-configured provider (API base type, not the SDK one).
provider = trace.get_tracer_provider()

from importlib.metadata import version

Expand Down
1 change: 0 additions & 1 deletion sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ line-ending = "auto"

[tool.mypy]
python_version = "3.10"
# The lint job installs no extras, so the a2a adapter and tracing are not type-checked yet.
files = ["eggai"]
ignore_missing_imports = true
warn_unused_ignores = true
Expand Down
2 changes: 1 addition & 1 deletion sdk/tests/test_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ async def handler(msg):

# OTEL does not allow overriding the global TracerProvider, so a single shared
# provider + exporter is configured once at module level and cleared between tests.
otel = pytest.importorskip("opentelemetry")
otel = pytest.importorskip("opentelemetry.sdk")

from opentelemetry import trace as _otel_trace # noqa: E402
from opentelemetry.sdk.trace import TracerProvider as _TracerProvider # noqa: E402
Expand Down
Loading