Files
windmill/frontend/src/lib/components/copilot/chat/toolCallArguments.ts
T
GuilhemandClaude Fable 5 4ba17d0f9c fix: prevent truncated tool call args from bricking AI chat sessions (#9902)
* fix(frontend): prevent truncated ai chat tool-call arguments from bricking the session

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: heal empty tool-call arguments when replaying chat history

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: state sanitize invariant without drafting history

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:26:30 +02:00

59 lines
1.8 KiB
TypeScript

import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions.mjs'
/**
* A tool call whose `arguments` string is not valid JSON (typically a stream
* cut mid-arguments) must never reach a provider: the whole request is
* rejected, and once persisted in the session history every follow-up request
* fails the same way.
*
* Empty arguments count as valid: some providers stream '' for no-arg tool
* calls, and flagging those would wrongly report the tool as not executed.
* Callers persisting arguments must normalize '' to '{}' themselves.
*/
export function hasValidToolCallArguments(args: string | undefined): boolean {
if (!args) {
return true
}
try {
JSON.parse(args)
return true
} catch {
return false
}
}
// '' is valid per hasValidToolCallArguments but JSON.parse('') still throws
// provider-side, so replaying history additionally requires non-empty args.
function isReplayableArguments(args: string | undefined): boolean {
return !!args && hasValidToolCallArguments(args)
}
/**
* Replaces unparseable or empty assistant tool_call arguments with '{}' in the
* outgoing copy of the history, so a session whose persisted history contains
* a truncated tool call recovers instead of failing every request. The paired
* tool result already tells the model the call failed.
*/
export function sanitizeToolCallArguments(
messages: ChatCompletionMessageParam[]
): ChatCompletionMessageParam[] {
return messages.map((m) => {
if (
m.role !== 'assistant' ||
!m.tool_calls?.some(
(t) => t.type === 'function' && !isReplayableArguments(t.function.arguments)
)
) {
return m
}
return {
...m,
tool_calls: m.tool_calls.map((t) =>
t.type === 'function' && !isReplayableArguments(t.function.arguments)
? { ...t, function: { ...t.function, arguments: '{}' } }
: t
)
}
})
}