@exulu/backend 1.54.0 → 1.56.0

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.
@@ -1,18 +1,19 @@
1
1
  import { z } from "zod";
2
2
  import { createBashTool } from "bash-tool";
3
- import type { LanguageModel } from "ai";
3
+ import type { LanguageModel, Tool } from "ai";
4
4
  import type { ExuluContext } from "@SRC/exulu/context";
5
5
  import type { ExuluReranker } from "@SRC/exulu/reranker";
6
6
  import { ExuluTool } from "@SRC/exulu/tool";
7
7
  import type { User } from "@EXULU_TYPES/models/user";
8
8
  import { checkLicense } from "@EE/entitlements";
9
- import { ContextSampler } from "./context-sampler";
10
9
  import { classifyQuery } from "./classifier";
11
- import { createRetrievalTools } from "./tools";
10
+ import { createRetrievalTools, parseGlobalItemIds } from "./tools";
12
11
  import { STRATEGIES } from "./strategies";
13
12
  import { runAgentLoop } from "./agent-loop";
14
13
  import { TrajectoryLogger } from "./trajectory";
15
14
  import type { AgenticRetrievalOutput, QueryType } from "./types";
15
+ import type { ExuluItem } from "@SRC/index";
16
+ import { ContextSampler } from "./context-sampler";
16
17
 
17
18
  // Module-level sampler — shared across all tool instances so the cache is warm
18
19
  // across requests within the same process.
