dynamic_context.go RawHistoryFolder
1package core
2
3import (
4 "strings"
5 "time"
6
7 "github.com/bnema/zut/packages/provider"
8)
9
10const internalContextMarker = "internal_context"
11
12// appendDynamicContext persists a provider-visible, host-authored context item
13// only when its canonical value changes. It is deliberately separate from the
14// stable top-level instructions so changing time or extension state cannot
15// rewrite the instruction cache prefix.
16func (a *Agent) appendDynamicContext(turnContext string) {
17 parts := make([]string, 0, 2)
18 if timeContext := a.providerTimeContext().developerText(); timeContext != "" {
19 parts = append(parts, timeContext)
20 }
21 if turnContext = boundedTurnContext(turnContext); turnContext != "" {
22 parts = append(parts, "[Extension context]\n"+turnContext)
23 }
24 text := strings.Join(parts, "\n\n")
25 if text == "" {
26 return
27 }
28
29 message := provider.Message{
30 Role: provider.RoleDeveloper,
31 Content: []provider.Content{provider.TextBlock{Text: text}},
32 Time: time.Now(),
33 Meta: map[string]string{internalContextMarker: "true"},
34 }
35
36 a.mu.Lock()
37 for i := len(a.messages) - 1; i >= 0; i-- {
38 if !isInternalContextMessage(a.messages[i]) {
39 continue
40 }
41 if internalContextText(a.messages[i]) == text {
42 a.mu.Unlock()
43 return
44 }
45 break
46 }
47 // The first dynamic item must precede the accepted user task. Persisting it
48 // as an append would reverse that order on session reload, so report the
49 // resulting transcript replacement instead. Later snapshots extend the
50 // history at the next model boundary and remain ordinary appends.
51 first := !hasInternalContextMessage(a.messages)
52 insertAt := len(a.messages)
53 if first {
54 insertAt = 0
55 }
56 a.messages = append(a.messages, provider.Message{})
57 copy(a.messages[insertAt+1:], a.messages[insertAt:])
58 a.messages[insertAt] = message
59 a.rev++
60 onCompacted := a.OnTranscriptCompacted
61 persisted := append([]provider.Message(nil), a.messages...)
62 a.mu.Unlock()
63 if first && onCompacted != nil {
64 onCompacted(persisted)
65 return
66 }
67 a.fireMessageAppended(message)
68}
69
70func hasInternalContextMessage(messages []provider.Message) bool {
71 for _, message := range messages {
72 if isInternalContextMessage(message) {
73 return true
74 }
75 }
76 return false
77}
78
79func isInternalContextMessage(message provider.Message) bool {
80 return message.Role == provider.RoleDeveloper && message.Meta[internalContextMarker] == "true"
81}
82
83func latestInternalContext(messages []provider.Message) (*provider.Message, []provider.Message) {
84 filtered := make([]provider.Message, 0, len(messages))
85 var latest *provider.Message
86 for _, message := range messages {
87 if isInternalContextMessage(message) {
88 copy := message
89 latest = ©
90 continue
91 }
92 filtered = append(filtered, message)
93 }
94 return latest, filtered
95}
96
97func internalContextText(message provider.Message) string {
98 if len(message.Content) != 1 {
99 return ""
100 }
101 text, _ := message.Content[0].(provider.TextBlock)
102 return text.Text
103}