mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-21 10:27:31 +02:00
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>
158 lines
5.0 KiB
Go
158 lines
5.0 KiB
Go
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))
|
|
}
|
|
}
|