Keep trace summaries from breaking Logs and fix their counts

A failed trace-summary lookup now leaves the Logs page intact without
chips. Tool-call counts include only calls that ran, not their paused,
denied or skipped records. A local guardrail that fires unchanged on every
streamed segment is recorded once, so it cannot use up the span cap.
This commit is contained in:
arc53-machine committed 2026-09-23 22:09:58 +01:00
1 parent 24796ae61f
commit 9f7f0b2f1e
7 files changed
+134 -19

No files matched your search

+26 -5
View File
@@ -38,7 +38,23 @@ def _start_guardrail_span(stage: Stage, controls) -> "tracing.Span":
)
def _describe_decision(span, decision: StageDecision) -> None:
def _content_fired(decision: StageDecision) -> bool:
return bool(decision.triggered or decision.blocked or decision.redacted)
def _decision_key(decision: StageDecision) -> tuple:
"""Identity of a guardrail outcome, for recording each distinct one once."""
return (
"guardrail",
decision.stage.value,
tuple(sorted(v.check for v in decision.triggered)),
tuple(sorted(v.check for v in decision.unevaluated)),
decision.blocked,
decision.redacted,
)
def _describe_decision(span: "tracing.Span", decision: StageDecision) -> None:
"""Record a stage decision on its span; a firing guardrail drops trace previews."""
triggered = [v.check for v in decision.triggered]
span.set(
@@ -50,7 +66,7 @@ def _describe_decision(span, decision: StageDecision) -> None:
"docsgpt.guardrail.unevaluated": [v.check for v in decision.unevaluated] or None,
}
)
if triggered or decision.blocked or decision.redacted:
if _content_fired(decision):
# The scanned text (or text near it) sits in other spans' previews:
# the retrieval query, tool results, the answer. Keep none of it.
tracing.mark_content_blocked()
@@ -136,10 +152,15 @@ class GuardrailEngine:
decision.verdicts = [self._run_control(c, text, stage) for c in controls]
self._reduce(decision)
# The output guard evaluates every streamed segment; tracing each
# clean local scan would bury the trace, so only a firing is kept.
# clean local scan would bury the trace, so only a firing is kept,
# and a firing that repeats unchanged segment after segment is
# recorded once so it cannot use up the trace's span cap.
if not decision.clean or decision.unevaluated:
with _start_guardrail_span(stage, controls) as span:
_describe_decision(span, decision)
if tracing.first_occurrence(_decision_key(decision)):
with _start_guardrail_span(stage, controls) as span:
_describe_decision(span, decision)
elif _content_fired(decision):
tracing.mark_content_blocked()
self._record(decision)
return decision