@@ -118,3 +118,58 @@ It force-flushes the provider during shutdown but leaves provider shutdown to th
118118application. If no provider is passed, the OTEL sink first checks for a globally
119119registered SDK provider, then falls back to its existing Agent Control-owned OTLP
120120pipeline.
121+
122+
123+ ## Whole control-check elapsed time
124+
125+ ` evaluate_controls() ` , ` check_evaluation_with_local() ` and the server-only
126+ ` evaluation.check_evaluation() ` return ` EvaluationResult.control_check_elapsed_ms `
127+ and a unique ` control_check_id ` . Each value measures ** one pre or post check**
128+ with a monotonic clock. It includes preparation inside the evaluation helper,
129+ local controls, and the conditional server call (network/auth refresh/retry wait
130+ included). It excludes ` init() ` , decorator payload preparation before the helper,
131+ the protected tool/LLM function, and export of the whole-check event itself.
132+ High-level ` evaluate_controls() ` step construction and client setup happen before
133+ the timed helper and are excluded, as is parent trace-context resolution.
134+ The decorator server-only fallback also measures its HTTP evaluation attempt;
135+ its already-built payload and client initialization are outside that boundary.
136+ If local evaluation fails and the decorator falls back, these are separate check
137+ attempts with separate IDs rather than one fabricated combined elapsed value.
138+ Existing per-control ` execution_duration_ms ` values are unchanged.
139+
140+ ``` python
141+ # After agent_control.init(...) has configured the SDK session.
142+ result = await agent_control.evaluate_controls(
143+ agent_name = " my-agent" , step_name = " search" , input = {" query" : " example" },
144+ stage = " pre" , step_type = " tool"
145+ )
146+ print (result.control_check_id, result.control_check_elapsed_ms)
147+ ```
148+
149+ Local denial, a completed result containing evaluator errors, and a check with
150+ no applicable controls still have measured elapsed time. The last case measures
151+ the check's preparation, rather than assigning zero as a placeholder. A genuinely
152+ measured zero is valid. Older/manually constructed results have ` None ` unless
153+ actually measured by these helpers. An SDK check that raises or is cancelled
154+ still propagates that outcome; it does not return a fabricated evaluation result.
155+
156+ With observability enabled and an ` otel ` sink selected, the same provider emits
157+ one ` agent_control.control_check ` span as a child of the correlated step. Its
158+ start is captured when the check begins and its end uses that start plus measured
159+ monotonic elapsed nanoseconds; these are not reconstructed from control children.
160+ Attributes include ` agent_control.control_check_elapsed_ms ` , the check ID,
161+ ` check_stage ` , and ` control_check_status ` (` completed ` , ` error ` , ` cancelled ` ).
162+ Safe IDs/stage/elapsed/status are also supplied in the JSON ` metadata ` attribute.
163+ No input/output or exception text is recorded. Existing Orbit ingestion treats
164+ this as a workflow span; it is ** not** an individual shield/control span. Showing
165+ these spans/metadata in a dedicated session summary requires UI/Orbit follow-up.
166+
167+ Existing HTTP control-event ingestion accepts only individual control events;
168+ it does not receive whole-check events. Custom registered sinks may opt in by
169+ implementing ` agent_control_telemetry.ControlCheckEventSink ` in addition to their
170+ existing ` write_events ` method. Control-only sinks remain compatible. Sink
171+ failures never replace a result, exception or cancellation.
172+
173+ An elapsed value is neither an average nor the maximum of child controls.
174+ Several checks may overlap in a session, so summing their elapsed values does
175+ not establish session wall time or a counterfactual "agent overhead".
0 commit comments