omnikey-cli 1.0.23 → 1.0.25

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,134 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.authenticateFromAuthHeader = authenticateFromAuthHeader;
7
+ exports.createLazyAuthContext = createLazyAuthContext;
8
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
+ const config_1 = require("../config");
10
+ const subscription_1 = require("../models/subscription");
11
+ const authMiddleware_1 = require("../authMiddleware");
12
+ /**
13
+ * Authenticates a WebSocket connection from a Bearer token in the Authorization header.
14
+ *
15
+ * In self-hosted mode, skips JWT verification and returns the self-hosted subscription directly.
16
+ * Otherwise, verifies the JWT, looks up the subscription by ID, and checks that it is not expired.
17
+ * Marks the subscription as expired and persists the change if the license key has passed its expiry date.
18
+ *
19
+ * @param authHeader - The raw `Authorization` header value (e.g. `"Bearer <token>"`).
20
+ * @param log - Logger instance scoped to the current connection.
21
+ * @returns The authenticated `Subscription`, or `null` if authentication fails for any reason.
22
+ */
23
+ async function authenticateFromAuthHeader(authHeader, log) {
24
+ if (config_1.config.isSelfHosted) {
25
+ log.info('Self-hosted mode: skipping JWT authentication for agent WebSocket connection.');
26
+ try {
27
+ const subscription = await (0, authMiddleware_1.selfHostedSubscription)();
28
+ log.info('Retrieved self-hosted subscription for agent WebSocket connection', {
29
+ subscriptionId: subscription.id,
30
+ });
31
+ return subscription;
32
+ }
33
+ catch (err) {
34
+ log.error('Failed to retrieve self-hosted subscription for agent WebSocket connection', {
35
+ error: err,
36
+ });
37
+ return null;
38
+ }
39
+ }
40
+ if (!config_1.config.jwtSecret) {
41
+ log.error('JWT secret is not configured. Cannot authenticate subscription from auth header.');
42
+ return null;
43
+ }
44
+ if (!authHeader) {
45
+ log.warn('Agent WebSocket connection missing authorization header');
46
+ return null;
47
+ }
48
+ const [scheme, token] = authHeader.split(' ');
49
+ if (scheme !== 'Bearer' || !token) {
50
+ log.warn('Agent WebSocket connection has malformed authorization header');
51
+ return null;
52
+ }
53
+ try {
54
+ const decoded = jsonwebtoken_1.default.verify(token, config_1.config.jwtSecret);
55
+ const subscription = await subscription_1.Subscription.findByPk(decoded.sid);
56
+ if (!subscription) {
57
+ log.warn('Agent WebSocket auth failed: subscription not found', {
58
+ sid: decoded.sid,
59
+ });
60
+ return null;
61
+ }
62
+ if (subscription.subscriptionStatus === 'expired') {
63
+ log.warn('Agent WebSocket auth failed: subscription expired', {
64
+ sid: decoded.sid,
65
+ });
66
+ return null;
67
+ }
68
+ const now = new Date();
69
+ if (subscription.licenseKeyExpiresAt && subscription.licenseKeyExpiresAt <= now) {
70
+ subscription.subscriptionStatus = 'expired';
71
+ await subscription.save();
72
+ log.info('Agent WebSocket auth: subscription key expired during connection', {
73
+ subscriptionId: subscription.id,
74
+ });
75
+ return null;
76
+ }
77
+ log.debug('Agent WebSocket auth succeeded', {
78
+ subscriptionId: subscription.id,
79
+ status: subscription.subscriptionStatus,
80
+ });
81
+ return subscription;
82
+ }
83
+ catch (err) {
84
+ log.warn('Agent WebSocket auth failed: invalid or expired JWT', { error: err });
85
+ return null;
86
+ }
87
+ }
88
+ /**
89
+ * Creates a lazy authentication context for a WebSocket connection.
90
+ *
91
+ * Authentication is deferred until the first call to `ensureAuthenticated`, and the result
92
+ * is cached so subsequent calls resolve immediately without re-verifying the token.
93
+ * Concurrent calls during the first authentication are coalesced into a single in-flight promise.
94
+ *
95
+ * @param authHeader - The raw `Authorization` header value forwarded from the upgrade request.
96
+ * @param log - Logger instance scoped to the current connection.
97
+ * @returns An `AuthContext` with `ensureAuthenticated` and `getSubscription` accessors.
98
+ */
99
+ function createLazyAuthContext(authHeader, log) {
100
+ let authenticatedSubscription = null;
101
+ let authFailed = false;
102
+ let authPromise = null;
103
+ const ensureAuthenticated = async () => {
104
+ if (authenticatedSubscription) {
105
+ return true;
106
+ }
107
+ if (authFailed) {
108
+ return false;
109
+ }
110
+ if (!authPromise) {
111
+ authPromise = (async () => {
112
+ try {
113
+ const sub = await authenticateFromAuthHeader(authHeader, log);
114
+ if (!sub) {
115
+ authFailed = true;
116
+ return;
117
+ }
118
+ authenticatedSubscription = sub;
119
+ log.info('Agent WebSocket authenticated', {
120
+ subscriptionId: authenticatedSubscription.id,
121
+ });
122
+ }
123
+ catch (err) {
124
+ authFailed = true;
125
+ log.error('Unexpected error during agent WebSocket auth', { error: err });
126
+ }
127
+ })();
128
+ }
129
+ await authPromise;
130
+ return Boolean(authenticatedSubscription);
131
+ };
132
+ const getSubscription = () => authenticatedSubscription;
133
+ return { ensureAuthenticated, getSubscription };
134
+ }
@@ -38,7 +38,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.attachAgentWebSocketServer = attachAgentWebSocketServer;
40
40
  const ws_1 = __importStar(require("ws"));
