返回 DeepSeek-Reasonix
audit.go
根目录 / internal / capability / audit.go
1 package capability
2
3 import (
4 "maps"
5 "sync"
6 )
7
8 // Audit is a non-persisted capability/routing counters sink, mirroring
9 // readiness audit collection for run --metrics and e2ebench.
10 type Audit struct {
11 mu sync.Mutex
12
13 Routes int
14 RoutedCandidates int
15 RoutedRequire int
16 RoutedPrefer int
17 RoutedSuggest int
18 Declines int
19 SemanticRoutes int
20 SemanticFallbacks int
21 RequireMissing int
22 RequireRecovered int
23 PreferMissing int
24 PreferRecovered int
25 SkillInvocations int
26 SkillFailures int
27 SkillUnavailable int
28 MCPInspect int
29 MCPCall int
30 MCPCallFailures int
31 ReviewBlocks int
32 SecurityReviewBlocks int
33 RouterPromptTokens int
34 RouterCompletionTokens int
35 RouterCost float64
36 RouterLatencyMs int64
37 Discovery DiscoveryAudit
38 Arguments ArgumentAudit
39 LoopGuard LoopGuardAudit
40 MCPLists MCPListAudit
41 ToolExec ToolExecAudit
42 Phases PhaseAudit
43 }
44
45 // DiscoveryAudit counts model list/search/inspect actions, not MCP network.
46 type DiscoveryAudit struct {
47 Lists, Searches, Inspects int
48 ResultCount, ResultBytes int
49 NetworkCalls int
50 }
51
52 // ArgumentAudit is host-side schema validation without argument values.
53 type ArgumentAudit struct {
54 Validations, Fail, Skip, RemoteDispatch int
55 }
56
57 // LoopGuardAudit retains the metrics wire layout. RepeatFailures and
58 // BlockedCalls are retired schema-guard counters; new runs leave them zero.
59 type LoopGuardAudit struct {
60 RepeatFailures, RepeatClarifications, SoftBudgetNudges int
61 BlockedCalls int
62 }
63
64 // MCPListAudit distinguishes shared-host, disk-cache, and remote tools/list.
65 type MCPListAudit struct {
66 SharedHost, DiskCache, Remote int
67 DurationMs int64
68 ToolCount, SchemaBytes int
69 Triggers map[string]int `json:"triggers,omitempty"`
70 }
71
72 // ToolExecAudit records classified tool execution without payloads.
73 type ToolExecAudit struct {
74 Calls, ReadOnly, Parallel int
75 QueueMs, ExecMs int64
76 RawBytes, VisibleBytes int
77 }
78
79 // PhaseAudit is content-free time spent in host phases.
80 type PhaseAudit struct {
81 ProviderWaitMs, ToolExecMs, SubagentWaitMs int64
82 UserWaitMs, CompactMs, ReviewMs int64
83 }
84
85 // RecordCapabilityDiscovery distinguishes model discovery actions from actual
86 // MCP network traffic. action is list, search, or inspect.
87 func (a *Audit) RecordCapabilityDiscovery(action string, results, bytes int, network bool) {
88 if a == nil {
89 return
90 }
91 a.mu.Lock()
92 defer a.mu.Unlock()
93 switch action {
94 case "list":
95 a.Discovery.Lists++
96 case "search":
97 a.Discovery.Searches++
98 case "inspect":
99 a.Discovery.Inspects++
100 }
101 a.Discovery.ResultCount += results
102 a.Discovery.ResultBytes += bytes
103 if network {
104 a.Discovery.NetworkCalls++
105 }
106 }
107
108 // RecordArgumentValidation records host validation without argument values.
109 // remoteDispatched must stay false for validation failures.
110 func (a *Audit) RecordArgumentValidation(failed, skipped, remoteDispatched bool) {
111 if a == nil {
112 return
113 }
114 a.mu.Lock()
115 defer a.mu.Unlock()
116 a.Arguments.Validations++
117 if failed {
118 a.Arguments.Fail++
119 }
120 if skipped {
121 a.Arguments.Skip++
122 }
123 if remoteDispatched {
124 a.Arguments.RemoteDispatch++
125 }
126 }
127
128 // RecordRemoteDispatch marks that a host-validated call reached tools/call.
129 func (a *Audit) RecordRemoteDispatch() {
130 if a == nil {
131 return
132 }
133 a.mu.Lock()
134 a.Arguments.RemoteDispatch++
135 a.mu.Unlock()
136 }
137
138 // RecordMCPList records one tools/list observation by source and host trigger.
139 func (a *Audit) RecordMCPList(source, trigger string, durationMs int64, toolCount, schemaBytes int) {
140 if a == nil {
141 return
142 }
143 a.mu.Lock()
144 defer a.mu.Unlock()
145 switch source {
146 case "shared_host":
147 a.MCPLists.SharedHost++
148 case "disk_cache":
149 a.MCPLists.DiskCache++
150 case "remote":
151 a.MCPLists.Remote++
152 }
153 a.MCPLists.DurationMs += durationMs
154 a.MCPLists.ToolCount += toolCount
155 a.MCPLists.SchemaBytes += schemaBytes
156 if trigger != "" {
157 if a.MCPLists.Triggers == nil {
158 a.MCPLists.Triggers = map[string]int{}
159 }
160 a.MCPLists.Triggers[trigger]++
161 }
162 }
163
164 // RecordLoopGuard records a host loop-guard action without payloads.
165 func (a *Audit) RecordLoopGuard(kind string) {
166 if a == nil {
167 return
168 }
169 a.mu.Lock()
170 defer a.mu.Unlock()
171 switch kind {
172 case "repeat_clarification":
173 a.LoopGuard.RepeatClarifications++
174 case "soft_budget":
175 a.LoopGuard.SoftBudgetNudges++
176 }
177 }
178
179 // RecordToolExecution records one classified tool run without arguments.
180 func (a *Audit) RecordToolExecution(readOnly, parallel bool, queueMs, execMs int64, rawBytes, visibleBytes int) {
181 if a == nil {
182 return
183 }
184 a.mu.Lock()
185 defer a.mu.Unlock()
186 a.ToolExec.Calls++
187 if readOnly {
188 a.ToolExec.ReadOnly++
189 }
190 if parallel {
191 a.ToolExec.Parallel++
192 }
193 a.ToolExec.QueueMs += queueMs
194 a.ToolExec.ExecMs += execMs
195 a.ToolExec.RawBytes += rawBytes
196 a.ToolExec.VisibleBytes += visibleBytes
197 }
198
199 // RecordPhaseMs accumulates content-free phase durations.
200 func (a *Audit) RecordPhaseMs(phase string, ms int64) {
201 if a == nil || ms <= 0 {
202 return
203 }
204 a.mu.Lock()
205 defer a.mu.Unlock()
206 switch phase {
207 case "provider":
208 a.Phases.ProviderWaitMs += ms
209 case "tool":
210 a.Phases.ToolExecMs += ms
211 case "subagent":
212 a.Phases.SubagentWaitMs += ms
213 case "user":
214 a.Phases.UserWaitMs += ms
215 case "compact":
216 a.Phases.CompactMs += ms
217 case "review":
218 a.Phases.ReviewMs += ms
219 }
220 }
221
222 // RecordDecision captures the route-to-invocation funnel before the model acts.
223 func (a *Audit) RecordDecision(decision RouteDecision) {
224 if a == nil {
225 return
226 }
227 a.mu.Lock()
228 defer a.mu.Unlock()
229 for _, candidate := range decision.Candidates {
230 a.RoutedCandidates++
231 switch candidate.Policy {
232 case AutoUseRequire:
233 a.RoutedRequire++
234 case AutoUsePrefer:
235 a.RoutedPrefer++
236 case AutoUseSuggest:
237 a.RoutedSuggest++
238 }
239 }
240 }
241
242 // RecordDecline counts an explicit model decision not to use a preferred route.
243 func (a *Audit) RecordDecline() {
244 if a == nil {
245 return
246 }
247 a.mu.Lock()
248 a.Declines++
249 a.mu.Unlock()
250 }
251
252 // RecordRoute increments deterministic/hybrid route counts.
253 func (a *Audit) RecordRoute(semantic, fallback bool) {
254 if a == nil {
255 return
256 }
257 a.mu.Lock()
258 defer a.mu.Unlock()
259 a.Routes++
260 if semantic {
261 a.SemanticRoutes++
262 }
263 if fallback {
264 a.SemanticFallbacks++
265 }
266 }
267
268 // RecordGate records require/prefer missing and recovery.
269 func (a *Audit) RecordGate(requireMissing, preferMissing, recovered bool) {
270 if a == nil {
271 return
272 }
273 a.mu.Lock()
274 defer a.mu.Unlock()
275 if requireMissing {
276 a.RequireMissing++
277 }
278 if preferMissing {
279 a.PreferMissing++
280 }
281 if recovered {
282 if requireMissing {
283 a.RequireRecovered++
284 }
285 if preferMissing {
286 a.PreferRecovered++
287 }
288 }
289 }
290
291 // RecordSkill records skill invocation outcomes.
292 func (a *Audit) RecordSkill(failed, unavailable bool) {
293 if a == nil {
294 return
295 }
296 a.mu.Lock()
297 defer a.mu.Unlock()
298 a.SkillInvocations++
299 if failed {
300 a.SkillFailures++
301 }
302 if unavailable {
303 a.SkillUnavailable++
304 }
305 }
306
307 // RecordMCPProxy records use_capability proxy activity.
308 func (a *Audit) RecordMCPProxy(inspect, call, failed bool) {
309 if a == nil {
310 return
311 }
312 a.mu.Lock()
313 defer a.mu.Unlock()
314 if inspect {
315 a.MCPInspect++
316 }
317 if call {
318 a.MCPCall++
319 }
320 if failed {
321 a.MCPCallFailures++
322 }
323 }
324
325 // RecordGateRecovery records that gate kinds which missed earlier in the turn
326 // later passed cleanly — the capability was actually invoked after the nudge.
327 // Kept separate from RecordGate so a recovery never double-counts as a miss.
328 func (a *Audit) RecordGateRecovery(require, prefer bool) {
329 if a == nil {
330 return
331 }
332 a.mu.Lock()
333 defer a.mu.Unlock()
334 if require {
335 a.RequireRecovered++
336 }
337 if prefer {
338 a.PreferRecovered++
339 }
340 }
341
342 // RecordRouterUsage accumulates the semantic router's own model spend:
343 // prompt/completion tokens, priced cost, and wall-clock latency per call.
344 func (a *Audit) RecordRouterUsage(promptTokens, completionTokens int, cost float64, latencyMs int64) {
345 if a == nil {
346 return
347 }
348 a.mu.Lock()
349 defer a.mu.Unlock()
350 a.RouterPromptTokens += promptTokens
351 a.RouterCompletionTokens += completionTokens
352 a.RouterCost += cost
353 a.RouterLatencyMs += latencyMs
354 }
355
356 // RecordReviewBlock records blocking structured review outcomes.
357 func (a *Audit) RecordReviewBlock(security bool) {
358 if a == nil {
359 return
360 }
361 a.mu.Lock()
362 defer a.mu.Unlock()
363 if security {
364 a.SecurityReviewBlocks++
365 } else {
366 a.ReviewBlocks++
367 }
368 }
369
370 // Snapshot returns a copy of counters for metrics export.
371 func (a *Audit) Snapshot() Audit {
372 if a == nil {
373 return Audit{}
374 }
375 a.mu.Lock()
376 defer a.mu.Unlock()
377 return Audit{
378 Routes: a.Routes,
379 RoutedCandidates: a.RoutedCandidates,
380 RoutedRequire: a.RoutedRequire,
381 RoutedPrefer: a.RoutedPrefer,
382 RoutedSuggest: a.RoutedSuggest,
383 Declines: a.Declines,
384 SemanticRoutes: a.SemanticRoutes,
385 SemanticFallbacks: a.SemanticFallbacks,
386 RequireMissing: a.RequireMissing,
387 RequireRecovered: a.RequireRecovered,
388 PreferMissing: a.PreferMissing,
389 PreferRecovered: a.PreferRecovered,
390 SkillInvocations: a.SkillInvocations,
391 SkillFailures: a.SkillFailures,
392 SkillUnavailable: a.SkillUnavailable,
393 MCPInspect: a.MCPInspect,
394 MCPCall: a.MCPCall,
395 MCPCallFailures: a.MCPCallFailures,
396 Discovery: a.Discovery,
397 Arguments: a.Arguments,
398 LoopGuard: a.LoopGuard,
399 MCPLists: cloneMCPListAudit(a.MCPLists),
400 ToolExec: a.ToolExec,
401 Phases: a.Phases,
402 ReviewBlocks: a.ReviewBlocks,
403 SecurityReviewBlocks: a.SecurityReviewBlocks,
404 RouterPromptTokens: a.RouterPromptTokens,
405 RouterCompletionTokens: a.RouterCompletionTokens,
406 RouterCost: a.RouterCost,
407 RouterLatencyMs: a.RouterLatencyMs,
408 }
409 }
410
411 func cloneMCPListAudit(in MCPListAudit) MCPListAudit {
412 out := in
413 if len(in.Triggers) > 0 {
414 out.Triggers = make(map[string]int, len(in.Triggers))
415 maps.Copy(out.Triggers, in.Triggers)
416 }
417 return out
418 }
419
419 lines GO