@probelabs/probe 0.6.0-rc318 → 0.6.0-rc320

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.
@@ -147,6 +147,77 @@ export function debugLogToolResults(toolResults) {
147
147
  }
148
148
  }
149
149
 
150
+ function isPlainJsonSchemaObject(value) {
151
+ return value && typeof value === 'object' && !Array.isArray(value) && !value._def;
152
+ }
153
+
154
+ function sanitizeRequiredFieldsInJsonSchema(schema) {
155
+ if (!isPlainJsonSchemaObject(schema)) {
156
+ return;
157
+ }
158
+
159
+ if (Array.isArray(schema.required) && isPlainJsonSchemaObject(schema.properties)) {
160
+ const propertyNames = new Set(Object.keys(schema.properties));
161
+ const filteredRequired = schema.required.filter((name) => propertyNames.has(name));
162
+ if (filteredRequired.length > 0) {
163
+ schema.required = filteredRequired;
164
+ } else {
165
+ delete schema.required;
166
+ }
167
+ }
168
+
169
+ const visitSchemaMap = (schemaMap) => {
170
+ if (!isPlainJsonSchemaObject(schemaMap)) return;
171
+ for (const childSchema of Object.values(schemaMap)) {
172
+ sanitizeRequiredFieldsInJsonSchema(childSchema);
173
+ }
174
+ };
175
+
176
+ visitSchemaMap(schema.properties);
177
+ visitSchemaMap(schema.patternProperties);
178
+ visitSchemaMap(schema.definitions);
179
+ visitSchemaMap(schema.$defs);
180
+ visitSchemaMap(schema.dependentSchemas);
181
+
182
+ if (isPlainJsonSchemaObject(schema.items)) {
183
+ sanitizeRequiredFieldsInJsonSchema(schema.items);
184
+ } else if (Array.isArray(schema.items)) {
185
+ for (const itemSchema of schema.items) {
186
+ sanitizeRequiredFieldsInJsonSchema(itemSchema);
187
+ }
188
+ }
189
+
190
+ for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
191
+ if (Array.isArray(schema[keyword])) {
192
+ for (const childSchema of schema[keyword]) {
193
+ sanitizeRequiredFieldsInJsonSchema(childSchema);
194
+ }
195
+ }
196
+ }
197
+
198
+ if (isPlainJsonSchemaObject(schema.not)) {
199
+ sanitizeRequiredFieldsInJsonSchema(schema.not);
200
+ }
201
+
202
+ if (isPlainJsonSchemaObject(schema.additionalProperties)) {
203
+ sanitizeRequiredFieldsInJsonSchema(schema.additionalProperties);
204
+ }
205
+ }
206
+
207
+ export function sanitizeToolInputSchema(schema) {
208
+ if (!isPlainJsonSchemaObject(schema)) {
209
+ return schema;
210
+ }
211
+
212
+ try {
213
+ const clonedSchema = JSON.parse(JSON.stringify(schema));
214
+ sanitizeRequiredFieldsInJsonSchema(clonedSchema);
215
+ return clonedSchema;
216
+ } catch {
217
+ return schema;
218
+ }
219
+ }
220
+
150
221
  /**
151
222
  * ProbeAgent class to handle AI interactions with code search capabilities
152
223
  */
@@ -1888,7 +1959,8 @@ export class ProbeAgent {
1888
1959
  const wrapTool = (toolName, schema, description, executeFn) => {
1889
1960
  // Auto-wrap plain JSON Schema objects with jsonSchema() for AI SDK 5 compatibility
1890
1961
  // Zod schemas have a _def property; plain objects need wrapping
1891
- const resolvedSchema = schema && schema._def ? schema : jsonSchema(schema);
1962
+ const sanitizedSchema = sanitizeToolInputSchema(schema);
1963
+ const resolvedSchema = sanitizedSchema && sanitizedSchema._def ? sanitizedSchema : jsonSchema(sanitizedSchema);
1892
1964
  return tool({
1893
1965
  description,
1894
1966
  inputSchema: resolvedSchema,
@@ -2090,8 +2162,9 @@ export class ProbeAgent {
2090
2162
  for (const [name, mcpTool] of Object.entries(mcpTools)) {
2091
2163
  // MCP tools have raw JSON Schema inputSchema that must be wrapped with jsonSchema()
2092
2164
  // for the Vercel AI SDK. Without wrapping, asSchema() misidentifies them as Zod schemas.
2093
- const mcpSchema = mcpTool.inputSchema || mcpTool.parameters;
2094
- const wrappedSchema = mcpSchema && mcpSchema._def ? mcpSchema : jsonSchema(mcpSchema || { type: 'object', properties: {} });
2165
+ const mcpSchema = mcpTool.inputSchema || mcpTool.parameters || { type: 'object', properties: {} };
2166
+ const sanitizedSchema = sanitizeToolInputSchema(mcpSchema);
2167
+ const wrappedSchema = sanitizedSchema && sanitizedSchema._def ? sanitizedSchema : jsonSchema(sanitizedSchema);
2095
2168
  nativeTools[name] = tool({
2096
2169
  description: mcpTool.description || `MCP tool: ${name}`,
2097
2170
  inputSchema: wrappedSchema,
@@ -719,14 +719,12 @@ export const searchTool = (options = {}) => {
719
719
  // ── Delegate-level semantic dedup ────────────────────────────
720
720
  // Each delegate is a full flash agent session (minutes, not seconds).
721
721
  // Use LLM to detect semantic duplicates and suggest rewrites.
722
- // Compare against ALL previous delegations (not filtered by path) because
723
- // the parent model often narrows the path while asking the same concept
724
- // (e.g., "dedup" at /src → "deduplicate" at /src/search.js).
725
722
  const delegatePath = searchPath || '';
723
+ const samePathDelegations = previousDelegations.filter(d => d.path === delegatePath);
726
724
 
727
725
  let effectiveQuery = searchQuery;
728
726
 
729
- if (previousDelegations.length > 0) {
727
+ if (samePathDelegations.length > 0) {
730
728
  const dedupProvider = options.searchDelegateProvider || process.env.PROBE_SEARCH_DELEGATE_PROVIDER || options.provider || process.env.FORCE_PROVIDER || null;
731
729
  const dedupModelName = options.searchDelegateModel || process.env.PROBE_SEARCH_DELEGATE_MODEL || options.model || process.env.MODEL_NAME || null;
732
730
  // Lazily create the dedup model (same provider/model as delegate)
@@ -742,8 +740,8 @@ export const searchTool = (options = {}) => {
742
740
 
743
741
  const dedupSpanAttrs = {
744
742
  'dedup.query': searchQuery,
745
- 'dedup.previous_count': String(previousDelegations.length),
746
- 'dedup.previous_queries': previousDelegations.map(d => d.query).join(' | '),
743
+ 'dedup.previous_count': String(samePathDelegations.length),
744
+ 'dedup.previous_queries': samePathDelegations.map(d => d.query).join(' | '),
747
745
  'dedup.provider': dedupProvider || '',
748
746
  'dedup.model': dedupModelName || '',
749
747
  'dedup.model_available': cachedDedupModel ? 'true' : 'false',
@@ -751,7 +749,7 @@ export const searchTool = (options = {}) => {
751
749
 
752
750
  const dedup = options.tracer?.withSpan
753
751
  ? await options.tracer.withSpan('search.delegate.dedup', async () => {
754
- return await checkDelegateDedup(searchQuery, previousDelegations, cachedDedupModel, debug);
752
+ return await checkDelegateDedup(searchQuery, samePathDelegations, cachedDedupModel, debug);
755
753
  }, dedupSpanAttrs, (span, result) => {
756
754
  span.setAttributes({
757
755
  'dedup.action': result.action,
@@ -760,14 +758,14 @@ export const searchTool = (options = {}) => {
760
758
  'dedup.error': result.error || '',
761
759
  });
762
760
  })
763
- : await checkDelegateDedup(searchQuery, previousDelegations, cachedDedupModel, debug);
761
+ : await checkDelegateDedup(searchQuery, samePathDelegations, cachedDedupModel, debug);
764
762
 
765
763
  if (debug) {
766
764
  console.error(`[DEDUP-LLM] Query: "${searchQuery}" → ${dedup.action}: ${dedup.reason}${dedup.rewritten ? ` → "${dedup.rewritten}"` : ''}`);
767
765
  }
768
766
 
769
767
  if (dedup.action === 'block') {
770
- const prevQueries = previousDelegations.map(d => `"${d.query}"`).join(', ');
768
+ const prevQueries = samePathDelegations.map(d => `"${d.query}"`).join(', ');
771
769
  return `DELEGATE BLOCKED: "${searchQuery}" is semantically duplicate of previous delegation(s) [${prevQueries}]. ${dedup.reason}\n\nDo NOT re-delegate the same concept. Use extract() on files already found, or synthesize your answer from existing results.`;
772
770
  }
773
771