@probelabs/probe 0.6.0-rc303 → 0.6.0-rc305

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,159 +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
- "",
27565
- "When a search returns no results:",
27566
- '- If you searched a SUBFOLDER (e.g., path="gateway/"), the term might exist elsewhere.',
27567
- " Try searching from the workspace root (omit the path parameter) or a different directory.",
27568
- " But do NOT retry the same subfolder with different quoting \u2014 that will not help.",
27569
- "- If you searched the WORKSPACE ROOT and got no results, the term does not exist in this codebase.",
27570
- ' Changing quotes, adding "func " prefix, or switching to method syntax will NOT help.',
27571
- "- These are ALL the same failed search, NOT different searches:",
27572
- ' search("func ctxGetData") \u2192 no results',
27573
- ' search("ctxGetData") \u2192 no results \u2190 WASTED, same concept, different quoting',
27574
- " search(ctxGetData) \u2192 no results \u2190 WASTED, same concept, no quotes",
27575
- ' search("ctx.GetData") \u2192 no results \u2190 WASTED, method syntax of same concept',
27576
- ' After the FIRST "no results" at a given scope, either widen the search path or try',
27577
- " a fundamentally different approach: search for a broader concept, use listFiles",
27578
- " to discover actual function names, or extract a known file to read real code.",
27579
- "- If 2 searches return no results for a concept (across different scopes), the code likely",
27580
- " uses different naming than you expect \u2014 discover the real names via extract or listFiles.",
27581
- "",
27582
- "When to use exact=true:",
27583
- "- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).",
27584
- "- exact=true matches the literal string only \u2014 no stemming, no splitting.",
27585
- '- This is ideal for precise lookups: exact=true "ForwardMessage", exact=true "SessionLimiter", exact=true "ThrottleRetryLimit".',
27586
- "- IMPORTANT: Use exact=true when searching for strings containing punctuation, quotes, or empty values.",
27587
- " Default BM25 search strips punctuation and treats quoted empty strings as noise.",
27588
- ` Example: searching for 'description: ""' with exact=false will NOT find empty description fields \u2014 it just matches "description".`,
27589
- ` Use exact=true for literal patterns like 'description: ""', 'value: \\'\\'', or any YAML/config field with specific punctuation.`,
27590
- "- Do NOT use exact=true for exploratory/conceptual queries \u2014 use the default for those.",
27591
- "",
27592
- "Combining searches with OR:",
27593
- '- Multiple unquoted words use OR logic: rate limit matches files containing EITHER "rate" OR "limit".',
27594
- `- IMPORTANT: Multiple quoted terms use AND logic by default: '"RateLimit" "middleware"' requires BOTH in the same file.`,
27595
- `- To search for ANY of several quoted symbols, use the explicit OR operator: '"ForwardMessage" OR "SessionLimiter"'.`,
27596
- '- Without quotes, camelCase like limitDRL gets split into "limit" + "DRL" \u2014 not what you want for symbol lookup.',
27597
- "- Use OR to search for multiple related symbols in ONE search instead of separate searches.",
27598
- "- This is much faster than running separate searches sequentially.",
27599
- `- Example: search '"ForwardMessage" OR "SessionLimiter"' finds files with either exact symbol in one call.`,
27600
- `- Example: search '"limitDRL" OR "doRollingWindowWrite"' finds both rate limiting functions at once.`,
27601
- "- Use AND (or just put quoted terms together) when you need both terms in the same file.",
27602
- "",
27603
- "Parallel tool calls:",
27604
- "- When you need to search for INDEPENDENT concepts, call multiple search tools IN PARALLEL (same response).",
27605
- "- Do NOT wait for one search to finish before starting the next if they are independent.",
27606
- '- Example: for "rate limiting and session management", call search "rate limiting" AND search "session management" in parallel.',
27607
- "- Similarly, call multiple extract tools in parallel when verifying different files.",
27608
- "",
27609
- "GOOD search strategy (do this):",
27610
- ' Query: "How does authentication work and how are sessions managed?"',
27611
- ' \u2192 search "authentication" + search "session management" IN PARALLEL (two independent concepts)',
27612
- ' Query: "Find the IP allowlist middleware"',
27613
- ' \u2192 search "allowlist middleware" (one search, probe handles IP/ip/Ip variations)',
27614
- ' Query: "Find ForwardMessage and SessionLimiter"',
27615
- ` \u2192 search '"ForwardMessage" OR "SessionLimiter"' (one OR search finds both exact symbols)`,
27616
- ' OR: search exact=true "ForwardMessage" + search exact=true "SessionLimiter" IN PARALLEL',
27617
- ' Query: "Find limitDRL and limitRedis functions"',
27618
- ` \u2192 search '"limitDRL" OR "limitRedis"' (one OR search, quoted to prevent camelCase splitting)`,
27619
- ' Query: "Find ThrottleRetryLimit usage"',
27620
- ' \u2192 search exact=true "ThrottleRetryLimit" (one search, if no results the symbol does not exist \u2014 stop)',
27621
- ' Query: "How does BM25 scoring work with SIMD optimization?"',
27622
- ' \u2192 search "BM25 scoring" + search "SIMD optimization" IN PARALLEL (two different concepts)',
27623
- "",
27624
- "BAD search strategy (never do this):",
27625
- ' \u2192 search "AllowedIPs" \u2192 search "allowedIps" \u2192 search "allowed_ips" (WRONG: case/style variations, probe handles them)',
27626
- ` \u2192 search "limitDRL" \u2192 search "LimitDRL" (WRONG: case variation \u2014 combine with OR: '"limitDRL" OR "limitRedis"')`,
27627
- ' \u2192 search "throttle_retry_limit" after searching "ThrottleRetryLimit" (WRONG: snake_case variation, probe handles it)',
27628
- ' \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)',
27629
- ' \u2192 search "func (k *RateLimitAndQuotaCheck) handleRateLimitFailure" (WRONG: do not search full function signatures, just use exact=true "handleRateLimitFailure")',
27630
- ' \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" (WRONG: repeating the exact same query)',
27631
- ' \u2192 search "authentication" \u2192 wait \u2192 search "session management" \u2192 wait (WRONG: these are independent, run them in parallel)',
27632
- "",
27633
- " WORST pattern \u2014 retrying a non-existent function with quote/syntax variations (this wastes 30 minutes):",
27634
- ' \u2192 search "func ctxGetData" \u2192 no results',
27635
- ' \u2192 search "ctxGetData" \u2192 no results \u2190 WRONG: same term without "func" prefix',
27636
- ' \u2192 search "ctx.GetData" \u2192 no results \u2190 WRONG: method syntax of same concept',
27637
- ' \u2192 search "ctx.SetData" \u2192 no results \u2190 WRONG: Set variant of same concept',
27638
- " \u2192 search ctxGetData \u2192 no results \u2190 WRONG: unquoted version of same term",
27639
- " \u2192 extract api.go \u2192 extract api.go \u2192 extract api.go (8 times!) \u2190 WRONG: re-reading same file",
27640
- ' FIX: After "func ctxGetData" returns no results in gateway/:',
27641
- " Option A: Widen scope \u2014 search from the workspace root (omit path) in case the",
27642
- " function is defined in a different package (e.g., apidef/, user/, config/).",
27643
- " Option B: Discover real names \u2014 extract a file you KNOW uses context (e.g., a",
27644
- " middleware file) and READ what functions it actually calls.",
27645
- " Option C: Browse \u2014 use listFiles to see what files exist and extract the relevant ones.",
27646
- " NEVER: retry the same concept with different quoting in the same directory.",
27647
- "",
27648
- "Keyword tips:",
27649
- "- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.",
27650
- '- Avoid searching for these alone \u2014 combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
27651
- '- To bypass stopword filtering: wrap terms in quotes ("return", "struct") or set exact=true. Both disable stemming and splitting too.',
27652
- '- camelCase terms are split: getUserData becomes "get", "user", "data" \u2014 so one search covers all naming styles.',
27653
- '- Do NOT search for full function signatures like "func (r *Type) Method(args)". Just search for the method name with exact=true.',
27654
- '- Do NOT search for file names (e.g., "sliding_log.go"). Use listFiles to discover files by name.',
27655
- "",
27656
- "PAGINATION:",
27657
- "- Search results are paginated (~20k tokens per page).",
27658
- "- If your search returned relevant files, call the same query with nextPage=true to check for more.",
27659
- '- Keep paginating while results stay relevant. Stop when results are off-topic or "All results retrieved".',
27660
- "",
27661
- "WHEN TO STOP:",
27662
- "- After you have explored the main concept AND related subsystems.",
27663
- "- Once you have 5-15 targets covering different aspects of the query.",
27664
- '- If you get a "DUPLICATE SEARCH BLOCKED" message, do NOT rephrase the same query \u2014 try a FUNDAMENTALLY different approach:',
27665
- " * Switch between exact=true and exact=false",
27666
- " * Search for a broader term and filter results manually",
27667
- " * Use listFiles to browse the directory structure directly",
27668
- " * Look for related/surrounding patterns instead of the exact string",
27669
- "- If 2-3 genuinely different search approaches fail, STOP and report what you tried and why it failed.",
27670
- " Do NOT keep trying variations of the same failing concept.",
27671
- "",
27672
- "Strategy:",
27673
- "1. Analyze the query \u2014 identify key concepts, then brainstorm SYNONYMS and alternative terms for each.",
27674
- ' Code naming often differs from the concept: "authentication" \u2192 verify, credentials, login, auth;',
27675
- ' "rate limiting" \u2192 throttle, quota, limiter, bucket; "error handling" \u2192 catch, recover, panic.',
27676
- " Think about what a developer would NAME the function/struct/variable, not just the concept.",
27677
- "2. Run INDEPENDENT searches in PARALLEL \u2014 search for the main concept AND synonyms simultaneously.",
27678
- " After each search, check if results are relevant. If yes, call nextPage=true for more results.",
27679
- `3. Combine related symbols into OR searches: '"symbolA" OR "symbolB"' finds files with either.`,
27680
- "4. For known symbol names use exact=true. For concepts use default (exact=false).",
27681
- "5. After your first round of searches, READ the extracted code and look for connected code:",
27682
- " - Function calls to other important functions \u2192 include those targets.",
27683
- " - Type references and imports \u2192 include type definitions.",
27684
- " - Registered handlers/middleware \u2192 include all registered items.",
27685
- "6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.",
27686
- "7. If a search returns NO results: widen the path scope if you searched a subfolder, or move on. Do NOT retry with quote/syntax variations \u2014 they search the same index.",
27687
- "8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.",
27688
- "",
27689
- `Query: ${searchQuery}`,
27690
- `Search path(s): ${searchPath}`,
27691
- `Options: exact=${exact ? "true" : "false"}, language=${language || "auto"}, allow_tests=${allowTests ? "true" : "false"}.`,
27692
- "",
27693
- 'Return ONLY valid JSON: {"targets": ["path/to/file.ext#Symbol", "path/to/file.ext:line", "path/to/file.ext:start-end"]}',
27694
- '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.',
27695
- "Prefer #Symbol when a function/class name is clear; otherwise use line numbers.",
27696
- "Deduplicate targets. Do NOT explain or answer - ONLY return the JSON targets.",
27697
- "",
27698
- "Remember: if your search returned relevant results, use nextPage=true to check for more before outputting."
27699
- ].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>`;
27700
28367
  }
27701
28368
  var import_ai, import_fs5, CODE_SEARCH_SCHEMA, searchTool, queryTool, extractTool, delegateTool, analyzeAllTool;
27702
28369
  var init_vercel = __esm({
@@ -27712,17 +28379,54 @@ var init_vercel = __esm({
27712
28379
  import_fs5 = require("fs");
27713
28380
  init_error_types();
27714
28381
  init_hashline();
28382
+ init_provider();
27715
28383
  init_simpleTelemetry();
27716
28384
  CODE_SEARCH_SCHEMA = {
27717
28385
  type: "object",
27718
28386
  properties: {
27719
- 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: {
27720
28397
  type: "array",
27721
- items: { type: "string" },
27722
- 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."
27723
28427
  }
27724
28428
  },
27725
- required: ["targets"],
28429
+ required: ["confidence", "reason", "groups", "searches"],
27726
28430
  additionalProperties: false
27727
28431
  };
27728
28432
  searchTool = (options = {}) => {
@@ -27747,14 +28451,16 @@ var init_vercel = __esm({
27747
28451
  const MAX_CONSECUTIVE_NO_RESULTS = 4;
27748
28452
  const failedConcepts = /* @__PURE__ */ new Map();
27749
28453
  const MAX_PAGES_PER_QUERY = 3;
28454
+ const previousDelegations = [];
28455
+ let cachedDedupModel = void 0;
27750
28456
  function normalizeQueryConcept(query2) {
27751
28457
  if (!query2) return "";
27752
- return query2.replace(/^["']|["']$/g, "").replace(/\./g, "").replace(/[_\-\s]+/g, "").toLowerCase().trim();
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();
27753
28459
  }
27754
28460
  return (0, import_ai.tool)({
27755
28461
  name: "search",
27756
28462
  description: searchDelegate ? searchDelegateDescription : searchDescription,
27757
- inputSchema: searchSchema,
28463
+ inputSchema: searchDelegate ? searchDelegateSchema : searchSchema,
27758
28464
  execute: async ({ query: searchQuery, path: path9, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage, workingDirectory }) => {
27759
28465
  if (!exact && searchQuery) {
27760
28466
  const originalQuery = searchQuery;
@@ -27799,7 +28505,8 @@ var init_vercel = __esm({
27799
28505
  return await search(searchOptions);
27800
28506
  };
27801
28507
  if (!searchDelegate) {
27802
- const searchKey = `${searchPath}::${searchQuery}::${exact || false}`;
28508
+ const searchKey = `${searchPath}::${searchQuery}::${exact || false}::${language || ""}`;
28509
+ let circuitBreakerWarning = "";
27803
28510
  if (!nextPage) {
27804
28511
  if (previousSearches.has(searchKey)) {
27805
28512
  const blockCount = (dupBlockCounts.get(searchKey) || 0) + 1;
@@ -27840,19 +28547,13 @@ Change your strategy:${scopeHint}
27840
28547
  }
27841
28548
  if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS) {
27842
28549
  if (debug) {
27843
- console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, blocking: "${searchQuery}"`);
28550
+ console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, warning: "${searchQuery}"`);
27844
28551
  }
27845
28552
  const isSubfolderCB = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
27846
- const cbScopeHint = isSubfolderCB ? `
27847
- - You have been searching in "${path9}" \u2014 try searching from the workspace root or a different directory` : "";
27848
- return `CIRCUIT BREAKER: Your last ${consecutiveNoResults} searches ALL returned no results. You appear to be guessing function/type names that don't match what's actually in the code.
27849
-
27850
- Change your approach:${cbScopeHint}
27851
- 1. Use extract on files you already found \u2014 read the actual code to discover real function names
27852
- 2. Use listFiles to browse directories and see what files/functions actually exist
27853
- 3. If you found some results earlier, those are likely sufficient \u2014 provide your final answer
28553
+ const cbScopeHint = isSubfolderCB ? ` You have been searching in "${path9}" \u2014 consider searching from the workspace root or a different directory.` : "";
28554
+ circuitBreakerWarning = `
27854
28555
 
27855
- Retrying search query variations will not help. Discover real names from real code instead.`;
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.`;
27856
28557
  }
27857
28558
  } else {
27858
28559
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
@@ -27874,9 +28575,9 @@ Retrying search query variations will not help. Discover real names from real co
27874
28575
  console.error(`[NO-RESULTS] consecutiveNoResults=${consecutiveNoResults}, concept "${normalizeQueryConcept(searchQuery)}" failed ${failedConcepts.get(normalizedKey)}x`);
27875
28576
  }
27876
28577
  if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, "").trim())) {
27877
- 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;
27878
28579
  }
27879
- if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1) {
28580
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1 && !circuitBreakerWarning) {
27880
28581
  const isSubfolderWarn = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
27881
28582
  const warnScopeHint = isSubfolderWarn ? ` You are searching in "${path9}" \u2014 consider searching from the workspace root or a different directory.` : "";
27882
28583
  return result + `
