mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
1636 lines
54 KiB
Go
1636 lines
54 KiB
Go
// projector.go 负责把 JSON history 投影成 prompt replay 和 legacy checkpoint 视图。
|
|
package forwarder
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"cursor/gen/agentv1"
|
|
modeladapter "cursor/internal/backend/agent/model"
|
|
promptengine "cursor/internal/backend/agent/prompt"
|
|
)
|
|
|
|
const projectedConversationMaxTokens = 130000
|
|
|
|
type HistoryProjector struct {
|
|
}
|
|
|
|
type CheckpointBlob struct {
|
|
ID []byte
|
|
Data []byte
|
|
}
|
|
|
|
type CheckpointProjection struct {
|
|
State *agentv1.ConversationStateStructure
|
|
Blobs []CheckpointBlob
|
|
}
|
|
|
|
type checkpointBlobGraph struct {
|
|
blobs map[[sha256.Size]byte][]byte
|
|
order [][sha256.Size]byte
|
|
}
|
|
|
|
func newCheckpointBlobGraph() *checkpointBlobGraph {
|
|
return &checkpointBlobGraph{blobs: make(map[[sha256.Size]byte][]byte)}
|
|
}
|
|
|
|
func (graph *checkpointBlobGraph) add(data []byte) []byte {
|
|
if graph == nil || len(data) == 0 {
|
|
return nil
|
|
}
|
|
id := sha256.Sum256(data)
|
|
if _, exists := graph.blobs[id]; !exists {
|
|
graph.blobs[id] = append([]byte(nil), data...)
|
|
graph.order = append(graph.order, id)
|
|
}
|
|
return append([]byte(nil), id[:]...)
|
|
}
|
|
|
|
func (graph *checkpointBlobGraph) list() []CheckpointBlob {
|
|
if graph == nil || len(graph.order) == 0 {
|
|
return nil
|
|
}
|
|
blobs := make([]CheckpointBlob, 0, len(graph.order))
|
|
for _, id := range graph.order {
|
|
blobs = append(blobs, CheckpointBlob{
|
|
ID: append([]byte(nil), id[:]...),
|
|
Data: append([]byte(nil), graph.blobs[id]...),
|
|
})
|
|
}
|
|
return blobs
|
|
}
|
|
|
|
// NewHistoryProjector 创建 history 投影器。
|
|
func NewHistoryProjector() *HistoryProjector {
|
|
return &HistoryProjector{}
|
|
}
|
|
|
|
// ProjectPromptReplay 把 conversation history 还原为 provider 可消费的消息列表。
|
|
func (projector *HistoryProjector) ProjectPromptReplay(conversation *ConversationFile) ([]modeladapter.Message, error) {
|
|
if conversation == nil {
|
|
return nil, nil
|
|
}
|
|
entries := replayablePromptProjectionEntries(conversation.Entries)
|
|
messages := make([]modeladapter.Message, 0, len(entries)*2)
|
|
seenToolCalls := make(map[string]struct{})
|
|
openToolCalls := make(map[string]struct{})
|
|
toolCallMessageIndexes := make(map[string]int)
|
|
for _, entry := range entries {
|
|
switch strings.TrimSpace(entry.Kind) {
|
|
case "model_message":
|
|
var payload modelMessageEntryPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return nil, fmt.Errorf("decode model_message entry: %w", err)
|
|
}
|
|
message := cloneReplayModelMessage(payload.Message)
|
|
if strings.TrimSpace(message.Role) != "" {
|
|
messages = append(messages, message)
|
|
}
|
|
case "compaction_summary", "compacted_summary":
|
|
summary, ok := decodeCompactionSummaryEntry(entry)
|
|
if ok {
|
|
messages = append(messages, modeladapter.Message{
|
|
Role: "user",
|
|
Content: "<conversation_summary>\n" + summary + "\n</conversation_summary>",
|
|
})
|
|
}
|
|
case "user_message":
|
|
userMessage := &agentv1.UserMessage{}
|
|
if err := protojson.Unmarshal(entry.Payload, userMessage); err != nil {
|
|
return nil, fmt.Errorf("decode user_message entry: %w", err)
|
|
}
|
|
message, ok := promptengine.BuildUserMessageReplayMessage(userMessage)
|
|
if ok {
|
|
messages = append(messages, toModelMessage(message))
|
|
}
|
|
case "request_context":
|
|
requestContext := &agentv1.RequestContext{}
|
|
if err := protojson.Unmarshal(entry.Payload, requestContext); err != nil {
|
|
return nil, fmt.Errorf("decode request_context entry: %w", err)
|
|
}
|
|
for _, replay := range promptengine.BuildRequestContextReplayMessages(requestContext) {
|
|
messages = append(messages, toModelMessage(replay))
|
|
}
|
|
case "prompt_context":
|
|
var payload promptContextEntryPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return nil, fmt.Errorf("decode prompt_context entry: %w", err)
|
|
}
|
|
context := normalizePromptContextMessage(PromptContextMessage{
|
|
Source: payload.Source,
|
|
ContentHash: payload.ContentHash,
|
|
Message: modeladapter.Message{
|
|
Role: firstNonEmpty(strings.TrimSpace(payload.Role), "user"),
|
|
Content: strings.TrimSpace(payload.Content),
|
|
},
|
|
Persist: true,
|
|
})
|
|
if isReplayablePromptContext(context) {
|
|
messages = append(messages, context.Message)
|
|
}
|
|
case "assistant_text":
|
|
var payload assistantTextPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return nil, fmt.Errorf("decode assistant_text entry: %w", err)
|
|
}
|
|
if strings.TrimSpace(payload.Text) == "" && strings.TrimSpace(payload.ReasoningContent) != "" && len(openToolCalls) > 0 {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(payload.Text) == "" && !hasReplayableReasoningPayload(payload.ReasoningContent, payload.ReasoningSignature, payload.ReasoningSignatureSource) {
|
|
continue
|
|
}
|
|
messages = append(messages, modeladapter.Message{
|
|
Role: "assistant",
|
|
Content: strings.TrimSpace(payload.Text),
|
|
ReasoningContent: payload.ReasoningContent,
|
|
ReasoningSignature: payload.ReasoningSignature,
|
|
ReasoningSignatureSource: payload.ReasoningSignatureSource,
|
|
OpenAIResponsesReasoningID: payload.ReasoningItemID,
|
|
OpenAIResponsesReasoningStatus: payload.ReasoningStatus,
|
|
OpenAIResponsesReasoningSummary: append(json.RawMessage(nil), payload.ReasoningSummary...),
|
|
})
|
|
case "tool_call":
|
|
var payload toolCallEntryPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return nil, fmt.Errorf("decode tool_call entry: %w", err)
|
|
}
|
|
toolCall := &agentv1.ToolCall{}
|
|
if err := protojson.Unmarshal(payload.ToolCall, toolCall); err != nil {
|
|
return nil, fmt.Errorf("decode tool_call payload: %w", err)
|
|
}
|
|
replayMessage, ok := promptengine.BuildAssistantToolCallReplayMessage(payload.ToolCallID, toolCall)
|
|
if !ok {
|
|
continue
|
|
}
|
|
replayMessage.ReasoningContent = payload.ReasoningContent
|
|
replayMessage.ReasoningSignature = payload.ReasoningSignature
|
|
replayMessage.ReasoningSignatureSource = payload.ReasoningSignatureSource
|
|
replayMessage.OpenAIResponsesReasoningID = payload.ReasoningItemID
|
|
replayMessage.OpenAIResponsesReasoningStatus = payload.ReasoningStatus
|
|
replayMessage.OpenAIResponsesReasoningSummary = append(json.RawMessage(nil), payload.ReasoningSummary...)
|
|
applyPromptProviderMetadataToFirstToolCall(&replayMessage, payload.ProviderItemID, payload.ProviderCallID, payload.ProviderStatus)
|
|
messages = append(messages, toModelMessage(replayMessage))
|
|
if toolCallID := strings.TrimSpace(payload.ToolCallID); toolCallID != "" {
|
|
toolCallMessageIndexes[toolCallID] = len(messages) - 1
|
|
seenToolCalls[toolCallID] = struct{}{}
|
|
openToolCalls[toolCallID] = struct{}{}
|
|
}
|
|
case "tool_result":
|
|
var payload toolResultEntryPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return nil, fmt.Errorf("decode tool_result entry: %w", err)
|
|
}
|
|
historicalToolResult := isHistoricalReplayToolResult(conversation, entry)
|
|
toolCallID := strings.TrimSpace(payload.ToolCallID)
|
|
if _, ok := seenToolCalls[toolCallID]; ok {
|
|
delete(openToolCalls, toolCallID)
|
|
if index, found := toolCallMessageIndexes[toolCallID]; found && index >= 0 && index < len(messages) {
|
|
overrideModelToolReplayFromEntry(&messages[index], payload.ToolName, payload.Arguments)
|
|
delete(toolCallMessageIndexes, toolCallID)
|
|
}
|
|
var toolCall *agentv1.ToolCall
|
|
if len(payload.ToolCall) > 0 {
|
|
decoded := &agentv1.ToolCall{}
|
|
if err := protojson.Unmarshal(payload.ToolCall, decoded); err == nil {
|
|
toolCall = decoded
|
|
}
|
|
}
|
|
toolName := strings.TrimSpace(payload.ToolName)
|
|
if toolName == "" && toolCall != nil {
|
|
toolName = inferToolName(toolCall)
|
|
}
|
|
if toolCallID == "" || toolName == "" {
|
|
continue
|
|
}
|
|
if isLegacyPlainWriteReplay(toolName, len(payload.ToolCall) > 0) {
|
|
continue
|
|
}
|
|
if toolCall != nil {
|
|
replayMessage, ok := promptengine.BuildToolResultReplayMessage(toolCallID, toolCall)
|
|
if ok {
|
|
replayMessage.Name = toolName
|
|
replayMessage.Content = limitProjectedToolResultReplay(toolName, replayMessage.Content, payload.ResultText, true, historicalToolResult)
|
|
messages = append(messages, toModelMessage(replayMessage))
|
|
continue
|
|
}
|
|
}
|
|
messages = append(messages, modeladapter.Message{
|
|
Role: "tool",
|
|
Name: toolName,
|
|
ToolCallID: toolCallID,
|
|
Content: limitProjectedToolResultReplay(toolName, payload.ResultText, "", false, historicalToolResult),
|
|
})
|
|
continue
|
|
}
|
|
if len(payload.ToolCall) > 0 {
|
|
toolCall := &agentv1.ToolCall{}
|
|
if err := protojson.Unmarshal(payload.ToolCall, toolCall); err != nil {
|
|
return nil, fmt.Errorf("decode tool_result tool_call entry: %w", err)
|
|
}
|
|
replayMessages, ok := promptengine.BuildToolCallReplayMessages(payload.ToolCallID, toolCall)
|
|
if !ok {
|
|
continue
|
|
}
|
|
overrideToolReplayFromEntry(replayMessages, payload.ToolName, payload.Arguments)
|
|
for index := range replayMessages {
|
|
if strings.TrimSpace(replayMessages[index].Role) != "assistant" || len(replayMessages[index].ToolCalls) == 0 {
|
|
continue
|
|
}
|
|
replayMessages[index].ReasoningContent = payload.ReasoningContent
|
|
replayMessages[index].ReasoningSignature = payload.ReasoningSignature
|
|
replayMessages[index].ReasoningSignatureSource = payload.ReasoningSignatureSource
|
|
replayMessages[index].OpenAIResponsesReasoningID = payload.ReasoningItemID
|
|
replayMessages[index].OpenAIResponsesReasoningStatus = payload.ReasoningStatus
|
|
replayMessages[index].OpenAIResponsesReasoningSummary = append(json.RawMessage(nil), payload.ReasoningSummary...)
|
|
applyPromptProviderMetadataToFirstToolCall(&replayMessages[index], payload.ProviderItemID, payload.ProviderCallID, payload.ProviderStatus)
|
|
}
|
|
for index := range replayMessages {
|
|
if strings.TrimSpace(replayMessages[index].Role) == "tool" {
|
|
toolName := firstNonEmpty(strings.TrimSpace(replayMessages[index].Name), strings.TrimSpace(payload.ToolName))
|
|
replayMessages[index].Content = limitProjectedToolResultReplay(toolName, replayMessages[index].Content, payload.ResultText, true, historicalToolResult)
|
|
}
|
|
messages = append(messages, toModelMessage(replayMessages[index]))
|
|
}
|
|
continue
|
|
}
|
|
if strings.TrimSpace(payload.ToolCallID) == "" || strings.TrimSpace(payload.ToolName) == "" {
|
|
continue
|
|
}
|
|
if isLegacyPlainWriteReplay(strings.TrimSpace(payload.ToolName), len(payload.ToolCall) > 0) {
|
|
continue
|
|
}
|
|
if !hasReplayableReasoningPayload(payload.ReasoningContent, payload.ReasoningSignature, payload.ReasoningSignatureSource) {
|
|
continue
|
|
}
|
|
effectiveToolName := effectiveReplayToolName(strings.TrimSpace(payload.ToolName), strings.TrimSpace(payload.ToolName))
|
|
effectiveArguments := firstNonEmpty(strings.TrimSpace(payload.Arguments), "{}")
|
|
if isLegacyPatchEditToolName(payload.ToolName) {
|
|
effectiveArguments = "{}"
|
|
}
|
|
messages = append(messages,
|
|
modeladapter.Message{
|
|
Role: "assistant",
|
|
ReasoningContent: payload.ReasoningContent,
|
|
ReasoningSignature: payload.ReasoningSignature,
|
|
ReasoningSignatureSource: payload.ReasoningSignatureSource,
|
|
OpenAIResponsesReasoningID: payload.ReasoningItemID,
|
|
OpenAIResponsesReasoningStatus: payload.ReasoningStatus,
|
|
OpenAIResponsesReasoningSummary: append(json.RawMessage(nil), payload.ReasoningSummary...),
|
|
ToolCalls: []modeladapter.ToolCallDescriptor{{
|
|
ID: strings.TrimSpace(payload.ToolCallID),
|
|
Type: "function",
|
|
OpenAIResponsesID: strings.TrimSpace(payload.ProviderItemID),
|
|
OpenAIResponsesCallID: strings.TrimSpace(payload.ProviderCallID),
|
|
OpenAIResponsesStatus: strings.TrimSpace(payload.ProviderStatus),
|
|
Function: modeladapter.ToolCallFunctionShape{
|
|
Name: effectiveToolName,
|
|
Arguments: effectiveArguments,
|
|
},
|
|
}},
|
|
},
|
|
modeladapter.Message{
|
|
Role: "tool",
|
|
Name: effectiveToolName,
|
|
ToolCallID: strings.TrimSpace(payload.ToolCallID),
|
|
Content: limitProjectedToolResultReplay(payload.ToolName, payload.ResultText, "", false, historicalToolResult),
|
|
},
|
|
)
|
|
}
|
|
}
|
|
return normalizeReplayMessageSequence(messages), nil
|
|
}
|
|
|
|
func compactedPromptProjectionEntries(entries []HistoryEntry) []HistoryEntry {
|
|
if len(entries) == 0 {
|
|
return nil
|
|
}
|
|
compactionIndex := -1
|
|
for index := len(entries) - 1; index >= 0; index-- {
|
|
if isCompactionSummaryKind(entries[index].Kind) {
|
|
compactionIndex = index
|
|
break
|
|
}
|
|
}
|
|
if compactionIndex < 0 {
|
|
return entries
|
|
}
|
|
var compactionPayload compactionSummaryEntryPayload
|
|
_ = json.Unmarshal(entries[compactionIndex].Payload, &compactionPayload)
|
|
preservedIndexes := map[int]struct{}{}
|
|
if compactionPayload.PreserveCurrentTurnInputs {
|
|
latestToolCallID := latestCompletedToolCallIDForTurn(entries, compactionPayload.CurrentTurnSeq, compactionPayload.CurrentRequestID)
|
|
preservedIndexes = autoCompactionPreservedEntryIndexes(entries, compactionPayload.CurrentTurnSeq, compactionPayload.CurrentRequestID, latestToolCallID)
|
|
}
|
|
filtered := make([]HistoryEntry, 0, len(entries)-compactionIndex)
|
|
for index, entry := range entries {
|
|
if index < compactionIndex && isPromptReplayEntryKind(entry.Kind) {
|
|
if _, ok := preservedIndexes[index]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
if index < compactionIndex {
|
|
if rewritten, ok := compactedProjectionPreservedEntry(entry); ok {
|
|
entry = rewritten
|
|
}
|
|
}
|
|
filtered = append(filtered, entry)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func replayablePromptProjectionEntries(entries []HistoryEntry) []HistoryEntry {
|
|
return sanitizeCanceledReplayEntries(compactedPromptProjectionEntries(entries))
|
|
}
|
|
|
|
func checkpointProjectionEntries(entries []HistoryEntry) []HistoryEntry {
|
|
return sanitizeCanceledReplayEntries(entries)
|
|
}
|
|
|
|
const (
|
|
cancelReplayPolicyDropTurn = "drop_turn"
|
|
cancelReplayPolicyDropUnstarted = "drop_unstarted_turn"
|
|
cancelReplayPolicyKeepStableInput = "keep_stable_input"
|
|
)
|
|
|
|
func sanitizeCanceledReplayEntries(entries []HistoryEntry) []HistoryEntry {
|
|
if len(entries) == 0 {
|
|
return nil
|
|
}
|
|
canceledTurns := canceledReplayPolicies(entries)
|
|
if len(canceledTurns) == 0 {
|
|
return entries
|
|
}
|
|
activeCanceledTurns := canceledReplayActivityTurns(entries)
|
|
filtered := make([]HistoryEntry, 0, len(entries))
|
|
for _, entry := range entries {
|
|
if entry.TurnSeq > 0 {
|
|
if policy, canceled := canceledTurns[entry.TurnSeq]; canceled {
|
|
if policy == cancelReplayPolicyDropUnstarted {
|
|
if _, active := activeCanceledTurns[entry.TurnSeq]; active {
|
|
policy = cancelReplayPolicyKeepStableInput
|
|
} else {
|
|
policy = cancelReplayPolicyDropTurn
|
|
}
|
|
}
|
|
if policy == cancelReplayPolicyDropTurn || !isStableCanceledTurnInputEntry(entry) {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
filtered = append(filtered, entry)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func canceledReplayPolicies(entries []HistoryEntry) map[int64]string {
|
|
canceledTurns := make(map[int64]string)
|
|
for _, entry := range entries {
|
|
if entry.TurnSeq <= 0 {
|
|
continue
|
|
}
|
|
policy, ok := canceledReplayPolicyForEntry(entry)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if policy == cancelReplayPolicyDropTurn {
|
|
canceledTurns[entry.TurnSeq] = policy
|
|
continue
|
|
}
|
|
if _, exists := canceledTurns[entry.TurnSeq]; !exists {
|
|
canceledTurns[entry.TurnSeq] = policy
|
|
}
|
|
}
|
|
return canceledTurns
|
|
}
|
|
|
|
func canceledReplayActivityTurns(entries []HistoryEntry) map[int64]struct{} {
|
|
activeTurns := make(map[int64]struct{})
|
|
for _, entry := range entries {
|
|
if entry.TurnSeq <= 0 || !isCanceledTurnActivityEntry(entry) {
|
|
continue
|
|
}
|
|
activeTurns[entry.TurnSeq] = struct{}{}
|
|
}
|
|
return activeTurns
|
|
}
|
|
|
|
func canceledReplayPolicyForEntry(entry HistoryEntry) (string, bool) {
|
|
if strings.TrimSpace(entry.Kind) != "metadata" {
|
|
return "", false
|
|
}
|
|
var payload metadataPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return "", false
|
|
}
|
|
if strings.TrimSpace(payload.Type) != "control" {
|
|
return "", false
|
|
}
|
|
if strings.TrimSpace(readStringValue(payload.Value["status"])) != "canceled" {
|
|
return "", false
|
|
}
|
|
return normalizeCancelReplayPolicy(
|
|
readStringValue(payload.Value["replay_policy"]),
|
|
readStringValue(payload.Value["reason"]),
|
|
), true
|
|
}
|
|
|
|
func normalizeCancelReplayPolicy(policy string, reason string) string {
|
|
switch strings.TrimSpace(policy) {
|
|
case cancelReplayPolicyDropTurn:
|
|
if strings.Contains(strings.ToLower(strings.TrimSpace(reason)), "superseded by newer request") {
|
|
return cancelReplayPolicyDropUnstarted
|
|
}
|
|
return cancelReplayPolicyDropTurn
|
|
case cancelReplayPolicyDropUnstarted:
|
|
return cancelReplayPolicyDropUnstarted
|
|
case cancelReplayPolicyKeepStableInput:
|
|
return cancelReplayPolicyKeepStableInput
|
|
default:
|
|
return cancelReplayPolicyForReason(reason)
|
|
}
|
|
}
|
|
|
|
func cancelReplayPolicyForReason(reason string) string {
|
|
normalized := strings.ToLower(strings.TrimSpace(reason))
|
|
switch {
|
|
case strings.Contains(normalized, "superseded by newer request"):
|
|
return cancelReplayPolicyDropUnstarted
|
|
default:
|
|
return cancelReplayPolicyKeepStableInput
|
|
}
|
|
}
|
|
|
|
func isStableCanceledTurnInputEntry(entry HistoryEntry) bool {
|
|
switch strings.TrimSpace(entry.Kind) {
|
|
case "request_context", "user_message", "prompt_context":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isCanceledTurnActivityEntry(entry HistoryEntry) bool {
|
|
switch strings.TrimSpace(entry.Kind) {
|
|
case "model_message", "assistant_text", "tool_call", "tool_result":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func compactedProjectionPreservedEntry(entry HistoryEntry) (HistoryEntry, bool) {
|
|
if strings.TrimSpace(entry.Kind) != "tool_result" {
|
|
return entry, false
|
|
}
|
|
if rewritten, ok := rewriteAutoCompactionToolResultEntry(entry, autoCompactionPreservedToolResultLimitBytes, false); ok {
|
|
return rewritten, true
|
|
}
|
|
return entry, true
|
|
}
|
|
|
|
func isPromptReplayEntryKind(kind string) bool {
|
|
switch strings.TrimSpace(kind) {
|
|
case "model_message", "compaction_summary", "compacted_summary", "user_message", "request_context", "prompt_context", "assistant_text", "tool_call", "tool_result":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func decodeCompactionSummaryEntry(entry HistoryEntry) (string, bool) {
|
|
var payload compactionSummaryEntryPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return "", false
|
|
}
|
|
text := strings.TrimSpace(payload.Summary)
|
|
return text, text != ""
|
|
}
|
|
|
|
func isHistoricalReplayToolResult(conversation *ConversationFile, entry HistoryEntry) bool {
|
|
if conversation == nil || entry.TurnSeq <= 0 {
|
|
return false
|
|
}
|
|
currentTurnSeq := conversation.NextTurnSeq - 1
|
|
return currentTurnSeq > 0 && entry.TurnSeq < currentTurnSeq
|
|
}
|
|
|
|
// ProjectLegacyCheckpoint 按需从 JSON history 投影出兼容旧客户端的 checkpoint 结构。
|
|
func (projector *HistoryProjector) ProjectLegacyCheckpoint(conversation *ConversationFile) (*agentv1.ConversationStateStructure, error) {
|
|
projection, err := projector.ProjectCheckpointProjection(conversation)
|
|
if err != nil || projection == nil {
|
|
return nil, err
|
|
}
|
|
return projection.State, nil
|
|
}
|
|
|
|
// ProjectCheckpointProjection 同时返回 checkpoint 状态及其引用的内容寻址 Blob。
|
|
func (projector *HistoryProjector) ProjectCheckpointProjection(conversation *ConversationFile) (*CheckpointProjection, error) {
|
|
blobs := newCheckpointBlobGraph()
|
|
state := &agentv1.ConversationStateStructure{
|
|
TokenDetails: &agentv1.ConversationTokenDetails{
|
|
UsedTokens: conversationTokenDetailsUsedTokens(conversation),
|
|
MaxTokens: conversationTokenDetailsMaxTokens(conversation),
|
|
},
|
|
Summary: latestCompactionSummaryBytes(conversation),
|
|
SummaryArchive: previousCompactionSummaryBytes(conversation),
|
|
SummaryArchives: compactionSummaryArchives(conversation),
|
|
SelfSummaryCount: uint32(len(compactionSummaryTexts(conversation))),
|
|
}
|
|
if conversation == nil {
|
|
mode := agentv1.AgentMode_AGENT_MODE_AGENT
|
|
state.Mode = &mode
|
|
return &CheckpointProjection{State: state}, nil
|
|
}
|
|
mode, err := parseModeAlias(conversation.Mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.Mode = &mode
|
|
structuredState, err := projectConversationStructuredState(conversation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if structuredState.HasPlan {
|
|
state.Plan = encodeConversationPlanBytes(structuredState.PlanText)
|
|
}
|
|
state.Plans = clonePlanRegistryEntries(structuredState.Plans)
|
|
if structuredState.HasTodos {
|
|
state.Todos = encodeConversationTodoBytes(structuredState.Todos)
|
|
}
|
|
turnIDs, err := projectCheckpointTurnBlobs(conversation, blobs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.Turns = turnIDs
|
|
replayMessages, err := projector.ProjectPromptReplay(conversation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
promptReplay := make([]promptengine.Message, 0, len(replayMessages))
|
|
for _, message := range replayMessages {
|
|
promptReplay = append(promptReplay, promptengine.Message{
|
|
Role: message.Role,
|
|
Content: message.Content,
|
|
ContentParts: toPromptContentParts(message.ContentParts),
|
|
ReasoningContent: message.ReasoningContent,
|
|
ReasoningSignature: message.ReasoningSignature,
|
|
ReasoningSignatureSource: message.ReasoningSignatureSource,
|
|
OpenAIResponsesReasoningID: message.OpenAIResponsesReasoningID,
|
|
OpenAIResponsesReasoningStatus: message.OpenAIResponsesReasoningStatus,
|
|
OpenAIResponsesReasoningSummary: append(json.RawMessage(nil), message.OpenAIResponsesReasoningSummary...),
|
|
ToolCalls: toPromptToolCalls(message.ToolCalls),
|
|
ToolCallID: message.ToolCallID,
|
|
Name: message.Name,
|
|
})
|
|
}
|
|
promptReplay = filterCheckpointPersistentToolReplay(promptReplay)
|
|
rootPromptMessages, err := promptengine.EncodeReplayMessages(promptReplay)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.RootPromptMessagesJson = rootPromptMessages
|
|
return &CheckpointProjection{State: state, Blobs: blobs.list()}, nil
|
|
}
|
|
|
|
func projectCheckpointTurnBlobs(conversation *ConversationFile, blobs *checkpointBlobGraph) ([][]byte, error) {
|
|
if conversation == nil || blobs == nil {
|
|
return nil, nil
|
|
}
|
|
grouped := make(map[int64][]HistoryEntry)
|
|
order := make([]int64, 0, conversation.NextTurnSeq)
|
|
for _, entry := range checkpointProjectionEntries(conversation.Entries) {
|
|
if entry.TurnSeq <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := grouped[entry.TurnSeq]; !ok {
|
|
order = append(order, entry.TurnSeq)
|
|
}
|
|
grouped[entry.TurnSeq] = append(grouped[entry.TurnSeq], entry)
|
|
}
|
|
|
|
turnIDs := make([][]byte, 0, len(order))
|
|
for _, turnSeq := range order {
|
|
entries := grouped[turnSeq]
|
|
var userMessageID []byte
|
|
var turnRequestID string
|
|
stepIDs := make([][]byte, 0, len(entries))
|
|
seenToolCalls := make(map[string]struct{})
|
|
openToolCalls := make(map[string]struct{})
|
|
for _, entry := range entries {
|
|
if turnRequestID == "" {
|
|
turnRequestID = strings.TrimSpace(entry.RequestID)
|
|
}
|
|
switch strings.TrimSpace(entry.Kind) {
|
|
case "user_message":
|
|
userMessage := &agentv1.UserMessage{}
|
|
if err := protojson.Unmarshal(entry.Payload, userMessage); err != nil {
|
|
return nil, fmt.Errorf("decode checkpoint user_message: %w", err)
|
|
}
|
|
payload, err := proto.Marshal(userMessage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userMessageID = blobs.add(payload)
|
|
case "assistant_text":
|
|
var payload assistantTextPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(payload.Text) == "" && strings.TrimSpace(payload.ReasoningContent) != "" && len(openToolCalls) > 0 {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(payload.ReasoningContent) != "" {
|
|
stepID, err := addCheckpointStepBlob(blobs, &agentv1.ConversationStep{
|
|
Message: &agentv1.ConversationStep_ThinkingMessage{
|
|
ThinkingMessage: &agentv1.ThinkingMessage{Text: payload.ReasoningContent},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stepIDs = append(stepIDs, stepID)
|
|
}
|
|
if strings.TrimSpace(payload.Text) == "" {
|
|
continue
|
|
}
|
|
stepID, err := addCheckpointStepBlob(blobs, &agentv1.ConversationStep{
|
|
Message: &agentv1.ConversationStep_AssistantMessage{
|
|
AssistantMessage: &agentv1.AssistantMessage{Text: strings.TrimSpace(payload.Text)},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stepIDs = append(stepIDs, stepID)
|
|
case "tool_call":
|
|
var payload toolCallEntryPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(payload.ReasoningContent) != "" {
|
|
stepID, err := addCheckpointStepBlob(blobs, &agentv1.ConversationStep{
|
|
Message: &agentv1.ConversationStep_ThinkingMessage{
|
|
ThinkingMessage: &agentv1.ThinkingMessage{Text: payload.ReasoningContent},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stepIDs = append(stepIDs, stepID)
|
|
}
|
|
toolCall := &agentv1.ToolCall{}
|
|
if err := protojson.Unmarshal(payload.ToolCall, toolCall); err != nil {
|
|
return nil, err
|
|
}
|
|
if !shouldPersistToolResultName(firstNonEmpty(strings.TrimSpace(payload.ToolName), inferToolName(toolCall))) {
|
|
continue
|
|
}
|
|
stepID, err := addCheckpointStepBlob(blobs, &agentv1.ConversationStep{
|
|
Message: &agentv1.ConversationStep_ToolCall{ToolCall: toolCall},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stepIDs = append(stepIDs, stepID)
|
|
if toolCallID := strings.TrimSpace(payload.ToolCallID); toolCallID != "" {
|
|
seenToolCalls[toolCallID] = struct{}{}
|
|
openToolCalls[toolCallID] = struct{}{}
|
|
}
|
|
case "tool_result":
|
|
var payload toolResultEntryPayload
|
|
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
if toolCallID := strings.TrimSpace(payload.ToolCallID); toolCallID != "" {
|
|
if _, ok := seenToolCalls[toolCallID]; ok {
|
|
delete(openToolCalls, toolCallID)
|
|
continue
|
|
}
|
|
}
|
|
if strings.TrimSpace(payload.ReasoningContent) != "" {
|
|
stepID, err := addCheckpointStepBlob(blobs, &agentv1.ConversationStep{
|
|
Message: &agentv1.ConversationStep_ThinkingMessage{
|
|
ThinkingMessage: &agentv1.ThinkingMessage{Text: payload.ReasoningContent},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stepIDs = append(stepIDs, stepID)
|
|
}
|
|
if len(payload.ToolCall) == 0 {
|
|
continue
|
|
}
|
|
toolCall := &agentv1.ToolCall{}
|
|
if err := protojson.Unmarshal(payload.ToolCall, toolCall); err != nil {
|
|
return nil, err
|
|
}
|
|
if !shouldPersistToolResultName(firstNonEmpty(strings.TrimSpace(payload.ToolName), inferToolName(toolCall))) {
|
|
continue
|
|
}
|
|
stepID, err := addCheckpointStepBlob(blobs, &agentv1.ConversationStep{
|
|
Message: &agentv1.ConversationStep_ToolCall{ToolCall: toolCall},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stepIDs = append(stepIDs, stepID)
|
|
}
|
|
}
|
|
if len(userMessageID) == 0 && len(stepIDs) == 0 {
|
|
continue
|
|
}
|
|
agentTurn := &agentv1.AgentConversationTurnStructure{
|
|
UserMessage: userMessageID,
|
|
Steps: stepIDs,
|
|
}
|
|
if turnRequestID != "" {
|
|
agentTurn.RequestId = &turnRequestID
|
|
}
|
|
turnPayload, err := proto.Marshal(&agentv1.ConversationTurnStructure{
|
|
Turn: &agentv1.ConversationTurnStructure_AgentConversationTurn{
|
|
AgentConversationTurn: agentTurn,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
turnIDs = append(turnIDs, blobs.add(turnPayload))
|
|
}
|
|
return turnIDs, nil
|
|
}
|
|
|
|
func addCheckpointStepBlob(blobs *checkpointBlobGraph, step *agentv1.ConversationStep) ([]byte, error) {
|
|
payload, err := proto.Marshal(step)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return blobs.add(payload), nil
|
|
}
|
|
|
|
func conversationTokenDetailsUsedTokens(conversation *ConversationFile) uint32 {
|
|
if conversation == nil {
|
|
return 0
|
|
}
|
|
return conversation.TokenDetailsUsedTokens
|
|
}
|
|
|
|
func conversationTokenDetailsMaxTokens(conversation *ConversationFile) uint32 {
|
|
if conversation == nil || conversation.TokenDetailsMaxTokens == 0 {
|
|
return projectedConversationMaxTokens
|
|
}
|
|
return conversation.TokenDetailsMaxTokens
|
|
}
|
|
|
|
func latestCompactionSummaryBytes(conversation *ConversationFile) []byte {
|
|
texts := compactionSummaryTexts(conversation)
|
|
if len(texts) == 0 {
|
|
return nil
|
|
}
|
|
return encodeConversationSummaryBytes(texts[len(texts)-1])
|
|
}
|
|
|
|
func previousCompactionSummaryBytes(conversation *ConversationFile) []byte {
|
|
texts := compactionSummaryTexts(conversation)
|
|
if len(texts) < 2 {
|
|
return nil
|
|
}
|
|
return encodeConversationSummaryBytes(texts[len(texts)-2])
|
|
}
|
|
|
|
func compactionSummaryArchives(conversation *ConversationFile) [][]byte {
|
|
texts := compactionSummaryTexts(conversation)
|
|
if len(texts) == 0 {
|
|
return nil
|
|
}
|
|
archives := make([][]byte, 0, len(texts))
|
|
for _, text := range texts {
|
|
if encoded := encodeConversationSummaryBytes(text); len(encoded) > 0 {
|
|
archives = append(archives, encoded)
|
|
}
|
|
}
|
|
return archives
|
|
}
|
|
|
|
func compactionSummaryTexts(conversation *ConversationFile) []string {
|
|
if conversation == nil || len(conversation.Entries) == 0 {
|
|
return nil
|
|
}
|
|
texts := make([]string, 0)
|
|
for _, entry := range conversation.Entries {
|
|
if !isCompactionSummaryKind(entry.Kind) {
|
|
continue
|
|
}
|
|
if text, ok := decodeCompactionSummaryEntry(entry); ok {
|
|
texts = append(texts, text)
|
|
}
|
|
}
|
|
return texts
|
|
}
|
|
|
|
func isCompactionSummaryKind(kind string) bool {
|
|
switch strings.TrimSpace(kind) {
|
|
case "compaction_summary", "compacted_summary":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func applyPromptProviderMetadataToFirstToolCall(message *promptengine.Message, providerItemID string, providerCallID string, providerStatus string) {
|
|
if message == nil || len(message.ToolCalls) == 0 {
|
|
return
|
|
}
|
|
message.ToolCalls[0].OpenAIResponsesID = strings.TrimSpace(providerItemID)
|
|
message.ToolCalls[0].OpenAIResponsesCallID = strings.TrimSpace(providerCallID)
|
|
message.ToolCalls[0].OpenAIResponsesStatus = strings.TrimSpace(providerStatus)
|
|
}
|
|
|
|
func normalizeReplayMessageSequence(messages []modeladapter.Message) []modeladapter.Message {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
normalized := make([]modeladapter.Message, 0, len(messages))
|
|
for _, item := range messages {
|
|
message := cloneReplayModelMessage(item)
|
|
if mergeReplayAssistantToolCalls(&normalized, message) {
|
|
continue
|
|
}
|
|
normalized = append(normalized, message)
|
|
}
|
|
normalized = filterProviderSuppressedToolReplayMessages(normalized)
|
|
normalized = coalesceInterleavedReplayToolBatches(normalized)
|
|
return trimReplayDanglingAssistantToolCalls(normalized)
|
|
}
|
|
|
|
func filterProviderSuppressedToolReplayMessages(messages []modeladapter.Message) []modeladapter.Message {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
filtered := make([]modeladapter.Message, 0, len(messages))
|
|
skippedToolCallIDs := make(map[string]struct{})
|
|
for _, item := range messages {
|
|
message := cloneReplayModelMessage(item)
|
|
if strings.TrimSpace(message.Role) == "assistant" && len(message.ToolCalls) > 0 {
|
|
nextToolCalls := make([]modeladapter.ToolCallDescriptor, 0, len(message.ToolCalls))
|
|
for _, toolCall := range message.ToolCalls {
|
|
if isProviderPromptReplaySuppressedToolName(toolCall.Function.Name) {
|
|
if toolCallID := strings.TrimSpace(toolCall.ID); toolCallID != "" {
|
|
skippedToolCallIDs[toolCallID] = struct{}{}
|
|
}
|
|
continue
|
|
}
|
|
toolCall.Index = len(nextToolCalls)
|
|
nextToolCalls = append(nextToolCalls, toolCall)
|
|
}
|
|
if len(nextToolCalls) == 0 && strings.TrimSpace(message.Content) == "" && len(message.ContentParts) == 0 && !hasReplayableReasoningPayload(message.ReasoningContent, message.ReasoningSignature, message.ReasoningSignatureSource) {
|
|
continue
|
|
}
|
|
message.ToolCalls = nextToolCalls
|
|
filtered = append(filtered, message)
|
|
continue
|
|
}
|
|
if strings.TrimSpace(message.Role) == "tool" {
|
|
if _, ok := skippedToolCallIDs[strings.TrimSpace(message.ToolCallID)]; ok {
|
|
continue
|
|
}
|
|
if isProviderPromptReplaySuppressedToolName(message.Name) {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, message)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func isProviderPromptReplaySuppressedToolName(toolName string) bool {
|
|
switch strings.TrimSpace(toolName) {
|
|
case "GenerateImage":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func cloneReplayModelMessage(message modeladapter.Message) modeladapter.Message {
|
|
cloned := message
|
|
if len(message.ContentParts) > 0 {
|
|
cloned.ContentParts = append([]modeladapter.ContentPart(nil), message.ContentParts...)
|
|
}
|
|
if len(message.ToolCalls) > 0 {
|
|
cloned.ToolCalls = append([]modeladapter.ToolCallDescriptor(nil), message.ToolCalls...)
|
|
}
|
|
if len(message.OpenAIResponsesReasoningSummary) > 0 {
|
|
cloned.OpenAIResponsesReasoningSummary = append(json.RawMessage(nil), message.OpenAIResponsesReasoningSummary...)
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func mergeReplayAssistantToolCalls(messages *[]modeladapter.Message, message modeladapter.Message) bool {
|
|
if len(*messages) == 0 {
|
|
return false
|
|
}
|
|
last := &(*messages)[len(*messages)-1]
|
|
if !canMergeReplayAssistantToolCalls(*last, message) {
|
|
return false
|
|
}
|
|
startIndex := len(last.ToolCalls)
|
|
for index, toolCall := range message.ToolCalls {
|
|
item := toolCall
|
|
item.Index = startIndex + index
|
|
last.ToolCalls = append(last.ToolCalls, item)
|
|
}
|
|
last.ReasoningContent = mergeReplayReasoning(last.ReasoningContent, message.ReasoningContent)
|
|
mergeReplayReasoningMetadata(last, message)
|
|
return true
|
|
}
|
|
|
|
func canMergeReplayAssistantToolCalls(last modeladapter.Message, current modeladapter.Message) bool {
|
|
if strings.TrimSpace(last.Role) != "assistant" || strings.TrimSpace(current.Role) != "assistant" {
|
|
return false
|
|
}
|
|
if len(last.ToolCalls) == 0 || len(current.ToolCalls) == 0 {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(last.ToolCallID) != "" || strings.TrimSpace(last.Name) != "" {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(current.ToolCallID) != "" || strings.TrimSpace(current.Name) != "" {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(current.Content) != "" || len(current.ContentParts) > 0 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func coalesceInterleavedReplayToolBatches(messages []modeladapter.Message) []modeladapter.Message {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
normalized := make([]modeladapter.Message, 0, len(messages))
|
|
for index := 0; index < len(messages); index++ {
|
|
message := cloneReplayModelMessage(messages[index])
|
|
groupID := replayAssistantToolGroupID(message)
|
|
if groupID == "" {
|
|
normalized = append(normalized, message)
|
|
continue
|
|
}
|
|
|
|
batch := message
|
|
toolResults := make(map[string]modeladapter.Message)
|
|
toolResultOrder := make([]string, 0)
|
|
changed := false
|
|
nextIndex := index + 1
|
|
for nextIndex < len(messages) {
|
|
next := cloneReplayModelMessage(messages[nextIndex])
|
|
if strings.TrimSpace(next.Role) == "tool" && replayToolCallGroupID(next.ToolCallID) == groupID {
|
|
toolCallID := strings.TrimSpace(next.ToolCallID)
|
|
if _, ok := toolResults[toolCallID]; !ok {
|
|
toolResultOrder = append(toolResultOrder, toolCallID)
|
|
}
|
|
toolResults[toolCallID] = next
|
|
changed = true
|
|
nextIndex++
|
|
continue
|
|
}
|
|
if replayAssistantToolGroupID(next) == groupID && canMergeReplayAssistantToolCalls(batch, next) {
|
|
startIndex := len(batch.ToolCalls)
|
|
for toolIndex, toolCall := range next.ToolCalls {
|
|
item := toolCall
|
|
item.Index = startIndex + toolIndex
|
|
batch.ToolCalls = append(batch.ToolCalls, item)
|
|
}
|
|
batch.ReasoningContent = mergeReplayReasoning(batch.ReasoningContent, next.ReasoningContent)
|
|
mergeReplayReasoningMetadata(&batch, next)
|
|
changed = true
|
|
nextIndex++
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
if !changed {
|
|
normalized = append(normalized, message)
|
|
continue
|
|
}
|
|
normalized = append(normalized, batch)
|
|
emittedResults := make(map[string]struct{}, len(toolResults))
|
|
for _, toolCall := range batch.ToolCalls {
|
|
toolCallID := strings.TrimSpace(toolCall.ID)
|
|
result, ok := toolResults[toolCallID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
normalized = append(normalized, result)
|
|
emittedResults[toolCallID] = struct{}{}
|
|
}
|
|
for _, toolCallID := range toolResultOrder {
|
|
if _, ok := emittedResults[toolCallID]; ok {
|
|
continue
|
|
}
|
|
normalized = append(normalized, toolResults[toolCallID])
|
|
}
|
|
index = nextIndex - 1
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func replayAssistantToolGroupID(message modeladapter.Message) string {
|
|
if strings.TrimSpace(message.Role) != "assistant" || len(message.ToolCalls) == 0 {
|
|
return ""
|
|
}
|
|
groupID := ""
|
|
for _, toolCall := range message.ToolCalls {
|
|
nextGroupID := replayToolCallGroupID(toolCall.ID)
|
|
if nextGroupID == "" {
|
|
return ""
|
|
}
|
|
if groupID == "" {
|
|
groupID = nextGroupID
|
|
continue
|
|
}
|
|
if groupID != nextGroupID {
|
|
return ""
|
|
}
|
|
}
|
|
return groupID
|
|
}
|
|
|
|
func replayToolCallGroupID(toolCallID string) string {
|
|
trimmed := strings.TrimSpace(toolCallID)
|
|
if trimmed == "" {
|
|
return ""
|
|
}
|
|
if namespace, _, ok := strings.Cut(trimmed, "::"); ok {
|
|
return strings.TrimSpace(namespace)
|
|
}
|
|
if strings.HasPrefix(trimmed, "tc_") {
|
|
parts := strings.SplitN(trimmed, "_", 3)
|
|
if len(parts) >= 2 && strings.TrimSpace(parts[1]) != "" {
|
|
return "tc_" + strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func mergeReplayReasoning(left string, right string) string {
|
|
left = strings.TrimSpace(left)
|
|
right = strings.TrimSpace(right)
|
|
switch {
|
|
case left == "":
|
|
return right
|
|
case right == "", right == left:
|
|
return left
|
|
default:
|
|
return left + "\n\n" + right
|
|
}
|
|
}
|
|
|
|
func mergeReplayReasoningSignature(left string, right string) string {
|
|
left = strings.TrimSpace(left)
|
|
right = strings.TrimSpace(right)
|
|
switch {
|
|
case left == "":
|
|
return right
|
|
case right == "", right == left:
|
|
return left
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func mergeReplayReasoningSignatureSource(left string, right string) string {
|
|
left = strings.TrimSpace(left)
|
|
right = strings.TrimSpace(right)
|
|
switch {
|
|
case left == "":
|
|
return right
|
|
case right == "", right == left:
|
|
return left
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func mergeReplayReasoningMetadata(last *modeladapter.Message, current modeladapter.Message) {
|
|
if last == nil {
|
|
return
|
|
}
|
|
leftSignature := strings.TrimSpace(last.ReasoningSignature)
|
|
rightSignature := strings.TrimSpace(current.ReasoningSignature)
|
|
mergedSignature := mergeReplayReasoningSignature(leftSignature, rightSignature)
|
|
last.ReasoningSignature = mergedSignature
|
|
if mergedSignature == "" {
|
|
last.ReasoningSignatureSource = ""
|
|
last.OpenAIResponsesReasoningID = ""
|
|
last.OpenAIResponsesReasoningStatus = ""
|
|
last.OpenAIResponsesReasoningSummary = nil
|
|
return
|
|
}
|
|
if leftSignature == "" && rightSignature != "" {
|
|
last.ReasoningSignatureSource = strings.TrimSpace(current.ReasoningSignatureSource)
|
|
last.OpenAIResponsesReasoningID = current.OpenAIResponsesReasoningID
|
|
last.OpenAIResponsesReasoningStatus = current.OpenAIResponsesReasoningStatus
|
|
last.OpenAIResponsesReasoningSummary = append(json.RawMessage(nil), current.OpenAIResponsesReasoningSummary...)
|
|
return
|
|
}
|
|
if leftSignature == rightSignature {
|
|
last.ReasoningSignatureSource = mergeReplayReasoningSignatureSource(last.ReasoningSignatureSource, current.ReasoningSignatureSource)
|
|
if strings.TrimSpace(last.OpenAIResponsesReasoningID) == "" {
|
|
last.OpenAIResponsesReasoningID = current.OpenAIResponsesReasoningID
|
|
}
|
|
if strings.TrimSpace(last.OpenAIResponsesReasoningStatus) == "" {
|
|
last.OpenAIResponsesReasoningStatus = current.OpenAIResponsesReasoningStatus
|
|
}
|
|
if len(last.OpenAIResponsesReasoningSummary) == 0 {
|
|
last.OpenAIResponsesReasoningSummary = append(json.RawMessage(nil), current.OpenAIResponsesReasoningSummary...)
|
|
}
|
|
}
|
|
}
|
|
|
|
func trimReplayDanglingAssistantToolCalls(messages []modeladapter.Message) []modeladapter.Message {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
trimmed := make([]modeladapter.Message, 0, len(messages))
|
|
for index := 0; index < len(messages); index++ {
|
|
message := cloneReplayModelMessage(messages[index])
|
|
if strings.TrimSpace(message.Role) != "assistant" || len(message.ToolCalls) == 0 {
|
|
trimmed = append(trimmed, message)
|
|
continue
|
|
}
|
|
|
|
end := index + 1
|
|
responded := make(map[string]struct{}, len(message.ToolCalls))
|
|
for end < len(messages) && strings.TrimSpace(messages[end].Role) == "tool" {
|
|
toolCallID := strings.TrimSpace(messages[end].ToolCallID)
|
|
if toolCallID != "" {
|
|
responded[toolCallID] = struct{}{}
|
|
}
|
|
end++
|
|
}
|
|
|
|
nextToolCalls := make([]modeladapter.ToolCallDescriptor, 0, len(message.ToolCalls))
|
|
allowedToolCallIDs := make(map[string]struct{}, len(message.ToolCalls))
|
|
for _, toolCall := range message.ToolCalls {
|
|
toolCallID := strings.TrimSpace(toolCall.ID)
|
|
if _, ok := responded[toolCallID]; !ok {
|
|
continue
|
|
}
|
|
item := toolCall
|
|
item.Index = len(nextToolCalls)
|
|
nextToolCalls = append(nextToolCalls, item)
|
|
allowedToolCallIDs[toolCallID] = struct{}{}
|
|
}
|
|
|
|
if len(nextToolCalls) > 0 {
|
|
message.ToolCalls = nextToolCalls
|
|
trimmed = append(trimmed, message)
|
|
for toolIndex := index + 1; toolIndex < end; toolIndex++ {
|
|
toolMessage := cloneReplayModelMessage(messages[toolIndex])
|
|
if _, ok := allowedToolCallIDs[strings.TrimSpace(toolMessage.ToolCallID)]; !ok {
|
|
continue
|
|
}
|
|
trimmed = append(trimmed, toolMessage)
|
|
}
|
|
} else if strings.TrimSpace(message.Content) != "" || len(message.ContentParts) > 0 || hasReplayableReasoningPayload(message.ReasoningContent, message.ReasoningSignature, message.ReasoningSignatureSource) {
|
|
message.ToolCalls = nil
|
|
trimmed = append(trimmed, message)
|
|
}
|
|
|
|
index = end - 1
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
func shouldPersistToolResultName(toolName string) bool {
|
|
switch strings.TrimSpace(toolName) {
|
|
case "PatchEdit", "PatchEditLines", "PatchEditSpan", "Edit", "Write", "GenerateImage":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func filterCheckpointTurns(rawTurns [][]byte) [][]byte {
|
|
if len(rawTurns) == 0 {
|
|
return nil
|
|
}
|
|
filtered := make([][]byte, 0, len(rawTurns))
|
|
for _, rawTurn := range rawTurns {
|
|
if len(rawTurn) == 0 {
|
|
continue
|
|
}
|
|
turn := &agentv1.ConversationTurnStructure{}
|
|
if err := proto.Unmarshal(rawTurn, turn); err != nil {
|
|
filtered = append(filtered, append([]byte(nil), rawTurn...))
|
|
continue
|
|
}
|
|
agentTurn := turn.GetAgentConversationTurn()
|
|
if agentTurn == nil {
|
|
filtered = append(filtered, append([]byte(nil), rawTurn...))
|
|
continue
|
|
}
|
|
|
|
nextSteps := make([][]byte, 0, len(agentTurn.GetSteps()))
|
|
for _, rawStep := range agentTurn.GetSteps() {
|
|
if len(rawStep) == 0 {
|
|
continue
|
|
}
|
|
step := &agentv1.ConversationStep{}
|
|
if err := proto.Unmarshal(rawStep, step); err != nil {
|
|
continue
|
|
}
|
|
if toolCall := step.GetToolCall(); toolCall != nil && !shouldPersistToolResultName(inferToolName(toolCall)) {
|
|
continue
|
|
}
|
|
nextSteps = append(nextSteps, append([]byte(nil), rawStep...))
|
|
}
|
|
if len(agentTurn.GetUserMessage()) == 0 && len(nextSteps) == 0 {
|
|
continue
|
|
}
|
|
encoded, err := proto.Marshal(&agentv1.ConversationTurnStructure{
|
|
Turn: &agentv1.ConversationTurnStructure_AgentConversationTurn{
|
|
AgentConversationTurn: &agentv1.AgentConversationTurnStructure{
|
|
UserMessage: append([]byte(nil), agentTurn.GetUserMessage()...),
|
|
Steps: nextSteps,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
filtered = append(filtered, append([]byte(nil), rawTurn...))
|
|
continue
|
|
}
|
|
filtered = append(filtered, encoded)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func filterCheckpointPersistentToolReplay(messages []promptengine.Message) []promptengine.Message {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
filtered := make([]promptengine.Message, 0, len(messages))
|
|
skippedToolCallIDs := make(map[string]struct{})
|
|
for _, message := range messages {
|
|
if strings.TrimSpace(message.Role) == "assistant" && len(message.ToolCalls) > 0 {
|
|
nextToolCalls := make([]promptengine.ToolCallDescriptor, 0, len(message.ToolCalls))
|
|
for _, toolCall := range message.ToolCalls {
|
|
if !shouldPersistToolResultName(toolCall.Function.Name) {
|
|
skippedToolCallIDs[strings.TrimSpace(toolCall.ID)] = struct{}{}
|
|
continue
|
|
}
|
|
nextToolCalls = append(nextToolCalls, toolCall)
|
|
}
|
|
if len(nextToolCalls) == 0 && strings.TrimSpace(message.Content) == "" && !hasReplayableReasoningPayload(message.ReasoningContent, message.ReasoningSignature, message.ReasoningSignatureSource) {
|
|
continue
|
|
}
|
|
message.ToolCalls = nextToolCalls
|
|
filtered = append(filtered, message)
|
|
continue
|
|
}
|
|
if strings.TrimSpace(message.Role) == "tool" {
|
|
if _, ok := skippedToolCallIDs[strings.TrimSpace(message.ToolCallID)]; ok {
|
|
continue
|
|
}
|
|
if !shouldPersistToolResultName(message.Name) {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, message)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func restoreImportedReplayUserMessages(messages []promptengine.Message, importedTurns [][]byte) []promptengine.Message {
|
|
if len(messages) == 0 || len(importedTurns) == 0 {
|
|
return messages
|
|
}
|
|
cursor := 0
|
|
for _, rawTurn := range importedTurns {
|
|
if len(rawTurn) == 0 {
|
|
continue
|
|
}
|
|
turn := &agentv1.ConversationTurnStructure{}
|
|
if err := proto.Unmarshal(rawTurn, turn); err != nil {
|
|
continue
|
|
}
|
|
agentTurn := turn.GetAgentConversationTurn()
|
|
if agentTurn == nil || len(agentTurn.GetUserMessage()) == 0 {
|
|
continue
|
|
}
|
|
userMessage := &agentv1.UserMessage{}
|
|
if err := proto.Unmarshal(agentTurn.GetUserMessage(), userMessage); err != nil {
|
|
continue
|
|
}
|
|
replay, ok := promptengine.BuildUserMessageReplayMessage(userMessage)
|
|
if !ok || len(replay.ContentParts) == 0 {
|
|
continue
|
|
}
|
|
for cursor < len(messages) {
|
|
if strings.TrimSpace(messages[cursor].Role) == "user" &&
|
|
strings.TrimSpace(messages[cursor].Content) == strings.TrimSpace(replay.Content) {
|
|
if len(messages[cursor].ContentParts) == 0 {
|
|
messages[cursor].ContentParts = replay.ContentParts
|
|
}
|
|
if strings.TrimSpace(messages[cursor].Content) == "" {
|
|
messages[cursor].Content = replay.Content
|
|
}
|
|
cursor++
|
|
break
|
|
}
|
|
cursor++
|
|
}
|
|
}
|
|
return messages
|
|
}
|
|
|
|
func isLegacyPlainWriteReplay(toolName string, hasStructuredToolCall bool) bool {
|
|
return !hasStructuredToolCall && strings.TrimSpace(toolName) == "Write"
|
|
}
|
|
|
|
func overrideToolReplayFromEntry(messages []promptengine.Message, toolName string, arguments string) {
|
|
overrideName := strings.TrimSpace(toolName)
|
|
overrideArgs := strings.TrimSpace(arguments)
|
|
if overrideName == "" || len(messages) == 0 {
|
|
return
|
|
}
|
|
for index := range messages {
|
|
switch strings.TrimSpace(messages[index].Role) {
|
|
case "assistant":
|
|
if len(messages[index].ToolCalls) == 0 {
|
|
continue
|
|
}
|
|
for toolIndex := range messages[index].ToolCalls {
|
|
currentName := strings.TrimSpace(messages[index].ToolCalls[toolIndex].Function.Name)
|
|
effectiveName := effectiveReplayToolName(currentName, overrideName)
|
|
effectiveArgs := firstNonEmpty(
|
|
overrideArgs,
|
|
strings.TrimSpace(messages[index].ToolCalls[toolIndex].Function.Arguments),
|
|
"{}",
|
|
)
|
|
if isLegacyPatchEditToolName(overrideName) {
|
|
effectiveArgs = firstNonEmpty(strings.TrimSpace(messages[index].ToolCalls[toolIndex].Function.Arguments), "{}")
|
|
}
|
|
messages[index].ToolCalls[toolIndex].Function.Name = effectiveName
|
|
messages[index].ToolCalls[toolIndex].Function.Arguments = firstNonEmpty(effectiveArgs, "{}")
|
|
}
|
|
case "tool":
|
|
messages[index].Name = effectiveReplayToolName(strings.TrimSpace(messages[index].Name), overrideName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func overrideModelToolReplayFromEntry(message *modeladapter.Message, toolName string, arguments string) {
|
|
if message == nil {
|
|
return
|
|
}
|
|
overrideName := strings.TrimSpace(toolName)
|
|
overrideArgs := strings.TrimSpace(arguments)
|
|
if overrideName == "" {
|
|
return
|
|
}
|
|
switch strings.TrimSpace(message.Role) {
|
|
case "assistant":
|
|
if len(message.ToolCalls) == 0 {
|
|
return
|
|
}
|
|
for index := range message.ToolCalls {
|
|
currentName := strings.TrimSpace(message.ToolCalls[index].Function.Name)
|
|
effectiveName := effectiveReplayToolName(currentName, overrideName)
|
|
effectiveArgs := firstNonEmpty(
|
|
overrideArgs,
|
|
strings.TrimSpace(message.ToolCalls[index].Function.Arguments),
|
|
"{}",
|
|
)
|
|
if isLegacyPatchEditToolName(overrideName) {
|
|
effectiveArgs = firstNonEmpty(strings.TrimSpace(message.ToolCalls[index].Function.Arguments), "{}")
|
|
}
|
|
message.ToolCalls[index].Function.Name = effectiveName
|
|
message.ToolCalls[index].Function.Arguments = firstNonEmpty(effectiveArgs, "{}")
|
|
}
|
|
case "tool":
|
|
message.Name = effectiveReplayToolName(strings.TrimSpace(message.Name), overrideName)
|
|
}
|
|
}
|
|
|
|
func effectiveReplayToolName(currentName string, overrideName string) string {
|
|
if isLegacyPatchEditToolName(currentName) || isLegacyPatchEditToolName(overrideName) {
|
|
return "Edit"
|
|
}
|
|
switch strings.TrimSpace(currentName) {
|
|
case "PatchEdit", "Edit", "Write":
|
|
switch strings.TrimSpace(overrideName) {
|
|
case "PatchEdit":
|
|
return strings.TrimSpace(overrideName)
|
|
case "Edit":
|
|
return "Edit"
|
|
case "Write":
|
|
return "Write"
|
|
}
|
|
return strings.TrimSpace(currentName)
|
|
default:
|
|
return strings.TrimSpace(overrideName)
|
|
}
|
|
}
|
|
|
|
func isLegacyPatchEditToolName(name string) bool {
|
|
switch strings.TrimSpace(name) {
|
|
case "PatchEditLines", "PatchEditSpan":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isLegacyPlainWriteToolCall(toolCall promptengine.ToolCallDescriptor) bool {
|
|
if strings.TrimSpace(toolCall.Function.Name) != "Write" {
|
|
return false
|
|
}
|
|
args := strings.TrimSpace(toolCall.Function.Arguments)
|
|
return args == "" || args == "{}" || args == "null"
|
|
}
|
|
|
|
func filterLegacyPlainWriteReplay(messages []promptengine.Message) []promptengine.Message {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
filtered := make([]promptengine.Message, 0, len(messages))
|
|
skippedToolCallIDs := make(map[string]struct{})
|
|
for _, message := range messages {
|
|
if strings.TrimSpace(message.Role) == "assistant" && len(message.ToolCalls) > 0 {
|
|
nextToolCalls := make([]promptengine.ToolCallDescriptor, 0, len(message.ToolCalls))
|
|
for _, toolCall := range message.ToolCalls {
|
|
if isLegacyPlainWriteToolCall(toolCall) {
|
|
skippedToolCallIDs[strings.TrimSpace(toolCall.ID)] = struct{}{}
|
|
continue
|
|
}
|
|
nextToolCalls = append(nextToolCalls, toolCall)
|
|
}
|
|
if len(nextToolCalls) == 0 && strings.TrimSpace(message.Content) == "" && !hasReplayableReasoningPayload(message.ReasoningContent, message.ReasoningSignature, message.ReasoningSignatureSource) {
|
|
continue
|
|
}
|
|
message.ToolCalls = nextToolCalls
|
|
filtered = append(filtered, message)
|
|
continue
|
|
}
|
|
if strings.TrimSpace(message.Role) == "tool" {
|
|
if _, ok := skippedToolCallIDs[strings.TrimSpace(message.ToolCallID)]; ok {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, message)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func filterInternalPromptContextReplay(messages []promptengine.Message) []promptengine.Message {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
filtered := make([]promptengine.Message, 0, len(messages))
|
|
for _, message := range messages {
|
|
if isInternalPromptContextReplayMessage(message) {
|
|
continue
|
|
}
|
|
filtered = append(filtered, message)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func isInternalPromptContextReplayMessage(message promptengine.Message) bool {
|
|
if strings.TrimSpace(message.Role) != "user" {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(message.Name) != "" || strings.TrimSpace(message.ToolCallID) != "" || len(message.ToolCalls) > 0 || len(message.ContentParts) > 0 {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(message.ReasoningContent) != "" || strings.TrimSpace(message.ReasoningSignature) != "" {
|
|
return false
|
|
}
|
|
return isInternalPromptContextContent(message.Content)
|
|
}
|
|
|
|
func isInternalPromptContextContent(content string) bool {
|
|
trimmed := strings.TrimSpace(content)
|
|
switch {
|
|
case trimmed == strings.TrimSpace(todoSectionReminderMessage):
|
|
return true
|
|
case strings.HasPrefix(trimmed, "<system_reminder>") &&
|
|
strings.HasSuffix(trimmed, "</system_reminder>") &&
|
|
strings.Contains(trimmed, "You recently successfully edited ") &&
|
|
strings.Contains(trimmed, "latest source of truth is the most recent successful"):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// toModelMessage 把 promptengine 的消息结构转换为 modeladapter 消息结构。
|
|
func toModelMessage(message promptengine.Message) modeladapter.Message {
|
|
return modeladapter.Message{
|
|
Role: message.Role,
|
|
Content: message.Content,
|
|
ContentParts: toModelContentParts(message.ContentParts),
|
|
ReasoningContent: message.ReasoningContent,
|
|
ReasoningSignature: message.ReasoningSignature,
|
|
ReasoningSignatureSource: message.ReasoningSignatureSource,
|
|
OpenAIResponsesReasoningID: message.OpenAIResponsesReasoningID,
|
|
OpenAIResponsesReasoningStatus: message.OpenAIResponsesReasoningStatus,
|
|
OpenAIResponsesReasoningSummary: append(json.RawMessage(nil), message.OpenAIResponsesReasoningSummary...),
|
|
ToolCalls: toModelToolCalls(message.ToolCalls),
|
|
ToolCallID: message.ToolCallID,
|
|
Name: message.Name,
|
|
}
|
|
}
|
|
|
|
func toModelContentParts(items []promptengine.ContentPart) []modeladapter.ContentPart {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
output := make([]modeladapter.ContentPart, 0, len(items))
|
|
for _, item := range items {
|
|
part := modeladapter.ContentPart{
|
|
Type: item.Type,
|
|
Text: item.Text,
|
|
}
|
|
if item.Image != nil {
|
|
part.Image = &modeladapter.ImageContent{
|
|
MIMEType: item.Image.MIMEType,
|
|
Path: item.Image.Path,
|
|
Data: item.Image.Data,
|
|
}
|
|
}
|
|
output = append(output, part)
|
|
}
|
|
return output
|
|
}
|
|
|
|
func toPromptContentParts(items []modeladapter.ContentPart) []promptengine.ContentPart {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
output := make([]promptengine.ContentPart, 0, len(items))
|
|
for _, item := range items {
|
|
part := promptengine.ContentPart{
|
|
Type: item.Type,
|
|
Text: item.Text,
|
|
}
|
|
if item.Image != nil {
|
|
part.Image = &promptengine.ImageContent{
|
|
MIMEType: item.Image.MIMEType,
|
|
Path: item.Image.Path,
|
|
Data: item.Image.Data,
|
|
}
|
|
}
|
|
output = append(output, part)
|
|
}
|
|
return output
|
|
}
|
|
|
|
// toModelToolCalls 把 promptengine 的 tool call 描述转换为 modeladapter 版本。
|
|
func toModelToolCalls(items []promptengine.ToolCallDescriptor) []modeladapter.ToolCallDescriptor {
|
|
output := make([]modeladapter.ToolCallDescriptor, 0, len(items))
|
|
for _, item := range items {
|
|
output = append(output, modeladapter.ToolCallDescriptor{
|
|
ID: item.ID,
|
|
Index: item.Index,
|
|
Type: item.Type,
|
|
OpenAIResponsesID: item.OpenAIResponsesID,
|
|
OpenAIResponsesCallID: item.OpenAIResponsesCallID,
|
|
OpenAIResponsesStatus: item.OpenAIResponsesStatus,
|
|
Function: modeladapter.ToolCallFunctionShape{
|
|
Name: item.Function.Name,
|
|
Arguments: item.Function.Arguments,
|
|
},
|
|
})
|
|
}
|
|
return output
|
|
}
|
|
|
|
// toPromptToolCalls 把 modeladapter 的 tool call 描述转换回 promptengine 版本。
|
|
func toPromptToolCalls(items []modeladapter.ToolCallDescriptor) []promptengine.ToolCallDescriptor {
|
|
output := make([]promptengine.ToolCallDescriptor, 0, len(items))
|
|
for _, item := range items {
|
|
output = append(output, promptengine.ToolCallDescriptor{
|
|
ID: item.ID,
|
|
Index: item.Index,
|
|
Type: item.Type,
|
|
OpenAIResponsesID: item.OpenAIResponsesID,
|
|
OpenAIResponsesCallID: item.OpenAIResponsesCallID,
|
|
OpenAIResponsesStatus: item.OpenAIResponsesStatus,
|
|
Function: promptengine.ToolCallFunctionShape{
|
|
Name: item.Function.Name,
|
|
Arguments: item.Function.Arguments,
|
|
},
|
|
})
|
|
}
|
|
return output
|
|
}
|