Skip to content

Commit 51c4877

Browse files
committed
fix: show nested agent tool calls with visual hierarchy
Add support for displaying nested agent calls (agent -> subagent -> subagent) with proper indentation and visual indicators. The UI now shows delegated calls in a collapsible section with: - Indented display for each nesting level - Colored borders to indicate hierarchy depth - Level indicators showing nesting depth - Protection against excessive nesting (max 10 levels) Includes comprehensive test coverage for nested call scenarios. Fixes #972 Signed-off-by: Naveen Alok <[email protected]>
1 parent 6e46a00 commit 51c4877

File tree

4 files changed

+775
-75
lines changed

4 files changed

+775
-75
lines changed

ui/src/components/chat/AgentCallDisplay.tsx

Lines changed: 122 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
11
import { useMemo, useState } from "react";
22
import { FunctionCall } from "@/types";
33
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
4-
import { convertToUserFriendlyName } from "@/lib/utils";
4+
import { convertToUserFriendlyName, isAgentToolName } from "@/lib/utils";
55
import { ChevronDown, ChevronUp, MessageSquare, Loader2, AlertCircle, CheckCircle } from "lucide-react";
66
import KagentLogo from "../kagent-logo";
7+
import ToolDisplay, { ToolCallStatus } from "@/components/ToolDisplay";
78

89
export type AgentCallStatus = "requested" | "executing" | "completed";
910

