-
Notifications
You must be signed in to change notification settings - Fork 784
Reimplement Weave tracer and unify emitter interface #411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR reimplements the Weave tracer integration and unifies the emitter interface across different tracer backends. Key changes include:
- Introduces a common
SpanRecordingContextprotocol andSpanCoreFieldstype for tracer-agnostic span creation - Refactors emitters (operation, message, exception, object, annotation) to use the unified interface
- Replaces the old Weave tracer implementation with a new callback-based architecture using an in-memory trace server
Reviewed changes
Copilot reviewed 36 out of 38 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| agentlightning/tracer/weave.py | Complete rewrite of Weave tracer using callback pattern and unified span recording interface |
| agentlightning/tracer/dummy.py | New dummy tracer for testing without backend dependencies |
| agentlightning/tracer/base.py | Added active tracer management and new abstract methods for span creation |
| agentlightning/tracer/otel.py | Updated to implement unified span creation interface |
| agentlightning/emitter/*.py | Refactored all emitters to use get_active_tracer() and SpanRecordingContext |
| agentlightning/utils/otel.py | Added sanitization, flattening, and exception formatting utilities |
| agentlightning/types/tracer.py | Added SpanCoreFields, SpanRecordingContext, and StatusCode types |
| agentlightning/instrumentation/weave.py | Implemented comprehensive InMemoryWeaveTraceServer for testing |
| tests/**/*.py | Updated tests to use new recording tracer patterns and unified interfaces |
| pyproject.toml | Updated weave dependency version constraint and added setuptools override |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| with pytest.raises(ValueError): | ||
| complicated(1, "req", 7, 8, 9, kwonly="x", kwdefault="y", tag="value") |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test now expects a ValueError to be raised but doesn't verify the actual result or behavior of the function. The original test verified the output result against expected values. Consider asserting on the specific error message or adding a separate test for the success case to ensure the function still works correctly when valid inputs are provided.
| try: | ||
| weave_client.flush() | ||
| # It's possible that the call end futures are from a dedicated Weave thread pool, | ||
| await asyncio.gather(*[asyncio.wrap_future(future) for future in self._complete_call_futures]) |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The asyncio.gather call attempts to wrap futures that may already be from a different event loop or thread pool. If the futures in self._complete_call_futures are from a dedicated Weave thread pool (as mentioned in the comment on line 372), wrapping them with asyncio.wrap_future could fail or cause deadlocks. Consider checking if the futures are already asyncio futures before wrapping, or handling the case where they originate from a different executor.
| await asyncio.gather(*[asyncio.wrap_future(future) for future in self._complete_call_futures]) | |
| await asyncio.gather(*[ | |
| future if isinstance(future, asyncio.Future) else asyncio.wrap_future(future) | |
| for future in self._complete_call_futures | |
| ]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this. Let's test it out later.
tests/emitter/test_operation.py
Outdated
| assert op.span().start_time < op.span().end_time # type: ignore | ||
| assert op.span().start_time < time.time() # type: ignore | ||
| assert op.span().end_time < time.time() # type: ignore | ||
| assert op.span().start_time < op.span().end_time # type: ignore |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assertions on lines 412 and 415 are duplicates - both check that start_time is less than end_time. This appears to be a copy-paste error. Consider removing the duplicate assertion on line 415.
|
|
||
|
|
||
| def test_operation_context_records_exceptions(monkeypatch: pytest.MonkeyPatch) -> None: | ||
| tracer = _install_recording_tracer(monkeypatch) |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable tracer is not used.
| assert status.status_code == StatusCode.ERROR | ||
| assert status.description == "boom" | ||
| assert use_span.exit_calls[-1][1].args == ("boom",) # type: ignore | ||
| recording = tracer.recordings[-1] |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
|
/ci |
|
🚀 CI Watcher for correlation id-3652764795-mj6m877e triggered by comment 3652764795
✅ All runs completed. |
|
/ci |
|
🚀 CI Watcher for correlation id-3653007475-mj6oy8b8 triggered by comment 3653007475
✅ All runs completed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 45 out of 47 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
agentlightning/types/core.py:359
- Overridden method signature does not match call, where it is passed an argument named 'store'. Overriding method method OtelTracer.init_worker matches the call.
Overridden method signature does not match call, where it is passed an argument named 'store'. Overriding method method WeaveTracer.init_worker matches the call.
Overridden method signature does not match call, where it is passed an argument named 'store'. Overriding method method WeaveTracer.init_worker matches the call.
Overridden method signature does not match call, where it is passed an argument named 'store'. Overriding method method WeaveTracer.init_worker matches the call.
def init_worker(self, worker_id: int, *args: Any, **kwargs: Any) -> None:
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| with agentops_tracer.operation_context("agentops-op", attributes={"foo": "bar"}) as ctx: | ||
| raise ValueError("agentops boom") | ||
|
|
||
| recorded_span = ctx.get_recorded_span() # type: ignore |
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
| with tracer.operation_context("dummy-error") as ctx: | ||
| raise RuntimeError("boom") | ||
|
|
||
| recorded_span = ctx.get_recorded_span() # type: ignore |
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
| ctx.record_attributes({"custom": "attr"}) | ||
| raise RuntimeError("otel failure") | ||
|
|
||
| recorded_span = ctx.get_recorded_span() # type: ignore |
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
| with tracer.operation_context("weave-op", attributes={"foo": "bar"}) as ctx: | ||
| raise RuntimeError("failed") | ||
|
|
||
| recorded_span = ctx.get_recorded_span() # type: ignore |
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
|
/ci |
|
🚀 CI Watcher for correlation id-3653439454-mj6qsqzi triggered by comment 3653439454
✅ All runs completed. |
The current Weave implementation bases on registering a handler in finish_call. This suffers from drawbacks that the recorded call is not complete, as Weave will internally creates a different CallSchema object and upload to server. In parallel, Weave also provides (implicitly) a way for customizing TraceServer. If properly substituting the TraceServer with a customized implementation, we would be able to get all the traces.
This PR also refactors the emitter interface. The current approach heavily depends on otel, which is not a must for Weave. By decoupling emitter from tracer implementation (which now divides into Otel and non-Otel), and
create_spanandoperation_contextare added to tracer interface, we now have a way to clearly separate the creation of spans (via SpanCoreFields) from the propagation of spans (to tracers).