返回 DeepSeek-Reasonix
session_context.go
根目录 / internal / agent / session_context.go
1 package agent
2
3 import (
4 "context"
5 "slices"
6
7 "reasonix/internal/event"
8 "reasonix/internal/provider"
9 "reasonix/internal/sessioncontext"
10 )
11
12 // TurnContextBundle carries role-specific runtime snapshots without changing
13 // Runner.Run. BootstrapOnly lets synthetic continuations repair an upgraded
14 // legacy session that has no snapshot, while never consuming later updates.
15 type TurnContextBundle struct {
16 Executor sessioncontext.Snapshot
17 Planner sessioncontext.Snapshot
18 BootstrapOnly bool
19 }
20
21 type turnContextBundleKey struct{}
22 type turnContextRoleKey struct{}
23
24 type turnContextRole uint8
25
26 const (
27 turnContextExecutor turnContextRole = iota
28 turnContextPlanner
29 )
30
31 type turnContextDiagnostics struct {
32 snapshot sessioncontext.Snapshot
33 stats sessioncontext.Diagnostics
34 target string
35 reasons []string
36 }
37
38 // WithTurnContextBundle attaches provider-visible runtime context to one host
39 // turn. Empty bundles preserve the old context identity and request fast path.
40 func WithTurnContextBundle(ctx context.Context, bundle TurnContextBundle) context.Context {
41 if ctx == nil {
42 ctx = context.Background()
43 }
44 if bundle.Executor.Content == "" && bundle.Planner.Content == "" {
45 return ctx
46 }
47 return context.WithValue(ctx, turnContextBundleKey{}, bundle)
48 }
49
50 // WithoutTurnContextBundle prevents a child agent from inheriting the parent's
51 // provider-visible runtime snapshot through context.Context. Child sessions
52 // receive only the explicit task/workspace context assembled by their caller;
53 // fork/continue transcript inheritance remains an explicit session operation.
54 func WithoutTurnContextBundle(ctx context.Context) context.Context {
55 if ctx == nil {
56 ctx = context.Background()
57 }
58 return context.WithValue(ctx, turnContextBundleKey{}, struct{}{})
59 }
60
61 func (a *Agent) prepareProviderTurn(ctx context.Context, input string) string {
62 return a.withTurnPreferences(input)
63 }
64
65 func withPlannerTurnContext(ctx context.Context) context.Context {
66 if ctx == nil {
67 ctx = context.Background()
68 }
69 return context.WithValue(ctx, turnContextRoleKey{}, turnContextPlanner)
70 }
71
72 func turnContextFromContext(ctx context.Context) (sessioncontext.Snapshot, bool, turnContextRole) {
73 if ctx == nil {
74 return sessioncontext.Snapshot{}, false, turnContextExecutor
75 }
76 bundle, ok := ctx.Value(turnContextBundleKey{}).(TurnContextBundle)
77 if !ok {
78 return sessioncontext.Snapshot{}, false, turnContextExecutor
79 }
80 role, _ := ctx.Value(turnContextRoleKey{}).(turnContextRole)
81 snapshot := bundle.Executor
82 if role == turnContextPlanner {
83 snapshot = bundle.Planner
84 }
85 if snapshot.Content == "" {
86 return sessioncontext.Snapshot{}, false, role
87 }
88 parsed, valid := sessioncontext.Parse(snapshot.Content)
89 if !valid || parsed.Digest != snapshot.Digest {
90 return sessioncontext.Snapshot{}, false, role
91 }
92 return parsed, bundle.BootstrapOnly, role
93 }
94
95 // AppendTurnContext appends the role-appropriate snapshot exactly once per
96 // digest. It derives state from model-visible history so resume, rewind, fork,
97 // and compaction need no persisted sidecar field.
98 func (a *Agent) AppendTurnContext(ctx context.Context) bool {
99 return a.appendTurnContextAndMessages(ctx)
100 }
101
102 // AppendTurnContextAndUser appends a role-appropriate snapshot and the real
103 // user message in one Session.AddBatch. This keeps mid-turn autosave from
104 // persisting a context-only admission boundary.
105 func (a *Agent) AppendTurnContextAndUser(ctx context.Context, user provider.Message) bool {
106 appended, _ := a.AppendTurnContextAndUserChecked(ctx, user)
107 return appended
108 }
109
110 // AppendTurnContextAndUserChecked is the controller-facing admission path. It
111 // reports event-store failures before the legacy transcript is changed.
112 func (a *Agent) AppendTurnContextAndUserChecked(ctx context.Context, user provider.Message) (bool, error) {
113 if a == nil || a.sess.session() == nil {
114 return false, nil
115 }
116 if user.ID == "" {
117 user.ID = turnUserMessageID(ctx, a.sess.session())
118 }
119 appendedContext, err := a.appendTurnContextAndMessagesChecked(ctx, user)
120 if err != nil {
121 return false, err
122 }
123 emitAdmittedUserMessage(a.svc.sink, user)
124 return appendedContext, nil
125 }
126
127 func (a *Agent) appendTurnContextAndMessages(ctx context.Context, messages ...provider.Message) bool {
128 appended, _ := a.appendTurnContextAndMessagesChecked(ctx, messages...)
129 return appended
130 }
131
132 func (a *Agent) appendTurnContextAndMessagesChecked(ctx context.Context, messages ...provider.Message) (bool, error) {
133 if a == nil {
134 return false, nil
135 }
136 sess := a.sess.session()
137 if sess == nil {
138 return false, nil
139 }
140 contextMessage, appendContext := a.prepareTurnContext(ctx)
141 if !appendContext && len(messages) == 0 {
142 return false, nil
143 }
144 batch := make([]provider.Message, 0, len(messages)+1)
145 if appendContext {
146 batch = append(batch, contextMessage)
147 }
148 batch = append(batch, messages...)
149 if err := a.appendCommittedMessages(ctx, "turn-context-and-user", batch...); err != nil {
150 return false, err
151 }
152 return appendContext, nil
153 }
154
155 func (a *Agent) prepareTurnContext(ctx context.Context) (provider.Message, bool) {
156 if a == nil {
157 return provider.Message{}, false
158 }
159 sess := a.sess.session()
160 if sess == nil {
161 return provider.Message{}, false
162 }
163 snapshot, bootstrapOnly, role := turnContextFromContext(ctx)
164 if snapshot.Content == "" {
165 return provider.Message{}, false
166 }
167 visible := a.modelVisibleMessages()
168 previous, found := latestTurnContextSnapshot(visible)
169 a.turn.sessionContext = turnContextDiagnostics{
170 snapshot: snapshot,
171 stats: sessioncontext.SectionDiagnostics(snapshot),
172 target: role.String(),
173 }
174 if found {
175 if bootstrapOnly || previous.Digest == snapshot.Digest {
176 return provider.Message{}, false
177 }
178 a.turn.sessionContext.reasons = changedTurnContextReasons(previous, snapshot)
179 } else {
180 reason := "first_seen"
181 if bootstrapOnly || hasPriorConversation(visible) {
182 reason = "legacy_resume"
183 }
184 a.turn.sessionContext.reasons = []string{reason}
185 }
186 return HostGeneratedUserMessage(snapshot.Content), true
187 }
188
189 func latestTurnContextSnapshot(messages []provider.Message) (sessioncontext.Snapshot, bool) {
190 for i := range slices.Backward(messages) {
191 message := messages[i]
192 if message.Role != provider.RoleUser || message.Origin != provider.MessageOriginHost {
193 continue
194 }
195 if snapshot, ok := sessioncontext.Parse(message.Content); ok {
196 return snapshot, true
197 }
198 }
199 return sessioncontext.Snapshot{}, false
200 }
201
202 func isSessionContextMessage(message provider.Message) bool {
203 if message.Role != provider.RoleUser || message.Origin != provider.MessageOriginHost {
204 return false
205 }
206 return sessioncontext.IsContent(message.Content)
207 }
208
209 func (r turnContextRole) String() string {
210 if r == turnContextPlanner {
211 return "planner"
212 }
213 return "executor"
214 }
215
216 func hasPriorConversation(messages []provider.Message) bool {
217 for _, message := range messages {
218 if message.Role != provider.RoleSystem && !message.LocalOnly {
219 return true
220 }
221 }
222 return false
223 }
224
225 func changedTurnContextReasons(previous, current sessioncontext.Snapshot) []string {
226 var reasons []string
227 if previous.Sections.Environment != current.Sections.Environment || previous.Sections.Workspace != current.Sections.Workspace {
228 reasons = append(reasons, "runtime_changed")
229 }
230 if previous.Sections.BackgroundMemory != current.Sections.BackgroundMemory {
231 reasons = append(reasons, "memory_changed")
232 }
233 if previous.Sections.SkillsCatalog != current.Sections.SkillsCatalog {
234 reasons = append(reasons, "skills_changed")
235 }
236 if len(reasons) == 0 {
237 reasons = append(reasons, "snapshot_changed")
238 }
239 return reasons
240 }
241
242 func (a *Agent) attachSessionContextDiagnostics(diagnostics *CacheDiagnostics) {
243 if a == nil || diagnostics == nil || a.turn.sessionContext.snapshot.Content == "" {
244 return
245 }
246 diagnostics.SessionContext = eventSessionContextDiagnostics(a.turn.sessionContext)
247 }
248
249 func eventSessionContextDiagnostics(observed turnContextDiagnostics) *event.SessionContextDiagnostics {
250 if observed.snapshot.Content == "" {
251 return nil
252 }
253 section := func(stat sessioncontext.SectionStat) event.SessionContextSectionDiagnostics {
254 return event.SessionContextSectionDiagnostics{Digest: stat.Digest, Chars: stat.Chars}
255 }
256 return &event.SessionContextDiagnostics{
257 Version: observed.snapshot.Version, Digest: observed.snapshot.Digest,
258 TargetRole: observed.target, Reasons: append([]string(nil), observed.reasons...),
259 Environment: section(observed.stats.Environment), Workspace: section(observed.stats.Workspace),
260 BackgroundMemory: section(observed.stats.BackgroundMemory), SkillsCatalog: section(observed.stats.SkillsCatalog),
261 }
262 }
263
264 func captureTurnContextShape(system string, schemas []provider.ToolSchema, rewriteVersion int, messages []provider.Message) PrefixShape {
265 shape := CaptureShape(system, schemas, rewriteVersion)
266 if snapshot, ok := latestTurnContextSnapshot(messages); ok {
267 shape.SessionContextDigest = snapshot.Digest
268 shape.PrefixHash = shortHash(map[string]string{
269 "stable": shape.PrefixHash, "session_context": snapshot.Digest,
270 })
271 }
272 return shape
273 }
274
274 lines GO