@probelabs/probe 0.6.0-rc253 → 0.6.0-rc254

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.
@@ -4005,6 +4005,33 @@ Follow these instructions carefully:
4005
4005
  break;
4006
4006
  }
4007
4007
 
4008
+ // Issue #443: Check if response contains valid schema-matching JSON
4009
+ // Before triggering error.no_tool_call, strip markdown fences and validate
4010
+ // This handles cases where AI returns valid JSON without using attempt_completion
4011
+ if (options.schema) {
4012
+ // Remove thinking tags first
4013
+ let contentToCheck = assistantResponseContent;
4014
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
4015
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*$/gi, '').trim();
4016
+
4017
+ // Try to extract and validate JSON
4018
+ const cleanedJson = cleanSchemaResponse(contentToCheck);
4019
+ try {
4020
+ JSON.parse(cleanedJson);
4021
+ const validation = validateJsonResponse(cleanedJson, { debug: this.debug, schema: options.schema });
4022
+ if (validation.isValid) {
4023
+ if (this.debug) {
4024
+ console.log(`[DEBUG] Issue #443: Accepting valid JSON response without attempt_completion (${cleanedJson.length} chars)`);
4025
+ }
4026
+ finalResult = cleanedJson;
4027
+ completionAttempted = true;
4028
+ break;
4029
+ }
4030
+ } catch {
4031
+ // Not valid JSON - continue to standard no_tool_call handling
4032
+ }
4033
+ }
4034
+
4008
4035
  // Increment consecutive no-tool counter (catches alternating stuck responses)
4009
4036
  consecutiveNoToolCount++;
4010
4037
 
@@ -183,6 +183,16 @@ export function generateSandboxGlobals(options) {
183
183
  });
184
184
  }
185
185
 
186
+ // Issue #444: Auto-coerce object paths to strings for search()
187
+ // AI-generated DSL sometimes passes field objects instead of field.file_path strings
188
+ if (params.path && typeof params.path === 'object') {
189
+ const coercedPath = params.path.file_path || params.path.path || params.path.directory || params.path.filename;
190
+ if (coercedPath && typeof coercedPath === 'string') {
191
+ logFn?.(`[${name}] Warning: Coerced object path to string "${coercedPath}" (issue #444)`);
192
+ params.path = coercedPath;
193
+ }
194
+ }
195
+
186
196
  const validated = schema.safeParse(params);
