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 +73 -3
- package/backend-dist/agent/agentPrompts.js +40 -73
- package/backend-dist/agent/agentServer.js +130 -126
- package/dist/daemon.js +99 -44
- package/dist/index.js +25 -2
- package/dist/removeConfig.js +44 -18
- package/dist/setConfig.js +19 -0
- package/dist/showConfig.js +45 -0
- package/package.json +1 -1
- package/src/daemon.ts +107 -49
- package/src/index.ts +28 -2
- package/src/removeConfig.ts +39 -21
- package/src/setConfig.ts +16 -0
- package/src/showConfig.ts +47 -0
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
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
|
|
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
|
-
**
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
- If
|
|
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
|
-
**
|
|
45
|
-
|
|
46
|
-
- \`
|
|
47
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
#
|
|
46
|
+
# commands here
|
|
78
47
|
</shell_script>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
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 });
|