返回 DeepSeek-Reasonix
context_command.go
根目录 / internal / control / context_command.go
1 package control
2
3 import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "reasonix/internal/agent"
9 )
10
11 // ContextReport renders the current context state as a one-line summary plus
12 // detail lines. The window is the denominator of every compaction decision, so
13 // a wrong one looks exactly like a full context until the number is visible.
14 func (c *Controller) ContextReport() (summary, detail string) {
15 if c.executor == nil {
16 return "context: unavailable", ""
17 }
18 return renderContextReport(c.executor.ContextReport())
19 }
20
21 // runSessionVerb runs a session mutation off the dispatch path and reports the
22 // outcome, so the slash switch holds routing rather than goroutine plumbing.
23 func (c *Controller) runSessionVerb(run func() error, done, failPrefix string) {
24 go func() {
25 if err := run(); err != nil {
26 c.notice(failPrefix + err.Error())
27 return
28 }
29 c.notice(done)
30 }()
31 }
32
33 func renderContextReport(rep agent.ContextReport) (summary, detail string) {
34 if rep.Window <= 0 {
35 return "context: maintenance disabled (context_window = 0)",
36 fmt.Sprintf("%-18s%s\n%-18s%s", "latest prompt",
37 thousands(rep.LatestPrompt), "canonical", thousands(rep.CanonicalTokens))
38 }
39
40 var b strings.Builder
41 line := func(label string, value string) {
42 fmt.Fprintf(&b, "%-18s%s\n", label, value)
43 }
44 hard := ""
45 if rep.HardCeiling > 0 && rep.HardCeiling < rep.Window {
46 hard = fmt.Sprintf(" (usable %s after output reserve)", thousands(rep.HardCeiling))
47 }
48 line("window", thousands(rep.Window)+hard)
49 line("latest prompt", fmt.Sprintf("%s (%s of window)", thousands(rep.LatestPrompt), percentOf(rep.LatestPrompt, rep.Window)))
50 line("canonical", thousands(rep.CanonicalTokens))
51 visible := thousands(rep.ProjectionTokens)
52 if rep.Projected {
53 visible += " (projected)"
54 }
55 line("model-visible", visible)
56 line("thresholds", fmt.Sprintf("snip %s · fold %s · force %s",
57 thousands(rep.SnipThreshold), thousands(rep.FoldThreshold), thousands(rep.ForceThreshold)))
58 if rep.LastMode != "" {
59 last := rep.LastMode
60 if rep.LastTrigger != "" {
61 last += " · " + rep.LastTrigger
62 }
63 if rep.LastSource > 0 && rep.LastResult > 0 {
64 last += fmt.Sprintf(" · %s -> %s", thousands(rep.LastSource), thousands(rep.LastResult))
65 }
66 line("last maintenance", last)
67 } else {
68 line("last maintenance", "none this session")
69 }
70 if rep.CacheState != "" {
71 line("cache", rep.CacheState)
72 }
73 if rep.BlockedReason != "" {
74 line("blocked", rep.BlockedReason)
75 }
76 return contextSummaryLine(rep), strings.TrimRight(b.String(), "\n")
77 }
78
79 // contextSummaryLine names the next thing that will happen, since "70% full" on
80 // its own does not say whether that is close to anything.
81 func contextSummaryLine(rep agent.ContextReport) string {
82 next := "fold at " + percentOf(rep.FoldThreshold, rep.Window)
83 switch {
84 case rep.LatestPrompt >= rep.ForceThreshold:
85 next = "at the force threshold"
86 case rep.LatestPrompt >= rep.FoldThreshold:
87 next = "folding"
88 case rep.LatestPrompt >= rep.SnipThreshold:
89 next = "snipping stale tool results"
90 }
91 s := fmt.Sprintf("context %s / %s (%s) · next: %s",
92 thousands(rep.LatestPrompt), thousands(rep.Window),
93 percentOf(rep.LatestPrompt, rep.Window), next)
94 if rep.BlockedReason != "" {
95 s += " · maintenance blocked"
96 }
97 return s
98 }
99
100 func percentOf(n, total int) string {
101 if total <= 0 {
102 return "n/a"
103 }
104 return strconv.Itoa((n*100+total/2)/total) + "%"
105 }
106
107 // thousands groups digits so a six-digit token count is readable at a glance.
108 func thousands(n int) string {
109 s := strconv.Itoa(n)
110 neg := strings.HasPrefix(s, "-")
111 s = strings.TrimPrefix(s, "-")
112 for i := len(s) - 3; i > 0; i -= 3 {
113 s = s[:i] + "," + s[i:]
114 }
115 if neg {
116 return "-" + s
117 }
118 return s
119 }
120
120 lines GO