Merge pull request 'Align context trimming to tool groups and take the N2 WP1 pins' (#86) from feature/n2-context-wp1 into main
ci / vet-and-test (push) Successful in 42s
ci / vet-and-test (push) Successful in 42s
Align context trimming to tool groups and take the N2 WP1 pins
This commit was merged in pull request #86.
This commit is contained in:
Vendored
+1
-1
Submodule deps/agent-native updated: ead690be30...64b8bd4c98
Vendored
+1
-1
Submodule deps/agent-squad updated: 029edc08e7...56941bf8ea
@@ -207,12 +207,12 @@ func trimOldest(msgs []agentllm.Message, window, reserve int, rep *Report) []age
|
||||
}
|
||||
dropped := 0
|
||||
for estimateAll(msgs) > limit {
|
||||
idx := oldestDrop(msgs)
|
||||
if idx < 0 {
|
||||
start, end := oldestDropRange(msgs)
|
||||
if start < 0 {
|
||||
break
|
||||
}
|
||||
msgs = append(msgs[:idx], msgs[idx+1:]...)
|
||||
dropped++
|
||||
dropped += end - start
|
||||
msgs = append(msgs[:start], msgs[end:]...)
|
||||
}
|
||||
if dropped > 0 {
|
||||
rep.Truncations = append(rep.Truncations, fmtDropped(dropped))
|
||||
@@ -226,9 +226,14 @@ func fmtDropped(n int) string {
|
||||
|
||||
func itoa(n int) string { return strconv.Itoa(n) }
|
||||
|
||||
func oldestDrop(msgs []agentllm.Message) int {
|
||||
// oldestDropRange 返回下一次丢弃的区间 [start, end)(找不到返回 -1,-1)。
|
||||
// 丢弃按工具组对齐:带 tool_calls 的 assistant 与其连续 tool 结果同退,
|
||||
// 开头的孤立 tool 结果组整组同退——裁切后既不剩无结果的调用,
|
||||
// 也不剩无调用的结果(与库层 alignToolTail 同语义、方向相反)。
|
||||
// 组越过最后一条(当前轮)时不再丢弃:窗口允许略超,配对不可破坏。
|
||||
func oldestDropRange(msgs []agentllm.Message) (int, int) {
|
||||
if len(msgs) < 3 {
|
||||
return -1
|
||||
return -1, -1
|
||||
}
|
||||
// 保留开头的系统前缀和最后一条(当前轮)。
|
||||
start := 0
|
||||
@@ -236,9 +241,26 @@ func oldestDrop(msgs []agentllm.Message) int {
|
||||
start++
|
||||
}
|
||||
if start >= len(msgs)-1 {
|
||||
return -1
|
||||
return -1, -1
|
||||
}
|
||||
return start
|
||||
end := start + 1
|
||||
m := msgs[start]
|
||||
switch {
|
||||
case m.Role == agentllm.RoleAssistant && len(m.ToolCalls) > 0:
|
||||
// assistant 及其连续 tool 结果整组同退。
|
||||
for end < len(msgs) && msgs[end].Role == agentllm.RoleTool {
|
||||
end++
|
||||
}
|
||||
case m.Role == agentllm.RoleTool:
|
||||
// 开头即孤立 tool 结果(其 assistant 必已被丢或不存在):整组同退。
|
||||
for end < len(msgs) && msgs[end].Role == agentllm.RoleTool {
|
||||
end++
|
||||
}
|
||||
}
|
||||
if end > len(msgs)-1 {
|
||||
return -1, -1
|
||||
}
|
||||
return start, end
|
||||
}
|
||||
|
||||
func estimateAll(msgs []agentllm.Message) int {
|
||||
|
||||
@@ -62,3 +62,118 @@ func TestTrimDropsOldestHistory(t *testing.T) {
|
||||
t.Fatal("expected truncation")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- N2 WP1:裁切工具组对齐(验收② 产品侧机器证据)----
|
||||
|
||||
// assertNoOrphanToolPairs 双向断言:每个 tool_call 有结果,每个结果有调用。
|
||||
func assertNoOrphanToolPairs(t *testing.T, msgs []agentllm.Message) {
|
||||
t.Helper()
|
||||
calls := map[string]bool{}
|
||||
results := map[string]bool{}
|
||||
for _, m := range msgs {
|
||||
for _, c := range m.ToolCalls {
|
||||
calls[c.ID] = true
|
||||
}
|
||||
if m.Role == agentllm.RoleTool && m.ToolCallID != "" {
|
||||
results[m.ToolCallID] = true
|
||||
}
|
||||
}
|
||||
for id := range calls {
|
||||
if !results[id] {
|
||||
t.Fatalf("孤立 tool_call:%s(无结果)", id)
|
||||
}
|
||||
}
|
||||
for id := range results {
|
||||
if !calls[id] {
|
||||
t.Fatalf("悬空 tool 结果:%s(无调用)", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toolMsgs() []agentllm.Message {
|
||||
call := func(id, name string) agentllm.ToolCall {
|
||||
return agentllm.ToolCall{ID: id, Name: name, Arguments: "{}"}
|
||||
}
|
||||
return []agentllm.Message{
|
||||
{Role: agentllm.RoleSystem, Content: "s"},
|
||||
{Role: agentllm.RoleUser, Content: strings.Repeat("旧一", 300)},
|
||||
{Role: agentllm.RoleAssistant, ToolCalls: []agentllm.ToolCall{call("tc1", "read")}},
|
||||
{Role: agentllm.RoleTool, ToolCallID: "tc1", Name: "read", Content: strings.Repeat("结果一", 200)},
|
||||
{Role: agentllm.RoleUser, Content: strings.Repeat("旧二", 300)},
|
||||
{Role: agentllm.RoleAssistant, ToolCalls: []agentllm.ToolCall{call("tc2", "grep"), call("tc3", "read")}},
|
||||
{Role: agentllm.RoleTool, ToolCallID: "tc2", Name: "grep", Content: strings.Repeat("结果二", 200)},
|
||||
{Role: agentllm.RoleTool, ToolCallID: "tc3", Name: "read", Content: strings.Repeat("结果三", 200)},
|
||||
{Role: agentllm.RoleUser, Content: "现在"},
|
||||
}
|
||||
}
|
||||
|
||||
// 连续多轮裁切后配对仍成立:assistant 与其结果同退,不逐条拆散。
|
||||
func TestTrimOldestKeepsToolPairs(t *testing.T) {
|
||||
out, rep := FinishAssembly(toolMsgs(), &Report{}, Tail{Window: 1600})
|
||||
assertNoOrphanToolPairs(t, out)
|
||||
if len(rep.Truncations) == 0 {
|
||||
t.Fatal("expected truncation")
|
||||
}
|
||||
if out[len(out)-1].Content != "现在" {
|
||||
t.Fatal("当前轮必须保留")
|
||||
}
|
||||
// 窗口再压:旧二组也须整组同退,仍不出现孤立配对。
|
||||
out2, _ := FinishAssembly(toolMsgs(), &Report{}, Tail{Window: 900})
|
||||
assertNoOrphanToolPairs(t, out2)
|
||||
}
|
||||
|
||||
// 开头即孤立 tool 结果组:整组同退(其 assistant 已不存在)。
|
||||
func TestTrimOldestDropsLeadingOrphanToolRun(t *testing.T) {
|
||||
msgs := []agentllm.Message{
|
||||
{Role: agentllm.RoleSystem, Content: "s"},
|
||||
{Role: agentllm.RoleTool, ToolCallID: "ghost", Name: "read", Content: strings.Repeat("孤", 500)},
|
||||
{Role: agentllm.RoleUser, Content: strings.Repeat("旧", 500)},
|
||||
{Role: agentllm.RoleUser, Content: "现在"},
|
||||
}
|
||||
out, _ := FinishAssembly(msgs, &Report{}, Tail{Window: 700})
|
||||
assertNoOrphanToolPairs(t, out)
|
||||
for _, m := range out {
|
||||
if m.ToolCallID == "ghost" {
|
||||
t.Fatal("孤立结果组应整组丢弃")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具组越过当前轮时不再丢弃:窗口可略超,配对不可破坏。
|
||||
func TestTrimOldestStopsWhenGroupCrossesCurrentTurn(t *testing.T) {
|
||||
msgs := []agentllm.Message{
|
||||
{Role: agentllm.RoleSystem, Content: "s"},
|
||||
{Role: agentllm.RoleAssistant, ToolCalls: []agentllm.ToolCall{{ID: "tc9", Name: "read", Arguments: "{}"}}},
|
||||
{Role: agentllm.RoleTool, ToolCallID: "tc9", Name: "read", Content: strings.Repeat("结果", 900)},
|
||||
}
|
||||
start, end := oldestDropRange(msgs)
|
||||
if start != -1 || end != -1 {
|
||||
t.Fatalf("组越过当前轮应返回 -1,-1,得到 %d,%d", start, end)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- N2 WP1:PrefixHash 稳定契约(同一输入 → 同一前缀哈希)----
|
||||
|
||||
func TestPrefixHashStableForIdenticalInput(t *testing.T) {
|
||||
build := func() []agentllm.Message {
|
||||
return []agentllm.Message{
|
||||
{Role: agentllm.RoleSystem, Content: "系统前缀A"},
|
||||
{Role: agentllm.RoleSystem, Content: "系统前缀B"},
|
||||
{Role: agentllm.RoleUser, Content: "历史一"},
|
||||
{Role: agentllm.RoleAssistant, Content: "回复一"},
|
||||
{Role: agentllm.RoleUser, Content: "现在"},
|
||||
}
|
||||
}
|
||||
_, rep1 := FinishAssembly(build(), &Report{}, Tail{Instructions: "个人指令"})
|
||||
_, rep2 := FinishAssembly(build(), &Report{}, Tail{Instructions: "个人指令"})
|
||||
if rep1.PrefixHash == "" || rep1.PrefixHash != rep2.PrefixHash {
|
||||
t.Fatalf("同一输入的前缀哈希必须一致:%q vs %q", rep1.PrefixHash, rep2.PrefixHash)
|
||||
}
|
||||
// 前缀变化必须改变哈希(钉住哈希语义,防退化为常量)。
|
||||
other := build()
|
||||
other[0].Content = "系统前缀X"
|
||||
_, rep3 := FinishAssembly(other, &Report{}, Tail{Instructions: "个人指令"})
|
||||
if rep3.PrefixHash == rep1.PrefixHash {
|
||||
t.Fatal("前缀变化后哈希必须不同")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user