@probelabs/probe 0.6.0-rc302 → 0.6.0-rc304

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.
@@ -22274,6 +22274,87 @@ var init_dist3 = __esm({
22274
22274
  }
22275
22275
  });
22276
22276
 
22277
+ // src/utils/provider.js
22278
+ function createProviderInstance(config2) {
22279
+ switch (config2.provider) {
22280
+ case "anthropic":
22281
+ return (0, import_anthropic.createAnthropic)({
22282
+ apiKey: config2.apiKey,
22283
+ ...config2.baseURL && { baseURL: config2.baseURL }
22284
+ });
22285
+ case "openai":
22286
+ return (0, import_openai.createOpenAI)({
22287
+ compatibility: "strict",
22288
+ apiKey: config2.apiKey,
22289
+ ...config2.baseURL && { baseURL: config2.baseURL }
22290
+ });
22291
+ case "google":
22292
+ return (0, import_google.createGoogleGenerativeAI)({
22293
+ apiKey: config2.apiKey,
22294
+ ...config2.baseURL && { baseURL: config2.baseURL }
22295
+ });
22296
+ case "bedrock": {
22297
+ const bedrockConfig = {};
22298
+ if (config2.apiKey) {
22299
+ bedrockConfig.apiKey = config2.apiKey;
22300
+ } else if (config2.accessKeyId && config2.secretAccessKey) {
22301
+ bedrockConfig.accessKeyId = config2.accessKeyId;
22302
+ bedrockConfig.secretAccessKey = config2.secretAccessKey;
22303
+ if (config2.sessionToken) {
22304
+ bedrockConfig.sessionToken = config2.sessionToken;
22305
+ }
22306
+ }
22307
+ if (config2.region) bedrockConfig.region = config2.region;
22308
+ if (config2.baseURL) bedrockConfig.baseURL = config2.baseURL;
22309
+ return createAmazonBedrock(bedrockConfig);
22310
+ }
22311
+ default:
22312
+ throw new Error(`Unknown provider "${config2.provider}"`);
22313
+ }
22314
+ }
22315
+ function resolveApiKey(providerName) {
22316
+ switch (providerName) {
22317
+ case "anthropic":
22318
+ return process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN;
22319
+ case "openai":
22320
+ return process.env.OPENAI_API_KEY;
22321
+ case "google":
22322
+ return process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
22323
+ case "bedrock":
22324
+ return process.env.AWS_BEDROCK_API_KEY;
22325
+ default:
22326
+ return void 0;
22327
+ }
22328
+ }
22329
+ async function createLanguageModel(providerName, modelName) {
22330
+ if (!providerName) return null;
22331
+ const resolvedModel = modelName || DEFAULT_MODELS[providerName];
22332
+ if (!resolvedModel) return null;
22333
+ try {
22334
+ const apiKey = resolveApiKey(providerName);
22335
+ const provider = createProviderInstance({ provider: providerName, ...apiKey ? { apiKey } : {} });
22336
+ return provider(resolvedModel);
22337
+ } catch {
22338
+ return null;
22339
+ }
22340
+ }
22341
+ var import_anthropic, import_openai, import_google, DEFAULT_MODELS;
22342
+ var init_provider = __esm({
22343
+ "src/utils/provider.js"() {
22344
+ "use strict";
22345
+ import_anthropic = require("@ai-sdk/anthropic");
22346
+ import_openai = require("@ai-sdk/openai");
22347
+ import_google = require("@ai-sdk/google");
22348
+ init_dist3();
22349
+ DEFAULT_MODELS = {
22350
+ anthropic: "claude-sonnet-4-6",
22351
+ openai: "gpt-5.2",
22352
+ google: "gemini-2.5-flash",
22353
+ bedrock: "anthropic.claude-sonnet-4-6"
22354
+ };
22355
+ }
22356
+ });
22357
+
22277
22358
  // node_modules/gpt-tokenizer/esm/bpeRanks/o200k_base.js
22278
22359
  var c0, c1, bpe, o200k_base_default;
22279
22360
  var init_o200k_base = __esm({
@@ -24014,16 +24095,128 @@ var init_tokenCounter = __esm({
24014
24095
  });
24015
24096
 
24016
24097
  // src/agent/otelLogBridge.js
24017
- var import_module, _require;
24098
+ function getOtelApi() {
24099
+ if (otelApiAttempted) return otelApi;
24100
+ otelApiAttempted = true;
24101
+ try {
24102
+ otelApi = (function(name15) {
24103
+ return _require(name15);
24104
+ })("@opentelemetry/api");
24105
+ } catch {
24106
+ }
24107
+ return otelApi;
24108
+ }
24109
+ function getOtelLogger() {
24110
+ if (otelLoggerAttempted) return otelLogger;
24111
+ otelLoggerAttempted = true;
24112
+ try {
24113
+ const { logs } = (function(name15) {
24114
+ return _require(name15);
24115
+ })("@opentelemetry/api-logs");
24116
+ otelLogger = logs.getLogger("probe-agent");
24117
+ } catch {
24118
+ }
24119
+ return otelLogger;
24120
+ }
24121
+ function getTraceSuffix() {
24122
+ try {
24123
+ const api2 = getOtelApi();
24124
+ if (!api2) return "";
24125
+ const span = api2.trace.getSpan(api2.context.active());
24126
+ const ctx = span?.spanContext?.();
24127
+ if (!ctx?.traceId) return "";
24128
+ return ` [trace_id=${ctx.traceId} span_id=${ctx.spanId}]`;
24129
+ } catch {
24130
+ return "";
24131
+ }
24132
+ }
24133
+ function emitOtelLog(msg, level) {
24134
+ try {
24135
+ const logger = getOtelLogger();
24136
+ if (!logger) return;
24137
+ const api2 = getOtelApi();
24138
+ let traceId, spanId;
24139
+ if (api2) {
24140
+ const span = api2.trace.getSpan(api2.context.active());
24141
+ const ctx = span?.spanContext?.();
24142
+ if (ctx?.traceId) {
24143
+ traceId = ctx.traceId;
24144
+ spanId = ctx.spanId;
24145
+ }
24146
+ }
24147
+ logger.emit({
24148
+ severityNumber: OTEL_SEVERITY[level] || 9,
24149
+ severityText: level.toUpperCase(),
24150
+ body: msg,
24151
+ attributes: {
24152
+ "probe.logger": true,
24153
+ ...traceId ? { trace_id: traceId, span_id: spanId } : {}
24154
+ }
24155
+ });
24156
+ } catch {
24157
+ }
24158
+ }
24159
+ function patchConsole() {
24160
+ if (patched) return;
24161
+ const methods = ["log", "info", "warn", "error"];
24162
+ const c = globalThis.console;
24163
+ for (const m of methods) {
24164
+ const orig = c[m].bind(c);
24165
+ originals[m] = orig;
24166
+ c[m] = (...args) => {
24167
+ const msgParts = args.map(
24168
+ (a) => typeof a === "string" ? a : a instanceof Error ? a.message : JSON.stringify(a)
24169
+ );
24170
+ const msg = msgParts.join(" ");
24171
+ emitOtelLog(msg, m === "log" ? "log" : m);
24172
+ const suffix = getTraceSuffix();
24173
+ if (suffix) {
24174
+ if (typeof args[0] === "string") {
24175
+ args[0] = args[0] + suffix;
24176
+ } else {
24177
+ args.push(suffix);
24178
+ }
24179
+ }
24180
+ return orig(...args);
24181
+ };
24182
+ }
24183
+ patched = true;
24184
+ }
24185
+ var import_module, _require, OTEL_SEVERITY, patched, originals, otelApi, otelApiAttempted, otelLogger, otelLoggerAttempted;
24018
24186
  var init_otelLogBridge = __esm({
24019
24187
  "src/agent/otelLogBridge.js"() {
24020
24188
  "use strict";
24021
24189
  import_module = require("module");
24022
24190
  _require = (0, import_module.createRequire)("file:///");
24191
+ OTEL_SEVERITY = {
24192
+ log: 9,
24193
+ // INFO
24194
+ info: 9,
24195
+ // INFO
24196
+ warn: 13,
24197
+ // WARN
24198
+ error: 17,
24199
+ // ERROR
24200
+ debug: 5
24201
+ // DEBUG
24202
+ };
24203
+ patched = false;
24204
+ originals = {};
24205
+ otelApi = null;
24206
+ otelApiAttempted = false;
24207
+ otelLogger = null;
24208
+ otelLoggerAttempted = false;
24023
24209
  }
24024
24210
  });
24025
24211
 
24026
24212
  // src/agent/simpleTelemetry.js
24213
+ var simpleTelemetry_exports = {};
24214
+ __export(simpleTelemetry_exports, {
24215
+ SimpleAppTracer: () => SimpleAppTracer,
24216
+ SimpleTelemetry: () => SimpleTelemetry,
24217
+ initializeSimpleTelemetryFromOptions: () => initializeSimpleTelemetryFromOptions,
24218
+ truncateForSpan: () => truncateForSpan
24219
+ });
24027
24220
  function truncateForSpan(text, maxLen = 4096) {
24028
24221
  if (!text || text.length <= maxLen) return text || "";
24029
24222
  const half = Math.floor((maxLen - 40) / 2);
@@ -24032,13 +24225,455 @@ function truncateForSpan(text, maxLen = 4096) {
24032
24225
  ... [${omitted} chars omitted] ...
24033
24226
  ` + text.substring(text.length - half);
24034
24227
  }
24035
- var import_fs, import_path;
24228
+ function initializeSimpleTelemetryFromOptions(options) {
24229
+ const telemetry = new SimpleTelemetry({
24230
+ serviceName: "probe-agent",
24231
+ enableFile: options.traceFile !== void 0,
24232
+ enableConsole: options.traceConsole,
24233
+ filePath: options.traceFile || "./traces.jsonl"
24234
+ });
24235
+ patchConsole();
24236
+ return telemetry;
24237
+ }
24238
+ var import_fs, import_path, SimpleTelemetry, SimpleAppTracer;
24036
24239
  var init_simpleTelemetry = __esm({
24037
24240
  "src/agent/simpleTelemetry.js"() {
24038
24241
  "use strict";
24039
24242
  import_fs = require("fs");
24040
24243
  import_path = require("path");
24041
24244
  init_otelLogBridge();
24245
+ SimpleTelemetry = class {
24246
+ constructor(options = {}) {
24247
+ this.serviceName = options.serviceName || "probe-agent";
24248
+ this.enableFile = options.enableFile || false;
24249
+ this.enableConsole = options.enableConsole || false;
24250
+ this.filePath = options.filePath || "./traces.jsonl";
24251
+ this.stream = null;
24252
+ if (this.enableFile) {
24253
+ this.initializeFileExporter();
24254
+ }
24255
+ }
24256
+ initializeFileExporter() {
24257
+ try {
24258
+ const dir = (0, import_path.dirname)(this.filePath);
24259
+ if (!(0, import_fs.existsSync)(dir)) {
24260
+ (0, import_fs.mkdirSync)(dir, { recursive: true });
24261
+ }
24262
+ this.stream = (0, import_fs.createWriteStream)(this.filePath, { flags: "a" });
24263
+ this.stream.on("error", (error40) => {
24264
+ console.error(`[SimpleTelemetry] Stream error: ${error40.message}`);
24265
+ });
24266
+ console.log(`[SimpleTelemetry] File exporter initialized: ${this.filePath}`);
24267
+ } catch (error40) {
24268
+ console.error(`[SimpleTelemetry] Failed to initialize file exporter: ${error40.message}`);
24269
+ }
24270
+ }
24271
+ createSpan(name15, attributes = {}) {
24272
+ const span = {
24273
+ traceId: this.generateTraceId(),
24274
+ spanId: this.generateSpanId(),
24275
+ name: name15,
24276
+ startTime: Date.now(),
24277
+ attributes: { ...attributes, service: this.serviceName },
24278
+ events: [],
24279
+ status: "OK"
24280
+ };
24281
+ return {
24282
+ ...span,
24283
+ addEvent: (eventName, eventAttributes = {}) => {
24284
+ span.events.push({
24285
+ name: eventName,
24286
+ time: Date.now(),
24287
+ attributes: eventAttributes
24288
+ });
24289
+ },
24290
+ setAttributes: (attrs) => {
24291
+ Object.assign(span.attributes, attrs);
24292
+ },
24293
+ setStatus: (status) => {
24294
+ span.status = status;
24295
+ },
24296
+ end: () => {
24297
+ span.endTime = Date.now();
24298
+ span.duration = span.endTime - span.startTime;
24299
+ this.exportSpan(span);
24300
+ }
24301
+ };
24302
+ }
24303
+ exportSpan(span) {
24304
+ const spanData = {
24305
+ ...span,
24306
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
24307
+ };
24308
+ if (this.enableConsole) {
24309
+ console.log("[Trace]", JSON.stringify(spanData, null, 2));
24310
+ }
24311
+ if (this.enableFile && this.stream) {
24312
+ this.stream.write(JSON.stringify(spanData) + "\n");
24313
+ }
24314
+ }
24315
+ generateTraceId() {
24316
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
24317
+ }
24318
+ generateSpanId() {
24319
+ return Math.random().toString(36).substring(2, 10);
24320
+ }
24321
+ async flush() {
24322
+ if (this.stream) {
24323
+ return new Promise((resolve9) => {
24324
+ this.stream.once("drain", resolve9);
24325
+ if (!this.stream.writableNeedDrain) {
24326
+ resolve9();
24327
+ }
24328
+ });
24329
+ }
24330
+ }
24331
+ async shutdown() {
24332
+ if (this.stream) {
24333
+ return new Promise((resolve9) => {
24334
+ this.stream.end(() => {
24335
+ console.log(`[SimpleTelemetry] File stream closed: ${this.filePath}`);
24336
+ resolve9();
24337
+ });
24338
+ });
24339
+ }
24340
+ }
24341
+ };
24342
+ SimpleAppTracer = class {
24343
+ constructor(telemetry, sessionId = null) {
24344
+ this.telemetry = telemetry;
24345
+ this.sessionId = sessionId || this.generateSessionId();
24346
+ }
24347
+ generateSessionId() {
24348
+ return Math.random().toString(36).substring(2, 15);
24349
+ }
24350
+ isEnabled() {
24351
+ return this.telemetry !== null;
24352
+ }
24353
+ createSessionSpan(attributes = {}) {
24354
+ if (!this.isEnabled()) return null;
24355
+ return this.telemetry.createSpan("agent.session", {
24356
+ "session.id": this.sessionId,
24357
+ ...attributes
24358
+ });
24359
+ }
24360
+ createAISpan(modelName, provider, attributes = {}) {
24361
+ if (!this.isEnabled()) return null;
24362
+ return this.telemetry.createSpan("ai.request", {
24363
+ "ai.model": modelName,
24364
+ "ai.provider": provider,
24365
+ "session.id": this.sessionId,
24366
+ ...attributes
24367
+ });
24368
+ }
24369
+ createToolSpan(toolName, attributes = {}) {
24370
+ if (!this.isEnabled()) return null;
24371
+ return this.telemetry.createSpan("tool.call", {
24372
+ "tool.name": toolName,
24373
+ "session.id": this.sessionId,
24374
+ ...attributes
24375
+ });
24376
+ }
24377
+ addEvent(name15, attributes = {}) {
24378
+ if (this.telemetry && this.telemetry.enableConsole) {
24379
+ console.log("[Event]", name15, attributes);
24380
+ }
24381
+ }
24382
+ /**
24383
+ * Record a generic event (used by completionPrompt and other features)
24384
+ */
24385
+ // visor-disable: SimpleAppTracer uses this.sessionId because it's a per-session instance. AppTracer extracts from attributes because it's a singleton managing multiple sessions. Different architectures require different approaches.
24386
+ recordEvent(name15, attributes = {}) {
24387
+ if (!this.isEnabled()) return;
24388
+ this.addEvent(name15, {
24389
+ "session.id": this.sessionId,
24390
+ ...attributes
24391
+ });
24392
+ }
24393
+ /**
24394
+ * Record delegation events
24395
+ */
24396
+ recordDelegationEvent(eventType, data2 = {}) {
24397
+ if (!this.isEnabled()) return;
24398
+ this.addEvent(`delegation.${eventType}`, {
24399
+ "session.id": this.sessionId,
24400
+ ...data2
24401
+ });
24402
+ }
24403
+ /**
24404
+ * Record JSON validation events
24405
+ */
24406
+ recordJsonValidationEvent(eventType, data2 = {}) {
24407
+ if (!this.isEnabled()) return;
24408
+ this.addEvent(`json_validation.${eventType}`, {
24409
+ "session.id": this.sessionId,
24410
+ ...data2
24411
+ });
24412
+ }
24413
+ /**
24414
+ * Record Mermaid validation events
24415
+ */
24416
+ recordMermaidValidationEvent(eventType, data2 = {}) {
24417
+ if (!this.isEnabled()) return;
24418
+ this.addEvent(`mermaid_validation.${eventType}`, {
24419
+ "session.id": this.sessionId,
24420
+ ...data2
24421
+ });
24422
+ }
24423
+ /**
24424
+ * Record task management events
24425
+ */
24426
+ recordTaskEvent(eventType, data2 = {}) {
24427
+ if (!this.isEnabled()) return;
24428
+ this.addEvent(`task.${eventType}`, {
24429
+ "session.id": this.sessionId,
24430
+ ...data2
24431
+ });
24432
+ }
24433
+ /**
24434
+ * Record MCP (Model Context Protocol) events
24435
+ * Tracks server connections, tool discovery, method filtering, and tool execution
24436
+ */
24437
+ recordMcpEvent(eventType, data2 = {}) {
24438
+ if (!this.isEnabled()) return;
24439
+ this.addEvent(`mcp.${eventType}`, {
24440
+ "session.id": this.sessionId,
24441
+ ...data2
24442
+ });
24443
+ }
24444
+ /**
24445
+ * Record bash tool events
24446
+ * Tracks command permission checks, allowed/denied commands, and execution
24447
+ */
24448
+ recordBashEvent(eventType, data2 = {}) {
24449
+ if (!this.isEnabled()) return;
24450
+ this.addEvent(`bash.${eventType}`, {
24451
+ "session.id": this.sessionId,
24452
+ ...data2
24453
+ });
24454
+ }
24455
+ setAttributes(attributes) {
24456
+ if (this.telemetry && this.telemetry.enableConsole) {
24457
+ console.log("[Attributes]", attributes);
24458
+ }
24459
+ }
24460
+ /**
24461
+ * Hash content for deduplication/comparison purposes
24462
+ * @param {string} content - The content to hash
24463
+ * @returns {string} - Hex string hash
24464
+ */
24465
+ hashContent(content) {
24466
+ let hash2 = 0;
24467
+ const len = Math.min(content.length, 1e3);
24468
+ for (let i = 0; i < len; i++) {
24469
+ hash2 = (hash2 << 5) - hash2 + content.charCodeAt(i);
24470
+ hash2 |= 0;
24471
+ }
24472
+ return hash2.toString(16);
24473
+ }
24474
+ /**
24475
+ * Record a conversation turn (assistant response or tool result)
24476
+ * @param {string} role - The role (assistant, tool_result)
24477
+ * @param {string} content - The turn content
24478
+ * @param {Object} metadata - Additional metadata
24479
+ */
24480
+ recordConversationTurn(role, content, metadata = {}) {
24481
+ if (!this.isEnabled()) return;
24482
+ this.addEvent(`conversation.turn.${role}`, {
24483
+ "session.id": this.sessionId,
24484
+ "conversation.role": role,
24485
+ "conversation.content": content.substring(0, 1e4),
24486
+ "conversation.content.length": content.length,
24487
+ "conversation.content.hash": this.hashContent(content),
24488
+ ...metadata
24489
+ });
24490
+ }
24491
+ /**
24492
+ * Record error events with classification
24493
+ * @param {string} errorType - The type of error (wrapped_tool, unrecognized_tool, no_tool_call, circuit_breaker, etc.)
24494
+ * @param {Object} errorDetails - Error details including message, stack, context
24495
+ */
24496
+ recordErrorEvent(errorType, errorDetails = {}) {
24497
+ if (!this.isEnabled()) return;
24498
+ this.addEvent(`error.${errorType}`, {
24499
+ "session.id": this.sessionId,
24500
+ "error.type": errorType,
24501
+ "error.message": errorDetails.message?.substring(0, 1e3) || null,
24502
+ "error.stack": errorDetails.stack?.substring(0, 2e3) || null,
24503
+ "error.recoverable": errorDetails.recoverable ?? true,
24504
+ "error.context": JSON.stringify(errorDetails.context || {}).substring(0, 1e3),
24505
+ ...Object.fromEntries(
24506
+ Object.entries(errorDetails).filter(([k]) => !["message", "stack", "context", "recoverable"].includes(k)).map(([k, v]) => [`error.${k}`, v])
24507
+ )
24508
+ });
24509
+ }
24510
+ /**
24511
+ * Record AI thinking/reasoning content
24512
+ * @param {string} thinkingContent - The thinking content from AI response
24513
+ * @param {Object} metadata - Additional metadata
24514
+ */
24515
+ recordThinkingContent(thinkingContent, metadata = {}) {
24516
+ if (!this.isEnabled() || !thinkingContent) return;
24517
+ this.addEvent("ai.thinking", {
24518
+ "session.id": this.sessionId,
24519
+ "ai.thinking.content": thinkingContent.substring(0, 5e4),
24520
+ "ai.thinking.length": thinkingContent.length,
24521
+ "ai.thinking.hash": this.hashContent(thinkingContent),
24522
+ ...metadata
24523
+ });
24524
+ }
24525
+ /**
24526
+ * Record AI tool call decision
24527
+ * @param {string} toolName - The tool name AI decided to call
24528
+ * @param {Object} params - The parameters AI provided
24529
+ * @param {Object} metadata - Additional metadata
24530
+ */
24531
+ recordToolDecision(toolName, params, metadata = {}) {
24532
+ if (!this.isEnabled()) return;
24533
+ this.addEvent("ai.tool_decision", {
24534
+ "session.id": this.sessionId,
24535
+ "ai.tool_decision.name": toolName,
24536
+ "ai.tool_decision.params": JSON.stringify(params || {}).substring(0, 2e3),
24537
+ ...metadata
24538
+ });
24539
+ }
24540
+ /**
24541
+ * Record tool result after execution
24542
+ * @param {string} toolName - The tool that was executed
24543
+ * @param {string|Object} result - The tool result
24544
+ * @param {boolean} success - Whether the tool succeeded
24545
+ * @param {number} durationMs - Execution duration in milliseconds
24546
+ * @param {Object} metadata - Additional metadata
24547
+ */
24548
+ recordToolResult(toolName, result, success2, durationMs, metadata = {}) {
24549
+ if (!this.isEnabled()) return;
24550
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
24551
+ this.addEvent("tool.result", {
24552
+ "session.id": this.sessionId,
24553
+ "tool.name": toolName,
24554
+ "tool.result": resultStr.substring(0, 1e4),
24555
+ "tool.result.length": resultStr.length,
24556
+ "tool.result.hash": this.hashContent(resultStr),
24557
+ "tool.duration_ms": durationMs,
24558
+ "tool.success": success2,
24559
+ ...metadata
24560
+ });
24561
+ }
24562
+ /**
24563
+ * Record MCP tool execution start
24564
+ * @param {string} toolName - MCP tool name
24565
+ * @param {string} serverName - MCP server name
24566
+ * @param {Object} params - Tool parameters
24567
+ * @param {Object} metadata - Additional metadata
24568
+ */
24569
+ recordMcpToolStart(toolName, serverName, params, metadata = {}) {
24570
+ if (!this.isEnabled()) return;
24571
+ this.addEvent("mcp.tool.start", {
24572
+ "session.id": this.sessionId,
24573
+ "mcp.tool.name": toolName,
24574
+ "mcp.tool.server": serverName || "unknown",
24575
+ "mcp.tool.params": JSON.stringify(params || {}).substring(0, 2e3),
24576
+ ...metadata
24577
+ });
24578
+ }
24579
+ /**
24580
+ * Record MCP tool execution end
24581
+ * @param {string} toolName - MCP tool name
24582
+ * @param {string} serverName - MCP server name
24583
+ * @param {string|Object} result - Tool result
24584
+ * @param {boolean} success - Whether succeeded
24585
+ * @param {number} durationMs - Execution duration
24586
+ * @param {string} errorMessage - Error message if failed
24587
+ * @param {Object} metadata - Additional metadata
24588
+ */
24589
+ recordMcpToolEnd(toolName, serverName, result, success2, durationMs, errorMessage = null, metadata = {}) {
24590
+ if (!this.isEnabled()) return;
24591
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result || "");
24592
+ this.addEvent("mcp.tool.end", {
24593
+ "session.id": this.sessionId,
24594
+ "mcp.tool.name": toolName,
24595
+ "mcp.tool.server": serverName || "unknown",
24596
+ "mcp.tool.result": resultStr.substring(0, 1e4),
24597
+ "mcp.tool.result.length": resultStr.length,
24598
+ "mcp.tool.duration_ms": durationMs,
24599
+ "mcp.tool.success": success2,
24600
+ "mcp.tool.error": errorMessage,
24601
+ ...metadata
24602
+ });
24603
+ }
24604
+ /**
24605
+ * Record iteration lifecycle event
24606
+ * @param {string} eventType - start or end
24607
+ * @param {number} iteration - Iteration number
24608
+ * @param {Object} data - Additional data
24609
+ */
24610
+ recordIterationEvent(eventType, iteration, data2 = {}) {
24611
+ if (!this.isEnabled()) return;
24612
+ this.addEvent(`iteration.${eventType}`, {
24613
+ "session.id": this.sessionId,
24614
+ "iteration": iteration,
24615
+ ...data2
24616
+ });
24617
+ }
24618
+ /**
24619
+ * Record per-turn token breakdown
24620
+ * @param {number} iteration - Iteration number
24621
+ * @param {Object} tokenData - Token metrics
24622
+ */
24623
+ recordTokenTurn(iteration, tokenData = {}) {
24624
+ if (!this.isEnabled()) return;
24625
+ this.addEvent("tokens.turn", {
24626
+ "session.id": this.sessionId,
24627
+ "iteration": iteration,
24628
+ "tokens.input": tokenData.inputTokens || 0,
24629
+ "tokens.output": tokenData.outputTokens || 0,
24630
+ "tokens.total": (tokenData.inputTokens || 0) + (tokenData.outputTokens || 0),
24631
+ "tokens.cache_read": tokenData.cacheReadTokens || 0,
24632
+ "tokens.cache_write": tokenData.cacheWriteTokens || 0,
24633
+ "tokens.context_used": tokenData.contextTokens || 0,
24634
+ "tokens.context_remaining": tokenData.maxContextTokens ? tokenData.maxContextTokens - (tokenData.contextTokens || 0) : null
24635
+ });
24636
+ }
24637
+ async withSpan(spanName, fn, attributes = {}, onResult = null) {
24638
+ if (!this.isEnabled()) {
24639
+ return fn();
24640
+ }
24641
+ const span = this.telemetry.createSpan(spanName, {
24642
+ "session.id": this.sessionId,
24643
+ ...attributes
24644
+ });
24645
+ try {
24646
+ const result = await fn();
24647
+ span.setStatus("OK");
24648
+ if (onResult) {
24649
+ try {
24650
+ onResult(span, result);
24651
+ } catch (_) {
24652
+ }
24653
+ }
24654
+ return result;
24655
+ } catch (error40) {
24656
+ span.setStatus("ERROR");
24657
+ span.addEvent("exception", {
24658
+ "exception.message": error40.message,
24659
+ "exception.stack": error40.stack
24660
+ });
24661
+ throw error40;
24662
+ } finally {
24663
+ span.end();
24664
+ }
24665
+ }
24666
+ async flush() {
24667
+ if (this.telemetry) {
24668
+ await this.telemetry.flush();
24669
+ }
24670
+ }
24671
+ async shutdown() {
24672
+ if (this.telemetry) {
24673
+ await this.telemetry.shutdown();
24674
+ }
24675
+ }
24676
+ };
24042
24677
  }
24043
24678
  });
24044
24679
 
@@ -26267,6 +26902,7 @@ async function delegate({
26267
26902
  });
26268
26903
  let parentAbortHandler;
26269
26904
  let parentAbortHardCancelId = null;
26905
+ let raceSettled = false;
26270
26906
  const parentAbortPromise = new Promise((_, reject2) => {
26271
26907
  if (parentAbortSignal) {
26272
26908
  if (parentAbortSignal.aborted) {
@@ -26275,6 +26911,7 @@ async function delegate({
26275
26911
  return;
26276
26912
  }
26277
26913
  parentAbortHandler = () => {
26914
+ if (raceSettled) return;
26278
26915
  subagent.triggerGracefulWindDown();
26279
26916
  if (debug) {
26280
26917
  console.error(`[DELEGATE] Parent abort signal received \u2014 triggered graceful wind-down on subagent ${sessionId}`);
@@ -26287,6 +26924,7 @@ async function delegate({
26287
26924
  });
26288
26925
  }
26289
26926
  parentAbortHardCancelId = setTimeout(() => {
26927
+ if (raceSettled) return;
26290
26928
  if (debug) {
26291
26929
  console.error(`[DELEGATE] Graceful wind-down deadline expired \u2014 hard cancelling subagent ${sessionId}`);
26292
26930
  }
@@ -26312,6 +26950,7 @@ async function delegate({
26312
26950
  try {
26313
26951
  response = await Promise.race(racers);
26314
26952
  } finally {
26953
+ raceSettled = true;
26315
26954
  if (parentAbortHandler && parentAbortSignal) {
26316
26955
  parentAbortSignal.removeEventListener("abort", parentAbortHandler);
26317
26956
  }
@@ -26352,10 +26991,12 @@ async function delegate({
26352
26991
  "delegation.success": true
26353
26992
  });
26354
26993
  if (delegationSpan) {
26994
+ const { truncateForSpan: truncateForSpan2 } = await Promise.resolve().then(() => (init_simpleTelemetry(), simpleTelemetry_exports));
26355
26995
  delegationSpan.setAttributes({
26356
26996
  "delegation.result.success": true,
26357
26997
  "delegation.result.response_length": response.length,
26358
- "delegation.result.duration_ms": duration3
26998
+ "delegation.result.duration_ms": duration3,
26999
+ "delegation.result": truncateForSpan2(response, 4096)
26359
27000
  });
26360
27001
  delegationSpan.setStatus({ code: 1 });
26361
27002
  delegationSpan.end();
@@ -26408,9 +27049,13 @@ var init_delegate = __esm({
26408
27049
  init_ProbeAgent();
26409
27050
  DelegationManager = class {
26410
27051
  constructor(options = {}) {
26411
- this.maxConcurrent = options.maxConcurrent ?? parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || "3", 10);
26412
- this.maxPerSession = options.maxPerSession ?? parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || "10", 10);
26413
- this.defaultQueueTimeout = options.queueTimeout ?? parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || "60000", 10);
27052
+ const parseSafe = (val, fallback) => {
27053
+ const n = parseInt(val, 10);
27054
+ return Number.isNaN(n) ? fallback : n;
27055
+ };
27056
+ this.maxConcurrent = options.maxConcurrent ?? parseSafe(process.env.MAX_CONCURRENT_DELEGATIONS, 3);
27057
+ this.maxPerSession = options.maxPerSession ?? parseSafe(process.env.MAX_DELEGATIONS_PER_SESSION, 10);
27058
+ this.defaultQueueTimeout = options.queueTimeout ?? parseSafe(process.env.DELEGATION_QUEUE_TIMEOUT, 6e4);
26414
27059
  this.sessionDelegations = /* @__PURE__ */ new Map();
26415
27060
  this.globalActive = 0;
26416
27061
  this.waitQueue = [];
@@ -27248,12 +27893,16 @@ function resolveTargetPath(target, cwd) {
27248
27893
  }
27249
27894
  return filePart + suffix;
27250
27895
  }
27251
- var import_path6, searchSchema, searchAllSchema, querySchema, extractSchema, delegateSchema, listSkillsSchema, useSkillSchema, listFilesSchema, searchFilesSchema, readImageSchema, bashSchema, analyzeAllSchema, executePlanSchema, cleanupExecutePlanSchema, searchDescription, searchDelegateDescription, queryDescription, extractDescription, delegateDescription, analyzeAllDescription;
27896
+ var import_path6, searchDelegateSchema, searchSchema, searchAllSchema, querySchema, extractSchema, delegateSchema, listSkillsSchema, useSkillSchema, listFilesSchema, searchFilesSchema, readImageSchema, bashSchema, analyzeAllSchema, executePlanSchema, cleanupExecutePlanSchema, searchDescription, searchDelegateDescription, queryDescription, extractDescription, delegateDescription, analyzeAllDescription;
27252
27897
  var init_common = __esm({
27253
27898
  "src/tools/common.js"() {
27254
27899
  "use strict";
27255
27900
  init_zod();
27256
27901
  import_path6 = require("path");
27902
+ searchDelegateSchema = external_exports2.object({
27903
+ query: external_exports2.string().describe('Natural language question about the code (e.g., "How does authentication work?", "Where is the rate limiting middleware?"). Do NOT use keyword syntax \u2014 just describe what you are looking for in plain English. A subagent will handle keyword searches for you.'),
27904
+ path: external_exports2.string().optional().default(".").describe("Path to search in.")
27905
+ });
27257
27906
  searchSchema = external_exports2.object({
27258
27907
  query: external_exports2.string().describe("Search query \u2014 natural language questions or Elasticsearch-style keywords both work. For keywords: use quotes for exact phrases, AND/OR for boolean logic, - for negation. Probe handles stemming and camelCase/snake_case splitting automatically, so do NOT try case or style variations of the same keyword."),
27259
27908
  path: external_exports2.string().optional().default(".").describe('Path to search in. For dependencies use "go:github.com/owner/repo", "js:package_name", or "rust:cargo_name" etc.'),
@@ -27319,7 +27968,17 @@ var init_common = __esm({
27319
27968
  clearSessionStore: external_exports2.boolean().optional().default(false).describe("Clear the session store (persisted data across execute_plan calls)")
27320
27969
  });
27321
27970
  searchDescription = 'Search code in the repository. Free-form questions are accepted, but Elasticsearch-style keyword queries work best. Use this tool first for any code-related questions. NOTE: By default, search handles stemming, case-insensitive matching, and camelCase/snake_case splitting automatically \u2014 do NOT manually try keyword variations like "getAllUsers" then "get_all_users" then "GetAllUsers". One search covers all variations.';
27322
- searchDelegateDescription = 'Search code in the repository by asking a question. Accepts natural language questions (e.g., "How does authentication work?", "Where is the user validation logic?"). A specialized subagent breaks down your question into targeted keyword searches and returns extracted code blocks. Do NOT formulate keyword queries yourself \u2014 just ask the question naturally.';
27971
+ searchDelegateDescription = `Find where relevant code is located by asking a natural language question. A subagent searches the codebase and returns file locations grouped by relevance, with reasons explaining why each group matters. Use extract() to read the actual code from the returned locations.
27972
+
27973
+ Returns JSON: { "confidence": "high|medium|low", "groups": [{ "reason": "why these files matter", "files": ["path#Symbol", ...] }] }
27974
+
27975
+ IMPORTANT \u2014 each call spawns a subagent (expensive, takes minutes). Be deliberate:
27976
+ - Ask plain English questions about WHERE code is, NOT keyword queries. Good: "How are user sessions extracted from cookies?" Bad: "ctxGetSession OR GetSession"
27977
+ - Each call should explore a DIFFERENT ANGLE of the problem. Don't rephrase \u2014 reframe:
27978
+ Good: 1) "How are sessions extracted from HTTP requests?" 2) "What middleware runs before route handlers?" 3) "How is the session cookie parsed and validated?"
27979
+ Bad: 1) "How does session extraction work?" 2) "Where is the session extracted?" 3) "Find session extraction code" \u2190 same question reworded
27980
+ - If a search returned no useful results, ask about a DIFFERENT part of the system. Think: what upstream/downstream component touches this?
27981
+ - After getting results, use extract() to read the files you need \u2014 search only locates, extract reads.`;
27323
27982
  queryDescription = "Search code using ast-grep structural pattern matching. Use this tool to find specific code structures like functions, classes, or methods.";
27324
27983
  extractDescription = "Extract code blocks from files based on file paths and optional line numbers. Use this tool to see complete context after finding relevant files. Line numbers from output can be used with edit start_line/end_line for precise editing.";
27325
27984
  delegateDescription = "Automatically delegate big distinct tasks to specialized probe subagents within the agentic loop. Used by AI agents to break down complex requests into focused, parallel tasks.";
@@ -27443,6 +28102,72 @@ function autoQuoteSearchTerms(query2) {
27443
28102
  });
27444
28103
  return result.join(" ");
27445
28104
  }
28105
+ async function checkDelegateDedup(newQuery, previousQueries, model, debug) {
28106
+ if (!model || previousQueries.length === 0) {
28107
+ return { action: "allow", reason: "no previous queries" };
28108
+ }
28109
+ const previousList = previousQueries.map((q, i) => {
28110
+ let line = `${i + 1}. "${q.query}" (path: ${q.path}, found results: ${q.hadResults})`;
28111
+ if (q.reason) line += `
28112
+ Outcome: ${q.reason}`;
28113
+ if (q.groups && q.groups.length > 0) {
28114
+ line += `
28115
+ Found: ${q.groups.map((g) => g.reason).join("; ")}`;
28116
+ }
28117
+ return line;
28118
+ }).join("\n");
28119
+ try {
28120
+ const result = await (0, import_ai.generateText)({
28121
+ model,
28122
+ maxTokens: 150,
28123
+ temperature: 0,
28124
+ prompt: `You decide if a code search query is redundant given previous queries in the same session.
28125
+
28126
+ PREVIOUS QUERIES:
28127
+ ${previousList}
28128
+
28129
+ NEW QUERY: "${newQuery}"
28130
+
28131
+ Respond with exactly one line: ACTION|REASON
28132
+ For rewrites: rewrite|REASON|REWRITTEN_QUERY
28133
+
28134
+ BLOCK when:
28135
+ - Same concept, different phrasing: "find X" / "definition of X" / "where is X" / "X implementation" \u2192 all the same
28136
+ - Synonym or narrower term of a previous query: "dedup" \u2192 "duplicate" \u2192 "unique" \u2192 all the same concept
28137
+ - Single generic word that's just a synonym of a previous failed query
28138
+ - Query is trying to brute-force the same concept with different keywords after previous failures
28139
+
28140
+ REWRITE when:
28141
+ - Previous query was too narrow and failed, new query targets the same goal but could use a FUNDAMENTALLY different search strategy (e.g. searching for a caller instead of the function name, or searching the config/registration site instead of the implementation)
28142
+ - Previous query found WRONG results (e.g. found "FallbackManager" when looking for "dedup logic") \u2014 rewrite to target the actual concept more precisely using implementation-level terms
28143
+
28144
+ ALLOW only when:
28145
+ - The new query targets a COMPLETELY DIFFERENT feature, module, or subsystem \u2014 not just a different word for the same thing
28146
+
28147
+ Only BLOCK when you are CERTAIN the queries target the same concept. When uncertain, ALLOW \u2014 a missed dedup is cheaper than blocking a valid search.
28148
+
28149
+ Examples:
28150
+ - Prev: "wrapToolWithEmitter" \u2192 New: "definition of wrapToolWithEmitter" \u2192 block|Same symbol
28151
+ - Prev: "search dedup" (no results) \u2192 New: "dedup" \u2192 block|Synonym of failed query
28152
+ - Prev: "dedup" (no results) \u2192 New: "duplicate" \u2192 block|Synonym of failed query
28153
+ - Prev: "dedup" (no results) \u2192 New: "unique" \u2192 block|Synonym of failed query
28154
+ - Prev: "auth middleware" \u2192 New: "rate limiting" \u2192 allow|Different subsystem
28155
+ - Prev: "search dedup" (no results) \u2192 New: "previousSearches Map" \u2192 rewrite|Searching for implementation detail instead of concept|previousSearches OR searchKey`
28156
+ });
28157
+ const line = result.text.trim().split("\n")[0];
28158
+ const parts = line.split("|");
28159
+ const action = (parts[0] || "").toLowerCase().trim();
28160
+ if (action === "block") {
28161
+ return { action: "block", reason: parts[1]?.trim() || "duplicate query" };
28162
+ } else if (action === "rewrite" && parts[2]) {
28163
+ return { action: "rewrite", reason: parts[1]?.trim() || "refined query", rewritten: parts[2].trim() };
28164
+ }
28165
+ return { action: "allow", reason: parts[1]?.trim() || "new concept" };
28166
+ } catch (err) {
28167
+ if (debug) console.error("[DEDUP-LLM] Error:", err.message);
28168
+ return { action: "allow", reason: "dedup check failed, allowing" };
28169
+ }
28170
+ }
27446
28171
  function normalizeTargets(targets) {
27447
28172
  if (!Array.isArray(targets)) return [];
27448
28173
  const seen = /* @__PURE__ */ new Set();
@@ -27505,8 +28230,8 @@ function fallbackTargetsFromText(text) {
27505
28230
  }
27506
28231
  return candidates;
27507
28232
  }
27508
- function parseDelegatedTargets(rawResponse) {
27509
- if (!rawResponse || typeof rawResponse !== "string") return [];
28233
+ function parseDelegatedResponse(rawResponse) {
28234
+ if (!rawResponse || typeof rawResponse !== "string") return null;
27510
28235
  const trimmed = rawResponse.trim();
27511
28236
  const tryParse = (text) => {
27512
28237
  try {
@@ -27523,14 +28248,37 @@ function parseDelegatedTargets(rawResponse) {
27523
28248
  }
27524
28249
  }
27525
28250
  if (parsed) {
27526
- if (Array.isArray(parsed)) {
27527
- return normalizeTargets(parsed);
28251
+ if (Array.isArray(parsed.groups)) {
28252
+ return {
28253
+ confidence: parsed.confidence || "medium",
28254
+ reason: parsed.reason || "",
28255
+ groups: parsed.groups.map((g) => ({
28256
+ reason: g.reason || "",
28257
+ files: normalizeTargets(g.files || [])
28258
+ })).filter((g) => g.files.length > 0),
28259
+ searches: Array.isArray(parsed.searches) ? parsed.searches : []
28260
+ };
27528
28261
  }
27529
28262
  if (Array.isArray(parsed.targets)) {
27530
- return normalizeTargets(parsed.targets);
28263
+ const files2 = normalizeTargets(parsed.targets);
28264
+ if (files2.length > 0) {
28265
+ return { confidence: "medium", reason: "", groups: [{ reason: "Search results", files: files2 }], searches: [] };
28266
+ }
28267
+ return null;
27531
28268
  }
28269
+ if (Array.isArray(parsed)) {
28270
+ const files2 = normalizeTargets(parsed);
28271
+ if (files2.length > 0) {
28272
+ return { confidence: "medium", reason: "", groups: [{ reason: "Search results", files: files2 }], searches: [] };
28273
+ }
28274
+ return null;
28275
+ }
28276
+ }
28277
+ const files = normalizeTargets(fallbackTargetsFromText(trimmed));
28278
+ if (files.length > 0) {
28279
+ return { confidence: "low", reason: "", groups: [{ reason: "Search results", files }], searches: [] };
27532
28280
  }
27533
- return normalizeTargets(fallbackTargetsFromText(trimmed));
28281
+ return null;
27534
28282
  }
27535
28283
  function splitTargetSuffix(target) {
27536
28284
  const searchStart = target.length > 2 && target[1] === ":" && /[a-zA-Z]/.test(target[0]) ? 2 : 0;
@@ -27544,129 +28292,78 @@ function splitTargetSuffix(target) {
27544
28292
  return { filePart: target, suffix: "" };
27545
28293
  }
27546
28294
  function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, allowTests }) {
27547
- return [
27548
- "You are a code-search subagent. Your job is to find ALL relevant code locations for the given query.",
27549
- "",
27550
- "The query may be complex - it could be a natural language question, a multi-part request, or a simple keyword.",
27551
- "Break down complex queries into multiple searches to cover all aspects.",
27552
- "",
27553
- "Available tools:",
27554
- "- search: Find code matching keywords or patterns. Results are paginated \u2014 use nextPage=true when results are relevant to get more. Run multiple searches for different aspects.",
27555
- "- extract: Verify code snippets to ensure targets are actually relevant before including them.",
27556
- "- listFiles: Understand directory structure to find where relevant code might live.",
27557
- "",
27558
- "CRITICAL - How probe search works (do NOT ignore):",
27559
- "- By default (exact=false), probe ALREADY handles stemming, case-insensitive matching, and camelCase/snake_case splitting automatically.",
27560
- '- Searching "allowed_ips" ALREADY matches "AllowedIPs", "allowedIps", "allowed_ips", etc. Do NOT manually try case/style variations.',
27561
- '- Searching "getUserData" ALREADY matches "get", "user", "data" and their variations.',
27562
- "- NEVER repeat the same search query \u2014 you will get the same results. Changing the path does NOT change this.",
27563
- "- NEVER search trivial variations of the same keyword (e.g., AllowedIPs then allowedIps then allowed_ips). This is wasteful \u2014 probe handles it.",
27564
- "- If a search returns no results, the term likely does not exist. Try a genuinely DIFFERENT keyword or concept, not a variation.",
27565
- "- If 2-3 searches return no results for a concept, STOP searching for it and move on. Do NOT keep retrying.",
27566
- "",
27567
- "When to use exact=true:",
27568
- "- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).",
27569
- "- exact=true matches the literal string only \u2014 no stemming, no splitting.",
27570
- '- This is ideal for precise lookups: exact=true "ForwardMessage", exact=true "SessionLimiter", exact=true "ThrottleRetryLimit".',
27571
- "- IMPORTANT: Use exact=true when searching for strings containing punctuation, quotes, or empty values.",
27572
- " Default BM25 search strips punctuation and treats quoted empty strings as noise.",
27573
- ` Example: searching for 'description: ""' with exact=false will NOT find empty description fields \u2014 it just matches "description".`,
27574
- ` Use exact=true for literal patterns like 'description: ""', 'value: \\'\\'', or any YAML/config field with specific punctuation.`,
27575
- "- Do NOT use exact=true for exploratory/conceptual queries \u2014 use the default for those.",
27576
- "",
27577
- "Combining searches with OR:",
27578
- '- Multiple unquoted words use OR logic: rate limit matches files containing EITHER "rate" OR "limit".',
27579
- `- IMPORTANT: Multiple quoted terms use AND logic by default: '"RateLimit" "middleware"' requires BOTH in the same file.`,
27580
- `- To search for ANY of several quoted symbols, use the explicit OR operator: '"ForwardMessage" OR "SessionLimiter"'.`,
27581
- '- Without quotes, camelCase like limitDRL gets split into "limit" + "DRL" \u2014 not what you want for symbol lookup.',
27582
- "- Use OR to search for multiple related symbols in ONE search instead of separate searches.",
27583
- "- This is much faster than running separate searches sequentially.",
27584
- `- Example: search '"ForwardMessage" OR "SessionLimiter"' finds files with either exact symbol in one call.`,
27585
- `- Example: search '"limitDRL" OR "doRollingWindowWrite"' finds both rate limiting functions at once.`,
27586
- "- Use AND (or just put quoted terms together) when you need both terms in the same file.",
27587
- "",
27588
- "Parallel tool calls:",
27589
- "- When you need to search for INDEPENDENT concepts, call multiple search tools IN PARALLEL (same response).",
27590
- "- Do NOT wait for one search to finish before starting the next if they are independent.",
27591
- '- Example: for "rate limiting and session management", call search "rate limiting" AND search "session management" in parallel.',
27592
- "- Similarly, call multiple extract tools in parallel when verifying different files.",
27593
- "",
27594
- "GOOD search strategy (do this):",
27595
- ' Query: "How does authentication work and how are sessions managed?"',
27596
- ' \u2192 search "authentication" + search "session management" IN PARALLEL (two independent concepts)',
27597
- ' Query: "Find the IP allowlist middleware"',
27598
- ' \u2192 search "allowlist middleware" (one search, probe handles IP/ip/Ip variations)',
27599
- ' Query: "Find ForwardMessage and SessionLimiter"',
27600
- ` \u2192 search '"ForwardMessage" OR "SessionLimiter"' (one OR search finds both exact symbols)`,
27601
- ' OR: search exact=true "ForwardMessage" + search exact=true "SessionLimiter" IN PARALLEL',
27602
- ' Query: "Find limitDRL and limitRedis functions"',
27603
- ` \u2192 search '"limitDRL" OR "limitRedis"' (one OR search, quoted to prevent camelCase splitting)`,
27604
- ' Query: "Find ThrottleRetryLimit usage"',
27605
- ' \u2192 search exact=true "ThrottleRetryLimit" (one search, if no results the symbol does not exist \u2014 stop)',
27606
- ' Query: "How does BM25 scoring work with SIMD optimization?"',
27607
- ' \u2192 search "BM25 scoring" + search "SIMD optimization" IN PARALLEL (two different concepts)',
27608
- "",
27609
- "BAD search strategy (never do this):",
27610
- ' \u2192 search "AllowedIPs" \u2192 search "allowedIps" \u2192 search "allowed_ips" (WRONG: case/style variations, probe handles them)',
27611
- ` \u2192 search "limitDRL" \u2192 search "LimitDRL" (WRONG: case variation \u2014 combine with OR: '"limitDRL" OR "limitRedis"')`,
27612
- ' \u2192 search "throttle_retry_limit" after searching "ThrottleRetryLimit" (WRONG: snake_case variation, probe handles it)',
27613
- ' \u2192 search "ThrottleRetryLimit" path=tyk \u2192 search "ThrottleRetryLimit" path=gateway \u2192 search "ThrottleRetryLimit" path=apidef (WRONG: same query on different paths \u2014 probe searches recursively)',
27614
- ' \u2192 search "func (k *RateLimitAndQuotaCheck) handleRateLimitFailure" (WRONG: do not search full function signatures, just use exact=true "handleRateLimitFailure")',
27615
- ' \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" (WRONG: repeating the exact same query)',
27616
- ' \u2192 search "authentication" \u2192 wait \u2192 search "session management" \u2192 wait (WRONG: these are independent, run them in parallel)',
27617
- "",
27618
- "Keyword tips:",
27619
- "- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.",
27620
- '- Avoid searching for these alone \u2014 combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
27621
- '- To bypass stopword filtering: wrap terms in quotes ("return", "struct") or set exact=true. Both disable stemming and splitting too.',
27622
- '- camelCase terms are split: getUserData becomes "get", "user", "data" \u2014 so one search covers all naming styles.',
27623
- '- Do NOT search for full function signatures like "func (r *Type) Method(args)". Just search for the method name with exact=true.',
27624
- '- Do NOT search for file names (e.g., "sliding_log.go"). Use listFiles to discover files by name.',
27625
- "",
27626
- "PAGINATION:",
27627
- "- Search results are paginated (~20k tokens per page).",
27628
- "- If your search returned relevant files, call the same query with nextPage=true to check for more.",
27629
- '- Keep paginating while results stay relevant. Stop when results are off-topic or "All results retrieved".',
27630
- "",
27631
- "WHEN TO STOP:",
27632
- "- After you have explored the main concept AND related subsystems.",
27633
- "- Once you have 5-15 targets covering different aspects of the query.",
27634
- '- If you get a "DUPLICATE SEARCH BLOCKED" message, do NOT rephrase the same query \u2014 try a FUNDAMENTALLY different approach:',
27635
- " * Switch between exact=true and exact=false",
27636
- " * Search for a broader term and filter results manually",
27637
- " * Use listFiles to browse the directory structure directly",
27638
- " * Look for related/surrounding patterns instead of the exact string",
27639
- "- If 2-3 genuinely different search approaches fail, STOP and report what you tried and why it failed.",
27640
- " Do NOT keep trying variations of the same failing concept.",
27641
- "",
27642
- "Strategy:",
27643
- "1. Analyze the query \u2014 identify key concepts, then brainstorm SYNONYMS and alternative terms for each.",
27644
- ' Code naming often differs from the concept: "authentication" \u2192 verify, credentials, login, auth;',
27645
- ' "rate limiting" \u2192 throttle, quota, limiter, bucket; "error handling" \u2192 catch, recover, panic.',
27646
- " Think about what a developer would NAME the function/struct/variable, not just the concept.",
27647
- "2. Run INDEPENDENT searches in PARALLEL \u2014 search for the main concept AND synonyms simultaneously.",
27648
- " After each search, check if results are relevant. If yes, call nextPage=true for more results.",
27649
- `3. Combine related symbols into OR searches: '"symbolA" OR "symbolB"' finds files with either.`,
27650
- "4. For known symbol names use exact=true. For concepts use default (exact=false).",
27651
- "5. After your first round of searches, READ the extracted code and look for connected code:",
27652
- " - Function calls to other important functions \u2192 include those targets.",
27653
- " - Type references and imports \u2192 include type definitions.",
27654
- " - Registered handlers/middleware \u2192 include all registered items.",
27655
- "6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.",
27656
- "7. If a search returns NO results, the term does not exist. Do NOT retry with variations. Move on.",
27657
- "8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.",
27658
- "",
27659
- `Query: ${searchQuery}`,
27660
- `Search path(s): ${searchPath}`,
27661
- `Options: exact=${exact ? "true" : "false"}, language=${language || "auto"}, allow_tests=${allowTests ? "true" : "false"}.`,
27662
- "",
27663
- 'Return ONLY valid JSON: {"targets": ["path/to/file.ext#Symbol", "path/to/file.ext:line", "path/to/file.ext:start-end"]}',
27664
- 'IMPORTANT: Use ABSOLUTE file paths in targets (e.g., "/full/path/to/file.ext#Symbol"). If you only have relative paths, make them relative to the search path above.',
27665
- "Prefer #Symbol when a function/class name is clear; otherwise use line numbers.",
27666
- "Deduplicate targets. Do NOT explain or answer - ONLY return the JSON targets.",
27667
- "",
27668
- "Remember: if your search returned relevant results, use nextPage=true to check for more before outputting."
27669
- ].join("\n");
28295
+ return `<role>
28296
+ You are a code-location subagent. Your job is to find WHERE relevant code lives for the given question.
28297
+ You are NOT answering the question \u2014 you are finding the code locations that would help answer it.
28298
+ </role>
28299
+
28300
+ <task>
28301
+ <question>${searchQuery}</question>
28302
+ <search-path>${searchPath}</search-path>
28303
+ <options language="${language || "auto"}" allow_tests="${allowTests ? "true" : "false"}" />
28304
+ </task>
28305
+
28306
+ <tools>
28307
+ <tool name="search">
28308
+ Find code matching keywords or patterns. Results are paginated \u2014 use nextPage=true when results are relevant to get more.
28309
+ </tool>
28310
+ <tool name="extract">
28311
+ Read code to verify a file is actually relevant before including it.
28312
+ </tool>
28313
+ <tool name="listFiles">
28314
+ Browse directory structure to discover where code might live.
28315
+ </tool>
28316
+ </tools>
28317
+
28318
+ <search-engine-behavior>
28319
+ - Probe handles stemming, case-insensitive matching, and camelCase/snake_case splitting automatically.
28320
+ - "allowed_ips" ALREADY matches "AllowedIPs", "allowedIps", etc. Do NOT try case/style variations.
28321
+ - NEVER repeat the same search query \u2014 you will get the same results.
28322
+ - If a search returns no results at workspace root, the term does not exist. Move on.
28323
+ - If a search returns no results in a subfolder, try the workspace root or a different directory.
28324
+ - Use exact=true for known symbol names. Use default for conceptual/exploratory queries.
28325
+ - Combine related symbols with OR: "SymbolA" OR "SymbolB" finds files with either.
28326
+ - Run INDEPENDENT searches in PARALLEL \u2014 do not wait between unrelated searches.
28327
+ </search-engine-behavior>
28328
+
28329
+ <strategy>
28330
+ 1. Analyze the question \u2014 identify key concepts and brainstorm what a developer would NAME the relevant code.
28331
+ 2. Start your first search with the FULL search-path provided above. Do NOT narrow to a subdirectory on first try \u2014 the code may live anywhere in the tree.
28332
+ 3. Search for the main concept and synonyms in parallel.
28333
+ 4. Use extract to verify relevance \u2014 skim the code to confirm it ACTUALLY relates to the question.
28334
+ 5. Follow the trail: if you find a function, look for its callers, type definitions, and registered handlers.
28335
+ 6. Group your findings by WHY they are relevant (not by how you found them).
28336
+ </strategy>
28337
+
28338
+ <relevance-filtering priority="critical">
28339
+ - Only include files you have VERIFIED are relevant by reading them with extract.
28340
+ - Do NOT include files just because they matched a keyword \u2014 confirm the match is meaningful.
28341
+ - A file that mentions "session" in a comment is NOT relevant to "How do sessions work?" \u2014 look for the actual implementation.
28342
+ - Fewer verified-relevant files are far more valuable than many unverified keyword matches.
28343
+ - If a file is tangentially related but not core to the question, leave it out.
28344
+ - If NO files are truly relevant, return EMPTY groups with confidence "low". An honest empty result is far better than a wrong result. Never fill groups with loosely related files just to have something.
28345
+ </relevance-filtering>
28346
+
28347
+ <stop-conditions>
28348
+ - Once you have found locations covering the main concept and related subsystems.
28349
+ - If 2-3 different search approaches fail, stop and report what you have.
28350
+ - Do NOT keep trying quote/syntax variations of the same failing keyword.
28351
+ </stop-conditions>
28352
+
28353
+ <on-iteration-limit>
28354
+ If you run out of tool iterations, you MUST still output your JSON response with whatever you found so far.
28355
+ Set confidence to "low" if your search was incomplete.
28356
+ Include ALL files you verified as relevant, even if coverage is partial.
28357
+ The "searches" field helps the caller understand what was attempted.
28358
+ </on-iteration-limit>
28359
+
28360
+ <output-rules>
28361
+ - Return ONLY valid JSON matching the schema. No markdown, no explanation.
28362
+ - ONLY include files you have verified are relevant. No noise.
28363
+ - Group files by RELEVANCE to the question, not by search query.
28364
+ - Use ABSOLUTE file paths. Prefer #Symbol for functions/classes; otherwise use line ranges.
28365
+ - Deduplicate files across groups.
28366
+ </output-rules>`;
27670
28367
  }
27671
28368
  var import_ai, import_fs5, CODE_SEARCH_SCHEMA, searchTool, queryTool, extractTool, delegateTool, analyzeAllTool;
27672
28369
  var init_vercel = __esm({
@@ -27682,17 +28379,54 @@ var init_vercel = __esm({
27682
28379
  import_fs5 = require("fs");
27683
28380
  init_error_types();
27684
28381
  init_hashline();
28382
+ init_provider();
27685
28383
  init_simpleTelemetry();
27686
28384
  CODE_SEARCH_SCHEMA = {
27687
28385
  type: "object",
27688
28386
  properties: {
27689
- targets: {
28387
+ confidence: {
28388
+ type: "string",
28389
+ enum: ["high", "medium", "low"],
28390
+ description: "How confident you are that these locations answer the question."
28391
+ },
28392
+ reason: {
28393
+ type: "string",
28394
+ description: "Brief explanation of confidence level \u2014 what was found, partially found, or not found."
28395
+ },
28396
+ groups: {
27690
28397
  type: "array",
27691
- items: { type: "string" },
27692
- description: 'List of file targets like "path/to/file.ext#Symbol" or "path/to/file.ext:line" or "path/to/file.ext:start-end".'
28398
+ items: {
28399
+ type: "object",
28400
+ properties: {
28401
+ reason: {
28402
+ type: "string",
28403
+ description: "Why these files are relevant \u2014 what aspect of the question they address (not how the code works)."
28404
+ },
28405
+ files: {
28406
+ type: "array",
28407
+ items: { type: "string" },
28408
+ description: 'File targets like "path/to/file.ext#Symbol" or "path/to/file.ext:10-20".'
28409
+ }
28410
+ },
28411
+ required: ["reason", "files"]
28412
+ },
28413
+ description: "Groups of related files, each with a reason explaining why they matter."
28414
+ },
28415
+ searches: {
28416
+ type: "array",
28417
+ items: {
28418
+ type: "object",
28419
+ properties: {
28420
+ query: { type: "string", description: "The search query used." },
28421
+ path: { type: "string", description: "The path searched in." },
28422
+ had_results: { type: "boolean", description: "Whether the search returned any results." }
28423
+ },
28424
+ required: ["query", "path", "had_results"]
28425
+ },
28426
+ description: "All search queries executed during this session, with their paths and outcomes."
27693
28427
  }
27694
28428
  },
27695
- required: ["targets"],
28429
+ required: ["confidence", "reason", "groups", "searches"],
27696
28430
  additionalProperties: false
27697
28431
  };
27698
28432
  searchTool = (options = {}) => {
@@ -27713,11 +28447,20 @@ var init_vercel = __esm({
27713
28447
  const previousSearches = /* @__PURE__ */ new Map();
27714
28448
  const dupBlockCounts = /* @__PURE__ */ new Map();
27715
28449
  const paginationCounts = /* @__PURE__ */ new Map();
28450
+ let consecutiveNoResults = 0;
28451
+ const MAX_CONSECUTIVE_NO_RESULTS = 4;
28452
+ const failedConcepts = /* @__PURE__ */ new Map();
27716
28453
  const MAX_PAGES_PER_QUERY = 3;
28454
+ const previousDelegations = [];
28455
+ let cachedDedupModel = void 0;
28456
+ function normalizeQueryConcept(query2) {
28457
+ if (!query2) return "";
28458
+ return query2.replace(/^["']|["']$/g, "").replace(/^(definition\s+of|implementation\s+of|usage\s+of|find|where\s+is|how\s+does|locate|show\s+me|get|look\s+for)\s+/i, "").replace(/^["']|["']$/g, "").replace(/\./g, "").replace(/[_\-\s]+/g, "").toLowerCase().trim();
28459
+ }
27717
28460
  return (0, import_ai.tool)({
27718
28461
  name: "search",
27719
28462
  description: searchDelegate ? searchDelegateDescription : searchDescription,
27720
- inputSchema: searchSchema,
28463
+ inputSchema: searchDelegate ? searchDelegateSchema : searchSchema,
27721
28464
  execute: async ({ query: searchQuery, path: path9, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage, workingDirectory }) => {
27722
28465
  if (!exact && searchQuery) {
27723
28466
  const originalQuery = searchQuery;
@@ -27762,7 +28505,8 @@ var init_vercel = __esm({
27762
28505
  return await search(searchOptions);
27763
28506
  };
27764
28507
  if (!searchDelegate) {
27765
- const searchKey = `${searchPath}::${searchQuery}::${exact || false}`;
28508
+ const searchKey = `${searchPath}::${searchQuery}::${exact || false}::${language || ""}`;
28509
+ let circuitBreakerWarning = "";
27766
28510
  if (!nextPage) {
27767
28511
  if (previousSearches.has(searchKey)) {
27768
28512
  const blockCount = (dupBlockCounts.get(searchKey) || 0) + 1;
@@ -27782,6 +28526,35 @@ var init_vercel = __esm({
27782
28526
  }
27783
28527
  previousSearches.set(searchKey, { hadResults: false });
27784
28528
  paginationCounts.set(searchKey, 0);
28529
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
28530
+ if (failedConcepts.has(normalizedKey) && failedConcepts.get(normalizedKey) >= 2) {
28531
+ const conceptCount = failedConcepts.get(normalizedKey) + 1;
28532
+ failedConcepts.set(normalizedKey, conceptCount);
28533
+ if (debug) {
28534
+ console.error(`[CONCEPT-DEDUP] Blocked variation of failed concept (${conceptCount}x): "${searchQuery}" normalized to "${normalizeQueryConcept(searchQuery)}"`);
28535
+ }
28536
+ const isSubfolder = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
28537
+ const scopeHint = isSubfolder ? `
28538
+ - Try searching from the workspace root (omit the path parameter) \u2014 the term may exist in a different directory` : `
28539
+ - The term does not exist in this codebase at any path`;
28540
+ return `CONCEPT ALREADY FAILED (${conceptCount} variations tried). You already searched for "${normalizeQueryConcept(searchQuery)}" with different quoting/syntax in this path and got NO results each time. Changing quotes, adding "func" prefix, or switching to method syntax will NOT change the results.
28541
+
28542
+ Change your strategy:${scopeHint}
28543
+ - Use extract on a file you ALREADY found to read actual code and discover real function/type names
28544
+ - Use listFiles to browse directories and find what functions actually exist
28545
+ - Search for a BROADER concept (e.g., instead of "ctxGetData", try "context" or "middleware data access")
28546
+ - If you have enough information from prior searches, provide your final answer NOW`;
28547
+ }
28548
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS) {
28549
+ if (debug) {
28550
+ console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, warning: "${searchQuery}"`);
28551
+ }
28552
+ const isSubfolderCB = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
28553
+ const cbScopeHint = isSubfolderCB ? ` You have been searching in "${path9}" \u2014 consider searching from the workspace root or a different directory.` : "";
28554
+ circuitBreakerWarning = `
28555
+
28556
+ \u26A0\uFE0F CIRCUIT BREAKER: Your last ${consecutiveNoResults} searches ALL returned no results.${cbScopeHint} You MUST change your approach: use extract on files you already found, use listFiles to browse directories, or provide your final answer. Guessing names will not help.`;
28557
+ }
27785
28558
  } else {
27786
28559
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
27787
28560
  paginationCounts.set(searchKey, pageCount);
@@ -27795,10 +28568,24 @@ var init_vercel = __esm({
27795
28568
  try {
27796
28569
  const result = maybeAnnotate(await runRawSearch());
27797
28570
  if (typeof result === "string" && result.includes("No results found")) {
28571
+ consecutiveNoResults++;
28572
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
28573
+ failedConcepts.set(normalizedKey, (failedConcepts.get(normalizedKey) || 0) + 1);
28574
+ if (debug) {
28575
+ console.error(`[NO-RESULTS] consecutiveNoResults=${consecutiveNoResults}, concept "${normalizeQueryConcept(searchQuery)}" failed ${failedConcepts.get(normalizedKey)}x`);
28576
+ }
27798
28577
  if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, "").trim())) {
27799
- return result + "\n\n\u26A0\uFE0F Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names).";
28578
+ return result + "\n\n\u26A0\uFE0F Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names)." + circuitBreakerWarning;
28579
+ }
28580
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1 && !circuitBreakerWarning) {
28581
+ const isSubfolderWarn = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
28582
+ const warnScopeHint = isSubfolderWarn ? ` You are searching in "${path9}" \u2014 consider searching from the workspace root or a different directory.` : "";
28583
+ return result + `
28584
+
28585
+ \u26A0\uFE0F WARNING: ${consecutiveNoResults} consecutive searches returned no results.${warnScopeHint} Before your next action: use extract on a file you already found to read actual code, or use listFiles to discover what functions really exist. One more failed search will trigger the circuit breaker.`;
27800
28586
  }
27801
28587
  } else if (typeof result === "string") {
28588
+ consecutiveNoResults = 0;
27802
28589
  const entry = previousSearches.get(searchKey);
27803
28590
  if (entry) entry.hadResults = true;
27804
28591
  }
