feat(ai): session harness contract — incomplete, run_id, confirm bind
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.
This commit is contained in:
1 parent
0aeee2b692
commit
5416e86379
17 files changed
+1041
-61
No files matched your search
@@ -0,0 +1,165 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user