diff --git a/go/core/internal/controller/translator/agent/compiler.go b/go/core/internal/controller/translator/agent/compiler.go index 9596e28db5..2e51e426b4 100644 --- a/go/core/internal/controller/translator/agent/compiler.go +++ b/go/core/internal/controller/translator/agent/compiler.go @@ -34,7 +34,7 @@ type tState struct { func (s *tState) with(agent v1alpha2.AgentObject) *tState { visited := make([]string, len(s.visitedAgents), len(s.visitedAgents)+1) copy(visited, s.visitedAgents) - visited = append(visited, utils.GetObjectRef(agent)) + visited = append(visited, agentStateKey(agent)) return &tState{ depth: s.depth + 1, visitedAgents: visited, @@ -45,6 +45,59 @@ func (t *tState) isVisited(agentName string) bool { return slices.Contains(t.visitedAgents, agentName) } +// agentObjectKind returns the Kubernetes kind backing an AgentObject. +func agentObjectKind(agent v1alpha2.AgentObject) string { + switch agent.(type) { + case *v1alpha2.SandboxAgent: + return "SandboxAgent" + default: + return "Agent" + } +} + +// agentStateKey is a kind-qualified identity used for cycle/self-reference checks. +func agentStateKey(agent v1alpha2.AgentObject) string { + return agentObjectKind(agent) + "/" + utils.GetObjectRef(agent) +} + +// getToolAgent resolves an Agent tool reference to its backing object, honoring +// the reference Kind. An empty Kind defaults to Agent. +func (a *adkApiTranslator) getToolAgent( + ctx context.Context, + ref *v1alpha2.TypedReference, + defaultNamespace string, +) (v1alpha2.AgentObject, error) { + key := ref.NamespacedName(defaultNamespace) + fetchAgent := func(obj v1alpha2.AgentObject) (v1alpha2.AgentObject, error) { + return obj, a.kube.Get(ctx, key, obj) + } + + switch ref.Kind { + case "", "Agent": + return fetchAgent(&v1alpha2.Agent{}) + case "SandboxAgent": + return fetchAgent(&v1alpha2.SandboxAgent{}) + + default: + return nil, fmt.Errorf("unsupported agent tool kind %q for agent %s", ref.Kind, key) + } +} + +// sandboxA2APathPrefix mirrors httpserver.APIPathA2ASandboxes (not imported to +// avoid an import cycle). Sandbox agents have no stable Service, so A2A calls +// to them are proxied through the controller. +const sandboxA2APathPrefix = "/api/a2a-sandboxes" + +// toolAgentURL returns the A2A URL a parent agent should use to call a sub-agent. +func toolAgentURL(agent v1alpha2.AgentObject) string { + if agent.GetWorkloadMode() == v1alpha2.WorkloadModeSandbox { + return fmt.Sprintf("http://%s.%s:8083%s/%s/%s", + utils.GetControllerName(), utils.GetResourceNamespace(), + sandboxA2APathPrefix, agent.GetNamespace(), agent.GetName()) + } + return fmt.Sprintf("http://%s.%s:8080", agent.GetName(), agent.GetNamespace()) +} + func TranslateAgent( ctx context.Context, translator AdkApiTranslator, @@ -118,7 +171,7 @@ func (a *adkApiTranslator) validateAgent(ctx context.Context, agent v1alpha2.Age agentRef := utils.GetObjectRef(agent) spec := agent.GetAgentSpec() - if state.isVisited(agentRef) { + if state.isVisited(agentStateKey(agent)) { return fmt.Errorf("cycle detected in agent tool chain: %s -> %s", agentRef, agentRef) } @@ -138,18 +191,15 @@ func (a *adkApiTranslator) validateAgent(ctx context.Context, agent v1alpha2.Age return fmt.Errorf("tool must have an agent reference") } - agentRef := tool.Agent.NamespacedName(agent.GetNamespace()) - - if agentRef.Namespace == agent.GetNamespace() && agentRef.Name == agent.GetName() { - return fmt.Errorf("agent tool cannot be used to reference itself, %s", agentRef) - } - - toolAgent := &v1alpha2.Agent{} - err := a.kube.Get(ctx, agentRef, toolAgent) + toolAgent, err := a.getToolAgent(ctx, tool.Agent, agent.GetNamespace()) if err != nil { return err } + if agentStateKey(toolAgent) == agentStateKey(agent) { + return fmt.Errorf("agent tool cannot be used to reference itself, %s", utils.GetObjectRef(toolAgent)) + } + err = a.validateAgent(ctx, toolAgent, state.with(agent)) if err != nil { return err @@ -271,21 +321,19 @@ func (a *adkApiTranslator) translateInlineAgent(ctx context.Context, agent v1alp secretHashBytes = append(secretHashBytes, toolHashBytes...) } case tool.Agent != nil: - agentRef := tool.Agent.NamespacedName(agent.GetNamespace()) - - if agentRef.Namespace == agent.GetNamespace() && agentRef.Name == agent.GetName() { - return nil, nil, nil, fmt.Errorf("agent tool cannot be used to reference itself, %s", agentRef) - } - - toolAgent := &v1alpha2.Agent{} - err := a.kube.Get(ctx, agentRef, toolAgent) + toolAgent, err := a.getToolAgent(ctx, tool.Agent, agent.GetNamespace()) if err != nil { return nil, nil, nil, err } - switch toolAgent.Spec.Type { + if agentStateKey(toolAgent) == agentStateKey(agent) { + return nil, nil, nil, fmt.Errorf("agent tool cannot be used to reference itself, %s", utils.GetObjectRef(toolAgent)) + } + + toolSpec := toolAgent.GetAgentSpec() + switch toolSpec.Type { case v1alpha2.AgentType_BYO, v1alpha2.AgentType_Declarative: - originalURL := fmt.Sprintf("http://%s.%s:8080", toolAgent.Name, toolAgent.Namespace) + originalURL := toolAgentURL(toolAgent) targetURL := originalURL if a.globalProxyURL != "" { @@ -299,10 +347,10 @@ func (a *adkApiTranslator) translateInlineAgent(ctx context.Context, agent v1alp Name: utils.ConvertToPythonIdentifier(utils.GetObjectRef(toolAgent)), Url: targetURL, Headers: headers, - Description: toolAgent.Spec.Description, + Description: toolSpec.Description, }) default: - return nil, nil, nil, fmt.Errorf("unknown agent type: %s", toolAgent.Spec.Type) + return nil, nil, nil, fmt.Errorf("unknown agent type: %s", toolSpec.Type) } default: diff --git a/go/core/internal/controller/translator/agent/sandbox_agent_tool_test.go b/go/core/internal/controller/translator/agent/sandbox_agent_tool_test.go new file mode 100644 index 0000000000..2f20387723 --- /dev/null +++ b/go/core/internal/controller/translator/agent/sandbox_agent_tool_test.go @@ -0,0 +1,170 @@ +package agent_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + schemev1 "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + agenttranslator "github.com/kagent-dev/kagent/go/core/internal/controller/translator/agent" +) + +// Test_AdkApiTranslator_SandboxAgentTool tests that agent tool references are +// resolved by Kind, so both Agent and SandboxAgent objects can be used as tools. +func Test_AdkApiTranslator_SandboxAgentTool(t *testing.T) { + ctx := context.Background() + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + + declarativeSpec := func(tools ...*v1alpha2.Tool) v1alpha2.AgentSpec { + return v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Description: "test agent", + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "Test", + ModelConfig: "default-model", + Tools: tools, + }, + } + } + + agentToolRef := func(name, kind string) *v1alpha2.Tool { + return &v1alpha2.Tool{ + Type: v1alpha2.ToolProviderType_Agent, + Agent: &v1alpha2.TypedReference{ + Name: name, + Kind: kind, + }, + } + } + + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "default-model", Namespace: "test"}, + Spec: v1alpha2.ModelConfigSpec{ + Provider: "OpenAI", + Model: "gpt-4o", + }, + } + testNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}} + + // A SandboxAgent and a regular Agent sharing the same name, to verify + // kind-based resolution and kind-qualified self-reference checks. + sandboxTool := &v1alpha2.SandboxAgent{ + ObjectMeta: metav1.ObjectMeta{Name: "shared-name", Namespace: "test"}, + Spec: v1alpha2.SandboxAgentSpec{AgentSpec: declarativeSpec()}, + } + regularTool := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "shared-name", Namespace: "test"}, + Spec: declarativeSpec(), + } + + tests := []struct { + name string + agent v1alpha2.AgentObject + wantURL string + wantErr bool + errContains string + }{ + { + name: "kind SandboxAgent resolves SandboxAgent and routes via controller proxy", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "parent", Namespace: "test"}, + Spec: declarativeSpec(agentToolRef("shared-name", "SandboxAgent")), + }, + wantURL: "http://kagent-controller.kagent:8083/api/a2a-sandboxes/test/shared-name", + }, + { + name: "empty kind defaults to Agent", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "parent", Namespace: "test"}, + Spec: declarativeSpec(agentToolRef("shared-name", "")), + }, + wantURL: "http://shared-name.test:8080", + }, + { + name: "kind Agent resolves Agent and uses direct service URL", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "parent", Namespace: "test"}, + Spec: declarativeSpec(agentToolRef("shared-name", "Agent")), + }, + wantURL: "http://shared-name.test:8080", + }, + { + name: "unsupported kind is rejected", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "parent", Namespace: "test"}, + Spec: declarativeSpec(agentToolRef("shared-name", "AgentHarness")), + }, + wantErr: true, + errContains: `unsupported agent tool kind "AgentHarness"`, + }, + { + name: "missing SandboxAgent returns not found", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "parent", Namespace: "test"}, + Spec: declarativeSpec(agentToolRef("does-not-exist", "SandboxAgent")), + }, + wantErr: true, + errContains: "not found", + }, + { + name: "SandboxAgent referencing itself is rejected", + agent: &v1alpha2.SandboxAgent{ + ObjectMeta: metav1.ObjectMeta{Name: "shared-name", Namespace: "test"}, + Spec: v1alpha2.SandboxAgentSpec{ + AgentSpec: declarativeSpec(agentToolRef("shared-name", "SandboxAgent")), + }, + }, + wantErr: true, + errContains: "reference itself", + }, + { + name: "Agent referencing a SandboxAgent with the same name is not a self-reference", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "shared-name", Namespace: "test"}, + Spec: declarativeSpec(agentToolRef("shared-name", "SandboxAgent")), + }, + wantURL: "http://kagent-controller.kagent:8083/api/a2a-sandboxes/test/shared-name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kubeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(modelConfig, testNamespace, sandboxTool, regularTool). + Build() + + translator := agenttranslator.NewAdkApiTranslator( + kubeClient, + types.NamespacedName{Name: "default-model", Namespace: "test"}, + nil, + "", + nil, + ) + + inputs, err := translator.CompileAgent(ctx, tt.agent) + + if tt.wantErr { + require.Error(t, err) + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + return + } + + require.NoError(t, err) + require.NotNil(t, inputs) + require.NotNil(t, inputs.Config) + require.Len(t, inputs.Config.RemoteAgents, 1) + assert.Equal(t, tt.wantURL, inputs.Config.RemoteAgents[0].Url) + }) + } +} diff --git a/ui/src/components/create/SelectToolsDialog.tsx b/ui/src/components/create/SelectToolsDialog.tsx index d5d5201d3c..03a9e5964d 100644 --- a/ui/src/components/create/SelectToolsDialog.tsx +++ b/ui/src/components/create/SelectToolsDialog.tsx @@ -1,16 +1,45 @@ import { useState, useMemo, useRef, useLayoutEffect } from "react"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; -import { Search, Filter, ChevronDown, ChevronRight, AlertCircle, PlusCircle, XCircle, FunctionSquare, LucideIcon } from "lucide-react"; +import { + Search, + Filter, + ChevronDown, + ChevronRight, + AlertCircle, + PlusCircle, + XCircle, + FunctionSquare, + LucideIcon, +} from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Badge } from "@/components/ui/badge"; import type { AgentResponse, Tool, ToolsResponse } from "@/types"; import ProviderFilter from "./ProviderFilter"; import Link from "next/link"; -import { getToolResponseDisplayName, getToolResponseDescription, getToolResponseCategory, getToolResponseIdentifier, getToolIdentifier, isAgentTool, isAgentResponse, isMcpTool, toolResponseToAgentTool, groupMcpToolsByServer, serverNamesMatch } from "@/lib/toolUtils"; +import { + getToolResponseDisplayName, + getToolResponseDescription, + getToolResponseCategory, + getToolResponseIdentifier, + getToolIdentifier, + isAgentTool, + isAgentResponse, + isMcpTool, + toolResponseToAgentTool, + groupMcpToolsByServer, + serverNamesMatch, +} from "@/lib/toolUtils"; import { toast } from "sonner"; import KagentLogo from "../kagent-logo"; import { k8sRefUtils } from "@/lib/k8sUtils"; @@ -29,10 +58,10 @@ interface SelectToolsDialogProps { currentAgentNamespace: string; } - - // Helper function to get display info for a tool or agent -const getItemDisplayInfo = (item: ToolsResponse | AgentResponse): { +const getItemDisplayInfo = ( + item: ToolsResponse | AgentResponse, +): { displayName: string; description?: string; identifier: string; @@ -41,18 +70,20 @@ const getItemDisplayInfo = (item: ToolsResponse | AgentResponse): { iconColor: string; isAgent: boolean; } => { - if (isAgentResponse(item)) { const agentResp = item as AgentResponse; - const displayName = k8sRefUtils.toRef(agentResp.agent.metadata.namespace || "", agentResp.agent.metadata.name); + const displayName = k8sRefUtils.toRef( + agentResp.agent.metadata.namespace || "", + agentResp.agent.metadata.name, + ); return { displayName, description: agentResp.agent.spec.description, identifier: `agent-${displayName}`, - providerText: "Agent", + providerText: agentResp.agent.kind || "Agent", Icon: KagentLogo, iconColor: "text-green-500", - isAgent: true + isAgent: true, }; } else { const tool = item as ToolsResponse; @@ -63,19 +94,32 @@ const getItemDisplayInfo = (item: ToolsResponse | AgentResponse): { providerText: getToolResponseCategory(tool), Icon: FunctionSquare, iconColor: "text-blue-400", - isAgent: false + isAgent: false, }; } }; -export const SelectToolsDialog: React.FC = ({ open, onOpenChange, availableTools, selectedTools, onToolsSelected, availableAgents, loadingAgents, currentAgentNamespace }) => { +export const SelectToolsDialog: React.FC = ({ + open, + onOpenChange, + availableTools, + selectedTools, + onToolsSelected, + availableAgents, + loadingAgents, + currentAgentNamespace, +}) => { const [searchTerm, setSearchTerm] = useState(""); const [localSelectedTools, setLocalSelectedTools] = useState([]); const [categories, setCategories] = useState>(new Set()); - const [selectedCategories, setSelectedCategories] = useState>(new Set()); + const [selectedCategories, setSelectedCategories] = useState>( + new Set(), + ); const [showFilters, setShowFilters] = useState(false); - const [expandedCategories, setExpandedCategories] = useState<{ [key: string]: boolean }>({}); - + const [expandedCategories, setExpandedCategories] = useState<{ + [key: string]: boolean; + }>({}); + // Track previous open state to detect when dialog just opened const wasOpenRef = useRef(open); @@ -90,11 +134,11 @@ export const SelectToolsDialog: React.FC = ({ open, onOp const uniqueCategories = new Set(); const categoryCollapseState: { [key: string]: boolean } = {}; - + availableTools.forEach((tool) => { - const category = getToolResponseCategory(tool); - uniqueCategories.add(category); - categoryCollapseState[category] = true; + const category = getToolResponseCategory(tool); + uniqueCategories.add(category); + categoryCollapseState[category] = true; }); if (availableAgents.length > 0) { @@ -112,7 +156,11 @@ export const SelectToolsDialog: React.FC = ({ open, onOp const actualSelectedCount = useMemo(() => { return localSelectedTools.reduce((acc, tool) => { - if (tool.mcpServer && tool.mcpServer.toolNames && tool.mcpServer.toolNames.length > 0) { + if ( + tool.mcpServer && + tool.mcpServer.toolNames && + tool.mcpServer.toolNames.length > 0 + ) { return acc + tool.mcpServer.toolNames.length; } return acc + 1; @@ -134,31 +182,47 @@ export const SelectToolsDialog: React.FC = ({ open, onOp const toolDescription = getToolResponseDescription(tool).toLowerCase(); const toolProvider = server.server_name?.toLowerCase() || ""; - const matchesSearch = toolName.includes(searchLower) || toolDescription.includes(searchLower) || toolProvider.includes(searchLower); + const matchesSearch = + toolName.includes(searchLower) || + toolDescription.includes(searchLower) || + toolProvider.includes(searchLower); const toolCategory = getToolResponseCategory(tool); - const matchesCategory = selectedCategories.size === 0 || selectedCategories.has(toolCategory); + const matchesCategory = + selectedCategories.size === 0 || selectedCategories.has(toolCategory); return matchesSearch && matchesCategory; }); - const agentCategorySelected = selectedCategories.size === 0 || selectedCategories.has("Agents"); - const agents = agentCategorySelected ? availableAgents.filter(agentResp => { - const agentRef = k8sRefUtils.toRef(agentResp.agent.metadata.namespace || "", agentResp.agent.metadata.name).toLowerCase(); - const agentDesc = agentResp.agent.spec.description?.toLowerCase(); - return agentRef.includes(searchLower) || (agentDesc && agentDesc.includes(searchLower)); - }) - : []; + const agentCategorySelected = + selectedCategories.size === 0 || selectedCategories.has("Agents"); + const agents = agentCategorySelected + ? availableAgents.filter((agentResp) => { + const agentRef = k8sRefUtils + .toRef( + agentResp.agent.metadata.namespace || "", + agentResp.agent.metadata.name, + ) + .toLowerCase(); + const agentDesc = agentResp.agent.spec.description?.toLowerCase(); + return ( + agentRef.includes(searchLower) || + (agentDesc && agentDesc.includes(searchLower)) + ); + }) + : []; return { tools, agents }; }, [availableTools, availableAgents, searchTerm, selectedCategories]); const groupedAvailableItems = useMemo(() => { - const groups: { [key: string]: Array< ToolsResponse | AgentResponse> } = {}; - + const groups: { [key: string]: Array } = {}; + const sortedTools = [...filteredAvailableItems.tools].sort((a, b) => { - return getToolResponseDisplayName(a.tool).localeCompare(getToolResponseDisplayName(b.tool)); + return getToolResponseDisplayName(a.tool).localeCompare( + getToolResponseDisplayName(b.tool), + ); }); - + sortedTools.forEach(({ tool }) => { const category = getToolResponseCategory(tool); if (!groups[category]) { @@ -169,15 +233,27 @@ export const SelectToolsDialog: React.FC = ({ open, onOp if (filteredAvailableItems.agents.length > 0) { groups["Agents"] = filteredAvailableItems.agents.sort((a, b) => { - const aRef = k8sRefUtils.toRef(a.agent.metadata.namespace || "", a.agent.metadata.name) - const bRef = k8sRefUtils.toRef(b.agent.metadata.namespace || "", b.agent.metadata.name) - return aRef.localeCompare(bRef) + const aRef = k8sRefUtils.toRef( + a.agent.metadata.namespace || "", + a.agent.metadata.name, + ); + const bRef = k8sRefUtils.toRef( + b.agent.metadata.namespace || "", + b.agent.metadata.name, + ); + return aRef.localeCompare(bRef); }); } - - return Object.entries(groups).sort(([catA], [catB]) => catA.localeCompare(catB)) - .reduce((acc, [key, value]) => { acc[key] = value; return acc; }, {} as typeof groups); - + + return Object.entries(groups) + .sort(([catA], [catB]) => catA.localeCompare(catB)) + .reduce( + (acc, [key, value]) => { + acc[key] = value; + return acc; + }, + {} as typeof groups, + ); }, [filteredAvailableItems]); const isItemSelected = (item: ToolsResponse | AgentResponse): boolean => { @@ -188,13 +264,13 @@ export const SelectToolsDialog: React.FC = ({ open, onOp // variables with the agent to which the tool is being added const itemNamespace = agentResp.agent.metadata.namespace || ""; const itemName = agentResp.agent.metadata.name; - - return localSelectedTools.some(tool => { + + return localSelectedTools.some((tool) => { if (!isAgentTool(tool)) return false; - + const toolName = tool.agent?.name; const toolNamespace = tool.agent?.namespace; - + // Match by name and namespace if (toolNamespace) { return toolNamespace === itemNamespace && toolName === itemName; @@ -205,14 +281,17 @@ export const SelectToolsDialog: React.FC = ({ open, onOp }); } else { const toolItem = item as ToolsResponse; - - return localSelectedTools.some(tool => { + + return localSelectedTools.some((tool) => { if (!isMcpTool(tool)) return false; const mcpTool = tool as Tool; - - const serverMatch = serverNamesMatch(mcpTool.mcpServer?.name || "", toolItem.server_name); + + const serverMatch = serverNamesMatch( + mcpTool.mcpServer?.name || "", + toolItem.server_name, + ); const toolIdMatch = mcpTool.mcpServer?.toolNames?.includes(toolItem.id); - + return serverMatch && toolIdMatch; }); } @@ -233,55 +312,65 @@ export const SelectToolsDialog: React.FC = ({ open, onOp const agentResp = item as AgentResponse; const agentNamespace = agentResp.agent.metadata.namespace || ""; const agentName = agentResp.agent.metadata.name; - + toolToAdd = { type: "Agent", agent: { name: agentName, namespace: agentNamespace, - kind: "Agent", + kind: agentResp.agent.kind || "Agent", apiGroup: "kagent.dev", - } + }, }; - - setLocalSelectedTools(prev => [...prev, toolToAdd]); + + setLocalSelectedTools((prev) => [...prev, toolToAdd]); } else { const tool = item as ToolsResponse; - + const existingServerToolIndex = localSelectedTools.findIndex( - t => isMcpTool(t) && serverNamesMatch(t.mcpServer?.name || "", tool.server_name) + (t) => + isMcpTool(t) && + serverNamesMatch(t.mcpServer?.name || "", tool.server_name), ); if (existingServerToolIndex >= 0) { const existingTool = localSelectedTools[existingServerToolIndex]; - + if (existingTool.mcpServer?.toolNames?.includes(tool.id)) { return; } - + const updatedTool = { ...existingTool, mcpServer: { ...existingTool.mcpServer!, - toolNames: [...(existingTool.mcpServer!.toolNames || []), tool.id] - } + toolNames: [...(existingTool.mcpServer!.toolNames || []), tool.id], + }, }; - - setLocalSelectedTools(prev => - prev.map((t, idx) => idx === existingServerToolIndex ? updatedTool : t) + + setLocalSelectedTools((prev) => + prev.map((t, idx) => + idx === existingServerToolIndex ? updatedTool : t, + ), ); } else { toolToAdd = toolResponseToAgentTool(tool, tool.server_name); - setLocalSelectedTools(prev => [...prev, toolToAdd]); + setLocalSelectedTools((prev) => [...prev, toolToAdd]); } } }; const handleRemoveTool = (toolToRemove: Tool) => { - setLocalSelectedTools(prev => prev.filter(tool => tool !== toolToRemove)); + setLocalSelectedTools((prev) => + prev.filter((tool) => tool !== toolToRemove), + ); }; - const setRequireApprovalForMcpTool = (target: Tool, mcpToolName: string, requireApproval: boolean) => { + const setRequireApprovalForMcpTool = ( + target: Tool, + mcpToolName: string, + requireApproval: boolean, + ) => { const targetId = getToolIdentifier(target); setLocalSelectedTools((prev) => prev.map((t) => { @@ -300,21 +389,23 @@ export const SelectToolsDialog: React.FC = ({ open, onOp ...t, mcpServer: { ...mcp, - ...(next.length > 0 ? { requireApproval: next } : { requireApproval: undefined }), + ...(next.length > 0 + ? { requireApproval: next } + : { requireApproval: undefined }), }, }; - }) + }), ); }; const handleSave = () => { const { groupedTools, errors } = groupMcpToolsByServer(localSelectedTools); - + if (errors.length > 0) { - const errorList = errors.join('\n- '); + const errorList = errors.join("\n- "); toast.warning(`Tools skipped:\n- ${errorList}`); } - + onToolsSelected(groupedTools); onOpenChange(false); }; @@ -348,9 +439,20 @@ export const SelectToolsDialog: React.FC = ({ open, onOp const highlightMatch = (text: string, highlight: string) => { if (!highlight || !text) return text; - const parts = text.split(new RegExp(`(${highlight.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')})`, 'gi')); + const parts = text.split( + new RegExp( + `(${highlight.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")})`, + "gi", + ), + ); return parts.map((part, i) => - part.toLowerCase() === highlight.toLowerCase() ? {part} : part + part.toLowerCase() === highlight.toLowerCase() ? ( + + {part} + + ) : ( + part + ), ); }; @@ -360,8 +462,13 @@ export const SelectToolsDialog: React.FC = ({ open, onOp Select Tools and Agents - You can use tools and agents to create your agent. The tools are grouped by category. You can select a tool by clicking on it. To add or manage tool servers, use{" "} - + You can use tools and agents to create your agent. The tools are + grouped by category. You can select a tool by clicking on it. To add + or manage tool servers, use{" "} + MCP and tools . @@ -375,13 +482,23 @@ export const SelectToolsDialog: React.FC = ({ open, onOp
- setSearchTerm(e.target.value)} className="pl-10 pr-4 py-2 h-10" /> + setSearchTerm(e.target.value)} + className="pl-10 pr-4 py-2 h-10" + />
{categories.size > 1 && ( - - )} + + )}
{showFilters && categories.size > 1 && ( @@ -401,107 +518,207 @@ export const SelectToolsDialog: React.FC = ({ open, onOp

Loading Agents...

)} - {!loadingAgents && Object.keys(groupedAvailableItems).length > 0 ? ( + {!loadingAgents && + Object.keys(groupedAvailableItems).length > 0 ? (
- {Object.entries(groupedAvailableItems).map(([category, items]) => { - const itemsSelectedInCategory = items.reduce((count, item) => { - return count + (isItemSelected(item) ? 1 : 0); - }, 0); + {Object.entries(groupedAvailableItems).map( + ([category, items]) => { + const itemsSelectedInCategory = items.reduce( + (count, item) => { + return count + (isItemSelected(item) ? 1 : 0); + }, + 0, + ); - return ( -
+ return (
toggleCategory(category)} + key={category} + className="border rounded-lg overflow-hidden bg-card" > -
- {expandedCategories[category] ? : } -

{highlightMatch(category, searchTerm)}

- {items.length} -
-
- {itemsSelectedInCategory > 0 && ( - {itemsSelectedInCategory} selected - )} +
toggleCategory(category)} + > +
+ {expandedCategories[category] ? ( + + ) : ( + + )} +

+ {highlightMatch(category, searchTerm)} +

+ + {items.length} + +
+
+ {itemsSelectedInCategory > 0 && ( + + {itemsSelectedInCategory} selected + + )} +
-
- - {expandedCategories[category] && ( -
- {items.map((item) => { - const { displayName, description, identifier, providerText } = getItemDisplayInfo(item); - const isSelected = isItemSelected(item); - const isDisabled = !isSelected && isLimitReached; - return ( -
!isDisabled && !isSelected && handleAddItem(item)} - > -
-

{highlightMatch(displayName, searchTerm)}

- {description &&

{highlightMatch(description, searchTerm)}

} - {providerText &&

{highlightMatch(providerText, searchTerm)}

} -
- {!isSelected && !isDisabled && ( - - )} - {isSelected && ( - + )} + {isSelected && ( + - )} -
- ); - })} -
- )} -
- ); - })} + }} + > + + + )} +
+ ); + })} +
+ )} + + ); + }, + )} ) : (

No tools found

-

Try adjusting your search or filters.

+

+ Try adjusting your search or filters. +

)} @@ -510,8 +727,15 @@ export const SelectToolsDialog: React.FC = ({ open, onOp {/* Right Panel: Selected Tools */}
-

Selected ({actualSelectedCount}/{MAX_TOOLS_LIMIT})

-
@@ -519,9 +743,7 @@ export const SelectToolsDialog: React.FC = ({ open, onOp {isLimitReached && actualSelectedCount >= MAX_TOOLS_LIMIT && (
-
- Tool limit reached. Deselect a tool to add another. -
+
Tool limit reached. Deselect a tool to add another.
)} @@ -529,102 +751,146 @@ export const SelectToolsDialog: React.FC = ({ open, onOp {localSelectedTools.length > 0 ? (
{localSelectedTools.flatMap((tool) => { - if (tool.mcpServer && tool.mcpServer.toolNames && tool.mcpServer.toolNames.length > 0) { - return tool.mcpServer.toolNames.map((toolName: string) => { - const foundTool = availableTools.find( - t => serverNamesMatch(t.server_name, tool.mcpServer?.name || "") && t.id === toolName - ); - const specificDescription = foundTool?.description || "Description not available"; - - // Show server name with namespace for consistency - const serverName = tool.mcpServer?.name || ""; - const serverNamespace = tool.mcpServer?.namespace || currentAgentNamespace; - const serverDisplayName = `${serverNamespace}/${serverName}`; - const displayName = `${toolName} (${serverDisplayName})`; - const approvalSet = new Set(tool.mcpServer?.requireApproval || []); - const requiresApproval = approvalSet.has(toolName); - - const approvalFieldId = `dialog-req-${getToolIdentifier(tool)}-${toolName}`.replace( - /[^a-zA-Z0-9_-]/g, - "_" - ); - - return ( -
-
- -
-

- {displayName} -

-

- {specificDescription} -

-
- -
-
- - setRequireApprovalForMcpTool(tool, toolName, checked) - } - /> -
- ); - }); +
+ +
+

+ + {displayName} + +

+

+ {specificDescription} +

+
+ +
+
+ + setRequireApprovalForMcpTool( + tool, + toolName, + checked, + ) + } + /> + +
+
+ ); + }, + ); } else { const matchedAgent = isAgentTool(tool) - ? availableAgents.find(a => { + ? availableAgents.find((a) => { const agentName = tool.agent?.name; const agentNamespace = tool.agent?.namespace; - + // Match by name and namespace (if namespace is specified) if (agentNamespace) { - return a.agent.metadata.namespace === agentNamespace && - a.agent.metadata.name === agentName; + return ( + a.agent.metadata.namespace === agentNamespace && + a.agent.metadata.name === agentName + ); } // If no namespace specified, match by name only return a.agent.metadata.name === agentName; @@ -632,19 +898,36 @@ export const SelectToolsDialog: React.FC = ({ open, onOp : undefined; const matchedTool = !isAgentTool(tool) - ? availableTools.find(s => serverNamesMatch(s.server_name, tool.mcpServer?.name || "")) + ? availableTools.find((s) => + serverNamesMatch( + s.server_name, + tool.mcpServer?.name || "", + ), + ) : undefined; - const { displayName, description, Icon, iconColor } = getItemDisplayInfo( - (matchedAgent as AgentResponse) || (matchedTool as ToolsResponse) - ); - - return [( -
- + const { displayName, description, Icon, iconColor } = + getItemDisplayInfo( + (matchedAgent as AgentResponse) || + (matchedTool as ToolsResponse), + ); + + return [ +
+
-

- {displayName} +

+ + {displayName} +

{description && (

= ({ open, onOp > -

- )]; +
, + ]; } })}
@@ -672,7 +955,9 @@ export const SelectToolsDialog: React.FC = ({ open, onOp

No tools selected

-

Select tools or agents from the left panel.

+

+ Select tools or agents from the left panel. +

)} @@ -686,8 +971,13 @@ export const SelectToolsDialog: React.FC = ({ open, onOp Select up to {MAX_TOOLS_LIMIT} tools for your agent.
- - +