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:
logan committed 2026-10-15 13:58:10 +08:00
1 parent 0aeee2b692
commit 5416e86379
17 files changed
+1041 -61

No files matched your search

+165
View File
@@ -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
}