voyageai-cli 1.26.0 → 1.26.1

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.
package/src/lib/chat.js CHANGED
@@ -3,8 +3,9 @@
3
3
  /**
4
4
  * Chat Orchestrator
5
5
  *
6
- * Coordinates the retrieval pipeline (embed search rerank)
6
+ * Coordinates the retrieval pipeline (embed -> search -> rerank)
7
7
  * with LLM generation and history management.
8
+ * Supports both pipeline mode (fixed RAG) and agent mode (tool-calling).
8
9
  */
9
10
 
10
11
  const { generateEmbeddings, apiRequest } = require('./api');
@@ -32,12 +33,12 @@ function resolveSourceLabel(doc) {
32
33
  return doc.source || meta.source || doc._id?.toString() || 'unknown';
33
34
  }
34
35
  const { getMongoCollection } = require('./mongo');
35
- const { buildMessages } = require('./prompt');
36
+ const { buildMessages, buildAgentMessages } = require('./prompt');
36
37
  const { getDefaultModel, DEFAULT_RERANK_MODEL } = require('./catalog');
37
38
  const { loadProject } = require('./project');
38
39
 
39
40
  /**
40
- * Perform retrieval: embed query vector search optional rerank.
41
+ * Perform retrieval: embed query -> vector search -> optional rerank.
41
42
  *
42
43
  * @param {object} params
43
44
  * @param {string} params.query - User's question
@@ -154,7 +155,7 @@ async function retrieve({ query, db, collection, opts = {} }) {
154
155
  }
155
156
 
156
157
  /**
157
- * Execute a single chat turn: retrieve context build prompt generate response.
158
+ * Execute a single chat turn: retrieve context -> build prompt -> generate response.
158
159
  *
159
160
  * @param {object} params
160
161
  * @param {string} params.query - User's question
@@ -246,7 +247,172 @@ async function* chatTurn({ query, db, collection, llm, history, opts = {} }) {
246
247
  };
247
248
  }
248
249
 
250
+ /**
251
+ * Execute a single agent chat turn: LLM decides which tools to call.
252
+ *
253
+ * @param {object} params
254
+ * @param {string} params.query - User's question
255
+ * @param {object} params.llm - LLM provider instance (must have chatWithTools)
256
+ * @param {import('./history').ChatHistory} params.history - Chat history
257
+ * @param {object} [params.opts] - Additional options
258
+ * @param {string} [params.opts.systemPrompt] - Override agent system prompt
259
+ * @param {number} [params.opts.maxIterations] - Max tool-calling iterations (default 10)
260
+ * @param {string} [params.opts.db] - Default database for tool calls
261
+ * @param {string} [params.opts.collection] - Default collection for tool calls
262
+ * @returns {AsyncGenerator<{type: string, data: any}>}
263
+ * Yields: { type: 'tool_call', data: { name, args, result, error, timeMs } }
264
+ * { type: 'chunk', data: string }
265
+ * { type: 'done', data: { fullResponse, toolCalls, metadata } }
266
+ */
267
+ async function* agentChatTurn({ query, llm, history, opts = {} }) {
268
+ const { getToolDefinitions, executeTool } = require('./tool-registry');
269
+
270
+ const maxIterations = opts.maxIterations || 10;
271
+ const start = Date.now();
272
+
273
+ // 1. Build initial messages
274
+ const initialMessages = buildAgentMessages({
275
+ query,
276
+ history: history.getMessagesWithBudget(8000),
277
+ systemPrompt: opts.systemPrompt,
278
+ });
279
+
280
+ // 2. Get tool definitions for this provider
281
+ const format = llm.name === 'anthropic' ? 'anthropic' : 'openai';
282
+ const tools = getToolDefinitions(format);
283
+
284
+ // Track messages for the tool-calling loop (mutable copy)
285
+ const messages = [...initialMessages];
286
+ const toolCallLog = [];
287
+
288
+ // 3. Agent loop
289
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
290
+ const response = await llm.chatWithTools(messages, tools);
291
+
292
+ // Text response: done
293
+ if (response.type === 'text') {
294
+ const fullResponse = response.content;
295
+ yield { type: 'chunk', data: fullResponse };
296
+
297
+ const totalTimeMs = Date.now() - start;
298
+
299
+ // Store turns in history
300
+ await history.addTurn({ role: 'user', content: query });
301
+ await history.addTurn({
302
+ role: 'assistant',
303
+ content: fullResponse,
304
+ metadata: {
305
+ mode: 'agent',
306
+ llmProvider: llm.name,
307
+ llmModel: llm.model,
308
+ toolCallCount: toolCallLog.length,
309
+ iterationCount: iteration + 1,
310
+ totalTimeMs,
311
+ },
312
+ });
313
+
314
+ yield {
315
+ type: 'done',
316
+ data: {
317
+ fullResponse,
318
+ toolCalls: toolCallLog,
319
+ metadata: {
320
+ mode: 'agent',
321
+ iterationCount: iteration + 1,
322
+ toolCallCount: toolCallLog.length,
323
+ totalTimeMs,
324
+ },
325
+ },
326
+ };
327
+ return;
328
+ }
329
+
330
+ // Tool calls: execute each and continue loop
331
+ if (response.type === 'tool_calls') {
332
+ // Append assistant tool-call message
333
+ messages.push(llm.formatAssistantToolCall(response));
334
+
335
+ for (const call of response.calls) {
336
+ const callStart = Date.now();
337
+ let result;
338
+ let error = null;
339
+
340
+ // Inject default db/collection if not provided
341
+ const args = { ...call.arguments };
342
+ if (opts.db && !args.db) args.db = opts.db;
343
+ if (opts.collection && !args.collection) args.collection = opts.collection;
344
+
345
+ try {
346
+ result = await executeTool(call.name, args);
347
+ } catch (err) {
348
+ error = err.message;
349
+ result = { content: [{ type: 'text', text: `Error: ${err.message}` }] };
350
+ }
351
+
352
+ const callTimeMs = Date.now() - callStart;
353
+
354
+ // Extract text content from result for the LLM
355
+ const resultText = result.content
356
+ ? result.content.map(c => c.text || JSON.stringify(c)).join('\n')
357
+ : JSON.stringify(result.structuredContent || {});
358
+
359
+ // Append tool result message
360
+ messages.push(llm.formatToolResult(call.id, resultText, !!error));
361
+
362
+ const logEntry = {
363
+ name: call.name,
364
+ args,
365
+ result: result.structuredContent || null,
366
+ error,
367
+ timeMs: callTimeMs,
368
+ };
369
+ toolCallLog.push(logEntry);
370
+
371
+ yield { type: 'tool_call', data: logEntry };
372
+ }
373
+
374
+ // Continue loop to let LLM see results and decide next action
375
+ continue;
376
+ }
377
+ }
378
+
379
+ // Max iterations reached: yield a fallback message
380
+ const fallback = 'I reached the maximum number of tool-calling iterations. Here is what I found so far based on the tool results above.';
381
+ yield { type: 'chunk', data: fallback };
382
+
383
+ await history.addTurn({ role: 'user', content: query });
384
+ await history.addTurn({
385
+ role: 'assistant',
386
+ content: fallback,
387
+ metadata: {
388
+ mode: 'agent',
389
+ llmProvider: llm.name,
390
+ llmModel: llm.model,
391
+ toolCallCount: toolCallLog.length,
392
+ iterationCount: maxIterations,
393
+ totalTimeMs: Date.now() - start,
394
+ maxIterationsReached: true,
395
+ },
396
+ });
397
+
398
+ yield {
399
+ type: 'done',
400
+ data: {
401
+ fullResponse: fallback,
402
+ toolCalls: toolCallLog,
403
+ metadata: {
404
+ mode: 'agent',
405
+ iterationCount: maxIterations,
406
+ toolCallCount: toolCallLog.length,
407
+ totalTimeMs: Date.now() - start,
408
+ maxIterationsReached: true,
409
+ },
410
+ },
411
+ };
412
+ }
413
+
249
414
  module.exports = {
250
415
  retrieve,
251
416
  chatTurn,
417
+ agentChatTurn,
252
418
  };
