@jsonstudio/llms 0.6.1892 → 0.6.2172

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.js +16 -2
  2. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +7 -1
  3. package/dist/conversion/compat/actions/deepseek-web-response.js +302 -40
  4. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +5 -0
  5. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +7 -4
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +1 -0
  7. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +12 -0
  8. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
  9. package/dist/conversion/compat/actions/tool-text-request-guidance.d.ts +9 -0
  10. package/dist/conversion/compat/actions/tool-text-request-guidance.js +177 -0
  11. package/dist/conversion/compat/antigravity-session-signature.d.ts +6 -0
  12. package/dist/conversion/compat/antigravity-session-signature.js +15 -0
  13. package/dist/conversion/compat/profiles/chat-deepseek-web.json +52 -1
  14. package/dist/conversion/compat/profiles/chat-glm.json +22 -0
  15. package/dist/conversion/compat/profiles/chat-iflow.json +4 -0
  16. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +13 -27
  17. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +10 -1
  18. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +13 -4
  19. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -53
  20. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline.js +8 -4
  22. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +191 -9
  23. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +118 -15
  24. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +65 -2
  25. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.d.ts +34 -0
  26. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.js +75 -0
  27. package/dist/conversion/hub/process/chat-process.js +85 -18
  28. package/dist/conversion/hub/response/provider-response.js +21 -50
  29. package/dist/conversion/hub/response/response-runtime.js +71 -10
  30. package/dist/conversion/responses/responses-openai-bridge/response-payload.d.ts +3 -0
  31. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +576 -0
  32. package/dist/conversion/responses/responses-openai-bridge/types.d.ts +42 -0
  33. package/dist/conversion/responses/responses-openai-bridge/types.js +1 -0
  34. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -44
  35. package/dist/conversion/responses/responses-openai-bridge.js +193 -504
  36. package/dist/conversion/shared/anthropic-message-utils.js +82 -2
  37. package/dist/conversion/shared/bridge-message-utils.js +92 -39
  38. package/dist/conversion/shared/snapshot-hooks.js +8 -13
  39. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +2 -0
  40. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +129 -0
  41. package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +4 -0
  42. package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +637 -0
  43. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +21 -0
  44. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +177 -0
  45. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +5 -0
  46. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +385 -0
  47. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +10 -0
  48. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +602 -0
  49. package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +5 -0
  50. package/dist/conversion/shared/text-markup-normalizer/extractors.js +4 -0
  51. package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +2 -0
  52. package/dist/conversion/shared/text-markup-normalizer/normalize.js +76 -0
  53. package/dist/conversion/shared/text-markup-normalizer.d.ts +3 -25
  54. package/dist/conversion/shared/text-markup-normalizer.js +2 -1386
  55. package/dist/conversion/shared/tool-governor.js +136 -10
  56. package/dist/filters/utils/snapshot-writer.js +3 -3
  57. package/dist/router/virtual-router/bootstrap/auth-utils.d.ts +6 -0
  58. package/dist/router/virtual-router/bootstrap/auth-utils.js +288 -0
  59. package/dist/router/virtual-router/bootstrap/claude-code-helpers.d.ts +11 -0
  60. package/dist/router/virtual-router/bootstrap/claude-code-helpers.js +18 -0
  61. package/dist/router/virtual-router/bootstrap/config-defaults.d.ts +5 -0
  62. package/dist/router/virtual-router/bootstrap/config-defaults.js +13 -0
  63. package/dist/router/virtual-router/bootstrap/config-normalizers.d.ts +4 -0
  64. package/dist/router/virtual-router/bootstrap/config-normalizers.js +106 -0
  65. package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +7 -0
  66. package/dist/router/virtual-router/bootstrap/profile-builder.js +68 -0
  67. package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +40 -0
  68. package/dist/router/virtual-router/bootstrap/provider-normalization.js +212 -0
  69. package/dist/router/virtual-router/bootstrap/responses-helpers.d.ts +15 -0
  70. package/dist/router/virtual-router/bootstrap/responses-helpers.js +65 -0
  71. package/dist/router/virtual-router/bootstrap/routing-config.d.ts +23 -0
  72. package/dist/router/virtual-router/bootstrap/routing-config.js +293 -0
  73. package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +12 -0
  74. package/dist/router/virtual-router/bootstrap/streaming-helpers.js +128 -0
  75. package/dist/router/virtual-router/bootstrap/utils.d.ts +5 -0
  76. package/dist/router/virtual-router/bootstrap/utils.js +41 -0
  77. package/dist/router/virtual-router/bootstrap/web-search-config.d.ts +4 -0
  78. package/dist/router/virtual-router/bootstrap/web-search-config.js +131 -0
  79. package/dist/router/virtual-router/bootstrap.d.ts +0 -4
  80. package/dist/router/virtual-router/bootstrap.js +31 -1275
  81. package/dist/router/virtual-router/classifier.js +32 -14
  82. package/dist/router/virtual-router/engine/antigravity/alias-lease.js +2 -2
  83. package/dist/router/virtual-router/engine/cooldown-manager.d.ts +34 -0
  84. package/dist/router/virtual-router/engine/cooldown-manager.js +118 -0
  85. package/dist/router/virtual-router/engine/route-analytics.d.ts +28 -0
  86. package/dist/router/virtual-router/engine/route-analytics.js +44 -0
  87. package/dist/router/virtual-router/engine/routing-pools/index.js +165 -4
  88. package/dist/router/virtual-router/engine/sticky-session-manager.d.ts +29 -0
  89. package/dist/router/virtual-router/engine/sticky-session-manager.js +55 -0
  90. package/dist/router/virtual-router/engine-logging.d.ts +42 -1
  91. package/dist/router/virtual-router/engine-logging.js +82 -15
  92. package/dist/router/virtual-router/engine-selection/multimodal-capability.d.ts +3 -0
  93. package/dist/router/virtual-router/engine-selection/multimodal-capability.js +26 -0
  94. package/dist/router/virtual-router/engine-selection/route-utils.js +6 -2
  95. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +1 -0
  96. package/dist/router/virtual-router/engine-selection/tier-selection.js +31 -1
  97. package/dist/router/virtual-router/engine.d.ts +21 -7
  98. package/dist/router/virtual-router/engine.js +198 -194
  99. package/dist/router/virtual-router/features.js +12 -4
  100. package/dist/router/virtual-router/message-utils.d.ts +8 -0
  101. package/dist/router/virtual-router/message-utils.js +170 -45
  102. package/dist/router/virtual-router/pre-command-file-resolver.js +40 -2
  103. package/dist/router/virtual-router/routing-instructions.d.ts +8 -0
  104. package/dist/router/virtual-router/routing-instructions.js +18 -2
  105. package/dist/router/virtual-router/routing-stop-message-actions.js +34 -10
  106. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +2 -0
  107. package/dist/router/virtual-router/routing-stop-message-state-codec.js +50 -1
  108. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  109. package/dist/router/virtual-router/stop-message-state-sync.js +3 -0
  110. package/dist/router/virtual-router/token-counter.js +51 -10
  111. package/dist/router/virtual-router/tool-signals.js +4 -0
  112. package/dist/router/virtual-router/types.d.ts +15 -0
  113. package/dist/servertool/clock/session-scope.d.ts +3 -0
  114. package/dist/servertool/clock/session-scope.js +52 -0
  115. package/dist/servertool/clock/state.js +9 -0
  116. package/dist/servertool/clock/tasks.js +12 -1
  117. package/dist/servertool/clock/types.d.ts +3 -0
  118. package/dist/servertool/engine.js +177 -31
  119. package/dist/servertool/handlers/clock-auto.js +2 -8
  120. package/dist/servertool/handlers/clock.js +6 -9
  121. package/dist/servertool/handlers/recursive-detection-guard.js +53 -14
  122. package/dist/servertool/handlers/stop-message-auto/blocked-report.d.ts +16 -0
  123. package/dist/servertool/handlers/stop-message-auto/blocked-report.js +349 -0
  124. package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -0
  125. package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +503 -0
  126. package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +38 -0
  127. package/dist/servertool/handlers/stop-message-auto/routing-state.js +149 -0
  128. package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +67 -0
  129. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +387 -0
  130. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -1
  131. package/dist/servertool/handlers/stop-message-auto.js +80 -556
  132. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.d.ts +18 -0
  133. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.js +398 -0
  134. package/dist/servertool/handlers/stop-message-stage-policy/decision.d.ts +9 -0
  135. package/dist/servertool/handlers/stop-message-stage-policy/decision.js +127 -0
  136. package/dist/servertool/handlers/stop-message-stage-policy/observation.d.ts +2 -0
  137. package/dist/servertool/handlers/stop-message-stage-policy/observation.js +179 -0
  138. package/dist/servertool/handlers/stop-message-stage-policy/templates.d.ts +4 -0
  139. package/dist/servertool/handlers/stop-message-stage-policy/templates.js +96 -0
  140. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.d.ts +9 -0
  141. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.js +89 -0
  142. package/dist/servertool/handlers/stop-message-stage-policy/types.d.ts +59 -0
  143. package/dist/servertool/handlers/stop-message-stage-policy/types.js +1 -0
  144. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +3 -43
  145. package/dist/servertool/handlers/stop-message-stage-policy.js +2 -684
  146. package/dist/servertool/handlers/web-search.js +117 -0
  147. package/dist/servertool/server-side-tools.d.ts +0 -1
  148. package/dist/servertool/server-side-tools.js +4 -3
  149. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  150. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +1 -0
  151. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +110 -37
  152. package/dist/telemetry/stats-center.d.ts +9 -0
  153. package/dist/telemetry/stats-center.js +29 -1
  154. package/dist/tools/apply-patch/structured/coercion.js +3 -11
  155. package/dist/tools/exec-command/validator.d.ts +1 -0
  156. package/dist/tools/exec-command/validator.js +132 -0
  157. package/dist/tools/tool-registry.d.ts +1 -0
  158. package/dist/tools/tool-registry.js +1 -1
  159. package/package.json +1 -1
