Keep tool output out of span errors and fix trace status and listing

Tool results, denial comments and tool exception text now reach a span only
as a capture-gated preview; span.error is a fixed message, since it is
stored and exported regardless of content settings. A turn whose stream
yields an error event is recorded as failed. Search traces are listed by a
query copied into the small summary column, so the Logs timeline never
reads the spans JSONB. Adds GraphRAG span tests.
This commit is contained in:
arc53-machine committed 2026-09-23 18:11:19 +01:00
1 parent 285fdc914c
commit efec1a2f83
9 files changed
+134 -13

No files matched your search

+13 -5
View File
@@ -81,11 +81,15 @@ def finish_tool_span(span: Any, data: Dict[str, Any]) -> None:
if data.get("result") is not None:
span.preview("result", data.get("result"))
span_status = _TOOL_SPAN_STATUS.get(status, tracing.STATUS_OK)
# ``error`` and ``result`` are tool output or user text (a denial
# comment), so they travel only as previews, which honour the content
# capture settings; ``span.error`` is exported and stored unconditionally.
if data.get("error"):
span.preview("error", data["error"])
if span_status == tracing.STATUS_ERROR:
span.error = str(data.get("error") or data.get("result") or "tool error")[:500]
span.set(**{"error.type": str(data.get("error_type") or "ToolError")})
elif data.get("error"):
span.error = str(data["error"])[:500]
error_type = str(data.get("error_type") or "ToolError")
span.error = f"Tool call failed ({error_type})"
span.set(**{"error.type": error_type})
span.end(span_status)
@@ -1021,7 +1025,11 @@ class ToolExecutor:
try:
outcome = yield from self._execute(tools_dict, call, llm_class_name)
except Exception as exc:
span.end(error=exc)
# A tool's exception text can quote its response; like tool
# output it goes only into the capture-gated preview.
span.preview("error", str(exc))
span.error = f"Tool call failed ({type(exc).__name__})"
span.end(tracing.STATUS_ERROR, attributes={"error.type": type(exc).__name__})
raise
except GeneratorExit:
span.end(tracing.STATUS_CANCELLED)
+7
View File
@@ -860,6 +860,13 @@ class BaseAnswerResource:
# error silently — the exact shape of the bug being fixed here.
if stream_error:
query_metadata.setdefault("error", stream_error)
# A yielded error (e.g. a failed workflow node) ends the
# generator normally, so no span raised; the user still saw
# the turn fail, and its trace should say so. A pause below
# overrides this, as it does for the message row.
trace = tracing.current_trace()
if trace is not None:
trace.outcome = tracing.STATUS_ERROR
# ---- Paused: save continuation state and end stream early ----
if paused:
+4 -3
View File
@@ -99,9 +99,10 @@ def _trace_branch(name: str, sources_sql: str, scope: str) -> dict:
return {
"name": name,
"level": "CASE WHEN t.status = 'error' THEN 'error' ELSE 'info' END",
# A search's query sits in its retrieval span's preview; graph builds
# are named after their source.
"summary": "COALESCE(t.spans->0->'preview'->>'query', t.name, t.source)",
# A search is listed by its query (copied into the small ``summary``
# at flush so this never detoasts ``spans``); graph builds are named
# after their source.
"summary": "COALESCE(t.summary->>'query', t.name, t.source)",
"where": [f"t.source IN {sources_sql}", scope],
"sql": f"""
SELECT '{name}' AS event_type,
+9 -1
View File
@@ -431,6 +431,14 @@ class Trace:
def to_record(self) -> Dict[str, Any]:
"""The ``request_traces`` row for this (finished) trace."""
summary = self.summary()
# Traces without a log row of their own (searches) are listed by
# their query; keep it in the small summary so listing never reads
# ``spans``. Dropped with the other previews when content is blocked.
if not self.content_blocked and self.spans:
query = self.spans[0].previews.get("query")
if isinstance(query, str):
summary["query"] = query
spans = []
for s in self.spans:
entry: Dict[str, Any] = {
@@ -464,7 +472,7 @@ class Trace:
"duration_ms": int(round(self.duration_ms or 0)),
"span_count": len(spans),
"dropped_spans": self.dropped_spans,
"summary": self.summary(),
"summary": summary,
"spans": spans,
"otel_trace_id": self.otel_trace_id,
}