@@ -27892,7 +28593,7 @@ Retrying search query variations will not help. Discover real names from real co
27892
28593
  options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {
27893
28594
  });
27894
28595
  }
27895
- return result;
28596
+ return typeof result === "string" ? result + circuitBreakerWarning : result;
27896
28597
  } catch (error40) {
27897
28598
  console.error("Error executing search command:", error40);
27898
28599
  const formatted = formatErrorForAI(error40);
@@ -27902,12 +28603,58 @@ Retrying search query variations will not help. Discover real names from real co
27902
28603
  return formatted;
27903
28604
  }
27904
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);
27905
28652
  try {
27906
28653
  if (debug) {
27907
- console.error(`Delegating search with query: "${searchQuery}", path: "${searchPath}"`);
28654
+ console.error(`Delegating search with query: "${effectiveQuery}", path: "${searchPath}"${effectiveQuery !== searchQuery ? ` (rewritten from: "${searchQuery}")` : ""}`);
27908
28655
  }
27909
28656
  const delegateTask = buildSearchDelegateTask({
27910
- searchQuery,
28657
+ searchQuery: effectiveQuery,
27911
28658
  searchPath,
27912
28659
  exact,
27913
28660
  language,
@@ -27934,18 +28681,33 @@ Retrying search query variations will not help. Discover real names from real co
27934
28681
  });
27935
28682
  const delegateResult = options.tracer?.withSpan ? await options.tracer.withSpan("search.delegate", runDelegation, {
27936
28683
  "search.query": searchQuery,
27937
- "search.path": searchPath
28684
+ "search.path": searchPath,
28685
+ ...effectiveQuery !== searchQuery ? { "search.query.rewritten": effectiveQuery } : {}
27938
28686
  }, (span, result) => {
27939
- 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}`);
27940
28689
  span.setAttributes({
27941
28690
  "search.delegate.output": truncateForSpan(text),
27942
- "search.delegate.output_length": text.length
28691
+ "search.delegate.output_length": String(text.length)
27943
28692
  });
27944
28693
  }) : await runDelegation();
27945
- const targets = parseDelegatedTargets(delegateResult);
27946
- 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
+ }
27947
28709
  if (debug) {
27948
- 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");
27949
28711
  }
27950
28712
  const fallbackResult = maybeAnnotate(await runRawSearch());
27951
28713
  if (options.fileTracker && typeof fallbackResult === "string") {
@@ -27956,57 +28718,35 @@ Retrying search query variations will not help. Discover real names from real co
27956
28718
  }
27957
28719
  const delegateBase = options.allowedFolders?.[0] || options.cwd || ".";
27958
28720
  const resolutionBase = searchPaths[0] || options.cwd || ".";
27959
- const resolvedTargets = targets.map((target) => resolveTargetPath(target, delegateBase));
27960
- const validatedTargets = [];
27961
- for (const target of resolvedTargets) {
27962
- const { filePart, suffix } = splitTargetSuffix(target);
27963
- if ((0, import_fs5.existsSync)(filePart)) {
27964
- validatedTargets.push(target);
27965
- continue;
27966
- }
27967
- let fixed = false;
27968
- const parts = filePart.split("/").filter(Boolean);
27969
- for (let i = 0; i < parts.length - 1; i++) {
27970
- if (parts[i] === parts[i + 1]) {
27971
- const candidate = "/" + [...parts.slice(0, i), ...parts.slice(i + 1)].join("/");
27972
- if ((0, import_fs5.existsSync)(candidate)) {
27973
- validatedTargets.push(candidate + suffix);
27974
- if (debug) console.error(`[search-delegate] Fixed doubled path segment: ${filePart} \u2192 ${candidate}`);
27975
- fixed = true;
27976
- 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
+ }
27977
28734
  }
27978
28735
  }
27979
- }
27980
- if (fixed) continue;
27981
- for (const altBase of [resolutionBase, options.cwd].filter(Boolean)) {
27982
- if (altBase === delegateBase) continue;
27983
- const altResolved = resolveTargetPath(target, altBase);
27984
- const { filePart: altFile } = splitTargetSuffix(altResolved);
27985
- if ((0, import_fs5.existsSync)(altFile)) {
27986
- validatedTargets.push(altResolved);
27987
- if (debug) console.error(`[search-delegate] Resolved with alt base: ${filePart} \u2192 ${altFile}`);
27988
- fixed = true;
27989
- 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
+ }
27990
28744
  }
27991
- }
27992
- if (fixed) continue;
27993
- if (debug) console.error(`[search-delegate] Warning: target may not exist: ${filePart}`);
27994
- validatedTargets.push(target);
27995
- }
27996
- const extractOptions = {
27997
- files: validatedTargets,
27998
- cwd: resolutionBase,
27999
- allowTests: allow_tests ?? true
28000
- };
28001
- if (outline) {
28002
- extractOptions.format = "xml";
28745
+ if (debug) console.error(`[search-delegate] Warning: target may not exist: ${filePart}`);
28746
+ return target;
28747
+ }).map((target) => target.split(wsPrefix).join(""));
28003
28748
  }
28004
- const extractResult = await extract(extractOptions);
28005
- if (resolutionBase && typeof extractResult === "string") {
28006
- const wsPrefix = resolutionBase.endsWith("/") ? resolutionBase : resolutionBase + "/";
28007
- return maybeAnnotate(extractResult.split(wsPrefix).join(""));
28008
- }
28009
- return maybeAnnotate(extractResult);
28749
+ return JSON.stringify(structured, null, 2);
28010
28750
  } catch (error40) {
28011
28751
  console.error("Delegated search failed, falling back to raw search:", error40);
28012
28752
  try {
@@ -49649,7 +50389,10 @@ var init_TaskManager = __esm({
49649
50389
  * @throws {Error} If dependencies are invalid or create a cycle
49650
50390
  */
49651
50391
  createTask(taskData) {
49652
- const id = this._generateId();
50392
+ const id = taskData.id || this._generateId();
50393
+ if (taskData.id && this.tasks.has(taskData.id)) {
50394
+ throw new Error(`Task ID "${taskData.id}" already exists. Choose a different ID. Available tasks: ${this._getAvailableTaskIds()}`);
50395
+ }
49653
50396
  const now = this._now();
49654
50397
  const dependencies = taskData.dependencies || [];
49655
50398
  for (const depId of dependencies) {
@@ -49717,33 +50460,30 @@ var init_TaskManager = __esm({
49717
50460
  }
49718
50461
  }
49719
50462
  }
49720
- const idMap = /* @__PURE__ */ new Map();
49721
- const batchAutoIds = /* @__PURE__ */ new Set();
49722
- let nextCounter = this.taskCounter;
50463
+ const batchIds = /* @__PURE__ */ new Set();
49723
50464
  for (const taskData of tasksData) {
49724
- nextCounter++;
49725
- const autoId = `task-${nextCounter}`;
49726
- batchAutoIds.add(autoId);
49727
50465
  if (taskData.id) {
49728
- idMap.set(taskData.id, autoId);
50466
+ if (this.tasks.has(taskData.id)) {
50467
+ throw new Error(`Task ID "${taskData.id}" already exists. Choose a different ID. Available tasks: ${this._getAvailableTaskIds()}`);
50468
+ }
50469
+ if (batchIds.has(taskData.id)) {
50470
+ throw new Error(`Duplicate task ID "${taskData.id}" in batch. Each task must have a unique ID.`);
50471
+ }
50472
+ batchIds.add(taskData.id);
49729
50473
  }
49730
50474
  }
49731
50475
  const resolvedTasksData = tasksData.map((taskData) => {
49732
50476
  const resolved = { ...taskData };
49733
- delete resolved.id;
49734
50477
  if (resolved.dependencies) {
49735
50478
  resolved.dependencies = resolved.dependencies.map((depId) => {
49736
- if (idMap.has(depId)) return idMap.get(depId);
49737
- if (this.tasks.has(depId) || batchAutoIds.has(depId)) return depId;
49738
- const batchIds = idMap.size > 0 ? Array.from(idMap.keys()).join(", ") : "(none provided)";
49739
- throw new Error(`Dependency "${depId}" does not exist. Each task in the batch must have an "id" field, and dependencies must reference those IDs. Current batch IDs: ${batchIds}. Existing tasks: ${this._getAvailableTaskIds()}`);
50479
+ if (batchIds.has(depId) || this.tasks.has(depId)) return depId;
50480
+ const knownIds = batchIds.size > 0 ? Array.from(batchIds).join(", ") : "(none provided)";
50481
+ throw new Error(`Dependency "${depId}" does not exist. Each task in the batch must have an "id" field, and dependencies must reference those IDs. Current batch IDs: ${knownIds}. Existing tasks: ${this._getAvailableTaskIds()}`);
49740
50482
  });
49741
50483
  }
49742
50484
  if (resolved.after) {
49743
- if (idMap.has(resolved.after)) {
49744
- resolved.after = idMap.get(resolved.after);
49745
- } else if (!this.tasks.has(resolved.after) && !batchAutoIds.has(resolved.after)) {
49746
- throw new Error(`Task "${resolved.after}" does not exist. Cannot insert after non-existent task. Available tasks: ${this._getAvailableTaskIds()}${idMap.size > 0 ? `, batch IDs: ${Array.from(idMap.keys()).join(", ")}` : ""}`);
50485
+ if (!batchIds.has(resolved.after) && !this.tasks.has(resolved.after)) {
50486
+ throw new Error(`Task "${resolved.after}" does not exist. Cannot insert after non-existent task. Available tasks: ${this._getAvailableTaskIds()}${batchIds.size > 0 ? `, batch IDs: ${Array.from(batchIds).join(", ")}` : ""}`);
49747
50487
  }
49748
50488
  }
49749
50489
  return resolved;
@@ -98133,14 +98873,11 @@ function buildFallbackProvidersFromEnv(options = {}) {
98133
98873
  }
98134
98874
  return providers;
98135
98875
  }
98136
- var import_anthropic, import_openai, import_google, FALLBACK_STRATEGIES, DEFAULT_MODELS, FallbackManager;
98876
+ var FALLBACK_STRATEGIES, DEFAULT_MODELS2, FallbackManager;
98137
98877
  var init_FallbackManager = __esm({
98138
98878
  "src/agent/FallbackManager.js"() {
98139
98879
  "use strict";
98140
- import_anthropic = require("@ai-sdk/anthropic");
98141
- import_openai = require("@ai-sdk/openai");
98142
- import_google = require("@ai-sdk/google");
98143
- init_dist3();
98880
+ init_provider();
98144
98881
  FALLBACK_STRATEGIES = {
98145
98882
  SAME_MODEL: "same-model",
98146
98883
  // Try same model on different providers
@@ -98151,12 +98888,7 @@ var init_FallbackManager = __esm({
98151
98888
  CUSTOM: "custom"
98152
98889
  // Use custom provider list
98153
98890
  };
98154
- DEFAULT_MODELS = {
98155
- anthropic: "claude-sonnet-4-6",
98156
- openai: "gpt-5.2",
98157
- google: "gemini-2.5-flash",
98158
- bedrock: "anthropic.claude-sonnet-4-6"
98159
- };
98891
+ DEFAULT_MODELS2 = DEFAULT_MODELS;
98160
98892
  FallbackManager = class {
98161
98893
  /**
98162
98894
  * Create a new FallbackManager
@@ -98229,45 +98961,7 @@ var init_FallbackManager = __esm({
98229
98961
  */
98230
98962
  _createProviderInstance(config2) {
98231
98963
  try {
98232
- switch (config2.provider) {
98233
- case "anthropic":
98234
- return (0, import_anthropic.createAnthropic)({
98235
- apiKey: config2.apiKey,
98236
- ...config2.baseURL && { baseURL: config2.baseURL }
98237
- });
98238
- case "openai":
98239
- return (0, import_openai.createOpenAI)({
98240
- compatibility: "strict",
98241
- apiKey: config2.apiKey,
98242
- ...config2.baseURL && { baseURL: config2.baseURL }
98243
- });
98244
- case "google":
98245
- return (0, import_google.createGoogleGenerativeAI)({
98246
- apiKey: config2.apiKey,
98247
- ...config2.baseURL && { baseURL: config2.baseURL }
98248
- });
98249
- case "bedrock": {
98250
- const bedrockConfig = {};
98251
- if (config2.apiKey) {
98252
- bedrockConfig.apiKey = config2.apiKey;
98253
- } else if (config2.accessKeyId && config2.secretAccessKey) {
98254
- bedrockConfig.accessKeyId = config2.accessKeyId;
98255
- bedrockConfig.secretAccessKey = config2.secretAccessKey;
98256
- if (config2.sessionToken) {
98257
- bedrockConfig.sessionToken = config2.sessionToken;
98258
- }
98259
- }
98260
- if (config2.region) {
98261
- bedrockConfig.region = config2.region;
98262
- }
98263
- if (config2.baseURL) {
98264
- bedrockConfig.baseURL = config2.baseURL;
98265
- }
98266
- return createAmazonBedrock(bedrockConfig);
98267
- }
98268
- default:
98269
- throw new Error(`FallbackManager: Unknown provider "${config2.provider}"`);
98270
- }
98964
+ return createProviderInstance(config2);
98271
98965
  } catch (error40) {
98272
98966
  const providerName = this._getProviderDisplayName(config2);
98273
98967
  throw new Error(`Failed to create provider instance for ${providerName}: ${error40.message}`);
@@ -98280,7 +98974,7 @@ var init_FallbackManager = __esm({
98280
98974
  * @private
98281
98975
  */
98282
98976
  _getModelName(config2) {
98283
- return config2.model || DEFAULT_MODELS[config2.provider];
98977
+ return config2.model || DEFAULT_MODELS2[config2.provider];
98284
98978
  }
98285
98979
  /**
98286
98980
  * Get provider display name for logging
@@ -100197,14 +100891,11 @@ function debugLogToolResults(toolResults) {
100197
100891
  console.log(`[DEBUG] tool: ${tr.toolName} | args: ${debugTruncate(argsStr)} | result: ${debugTruncate(resultStr)}`);
100198
100892
  }
100199
100893
  }
100200
- 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;
100201
100895
  var init_ProbeAgent = __esm({
100202
100896
  "src/agent/ProbeAgent.js"() {
100203
100897
  import_dotenv2 = __toESM(require_main(), 1);
100204
- import_anthropic2 = require("@ai-sdk/anthropic");
100205
- import_openai2 = require("@ai-sdk/openai");
100206
- import_google2 = require("@ai-sdk/google");
100207
- init_dist3();
100898
+ init_provider();
100208
100899
  import_ai6 = require("ai");
100209
100900
  import_crypto9 = require("crypto");
100210
100901
  import_events4 = require("events");
@@ -101443,11 +102134,8 @@ var init_ProbeAgent = __esm({
101443
102134
  * Initialize Anthropic model
101444
102135
  */
101445
102136
  initializeAnthropicModel(apiKey, apiUrl, modelName) {
101446
- this.provider = (0, import_anthropic2.createAnthropic)({
101447
- apiKey,
101448
- ...apiUrl && { baseURL: apiUrl }
101449
- });
101450
- this.model = modelName || "claude-sonnet-4-6";
102137
+ this.provider = createProviderInstance({ provider: "anthropic", apiKey, ...apiUrl && { baseURL: apiUrl } });
102138
+ this.model = modelName || DEFAULT_MODELS.anthropic;
101451
102139
  this.apiType = "anthropic";
101452
102140
  if (this.debug) {
101453
102141
  console.log(`Using Anthropic API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ""}`);
@@ -101457,12 +102145,8 @@ var init_ProbeAgent = __esm({
101457
102145
  * Initialize OpenAI model
101458
102146
  */
101459
102147
  initializeOpenAIModel(apiKey, apiUrl, modelName) {
101460
- this.provider = (0, import_openai2.createOpenAI)({
101461
- compatibility: "strict",
101462
- apiKey,
101463
- ...apiUrl && { baseURL: apiUrl }
101464
- });
101465
- this.model = modelName || "gpt-5.2";
102148
+ this.provider = createProviderInstance({ provider: "openai", apiKey, ...apiUrl && { baseURL: apiUrl } });
102149
+ this.model = modelName || DEFAULT_MODELS.openai;
101466
102150
  this.apiType = "openai";
101467
102151
  if (this.debug) {
101468
102152
  console.log(`Using OpenAI API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ""}`);
@@ -101472,10 +102156,7 @@ var init_ProbeAgent = __esm({
101472
102156
  * Initialize Google model
101473
102157
  */
101474
102158
  initializeGoogleModel(apiKey, apiUrl, modelName) {
101475
- this.provider = (0, import_google2.createGoogleGenerativeAI)({
101476
- apiKey,
101477
- ...apiUrl && { baseURL: apiUrl }
101478
- });
102159
+ this.provider = createProviderInstance({ provider: "google", apiKey, ...apiUrl && { baseURL: apiUrl } });
101479
102160
  this.model = modelName || "gemini-2.5-pro";
101480
102161
  this.apiType = "google";
101481
102162
  if (this.debug) {
@@ -101919,24 +102600,16 @@ var init_ProbeAgent = __esm({
101919
102600
  * Initialize AWS Bedrock model
101920
102601
  */
101921
102602
  initializeBedrockModel(accessKeyId, secretAccessKey, region, sessionToken, apiKey, baseURL, modelName) {
101922
- const config2 = {};
101923
- if (apiKey) {
101924
- config2.apiKey = apiKey;
101925
- } else if (accessKeyId && secretAccessKey) {
101926
- config2.accessKeyId = accessKeyId;
101927
- config2.secretAccessKey = secretAccessKey;
101928
- if (sessionToken) {
101929
- config2.sessionToken = sessionToken;
101930
- }
101931
- }
101932
- if (region) {
101933
- config2.region = region;
101934
- }
101935
- if (baseURL) {
101936
- config2.baseURL = baseURL;
101937
- }
101938
- this.provider = createAmazonBedrock(config2);
101939
- 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;
101940
102613
  this.apiType = "bedrock";
101941
102614
  if (this.debug) {
101942
102615
  const authMethod = apiKey ? "API Key" : "AWS Credentials";
@@ -102519,7 +103192,7 @@ ${this.architectureContext.content}
102519
103192
  } else {
102520
103193
  systemPrompt += predefinedPrompts["code-explorer"] + "\n\n";
102521
103194
  }
102522
- 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.";
102523
103196
  systemPrompt += `You have access to powerful code search and analysis tools through MCP:
102524
103197
  ${searchToolDesc1}
102525
103198
  - extract: Extract specific code sections with context
@@ -102529,8 +103202,8 @@ ${searchToolDesc1}
102529
103202
  systemPrompt += `
102530
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.`;
102531
103204
  }
102532
- 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.";
102533
- 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";
102534
103207
  systemPrompt += `
102535
103208
 
102536
103209
  When exploring code:
@@ -102574,7 +103247,7 @@ Workspace: ${this.allowedFolders.join(", ")}`;
102574
103247
  } else {
102575
103248
  systemPrompt += predefinedPrompts["code-explorer"] + "\n\n";
102576
103249
  }
102577
- 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.";
102578
103251
  systemPrompt += `You have access to powerful code search and analysis tools through MCP:
102579
103252
  ${searchToolDesc2}
102580
103253
  - extract: Extract specific code sections with context
@@ -102584,8 +103257,8 @@ ${searchToolDesc2}
102584
103257
  systemPrompt += `
102585
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.`;
102586
103259
  }
102587
- 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.";
102588
- 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";
102589
103262
  systemPrompt += `
102590
103263
 
102591
103264
  When exploring code:
@@ -102645,10 +103318,10 @@ Workspace: ${this.allowedFolders.join(", ")}`;
102645
103318
  Follow these instructions carefully:
102646
103319
  1. Analyze the user's request.
102647
103320
  2. Use the available tools step-by-step to fulfill the request.
102648
- 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."}
102649
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).
102650
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.
102651
- 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."}
102652
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 ? `
102653
103326
  7. When modifying files, choose the appropriate tool:
