返回 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 sessionWithFreshSystemPrompt(session *agent.Session, system string) *agent.Session {
59 if session == nil {
60 return nil
61 }
62 messages := session.Snapshot()
63 persisted := systemPromptFrom(messages)
64 if persisted == "" && strings.TrimSpace(system) == "" {
65 return session
66 }
67 logSystemPromptSwap(persisted, system, "")
68 return session.CloneWithMessages(withFreshSystemPrompt(messages, system))
69 }
70
71 func resumeWithFreshSystemPrompt(ctrl interface {
72 History() []provider.Message
73 Resume(*agent.Session, string)
74 SetSessionPath(string)
75 }, messages []provider.Message, path string) {
76 if ctrl == nil {
77 return
78 }
79 if len(messages) > 0 {
80 fresh := systemPromptFrom(ctrl.History())
81 logSystemPromptSwap(systemPromptFrom(messages), fresh, path)
82 next := withFreshSystemPrompt(messages, fresh)
83 if path != "" {
84 if loaded, err := agent.LoadSession(path); err == nil && loaded != nil {
85 if resumed, ok := loaded.CloneWithMessagesIfCompatible(next); ok {
86 ctrl.Resume(resumed, path)
87 return
88 }
89 }
90 }
91 ctrl.Resume(agent.NewSession("").CloneWithMessages(next), path)
92 return
93 }
94 if path != "" {
95 ctrl.SetSessionPath(path)
96 }
97 }
98
99 // resumeWithFreshSystemPromptAndGoal resumes an existing session without
100 // seeding Goal state before Resume. A goal-state sidecar is authoritative;
101 // only legacy sessions that predate the sidecar fall back to the tab profile.
102 func resumeWithFreshSystemPromptAndGoal(ctrl control.SessionAPI, messages []provider.Message, path, legacyGoal string) {
103 if ctrl == nil {
104 return
105 }
106 _, sidecarErr := os.Stat(store.SessionGoalState(path))
107 resumeWithFreshSystemPrompt(ctrl, messages, path)
108 if os.IsNotExist(sidecarErr) && strings.TrimSpace(legacyGoal) != "" {
109 ctrl.SetGoal(strings.TrimSpace(legacyGoal))
110 }
111 }
112
113 func resumeLoadedSessionAndGoal(ctrl control.SessionAPI, session *agent.Session, path, legacyGoal string) {
114 if ctrl == nil || session == nil {
115 return
116 }
117 _, sidecarErr := os.Stat(store.SessionGoalState(path))
118 ctrl.Resume(sessionWithFreshSystemPrompt(session, systemPromptFrom(ctrl.History())), path)
119 if os.IsNotExist(sidecarErr) && strings.TrimSpace(legacyGoal) != "" {
120 ctrl.SetGoal(strings.TrimSpace(legacyGoal))
121 }
122 }
123
124 // configureControllerRuntime applies the non-persisted runtime posture before
125 // Resume. Session grants are copied before a lease is acquired so a replacement
126 // is fully configured but cannot run against the session until ownership is
127 // established.
128 func configureControllerRuntime(ctrl, oldCtrl control.SessionAPI, runtime normalizedTabRuntime) {
129 if ctrl == nil {
130 return
131 }
132 ctrl.EnableInteractiveApproval()
133 applyTabModeToController(ctrl, runtime.tabMode())
134 applyTabToolApprovalModeToController(ctrl, runtime.toolApprovalMode)
135 if next, ok := ctrl.(*control.Controller); ok {
136 if prev, ok := oldCtrl.(*control.Controller); ok {
137 next.RestoreSessionAuthorizations(prev.SessionAuthorizations())
138 }
139 }
140 }
141
142 func normalizeRestoredControllerRuntime(ctrl control.SessionAPI, requested normalizedTabRuntime) (normalizedTabRuntime, error) {
143 if ctrl == nil {
144 return normalizedTabRuntime{}, fmt.Errorf("replacement controller is nil")
145 }
146 plan := requested.collaborationMode == "plan"
147 ctrl.SetPlanMode(plan)
148 applyTabToolApprovalModeToController(ctrl, requested.toolApprovalMode)
149 if plan && ctrl.GoalStatus() == control.GoalStatusRunning {
150 // Explicit Plan wins over inconsistent legacy data. Clearing the running
151 // Goal also prevents a stale scope from being executed after approval.
152 ctrl.ClearGoal()
153 }
154
155 actual := requested
156 actual.collaborationMode = "normal"
157 actual.legacyGoal = ""
158 switch {
159 case ctrl.PlanMode():
160 actual.collaborationMode = "plan"
161 case ctrl.GoalStatus() == control.GoalStatusRunning && strings.TrimSpace(ctrl.Goal()) != "":
162 actual.collaborationMode = "goal"
163 actual.legacyGoal = strings.TrimSpace(ctrl.Goal())
164 }
165 actual.toolApprovalMode = normalizeToolApprovalMode(ctrl.ToolApprovalMode())
166 if ctrl.PlanMode() != (actual.collaborationMode == "plan") {
167 return normalizedTabRuntime{}, fmt.Errorf("replacement collaboration mode validation failed")
168 }
169 if actual.toolApprovalMode != normalizeToolApprovalMode(requested.toolApprovalMode) {
170 return normalizedTabRuntime{}, fmt.Errorf("replacement tool approval mode = %q, want %q", actual.toolApprovalMode, requested.toolApprovalMode)
171 }
172 return actual, nil
173 }
174
175 func resumeControllerRuntimeWithMessages(ctrl control.SessionAPI, messages []provider.Message, path string, requested normalizedTabRuntime) (normalizedTabRuntime, error) {
176 resumeWithFreshSystemPromptAndGoal(ctrl, messages, path, requested.legacyGoal)
177 return normalizeRestoredControllerRuntime(ctrl, requested)
178 }
179
180 func resumeControllerRuntimeWithSession(ctrl control.SessionAPI, session *agent.Session, path string, requested normalizedTabRuntime) (normalizedTabRuntime, error) {
181 if session != nil {
182 resumeLoadedSessionAndGoal(ctrl, session, path, requested.legacyGoal)
183 } else {
184 resumeWithFreshSystemPromptAndGoal(ctrl, nil, path, requested.legacyGoal)
185 }
186 return normalizeRestoredControllerRuntime(ctrl, requested)
187 }
188
188 lines GO