Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 136 additions & 53 deletions tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ When you receive a tool result, analyze it and provide a helpful response to the

return result;
}

function normalizeMessages(messages) {
return messages.map(msg => {
const normalized = { role: msg.role };

if (msg.role === 'assistant' && msg.tool_calls) {
normalized.content = msg.tool_calls.map(tc =>
normalized.content = msg.tool_calls.map(tc =>
`TOOL_CALL: ${tc.function.name}\nARGUMENTS: ${tc.function.arguments}`
).join('\n\n');
} else if (typeof msg.content === 'string') {
Expand All @@ -76,7 +76,7 @@ function normalizeMessages(messages) {
} else if (msg.content) {
normalized.content = msg.content;
}

return normalized;
});
}
Expand Down Expand Up @@ -120,72 +120,72 @@ function parseMinimaxXML(responseText) {

const toolCalls = [];
const invokeMatches = minimaxMatch[1].matchAll(/<invoke\s+([^>]+)>(.*?)<\/invoke>/gs);

for (const invokeMatch of invokeMatches) {
const attrs = invokeMatch[1];
const innerContent = invokeMatch[2];

let toolName = '';
const args = {};

const attrPairs = attrs.matchAll(/(\w+):\s*"([^"]*)"/g);
for (const attrPair of attrPairs) {
const [, attrName, attrValue] = attrPair;
if (!toolName) {
toolName = extractToolName(attrName);
}

const mappedName = mapToolAttribute(attrName);
if (mappedName) {
args[mappedName] = attrValue;
}
}

const paramMatches = innerContent.matchAll(/<parameter\s+name="([^"]+)">([^<]*)<\/parameter>/g);
for (const paramMatch of paramMatches) {
args[paramMatch[1]] = paramMatch[2].trim();
}

if (toolName && Object.keys(args).length > 0) {
toolCalls.push(createToolCallObject(toolName, args, toolCalls.length));
}
}

return toolCalls;
}

function parseClaudeXML(responseText) {
const toolCalls = [];
const functionCallMatches = responseText.matchAll(/<invoke\s+name="([^"]+)">\s*<parameter_list>\s*(.*?)\s*<\/parameter_list>\s*<\/invoke>/gs);

for (const match of functionCallMatches) {
const [, toolName, parametersXml] = match;
const args = {};

const paramMatches = parametersXml.matchAll(/<parameter\s+name="([^"]+)">(.*?)<\/parameter>/gs);
for (const paramMatch of paramMatches) {
const [, paramName, paramValue] = paramMatch;
args[paramName] = paramValue.trim();
}

if (toolName && Object.keys(args).length > 0) {
toolCalls.push(createToolCallObject(toolName, args, toolCalls.length));
}
}

return toolCalls;
}