@@ -27806,7 +28593,7 @@ var init_vercel = __esm({
27806
28593
  options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {
27807
28594
  });
27808
28595
  }
27809
- return result;
28596
+ return typeof result === "string" ? result + circuitBreakerWarning : result;
27810
28597
  } catch (error40) {
27811
28598
  console.error("Error executing search command:", error40);
27812
28599
  const formatted = formatErrorForAI(error40);
@@ -27816,12 +28603,58 @@ var init_vercel = __esm({
27816
28603
  return formatted;
27817
28604
  }
27818
28605
  }
28606
+ const delegatePath = searchPath || "";
28607
+ let effectiveQuery = searchQuery;
28608
+ if (previousDelegations.length > 0) {
28609
+ if (cachedDedupModel === void 0) {
28610
+ const dedupProvider = options.searchDelegateProvider || process.env.PROBE_SEARCH_DELEGATE_PROVIDER || options.provider || process.env.FORCE_PROVIDER || null;
28611
+ const dedupModelName = options.searchDelegateModel || process.env.PROBE_SEARCH_DELEGATE_MODEL || options.model || process.env.MODEL_NAME || null;
28612
+ if (debug) {
28613
+ console.error(`[DEDUP-LLM] Creating model: provider=${dedupProvider}, model=${dedupModelName}`);
28614
+ }
28615
+ cachedDedupModel = await createLanguageModel(dedupProvider, dedupModelName);
28616
+ if (debug) {
28617
+ console.error(`[DEDUP-LLM] Model created: ${cachedDedupModel ? "success" : "null"}`);
28618
+ }
28619
+ }
28620
+ const dedupSpanAttrs = {
28621
+ "dedup.query": searchQuery,
28622
+ "dedup.previous_count": String(previousDelegations.length),
28623
+ "dedup.previous_queries": previousDelegations.map((d) => d.query).join(" | ")
28624
+ };
28625
+ const dedup = options.tracer?.withSpan ? await options.tracer.withSpan("search.delegate.dedup", async () => {
28626
+ return await checkDelegateDedup(searchQuery, previousDelegations, cachedDedupModel, debug);
28627
+ }, dedupSpanAttrs, (span, result) => {
28628
+ span.setAttributes({
28629
+ "dedup.action": result.action,
28630
+ "dedup.reason": result.reason || "",
28631
+ "dedup.rewritten": result.rewritten || ""
28632
+ });
28633
+ }) : await checkDelegateDedup(searchQuery, previousDelegations, cachedDedupModel, debug);
28634
+ if (debug) {
28635
+ console.error(`[DEDUP-LLM] Query: "${searchQuery}" \u2192 ${dedup.action}: ${dedup.reason}${dedup.rewritten ? ` \u2192 "${dedup.rewritten}"` : ""}`);
28636
+ }
28637
+ if (dedup.action === "block") {
28638
+ const prevQueries = previousDelegations.map((d) => `"${d.query}"`).join(", ");
28639
+ return `DELEGATE BLOCKED: "${searchQuery}" is semantically duplicate of previous delegation(s) [${prevQueries}]. ${dedup.reason}
28640
+
28641
+ Do NOT re-delegate the same concept. Use extract() on files already found, or synthesize your answer from existing results.`;
28642
+ }
28643
+ if (dedup.action === "rewrite" && dedup.rewritten) {
28644
+ effectiveQuery = dedup.rewritten;
28645
+ if (debug) {
28646
+ console.error(`[DEDUP-LLM] Rewritten query: "${searchQuery}" \u2192 "${effectiveQuery}"`);
28647
+ }
28648
+ }
28649
+ }
28650
+ const delegationRecord = { query: effectiveQuery, path: delegatePath, hadResults: false };
28651
+ previousDelegations.push(delegationRecord);
27819
28652
  try {
27820
28653
  if (debug) {
27821
- console.error(`Delegating search with query: "${searchQuery}", path: "${searchPath}"`);
28654
+ console.error(`Delegating search with query: "${effectiveQuery}", path: "${searchPath}"${effectiveQuery !== searchQuery ? ` (rewritten from: "${searchQuery}")` : ""}`);
27822
28655
  }
27823
28656
  const delegateTask = buildSearchDelegateTask({
27824
- searchQuery,
28657
+ searchQuery: effectiveQuery,
27825
28658
  searchPath,
27826
28659
  exact,
27827
28660
  language,
@@ -27848,18 +28681,33 @@ var init_vercel = __esm({
27848
28681
  });
27849
28682
  const delegateResult = options.tracer?.withSpan ? await options.tracer.withSpan("search.delegate", runDelegation, {
27850
28683
  "search.query": searchQuery,
27851
- "search.path": searchPath
28684
+ "search.path": searchPath,
28685
+ ...effectiveQuery !== searchQuery ? { "search.query.rewritten": effectiveQuery } : {}
27852
28686
  }, (span, result) => {
27853
- const text = typeof result === "string" ? result : "";
28687
+ const text = typeof result === "string" ? result : JSON.stringify(result) || "";
28688
+ if (debug) console.error(`[search-delegate] onResult: type=${typeof result}, length=${text.length}`);
27854
28689
  span.setAttributes({
27855
28690
  "search.delegate.output": truncateForSpan(text),
27856
- "search.delegate.output_length": text.length
28691
+ "search.delegate.output_length": String(text.length)
27857
28692
  });
27858
28693
  }) : await runDelegation();
27859
- const targets = parseDelegatedTargets(delegateResult);
27860
- if (!targets.length) {
28694
+ const structured = parseDelegatedResponse(delegateResult);
28695
+ if (delegationRecord && structured) {
28696
+ delegationRecord.hadResults = structured.groups.length > 0;
28697
+ delegationRecord.reason = structured.reason || "";
28698
+ delegationRecord.groups = structured.groups.map((g) => ({ reason: g.reason }));
28699
+ }
28700
+ if (!structured || structured.groups.length === 0) {
28701
+ if (structured && structured.confidence === "low" && structured.reason) {
28702
+ if (debug) {
28703
+ console.error(`Delegated search explicitly found nothing: ${structured.reason}`);
28704
+ }
28705
+ return `NOT FOUND: The search delegate thoroughly searched for "${searchQuery}" and concluded: ${structured.reason}
28706
+
28707
+ Do NOT search for analogies or loosely related concepts. If the feature does not exist in the codebase, say so in your final answer.`;
28708
+ }
27861
28709
  if (debug) {
27862
- console.error("Delegated search returned no targets; falling back to raw search");
28710
+ console.error("Delegated search returned no results; falling back to raw search");
27863
28711
  }
27864
28712
  const fallbackResult = maybeAnnotate(await runRawSearch());
27865
28713
  if (options.fileTracker && typeof fallbackResult === "string") {
@@ -27870,57 +28718,35 @@ var init_vercel = __esm({
27870
28718
  }
27871
28719
  const delegateBase = options.allowedFolders?.[0] || options.cwd || ".";
27872
28720
  const resolutionBase = searchPaths[0] || options.cwd || ".";
27873
- const resolvedTargets = targets.map((target) => resolveTargetPath(target, delegateBase));
27874
- const validatedTargets = [];
27875
- for (const target of resolvedTargets) {
27876
- const { filePart, suffix } = splitTargetSuffix(target);
27877
- if ((0, import_fs5.existsSync)(filePart)) {
27878
- validatedTargets.push(target);
27879
- continue;
27880
- }
27881
- let fixed = false;
27882
- const parts = filePart.split("/").filter(Boolean);
27883
- for (let i = 0; i < parts.length - 1; i++) {
27884
- if (parts[i] === parts[i + 1]) {
27885
- const candidate = "/" + [...parts.slice(0, i), ...parts.slice(i + 1)].join("/");
27886
- if ((0, import_fs5.existsSync)(candidate)) {
27887
- validatedTargets.push(candidate + suffix);
27888
- if (debug) console.error(`[search-delegate] Fixed doubled path segment: ${filePart} \u2192 ${candidate}`);
27889
- fixed = true;
27890
- break;
28721
+ const wsPrefix = resolutionBase.endsWith("/") ? resolutionBase : resolutionBase + "/";
28722
+ for (const group of structured.groups) {
28723
+ group.files = group.files.map((target) => resolveTargetPath(target, delegateBase)).map((target) => {
28724
+ const { filePart, suffix } = splitTargetSuffix(target);
28725
+ if ((0, import_fs5.existsSync)(filePart)) return target;
28726
+ const parts = filePart.split("/").filter(Boolean);
28727
+ for (let i = 0; i < parts.length - 1; i++) {
28728
+ if (parts[i] === parts[i + 1]) {
28729
+ const candidate = "/" + [...parts.slice(0, i), ...parts.slice(i + 1)].join("/");
28730
+ if ((0, import_fs5.existsSync)(candidate)) {
28731
+ if (debug) console.error(`[search-delegate] Fixed doubled path: ${filePart} \u2192 ${candidate}`);
28732
+ return candidate + suffix;
28733
+ }
27891
28734
  }
27892
28735
  }
27893
- }
27894
- if (fixed) continue;
27895
- for (const altBase of [resolutionBase, options.cwd].filter(Boolean)) {
27896
- if (altBase === delegateBase) continue;
27897
- const altResolved = resolveTargetPath(target, altBase);
27898
- const { filePart: altFile } = splitTargetSuffix(altResolved);
27899
- if ((0, import_fs5.existsSync)(altFile)) {
27900
- validatedTargets.push(altResolved);
27901
- if (debug) console.error(`[search-delegate] Resolved with alt base: ${filePart} \u2192 ${altFile}`);
27902
- fixed = true;
27903
- break;
28736
+ for (const altBase of [resolutionBase, options.cwd].filter(Boolean)) {
28737
+ if (altBase === delegateBase) continue;
28738
+ const altResolved = resolveTargetPath(target, altBase);
28739
+ const { filePart: altFile } = splitTargetSuffix(altResolved);
28740
+ if ((0, import_fs5.existsSync)(altFile)) {
28741
+ if (debug) console.error(`[search-delegate] Resolved with alt base: ${filePart} \u2192 ${altFile}`);
28742
+ return altResolved;
28743
+ }
27904
28744
  }
27905
- }
27906
- if (fixed) continue;
27907
- if (debug) console.error(`[search-delegate] Warning: target may not exist: ${filePart}`);
27908
- validatedTargets.push(target);
28745
+ if (debug) console.error(`[search-delegate] Warning: target may not exist: ${filePart}`);
28746
+ return target;
28747
+ }).map((target) => target.split(wsPrefix).join(""));
27909
28748
  }
27910
- const extractOptions = {
27911
- files: validatedTargets,
27912
- cwd: resolutionBase,
27913
- allowTests: allow_tests ?? true
27914
- };
27915
- if (outline) {
27916
- extractOptions.format = "xml";
27917
- }
27918
- const extractResult = await extract(extractOptions);
27919
- if (resolutionBase && typeof extractResult === "string") {
27920
- const wsPrefix = resolutionBase.endsWith("/") ? resolutionBase : resolutionBase + "/";
27921
- return maybeAnnotate(extractResult.split(wsPrefix).join(""));
27922
- }
27923
- return maybeAnnotate(extractResult);
28749
+ return JSON.stringify(structured, null, 2);
27924
28750
  } catch (error40) {
27925
28751
  console.error("Delegated search failed, falling back to raw search:", error40);
27926
28752
  try {
@@ -98047,14 +98873,11 @@ function buildFallbackProvidersFromEnv(options = {}) {
98047
98873
  }
98048
98874
  return providers;
98049
98875
  }
98050
- var import_anthropic, import_openai, import_google, FALLBACK_STRATEGIES, DEFAULT_MODELS, FallbackManager;
98876
+ var FALLBACK_STRATEGIES, DEFAULT_MODELS2, FallbackManager;
98051
98877
  var init_FallbackManager = __esm({
98052
98878
  "src/agent/FallbackManager.js"() {
98053
98879
  "use strict";
98054
- import_anthropic = require("@ai-sdk/anthropic");
98055
- import_openai = require("@ai-sdk/openai");
98056
- import_google = require("@ai-sdk/google");
98057
- init_dist3();
98880
+ init_provider();
98058
98881
  FALLBACK_STRATEGIES = {
98059
98882
  SAME_MODEL: "same-model",
98060
98883
  // Try same model on different providers
@@ -98065,12 +98888,7 @@ var init_FallbackManager = __esm({
98065
98888
  CUSTOM: "custom"
98066
98889
  // Use custom provider list
98067
98890
  };
98068
- DEFAULT_MODELS = {
98069
- anthropic: "claude-sonnet-4-6",
98070
- openai: "gpt-5.2",
98071
- google: "gemini-2.5-flash",
98072
- bedrock: "anthropic.claude-sonnet-4-6"
98073
- };
98891
+ DEFAULT_MODELS2 = DEFAULT_MODELS;
98074
98892
  FallbackManager = class {
98075
98893
  /**
98076
98894
  * Create a new FallbackManager
@@ -98143,45 +98961,7 @@ var init_FallbackManager = __esm({
98143
98961
  */
98144
98962
  _createProviderInstance(config2) {
98145
98963
  try {
98146
- switch (config2.provider) {
98147
- case "anthropic":
98148
- return (0, import_anthropic.createAnthropic)({
98149
- apiKey: config2.apiKey,
98150
- ...config2.baseURL && { baseURL: config2.baseURL }
98151
- });
98152
- case "openai":
98153
- return (0, import_openai.createOpenAI)({
98154
- compatibility: "strict",
98155
- apiKey: config2.apiKey,
98156
- ...config2.baseURL && { baseURL: config2.baseURL }
98157
- });
98158
- case "google":
98159
- return (0, import_google.createGoogleGenerativeAI)({
98160
- apiKey: config2.apiKey,
98161
- ...config2.baseURL && { baseURL: config2.baseURL }
98162
- });
98163
- case "bedrock": {
98164
- const bedrockConfig = {};
98165
- if (config2.apiKey) {
98166
- bedrockConfig.apiKey = config2.apiKey;
98167
- } else if (config2.accessKeyId && config2.secretAccessKey) {
98168
- bedrockConfig.accessKeyId = config2.accessKeyId;
98169
- bedrockConfig.secretAccessKey = config2.secretAccessKey;
98170
- if (config2.sessionToken) {
98171
- bedrockConfig.sessionToken = config2.sessionToken;
98172
- }
98173
- }
98174
- if (config2.region) {
98175
- bedrockConfig.region = config2.region;
98176
- }
98177
- if (config2.baseURL) {
98178
- bedrockConfig.baseURL = config2.baseURL;
98179
- }
98180
- return createAmazonBedrock(bedrockConfig);
98181
- }
98182
- default:
98183
- throw new Error(`FallbackManager: Unknown provider "${config2.provider}"`);
98184
- }
98964
+ return createProviderInstance(config2);
98185
98965
  } catch (error40) {
98186
98966
  const providerName = this._getProviderDisplayName(config2);
98187
98967
  throw new Error(`Failed to create provider instance for ${providerName}: ${error40.message}`);
@@ -98194,7 +98974,7 @@ var init_FallbackManager = __esm({
98194
98974
  * @private
98195
98975
  */
98196
98976
  _getModelName(config2) {
98197
- return config2.model || DEFAULT_MODELS[config2.provider];
98977
+ return config2.model || DEFAULT_MODELS2[config2.provider];
98198
98978
  }
98199
98979
  /**
98200
98980
  * Get provider display name for logging
@@ -100111,14 +100891,11 @@ function debugLogToolResults(toolResults) {
100111
100891
  console.log(`[DEBUG] tool: ${tr.toolName} | args: ${debugTruncate(argsStr)} | result: ${debugTruncate(resultStr)}`);
100112
100892
  }
100113
100893
  }
100114
- var import_dotenv2, import_anthropic2, import_openai2, import_google2, import_ai6, import_crypto9, import_events4, import_fs15, import_promises6, import_path18, ENGINE_ACTIVITY_TIMEOUT_DEFAULT, ENGINE_ACTIVITY_TIMEOUT_MIN, ENGINE_ACTIVITY_TIMEOUT_MAX, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, MAX_IMAGE_FILE_SIZE, ProbeAgent;
100894
+ var import_dotenv2, import_ai6, import_crypto9, import_events4, import_fs15, import_promises6, import_path18, ENGINE_ACTIVITY_TIMEOUT_DEFAULT, ENGINE_ACTIVITY_TIMEOUT_MIN, ENGINE_ACTIVITY_TIMEOUT_MAX, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, MAX_IMAGE_FILE_SIZE, ProbeAgent;
100115
100895
  var init_ProbeAgent = __esm({
100116
100896
  "src/agent/ProbeAgent.js"() {
100117
100897
  import_dotenv2 = __toESM(require_main(), 1);
100118
- import_anthropic2 = require("@ai-sdk/anthropic");
100119
- import_openai2 = require("@ai-sdk/openai");
100120
- import_google2 = require("@ai-sdk/google");
100121
- init_dist3();
100898
+ init_provider();
100122
100899
  import_ai6 = require("ai");
100123
100900
  import_crypto9 = require("crypto");
100124
100901
  import_events4 = require("events");
@@ -101357,11 +102134,8 @@ var init_ProbeAgent = __esm({
101357
102134
  * Initialize Anthropic model
101358
102135
  */
101359
102136
  initializeAnthropicModel(apiKey, apiUrl, modelName) {
101360
- this.provider = (0, import_anthropic2.createAnthropic)({
101361
- apiKey,
101362
- ...apiUrl && { baseURL: apiUrl }
101363
- });
101364
- this.model = modelName || "claude-sonnet-4-6";
102137
+ this.provider = createProviderInstance({ provider: "anthropic", apiKey, ...apiUrl && { baseURL: apiUrl } });
102138
+ this.model = modelName || DEFAULT_MODELS.anthropic;
101365
102139
  this.apiType = "anthropic";
101366
102140
  if (this.debug) {
101367
102141
  console.log(`Using Anthropic API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ""}`);
@@ -101371,12 +102145,8 @@ var init_ProbeAgent = __esm({
101371
102145
  * Initialize OpenAI model
101372
102146
  */
101373
102147
  initializeOpenAIModel(apiKey, apiUrl, modelName) {
101374
- this.provider = (0, import_openai2.createOpenAI)({
101375
- compatibility: "strict",
101376
- apiKey,
101377
- ...apiUrl && { baseURL: apiUrl }
101378
- });
101379
- this.model = modelName || "gpt-5.2";
102148
+ this.provider = createProviderInstance({ provider: "openai", apiKey, ...apiUrl && { baseURL: apiUrl } });
102149
+ this.model = modelName || DEFAULT_MODELS.openai;
101380
102150
  this.apiType = "openai";
101381
102151
  if (this.debug) {
101382
102152
  console.log(`Using OpenAI API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ""}`);
@@ -101386,10 +102156,7 @@ var init_ProbeAgent = __esm({
101386
102156
  * Initialize Google model
101387
102157
  */
101388
102158
  initializeGoogleModel(apiKey, apiUrl, modelName) {
101389
- this.provider = (0, import_google2.createGoogleGenerativeAI)({
101390
- apiKey,
101391
- ...apiUrl && { baseURL: apiUrl }
101392
- });
102159
+ this.provider = createProviderInstance({ provider: "google", apiKey, ...apiUrl && { baseURL: apiUrl } });
101393
102160
  this.model = modelName || "gemini-2.5-pro";
101394
102161
  this.apiType = "google";
101395
102162
  if (this.debug) {
@@ -101833,24 +102600,16 @@ var init_ProbeAgent = __esm({
101833
102600
  * Initialize AWS Bedrock model
101834
102601
  */
101835
102602
  initializeBedrockModel(accessKeyId, secretAccessKey, region, sessionToken, apiKey, baseURL, modelName) {
101836
- const config2 = {};
101837
- if (apiKey) {
101838
- config2.apiKey = apiKey;
101839
- } else if (accessKeyId && secretAccessKey) {
101840
- config2.accessKeyId = accessKeyId;
101841
- config2.secretAccessKey = secretAccessKey;
101842
- if (sessionToken) {
101843
- config2.sessionToken = sessionToken;
101844
- }
101845
- }
101846
- if (region) {
101847
- config2.region = region;
101848
- }
101849
- if (baseURL) {
101850
- config2.baseURL = baseURL;
101851
- }
101852
- this.provider = createAmazonBedrock(config2);
101853
- this.model = modelName || "anthropic.claude-sonnet-4-6";
102603
+ this.provider = createProviderInstance({
102604
+ provider: "bedrock",
102605
+ apiKey,
102606
+ accessKeyId,
102607
+ secretAccessKey,
102608
+ sessionToken,
102609
+ region,
102610
+ baseURL
102611
+ });
102612
+ this.model = modelName || DEFAULT_MODELS.bedrock;
101854
102613
  this.apiType = "bedrock";
101855
102614
  if (this.debug) {
101856
102615
  const authMethod = apiKey ? "API Key" : "AWS Credentials";
@@ -102433,7 +103192,7 @@ ${this.architectureContext.content}
102433
103192
  } else {
102434
103193
  systemPrompt += predefinedPrompts["code-explorer"] + "\n\n";
102435
103194
  }
102436
- const searchToolDesc1 = this.searchDelegate ? '- search: Ask natural language questions to find code (e.g., "How does authentication work?"). A subagent handles keyword searches and returns extracted code blocks. Do NOT formulate keyword queries \u2014 just ask questions.' : "- search: Find code patterns using keyword queries with Elasticsearch syntax. Handles stemming and case variations automatically \u2014 do NOT try manual keyword variations.";
103195
+ const searchToolDesc1 = this.searchDelegate ? '- search: Ask natural language questions to find code locations (e.g., "How does authentication work?"). Returns structured JSON with file locations grouped by relevance. Use extract() on the returned files to read the actual code. Do NOT formulate keyword queries \u2014 just ask questions.' : "- search: Find code patterns using keyword queries with Elasticsearch syntax. Handles stemming and case variations automatically \u2014 do NOT try manual keyword variations.";
102437
103196
  systemPrompt += `You have access to powerful code search and analysis tools through MCP:
102438
103197
  ${searchToolDesc1}
102439
103198
  - extract: Extract specific code sections with context
@@ -102443,8 +103202,8 @@ ${searchToolDesc1}
102443
103202
  systemPrompt += `
102444
103203
  - bash: Execute bash commands for system operations (building, running tests, git, etc.). NEVER use bash for code exploration (no grep, cat, find, head, tail) \u2014 always use search and extract tools instead, they are faster and more accurate.`;
102445
103204
  }
102446
- const searchGuidance1 = this.searchDelegate ? "1. Start with search \u2014 ask a question about what you want to understand. It returns extracted code blocks directly." : "1. Start with search to find relevant code patterns. One search per concept is usually enough \u2014 probe handles stemming and case variations.";
102447
- const extractGuidance1 = this.searchDelegate ? "2. Use extract only if you need more context or a full file" : "2. Use extract to get detailed context when needed";
103205
+ const searchGuidance1 = this.searchDelegate ? "1. Start with search \u2014 ask a question about what you want to understand. It returns file locations grouped by relevance (JSON with confidence and groups)." : "1. Start with search to find relevant code patterns. One search per concept is usually enough \u2014 probe handles stemming and case variations.";
103206
+ const extractGuidance1 = this.searchDelegate ? '2. Use extract on the file locations returned by search to read the actual code. Each group has a "reason" explaining why those files matter.' : "2. Use extract to get detailed context when needed";
102448
103207
  systemPrompt += `
102449
103208
 
102450
103209
  When exploring code:
@@ -102488,7 +103247,7 @@ Workspace: ${this.allowedFolders.join(", ")}`;
102488
103247
  } else {
102489
103248
  systemPrompt += predefinedPrompts["code-explorer"] + "\n\n";
102490
103249
  }
102491
- const searchToolDesc2 = this.searchDelegate ? '- search: Ask natural language questions to find code (e.g., "How does authentication work?"). A subagent handles keyword searches and returns extracted code blocks. Do NOT formulate keyword queries \u2014 just ask questions.' : "- search: Find code patterns using keyword queries with Elasticsearch syntax. Handles stemming and case variations automatically \u2014 do NOT try manual keyword variations.";
103250
+ const searchToolDesc2 = this.searchDelegate ? '- search: Ask natural language questions to find code locations (e.g., "How does authentication work?"). Returns structured JSON with file locations grouped by relevance. Use extract() on the returned files to read the actual code. Do NOT formulate keyword queries \u2014 just ask questions.' : "- search: Find code patterns using keyword queries with Elasticsearch syntax. Handles stemming and case variations automatically \u2014 do NOT try manual keyword variations.";
102492
103251
  systemPrompt += `You have access to powerful code search and analysis tools through MCP:
102493
103252
  ${searchToolDesc2}
102494
103253
  - extract: Extract specific code sections with context
@@ -102498,8 +103257,8 @@ ${searchToolDesc2}
102498
103257
  systemPrompt += `
102499
103258
  - bash: Execute bash commands for system operations (building, running tests, git, etc.). NEVER use bash for code exploration (no grep, cat, find, head, tail) \u2014 always use search and extract tools instead, they are faster and more accurate.`;
102500
103259
  }
102501
- const searchGuidance2 = this.searchDelegate ? "1. Start with search \u2014 ask a question about what you want to understand. It returns extracted code blocks directly." : "1. Start with search to find relevant code patterns. One search per concept is usually enough \u2014 probe handles stemming and case variations.";
102502
- const extractGuidance2 = this.searchDelegate ? "2. Use extract only if you need more context or a full file" : "2. Use extract to get detailed context when needed";
103260
+ const searchGuidance2 = this.searchDelegate ? "1. Start with search \u2014 ask a question about what you want to understand. It returns file locations grouped by relevance (JSON with confidence and groups)." : "1. Start with search to find relevant code patterns. One search per concept is usually enough \u2014 probe handles stemming and case variations.";
103261
+ const extractGuidance2 = this.searchDelegate ? '2. Use extract on the file locations returned by search to read the actual code. Each group has a "reason" explaining why those files matter.' : "2. Use extract to get detailed context when needed";
102503
103262
  systemPrompt += `
102504
103263
 
102505
103264
  When exploring code:
@@ -102559,10 +103318,10 @@ Workspace: ${this.allowedFolders.join(", ")}`;
102559
103318
  Follow these instructions carefully:
102560
103319
  1. Analyze the user's request.
102561
103320
  2. Use the available tools step-by-step to fulfill the request.
102562
- 3. You MUST use the search tool before answering ANY code-related question. NEVER answer from memory or general knowledge \u2014 your answers must be grounded in actual code found via search/extract.${this.searchDelegate ? " Ask natural language questions \u2014 the search subagent handles keyword formulation and returns extracted code blocks. Use extract only to expand context or read full files." : " Search handles stemming and case variations automatically \u2014 do NOT try keyword variations manually. Read full files only if really necessary."}
103321
+ 3. You MUST use the search tool before answering ANY code-related question. NEVER answer from memory or general knowledge \u2014 your answers must be grounded in actual code found via search/extract.${this.searchDelegate ? " Ask natural language questions \u2014 the search subagent handles keyword formulation and returns file locations grouped by relevance. Then use extract() on those locations to read the actual code." : " Search handles stemming and case variations automatically \u2014 do NOT try keyword variations manually. Read full files only if really necessary."}
102563
103322
  4. Ensure to get really deep and understand the full picture before answering. Follow call chains \u2014 if function A calls B, search for B too. Look for related subsystems (e.g., if asked about rate limiting, also check for quota, throttling, smoothing).
102564
103323
  5. Once the task is fully completed, provide your final answer directly as text. Always cite specific files and line numbers as evidence. Do NOT output planning or thinking text \u2014 go straight to the answer.
102565
- 6. ${this.searchDelegate ? "Ask clear, specific questions when searching. Each search should target a distinct concept or question." : "Prefer concise and focused search queries. Use specific keywords and phrases to narrow down results."}
103324
+ 6. ${this.searchDelegate ? 'Ask clear, specific questions when searching. Each search should target a distinct concept or question. NEVER re-search the same concept with different phrasing \u2014 if you already searched for "wrapToolWithEmitter", do NOT search again for "definition of wrapToolWithEmitter" or "how wrapToolWithEmitter works". Use extract() on the files already found instead. Limit yourself to one search per distinct concept. When formulating queries, describe WHAT you are looking for, not WHERE \u2014 the search agent will search the full codebase. Do NOT include file names or class names in the query unless that IS the concept (e.g., say "search dedup logic" not "search dedup ProbeAgent").' : "Prefer concise and focused search queries. Use specific keywords and phrases to narrow down results."}
102566
103325
  7. NEVER use bash for code exploration (no grep, cat, find, head, tail, awk, sed) \u2014 always use search and extract tools instead. Bash is only for system operations like building, running tests, or git commands.${this.allowEdit ? `
102567
103326
  7. When modifying files, choose the appropriate tool:
102568
103327
  - Use 'edit' for all code modifications:
@@ -103289,9 +104048,11 @@ Provide your BEST answer NOW using the information you have already gathered. Do
103289
104048
  const searchesTried = _toolCallLog.filter((tc) => tc.name === "search").map((tc) => `"${tc.args.query || ""}"${tc.args.exact ? " (exact)" : ""}`).filter((v, i, a) => a.indexOf(v) === i);
103290
104049
  const searchSummary = searchesTried.length > 0 ? `
103291
104050
  Searches attempted: ${searchesTried.join(", ")}` : "";
104051
+ const isCodeSearcher = this.promptType === "code-searcher";
104052
+ const lastIterMessage = isCodeSearcher ? `\u26A0\uFE0F LAST ITERATION \u2014 you are out of tool calls. Output your JSON response NOW with whatever files you have verified so far. Set confidence to "low" if your search was incomplete. Include the "searches" array listing all search queries you made with their paths and outcomes.${searchSummary}` : `\u26A0\uFE0F LAST ITERATION \u2014 you are out of tool calls. Provide your BEST answer NOW with the information gathered so far. If you could not find what was requested, explain exactly what you searched for and why it did not work, so the caller can try a different approach.${searchSummary}`;
103292
104053
  return {
103293
104054
  toolChoice: "none",
103294
- userMessage: `\u26A0\uFE0F LAST ITERATION \u2014 you are out of tool calls. Provide your BEST answer NOW with the information gathered so far. If you could not find what was requested, explain exactly what you searched for and why it did not work, so the caller can try a different approach.${searchSummary}`
104055
+ userMessage: lastIterMessage
103295
104056
  };
103296
104057
  }
103297
104058
  if (steps.length >= 2) {
@@ -103826,29 +104587,41 @@ Be thorough \u2014 this is the user's only response. Include all useful informat
103826
104587
  if (!finalResult || finalResult === DEFAULT_MAX_ITER_MSG) {
103827
104588
  try {
103828
104589
  const searchQueries = [];
104590
+ const searchDetails = [];
103829
104591
  const toolCounts = {};
103830
104592
  for (const tc of _toolCallLog) {
103831
104593
  toolCounts[tc.name] = (toolCounts[tc.name] || 0) + 1;
103832
104594
  if (tc.name === "search") {
103833
104595
  const q = tc.args.query || "";
104596
+ const p = tc.args.path || ".";
103834
104597
  const exact = tc.args.exact ? " (exact)" : "";
103835
104598
  searchQueries.push(`"${q}"${exact}`);
104599
+ searchDetails.push({ query: q, path: p, had_results: false });
103836
104600
  }
103837
104601
  }
103838
104602
  const toolBreakdown = Object.entries(toolCounts).map(([name15, count]) => `${name15}: ${count}x`).join(", ");
103839
104603
  const uniqueSearches = [...new Set(searchQueries)];
103840
- let summary = `I was unable to complete your request after ${currentIteration} tool iterations.
104604
+ if (this.promptType === "code-searcher") {
104605
+ finalResult = JSON.stringify({
104606
+ confidence: "low",
104607
+ reason: "Search incomplete \u2014 iteration limit reached",
104608
+ groups: [],
104609
+ searches: searchDetails
104610
+ });
104611
+ } else {
104612
+ let summary = `I was unable to complete your request after ${currentIteration} tool iterations.
103841
104613
 
103842
104614
  `;
103843
- summary += `Tool calls made: ${toolBreakdown || "none"}
104615
+ summary += `Tool calls made: ${toolBreakdown || "none"}
103844
104616
  `;
103845
- if (uniqueSearches.length > 0) {
103846
- summary += `Search queries tried: ${uniqueSearches.join(", ")}
104617
+ if (uniqueSearches.length > 0) {
104618
+ summary += `Search queries tried: ${uniqueSearches.join(", ")}
103847
104619
  `;
103848
- }
103849
- summary += `
104620
+ }
104621
+ summary += `
103850
104622
  The search approach may be fundamentally wrong for this query. Consider: using exact=true for literal string matching, using bash/grep for pattern-based file searches, or trying a completely different strategy instead of repeating similar searches.`;
103851
- finalResult = summary;
104623
+ finalResult = summary;
104624
+ }
103852
104625
  } catch {
103853
104626
  finalResult = DEFAULT_MAX_ITER_MSG;
103854
104627
  }