function messageContent(message) { return Array.isArray(message?.content) ? message.content : []; } function toolItemId(item, type) { if (item?.type !== type) return null; return String(item?.id ?? '').trim() || null; } function cloneMessageWithContent(message, content) { return { ...message, content }; } /** * Goose can persist concurrently completed tools out of order. OpenAI-compatible * providers reject the next turn when an assistant tool request is not followed * immediately by its matching tool response. Re-pair completed calls and remove * incomplete/orphaned protocol items while preserving all visible text. */ export function repairConversationToolHistory(conversation) { if (!Array.isArray(conversation) || conversation.length === 0) { return { conversation: Array.isArray(conversation) ? conversation : [], changed: false, repairedToolResponses: 0, droppedToolRequests: 0, droppedOrphanResponses: 0, }; } const responsesById = new Map(); let totalToolResponses = 0; for (const message of conversation) { for (const item of messageContent(message)) { const id = toolItemId(item, 'toolResponse'); if (!id) continue; totalToolResponses += 1; if (!responsesById.has(id)) responsesById.set(id, { message, item }); } } const repaired = []; const consumedResponseIds = new Set(); let repairedToolResponses = 0; let droppedToolRequests = 0; for (const message of conversation) { const content = messageContent(message); const requestItems = content.filter((item) => item?.type === 'toolRequest'); const responseItems = content.filter((item) => item?.type === 'toolResponse'); if (requestItems.length === 0) { if (responseItems.length === 0) { repaired.push(message); continue; } const remaining = content.filter((item) => item?.type !== 'toolResponse'); if (remaining.length > 0) repaired.push(cloneMessageWithContent(message, remaining)); continue; } const matchedRequests = []; for (const item of requestItems) { const id = toolItemId(item, 'toolRequest'); if (id && responsesById.has(id)) { matchedRequests.push(item); } else { droppedToolRequests += 1; } } const retainedContent = content.filter( (item) => item?.type !== 'toolRequest' && item?.type !== 'toolResponse', ); retainedContent.push(...matchedRequests); if (retainedContent.length > 0) { const unchanged = retainedContent.length === content.length && retainedContent.every((item, index) => item === content[index]); repaired.push(unchanged ? message : cloneMessageWithContent(message, retainedContent)); } for (const request of matchedRequests) { const id = toolItemId(request, 'toolRequest'); const response = responsesById.get(id); if (!response || consumedResponseIds.has(id)) continue; consumedResponseIds.add(id); repairedToolResponses += 1; const originalContent = messageContent(response.message); repaired.push( originalContent.length === 1 && originalContent[0] === response.item ? response.message : cloneMessageWithContent(response.message, [response.item]), ); } } const droppedOrphanResponses = Math.max(0, totalToolResponses - consumedResponseIds.size); const changed = repaired.length !== conversation.length || repaired.some((message, index) => message !== conversation[index]); return { conversation: changed ? repaired : conversation, changed, repairedToolResponses, droppedToolRequests, droppedOrphanResponses, }; }