@exulu/backend 1.53.1 → 1.55.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.
@@ -0,0 +1,375 @@
1
+ import { z } from "zod";
2
+ import { createBashTool } from "bash-tool";
3
+ import type { LanguageModel, Tool } from "ai";
4
+ import type { ExuluContext } from "@SRC/exulu/context";
5
+ import type { ExuluReranker } from "@SRC/exulu/reranker";
6
+ import { ExuluTool } from "@SRC/exulu/tool";
7
+ import type { User } from "@EXULU_TYPES/models/user";
8
+ import { checkLicense } from "@EE/entitlements";
9
+ import { ContextSampler } from "./context-sampler";
10
+ import { classifyQuery } from "./classifier";
11
+ import { createRetrievalTools, parseGlobalItemIds } from "./tools";
12
+ import { STRATEGIES } from "./strategies";
13
+ import { runAgentLoop } from "./agent-loop";
14
+ import { TrajectoryLogger } from "./trajectory";
15
+ import type { AgenticRetrievalOutput, QueryType } from "./types";
16
+
17
+ // Module-level sampler — shared across all tool instances so the cache is warm
18
+ // across requests within the same process.
19
+ const sampler = new ContextSampler();
20
+
21
+ async function* executeV3({
22
+ query,
23
+ contexts,
24
+ reranker,
25
+ model,
26
+ user,
27
+ role,
28
+ customInstructions,
29
+ logTrajectory,
30
+ sessionId,
31
+ preselectedItemIds,
32
+ }: {
33
+ query: string;
34
+ contexts: ExuluContext[];
35
+ reranker?: ExuluReranker;
36
+ model: LanguageModel;
37
+ user?: User;
38
+ role?: string;
39
+ customInstructions?: string;
40
+ logTrajectory?: boolean;
41
+ sessionId?: string;
42
+ preselectedItemIds?: string[];
43
+ }): AsyncGenerator<AgenticRetrievalOutput> {
44
+ // ── 1. Parse preselected item IDs (global format: "<context_id>/<item_id>") ─
45
+ const preselectedByContext = preselectedItemIds?.length
46
+ ? parseGlobalItemIds(preselectedItemIds)
47
+ : undefined;
48
+
49
+ // When preselection is active, restrict to only contexts that have selected items
50
+ const activeContexts = preselectedByContext?.size
51
+ ? contexts.filter((c) => preselectedByContext.has(c.id))
52
+ : contexts;
53
+
54
+ // ── 2. Sample example records from each context (cached) ──────────────────
55
+ console.log("[EXULU] v3 — sampling contexts");
56
+ const samples = await sampler.getSamples(activeContexts, user, role);
57
+
58
+ // ── 3. Classify query (single fast LLM call) ──────────────────────────────
59
+ console.log("[EXULU] v3 — classifying query");
60
+ let classification;
61
+ try {
62
+ classification = await classifyQuery(query, activeContexts, samples, model);
63
+ } catch (err) {
64
+ console.warn("[EXULU] v3 — classification failed, falling back to exploratory:", err);
65
+ classification = {
66
+ queryType: "exploratory" as QueryType,
67
+ language: "eng",
68
+ suggestedContextIds: [],
69
+ };
70
+ }
71
+ console.log("[EXULU] v3 — classified as:", classification);
72
+
73
+ // ── 4. Select strategy ────────────────────────────────────────────────────
74
+ const strategy = STRATEGIES[classification.queryType];
75
+
76
+ // Build context guidance: the classifier is a priority hint, not a hard filter.
77
+ // All contexts remain available so the agent can fall back if suggested ones miss.
78
+ const suggestedIds = classification.suggestedContextIds;
79
+ const fallbackIds = activeContexts
80
+ .filter((c) => !suggestedIds.includes(c.id))
81
+ .map((c) => c.id);
82
+ const contextBase =
83
+ suggestedIds.length > 0
84
+ ? `Suggested priority contexts: [${suggestedIds.join(", ")}]. Also available: [${fallbackIds.join(", ")}]. Custom instructions may require searching additional or all contexts — follow them.`
85
+ : `All contexts available: [${activeContexts.map((c) => c.id).join(", ")}].`;
86
+
87
+ const preselectedNote = preselectedByContext?.size
88
+ ? `\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.`
89
+ : "";
90
+
91
+ const contextGuidance = contextBase + preselectedNote;
92
+
93
+ // ── 5. Initialize tools ───────────────────────────────────────────────────
94
+ const bashToolkit = await createBashTool({ files: {} });
95
+
96
+ const retrievalTools = createRetrievalTools({
97
+ contexts: activeContexts,
98
+ user,
99
+ role,
100
+ updateVirtualFiles: (files) => bashToolkit.sandbox.writeFiles(files),
101
+ preselectedItemsByContext: preselectedByContext,
102
+ });
103
+
104
+ // Build the tool set for this strategy
105
+ const activeTools: Record<string, Tool> = {};
106
+ for (const name of strategy.retrieval_tools) {
107
+ if (name in retrievalTools) {
108
+ activeTools[name] = retrievalTools[name as keyof typeof retrievalTools];
109
+ }
110
+ }
111
+ if (strategy.include_bash) {
112
+ Object.assign(activeTools, bashToolkit.tools);
113
+ }
114
+
115
+ // ── 6. Set up trajectory logging ──────────────────────────────────────────
116
+ const trajectory = new TrajectoryLogger(query, classification, undefined, preselectedItemIds);
117
+
118
+ // ── 7. Run agent loop ─────────────────────────────────────────────────────
119
+ let finalOutput: AgenticRetrievalOutput | undefined;
120
+ let executionError: Error | undefined;
121
+
122
+ try {
123
+ for await (const output of runAgentLoop({
124
+ query,
125
+ strategy,
126
+ tools: activeTools,
127
+ model,
128
+ reranker,
129
+ contextGuidance,
130
+ customInstructions,
131
+ classification,
132
+ sessionId,
133
+ onStepComplete: (step) => trajectory.recordStep(step),
134
+ onTrajectoryStep: (data) => trajectory.recordRichStep(data),
135
+ })) {
136
+ finalOutput = output;
137
+ yield output;
138
+ }
139
+ } catch (err) {
140
+ executionError = err as Error;
141
+ console.error("[EXULU] v3 — agent loop error:", err);
142
+ throw err;
143
+ } finally {
144
+ if (finalOutput) {
145
+ const trajectoryFile = await trajectory.finalize(finalOutput, !executionError, executionError, logTrajectory);
146
+ if (trajectoryFile) {
147
+ finalOutput.trajectoryFile = trajectoryFile;
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Creates the v3 ExuluTool for agentic context retrieval.
155
+ *
156
+ * Compared to v2:
157
+ * - Single LLM call per step (vs two in v2)
158
+ * - Query classification upfront → strategy-based step budget (1–3 vs hardcoded 2)
159
+ * - Context example records sampled at init and cached
160
+ * - Strategy-specific instructions and tool sets
161
+ */
162
+ export function createAgenticRetrievalToolV3({
163
+ contexts,
164
+ instructions: adminInstructions,
165
+ rerankers,
166
+ user,
167
+ role,
168
+ model,
169
+ preselectedItemIds,
170
+ }: {
171
+ contexts: ExuluContext[];
172
+ rerankers: ExuluReranker[];
173
+ user?: User;
174
+ role?: string;
175
+ model?: LanguageModel;
176
+ instructions?: string;
177
+ preselectedItemIds?: string[];
178
+ }): ExuluTool | undefined {
179
+ const license = checkLicense();
180
+ if (!license["agentic-retrieval"]) {
181
+ console.warn("[EXULU] Not licensed for agentic retrieval");
182
+ return undefined;
183
+ }
184
+
185
+ const contextNames = contexts.map((c) => c.id).join(", ");
186
+
187
+ return new ExuluTool({
188
+ id: "agentic_context_search",
189
+ name: "Agentic Context Search",
190
+ description: `Intelligent context search with query classification, strategy-based retrieval, and virtual filesystem filtering. Searches: ${contextNames}`,
191
+ category: "contexts",
192
+ needsApproval: false,
193
+ type: "context",
194
+ config: [
195
+ {
196
+ name: "instructions",
197
+ description: "Custom instructions for the retrieval agent",
198
+ type: "string",
199
+ default: "",
200
+ },
201
+ {
202
+ name: "reranker",
203
+ description: "Reranker to use for result ranking",
204
+ type: "string",
205
+ default: "none",
206
+ },
207
+ {
208
+ name: "managed_context",
209
+ description: "Makes sure the user defines which items from which contexts the agentic retrieval tool will search in",
210
+ type: "boolean",
211
+ default: false,
212
+ },
213
+ {
214
+ name: "reasoning_model",
215
+ 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",
216
+ type: "string",
217
+ default: "",
218
+ },
219
+ {
220
+ name: "search_model",
221
+ description: "By default the agentic retrieval tool uses the model from the agent calling the tool, but you can overwrite this here for the search phase",
222
+ type: "string",
223
+ default: "",
224
+ },
225
+ {
226
+ name: "require_preselected_contexts",
227
+ 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",
228
+ type: "boolean",
229
+ default: false,
230
+ },
231
+ {
232
+ name: "log_trajectories",
233
+ description: "Save a detailed markdown + JSON log of every retrieval execution to disk. Useful for debugging and evaluation.",
234
+ type: "boolean",
235
+ default: false,
236
+ },
237
+ ...contexts.map((ctx) => ({
238
+ name: ctx.id,
239
+ description: `Enable search in "${ctx.name}". ${ctx.description}`,
240
+ type: "boolean" as const,
241
+ default: true,
242
+ })),
243
+ ],
244
+ inputSchema: z.object({
245
+ query: z.string().describe("The question or query to answer"),
246
+ userInstructions: z
247
+ .string()
248
+ .optional()
249
+ .describe("Additional instructions from the user to guide retrieval"),
250
+ confirmedContextIds: z
251
+ .array(z.string())
252
+ .optional()
253
+ .describe(
254
+ "Knowledge base IDs explicitly confirmed by the user to be used in the retrieval. " +
255
+ "When presen only searches these contexts. "
256
+ )
257
+ }),
258
+ execute: async function* ({
259
+ query,
260
+ userInstructions,
261
+ confirmedContextIds,
262
+ toolVariablesConfig,
263
+ sessionID,
264
+ }: {
265
+ query: string;
266
+ userInstructions?: string;
267
+ confirmedContextIds?: string[];
268
+ toolVariablesConfig?: Record<string, any>;
269
+ sessionID?: string;
270
+ }) {
271
+
272
+ /* ROADMAP:
273
+ const app = exuluApp.get();
274
+ let reasoningModel: LanguageModel | undefined = model;
275
+ let searchModel: LanguageModel | undefined = model;
276
+
277
+
278
+ if (toolVariablesConfig?.reasoning_model) {
279
+ reasoningModel = app.provider(toolVariablesConfig.reasoning_model)?.model?.create({});
280
+ if (!reasoningModel) {
281
+ throw new Error("Reasoning model not found");
282
+ }
283
+ }
284
+
285
+ if (toolVariablesConfig?.search_model) {
286
+ searchModel = app.provider(toolVariablesConfig.search_model);
287
+ if (!searchModel) {
288
+ throw new Error("Search model not found");
289
+ }
290
+ } */
291
+
292
+ if (!model) {
293
+ yield { result: "Model is required for executing the agentic retrieval tool" };
294
+ return;
295
+ }
296
+
297
+ let activeContexts = contexts;
298
+ let configuredReranker: ExuluReranker | undefined;
299
+ let configInstructions = "";
300
+ let logTrajectory = false;
301
+ let requiresPreselectedContexts = false;
302
+ let managedContextEnabled = false;
303
+
304
+ if (toolVariablesConfig) {
305
+ configInstructions = toolVariablesConfig["instructions"] ?? "";
306
+ logTrajectory =
307
+ toolVariablesConfig["log_trajectories"] === true ||
308
+ toolVariablesConfig["log_trajectories"] === "true";
309
+
310
+ managedContextEnabled = toolVariablesConfig["managed_context"] === true || toolVariablesConfig["managed_context"] === "true";
311
+
312
+ activeContexts = contexts.filter(
313
+ (ctx) =>
314
+ toolVariablesConfig[ctx.id] === true ||
315
+ toolVariablesConfig[ctx.id] === "true" ||
316
+ toolVariablesConfig[ctx.id] === 1,
317
+ );
318
+ if (activeContexts.length === 0) activeContexts = contexts;
319
+
320
+ requiresPreselectedContexts = toolVariablesConfig["require_preselected_contexts"] === true || toolVariablesConfig["require_preselected_contexts"] === "true";
321
+
322
+ const rerankerId = toolVariablesConfig["reranker"];
323
+
324
+ if (rerankerId && rerankerId !== "none") {
325
+ configuredReranker = rerankers.find((r) => r.id === rerankerId);
326
+ }
327
+ }
328
+
329
+ console.log("[EXULU] Managed context enabled:", managedContextEnabled);
330
+ console.log("[EXULU] Preselected item IDs:", preselectedItemIds);
331
+
332
+ if (managedContextEnabled && !preselectedItemIds?.length) {
333
+ 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.");
334
+ 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." };
335
+ return;
336
+ }
337
+
338
+ if (requiresPreselectedContexts && !confirmedContextIds?.length && !preselectedItemIds?.length) {
339
+ 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.");
340
+ 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." };
341
+ return;
342
+ }
343
+
344
+ if (confirmedContextIds?.length) {
345
+ const confirmed = new Set(confirmedContextIds);
346
+ const filtered = activeContexts.filter((c) => confirmed.has(c.id));
347
+ if (filtered.length > 0) activeContexts = filtered;
348
+ }
349
+
350
+ const combinedInstructions = [
351
+ configInstructions ? `Configuration instructions: ${configInstructions}` : "",
352
+ adminInstructions ? `Admin instructions: ${adminInstructions}` : "",
353
+ userInstructions ? `User instructions: ${userInstructions}` : "",
354
+ ]
355
+ .filter(Boolean)
356
+ .join("\n");
357
+
358
+ for await (const output of executeV3({
359
+ query,
360
+ contexts: activeContexts,
361
+ reranker: configuredReranker,
362
+ model,
363
+ user,
364
+ role,
365
+ customInstructions: combinedInstructions || undefined,
366
+ logTrajectory,
367
+ sessionId: sessionID,
368
+ preselectedItemIds,
369
+ })) {
370
+ yield { result: JSON.stringify(output) };
371
+ }
372
+ return;
373
+ },
374
+ });
375
+ }
@@ -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
+ }
@@ -0,0 +1,171 @@
1
+ import type { QueryType } from "./types";
2
+
3
+ export interface StrategyConfig {
4
+ queryType: QueryType;
5
+ /** How many agent loop iterations are allowed */
6
+ stepBudget: number;
7
+ /** Which tool names from createRetrievalTools() are exposed */
8
+ retrieval_tools: string[];
9
+ /** Whether bash tools should be included */
10
+ include_bash: boolean;
11
+ instructions: string;
12
+ }
13
+
14
+ // ──────────────────────────────────────────────────────────────────────────────
15
+ // Per-strategy instructions
16
+ // These are intentionally in separate exported strings so they can be tuned
17
+ // without touching the rest of the code.
18
+ // ──────────────────────────────────────────────────────────────────────────────
19
+
20
+ export const BASE_INSTRUCTIONS = `
21
+ You are an intelligent retrieval assistant. Your only job is to retrieve relevant information from
22
+ the available knowledge bases and return it. You do NOT answer the user's question yourself —
23
+ another component will do that based on what you retrieve.
24
+
25
+ Always respond in the SAME LANGUAGE as the user's query.
26
+ Always write search queries in the SAME LANGUAGE as the user's query — do NOT translate to English.
27
+
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.
32
+ 2. After finding a relevant document, use get_more_content_from_{item} dynamic tools to load
33
+ additional pages/sections. The specific answer is often NOT in the first retrieved chunk —
34
+ always explore adjacent content before concluding.
35
+ 3. If your first search returned related but not specific enough content, run a follow-up search
36
+ with more targeted terms or an alternative phrasing of the key concept.
37
+
38
+ Never give up after a single search — always try at least one follow-up before finishing.
39
+
40
+ When retrieval is complete (sufficient content found OR all reasonable strategies exhausted),
41
+ you MUST call the finish_retrieval tool — do NOT write a text conclusion.
42
+ `.trim();
43
+
44
+ export const AGGREGATE_INSTRUCTIONS = `
45
+ ${BASE_INSTRUCTIONS}
46
+
47
+ STRATEGY: This is a COUNTING or AGGREGATION query.
48
+ - Use count_items_or_chunks exclusively — it accepts multiple knowledge bases in one call for efficiency
49
+ - Do NOT use search_content — it loads unnecessary data
50
+ - Return immediately after counting — one step is sufficient
51
+ - If the count needs a content filter, use content_query parameter
52
+ `.trim();
53
+
54
+ export const LIST_INSTRUCTIONS = `
55
+ ${BASE_INSTRUCTIONS}
56
+
57
+ STRATEGY: This is a LISTING query — the user wants a list of matching items/documents.
58
+
59
+ Decision tree:
60
+ - "List documents BY NAME/TITLE" → search_items_by_name
61
+ - "List documents ABOUT a topic/subject" → search_content with includeContent: false
62
+
63
+ Always prefer search_content with includeContent: false for content-based listing:
64
+ - This searches actual document content and returns matching document names
65
+ - It does NOT load chunk text, keeping token use minimal
66
+ - Dynamic page-content tools will be created if the user needs to drill into specific documents
67
+
68
+ When to use search_items_by_name:
69
+ - Query explicitly mentions document titles or filename patterns
70
+ - User asks for documents whose NAME contains a keyword
71
+
72
+ Never set includeContent: true for a listing query unless explicitly asked for the actual text.
73
+ `.trim();
74
+
75
+ export const TARGETED_INSTRUCTIONS = `
76
+ ${BASE_INSTRUCTIONS}
77
+
78
+ STRATEGY: This is a TARGETED query — the user wants specific information from a document.
79
+
80
+ Search language:
81
+ - Always write search queries in the SAME LANGUAGE as the user's query.
82
+ - Do NOT translate the query to English — the documents are indexed in their original language.
83
+
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"
101
+
102
+ Step 2+ — depth and follow-up:
103
+ - For any relevant document found with fewer than 5 chunks, use get_more_content_from_{item}
104
+ to load adjacent sections. The specific answer is often in a nearby chunk, not the top result.
105
+ - If the topic was found but the exact detail is missing, search again with more specific terms
106
+ (e.g., add a key technical term, parameter name, or section keyword to the query).
107
+ - Try alternative phrasings if the first query doesn't surface the right answer.
108
+
109
+ Product-specific filtering:
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
112
+ wide search. Never start with item_names filtering alone.
113
+ `.trim();
114
+
115
+ export const EXPLORATORY_INSTRUCTIONS = `
116
+ ${BASE_INSTRUCTIONS}
117
+
118
+ STRATEGY: This is an EXPLORATORY query — general question requiring broad search.
119
+
120
+ Recommended approach:
121
+ 1. Search each relevant knowledge base separately with hybrid search (includeContent: true, limit: 20) — one call per knowledge base
122
+ 2. If results are insufficient: try alternative search terms or different search method
123
+ 3. Use save_search_results + bash grep when you need to scan many results without context bloat
124
+ 4. Use dynamic get_more_content_from_{item} tools to read adjacent pages when a relevant item is found
125
+
126
+ When to declare done:
127
+ - You have retrieved chunks that cover the key aspects of the query from all relevant knowledge bases
128
+ - OR you have tried 3+ different search strategies and found nothing relevant
129
+
130
+ Do NOT use count_items_or_chunks for exploratory queries — the user wants content, not statistics.
131
+ `.trim();
132
+
133
+ // ──────────────────────────────────────────────────────────────────────────────
134
+ // Strategy map
135
+ // ──────────────────────────────────────────────────────────────────────────────
136
+
137
+ export const STRATEGIES: Record<QueryType, StrategyConfig> = {
138
+ aggregate: {
139
+ queryType: "aggregate",
140
+ stepBudget: 1,
141
+ retrieval_tools: ["count_items_or_chunks"],
142
+ include_bash: false,
143
+ instructions: AGGREGATE_INSTRUCTIONS,
144
+ },
145
+ list: {
146
+ queryType: "list",
147
+ stepBudget: 3,
148
+ retrieval_tools: ["count_items_or_chunks", "search_items_by_name", "search_content"],
149
+ include_bash: false,
150
+ instructions: LIST_INSTRUCTIONS,
151
+ },
152
+ targeted: {
153
+ queryType: "targeted",
154
+ stepBudget: 5,
155
+ retrieval_tools: ["search_items_by_name", "search_content", "save_search_results"],
156
+ include_bash: true,
157
+ instructions: TARGETED_INSTRUCTIONS,
158
+ },
159
+ exploratory: {
160
+ queryType: "exploratory",
161
+ stepBudget: 5,
162
+ retrieval_tools: [
163
+ "count_items_or_chunks",
164
+ "search_items_by_name",
165
+ "search_content",
166
+ "save_search_results",
167
+ ],
168
+ include_bash: true,
169
+ instructions: EXPLORATORY_INSTRUCTIONS,
170
+ },
171
+ };