package session import ( "os" "path/filepath" "strings" "agent-native/runtime/llm" ) const ( // DefaultKeepToolResults 是运行内清理时保留的最近工具结果条数。 DefaultKeepToolResults = 5 // DefaultContextWindow 是未配置模型窗口时的保守字符数。 DefaultContextWindow = 32000 // DefaultOutputReserve 是留给模型输出的字符预留。 DefaultOutputReserve = 4096 // DefaultContextBuffer 是窗口估算的余量,避免贴着上限才清理。 DefaultContextBuffer = 1024 ) // EstimateChars 用字符数近似这次请求的体积。 func EstimateChars(msgs []llm.Message) int { n := 0 for _, m := range msgs { n += len([]rune(m.Content)) for _, c := range m.ToolCalls { n += len([]rune(c.Name)) + len([]rune(c.Arguments)) } } return n } // NeedsClear 在估算超过「窗口 − 输出预留 − 缓冲」时返回真。 // window ≤ 0 时用保守默认值。 func NeedsClear(estimate, window, outputReserve, buffer int) bool { if window <= 0 { window = DefaultContextWindow } room := window - outputReserve - buffer if room < 1 { room = 1 } return estimate > room } // ClearOldToolResults 把最近 keep 条之前的工具结果换成占位。 // 消息条数、顺序和 tool_call 配对都不变;占位写明完整结果仍在会话记录里。 // keep ≤ 0 时保留默认的 5 条。返回的切片不与输入共享被改写的消息。 func ClearOldToolResults(msgs []llm.Message, keep int) []llm.Message { return ClearOldToolResultsTo(msgs, keep, "") } // ClearOldToolResultsTo 在 ClearOldToolResults 的基础上支持落盘: // dir 非空时,把被清理结果的完整内容写进 /.txt, // 占位改为路径指引(ZCode 式「旧工具结果换成路径」); // 写盘失败或调用 ID 为空时回落为转录指针占位,不阻断本轮。 func ClearOldToolResultsTo(msgs []llm.Message, keep int, dir string) []llm.Message { if keep <= 0 { keep = DefaultKeepToolResults } out := append([]llm.Message{}, msgs...) var toolIdx []int for i, m := range out { if m.Role == llm.RoleTool { toolIdx = append(toolIdx, i) } } if len(toolIdx) <= keep { return out } for _, i := range toolIdx[:len(toolIdx)-keep] { name := out[i].Name if name == "" { name = "工具" } if dir != "" && out[i].ToolCallID != "" && out[i].Content != "" { if path, ok := spillResult(dir, out[i].ToolCallID, out[i].Content); ok { out[i].Content = "旧工具结果已清理。完整内容见 " + path + "(" + name + ")。" continue } } loc := "会话记录" if out[i].ToolCallID != "" { loc = "会话记录中的调用 " + out[i].ToolCallID } out[i].Content = "旧工具结果已清理。完整内容见" + loc + "(" + name + ")。" } return out } // spillResult 把一条旧工具结果写盘,返回路径。文件名只留安全字符。 func spillResult(dir, callID, content string) (string, bool) { var sb strings.Builder for _, r := range callID { switch { case r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.': sb.WriteRune(r) default: sb.WriteByte('_') } } if sb.Len() == 0 { return "", false } if err := os.MkdirAll(dir, 0o755); err != nil { return "", false } path := filepath.Join(dir, sb.String()+".txt") if err := os.WriteFile(path, []byte(content), 0o644); err != nil { return "", false } return path, true }