function parseOpenAIToolCalls(responseText) {
const toolCalls = [];

try {
const toolCallMatch = responseText.match(/"tool_calls"\s*:\s*\[(.*?)\]/s);
if (!toolCallMatch) return [];

const toolCallArray = `[${toolCallMatch[1]}]`;
const parsedToolCalls = JSON.parse(toolCallArray);

for (const toolCall of parsedToolCalls) {
if (toolCall.function && toolCall.function.name) {
toolCalls.push({
Expand All @@ -201,70 +201,153 @@ function parseOpenAIToolCalls(responseText) {
} catch (error) {
return [];
}

return toolCalls;
}

/**
* Sanitizes a JSON string by escaping unescaped control characters
* (newlines, tabs, etc.) that appear inside JSON string values.
* This handles patch texts and other multi-line content.
*/
function sanitizeJsonString(jsonStr) {
let result = '';
let inString = false;
let escapeNext = false;

for (let i = 0; i < jsonStr.length; i++) {
const char = jsonStr[i];

if (escapeNext) {
result += char;
escapeNext = false;
continue;
}

if (char === '\\') {
result += char;
escapeNext = true;
continue;
}

if (char === '"' && !inString) {
inString = true;
result += char;
continue;
} else if (char === '"' && inString) {
inString = false;
result += char;
continue;
}

// Inside a string: escape control characters
if (inString) {
if (char === '\n') {
result += '\\n';
} else if (char === '\r') {
result += '\\r';
} else if (char === '\t') {
result += '\\t';
} else if (char.charCodeAt(0) < 0x20) {
result += '\\u' + ('000' + char.charCodeAt(0).toString(16)).slice(-4);
} else {
result += char;
}
} else {
result += char;
}
}

return result;
}

function parseTextFormat(responseText) {
const toolCalls = [];
let currentIndex = 0;

const patterns = [
/TOOL_CALL:\s*(\w+).*?ARGUMENTS:\s*\{/is,
/TOOL_CALL:\s*(\w+)ARGUMENTS:\s*\{/is,
];


const toolCallPattern = /TOOL_CALL:\s*(\w+)[\s\S]*?ARGUMENTS:\s*\{/i;

while (currentIndex < responseText.length) {
let toolCallMatch = null;

for (const pattern of patterns) {
toolCallMatch = responseText.substring(currentIndex).match(pattern);
if (toolCallMatch) break;
}

const remainingText = responseText.substring(currentIndex);
const toolCallMatch = remainingText.match(toolCallPattern);

if (!toolCallMatch) break;

const toolName = toolCallMatch[1];
const fullMatchText = toolCallMatch[0];
const matchStartIndex = currentIndex + toolCallMatch.index;
const argsStartIndex = matchStartIndex + fullMatchText.lastIndexOf('{');

const argsStartIndex = matchStartIndex + toolCallMatch[0].lastIndexOf('{');

// Brace counting that respects JSON strings (doesn't count braces inside strings)
let braceCount = 0;
let inString = false;
let escapeNext = false;
let argsEndIndex = argsStartIndex;
let foundClosingBrace = false;

for (let i = argsStartIndex; i < responseText.length; i++) {
const char = responseText[i];
if (char === '{') {
braceCount++;
} else if (char === '}') {
braceCount--;
if (braceCount === 0) {
argsEndIndex = i + 1;
foundClosingBrace = true;
break;

if (escapeNext) {
escapeNext = false;
continue;
}

if (char === '\\') {
escapeNext = true;
continue;
}

if (char === '"' && !inString) {
inString = true;
continue;
} else if (char === '"' && inString) {
inString = false;
continue;
}

if (!inString) {
if (char === '{') {
braceCount++;
} else if (char === '}') {
braceCount--;
if (braceCount === 0) {
argsEndIndex = i + 1;
foundClosingBrace = true;
break;
}
}
}
}

if (!foundClosingBrace) {
console.warn('[Tool Parsing] Incomplete JSON for tool call:', toolName);
break;
}

const argsString = responseText.substring(argsStartIndex, argsEndIndex);


let argsString = responseText.substring(argsStartIndex, argsEndIndex);

// Sanitize: escape unescaped control characters inside JSON strings
argsString = sanitizeJsonString(argsString);

try {
const args = JSON.parse(argsString);
toolCalls.push(createToolCallObject(toolName, args, toolCalls.length));
currentIndex = argsEndIndex;
} catch (error) {
console.error('Failed to parse tool call arguments:', error);
console.debug('[Tool Parsing] Failed to parse:', argsString.substring(0, 200));
break;
console.error('Failed to parse tool call arguments:', error.message);
console.debug('[Tool Parsing] Failed substring (first 300 chars):', argsString.substring(0, 300));

// Try to recover: find the next TOOL_CALL and skip this broken one
const nextToolCall = responseText.substring(argsEndIndex).match(/TOOL_CALL:\s*(\w+)/i);
if (nextToolCall) {
currentIndex = argsEndIndex + nextToolCall.index;
console.warn('[Tool Parsing] Skipping broken JSON, trying next tool call at index', currentIndex);
} else {
break;
}
}
}

return toolCalls;
}

Expand Down