Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements. Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate. Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable. Refs: Versioned dependencies U1/U3; feature/dependency-workspace
116 lines
3.4 KiB
Go
116 lines
3.4 KiB
Go
package session
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/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 非空时,把被清理结果的完整内容写进 <dir>/<toolCallID>.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
|
||
}
|