@exulu/backend 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ import {
7
7
  validatePythonEnvironment
8
8
  } from "./chunk-T6JVFT7L.js";
9
9
  import {
10
+ COMPACTION_INSUFFICIENT,
11
+ ContextCompactionRequiredError,
12
+ DEFAULT_CONTEXT_WINDOW,
10
13
  ExuluTool,
11
14
  LITELLM_UI_PATH,
12
15
  LiteLLMAdminError,
@@ -18,16 +21,19 @@ import {
18
21
  buildTags,
19
22
  checkLicense,
20
23
  checkRecordAccess,
24
+ contextOccupancy,
21
25
  convertExuluToolsToAiSdkTools,
22
26
  copyS3Object,
23
27
  createAgenticRetrievalTool,
24
- createProjectItemsRetrievalTool,
25
28
  createTaggedFetch,
26
29
  createUppyRoutes,
27
30
  decryptOauthState,
28
31
  deleteS3Object,
32
+ deriveContextBudget,
29
33
  downloadKeyIntoSandbox,
30
34
  enableLiteLLMClientMode,
35
+ estimateMessageTokens,
36
+ estimateTokens,
31
37
  exchangeCodeForTokens,
32
38
  exuluApp,
33
39
  getBudgetSettings,
@@ -40,10 +46,12 @@ import {
40
46
  getTagDailyActivity,
41
47
  getToken,
42
48
  getUserBudgetView,
49
+ guardExtractedFileText,
43
50
  invalidateBudgetCaches,
44
51
  isLiteLLMEnabled,
45
52
  listS3ObjectsByPrefix,
46
53
  listTagsByPrefix,
54
+ mapStreamErrorMessage,
47
55
  oauthRegistry,
48
56
  oauthTokenStore,
49
57
  postgresClient,
@@ -55,15 +63,17 @@ import {
55
63
  sanitizeToolName,
56
64
  setBudgetSettings,
57
65
  setLiteLLMPackageRoot,
66
+ sliceHistoryAtCheckpoint,
58
67
  startLiteLLMSupervisor,
59
68
  tagDelete,
60
69
  tagInfo,
70
+ truncateToolOutput,
61
71
  updateStatistic,
62
72
  uploadFile,
63
73
  upsertBudget,
64
74
  waitForLiteLLMReady,
65
75
  withRetry
66
- } from "./chunk-IJ4HNHOT.js";
76
+ } from "./chunk-RVZWZNWG.js";
67
77
  import {
68
78
  findLiteLLMModel
69
79
  } from "./chunk-7CCMW3IW.js";
@@ -2213,6 +2223,13 @@ var agentsSchema = {
2213
2223
  name: "sandbox_enabled",
2214
2224
  type: "boolean",
2215
2225
  default: false
2226
+ },
2227
+ {
2228
+ // Per-turn budget for ALL tool steps on one chat message (bash, files,
2229
+ // knowledge search, integrations). 0/null = platform default
2230
+ // (DEFAULT_MAX_STEPS in resolve-max-steps.ts). Auto-ALTERed on boot.
2231
+ name: "max_tool_steps",
2232
+ type: "number"
2216
2233
  }
2217
2234
  ]
2218
2235
  };
@@ -4252,7 +4269,7 @@ var ExuluContext2 = class {
4252
4269
  embedder,
4253
4270
  chunker,
4254
4271
  processor,
4255
- active,
4272
+ active: active2,
4256
4273
  fields,
4257
4274
  queryRewriter,
4258
4275
  resultReranker,
@@ -4283,7 +4300,7 @@ var ExuluContext2 = class {
4283
4300
  this.description = description;
4284
4301
  this.embedder = embedder;
4285
4302
  this.chunker = chunker;
4286
- this.active = active;
4303
+ this.active = active2;
4287
4304
  this.queryRewriter = queryRewriter;
4288
4305
  this.resultReranker = resultReranker;
4289
4306
  this.entities = entities;
@@ -5398,14 +5415,26 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
5398
5415
  )
5399
5416
  );
5400
5417
  if (args.project) {
5401
- const projectTool = await createProjectItemsRetrievalTool({
5402
- projectId: args.project,
5403
- user,
5404
- role: user.role?.id,
5405
- contexts
5406
- });
5407
- if (projectTool) {
5408
- result.tools.unshift(projectTool);
5418
+ const hasAgentic = result.tools.some(
5419
+ (tool2) => tool2?.id === "agentic_context_search"
5420
+ );
5421
+ if (!hasAgentic) {
5422
+ const instance = createAgenticRetrievalTool({
5423
+ contexts: [],
5424
+ user,
5425
+ role: user.role?.id,
5426
+ model: void 0
5427
+ });
5428
+ if (instance) {
5429
+ result.tools.unshift({
5430
+ id: instance.id,
5431
+ name: instance.name,
5432
+ description: instance.description,
5433
+ category: instance.category,
5434
+ type: instance.type,
5435
+ config: []
5436
+ });
5437
+ }
5409
5438
  }
5410
5439
  }
5411
5440
  result.tools = result.tools.filter((tool2) => tool2 !== null);
@@ -11275,14 +11304,15 @@ import bodyParser from "body-parser";
11275
11304
  import CryptoJS4 from "crypto-js";
11276
11305
  import OpenAI from "openai";
11277
11306
  import fs2 from "fs";
11278
- import { randomUUID as randomUUID2 } from "crypto";
11307
+ import { randomUUID as randomUUID3 } from "crypto";
11279
11308
  import "@opentelemetry/api";
11280
11309
  import JSZip2 from "jszip";
11281
11310
  import { createIdGenerator } from "ai";
11282
11311
  import cookieParser from "cookie-parser";
11283
11312
 
11284
11313
  // src/exulu/resolve-max-steps.ts
11285
- function resolveMaxStepsFromToolConfigs(toolConfigs) {
11314
+ var DEFAULT_MAX_STEPS = 10;
11315
+ function resolveRetrievalCallBudget(toolConfigs) {
11286
11316
  const agentic = toolConfigs?.find((t) => t.id === "agentic_context_search");
11287
11317
  if (!agentic?.config) return void 0;
11288
11318
  const entry = agentic.config.find((c) => c.name === "max_steps");
@@ -11291,15 +11321,26 @@ function resolveMaxStepsFromToolConfigs(toolConfigs) {
11291
11321
  const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
11292
11322
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
11293
11323
  }
11324
+ function resolveTurnStepBudget(maxStepCount, agent) {
11325
+ if (typeof maxStepCount === "number" && Number.isFinite(maxStepCount) && maxStepCount > 0) {
11326
+ return Math.floor(maxStepCount);
11327
+ }
11328
+ const raw = agent?.max_tool_steps;
11329
+ const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
11330
+ if (Number.isFinite(n) && n > 0) {
11331
+ return Math.floor(n);
11332
+ }
11333
+ return DEFAULT_MAX_STEPS;
11334
+ }
11294
11335
  function flattenPart(part) {
11295
11336
  const p = part;
11296
11337
  if (p?.type === "text") return p.text ?? "";
11297
11338
  if (p?.type === "tool-call") {
11298
- return `[searched ${p.toolName}: ${JSON.stringify(p.input ?? {}).slice(0, 300)}]`;
11339
+ return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
11299
11340
  }
11300
11341
  if (p?.type === "tool-result") {
11301
11342
  const out = p.output?.value ?? p.output;
11302
- return `[results from ${p.toolName}]: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
11343
+ return `The "${p.toolName}" tool returned: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
11303
11344
  }
11304
11345
  return "";
11305
11346
  }
@@ -11317,7 +11358,7 @@ function flattenToolHistory(messages) {
11317
11358
  return m;
11318
11359
  });
11319
11360
  }
11320
- var FINAL_ANSWER_INSTRUCTION = "Answer the user's original question now, in plain text, using only the material already retrieved above. Do not attempt any further tool calls.";
11361
+ var FINAL_ANSWER_INSTRUCTION = `This is your last step for this turn. Answer the user's original question now, in plain text, using only the information gathered above. If you could not finish the task, tell the user you reached the maximum number of tool steps, summarize what you found and did so far, and say what remains \u2014 they can ask you to continue. Do not attempt any further tool calls. Write your answer as normal prose for the user: do not output tool-call syntax, JSON commands, or bracketed lines such as "[called tool ...]" \u2014 describe anything you did or still plan to do in plain language.`;
11321
11362
  function finalAnswerGuard(maxSteps) {
11322
11363
  return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
11323
11364
  toolChoice: "none",
@@ -11330,6 +11371,68 @@ function finalAnswerGuard(maxSteps) {
11330
11371
  } : {}
11331
11372
  } : void 0;
11332
11373
  }
