fix: deduplicate tool_call stream events by toolCallId (#199)

The Letta server streams tool_call_message events token-by-token as
the model generates tool arguments. A single tool call (e.g.
memory_rethink with a large new_memory arg) can produce hundreds of
wire events, all sharing the same toolCallId. Without dedup, the bot
miscounts these as separate tool calls -- logging "Calling tool: X"
hundreds of times and potentially triggering the tool-loop abort at
maxToolCalls (default 100) for what is actually a single call.

Track seen toolCallIds per stream and skip chunks already yielded.

Written by Cameron and Letta Code

"In the beginner's mind there are many possibilities, but in the
expert's mind there are few." -- Shunryu Suzuki
This commit is contained in:
Cameron
2026-02-06 16:52:33 -08:00
committed by GitHub
parent f413df8df7
commit 9cb35228fd

View File

@@ -477,8 +477,18 @@ export class LettaBot {
adapter.sendTypingIndicator(msg.chatId).catch(() => {});
}, 4000);
const seenToolCallIds = new Set<string>();
try {
for await (const streamMsg of session.stream()) {
// Deduplicate tool_call chunks: the server streams tool_call_message
// events token-by-token as arguments are generated, so a single tool
// call produces many wire events with the same toolCallId.
// Only count/log the first chunk per unique toolCallId.
if (streamMsg.type === 'tool_call') {
const toolCallId = (streamMsg as any).toolCallId;
if (toolCallId && seenToolCallIds.has(toolCallId)) continue;
if (toolCallId) seenToolCallIds.add(toolCallId);
}
const msgUuid = (streamMsg as any).uuid;
watchdog.ping();
receivedAnyData = true;