feat: Add AG2 instrumentation integration#1849
feat: Add AG2 instrumentation integration#1849faridun-ag2 wants to merge 7 commits intopydantic:mainfrom
Conversation
…ound counting - Restructure instrument_ag2 to patch eagerly and return uninstrument_context(), matching the pattern used by instrument_fastapi/instrument_print - Rewrite tests to use inline_snapshot pattern per project conventions - Handle positional args in wrappers (generate_reply, run_chat, execute_function) - Fix ag2.total_rounds to count speaker transitions instead of duplicating message count - Add test for eager patching behavior (instrument_ag2 without `with` block) - Fix DeprecationWarning from jsonschema.RefResolver in test_logfire_api.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| def should_trace(agent_obj: Any) -> bool: | ||
| return target_ids is None or id(agent_obj) in target_ids |
There was a problem hiding this comment.
📝 Info: Inner spans (agent turns, tools, rounds) are not gated by the agent filter
The should_trace check (logfire/_internal/integrations/ag2.py:46-47) only gates conversation-level spans in wrap_run/wrap_a_run. The generate_reply, execute_function, select_speaker, and run_chat wrappers create spans unconditionally for ALL agents, even when the agent parameter restricts tracing to specific instances. The docstring at logfire/_internal/main.py:1163-1164 says inner spans are emitted "for all participants within a traced conversation," but in reality they fire globally regardless of whether a parent conversation span exists. This is a reasonable design trade-off (filtering by parent context would be much more complex), but the docstring is slightly misleading.
Was this helpful? React with 👍 or 👎 to provide feedback.
- Remove developer-specific absolute paths from quickstart README - Clarify in instrument_ag2 docstring that the agent parameter only scopes conversation-level spans, not inner spans Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| def wrap_run(original: Callable[..., Any]) -> Callable[..., Any]: | ||
| @wraps(original) | ||
| def _wrapped(self: Any, *args: Any, **kwargs: Any) -> Any: | ||
| response = original(self, *args, **kwargs) | ||
| if not should_trace(self): | ||
| return response | ||
| _wrap_response_process( | ||
| response=response, | ||
| logfire_instance=logfire_instance, | ||
| runner=self, | ||
| recipient=_resolve_recipient(args, kwargs), | ||
| message=kwargs.get('message'), | ||
| record_content=record_content, | ||
| suppress_other_instrumentation=suppress_other_instrumentation, | ||
| ) | ||
| return response | ||
|
|
||
| return _wrapped |
There was a problem hiding this comment.
🚩 No handle_internal_errors guard around span/attribute operations
The AG2 wrappers (wrap_run, wrap_generate_reply, wrap_execute_function, etc.) call logfire_instance.span() and span.set_attribute() without wrapping in handle_internal_errors. If any span operation fails unexpectedly, the exception propagates into the user's AG2 code. Notably, _wrap_response_process does setattr(response, 'process', wrapped) on a response object — if this fails (e.g., frozen object), the response is lost since the exception prevents reaching return response in wrap_run at line 75. The claude_agent_sdk.py integration uses with handle_internal_errors: in several places (logfire/_internal/integrations/claude_agent_sdk.py:177,223,253,408) to prevent instrumentation errors from breaking user code.
Was this helpful? React with 👍 or 👎 to provide feedback.
- Add ag2[openai] as a dev dependency so pyright can resolve autogen types in CI - Remove trailing whitespace in docs/integrations/llms/ag2.md - Restore proper type annotations in ag2.py (ConversableAgent instead of Any) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| cm = suppress_instrumentation() if suppress_other_instrumentation else nullcontext() | ||
| with cm: | ||
| attrs = _conversation_attrs(runner, recipient, message, record_content) | ||
| with logfire_instance.span(SPAN_CONVERSATION, **attrs) as span: | ||
| result = await process(*args, **kwargs) | ||
| _set_conversation_summary_attributes(span, recipient) | ||
| return result |
There was a problem hiding this comment.
🔴 suppress_instrumentation wraps around the AG2 span, suppressing all AG2 spans including the conversation span itself
When suppress_other_instrumentation=True, the suppress_instrumentation() context manager is entered before the conversation span is created, which causes CheckSuppressInstrumentationProcessorWrapper.on_start() (logfire/_internal/exporters/processor_wrapper.py:49) to drop the conversation span itself (and all inner AG2 spans). This silently produces zero telemetry output.
Compare with the correct pattern in the OpenAI integration (logfire/_internal/integrations/llm_providers/llm_provider.py:143-144), which creates the LLM span first, then enters suppression inside it so only inner HTTP spans are suppressed.
The same issue exists in both the sync wrapper (line 274-281) and the async wrapper (line 260-267).
Prompt for agents
In _wrap_response_process in logfire/_internal/integrations/ag2.py, both the sync (wrapped_process_sync, lines 274-281) and async (wrapped_process_async, lines 260-267) wrappers place suppress_instrumentation() OUTSIDE the logfire_instance.span() call. This causes the AG2 conversation span and all inner spans to be suppressed when suppress_other_instrumentation=True.
The fix is to move the suppression INSIDE the span, matching the pattern used in the OpenAI integration (logfire/_internal/integrations/llm_providers/llm_provider.py lines 143-144). The conversation span should be created first, then suppression should be entered inside it so that only non-AG2 inner instrumentation (e.g. HTTPX spans from OpenAI HTTP calls) is suppressed.
For the async wrapper, change from:
cm = suppress_instrumentation() if suppress_other_instrumentation else nullcontext()
with cm:
attrs = _conversation_attrs(...)
with logfire_instance.span(SPAN_CONVERSATION, **attrs) as span:
result = await process(...)
To:
attrs = _conversation_attrs(...)
with logfire_instance.span(SPAN_CONVERSATION, **attrs) as span:
cm = suppress_instrumentation() if suppress_other_instrumentation else nullcontext()
with cm:
result = await process(...)
_set_conversation_summary_attributes(span, recipient)
return result
Apply the same fix to the sync wrapper.
Was this helpful? React with 👍 or 👎 to provide feedback.
Move suppress_instrumentation() inside the conversation span so the AG2 span itself is not suppressed — only inner non-AG2 instrumentation (e.g. HTTPX spans from OpenAI calls) is suppressed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Adds first-class observability support for AG2 (formerly AutoGen) multi-agent conversations via `logfire.instrument_ag2()`.
What's included
Core integration
API stubs & shims
Documentation
Tests
Quickstart example
Spans emitted
Validation