11+
// Constants
12+
const MAX_NESTING_DEPTH = 10;
13+
const NESTING_INDENT_REM = 1.5;
14+
15+
interface NestedToolCall {
16+
id: string;
17+
call: FunctionCall;
18+
result?: {
19+
content: string;
20+
is_error?: boolean;
21+
};
22+
status: ToolCallStatus;
23+
nestedCalls?: NestedToolCall[];
24+
}
25+
1026
interface AgentCallDisplayProps {
1127
call: FunctionCall;
1228
result?: {
@@ -15,14 +31,31 @@ interface AgentCallDisplayProps {
1531
};
1632
status?: AgentCallStatus;
1733
isError?: boolean;
34+
nestedCalls?: NestedToolCall[]; // Support for nested agent/tool calls
35+
depth?: number; // Track nesting depth for visual indentation
1836
}
1937

20-
const AgentCallDisplay = ({ call, result, status = "requested", isError = false }: AgentCallDisplayProps) => {
38+
const AgentCallDisplay = ({ call, result, status = "requested", isError = false, nestedCalls = [], depth = 0 }: AgentCallDisplayProps) => {
2139
const [areInputsExpanded, setAreInputsExpanded] = useState(false);
2240
const [areResultsExpanded, setAreResultsExpanded] = useState(false);
41+
const [areNestedCallsExpanded, setAreNestedCallsExpanded] = useState(true); // Expanded by default for better visibility
2342

2443
const agentDisplay = useMemo(() => convertToUserFriendlyName(call.name), [call.name]);
2544
const hasResult = result !== undefined;
45+
const hasNestedCalls = nestedCalls && nestedCalls.length > 0;
46+
47+
// Protection against infinite recursion
48+
if (depth > MAX_NESTING_DEPTH) {
49+
console.warn(`Maximum nesting depth (${MAX_NESTING_DEPTH}) reached for agent call:`, call.name);
50+
return (
51+
<div className="p-2 text-xs text-muted-foreground border border-yellow-500 rounded">
52+
⚠️ Maximum nesting depth reached
53+
</div>
54+
);
55+
}
56+
57+
// Calculate left margin based on nesting depth
58+
const marginLeft = depth > 0 ? `${depth * NESTING_INDENT_REM}rem` : '0';
2659

2760
const getStatusDisplay = () => {
2861
if (isError && status === "executing") {
@@ -69,59 +102,100 @@ const AgentCallDisplay = ({ call, result, status = "requested", isError = false
69102
};
70103

71104
return (
72-
<Card className={`w-full mx-auto my-1 min-w-full ${isError ? 'border-red-300' : ''}`}>
73-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
74-
<CardTitle className="text-xs flex space-x-5">
75-
<div className="flex items-center font-medium">
76-
<KagentLogo className="w-4 h-4 mr-2" />
77-
{agentDisplay}
78-
</div>
79-
<div className="font-light">{call.id}</div>
80-
</CardTitle>
81-
<div className="flex justify-center items-center text-xs">
82-
{getStatusDisplay()}
83-
</div>
84-
</CardHeader>
85-
<CardContent>
86-
<div className="space-y-2 mt-2">
87-
<button className="text-xs flex items-center gap-2" onClick={() => setAreInputsExpanded(!areInputsExpanded)}>
88-
<MessageSquare className="w-4 h-4" />
89-
<span>Input</span>
90-
{areInputsExpanded ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
91-
</button>
92-
{areInputsExpanded && (
93-
<div className="mt-2 bg-muted/50 p-3 rounded">
94-
<pre className="text-sm whitespace-pre-wrap break-words">{JSON.stringify(call.args, null, 2)}</pre>
105+
<div style={{ marginLeft }}>
106+
<Card className={`w-full mx-auto my-1 min-w-full ${isError ? 'border-red-300' : ''} ${depth > 0 ? 'border-l-4 border-l-blue-400' : ''}`}>
107+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
108+
<CardTitle className="text-xs flex space-x-5">
109+
<div className="flex items-center font-medium">
110+
<KagentLogo className="w-4 h-4 mr-2" />
111+
{agentDisplay}
112+
{depth > 0 && <span className="ml-2 text-xs text-muted-foreground">(nested level {depth})</span>}
95113
</div>
96-
)}
97-
</div>
114+
<div className="font-light">{call.id}</div>
115+
</CardTitle>
116+
<div className="flex justify-center items-center text-xs">
117+
{getStatusDisplay()}
118+
</div>
119+
</CardHeader>
120+
<CardContent>
121+
<div className="space-y-2 mt-2">
122+
<button className="text-xs flex items-center gap-2" onClick={() => setAreInputsExpanded(!areInputsExpanded)}>
123+
<MessageSquare className="w-4 h-4" />
124+
<span>Input</span>
125+
{areInputsExpanded ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
126+
</button>
127+
{areInputsExpanded && (
128+
<div className="mt-2 bg-muted/50 p-3 rounded">
129+
<pre className="text-sm whitespace-pre-wrap break-words">{JSON.stringify(call.args, null, 2)}</pre>
130+
</div>
131+
)}
132+
</div>
98133

99-
<div className="mt-4 w-full">
100-
{status === "executing" && !hasResult && (
101-
<div className="flex items-center gap-2 py-2">
102-
<Loader2 className="h-4 w-4 animate-spin" />
103-
<span className="text-sm">{agentDisplay} is responding...</span>
104-
</div>
105-
)}
106-
{hasResult && result?.content && (
107-
<div className="space-y-2">
108-
<button className="text-xs flex items-center gap-2" onClick={() => setAreResultsExpanded(!areResultsExpanded)}>
109-
<MessageSquare className="w-4 h-4" />
110-
<span>Output</span>
111-
{areResultsExpanded ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
134+
<div className="mt-4 w-full">
135+
{status === "executing" && !hasResult && (
136+
<div className="flex items-center gap-2 py-2">
137+
<Loader2 className="h-4 w-4 animate-spin" />
138+
<span className="text-sm">{agentDisplay} is responding...</span>
139+
</div>
140+
)}
141+
{hasResult && result?.content && (
142+
<div className="space-y-2">
143+
<button className="text-xs flex items-center gap-2" onClick={() => setAreResultsExpanded(!areResultsExpanded)}>
144+
<MessageSquare className="w-4 h-4" />
145+
<span>Output</span>
146+
{areResultsExpanded ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
147+
</button>
148+
{areResultsExpanded && (
149+
<div className={`mt-2 ${isError ? 'bg-red-50 dark:bg-red-950/10' : 'bg-muted/50'} p-3 rounded`}>
150+
<pre className={`text-sm whitespace-pre-wrap break-words ${isError ? 'text-red-600 dark:text-red-400' : ''}`}>
151+
{result?.content}
152+
</pre>
153+
</div>
154+
)}
155+
</div>
156+
)}
157+
</div>
158+
159+
{/* Nested agent/tool calls section */}
160+
{hasNestedCalls && (
161+
<div className="mt-4 border-t pt-4">
162+
<button
163+
className="text-xs flex items-center gap-2 font-semibold mb-2"
164+
onClick={() => setAreNestedCallsExpanded(!areNestedCallsExpanded)}
165+
>
166+
<span>Delegated Calls ({nestedCalls.length})</span>
167+
{areNestedCallsExpanded ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
112168
</button>
113-
{areResultsExpanded && (
114-
<div className={`mt-2 ${isError ? 'bg-red-50 dark:bg-red-950/10' : 'bg-muted/50'} p-3 rounded`}>
115-
<pre className={`text-sm whitespace-pre-wrap break-words ${isError ? 'text-red-600 dark:text-red-400' : ''}`}>
116-
{result?.content}
117-
</pre>
169+
{areNestedCallsExpanded && (
170+
<div className="space-y-2 mt-2">
171+
{nestedCalls.map((nestedCall) => (
172+
isAgentToolName(nestedCall.call.name) ? (
173+
<AgentCallDisplay
174+
key={nestedCall.id}
175+
call={nestedCall.call}
176+
result={nestedCall.result}
177+
status={nestedCall.status}
178+
isError={nestedCall.result?.is_error}
179+
nestedCalls={nestedCall.nestedCalls}
180+
depth={depth + 1}
181+
/>
182+
) : (
183+
<ToolDisplay
184+
key={nestedCall.id}
185+
call={nestedCall.call}
186+
result={nestedCall.result}
187+
status={nestedCall.status}
188+
isError={nestedCall.result?.is_error}
189+
/>
190+
)
191+
))}
118192
</div>
119193
)}
120194
</div>
121195
)}
122-
</div>
123-
</CardContent>
124-
</Card>
196+
</CardContent>
197+
</Card>
198+
</div>
125199
);
126200
};
127201

0 commit comments

Comments
 (0)