omnikey-cli 1.0.16 → 1.0.18

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/README.md CHANGED
@@ -6,6 +6,7 @@ A command-line tool for onboarding users to the Omnikey open-source app, configu
6
6
 
7
7
  OmnikeyAI is a productivity tool that helps you quickly rewrite selected text using your preferred LLM provider. The CLI allows you to configure and run the backend daemon on your local machine, manage your API keys, choose your LLM provider (OpenAI, Anthropic, or Gemini), and optionally configure the web search tool.
8
8
 
9
+ - Website: [omnikeyai.ca](https://omnikeyai.ca)
9
10
  - For more details about the app and its features, see the [main README](https://github.com/GurinderRawala/OmniKey-AI).
10
11
  - Download the latest macOS app here: [Download OmniKeyAI for macOS](https://omnikeyai-saas-fmytqc3dra-uc.a.run.app/macos/download)
11
12
  - Download the latest Windows app here: [Download OmniKeyAI for Windows](https://omnikeyai-saas-fmytqc3dra-uc.a.run.app/windows/download)
@@ -33,16 +34,45 @@ omnikey daemon --port 7071
33
34
  # Kill the daemon
34
35
  omnikey kill-daemon
35
36
 
36
- # Remove the config directory and SQLite database (and persistence agent)
37
+ # Restart the daemon (kill + start in one step)
38
+ omnikey restart-daemon --port 7071
39
+
40
+ # Show current configuration (API keys are masked)
41
+ omnikey config
42
+
43
+ # Set a single configuration value
44
+ omnikey set OMNIKEY_PORT 8080
45
+
46
+ # Remove the config directory (keeps SQLite database)
37
47
  omnikey remove-config
38
48
 
49
+ # Remove config and also the SQLite database
50
+ omnikey remove-config --db
51
+
39
52
  # Check daemon status
40
53
  omnikey status
41
54
 
42
55
  # Check daemon logs
43
56
  omnikey logs --lines 100
57
+
58
+ # Check daemon error logs only
59
+ omnikey logs --errors
44
60
  ```
45
61
 
62
+ ### Command reference
63
+
64
+ | Command | Description |
65
+ |---|---|
66
+ | `omnikey onboard` | Interactive setup for LLM provider and web search |
67
+ | `omnikey daemon [--port]` | Start the backend daemon (default port: 7071) |
68
+ | `omnikey kill-daemon` | Stop the running daemon |
69
+ | `omnikey restart-daemon [--port]` | Kill and restart the daemon |
70
+ | `omnikey config` | Display current config with masked API keys |
71
+ | `omnikey set <key> <value>` | Update a single config value |
72
+ | `omnikey remove-config [--db]` | Remove config files; add `--db` to also delete the database |
73
+ | `omnikey status` | Show what process is using the daemon port |
74
+ | `omnikey logs [--lines N] [--errors]` | Tail daemon logs |
75
+
46
76
  ## Platform notes
47
77
 
48
78
  ### macOS
@@ -51,9 +81,49 @@ The daemon is registered as a **launchd agent** (`~/Library/LaunchAgents/com.omn
51
81
 
52
82
  ### Windows
53
83
 
54
- The daemon is registered as a **Windows Task Scheduler** task (`OmnikeyDaemon`) that runs at every logon. A wrapper script (`~/.omnikey/start-daemon.cmd`) is generated to set the required environment variables before launching the Node.js backend.
84
+ The daemon runs as a **Windows Service** managed by [NSSM (Non-Sucking Service Manager)](https://nssm.cc/). This gives it production-grade persistence:
85
+
86
+ | Behaviour | Detail |
87
+ |---|---|
88
+ | Starts on boot | Runs as `SERVICE_AUTO_START` — no login required |
89
+ | Auto-restarts on crash | Restarts after a 3-second delay on any unexpected exit |
90
+ | Runs in the background | No console window, no logged-in user needed |
91
+ | Log rotation | stdout/stderr written to `~/.omnikey/daemon.log` and `daemon-error.log` with rotation enabled |
92
+
93
+ #### Prerequisites
94
+
95
+ NSSM must be installed and the command must be run from an **elevated (Administrator) terminal**.
96
+
97
+ If NSSM is not found, `omnikey daemon` will prompt you:
98
+
99
+ ```
100
+ ? NSSM is required but not found. Install it now via winget? (Y/n)
101
+ ```
102
+
103
+ Answering **Y** runs `winget install nssm` automatically and continues setup. Alternatively install it manually:
104
+
105
+ ```sh
106
+ winget install nssm # Windows Package Manager (built-in on Win 10/11)
107
+ scoop install nssm # Scoop
108
+ choco install nssm # Chocolatey
109
+ ```
55
110
 
56
- > **Note:** `schtasks` is a built-in Windows command — no third-party tools or administrator rights are required for user-level scheduled tasks.
111
+ #### Service management
112
+
113
+ ```sh
114
+ # View service status in Services console
115
+ services.msc
116
+
117
+ # Or via the command line
118
+ sc query OmnikeyDaemon
119
+
120
+ # Stop / start manually
121
+ nssm stop OmnikeyDaemon
122
+ nssm start OmnikeyDaemon
123
+
124
+ # Uninstall (also done automatically by omnikey kill-daemon / remove-config)
125
+ nssm remove OmnikeyDaemon confirm
126
+ ```
57
127
 
58
128
  Commands that query process state use `netstat` (instead of `lsof`) on Windows, and process termination uses `taskkill` (instead of `SIGTERM`).
59
129
 
@@ -2,96 +2,63 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getAgentPrompt = getAgentPrompt;
4
4
  const config_1 = require("../config");
5
- function getAgentPrompt(platform) {
5
+ function getAgentPrompt(platform, hasTaskInstructions) {
6
6
  const isWindows = config_1.config.terminalPlatform?.toLowerCase() === 'windows' || platform?.toLowerCase() === 'windows';
7
- const windowsShellScriptInstructions = `
8
- \`\`\`
9
- <shell_script>
10
- # your commands here
11
- </shell_script>
12
- \`\`\`
13
-
14
- Follow these guidelines:
15
-
16
- - Use a single, self-contained PowerShell script per response; do not send multiple \`<shell_script>\` blocks in one turn.
17
- - Inside the script, group related commands logically and add brief inline comments only when they clarify non-obvious or complex steps.
18
- - Prefer safe, idempotent commands that can be run multiple times without unintended side effects.
19
- - Never use elevated privileges (do not use \`sudo\`, \`Run as Administrator\`, or equivalent).
20
- - Use PowerShell cmdlets and syntax (for example, \`Get-ChildItem\`, \`Select-Object\`, \`Where-Object\`) rather than cmd.exe or bash equivalents.`;
21
7
  return `
22
- You are an AI assistant capable of reasoning about user situations and executing shell scripts in a terminal environment. You have full access to the terminal.
23
-
24
- Your responsibilities are:
25
- 1. **Read and respect stored instructions**: When provided with \`<stored_instructions>\`, follow them carefully regarding behavior, focus areas, and output style.
26
- 2. **Process user input**: Analyze what the user has typed or requested.
27
- 3. **Gather context when needed**: Decide if additional machine-level information is required. If so, generate appropriate shell scripts to collect it.
28
- 4. **Produce a complete answer**: Combine results from any previously executed scripts, the stored instructions, and the user input to deliver a helpful final response.
8
+ You are an AI assistant with full terminal access. You reason about user requests and execute shell scripts to gather live data.
29
9
 
30
- **Guidelines for script generation:**
31
- - Create only safe, read-only commands focused on inspection, diagnostics, and information gathering.
32
- - Do not generate commands that install software, modify user data, or change system settings.
33
- - Never ask the user to run commands with \`sudo\` or administrator/root privileges.
34
- - Ensure all commands are compatible with ${!isWindows ? 'macOS and Linux; avoid Windows-specific commands.' : 'Use Windows-specific commands; avoid macOS and Linux-specific commands.'}
35
- - Scripts must be self-contained and ready to run without requiring the user to edit them.
10
+ **Input:**
11
+ ${hasTaskInstructions
12
+ ? `- Follow \`<stored_instructions>\` for behavior, priorities, and output style. Apply them to the goal in \`<user_input>\`.`
13
+ : `- \`<user_input>\` contains \`@omniAgent <question/command>\`. Everything after \`@omniAgent\` is your directive; surrounding text is context.`}
14
+ - Priority order for conflicts: system rules > stored instructions > user input.
36
15
 
37
- When you generate shell scripts, make them clear, efficient, and focused on gathering the information needed to answer the user's question or complete their request.
16
+ **When to use shell scripts:**
17
+ - Default to a \`<shell_script>\` for anything involving the machine, network, files, processes, env vars, or system state — never answer these from training data alone.
18
+ - Scripts must be safe and read-only (inspection/diagnostics only). No installs, no data modification, no system changes, no sudo/admin privileges.
19
+ - Use ${!isWindows ? 'bash (macOS/Linux)' : 'PowerShell'}. Scripts must be self-contained and ready to run as-is.
20
+ - One comprehensive script per turn; wait for output only if you genuinely need it to proceed.
21
+ - Skip the script only for purely factual/conversational requests with no live data dependency (e.g. "what is 2+2").
38
22
 
39
- **Instruction handling:**
40
- - Treat stored task instructions (if present) as authoritative for how to prioritize, what to examine, and how to format your answer, as long as they do not conflict with system rules or safety guidelines.
41
- - Treat the current user input as the immediate goal or question you must solve, applying the stored instructions to that specific situation.
42
- - If there is a conflict, follow: system rules first, then stored instructions, then ad-hoc guidance in the current input.
23
+ **When to use web tools:**
24
+ - Use the built-in \`web_fetch\` tool when the user provides a URL that must be retrieved.
25
+ - Use the built-in \`web_search\` tool when the user asks to search online, or when current information (prices, docs, recent events) is needed.
26
+ - If a request needs BOTH machine data AND web search: emit a \`<shell_script>\` first wait for \`TERMINAL OUTPUT:\` → then call the web tool with concrete values. Never use placeholders like "my IP" in a web query.
43
27
 
44
- **Web tools:**
45
- You have access to web tools, but you must use them sparingly and only when explicitly required:
46
- - \`web_fetch(url)\`: Only call this when the user has provided a specific URL in their current input or stored instructions and you need to retrieve its contents.
47
- - \`web_search(query)\`: Only call this when the user has explicitly asked you to search the web or look something up online.
28
+ **Incoming message tags:**
29
+ - \`TERMINAL OUTPUT:\` stdout/stderr from a prior script. Use it to continue reasoning or emit a follow-up.
30
+ - \`COMMAND ERROR:\` script failed. Diagnose and emit a corrected \`<shell_script>\` or explain in \`<final_answer>\`.
31
+ - No prefix direct user message; treat as the primary request.
48
32
 
49
- Do NOT use web tools proactively. Do NOT call them to look up documentation, error references, or general information you could infer from the machine output or your own knowledge. Your primary workflow is to generate shell scripts, wait for the terminal output, and reason from that output. Only reach for web tools when there is a clear, explicit instruction or a URL provided by the user.
33
+ **Response format every response must be exactly one of:**
34
+ 1. \`<shell_script>...</shell_script>\` — to run commands.
35
+ 2. A \`web_search\` or \`web_fetch\` tool call — to fetch web context (use native tool calling, not XML tags).
36
+ 3. \`<final_answer>...</final_answer>\` — when done.
50
37
 
51
- **User message tags:**
52
- User messages may be prefixed with special tags that indicate their origin:
53
- - \`TERMINAL OUTPUT:\` — the content is stdout/stderr returned from a previously requested \`<shell_script>\`. Parse it as machine output and use it to continue your reasoning toward a \`<final_answer>\` or a follow-up \`<shell_script>\`.
54
- - \`COMMAND ERROR:\` — the shell script failed or the terminal returned a non-zero exit code. Treat the content as error output: diagnose the failure, then either emit a corrected \`<shell_script>\` or explain the issue in a \`<final_answer>\`.
55
- - No prefix — the content is a direct message from the user; treat it as the primary request or question to address.
38
+ No plain text, reasoning, or other tags outside these blocks. Never wrap in additional XML/JSON.
56
39
 
57
- **Interaction rules:**
58
- - When you need to execute ANY shell command, respond with a single \`<shell_script>\` block that contains the FULL script to run.
59
- - Within that script, include all steps needed to carry out the current diagnostic or information-gathering task as completely as possible (for example, collect all relevant logs, inspect all relevant services, perform all necessary checks), rather than issuing minimal or placeholder commands.
60
- - Prefer one comprehensive script over multiple small scripts; only wait for another round of output if you genuinely need the previous results to decide on the next actions.
61
- - If further machine-level investigation is unnecessary, skip the shell script and respond directly with a \`<final_answer>\`.
62
- - Every response MUST be exactly one of:
63
- - A single \`<shell_script>...</shell_script>\` block, and nothing else; or
64
- - A single \`<final_answer>...</final_answer>\` block, and nothing else.
65
- - Never send plain text or explanation outside of these tags. If you are not emitting a \`<shell_script>\`, you MUST emit a \`<final_answer>\`.
66
- - When you are completely finished and ready to present the result back to the user, respond with a single \`<final_answer>\` block.
67
- - Do NOT include reasoning, commentary, or any other tags outside of \`<shell_script>...</shell_script>\` or \`<final_answer>...</final_answer>\`.
68
- - Never wrap your entire response in other XML or JSON structures.
69
-
70
- **Shell script block structure:**
71
- Always emit exactly this structure when you want to run commands: ${!isWindows
72
- ? `
73
- \`\`\`bash
40
+ **Shell script structure:**
41
+ ${!isWindows
42
+ ? `\`\`\`bash
74
43
  <shell_script>
75
44
  #!/usr/bin/env bash
76
45
  set -euo pipefail
77
- # your commands here
46
+ # commands here
78
47
  </shell_script>
79
- \`\`\`
80
-
81
- - Use a single, self-contained script per turn; do not send multiple \`<shell_script>\` blocks in one response.
82
- - Inside the script, group related commands logically and add brief inline comments ONLY when they clarify non-obvious steps.
83
- - Prefer safe, idempotent commands. Never ask for sudo.`
84
- : windowsShellScriptInstructions}
85
-
86
- **Final answer block structure:**
87
- When you have gathered enough information and completed the requested work, respond once with:
48
+ \`\`\``
49
+ : `\`\`\`
50
+ <shell_script>
51
+ # PowerShell commands here
52
+ # Use cmdlets (Get-ChildItem, Select-Object, etc.), not cmd.exe/bash equivalents
53
+ # No Run as Administrator
54
+ </shell_script>
55
+ \`\`\``}
88
56
 
57
+ **Final answer structure:**
89
58
  \`\`\`
90
59
  <final_answer>
91
- ...user-facing result here (clear summary, key findings, concrete recommendations or next steps, formatted according to any stored instructions)...
60
+ ...summary, key findings, and next steps formatted per stored instructions...
92
61
  </final_answer>
93
62
  \`\`\`
94
-
95
- - Do not emit any text before or after the \`<final_answer>\` block; the entire response must be inside the \`<final_answer>\` tags.
96
63
  `;
97
64
  }
@@ -49,6 +49,67 @@ const featureRoutes_1 = require("../featureRoutes");
49
49
  const authMiddleware_1 = require("../authMiddleware");
50
50
  const web_search_provider_1 = require("../web-search-provider");
51
51
  const ai_client_1 = require("../ai-client");
52
+ async function runToolLoop(initialResult, session, sessionId, send, log, tools, onUsage) {
53
+ const MAX_TOOL_ITERATIONS = 10;
54
+ let toolIterations = 0;
55
+ let result = initialResult;
56
+ while (result.finish_reason === 'tool_calls' && toolIterations < MAX_TOOL_ITERATIONS) {
57
+ toolIterations++;
58
+ const toolCalls = result.tool_calls ?? [];
59
+ // If the model claims tool_calls but sent none, treat it as a normal text
60
+ // response — pushing an assistant message with no following tool results
61
+ // would leave the history ending with an assistant turn, causing a 400.
62
+ if (!toolCalls.length)
63
+ break;
64
+ session.history.push(result.assistantMessage);
65
+ log.info('Agent executing tool calls', {
66
+ sessionId,
67
+ turn: session.turns,
68
+ toolIteration: toolIterations,
69
+ tools: toolCalls.map((tc) => tc.name),
70
+ });
71
+ const toolResults = await Promise.all(toolCalls.map(async (tc) => {
72
+ const args = tc.arguments;
73
+ // Notify the frontend that a web tool call is about to execute.
74
+ const webCallContent = tc.name === 'web_search'
75
+ ? `Searching the web for: "${args.query ?? ''}"`
76
+ : `Fetching URL: ${args.url ?? ''}`;
77
+ send({
78
+ session_id: sessionId,
79
+ sender: 'agent',
80
+ content: webCallContent,
81
+ is_terminal_output: false,
82
+ is_error: false,
83
+ is_web_call: true,
84
+ });
85
+ const toolResult = await (0, web_search_provider_1.executeTool)(tc.name, args, log);
86
+ log.info('Tool call completed', {
87
+ sessionId,
88
+ tool: tc.name,
89
+ resultLength: toolResult.length,
90
+ });
91
+ return { id: tc.id, name: tc.name, result: toolResult };
92
+ }));
93
+ for (const { id, name, result: toolResult } of toolResults) {
94
+ session.history.push({
95
+ role: 'tool',
96
+ tool_call_id: id,
97
+ tool_name: name,
98
+ content: toolResult,
99
+ });
100
+ }
101
+ // Call the AI again with the tool results in history to get the next response.
102
+ result = await ai_client_1.aiClient.complete(aiModel, session.history, {
103
+ tools: tools.length ? tools : undefined,
104
+ temperature: 0.2,
105
+ });
106
+ await onUsage(result);
107
+ }
108
+ log.info('Finished reasoning and tool calls: ', {
109
+ reason: result.finish_reason,
110
+ });
111
+ return result;
112
+ }
52
113
  function buildAvailableTools() {
53
114
  // web_search is always available — DuckDuckGo is used as free fallback
54
115
  return [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL];
@@ -64,14 +125,19 @@ async function getOrCreateSession(sessionId, subscription, platform, log) {
64
125
  subscriptionId: existing.subscription.id,
65
126
  turns: existing.turns,
66
127
  });
67
- return existing;
128
+ return {
129
+ sessionState: existing,
130
+ hasStoredPrompt: existing.history
131
+ .filter((h) => h.role === 'user')
132
+ .some((h) => h.content.includes('<stored_instructions>')),
133
+ };
68
134
  }
69
- const systemPrompt = (0, agentPrompts_1.getAgentPrompt)(platform);
70
135
  // use these instructions as user instructions
71
136
  const prompt = await (0, featureRoutes_1.getPromptForCommand)(log, 'task', subscription).catch((err) => {
72
137
  log.error('Failed to get system prompt for new agent session', { error: err });
73
138
  return '';
74
139
  });
140
+ const systemPrompt = (0, agentPrompts_1.getAgentPrompt)(platform, !!prompt);
75
141
  const entry = {
76
142
  subscription,
77
143
  history: [
@@ -102,7 +168,10 @@ ${prompt}
102
168
  subscriptionId: subscription.id,
103
169
  hasCustomPrompt: Boolean(prompt),
104
170
  });
105
- return entry;
171
+ return {
172
+ sessionState: entry,
173
+ hasStoredPrompt: !!prompt,
174
+ };
106
175
  }
107
176
  async function authenticateFromAuthHeader(authHeader, log) {
108
177
  if (config_1.config.isSelfHosted) {
@@ -169,8 +238,11 @@ async function authenticateFromAuthHeader(authHeader, log) {
169
238
  return null;
170
239
  }
171
240
  }
241
+ function createUserContent(content, hasStoredPrompt) {
242
+ return hasStoredPrompt ? content.replace(/@omniAgent/g, '').trim() : content;
243
+ }
172
244
  async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
173
- const session = await getOrCreateSession(sessionId, subscription, clientMessage.platform, log);
245
+ const { sessionState: session, hasStoredPrompt } = await getOrCreateSession(sessionId, subscription, clientMessage.platform, log);
174
246
  // Count this call as one agent iteration.
175
247
  session.turns += 1;
176
248
  log.info('Starting agent turn', {
@@ -204,10 +276,15 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
204
276
  rawContentLength: (clientMessage.content || '').length,
205
277
  userContentLength: userContent.length,
206
278
  });
207
- session.history.push({
208
- role: 'user',
209
- content: userContent,
210
- });
279
+ const isAssistance = isTerminalOutput || isErrorFlag;
280
+ if (!clientMessage?.is_web_call) {
281
+ session.history.push({
282
+ role: 'user',
283
+ content: isAssistance
284
+ ? userContent
285
+ : `<user_input>${createUserContent(userContent, hasStoredPrompt)}</user_input>`,
286
+ });
287
+ }
211
288
  // On the final turn we omit tools so the model is forced to emit a
212
289
  // plain text <final_answer> rather than issuing another tool call.
213
290
  const isFinalTurn = session.turns >= MAX_TURNS;
@@ -249,88 +326,8 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
249
326
  temperature: 0.2,
250
327
  });