41
- const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
42
41
  const cuid_1 = __importDefault(require("cuid"));
43
42
  const config_1 = require("../config");
44
43
  const logger_1 = require("../logger");
@@ -46,8 +45,9 @@ const subscription_1 = require("../models/subscription");
46
45
  const subscriptionUsage_1 = require("../models/subscriptionUsage");
47
46
  const agentPrompts_1 = require("./agentPrompts");
48
47
  const featureRoutes_1 = require("../featureRoutes");
49
- const authMiddleware_1 = require("../authMiddleware");
50
- const web_search_provider_1 = require("../web-search-provider");
48
+ const web_search_provider_1 = require("../web-search/web-search-provider");
49
+ const agentAuth_1 = require("./agentAuth");
50
+ const utils_1 = require("./utils");
51
51
  const ai_client_1 = require("../ai-client");
52
52
  async function runToolLoop(initialResult, session, sessionId, send, log, tools, onUsage) {
53
53
  const MAX_TOOL_ITERATIONS = 10;
@@ -61,7 +61,7 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
61
61
  // would leave the history ending with an assistant turn, causing a 400.
62
62
  if (!toolCalls.length)
63
63
  break;
64
- session.history.push(result.assistantMessage);
64
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, result.assistantMessage);
65
65
  log.info('Agent executing tool calls', {
66
66
  sessionId,
67
67
  turn: session.turns,
@@ -91,7 +91,7 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
91
91
  return { id: tc.id, name: tc.name, result: toolResult };
92
92
  }));
93
93
  for (const { id, name, result: toolResult } of toolResults) {
94
- session.history.push({
94
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
95
95
  role: 'tool',
96
96
  tool_call_id: id,
97
97
  tool_name: name,
@@ -109,19 +109,19 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
109
109
  // force a final text response by calling again without tools.
110
110
  if (result.finish_reason === 'tool_calls') {
111
111
  log.warn('Tool loop hit MAX_TOOL_ITERATIONS; forcing final conclusion', { sessionId });
112
- session.history.push(result.assistantMessage);
112
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, result.assistantMessage);
113
113
  // The API requires a tool_result for every tool_use in the preceding
114
114
  // assistant message. Add synthetic results for any unexecuted calls so
115
115
  // the history remains valid before we send the follow-up user message.
116
116
  for (const tc of result.tool_calls ?? []) {
117
- session.history.push({
117
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
118
118
  role: 'tool',
119
119
  tool_call_id: tc.id,
120
120
  tool_name: tc.name,
121
121
  content: 'Tool call limit reached. Result unavailable.',
122
122
  });
123
123
  }
124
- session.history.push({
124
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
125
125
  role: 'user',
126
126
  content: 'You have reached the maximum number of tool calls. Do NOT make any further tool calls or web searches. You MUST now provide a final answer directly. If you still need to gather information from the system, generate a `<shell_scripts>` block instead of making tool calls.',
127
127
  });
@@ -136,10 +136,6 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
136
136
  });
137
137
  return result;
138
138
  }
