返回 DeepSeek-Reasonix
cache_shape.go
根目录 / internal / agent / cache_shape.go
1 package agent
2
3 import (
4 "crypto/sha256"
5 "encoding/json"
6 "fmt"
7 "sort"
8
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 )
12
13 // PrefixShape hashes the portions of the request prefix that influence
14 // provider-side prompt-cache reuse. Comparing snapshots across turns
15 // lets us explain *why* a cache miss happened.
16 type PrefixShape struct {
17 SystemHash string
18 ToolsHash string
19 PrefixHash string
20 LogRewriteVersion int
21 ToolSchemaTokens int
22 }
23
24 // CacheDiagnostics is a type alias for event.CacheDiagnostics so the agent
25 // can construct and compare diagnostics without importing event itself in
26 // every call site, while still assigning to event.Event.CacheDiagnostics.
27 type CacheDiagnostics = event.CacheDiagnostics
28
29 func shortHash(v interface{}) string {
30 b, _ := json.Marshal(v)
31 h := sha256.Sum256(b)
32 return fmt.Sprintf("%x", h[:8])
33 }
34
35 // CaptureShape takes a snapshot of the current prefix state.
36 func CaptureShape(systemPrompt string, schemas []provider.ToolSchema, rewriteVersion int) PrefixShape {
37 normalizedSchemas := normalizeToolSchemas(schemas)
38 toolsJSON, _ := json.Marshal(normalizedSchemas)
39 return PrefixShape{
40 SystemHash: shortHash(systemPrompt),
41 ToolsHash: shortHash(string(toolsJSON)),
42 PrefixHash: shortHash(map[string]interface{}{
43 "system": systemPrompt,
44 "tools": string(toolsJSON),
45 }),
46 LogRewriteVersion: rewriteVersion,
47 ToolSchemaTokens: estimateTokens(string(toolsJSON)),
48 }
49 }
50
51 func normalizeToolSchemas(schemas []provider.ToolSchema) []provider.ToolSchema {
52 out := make([]provider.ToolSchema, len(schemas))
53 copy(out, schemas)
54 sort.Slice(out, func(i, j int) bool {
55 if out[i].Name != out[j].Name {
56 return out[i].Name < out[j].Name
57 }
58 if out[i].Description != out[j].Description {
59 return out[i].Description < out[j].Description
60 }
61 return string(out[i].Parameters) < string(out[j].Parameters)
62 })
63 return out
64 }
65
66 // CompareShape returns diagnostics describing what changed between two shapes.
67 // contentReasons is the set of provider-visible rewrite reasons (e.g.
68 // "compact_auto", "snip", "rewind_truncate") drained from the Session since
69 // prev was captured — see Session.DrainContentRewriteReasons. It is the sole
70 // source of rewrite-caused reasons: a bare LogRewriteVersion change with no
71 // drained reason means only local-only metadata was touched (a decision
72 // receipt, tool-call preview/resolution, or an Edited-message replace), which
73 // never reaches the provider and so must not be reported as a cache change.
74 func CompareShape(prev, cur PrefixShape, usage *provider.Usage, contentReasons []string) CacheDiagnostics {
75 reasons := []string{}
76 if prev.SystemHash != "" && prev.SystemHash != cur.SystemHash {
77 reasons = append(reasons, "system")
78 }
79 if prev.ToolsHash != "" && prev.ToolsHash != cur.ToolsHash {
80 reasons = append(reasons, "tools")
81 }
82 reasons = append(reasons, contentReasons...)
83 var miss, hit int
84 if usage != nil {
85 miss = usage.CacheMissTokens
86 hit = usage.CacheHitTokens
87 }
88 return CacheDiagnostics{
89 PrefixHash: cur.PrefixHash,
90 PrefixChanged: len(reasons) > 0,
91 PrefixChangeReasons: reasons,
92 SystemHash: cur.SystemHash,
93 ToolsHash: cur.ToolsHash,
94 LogRewriteVersion: cur.LogRewriteVersion,
95 ToolSchemaTokens: cur.ToolSchemaTokens,
96 CacheMissTokens: miss,
97 CacheHitTokens: hit,
98 }
99 }
100
101 // estimateTokens gives a rough token count from byte length.
102 // A proper tokenizer would be more accurate, but for diagnostic
103 // purposes a byte-based estimate is sufficient and zero-alloc.
104 func estimateTokens(s string) int {
105 // ~4 chars per token is a workable heuristic for code-heavy JSON.
106 if len(s) == 0 {
107 return 0
108 }
109 return len(s) / 4
110 }
111
112 // SchemaTokenCosts returns per-tool token cost estimates for display.
113 func SchemaTokenCosts(schemas []provider.ToolSchema) []ToolSchemaCost {
114 out := make([]ToolSchemaCost, 0, len(schemas))
115 for _, s := range schemas {
116 b, _ := json.Marshal(s)
117 out = append(out, ToolSchemaCost{Name: s.Name, Tokens: estimateTokens(string(b))})
118 }
119 return out
120 }
121
122 // ToolSchemaCost is a per-tool token cost estimate for diagnostic display.
123 type ToolSchemaCost struct {
124 Name string
125 Tokens int
126 }
127
127 lines GO