Assistant: suppress repeated render-tool calls
All checks were successful
Deploy to VPS / test (push) Successful in 1m31s
Deploy to VPS / deploy (push) Successful in 2m1s

After the client returns a render tool's result, the model sometimes
re-issues the identical call with a fresh id on the follow-up run,
painting a second copy of the spending-summary card (observed live on
MAF 1.10 / CK 1.62). DeduplicateToolCallMiddleware now tracks which
render tools already ran in the current user turn and drops repeats —
their streamed TOOL_CALL_* events and their entries in the run's
MESSAGES_SNAPSHOT. The system prompt also tells the model the card is
already displayed once its "ok" result is present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-07-04 18:10:51 -06:00
parent bbcfaa7808
commit eb400ab1e9
2 changed files with 62 additions and 0 deletions

View File

@@ -56,6 +56,9 @@ Generative UI — render tools:
nothing. The rendered card IS the complete response. Any text you write in nothing. The rendered card IS the complete response. Any text you write in
the same message as a render call will appear as a duplicate below the the same message as a render call will appear as a duplicate below the
card, which is wrong. card, which is wrong.
- When a render_spending_summary tool result ("ok") is already present for
the current question, the card is already on screen. Do NOT call the tool
again and do NOT add text — end your response with no further output.
""" """

View File

@@ -120,6 +120,27 @@ class DeduplicateToolCallMiddleware extends (Middleware as any) {
const open = new Set<string>(); // tool call IDs currently in progress const open = new Set<string>(); // tool call IDs currently in progress
const closed = new Set<string>(); // tool call IDs that already received END const closed = new Set<string>(); // tool call IDs that already received END
// Render tools are display-once: after the client returns their result,
// the model sometimes re-issues the same call (fresh id) on the
// follow-up run, painting a second identical card. Track which render
// tools already ran in the current user turn (from the request's own
// message history) and suppress repeats — both their streamed events
// and their entries in this run's MESSAGES_SNAPSHOT.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const inputMessages: any[] = Array.isArray(input?.messages) ? input.messages : [];
let lastUserIdx = -1;
inputMessages.forEach((m, i) => { if (m?.role === "user") lastUserIdx = i; });
const renderedThisTurn = new Set<string>();
for (const m of inputMessages.slice(lastUserIdx + 1)) {
const calls = m?.toolCalls ?? m?.tool_calls;
if (m?.role !== "assistant" || !Array.isArray(calls)) continue;
for (const tc of calls) {
const name = tc?.function?.name;
if (name && RENDER_TOOLS.has(name)) renderedThisTurn.add(name);
}
}
const suppressed = new Set<string>(); // toolCallIds of dropped repeats
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Observable<any>((observer) => { return new Observable<any>((observer) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -132,13 +153,51 @@ class DeduplicateToolCallMiddleware extends (Middleware as any) {
if (type === EventType.TOOL_CALL_START) { if (type === EventType.TOOL_CALL_START) {
if (!id || closed.has(id) || open.has(id)) return; // duplicate if (!id || closed.has(id) || open.has(id)) return; // duplicate
const name: string | undefined = event?.toolCallName;
if (name && RENDER_TOOLS.has(name)) {
if (renderedThisTurn.has(name)) {
suppressed.add(id);
return; // repeat render call — drop the whole call
}
renderedThisTurn.add(name);
}
open.add(id); open.add(id);
} else if (type === EventType.TOOL_CALL_ARGS || type === EventType.TOOL_CALL_END) { } else if (type === EventType.TOOL_CALL_ARGS || type === EventType.TOOL_CALL_END) {
if (id && suppressed.has(id)) return;
if (id && closed.has(id)) return; // already completed, drop if (id && closed.has(id)) return; // already completed, drop
if (type === EventType.TOOL_CALL_END && id) { if (type === EventType.TOOL_CALL_END && id) {
open.delete(id); open.delete(id);
closed.add(id); closed.add(id);
} }
} else if (type === EventType.MESSAGES_SNAPSHOT && suppressed.size > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const msgs: any[] = Array.isArray(event?.messages) ? event.messages : [];
const cleaned = msgs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((m: any) => {
const calls = m?.toolCalls ?? m?.tool_calls;
if (m?.role !== "assistant" || !Array.isArray(calls)) return m;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const kept = calls.filter((tc: any) => !suppressed.has(String(tc?.id ?? "")));
if (kept.length === calls.length) return m;
const { toolCalls: _a, tool_calls: _b, ...rest } = m;
return kept.length > 0 ? { ...rest, toolCalls: kept } : rest;
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((m: any) => {
if (m?.role === "tool" && suppressed.has(String(m?.toolCallId ?? m?.tool_call_id ?? ""))) {
return false;
}
if (m?.role === "assistant") {
const calls = m?.toolCalls ?? m?.tool_calls;
const hasCalls = Array.isArray(calls) && calls.length > 0;
const hasText = typeof m?.content === "string" && m.content.length > 0;
if (!hasCalls && !hasText) return false;
}
return true;
});
observer.next({ ...event, messages: cleaned });
return;
} }
observer.next(event); // emit raw BaseEvent observer.next(event); // emit raw BaseEvent