139
- function buildAvailableTools() {
140
- // web_search is always available — DuckDuckGo is used as free fallback
141
- return [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL];
142
- }
143
139
  const aiModel = (0, ai_client_1.getDefaultModel)(config_1.config.aiProvider, 'smart');
144
140
  const sessionMessages = new Map();
145
141
  const MAX_TURNS = 10;
@@ -199,79 +195,6 @@ ${prompt}
199
195
  hasStoredPrompt: !!prompt,
200
196
  };
201
197
  }
202
- async function authenticateFromAuthHeader(authHeader, log) {
203
- if (config_1.config.isSelfHosted) {
204
- log.info('Self-hosted mode: skipping JWT authentication for agent WebSocket connection.');
205
- try {
206
- const subscription = await (0, authMiddleware_1.selfHostedSubscription)();
207
- log.info('Retrieved self-hosted subscription for agent WebSocket connection', {
208
- subscriptionId: subscription.id,
209
- });
210
- return subscription;
211
- }
212
- catch (err) {
213
- log.error('Failed to retrieve self-hosted subscription for agent WebSocket connection', {
214
- error: err,
215
- });
216
- return null;
217
- }
218
- }
219
- if (!config_1.config.jwtSecret) {
220
- log.error('JWT secret is not configured. Cannot authenticate subscription from auth header.');
221
- return null;
222
- }
223
- if (!authHeader) {
224
- log.warn('Agent WebSocket connection missing authorization header');
225
- return null;
226
- }
227
- const [scheme, token] = authHeader.split(' ');
228
- if (scheme !== 'Bearer' || !token) {
229
- log.warn('Agent WebSocket connection has malformed authorization header');
230
- return null;
231
- }
232
- try {
233
- const decoded = jsonwebtoken_1.default.verify(token, config_1.config.jwtSecret);
234
- const subscription = await subscription_1.Subscription.findByPk(decoded.sid);
235
- if (!subscription) {
236
- log.warn('Agent WebSocket auth failed: subscription not found', {
237
- sid: decoded.sid,
238
- });
239
- return null;
240
- }
241
- if (subscription.subscriptionStatus === 'expired') {
242
- log.warn('Agent WebSocket auth failed: subscription expired', {
243
- sid: decoded.sid,
244
- });
245
- return null;
246
- }
247
- const now = new Date();
248
- if (subscription.licenseKeyExpiresAt && subscription.licenseKeyExpiresAt <= now) {
249
- subscription.subscriptionStatus = 'expired';
250
- await subscription.save();
251
- log.info('Agent WebSocket auth: subscription key expired during connection', {
252
- subscriptionId: subscription.id,
253
- });
254
- return null;
255
- }
256
- log.debug('Agent WebSocket auth succeeded', {
257
- subscriptionId: subscription.id,
258
- status: subscription.subscriptionStatus,
259
- });
260
- return subscription;
261
- }
262
- catch (err) {
263
- log.warn('Agent WebSocket auth failed: invalid or expired JWT', { error: err });
264
- return null;
265
- }
266
- }
267
- function createUserContent(content, hasStoredPrompt) {
268
- return hasStoredPrompt
269
- ? content
270
- .toLowerCase()
271
- .replace(/@omniagent/g, '')
272
- .trim()
273
- : content;
274
- }
275
198
  async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
