Files
cursor-byok/internal/backend/forwarder/tool_result_replay_truncation.go
T
2026-06-30 10:38:52 +08:00

395 lines
11 KiB
Go

package forwarder
import (
"encoding/json"
"fmt"
"strings"
"unicode/utf8"
)
const (
projectedReplayKiB = 1024
projectedReadReplayLimit = 64 * projectedReplayKiB
projectedShellReplayLimit = 128 * projectedReplayKiB
projectedShellStreamLimit = 16 * projectedReplayKiB
projectedShellInterleavedLimit = 32 * projectedReplayKiB
projectedGrepReplayLimit = 32 * projectedReplayKiB
projectedEditReplayLimit = 32 * projectedReplayKiB
projectedPatchEditReplayLimit = 4 * projectedReplayKiB
projectedWebFetchReplayLimit = 32 * projectedReplayKiB
projectedWebSearchReplayLimit = 16 * projectedReplayKiB
projectedMcpReplayLimit = 32 * projectedReplayKiB
)
func limitProjectedToolResultReplay(toolName string, content string, resultText string, fromStoredToolCall bool, historical bool) string {
if compacted, ok := compactProjectedGenerateImageResultReplay(toolName, content, resultText); ok {
return compacted
}
if historical {
if compacted, ok := compactHistoricalEditErrorReplay(toolName, content); ok {
content = compacted
fromStoredToolCall = false
}
}
if compacted, ok := compactProjectedEditToolResultReplay(toolName, content); ok {
content = compacted
fromStoredToolCall = false
}
if compacted, ok := compactProjectedShellToolResultReplay(toolName, content); ok {
content = compacted
fromStoredToolCall = false
}
limit, ok := projectedToolReplayLimit(toolName)
if !ok {
return strings.TrimSpace(content)
}
content = strings.TrimSpace(content)
if len(content) <= limit {
return content
}
if fromStoredToolCall {
fallback := strings.TrimSpace(resultText)
notice := fmt.Sprintf("[tool result replay truncated: stored ToolCall result exceeded %d bytes]", limit)
if fallback == "" {
fallback = notice
} else {
fallback += "\n\n" + notice
}
return truncateProjectedReplayText(toolName, fallback, limit)
}
return truncateProjectedReplayText(toolName, content, limit)
}
func projectedToolReplayLimit(toolName string) (int, bool) {
switch strings.TrimSpace(toolName) {
case "GenerateImage":
return projectedWebSearchReplayLimit, true
case "Read":
return projectedReadReplayLimit, true
case "Shell":
return projectedShellReplayLimit, true
case "Grep":
return projectedGrepReplayLimit, true
case "PatchEdit", "PatchEditLines", "PatchEditSpan":
return projectedPatchEditReplayLimit, true
case "Edit", "Write":
return projectedEditReplayLimit, true
case "WebFetch":
return projectedWebFetchReplayLimit, true
case "WebSearch":
return projectedWebSearchReplayLimit, true
case "CallMcpTool", "FetchMcpResource", "ListMcpResources":
return projectedMcpReplayLimit, true
default:
return 0, false
}
}
func compactProjectedGenerateImageResultReplay(toolName string, content string, resultText string) (string, bool) {
if strings.TrimSpace(toolName) != "GenerateImage" {
return "", false
}
fallback := strings.TrimSpace(resultText)
trimmed := strings.TrimSpace(content)
if trimmed == "" || trimmed == "{}" || trimmed == "null" {
if fallback == "" {
return "", false
}
return fallback, true
}
var payload any
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
if fallback == "" {
return truncateProjectedReplayText("GenerateImage", trimmed, projectedWebSearchReplayLimit), true
}
return fallback, true
}
if compactGenerateImagePayload(payload) {
encoded, err := json.Marshal(payload)
if err == nil {
return string(encoded), true
}
}
if fallback != "" {
return fallback, true
}
return trimmed, true
}
func compactGenerateImagePayload(value any) bool {
changed := false
switch item := value.(type) {
case map[string]any:
for key, child := range item {
if text, ok := child.(string); ok {
switch key {
case "image_data", "imageData":
item[key] = fmt.Sprintf("[base64 image data omitted from replay; bytes=%d]", len(strings.TrimSpace(text)))
changed = true
continue
}
}
if compactGenerateImagePayload(child) {
changed = true
}
}
case []any:
for _, child := range item {
if compactGenerateImagePayload(child) {
changed = true
}
}
}
return changed
}
func patchEditReplayLimit(toolName string) int {
switch strings.TrimSpace(toolName) {
case "PatchEdit", "PatchEditLines", "PatchEditSpan":
return projectedPatchEditReplayLimit
default:
return projectedEditReplayLimit
}
}
func compactProjectedShellToolResultReplay(toolName string, content string) (string, bool) {
if strings.TrimSpace(toolName) != "Shell" {
return "", false
}
trimmed := strings.TrimSpace(content)
if trimmed == "" || trimmed == "{}" || trimmed == "null" {
return "", false
}
var payload any
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
return "", false
}
if !compactProjectedShellFields(payload) {
return "", false
}
encoded, err := json.Marshal(payload)
if err != nil {
return "", false
}
return string(encoded), true
}
func compactProjectedShellFields(value any) bool {
changed := false
switch item := value.(type) {
case map[string]any:
for key, child := range item {
if text, ok := child.(string); ok {
switch key {
case "stdout", "stderr":
next := truncateProjectedReplayTextMiddle("Shell "+key, text, projectedShellStreamLimit)
if next != text {
item[key] = next
changed = true
}
continue
case "interleaved_output", "interleavedOutput":
next := truncateProjectedReplayTextMiddle("Shell interleaved output", text, projectedShellInterleavedLimit)
if next != text {
item[key] = next
changed = true
}
continue
}
}
if compactProjectedShellFields(child) {
changed = true
}
}
case []any:
for _, child := range item {
if compactProjectedShellFields(child) {
changed = true
}
}
}
return changed
}
func compactProjectedEditToolResultReplay(toolName string, content string) (string, bool) {
switch strings.TrimSpace(toolName) {
case "PatchEdit", "PatchEditLines", "PatchEditSpan", "Edit", "Write":
default:
return "", false
}
trimmed := strings.TrimSpace(content)
if trimmed == "" || trimmed == "{}" || trimmed == "null" {
return "", false
}
var payload map[string]any
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
return "", false
}
success, ok := payload["success"].(map[string]any)
if !ok {
return "", false
}
diffString := firstJSONText(success, "diff_string", "diffString")
beforeContent := ""
afterContent := ""
if diffString == "" {
beforeContent = firstJSONText(success, "before_full_file_content", "beforeFullFileContent")
afterContent = firstJSONText(success, "after_full_file_content", "afterFullFileContent")
if beforeContent != "" {
diffString, _, _ = computeEditDiff(beforeContent, afterContent)
}
}
if diffString != "" {
diffString = truncateProjectedReplayText(firstNonEmpty(strings.TrimSpace(toolName), "PatchEdit"), diffString, patchEditReplayLimit(toolName))
encoded, err := json.Marshal(map[string]any{
"success": map[string]any{
"diff_string": diffString,
},
})
if err != nil {
return "", false
}
return string(encoded), true
}
if afterContent != "" {
afterContent = truncateProjectedReplayText(firstNonEmpty(strings.TrimSpace(toolName), "Write"), afterContent, patchEditReplayLimit(toolName))
encoded, err := json.Marshal(map[string]any{
"success": map[string]any{
"after_full_file_content": afterContent,
},
})
if err != nil {
return "", false
}
return string(encoded), true
}
return "", false
}
func compactHistoricalEditErrorReplay(toolName string, content string) (string, bool) {
switch strings.TrimSpace(toolName) {
case "PatchEdit", "PatchEditLines", "PatchEditSpan", "Edit", "Write":
default:
return "", false
}
trimmed := strings.TrimSpace(content)
if trimmed == "" {
return "", false
}
var payload map[string]any
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
return "", false
}
errorPayload, ok := payload["error"].(map[string]any)
if !ok {
return "", false
}
if firstJSONText(errorPayload, "error", "modelVisibleError") == "" {
return "", false
}
encoded, err := json.Marshal(map[string]any{
"error": map[string]any{
"error": "historical edit error omitted from replay; re-read the file before editing",
},
})
if err != nil {
return "", false
}
return string(encoded), true
}
func firstJSONText(payload map[string]any, keys ...string) string {
for _, key := range keys {
if value, ok := payload[key].(string); ok {
return value
}
}
return ""
}
func truncateProjectedReplayText(toolName string, text string, limit int) string {
if limit <= 0 || len(text) <= limit {
return strings.TrimSpace(text)
}
original := len(text)
notice := fmt.Sprintf("\n\n[truncated: %s result exceeded %d bytes; showing %d of %d bytes]", toolName, limit, limit, original)
for {
keep := limit - len(notice)
if keep <= 0 {
return truncateProjectedUTF8(text, limit)
}
kept := truncateProjectedUTF8(text, keep)
nextNotice := fmt.Sprintf("\n\n[truncated: %s result exceeded %d bytes; showing %d of %d bytes]", toolName, limit, len(kept), original)
output := strings.TrimRight(kept, "\n") + nextNotice
if len(output) <= limit || nextNotice == notice {
return output
}
notice = nextNotice
}
}
func truncateProjectedReplayTextMiddle(toolName string, text string, limit int) string {
if limit <= 0 {
return ""
}
if len(text) <= limit {
return text
}
original := len(text)
notice := fmt.Sprintf("\n\n[truncated: %s result exceeded %d bytes; omitted middle; showing %d of %d bytes]\n\n", toolName, limit, limit, original)
for {
keep := limit - len(notice)
if keep <= 0 {
return truncateProjectedUTF8(text, limit)
}
headLimit := keep / 2
tailLimit := keep - headLimit
head := truncateProjectedUTF8(text, headLimit)
tail := truncateProjectedUTF8Suffix(text, tailLimit)
kept := len(head) + len(tail)
nextNotice := fmt.Sprintf("\n\n[truncated: %s result exceeded %d bytes; omitted middle; showing %d of %d bytes]\n\n", toolName, limit, kept, original)
output := head + nextNotice + tail
if len(output) <= limit || nextNotice == notice {
return output
}
notice = nextNotice
}
}
func truncateProjectedUTF8(text string, limit int) string {
if limit <= 0 {
return ""
}
if len(text) <= limit {
return text
}
if limit > len(text) {
limit = len(text)
}
truncated := text[:limit]
for !utf8.ValidString(truncated) && len(truncated) > 0 {
truncated = truncated[:len(truncated)-1]
}
return truncated
}
func truncateProjectedUTF8Suffix(text string, limit int) string {
if limit <= 0 {
return ""
}
if len(text) <= limit {
return text
}
start := len(text) - limit
if start < 0 {
start = 0
}
suffix := text[start:]
for !utf8.ValidString(suffix) && start < len(text) {
start++
suffix = text[start:]
}
return suffix
}