package multiagent import ( "context" "fmt" "path/filepath" "strings" "testing" "cyberstrike-ai/internal/config" localbk "github.com/cloudwego/eino-ext/adk/backend/local" "github.com/cloudwego/eino/adk" "github.com/cloudwego/eino/components/tool" "github.com/cloudwego/eino/schema" ) func TestReductionCacheRootDir(t *testing.T) { got := reductionCacheRootDir("", "proj-1", "conv-1") want := filepath.Join("tmp", "reduction", "projects", "proj-1") if got != want { t.Fatalf("project scope: got %q want %q", got, want) } got = reductionCacheRootDir("", "", "conv-abc") want = filepath.Join("tmp", "reduction", "conversations", "conv-abc") if got != want { t.Fatalf("conversation scope: got %q want %q", got, want) } custom := reductionCacheRootDir("/data/cache", "p1", "c1") if !strings.HasSuffix(custom, filepath.Join("projects", "p1")) { t.Fatalf("custom base should still scope by project, got %q", custom) } } func TestBuildAgenticReductionMiddlewareClearsOldAgenticToolResult(t *testing.T) { ctx := context.Background() loc, err := localbk.NewBackend(ctx, &localbk.Config{}) if err != nil { t.Fatalf("NewBackend: %v", err) } root := t.TempDir() mw, err := buildAgenticReductionMiddleware(ctx, config.MultiAgentEinoMiddlewareConfig{ ReductionRootDir: root, ReductionMaxTokensForClear: 1, }, "", "conv-1", loc, nil) if err != nil { t.Fatalf("buildAgenticReductionMiddleware: %v", err) } oldText := strings.Repeat("old-tool-output-", 20) newText := strings.Repeat("new-tool-output-", 20) state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{ Messages: []*schema.AgenticMessage{ agenticAssistantToolCall("old-call", "execute", `{"command":"old"}`), agenticToolResult("old-call", "execute", oldText), agenticAssistantToolCall("new-call", "execute", `{"command":"new"}`), agenticToolResult("new-call", "execute", newText), }, } _, out, err := mw.BeforeModelRewriteState(ctx, state, nil) if err != nil { t.Fatalf("BeforeModelRewriteState: %v", err) } oldGot := out.Messages[1].ContentBlocks[0].FunctionToolResult.Content[0].Text.Text newGot := out.Messages[3].ContentBlocks[0].FunctionToolResult.Content[0].Text.Text if oldGot == oldText { t.Fatal("agentic reduction did not clear old oversized tool result") } if !strings.Contains(oldGot, "read_file") { t.Fatalf("cleared content should mention read_file, got %q", oldGot) } if newGot != newText { t.Fatalf("latest tool result should be retained, got %q", newGot) } } func agenticAssistantToolCall(callID, name, arguments string) *schema.AgenticMessage { return &schema.AgenticMessage{ Role: schema.AgenticRoleTypeAssistant, ContentBlocks: []*schema.ContentBlock{schema.NewContentBlock(&schema.FunctionToolCall{ CallID: callID, Name: name, Arguments: arguments, })}, } } func agenticToolResult(callID, name, text string) *schema.AgenticMessage { return &schema.AgenticMessage{ Role: schema.AgenticRoleTypeUser, ContentBlocks: []*schema.ContentBlock{schema.NewContentBlock(&schema.FunctionToolResult{ CallID: callID, Name: name, Content: []*schema.FunctionToolResultContentBlock{{ Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: text}, }}, })}, } } func TestBuildAgenticReductionMiddlewareHandlesSingleAgenticToolResult(t *testing.T) { ctx := context.Background() loc, err := localbk.NewBackend(ctx, &localbk.Config{}) if err != nil { t.Fatalf("NewBackend: %v", err) } mw, err := buildAgenticReductionMiddleware(ctx, config.MultiAgentEinoMiddlewareConfig{ ReductionRootDir: t.TempDir(), ReductionMaxTokensForClear: 1, }, "", "conv-1", loc, nil) if err != nil { t.Fatalf("buildAgenticReductionMiddleware: %v", err) } state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{ Messages: []*schema.AgenticMessage{ { Role: schema.AgenticRoleTypeUser, ContentBlocks: []*schema.ContentBlock{schema.NewContentBlock(&schema.FunctionToolResult{ CallID: "call-1", Name: "execute", Content: []*schema.FunctionToolResultContentBlock{{ Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: strings.Repeat("tool-output-", 20)}, }}, })}, }, }, } _, out, err := mw.BeforeModelRewriteState(ctx, state, nil) if err != nil { t.Fatalf("BeforeModelRewriteState: %v", err) } got := out.Messages[0].ContentBlocks[0].FunctionToolResult.Content[0].Text.Text if got != strings.Repeat("tool-output-", 20) { t.Fatalf("single retained tool result should not be cleared, got %q", got) } } func TestPrependEinoAgenticMiddlewaresRespectsReductionPlacement(t *testing.T) { ctx := context.Background() loc, err := localbk.NewBackend(ctx, &localbk.Config{}) if err != nil { t.Fatalf("NewBackend: %v", err) } patchToolCalls := false mw := &config.MultiAgentEinoMiddlewareConfig{ ReductionEnable: true, ReductionRootDir: t.TempDir(), ReductionMaxTokensForClear: 100, PatchToolCalls: &patchToolCalls, } _, mainHandlers, _, err := prependEinoAgenticMiddlewares(ctx, mw, einoMWMain, nil, loc, "", "conv-1", "", nil) if err != nil { t.Fatalf("prepend main: %v", err) } if len(mainHandlers) != 1 { t.Fatalf("main handlers = %d, want reduction", len(mainHandlers)) } _, subHandlers, _, err := prependEinoAgenticMiddlewares(ctx, mw, einoMWSub, nil, loc, "", "conv-1", "", nil) if err != nil { t.Fatalf("prepend sub: %v", err) } if len(subHandlers) != 0 { t.Fatalf("sub handlers = %d, want skipped when reduction_sub_agents=false", len(subHandlers)) } mw.ReductionSubAgents = true _, subHandlers, _, err = prependEinoAgenticMiddlewares(ctx, mw, einoMWSub, nil, loc, "", "conv-1", "", nil) if err != nil { t.Fatalf("prepend sub enabled: %v", err) } if len(subHandlers) != 1 { t.Fatalf("sub handlers = %d, want reduction when reduction_sub_agents=true", len(subHandlers)) } } func TestPrependEinoAgenticMiddlewaresMountsToolSearchAndPatchToolCalls(t *testing.T) { ctx := context.Background() mw := &config.MultiAgentEinoMiddlewareConfig{ ToolSearchEnable: true, ToolSearchMinTools: 20, ToolSearchAlwaysVisible: 5, } outTools, handlers, toolSearchActive, err := prependEinoAgenticMiddlewares(ctx, mw, einoMWMain, stubTools(25), nil, "", "conv-test", "", nil) if err != nil { t.Fatalf("prependEinoAgenticMiddlewares: %v", err) } if !toolSearchActive { t.Fatal("agentic tool_search should be active") } if len(outTools) != 5 { t.Fatalf("mounted tools = %d, want static visible tools only", len(outTools)) } if len(handlers) != 2 { t.Fatalf("handlers = %d, want patchtoolcalls + toolsearch", len(handlers)) } } type stubTool struct{ name string } func (s stubTool) Info(_ context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{Name: s.name}, nil } func TestSplitToolsForToolSearch(t *testing.T) { mk := func(n int) []tool.BaseTool { out := make([]tool.BaseTool, n) for i := 0; i < n; i++ { out[i] = stubTool{name: fmt.Sprintf("t%d", i)} } return out } static, dynamic, ok := splitToolsForToolSearch(mk(4), 3) if ok || len(static) != 4 || dynamic != nil { t.Fatalf("expected no split when len<=alwaysVisible+1, got ok=%v static=%d dynamic=%v", ok, len(static), dynamic) } static, dynamic, ok = splitToolsForToolSearch(mk(20), 5) if !ok || len(static) != 5 || len(dynamic) != 15 { t.Fatalf("expected split 5+15, got ok=%v static=%d dynamic=%d", ok, len(static), len(dynamic)) } }