package/src/lib/llm.js CHANGED
@@ -6,8 +6,8 @@ const { loadProject } = require('./project');
6
6
  /**
7
7
  * LLM Provider Adapter
8
8
  *
9
- * Provider-agnostic LLM client with streaming support.
10
- * Uses native fetch zero new dependencies.
9
+ * Provider-agnostic LLM client with streaming and tool-calling support.
10
+ * Uses native fetch, zero new dependencies.
11
11
  */
12
12
 
13
13
  // Provider default models
@@ -107,6 +107,8 @@ class AnthropicProvider {
107
107
  }
108
108
  }
109
109
 
110
+ get supportsTools() { return true; }
111
+
110
112
  async *chat(messages, options = {}) {
111
113
  const model = options.model || this.model;
112
114
  const maxTokens = options.maxTokens || 4096;
@@ -156,6 +158,112 @@ class AnthropicProvider {
156
158
  });
157
159
  }
158
160
 
161
+ /**
162
+ * Non-streaming tool-calling request.
163
+ * @param {Array} messages - Conversation messages
164
+ * @param {Array} tools - Tool definitions in Anthropic format
165
+ * @param {object} [options]
166
+ * @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string}>}
167
+ */
168
+ async chatWithTools(messages, tools, options = {}) {
169
+ const model = options.model || this.model;
170
+ const maxTokens = options.maxTokens || 4096;
171
+
172
+ const systemMsg = messages.find(m => m.role === 'system');
173
+ const nonSystemMsgs = messages.filter(m => m.role !== 'system');
174
+
175
+ const body = {
176
+ model,
177
+ max_tokens: maxTokens,
178
+ stream: false,
179
+ messages: nonSystemMsgs,
180
+ tools,
181
+ };
182
+ if (systemMsg) {
183
+ body.system = systemMsg.content;
184
+ }
185
+
186
+ const res = await fetch(`${this.baseUrl}/v1/messages`, {
187
+ method: 'POST',
188
+ headers: {
189
+ 'Content-Type': 'application/json',
190
+ 'x-api-key': this.apiKey,
191
+ 'anthropic-version': '2023-06-01',
192
+ },
193
+ body: JSON.stringify(body),
194
+ });
195
+
196
+ if (!res.ok) {
197
+ const errBody = await res.text();
198
+ throw new Error(`Anthropic API error (${res.status}): ${errBody}`);
199
+ }
200
+
201
+ const json = await res.json();
202
+ const stopReason = json.stop_reason || 'end_turn';
203
+
204
+ // Check for tool_use blocks
205
+ const toolBlocks = (json.content || []).filter(b => b.type === 'tool_use');
206
+ if (toolBlocks.length > 0) {
207
+ return {
208
+ type: 'tool_calls',
209
+ calls: toolBlocks.map(b => ({
210
+ id: b.id,
211
+ name: b.name,
212
+ arguments: b.input,
213
+ })),
214
+ stopReason,
215
+ _raw: json.content,
216
+ };
217
+ }
218
+
219
+ // Text response
220
+ const textBlocks = (json.content || []).filter(b => b.type === 'text');
221
+ return {
222
+ type: 'text',
223
+ content: textBlocks.map(b => b.text).join(''),
224
+ stopReason,
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Format a tool-calling response as an assistant message.
230
+ * @param {object} response - Response from chatWithTools
231
+ * @returns {{role: string, content: Array}}
232
+ */
233
+ formatAssistantToolCall(response) {
234
+ if (response._raw) {
235
+ return { role: 'assistant', content: response._raw };
236
+ }
237
+ return {
238
+ role: 'assistant',
239
+ content: response.calls.map(c => ({
240
+ type: 'tool_use',
241
+ id: c.id,
242
+ name: c.name,
243
+ input: c.arguments,
244
+ })),
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Format a tool result as a user message.
250
+ * @param {string} callId - Tool call ID
251
+ * @param {string} content - Stringified result
252
+ * @param {boolean} [isError=false]
253
+ * @returns {{role: string, content: Array}}
254
+ */
255
+ formatToolResult(callId, content, isError = false) {
256
+ return {
257
+ role: 'user',
258
+ content: [{
259
+ type: 'tool_result',
260
+ tool_use_id: callId,
261
+ content,
262
+ ...(isError && { is_error: true }),
263
+ }],
264
+ };
265
+ }
266
+
159
267
  async ping() {
160
268
  try {
161
269
  const res = await fetch(`${this.baseUrl}/v1/messages`, {
@@ -202,6 +310,8 @@ class OpenAIProvider {
202
310
  }
203
311
  }
204
312
 
313
+ get supportsTools() { return true; }
314
+
205
315
  async *chat(messages, options = {}) {
206
316
  const model = options.model || this.model;
207
317
  const maxTokens = options.maxTokens || 4096;
@@ -242,6 +352,103 @@ class OpenAIProvider {
242
352
  });
243
353
  }
244
354
 
355
+ /**
356
+ * Non-streaming tool-calling request (OpenAI format).
357
+ * @param {Array} messages - Conversation messages
358
+ * @param {Array} tools - Tool definitions in OpenAI format
359
+ * @param {object} [options]
360
+ * @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string}>}
361
+ */
362
+ async chatWithTools(messages, tools, options = {}) {
363
+ const model = options.model || this.model;
364
+ const maxTokens = options.maxTokens || 4096;
365
+
366
+ const body = {
367
+ model,
368
+ max_tokens: maxTokens,
369
+ stream: false,
370
+ messages,
371
+ tools,
372
+ };
373
+
374
+ const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
375
+ method: 'POST',
376
+ headers: {
377
+ 'Content-Type': 'application/json',
378
+ 'Authorization': `Bearer ${this.apiKey}`,
379
+ },
380
+ body: JSON.stringify(body),
381
+ });
382
+
383
+ if (!res.ok) {
384
+ const errBody = await res.text();
385
+ throw new Error(`OpenAI API error (${res.status}): ${errBody}`);
386
+ }
387
+
388
+ const json = await res.json();
389
+ const choice = json.choices?.[0] || {};
390
+ const msg = choice.message || {};
391
+ const stopReason = choice.finish_reason || 'stop';
392
+
393
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
394
+ return {
395
+ type: 'tool_calls',
396
+ calls: msg.tool_calls.map(tc => ({
397
+ id: tc.id,
398
+ name: tc.function.name,
399
+ arguments: typeof tc.function.arguments === 'string'
400
+ ? JSON.parse(tc.function.arguments)
401
+ : tc.function.arguments,
402
+ })),
403
+ stopReason,
404
+ _raw: msg,
405
+ };
406
+ }
407
+
408
+ return {
409
+ type: 'text',
410
+ content: msg.content || '',
411
+ stopReason,
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Format a tool-calling response as an assistant message.
417
+ * @param {object} response - Response from chatWithTools
418
+ * @returns {{role: string, content: string|null, tool_calls: Array}}
419
+ */
420
+ formatAssistantToolCall(response) {
421
+ if (response._raw) {
422
+ return response._raw;
423
+ }
424
+ return {
425
+ role: 'assistant',
426
+ content: null,
427
+ tool_calls: response.calls.map(c => ({
428
+ id: c.id,
429
+ type: 'function',
430
+ function: {
431
+ name: c.name,
432
+ arguments: JSON.stringify(c.arguments),
433
+ },
434
+ })),
435
+ };
436
+ }
437
+
438
+ /**
439
+ * Format a tool result as a tool message.
440
+ * @param {string} callId - Tool call ID
441
+ * @param {string} content - Stringified result
442
+ * @returns {{role: string, tool_call_id: string, content: string}}
443
+ */
444
+ formatToolResult(callId, content) {
445
+ return {
446
+ role: 'tool',
447
+ tool_call_id: callId,
448
+ content,
449
+ };
450
+ }
451
+
245
452
  async ping() {
246
453
  try {
247
454
  const res = await fetch(`${this.baseUrl}/v1/models`, {
@@ -268,6 +475,8 @@ class OllamaProvider {
268
475
  this.baseUrl = config.baseUrl || PROVIDER_BASE_URLS.ollama;
269
476
  }
270
477
 
478
+ get supportsTools() { return true; }
479
+
271
480
  async *chat(messages, options = {}) {
272
481
  const model = options.model || this.model;
273
482
  const stream = options.stream !== false;
@@ -303,6 +512,99 @@ class OllamaProvider {
303
512
  });
304
513
  }
305
514
 
515
+ /**
516
+ * Non-streaming tool-calling request (OpenAI-compatible format).
517
+ * @param {Array} messages - Conversation messages
518
+ * @param {Array} tools - Tool definitions in OpenAI format
519
+ * @param {object} [options]
520
+ * @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string}>}
521
+ */
522
+ async chatWithTools(messages, tools, options = {}) {
523
+ const model = options.model || this.model;
524
+
525
+ const body = {
526
+ model,
527
+ stream: false,
528
+ messages,
529
+ tools,
530
+ };
531
+
532
+ const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
533
+ method: 'POST',
534
+ headers: { 'Content-Type': 'application/json' },
535
+ body: JSON.stringify(body),
536
+ });
537
+
538
+ if (!res.ok) {
539
+ const errBody = await res.text();
540
+ throw new Error(`Ollama API error (${res.status}): ${errBody}`);
541
+ }
542
+
543
+ const json = await res.json();
544
+ const choice = json.choices?.[0] || {};
545
+ const msg = choice.message || {};
546
+ const stopReason = choice.finish_reason || 'stop';
547
+
548
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
549
+ return {
550
+ type: 'tool_calls',
551
+ calls: msg.tool_calls.map(tc => ({
552
+ id: tc.id || `call_${Date.now()}`,
553
+ name: tc.function.name,
554
+ arguments: typeof tc.function.arguments === 'string'
555
+ ? JSON.parse(tc.function.arguments)
556
+ : tc.function.arguments,
557
+ })),
558
+ stopReason,
559
+ _raw: msg,
560
+ };
561
+ }
562
+
563
+ return {
564
+ type: 'text',
565
+ content: msg.content || '',
566
+ stopReason,
567
+ };
568
+ }
569
+
570
+ /**
571
+ * Format a tool-calling response as an assistant message.
572
+ * (Same as OpenAI format since Ollama uses OpenAI-compatible API)
573
+ * @param {object} response - Response from chatWithTools
574
+ * @returns {{role: string, content: string|null, tool_calls: Array}}
575
+ */
576
+ formatAssistantToolCall(response) {
577
+ if (response._raw) {
578
+ return response._raw;
579
+ }
580
+ return {
581
+ role: 'assistant',
582
+ content: null,
583
+ tool_calls: response.calls.map(c => ({
584
+ id: c.id,
585
+ type: 'function',
586
+ function: {
587
+ name: c.name,
588
+ arguments: JSON.stringify(c.arguments),
589
+ },
590
+ })),
591
+ };
592
+ }
593
+
594
+ /**
595
+ * Format a tool result as a tool message.
596
+ * @param {string} callId - Tool call ID
597
+ * @param {string} content - Stringified result
598
+ * @returns {{role: string, tool_call_id: string, content: string}}
599
+ */
600
+ formatToolResult(callId, content) {
601
+ return {
602
+ role: 'tool',
603
+ tool_call_id: callId,
604
+ content,
605
+ };
606
+ }
607
+
306
608
  async ping() {
307
609
  try {
308
610
  const res = await fetch(`${this.baseUrl}/v1/models`);
package/src/lib/mongo.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Get MongoDB URI or exit with a helpful error.
4
+ * Get MongoDB URI or throw with a helpful error.
5
5
  * Checks: env var → config file.
6
6
  * @returns {string}
7
7
  */
@@ -9,11 +9,11 @@ function requireMongoUri() {
9
9
  const { getConfigValue } = require('./config');
10
10
  const uri = process.env.MONGODB_URI || getConfigValue('mongodbUri');
11
11
  if (!uri) {
12
- console.error('Error: MONGODB_URI is not set.');
13
- console.error('');
14
- console.error('Option 1: export MONGODB_URI="mongodb+srv://user:pass@cluster.mongodb.net/"');
15
- console.error('Option 2: vai config set mongodb-uri "mongodb+srv://user:pass@cluster.mongodb.net/"');
16
- process.exit(1);
12
+ throw new Error(
13
+ 'MONGODB_URI is not set.\n' +
14
+ 'Option 1: export MONGODB_URI="mongodb+srv://user:pass@cluster.mongodb.net/"\n' +
15
+ 'Option 2: vai config set mongodb-uri "mongodb+srv://user:pass@cluster.mongodb.net/"'
16
+ );
17
17
  }
18
18
  return uri;
19
19
  }
package/src/lib/prompt.js CHANGED
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * Constructs the message array sent to the LLM from
7
7
  * retrieved documents, conversation history, and user query.
8
+ * Supports both pipeline mode (fixed RAG) and agent mode (tool-calling).
8
9
  */
9
10
 
10
11
  const DEFAULT_SYSTEM_PROMPT = `You are an assistant powered by a retrieval-augmented generation (RAG) pipeline built with Voyage AI embeddings and MongoDB Atlas Vector Search. Your answers are grounded in documents retrieved from the user's knowledge base.
@@ -23,6 +24,32 @@ const DEFAULT_SYSTEM_PROMPT = `You are an assistant powered by a retrieval-augme
23
24
  4. Be concise. Prefer short, direct answers. Use lists or structure when it aids clarity.
24
25
  5. For follow-up questions, rely on the newly retrieved context for that turn. Prior context may be stale.`;
25
26
 
27
+ const AGENT_SYSTEM_PROMPT = `You are an AI assistant with access to a suite of Voyage AI and MongoDB Atlas tools. You can search knowledge bases, embed text, compare documents, explore collections, and more. Use your tools to answer the user's questions accurately.
28
+
29
+ ## Available tools
30
+
31
+ - **vai_query**: Full RAG pipeline (embed, vector search, rerank). Use this as your primary tool for answering questions from the knowledge base.
32
+ - **vai_search**: Raw vector search without reranking. Faster, useful for exploratory queries.
33
+ - **vai_rerank**: Rerank candidate documents against a query. Use when you have documents from another source.
34
+ - **vai_embed**: Get the raw embedding vector for a text. Use for debugging or custom logic.
35
+ - **vai_similarity**: Compare two texts semantically. Returns a cosine similarity score.
36
+ - **vai_collections**: List available collections with document counts and vector index info. Call this first if you need to discover which knowledge bases exist.
37
+ - **vai_models**: List available Voyage AI models with pricing. Use when the user asks about model options.
38
+ - **vai_topics**: List educational topics that vai can explain.
39
+ - **vai_explain**: Get a detailed explanation of a topic (embeddings, RAG, vector search, etc).
40
+ - **vai_estimate**: Estimate costs for embedding and query operations.
41
+ - **vai_ingest**: Add new content to a collection (chunk, embed, store).
42
+
43
+ ## Answering rules
44
+
45
+ 1. Always use tools to retrieve information before answering. Do not guess or make up facts.
46
+ 2. Cite sources from tool results using [Source: <label>] format.
47
+ 3. You may call multiple tools in sequence. For example: vai_collections to discover collections, then vai_query to search one.
48
+ 4. If a tool returns no results or errors, explain what happened and suggest alternatives.
49
+ 5. Be concise. Prefer short, direct answers. Use lists or structure when it aids clarity.
50
+ 6. For questions about Voyage AI concepts, use vai_explain rather than answering from memory.
51
+ 7. If the user asks you to ingest content, use vai_ingest. Confirm what was stored.`;
52
+
26
53
  /**
27
54
  * Format retrieved documents into a context block.
28
55
  * @param {Array<{source: string, text: string, score: number}>} docs
@@ -66,7 +93,7 @@ ${customPrompt}`;
66
93
  }
67
94
 
68
95
  /**
69
- * Build the message array for the LLM.
96
+ * Build the message array for the LLM (pipeline mode).
70
97
  *
71
98
  * @param {object} params
72
99
  * @param {string} params.query - Current user question
@@ -103,9 +130,41 @@ function buildMessages({ query, contextDocs = [], history = [], systemPrompt })
103
130
  return messages;
104
131
  }
105
132
 
133
+ /**
134
+ * Build the message array for agent mode (no context injection).
135
+ * The agent fetches its own context via tool calls.
136
+ *
137
+ * @param {object} params
138
+ * @param {string} params.query - Current user question
139
+ * @param {Array} [params.history] - Previous conversation turns [{role, content}]
140
+ * @param {string} [params.systemPrompt] - Override the agent system prompt
141
+ * @returns {Array<{role: string, content: string}>}
142
+ */
143
+ function buildAgentMessages({ query, history = [], systemPrompt }) {
144
+ const messages = [];
145
+
146
+ // 1. Agent system prompt
147
+ messages.push({
148
+ role: 'system',
149
+ content: systemPrompt || AGENT_SYSTEM_PROMPT,
150
+ });
151
+
152
+ // 2. Conversation history
153
+ for (const turn of history) {
154
+ messages.push({ role: turn.role, content: turn.content });
155
+ }
156
+
157
+ // 3. Current user message (no context injection, agent decides what to fetch)
158
+ messages.push({ role: 'user', content: query });
159
+
160
+ return messages;
161
+ }
162
+
106
163
  module.exports = {
107
164
  DEFAULT_SYSTEM_PROMPT,
165
+ AGENT_SYSTEM_PROMPT,
108
166
  buildSystemPrompt,
109
167
  formatContextBlock,
110
168
  buildMessages,
169
+ buildAgentMessages,
111
170
  };