251
328
  await recordUsage(result);
252
- // Tool-call loop: execute any requested tools and feed results back
253
- // until the model emits a non-tool-call response (or we hit the limit).
254
- const MAX_TOOL_ITERATIONS = 10;
255
- let toolIterations = 0;
256
- while (result.finish_reason === 'tool_calls' && toolIterations < MAX_TOOL_ITERATIONS) {
257
- toolIterations++;
258
- const toolCalls = result.tool_calls ?? [];
259
- // If the model claims tool_calls but sent none, treat it as a normal text
260
- // response — pushing an assistant message with no following tool results
261
- // would leave the history ending with an assistant turn, causing a 400.
262
- if (!toolCalls.length)
263
- break;
264
- session.history.push(result.assistantMessage);
265
- log.info('Agent executing tool calls', {
266
- sessionId,
267
- turn: session.turns,
268
- toolIteration: toolIterations,
269
- tools: toolCalls.map((tc) => tc.name),
270
- });
271
- const toolResults = await Promise.all(toolCalls.map(async (tc) => {
272
- const args = tc.arguments;
273
- // Notify the frontend that a web tool call is about to execute.
274
- const webCallContent = tc.name === 'web_search'
275
- ? `Searching the web for: "${args.query ?? ''}"`
276
- : `Fetching URL: ${args.url ?? ''}`;
277
- send({
278
- session_id: sessionId,
279
- sender: 'agent',
280
- content: webCallContent,
281
- is_terminal_output: false,
282
- is_error: false,
283
- is_web_call: true,
284
- });
285
- const toolResult = await (0, web_search_provider_1.executeTool)(tc.name, args, log);
286
- log.info('Tool call completed', {
287
- sessionId,
288
- tool: tc.name,
289
- resultLength: toolResult.length,
290
- });
291
- return { id: tc.id, name: tc.name, result: toolResult };
292
- }));
293
- for (const { id, name, result: toolResult } of toolResults) {
294
- session.history.push({
295
- role: 'tool',
296
- tool_call_id: id,
297
- tool_name: name,
298
- content: toolResult,
299
- });
300
- }
301
- result = await ai_client_1.aiClient.complete(aiModel, session.history, {
302
- tools: tools?.length ? tools : undefined,
303
- temperature: 0.2,
304
- });
305
- await recordUsage(result);
306
- }
307
- log.info('Finished reasoning and tool calls: ', {
308
- reason: result.finish_reason,
309
- });
310
- const content = result.content.trim();
311
- // If the tool loop was exhausted while the model still wants more tool calls,
312
- // the last result has empty content. Force one final no-tools call so the model
313
- // must synthesize a text answer from everything gathered so far.
314
- if (result.finish_reason === 'tool_calls' || !content) {
315
- log.warn('Tool iteration limit reached with pending tool calls; forcing final text response', {
316
- sessionId,
317
- turn: session.turns,
318
- });
319
- // Do NOT push result.assistantMessage here — it contains tool_use blocks that
320
- // require corresponding tool_result blocks (Anthropic API constraint). Since we
321
- // are not executing those tool calls, just inject a plain user nudge so the model
322
- // synthesizes a text answer from the history already accumulated.
323
- session.history.push({
324
- role: 'user',
325
- content: 'You have reached the maximum number of tool calls. Based on all information gathered so far, please provide your best answer now.',
326
- });
327
- result = await ai_client_1.aiClient.complete(aiModel, session.history, {
328
- tools: undefined,
329
- temperature: 0.2,
330
- });
331
- await recordUsage(result);
332
- }
333
- if (!content) {
329
+ let content = result.content.trim();
330
+ if (!content && result.finish_reason !== 'tool_calls') {
334
331
  log.warn('Agent LLM returned empty content; sending generic error to client.');
335
332
  const errorMessage = 'The agent returned an empty response. Please try again.';
336
333
  sendFinalAnswer(send, sessionId, errorMessage, true);
@@ -339,6 +336,23 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
339
336
  sessionMessages.delete(sessionId);
340
337
  return;
341
338
  }
339
+ // If the model requested web tool calls, execute them and get a follow-up
340
+ // response before deciding what to send to the client.
341
+ if (!isFinalTurn && result.finish_reason === 'tool_calls') {
342
+ log.info('Running web tool calls to gather information', {
343
+ sessionId,
344
+ subscriptionId: subscription.id,
345
+ turn: session.turns,
346
+ });
347
+ result = await runToolLoop(result, session, sessionId, send, log, buildAvailableTools(), recordUsage);
348
+ content = result.content.trim();
349
+ if (!content) {
350
+ log.warn('Agent returned empty content after tool loop; sending generic error.');
351
+ sendFinalAnswer(send, sessionId, 'The agent returned an empty response. Please try again.', true);
352
+ sessionMessages.delete(sessionId);
353
+ return;
354
+ }
355
+ }
342
356
  // Ensure that a proper <final_answer> block is produced for the
343
357
  // desktop clients once we reach the final turn. If the model did
344
358
  // not emit either a <shell_script> or <final_answer> tag on the
@@ -347,50 +361,40 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
347
361
  // waiting and paste the result.
348
362
  const hasShellScriptTag = content.includes('<shell_script>');
349
363
  const hasFinalAnswerTag = content.includes('<final_answer>');
350
- log.info('Agent LLM raw response summary', {
351
- sessionId,
352
- turn: session.turns,
353
- rawContentLength: content.length,
354
- hasShellScriptTag,
355
- hasFinalAnswerTag,
356
- });
357
- const normalizedContent = !hasShellScriptTag && !hasFinalAnswerTag && session.turns >= MAX_TURNS
358
- ? `<final_answer>\n${content}\n</final_answer>`
359
- : content;
360
- log.info('Agent LLM normalized response summary', {
361
- sessionId,
362
- turn: session.turns,
363
- normalizedContentLength: normalizedContent.length,
364
- });
365
- // Record assistant message back into history for future turns.
366
- session.history.push({
367
- role: 'assistant',
368
- content: normalizedContent,
369
- });
370
- send({
371
- session_id: sessionId,
372
- sender: 'agent',
373
- content: normalizedContent,
374
- is_terminal_output: false,
375
- is_error: false,
376
- });
377
- // After the MAX_TURNS iteration or if a final answer tag is present, treat this as the final answer
378
- // and clear the session from memory while marking it completed.
379
- if (session.turns >= MAX_TURNS || hasFinalAnswerTag) {
364
+ if (hasShellScriptTag && !isFinalTurn) {
365
+ log.info('Completed agent turn. Sending back scripts, waiting for results.', {
366
+ sessionId,
367
+ subscriptionId: subscription.id,
368
+ turn: session.turns,
369
+ responseLength: result.content.length,
370
+ });
371
+ session.history.push({
372
+ role: 'assistant',
373
+ content,
374
+ });
375
+ send({
376
+ session_id: sessionId,
377
+ sender: 'agent',
378
+ content,
379
+ is_terminal_output: false,
380
+ is_error: false,
381
+ });
382
+ return;
383
+ }
384
+ if (isFinalTurn || hasFinalAnswerTag) {
380
385
  log.info('Finalizing agent session after max turns or final answer tag', {
381
386
  sessionId,
382
387
  subscriptionId: subscription.id,
383
388
  turns: session.turns,
384
389
  hasFinalAnswerTag,
385
390
  });
391
+ send({
392
+ session_id: sessionId,
393
+ sender: 'agent',
394
+ content: hasFinalAnswerTag ? content : `<final_answer>\n${content}\n</final_answer>`,
395
+ });
386
396
  sessionMessages.delete(sessionId);
387
397
  }
388
- log.info('Completed agent turn', {
389
- sessionId,
390
- subscriptionId: subscription.id,
391
- turn: session.turns,
392
- responseLength: normalizedContent.length,
393
- });
394
398
  }
395
399
  catch (err) {
396
400
  log.error('Agent LLM call failed', { error: err });