@@ -1,1386 +1,2 @@
1
- // Normalize textual markup into OpenAI tool_calls shape.
2
- // Gated by RCC_TEXT_MARKUP_COMPAT=1 to avoid overreach.
3
- import { isImagePath } from './media.js';
4
- import { isStructuredApplyPatchPayload } from '../../tools/apply-patch-structured.js';
5
- // Strict allowlist for tool names and their argument keys to avoid picking up
6
- // stray markup or free-form text as JSON keys (reduces false positives).
7
- const KNOWN_TOOLS = new Set([
8
- 'shell',
9
- 'exec_command',
10
- 'write_stdin',
11
- 'apply_patch',
12
- 'update_plan',
13
- 'view_image',
14
- 'list_mcp_resources',
15
- 'read_mcp_resource',
16
- 'list_mcp_resource_templates',
17
- // 文件/目录类工具(CLI 侧已有约定;此处只做文本→tool_calls 收割)
18
- 'list_directory'
19
- ]);
20
- const ALLOWED_KEYS = {
21
- shell: new Set(['command', 'justification', 'timeout_ms', 'with_escalated_permissions', 'workdir']),
22
- exec_command: new Set([
23
- 'cmd',
24
- 'command',
25
- 'workdir',
26
- 'justification',
27
- 'login',
28
- 'tty',
29
- 'shell',
30
- 'yield_time_ms',
31
- 'max_output_tokens',
32
- 'sandbox_permissions',
33
- 'timeout_ms'
34
- ]),
35
- write_stdin: new Set(['session_id', 'chars', 'text', 'yield_time_ms', 'max_output_tokens']),
36
- // apply_patch supports both freeform patch text (patch/input/instructions/text) and structured payloads (file/changes)
37
- apply_patch: new Set(['patch', 'input', 'instructions', 'text', 'file', 'changes']),
38
- update_plan: new Set(['explanation', 'plan']),
39
- view_image: new Set(['path']),
40
- list_mcp_resources: new Set(['server', 'cursor', 'filter', 'root']),
41
- read_mcp_resource: new Set(['server', 'uri', 'cursor']),
42
- list_mcp_resource_templates: new Set(['server', 'cursor']),
43
- list_directory: new Set(['path', 'recursive'])
44
- };
45
- function normalizeKey(raw) {
46
- try {
47
- const t = String(raw || '').trim();
48
- if (!t)
49
- return '';
50
- // Pick the last alphanumeric/underscore token (e.g., '空的\n<tool_call>command' -> 'command')
51
- const m = t.match(/([A-Za-z_][A-Za-z0-9_]*)$/);
52
- return m ? m[1] : t;
53
- }
54
- catch {
55
- return String(raw || '');
56
- }
57
- }
58
- function readDefaultWorkdirFromEnv() {
59
- try {
60
- const env = String(process?.env?.ROUTECODEX_WORKDIR || '').trim() ||
61
- String(process?.env?.RCC_WORKDIR || '').trim() ||
62
- String(process?.env?.CLAUDE_WORKDIR || '').trim();
63
- return env ? env : undefined;
64
- }
65
- catch {
66
- return undefined;
67
- }
68
- }
69
- function filterArgsForTool(name, args) {
70
- try {
71
- const nm = String(name || '').toLowerCase();
72
- const allow = ALLOWED_KEYS[nm];
73
- if (!allow) {
74
- // Unknown tool; be conservative: keep only simple safe keys if present
75
- const safe = new Set([
76
- 'cmd',
77
- 'command',
78
- 'workdir',
79
- 'patch',
80
- 'input',
81
- 'instructions',
82
- 'text',
83
- 'plan',
84
- 'explanation',
85
- 'path',
86
- 'server',
87
- 'uri',
88
- 'cursor',
89
- 'filter',
90
- 'root'
91
- ]);
92
- const out = {};
93
- for (const [k, v] of Object.entries(args || {})) {
94
- const key = normalizeKey(k);
95
- if (safe.has(key))
96
- out[key] = v;
97
- }
98
- return out;
99
- }
100
- const out = {};
101
- for (const [k, v] of Object.entries(args || {})) {
102
- const key = normalizeKey(k);
103
- if (allow.has(key))
104
- out[key] = v;
105
- }
106
- return out;
107
- }
108
- catch {
109
- return args;
110
- }
111
- }
112
- function enabled() {
113
- try {
114
- return String(process?.env?.RCC_TEXT_MARKUP_COMPAT ?? '1').trim() !== '0';
115
- }
116
- catch {
117
- return true;
118
- }
119
- }
120
- function escapeControlCharsInsideJsonStrings(raw) {
121
- try {
122
- if (typeof raw !== 'string' || !raw)
123
- return null;
124
- let out = '';
125
- let changed = false;
126
- let inString = false;
127
- let escaped = false;
128
- for (let i = 0; i < raw.length; i++) {
129
- const ch = raw[i];
130
- if (inString) {
131
- if (!escaped && ch === '"') {
132
- inString = false;
133
- out += ch;
134
- continue;
135
- }
136
- if (!escaped && ch === '\\') {
137
- escaped = true;
138
- out += ch;
139
- continue;
140
- }
141
- // JSON disallows unescaped control chars inside strings; models sometimes emit raw newlines/tabs.
142
- if (!escaped) {
143
- if (ch === '\n') {
144
- out += '\\n';
145
- changed = true;
146
- continue;
147
- }
148
- if (ch === '\r') {
149
- if (raw[i + 1] === '\n') {
150
- i++;
151
- }
152
- out += '\\n';
153
- changed = true;
154
- continue;
155
- }
156
- if (ch === '\t') {
157
- out += '\\t';
158
- changed = true;
159
- continue;
160
- }
161
- const code = ch.charCodeAt(0);
162
- if (code >= 0 && code < 0x20) {
163
- out += `\\u${code.toString(16).padStart(4, '0')}`;
164
- changed = true;
165
- continue;
166
- }
167
- }
168
- // fallthrough: normal character inside string
169
- out += ch;
170
- escaped = false;
171
- continue;
172
- }
173
- if (ch === '"') {
174
- inString = true;
175
- out += ch;
176
- continue;
177
- }
178
- out += ch;
179
- }
180
- return changed ? out : null;
181
- }
182
- catch {
183
- return null;
184
- }
185
- }
186
- function tryParseJsonWithModelRepairs(raw) {
187
- try {
188
- if (typeof raw !== 'string')
189
- return null;
190
- const text = raw.trim();
191
- if (!text)
192
- return null;
193
- try {
194
- return JSON.parse(text);
195
- }
196
- catch {
197
- const repaired = escapeControlCharsInsideJsonStrings(text);
198
- if (!repaired)
199
- return null;
200
- try {
201
- return JSON.parse(repaired);
202
- }
203
- catch {
204
- return null;
205
- }
206
- }
207
- }
208
- catch {
209
- return null;
210
- }
211
- }
212
- function salvageToolArgsFromRawText(toolName, rawArgs) {
213
- try {
214
- const lname = String(toolName || '').toLowerCase();
215
- const text = String(rawArgs || '');
216
- const out = {};
217
- const pickString = (re) => {
218
- const m = text.match(re);
219
- if (!m)
220
- return undefined;
221
- return String(m[1] ?? '');
222
- };
223
- const pickNumber = (re) => {
224
- const m = text.match(re);
225
- if (!m)
226
- return undefined;
227
- const n = Number.parseInt(String(m[1] ?? ''), 10);
228
- return Number.isFinite(n) ? n : undefined;
229
- };
230
- if (lname === 'exec_command') {
231
- const cmd = pickString(/"(?:cmd|command)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
232
- const workdir = pickString(/"(?:workdir|cwd)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
233
- const timeout = pickNumber(/"(?:timeout_ms|timeout)"\s*:\s*(\d+)\s*(?:,|})/i);
234
- if (cmd !== undefined)
235
- out.cmd = cmd;
236
- if (cmd !== undefined)
237
- out.command = cmd;
238
- if (workdir !== undefined)
239
- out.workdir = workdir;
240
- if (timeout !== undefined)
241
- out.timeout_ms = timeout;
242
- return Object.keys(out).length ? out : null;
243
- }
244
- if (lname === 'write_stdin') {
245
- const sessionId = pickNumber(/"(?:session_id|sessionId)"\s*:\s*(\d+)\s*(?:,|})/i);
246
- const chars = pickString(/"(?:chars|text|input|data)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
247
- if (sessionId !== undefined)
248
- out.session_id = sessionId;
249
- if (chars !== undefined)
250
- out.chars = chars;
251
- return Object.keys(out).length ? out : null;
252
- }
253
- if (lname === 'apply_patch') {
254
- const patch = pickString(/"(?:patch|text|input|instructions)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
255
- if (patch !== undefined)
256
- out.patch = patch;
257
- return Object.keys(out).length ? out : null;
258
- }
259
- return null;
260
- }
261
- catch {
262
- return null;
263
- }
264
- }
265
- // 已移除所有 rcc.tool.v1 相关处理:不再识别或剥离 rcc 封装
266
- // Extract <tool:NAME>...</tool:NAME> blocks used by some tool-mode outputs.
267
- // Example:
268
- // <tool:exec_command>
269
- // <command>cd /repo && npm test</command>
270
- // <timeout_ms>120000</timeout_ms>
271
- // <requires_approval>false</requires_approval>
272
- // </tool:exec_command>
273
- export function extractToolNamespaceXmlBlocksFromText(text) {
274
- try {
275
- if (typeof text !== 'string' || !text)
276
- return null;
277
- const hasOpenTag = text.includes('<tool:');
278
- const hasCloseTag = /<\/\s*tool:\s*[A-Za-z0-9_]+\s*>/i.test(text);
279
- const hasToolPrefix = /(?:^|\n)\s*(?:[•*+-]\s*)?tool:[A-Za-z0-9_]+/i.test(text);
280
- if (!hasOpenTag && !(hasCloseTag && hasToolPrefix)) {
281
- return null;
282
- }
283
- const out = [];
284
- const blockRe = /<\s*tool:([A-Za-z0-9_]+)\s*>([\s\S]*?)<\/\s*tool:\s*\1\s*>/gi;
285
- let bm;
286
- while ((bm = blockRe.exec(text)) !== null) {
287
- const rawName = String(bm[1] || '').trim();
288
- const lname = rawName.toLowerCase();
289
- if (!lname || !KNOWN_TOOLS.has(lname))
290
- continue;
291
- const inner = String(bm[2] || '');
292
- const argsObj = {};
293
- const kvRe = /<\s*([A-Za-z_][A-Za-z0-9_]*)\s*>([\s\S]*?)<\/\s*\1\s*>/g;
294
- let km;
295
- while ((km = kvRe.exec(inner)) !== null) {
296
- const rawKey = String(km[1] || '').trim();
297
- const key = normalizeKey(rawKey).toLowerCase();
298
- if (!key)
299
- continue;
300
- if (key === 'requires_approval')
301
- continue;
302
- const rawVal = String(km[2] ?? '');
303
- const value = tryParsePrimitiveValue(rawVal);
304
- if (lname === 'exec_command' && (key === 'command' || key === 'cmd')) {
305
- const cmd = coerceCommandValueToString(value);
306
- if (cmd.trim().length) {
307
- argsObj.cmd = cmd;
308
- argsObj.command = cmd;
309
- }
310
- continue;
311
- }
312
- if (lname === 'exec_command' && (key === 'cwd' || key === 'workdir')) {
313
- const wd = String(value ?? rawVal).trim();
314
- if (wd)
315
- argsObj.workdir = wd;
316
- continue;
317
- }
318
- if (lname === 'write_stdin') {
319
- if (key === 'input' || key === 'data' || key === 'chars' || key === 'text') {
320
- argsObj.chars = rawVal;
321
- continue;
322
- }
323
- if (key === 'session_id') {
324
- const n = Number.parseInt(String(value), 10);
325
- argsObj.session_id = Number.isFinite(n) ? n : value;
326
- continue;
327
- }
328
- }
329
- if (lname === 'apply_patch' && (key === 'patch' || key === 'input' || key === 'text' || key === 'instructions')) {
330
- const patchText = typeof value === 'string' ? value : rawVal;
331
- if (typeof patchText === 'string' && patchText.trim().length) {
332
- argsObj.patch = patchText;
333
- }
334
- continue;
335
- }
336
- argsObj[key] = value;
337
- }
338
- const filtered = filterArgsForTool(lname, argsObj);
339
- if (lname === 'exec_command') {
340
- const cmd = typeof filtered?.cmd === 'string' ? String(filtered.cmd).trim() : '';
341
- if (!cmd)
342
- continue;
343
- }
344
- if (lname === 'write_stdin') {
345
- const sid = filtered?.session_id;
346
- if (sid === undefined || sid === null || String(sid).trim().length === 0)
347
- continue;
348
- }
349
- if (lname === 'apply_patch') {
350
- const patchText = typeof filtered?.patch === 'string' ? String(filtered.patch).trim() : '';
351
- if (!patchText)
352
- continue;
353
- }
354
- let argsStr = '{}';
355
- try {
356
- argsStr = JSON.stringify(filtered);
357
- }
358
- catch {
359
- argsStr = '{}';
360
- }
361
- out.push({ id: `call_${Math.random().toString(36).slice(2, 10)}`, name: lname, args: argsStr });
362
- }
363
- // Broken variant (seen in the wild):
364
- // tool:exec_command (tool:exec_command)
365
- // <command>...</command>
366
- // </tool:exec_command>
367
- //
368
- // Best-effort: treat it as an implicit <tool:NAME> open tag.
369
- const prefixBlockRe = /(?:^|\n)\s*(?:[•*+-]\s*)?tool:([A-Za-z0-9_]+)[^\n]*\n([\s\S]*?)<\/\s*tool:\s*\1\s*>/gi;
370
- let pm;
371
- while ((pm = prefixBlockRe.exec(text)) !== null) {
372
- const rawName = String(pm[1] || '').trim();
373
- const lname = rawName.toLowerCase();
374
- if (!lname || !KNOWN_TOOLS.has(lname))
375
- continue;
376
- const inner = String(pm[2] || '');
377
- const argsObj = {};
378
- const kvRe = /<\s*([A-Za-z_][A-Za-z0-9_]*)\s*>([\s\S]*?)<\/\s*\1\s*>/g;
379
- let km;
380
- while ((km = kvRe.exec(inner)) !== null) {
381
- const rawKey = String(km[1] || '').trim();
382
- const key = normalizeKey(rawKey).toLowerCase();
383
- if (!key)
384
- continue;
385
- if (key === 'requires_approval')
386
- continue;
387
- const rawVal = String(km[2] ?? '');
388
- const value = tryParsePrimitiveValue(rawVal);
389
- if (lname === 'exec_command' && (key === 'command' || key === 'cmd')) {
390
- const cmd = coerceCommandValueToString(value);
391
- if (cmd.trim().length) {
392
- argsObj.cmd = cmd;
393
- argsObj.command = cmd;
394
- }
395
- continue;
396
- }
397
- if (lname === 'exec_command' && (key === 'cwd' || key === 'workdir')) {
398
- const wd = String(value ?? rawVal).trim();
399
- if (wd)
400
- argsObj.workdir = wd;
401
- continue;
402
- }
403
- if (lname === 'write_stdin') {
404
- if (key === 'input' || key === 'data' || key === 'chars' || key === 'text') {
405
- argsObj.chars = rawVal;
406
- continue;
407
- }
408
- if (key === 'session_id') {
409
- const n = Number.parseInt(String(value), 10);
410
- argsObj.session_id = Number.isFinite(n) ? n : value;
411
- continue;
412
- }
413
- }
414
- if (lname === 'apply_patch' && (key === 'patch' || key === 'input' || key === 'text' || key === 'instructions')) {
415
- const patchText = typeof value === 'string' ? value : rawVal;
416
- if (typeof patchText === 'string' && patchText.trim().length) {
417
- argsObj.patch = patchText;
418
- }
419
- continue;
420
- }
421
- argsObj[key] = value;
422
- }
423
- const filtered = filterArgsForTool(lname, argsObj);
424
- if (lname === 'exec_command') {
425
- const cmd = typeof filtered?.cmd === 'string' ? String(filtered.cmd).trim() : '';
426
- if (!cmd)
427
- continue;
428
- }
429
- if (lname === 'write_stdin') {
430
- const sid = filtered?.session_id;
431
- if (sid === undefined || sid === null || String(sid).trim().length === 0)
432
- continue;
433
- }
434
- if (lname === 'apply_patch') {
435
- const patchText = typeof filtered?.patch === 'string' ? String(filtered.patch).trim() : '';
436
- if (!patchText)
437
- continue;
438
- }
439
- let argsStr = '{}';
440
- try {
441
- argsStr = JSON.stringify(filtered);
442
- }
443
- catch {
444
- argsStr = '{}';
445
- }
446
- out.push({ id: `call_${Math.random().toString(36).slice(2, 10)}`, name: lname, args: argsStr });
447
- }
448
- return out.length ? out : null;
449
- }
450
- catch {
451
- return null;
452
- }
453
- }
454
- function extractStructuredApplyPatchPayloads(text) {
455
- const payloads = [];
456
- try {
457
- const fenceRe = /```(?:json|apply_patch|toon)?\s*([\s\S]*?)\s*```/gi;
458
- let fm;
459
- while ((fm = fenceRe.exec(text)) !== null) {
460
- const body = fm[1] || '';
461
- try {
462
- const parsed = JSON.parse(body);
463
- if (isStructuredApplyPatchPayload(parsed)) {
464
- payloads.push(parsed);
465
- }
466
- }
467
- catch { /* ignore invalid JSON */ }
468
- }
469
- if (!payloads.length && typeof text === 'string' && text.includes('"changes"')) {
470
- try {
471
- const parsed = JSON.parse(text);
472
- if (isStructuredApplyPatchPayload(parsed)) {
473
- payloads.push(parsed);
474
- }
475
- }
476
- catch { /* ignore */ }
477
- }
478
- }
479
- catch { /* ignore */ }
480
- return payloads;
481
- }
482
- function tryParseJsonValue(text) {
483
- try {
484
- const trimmed = String(text || '').trim();
485
- if (!trimmed)
486
- return null;
487
- if (!(trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"'))) {
488
- return null;
489
- }
490
- return JSON.parse(trimmed);
491
- }
492
- catch {
493
- return null;
494
- }
495
- }
496
- function tryParsePrimitiveValue(text) {
497
- try {
498
- const trimmed = String(text ?? '').trim();
499
- if (trimmed.length === 0)
500
- return '';
501
- const parsed = tryParseJsonValue(trimmed);
502
- if (parsed !== null)
503
- return parsed;
504
- if (trimmed === 'true' || trimmed === 'false')
505
- return trimmed === 'true';
506
- if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) {
507
- const n = Number(trimmed);
508
- if (Number.isFinite(n))
509
- return n;
510
- }
511
- return trimmed;
512
- }
513
- catch {
514
- return String(text ?? '');
515
- }
516
- }
517
- function coerceCommandValueToString(value) {
518
- try {
519
- if (value === null || value === undefined)
520
- return '';
521
- if (typeof value === 'string')
522
- return value.trim();
523
- if (Array.isArray(value)) {
524
- return value.map((v) => String(v)).join(' ').trim();
525
- }
526
- if (typeof value === 'object') {
527
- const rec = value;
528
- const direct = typeof rec.cmd === 'string'
529
- ? String(rec.cmd)
530
- : typeof rec.command === 'string'
531
- ? String(rec.command)
532
- : '';
533
- if (direct.trim().length)
534
- return direct.trim();
535
- try {
536
- return JSON.stringify(value);
537
- }
538
- catch {
539
- return String(value);
540
- }
541
- }
542
- return String(value).trim();
543
- }
544
- catch {
545
- return '';
546
- }
547
- }
548
- function looksLikeBrokenToolMarkup(text) {
549
- try {
550
- const t = String(text || '');
551
- if (!t)
552
- return false;
553
- return (t.includes('<exec_command') ||
554
- t.includes('<invoke') ||
555
- t.includes('<function_calls') ||
556
- t.includes('<parameter') ||
557
- t.includes('<tool_call') ||
558
- t.includes('</tool_call') ||
559
- t.includes('<arg_value') ||
560
- t.includes('</arg_value') ||
561
- t.includes('</func_call') ||
562
- /\bRan\b\s+\[/.test(t));
563
- }
564
- catch {
565
- return false;
566
- }
567
- }
568
- function stripTrailingXmlGarbage(line) {
569
- try {
570
- const idx = line.indexOf('<');
571
- if (idx >= 0) {
572
- return line.slice(0, idx).trim();
573
- }
574
- return line.trim();
575
- }
576
- catch {
577
- return String(line || '').trim();
578
- }
579
- }
580
- function normalizePossibleCommandLine(raw) {
581
- try {
582
- let line = String(raw || '');
583
- line = line.replace(/^\s*(?:[•*+-]\s*)?/, '');
584
- line = line.replace(/^\s*\$\s*/, '');
585
- return stripTrailingXmlGarbage(line);
586
- }
587
- catch {
588
- return '';
589
- }
590
- }
591
- function looksLikeShellCommand(line) {
592
- try {
593
- const t = String(line || '').trim();
594
- if (!t)
595
- return false;
596
- if (t.startsWith('```') || t.startsWith('>'))
597
- return false;
598
- if (t.startsWith('zsh:') || t.startsWith('bash:'))
599
- return false;
600
- // Conservative allowlist: commands commonly emitted by Codex/Claude in tool-mode.
601
- return /^(?:rg|ls|cat|node|npm|pnpm|yarn|git|find|sed|head|tail|wc|mkdir|rm|cp|mv|pwd)\b/.test(t);
602
- }
603
- catch {
604
- return false;
605
- }
606
- }
607
- // Extract Anthropic/GLM-style XML tool tags that use <parameter name="...">...</parameter>, e.g.:
608
- // <exec_command>
609
- // <parameter name="command">["cd","/repo","&&","npm","run","build"]</parameter>
610
- // <parameter name="workdir">/repo</parameter>
611
- // </exec_command>
612
- export function extractParameterXmlToolsFromText(text) {
613
- try {
614
- if (typeof text !== 'string' || !text)
615
- return null;
616
- const out = [];
617
- // Some providers/models emit mismatched closing tags like </func_call>; accept a small set of known variants.
618
- const toolRe = /<\s*(exec_command)\s*>([\s\S]*?)<\/\s*(?:\1|func_call|tool_call)\s*>/gi;
619
- let tm;
620
- while ((tm = toolRe.exec(text)) !== null) {
621
- const rawName = (tm[1] || '').trim();
622
- const inner = (tm[2] || '').trim();
623
- if (!rawName || !inner)
624
- continue;
625
- const lname = rawName.toLowerCase();
626
- const argsObj = {};
627
- const paramRe = /<\s*parameter\s+name\s*=\s*"([^"]+)"\s*>([\s\S]*?)<\/\s*parameter\s*>/gi;
628
- let pm;
629
- while ((pm = paramRe.exec(inner)) !== null) {
630
- const rawKey = (pm[1] || '').trim();
631
- const key = normalizeKey(rawKey);
632
- if (!key)
633
- continue;
634
- const rawVal = (pm[2] || '').trim();
635
- if (!rawVal)
636
- continue;
637
- const parsed = tryParseJsonValue(rawVal);
638
- if (key.toLowerCase() === 'command' || key.toLowerCase() === 'cmd') {
639
- const cmd = coerceCommandValueToString(parsed ?? rawVal);
640
- if (cmd.trim().length) {
641
- argsObj.cmd = cmd;
642
- argsObj.command = cmd;
643
- }
644
- continue;
645
- }
646
- if (key.toLowerCase() === 'workdir') {
647
- argsObj.workdir = rawVal;
648
- continue;
649
- }
650
- argsObj[key] = parsed ?? rawVal;
651
- }
652
- const filtered = filterArgsForTool(lname, argsObj);
653
- if (!filtered || typeof filtered !== 'object' || Array.isArray(filtered))
654
- continue;
655
- const cmd = typeof filtered.cmd === 'string' ? String(filtered.cmd).trim() : '';
656
- if (!cmd)
657
- continue;
658
- // If caller didn't provide workdir, fallback to a conservative env-derived workdir.
659
- try {
660
- if (!filtered.workdir) {
661
- const envWorkdir = readDefaultWorkdirFromEnv();
662
- if (envWorkdir)
663
- filtered.workdir = envWorkdir;
664
- }
665
- }
666
- catch {
667
- /* ignore */
668
- }
669
- let argsStr = '{}';
670
- try {
671
- argsStr = JSON.stringify(filtered);
672
- }
673
- catch {
674
- argsStr = '{}';
675
- }
676
- out.push({
677
- id: `call_${Math.random().toString(36).slice(2, 10)}`,
678
- name: lname,
679
- args: argsStr
680
- });
681
- }
682
- return out.length ? out : null;
683
- }
684
- catch {
685
- return null;
686
- }
687
- }
688
- function applyInvokeParameterAliases(toolName, argsObj) {
689
- try {
690
- const lname = String(toolName || '').toLowerCase();
691
- if (lname === 'write_stdin') {
692
- // Some models emit `data` / `wait` instead of our canonical `chars` / `yield_time_ms`.
693
- if (Object.prototype.hasOwnProperty.call(argsObj, 'data')) {
694
- const data = argsObj.data;
695
- if (!Object.prototype.hasOwnProperty.call(argsObj, 'chars') && !Object.prototype.hasOwnProperty.call(argsObj, 'text')) {
696
- argsObj.chars = data == null ? '' : String(data);
697
- }
698
- delete argsObj.data;
699
- }
700
- if (Object.prototype.hasOwnProperty.call(argsObj, 'wait') && !Object.prototype.hasOwnProperty.call(argsObj, 'yield_time_ms')) {
701
- const wait = argsObj.wait;
702
- let ms;
703
- if (typeof wait === 'number' && Number.isFinite(wait)) {
704
- // Heuristic: small values are usually seconds; large values are likely already ms.
705
- ms = wait <= 1000 ? Math.round(wait * 1000) : Math.round(wait);
706
- }
707
- else if (typeof wait === 'string' && wait.trim().length) {
708
- const num = Number(wait.trim());
709
- if (Number.isFinite(num)) {
710
- ms = num <= 1000 ? Math.round(num * 1000) : Math.round(num);
711
- }
712
- }
713
- if (typeof ms === 'number' && Number.isFinite(ms) && ms >= 0) {
714
- argsObj.yield_time_ms = ms;
715
- }
716
- delete argsObj.wait;
717
- }
718
- }
719
- }
720
- catch {
721
- // ignore
722
- }
723
- }
724
- // Extract <function_calls><invoke name="..."><parameter name="...">...</parameter>...</invoke> blocks.
725
- // Example seen in Gemini CLI / antigravity compact output:
726
- // <function_calls>
727
- // <invoke name="write_stdin">
728
- // <parameter name="session_id">91806</parameter>
729
- // <parameter name="data"></parameter>
730
- // <parameter name="wait">10</parameter>
731
- // </invoke>
732
- // </function_calls>
733
- export function extractInvokeToolsFromText(text) {
734
- try {
735
- if (typeof text !== 'string' || !text)
736
- return null;
737
- const out = [];
738
- const invokeRe = /<\s*invoke\s+name\s*=\s*"([^">]+)"\s*>([\s\S]*?)<\/\s*invoke\s*>/gi;
739
- let im;
740
- while ((im = invokeRe.exec(text)) !== null) {
741
- const rawName = (im[1] || '').trim();
742
- const inner = (im[2] || '').trim();
743
- if (!rawName || !inner)
744
- continue;
745
- const lname = rawName.toLowerCase();
746
- const argsObj = {};
747
- const paramRe = /<\s*parameter\s+name\s*=\s*"([^">]+)"\s*>([\s\S]*?)<\/\s*parameter\s*>/gi;
748
- let pm;
749
- while ((pm = paramRe.exec(inner)) !== null) {
750
- const rawKey = (pm[1] || '').trim();
751
- const key = normalizeKey(rawKey);
752
- if (!key)
753
- continue;
754
- const rawVal = (pm[2] ?? '').toString();
755
- const value = tryParsePrimitiveValue(rawVal);
756
- if (lname === 'exec_command' && (key.toLowerCase() === 'command' || key.toLowerCase() === 'cmd')) {
757
- const cmd = coerceCommandValueToString(value);
758
- if (cmd.trim().length) {
759
- argsObj.cmd = cmd;
760
- argsObj.command = cmd;
761
- }
762
- continue;
763
- }
764
- argsObj[key] = value;
765
- }
766
- applyInvokeParameterAliases(lname, argsObj);
767
- const filtered = filterArgsForTool(lname, argsObj);
768
- let argsStr = '{}';
769
- try {
770
- argsStr = JSON.stringify(filtered);
771
- }
772
- catch {
773
- argsStr = '{}';
774
- }
775
- out.push({ id: `call_${Math.random().toString(36).slice(2, 10)}`, name: lname, args: argsStr });
776
- }
777
- // Some Gemini CLI / antigravity variants emit a looser XML shape:
778
- // <function_calls>
779
- // <invoke>
780
- // <tool_name>exec_command</tool_name>
781
- // <parameters>
782
- // <command>echo hi</command>
783
- // </parameters>
784
- // </invoke>
785
- // </function_calls>
786
- //
787
- // Best-effort: only accept known tools, and only read simple <parameters><k>v</k></parameters>.
788
- if (out.length === 0) {
789
- const invokeBareRe = /<\s*invoke\s*>([\s\S]*?)<\/\s*invoke\s*>/gi;
790
- let bm;
791
- while ((bm = invokeBareRe.exec(text)) !== null) {
792
- const inner = (bm[1] || '').trim();
793
- if (!inner)
794
- continue;
795
- const toolNameMatch = /<\s*(?:tool_name|toolname|tool)\s*>\s*([\s\S]*?)\s*<\/\s*(?:tool_name|toolname|tool)\s*>/i.exec(inner);
796
- const rawName = (toolNameMatch?.[1] || '').trim();
797
- if (!rawName)
798
- continue;
799
- const lname = rawName.toLowerCase();
800
- if (!KNOWN_TOOLS.has(lname))
801
- continue;
802
- const argsObj = {};
803
- const paramsMatch = /<\s*parameters\s*>([\s\S]*?)<\/\s*parameters\s*>/i.exec(inner);
804
- if (paramsMatch && paramsMatch[1]) {
805
- const paramsInner = String(paramsMatch[1] || '');
806
- const kvRe = /<\s*([A-Za-z_][A-Za-z0-9_]*)\s*>([\s\S]*?)<\/\s*\1\s*>/g;
807
- let km;
808
- while ((km = kvRe.exec(paramsInner)) !== null) {
809
- const rawKey = (km[1] || '').trim();
810
- const key = normalizeKey(rawKey);
811
- if (!key)
812
- continue;
813
- const rawVal = (km[2] ?? '').toString();
814
- const value = tryParsePrimitiveValue(rawVal);
815
- if (lname === 'exec_command' && (key.toLowerCase() === 'command' || key.toLowerCase() === 'cmd')) {
816
- const cmd = coerceCommandValueToString(value);
817
- if (cmd.trim().length) {
818
- argsObj.cmd = cmd;
819
- argsObj.command = cmd;
820
- }
821
- continue;
822
- }
823
- argsObj[key] = value;
824
- }
825
- }
826
- applyInvokeParameterAliases(lname, argsObj);
827
- const filtered = filterArgsForTool(lname, argsObj);
828
- let argsStr = '{}';
829
- try {
830
- argsStr = JSON.stringify(filtered);
831
- }
832
- catch {
833
- argsStr = '{}';
834
- }
835
- out.push({ id: `call_${Math.random().toString(36).slice(2, 10)}`, name: lname, args: argsStr });
836
- }
837
- }
838
- return out.length ? out : null;
839
- }
840
- catch {
841
- return null;
842
- }
843
- }
844
- function tryParseJsonArrayOnLine(line) {
845
- try {
846
- const trimmed = String(line || '').trim();
847
- if (!trimmed.startsWith('[') || !trimmed.endsWith(']'))
848
- return null;
849
- const parsed = JSON.parse(trimmed);
850
- if (!Array.isArray(parsed))
851
- return null;
852
- const out = [];
853
- for (const entry of parsed) {
854
- if (entry == null)
855
- continue;
856
- out.push(String(entry));
857
- }
858
- return out.length ? out : null;
859
- }
860
- catch {
861
- return null;
862
- }
863
- }
864
- export function extractBareExecCommandFromText(text) {
865
- try {
866
- if (typeof text !== 'string' || !text)
867
- return null;
868
- // Only attempt this when we see clear evidence of a broken tool markup (avoid turning examples into tool calls).
869
- if (!looksLikeBrokenToolMarkup(text))
870
- return null;
871
- const candidates = [];
872
- const lines = text.split(/\r?\n/);
873
- for (let i = 0; i < lines.length; i += 1) {
874
- const raw = String(lines[i] || '');
875
- const line = normalizePossibleCommandLine(raw);
876
- if (!line)
877
- continue;
878
- // Pattern A: "Ran [\"ls\", \"-l\", ...]"
879
- const ran = line.match(/\bRan\b\s+(\[[\s\S]*\])\s*$/i);
880
- if (ran && ran[1]) {
881
- const arr = tryParseJsonArrayOnLine(ran[1]);
882
- if (arr) {
883
- candidates.push({ cmd: arr.join(' ').trim() });
884
- continue;
885
- }
886
- }
887
- // Pattern B: a single-line shell command (rg/ls/node/...)
888
- if (looksLikeShellCommand(line)) {
889
- candidates.push({ cmd: line.trim() });
890
- continue;
891
- }
892
- }
893
- if (!candidates.length)
894
- return null;
895
- const picked = candidates[candidates.length - 1];
896
- const argsObj = { cmd: picked.cmd };
897
- const envWorkdir = readDefaultWorkdirFromEnv();
898
- if (envWorkdir) {
899
- argsObj.workdir = envWorkdir;
900
- }
901
- const filtered = filterArgsForTool('exec_command', argsObj);
902
- let argsStr = '{}';
903
- try {
904
- argsStr = JSON.stringify(filtered);
905
- }
906
- catch {
907
- argsStr = '{}';
908
- }
909
- return [
910
- {
911
- id: `call_${Math.random().toString(36).slice(2, 10)}`,
912
- name: 'exec_command',
913
- args: argsStr
914
- }
915
- ];
916
- }
917
- catch {
918
- return null;
919
- }
920
- }
921
- export function extractApplyPatchCallsFromText(text) {
922
- try {
923
- if (typeof text !== 'string' || !text)
924
- return null;
925
- const payloads = extractStructuredApplyPatchPayloads(text);
926
- if (!payloads.length)
927
- return null;
928
- const out = [];
929
- const genId = () => `call_${Math.random().toString(36).slice(2, 10)}`;
930
- for (const payload of payloads) {
931
- let argsStr = '{}';
932
- try {
933
- argsStr = JSON.stringify(payload);
934
- }
935
- catch {
936
- argsStr = '{"changes":[]}';
937
- }
938
- out.push({ id: genId(), name: 'apply_patch', args: argsStr });
939
- }
940
- return out;
941
- }
942
- catch {
943
- return null;
944
- }
945
- }
946
- export function extractExecuteBlocksFromText(text) {
947
- try {
948
- if (typeof text !== 'string' || !text)
949
- return null;
950
- const re = /<function=execute>\s*<parameter=command>([\s\S]*?)<\/parameter>\s*<\/function>/gi;
951
- const out = [];
952
- let m;
953
- while ((m = re.exec(text)) !== null) {
954
- const commandRaw = (m[1] || '').trim();
955
- if (!commandRaw)
956
- continue;
957
- let args = '{}';
958
- try {
959
- args = JSON.stringify({ command: commandRaw });
960
- }
961
- catch {
962
- args = '{"command":""}';
963
- }
964
- out.push({ id: `call_${Math.random().toString(36).slice(2, 10)}`, name: 'shell', args });
965
- }
966
- return out.length ? out : null;
967
- }
968
- catch {
969
- return null;
970
- }
971
- }
972
- // Extract XML-like <tool_call> blocks used by some agents:
973
- // Example:
974
- // <tool_call>
975
- // shell
976
- // <arg_key>command</arg_key>
977
- // <arg_value>["cat","codex-protocol/README.md"]</arg_value>
978
- // </tool_call>
979
- export function extractXMLToolCallsFromText(text) {
980
- try {
981
- if (typeof text !== 'string' || !text)
982
- return null;
983
- const out = [];
984
- const blockRe = /<tool_call[\s\S]*?>([\s\S]*?)<\/tool_call>/gi;
985
- let bm;
986
- while ((bm = blockRe.exec(text)) !== null) {
987
- const inner = (bm[1] || '').trim();
988
- if (!inner)
989
- continue;
990
- // 1) try function/name tags
991
- let name = '';
992
- const fnTag = inner.match(/<\s*(?:function|name)\s*>\s*([a-zA-Z0-9_.-]+)\s*<\/(?:function|name)\s*>/i);
993
- if (fnTag && fnTag[1]) {
994
- name = String(fnTag[1]).trim();
995
- }
996
- else {
997
- // 2) else pick the first non-empty line without tags as the name
998
- const lines = inner.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
999
- const candidate = lines.find(l => !l.startsWith('<') && /^[a-zA-Z0-9_.-]+$/.test(l));
1000
- if (candidate)
1001
- name = candidate;
1002
- }
1003
- if (!name)
1004
- continue;
1005
- // Guard unknown tool names early if clearly invalid
1006
- const lname = String(name).toLowerCase();
1007
- if (!KNOWN_TOOLS.has(lname)) {
1008
- // still allow through, but later key filtering will drastically reduce noise
1009
- }
1010
- // Collect arg_key/arg_value pairs
1011
- const argRe = /<\s*arg_key\s*>\s*([^<]+?)\s*<\/(?:arg_key)\s*>\s*<\s*arg_value\s*>\s*([\s\S]*?)\s*<\/(?:arg_value)\s*>/gi;
1012
- let am;
1013
- const argsObj = {};
1014
- while ((am = argRe.exec(inner)) !== null) {
1015
- const k = normalizeKey((am[1] || '').trim());
1016
- let vRaw = (am[2] || '').trim();
1017
- if (!k)
1018
- continue;
1019
- // If value looks like JSON array/object, parse; else keep as string
1020
- let v = vRaw;
1021
- if ((vRaw.startsWith('[') && vRaw.endsWith(']')) || (vRaw.startsWith('{') && vRaw.endsWith('}'))) {
1022
- try {
1023
- v = JSON.parse(vRaw);
1024
- }
1025
- catch {
1026
- v = vRaw;
1027
- }
1028
- }
1029
- argsObj[k] = v;
1030
- }
1031
- // If no args collected but inner contains JSON object, try as whole arguments
1032
- const hasAnyArg = Object.keys(argsObj).length > 0;
1033
- if (!hasAnyArg) {
1034
- const jsonMatch = inner.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
1035
- if (jsonMatch) {
1036
- try {
1037
- const val = JSON.parse(jsonMatch[0]);
1038
- argsObj.arguments = val;
1039
- }
1040
- catch { /* ignore */ }
1041
- }
1042
- }
1043
- // Drop keys not allowed for this tool to prevent noise (e.g., '空的<tool_call>command')
1044
- const filtered = filterArgsForTool(lname, argsObj);
1045
- // Guard: view_image must have a valid image path
1046
- try {
1047
- if (lname === 'view_image') {
1048
- const p = (filtered && typeof filtered === 'object') ? filtered.path : undefined;
1049
- if (!isImagePath(p)) {
1050
- continue;
1051
- }
1052
- }
1053
- }
1054
- catch { /* ignore guard errors */ }
1055
- let argsStr = '{}';
1056
- try {
1057
- argsStr = JSON.stringify(filtered);
1058
- }
1059
- catch {
1060
- argsStr = '{}';
1061
- }
1062
- out.push({ id: `call_${Math.random().toString(36).slice(2, 10)}`, name, args: argsStr });
1063
- }
1064
- // Fallback: loose XML variant (missing <tool_call> start), e.g.:
1065
- // shell\n<arg_key>command</arg_key>\n<arg_value>["ls","-la"]</arg_value>\n</tool_call>
1066
- try {
1067
- const pairRe = /<\s*arg_key\s*>\s*([^<]+?)\s*<\/\s*arg_key\s*>\s*<\s*arg_value\s*>\s*([\s\S]*?)\s*<\/\s*arg_value\s*>/gi;
1068
- let pm;
1069
- while ((pm = pairRe.exec(text)) !== null) {
1070
- const start = pm.index;
1071
- // find a likely function name on the immediately preceding non-empty line
1072
- let name = '';
1073
- try {
1074
- const before = text.slice(0, start);
1075
- const lines = before.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
1076
- const cand = lines.length ? lines[lines.length - 1] : '';
1077
- if (/^[a-zA-Z0-9_.-]{2,}$/.test(cand))
1078
- name = cand;
1079
- }
1080
- catch { /* ignore */ }
1081
- // 如果前一行抽不到合法函数名,但 arg_key 明显是 toon/command,
1082
- // 视为 CLI 通路下的 exec_command 工具,避免把半截 <tool_call> 当成纯文本丢弃。
1083
- const rawKey = (pm[1] || '').trim();
1084
- const normalizedKey = normalizeKey(rawKey).toLowerCase();
1085
- if (!name && (normalizedKey === 'toon' || normalizedKey === 'command')) {
1086
- name = 'exec_command';
1087
- }
1088
- if (!name)
1089
- continue;
1090
- const k = normalizeKey(rawKey);
1091
- let vRaw = (pm[2] || '').trim();
1092
- const argsObj = {};
1093
- if (k) {
1094
- let v = vRaw;
1095
- if ((vRaw.startsWith('[') && vRaw.endsWith(']')) || (vRaw.startsWith('{') && vRaw.endsWith('}'))) {
1096
- try {
1097
- v = JSON.parse(vRaw);
1098
- }
1099
- catch {
1100
- v = vRaw;
1101
- }
1102
- }
1103
- argsObj[k] = v;
1104
- }
1105
- const lname2 = String(name || '').toLowerCase();
1106
- const filtered2 = filterArgsForTool(lname2, argsObj);
1107
- // Guard: view_image must have a valid image path
1108
- try {
1109
- if (lname2 === 'view_image') {
1110
- const p = (filtered2 && typeof filtered2 === 'object') ? filtered2.path : undefined;
1111
- if (!isImagePath(p)) {
1112
- continue;
1113
- }
1114
- }
1115
- }
1116
- catch { /* ignore guard errors */ }
1117
- let argsStr = '{}';
1118
- try {
1119
- argsStr = JSON.stringify(filtered2);
1120
- }
1121
- catch {
1122
- argsStr = '{}';
1123
- }
1124
- out.push({ id: `call_${Math.random().toString(36).slice(2, 10)}`, name, args: argsStr });
1125
- }
1126
- }
1127
- catch { /* ignore */ }
1128
- return out.length ? out : null;
1129
- }
1130
- catch {
1131
- return null;
1132
- }
1133
- }
1134
- /**
1135
- * 提取简单 XML 形式的工具调用块,例如:
1136
- *
1137
- * <list_directory>
1138
- * <path>/path/to/dir</path>
1139
- * <recursive>false</recursive>
1140
- * </list_directory>
1141
- *
1142
- * 仅针对已知工具名(目前为 list_directory),避免误伤普通 XML 文本。
1143
- */
1144
- export function extractSimpleXmlToolsFromText(text) {
1145
- try {
1146
- if (typeof text !== 'string' || !text)
1147
- return null;
1148
- const out = [];
1149
- const blockRe = /<\s*([A-Za-z0-9_.-]+)\s*>([\s\S]*?)<\/\s*\1\s*>/gi;
1150
- let bm;
1151
- while ((bm = blockRe.exec(text)) !== null) {
1152
- const rawName = (bm[1] || '').trim();
1153
- const lname = rawName.toLowerCase();
1154
- if (!lname || lname === 'tool_call')
1155
- continue;
1156
- // 目前仅支持 list_directory,后续按需扩展
1157
- if (lname !== 'list_directory')
1158
- continue;
1159
- const inner = bm[2] || '';
1160
- const args = {};
1161
- const argRe = /<\s*([A-Za-z0-9_]+)\s*>([\s\S]*?)<\/\s*\1\s*>/gi;
1162
- let am;
1163
- while ((am = argRe.exec(inner)) !== null) {
1164
- const key = normalizeKey((am[1] || '').trim());
1165
- if (!key)
1166
- continue;
1167
- let rawVal = (am[2] || '').trim();
1168
- let v = rawVal;
1169
- if ((rawVal.startsWith('[') && rawVal.endsWith(']')) || (rawVal.startsWith('{') && rawVal.endsWith('}'))) {
1170
- try {
1171
- v = JSON.parse(rawVal);
1172
- }
1173
- catch {
1174
- v = rawVal;
1175
- }
1176
- }
1177
- else if (rawVal === 'true' || rawVal === 'false') {
1178
- v = rawVal === 'true';
1179
- }
1180
- args[key] = v;
1181
- }
1182
- const filtered = filterArgsForTool(lname, args);
1183
- let argsStr = '{}';
1184
- try {
1185
- argsStr = JSON.stringify(filtered);
1186
- }
1187
- catch {
1188
- argsStr = '{}';
1189
- }
1190
- out.push({
1191
- id: `call_${Math.random().toString(36).slice(2, 10)}`,
1192
- name: lname,
1193
- args: argsStr
1194
- });
1195
- }
1196
- return out.length ? out : null;
1197
- }
1198
- catch {
1199
- return null;
1200
- }
1201
- }
1202
- // Extract Qwen-style tool call tokens that appear in plain text, e.g.:
1203
- // <|tool_calls_section_begin|>
1204
- // <|tool_call_begin|> functions.exec_command:66 <|tool_call_argument_begin|> {"cmd":"pwd"} <|tool_call_end|>
1205
- // <|tool_calls_section_end|>
1206
- // Some upstreams emit this even when OpenAI tool_calls are supported; we harvest it into tool_calls.
1207
- export function extractQwenToolCallTokensFromText(text) {
1208
- try {
1209
- if (typeof text !== 'string' || !text)
1210
- return null;
1211
- // Upstreams (notably Qwen-family models behind OpenAI-compatible shells) may insert
1212
- // whitespace/newlines inside token markers, e.g. "<| tool_call_begin|>".
1213
- // Avoid brittle substring checks; do a cheap regex presence test instead.
1214
- if (!/<\|\s*tool_call_begin\s*\|>/i.test(text) && !/<\|\s*tool_call_argument_begin\s*\|>/i.test(text)) {
1215
- return null;
1216
- }
1217
- const out = [];
1218
- // tolerate whitespace/newlines and missing argument_end token
1219
- const re = /<\|\s*tool_call_begin\s*\|>\s*([A-Za-z0-9_.-]+)(?::(\d+))?\s*(?:<\|\s*tool_call_argument_begin\s*\|>\s*([\s\S]*?)\s*)?<\|\s*tool_call_end\s*\|>/gi;
1220
- let m;
1221
- while ((m = re.exec(text)) !== null) {
1222
- const rawName = String(m[1] || '').trim();
1223
- const numericId = typeof m[2] === 'string' && m[2].trim().length ? m[2].trim() : undefined;
1224
- const rawArgs = typeof m[3] === 'string' ? String(m[3]).trim() : '';
1225
- if (!rawName)
1226
- continue;
1227
- let name = rawName;
1228
- if (name.startsWith('functions.')) {
1229
- name = name.slice('functions.'.length);
1230
- }
1231
- const lname = name.toLowerCase();
1232
- if (!lname)
1233
- continue;
1234
- let parsedArgs = null;
1235
- if (!rawArgs) {
1236
- parsedArgs = {};
1237
- }
1238
- else {
1239
- parsedArgs = tryParseJsonWithModelRepairs(rawArgs);
1240
- if (parsedArgs === null) {
1241
- parsedArgs = salvageToolArgsFromRawText(lname, rawArgs) ?? { _raw: rawArgs };
1242
- }
1243
- }
1244
- const argsObj = parsedArgs && typeof parsedArgs === 'object' && !Array.isArray(parsedArgs)
1245
- ? parsedArgs
1246
- : { value: parsedArgs };
1247
- // Alias normalization (models often use "command" instead of "cmd", etc.)
1248
- if (lname === 'exec_command') {
1249
- const cmd = typeof argsObj.cmd === 'string' ? String(argsObj.cmd) : '';
1250
- const command = typeof argsObj.command === 'string' ? String(argsObj.command) : '';
1251
- if (!cmd && command) {
1252
- argsObj.cmd = command;
1253
- }
1254
- const cwd = typeof argsObj.cwd === 'string' ? String(argsObj.cwd) : '';
1255
- const workdir = typeof argsObj.workdir === 'string' ? String(argsObj.workdir) : '';
1256
- if (!workdir && cwd) {
1257
- argsObj.workdir = cwd;
1258
- }
1259
- }
1260
- if (lname === 'write_stdin') {
1261
- const chars = typeof argsObj.chars === 'string' ? String(argsObj.chars) : '';
1262
- const textVal = typeof argsObj.text === 'string' ? String(argsObj.text) : '';
1263
- const input = typeof argsObj.input === 'string' ? String(argsObj.input) : '';
1264
- if (!chars && (textVal || input)) {
1265
- argsObj.chars = textVal || input;
1266
- }
1267
- const sid = argsObj.session_id;
1268
- const sidAlt = argsObj.sessionId;
1269
- if ((sid === undefined || sid === null || String(sid).trim() === '') && (sidAlt !== undefined && sidAlt !== null)) {
1270
- argsObj.session_id = sidAlt;
1271
- }
1272
- }
1273
- if (lname === 'apply_patch') {
1274
- const patch = typeof argsObj.patch === 'string' ? String(argsObj.patch) : '';
1275
- const textPatch = typeof argsObj.text === 'string' ? String(argsObj.text) : '';
1276
- const inputPatch = typeof argsObj.input === 'string' ? String(argsObj.input) : '';
1277
- const instructions = typeof argsObj.instructions === 'string' ? String(argsObj.instructions) : '';
1278
- if (!patch && (textPatch || inputPatch || instructions)) {
1279
- argsObj.patch = textPatch || inputPatch || instructions;
1280
- }
1281
- }
1282
- const filtered = filterArgsForTool(lname, argsObj);
1283
- // Basic guards for known tools
1284
- if (lname === 'exec_command') {
1285
- const cmd = typeof filtered?.cmd === 'string' ? String(filtered.cmd).trim() : '';
1286
- if (!cmd)
1287
- continue;
1288
- }
1289
- if (lname === 'write_stdin') {
1290
- // Allow empty args: our servertool schema may inject session_id automatically when a session is active.
1291
- // Some models emit polling writes as `{}`.
1292
- }
1293
- if (lname === 'apply_patch') {
1294
- const patchText = typeof filtered?.patch === 'string' ? String(filtered.patch).trim() : '';
1295
- if (!patchText)
1296
- continue;
1297
- }
1298
- let argsStr = '{}';
1299
- try {
1300
- argsStr = JSON.stringify(filtered);
1301
- }
1302
- catch {
1303
- argsStr = '{}';
1304
- }
1305
- const stableId = (() => {
1306
- if (numericId && /^[0-9]{1,10}$/.test(numericId)) {
1307
- const namePart = lname.replace(/[^A-Za-z0-9_-]/g, '_');
1308
- const id = `call_${namePart}_${numericId}`
1309
- .replace(/_{2,}/g, '_')
1310
- .replace(/^_+/, '')
1311
- .replace(/_+$/, '');
1312
- return id || undefined;
1313
- }
1314
- return undefined;
1315
- })();
1316
- out.push({ id: stableId ?? `call_${Math.random().toString(36).slice(2, 10)}`, name: lname, args: argsStr });
1317
- }
1318
- return out.length ? out : null;
1319
- }
1320
- catch {
1321
- return null;
1322
- }
1323
- }
1324
- export function normalizeAssistantTextToToolCalls(message) {
1325
- if (!enabled())
1326
- return message;
1327
- try {
1328
- if (!message || typeof message !== 'object')
1329
- return message;
1330
- if (Array.isArray(message.tool_calls) && message.tool_calls.length)
1331
- return message;
1332
- const content = message.content;
1333
- const candidates = [];
1334
- if (typeof content === 'string') {
1335
- candidates.push(content);
1336
- }
1337
- else if (Array.isArray(content)) {
1338
- for (const part of content) {
1339
- if (!part || typeof part !== 'object')
1340
- continue;
1341
- const p = part;
1342
- if (typeof p.text === 'string' && p.text.trim()) {
1343
- candidates.push(p.text);
1344
- continue;
1345
- }
1346
- if (typeof p.content === 'string' && p.content.trim()) {
1347
- candidates.push(p.content);
1348
- continue;
1349
- }
1350
- }
1351
- }
1352
- if (typeof message.reasoning === 'string' && message.reasoning.trim()) {
1353
- candidates.push(String(message.reasoning));
1354
- }
1355
- if (typeof message.thinking === 'string' && message.thinking.trim()) {
1356
- candidates.push(String(message.thinking));
1357
- }
1358
- if (!candidates.length)
1359
- return message;
1360
- let calls = null;
1361
- for (const text of candidates) {
1362
- // Order: xml-like <tool_call> → apply_patch → execute blocks → 简单 XML 工具(list_directory 等) → bare command salvage
1363
- calls =
1364
- extractQwenToolCallTokensFromText(text) ||
1365
- extractToolNamespaceXmlBlocksFromText(text) ||
1366
- extractParameterXmlToolsFromText(text) ||
1367
- extractInvokeToolsFromText(text) ||
1368
- extractXMLToolCallsFromText(text) ||
1369
- extractApplyPatchCallsFromText(text) ||
1370
- extractExecuteBlocksFromText(text) ||
1371
- extractSimpleXmlToolsFromText(text) ||
1372
- extractBareExecCommandFromText(text);
1373
- if (calls && calls.length)
1374
- break;
1375
- }
1376
- if (calls && calls.length) {
1377
- const toolCalls = calls.map((c) => ({ id: c.id, type: 'function', function: { name: c.name, arguments: c.args } }));
1378
- const copy = { ...message };
1379
- copy.tool_calls = toolCalls;
1380
- copy.content = '';
1381
- return copy;
1382
- }
1383
- }
1384
- catch { /* ignore */ }
1385
- return message;
1386
- }
1
+ export { extractApplyPatchCallsFromText, extractBareExecCommandFromText, extractExecuteBlocksFromText, extractExploredListDirectoryCallsFromText, extractInvokeToolsFromText, extractJsonToolCallsFromText, extractParameterXmlToolsFromText, extractQwenToolCallTokensFromText, extractSimpleXmlToolsFromText, extractToolNamespaceXmlBlocksFromText, extractXMLToolCallsFromText } from './text-markup-normalizer/extractors.js';
2
+ export { normalizeAssistantTextToToolCalls } from './text-markup-normalizer/normalize.js';