fix: properly persist and render tool calls in agent chat
- Capture tool call inputs from onChunk and onStepFinish callbacks - Store toolCalls array in ASSISTANT messages with proper format - Update chat page to combine ASSISTANT toolCalls (input) with TOOL messages (output) - Show complete tool call cards with both input args and output results - Display token usage in assistant messages for debugging - Handle pending tool calls from ASSISTANT messages without results 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d6de3e025a
commit
6f8617fead
2 changed files with 120 additions and 29 deletions
|
|
@ -69,6 +69,13 @@ interface Message {
|
|||
toolName?: string;
|
||||
toolCallId?: string;
|
||||
toolResult?: unknown;
|
||||
toolCalls?: Array<{
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
}>;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
|
@ -754,17 +761,34 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
|||
}
|
||||
};
|
||||
|
||||
// Build a lookup map for tool call inputs from ASSISTANT messages
|
||||
// This allows us to show the input args when rendering TOOL messages
|
||||
const getToolCallInput = (toolCallId: string): unknown => {
|
||||
for (const msg of messages) {
|
||||
if (msg.role === 'ASSISTANT' && msg.toolCalls) {
|
||||
const tc = msg.toolCalls.find((t) => t.toolCallId === toolCallId);
|
||||
if (tc) return tc.args;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Check if a tool call has a corresponding TOOL message (meaning it completed)
|
||||
const hasToolResult = (toolCallId: string): boolean => {
|
||||
return messages.some((m) => m.role === 'TOOL' && m.toolCallId === toolCallId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-4 py-2">
|
||||
<div
|
||||
className={`flex ${message.role === 'USER' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
{message.role === 'TOOL' ? (
|
||||
{/* TOOL message - shows the result of a tool call */}
|
||||
{message.role === 'TOOL' && (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[80%]">
|
||||
<ToolCallCard
|
||||
toolCall={{
|
||||
toolCallId: message.toolCallId || message.id,
|
||||
toolName: message.toolName || 'Unknown Tool',
|
||||
input: getToolCallInput(message.toolCallId || ''),
|
||||
output: getToolOutput(),
|
||||
status: 'success',
|
||||
}}
|
||||
|
|
@ -772,18 +796,64 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
|||
onToggle={() => toggleToolCall(message.toolCallId || message.id)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`max-w-[80%] rounded-lg p-4 ${
|
||||
message.role === 'USER'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-surface-secondary'
|
||||
}`}
|
||||
>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* USER message */}
|
||||
{message.role === 'USER' && (
|
||||
<div className="flex justify-end">
|
||||
<div className="max-w-[80%] rounded-lg p-4 bg-primary text-primary-foreground">
|
||||
<p className="whitespace-pre-wrap text-sm">{message.content}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ASSISTANT message - may contain text and/or tool calls */}
|
||||
{message.role === 'ASSISTANT' && (
|
||||
<div className="space-y-2">
|
||||
{/* Assistant text content */}
|
||||
{message.content && (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
|
||||
<p className="whitespace-pre-wrap text-sm">{message.content}</p>
|
||||
{/* Token usage for debugging */}
|
||||
{(message.inputTokens || message.outputTokens) && (
|
||||
<div className="mt-2 pt-2 border-t border-border/50 text-[10px] text-foreground-tertiary font-mono">
|
||||
{message.inputTokens && <span>In: {message.inputTokens}</span>}
|
||||
{message.inputTokens && message.outputTokens && (
|
||||
<span> • </span>
|
||||
)}
|
||||
{message.outputTokens && (
|
||||
<span>Out: {message.outputTokens}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show tool calls from this assistant message that don't have results yet */}
|
||||
{message.toolCalls &&
|
||||
message.toolCalls
|
||||
.filter((tc) => !hasToolResult(tc.toolCallId))
|
||||
.map((tc) => (
|
||||
<div key={tc.toolCallId} className="flex justify-start">
|
||||
<div className="max-w-[80%]">
|
||||
<ToolCallCard
|
||||
toolCall={{
|
||||
toolCallId: tc.toolCallId,
|
||||
toolName: tc.toolName,
|
||||
input: tc.args,
|
||||
status: 'pending',
|
||||
}}
|
||||
isExpanded={expandedToolCalls.has(tc.toolCallId)}
|
||||
onToggle={() => toggleToolCall(tc.toolCallId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -266,8 +266,9 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
|
|||
try {
|
||||
const startTime = Date.now();
|
||||
let fullContent = '';
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Dynamic tool call structure
|
||||
let allToolCalls: any[] = [];
|
||||
// Accumulate tool calls with their input args
|
||||
const toolCallsMap: Map<string, { toolCallId: string; toolName: string; args: unknown }> =
|
||||
new Map();
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
|
||||
|
|
@ -278,16 +279,39 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
|
|||
tools,
|
||||
stopWhen: stepCountIs(agent.maxToolCallsPerTurn),
|
||||
onChunk: async ({ chunk }) => {
|
||||
// Stream tool calls as they come in
|
||||
// Stream tool calls as they come in and capture their inputs
|
||||
if (chunk.type === 'tool-call') {
|
||||
const input = 'args' in chunk ? chunk.args : chunk.input;
|
||||
// Store tool call with input for later persistence
|
||||
toolCallsMap.set(chunk.toolCallId, {
|
||||
toolCallId: chunk.toolCallId,
|
||||
toolName: chunk.toolName,
|
||||
args: input,
|
||||
});
|
||||
sendEvent('tool_call', {
|
||||
toolCallId: chunk.toolCallId,
|
||||
toolName: chunk.toolName,
|
||||
input: 'args' in chunk ? chunk.args : chunk.input,
|
||||
input,
|
||||
});
|
||||
}
|
||||
},
|
||||
onStepFinish: async ({ toolResults, usage }) => {
|
||||
onStepFinish: async ({ toolCalls, toolResults, usage }) => {
|
||||
// Capture tool calls from step finish (backup in case onChunk missed any)
|
||||
if (toolCalls && Array.isArray(toolCalls)) {
|
||||
for (const tc of toolCalls) {
|
||||
if (!toolCallsMap.has(tc.toolCallId)) {
|
||||
// Use 'input' from DynamicToolCall or fall back to type assertion for typed calls
|
||||
const args =
|
||||
'input' in tc ? tc.input : 'args' in tc ? (tc as { args: unknown }).args : {};
|
||||
toolCallsMap.set(tc.toolCallId, {
|
||||
toolCallId: tc.toolCallId,
|
||||
toolName: tc.toolName,
|
||||
args,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send tool results
|
||||
if (toolResults && toolResults.length > 0) {
|
||||
for (const tr of toolResults) {
|
||||
|
|
@ -325,17 +349,10 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
|
|||
}
|
||||
|
||||
// Get final response data
|
||||
const finalResponse = await result.response;
|
||||
const finalUsage = await result.usage;
|
||||
|
||||
// Extract tool calls from final response
|
||||
if (finalResponse.messages) {
|
||||
for (const msg of finalResponse.messages) {
|
||||
if ('toolCalls' in msg && msg.toolCalls && Array.isArray(msg.toolCalls)) {
|
||||
allToolCalls = [...allToolCalls, ...(msg.toolCalls as unknown[])];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Convert tool calls map to array for storage
|
||||
const allToolCalls = Array.from(toolCallsMap.values());
|
||||
|
||||
// Update token counts from final usage
|
||||
if (finalUsage) {
|
||||
|
|
@ -349,7 +366,11 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
|
|||
conversationId: conversation.id,
|
||||
role: 'ASSISTANT',
|
||||
content: fullContent,
|
||||
toolCalls: allToolCalls.length > 0 ? allToolCalls : Prisma.JsonNull,
|
||||
// Cast to Prisma-compatible JSON type
|
||||
toolCalls:
|
||||
allToolCalls.length > 0
|
||||
? (allToolCalls as unknown as Prisma.InputJsonValue)
|
||||
: Prisma.JsonNull,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue