@probelabs/probe 0.6.0-rc206 → 0.6.0-rc208

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/bin/binaries/{probe-v0.6.0-rc206-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc208-aarch64-apple-darwin.tar.gz} +0 -0
  2. package/bin/binaries/probe-v0.6.0-rc208-aarch64-unknown-linux-musl.tar.gz +0 -0
  3. package/bin/binaries/probe-v0.6.0-rc208-x86_64-apple-darwin.tar.gz +0 -0
  4. package/bin/binaries/probe-v0.6.0-rc208-x86_64-pc-windows-msvc.zip +0 -0
  5. package/bin/binaries/probe-v0.6.0-rc208-x86_64-unknown-linux-musl.tar.gz +0 -0
  6. package/build/agent/ProbeAgent.js +144 -2
  7. package/build/agent/bashPermissions.js +88 -7
  8. package/build/agent/index.js +450 -18
  9. package/build/agent/mcp/client.js +234 -4
  10. package/build/agent/mcp/config.js +87 -0
  11. package/build/agent/mcp/xmlBridge.js +15 -5
  12. package/build/agent/simpleTelemetry.js +26 -0
  13. package/build/tools/bash.js +5 -3
  14. package/build/tools/common.js +31 -0
  15. package/cjs/agent/ProbeAgent.cjs +428 -18
  16. package/cjs/agent/simpleTelemetry.cjs +22 -0
  17. package/cjs/index.cjs +450 -18
  18. package/package.json +1 -1
  19. package/src/agent/ProbeAgent.js +144 -2
  20. package/src/agent/bashPermissions.js +88 -7
  21. package/src/agent/mcp/client.js +234 -4
  22. package/src/agent/mcp/config.js +87 -0
  23. package/src/agent/mcp/xmlBridge.js +15 -5
  24. package/src/agent/simpleTelemetry.js +26 -0
  25. package/src/tools/bash.js +5 -3
  26. package/src/tools/common.js +31 -0
  27. package/bin/binaries/probe-v0.6.0-rc206-aarch64-unknown-linux-musl.tar.gz +0 -0
  28. package/bin/binaries/probe-v0.6.0-rc206-x86_64-apple-darwin.tar.gz +0 -0
  29. package/bin/binaries/probe-v0.6.0-rc206-x86_64-pc-windows-msvc.zip +0 -0
  30. package/bin/binaries/probe-v0.6.0-rc206-x86_64-unknown-linux-musl.tar.gz +0 -0
@@ -91,6 +91,45 @@ const MAX_TOOL_ITERATIONS = (() => {
91
91
  })();
92
92
  const MAX_HISTORY_MESSAGES = 100;
93
93
 
94
+ /**
95
+ * Extract tool name from wrapped_tool:toolName format
96
+ * @param {string} wrappedToolError - Error string in format 'wrapped_tool:toolName'
97
+ * @returns {string} The extracted tool name or 'unknown' if format is invalid
98
+ */
99
+ function extractWrappedToolName(wrappedToolError) {
100
+ if (!wrappedToolError || typeof wrappedToolError !== 'string') {
101
+ return 'unknown';
102
+ }
103
+ const colonIndex = wrappedToolError.indexOf(':');
104
+ return colonIndex !== -1 ? wrappedToolError.slice(colonIndex + 1) : 'unknown';
105
+ }
106
+
107
+ /**
108
+ * Check if an error indicates a wrapped tool format error
109
+ * @param {string|null} error - Error from detectUnrecognizedToolCall
110
+ * @returns {boolean} True if it's a wrapped tool error
111
+ */
112
+ function isWrappedToolError(error) {
113
+ return error && typeof error === 'string' && error.startsWith('wrapped_tool:');
114
+ }
115
+
116
+ /**
117
+ * Create error message for wrapped tool format issues
118
+ * @param {string} wrappedToolName - The tool name that was incorrectly wrapped
119
+ * @returns {string} User-friendly error message with correct format instructions
120
+ */
121
+ function createWrappedToolErrorMessage(wrappedToolName) {
122
+ return `Your response contained an incorrectly formatted tool call (${wrappedToolName} wrapped in XML tags). This cannot be used.
123
+
124
+ Please use the CORRECT format:
125
+
126
+ <${wrappedToolName}>
127
+ Your content here
128
+ </${wrappedToolName}>
129
+
130
+ Do NOT wrap in other tags like <api_call>, <tool_name>, <function>, etc.`;
131
+ }
132
+
94
133
  // Supported image file extensions (imported from shared config)
95
134
 
96
135
  // Maximum image file size (20MB) to prevent OOM attacks
@@ -2542,6 +2581,11 @@ Follow these instructions carefully:
2542
2581
  }
2543
2582
  }
2544
2583
 
2584
+ // Circuit breaker for repeated format errors
2585
+ let lastFormatErrorType = null;
2586
+ let sameFormatErrorCount = 0;
2587
+ const MAX_REPEATED_FORMAT_ERRORS = 3;
2588
+
2545
2589
  // Tool iteration loop (only for non-CLI engines like Vercel/Anthropic/OpenAI)
2546
2590
  while (currentIteration < maxIterations && !completionAttempted) {
2547
2591
  currentIteration++;
@@ -2835,7 +2879,28 @@ Follow these instructions carefully:
2835
2879
  );
2836
2880
 
2837
2881
  if (lastAssistantMessage) {
2838
- finalResult = lastAssistantMessage.content;
2882
+ const prevContent = lastAssistantMessage.content;
2883
+
2884
+ // Check for patterns indicating a failed/wrapped tool call attempt
2885
+ // Use detectUnrecognizedToolCall for consistent detection logic
2886
+ const wrappedToolError = detectUnrecognizedToolCall(prevContent, validTools);
2887
+
2888
+ if (isWrappedToolError(wrappedToolError)) {
2889
+ // Previous response was a broken tool call attempt - don't reuse it
2890
+ const wrappedToolName = extractWrappedToolName(wrappedToolError);
2891
+ if (this.debug) {
2892
+ console.log(`[DEBUG] Previous response contains wrapped tool '${wrappedToolName}' - rejecting for __PREVIOUS_RESPONSE__`);
2893
+ }
2894
+ currentMessages.push({ role: 'assistant', content: assistantResponseContent });
2895
+ currentMessages.push({
2896
+ role: 'user',
2897
+ content: createWrappedToolErrorMessage(wrappedToolName)
2898
+ });
2899
+ completionAttempted = false;
2900
+ continue; // Don't use broken response, continue the loop
2901
+ }
2902
+
2903
+ finalResult = prevContent;
2839
2904
  if (this.debug) console.log(`[DEBUG] Using previous response as completion: ${finalResult.substring(0, 100)}...`);
2840
2905
  } else {
2841
2906
  finalResult = 'Error: No previous response found to use as completion.';
@@ -3165,7 +3230,32 @@ Follow these instructions carefully:
3165
3230
  const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
3166
3231
 
3167
3232
  let reminderContent;
3168
- if (unrecognizedTool) {
3233
+ if (isWrappedToolError(unrecognizedTool)) {
3234
+ // AI wrapped a valid tool name in arbitrary XML tags - provide clear format error
3235
+ const wrappedToolName = extractWrappedToolName(unrecognizedTool);
3236
+ if (this.debug) {
3237
+ console.log(`[DEBUG] Detected wrapped tool '${wrappedToolName}' in assistant response - wrong XML format.`);
3238
+ }
3239
+ const toolError = new ParameterError(
3240
+ `Tool '${wrappedToolName}' found but in WRONG FORMAT - do not wrap tools in other XML tags.`,
3241
+ {
3242
+ suggestion: `Use the tool tag DIRECTLY without any wrapper:
3243
+
3244
+ CORRECT FORMAT:
3245
+ <${wrappedToolName}>
3246
+ <param>value</param>
3247
+ </${wrappedToolName}>
3248
+
3249
+ WRONG (what you did - do not wrap in other tags):
3250
+ <api_call><tool_name>${wrappedToolName}</tool_name>...</api_call>
3251
+ <function>${wrappedToolName}</function>
3252
+ <call name="${wrappedToolName}">...</call>
3253
+
3254
+ Remove ALL wrapper tags and use <${wrappedToolName}> directly as the outermost tag.`
3255
+ }
3256
+ );
3257
+ reminderContent = `<tool_result>\n${formatErrorForAI(toolError)}\n</tool_result>`;
3258
+ } else if (unrecognizedTool) {
3169
3259
  // AI tried to use a tool that's not available - provide clear error
3170
3260
  if (this.debug) {
3171
3261
  console.log(`[DEBUG] Detected unrecognized tool '${unrecognizedTool}' in assistant response.`);
@@ -3175,6 +3265,33 @@ Follow these instructions carefully:
3175
3265
  });
3176
3266
  reminderContent = `<tool_result>\n${formatErrorForAI(toolError)}\n</tool_result>`;
3177
3267
  } else {
3268
+ // No tool call detected at all - check if this is the last iteration
3269
+ // On the last iteration, if the AI gave a substantive response without using
3270
+ // attempt_completion, accept it as the final answer rather than losing the content
3271
+ if (currentIteration >= maxIterations) {
3272
+ // Clean up the response - remove thinking tags
3273
+ let cleanedResponse = assistantResponseContent;
3274
+ // Remove <thinking>...</thinking> blocks
3275
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
3276
+ // Also remove unclosed thinking tags
3277
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, '').trim();
3278
+
3279
+ // Only use if there's substantial content (not just a failed tool call attempt)
3280
+ const hasSubstantialContent = cleanedResponse.length > 50 &&
3281
+ !cleanedResponse.includes('<api_call>') &&
3282
+ !cleanedResponse.includes('<tool_name>') &&
3283
+ !cleanedResponse.includes('<function>');
3284
+
3285
+ if (hasSubstantialContent) {
3286
+ if (this.debug) {
3287
+ console.log(`[DEBUG] Max iterations reached - accepting AI response as final answer (${cleanedResponse.length} chars)`);
3288
+ }
3289
+ finalResult = cleanedResponse;
3290
+ completionAttempted = true;
3291
+ break;
3292
+ }
3293
+ }
3294
+
3178
3295
  // Standard reminder - no tool call detected at all
3179
3296
  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.
3180
3297
 
@@ -3206,6 +3323,31 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
3206
3323
  console.log(`[DEBUG] No tool call detected in assistant response. Prompting for tool use.`);
3207
3324
  }
3208
3325
  }
3326
+
3327
+ // Circuit breaker: track repeated format errors and break early
3328
+ // For wrapped_tool errors, track them as a category (any wrapped_tool counts)
3329
+ // For other errors, track the exact error type
3330
+ if (unrecognizedTool) {
3331
+ const isWrapped = isWrappedToolError(unrecognizedTool);
3332
+ const errorCategory = isWrapped ? 'wrapped_tool' : unrecognizedTool;
3333
+
3334
+ if (errorCategory === lastFormatErrorType) {
3335
+ sameFormatErrorCount++;
3336
+ if (sameFormatErrorCount >= MAX_REPEATED_FORMAT_ERRORS) {
3337
+ const errorDesc = isWrapped ? 'wrapped tool format' : unrecognizedTool;
3338
+ console.error(`[ERROR] Format error category '${errorCategory}' repeated ${sameFormatErrorCount} times. Breaking loop early to prevent infinite iteration.`);
3339
+ finalResult = `Error: Unable to complete request. The AI model repeatedly used incorrect tool call format (${errorDesc}). Please try rephrasing your question or using a different model.`;
3340
+ break;
3341
+ }
3342
+ } else {
3343
+ lastFormatErrorType = errorCategory;
3344
+ sameFormatErrorCount = 1;
3345
+ }
3346
+ } else {
3347
+ // Reset counter if it's a different kind of "no tool call" situation
3348
+ lastFormatErrorType = null;
3349
+ sameFormatErrorCount = 0;
3350
+ }
3209
3351
  }
3210
3352
 
3211
3353
  // Keep message history manageable
@@ -85,9 +85,11 @@ export class BashPermissionChecker {
85
85
  * @param {boolean} [config.disableDefaultAllow] - Disable default allow list
86
86
  * @param {boolean} [config.disableDefaultDeny] - Disable default deny list
87
87
  * @param {boolean} [config.debug] - Enable debug logging
88
+ * @param {Object} [config.tracer] - Optional tracer for telemetry
88
89
  */
89
90
  constructor(config = {}) {
90
91
  this.debug = config.debug || false;
92
+ this.tracer = config.tracer || null;
91
93
 
92
94
  // Build allow patterns
93
95
  this.allowPatterns = [];
@@ -122,6 +124,27 @@ export class BashPermissionChecker {
122
124
  if (this.debug) {
123
125
  console.log(`[BashPermissions] Total patterns - Allow: ${this.allowPatterns.length}, Deny: ${this.denyPatterns.length}`);
124
126
  }
127
+
128
+ // Record initialization event
129
+ this.recordBashEvent('permissions.initialized', {
130
+ allowPatternCount: this.allowPatterns.length,
131
+ denyPatternCount: this.denyPatterns.length,
132
+ hasCustomAllowPatterns: !!(config.allow && config.allow.length > 0),
133
+ hasCustomDenyPatterns: !!(config.deny && config.deny.length > 0),
134
+ disableDefaultAllow: !!config.disableDefaultAllow,
135
+ disableDefaultDeny: !!config.disableDefaultDeny
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Record a bash telemetry event if tracer is available
141
+ * @param {string} eventType - Event type (e.g., 'permission.checked', 'permission.denied')
142
+ * @param {Object} data - Event data
143
+ */
144
+ recordBashEvent(eventType, data = {}) {
145
+ if (this.tracer && typeof this.tracer.recordBashEvent === 'function') {
146
+ this.tracer.recordBashEvent(eventType, data);
147
+ }
125
148
  }
126
149
 
127
150
  /**
@@ -131,11 +154,17 @@ export class BashPermissionChecker {
131
154
  */
132
155
  check(command) {
133
156
  if (!command || typeof command !== 'string') {
134
- return {
157
+ const result = {
135
158
  allowed: false,
136
159
  reason: 'Invalid or empty command',
137
160
  command: command
138
161
  };
162
+ this.recordBashEvent('permission.denied', {
163
+ command: String(command),
164
+ reason: result.reason,
165
+ isComplex: false
166
+ });
167
+ return result;
139
168
  }
140
169
 
141
170
  // Check if this is a complex command
@@ -150,19 +179,32 @@ export class BashPermissionChecker {
150
179
  const parsed = parseCommand(command);
151
180
 
152
181
  if (parsed.error) {
153
- return {
182
+ const result = {
154
183
  allowed: false,
155
184
  reason: parsed.error,
156
185
  command: command
157
186
  };
187
+ this.recordBashEvent('permission.denied', {
188
+ command,
189
+ reason: result.reason,
190
+ isComplex: false,
191
+ parseError: true
192
+ });
193
+ return result;
158
194
  }
159
195
 
160
196
  if (!parsed.command) {
161
- return {
197
+ const result = {
162
198
  allowed: false,
163
199
  reason: 'No valid command found',
164
200
  command: command
165
201
  };
202
+ this.recordBashEvent('permission.denied', {
203
+ command,
204
+ reason: result.reason,
205
+ isComplex: false
206
+ });
207
+ return result;
166
208
  }
167
209
 
168
210
  if (this.debug) {
@@ -173,24 +215,39 @@ export class BashPermissionChecker {
173
215
  // Check deny patterns first (deny takes precedence)
174
216
  if (matchesAnyPattern(parsed, this.denyPatterns)) {
175
217
  const matchedPatterns = this.denyPatterns.filter(pattern => matchesPattern(parsed, pattern));
176
- return {
218
+ const result = {
177
219
  allowed: false,
178
220
  reason: `Command matches deny pattern: ${matchedPatterns[0]}`,
179
221
  command: command,
180
222
  parsed: parsed,
181
223
  matchedPatterns: matchedPatterns
182
224
  };
225
+ this.recordBashEvent('permission.denied', {
226
+ command,
227
+ parsedCommand: parsed.command,
228
+ reason: 'matches_deny_pattern',
229
+ matchedPattern: matchedPatterns[0],
230
+ isComplex: false
231
+ });
232
+ return result;
183
233
  }
184
234
 
185
235
  // Check allow patterns
186
236
  if (this.allowPatterns.length > 0) {
187
237
  if (!matchesAnyPattern(parsed, this.allowPatterns)) {
188
- return {
238
+ const result = {
189
239
  allowed: false,
190
240
  reason: 'Command not in allow list',
191
241
  command: command,
192
242
  parsed: parsed
193
243
  };
244
+ this.recordBashEvent('permission.denied', {
245
+ command,
246
+ parsedCommand: parsed.command,
247
+ reason: 'not_in_allow_list',
248
+ isComplex: false
249
+ });
250
+ return result;
194
251
  }
195
252
  }
196
253
 
@@ -206,6 +263,12 @@ export class BashPermissionChecker {
206
263
  console.log(`[BashPermissions] ALLOWED - command passed all checks`);
207
264
  }
208
265
 
266
+ this.recordBashEvent('permission.allowed', {
267
+ command,
268
+ parsedCommand: parsed.command,
269
+ isComplex: false
270
+ });
271
+
209
272
  return result;
210
273
  }
211
274
 
@@ -235,13 +298,20 @@ export class BashPermissionChecker {
235
298
  if (this.debug) {
236
299
  console.log(`[BashPermissions] DENIED - matches complex deny pattern: ${pattern}`);
237
300
  }
238
- return {
301
+ const result = {
239
302
  allowed: false,
240
303
  reason: `Command matches deny pattern: ${pattern}`,
241
304
  command: command,
242
305
  isComplex: true,
243
306
  matchedPatterns: [pattern]
244
307
  };
308
+ this.recordBashEvent('permission.denied', {
309
+ command,
310
+ reason: 'matches_deny_pattern',
311
+ matchedPattern: pattern,
312
+ isComplex: true
313
+ });
314
+ return result;
245
315
  }
246
316
  }
247
317
 
@@ -251,12 +321,18 @@ export class BashPermissionChecker {
251
321
  if (this.debug) {
252
322
  console.log(`[BashPermissions] ALLOWED - matches complex allow pattern: ${pattern}`);
253
323
  }
254
- return {
324
+ const result = {
255
325
  allowed: true,
256
326
  command: command,
257
327
  isComplex: true,
258
328
  matchedPattern: pattern
259
329
  };
330
+ this.recordBashEvent('permission.allowed', {
331
+ command,
332
+ matchedPattern: pattern,
333
+ isComplex: true
334
+ });
335
+ return result;
260
336
  }
261
337
  }
262
338
 
@@ -264,6 +340,11 @@ export class BashPermissionChecker {
264
340
  if (this.debug) {
265
341
  console.log(`[BashPermissions] DENIED - no matching complex pattern found`);
266
342
  }
343
+ this.recordBashEvent('permission.denied', {
344
+ command,
345
+ reason: 'no_matching_complex_pattern',
346
+ isComplex: true
347
+ });
267
348
  return {
268
349
  allowed: false,
269
350
  reason: 'Complex shell commands require explicit allow patterns (e.g., "cd * && git *")',