Skip to content

Commit 9f65d01

Browse files
committed
🤖 fix: Show 'failed' status when status_set validation fails
## Problem When status_set validation failed (e.g., invalid emoji), the tool showed 'completed' status in the UI even though it failed. This made users think validation was silently failing, especially when the status didn't appear in the sidebar. ## Root Cause Tool status determination only checked part.state === 'output-available', which is true even for failed results. It didn't check result.success. ## Solution 1. Enhanced status determination to check result.success for tools that return { success: boolean } pattern 2. Show 'failed' status when result.success === false 3. Display error message in StatusSetToolCall UI for failed validations ## Testing - Added unit tests for failed/completed status display - All 839 tests pass - Typecheck passes When validation fails now: - Tool shows 'failed' status (not 'completed') - Error message displays in UI - agentStatus NOT updated (correct existing behavior) - Clear feedback to user that validation failed
1 parent f802ab7 commit 9f65d01

File tree

3 files changed

+146
-9
lines changed

3 files changed

+146
-9
lines changed

src/components/tools/StatusSetToolCall.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ interface StatusSetToolCallProps {
1212

1313
export const StatusSetToolCall: React.FC<StatusSetToolCallProps> = ({
1414
args,
15-
result: _result,
15+
result,
1616
status = "pending",
1717
}) => {
1818
const statusDisplay = getStatusDisplay(status);
1919

20+
// Show error message if validation failed
21+
const errorMessage =
22+
status === "failed" && result && typeof result === "object" && "error" in result
23+
? String(result.error)
24+
: undefined;
25+
2026
return (
2127
<ToolContainer expanded={false}>
2228
<ToolHeader>
@@ -25,6 +31,7 @@ export const StatusSetToolCall: React.FC<StatusSetToolCallProps> = ({
2531
<Tooltip>status_set</Tooltip>
2632
</TooltipWrapper>
2733
<span className="text-muted-foreground italic">{args.message}</span>
34+
{errorMessage && <span className="text-error-foreground text-sm">({errorMessage})</span>}
2835
<StatusIndicator status={status}>{statusDisplay}</StatusIndicator>
2936
</ToolHeader>
3037
</ToolContainer>

src/utils/messages/StreamingMessageAggregator.status.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,124 @@ describe("StreamingMessageAggregator - Agent Status", () => {
257257
// Status should be cleared on new stream start
258258
expect(aggregator.getAgentStatus()).toBeUndefined();
259259
});
260+
261+
it("should show 'failed' status in UI when status_set validation fails", () => {
262+
const aggregator = new StreamingMessageAggregator("2024-01-01T00:00:00.000Z");
263+
const messageId = "msg1";
264+
265+
// Start a stream
266+
aggregator.handleStreamStart({
267+
type: "stream-start",
268+
workspaceId: "workspace1",
269+
messageId,
270+
model: "test-model",
271+
historySequence: 1,
272+
});
273+
274+
// Add a status_set tool call with invalid emoji
275+
aggregator.handleToolCallStart({
276+
type: "tool-call-start",
277+
workspaceId: "workspace1",
278+
messageId,
279+
toolCallId: "tool1",
280+
toolName: "status_set",
281+
args: { emoji: "not-an-emoji", message: "test" },
282+
tokens: 10,
283+
timestamp: Date.now(),
284+
});
285+
286+
// Complete with validation failure
287+
aggregator.handleToolCallEnd({
288+
type: "tool-call-end",
289+
workspaceId: "workspace1",
290+
messageId,
291+
toolCallId: "tool1",
292+
toolName: "status_set",
293+
result: { success: false, error: "emoji must be a single emoji character" },
294+
});
295+
296+
// End the stream to finalize message
297+
aggregator.handleStreamEnd({
298+
type: "stream-end",
299+
workspaceId: "workspace1",
300+
messageId,
301+
metadata: { model: "test-model" },
302+
parts: [],
303+
});
304+
305+
// Check that the tool message shows 'failed' status in the UI
306+
const displayedMessages = aggregator.getDisplayedMessages();
307+
const toolMessage = displayedMessages.find((m) => m.type === "tool");
308+
expect(toolMessage).toBeDefined();
309+
expect(toolMessage?.type).toBe("tool");
310+
if (toolMessage?.type === "tool") {
311+
expect(toolMessage.status).toBe("failed");
312+
expect(toolMessage.toolName).toBe("status_set");
313+
}
314+
315+
// And status should NOT be updated in aggregator
316+
expect(aggregator.getAgentStatus()).toBeUndefined();
317+
});
318+
319+
it("should show 'completed' status in UI when status_set validation succeeds", () => {
320+
const aggregator = new StreamingMessageAggregator("2024-01-01T00:00:00.000Z");
321+
const messageId = "msg1";
322+
323+
// Start a stream
324+
aggregator.handleStreamStart({
325+
type: "stream-start",
326+
workspaceId: "workspace1",
327+
messageId,
328+
model: "test-model",
329+
historySequence: 1,
330+
});
331+
332+
// Add a successful status_set tool call
333+
aggregator.handleToolCallStart({
334+
type: "tool-call-start",
335+
workspaceId: "workspace1",
336+
messageId,
337+
toolCallId: "tool1",
338+
toolName: "status_set",
339+
args: { emoji: "🔍", message: "Analyzing code" },
340+
tokens: 10,
341+
timestamp: Date.now(),
342+
});
343+
344+
// Complete successfully
345+
aggregator.handleToolCallEnd({
346+
type: "tool-call-end",
347+
workspaceId: "workspace1",
348+
messageId,
349+
toolCallId: "tool1",
350+
toolName: "status_set",
351+
result: { success: true, emoji: "🔍", message: "Analyzing code" },
352+
});
353+
354+
// End the stream to finalize message
355+
aggregator.handleStreamEnd({
356+
type: "stream-end",
357+
workspaceId: "workspace1",
358+
messageId,
359+
metadata: { model: "test-model" },
360+
parts: [],
361+
});
362+
363+
// Check that the tool message shows 'completed' status in the UI
364+
const displayedMessages = aggregator.getDisplayedMessages();
365+
const toolMessage = displayedMessages.find((m) => m.type === "tool");
366+
expect(toolMessage).toBeDefined();
367+
expect(toolMessage?.type).toBe("tool");
368+
if (toolMessage?.type === "tool") {
369+
expect(toolMessage.status).toBe("completed");
370+
expect(toolMessage.toolName).toBe("status_set");
371+
}
372+
373+
// And status SHOULD be updated in aggregator
374+
const status = aggregator.getAgentStatus();
375+
expect(status).toBeDefined();
376+
expect(status?.emoji).toBe("🔍");
377+
expect(status?.message).toBe("Analyzing code");
378+
});
379+
260380
});

src/utils/messages/StreamingMessageAggregator.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -769,14 +769,24 @@ export class StreamingMessageAggregator {
769769
timestamp: part.timestamp ?? baseTimestamp,
770770
});
771771
} else if (isDynamicToolPart(part)) {
772-
const status =
773-
part.state === "output-available"
774-
? "completed"
775-
: part.state === "input-available" && message.metadata?.partial
776-
? "interrupted"
777-
: part.state === "input-available"
778-
? "executing"
779-
: "pending";
772+
// Determine status based on part state and result
773+
let status: "pending" | "executing" | "completed" | "failed" | "interrupted";
774+
if (part.state === "output-available") {
775+
// Check if result indicates failure (for tools that return { success: boolean })
776+
const output = part.output as unknown;
777+
const isFailed =
778+
typeof output === "object" &&
779+
output !== null &&
780+
"success" in output &&
781+
output.success === false;
782+
status = isFailed ? "failed" : "completed";
783+
} else if (part.state === "input-available" && message.metadata?.partial) {
784+
status = "interrupted";
785+
} else if (part.state === "input-available") {
786+
status = "executing";
787+
} else {
788+
status = "pending";
789+
}
780790

781791
displayedMessages.push({
782792
type: "tool",

0 commit comments

Comments
 (0)