( messages: Message[], tools: Tools = [], )
| 1989 | } |
| 1990 | |
| 1991 | export function normalizeMessagesForAPI( |
| 1992 | messages: Message[], |
| 1993 | tools: Tools = [], |
| 1994 | ): (UserMessage | AssistantMessage)[] { |
| 1995 | // Build set of available tool names for filtering unavailable tool references |
| 1996 | const availableToolNames = new Set(tools.map(t => t.name)) |
| 1997 | |
| 1998 | // First, reorder attachments to bubble up until they hit a tool result or assistant message |
| 1999 | // Then strip virtual messages — they're display-only (e.g. REPL inner tool |
| 2000 | // calls) and must never reach the API. |
| 2001 | const reorderedMessages = reorderAttachmentsForAPI(messages).filter( |
| 2002 | m => !((m.type === 'user' || m.type === 'assistant') && m.isVirtual), |
| 2003 | ) |
| 2004 | |
| 2005 | // Build a map from error text → which block types to strip from the preceding user message. |
| 2006 | const errorToBlockTypes: Record<string, Set<string>> = { |
| 2007 | [getPdfTooLargeErrorMessage()]: new Set(['document']), |
| 2008 | [getPdfPasswordProtectedErrorMessage()]: new Set(['document']), |
| 2009 | [getPdfInvalidErrorMessage()]: new Set(['document']), |
| 2010 | [getImageTooLargeErrorMessage()]: new Set(['image']), |
| 2011 | [getRequestTooLargeErrorMessage()]: new Set(['document', 'image']), |
| 2012 | } |
| 2013 | |
| 2014 | // Walk the reordered messages to build a targeted strip map: |
| 2015 | // userMessageUUID → set of block types to strip from that message. |
| 2016 | const stripTargets = new Map<string, Set<string>>() |
| 2017 | for (let i = 0; i < reorderedMessages.length; i++) { |
| 2018 | const msg = reorderedMessages[i]! |
| 2019 | if (!isSyntheticApiErrorMessage(msg)) { |
| 2020 | continue |
| 2021 | } |
| 2022 | // Determine which error this is |
| 2023 | const errorText = |
| 2024 | Array.isArray(msg.message.content) && |
| 2025 | msg.message.content[0]?.type === 'text' |
| 2026 | ? msg.message.content[0].text |
| 2027 | : undefined |
| 2028 | if (!errorText) { |
| 2029 | continue |
| 2030 | } |
| 2031 | const blockTypesToStrip = errorToBlockTypes[errorText] |
| 2032 | if (!blockTypesToStrip) { |
| 2033 | continue |
| 2034 | } |
| 2035 | // Walk backward to find the nearest preceding isMeta user message |
| 2036 | for (let j = i - 1; j >= 0; j--) { |
| 2037 | const candidate = reorderedMessages[j]! |
| 2038 | if (candidate.type === 'user' && candidate.isMeta) { |
| 2039 | const existing = stripTargets.get(candidate.uuid) |
| 2040 | if (existing) { |
| 2041 | for (const t of blockTypesToStrip) { |
| 2042 | existing.add(t) |
| 2043 | } |
| 2044 | } else { |
| 2045 | stripTargets.set(candidate.uuid, new Set(blockTypesToStrip)) |
| 2046 | } |
| 2047 | break |
| 2048 | } |
no test coverage detected