11374
+ function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
11375
+ if (limit == null || limit <= 0 || !agenticToolKey || !allToolKeys.includes(agenticToolKey)) {
11376
+ return () => void 0;
11377
+ }
11378
+ const remainingTools = allToolKeys.filter((k) => k !== agenticToolKey);
11379
+ return ({ steps }) => {
11380
+ const calls = (steps ?? []).flatMap((s) => s?.toolCalls ?? []).filter((c) => c?.toolName === agenticToolKey).length;
11381
+ if (calls < limit) return void 0;
11382
+ return { activeTools: remainingTools };
11383
+ };
11384
+ }
11385
+
11386
+ // src/exulu/context-guard.ts
11387
+ var KEEP_RECENT_TOOL_MESSAGES = 2;
11388
+ var COLLAPSE_KEEP_CHARS = 400;
11389
+ var COLLAPSE_MARKER = " \u2026[older tool output collapsed mid-response to fit the context window \u2014 the full output is in the session file named in the notice above, if one was saved]";
11390
+ function contextGuard(contextWindow) {
11391
+ const budget = deriveContextBudget(contextWindow);
11392
+ return async ({ messages }) => {
11393
+ if (!Array.isArray(messages) || messages.length === 0) return void 0;
11394
+ const tokens = estimateTokens(JSON.stringify(messages));
11395
+ if (tokens < budget.usableWindow) return void 0;
11396
+ const toolIndices = messages.map((m, i) => m?.role === "tool" ? i : -1).filter((i) => i !== -1);
11397
+ const collapsible = new Set(toolIndices.slice(0, Math.max(0, toolIndices.length - KEEP_RECENT_TOOL_MESSAGES)));
11398
+ if (collapsible.size === 0) return void 0;
11399
+ let changed = false;
11400
+ const next = messages.map((m, i) => {
11401
+ if (!collapsible.has(i)) return m;
11402
+ const msg = m;
11403
+ if (!Array.isArray(msg.content)) return m;
11404
+ const content = msg.content.map((part) => {
11405
+ const p = part;
11406
+ if (p?.type !== "tool-result") return part;
11407
+ const out = p.output?.value ?? p.output;
11408
+ const asText = typeof out === "string" ? out : JSON.stringify(out ?? "");
11409
+ if (asText.length <= COLLAPSE_KEEP_CHARS + COLLAPSE_MARKER.length) return part;
11410
+ changed = true;
11411
+ return { ...part, output: { type: "text", value: asText.slice(0, COLLAPSE_KEEP_CHARS) + COLLAPSE_MARKER } };
11412
+ });
11413
+ return { ...m, content };
11414
+ });
11415
+ return changed ? { messages: next } : void 0;
11416
+ };
11417
+ }
11418
+ function composePrepareSteps(...guards) {
11419
+ return async (opts) => {
11420
+ let merged;
11421
+ let messages = opts.messages;
11422
+ for (const guard of guards) {
11423
+ const result = await guard({ ...opts, messages });
11424
+ if (!result) continue;
11425
+ merged = { ...merged ?? {}, ...result };
11426
+ if (Array.isArray(result.messages)) {
11427
+ messages = result.messages;
11428
+ }
11429
+ }
11430
+ if (merged && messages && !("messages" in merged)) {
11431
+ merged.messages = messages;
11432
+ }
11433
+ return merged;
11434
+ };
11435
+ }
11333
11436
 
11334
11437
  // src/exulu/auto-decline-stale-approvals.ts
11335
11438
  var AUTO_DECLINE_REASON = "Automatically declined because the user sent a new message instead of responding to the approval request.";
