Files
Agent-Native/runtime/session/microcompact.go
T
weichen ae42c9402d
ci / vet-and-test (pull_request) Successful in 52s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Spill cleared tool results to disk and pin the cache-stability contract (N2 WP1)
Why: the completion doc's N2 requires old tool results to become file
paths (ZCode compact pattern) and cache-friendly prefix rules; today the
micro-compactor only left a transcript pointer and nothing pinned prefix
stability. alignToolTail had no direct edge tests.
Evidence: GOWORK=off go vet + go test -count=1 ./... green; new
TestClearOldToolResultsSpillsToDisk / TestSpillResultSanitizesCallID /
TestAlignToolTailEdges / TestSystemTextDeterministicAcrossIdenticalBuilds;
legacy placeholder bytes unchanged when ResultsDir is empty
(TestClearOldToolResultsEmptyDirKeepsLegacyPlaceholder).
Impact: opt-in via Config.ResultsDir; the native adapter wires it to
<workDir>/.agentx/results. No behavior change for other hosts.
Refs: 2026-10-07-agent-stack-n2-plan WP1
2026-10-07 12:59:05 +00:00

116 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 非空时,把被清理结果的完整内容写进 <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
}