返回 DeepSeek-Reasonix
session_prompt.go
根目录 / desktop / session_prompt.go
1 package main
2
3 import (
4 "fmt"
5 "log/slog"
6 "os"
7 "strings"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/control"
11 "reasonix/internal/provider"
12 "reasonix/internal/store"
13 )
14
15 func systemPromptFrom(messages []provider.Message) string {
16 for _, m := range messages {
17 if m.Role == provider.RoleSystem {
18 return m.Content
19 }
20 }
21 return ""
22 }
23
24 // logSystemPromptSwap leaves a trace whenever a resume/rebind replaces a
25 // conversation's persisted system prompt with different bytes: that swap
26 // invalidates the whole conversation's provider prefix cache (misses bill at
27 // 10x hits) and persists the rewrite. With probe snapshots keeping composition
28 // deterministic, this should fire only on genuine config changes — if it shows
29 // up in field logs without one, a new nondeterminism source crept into the
30 // prompt assembly.
31 func logSystemPromptSwap(persisted, fresh, path string) {
32 if persisted == "" || fresh == "" || persisted == fresh {
33 return
34 }
35 slog.Warn("desktop: resume swapped a differing system prompt; conversation prefix cache will miss",
36 "path", path, "persisted_len", len(persisted), "fresh_len", len(fresh))
37 }
38
39 func withFreshSystemPrompt(messages []provider.Message, system string) []provider.Message {
40 if strings.TrimSpace(system) == "" {
41 return messages
42 }
43 out := append([]provider.Message(nil), messages...)
44 for i := range out {
45 if out[i].Role == provider.RoleSystem {
46 out[i].Content = system
47 out[i].ReasoningContent = ""
48 out[i].ReasoningSignature = ""
49 out[i].ToolCalls = nil
50 out[i].ToolCallID = ""
51 out[i].Name = ""
52 return out
53 }
54 }
55 return append([]provider.Message{{Role: provider.RoleSystem, Content: system}}, out...)
56 }
57
58 func noteLegacyPinnedSystemMigration(session *agent.Session, persisted, fresh string) {
59 if session == nil || persisted == "" || fresh == "" || persisted == fresh {
60 return
61 }
62 // The resumed Session already contains the refreshed bytes, so this is only a
63 // diagnostics boundary. Do not increment RewriteVersion: the persistence
64 // baseline must remain compatible with the session loaded from disk.
65 session.NoteContentRewrite("legacy_pinned_system_migration")
66 }
67
68 func sessionWithFreshSystemPrompt(session *agent.Session, system string) *agent.Session {
69 if session == nil {
70 return nil
71 }
72 messages := session.Snapshot()
73 persisted := systemPromptFrom(messages)
74 if persisted == "" && strings.TrimSpace(system) == "" {
75 return session
76 }
77 logSystemPromptSwap(persisted, system, "")
78 resumed := session.CloneWithMessages(withFreshSystemPrompt(messages, system))
79 noteLegacyPinnedSystemMigration(resumed, persisted, system)
80 return resumed
81 }
82
83 func resumeWithFreshSystemPrompt(ctrl interface {
84 History() []provider.Message
85 Resume(*agent.Session, string)
86 SetSessionPath(string)
87 }, messages []provider.Message, path string) {
88 if ctrl == nil {
89 return
90 }
91 if len(messages) > 0 {
92 fresh := systemPromptFrom(ctrl.History())
93 persisted := systemPromptFrom(messages)
94 logSystemPromptSwap(persisted, fresh, path)
95 next := withFreshSystemPrompt(messages, fresh)
96 if path != "" {
97 if loaded, err := agent.LoadSession(path); err == nil && loaded != nil {
98 if resumed, ok := loaded.CloneWithMessagesIfCompatible(next); ok {
99 noteLegacyPinnedSystemMigration(resumed, persisted, fresh)
100 ctrl.Resume(resumed, path)
101 return
102 }
103 }
104 }
105 resumed := agent.NewSession("").CloneWithMessages(next)
106 noteLegacyPinnedSystemMigration(resumed, persisted, fresh)
107 ctrl.Resume(resumed, path)
108 return
109 }
110 if path != "" {
111 ctrl.SetSessionPath(path)
112 }
113 }
114
115 // resumeWithFreshSystemPromptAndGoal resumes an existing session without
116 // seeding Goal state before Resume. A goal-state sidecar is authoritative;
117 // only legacy sessions that predate the sidecar fall back to the tab profile.
118 func resumeWithFreshSystemPromptAndGoal(ctrl control.SessionAPI, messages []provider.Message, path, legacyGoal string) {
119 if ctrl == nil {
120 return
121 }
122 _, sidecarErr := os.Stat(store.SessionGoalState(path))
123 resumeWithFreshSystemPrompt(ctrl, messages, path)
124 if os.IsNotExist(sidecarErr) && strings.TrimSpace(legacyGoal) != "" {
125 if loader, ok := ctrl.(interface{ LoadInactiveGoal(string) }); ok {
126 loader.LoadInactiveGoal(strings.TrimSpace(legacyGoal))
127 }
128 }
129 }
130
131 func resumeLoadedSessionAndGoal(ctrl control.SessionAPI, session *agent.Session, path, legacyGoal string) {
132 if ctrl == nil || session == nil {
133 return
134 }
135 _, sidecarErr := os.Stat(store.SessionGoalState(path))
136 ctrl.Resume(sessionWithFreshSystemPrompt(session, systemPromptFrom(ctrl.History())), path)
137 if os.IsNotExist(sidecarErr) && strings.TrimSpace(legacyGoal) != "" {
138 if loader, ok := ctrl.(interface{ LoadInactiveGoal(string) }); ok {
139 loader.LoadInactiveGoal(strings.TrimSpace(legacyGoal))
140 }
141 }
142 }
143
144 // configureControllerRuntime applies the non-persisted runtime posture before
145 // Resume. Session grants are copied before a lease is acquired so a replacement
146 // is fully configured but cannot run against the session until ownership is
147 // established.
148 func configureControllerRuntime(ctrl, oldCtrl control.SessionAPI, runtime normalizedTabRuntime) {
149 if ctrl == nil {
150 return
151 }
152 ctrl.EnableInteractiveApproval()
153 applyTabModeToController(ctrl, runtime.tabMode())
154 applyTabToolApprovalModeToController(ctrl, runtime.toolApprovalMode)
155 applyTabQualityFloorToController(ctrl, runtime.qualityFloor)
156 if next, ok := ctrl.(*control.Controller); ok {
157 if prev, ok := oldCtrl.(*control.Controller); ok {
158 next.RestoreSessionAuthorizations(prev.SessionAuthorizations())
159 }
160 }
161 }
162
163 func normalizeRestoredControllerRuntime(ctrl control.SessionAPI, requested normalizedTabRuntime) (normalizedTabRuntime, error) {
164 if ctrl == nil {
165 return normalizedTabRuntime{}, fmt.Errorf("replacement controller is nil")
166 }
167 plan := requested.collaborationMode == "plan"
168 ctrl.SetPlanMode(plan)
169 applyTabToolApprovalModeToController(ctrl, requested.toolApprovalMode)
170 if plan && ctrl.GoalStatus() == control.GoalStatusRunning {
171 // Explicit Plan wins over inconsistent legacy data. Clearing the running
172 // Goal also prevents a stale scope from being executed after approval.
173 if err := ctrl.SetGoalDurable(""); err != nil {
174 return normalizedTabRuntime{}, fmt.Errorf("clear goal for Plan mode: %w", err)
175 }
176 }
177
178 actual := requested
179 actual.collaborationMode = "normal"
180 actual.legacyGoal = ""
181 switch {
182 case ctrl.PlanMode():
183 actual.collaborationMode = "plan"
184 case ctrl.GoalStatus() == control.GoalStatusRunning && strings.TrimSpace(ctrl.Goal()) != "":
185 actual.collaborationMode = "goal"
186 actual.legacyGoal = strings.TrimSpace(ctrl.Goal())
187 }
188 actual.toolApprovalMode = normalizeToolApprovalMode(ctrl.ToolApprovalMode())
189 if ctrl.PlanMode() != (actual.collaborationMode == "plan") {
190 return normalizedTabRuntime{}, fmt.Errorf("replacement collaboration mode validation failed")
191 }
192 if actual.toolApprovalMode != normalizeToolApprovalMode(requested.toolApprovalMode) {
193 return normalizedTabRuntime{}, fmt.Errorf("replacement tool approval mode = %q, want %q", actual.toolApprovalMode, requested.toolApprovalMode)
194 }
195 return actual, nil
196 }
197
198 func resumeControllerRuntimeWithMessages(ctrl control.SessionAPI, messages []provider.Message, path string, requested normalizedTabRuntime) (normalizedTabRuntime, error) {
199 resumeWithFreshSystemPromptAndGoal(ctrl, messages, path, requested.legacyGoal)
200 return normalizeRestoredControllerRuntime(ctrl, requested)
201 }
202
203 func resumeControllerRuntimeWithSession(ctrl control.SessionAPI, session *agent.Session, path string, requested normalizedTabRuntime) (normalizedTabRuntime, error) {
204 if session != nil {
205 resumeLoadedSessionAndGoal(ctrl, session, path, requested.legacyGoal)
206 } else {
207 resumeWithFreshSystemPromptAndGoal(ctrl, nil, path, requested.legacyGoal)
208 }
209 return normalizeRestoredControllerRuntime(ctrl, requested)
210 }
211
211 lines GO