(text, callerLookup = new Map())
| 1143 | * proposal. Convert it back to the caller's declared tool name. |
| 1144 | */ |
| 1145 | export function parseNativeFunctionCallsFromText(text, callerLookup = new Map()) { |
| 1146 | if (typeof text !== 'string' || !/<function_calls\b/i.test(text)) { |
| 1147 | return { text: text || '', toolCalls: [] }; |
| 1148 | } |
| 1149 | const calls = []; |
| 1150 | const keepOutsideBlocks = []; |
| 1151 | let lastBlockEnd = 0; |
| 1152 | const blockRe = /<function_calls\b[^>]*>([\s\S]*?)<\/function_calls>/gi; |
| 1153 | const invokeRe = /<invoke\b([^>]*)>([\s\S]*?)<\/invoke>/gi; |
| 1154 | let block; |
| 1155 | while ((block = blockRe.exec(text)) !== null) { |
| 1156 | keepOutsideBlocks.push(text.slice(lastBlockEnd, block.index)); |
| 1157 | lastBlockEnd = block.index + block[0].length; |
| 1158 | invokeRe.lastIndex = 0; |
| 1159 | let match; |
| 1160 | while ((match = invokeRe.exec(block[1])) !== null) { |
| 1161 | const nameAttr = match[1].match(/\bname\s*=\s*"([^"]+)"/i) |
| 1162 | || match[1].match(/\bname\s*=\s*'([^']+)'/i); |
| 1163 | const rawName = nameAttr ? unescapeXmlText(nameAttr[1]).trim() : ''; |
| 1164 | const entry = TOOL_MAP[rawName]; |
| 1165 | if (!rawName || !entry) continue; |
| 1166 | const params = {}; |
| 1167 | const paramRe = /<parameter\b([^>]*)>([\s\S]*?)<\/parameter>/gi; |
| 1168 | let pm; |
| 1169 | while ((pm = paramRe.exec(match[2])) !== null) { |
| 1170 | const paramNameAttr = pm[1].match(/\bname\s*=\s*"([^"]+)"/i) |
| 1171 | || pm[1].match(/\bname\s*=\s*'([^']+)'/i); |
| 1172 | const paramName = paramNameAttr ? unescapeXmlText(paramNameAttr[1]).trim() : ''; |
| 1173 | if (!paramName) continue; |
| 1174 | const rawValue = unescapeXmlText(pm[2].trim()); |
| 1175 | const parsed = safeJsonParse(rawValue); |
| 1176 | params[paramName] = Object.keys(parsed).length || /^[\[{]/.test(rawValue.trim()) |
| 1177 | ? parsed |
| 1178 | : rawValue; |
| 1179 | } |
| 1180 | const cascadeKind = entry.kind; |
| 1181 | const candidates = callerLookup?.get(cascadeKind) || []; |
| 1182 | const callerName = candidates[0]; |
| 1183 | if (!callerName) continue; |
| 1184 | const reverseFn = TOOL_MAP[callerName]?.reverse || identityArgs; |
| 1185 | let openaiArgs; |
| 1186 | try { openaiArgs = reverseFn(entry.forward(params)); } |
| 1187 | catch { openaiArgs = params; } |
| 1188 | calls.push({ |
| 1189 | id: `call_native_${calls.length}_${Date.now().toString(36)}`, |
| 1190 | name: callerName, |
| 1191 | argumentsJson: JSON.stringify(openaiArgs ?? {}), |
| 1192 | }); |
| 1193 | } |
| 1194 | } |
| 1195 | const trailing = text.slice(lastBlockEnd); |
| 1196 | const danglingOpen = trailing.search(/<function_calls\b/i); |
| 1197 | keepOutsideBlocks.push(danglingOpen === -1 ? trailing : trailing.slice(0, danglingOpen)); |
| 1198 | return { text: keepOutsideBlocks.join('').trim(), toolCalls: calls }; |
| 1199 | } |
| 1200 | |
| 1201 | /** |
| 1202 | * Streaming guard for Claude provider-native <function_calls> blocks in native |
no test coverage detected