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