'use strict'; const { parseToolCalls } = require('./parse'); // Tag pairs ordered longest-first: wrapper tags checked before inner tags. const XML_TOOL_TAG_PAIRS = [ { open: '' }, { open: '' }, { open: '' }, { open: '' }, { open: '' }, { open: '' }, ]; const XML_TOOL_OPENING_TAGS = XML_TOOL_TAG_PAIRS.map(p => p.open); function consumeXMLToolCapture(captured, toolNames, trimWrappingJSONFence) { const lower = captured.toLowerCase(); // Find the FIRST matching open/close pair, preferring wrapper tags. for (const pair of XML_TOOL_TAG_PAIRS) { const openIdx = lower.indexOf(pair.open); if (openIdx < 0) { continue; } // Find the LAST occurrence of the specific closing tag. const closeIdx = lower.lastIndexOf(pair.close); if (closeIdx < openIdx) { // Opening tag present but specific closing tag hasn't arrived. // Return not-ready — do NOT fall through to inner pairs. return { ready: false, prefix: '', calls: [], suffix: '' }; } const closeEnd = closeIdx + pair.close.length; const xmlBlock = captured.slice(openIdx, closeEnd); let prefixPart = captured.slice(0, openIdx); let suffixPart = captured.slice(closeEnd); const parsed = parseToolCalls(xmlBlock, toolNames); if (Array.isArray(parsed) && parsed.length > 0) { const trimmedFence = trimWrappingJSONFence(prefixPart, suffixPart); return { ready: true, prefix: trimmedFence.prefix, calls: parsed, suffix: trimmedFence.suffix, }; } // XML tool syntax but failed to parse — consume to avoid leak. return { ready: true, prefix: prefixPart, calls: [], suffix: suffixPart }; } return { ready: false, prefix: '', calls: [], suffix: '' }; } function hasOpenXMLToolTag(captured) { const lower = captured.toLowerCase(); for (const pair of XML_TOOL_TAG_PAIRS) { if (lower.includes(pair.open)) { if (!lower.includes(pair.close)) { return true; } } } return false; } function findPartialXMLToolTagStart(s) { const lastLT = s.lastIndexOf('<'); if (lastLT < 0) { return -1; } const tail = s.slice(lastLT); if (tail.includes('>')) { return -1; } const lowerTail = tail.toLowerCase(); for (const tag of XML_TOOL_OPENING_TAGS) { const tagWithLT = tag.startsWith('<') ? tag : '<' + tag; if (tagWithLT.startsWith(lowerTail)) { return lastLT; } } return -1; } function looksLikeXMLToolTagFragment(s) { const trimmed = (s || '').trim(); if (!trimmed) return false; const lower = trimmed.toLowerCase(); const fragments = [ 'tool_calls>', 'tool_call>', '/tool_calls>', '/tool_call>', 'function_calls>', 'function_call>', '/function_calls>', '/function_call>', 'invoke>', '/invoke>', 'tool_use>', '/tool_use>', 'tool_name>', '/tool_name>', 'parameters>', '/parameters>', ]; return fragments.some(f => lower.includes(f)); } module.exports = { consumeXMLToolCapture, hasOpenXMLToolTag, findPartialXMLToolTagStart, looksLikeXMLToolTagFragment, };