Add docs/16 for session state machine, incomplete/resume, queue semantics, and agent-loop/harness/MCP gap review. Wire RunOutcome on max-rounds, run_id through audit, TrimMessages/Redact hooks, ConfirmBindHash on write confirm path; session file skeleton without HTTP yet. No push.
166 lines
3.9 KiB
Go
166 lines
3.9 KiB
Go
package ai
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"strings"
|
||
)
|
||
|
||
// HarnessConfig Agent 运行时预算(桌面裁剪版;不做 Evolver)。
|
||
type HarnessConfig struct {
|
||
MaxRounds int // 默认 8,硬帽 16
|
||
MaxToolResultChars int // 回灌截断
|
||
MaxContextTokens int // trim 预算(粗估字符/4)
|
||
SameCallLimit int // 同工具同参上限;0=关闭
|
||
MinRetainMessages int // trim 至少保留条数(不含 system)
|
||
}
|
||
|
||
// DefaultHarness 与历史 agent.go 常量对齐。
|
||
func DefaultHarness() HarnessConfig {
|
||
return HarnessConfig{
|
||
MaxRounds: defaultAgentMaxRounds,
|
||
MaxToolResultChars: maxToolResultFeedChars,
|
||
MaxContextTokens: 8000,
|
||
SameCallLimit: 3,
|
||
MinRetainMessages: 4,
|
||
}
|
||
}
|
||
|
||
func (h HarnessConfig) ClampRounds(n int) int {
|
||
if n <= 0 {
|
||
n = h.MaxRounds
|
||
if n <= 0 {
|
||
n = defaultAgentMaxRounds
|
||
}
|
||
}
|
||
if n > 16 {
|
||
n = 16
|
||
}
|
||
return n
|
||
}
|
||
|
||
// NewRunID 生成短 run_id,贯穿审计与步骤。
|
||
func NewRunID() string {
|
||
var b [8]byte
|
||
_, _ = rand.Read(b[:])
|
||
return "run_" + hex.EncodeToString(b[:])
|
||
}
|
||
|
||
// EstimateTokens 粗估(字符/4);含 tool_calls JSON。
|
||
func EstimateTokens(msgs []Message) int {
|
||
n := 0
|
||
for _, m := range msgs {
|
||
n += len(m.Content)
|
||
if len(m.ToolCalls) > 0 {
|
||
if b, err := json.Marshal(m.ToolCalls); err == nil {
|
||
n += len(b)
|
||
}
|
||
}
|
||
n += len(m.Name) + len(m.ToolCallID) + 8
|
||
}
|
||
if n < 0 {
|
||
return 0
|
||
}
|
||
return n / 4
|
||
}
|
||
|
||
// TrimMessages 保留 system + 最近消息,控制在 MaxContextTokens*90%;
|
||
// 并按 tool_call_id 整组保留 assistant.tool_calls 与对应 tool。
|
||
func TrimMessages(msgs []Message, h HarnessConfig) []Message {
|
||
if len(msgs) == 0 {
|
||
return msgs
|
||
}
|
||
budget := h.MaxContextTokens
|
||
if budget <= 0 {
|
||
budget = 8000
|
||
}
|
||
limit := budget * 90 / 100
|
||
minRetain := h.MinRetainMessages
|
||
if minRetain <= 0 {
|
||
minRetain = 4
|
||
}
|
||
|
||
var system []Message
|
||
var rest []Message
|
||
for _, m := range msgs {
|
||
if m.Role == "system" {
|
||
system = append(system, m)
|
||
} else {
|
||
rest = append(rest, m)
|
||
}
|
||
}
|
||
if EstimateTokens(msgs) <= limit {
|
||
return ensureToolPairing(msgs)
|
||
}
|
||
|
||
// 从尾部向前取,直到接近预算
|
||
var kept []Message
|
||
for i := len(rest) - 1; i >= 0; i-- {
|
||
candidate := append([]Message{}, rest[i:]...)
|
||
candidate = append(append([]Message{}, system...), candidate...)
|
||
if EstimateTokens(candidate) > limit && len(kept) >= minRetain {
|
||
break
|
||
}
|
||
kept = rest[i:]
|
||
if len(kept) >= minRetain && EstimateTokens(candidate) <= limit {
|
||
break
|
||
}
|
||
}
|
||
if len(kept) == 0 && len(rest) > 0 {
|
||
// 至少留最后一条
|
||
kept = rest[len(rest)-1:]
|
||
}
|
||
out := append(append([]Message{}, system...), kept...)
|
||
return ensureToolPairing(out)
|
||
}
|
||
|
||
// ensureToolPairing 丢弃没有对应 assistant tool_calls 的孤立 tool,以及裁切后半截的 tool_calls。
|
||
func ensureToolPairing(msgs []Message) []Message {
|
||
toolIDs := map[string]bool{}
|
||
for _, m := range msgs {
|
||
if m.Role == "assistant" {
|
||
for _, tc := range m.ToolCalls {
|
||
if tc.ID != "" {
|
||
toolIDs[tc.ID] = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out := make([]Message, 0, len(msgs))
|
||
for _, m := range msgs {
|
||
if m.Role == "tool" {
|
||
if m.ToolCallID == "" || !toolIDs[m.ToolCallID] {
|
||
continue
|
||
}
|
||
}
|
||
if m.Role == "assistant" && len(m.ToolCalls) > 0 {
|
||
// 若后续缺少任一 tool 结果,整组 assistant 仍保留(由调用方补 incomplete);
|
||
// 这里只滤 tool 侧孤儿。
|
||
}
|
||
out = append(out, m)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ToolCallFingerprint 同参循环检测键。
|
||
func ToolCallFingerprint(name, argsJSON string) string {
|
||
name = strings.TrimSpace(name)
|
||
argsJSON = strings.Join(strings.Fields(argsJSON), " ")
|
||
return name + "\n" + argsJSON
|
||
}
|
||
|
||
// DetectRepeatedToolCall 若同一指纹已出现 limit 次(不含本次),返回 true。
|
||
func DetectRepeatedToolCall(history []string, fp string, limit int) bool {
|
||
if limit <= 0 || fp == "" {
|
||
return false
|
||
}
|
||
n := 0
|
||
for _, h := range history {
|
||
if h == fp {
|
||
n++
|
||
}
|
||
}
|
||
return n >= limit
|
||
}
|