|
1 | 1 | import { AGENT_TOOLS, AGENT_TOOL_NAMES, RESERVED_AGENT_TOOL_NAMES, getToolsForMode, SYSTEM_PROMPT_ASK, SYSTEM_PROMPT_ACT, SYSTEM_PROMPT_ACT_COMPACT, SYSTEM_PROMPT_ACT_MID, SYSTEM_PROMPT_DEV_APPENDIX, SYSTEM_PROMPT_WEBMCP_ASK, SYSTEM_PROMPT_WEBMCP_ACT } from './tools.js'; |
2 | 2 | import { handleDoneJson } from './cloud-output.js'; |
3 | 3 | import { LoopDetector } from './loop-detector.js'; |
| 4 | +import { parseToolCallsFromText } from './tool-call-parser.js'; |
4 | 5 | import { BROWSER_MUTATION_TOOLS, STATE_CHANGE_TOOLS as SHARED_STATE_CHANGE_TOOLS } from './mutation-tools.js'; |
5 | 6 | import { isCredentialField, CREDENTIAL_NOTE_STRICT, STRICT_SECRET_SYSTEM_NOTE } from './credential-fields.js'; |
6 | 7 | import { detectProgressAction, formatLedgerRow, formatLedgerSummary, isBlockedLedgerDowngrade, isTerminalLedgerStatus, isValidLedgerStatus, ledgerDoneBlock, ledgerRowKey, normalizeLedgerStatus, progressCounts, selectLedgerRows, unresolvedLedgerRows, upsertLedgerItems } from './progress-ledger.js'; |
@@ -18715,136 +18716,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d |
18715 | 18716 | * smaller set (e.g. COMPACT_TOOL_NAMES) to restrict in compact mode. |
18716 | 18717 | */ |
18717 | 18718 | _tryParseToolCallsFromText(text, allowedNames = AGENT_TOOL_NAMES) { |
18718 | | - if (!text || text.length > 10000) return []; |
18719 | | - |
18720 | | - const results = []; |
18721 | | - const parseXmlParamValue = (value) => { |
18722 | | - const cleaned = String(value || '') |
18723 | | - .replace(/<[^>]+>/g, '') |
18724 | | - .trim(); |
18725 | | - if (!cleaned) return ''; |
18726 | | - try { |
18727 | | - if (/^(?:"|'.*'|\{|\[|-?\d|true\b|false\b|null\b)/i.test(cleaned)) { |
18728 | | - return JSON.parse(cleaned.replace(/^'([\s\S]*)'$/, '"$1"')); |
18729 | | - } |
18730 | | - } catch { /* fall through to string cleanup */ } |
18731 | | - return cleaned.replace(/^["']+|["']+$/g, ''); |
18732 | | - }; |
18733 | | - |
18734 | | - // Collect candidate JSON strings from known wrapper patterns. |
18735 | | - const patterns = [ |
18736 | | - // <tool_call>JSON</tool_call> |
18737 | | - /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/gi, |
18738 | | - // <|tool_call|>JSON<|/tool_call|> or <|tool_call>JSON<tool_call|> |
18739 | | - /<\|tool_call\|?>\s*([\s\S]*?)\s*<\|?\/?tool_call\|?>/gi, |
18740 | | - // <functioncall>JSON</functioncall> |
18741 | | - /<functioncall>\s*([\s\S]*?)\s*<\/functioncall>/gi, |
18742 | | - ]; |
18743 | | - |
18744 | | - for (const re of patterns) { |
18745 | | - let m; |
18746 | | - while ((m = re.exec(text)) !== null) { |
18747 | | - const inner = m[1].trim(); |
18748 | | - // Try JSON first (most common). |
18749 | | - try { |
18750 | | - const obj = JSON.parse(inner); |
18751 | | - if (obj && obj.name && allowedNames.has(obj.name)) { |
18752 | | - results.push(obj); |
18753 | | - continue; |
18754 | | - } |
18755 | | - } catch { /* not JSON — try call:name{} format below */ } |
18756 | | - |
18757 | | - // call:toolName{key:<|"|>value<|"|>, ...} format. |
18758 | | - // Some local models use <|"|> as quote tokens and call:name as the |
18759 | | - // invocation syntax. Normalize to JSON and parse. |
18760 | | - const callMatch = /^call:(\w+)\s*\{([\s\S]*)\}$/.exec(inner); |
18761 | | - if (callMatch && allowedNames.has(callMatch[1])) { |
18762 | | - const toolName = callMatch[1]; |
18763 | | - let argsBody = callMatch[2] |
18764 | | - .replace(/<\|"\|>/g, '"') // replace quote tokens with real quotes |
18765 | | - .replace(/<\|'\\?\|>/g, "'"); // handle single-quote tokens if any |
18766 | | - // argsBody is now like: url:"https://example.com",text:"hello" |
18767 | | - // Wrap unquoted keys to make valid JSON: key:"val" → "key":"val" |
18768 | | - argsBody = argsBody.replace(/(?<=^|,)\s*(\w+)\s*:/g, '"$1":'); |
18769 | | - try { |
18770 | | - const args = JSON.parse(`{${argsBody}}`); |
18771 | | - results.push({ name: toolName, arguments: args }); |
18772 | | - } catch { |
18773 | | - // If JSON parse still fails, try treating entire body as single |
18774 | | - // string argument for zero-arg or simple calls. |
18775 | | - results.push({ name: toolName, arguments: {} }); |
18776 | | - } |
18777 | | - continue; |
18778 | | - } |
18779 | | - } |
18780 | | - } |
18781 | | - |
18782 | | - // XML-ish tool-call format used by some local/chat-template models: |
18783 | | - // <tool_call><function=click_ax><parameter=ref_id>ref_6</parameter>... |
18784 | | - const xmlToolRe = /<tool_call>\s*<function(?:\s*=\s*["']?([A-Za-z_]\w*)["']?|\s+name\s*=\s*["']?([A-Za-z_]\w*)["']?)\s*>\s*([\s\S]*?)\s*<\/function>\s*<\/tool_call>/gi; |
18785 | | - let xmlMatch; |
18786 | | - while ((xmlMatch = xmlToolRe.exec(text)) !== null) { |
18787 | | - const toolName = xmlMatch[1] || xmlMatch[2]; |
18788 | | - if (!allowedNames.has(toolName)) continue; |
18789 | | - const body = xmlMatch[3] || ''; |
18790 | | - const args = {}; |
18791 | | - const paramRe = /<parameter(?:\s*=\s*["']?([A-Za-z_]\w*)["']?|\s+name\s*=\s*["']?([A-Za-z_]\w*)["']?)\s*>\s*([\s\S]*?)\s*<\/parameter>/gi; |
18792 | | - let paramMatch; |
18793 | | - while ((paramMatch = paramRe.exec(body)) !== null) { |
18794 | | - const key = paramMatch[1] || paramMatch[2]; |
18795 | | - if (!key) continue; |
18796 | | - args[key] = parseXmlParamValue(paramMatch[3]); |
18797 | | - } |
18798 | | - results.push({ name: toolName, arguments: args }); |
18799 | | - } |
18800 | | - |
18801 | | - // Fallback: scan for bare JSON objects containing a "name" key with a |
18802 | | - // known tool name. Only look for top-level objects (starts with {). |
18803 | | - if (results.length === 0) { |
18804 | | - const bareRe = /\{[^{}]*"name"\s*:\s*"(\w+)"[^{}]*\}/g; |
18805 | | - let m; |
18806 | | - while ((m = bareRe.exec(text)) !== null) { |
18807 | | - if (!allowedNames.has(m[1])) continue; |
18808 | | - try { |
18809 | | - const obj = JSON.parse(m[0]); |
18810 | | - if (obj && obj.name && allowedNames.has(obj.name)) { |
18811 | | - results.push(obj); |
18812 | | - } |
18813 | | - } catch { /* skip */ } |
18814 | | - } |
18815 | | - } |
18816 | | - |
18817 | | - // Last resort: call:toolName{...} outside of any wrapper tags. |
18818 | | - if (results.length === 0) { |
18819 | | - const callRe = /call:(\w+)\s*\{([\s\S]*?)\}/g; |
18820 | | - let m; |
18821 | | - while ((m = callRe.exec(text)) !== null) { |
18822 | | - if (!allowedNames.has(m[1])) continue; |
18823 | | - const toolName = m[1]; |
18824 | | - let argsBody = m[2] |
18825 | | - .replace(/<\|"\|>/g, '"') |
18826 | | - .replace(/<\|'\\?\|>/g, "'"); |
18827 | | - argsBody = argsBody.replace(/(?<=^|,)\s*(\w+)\s*:/g, '"$1":'); |
18828 | | - try { |
18829 | | - const args = JSON.parse(`{${argsBody}}`); |
18830 | | - results.push({ name: toolName, arguments: args }); |
18831 | | - } catch { |
18832 | | - results.push({ name: toolName, arguments: {} }); |
18833 | | - } |
18834 | | - } |
18835 | | - } |
18836 | | - |
18837 | | - // Convert to OpenAI tool_calls format. |
18838 | | - return results.map((obj, i) => ({ |
18839 | | - id: `fallback_call_${Date.now()}_${i}`, |
18840 | | - type: 'function', |
18841 | | - function: { |
18842 | | - name: obj.name, |
18843 | | - arguments: typeof obj.arguments === 'string' |
18844 | | - ? obj.arguments |
18845 | | - : JSON.stringify(obj.arguments || obj.parameters || {}), |
18846 | | - }, |
18847 | | - })); |
| 18719 | + return parseToolCallsFromText(text, allowedNames); |
18848 | 18720 | } |
18849 | 18721 | // ───────────────────────────────────────────────────────────────────── |
18850 | 18722 |
|
|
0 commit comments