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

265 lines
9.3 KiB
Go

// compiler.go 负责把固定 prompt、自然历史和 tool catalog 编译成 provider 请求。
package forwarder
import (
"fmt"
"strings"
"cursor/gen/agentv1"
modeladapter "cursor/internal/backend/agent/model"
promptassets "cursor/prompt"
)
type PromptCompiler interface {
Compile(conversation *ConversationFile, mode agentv1.AgentMode, latestUserText string, modelName string) (CompiledConversation, error)
DerivePromptContexts(conversation *ConversationFile, mode agentv1.AgentMode, latestUserText string) ([]PromptContextMessage, error)
}
type DefaultPromptCompiler struct {
projector *HistoryProjector
catalog ToolCatalog
reminders ReminderInjector
rules *UserRuleStore
blobs contentBlobReader
}
// NewPromptCompiler 创建默认 prompt 编译器。
func NewPromptCompiler(projector *HistoryProjector, catalog ToolCatalog, reminders ReminderInjector, rules *UserRuleStore, blobReaders ...contentBlobReader) *DefaultPromptCompiler {
compiler := &DefaultPromptCompiler{
projector: projector,
catalog: catalog,
reminders: reminders,
rules: rules,
}
if len(blobReaders) > 0 {
compiler.blobs = blobReaders[0]
}
return compiler
}
// Compile 生成当前 turn 应发送给 provider 的消息和工具集合。
func (compiler *DefaultPromptCompiler) Compile(conversation *ConversationFile, mode agentv1.AgentMode, latestUserText string, modelName string) (CompiledConversation, error) {
if compiler == nil || compiler.projector == nil || compiler.catalog == nil {
return CompiledConversation{}, fmt.Errorf("prompt compiler dependencies are not initialized")
}
normalizedMode, err := validateSupportedActiveMode(mode)
if err != nil {
return CompiledConversation{}, err
}
subagentTypeName := ""
if conversation != nil {
subagentTypeName = conversation.SubagentTypeName
}
assetMode, err := promptAssetModeForConversation(normalizedMode, subagentTypeName)
if err != nil {
return CompiledConversation{}, err
}
systemPrompt, err := promptassets.ReadPrompt(assetMode)
if err != nil {
return CompiledConversation{}, err
}
tools, _, err := compiler.catalog.Load(normalizedMode, subagentTypeName)
if err != nil {
return CompiledConversation{}, err
}
replayMessages, err := compiler.projector.ProjectPromptReplay(conversation)
if err != nil {
return CompiledConversation{}, err
}
sharedRulesPrompt := ""
sharedRuleCount := 0
sharedRuleTotal := 0
if compiler.rules != nil && normalizedMode != agentv1.AgentMode_AGENT_MODE_DEBUG {
sharedRulesPrompt, sharedRuleTotal, sharedRuleCount, err = compiler.rules.BuildSystemPromptSection()
if err != nil {
return CompiledConversation{}, err
}
}
messages := make([]modeladapter.Message, 0, len(replayMessages)+1)
systemParts := []string{sanitizePromptAsset(systemPrompt, modelName)}
if strings.TrimSpace(sharedRulesPrompt) != "" {
systemParts = append(systemParts, sharedRulesPrompt)
}
systemText := strings.TrimSpace(strings.Join(filterNonEmpty(systemParts), "\n\n"))
if systemText != "" {
messages = append(messages, modeladapter.Message{
Role: "system",
Content: systemText,
})
}
stableReplayCount, err := compiler.stableReplayMessageCount(conversation, replayMessages)
if err != nil {
return CompiledConversation{}, err
}
replayMessages, err = enrichProviderReadImages(replayMessages, conversation, compiler.blobs)
if err != nil {
return CompiledConversation{}, err
}
messages = append(messages, replayMessages...)
return CompiledConversation{
Mode: normalizedMode,
Messages: messages,
StableMessageCount: stableReplayCount,
Tools: tools,
CompileSummary: fmt.Sprintf("mode=%s asset_mode=%s child=%t messages=%d tools=%d shared_rules_total=%d shared_rules_deduped=%d", normalizedMode.String(), string(assetMode), isChildConversationSubagentTypeName(subagentTypeName), len(messages), len(tools), sharedRuleTotal, sharedRuleCount),
}, nil
}
func (compiler *DefaultPromptCompiler) DerivePromptContexts(conversation *ConversationFile, mode agentv1.AgentMode, latestUserText string) ([]PromptContextMessage, error) {
if compiler == nil || compiler.projector == nil || compiler.catalog == nil || compiler.reminders == nil {
return nil, fmt.Errorf("prompt compiler dependencies are not initialized")
}
normalizedMode, err := validateSupportedActiveMode(mode)
if err != nil {
return nil, err
}
subagentTypeName := ""
if conversation != nil {
subagentTypeName = conversation.SubagentTypeName
}
_, toolNames, err := compiler.catalog.Load(normalizedMode, subagentTypeName)
if err != nil {
return nil, err
}
replayMessages, err := compiler.projector.ProjectPromptReplay(conversation)
if err != nil {
return nil, err
}
structuredStatePromptContexts, structuredStateTailMessages, err := buildStructuredStatePromptContexts(conversation)
if err != nil {
return nil, err
}
promptReminders := compiler.reminders.Inject(normalizedMode, conversation, replayMessages, latestUserText, toolNames)
candidates := make([]PromptContextMessage, 0, len(structuredStatePromptContexts)+len(structuredStateTailMessages)+len(promptReminders.PromptContexts)+len(promptReminders.TailMessages))
candidates = append(candidates, structuredStatePromptContexts...)
for _, message := range structuredStateTailMessages {
candidates = append(candidates, newPromptContextMessage(promptContextSourceStructuredTodoReminder, message, true))
}
candidates = append(candidates, promptReminders.PromptContexts...)
for index, message := range promptReminders.TailMessages {
candidates = append(candidates, newPromptContextMessage(fmt.Sprintf("tail_reminder/%d", index), message, true))
}
for index := range candidates {
candidates[index].Persist = true
}
return filterCurrentTurnPromptContexts(conversation, candidates), nil
}
func (compiler *DefaultPromptCompiler) stableReplayMessageCount(conversation *ConversationFile, replayMessages []modeladapter.Message) (int, error) {
if compiler == nil || compiler.projector == nil || conversation == nil || len(replayMessages) == 0 {
return 0, nil
}
currentTurnSeq := conversation.CurrentTurnSeq
if currentTurnSeq <= 0 {
currentTurnSeq = conversation.NextTurnSeq - 1
}
stableCount := 0
if currentTurnSeq > 0 {
stableConversation := cloneConversationFile(conversation)
stableConversation.Entries = stableReplayEntriesBeforeTurn(conversation.Entries, currentTurnSeq)
stableMessages, err := compiler.projector.ProjectPromptReplay(stableConversation)
if err != nil {
return 0, err
}
stableCount = len(stableMessages)
}
if requestPrefixReplayCount := replayMessageCountFromRequestPrefix(conversation); requestPrefixReplayCount > stableCount {
stableCount = requestPrefixReplayCount
}
if stableCount > len(replayMessages) {
return len(replayMessages), nil
}
return stableCount, nil
}
func replayMessageCountFromRequestPrefix(conversation *ConversationFile) int {
if conversation == nil || conversation.LatestRequestPrefix == nil {
return 0
}
requestID := strings.TrimSpace(conversation.CurrentRequestID)
if requestID == "" || strings.TrimSpace(conversation.LatestRequestPrefix.RequestID) != requestID {
return 0
}
return conversation.LatestRequestPrefix.ReplayMessageCount
}
func stableReplayEntriesBeforeTurn(entries []HistoryEntry, currentTurnSeq int64) []HistoryEntry {
if len(entries) == 0 || currentTurnSeq <= 0 {
return nil
}
filtered := make([]HistoryEntry, 0, len(entries))
for _, entry := range entries {
if entry.TurnSeq > 0 && entry.TurnSeq >= currentTurnSeq {
continue
}
filtered = append(filtered, entry)
}
return filtered
}
// filterNonEmpty 过滤掉空白字符串,便于安全拼接 system prompt 片段。
func filterNonEmpty(items []string) []string {
filtered := make([]string, 0, len(items))
for _, item := range items {
if strings.TrimSpace(item) != "" {
filtered = append(filtered, strings.TrimSpace(item))
}
}
return filtered
}
func mergeAdjacentPlainUserMessages(messages []modeladapter.Message) []modeladapter.Message {
if len(messages) == 0 {
return nil
}
merged := make([]modeladapter.Message, 0, len(messages))
for _, message := range messages {
text, ok := plainUserMessageText(message)
if !ok {
merged = append(merged, message)
continue
}
if len(merged) == 0 {
message.Role = "user"
message.Content = text
merged = append(merged, message)
continue
}
last := &merged[len(merged)-1]
if lastText, ok := plainUserMessageText(*last); ok {
last.Role = "user"
last.Content = lastText + "\n\n" + text
continue
}
message.Role = "user"
message.Content = text
merged = append(merged, message)
}
return merged
}
func plainUserMessageText(message modeladapter.Message) (string, bool) {
if strings.TrimSpace(message.Role) != "user" {
return "", false
}
if len(message.ContentParts) > 0 || len(message.ToolCalls) > 0 {
return "", false
}
if strings.TrimSpace(message.ToolCallID) != "" || strings.TrimSpace(message.Name) != "" {
return "", false
}
if strings.TrimSpace(message.ReasoningContent) != "" ||
strings.TrimSpace(message.ReasoningSignature) != "" ||
strings.TrimSpace(message.ReasoningSignatureSource) != "" ||
strings.TrimSpace(message.OpenAIResponsesReasoningID) != "" ||
strings.TrimSpace(message.OpenAIResponsesReasoningStatus) != "" ||
len(message.OpenAIResponsesReasoningSummary) > 0 {
return "", false
}
text := strings.TrimSpace(message.Content)
if text == "" {
return "", false
}
return text, true
}