From d22350a23075f1116bc0a15980bcb623d83a0d8a Mon Sep 17 00:00:00 2001 From: Pavol Loffay Date: Mon, 6 Jul 2026 18:08:16 +0200 Subject: [PATCH 1/2] Fix tool spans not being exported to tracing backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tool spans created in AuditLogger were not appearing in Tempo because start_span() was called without an explicit context parameter. In Python async code, OTEL context is not automatically propagated when spans are created inside async generators. Changes: - Add set_parent_context() method to AuditLogger to store the parent context - Pass explicit context to start_span() when creating tool spans - Add tool.name, tool.input (300 chars), tool.output (500 chars) attributes - Set span status to OK on successful tool result - Call set_parent_context() from query.py inside the agent.run span block This ensures tool spans are correctly parented under agent.run: proposal.lifecycle └── proposal.analyze └── agent.run └── tool.exec_command (now properly nested) Co-Authored-By: Claude Opus 4.5 --- src/lightspeed_agentic/audit.py | 27 +++++++++++++++++++++++++- src/lightspeed_agentic/routes/query.py | 5 +++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/lightspeed_agentic/audit.py b/src/lightspeed_agentic/audit.py index fcc5a9b..eb006a0 100644 --- a/src/lightspeed_agentic/audit.py +++ b/src/lightspeed_agentic/audit.py @@ -7,6 +7,9 @@ from datetime import UTC, datetime from typing import Any +from opentelemetry.context import Context +from opentelemetry.trace import StatusCode + from lightspeed_agentic.tracing import get_tracer from lightspeed_agentic.types import ProviderEvent @@ -35,10 +38,20 @@ def __init__( self._text_buffer: list[str] = [] self._thinking_buffer: list[str] = [] self._last_tool_name = "unknown" + self._last_tool_input = "" self._tool_span: Any = None self._tracer = get_tracer() + self._parent_context: Context | None = None self._emit("audit.agent.started", model=model, provider=provider) + def set_parent_context(self, ctx: Context) -> None: + """Set the parent span context for tool spans. + + Must be called from within the agent.run span block so that + tool spans are correctly parented under agent.run. + """ + self._parent_context = ctx + def process_event(self, event: ProviderEvent) -> None: match event.type: case "text_delta": @@ -52,7 +65,17 @@ def process_event(self, event: ProviderEvent) -> None: if self._tool_span is not None: self._tool_span.end() self._last_tool_name = event.name or "unknown" - self._tool_span = self._tracer.start_span(f"tool.{self._last_tool_name}") + self._last_tool_input = event.input or "" + # Create tool span with explicit parent context so it's correctly + # parented under agent.run even in async code + self._tool_span = self._tracer.start_span( + f"tool.{self._last_tool_name}", + context=self._parent_context, + attributes={ + "tool.name": self._last_tool_name, + "tool.input": self._last_tool_input[:300], + }, + ) self._emit( "audit.agent.tool.call", tool_name=self._last_tool_name, tool_input=event.input ) @@ -64,6 +87,8 @@ def process_event(self, event: ProviderEvent) -> None: success=True, ) if self._tool_span is not None: + self._tool_span.set_attribute("tool.output", event.output[:500]) + self._tool_span.set_status(StatusCode.OK) self._tool_span.end() self._tool_span = None case "result": diff --git a/src/lightspeed_agentic/routes/query.py b/src/lightspeed_agentic/routes/query.py index f907722..a4f8b7d 100644 --- a/src/lightspeed_agentic/routes/query.py +++ b/src/lightspeed_agentic/routes/query.py @@ -13,6 +13,8 @@ from fastapi import APIRouter, Request +from opentelemetry import context as otel_context + from lightspeed_agentic.audit import AuditLogger, derive_phase from lightspeed_agentic.logging import EventLogger from lightspeed_agentic.routes.models import RunRequest, RunResponse @@ -108,6 +110,9 @@ async def run() -> None: context=trace_ctx, attributes={"model": model, "provider": provider.name}, ): + # Pass the current context to AuditLogger so tool spans + # are correctly parented under agent.run + audit_logger.set_parent_context(otel_context.get_current()) result = provider.query( ProviderQueryOptions( prompt=prompt, From 375897d6a7bc50093a1ae9d1de0e6caba2ce4a25 Mon Sep 17 00:00:00 2001 From: Pavol Loffay Date: Mon, 6 Jul 2026 18:15:51 +0200 Subject: [PATCH 2/2] Fix import order for ruff lint Co-Authored-By: Claude Opus 4.5 --- src/lightspeed_agentic/routes/query.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lightspeed_agentic/routes/query.py b/src/lightspeed_agentic/routes/query.py index a4f8b7d..2a08afd 100644 --- a/src/lightspeed_agentic/routes/query.py +++ b/src/lightspeed_agentic/routes/query.py @@ -12,7 +12,6 @@ from typing import Any from fastapi import APIRouter, Request - from opentelemetry import context as otel_context from lightspeed_agentic.audit import AuditLogger, derive_phase