@@ -11593,7 +11696,9 @@ var ExuluProvider = class {
11593
11696
  agent,
11594
11697
  instructions,
11595
11698
  maxStepCount,
11596
- onTokenUsage
11699
+ onTokenUsage,
11700
+ contextWindow,
11701
+ disabledTools
11597
11702
  }) => {
11598
11703
  console.log(
11599
11704
  "[EXULU] Called generate sync for agent: " + this.name,
@@ -11621,9 +11726,7 @@ var ExuluProvider = class {
11621
11726
  if (messages && session && user) {
11622
11727
  const previousMessages = await getAgentMessages({
11623
11728
  session,
11624
- user: user.id,
11625
- limit: 50,
11626
- page: 1
11729
+ user: user.id
11627
11730
  });
11628
11731
  const previousMessagesContent = previousMessages.map(
11629
11732
  (message) => JSON.parse(message.content)
@@ -11632,6 +11735,12 @@ var ExuluProvider = class {
11632
11735
  // append the new message to the previous messages:
11633
11736
  messages: [...previousMessagesContent, ...messages]
11634
11737
  });
11738
+ const contextBudget = deriveContextBudget(contextWindow);
11739
+ const occupancy = contextOccupancy(messages);
11740
+ if (occupancy >= contextBudget.blockThreshold) {
11741
+ throw new ContextCompactionRequiredError(occupancy, contextBudget);
11742
+ }
11743
+ messages = sliceHistoryAtCheckpoint(messages);
11635
11744
  }
11636
11745
  console.log(
11637
11746
  "[EXULU] Message count for agent: " + this.name,
@@ -11696,6 +11805,34 @@ var ExuluProvider = class {
11696
11805
  if (memoryContext) {
11697
11806
  system += "\n\n" + memoryContext;
11698
11807
  }
11808
+ const tools = await convertExuluToolsToAiSdkTools(
11809
+ currentTools,
11810
+ currentSkills,
11811
+ approvedTools,
11812
+ allExuluTools,
11813
+ toolConfigs,
11814
+ providerapikey,
11815
+ contexts,
11816
+ user,
11817
+ exuluConfig,
11818
+ session,
11819
+ req,
11820
+ project,
11821
+ sessionItems,
11822
+ model,
11823
+ agent,
11824
+ memoryItems,
11825
+ contextWindow,
11826
+ disabledTools
11827
+ );
11828
+ const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
11829
+ const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
11830
+ const retrievalGuard = retrievalBudgetGuard(
11831
+ resolveRetrievalCallBudget(toolConfigs),
11832
+ agenticToolKey,
11833
+ Object.keys(tools)
11834
+ );
11835
+ const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
11699
11836
  const includesContextSearchTool = currentTools?.some(
11700
11837
  (tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
11701
11838
  );
@@ -11708,12 +11845,12 @@ var ExuluProvider = class {
11708
11845
  system += `
11709
11846
 
11710
11847
 
11711
-
11848
+
11712
11849
  When you use a context search tool, you will include references to the items
11713
11850
  retrieved from the tool call result inline in the response using this exact JSON format
11714
11851
  (all on one line, no line breaks):
11715
11852
  {item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
11716
-
11853
+
11717
11854
  IMPORTANT formatting rules:
11718
11855
  - Do NOT reference just chunks like "Looking at chunk_index 5 and chunk_index 0 from the search result", always use the JSON format above.
11719
11856
  - Use the exact format shown above, all on ONE line
@@ -11721,9 +11858,9 @@ var ExuluProvider = class {
11721
11858
  - Use the context ID from the tool result
11722
11859
  - Include the file/item name, not the full path
11723
11860
  - Separate multiple citations with spaces
11724
-
11861
+
11725
11862
  Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
11726
-
11863
+
11727
11864
  The citations will be rendered as interactive badges in the UI.
11728
11865
  `;
11729
11866
  }
@@ -11734,12 +11871,12 @@ var ExuluProvider = class {
11734
11871
  When you use a web search tool, you will include references to the results of the tool call result inline in the response using this exact JSON format
11735
11872
  (all on one line, no line breaks):
11736
11873
  {url: <url>, title: <title>, snippet: <snippet>}
11737
-
11874
+
11738
11875
  IMPORTANT formatting rules:
11739
11876
  - Use the exact format shown above, all on ONE line
11740
11877
  - Do NOT use quotes around field names or values
11741
11878
  - Separate multiple results with spaces
11742
-
11879
+
11743
11880
  Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
11744
11881
  `;
11745
11882
  }
@@ -11772,29 +11909,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
11772
11909
  system,
11773
11910
  prompt,
11774
11911
  maxRetries: 2,
11775
- tools: await convertExuluToolsToAiSdkTools(
11776
- currentTools,
11777
- currentSkills,
11778
- approvedTools,
11779
- allExuluTools,
11780
- toolConfigs,
11781
- providerapikey,
11782
- contexts,
11783
- user,
11784
- exuluConfig,
11785
- session,
11786
- req,
11787
- project,
11788
- sessionItems,
11789
- model,
11790
- agent,
11791
- memoryItems
11792
- ),
11912
+ tools,
11793
11913
  // Stop after the image_generation tool fires — the widget IS the
11794
11914
  // assistant's response, no follow-up text turn is wanted (same
11795
11915
  // reasoning as question_ask: the UI artifact is the message).
11796
- prepareStep: finalAnswerGuard(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5),
11797
- stopWhen: [stepCountIs(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5), hasToolCall("image_generation")]
11916
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
11917
+ stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
11798
11918
  });
11799
11919
  console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
11800
11920
  const {
@@ -11855,26 +11975,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
11855
11975
  ignoreIncompleteToolCalls: true
11856
11976
  }),
11857
11977
  maxRetries: 2,
11858
- tools: await convertExuluToolsToAiSdkTools(
11859
- currentTools,
11860
- currentSkills,
11861
- approvedTools,
11862
- allExuluTools,
11863
- toolConfigs,
11864
- providerapikey,
11865
- contexts,
11866
- user,
11867
- exuluConfig,
11868
- session,
11869
- req,
11870
- project,
11871
- sessionItems,
11872
- model,
11873
- agent,
11874
- memoryItems
11875
- ),
11876
- prepareStep: finalAnswerGuard(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5),
11877
- stopWhen: [stepCountIs(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5), hasToolCall("image_generation")]
11978
+ tools,
11979
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
11980
+ stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
11878
11981
  });
11879
11982
  if (statistics) {
11880
11983
  await Promise.all([
@@ -11926,7 +12029,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
11926
12029
  * - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
11927
12030
  * - Image files -> image parts (which ARE supported by Responses API)
11928
12031
  */
11929
- async processFilePartsInMessages(messages) {
12032
+ async processFilePartsInMessages(messages, offloadCtx) {
11930
12033
  const processedMessages = await Promise.all(
11931
12034
  messages.map(async (message) => {
11932
12035
  if (message.role !== "user" || !Array.isArray(message.parts)) {
@@ -11971,10 +12074,11 @@ When a tool execution is not approved by the user, do not retry it unless explic
11971
12074
  outputErrorToConsole: false,
11972
12075
  newlineDelimiter: "\n"
11973
12076
  });
12077
+ const guardedText = await guardExtractedFileText(filename, String(extractedText), offloadCtx);
11974
12078
  return {
11975
12079
  type: "text",
11976
12080
  text: `<file file name = "${filename}" >
11977
- ${extractedText}
12081
+ ${guardedText}
11978
12082
  </file>`
11979
12083
  };
11980
12084
  } catch (error) {
@@ -11990,7 +12094,6 @@ ${extractedText}
11990
12094
  ...message,
11991
12095
  parts: processedParts
11992
12096
  };
11993
- console.log("[EXULU] Result: " + JSON.stringify(result, null, 2));
11994
12097
  return result;
11995
12098
  })
11996
12099
  );
@@ -12013,7 +12116,9 @@ ${extractedText}
12013
12116
  exuluConfig,
12014
12117
  instructions,
12015
12118
  req,
12016
- maxStepCount
12119
+ maxStepCount,
12120
+ contextWindow,
12121
+ disabledTools
12017
12122
  }) => {
12018
12123
  if (!this.config) {
12019
12124
  console.error("[EXULU] Config is required for streaming.");
@@ -12034,9 +12139,7 @@ ${extractedText}
12034
12139
  console.log("[EXULU] loading previous messages from session: " + session);
12035
12140
  const previousMessages2 = await getAgentMessages({
12036
12141
  session,
12037
- user: user?.id,
12038
- limit: 50,
12039
- page: 1
12142
+ user: user?.id
12040
12143
  });
12041
12144
  previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
12042
12145
  }
@@ -12099,60 +12202,28 @@ ${extractedText}
12099
12202
  if (declined.length && session && user) {
12100
12203
  await saveChat({ session, user: user.id, messages: declined });
12101
12204
  }
12102
- messages = await this.processFilePartsInMessages(messages);
12205
+ messages = await this.processFilePartsInMessages(messages, {
12206
+ contextWindow,
12207
+ sessionID: session,
12208
+ user,
12209
+ exuluConfig
12210
+ });
12211
+ const chronologicalMessages = messages;
12212
+ const contextBudget = deriveContextBudget(contextWindow);
12213
+ const occupancy = contextOccupancy(chronologicalMessages);
12214
+ if (occupancy >= contextBudget.blockThreshold) {
12215
+ console.warn(
12216
+ `[EXULU] Blocking request: occupancy ${occupancy} >= blockThreshold ${contextBudget.blockThreshold} (window ${contextBudget.contextWindow}).`
12217
+ );
12218
+ throw new ContextCompactionRequiredError(occupancy, contextBudget);
12219
+ }
12220
+ messages = sliceHistoryAtCheckpoint(chronologicalMessages);
12103
12221
  const genericContext = "IMPORTANT: \n\n The current date is " + (/* @__PURE__ */ new Date()).toLocaleDateString() + " and the current time is " + (/* @__PURE__ */ new Date()).toLocaleTimeString() + ". If the user does not explicitly provide the current date, for examle when saying ' this weekend', you should assume they are talking with the current date in mind as a reference.";
12104
12222
  let system = instructions || "You are a helpful assistant. When you use a tool to answer a question do not explicitly comment on the result of the tool call unless the user has explicitly you to do something with the result.";
12105
12223
  if (user?.personal_system_prompt?.trim()) {
12106
12224
  system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
12107
12225
  }
12108
12226
  system += "\n\n" + genericContext;
12109
- const includesContextSearchTool = currentTools?.some(
12110
- (tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
12111
- );
12112
- const includesWebSearchTool = currentTools?.some(
12113
- (tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
12114
- );
12115
- console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
12116
- console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
12117
- console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
12118
- if (includesContextSearchTool) {
12119
- system += `
12120
-
12121
-
12122
-
12123
- When you use a context search tool, you will include references to the items
12124
- retrieved from the tool call result inline in the response using this exact JSON format
12125
- (all on one line, no line breaks):
12126
- {item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
12127
-
12128
- IMPORTANT formatting rules:
12129
- - Use the exact format shown above, all on ONE line
12130
- - Do NOT use quotes around field names or values
12131
- - Use the context ID from the tool result
12132
- - Include the file/item name, not the full path
12133
- - Separate multiple citations with spaces
12134
-
12135
- Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
12136
-
12137
- The citations will be rendered as interactive badges in the UI.
12138
- `;
12139
- }
12140
- if (includesWebSearchTool) {
12141
- system += `
12142
-
12143
-
12144
- When you use a web search tool, you will include references to the results of the tool call result inline in the response using this exact JSON format
12145
- (all on one line, no line breaks):
12146
- {url: <url>, title: <title>, snippet: <snippet>}
12147
-
12148
- IMPORTANT formatting rules:
12149
- - Use the exact format shown above, all on ONE line
12150
- - Do NOT use quotes around field names or values
12151
- - Separate multiple results with spaces
12152
-
12153
- Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
12154
- `;
12155
- }
12156
12227
  if (currentSkills?.length) {
12157
12228
  const skillsList = currentSkills.map((skill) => {
12158
12229
  const description = (skill.description ?? "").trim();
@@ -12207,6 +12278,11 @@ ${skillsList}
12207
12278
  read them with the readFile tool. Files you produce yourself (via writeFile or via shell
12208
12279
  commands like \`node create_doc.js\`) live in the same place. These files are scoped to
12209
12280
  this single session; they are NOT visible in other sessions, projects, or knowledge bases.
12281
+
12282
+ Note on large outputs: oversized tool outputs and large uploaded documents are automatically
12283
+ truncated in the conversation; the FULL content is saved as a session file (named in the
12284
+ truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
12285
+ to page through it \u2014 do not ask the user to re-upload.
12210
12286
  `;
12211
12287
  system += `
12212
12288
 
@@ -12230,9 +12306,66 @@ When a tool execution is not approved by the user, do not retry it unless explic
12230
12306
  sessionItems,
12231
12307
  model,
12232
12308
  agent,
12233
- memoryItems
12309
+ memoryItems,
12310
+ contextWindow,
12311
+ disabledTools
12234
12312
  );
12235
12313
  console.log("[EXULU] Converted tools", Object.keys(tools));
12314
+ const includesContextSearchTool = currentTools?.some(
12315
+ (tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
12316
+ );
12317
+ const includesWebSearchTool = currentTools?.some(
12318
+ (tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
12319
+ );
12320
+ console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
12321
+ console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
12322
+ console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
12323
+ if (includesContextSearchTool) {
12324
+ system += `
12325
+
12326
+
12327
+
12328
+ When you use a context search tool, you will include references to the items
12329
+ retrieved from the tool call result inline in the response using this exact JSON format
12330
+ (all on one line, no line breaks):
12331
+ {item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
12332
+
12333
+ IMPORTANT formatting rules:
12334
+ - Use the exact format shown above, all on ONE line
12335
+ - Do NOT use quotes around field names or values
12336
+ - Use the context ID from the tool result
12337
+ - Include the file/item name, not the full path
12338
+ - Separate multiple citations with spaces
12339
+
12340
+ Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
12341
+
12342
+ The citations will be rendered as interactive badges in the UI.
12343
+ `;
12344
+ }
12345
+ if (includesWebSearchTool) {
12346
+ system += `
12347
+
12348
+
12349
+ When you use a web search tool, you will include references to the results of the tool call result inline in the response using this exact JSON format
12350
+ (all on one line, no line breaks):
12351
+ {url: <url>, title: <title>, snippet: <snippet>}
12352
+
12353
+ IMPORTANT formatting rules:
12354
+ - Use the exact format shown above, all on ONE line
12355
+ - Do NOT use quotes around field names or values
12356
+ - Separate multiple results with spaces
12357
+
12358
+ Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
12359
+ `;
12360
+ }
12361
+ const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
12362
+ const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
12363
+ const retrievalGuard = retrievalBudgetGuard(
12364
+ resolveRetrievalCallBudget(toolConfigs),
12365
+ agenticToolKey,
12366
+ Object.keys(tools)
12367
+ );
12368
+ const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
12236
12369
  const result = streamText({
12237
12370
  temperature: 0,
12238
12371
  // TODO Make this configurable
@@ -12257,10 +12390,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
12257
12390
  `Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
12258
12391
  );
12259
12392
  },
12260
- // provide more loops for skills because they are more complex to execute
12261
- // todo allow configuring this per skill
12262
- prepareStep: finalAnswerGuard(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? (currentSkills?.length ? 10 : 5)),
12263
- stopWhen: [stepCountIs(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? (currentSkills?.length ? 10 : 5)), hasToolCall("image_generation")]
12393
+ // todo allow configuring the step budget per skill
12394
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
12395
+ stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
12264
12396
  });
12265
12397
  return {
12266
12398
  stream: result,
@@ -12271,19 +12403,14 @@ When a tool execution is not approved by the user, do not retry it unless explic
12271
12403
  };
12272
12404
  var getAgentMessages = async ({
12273
12405
  session,
12274
- user,
12275
- limit,
12276
- page
12406
+ user
12277
12407
  }) => {
12278
12408
  const { db } = await postgresClient();
12279
- console.log(
12280
- "[EXULU] getting agent messages for session: " + session + " and user: " + user + " and page: " + page
12281
- );
12282
- const query = db.from("agent_messages").where({ session, user: user || null }).limit(limit);
12283
- if (page > 0) {
12284
- query.offset((page - 1) * limit);
12285
- }
12286
- const messages = await query;
12409
+ console.log("[EXULU] getting agent messages for session: " + session + " and user: " + user);
12410
+ const messages = await db.from("agent_messages").where({ session, user: user || null }).orderBy([
12411
+ { column: "createdAt", order: "asc" },
12412
+ { column: "id", order: "asc" }
12413
+ ]);
12287
12414
  return messages;
12288
12415
  };
12289
12416
  var getSession = async ({ sessionID }) => {
@@ -12386,6 +12513,151 @@ ${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
12386
12513
  };
12387
12514
  };
12388
12515
 
12516
+ // src/exulu/resolve-context-window.ts
12517
+ var resolveContextWindow = async ({
12518
+ modelId,
12519
+ exuluProvider
12520
+ }) => {
12521
+ if (process.env.EXULU_USE_LITELLM === "true") {
12522
+ const entry = await findLiteLLMModel(modelId);
12523
+ const fromCatalog = entry?.max_input_tokens ?? entry?.max_tokens;
12524
+ if (fromCatalog != null && fromCatalog > 0) return fromCatalog;
12525
+ } else if (exuluProvider) {
12526
+ try {
12527
+ const fromProvider = exuluProvider.maxContextLength;
12528
+ if (fromProvider != null && fromProvider > 0) return fromProvider;
12529
+ } catch {
12530
+ }
12531
+ }
12532
+ console.warn(
12533
+ `[EXULU] Unknown context window for model "${modelId}" \u2014 assuming ${DEFAULT_CONTEXT_WINDOW}. Check the LiteLLM catalog / provider template metadata.`
12534
+ );
12535
+ return DEFAULT_CONTEXT_WINDOW;
12536
+ };
12537
+
12538
+ // src/exulu/active-streams.ts
12539
+ var active = /* @__PURE__ */ new Set();
12540
+ var markStreamActive = (sessionID) => {
12541
+ active.add(sessionID);
12542
+ };
12543
+ var clearStreamActive = (sessionID) => {
12544
+ active.delete(sessionID);
12545
+ };
12546
+ var isStreamActive = (sessionID) => active.has(sessionID);
12547
+
12548
+ // src/exulu/compact-session.ts
12549
+ import { randomUUID } from "crypto";
12550
+ import { generateText as generateText6, validateUIMessages as validateUIMessages2 } from "ai";
12551
+ var CompactionInsufficientError = class extends Error {
12552
+ constructor(reason) {
12553
+ super(JSON.stringify({ code: COMPACTION_INSUFFICIENT, message: reason }));
12554
+ this.name = "CompactionInsufficientError";
12555
+ }
12556
+ };
12557
+ var MIN_TAIL_MESSAGES = 2;
12558
+ var SUMMARY_TOOL_OUTPUT_SLICE = 1500;
12559
+ var SUMMARY_TOOL_INPUT_SLICE = 200;
12560
+ var SUMMARY_SYSTEM = `You compress chat histories for an AI assistant. Produce a dense, factual summary of the conversation below. Preserve:
12561
+ - the user's intent and any outstanding requests
12562
+ - key facts, decisions, and constraints
12563
+ - files, artifacts, and session files touched \u2014 ALWAYS keep exact file and item names so they stay retrievable
12564
+ - errors encountered and how they were resolved
12565
+ - pending tasks and the current state of the work
12566
+ Do not invent information. Do not include pleasantries. Write compact prose or bullet points.`;
12567
+ var splitTail = (messages, tailTokenBudget) => {
12568
+ const minTail = Math.min(MIN_TAIL_MESSAGES, messages.length);
12569
+ let cut = messages.length;
12570
+ let tokens = 0;
12571
+ for (let i = messages.length - 1; i >= 0; i--) {
12572
+ const t = estimateMessageTokens(messages[i]);
12573
+ const tailCount = messages.length - i;
12574
+ if (tailCount > minTail && tokens + t > tailTokenBudget) break;
12575
+ tokens += t;
12576
+ cut = i;
12577
+ }
12578
+ return { head: messages.slice(0, cut), tail: messages.slice(cut) };
12579
+ };
12580
+ var serializeForSummary = (messages) => messages.map((m) => {
12581
+ const parts = (m.parts ?? []).map((part) => {
12582
+ const p = part;
12583
+ if (p.type === "text") return p.text ?? "";
12584
+ if (p.type === "file") return `[file: ${p.filename ?? p.url ?? "attachment"}]`;
12585
+ if (p.type === "reasoning" || p.type === "step-start") return "";
12586
+ if (p.type?.startsWith("tool-") || p.type === "dynamic-tool") {
12587
+ const out = p.output?.value ?? p.output;
12588
+ const outText = typeof out === "string" ? out : JSON.stringify(out ?? "");
12589
+ return `[tool ${p.type}: ${JSON.stringify(p.input ?? {}).slice(0, SUMMARY_TOOL_INPUT_SLICE)}] \u2192 ${outText.slice(0, SUMMARY_TOOL_OUTPUT_SLICE)}`;
12590
+ }
12591
+ return "";
12592
+ }).filter(Boolean).join("\n");
12593
+ return `${m.role.toUpperCase()}:
12594
+ ${parts}`;
12595
+ }).join("\n\n");
12596
+ var compactSession = async ({
12597
+ sessionID,
12598
+ user,
12599
+ languageModel,
12600
+ contextWindow,
12601
+ steer,
12602
+ modelId,
12603
+ summarize
12604
+ }) => {
12605
+ const budget = deriveContextBudget(contextWindow);
12606
+ const rows = await getAgentMessages({ session: sessionID, user: user.id });
12607
+ const all = await validateUIMessages2({ messages: rows.map((r) => JSON.parse(r.content)) });
12608
+ const history = sliceHistoryAtCheckpoint(all);
12609
+ const { head, tail } = splitTail(history, budget.compactionTailTokens);
12610
+ if (head.length === 0) {
12611
+ throw new CompactionInsufficientError(
12612
+ "There is nothing left to compact \u2014 the recent messages already form the whole context. Start a new chat instead."
12613
+ );
12614
+ }
12615
+ let corpus = serializeForSummary(head);
12616
+ const originalTokens = estimateTokens(corpus);
12617
+ corpus = truncateToolOutput(corpus, contextWindow, "history", 0.3, Math.floor(budget.usableWindow * 0.8) * 4);
12618
+ const system = steer?.trim() ? `${SUMMARY_SYSTEM}
12619
+
12620
+ Focus especially on: ${steer.trim()}` : SUMMARY_SYSTEM;
12621
+ const doSummarize = summarize ?? (async ({ system: sys, prompt, maxOutputTokens }) => {
12622
+ const { text } = await generateText6({
12623
+ model: languageModel,
12624
+ system: sys,
12625
+ prompt,
12626
+ temperature: 0,
12627
+ maxRetries: 2,
12628
+ maxOutputTokens
12629
+ });
12630
+ return text;
12631
+ });
12632
+ const summary = await doSummarize({ system, prompt: corpus, maxOutputTokens: budget.summaryBudgetTokens });
12633
+ const summaryTokens = estimateTokens(summary);
12634
+ let tailTokens = 0;
12635
+ for (const m of tail) tailTokens += estimateMessageTokens(m);
12636
+ const occupancyEstimate = summaryTokens + tailTokens;
12637
+ if (occupancyEstimate >= budget.blockThreshold) {
12638
+ throw new CompactionInsufficientError(
12639
+ "Compacting cannot shrink this conversation below the context limit \u2014 a recent message or output is too large by itself. Start a new chat."
12640
+ );
12641
+ }
12642
+ const compaction = {
12643
+ coversUpTo: head[head.length - 1].id,
12644
+ originalTokens,
12645
+ summaryTokens,
12646
+ occupancyEstimate,
12647
+ ...steer?.trim() ? { steer: steer.trim() } : {}
12648
+ };
12649
+ const checkpoint = {
12650
+ id: `compaction_${randomUUID()}`,
12651
+ role: "user",
12652
+ parts: [{ type: "text", text: `[Conversation summary \u2014 earlier messages were compacted]
12653
+
12654
+ ${summary}` }],
12655
+ metadata: { compaction }
12656
+ };
12657
+ await saveChat({ session: sessionID, user: user.id, messages: [checkpoint], ...modelId ? { model: modelId } : {} });
12658
+ return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
12659
+ };
12660
+
12389
12661
  // src/exulu/transcribe.ts
12390
12662
  var TranscriptionError = class extends Error {
12391
12663
  constructor(upstreamStatus, message) {
@@ -12751,7 +13023,7 @@ function checkApiKeyScope(user, agentId) {
12751
13023
  import "express";
12752
13024
  import {
12753
13025
  streamText as streamText2,
12754
- generateText as generateText6,
13026
+ generateText as generateText7,
12755
13027
  stepCountIs as stepCountIs2,
12756
13028
  jsonSchema
12757
13029
  } from "ai";
@@ -12841,7 +13113,7 @@ function transformCompletion(text, inputTokens, outputTokens, ctx) {
12841
13113
  }
12842
13114
 
12843
13115
  // src/exulu/openai-gateway.ts
12844
- import { randomUUID } from "crypto";
13116
+ import { randomUUID as randomUUID2 } from "crypto";
12845
13117
  import "crypto-js";
12846
13118
  import express from "express";
12847
13119
  function convertOpenAIToolsToAiSdkTools(tools) {
@@ -13155,6 +13427,10 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
13155
13427
  }
13156
13428
  const providerapikey = resolved.apiKey;
13157
13429
  const languageModel = resolved.languageModel;
13430
+ const contextWindow = await resolveContextWindow({
13431
+ modelId: resolved.model.id,
13432
+ exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
13433
+ });
13158
13434
  const disabledTools = req.body.disabledTools ?? [];
13159
13435
  const enabledTools = await getEnabledTools(
13160
13436
  agent,
@@ -13179,12 +13455,50 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
13179
13455
  project?.id,
13180
13456
  void 0,
13181
13457
  languageModel,
13182
- agent
13458
+ agent,
13459
+ void 0,
13460
+ contextWindow,
13461
+ disabledTools
13183
13462
  );
13463
+ const gatewayAgenticEntry = enabledTools?.find((t) => t.id === "agentic_context_search");
13464
+ const gatewayAgenticKey = gatewayAgenticEntry ? sanitizeToolName(gatewayAgenticEntry.name) : void 0;
13465
+ const gatewayRetrievalGuard = retrievalBudgetGuard(
13466
+ resolveRetrievalCallBudget(agent.tools),
13467
+ gatewayAgenticKey,
13468
+ Object.keys(convertedTools)
13469
+ );
13470
+ const turnBudget = resolveTurnStepBudget(void 0, agent);
13184
13471
  const clientTools = Array.isArray(req.body.tools) ? req.body.tools : [];
13185
13472
  const activeTools = clientTools.length > 0 ? convertOpenAIToolsToAiSdkTools(clientTools) : convertedTools;
13186
13473
  const openaiMessages = req.body.messages ?? [];
13187
13474
  const { systemPrompt: requestSystemPrompt, coreMessages } = convertOpenAIMessagesToModelMessages(openaiMessages);
13475
+ const gatewayBudget = deriveContextBudget(contextWindow);
13476
+ const IMAGE_TOKEN_ALLOWANCE = 1e3;
13477
+ let imageCount = 0;
13478
+ const textOnlyMessages = openaiMessages.map((m) => {
13479
+ if (!Array.isArray(m.content)) return m;
13480
+ return {
13481
+ ...m,
13482
+ content: m.content.map((p) => {
13483
+ if (p && typeof p === "object" && "image_url" in p && p.image_url) {
13484
+ imageCount += 1;
13485
+ return { ...p, image_url: { url: "[image]" } };
13486
+ }
13487
+ return p;
13488
+ })
13489
+ };
13490
+ });
13491
+ const promptTokens = estimateTokens(JSON.stringify(textOnlyMessages)) + imageCount * IMAGE_TOKEN_ALLOWANCE;
13492
+ if (promptTokens >= gatewayBudget.blockThreshold) {
13493
+ res.status(400).json({
13494
+ error: {
13495
+ message: `This request is ~${promptTokens.toLocaleString("en-US")} tokens, which exceeds the model's usable context window (${gatewayBudget.usableWindow.toLocaleString("en-US")} tokens). Reduce the conversation history.`,
13496
+ type: "invalid_request_error",
13497
+ code: "context_length_exceeded"
13498
+ }
13499
+ });
13500
+ return;
13501
+ }
13188
13502
  const agentInstructions = agent.instructions ?? "";
13189
13503
  const systemParts = [
13190
13504
  agentInstructions ? `You are an agent named: ${agent.name}
@@ -13194,7 +13508,7 @@ ${project.description}` : ""}` : "",
13194
13508
  requestSystemPrompt
13195
13509
  ].filter(Boolean);
13196
13510
  const systemPrompt = systemParts.join("\n\n");
13197
- const completionId = `chatcmpl-${randomUUID()}`;
13511
+ const completionId = `chatcmpl-${randomUUID2()}`;
13198
13512
  const created = Math.floor(Date.now() / 1e3);
13199
13513
  const hasTools = Object.keys(activeTools).length > 0;
13200
13514
  const ctx = { completionId, created, modelId };
@@ -13208,7 +13522,8 @@ ${project.description}` : ""}` : "",
13208
13522
  messages: coreMessages,
13209
13523
  tools: hasTools ? activeTools : void 0,
13210
13524
  maxRetries: 2,
13211
- stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(5)],
13525
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
13526
+ stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)],
13212
13527
  onError: (error) => {
13213
13528
  console.error("[OPENAI GATEWAY] stream error:", error);
13214
13529
  }
@@ -13241,13 +13556,14 @@ ${project.description}` : ""}` : "",
13241
13556
  const usage = await result.usage;
13242
13557
  await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
13243
13558
  } else {
13244
- const { text, usage } = await generateText6({
13559
+ const { text, usage } = await generateText7({
13245
13560
  model: languageModel,
13246
13561
  system: systemPrompt || void 0,
13247
13562
  messages: coreMessages,
13248
13563
  tools: hasTools ? activeTools : void 0,
13249
13564
  maxRetries: 2,
13250
- stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(5)]
13565
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
13566
+ stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)]
13251
13567
  });
13252
13568
  res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
13253
13569
  await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
@@ -13745,7 +14061,7 @@ Mood: friendly and intelligent.
13745
14061
  });
13746
14062
  return;
13747
14063
  }
13748
- const uuid = randomUUID2();
14064
+ const uuid = randomUUID3();
13749
14065
  const image_url = await uploadFile(Buffer.from(image_base64, "base64"), `${uuid}.png`, config, {
13750
14066
  contentType: "image/png"
13751
14067
  }, authenticationResult.user?.id, void 0, true);
@@ -13949,6 +14265,10 @@ Mood: friendly and intelligent.
13949
14265
  const providerapikey = resolved.apiKey;
13950
14266
  const resolvedLanguageModel = resolved.languageModel;
13951
14267
  const resolvedModelId = resolved.model.id;
14268
+ const contextWindow = await resolveContextWindow({
14269
+ modelId: resolved.model.id,
14270
+ exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
14271
+ });
13952
14272
  if (!!headers.stream) {
13953
14273
  const statistics = {
13954
14274
  label: agent.name,
@@ -13967,27 +14287,46 @@ Mood: friendly and intelligent.
13967
14287
  const instructions = customInstructions ? `${agent.instructions}
13968
14288
 
13969
14289
  ${customInstructions}` : agent.instructions;
13970
- const result = await provider.generateStream({
13971
- contexts,
13972
- agent,
13973
- user,
13974
- instructions,
13975
- session: headers.session,
13976
- message,
13977
- previousMessages,
13978
- currentTools: enabledTools,
13979
- currentSkills: enabledSkills,
13980
- approvedTools,
13981
- allExuluTools: tools,
13982
- languageModel: resolvedLanguageModel,
13983
- providerapikey,
13984
- toolConfigs: agent.tools,
13985
- exuluConfig: config,
13986
- req
13987
- });
14290
+ if (headers.session) markStreamActive(headers.session);
14291
+ let result;
14292
+ try {
14293
+ result = await provider.generateStream({
14294
+ contexts,
14295
+ agent,
14296
+ user,
14297
+ instructions,
14298
+ session: headers.session,
14299
+ message,
14300
+ previousMessages,
14301
+ currentTools: enabledTools,
14302
+ currentSkills: enabledSkills,
14303
+ approvedTools,
14304
+ allExuluTools: tools,
14305
+ languageModel: resolvedLanguageModel,
14306
+ providerapikey,
14307
+ toolConfigs: agent.tools,
14308
+ exuluConfig: config,
14309
+ req,
14310
+ contextWindow,
14311
+ disabledTools
14312
+ });
14313
+ } catch (err) {
14314
+ if (headers.session) clearStreamActive(headers.session);
14315
+ if (err instanceof ContextCompactionRequiredError) {
14316
+ res.status(413).send(err.message);
14317
+ return;
14318
+ }
14319
+ throw err;
14320
+ }
13988
14321
  result.stream.consumeStream();
13989
14322
  result.stream.pipeUIMessageStreamToResponse(res, {
13990
14323
  messageMetadata: ({ part }) => {
14324
+ if (part.type === "finish-step") {
14325
+ return {
14326
+ lastStepInputTokens: part.usage.inputTokens,
14327
+ lastStepOutputTokens: part.usage.outputTokens
14328
+ };
14329
+ }
13991
14330
  if (part.type === "finish") {
13992
14331
  return {
13993
14332
  totalTokens: part.totalUsage.totalTokens,
@@ -14004,22 +14343,20 @@ ${customInstructions}` : agent.instructions;
14004
14343
  sendSources: true,
14005
14344
  onError: (error) => {
14006
14345
  console.error("[EXULU] chat response error.", error);
14007
- if (error == null) {
14008
- return "unknown error";
14009
- }
14010
- if (typeof error === "string") {
14011
- return error;
14012
- }
14013
- if (error instanceof Error) {
14014
- return error.message;
14015
- }
14016
- return JSON.stringify(error);
14346
+ if (headers.session) clearStreamActive(headers.session);
14347
+ let message2;
14348
+ if (error == null) message2 = "unknown error";
14349
+ else if (typeof error === "string") message2 = error;
14350
+ else if (error instanceof Error) message2 = error.message;
14351
+ else message2 = JSON.stringify(error);
14352
+ return mapStreamErrorMessage(message2);
14017
14353
  },
14018
14354
  generateMessageId: createIdGenerator({
14019
14355
  prefix: "msg_",
14020
14356
  size: 16
14021
14357
  }),
14022
14358
  onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
14359
+ if (headers.session) clearStreamActive(headers.session);
14023
14360
  console.log(
14024
14361
  "[EXULU] onFinish",
14025
14362
  messages?.map((msg) => msg.parts?.map((part) => part.type === "text" ? part.text : null)).join("\n")
@@ -14081,33 +14418,129 @@ ${customInstructions}` : agent.instructions;
14081
14418
  const instructions = customInstructions ? `${agent.instructions}
14082
14419
 
14083
14420
  ${customInstructions}` : agent.instructions;
14084
- const response = await provider.generateSync({
14085
- contexts,
14086
- agent,
14087
- user,
14088
- req,
14089
- instructions,
14090
- session: headers.session,
14091
- inputMessages: [req.body.message],
14092
- currentTools: enabledTools,
14093
- currentSkills: enabledSkills,
14094
- allExuluTools: tools,
14095
- languageModel: resolvedLanguageModel,
14096
- providerapikey,
14097
- exuluConfig: config,
14098
- toolConfigs: agent.tools,
14099
- statistics: {
14100
- label: agent.name,
14101
- trigger: "agent"
14102
- },
14103
- onTokenUsage: async ({ inputTokens, outputTokens }) => {
14421
+ let response;
14422
+ try {
14423
+ response = await provider.generateSync({
14424
+ contexts,
14425
+ agent,
14426
+ user,
14427
+ req,
14428
+ instructions,
14429
+ session: headers.session,
14430
+ inputMessages: [req.body.message],
14431
+ currentTools: enabledTools,
14432
+ currentSkills: enabledSkills,
14433
+ allExuluTools: tools,
14434
+ languageModel: resolvedLanguageModel,
14435
+ providerapikey,
14436
+ exuluConfig: config,
14437
+ toolConfigs: agent.tools,
14438
+ contextWindow,
14439
+ disabledTools,
14440
+ statistics: {
14441
+ label: agent.name,
14442
+ trigger: "agent"
14443
+ },
14444
+ onTokenUsage: async ({ inputTokens, outputTokens }) => {
14445
+ }
14446
+ });
14447
+ } catch (err) {
14448
+ if (err instanceof ContextCompactionRequiredError) {
14449
+ res.status(413).send(err.message);
14450
+ return;
14104
14451
  }
14105
- });
14452
+ throw err;
14453
+ }
14106
14454
  res.status(200).json(response);
14107
14455
  return;
14108
14456
  }
14109
14457
  });
14110
14458
  };
14459
+ const registerAgentCompactRoute = (slug) => {
14460
+ app.post(slug + "/:instance", async (req, res) => {
14461
+ const instance = req.params.instance;
14462
+ if (!instance) {
14463
+ res.status(400).json({ message: "Missing instance in request." });
14464
+ return;
14465
+ }
14466
+ const sessionID = req.headers["session"] || null;
14467
+ if (!sessionID) {
14468
+ res.status(400).json({ message: "Missing session header." });
14469
+ return;
14470
+ }
14471
+ const agent = await exuluApp.get().agent(instance);
14472
+ if (!agent) {
14473
+ res.status(404).json({ message: "Agent with id " + instance + " not found." });
14474
+ return;
14475
+ }
14476
+ const authenticationResult = await requestValidators.authenticate(req);
14477
+ if (!authenticationResult.user?.id) {
14478
+ res.status(authenticationResult.code || 401).json({ detail: `${authenticationResult.message}` });
14479
+ return;
14480
+ }
14481
+ const user = authenticationResult.user;
14482
+ const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
14483
+ if (!hasAccessToAgent) {
14484
+ res.status(401).json({ message: "You don't have access to this agent." });
14485
+ return;
14486
+ }
14487
+ const { db } = await postgresClient();
14488
+ const sessionRow = await db.from("agent_sessions").where({ id: sessionID }).first();
14489
+ if (!sessionRow) {
14490
+ res.status(404).json({ message: "Session not found for session ID: " + sessionID });
14491
+ return;
14492
+ }
14493
+ const hasAccessToSession = await checkRecordAccess(sessionRow, "write", user);
14494
+ if (!hasAccessToSession) {
14495
+ res.status(401).json({ message: "You don't have access to this session." });
14496
+ return;
14497
+ }
14498
+ if (isStreamActive(sessionID)) {
14499
+ res.status(409).json({ message: "A response is still streaming for this session \u2014 try again when it finishes." });
14500
+ return;
14501
+ }
14502
+ const overrideModelId = req.headers["x-exulu-model-override"];
14503
+ const modelId = overrideModelId ?? agent.model;
14504
+ if (!modelId) {
14505
+ res.status(400).json({ message: `Agent ${agent.name} (${agent.id}) has no model configured.` });
14506
+ return;
14507
+ }
14508
+ let resolved;
14509
+ try {
14510
+ resolved = await resolveModel({ modelId, user, providers, agent });
14511
+ } catch (err) {
14512
+ if (err instanceof ResolveModelError) {
14513
+ const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
14514
+ res.status(status).json({ message: err.message, code: err.code });
14515
+ return;
14516
+ }
14517
+ throw err;
14518
+ }
14519
+ const contextWindow = await resolveContextWindow({
14520
+ modelId: resolved.model.id,
14521
+ exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
14522
+ });
14523
+ const steer = typeof req.body?.steer === "string" ? req.body.steer : void 0;
14524
+ try {
14525
+ const result = await compactSession({
14526
+ sessionID,
14527
+ user,
14528
+ languageModel: resolved.languageModel,
14529
+ contextWindow,
14530
+ steer,
14531
+ modelId: resolved.model.id
14532
+ });
14533
+ res.json(result);
14534
+ } catch (err) {
14535
+ if (err instanceof CompactionInsufficientError) {
14536
+ res.status(422).send(err.message);
14537
+ return;
14538
+ }
14539
+ console.error("[EXULU] compactSession failed.", err);
14540
+ res.status(500).json({ message: err instanceof Error ? err.message : "Compaction failed." });
14541
+ }
14542
+ });
14543
+ };
14111
14544
  providers.forEach((provider) => {
14112
14545
  const slug = provider.slug;
14113
14546
  if (!slug) return;
@@ -14116,6 +14549,14 @@ ${customInstructions}` : agent.instructions;
14116
14549
  if (isLiteLLMEnabled() && providers.length > 0) {
14117
14550
  registerAgentRunRoute("/agents/litellm/run", providers[0]);
14118
14551
  }
14552
+ providers.forEach((provider) => {
14553
+ const slug = provider.slug;
14554
+ if (!slug) return;
14555
+ registerAgentCompactRoute(slug.replace(/\/run$/, "/compact"));
14556
+ });
14557
+ if (isLiteLLMEnabled() && providers.length > 0) {
14558
+ registerAgentCompactRoute("/agents/litellm/compact");
14559
+ }
14119
14560
  app.post("/agents/suggestions/:agentId", async (req, res) => {
14120
14561
  const agentId = req.params.agentId;
14121
14562
  if (!agentId) {
@@ -14467,7 +14908,7 @@ ${customInstructions}` : agent.instructions;
14467
14908
  const keys = [];
14468
14909
  const revisedPrompts = [];
14469
14910
  for (const img of images) {
14470
- const filename = `${randomUUID2()}.${img.extension}`;
14911
+ const filename = `${randomUUID3()}.${img.extension}`;
14471
14912
  const key = `sessions/${sessionId}/images/${toolCallId}/${filename}`;
14472
14913
  const fullKey = await uploadFile(
14473
14914
  img.buffer,
@@ -14755,7 +15196,7 @@ ${style.markdown}` : params.prompt;
14755
15196
  (d) => `- ${d.presignedUrl} (prompt: "${d.prompt}", model: ${d.model}${d.styleName ? `, style: ${d.styleName}` : ""})`
14756
15197
  );
14757
15198
  const messageText = "The user generated and selected the following image(s) in this chat:\n" + lines.join("\n");
14758
- const messageId = randomUUID2();
15199
+ const messageId = randomUUID3();
14759
15200
  const uiMessage = {
14760
15201
  id: messageId,
14761
15202
  role: "system",
@@ -15683,7 +16124,7 @@ ${style.markdown}` : params.prompt;
15683
16124
  res.status(404).json({ detail: "Skill not found." });
15684
16125
  return;
15685
16126
  }
15686
- const stagingKey = `user_${authResult.user.id}/skills/_staging/${randomUUID2()}${extension}`;
16127
+ const stagingKey = `user_${authResult.user.id}/skills/_staging/${randomUUID3()}${extension}`;
15687
16128
  const fullKey = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/${stagingKey}` : stagingKey;
15688
16129
  const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
15689
16130
  res.json({ uploadUrl, stagingKey });
@@ -16714,7 +17155,7 @@ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
16714
17155
 
16715
17156
  // src/mcp/index.ts
16716
17157
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16717
- import { randomUUID as randomUUID3 } from "crypto";
17158
+ import { randomUUID as randomUUID4 } from "crypto";
16718
17159
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
16719
17160
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
16720
17161
  import "express";
@@ -16819,7 +17260,7 @@ var ExuluMCP = class {
16819
17260
  throw new Error("Tool not found in converted tools array.");
16820
17261
  }
16821
17262
  const iterator = await convertedTool.execute(inputs, {
16822
- toolCallId: tool2.id + "_" + randomUUID3(),
17263
+ toolCallId: tool2.id + "_" + randomUUID4(),
16823
17264
  messages: []
16824
17265
  });
16825
17266
  let result;
@@ -17011,7 +17452,7 @@ var ExuluMCP = class {
17011
17452
  transport = this.transports[sessionId];
17012
17453
  } else if (!sessionId && isInitializeRequest(req.body)) {
17013
17454
  transport = new StreamableHTTPServerTransport({
17014
- sessionIdGenerator: () => randomUUID3(),
17455
+ sessionIdGenerator: () => randomUUID4(),
17015
17456
  onsessioninitialized: (sessionId2) => {
17016
17457
  this.transports[sessionId2] = transport;
17017
17458
  }
@@ -17992,7 +18433,7 @@ var ExuluEval = class {
17992
18433
 
17993
18434
  // src/templates/evals/index.ts
17994
18435
  import { z as z5 } from "zod";
17995
- import { generateText as generateText7, Output as Output2 } from "ai";
18436
+ import { generateText as generateText8, Output as Output2 } from "ai";
17996
18437
  var llmAsJudgeEval = () => {
17997
18438
  if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
17998
18439
  return new ExuluEval({
@@ -18037,7 +18478,7 @@ var llmAsJudgeEval = () => {
18037
18478
  rbacBypass: true
18038
18479
  });
18039
18480
  console.log("[EXULU] prompt", prompt);
18040
- const { output } = await generateText7({
18481
+ const { output } = await generateText8({
18041
18482
  temperature: 0,
18042
18483
  model: resolved.languageModel,
18043
18484
  system: "",
@@ -18460,7 +18901,7 @@ After asking a question, use the Question Read tool to check if the user has ans
18460
18901
 
18461
18902
  // src/templates/tools/question/question-ask.ts
18462
18903
  import z7 from "zod";
18463
- import { randomUUID as randomUUID4 } from "crypto";
18904
+ import { randomUUID as randomUUID5 } from "crypto";
18464
18905
  var AnswerOptionSchema = z7.object({
18465
18906
  id: z7.string().describe("Unique identifier for the answer option"),
18466
18907
  text: z7.string().describe("The text of the answer option")
@@ -18514,15 +18955,15 @@ var QuestionAskTool = new ExuluTool({
18514
18955
  throw new Error("You don't have access to this session " + session.id + ".");
18515
18956
  }
18516
18957
  const answerOptionsWithIds = answerOptions.map((text) => ({
18517
- id: randomUUID4(),
18958
+ id: randomUUID5(),
18518
18959
  text
18519
18960
  }));
18520
18961
  answerOptionsWithIds.push({
18521
- id: randomUUID4(),
18962
+ id: randomUUID5(),
18522
18963
  text: "None of the above..."
18523
18964
  });
18524
18965
  const newQuestion = {
18525
- id: randomUUID4(),
18966
+ id: randomUUID5(),
18526
18967
  question,
18527
18968
  answerOptions: answerOptionsWithIds,
18528
18969
  status: "pending"
@@ -21303,10 +21744,10 @@ var MarkdownChunker = class {
21303
21744
  // ee/python/documents/processing/doc_processor.ts
21304
21745
  import * as fs3 from "fs";
21305
21746
  import * as path from "path";
21306
- import { generateText as generateText8, Output as Output3 } from "ai";
21747
+ import { generateText as generateText9, Output as Output3 } from "ai";
21307
21748
  import { z as z12 } from "zod";
21308
21749
  import pLimit from "p-limit";
21309
- import { randomUUID as randomUUID5 } from "crypto";
21750
+ import { randomUUID as randomUUID6 } from "crypto";
21310
21751
  import * as mammoth from "mammoth";
21311
21752
  import TurndownService from "turndown";
21312
21753
  import WordExtractor from "word-extractor";
@@ -21760,7 +22201,7 @@ If the page contains a flow-chart, schematic, technical drawing or control board
21760
22201
 
21761
22202
  ### 7. Only populate \`corrected_text\` when \`needs_correction\` is true. If the OCR output is accurate, return \`needs_correction: false\` and \`corrected_content: null\`.
21762
22203
  `;
21763
- const result = await generateText8({
22204
+ const result = await generateText9({
21764
22205
  model,
21765
22206
  output: Output3.object({
21766
22207
  schema: z12.object({
@@ -22181,7 +22622,7 @@ var loadFile = async (file, name, tempDir) => {
22181
22622
  if (!fileType) {
22182
22623
  throw new Error("[EXULU] File name does not include extension, extension is required for document processing.");
22183
22624
  }
22184
- const UUID = randomUUID5();
22625
+ const UUID = randomUUID6();
22185
22626
  let buffer;
22186
22627
  if (Buffer.isBuffer(file)) {
22187
22628
  filePath = path.join(tempDir, `${UUID}.${fileType}`);
@@ -22211,7 +22652,7 @@ async function documentProcessor({
22211
22652
  if (!license["advanced-document-processing"]) {
22212
22653
  throw new Error("Advanced document processing is an enterprise feature, please add a valid Exulu enterprise license key to use it.");
22213
22654
  }
22214
- const uuid = randomUUID5();
22655
+ const uuid = randomUUID6();
22215
22656
  const tempDir = path.join(process.cwd(), "temp", uuid);
22216
22657
  const localFilesAndFoldersToDelete = [tempDir];
22217
22658
  console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);