187
197
  if (!validated.success) {
188
198
  throw new Error(`Invalid parameters for ${name}: ${validated.error.message}`);
@@ -232,6 +242,15 @@ export function generateSandboxGlobals(options) {
232
242
  // When schema is provided, auto-parse the JSON result for easier downstream processing
233
243
  if (llmCall) {
234
244
  const rawLLM = async (instruction, data, opts = {}) => {
245
+ // Issue #444: Guard against error strings being passed as data
246
+ // When previous tool calls fail, they return "ERROR: ..." strings
247
+ // Passing these to LLM() spawns useless delegates that can't help
248
+ const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
249
+ if (dataStr && dataStr.startsWith('ERROR:')) {
250
+ logFn?.('[LLM] Blocked: data contains error from previous tool call');
251
+ return 'ERROR: Previous tool call failed - ' + dataStr.substring(0, 200);
252
+ }
253
+
235
254
  const result = await llmCall(instruction, data, opts);
236
255
  // Auto-parse JSON when schema is provided and result is a string
237
256
  if (opts.schema && typeof result === 'string') {
@@ -21944,6 +21944,13 @@ function generateSandboxGlobals(options) {
21944
21944
  if (i < keys2.length) params[keys2[i]] = arg;
21945
21945
  });
21946
21946
  }
21947
+ if (params.path && typeof params.path === "object") {
21948
+ const coercedPath = params.path.file_path || params.path.path || params.path.directory || params.path.filename;
21949
+ if (coercedPath && typeof coercedPath === "string") {
21950
+ logFn?.(`[${name}] Warning: Coerced object path to string "${coercedPath}" (issue #444)`);
21951
+ params.path = coercedPath;
21952
+ }
21953
+ }
21947
21954
  const validated = schema.safeParse(params);
21948
21955
  if (!validated.success) {
21949
21956
  throw new Error(`Invalid parameters for ${name}: ${validated.error.message}`);
@@ -21980,6 +21987,11 @@ function generateSandboxGlobals(options) {
21980
21987
  }
21981
21988
  if (llmCall) {
21982
21989
  const rawLLM = async (instruction, data2, opts = {}) => {
21990
+ const dataStr = typeof data2 === "string" ? data2 : JSON.stringify(data2);
21991
+ if (dataStr && dataStr.startsWith("ERROR:")) {
21992
+ logFn?.("[LLM] Blocked: data contains error from previous tool call");
21993
+ return "ERROR: Previous tool call failed - " + dataStr.substring(0, 200);
21994
+ }
21983
21995
  const result = await llmCall(instruction, data2, opts);
21984
21996
  if (opts.schema && typeof result === "string") {
21985
21997
  try {
@@ -29334,6 +29346,7 @@ ${lastError}
29334
29346
 
29335
29347
  RULES REMINDER:
29336
29348
  - search(query) is KEYWORD SEARCH \u2014 pass a search query, NOT a filename. Use extract(filepath) to read file contents.
29349
+ - search(query, path) \u2014 the path argument must be a STRING, not an object. Use field.file_path, not field.
29337
29350
  - search() returns up to 20K tokens by default. Use search(query, path, {maxTokens: null}) for unlimited, or searchAll(query) to auto-paginate ALL results.
29338
29351
  - search(), searchAll(), query(), extract(), listFiles(), bash() all return STRINGS, not arrays.
29339
29352
  - Use chunk(stringData) to split a string into an array of chunks.
@@ -29342,7 +29355,8 @@ RULES REMINDER:
29342
29355
  - Do NOT define helper functions that call tools \u2014 write logic inline.
29343
29356
  - Do NOT use async/await, template literals, or shorthand properties.
29344
29357
  - Do NOT use regex literals (/pattern/) \u2014 use String methods like indexOf, includes, startsWith instead.
29345
- - String concatenation with +, not template literals.`;
29358
+ - String concatenation with +, not template literals.
29359
+ - IMPORTANT: If a tool returns "ERROR: ...", do NOT pass that error string to LLM() \u2014 handle or skip it.`;
29346
29360
  const fixedCode = await llmCallFn(fixPrompt, "", { maxTokens: 4e3, temperature: 0.2 });
29347
29361
  currentCode = stripCodeWrapping(fixedCode);
29348
29362
  planSpan?.addEvent?.("dsl.self_heal_complete", {
@@ -68566,6 +68580,7 @@ __export(schemaUtils_exports, {
68566
68580
  processSchemaResponse: () => processSchemaResponse,
68567
68581
  replaceMermaidDiagramsInJson: () => replaceMermaidDiagramsInJson,
68568
68582
  replaceMermaidDiagramsInMarkdown: () => replaceMermaidDiagramsInMarkdown,
68583
+ sanitizeMarkdownEscapesInJson: () => sanitizeMarkdownEscapesInJson,
68569
68584
  tryAutoWrapForSimpleSchema: () => tryAutoWrapForSimpleSchema,
68570
68585
  tryMaidAutoFix: () => tryMaidAutoFix,
68571
68586
  validateAndFixMermaidResponse: () => validateAndFixMermaidResponse,
@@ -68669,6 +68684,17 @@ function decodeHtmlEntities2(text) {
68669
68684
  }
68670
68685
  return decoded;
68671
68686
  }
68687
+ function sanitizeMarkdownEscapesInJson(jsonString) {
68688
+ if (!jsonString || typeof jsonString !== "string") {
68689
+ return jsonString;
68690
+ }
68691
+ return jsonString.replace(/\\\\|\\([^"\\\/bfnrtu])/g, (match2, captured) => {
68692
+ if (match2 === "\\\\") {
68693
+ return "\\\\";
68694
+ }
68695
+ return captured;
68696
+ });
68697
+ }
68672
68698
  function normalizeJsonQuotes(str) {
68673
68699
  if (!str || typeof str !== "string") {
68674
68700
  return str;
@@ -68721,6 +68747,15 @@ function cleanSchemaResponse(response) {
68721
68747
  if (resultWrapperMatch) {
68722
68748
  return cleanSchemaResponse(resultWrapperMatch[1]);
68723
68749
  }
68750
+ const toolCodeMatch = trimmed.match(/<tool_code>\s*([\s\S]*?)\s*<\/tool_code>/);
68751
+ if (toolCodeMatch) {
68752
+ let innerContent = toolCodeMatch[1].trim();
68753
+ const funcCallMatch = innerContent.match(/(?:print|attempt_completion)\s*\(\s*([{\[][\s\S]*[}\]])\s*\)/);
68754
+ if (funcCallMatch) {
68755
+ return cleanSchemaResponse(funcCallMatch[1]);
68756
+ }
68757
+ return cleanSchemaResponse(innerContent);
68758
+ }
68724
68759
  const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
68725
68760
  if (jsonBlockMatch) {
68726
68761
  return normalizeJsonQuotes(jsonBlockMatch[1].trim());
@@ -68788,9 +68823,25 @@ function validateJsonResponse(response, options = {}) {
68788
68823
  console.log(`[DEBUG] JSON validation: Schema validation enabled`);
68789
68824
  }
68790
68825
  }
68826
+ let responseToValidate = response;
68827
+ try {
68828
+ JSON.parse(response);
68829
+ } catch (initialError) {
68830
+ if (initialError.message && initialError.message.includes("escape")) {
68831
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
68832
+ try {
68833
+ JSON.parse(sanitized);
68834
+ responseToValidate = sanitized;
68835
+ if (debug) {
68836
+ console.log(`[DEBUG] JSON validation: Fixed Markdown escapes in JSON (issue #441)`);
68837
+ }
68838
+ } catch {
68839
+ }
68840
+ }
68841
+ }
68791
68842
  try {
68792
68843
  const parseStart = Date.now();
68793
- const parsed = JSON.parse(response);
68844
+ const parsed = JSON.parse(responseToValidate);
68794
68845
  const parseTime = Date.now() - parseStart;
68795
68846
  if (debug) {
68796
68847
  console.log(`[DEBUG] JSON validation: Successfully parsed in ${parseTime}ms`);
@@ -69132,7 +69183,21 @@ function tryAutoWrapForSimpleSchema(response, schema, options = {}) {
69132
69183
  console.log(`[DEBUG] Auto-wrap: Response is already valid JSON, skipping`);
69133
69184
  }
69134
69185
  return null;
69135
- } catch {
69186
+ } catch (initialError) {
69187
+ if (initialError.message && initialError.message.includes("escape")) {
69188
+ try {
69189
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
69190
+ JSON.parse(sanitized);
69191
+ if (debug) {
69192
+ console.log(`[DEBUG] Auto-wrap: Fixed Markdown escapes in JSON (issue #441), returning sanitized JSON`);
69193
+ }
69194
+ return sanitized;
69195
+ } catch {
69196
+ if (debug) {
69197
+ console.log(`[DEBUG] Auto-wrap: Markdown escape sanitization didn't fix JSON, proceeding with wrapping`);
69198
+ }
69199
+ }
69200
+ }
69136
69201
  }
69137
69202
  const wrapped = JSON.stringify({ [wrapperInfo.fieldName]: response });
69138
69203
  if (debug) {
@@ -84466,6 +84531,25 @@ ${errorXml}
84466
84531
  }
84467
84532
  break;
84468
84533
  }
84534
+ if (options.schema) {
84535
+ let contentToCheck = assistantResponseContent;
84536
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "").trim();
84537
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*$/gi, "").trim();
84538
+ const cleanedJson = cleanSchemaResponse(contentToCheck);
84539
+ try {
84540
+ JSON.parse(cleanedJson);
84541
+ const validation = validateJsonResponse(cleanedJson, { debug: this.debug, schema: options.schema });
84542
+ if (validation.isValid) {
84543
+ if (this.debug) {
84544
+ console.log(`[DEBUG] Issue #443: Accepting valid JSON response without attempt_completion (${cleanedJson.length} chars)`);
84545
+ }
84546
+ finalResult = cleanedJson;
84547
+ completionAttempted = true;
84548
+ break;
84549
+ }
84550
+ } catch {
84551
+ }
84552
+ }
84469
84553
  consecutiveNoToolCount++;
84470
84554
  const isIdentical = lastNoToolResponse !== null && assistantResponseContent === lastNoToolResponse;
84471
84555
  const isSemanticallyStuck = lastNoToolResponse !== null && areBothStuckResponses(lastNoToolResponse, assistantResponseContent);
@@ -165,6 +165,39 @@ export function decodeHtmlEntities(text) {
165
165
  return decoded;
166
166
  }
167
167
 
168
+ /**
169
+ * Sanitize Markdown escape sequences in JSON strings
170
+ *
171
+ * Markdown uses backslash escapes like \*, \_, \#, \~ etc. which are NOT valid
172
+ * JSON escape sequences. When AI models produce JSON with Markdown content,
173
+ * these escapes cause JSON.parse() to fail with "Invalid \escape" errors.
174
+ *
175
+ * This function removes the backslash from invalid escape sequences while
176
+ * preserving valid JSON escapes: \\, \", \/, \b, \f, \n, \r, \t, \uXXXX
177
+ *
178
+ * @param {string} jsonString - JSON string that may contain Markdown escapes
179
+ * @returns {string} - JSON string with invalid escapes sanitized
180
+ */
181
+ export function sanitizeMarkdownEscapesInJson(jsonString) {
182
+ if (!jsonString || typeof jsonString !== 'string') {
183
+ return jsonString;
184
+ }
185
+
186
+ // Strategy: Match either:
187
+ // 1. \\\\ (escaped backslash) - preserve as-is
188
+ // 2. \\X where X is NOT a valid JSON escape char - remove the backslash
189
+ //
190
+ // Valid JSON escape chars: " \ / b f n r t u
191
+ // This converts: \* → *, \_ → _, \# → #, \~ → ~, etc.
192
+ // But preserves: \\, \", \n, \t, \r, \b, \f, \/, \uXXXX
193
+ return jsonString.replace(/\\\\|\\([^"\\\/bfnrtu])/g, (match, captured) => {
194
+ if (match === '\\\\') {
195
+ return '\\\\'; // Preserve escaped backslash
196
+ }
197
+ return captured; // Remove backslash from invalid escape
198
+ });
199
+ }
200
+
168
201
  /**
169
202
  * Normalize JavaScript syntax to valid JSON syntax
170
203
  * Converts single quotes to double quotes for strings in JSON-like structures
@@ -261,6 +294,22 @@ export function cleanSchemaResponse(response) {
261
294
  return cleanSchemaResponse(resultWrapperMatch[1]);
262
295
  }
263
296
 
297
+ // Strip <tool_code>...</tool_code> wrapper (Gemini-style code execution format)
298
+ // Issue #443: Gemini sometimes wraps responses in <plan> + <tool_code> tags
299
+ // e.g., <tool_code>print(attempt_completion({"projects": ["repo1"]}))</tool_code>
300
+ const toolCodeMatch = trimmed.match(/<tool_code>\s*([\s\S]*?)\s*<\/tool_code>/);
301
+ if (toolCodeMatch) {
302
+ let innerContent = toolCodeMatch[1].trim();
303
+ // Extract JSON from print() or attempt_completion() wrappers
304
+ // e.g., print({"key": "value"}) or attempt_completion({"key": "value"})
305
+ const funcCallMatch = innerContent.match(/(?:print|attempt_completion)\s*\(\s*([{\[][\s\S]*[}\]])\s*\)/);
306
+ if (funcCallMatch) {
307
+ return cleanSchemaResponse(funcCallMatch[1]);
308
+ }
309
+ // Try cleaning the inner content directly
310
+ return cleanSchemaResponse(innerContent);
311
+ }
312
+
264
313
  // First, look for JSON after code block markers - similar to mermaid extraction
265
314
  // Try with json language specifier
266
315
  const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
@@ -370,9 +419,30 @@ export function validateJsonResponse(response, options = {}) {
370
419
  }
371
420
  }
372
421
 
422
+ // Try to parse the response, with fallback to sanitizing Markdown escapes (issue #441)
423
+ let responseToValidate = response;
424
+ try {
425
+ JSON.parse(response);
426
+ } catch (initialError) {
427
+ // Check if the error is due to invalid escape sequences (Markdown escapes like \*, \_)
428
+ if (initialError.message && initialError.message.includes('escape')) {
429
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
430
+ try {
431
+ JSON.parse(sanitized);
432
+ // Sanitized version parses - use it instead
433
+ responseToValidate = sanitized;
434
+ if (debug) {
435
+ console.log(`[DEBUG] JSON validation: Fixed Markdown escapes in JSON (issue #441)`);
436
+ }
437
+ } catch {
438
+ // Sanitization didn't help, continue with original (will fail below with proper error)
439
+ }
440
+ }
441
+ }
442
+
373
443
  try {
374
444
  const parseStart = Date.now();
375
- const parsed = JSON.parse(response);
445
+ const parsed = JSON.parse(responseToValidate);
376
446
  const parseTime = Date.now() - parseStart;
377
447
 
378
448
  if (debug) {
@@ -853,7 +923,26 @@ export function tryAutoWrapForSimpleSchema(response, schema, options = {}) {
853
923
  console.log(`[DEBUG] Auto-wrap: Response is already valid JSON, skipping`);
854
924
  }
855
925
  return null;
856
- } catch {
926
+ } catch (initialError) {
927
+ // Not valid JSON - check if it's due to Markdown escapes (issue #441)
928
+ // AI models sometimes produce JSON with Markdown escapes like \* or \_
929
+ // which are valid Markdown but NOT valid JSON escape sequences
930
+ if (initialError.message && initialError.message.includes('escape')) {
931
+ try {
932
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
933
+ JSON.parse(sanitized);
934
+ // Sanitized JSON is valid! Return it instead of wrapping
935
+ if (debug) {
936
+ console.log(`[DEBUG] Auto-wrap: Fixed Markdown escapes in JSON (issue #441), returning sanitized JSON`);
937
+ }
938
+ return sanitized;
939
+ } catch {
940
+ // Sanitization didn't help, proceed with wrapping
941
+ if (debug) {
942
+ console.log(`[DEBUG] Auto-wrap: Markdown escape sanitization didn't fix JSON, proceeding with wrapping`);
943
+ }
944
+ }
945
+ }
857
946
  // Not valid JSON, proceed with wrapping
858
947
  }
859
948
 
@@ -436,6 +436,7 @@ ${lastError}
436
436
 
437
437
  RULES REMINDER:
438
438
  - search(query) is KEYWORD SEARCH — pass a search query, NOT a filename. Use extract(filepath) to read file contents.
439
+ - search(query, path) — the path argument must be a STRING, not an object. Use field.file_path, not field.
439
440
  - search() returns up to 20K tokens by default. Use search(query, path, {maxTokens: null}) for unlimited, or searchAll(query) to auto-paginate ALL results.
440
441
  - search(), searchAll(), query(), extract(), listFiles(), bash() all return STRINGS, not arrays.
441
442
  - Use chunk(stringData) to split a string into an array of chunks.
@@ -444,7 +445,8 @@ RULES REMINDER:
444
445
  - Do NOT define helper functions that call tools — write logic inline.
445
446
  - Do NOT use async/await, template literals, or shorthand properties.
446
447
  - Do NOT use regex literals (/pattern/) — use String methods like indexOf, includes, startsWith instead.
447
- - String concatenation with +, not template literals.`;
448
+ - String concatenation with +, not template literals.
449
+ - IMPORTANT: If a tool returns "ERROR: ...", do NOT pass that error string to LLM() — handle or skip it.`;
448
450
 
449
451
  const fixedCode = await llmCallFn(fixPrompt, '', { maxTokens: 4000, temperature: 0.2 });
450
452
  // Strip markdown fences and XML tags the LLM might add
@@ -51415,6 +51415,13 @@ function generateSandboxGlobals(options) {
51415
51415
  if (i5 < keys2.length) params[keys2[i5]] = arg;
51416
51416
  });
51417
51417
  }
51418
+ if (params.path && typeof params.path === "object") {
51419
+ const coercedPath = params.path.file_path || params.path.path || params.path.directory || params.path.filename;
51420
+ if (coercedPath && typeof coercedPath === "string") {
51421
+ logFn?.(`[${name14}] Warning: Coerced object path to string "${coercedPath}" (issue #444)`);
51422
+ params.path = coercedPath;
51423
+ }
51424
+ }
51418
51425
  const validated = schema.safeParse(params);
51419
51426
  if (!validated.success) {
51420
51427
  throw new Error(`Invalid parameters for ${name14}: ${validated.error.message}`);
@@ -51451,6 +51458,11 @@ function generateSandboxGlobals(options) {
51451
51458
  }
51452
51459
  if (llmCall) {
51453
51460
  const rawLLM = async (instruction, data3, opts = {}) => {
51461
+ const dataStr = typeof data3 === "string" ? data3 : JSON.stringify(data3);
51462
+ if (dataStr && dataStr.startsWith("ERROR:")) {
51463
+ logFn?.("[LLM] Blocked: data contains error from previous tool call");
51464
+ return "ERROR: Previous tool call failed - " + dataStr.substring(0, 200);
51465
+ }
51454
51466
  const result = await llmCall(instruction, data3, opts);
51455
51467
  if (opts.schema && typeof result === "string") {
51456
51468
  try {
@@ -58804,6 +58816,7 @@ ${lastError}
58804
58816
 
58805
58817
  RULES REMINDER:
58806
58818
  - search(query) is KEYWORD SEARCH \u2014 pass a search query, NOT a filename. Use extract(filepath) to read file contents.
58819
+ - search(query, path) \u2014 the path argument must be a STRING, not an object. Use field.file_path, not field.
58807
58820
  - search() returns up to 20K tokens by default. Use search(query, path, {maxTokens: null}) for unlimited, or searchAll(query) to auto-paginate ALL results.
58808
58821
  - search(), searchAll(), query(), extract(), listFiles(), bash() all return STRINGS, not arrays.
58809
58822
  - Use chunk(stringData) to split a string into an array of chunks.
@@ -58812,7 +58825,8 @@ RULES REMINDER:
58812
58825
  - Do NOT define helper functions that call tools \u2014 write logic inline.
58813
58826
  - Do NOT use async/await, template literals, or shorthand properties.
58814
58827
  - Do NOT use regex literals (/pattern/) \u2014 use String methods like indexOf, includes, startsWith instead.
58815
- - String concatenation with +, not template literals.`;
58828
+ - String concatenation with +, not template literals.
58829
+ - IMPORTANT: If a tool returns "ERROR: ...", do NOT pass that error string to LLM() \u2014 handle or skip it.`;
58816
58830
  const fixedCode = await llmCallFn(fixPrompt, "", { maxTokens: 4e3, temperature: 0.2 });
58817
58831
  currentCode = stripCodeWrapping(fixedCode);
58818
58832
  planSpan?.addEvent?.("dsl.self_heal_complete", {
@@ -97603,6 +97617,7 @@ __export(schemaUtils_exports, {
97603
97617
  processSchemaResponse: () => processSchemaResponse,
97604
97618
  replaceMermaidDiagramsInJson: () => replaceMermaidDiagramsInJson,
97605
97619
  replaceMermaidDiagramsInMarkdown: () => replaceMermaidDiagramsInMarkdown,
97620
+ sanitizeMarkdownEscapesInJson: () => sanitizeMarkdownEscapesInJson,
97606
97621
  tryAutoWrapForSimpleSchema: () => tryAutoWrapForSimpleSchema,
97607
97622
  tryMaidAutoFix: () => tryMaidAutoFix,
97608
97623
  validateAndFixMermaidResponse: () => validateAndFixMermaidResponse,
@@ -97706,6 +97721,17 @@ function decodeHtmlEntities2(text) {
97706
97721
  }
97707
97722
  return decoded;
97708
97723
  }
97724
+ function sanitizeMarkdownEscapesInJson(jsonString) {
97725
+ if (!jsonString || typeof jsonString !== "string") {
97726
+ return jsonString;
97727
+ }
97728
+ return jsonString.replace(/\\\\|\\([^"\\\/bfnrtu])/g, (match2, captured) => {
97729
+ if (match2 === "\\\\") {
97730
+ return "\\\\";
97731
+ }
97732
+ return captured;
97733
+ });
97734
+ }
97709
97735
  function normalizeJsonQuotes(str) {
97710
97736
  if (!str || typeof str !== "string") {
97711
97737
  return str;
@@ -97758,6 +97784,15 @@ function cleanSchemaResponse(response) {
97758
97784
  if (resultWrapperMatch) {
97759
97785
  return cleanSchemaResponse(resultWrapperMatch[1]);
97760
97786
  }
97787
+ const toolCodeMatch = trimmed.match(/<tool_code>\s*([\s\S]*?)\s*<\/tool_code>/);
97788
+ if (toolCodeMatch) {
97789
+ let innerContent = toolCodeMatch[1].trim();
97790
+ const funcCallMatch = innerContent.match(/(?:print|attempt_completion)\s*\(\s*([{\[][\s\S]*[}\]])\s*\)/);
97791
+ if (funcCallMatch) {
97792
+ return cleanSchemaResponse(funcCallMatch[1]);
97793
+ }
97794
+ return cleanSchemaResponse(innerContent);
97795
+ }
97761
97796
  const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
97762
97797
  if (jsonBlockMatch) {
97763
97798
  return normalizeJsonQuotes(jsonBlockMatch[1].trim());
@@ -97825,9 +97860,25 @@ function validateJsonResponse(response, options = {}) {
97825
97860
  console.log(`[DEBUG] JSON validation: Schema validation enabled`);
97826
97861
  }
97827
97862
  }
97863
+ let responseToValidate = response;
97864
+ try {
97865
+ JSON.parse(response);
97866
+ } catch (initialError) {
97867
+ if (initialError.message && initialError.message.includes("escape")) {
97868
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
97869
+ try {
97870
+ JSON.parse(sanitized);
97871
+ responseToValidate = sanitized;
97872
+ if (debug) {
97873
+ console.log(`[DEBUG] JSON validation: Fixed Markdown escapes in JSON (issue #441)`);
97874
+ }
97875
+ } catch {
97876
+ }
97877
+ }
97878
+ }
97828
97879
  try {
97829
97880
  const parseStart = Date.now();
97830
- const parsed = JSON.parse(response);
97881
+ const parsed = JSON.parse(responseToValidate);
97831
97882
  const parseTime = Date.now() - parseStart;
97832
97883
  if (debug) {
97833
97884
  console.log(`[DEBUG] JSON validation: Successfully parsed in ${parseTime}ms`);
@@ -98169,7 +98220,21 @@ function tryAutoWrapForSimpleSchema(response, schema, options = {}) {
98169
98220
  console.log(`[DEBUG] Auto-wrap: Response is already valid JSON, skipping`);
98170
98221
  }
98171
98222
  return null;
98172
- } catch {
98223
+ } catch (initialError) {
98224
+ if (initialError.message && initialError.message.includes("escape")) {
98225
+ try {
98226
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
98227
+ JSON.parse(sanitized);
98228
+ if (debug) {
98229
+ console.log(`[DEBUG] Auto-wrap: Fixed Markdown escapes in JSON (issue #441), returning sanitized JSON`);
98230
+ }
98231
+ return sanitized;
98232
+ } catch {
98233
+ if (debug) {
98234
+ console.log(`[DEBUG] Auto-wrap: Markdown escape sanitization didn't fix JSON, proceeding with wrapping`);
98235
+ }
98236
+ }
98237
+ }
98173
98238
  }
98174
98239
  const wrapped = JSON.stringify({ [wrapperInfo.fieldName]: response });
98175
98240
  if (debug) {
@@ -113502,6 +113567,25 @@ ${errorXml}
113502
113567
  }
113503
113568
  break;
113504
113569
  }
113570
+ if (options.schema) {
113571
+ let contentToCheck = assistantResponseContent;
113572
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "").trim();
113573
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*$/gi, "").trim();
113574
+ const cleanedJson = cleanSchemaResponse(contentToCheck);
113575
+ try {
113576
+ JSON.parse(cleanedJson);
113577
+ const validation = validateJsonResponse(cleanedJson, { debug: this.debug, schema: options.schema });
113578
+ if (validation.isValid) {
113579
+ if (this.debug) {
113580
+ console.log(`[DEBUG] Issue #443: Accepting valid JSON response without attempt_completion (${cleanedJson.length} chars)`);
113581
+ }
113582
+ finalResult = cleanedJson;
113583
+ completionAttempted = true;
113584
+ break;
113585
+ }
113586
+ } catch {
113587
+ }
113588
+ }
113505
113589
  consecutiveNoToolCount++;
113506
113590
  const isIdentical = lastNoToolResponse !== null && assistantResponseContent === lastNoToolResponse;
113507
113591
  const isSemanticallyStuck = lastNoToolResponse !== null && areBothStuckResponses(lastNoToolResponse, assistantResponseContent);
package/cjs/index.cjs CHANGED
@@ -83496,6 +83496,7 @@ __export(schemaUtils_exports, {
83496
83496
  processSchemaResponse: () => processSchemaResponse,
83497
83497
  replaceMermaidDiagramsInJson: () => replaceMermaidDiagramsInJson,
83498
83498
  replaceMermaidDiagramsInMarkdown: () => replaceMermaidDiagramsInMarkdown,
83499
+ sanitizeMarkdownEscapesInJson: () => sanitizeMarkdownEscapesInJson,
83499
83500
  tryAutoWrapForSimpleSchema: () => tryAutoWrapForSimpleSchema,
83500
83501
  tryMaidAutoFix: () => tryMaidAutoFix,
83501
83502
  validateAndFixMermaidResponse: () => validateAndFixMermaidResponse,
@@ -83599,6 +83600,17 @@ function decodeHtmlEntities(text) {
83599
83600
  }
83600
83601
  return decoded;
83601
83602
  }
83603
+ function sanitizeMarkdownEscapesInJson(jsonString) {
83604
+ if (!jsonString || typeof jsonString !== "string") {
83605
+ return jsonString;
83606
+ }
83607
+ return jsonString.replace(/\\\\|\\([^"\\\/bfnrtu])/g, (match2, captured) => {
83608
+ if (match2 === "\\\\") {
83609
+ return "\\\\";
83610
+ }
83611
+ return captured;
83612
+ });
83613
+ }
83602
83614
  function normalizeJsonQuotes(str) {
83603
83615
  if (!str || typeof str !== "string") {
83604
83616
  return str;
@@ -83651,6 +83663,15 @@ function cleanSchemaResponse(response) {
83651
83663
  if (resultWrapperMatch) {
83652
83664
  return cleanSchemaResponse(resultWrapperMatch[1]);
83653
83665
  }
83666
+ const toolCodeMatch = trimmed.match(/<tool_code>\s*([\s\S]*?)\s*<\/tool_code>/);
83667
+ if (toolCodeMatch) {
83668
+ let innerContent = toolCodeMatch[1].trim();
83669
+ const funcCallMatch = innerContent.match(/(?:print|attempt_completion)\s*\(\s*([{\[][\s\S]*[}\]])\s*\)/);
83670
+ if (funcCallMatch) {
83671
+ return cleanSchemaResponse(funcCallMatch[1]);
83672
+ }
83673
+ return cleanSchemaResponse(innerContent);
83674
+ }
83654
83675
  const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
83655
83676
  if (jsonBlockMatch) {
83656
83677
  return normalizeJsonQuotes(jsonBlockMatch[1].trim());
@@ -83718,9 +83739,25 @@ function validateJsonResponse(response, options = {}) {
83718
83739
  console.log(`[DEBUG] JSON validation: Schema validation enabled`);
83719
83740
  }
83720
83741
  }
83742
+ let responseToValidate = response;
83743
+ try {
83744
+ JSON.parse(response);
83745
+ } catch (initialError) {
83746
+ if (initialError.message && initialError.message.includes("escape")) {
83747
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
83748
+ try {
83749
+ JSON.parse(sanitized);
83750
+ responseToValidate = sanitized;
83751
+ if (debug) {
83752
+ console.log(`[DEBUG] JSON validation: Fixed Markdown escapes in JSON (issue #441)`);
83753
+ }
83754
+ } catch {
83755
+ }
83756
+ }
83757
+ }
83721
83758
  try {
83722
83759
  const parseStart = Date.now();
83723
- const parsed = JSON.parse(response);
83760
+ const parsed = JSON.parse(responseToValidate);
83724
83761
  const parseTime = Date.now() - parseStart;
83725
83762
  if (debug) {
83726
83763
  console.log(`[DEBUG] JSON validation: Successfully parsed in ${parseTime}ms`);
@@ -84062,7 +84099,21 @@ function tryAutoWrapForSimpleSchema(response, schema, options = {}) {
84062
84099
  console.log(`[DEBUG] Auto-wrap: Response is already valid JSON, skipping`);
84063
84100
  }
84064
84101
  return null;
84065
- } catch {
84102
+ } catch (initialError) {
84103
+ if (initialError.message && initialError.message.includes("escape")) {
84104
+ try {
84105
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
84106
+ JSON.parse(sanitized);
84107
+ if (debug) {
84108
+ console.log(`[DEBUG] Auto-wrap: Fixed Markdown escapes in JSON (issue #441), returning sanitized JSON`);
84109
+ }
84110
+ return sanitized;
84111
+ } catch {
84112
+ if (debug) {
84113
+ console.log(`[DEBUG] Auto-wrap: Markdown escape sanitization didn't fix JSON, proceeding with wrapping`);
84114
+ }
84115
+ }
84116
+ }
84066
84117
  }
84067
84118
  const wrapped = JSON.stringify({ [wrapperInfo.fieldName]: response });
84068
84119
  if (debug) {
@@ -104258,6 +104309,13 @@ function generateSandboxGlobals(options) {
104258
104309
  if (i5 < keys2.length) params[keys2[i5]] = arg;
104259
104310
  });
104260
104311
  }
104312
+ if (params.path && typeof params.path === "object") {
104313
+ const coercedPath = params.path.file_path || params.path.path || params.path.directory || params.path.filename;
104314
+ if (coercedPath && typeof coercedPath === "string") {
104315
+ logFn?.(`[${name14}] Warning: Coerced object path to string "${coercedPath}" (issue #444)`);
104316
+ params.path = coercedPath;
104317
+ }
104318
+ }
104261
104319
  const validated = schema.safeParse(params);
104262
104320
  if (!validated.success) {
104263
104321
  throw new Error(`Invalid parameters for ${name14}: ${validated.error.message}`);
@@ -104294,6 +104352,11 @@ function generateSandboxGlobals(options) {
104294
104352
  }
104295
104353
  if (llmCall) {
104296
104354
  const rawLLM = async (instruction, data3, opts = {}) => {
104355
+ const dataStr = typeof data3 === "string" ? data3 : JSON.stringify(data3);
104356
+ if (dataStr && dataStr.startsWith("ERROR:")) {
104357
+ logFn?.("[LLM] Blocked: data contains error from previous tool call");
104358
+ return "ERROR: Previous tool call failed - " + dataStr.substring(0, 200);
104359
+ }
104297
104360
  const result = await llmCall(instruction, data3, opts);
104298
104361
  if (opts.schema && typeof result === "string") {
104299
104362
  try {
@@ -106591,6 +106654,7 @@ ${lastError}
106591
106654
 
106592
106655
  RULES REMINDER:
106593
106656
  - search(query) is KEYWORD SEARCH \u2014 pass a search query, NOT a filename. Use extract(filepath) to read file contents.
106657
+ - search(query, path) \u2014 the path argument must be a STRING, not an object. Use field.file_path, not field.
106594
106658
  - search() returns up to 20K tokens by default. Use search(query, path, {maxTokens: null}) for unlimited, or searchAll(query) to auto-paginate ALL results.
106595
106659
  - search(), searchAll(), query(), extract(), listFiles(), bash() all return STRINGS, not arrays.
106596
106660
  - Use chunk(stringData) to split a string into an array of chunks.
@@ -106599,7 +106663,8 @@ RULES REMINDER:
106599
106663
  - Do NOT define helper functions that call tools \u2014 write logic inline.
106600
106664
  - Do NOT use async/await, template literals, or shorthand properties.
106601
106665
  - Do NOT use regex literals (/pattern/) \u2014 use String methods like indexOf, includes, startsWith instead.
106602
- - String concatenation with +, not template literals.`;
106666
+ - String concatenation with +, not template literals.
106667
+ - IMPORTANT: If a tool returns "ERROR: ...", do NOT pass that error string to LLM() \u2014 handle or skip it.`;
106603
106668
  const fixedCode = await llmCallFn(fixPrompt, "", { maxTokens: 4e3, temperature: 0.2 });
106604
106669
  currentCode = stripCodeWrapping(fixedCode);
106605
106670
  planSpan?.addEvent?.("dsl.self_heal_complete", {
@@ -111855,6 +111920,25 @@ ${errorXml}
111855
111920
  }
111856
111921
  break;
111857
111922
  }
111923
+ if (options.schema) {
111924
+ let contentToCheck = assistantResponseContent;
111925
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "").trim();
111926
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*$/gi, "").trim();
111927
+ const cleanedJson = cleanSchemaResponse(contentToCheck);
111928
+ try {
111929
+ JSON.parse(cleanedJson);
111930
+ const validation = validateJsonResponse(cleanedJson, { debug: this.debug, schema: options.schema });
111931
+ if (validation.isValid) {
111932
+ if (this.debug) {
111933
+ console.log(`[DEBUG] Issue #443: Accepting valid JSON response without attempt_completion (${cleanedJson.length} chars)`);
111934
+ }
111935
+ finalResult = cleanedJson;
111936
+ completionAttempted = true;
111937
+ break;
111938
+ }
111939
+ } catch {
111940
+ }
111941
+ }
111858
111942
  consecutiveNoToolCount++;
111859
111943
  const isIdentical = lastNoToolResponse !== null && assistantResponseContent === lastNoToolResponse;
111860
111944
  const isSemanticallyStuck = lastNoToolResponse !== null && areBothStuckResponses(lastNoToolResponse, assistantResponseContent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc253",
3
+ "version": "0.6.0-rc254",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -4005,6 +4005,33 @@ Follow these instructions carefully:
4005
4005
  break;
4006
4006
  }
4007
4007
 
4008
+ // Issue #443: Check if response contains valid schema-matching JSON
4009
+ // Before triggering error.no_tool_call, strip markdown fences and validate
4010
+ // This handles cases where AI returns valid JSON without using attempt_completion
4011
+ if (options.schema) {
4012
+ // Remove thinking tags first
4013
+ let contentToCheck = assistantResponseContent;
4014
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
4015
+ contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*$/gi, '').trim();
4016
+
4017
+ // Try to extract and validate JSON
4018
+ const cleanedJson = cleanSchemaResponse(contentToCheck);
4019
+ try {
4020
+ JSON.parse(cleanedJson);
4021
+ const validation = validateJsonResponse(cleanedJson, { debug: this.debug, schema: options.schema });
4022
+ if (validation.isValid) {
4023
+ if (this.debug) {
4024
+ console.log(`[DEBUG] Issue #443: Accepting valid JSON response without attempt_completion (${cleanedJson.length} chars)`);
4025
+ }
4026
+ finalResult = cleanedJson;
4027
+ completionAttempted = true;
4028
+ break;
4029
+ }
4030
+ } catch {
4031
+ // Not valid JSON - continue to standard no_tool_call handling
4032
+ }
4033
+ }
4034
+
4008
4035
  // Increment consecutive no-tool counter (catches alternating stuck responses)
4009
4036
  consecutiveNoToolCount++;
4010
4037
 
@@ -183,6 +183,16 @@ export function generateSandboxGlobals(options) {
183
183
  });
184
184
  }
185
185
 
186
+ // Issue #444: Auto-coerce object paths to strings for search()
187
+ // AI-generated DSL sometimes passes field objects instead of field.file_path strings
188
+ if (params.path && typeof params.path === 'object') {
189
+ const coercedPath = params.path.file_path || params.path.path || params.path.directory || params.path.filename;
190
+ if (coercedPath && typeof coercedPath === 'string') {
191
+ logFn?.(`[${name}] Warning: Coerced object path to string "${coercedPath}" (issue #444)`);
192
+ params.path = coercedPath;
193
+ }
194
+ }
195
+
186
196
  const validated = schema.safeParse(params);
187
197
  if (!validated.success) {
188
198
  throw new Error(`Invalid parameters for ${name}: ${validated.error.message}`);
@@ -232,6 +242,15 @@ export function generateSandboxGlobals(options) {
232
242
  // When schema is provided, auto-parse the JSON result for easier downstream processing
233
243
  if (llmCall) {
234
244
  const rawLLM = async (instruction, data, opts = {}) => {
245
+ // Issue #444: Guard against error strings being passed as data
246
+ // When previous tool calls fail, they return "ERROR: ..." strings
247
+ // Passing these to LLM() spawns useless delegates that can't help
248
+ const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
249
+ if (dataStr && dataStr.startsWith('ERROR:')) {
250
+ logFn?.('[LLM] Blocked: data contains error from previous tool call');
251
+ return 'ERROR: Previous tool call failed - ' + dataStr.substring(0, 200);
252
+ }
253
+
235
254
  const result = await llmCall(instruction, data, opts);
236
255
  // Auto-parse JSON when schema is provided and result is a string
237
256
  if (opts.schema && typeof result === 'string') {
@@ -165,6 +165,39 @@ export function decodeHtmlEntities(text) {
165
165
  return decoded;
166
166
  }
167
167
 
168
+ /**
169
+ * Sanitize Markdown escape sequences in JSON strings
170
+ *
171
+ * Markdown uses backslash escapes like \*, \_, \#, \~ etc. which are NOT valid
172
+ * JSON escape sequences. When AI models produce JSON with Markdown content,
173
+ * these escapes cause JSON.parse() to fail with "Invalid \escape" errors.
174
+ *
175
+ * This function removes the backslash from invalid escape sequences while
176
+ * preserving valid JSON escapes: \\, \", \/, \b, \f, \n, \r, \t, \uXXXX
177
+ *
178
+ * @param {string} jsonString - JSON string that may contain Markdown escapes
179
+ * @returns {string} - JSON string with invalid escapes sanitized
180
+ */
181
+ export function sanitizeMarkdownEscapesInJson(jsonString) {
182
+ if (!jsonString || typeof jsonString !== 'string') {
183
+ return jsonString;
184
+ }
185
+
186
+ // Strategy: Match either:
187
+ // 1. \\\\ (escaped backslash) - preserve as-is
188
+ // 2. \\X where X is NOT a valid JSON escape char - remove the backslash
189
+ //
190
+ // Valid JSON escape chars: " \ / b f n r t u
191
+ // This converts: \* → *, \_ → _, \# → #, \~ → ~, etc.
192
+ // But preserves: \\, \", \n, \t, \r, \b, \f, \/, \uXXXX
193
+ return jsonString.replace(/\\\\|\\([^"\\\/bfnrtu])/g, (match, captured) => {
194
+ if (match === '\\\\') {
195
+ return '\\\\'; // Preserve escaped backslash
196
+ }
197
+ return captured; // Remove backslash from invalid escape
198
+ });
199
+ }
200
+
168
201
  /**
169
202
  * Normalize JavaScript syntax to valid JSON syntax
170
203
  * Converts single quotes to double quotes for strings in JSON-like structures
@@ -261,6 +294,22 @@ export function cleanSchemaResponse(response) {
261
294
  return cleanSchemaResponse(resultWrapperMatch[1]);
262
295
  }
263
296
 
297
+ // Strip <tool_code>...</tool_code> wrapper (Gemini-style code execution format)
298
+ // Issue #443: Gemini sometimes wraps responses in <plan> + <tool_code> tags
299
+ // e.g., <tool_code>print(attempt_completion({"projects": ["repo1"]}))</tool_code>
300
+ const toolCodeMatch = trimmed.match(/<tool_code>\s*([\s\S]*?)\s*<\/tool_code>/);
301
+ if (toolCodeMatch) {
302
+ let innerContent = toolCodeMatch[1].trim();
303
+ // Extract JSON from print() or attempt_completion() wrappers
304
+ // e.g., print({"key": "value"}) or attempt_completion({"key": "value"})
305
+ const funcCallMatch = innerContent.match(/(?:print|attempt_completion)\s*\(\s*([{\[][\s\S]*[}\]])\s*\)/);
306
+ if (funcCallMatch) {
307
+ return cleanSchemaResponse(funcCallMatch[1]);
308
+ }
309
+ // Try cleaning the inner content directly
310
+ return cleanSchemaResponse(innerContent);
311
+ }
312
+
264
313
  // First, look for JSON after code block markers - similar to mermaid extraction
265
314
  // Try with json language specifier
266
315
  const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
@@ -370,9 +419,30 @@ export function validateJsonResponse(response, options = {}) {
370
419
  }
371
420
  }
372
421
 
422
+ // Try to parse the response, with fallback to sanitizing Markdown escapes (issue #441)
423
+ let responseToValidate = response;
424
+ try {
425
+ JSON.parse(response);
426
+ } catch (initialError) {
427
+ // Check if the error is due to invalid escape sequences (Markdown escapes like \*, \_)
428
+ if (initialError.message && initialError.message.includes('escape')) {
429
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
430
+ try {
431
+ JSON.parse(sanitized);
432
+ // Sanitized version parses - use it instead
433
+ responseToValidate = sanitized;
434
+ if (debug) {
435
+ console.log(`[DEBUG] JSON validation: Fixed Markdown escapes in JSON (issue #441)`);
436
+ }
437
+ } catch {
438
+ // Sanitization didn't help, continue with original (will fail below with proper error)
439
+ }
440
+ }
441
+ }
442
+
373
443
  try {
374
444
  const parseStart = Date.now();
375
- const parsed = JSON.parse(response);
445
+ const parsed = JSON.parse(responseToValidate);
376
446
  const parseTime = Date.now() - parseStart;
377
447
 
378
448
  if (debug) {
@@ -853,7 +923,26 @@ export function tryAutoWrapForSimpleSchema(response, schema, options = {}) {
853
923
  console.log(`[DEBUG] Auto-wrap: Response is already valid JSON, skipping`);
854
924
  }
855
925
  return null;
856
- } catch {
926
+ } catch (initialError) {
927
+ // Not valid JSON - check if it's due to Markdown escapes (issue #441)
928
+ // AI models sometimes produce JSON with Markdown escapes like \* or \_
929
+ // which are valid Markdown but NOT valid JSON escape sequences
930
+ if (initialError.message && initialError.message.includes('escape')) {
931
+ try {
932
+ const sanitized = sanitizeMarkdownEscapesInJson(response);
933
+ JSON.parse(sanitized);
934
+ // Sanitized JSON is valid! Return it instead of wrapping
935
+ if (debug) {
936
+ console.log(`[DEBUG] Auto-wrap: Fixed Markdown escapes in JSON (issue #441), returning sanitized JSON`);
937
+ }
938
+ return sanitized;
939
+ } catch {
940
+ // Sanitization didn't help, proceed with wrapping
941
+ if (debug) {
942
+ console.log(`[DEBUG] Auto-wrap: Markdown escape sanitization didn't fix JSON, proceeding with wrapping`);
943
+ }
944
+ }
945
+ }
857
946
  // Not valid JSON, proceed with wrapping
858
947
  }
859
948
 
@@ -436,6 +436,7 @@ ${lastError}
436
436
 
437
437
  RULES REMINDER:
438
438
  - search(query) is KEYWORD SEARCH — pass a search query, NOT a filename. Use extract(filepath) to read file contents.
439
+ - search(query, path) — the path argument must be a STRING, not an object. Use field.file_path, not field.
439
440
  - search() returns up to 20K tokens by default. Use search(query, path, {maxTokens: null}) for unlimited, or searchAll(query) to auto-paginate ALL results.
440
441
  - search(), searchAll(), query(), extract(), listFiles(), bash() all return STRINGS, not arrays.
441
442
  - Use chunk(stringData) to split a string into an array of chunks.
@@ -444,7 +445,8 @@ RULES REMINDER:
444
445
  - Do NOT define helper functions that call tools — write logic inline.
445
446
  - Do NOT use async/await, template literals, or shorthand properties.
446
447
  - Do NOT use regex literals (/pattern/) — use String methods like indexOf, includes, startsWith instead.
447
- - String concatenation with +, not template literals.`;
448
+ - String concatenation with +, not template literals.
449
+ - IMPORTANT: If a tool returns "ERROR: ...", do NOT pass that error string to LLM() — handle or skip it.`;
448
450
 
449
451
  const fixedCode = await llmCallFn(fixPrompt, '', { maxTokens: 4000, temperature: 0.2 });
450
452
  // Strip markdown fences and XML tags the LLM might add