@@ -23,27 +24,45 @@ async function* executeV3({
23
24
  contexts,
24
25
  reranker,
25
26
  model,
27
+ toolVariablesConfig,
26
28
  user,
27
29
  role,
28
30
  customInstructions,
31
+ logTrajectory,
32
+ sessionId,
33
+ preselectedItemIds,
29
34
  }: {
30
35
  query: string;
31
36
  contexts: ExuluContext[];
32
37
  reranker?: ExuluReranker;
38
+ toolVariablesConfig?: Record<string, any>;
33
39
  model: LanguageModel;
34
40
  user?: User;
35
41
  role?: string;
36
42
  customInstructions?: string;
43
+ logTrajectory?: boolean;
44
+ sessionId?: string;
45
+ preselectedItemIds?: string[];
37
46
  }): AsyncGenerator<AgenticRetrievalOutput> {
38
- // ── 1. Sample example records from each context (cached) ──────────────────
47
+ // ── 1. Parse preselected item IDs (global format: "<context_id>/<item_id>")
48
+ const preselectedByContext = preselectedItemIds?.length
49
+ ? parseGlobalItemIds(preselectedItemIds)
50
+ : undefined;
51
+
52
+ // When preselection is active, restrict to only contexts that have selected items
53
+ const activeContexts = preselectedByContext?.size
54
+ ? contexts.filter((c) => preselectedByContext.has(c.id))
55
+ : contexts;
56
+
57
+ // ── 2. Sample example records from each context (cached) ──────────────────
39
58
  console.log("[EXULU] v3 — sampling contexts");
40
- const samples = await sampler.getSamples(contexts, user, role);
59
+ const samples = await sampler.getSamples(activeContexts, user, role);
41
60
 
42
- // ── 2. Classify query (single fast LLM call) ──────────────────────────────
61
+ // ── 3. Classify query (single fast LLM call) ──────────────────────────────
43
62
  console.log("[EXULU] v3 — classifying query");
44
63
  let classification;
45
64
  try {
46
- classification = await classifyQuery(query, contexts, samples, model);
65
+ classification = await classifyQuery(query, activeContexts, samples, model);
47
66
  } catch (err) {
48
67
  console.warn("[EXULU] v3 — classification failed, falling back to exploratory:", err);
49
68
  classification = {
@@ -54,32 +73,63 @@ async function* executeV3({
54
73
  }
55
74
  console.log("[EXULU] v3 — classified as:", classification);
56
75
 
57
- // ── 3. Select strategy ────────────────────────────────────────────────────
76
+ // ── 4. Select strategy ────────────────────────────────────────────────────
58
77
  const strategy = STRATEGIES[classification.queryType];
59
-
78
+ const contextSpecificInstructions = activeContexts.map(ctx => {
79
+ const instructions = toolVariablesConfig?.[`${ctx.id}_|_instructions`] ?? "";
80
+ if (instructions) {
81
+ return `
82
+ <${ctx.id}>
83
+ ${instructions}
84
+ </${ctx.id}>
85
+ `;
86
+ } else {
87
+ return null;
88
+ }
89
+ }).filter(Boolean).join("\n");
60
90
  // Build context guidance: the classifier is a priority hint, not a hard filter.
61
91
  // All contexts remain available so the agent can fall back if suggested ones miss.
62
92
  const suggestedIds = classification.suggestedContextIds;
63
- const fallbackIds = contexts
93
+ const fallbackIds = activeContexts
64
94
  .filter((c) => !suggestedIds.includes(c.id))
65
95
  .map((c) => c.id);
66
- const contextGuidance =
96
+ let contextBase =
67
97
  suggestedIds.length > 0
68
- ? `Suggested priority contexts: [${suggestedIds.join(", ")}]. Also available: [${fallbackIds.join(", ")}]. Custom instructions may require searching additional or all contexts — follow them.`
69
- : `All contexts available: [${contexts.map((c) => c.id).join(", ")}].`;
98
+ ? `
99
+ Suggested priority contexts: [${suggestedIds.join(", ")}].
100
+
101
+ Also available: [${fallbackIds.join(", ")}].
102
+
103
+ Custom instructions may require searching additional or all contexts — follow them.`
104
+ : `All contexts available: [${activeContexts.map((c) => c.id).join(", ")}].`;
105
+
106
+ const preselectedNote = preselectedByContext?.size
107
+ ? `\nSCOPE CONSTRAINT: Retrieval is scoped to preselected items/contexts. Per context: ${[...preselectedByContext.entries()].map(([ctx, ids]) => ids === null ? `${ctx} (full context)` : `${ctx} (${ids.length} item${ids.length === 1 ? "" : "s"})`).join(", ")}. All tools enforce this scope automatically. For full-context entries you may search freely; for item-restricted entries do NOT use search_items_by_name for discovery — go directly to search_content or save_search_results.`
108
+ : "";
109
+
110
+ if (contextSpecificInstructions?.length) {
111
+ contextBase += `
112
+ Context specific instructions:
113
+ ${contextSpecificInstructions}
114
+ `;
115
+ }
116
+
117
+ const contextGuidance = contextBase + preselectedNote;
70
118
 
71
- // ── 4. Initialize tools ───────────────────────────────────────────────────
119
+ // ── 5. Initialize tools ───────────────────────────────────────────────────
72
120
  const bashToolkit = await createBashTool({ files: {} });
73
121
 
74
122
  const retrievalTools = createRetrievalTools({
75
- contexts, // ALL contexts — agent decides which to search based on context guidance
123
+ contexts: activeContexts,
124
+ toolVariablesConfig,
76
125
  user,
77
126
  role,
78
127
  updateVirtualFiles: (files) => bashToolkit.sandbox.writeFiles(files),
128
+ preselectedItemsByContext: preselectedByContext,
79
129
  });
80
130
 
81
131
  // Build the tool set for this strategy
82
- const activeTools: Record<string, any> = {};
132
+ const activeTools: Record<string, Tool> = {};
83
133
  for (const name of strategy.retrieval_tools) {
84
134
  if (name in retrievalTools) {
85
135
  activeTools[name] = retrievalTools[name as keyof typeof retrievalTools];
@@ -89,10 +139,10 @@ async function* executeV3({
89
139
  Object.assign(activeTools, bashToolkit.tools);
90
140
  }
91
141
 
92
- // ── 5. Set up trajectory logging ──────────────────────────────────────────
93
- const trajectory = new TrajectoryLogger(query, classification);
142
+ // ── 6. Set up trajectory logging ──────────────────────────────────────────
143
+ const trajectory = new TrajectoryLogger(query, classification, undefined, preselectedItemIds);
94
144
 
95
- // ── 6. Run agent loop ─────────────────────────────────────────────────────
145
+ // ── 7. Run agent loop ─────────────────────────────────────────────────────
96
146
  let finalOutput: AgenticRetrievalOutput | undefined;
97
147
  let executionError: Error | undefined;
98
148
 
@@ -106,7 +156,9 @@ async function* executeV3({
106
156
  contextGuidance,
107
157
  customInstructions,
108
158
  classification,
159
+ sessionId,
109
160
  onStepComplete: (step) => trajectory.recordStep(step),
161
+ onTrajectoryStep: (data) => trajectory.recordRichStep(data),
110
162
  })) {
111
163
  finalOutput = output;
112
164
  yield output;
@@ -117,7 +169,7 @@ async function* executeV3({
117
169
  throw err;
118
170
  } finally {
119
171
  if (finalOutput) {
120
- const trajectoryFile = await trajectory.finalize(finalOutput, !executionError, executionError);
172
+ const trajectoryFile = await trajectory.finalize(finalOutput, !executionError, executionError, logTrajectory);
121
173
  if (trajectoryFile) {
122
174
  finalOutput.trajectoryFile = trajectoryFile;
123
175
  }
@@ -141,6 +193,8 @@ export function createAgenticRetrievalToolV3({
141
193
  user,
142
194
  role,
143
195
  model,
196
+ preselected,
197
+ memoryItems
144
198
  }: {
145
199
  contexts: ExuluContext[];
146
200
  rerankers: ExuluReranker[];
@@ -148,6 +202,8 @@ export function createAgenticRetrievalToolV3({
148
202
  role?: string;
149
203
  model?: LanguageModel;
150
204
  instructions?: string;
205
+ preselected?: string[];
206
+ memoryItems?: ExuluItem[];
151
207
  }): ExuluTool | undefined {
152
208
  const license = checkLicense();
153
209
  if (!license["agentic-retrieval"]) {
@@ -177,6 +233,12 @@ export function createAgenticRetrievalToolV3({
177
233
  type: "string",
178
234
  default: "none",
179
235
  },
236
+ {
237
+ name: "managed_context",
238
+ description: "Makes sure the user defines which items from which contexts the agentic retrieval tool will search in",
239
+ type: "boolean",
240
+ default: false,
241
+ },
180
242
  {
181
243
  name: "reasoning_model",
182
244
  description: "By default the agentic retrieval tool uses the model from the agent calling the tool, but you can overwrite this here for the reasoning phase",
@@ -189,35 +251,89 @@ export function createAgenticRetrievalToolV3({
189
251
  type: "string",
190
252
  default: "",
191
253
  },
254
+ {
255
+ name: "require_preselected_contexts",
256
+ description: "Require the user to preselect contexts before executing the tool, meaning the user will be asked to select the contexts they want to search in",
257
+ type: "boolean",
258
+ default: false,
259
+ },
260
+ {
261
+ name: "logging",
262
+ description: "Save a detailed markdown + JSON log of every retrieval execution to disk. Useful for debugging and evaluation.",
263
+ type: "boolean",
264
+ default: false,
265
+ },
192
266
  ...contexts.map((ctx) => ({
193
- name: ctx.id,
267
+ name: ctx.id + "_|_enabled",
194
268
  description: `Enable search in "${ctx.name}". ${ctx.description}`,
195
269
  type: "boolean" as const,
196
270
  default: true,
271
+ }
272
+ )),
273
+ ...contexts.map((ctx) => ({
274
+ name: `${ctx.id}_|_instructions`,
275
+ description: `Instructions for the retrieval agent about how to search in the ${ctx.name} context`,
276
+ type: "string" as const,
277
+ default: "",
278
+ })),
279
+ ...contexts.map((ctx) => ({
280
+ name: `${ctx.id}_|_priority`,
281
+ description: `Defines in which order the context should be searched in, the higher the number the higher the priority, if contexts have the same priority they are searched in parallel`,
282
+ type: "number" as const,
283
+ default: 0,
197
284
  })),
285
+ ...contexts.map((ctx) => ({
286
+ name: `${ctx.id}_|_max_results`,
287
+ description: `Defines the maximum number of results to return for the ${ctx.name} context`,
288
+ type: "number" as const,
289
+ default: 0,
290
+ })),
291
+ ...contexts.map((ctx) => ({
292
+ name: `${ctx.id}_|_max_steps`,
293
+ description: `Defines the maximum number of steps the agent is allowed to take when searching the ${ctx.name} context`,
294
+ type: "number" as const,
295
+ default: 0,
296
+ })),
297
+ ...contexts.map((ctx) => ({
298
+ name: `${ctx.id}_|_expand_chunks`,
299
+ description: `Defines if the agent automatically retrieves nearby chunks around the matched chunks, usefull if relevant content might be split up`,
300
+ type: "number" as const,
301
+ default: 0,
302
+ }))
198
303
  ],
199
304
  inputSchema: z.object({
200
- query: z.string().describe("The question or query to answer"),
305
+ userQuery: z.string().describe("The original unaltered question from the user"),
201
306
  userInstructions: z
202
307
  .string()
203
308
  .optional()
204
309
  .describe("Additional instructions from the user to guide retrieval"),
310
+ confirmedContextIds: z
311
+ .array(z.string())
312
+ .optional()
313
+ .describe(
314
+ "Knowledge base IDs explicitly confirmed by the user to be used in the retrieval. " +
315
+ "When presen only searches these contexts. "
316
+ )
205
317
  }),
206
318
  execute: async function* ({
207
- query,
319
+ userQuery,
208
320
  userInstructions,
321
+ confirmedContextIds,
209
322
  toolVariablesConfig,
323
+ sessionID,
210
324
  }: {
211
- query: string;
325
+ userQuery: string;
212
326
  userInstructions?: string;
327
+ confirmedContextIds?: string[];
213
328
  toolVariablesConfig?: Record<string, any>;
329
+ sessionID?: string;
214
330
  }) {
215
-
331
+
216
332
  /* ROADMAP:
217
333
  const app = exuluApp.get();
218
334
  let reasoningModel: LanguageModel | undefined = model;
219
335
  let searchModel: LanguageModel | undefined = model;
220
-
336
+
221
337
 
222
338
  if (toolVariablesConfig?.reasoning_model) {
223
339
  reasoningModel = app.provider(toolVariablesConfig.reasoning_model)?.model?.create({});
@@ -225,7 +341,7 @@ export function createAgenticRetrievalToolV3({
225
341
  throw new Error("Reasoning model not found");
226
342
  }
227
343
  }
228
-
344
+
229
345
  if (toolVariablesConfig?.search_model) {
230
346
  searchModel = app.provider(toolVariablesConfig.search_model);
231
347
  if (!searchModel) {
@@ -234,48 +350,108 @@ export function createAgenticRetrievalToolV3({
234
350
  } */
235
351
 
236
352
  if (!model) {
237
- throw new Error("Model is required for executing the agentic retrieval tool");
353
+ yield { result: "Model is required for executing the agentic retrieval tool" };
354
+ return;
238
355
  }
356
+
239
357
  let activeContexts = contexts;
240
358
  let configuredReranker: ExuluReranker | undefined;
241
359
  let configInstructions = "";
360
+ let logTrajectory = false;
361
+ let requiresPreselectedContexts = false;
362
+ let managedContextEnabled = false;
242
363
 
243
364
  if (toolVariablesConfig) {
244
365
  configInstructions = toolVariablesConfig["instructions"] ?? "";
366
+ logTrajectory =
367
+ toolVariablesConfig["logging"] === true ||
368
+ toolVariablesConfig["logging"] === "true";
369
+
370
+ managedContextEnabled = toolVariablesConfig["managed_context"] === true || toolVariablesConfig["managed_context"] === "true";
245
371
 
246
372
  activeContexts = contexts.filter(
247
373
  (ctx) =>
248
- toolVariablesConfig[ctx.id] === true ||
249
- toolVariablesConfig[ctx.id] === "true" ||
250
- toolVariablesConfig[ctx.id] === 1,
374
+ toolVariablesConfig[ctx.id + "_|_enabled"] === true ||
375
+ toolVariablesConfig[ctx.id + "_|_enabled"] === "true" ||
376
+ toolVariablesConfig[ctx.id + "_|_enabled"] === 1,
251
377
  );
252
378
  if (activeContexts.length === 0) activeContexts = contexts;
253
379
 
380
+ requiresPreselectedContexts = toolVariablesConfig["require_preselected_contexts"] === true || toolVariablesConfig["require_preselected_contexts"] === "true";
381
+
254
382
  const rerankerId = toolVariablesConfig["reranker"];
383
+
255
384
  if (rerankerId && rerankerId !== "none") {
256
385
  configuredReranker = rerankers.find((r) => r.id === rerankerId);
257
386
  }
258
387
  }
259
388
 
389
+ console.log("[EXULU] Managed context enabled:", managedContextEnabled);
390
+ console.log("[EXULU] Preselected item IDs:", preselected);
391
+
392
+ if (managedContextEnabled && !preselected?.length) {
393
+ console.log("[EXULU] Managed context was enabled for the agentic retrieval tool. This means that the user must preselect items that the agentic retrieval tool will search in, please notify the user to preselect items before executing the tool.");
394
+ yield { result: "Managed context was enabled for the agentic retrieval tool. This means that the user must preselect items that the agentic retrieval tool will search in, please notify the user to preselect items before executing the tool." };
395
+ return;
396
+ }
397
+
398
+ if (requiresPreselectedContexts && !confirmedContextIds?.length && !preselected?.length) {
399
+ console.log("[EXULU] The user must choose between the available contexts before executing the tool. The available contexts are: " + activeContexts.map((c) => c.id).join(", ") + ". If the question_ask tool is available use that to ask the user which contexts they want to search in, otherwise just ask them in plain text.");
400
+ yield { result: "The user must choose between the available contexts before executing the tool, the available contexts are: " + activeContexts.map((c) => c.id).join(", ") + ". If the question_ask tool is available use that to ask the user which contexts they want to search in, otherwise just ask them in plain text." };
401
+ return;
402
+ }
403
+
404
+ if (confirmedContextIds?.length) {
405
+ const confirmed = new Set(confirmedContextIds);
406
+ const filtered = activeContexts.filter((c) => confirmed.has(c.id));
407
+ if (filtered.length > 0) activeContexts = filtered;
408
+ }
409
+
260
410
  const combinedInstructions = [
261
- configInstructions ? `Configuration instructions: ${configInstructions}` : "",
262
- adminInstructions ? `Admin instructions: ${adminInstructions}` : "",
263
- userInstructions ? `User instructions: ${userInstructions}` : "",
411
+ configInstructions ? `
412
+ Configuration instructions:
413
+ <configuration_instructions>
414
+ ${configInstructions}
415
+ </configuration_instructions>
416
+ ` : "",
417
+ adminInstructions ? `
418
+ Admin instructions:
419
+ <admin_instructions>
420
+ ${adminInstructions}
421
+ </admin_instructions>
422
+ ` : "",
423
+ userInstructions ? `
424
+ User instructions:
425
+ <user_instructions>
426
+ ${userInstructions}
427
+ </user_instructions>
428
+ ` : "",
429
+ memoryItems ? `
430
+ Relevant memories (these are items that the agent has retrieved from the memory context and are relevant to the query):
431
+ <relevant_memories>
432
+ ${memoryItems?.map(item => JSON.stringify(item)).join("\n")}
433
+ </relevant_memories>
434
+ ` : "",
264
435
  ]
265
436
  .filter(Boolean)
266
437
  .join("\n");
267
438
 
268
439
  for await (const output of executeV3({
269
- query,
440
+ query: userQuery,
270
441
  contexts: activeContexts,
271
442
  reranker: configuredReranker,
443
+ toolVariablesConfig,
272
444
  model,
273
445
  user,
274
446
  role,
275
447
  customInstructions: combinedInstructions || undefined,
448
+ logTrajectory,
449
+ sessionId: sessionID,
450
+ preselectedItemIds: preselected,
276
451
  })) {
277
452
  yield { result: JSON.stringify(output) };
278
453
  }
454
+ return;
279
455
  },
280
456
  });
281
457
  }
@@ -0,0 +1,20 @@
1
+ import type { Tool as AITool } from "ai";
2
+
3
+ // Persists dynamic tools (get_more_content_from_X, get_X_page_N_content) created
4
+ // during an agentic retrieval run, keyed by session ID. This lets the outer chat
5
+ // agent call them directly on follow-up questions without re-running retrieval.
6
+ const registry = new Map<string, Map<string, AITool>>();
7
+
8
+ export function registerSessionTools(sessionId: string, tools: Record<string, AITool>): void {
9
+ const existing = registry.get(sessionId) ?? new Map();
10
+ for (const [name, toolDef] of Object.entries(tools)) {
11
+ existing.set(name, toolDef);
12
+ }
13
+ registry.set(sessionId, existing);
14
+ }
15
+
16
+ export function getSessionTools(sessionId: string): Record<string, AITool> {
17
+ const toolMap = registry.get(sessionId);
18
+ if (!toolMap || toolMap.size === 0) return {};
19
+ return Object.fromEntries(toolMap.entries());
20
+ }
@@ -25,9 +25,10 @@ another component will do that based on what you retrieve.
25
25
  Always respond in the SAME LANGUAGE as the user's query.
26
26
  Always write search queries in the SAME LANGUAGE as the user's query — do NOT translate to English.
27
27
 
28
- SEARCH APPROACH — go wide first, then deep:
29
- 1. First step: search broadly across all sources the system instructions indicate — do NOT
30
- pre-filter to a single context on step 1.
28
+ SEARCH APPROACH — one knowledge base at a time, then go deep:
29
+ 1. search_content and save_search_results accept ONE knowledge base per call. Make a separate
30
+ call for each knowledge base you need to cover never skip one. Search all relevant
31
+ knowledge bases before concluding, even if the first one already returned good results.
31
32
  2. After finding a relevant document, use get_more_content_from_{item} dynamic tools to load
32
33
  additional pages/sections. The specific answer is often NOT in the first retrieved chunk —
33
34
  always explore adjacent content before concluding.
@@ -44,9 +45,8 @@ export const AGGREGATE_INSTRUCTIONS = `
44
45
  ${BASE_INSTRUCTIONS}
45
46
 
46
47
  STRATEGY: This is a COUNTING or AGGREGATION query.
47
- - Use count_items_or_chunks exclusively
48
+ - Use count_items_or_chunks exclusively — it accepts multiple knowledge bases in one call for efficiency
48
49
  - Do NOT use search_content — it loads unnecessary data
49
- - Search ALL contexts in parallel in a single tool call
50
50
  - Return immediately after counting — one step is sufficient
51
51
  - If the count needs a content filter, use content_query parameter
52
52
  `.trim();
@@ -81,9 +81,23 @@ Search language:
81
81
  - Always write search queries in the SAME LANGUAGE as the user's query.
82
82
  - Do NOT translate the query to English — the documents are indexed in their original language.
83
83
 
84
- Step 1 — wide hybrid search (includeContent: true, limit 10):
85
- - Search broadly across all sources per the system instructions — do not limit to 1 context.
86
- - This gives you the best results from every relevant source at once.
84
+ Step 1 — match the opening move to what the query actually needs:
85
+
86
+ Query references a SPECIFIC NAMED DOCUMENT (product manual, titled report, named file):
87
+ → ALWAYS start with search_items_by_name — searches document name/title directly
88
+ → Only proceed to load content if the document is found
89
+
90
+ Query asks WHETHER a topic EXISTS or WHAT documents cover a topic (no specific title given):
91
+ → search_content with includeContent: false
92
+ → Returns matching document names without loading chunk text — efficient and precise
93
+ → Load content with dynamic get_{item}_page_{n}_content tools only if needed in step 2
94
+
95
+ Query asks for CONTENT itself (procedures, parameters, explanations, how-to):
96
+ → search_content with includeContent: true, limit 20, searchMethod: "hybrid"
97
+ → Make one call per knowledge base — search each separately before concluding
98
+
99
+ Query provides an EXACT TERM (error code, product code, ID, parameter name):
100
+ → search_content with searchMethod: "keyword"
87
101
 
88
102
  Step 2+ — depth and follow-up:
89
103
  - For any relevant document found with fewer than 5 chunks, use get_more_content_from_{item}
@@ -93,19 +107,9 @@ Step 2+ — depth and follow-up:
93
107
  - Try alternative phrasings if the first query doesn't surface the right answer.
94
108
 
95
109
  Product-specific filtering:
96
- - When the query mentions a specific product (e.g., "FST-3", "ECO"), you MAY use
97
- item_names: ["<product>"] on a follow-up search to narrow results — but only after an initial
110
+ - When the query mentions a specific named entity (product, model, version), you MAY use
111
+ item_names: ["<entity>"] on a follow-up search to narrow results — but only after an initial
98
112
  wide search. Never start with item_names filtering alone.
99
-
100
- Two-step approach — use includeContent: false first:
101
- - Only when you expect many results (>20) and need to identify the right document first.
102
- - Step 1: search_content with includeContent: false → see which documents/chunks match.
103
- - Step 2: use dynamic get_{item}_page_{n}_content tools to load specific pages.
104
-
105
- Search method selection:
106
- - hybrid (default): best for most queries
107
- - keyword: exact product codes, document IDs, error codes
108
- - semantic: conceptual questions, synonyms, paraphrasing
109
113
  `.trim();
110
114
 
111
115
  export const EXPLORATORY_INSTRUCTIONS = `
@@ -114,13 +118,13 @@ ${BASE_INSTRUCTIONS}
114
118
  STRATEGY: This is an EXPLORATORY query — general question requiring broad search.
115
119
 
116
120
  Recommended approach:
117
- 1. Start with a wide hybrid search across all relevant contexts (includeContent: true, limit: 10)
121
+ 1. Search each relevant knowledge base separately with hybrid search (includeContent: true, limit: 20) — one call per knowledge base
118
122
  2. If results are insufficient: try alternative search terms or different search method
119
123
  3. Use save_search_results + bash grep when you need to scan many results without context bloat
120
124
  4. Use dynamic get_more_content_from_{item} tools to read adjacent pages when a relevant item is found
121
125
 
122
126
  When to declare done:
123
- - You have retrieved chunks that cover the key aspects of the query
127
+ - You have retrieved chunks that cover the key aspects of the query from all relevant knowledge bases
124
128
  - OR you have tried 3+ different search strategies and found nothing relevant
125
129
 
126
130
  Do NOT use count_items_or_chunks for exploratory queries — the user wants content, not statistics.
@@ -140,7 +144,7 @@ export const STRATEGIES: Record<QueryType, StrategyConfig> = {
140
144
  },
141
145
  list: {
142
146
  queryType: "list",
143
- stepBudget: 2,
147
+ stepBudget: 3,
144
148
  retrieval_tools: ["count_items_or_chunks", "search_items_by_name", "search_content"],
145
149
  include_bash: false,
146
150
  instructions: LIST_INSTRUCTIONS,
@@ -154,7 +158,7 @@ export const STRATEGIES: Record<QueryType, StrategyConfig> = {
154
158
  },
155
159
  exploratory: {
156
160
  queryType: "exploratory",
157
- stepBudget: 4,
161
+ stepBudget: 5,
158
162
  retrieval_tools: [
159
163
  "count_items_or_chunks",
160
164
  "search_items_by_name",