102654
103327
  - Use 'edit' for all code modifications:
@@ -103375,9 +104048,11 @@ Provide your BEST answer NOW using the information you have already gathered. Do
103375
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);
103376
104049
  const searchSummary = searchesTried.length > 0 ? `
103377
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}`;
103378
104053
  return {
103379
104054
  toolChoice: "none",
103380
- 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
103381
104056
  };
103382
104057
  }
103383
104058
  if (steps.length >= 2) {
@@ -103912,29 +104587,41 @@ Be thorough \u2014 this is the user's only response. Include all useful informat
103912
104587
  if (!finalResult || finalResult === DEFAULT_MAX_ITER_MSG) {
103913
104588
  try {
103914
104589
  const searchQueries = [];
104590
+ const searchDetails = [];
103915
104591
  const toolCounts = {};
103916
104592
  for (const tc of _toolCallLog) {
103917
104593
  toolCounts[tc.name] = (toolCounts[tc.name] || 0) + 1;
103918
104594
  if (tc.name === "search") {
103919
104595
  const q = tc.args.query || "";
104596
+ const p = tc.args.path || ".";
103920
104597
  const exact = tc.args.exact ? " (exact)" : "";
103921
104598
  searchQueries.push(`"${q}"${exact}`);
104599
+ searchDetails.push({ query: q, path: p, had_results: false });
103922
104600
  }
103923
104601
  }
103924
104602
  const toolBreakdown = Object.entries(toolCounts).map(([name15, count]) => `${name15}: ${count}x`).join(", ");
103925
104603
  const uniqueSearches = [...new Set(searchQueries)];
103926
- 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.
103927
104613
 
103928
104614
  `;
103929
- summary += `Tool calls made: ${toolBreakdown || "none"}
104615
+ summary += `Tool calls made: ${toolBreakdown || "none"}
103930
104616
  `;
103931
- if (uniqueSearches.length > 0) {
103932
- summary += `Search queries tried: ${uniqueSearches.join(", ")}
104617
+ if (uniqueSearches.length > 0) {
104618
+ summary += `Search queries tried: ${uniqueSearches.join(", ")}
103933
104619
  `;
103934
- }
103935
- summary += `
104620
+ }
104621
+ summary += `
103936
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.`;
103937
- finalResult = summary;
104623
+ finalResult = summary;
104624
+ }
103938
104625
  } catch {
103939
104626
  finalResult = DEFAULT_MAX_ITER_MSG;
103940
104627
  }