symfonic.services.observability.capture¶
capture ¶
Prompt-capture policy — the one place "may I record this?" is answered.
Before this module the answer was a bare otel_capture_prompts boolean read
at each emission site. A boolean read in four places is four policies that
happen to agree, and the day one site forgets to read it is the day prompts
leak into a span exporter nobody audited.
Where the gate belongs. otel_capture_prompts has always governed one
thing: whether prompt and completion text is stamped onto a span attribute
and shipped to a collector. The shipped SpanRecorder still enforces it
there. It has never governed what an in-process CallbackHandler is handed —
emit_llm_end populates LLMEndEvent.output unconditionally — and
in-process delivery to code the adopter registered themselves is not an egress
boundary. Reading that flag before fan-out would therefore silently empty every
adopter payload the moment the run source moved to this bridge: a handler
persisting completions for quality review would keep running and start
recording empty strings, with no exception and no log line.
So :meth:PromptCapturePolicy.from_config delivers payloads, and redaction
before fan-out is a separate, explicit opt-in
(FrameworkConfig.observability_redact_payloads) for adopters whose
observers are themselves an egress boundary. Both answers remain one object
rather than a boolean re-read per site — the split is which question it
answers, not how many places answer it.
That opt-in is a declared field on FrameworkConfig, and it has to be:
the model is extra="ignore", so an undeclared keyword is dropped at
construction with nothing but a warning. A redaction control that is only ever
read through getattr is a control that fails open — the adopter sets it,
pydantic discards it, and every payload ships anyway. from_config still
reads it defensively so a duck-typed config object works, but the supported
surface is the field.
Being declared is necessary and not sufficient: a control nothing on the
wiring path calls fails open just as quietly. compose_event_sink therefore
takes the config itself and derives the policy from it, so the recipe
adopters are given honours the flag without anyone remembering to construct a
PromptCapturePolicy by hand. See :mod:.suite.
PromptCapturePolicy
dataclass
¶
PromptCapturePolicy(capture_prompts: bool = False, capture_outputs: bool = False, capture_tool_arguments: bool = False)
Three independent gates over the three payload classes.
They are independent because the risks are: an adopter may want the model's
output for quality review while never recording the user's prompt, and a
single flag cannot express that. from_config still maps the shipped
single flag onto all three, so the current configuration surface is
unchanged — the split is available, not imposed.
argument_names
staticmethod
¶
Sorted argument names, always available.
Names are structure; values are payload. Keeping the names when the
values are denied is what lets an operator see that a tool was called
with a customer_id without seeing which customer.
Source code in src/symfonic/services/observability/capture.py
from_config
classmethod
¶
The policy for in-process fan-out, which is not an egress boundary.
Deliberately does not read otel_capture_prompts: that flag
gates span attributes and is applied by the shipped SpanRecorder at
the exporter, where the data actually leaves the process. Reusing it
here would broaden its meaning from "do not put prompts in span
attributes" to "do not give any consumer the text", silently emptying
callback payloads that shipped code has always populated.
FrameworkConfig.observability_redact_payloads is the explicit
opt-in for adopters who want the bridge itself to withhold. It is read
through getattr so a duck-typed config still works, but it is a
declared field precisely because a getattr-only control on an
extra="ignore" model would be silently dropped and fail open.
Source code in src/symfonic/services/observability/capture.py
output_text ¶
prompt_text ¶
tool_arguments ¶
Tool call arguments, or None when withheld.