@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.
- package/bin/binaries/probe-v0.6.0-rc254-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc254-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc254-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc254-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc254-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +27 -0
- package/build/agent/dsl/environment.js +19 -0
- package/build/agent/index.js +87 -3
- package/build/agent/schemaUtils.js +91 -2
- package/build/tools/executePlan.js +3 -1
- package/cjs/agent/ProbeAgent.cjs +87 -3
- package/cjs/index.cjs +87 -3
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +27 -0
- package/src/agent/dsl/environment.js +19 -0
- package/src/agent/schemaUtils.js +91 -2
- package/src/tools/executePlan.js +3 -1
- package/bin/binaries/probe-v0.6.0-rc253-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-x86_64-unknown-linux-musl.tar.gz +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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') {
|
package/build/agent/index.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
package/cjs/agent/ProbeAgent.cjs
CHANGED
|
@@ -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(
|
|
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(
|
|
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
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -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') {
|
package/src/agent/schemaUtils.js
CHANGED
|
@@ -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(
|
|
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
|
|
package/src/tools/executePlan.js
CHANGED
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|