mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-18 03:57:06 +08:00
256 lines
9.0 KiB
Go
256 lines
9.0 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
|
|
}
|
|
|
|
// NewPromptCompiler 创建默认 prompt 编译器。
|
|
func NewPromptCompiler(projector *HistoryProjector, catalog ToolCatalog, reminders ReminderInjector, rules *UserRuleStore) *DefaultPromptCompiler {
|
|
return &DefaultPromptCompiler{
|
|
projector: projector,
|
|
catalog: catalog,
|
|
reminders: reminders,
|
|
rules: rules,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
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
|
|
}
|