Skip to content

feat(a2a): add loongsuite-instrumentation-a2a (#28) - #274

Open
RichardoMrMu wants to merge 23 commits into
alibaba:mainfrom
RichardoMrMu:feat/a2a-instrumentation
Open

RichardoMrMu wants to merge 23 commits into
alibaba:mainfrom
RichardoMrMu:feat/a2a-instrumentation

Conversation

@RichardoMrMu

Copy link
Copy Markdown
Contributor

What

Adds a new instrumentation package loongsuite-instrumentation-a2a, providing automatic OpenTelemetry instrumentation for the official A2A (Agent2Agent) Python SDK (a2a-sdk).

Closes #28 (roadmap #36 — "Add instrumentation for official A2A", contribution welcome).

How

Produces an ARMS gen-ai AGENT span (gen_ai.span.kind=AGENT, gen_ai.operation.name=invoke_agent) around each server-side agent turn, bracketing the user's AgentExecutor.execute invocation so that all downstream work nests under a single invoke_agent span with a shared trace_id. Records a2a.context_id, a2a.task_id, and the user input as gen_ai.input.messages (disable with OTEL_INSTRUMENTATION_A2A_CAPTURE_CONTENT=false).

Complementary, not duplicative. a2a-sdk already ships an OTel tracing layer (a2a.utils.telemetry) that decorates its transports and request handlers with generic spans under the instrumenting module a2a-python-sdk — those describe protocol plumbing, carry no gen-ai semconv, and do not wrap the user's execute implementation (which is an abstract method applications override). This package supplies exactly that missing gen-ai agent boundary, mirroring how sibling loongsuite packages layer ARMS gen-ai spans over frameworks that already emit some telemetry.

AgentExecutor.execute is an ABC method overridden by every concrete agent, so the instrumentor (1) walks the existing AgentExecutor subclass tree at instrument() time and wraps each subclass's own execute via wrapt, and (2) installs an __init_subclass__ hook on AgentExecutor so executors defined after instrumentation are wrapped as they are created. A sentinel prevents double-wrapping; uninstrument() unwraps every marked execute and restores __init_subclass__.

Testing

tests/test_instrumentor.py drives the real a2a-sdk AgentExecutor ABC with in-process executor subclasses and asserts on spans exported to an InMemorySpanExporter (no network). Each GREEN assertion is paired with a RED baseline:

  • RED: executing an agent with no instrumentor active produces no AGENT span.
  • GREEN (existing subclass): AGENT span with gen_ai.framework=a2a, invoke_agent, a2a.context_id/a2a.task_id; inner agent work nests under the AGENT span (same trace_id, correct parent); user input captured.
  • GREEN (late subclass): an executor defined after instrument() still gets an AGENT span — proving the __init_subclass__ hook.
  • RED-after-teardown: after uninstrument(), executing a fresh executor produces no AGENT span.
  • Plus exception recording (span status ERROR + re-raise) and idempotent double-uninstrument.

8 tests pass (pytest), on a2a-sdk 1.1.5 + opentelemetry-sdk.

Notes

  • Package layout, pyproject.toml, entry point, CHANGELOG and bootstrap-registry entry follow the existing loongsuite-instrumentation-* conventions (e.g. terminus2).
  • CHANGELOG.md updated (repo requirement).

…rc/opentelemetry/instrumentation/a2a/package.py
…rc/opentelemetry/instrumentation/a2a/version.py
…rc/opentelemetry/instrumentation/a2a/__init__.py
CI precommit failed on the new package: E402 in tests/conftest.py (imports intentionally after sys.path setup + pytest_configure), PLC0415 for lazy imports in src/tests, and I001/format. Follow the existing per-file-ignores convention used by every other loongsuite instrumentation package (PLC0415 for the package, E402+F811 for tests), and apply ruff import sorting + formatting. No behavior change; 8 tests still pass.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Instrumentation lifecycle bugs, default sensitive-content capture, and missing CI registration must be resolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity · 4 Medium severity

Open (6)
What changed in this PR

Adds automatic OpenTelemetry instrumentation for A2A server-side agent execution.

Changes:

  • Creates AGENT spans around AgentExecutor.execute.
  • Supports existing and late-defined executor subclasses.
  • Adds package metadata, tests, documentation, and bootstrap registration.
File Description
pyproject.toml Adds lint exceptions.
loongsuite-distro/​.../​loongsuite_instrumentation_a2a.py Registers bootstrap metadata.
.../​tests/​test_instrumentor.py Tests spans and lifecycle behavior.
.../​tests/​conftest.py Configures tracing fixtures.
.../​tests/​__init__.py Initializes the test package.
.../​test-requirements.txt Defines test dependencies.
.../​version.py Defines package version.
.../​package.py Declares instrumented dependency.
.../​a2a/​__init__.py Implements instrumentation.
.../​README.md Documents installation and usage.
.../​pyproject.toml Defines package metadata and entry point.
.../​LICENSE Adds Apache 2.0 license.
.../​CHANGELOG.md Records the initial release.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

build-backend = "hatchling.build"

[project]
name = "loongsuite-instrumentation-a2a"
@sipercai

Copy link
Copy Markdown
Collaborator

Thanks for contributing this integration! The server-side agent execution tracing is a useful addition. Two suggestions before merging:

  1. Could we use this repository’s opentelemetry-util-genai to manage semantic conventions and content-capture settings consistently? We can help extend the shared utility where needed. Please also ensure telemetry failures cannot interrupt application execution.

  2. The community is discussing A2A semantic conventions in OTel #195. Could we align the relevant fields with that draft, while keeping agent execution distinct from A2A protocol operations? Full protocol instrumentation can follow separately.

Happy to collaborate on the utility changes!

@RichardoMrMu

Copy link
Copy Markdown
Contributor Author

Thanks @sipercai — both suggestions are addressed, plus the Copilot findings, and I pushed the changes with tests.

1. Use opentelemetry-util-genai for semconv + content-capture; telemetry must not break execution

  • Span kind now comes from the shared GenAiSpanKindValues and the gen_ai.span.kind key from the shared GEN_AI_SPAN_KIND.
  • Content capture is now governed by the shared get_content_capturing_mode() / ContentCapturingMode. I removed the package-specific OTEL_INSTRUMENTATION_A2A_CAPTURE_CONTENT env; an absent/invalid OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT now defaults to NO_CONTENT, so user prompts are no longer exported without explicit opt-in (Copilot's privacy finding). README + tests updated: one test asserts SPAN_ONLY captures, another asserts the default suppresses content.
  • All attribute-setting is wrapped in a fail-safe helper: any telemetry error is swallowed (logged at debug) so instrumentation cannot interrupt execute. Added test_telemetry_failure_does_not_break_execution (a hostile context whose accessors raise — execute still returns its result).

2. Align with A2A semconv (#195); keep agent execution distinct from A2A protocol

  • This package remains strictly the agent-execution boundary (the invoke_agent AGENT span). I did not add protocol/method spans — full protocol instrumentation can follow separately, as you suggested.
  • Where the chore(ci): scope LoongSuite tests to changed packages #195 draft names stable context available at the execution boundary, I attach it with the draft's keys: a2a.task.id and a2a.task.state (renamed from the previous non-conforming a2a.task_id; context_id is kept as the a2a-sdk grouping id under a2a.context.id). Documented the scope + the chore(ci): scope LoongSuite tests to changed packages #195 link in the README and module docstring.

Copilot findings picked up in the same pass:

  • Span name now uses the concrete executor class (invoke_agent {ClassName}) instead of the constant, matching sibling agent instrumentations.
  • uninstrument() restores AgentExecutor.__init_subclass__ correctly: I record whether the base had its own hook and delattr our override (rather than the previous read-only __dict__ deletion that always raised and left a no-op installed). Added test_uninstrument_restores_init_subclass.
  • The __init_subclass__ replacement now delegates via super(AgentExecutor, cls).__init_subclass__(**kw) when the base has no own hook, so cooperative bases are not skipped, and it no longer swallows class-definition errors.
  • Singleton-safe init: the wrapper/hook bookkeeping is seeded only on the singleton's first construction (guarded by a flag), so constructing another A2AInstrumentor() after instrumenting no longer clears state and breaks uninstrument().
  • Registered the package in tox-loongsuite.ini (test + lint envs) and regenerated the LoongSuite workflow matrices so CI actually runs the A2A jobs.

11 unit tests pass locally against the real a2a-sdk; ruff check and ruff format --check are clean. Glad to iterate on the shared-utility extension if you'd like additional A2A fields surfaced.

@sipercai

sipercai commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks for the update! All 11 tests passed locally. One clarification: we'd like ExtendedTelemetryHandler to manage the span lifecycle, not just reuse util constants.

Please build an InvokeAgentInvocation and use start_invoke_agent, stop_invoke_agent, and fail_invoke_agent for execution, success, and failure. See the handler implementation; we can help with shared-util gaps.

Three remaining fixes:

  • Declare the opentelemetry-util-genai dependency; clean installation currently fails to import.
  • Isolate span start/end/error-recording failures too: fault injection still blocked execution or replaced the business result/exception.
  • Map contextId to gen_ai.conversation.id per #195, and use INTERNAL for this executor boundary.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The ineffective privacy opt-out and multiple telemetry correctness issues must be resolved before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 3 Medium severity · 1 Low severity

Open (5)
Resolved since last review (5)

``NO_CONTENT`` (no capture), matching the rest of loongsuite.
"""
try:
return get_content_capturing_mode() in _CONTENT_ON_SPAN_MODES
# left to a dedicated protocol instrumentation.
_A2A_TASK_ID = "a2a.task.id"
_A2A_TASK_STATE = "a2a.task.state"
_A2A_CONTEXT_ID = "a2a.context.id" # a2a-sdk RequestContext grouping id
Comment on lines +177 to +182
task_state = getattr(
getattr(current_task, "status", None), "state", None
)
state_value = getattr(task_state, "value", task_state)
if state_value:
span.set_attribute(_A2A_TASK_STATE, str(state_value))
Comment on lines +113 to +114
async def test_green_agent_span_existing_subclass(instrument, span_exporter):
ExecCls = _make_executor_cls()

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Add instrumentation for official A2A

6 participants