Files
cursor-byok/internal/backend/forwarder/prompt_context.go
T

133 lines
4.0 KiB
Go

package forwarder
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"strings"
modeladapter "cursor/internal/backend/agent/model"
)
const promptContextSourceSelectedCursorCommands = "selected_cursor_commands"
func newPromptContextMessage(source string, message modeladapter.Message, persist bool) PromptContextMessage {
context := PromptContextMessage{
Source: strings.TrimSpace(source),
Message: message,
Persist: persist,
}
context.ContentHash = promptContextContentHash(context.Message)
return context
}
func normalizePromptContextMessage(context PromptContextMessage) PromptContextMessage {
context.Source = strings.TrimSpace(context.Source)
context.Message.Role = strings.TrimSpace(context.Message.Role)
context.Message.Content = strings.TrimSpace(context.Message.Content)
context.ContentHash = strings.TrimSpace(context.ContentHash)
if context.ContentHash == "" {
context.ContentHash = promptContextContentHash(context.Message)
}
return context
}
func promptContextContentHash(message modeladapter.Message) string {
sum := sha256.Sum256([]byte(strings.TrimSpace(message.Role) + "\x00" + strings.TrimSpace(message.Content)))
return hex.EncodeToString(sum[:])
}
func filterCurrentTurnPromptContexts(conversation *ConversationFile, contexts []PromptContextMessage) []PromptContextMessage {
if len(contexts) == 0 {
return nil
}
seen := collectCurrentTurnPromptContextKeys(conversation)
filtered := make([]PromptContextMessage, 0, len(contexts))
for _, context := range contexts {
context = normalizePromptContextMessage(context)
if !isReplayablePromptContext(context) {
continue
}
if context.Persist {
if _, ok := seen[promptContextKey(context)]; ok {
continue
}
}
filtered = append(filtered, context)
}
return filtered
}
func collectCurrentTurnPromptContextKeys(conversation *ConversationFile) map[string]struct{} {
keys := make(map[string]struct{})
if conversation == nil {
return keys
}
currentTurnSeq := conversation.NextTurnSeq - 1
if currentTurnSeq <= 0 {
return keys
}
for _, entry := range conversation.Entries {
if entry.TurnSeq != currentTurnSeq || strings.TrimSpace(entry.Kind) != "prompt_context" {
continue
}
var payload promptContextEntryPayload
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
continue
}
context := normalizePromptContextMessage(PromptContextMessage{
Source: payload.Source,
ContentHash: payload.ContentHash,
Message: modeladapter.Message{
Role: payload.Role,
Content: payload.Content,
},
Persist: true,
})
if !isReplayablePromptContext(context) {
continue
}
keys[promptContextKey(context)] = struct{}{}
}
return keys
}
func promptContextKey(context PromptContextMessage) string {
context = normalizePromptContextMessage(context)
return context.Source + "\x00" + context.ContentHash
}
func isReplayablePromptContext(context PromptContextMessage) bool {
context = normalizePromptContextMessage(context)
if context.Source == "" || strings.TrimSpace(context.Message.Content) == "" {
return false
}
if context.Message.Role == "" {
context.Message.Role = "user"
}
if context.Message.Role != "user" && context.Message.Role != "system" {
return false
}
if strings.TrimSpace(context.Message.Name) != "" || strings.TrimSpace(context.Message.ToolCallID) != "" {
return false
}
return len(context.Message.ToolCalls) == 0 && len(context.Message.ContentParts) == 0
}
func newPromptContextEntry(turnSeq int64, requestID string, context PromptContextMessage) HistoryEntry {
context = normalizePromptContextMessage(context)
payload, _ := json.Marshal(promptContextEntryPayload{
Source: context.Source,
Role: firstNonEmpty(strings.TrimSpace(context.Message.Role), "user"),
Content: strings.TrimSpace(context.Message.Content),
ContentHash: context.ContentHash,
})
return HistoryEntry{
TurnSeq: turnSeq,
RequestID: strings.TrimSpace(requestID),
Role: "system",
Kind: "prompt_context",
Payload: payload,
}
}