@probelabs/probe 0.6.0-rc130 → 0.6.0-rc132

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.
@@ -117,7 +117,7 @@ export interface AnswerOptions {
117
117
  export interface CloneOptions {
118
118
  /** Session ID for the cloned agent (defaults to new UUID) */
119
119
  sessionId?: string;
120
- /** Remove internal messages (schema reminders, mermaid fixes, etc.) */
120
+ /** Remove internal messages (schema reminders, schema formatting prompts, mermaid fixes, etc.) */
121
121
  stripInternalMessages?: boolean;
122
122
  /** Keep the system message in cloned history */
123
123
  keepSystemMessage?: boolean;
@@ -78,6 +78,7 @@ export class ProbeAgent {
78
78
  * @param {number} [options.maxResponseTokens] - Maximum tokens for AI responses
79
79
  * @param {number} [options.maxIterations] - Maximum tool iterations (overrides MAX_TOOL_ITERATIONS env var)
80
80
  * @param {boolean} [options.disableMermaidValidation=false] - Disable automatic mermaid diagram validation and fixing
81
+ * @param {boolean} [options.disableJsonValidation=false] - Disable automatic JSON validation and fixing (prevents infinite recursion in JsonFixingAgent)
81
82
  * @param {boolean} [options.enableMcp=false] - Enable MCP tool integration
82
83
  * @param {string} [options.mcpConfigPath] - Path to MCP configuration file
83
84
  * @param {Object} [options.mcpConfig] - MCP configuration object (overrides mcpConfigPath)
@@ -98,6 +99,7 @@ export class ProbeAgent {
98
99
  this.maxResponseTokens = options.maxResponseTokens || parseInt(process.env.MAX_RESPONSE_TOKENS || '0', 10) || null;
99
100
  this.maxIterations = options.maxIterations || null;
100
101
  this.disableMermaidValidation = !!options.disableMermaidValidation;
102
+ this.disableJsonValidation = !!options.disableJsonValidation;
101
103
 
102
104
  // Storage adapter (defaults to in-memory)
103
105
  this.storageAdapter = options.storageAdapter || new InMemoryStorageAdapter();
@@ -302,7 +304,8 @@ export class ProbeAgent {
302
304
  }
303
305
 
304
306
  // Get API keys from environment variables
305
- const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
307
+ // Support both ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN (used by Z.AI)
308
+ const anthropicApiKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN;
306
309
  const openaiApiKey = process.env.OPENAI_API_KEY;
307
310
  // Support both GOOGLE_GENERATIVE_AI_API_KEY (official) and GOOGLE_API_KEY (legacy)
308
311
  const googleApiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GOOGLE_API_KEY;
@@ -314,7 +317,7 @@ export class ProbeAgent {
314
317
 
315
318
  // Get custom API URLs if provided
316
319
  const llmBaseUrl = process.env.LLM_BASE_URL;
317
- const anthropicApiUrl = process.env.ANTHROPIC_API_URL || llmBaseUrl;
320
+ const anthropicApiUrl = process.env.ANTHROPIC_API_URL || process.env.ANTHROPIC_BASE_URL || llmBaseUrl;
318
321
  const openaiApiUrl = process.env.OPENAI_API_URL || llmBaseUrl;
319
322
  const googleApiUrl = process.env.GOOGLE_API_URL || llmBaseUrl;
320
323
  const awsBedrockBaseUrl = process.env.AWS_BEDROCK_BASE_URL || llmBaseUrl;
@@ -364,7 +367,7 @@ export class ProbeAgent {
364
367
  } else if ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey) {
365
368
  this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
366
369
  } else {
367
- throw new Error('No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY (or GOOGLE_API_KEY), AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION), or AWS_BEDROCK_API_KEY environment variables.');
370
+ throw new Error('No API key provided. Please set ANTHROPIC_API_KEY (or ANTHROPIC_AUTH_TOKEN), OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY (or GOOGLE_API_KEY), AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION), or AWS_BEDROCK_API_KEY environment variables.');
368
371
  }
369
372
  }
370
373
 
@@ -1610,7 +1613,8 @@ When troubleshooting:
1610
1613
  // Build appropriate reminder message based on whether schema is provided
1611
1614
  let reminderContent;
1612
1615
  if (options.schema) { // Apply for ANY schema, not just JSON schemas
1613
- // When schema is provided, give specific instructions
1616
+ // When schema is provided, AI should either use tools OR provide natural response
1617
+ // Schema formatting will happen automatically afterward
1614
1618
  reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
1615
1619
 
1616
1620
  Remember: Use proper XML format with BOTH opening and closing tags:
@@ -1619,15 +1623,16 @@ Remember: Use proper XML format with BOTH opening and closing tags:
1619
1623
  <parameter>value</parameter>
1620
1624
  </tool_name>
1621
1625
 
1622
- IMPORTANT: A schema was provided. You MUST respond with data that matches this schema.
1623
- Use attempt_completion with your response directly inside the tags:
1626
+ IMPORTANT: A schema was provided for the final output format. You have two options:
1624
1627
 
1628
+ Option 1 - Use attempt_completion with your complete answer:
1625
1629
  <attempt_completion>
1626
- [Your response content matching the provided schema format]
1630
+ [Your complete answer here - will be automatically formatted to match the schema]
1627
1631
  </attempt_completion>
1628
1632
 
1629
- Your response must conform to this schema:
1630
- ${options.schema}`;
1633
+ Option 2 - Provide a natural response without any tool, and it will be automatically formatted.
1634
+
1635
+ Do NOT try to format your response as JSON yourself - this will be done automatically.`;
1631
1636
  } else {
1632
1637
  // Standard reminder without schema
1633
1638
  reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
@@ -1807,7 +1812,14 @@ Convert your previous response content into actual JSON data that follows this s
1807
1812
  console.log(`[DEBUG] JSON validation: Starting validation process for schema response`);
1808
1813
  console.log(`[DEBUG] JSON validation: Response length: ${finalResult.length} chars`);
1809
1814
  }
1810
-
1815
+
1816
+ // Clean the response first to extract JSON from markdown/code blocks
1817
+ finalResult = cleanSchemaResponse(finalResult);
1818
+
1819
+ if (this.debug) {
1820
+ console.log(`[DEBUG] JSON validation: After cleaning, length: ${finalResult.length} chars`);
1821
+ }
1822
+
1811
1823
  // Record JSON validation start in telemetry
1812
1824
  if (this.tracer) {
1813
1825
  this.tracer.recordJsonValidationEvent('started', {
@@ -1815,96 +1827,90 @@ Convert your previous response content into actual JSON data that follows this s
1815
1827
  'json_validation.schema_type': 'JSON'
1816
1828
  });
1817
1829
  }
1818
-
1830
+
1819
1831
  let validation = validateJsonResponse(finalResult, { debug: this.debug });
1820
1832
  let retryCount = 0;
1821
1833
  const maxRetries = 3;
1822
-
1834
+
1823
1835
  // First check if the response is valid JSON but is actually a schema definition
1824
1836
  if (validation.isValid && isJsonSchemaDefinition(finalResult, { debug: this.debug })) {
1825
1837
  if (this.debug) {
1826
- console.log(`[DEBUG] JSON validation: Response is a JSON schema definition instead of data, correcting...`);
1838
+ console.log(`[DEBUG] JSON validation: Response is a JSON schema definition instead of data, needs correction...`);
1827
1839
  }
1828
-
1829
- // Use specialized correction prompt for schema definition confusion
1830
- const schemaDefinitionPrompt = createSchemaDefinitionCorrectionPrompt(
1831
- finalResult,
1832
- options.schema,
1833
- 0
1834
- );
1835
-
1836
- finalResult = await this.answer(schemaDefinitionPrompt, [], {
1837
- ...options,
1838
- _schemaFormatted: true
1839
- });
1840
- finalResult = cleanSchemaResponse(finalResult);
1841
- validation = validateJsonResponse(finalResult);
1842
- retryCount = 1; // Start at 1 since we already did one correction
1840
+ // Mark as invalid so it goes through the fixing process
1841
+ validation = {
1842
+ isValid: false,
1843
+ error: 'Response is a JSON schema definition instead of actual data',
1844
+ enhancedError: 'Response is a JSON schema definition instead of actual data. Please return data that conforms to the schema, not the schema itself.'
1845
+ };
1843
1846
  }
1844
-
1845
- while (!validation.isValid && retryCount < maxRetries) {
1847
+
1848
+ // Use separate JsonFixingAgent for JSON corrections (isolates session like Mermaid fixing)
1849
+ if (!validation.isValid) {
1846
1850
  if (this.debug) {
1847
- console.log(`[DEBUG] JSON validation: Validation failed (attempt ${retryCount + 1}/${maxRetries}):`, validation.error);
1848
- console.log(`[DEBUG] JSON validation: Invalid response sample: ${finalResult.substring(0, 300)}${finalResult.length > 300 ? '...' : ''}`);
1851
+ console.log(`[DEBUG] JSON validation: Starting separate JsonFixingAgent session...`);
1849
1852
  }
1850
-
1851
- // Check if the invalid response is actually a schema definition
1852
- let correctionPrompt;
1853
- try {
1854
- if (isJsonSchemaDefinition(finalResult, { debug: this.debug })) {
1855
- if (this.debug) {
1856
- console.log(`[DEBUG] JSON validation: Response is still a schema definition, using specialized correction`);
1857
- }
1858
- correctionPrompt = createSchemaDefinitionCorrectionPrompt(
1859
- finalResult,
1853
+
1854
+ const { JsonFixingAgent } = await import('./schemaUtils.js');
1855
+ const jsonFixer = new JsonFixingAgent({
1856
+ path: this.allowedFolders[0],
1857
+ provider: this.clientApiProvider,
1858
+ model: this.model,
1859
+ debug: this.debug,
1860
+ tracer: this.tracer
1861
+ });
1862
+
1863
+ let currentResult = finalResult;
1864
+ let currentValidation = validation;
1865
+
1866
+ while (!currentValidation.isValid && retryCount < maxRetries) {
1867
+ if (this.debug) {
1868
+ console.log(`[DEBUG] JSON validation: Validation failed (attempt ${retryCount + 1}/${maxRetries}):`, currentValidation.error);
1869
+ console.log(`[DEBUG] JSON validation: Invalid response sample: ${currentResult.substring(0, 300)}${currentResult.length > 300 ? '...' : ''}`);
1870
+ }
1871
+
1872
+ try {
1873
+ // Use specialized JsonFixingAgent to fix the JSON in a separate session
1874
+ currentResult = await jsonFixer.fixJson(
1875
+ currentResult,
1860
1876
  options.schema,
1861
- retryCount
1862
- );
1863
- } else {
1864
- correctionPrompt = createJsonCorrectionPrompt(
1865
- finalResult,
1866
- options.schema,
1867
- validation.error,
1868
- retryCount
1877
+ currentValidation,
1878
+ retryCount + 1
1869
1879
  );
1880
+
1881
+ // Validate the corrected response
1882
+ currentValidation = validateJsonResponse(currentResult, { debug: this.debug });
1883
+ retryCount++;
1884
+
1885
+ if (this.debug) {
1886
+ if (!currentValidation.isValid && retryCount < maxRetries) {
1887
+ console.log(`[DEBUG] JSON validation: Still invalid after correction ${retryCount}, retrying...`);
1888
+ console.log(`[DEBUG] JSON validation: Corrected response sample: ${currentResult.substring(0, 300)}${currentResult.length > 300 ? '...' : ''}`);
1889
+ } else if (currentValidation.isValid) {
1890
+ console.log(`[DEBUG] JSON validation: Successfully corrected after ${retryCount} attempts with JsonFixingAgent`);
1891
+ }
1892
+ }
1893
+ } catch (error) {
1894
+ if (this.debug) {
1895
+ console.error(`[DEBUG] JSON validation: JsonFixingAgent error on attempt ${retryCount + 1}:`, error.message);
1896
+ }
1897
+ // If JsonFixingAgent fails, break out of loop
1898
+ break;
1870
1899
  }
1871
- } catch (error) {
1872
- // If we can't parse to check if it's a schema definition, use regular correction
1873
- correctionPrompt = createJsonCorrectionPrompt(
1874
- finalResult,
1875
- options.schema,
1876
- validation.error,
1877
- retryCount
1878
- );
1879
1900
  }
1880
-
1881
- finalResult = await this.answer(correctionPrompt, [], {
1882
- ...options,
1883
- _schemaFormatted: true
1884
- });
1885
- finalResult = cleanSchemaResponse(finalResult);
1886
-
1887
- // Validate the corrected response
1888
- validation = validateJsonResponse(finalResult, { debug: this.debug });
1889
- retryCount++;
1890
-
1891
- if (this.debug) {
1892
- if (!validation.isValid && retryCount < maxRetries) {
1893
- console.log(`[DEBUG] JSON validation: Still invalid after correction ${retryCount}, retrying...`);
1894
- console.log(`[DEBUG] JSON validation: Corrected response sample: ${finalResult.substring(0, 300)}${finalResult.length > 300 ? '...' : ''}`);
1895
- } else if (validation.isValid) {
1896
- console.log(`[DEBUG] JSON validation: Successfully corrected after ${retryCount} attempts`);
1897
- }
1901
+
1902
+ // Update finalResult with the fixed version
1903
+ finalResult = currentResult;
1904
+ validation = currentValidation;
1905
+
1906
+ if (!validation.isValid && this.debug) {
1907
+ console.log(`[DEBUG] JSON validation: Still invalid after ${maxRetries} correction attempts with JsonFixingAgent:`, validation.error);
1908
+ console.log(`[DEBUG] JSON validation: Final invalid response: ${finalResult.substring(0, 500)}${finalResult.length > 500 ? '...' : ''}`);
1909
+ } else if (validation.isValid && this.debug) {
1910
+ console.log(`[DEBUG] JSON validation: Final validation successful`);
1898
1911
  }
1899
1912
  }
1900
1913
 
1901
- if (!validation.isValid && this.debug) {
1902
- console.log(`[DEBUG] JSON validation: Still invalid after ${maxRetries} correction attempts:`, validation.error);
1903
- console.log(`[DEBUG] JSON validation: Final invalid response: ${finalResult.substring(0, 500)}${finalResult.length > 500 ? '...' : ''}`);
1904
- } else if (validation.isValid && this.debug) {
1905
- console.log(`[DEBUG] JSON validation: Final validation successful`);
1906
- }
1907
-
1908
1914
  // Record JSON validation completion in telemetry
1909
1915
  if (this.tracer) {
1910
1916
  this.tracer.recordJsonValidationEvent('completed', {
@@ -2208,6 +2214,7 @@ Convert your previous response content into actual JSON data that follows this s
2208
2214
  maxResponseTokens: this.maxResponseTokens,
2209
2215
  maxIterations: this.maxIterations,
2210
2216
  disableMermaidValidation: this.disableMermaidValidation,
2217
+ disableJsonValidation: this.disableJsonValidation,
2211
2218
  enableMcp: !!this.mcpBridge,
2212
2219
  mcpConfig: this.mcpConfig,
2213
2220
  enableBash: this.enableBash,
@@ -2231,11 +2238,60 @@ Convert your previous response content into actual JSON data that follows this s
2231
2238
 
2232
2239
  /**
2233
2240
  * Internal method to strip internal/temporary messages from history
2234
- * Removes: schema reminders, mermaid fix prompts, tool use reminders, etc.
2235
- * Keeps: system message, user messages, assistant responses, tool results
2241
+ * Strategy: Find the FIRST schema-related message and truncate everything from that point onwards.
2242
+ * This ensures that all schema formatting iterations (IMPORTANT, CRITICAL, corrections, etc.) are removed.
2243
+ * Keeps: system message, user messages, assistant responses, tool results up to the first schema message
2236
2244
  * @private
2237
2245
  */
2238
2246
  _stripInternalMessages(history, keepSystemMessage = true) {
2247
+ // Find the first schema-related message index
2248
+ let firstSchemaMessageIndex = -1;
2249
+
2250
+ for (let i = 0; i < history.length; i++) {
2251
+ const message = history[i];
2252
+
2253
+ // Skip system messages
2254
+ if (message.role === 'system') {
2255
+ continue;
2256
+ }
2257
+
2258
+ // Check if this is a schema-related message
2259
+ if (this._isSchemaMessage(message)) {
2260
+ firstSchemaMessageIndex = i;
2261
+ if (this.debug) {
2262
+ console.log(`[DEBUG] Found first schema message at index ${i}, truncating from here`);
2263
+ }
2264
+ break;
2265
+ }
2266
+ }
2267
+
2268
+ // If no schema message found, try to find other internal messages and remove them individually
2269
+ if (firstSchemaMessageIndex === -1) {
2270
+ return this._stripNonSchemaInternalMessages(history, keepSystemMessage);
2271
+ }
2272
+
2273
+ // Truncate at the first schema message, then also filter non-schema internal messages
2274
+ // from the remaining history before the schema
2275
+ const truncated = history.slice(0, firstSchemaMessageIndex);
2276
+
2277
+ // Now filter non-schema internal messages from the truncated history
2278
+ const filtered = this._stripNonSchemaInternalMessages(truncated, keepSystemMessage);
2279
+
2280
+ if (this.debug) {
2281
+ const removedCount = history.length - filtered.length;
2282
+ console.log(`[DEBUG] Truncated at schema message (index ${firstSchemaMessageIndex}) and filtered non-schema internal messages`);
2283
+ console.log(`[DEBUG] Removed ${removedCount} messages total (${history.length} → ${filtered.length})`);
2284
+ }
2285
+
2286
+ return filtered;
2287
+ }
2288
+
2289
+ /**
2290
+ * Strip non-schema internal messages (mermaid fixes, tool reminders, etc.) individually
2291
+ * Used when no schema messages are present in history
2292
+ * @private
2293
+ */
2294
+ _stripNonSchemaInternalMessages(history, keepSystemMessage = true) {
2239
2295
  const filtered = [];
2240
2296
 
2241
2297
  for (let i = 0; i < history.length; i++) {
@@ -2251,10 +2307,10 @@ Convert your previous response content into actual JSON data that follows this s
2251
2307
  continue;
2252
2308
  }
2253
2309
 
2254
- // Check if this is an internal message that should be stripped
2255
- if (this._isInternalMessage(message, i, history)) {
2310
+ // Check if this is a non-schema internal message (mermaid, tool reminders)
2311
+ if (this._isNonSchemaInternalMessage(message)) {
2256
2312
  if (this.debug) {
2257
- console.log(`[DEBUG] Stripping internal message at index ${i}: ${message.role}`);
2313
+ console.log(`[DEBUG] Stripping non-schema internal message at index ${i}: ${message.role}`);
2258
2314
  }
2259
2315
  continue;
2260
2316
  }
@@ -2267,30 +2323,69 @@ Convert your previous response content into actual JSON data that follows this s
2267
2323
  }
2268
2324
 
2269
2325
  /**
2270
- * Determine if a message is an internal/temporary message
2326
+ * Check if a message is schema-related (IMPORTANT, CRITICAL, etc.)
2271
2327
  * @private
2272
2328
  */
2273
- _isInternalMessage(message, index, history) {
2329
+ _isSchemaMessage(message) {
2274
2330
  if (message.role !== 'user') {
2275
- return false; // Only user messages can be internal reminders
2331
+ return false;
2276
2332
  }
2277
2333
 
2278
- // Handle null/undefined content
2279
2334
  if (!message.content) {
2280
2335
  return false;
2281
2336
  }
2282
2337
 
2283
- const content = typeof message.content === 'string'
2284
- ? message.content
2285
- : JSON.stringify(message.content);
2338
+ let content;
2339
+ try {
2340
+ content = typeof message.content === 'string'
2341
+ ? message.content
2342
+ : JSON.stringify(message.content);
2343
+ } catch (error) {
2344
+ // If content cannot be stringified (e.g., circular reference), skip this message
2345
+ if (this.debug) {
2346
+ console.log(`[DEBUG] Could not stringify message content in _isSchemaMessage: ${error.message}`);
2347
+ }
2348
+ return false;
2349
+ }
2286
2350
 
2287
2351
  // Schema reminder messages
2288
2352
  if (content.includes('IMPORTANT: A schema was provided') ||
2289
2353
  content.includes('You MUST respond with data that matches this schema') ||
2290
- content.includes('Your response must conform to this schema:')) {
2354
+ content.includes('Your response must conform to this schema:') ||
2355
+ content.includes('CRITICAL: You MUST respond with ONLY valid JSON DATA') ||
2356
+ content.includes('Schema to follow (this is just the structure')) {
2291
2357
  return true;
2292
2358
  }
2293
2359
 
2360
+ return false;
2361
+ }
2362
+
2363
+ /**
2364
+ * Check if a message is a non-schema internal message (mermaid, tool reminders, JSON corrections)
2365
+ * @private
2366
+ */
2367
+ _isNonSchemaInternalMessage(message) {
2368
+ if (message.role !== 'user') {
2369
+ return false;
2370
+ }
2371
+
2372
+ if (!message.content) {
2373
+ return false;
2374
+ }
2375
+
2376
+ let content;
2377
+ try {
2378
+ content = typeof message.content === 'string'
2379
+ ? message.content
2380
+ : JSON.stringify(message.content);
2381
+ } catch (error) {
2382
+ // If content cannot be stringified (e.g., circular reference), skip this message
2383
+ if (this.debug) {
2384
+ console.log(`[DEBUG] Could not stringify message content in _isNonSchemaInternalMessage: ${error.message}`);
2385
+ }
2386
+ return false;
2387
+ }
2388
+
2294
2389
  // Tool use reminder messages
2295
2390
  if (content.includes('Please use one of the available tools') &&
2296
2391
  content.includes('or use attempt_completion') &&
@@ -2321,6 +2416,7 @@ Convert your previous response content into actual JSON data that follows this s
2321
2416
  return false;
2322
2417
  }
2323
2418
 
2419
+
2324
2420
  /**
2325
2421
  * Clean up resources (including MCP connections)
2326
2422
  */