// Package context - grouping.go 将消息按 API 往返分组. // // 精妙之处(CLEVER): 按 API 往返分组保证压缩边界在完整对话轮次之间, // 不会出现"保留了 user 消息但丢了 assistant 回复"的破碎状态. package context import "encoding/json" // MessageGroup 是按 API 往返分组的消息组. type MessageGroup struct { Index int // 分组编号(0 = 前言) Messages []CompactMessage // 这一轮的所有消息 Tokens int // 估算 token 数 } // GroupByAPIRound 将消息按 API 往返分组. // Group 0: 前言(系统消息,初始附件--即第一个 assistant 之前的所有 user 消息) // Group 1+: 每轮 assistant + 后续 user(tool_results 等) // // 分组逻辑: // - 遇到 assistant 消息开始新组 // - 同一组内包含 assistant 消息和紧随其后的 user 消息(通常是 tool_result) // - 这样保证每组是一个完整的 API 往返 func GroupByAPIRound(messages []CompactMessage) []MessageGroup { if len(messages) == 0 { return nil } var groups []MessageGroup groupIdx := 0 // Group 0: 第一个 assistant 消息之前的所有消息 var preamble []CompactMessage i := 0 for i < len(messages) && messages[i].Role != "assistant" { preamble = append(preamble, messages[i]) i++ } if len(preamble) > 0 { groups = append(groups, MessageGroup{ Index: groupIdx, Messages: preamble, Tokens: EstimateTokens(preamble), }) groupIdx++ } // Group 1+: 每个 assistant 消息开始一个新组,包括后续的 user 消息 for i < len(messages) { var group []CompactMessage // 收集 assistant 消息 if messages[i].Role == "assistant" { group = append(group, messages[i]) i++ } // 收集后续的 user 消息(tool_result 等),直到遇到下一个 assistant for i < len(messages) && messages[i].Role != "assistant" { group = append(group, messages[i]) i++ } if len(group) > 0 { groups = append(groups, MessageGroup{ Index: groupIdx, Messages: group, Tokens: EstimateTokens(group), }) groupIdx++ } } return groups } // DetectOrphanedToolResults 检测孤立的 tool_result(无对应 tool_use). // 返回孤立 tool_result 的 tool_use_id 列表. // // 孤立 tool_result 可能在压缩时因截断位置不当产生-- // 比如 assistant 的 tool_use 被压缩了但对应的 user tool_result 保留了. func DetectOrphanedToolResults(messages []CompactMessage) []string { // 收集所有 tool_use ID toolUseIDs := make(map[string]bool) for _, msg := range messages { if msg.Role != "assistant" { continue } var blocks []map[string]any if err := json.Unmarshal(msg.Content, &blocks); err != nil { continue } for _, block := range blocks { if blockType, _ := block["type"].(string); blockType == "tool_use" { if id, _ := block["id"].(string); id != "" { toolUseIDs[id] = true } } } } // 检查所有 tool_result 是否有对应的 tool_use var orphaned []string for _, msg := range messages { if msg.Role != "user" { continue } var blocks []map[string]any if err := json.Unmarshal(msg.Content, &blocks); err != nil { continue } for _, block := range blocks { if blockType, _ := block["type"].(string); blockType == "tool_result" { if id, _ := block["tool_use_id"].(string); id != "" { if !toolUseIDs[id] { orphaned = append(orphaned, id) } } } } } return orphaned }