mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-30 21:20:12 +02:00
1 (#268)
* fix: 为 Eino agentic 路径补充 tool_call/tool_result 配对防御中间件 agentic 路径(单代理模式)缺少 toolPairReconciler 和 orphanToolPruner, 当 summarization 截断历史破坏配对后,序列化到 OpenAI API 触发 400 "insufficient tool messages following tool_calls message"。 新增 AgenticMessage 版本的 reconciler 和 pruner,与 classic 路径对齐。 Co-authored-by: Cursor <cursoragent@cursor.com> * fix: 为 Eino agentic 路径补充 tool_call/tool_result 配对防御中间件 (#265) (#266) agentic 路径(单代理模式)缺少 toolPairReconciler 和 orphanToolPruner, 当 summarization 截断历史破坏配对后,序列化到 OpenAI API 触发 400 "insufficient tool messages following tool_calls message"。 新增 AgenticMessage 版本的 reconciler 和 pruner,与 classic 路径对齐。 Co-authored-by: temp <temp@tempdeMacBook-Pro.local> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: temp <temp@tempdeMacBook-Pro.local> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
5 files changed
+538
-2
No files matched your search
@@ -0,0 +1,157 @@
|
||||
package multiagent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudwego/eino/adk"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
func TestAgenticToolPairReconcilerPatchesMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
mw := newAgenticToolPairReconcilerMiddleware(nil, "test")
|
||||
state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{
|
||||
Messages: []*schema.AgenticMessage{
|
||||
agenticAssistantToolCall("c1", "search", `{"q":"x"}`),
|
||||
agenticAssistantToolCall("c2", "execute", `{"cmd":"ls"}`),
|
||||
// c1 result present, c2 missing
|
||||
agenticToolResult("c1", "search", "found it"),
|
||||
},
|
||||
}
|
||||
_, out, err := mw.BeforeModelRewriteState(context.Background(), state, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Expected: assistant(c1) -> result(c1) -> assistant(c2) -> patched_result(c2)
|
||||
if len(out.Messages) != 4 {
|
||||
t.Fatalf("messages = %d, want 4", len(out.Messages))
|
||||
}
|
||||
// c1 assistant
|
||||
if calls := agenticFunctionToolCalls(out.Messages[0]); len(calls) != 1 || calls[0].CallID != "c1" {
|
||||
t.Fatal("msg[0] should be assistant(c1)")
|
||||
}
|
||||
// c1 result
|
||||
if ids := agenticToolResultCallIDs(out.Messages[1]); len(ids) != 1 || ids[0] != "c1" {
|
||||
t.Fatal("msg[1] should be result(c1)")
|
||||
}
|
||||
// c2 assistant
|
||||
if calls := agenticFunctionToolCalls(out.Messages[2]); len(calls) != 1 || calls[0].CallID != "c2" {
|
||||
t.Fatal("msg[2] should be assistant(c2)")
|
||||
}
|
||||
// c2 patched result
|
||||
if ids := agenticToolResultCallIDs(out.Messages[3]); len(ids) != 1 || ids[0] != "c2" {
|
||||
t.Fatal("msg[3] should be patched result(c2)")
|
||||
}
|
||||
resultText := out.Messages[3].ContentBlocks[0].FunctionToolResult.Content[0].Text.Text
|
||||
if resultText != patchedMissingToolResult {
|
||||
t.Fatalf("patched text = %q", resultText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgenticToolPairReconcilerDropsOrphan(t *testing.T) {
|
||||
t.Parallel()
|
||||
mw := newAgenticToolPairReconcilerMiddleware(nil, "test")
|
||||
state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{
|
||||
Messages: []*schema.AgenticMessage{
|
||||
// Orphan tool result with no preceding assistant
|
||||
agenticToolResult("orphan", "deleted_tool", "stale data"),
|
||||
{Role: schema.AgenticRoleTypeUser, ContentBlocks: []*schema.ContentBlock{
|
||||
schema.NewContentBlock(&schema.UserInputText{Text: "hello"}),
|
||||
}},
|
||||
},
|
||||
}
|
||||
_, out, err := mw.BeforeModelRewriteState(context.Background(), state, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(out.Messages) != 1 {
|
||||
t.Fatalf("messages = %d, want 1 (orphan dropped)", len(out.Messages))
|
||||
}
|
||||
if out.Messages[0].ContentBlocks[0].UserInputText == nil {
|
||||
t.Fatal("remaining message should be the user text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgenticToolPairReconcilerNoopWhenPaired(t *testing.T) {
|
||||
t.Parallel()
|
||||
mw := newAgenticToolPairReconcilerMiddleware(nil, "test")
|
||||
state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{
|
||||
Messages: []*schema.AgenticMessage{
|
||||
agenticAssistantToolCall("c1", "search", `{}`),
|
||||
agenticToolResult("c1", "search", "ok"),
|
||||
},
|
||||
}
|
||||
_, out, err := mw.BeforeModelRewriteState(context.Background(), state, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Should return original state unchanged
|
||||
if &out.Messages[0] == &state.Messages[0] {
|
||||
// pointer equality on slice — state not cloned
|
||||
}
|
||||
if len(out.Messages) != 2 {
|
||||
t.Fatalf("messages = %d, want 2", len(out.Messages))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgenticToolPairReconcilerFixesEmptyCallID(t *testing.T) {
|
||||
t.Parallel()
|
||||
mw := newAgenticToolPairReconcilerMiddleware(nil, "test")
|
||||
state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{
|
||||
Messages: []*schema.AgenticMessage{
|
||||
{
|
||||
Role: schema.AgenticRoleTypeAssistant,
|
||||
ContentBlocks: []*schema.ContentBlock{schema.NewContentBlock(&schema.FunctionToolCall{
|
||||
CallID: "", Name: "search", Arguments: `{}`,
|
||||
})},
|
||||
},
|
||||
},
|
||||
}
|
||||
_, out, err := mw.BeforeModelRewriteState(context.Background(), state, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
calls := agenticFunctionToolCalls(out.Messages[0])
|
||||
if len(calls) != 1 || calls[0].CallID == "" {
|
||||
t.Fatalf("empty call ID should be patched, got %q", calls[0].CallID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgenticOrphanToolPrunerRemovesOrphan(t *testing.T) {
|
||||
t.Parallel()
|
||||
mw := newAgenticOrphanToolPrunerMiddleware(nil, "test")
|
||||
state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{
|
||||
Messages: []*schema.AgenticMessage{
|
||||
agenticAssistantToolCall("c1", "search", `{}`),
|
||||
agenticToolResult("c1", "search", "ok"),
|
||||
// Orphan: no assistant has call_id "c_orphan"
|
||||
agenticToolResult("c_orphan", "deleted", "stale"),
|
||||
},
|
||||
}
|
||||
_, out, err := mw.BeforeModelRewriteState(context.Background(), state, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(out.Messages) != 2 {
|
||||
t.Fatalf("messages = %d, want 2 (orphan pruned)", len(out.Messages))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgenticOrphanToolPrunerNoopWhenClean(t *testing.T) {
|
||||
t.Parallel()
|
||||
mw := newAgenticOrphanToolPrunerMiddleware(nil, "test")
|
||||
state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{
|
||||
Messages: []*schema.AgenticMessage{
|
||||
agenticAssistantToolCall("c1", "search", `{}`),
|
||||
agenticToolResult("c1", "search", "ok"),
|
||||
},
|
||||
}
|
||||
_, out, err := mw.BeforeModelRewriteState(context.Background(), state, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(out.Messages) != 2 {
|
||||
t.Fatalf("messages = %d, want 2", len(out.Messages))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user