276
199
  const { sessionState: session, hasStoredPrompt } = await getOrCreateSession(sessionId, subscription, clientMessage.platform, log);
277
200
  // Count this call as one agent iteration.
@@ -284,7 +207,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
284
207
  // On the MAX_TURNS iteration, instruct the LLM to provide a final,
285
208
  // consolidated answer based on the full conversation context.
286
209
  if (session.turns === MAX_TURNS) {
287
- session.history.push({
210
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
288
211
  role: 'system',
289
212
  content: 'Provide a single, final, concise answer based on the entire conversation so far. Wrap the answer in a <final_answer>...</final_answer> block and do not ask for further input or mention additional shell scripts to run. Do not include any <shell_script> block in this response.',
290
213
  });
@@ -314,17 +237,17 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
314
237
  // represent environment feedback that the agent must reason about next.
315
238
  // Pushing them as 'assistant' would create two consecutive assistant turns
316
239
  // which breaks most LLM APIs and prevents the model from processing the output.
317
- session.history.push({
240
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
318
241
  role: 'user',
319
242
  content: isAssistance
320
243
  ? userContent
321
- : `<user_input>${createUserContent(userContent, hasStoredPrompt)}</user_input>`,
244
+ : `<user_input>${(0, utils_1.createUserContent)(userContent, hasStoredPrompt)}</user_input>`,
322
245
  });
323
246
  }
324
247
  // On the final turn we omit tools so the model is forced to emit a
325
248
  // plain text <final_answer> rather than issuing another tool call.
326
249
  const isFinalTurn = session.turns >= MAX_TURNS;
327
- const tools = isFinalTurn ? undefined : buildAvailableTools();
250
+ const tools = isFinalTurn ? undefined : (0, utils_1.buildAvailableTools)();
328
251
  const recordUsage = async (result) => {
329
252
  const usage = result.usage;
330
253
  if (!usage || !subscription.id || config_1.config.isSelfHosted)
@@ -366,7 +289,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
366
289
  if (!content && result.finish_reason !== 'tool_calls') {
367
290
  log.warn('Agent LLM returned empty content; sending generic error to client.');
368
291
  const errorMessage = 'The agent returned an empty response. Please try again.';
369
- sendFinalAnswer(send, sessionId, errorMessage, true);
292
+ (0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
370
293
  // Clear any cached session state so a subsequent attempt can
371
294
  // start fresh without a polluted history.
372
295
  sessionMessages.delete(sessionId);
@@ -380,7 +303,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
380
303
  subscriptionId: subscription.id,
381
304
  turn: session.turns,
382
305
  });
383
- const toolLoopResult = await runToolLoop(result, session, sessionId, send, log, buildAvailableTools(), recordUsage);
306
+ const toolLoopResult = await runToolLoop(result, session, sessionId, send, log, (0, utils_1.buildAvailableTools)(), recordUsage);
384
307
  const toolLoopContent = toolLoopResult.content.trim();
385
308
  const toolLoopHasShell = toolLoopContent.includes('<shell_script>');
386
309
  const toolLoopHasFinal = toolLoopContent.includes('<final_answer>');
@@ -402,9 +325,9 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
402
325
  // <final_answer>. The directive below tells it to use <shell_script> as
403
326
  // a fallback instead of asking the user to run commands.
404
327
  if (toolLoopResult.assistantMessage) {
405
- session.history.push(toolLoopResult.assistantMessage);
328
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, toolLoopResult.assistantMessage);
406
329
  }
407
- session.history.push({
330
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
408
331
  role: 'user',
409
332
  content: webToolFailed
410
333
  ? [
@@ -450,7 +373,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
450
373
  turn: session.turns,
451
374
  responseLength: result.content.length,
452
375
  });
453
- session.history.push({
376
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
454
377
  role: 'assistant',
455
378
  content,
456
379
  });
@@ -487,7 +410,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
487
410
  subscriptionId: subscription.id,
488
411
  turn: session.turns,
489
412
  });
490
- session.history.push({ role: 'assistant', content });
413
+ (0, utils_1.pushToSessionHistory)(log, session, { role: 'assistant', content });
491
414
  send({
492
415
  session_id: sessionId,
493
416
  sender: 'agent',
@@ -499,64 +422,19 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
499
422
  log.warn('Agent returned empty content with no recognized tags; sending error', {
500
423
  sessionId,
501
424
  });
502
- sendFinalAnswer(send, sessionId, 'The agent returned an empty response. Please try again.', true);
425
+ (0, utils_1.sendFinalAnswer)(send, sessionId, 'The agent returned an empty response. Please try again.', true);
503
426
  sessionMessages.delete(sessionId);
504
427
  }
505
428
  }
506
429
  catch (err) {
507
430
  log.error('Agent LLM call failed', { error: err });
508
431
  const errorMessage = 'Agent failed to call language model. Please try again later.';
509
- sendFinalAnswer(send, sessionId, errorMessage, true);
432
+ (0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
510
433
  // Clear any cached session state so a subsequent attempt can
511
434
  // start fresh without being polluted by a failed turn.
512
435
  sessionMessages.delete(sessionId);
513
436
  }
514
437
  }
515
- function sendFinalAnswer(send, sessionId, message, isError) {
516
- send({
517
- session_id: sessionId,
518
- sender: 'agent',
519
- content: `<final_answer>\n${message}\n</final_answer>`,
520
- is_terminal_output: false,
521
- is_error: isError,
522
- });
523
- }
524
- function createLazyAuthContext(authHeader, log) {
525
- let authenticatedSubscription = null;
526
- let authFailed = false;
527
- let authPromise = null;
528
- const ensureAuthenticated = async () => {
529
- if (authenticatedSubscription) {
530
- return true;
531
- }
532
- if (authFailed) {
533
- return false;
534
- }
535
- if (!authPromise) {
536
- authPromise = (async () => {
537
- try {
538
- const sub = await authenticateFromAuthHeader(authHeader, log);
539
- if (!sub) {
540
- authFailed = true;
541
- return;
542
- }
543
- authenticatedSubscription = sub;
544
- log.info('Agent WebSocket authenticated', {
545
- subscriptionId: authenticatedSubscription.id,
546
- });
547
- }
548
- catch (err) {
549
- authFailed = true;
550
- log.error('Unexpected error during agent WebSocket auth', { error: err });
551
- }
552
- })();
553
- }
554
- await authPromise;
555
- return Boolean(authenticatedSubscription);
556
- };
557
- const getSubscription = () => authenticatedSubscription;
558
- return { ensureAuthenticated, getSubscription };
559
- }
560
438
  function attachAgentWebSocketServer(server) {
561
439
  const wss = new ws_1.WebSocketServer({ server, path: '/ws/omni-agent' });
562
440
  wss.on('connection', (ws, req) => {
@@ -565,7 +443,7 @@ function attachAgentWebSocketServer(server) {
565
443
  log.info('Agent WebSocket connection opened');
566
444
  const authHeaderValue = req.headers['authorization'];
567
445
  const authHeader = Array.isArray(authHeaderValue) ? authHeaderValue[0] : authHeaderValue;
568
- const { ensureAuthenticated, getSubscription } = createLazyAuthContext(authHeader, log);
446
+ const { ensureAuthenticated, getSubscription } = (0, agentAuth_1.createLazyAuthContext)(authHeader, log);
569
447
  const send = (msg) => {
570
448
  try {
571
449
  ws.send(JSON.stringify(msg));
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAvailableTools = buildAvailableTools;
4
+ exports.createUserContent = createUserContent;
5
+ exports.sendFinalAnswer = sendFinalAnswer;
6
+ exports.pushToSessionHistory = pushToSessionHistory;
7
+ const web_search_provider_1 = require("../web-search/web-search-provider");
8
+ const ai_client_1 = require("../ai-client");
9
+ const config_1 = require("../config");
10
+ /**
11
+ * Returns the set of web tools available to the agent for every turn.
12
+ *
13
+ * `web_search` is always included because DuckDuckGo is used as a free
14
+ * fallback when no third-party search key is configured.
15
+ *
16
+ * @returns An array of `AITool` definitions ready to pass to the AI client.
17
+ */
18
+ function buildAvailableTools() {
19
+ return [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL];
20
+ }
21
+ /**
22
+ * Strips the `@omniagent` mention from user-supplied content.
23
+ *
24
+ * The desktop client prefixes messages with `@omniAgent` to trigger the agent.
25
+ * This helper removes that prefix (case-insensitive) so the raw directive
26
+ * reaches the model without the routing annotation.
27
+ *
28
+ * @param content - Raw content string from the client message.
29
+ * @param hasStoredPrompt - only remove the mention if the command has a stored prompt, otherwise it may be part of the user input
30
+ * @returns The cleaned content string with the mention removed and whitespace trimmed.
31
+ */
32
+ function createUserContent(content, hasStoredPrompt) {
33
+ if (hasStoredPrompt) {
34
+ return content.replace(/@omniagent/gi, '').trim();
35
+ }
36
+ return content;
37
+ }
38
+ /**
39
+ * Sends a `<final_answer>` message over the WebSocket and closes the agent turn.
40
+ *
41
+ * Wraps `message` in `<final_answer>` tags so the client knows the agent has
42
+ * finished reasoning and can display the result. Used for both successful
43
+ * conclusions and error responses.
44
+ *
45
+ * @param send - The WebSocket send function scoped to the current connection.
46
+ * @param sessionId - ID of the session this answer belongs to.
47
+ * @param message - The final answer text to send to the client.
48
+ * @param isError - When `true`, the client renders the message as an error.
49
+ */
50
+ function sendFinalAnswer(send, sessionId, message, isError) {
51
+ send({
52
+ session_id: sessionId,
53
+ sender: 'agent',
54
+ content: `<final_answer>\n${message}\n</final_answer>`,
55
+ is_terminal_output: false,
56
+ is_error: isError,
57
+ });
58
+ }
59
+ // Per-message hard string limit enforced by the provider API.
60
+ const MAX_MESSAGE_CONTENT = (0, ai_client_1.getMaxMessageContentLength)(config_1.config.aiProvider);
61
+ // Total character budget across all history messages (derived from the
62
+ // provider's context-window size minus headroom for output + system prompt).
63
+ const MAX_HISTORY_TOTAL = (0, ai_client_1.getMaxHistoryLength)(config_1.config.aiProvider);
64
+ const FINAL_ANSWER_REQUEST = {
65
+ role: 'user',
66
+ content: 'Content was truncated because a length limit was reached. ' +
67
+ 'You MUST stop making tool calls and provide a final answer immediately using <final_answer>...</final_answer>.',
68
+ };
69
+ /**
70
+ * Pushes a message onto the session history, enforcing two independent limits:
71
+ *
72
+ * 1. **Per-message limit** (`MAX_MESSAGE_CONTENT`) — the provider's hard cap
73
+ * on a single content string (e.g. Anthropic: 10 MB, OpenAI/Gemini: context-bound).
74
+ * 2. **Total history limit** (`MAX_HISTORY_TOTAL`) — the cumulative character
75
+ * budget derived from each provider's context-window size.
76
+ *
77
+ * When either limit is hit the message content is truncated and a separate
78
+ * `user` message is appended instructing the model to emit a final answer.
79
+ */
80
+ function pushToSessionHistory(logger, session, message) {
81
+ if (typeof message.content !== 'string') {
82
+ session.history.push(message);
83
+ return;
84
+ }
85
+ let content = message.content;
86
+ let limitHit = false;
87
+ // 1. Per-message content limit.
88
+ if (content.length > MAX_MESSAGE_CONTENT) {
89
+ content = content.slice(0, MAX_MESSAGE_CONTENT);
90
+ limitHit = true;
91
+ }
92
+ // 2. Total history length limit.
93
+ const currentTotal = session.history.reduce((acc, msg) => acc + (typeof msg.content === 'string' ? msg.content.length : 0), 0);
94
+ const remaining = MAX_HISTORY_TOTAL - currentTotal;
95
+ if (content.length > remaining) {
96
+ content = content.slice(0, Math.max(0, remaining - FINAL_ANSWER_REQUEST.content.length));
97
+ limitHit = true;
98
+ }
99
+ session.history.push({ ...message, content });
100
+ if (limitHit) {
101
+ logger.warn(`History limits exceeded. Message truncated to ${content.length} chars, total history is now ${currentTotal + content.length} chars.`);
102
+ session.history.push(FINAL_ANSWER_REQUEST);
103
+ }
104
+ }
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.aiClient = exports.AIClient = void 0;
7
7
  exports.getDefaultModel = getDefaultModel;
8
+ exports.getMaxMessageContentLength = getMaxMessageContentLength;
9
+ exports.getMaxHistoryLength = getMaxHistoryLength;
8
10
  const openai_1 = __importDefault(require("openai"));
9
11
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
10
12
  const genai_1 = require("@google/genai");
@@ -21,6 +23,44 @@ const DEFAULT_MODELS = {
21
23
  function getDefaultModel(provider, tier) {
22
24
  return DEFAULT_MODELS[provider][tier];
23
25
  }
26
+ /**
27
+ * Maximum character length for a single message content string per provider.
28
+ *
29
+ * - anthropic: hard API-enforced string limit of 10,485,760 chars; we stay
30
+ * just below it with a small safety buffer.
31
+ * - openai: no documented per-string limit; bounded by the context window
32
+ * (~272K tokens for GPT-5.1 ≈ ~1M chars). Use the history cap.
33
+ * - gemini: no documented per-string limit; bounded by the 1M-token
34
+ * context window (~4M chars). Use the history cap.
35
+ */
36
+ const MAX_MESSAGE_CONTENT_LENGTH_BY_PROVIDER = {
37
+ anthropic: 10000000,
38
+ openai: 800000,
39
+ gemini: 3500000,
40
+ };
41
+ /**
42
+ * Maximum total character length across all messages in the conversation
43
+ * history, derived from each provider's context-window size minus headroom
44
+ * for the system prompt and max output tokens.
45
+ *
46
+ * - anthropic: Claude Sonnet 4.6 — 1M token ctx, 64K max output
47
+ * ≈ (1,000,000 - 64,000 - 10,000) tokens × 4 chars ≈ 3.7M chars
48
+ * - openai: GPT-5.1 — ~272K token ctx, ~32K max output
49
+ * ≈ (272,000 - 32,000 - 5,000) tokens × 4 chars ≈ 940K chars
50
+ * - gemini: Gemini 2.5 Pro — 1M token ctx, ~32K max output
51
+ * ≈ (1,000,000 - 32,000 - 10,000) tokens × 4 chars ≈ 3.8M chars
52
+ */
53
+ const MAX_HISTORY_LENGTH_BY_PROVIDER = {
54
+ anthropic: 3500000,
55
+ openai: 800000,
56
+ gemini: 3500000,
57
+ };
58
+ function getMaxMessageContentLength(provider) {
59
+ return MAX_MESSAGE_CONTENT_LENGTH_BY_PROVIDER[provider];
60
+ }
61
+ function getMaxHistoryLength(provider) {
62
+ return MAX_HISTORY_LENGTH_BY_PROVIDER[provider];
63
+ }
24
64
  // ---------------------------------------------------------------------------
25
65
  // OpenAI adapter
26
66
  // ---------------------------------------------------------------------------
@@ -91,5 +91,5 @@ exports.config = {
91
91
  braveSearchApiKey: getEnv('BRAVE_SEARCH_API_KEY', false),
92
92
  tavilyApiKey: getEnv('TAVILY_API_KEY', false),
93
93
  searxngUrl: getEnv('SEARXNG_URL', false),
94
- terminalPlatform: getEnv('TERMINAL_PLATFORM', false)
94
+ terminalPlatform: getEnv('TERMINAL_PLATFORM', false),
95
95
  };