@exulu/backend 2.0.1 → 2.2.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,12 +46,15 @@ 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,
57
+ parseResetAt,
49
58
  postgresClient,
50
59
  provisionDefaultUserBudget,
51
60
  reportSystemDependencies,
@@ -55,15 +64,17 @@ import {
55
64
  sanitizeToolName,
56
65
  setBudgetSettings,
57
66
  setLiteLLMPackageRoot,
67
+ sliceHistoryAtCheckpoint,
58
68
  startLiteLLMSupervisor,
59
69
  tagDelete,
60
70
  tagInfo,
71
+ truncateToolOutput,
61
72
  updateStatistic,
62
73
  uploadFile,
63
74
  upsertBudget,
64
75
  waitForLiteLLMReady,
65
76
  withRetry
66
- } from "./chunk-IJ4HNHOT.js";
77
+ } from "./chunk-Y7JPNBFM.js";
67
78
  import {
68
79
  findLiteLLMModel
69
80
  } from "./chunk-7CCMW3IW.js";
@@ -2213,6 +2224,13 @@ var agentsSchema = {
2213
2224
  name: "sandbox_enabled",
2214
2225
  type: "boolean",
2215
2226
  default: false
2227
+ },
2228
+ {
2229
+ // Per-turn budget for ALL tool steps on one chat message (bash, files,
2230
+ // knowledge search, integrations). 0/null = platform default
2231
+ // (DEFAULT_MAX_STEPS in resolve-max-steps.ts). Auto-ALTERed on boot.
2232
+ name: "max_tool_steps",
2233
+ type: "number"
2216
2234
  }
2217
2235
  ]
2218
2236
  };
@@ -4252,7 +4270,7 @@ var ExuluContext2 = class {
4252
4270
  embedder,
4253
4271
  chunker,
4254
4272
  processor,
4255
- active,
4273
+ active: active2,
4256
4274
  fields,
4257
4275
  queryRewriter,
4258
4276
  resultReranker,
@@ -4283,7 +4301,7 @@ var ExuluContext2 = class {
4283
4301
  this.description = description;
4284
4302
  this.embedder = embedder;
4285
4303
  this.chunker = chunker;
4286
- this.active = active;
4304
+ this.active = active2;
4287
4305
  this.queryRewriter = queryRewriter;
4288
4306
  this.resultReranker = resultReranker;
4289
4307
  this.entities = entities;
@@ -5398,14 +5416,26 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
5398
5416
  )
5399
5417
  );
5400
5418
  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);
5419
+ const hasAgentic = result.tools.some(
5420
+ (tool2) => tool2?.id === "agentic_context_search"
5421
+ );
5422
+ if (!hasAgentic) {
5423
+ const instance = createAgenticRetrievalTool({
5424
+ contexts: [],
5425
+ user,
5426
+ role: user.role?.id,
5427
+ model: void 0
5428
+ });
5429
+ if (instance) {
5430
+ result.tools.unshift({
5431
+ id: instance.id,
5432
+ name: instance.name,
5433
+ description: instance.description,
5434
+ category: instance.category,
5435
+ type: instance.type,
5436
+ config: []
5437
+ });
5438
+ }
5409
5439
  }
5410
5440
  }
5411
5441
  result.tools = result.tools.filter((tool2) => tool2 !== null);
@@ -11105,21 +11135,7 @@ function isOsJunkPath(path2) {
11105
11135
  const basename = path2.split("/").pop() ?? "";
11106
11136
  return basename === ".DS_Store" || basename === "Thumbs.db" || basename === "desktop.ini";
11107
11137
  }
11108
- async function extractBundleToS3(opts) {
11109
- const { bytes, skillId, isZip, config } = opts;
11110
- if (!isZip) {
11111
- await uploadFile(
11112
- bytes,
11113
- `skills/${skillId}/v1/SKILL.md`,
11114
- config,
11115
- { contentType: "text/markdown" },
11116
- void 0,
11117
- void 0,
11118
- true
11119
- // global=true so the key isn't user-prefixed (skill files are shared)
11120
- );
11121
- return { filesCount: 1 };
11122
- }
11138
+ async function extractZipToPrefix(bytes, prefix, config) {
11123
11139
  let zip;
11124
11140
  try {
11125
11141
  zip = await JSZip.loadAsync(bytes);
@@ -11183,7 +11199,7 @@ async function extractBundleToS3(opts) {
11183
11199
  }
11184
11200
  let filesCount = 0;
11185
11201
  for (const { relPath, content } of prepared) {
11186
- const s3Key = `skills/${skillId}/v1/${relPath}`;
11202
+ const s3Key = `${prefix}${relPath}`;
11187
11203
  await uploadFile(
11188
11204
  content,
11189
11205
  s3Key,
@@ -11192,12 +11208,76 @@ async function extractBundleToS3(opts) {
11192
11208
  void 0,
11193
11209
  void 0,
11194
11210
  true
11195
- // global=true — see SKILL.md case above
11211
+ // global=true — skill files are shared across users
11196
11212
  );
11197
11213
  filesCount += 1;
11198
11214
  }
11199
11215
  return { filesCount };
11200
11216
  }
11217
+ async function extractBundleToS3(opts) {
11218
+ const { bytes, skillId, isZip, config } = opts;
11219
+ if (!isZip) {
11220
+ await uploadFile(
11221
+ bytes,
11222
+ `skills/${skillId}/v1/SKILL.md`,
11223
+ config,
11224
+ { contentType: "text/markdown" },
11225
+ void 0,
11226
+ void 0,
11227
+ true
11228
+ // global=true so the key isn't user-prefixed (skill files are shared)
11229
+ );
11230
+ return { filesCount: 1 };
11231
+ }
11232
+ return extractZipToPrefix(bytes, `skills/${skillId}/v1/`, config);
11233
+ }
11234
+ async function extractBundleToVersion(opts) {
11235
+ const { bytes, skillId, version, config } = opts;
11236
+ return extractZipToPrefix(bytes, `skills/${skillId}/v${version}/`, config);
11237
+ }
11238
+
11239
+ // src/skills/frontmatter.ts
11240
+ import JSZip2 from "jszip";
11241
+ function parseFrontmatter(md) {
11242
+ const match = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(md);
11243
+ if (!match) return {};
11244
+ const block = match[1];
11245
+ const out = {};
11246
+ for (const line of block.split(/\r?\n/)) {
11247
+ const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
11248
+ if (!m) continue;
11249
+ const key = m[1];
11250
+ let v = m[2].trim();
11251
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
11252
+ v = v.slice(1, -1);
11253
+ }
11254
+ out[key] = v;
11255
+ }
11256
+ return out;
11257
+ }
11258
+ async function parseSkillFrontmatter(zipBytes) {
11259
+ let zip;
11260
+ try {
11261
+ zip = await JSZip2.loadAsync(zipBytes);
11262
+ } catch {
11263
+ return {};
11264
+ }
11265
+ const paths = [];
11266
+ zip.forEach((p, entry) => {
11267
+ if (!entry.dir) paths.push(p);
11268
+ });
11269
+ const heads = new Set(paths.map((p) => p.split("/")[0]).filter(Boolean));
11270
+ let strip = (p) => p;
11271
+ if (heads.size === 1) {
11272
+ const head = [...heads][0] + "/";
11273
+ if (paths.every((p) => p.startsWith(head))) strip = (p) => p.slice(head.length);
11274
+ }
11275
+ const skillPath = paths.find((p) => strip(p) === "SKILL.md");
11276
+ if (!skillPath) return {};
11277
+ const md = await zip.file(skillPath).async("string");
11278
+ const fm = parseFrontmatter(md);
11279
+ return { name: fm.name, description: fm.description };
11280
+ }
11201
11281
 
11202
11282
  // src/sessions/pdf-preview-cache.ts
11203
11283
  import { exec } from "child_process";
@@ -11275,14 +11355,15 @@ import bodyParser from "body-parser";
11275
11355
  import CryptoJS4 from "crypto-js";
11276
11356
  import OpenAI from "openai";
11277
11357
  import fs2 from "fs";
11278
- import { randomUUID as randomUUID2 } from "crypto";
11358
+ import { randomUUID as randomUUID3 } from "crypto";
11279
11359
  import "@opentelemetry/api";
11280
- import JSZip2 from "jszip";
11360
+ import JSZip3 from "jszip";
11281
11361
  import { createIdGenerator } from "ai";
11282
11362
  import cookieParser from "cookie-parser";
11283
11363
 
11284
11364
  // src/exulu/resolve-max-steps.ts
11285
- function resolveMaxStepsFromToolConfigs(toolConfigs) {
11365
+ var DEFAULT_MAX_STEPS = 10;
11366
+ function resolveRetrievalCallBudget(toolConfigs) {
11286
11367
  const agentic = toolConfigs?.find((t) => t.id === "agentic_context_search");
11287
11368
  if (!agentic?.config) return void 0;
11288
11369
  const entry = agentic.config.find((c) => c.name === "max_steps");
@@ -11291,15 +11372,26 @@ function resolveMaxStepsFromToolConfigs(toolConfigs) {
11291
11372
  const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
11292
11373
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
11293
11374
  }
11375
+ function resolveTurnStepBudget(maxStepCount, agent) {
11376
+ if (typeof maxStepCount === "number" && Number.isFinite(maxStepCount) && maxStepCount > 0) {
11377
+ return Math.floor(maxStepCount);
11378
+ }
11379
+ const raw = agent?.max_tool_steps;
11380
+ const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
11381
+ if (Number.isFinite(n) && n > 0) {
11382
+ return Math.floor(n);
11383
+ }
11384
+ return DEFAULT_MAX_STEPS;
11385
+ }
11294
11386
  function flattenPart(part) {
11295
11387
  const p = part;
11296
11388
  if (p?.type === "text") return p.text ?? "";
11297
11389
  if (p?.type === "tool-call") {
11298
- return `[searched ${p.toolName}: ${JSON.stringify(p.input ?? {}).slice(0, 300)}]`;
11390
+ return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
11299
11391
  }
11300
11392
  if (p?.type === "tool-result") {
11301
11393
  const out = p.output?.value ?? p.output;
11302
- return `[results from ${p.toolName}]: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
11394
+ return `The "${p.toolName}" tool returned: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
11303
11395
  }
11304
11396
  return "";
11305
11397
  }
@@ -11317,7 +11409,7 @@ function flattenToolHistory(messages) {
11317
11409
  return m;
11318
11410
  });
11319
11411
  }
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.";
11412
+ 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
11413
  function finalAnswerGuard(maxSteps) {
11322
11414
  return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
11323
11415
  toolChoice: "none",
@@ -11330,6 +11422,68 @@ function finalAnswerGuard(maxSteps) {
11330
11422
  } : {}
11331
11423
  } : void 0;
11332
11424
  }
11425
+ function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
11426
+ if (limit == null || limit <= 0 || !agenticToolKey || !allToolKeys.includes(agenticToolKey)) {
11427
+ return () => void 0;
11428
+ }
11429
+ const remainingTools = allToolKeys.filter((k) => k !== agenticToolKey);
11430
+ return ({ steps }) => {
11431
+ const calls = (steps ?? []).flatMap((s) => s?.toolCalls ?? []).filter((c) => c?.toolName === agenticToolKey).length;
11432
+ if (calls < limit) return void 0;
11433
+ return { activeTools: remainingTools };
11434
+ };
11435
+ }
11436
+
11437
+ // src/exulu/context-guard.ts
11438
+ var KEEP_RECENT_TOOL_MESSAGES = 2;
11439
+ var COLLAPSE_KEEP_CHARS = 400;
11440
+ 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]";
11441
+ function contextGuard(contextWindow) {
11442
+ const budget = deriveContextBudget(contextWindow);
11443
+ return async ({ messages }) => {
11444
+ if (!Array.isArray(messages) || messages.length === 0) return void 0;
11445
+ const tokens = estimateTokens(JSON.stringify(messages));
11446
+ if (tokens < budget.usableWindow) return void 0;
11447
+ const toolIndices = messages.map((m, i) => m?.role === "tool" ? i : -1).filter((i) => i !== -1);
11448
+ const collapsible = new Set(toolIndices.slice(0, Math.max(0, toolIndices.length - KEEP_RECENT_TOOL_MESSAGES)));
11449
+ if (collapsible.size === 0) return void 0;
11450
+ let changed = false;
11451
+ const next = messages.map((m, i) => {
11452
+ if (!collapsible.has(i)) return m;
11453
+ const msg = m;
11454
+ if (!Array.isArray(msg.content)) return m;
11455
+ const content = msg.content.map((part) => {
11456
+ const p = part;
11457
+ if (p?.type !== "tool-result") return part;
11458
+ const out = p.output?.value ?? p.output;
11459
+ const asText = typeof out === "string" ? out : JSON.stringify(out ?? "");
11460
+ if (asText.length <= COLLAPSE_KEEP_CHARS + COLLAPSE_MARKER.length) return part;
11461
+ changed = true;
11462
+ return { ...part, output: { type: "text", value: asText.slice(0, COLLAPSE_KEEP_CHARS) + COLLAPSE_MARKER } };
11463
+ });
11464
+ return { ...m, content };
11465
+ });
11466
+ return changed ? { messages: next } : void 0;
11467
+ };
11468
+ }
11469
+ function composePrepareSteps(...guards) {
11470
+ return async (opts) => {
11471
+ let merged;
11472
+ let messages = opts.messages;
11473
+ for (const guard of guards) {
11474
+ const result = await guard({ ...opts, messages });
11475
+ if (!result) continue;
11476
+ merged = { ...merged ?? {}, ...result };
11477
+ if (Array.isArray(result.messages)) {
11478
+ messages = result.messages;
11479
+ }
11480
+ }
11481
+ if (merged && messages && !("messages" in merged)) {
11482
+ merged.messages = messages;
11483
+ }
11484
+ return merged;
11485
+ };
11486
+ }
11333
11487
 
11334
11488
  // src/exulu/auto-decline-stale-approvals.ts
11335
11489
  var AUTO_DECLINE_REASON = "Automatically declined because the user sent a new message instead of responding to the approval request.";
@@ -11593,7 +11747,9 @@ var ExuluProvider = class {
11593
11747
  agent,
11594
11748
  instructions,
11595
11749
  maxStepCount,
11596
- onTokenUsage
11750
+ onTokenUsage,
11751
+ contextWindow,
11752
+ disabledTools
11597
11753
  }) => {
11598
11754
  console.log(
11599
11755
  "[EXULU] Called generate sync for agent: " + this.name,
@@ -11621,9 +11777,7 @@ var ExuluProvider = class {
11621
11777
  if (messages && session && user) {
11622
11778
  const previousMessages = await getAgentMessages({
11623
11779
  session,
11624
- user: user.id,
11625
- limit: 50,
11626
- page: 1
11780
+ user: user.id
11627
11781
  });
11628
11782
  const previousMessagesContent = previousMessages.map(
11629
11783
  (message) => JSON.parse(message.content)
@@ -11632,6 +11786,12 @@ var ExuluProvider = class {
11632
11786
  // append the new message to the previous messages:
11633
11787
  messages: [...previousMessagesContent, ...messages]
11634
11788
  });
11789
+ const contextBudget = deriveContextBudget(contextWindow);
11790
+ const occupancy = contextOccupancy(messages);
11791
+ if (occupancy >= contextBudget.blockThreshold) {
11792
+ throw new ContextCompactionRequiredError(occupancy, contextBudget);
11793
+ }
11794
+ messages = sliceHistoryAtCheckpoint(messages);
11635
11795
  }
11636
11796
  console.log(
11637
11797
  "[EXULU] Message count for agent: " + this.name,
@@ -11696,6 +11856,34 @@ var ExuluProvider = class {
11696
11856
  if (memoryContext) {
11697
11857
  system += "\n\n" + memoryContext;
11698
11858
  }
11859
+ const tools = await convertExuluToolsToAiSdkTools(
11860
+ currentTools,
11861
+ currentSkills,
11862
+ approvedTools,
11863
+ allExuluTools,
11864
+ toolConfigs,
11865
+ providerapikey,
11866
+ contexts,
11867
+ user,
11868
+ exuluConfig,
11869
+ session,
11870
+ req,
11871
+ project,
11872
+ sessionItems,
11873
+ model,
11874
+ agent,
11875
+ memoryItems,
11876
+ contextWindow,
11877
+ disabledTools
11878
+ );
11879
+ const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
11880
+ const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
11881
+ const retrievalGuard = retrievalBudgetGuard(
11882
+ resolveRetrievalCallBudget(toolConfigs),
11883
+ agenticToolKey,
11884
+ Object.keys(tools)
11885
+ );
11886
+ const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
11699
11887
  const includesContextSearchTool = currentTools?.some(
11700
11888
  (tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
11701
11889
  );
@@ -11708,12 +11896,12 @@ var ExuluProvider = class {
11708
11896
  system += `
11709
11897
 
11710
11898
 
11711
-
11899
+
11712
11900
  When you use a context search tool, you will include references to the items
11713
11901
  retrieved from the tool call result inline in the response using this exact JSON format
11714
11902
  (all on one line, no line breaks):
11715
11903
  {item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
11716
-
11904
+
11717
11905
  IMPORTANT formatting rules:
11718
11906
  - 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
11907
  - Use the exact format shown above, all on ONE line
@@ -11721,9 +11909,9 @@ var ExuluProvider = class {
11721
11909
  - Use the context ID from the tool result
11722
11910
  - Include the file/item name, not the full path
11723
11911
  - Separate multiple citations with spaces
11724
-
11912
+
11725
11913
  Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
11726
-
11914
+
11727
11915
  The citations will be rendered as interactive badges in the UI.
11728
11916
  `;
11729
11917
  }
@@ -11734,12 +11922,12 @@ var ExuluProvider = class {
11734
11922
  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
11923
  (all on one line, no line breaks):
11736
11924
  {url: <url>, title: <title>, snippet: <snippet>}
11737
-
11925
+
11738
11926
  IMPORTANT formatting rules:
11739
11927
  - Use the exact format shown above, all on ONE line
11740
11928
  - Do NOT use quotes around field names or values
11741
11929
  - Separate multiple results with spaces
11742
-
11930
+
11743
11931
  Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
11744
11932
  `;
11745
11933
  }
@@ -11772,29 +11960,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
11772
11960
  system,
11773
11961
  prompt,
11774
11962
  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
- ),
11963
+ tools,
11793
11964
  // Stop after the image_generation tool fires — the widget IS the
11794
11965
  // assistant's response, no follow-up text turn is wanted (same
11795
11966
  // 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")]
11967
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
11968
+ stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
11798
11969
  });
11799
11970
  console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
11800
11971
  const {
@@ -11855,26 +12026,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
11855
12026
  ignoreIncompleteToolCalls: true
11856
12027
  }),
11857
12028
  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")]
12029
+ tools,
12030
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
12031
+ stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
11878
12032
  });
11879
12033
  if (statistics) {
11880
12034
  await Promise.all([
@@ -11926,7 +12080,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
11926
12080
  * - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
11927
12081
  * - Image files -> image parts (which ARE supported by Responses API)
11928
12082
  */
11929
- async processFilePartsInMessages(messages) {
12083
+ async processFilePartsInMessages(messages, offloadCtx) {
11930
12084
  const processedMessages = await Promise.all(
11931
12085
  messages.map(async (message) => {
11932
12086
  if (message.role !== "user" || !Array.isArray(message.parts)) {
@@ -11971,10 +12125,11 @@ When a tool execution is not approved by the user, do not retry it unless explic
11971
12125
  outputErrorToConsole: false,
11972
12126
  newlineDelimiter: "\n"
11973
12127
  });
12128
+ const guardedText = await guardExtractedFileText(filename, String(extractedText), offloadCtx);
11974
12129
  return {
11975
12130
  type: "text",
11976
12131
  text: `<file file name = "${filename}" >
11977
- ${extractedText}
12132
+ ${guardedText}
11978
12133
  </file>`
11979
12134
  };
11980
12135
  } catch (error) {
@@ -11990,7 +12145,6 @@ ${extractedText}
11990
12145
  ...message,
11991
12146
  parts: processedParts
11992
12147
  };
11993
- console.log("[EXULU] Result: " + JSON.stringify(result, null, 2));
11994
12148
  return result;
11995
12149
  })
11996
12150
  );
@@ -12013,7 +12167,9 @@ ${extractedText}
12013
12167
  exuluConfig,
12014
12168
  instructions,
12015
12169
  req,
12016
- maxStepCount
12170
+ maxStepCount,
12171
+ contextWindow,
12172
+ disabledTools
12017
12173
  }) => {
12018
12174
  if (!this.config) {
12019
12175
  console.error("[EXULU] Config is required for streaming.");
@@ -12034,9 +12190,7 @@ ${extractedText}
12034
12190
  console.log("[EXULU] loading previous messages from session: " + session);
12035
12191
  const previousMessages2 = await getAgentMessages({
12036
12192
  session,
12037
- user: user?.id,
12038
- limit: 50,
12039
- page: 1
12193
+ user: user?.id
12040
12194
  });
12041
12195
  previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
12042
12196
  }
@@ -12099,60 +12253,28 @@ ${extractedText}
12099
12253
  if (declined.length && session && user) {
12100
12254
  await saveChat({ session, user: user.id, messages: declined });
12101
12255
  }
12102
- messages = await this.processFilePartsInMessages(messages);
12256
+ messages = await this.processFilePartsInMessages(messages, {
12257
+ contextWindow,
12258
+ sessionID: session,
12259
+ user,
12260
+ exuluConfig
12261
+ });
12262
+ const chronologicalMessages = messages;
12263
+ const contextBudget = deriveContextBudget(contextWindow);
12264
+ const occupancy = contextOccupancy(chronologicalMessages);
12265
+ if (occupancy >= contextBudget.blockThreshold) {
12266
+ console.warn(
12267
+ `[EXULU] Blocking request: occupancy ${occupancy} >= blockThreshold ${contextBudget.blockThreshold} (window ${contextBudget.contextWindow}).`
12268
+ );
12269
+ throw new ContextCompactionRequiredError(occupancy, contextBudget);
12270
+ }
12271
+ messages = sliceHistoryAtCheckpoint(chronologicalMessages);
12103
12272
  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
12273
  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
12274
  if (user?.personal_system_prompt?.trim()) {
12106
12275
  system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
12107
12276
  }
12108
12277
  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
12278
  if (currentSkills?.length) {
12157
12279
  const skillsList = currentSkills.map((skill) => {
12158
12280
  const description = (skill.description ?? "").trim();
@@ -12207,6 +12329,11 @@ ${skillsList}
12207
12329
  read them with the readFile tool. Files you produce yourself (via writeFile or via shell
12208
12330
  commands like \`node create_doc.js\`) live in the same place. These files are scoped to
12209
12331
  this single session; they are NOT visible in other sessions, projects, or knowledge bases.
12332
+
12333
+ Note on large outputs: oversized tool outputs and large uploaded documents are automatically
12334
+ truncated in the conversation; the FULL content is saved as a session file (named in the
12335
+ truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
12336
+ to page through it \u2014 do not ask the user to re-upload.
12210
12337
  `;
12211
12338
  system += `
12212
12339
 
@@ -12230,9 +12357,66 @@ When a tool execution is not approved by the user, do not retry it unless explic
12230
12357
  sessionItems,
12231
12358
  model,
12232
12359
  agent,
12233
- memoryItems
12360
+ memoryItems,
12361
+ contextWindow,
12362
+ disabledTools
12234
12363
  );
12235
12364
  console.log("[EXULU] Converted tools", Object.keys(tools));
12365
+ const includesContextSearchTool = currentTools?.some(
12366
+ (tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
12367
+ );
12368
+ const includesWebSearchTool = currentTools?.some(
12369
+ (tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
12370
+ );
12371
+ console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
12372
+ console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
12373
+ console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
12374
+ if (includesContextSearchTool) {
12375
+ system += `
12376
+
12377
+
12378
+
12379
+ When you use a context search tool, you will include references to the items
12380
+ retrieved from the tool call result inline in the response using this exact JSON format
12381
+ (all on one line, no line breaks):
12382
+ {item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
12383
+
12384
+ IMPORTANT formatting rules:
12385
+ - Use the exact format shown above, all on ONE line
12386
+ - Do NOT use quotes around field names or values
12387
+ - Use the context ID from the tool result
12388
+ - Include the file/item name, not the full path
12389
+ - Separate multiple citations with spaces
12390
+
12391
+ Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
12392
+
12393
+ The citations will be rendered as interactive badges in the UI.
12394
+ `;
12395
+ }
12396
+ if (includesWebSearchTool) {
12397
+ system += `
12398
+
12399
+
12400
+ 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
12401
+ (all on one line, no line breaks):
12402
+ {url: <url>, title: <title>, snippet: <snippet>}
12403
+
12404
+ IMPORTANT formatting rules:
12405
+ - Use the exact format shown above, all on ONE line
12406
+ - Do NOT use quotes around field names or values
12407
+ - Separate multiple results with spaces
12408
+
12409
+ Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
12410
+ `;
12411
+ }
12412
+ const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
12413
+ const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
12414
+ const retrievalGuard = retrievalBudgetGuard(
12415
+ resolveRetrievalCallBudget(toolConfigs),
12416
+ agenticToolKey,
12417
+ Object.keys(tools)
12418
+ );
12419
+ const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
12236
12420
  const result = streamText({
12237
12421
  temperature: 0,
12238
12422
  // TODO Make this configurable
@@ -12257,10 +12441,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
12257
12441
  `Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
12258
12442
  );
12259
12443
  },
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")]
12444
+ // todo allow configuring the step budget per skill
12445
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
12446
+ stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
12264
12447
  });
12265
12448
  return {
12266
12449
  stream: result,
@@ -12271,19 +12454,14 @@ When a tool execution is not approved by the user, do not retry it unless explic
12271
12454
  };
12272
12455
  var getAgentMessages = async ({
12273
12456
  session,
12274
- user,
12275
- limit,
12276
- page
12457
+ user
12277
12458
  }) => {
12278
12459
  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;
12460
+ console.log("[EXULU] getting agent messages for session: " + session + " and user: " + user);
12461
+ const messages = await db.from("agent_messages").where({ session, user: user || null }).orderBy([
12462
+ { column: "createdAt", order: "asc" },
12463
+ { column: "id", order: "asc" }
12464
+ ]);
12287
12465
  return messages;
12288
12466
  };
12289
12467
  var getSession = async ({ sessionID }) => {
@@ -12386,18 +12564,163 @@ ${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
12386
12564
  };
12387
12565
  };
12388
12566
 
12389
- // src/exulu/transcribe.ts
12390
- var TranscriptionError = class extends Error {
12391
- constructor(upstreamStatus, message) {
12392
- super(message);
12393
- this.upstreamStatus = upstreamStatus;
12394
- this.name = "TranscriptionError";
12567
+ // src/exulu/resolve-context-window.ts
12568
+ var resolveContextWindow = async ({
12569
+ modelId,
12570
+ exuluProvider
12571
+ }) => {
12572
+ if (process.env.EXULU_USE_LITELLM === "true") {
12573
+ const entry = await findLiteLLMModel(modelId);
12574
+ const fromCatalog = entry?.max_input_tokens ?? entry?.max_tokens;
12575
+ if (fromCatalog != null && fromCatalog > 0) return fromCatalog;
12576
+ } else if (exuluProvider) {
12577
+ try {
12578
+ const fromProvider = exuluProvider.maxContextLength;
12579
+ if (fromProvider != null && fromProvider > 0) return fromProvider;
12580
+ } catch {
12581
+ }
12395
12582
  }
12583
+ console.warn(
12584
+ `[EXULU] Unknown context window for model "${modelId}" \u2014 assuming ${DEFAULT_CONTEXT_WINDOW}. Check the LiteLLM catalog / provider template metadata.`
12585
+ );
12586
+ return DEFAULT_CONTEXT_WINDOW;
12396
12587
  };
12397
- async function transcribeAudio(args) {
12398
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
12399
- const port = process.env.LITELLM_PORT ?? "4000";
12400
- const masterKey = process.env.LITELLM_MASTER_KEY;
12588
+
12589
+ // src/exulu/active-streams.ts
12590
+ var active = /* @__PURE__ */ new Set();
12591
+ var markStreamActive = (sessionID) => {
12592
+ active.add(sessionID);
12593
+ };
12594
+ var clearStreamActive = (sessionID) => {
12595
+ active.delete(sessionID);
12596
+ };
12597
+ var isStreamActive = (sessionID) => active.has(sessionID);
12598
+
12599
+ // src/exulu/compact-session.ts
12600
+ import { randomUUID } from "crypto";
12601
+ import { generateText as generateText6, validateUIMessages as validateUIMessages2 } from "ai";
12602
+ var CompactionInsufficientError = class extends Error {
12603
+ constructor(reason) {
12604
+ super(JSON.stringify({ code: COMPACTION_INSUFFICIENT, message: reason }));
12605
+ this.name = "CompactionInsufficientError";
12606
+ }
12607
+ };
12608
+ var MIN_TAIL_MESSAGES = 2;
12609
+ var SUMMARY_TOOL_OUTPUT_SLICE = 1500;
12610
+ var SUMMARY_TOOL_INPUT_SLICE = 200;
12611
+ var SUMMARY_SYSTEM = `You compress chat histories for an AI assistant. Produce a dense, factual summary of the conversation below. Preserve:
12612
+ - the user's intent and any outstanding requests
12613
+ - key facts, decisions, and constraints
12614
+ - files, artifacts, and session files touched \u2014 ALWAYS keep exact file and item names so they stay retrievable
12615
+ - errors encountered and how they were resolved
12616
+ - pending tasks and the current state of the work
12617
+ Do not invent information. Do not include pleasantries. Write compact prose or bullet points.`;
12618
+ var splitTail = (messages, tailTokenBudget) => {
12619
+ const minTail = Math.min(MIN_TAIL_MESSAGES, messages.length);
12620
+ let cut = messages.length;
12621
+ let tokens = 0;
12622
+ for (let i = messages.length - 1; i >= 0; i--) {
12623
+ const t = estimateMessageTokens(messages[i]);
12624
+ const tailCount = messages.length - i;
12625
+ if (tailCount > minTail && tokens + t > tailTokenBudget) break;
12626
+ tokens += t;
12627
+ cut = i;
12628
+ }
12629
+ return { head: messages.slice(0, cut), tail: messages.slice(cut) };
12630
+ };
12631
+ var serializeForSummary = (messages) => messages.map((m) => {
12632
+ const parts = (m.parts ?? []).map((part) => {
12633
+ const p = part;
12634
+ if (p.type === "text") return p.text ?? "";
12635
+ if (p.type === "file") return `[file: ${p.filename ?? p.url ?? "attachment"}]`;
12636
+ if (p.type === "reasoning" || p.type === "step-start") return "";
12637
+ if (p.type?.startsWith("tool-") || p.type === "dynamic-tool") {
12638
+ const out = p.output?.value ?? p.output;
12639
+ const outText = typeof out === "string" ? out : JSON.stringify(out ?? "");
12640
+ return `[tool ${p.type}: ${JSON.stringify(p.input ?? {}).slice(0, SUMMARY_TOOL_INPUT_SLICE)}] \u2192 ${outText.slice(0, SUMMARY_TOOL_OUTPUT_SLICE)}`;
12641
+ }
12642
+ return "";
12643
+ }).filter(Boolean).join("\n");
12644
+ return `${m.role.toUpperCase()}:
12645
+ ${parts}`;
12646
+ }).join("\n\n");
12647
+ var compactSession = async ({
12648
+ sessionID,
12649
+ user,
12650
+ languageModel,
12651
+ contextWindow,
12652
+ steer,
12653
+ modelId,
12654
+ summarize
12655
+ }) => {
12656
+ const budget = deriveContextBudget(contextWindow);
12657
+ const rows = await getAgentMessages({ session: sessionID, user: user.id });
12658
+ const all = await validateUIMessages2({ messages: rows.map((r) => JSON.parse(r.content)) });
12659
+ const history = sliceHistoryAtCheckpoint(all);
12660
+ const { head, tail } = splitTail(history, budget.compactionTailTokens);
12661
+ if (head.length === 0) {
12662
+ throw new CompactionInsufficientError(
12663
+ "There is nothing left to compact \u2014 the recent messages already form the whole context. Start a new chat instead."
12664
+ );
12665
+ }
12666
+ let corpus = serializeForSummary(head);
12667
+ const originalTokens = estimateTokens(corpus);
12668
+ corpus = truncateToolOutput(corpus, contextWindow, "history", 0.3, Math.floor(budget.usableWindow * 0.8) * 4);
12669
+ const system = steer?.trim() ? `${SUMMARY_SYSTEM}
12670
+
12671
+ Focus especially on: ${steer.trim()}` : SUMMARY_SYSTEM;
12672
+ const doSummarize = summarize ?? (async ({ system: sys, prompt, maxOutputTokens }) => {
12673
+ const { text } = await generateText6({
12674
+ model: languageModel,
12675
+ system: sys,
12676
+ prompt,
12677
+ temperature: 0,
12678
+ maxRetries: 2,
12679
+ maxOutputTokens
12680
+ });
12681
+ return text;
12682
+ });
12683
+ const summary = await doSummarize({ system, prompt: corpus, maxOutputTokens: budget.summaryBudgetTokens });
12684
+ const summaryTokens = estimateTokens(summary);
12685
+ let tailTokens = 0;
12686
+ for (const m of tail) tailTokens += estimateMessageTokens(m);
12687
+ const occupancyEstimate = summaryTokens + tailTokens;
12688
+ if (occupancyEstimate >= budget.blockThreshold) {
12689
+ throw new CompactionInsufficientError(
12690
+ "Compacting cannot shrink this conversation below the context limit \u2014 a recent message or output is too large by itself. Start a new chat."
12691
+ );
12692
+ }
12693
+ const compaction = {
12694
+ coversUpTo: head[head.length - 1].id,
12695
+ originalTokens,
12696
+ summaryTokens,
12697
+ occupancyEstimate,
12698
+ ...steer?.trim() ? { steer: steer.trim() } : {}
12699
+ };
12700
+ const checkpoint = {
12701
+ id: `compaction_${randomUUID()}`,
12702
+ role: "user",
12703
+ parts: [{ type: "text", text: `[Conversation summary \u2014 earlier messages were compacted]
12704
+
12705
+ ${summary}` }],
12706
+ metadata: { compaction }
12707
+ };
12708
+ await saveChat({ session: sessionID, user: user.id, messages: [checkpoint], ...modelId ? { model: modelId } : {} });
12709
+ return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
12710
+ };
12711
+
12712
+ // src/exulu/transcribe.ts
12713
+ var TranscriptionError = class extends Error {
12714
+ constructor(upstreamStatus, message) {
12715
+ super(message);
12716
+ this.upstreamStatus = upstreamStatus;
12717
+ this.name = "TranscriptionError";
12718
+ }
12719
+ };
12720
+ async function transcribeAudio(args) {
12721
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
12722
+ const port = process.env.LITELLM_PORT ?? "4000";
12723
+ const masterKey = process.env.LITELLM_MASTER_KEY;
12401
12724
  const model = process.env.TRANSCRIPTION_MODEL;
12402
12725
  if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
12403
12726
  if (!model) throw new Error("TRANSCRIPTION_MODEL is not set");
@@ -12751,7 +13074,7 @@ function checkApiKeyScope(user, agentId) {
12751
13074
  import "express";
12752
13075
  import {
12753
13076
  streamText as streamText2,
12754
- generateText as generateText6,
13077
+ generateText as generateText7,
12755
13078
  stepCountIs as stepCountIs2,
12756
13079
  jsonSchema
12757
13080
  } from "ai";
@@ -12841,7 +13164,7 @@ function transformCompletion(text, inputTokens, outputTokens, ctx) {
12841
13164
  }
12842
13165
 
12843
13166
  // src/exulu/openai-gateway.ts
12844
- import { randomUUID } from "crypto";
13167
+ import { randomUUID as randomUUID2 } from "crypto";
12845
13168
  import "crypto-js";
12846
13169
  import express from "express";
12847
13170
  function convertOpenAIToolsToAiSdkTools(tools) {
@@ -13155,6 +13478,10 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
13155
13478
  }
13156
13479
  const providerapikey = resolved.apiKey;
13157
13480
  const languageModel = resolved.languageModel;
13481
+ const contextWindow = await resolveContextWindow({
13482
+ modelId: resolved.model.id,
13483
+ exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
13484
+ });
13158
13485
  const disabledTools = req.body.disabledTools ?? [];
13159
13486
  const enabledTools = await getEnabledTools(
13160
13487
  agent,
@@ -13179,12 +13506,50 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
13179
13506
  project?.id,
13180
13507
  void 0,
13181
13508
  languageModel,
13182
- agent
13509
+ agent,
13510
+ void 0,
13511
+ contextWindow,
13512
+ disabledTools
13183
13513
  );
13514
+ const gatewayAgenticEntry = enabledTools?.find((t) => t.id === "agentic_context_search");
13515
+ const gatewayAgenticKey = gatewayAgenticEntry ? sanitizeToolName(gatewayAgenticEntry.name) : void 0;
13516
+ const gatewayRetrievalGuard = retrievalBudgetGuard(
13517
+ resolveRetrievalCallBudget(agent.tools),
13518
+ gatewayAgenticKey,
13519
+ Object.keys(convertedTools)
13520
+ );
13521
+ const turnBudget = resolveTurnStepBudget(void 0, agent);
13184
13522
  const clientTools = Array.isArray(req.body.tools) ? req.body.tools : [];
13185
13523
  const activeTools = clientTools.length > 0 ? convertOpenAIToolsToAiSdkTools(clientTools) : convertedTools;
13186
13524
  const openaiMessages = req.body.messages ?? [];
13187
13525
  const { systemPrompt: requestSystemPrompt, coreMessages } = convertOpenAIMessagesToModelMessages(openaiMessages);
13526
+ const gatewayBudget = deriveContextBudget(contextWindow);
13527
+ const IMAGE_TOKEN_ALLOWANCE = 1e3;
13528
+ let imageCount = 0;
13529
+ const textOnlyMessages = openaiMessages.map((m) => {
13530
+ if (!Array.isArray(m.content)) return m;
13531
+ return {
13532
+ ...m,
13533
+ content: m.content.map((p) => {
13534
+ if (p && typeof p === "object" && "image_url" in p && p.image_url) {
13535
+ imageCount += 1;
13536
+ return { ...p, image_url: { url: "[image]" } };
13537
+ }
13538
+ return p;
13539
+ })
13540
+ };
13541
+ });
13542
+ const promptTokens = estimateTokens(JSON.stringify(textOnlyMessages)) + imageCount * IMAGE_TOKEN_ALLOWANCE;
13543
+ if (promptTokens >= gatewayBudget.blockThreshold) {
13544
+ res.status(400).json({
13545
+ error: {
13546
+ 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.`,
13547
+ type: "invalid_request_error",
13548
+ code: "context_length_exceeded"
13549
+ }
13550
+ });
13551
+ return;
13552
+ }
13188
13553
  const agentInstructions = agent.instructions ?? "";
13189
13554
  const systemParts = [
13190
13555
  agentInstructions ? `You are an agent named: ${agent.name}
@@ -13194,7 +13559,7 @@ ${project.description}` : ""}` : "",
13194
13559
  requestSystemPrompt
13195
13560
  ].filter(Boolean);
13196
13561
  const systemPrompt = systemParts.join("\n\n");
13197
- const completionId = `chatcmpl-${randomUUID()}`;
13562
+ const completionId = `chatcmpl-${randomUUID2()}`;
13198
13563
  const created = Math.floor(Date.now() / 1e3);
13199
13564
  const hasTools = Object.keys(activeTools).length > 0;
13200
13565
  const ctx = { completionId, created, modelId };
@@ -13208,7 +13573,8 @@ ${project.description}` : ""}` : "",
13208
13573
  messages: coreMessages,
13209
13574
  tools: hasTools ? activeTools : void 0,
13210
13575
  maxRetries: 2,
13211
- stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(5)],
13576
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
13577
+ stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)],
13212
13578
  onError: (error) => {
13213
13579
  console.error("[OPENAI GATEWAY] stream error:", error);
13214
13580
  }
@@ -13241,13 +13607,14 @@ ${project.description}` : ""}` : "",
13241
13607
  const usage = await result.usage;
13242
13608
  await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
13243
13609
  } else {
13244
- const { text, usage } = await generateText6({
13610
+ const { text, usage } = await generateText7({
13245
13611
  model: languageModel,
13246
13612
  system: systemPrompt || void 0,
13247
13613
  messages: coreMessages,
13248
13614
  tools: hasTools ? activeTools : void 0,
13249
13615
  maxRetries: 2,
13250
- stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(5)]
13616
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
13617
+ stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)]
13251
13618
  });
13252
13619
  res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
13253
13620
  await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
@@ -13447,6 +13814,148 @@ var contentHeadersFor = (key, contentType, filename) => {
13447
13814
  };
13448
13815
  var getSharedArtifactByName = (db, name) => db("shared_artifacts").where({ name }).first();
13449
13816
 
13817
+ // src/skills/skill-access.ts
13818
+ async function resolveSkillByName(db, name) {
13819
+ const row = await db("skills").where({ name }).first();
13820
+ return row ?? null;
13821
+ }
13822
+ async function canAccessSkill(db, skill, action, user) {
13823
+ const rbac = await RBACResolver(db, "skill", skill.id, skill.rights_mode || "private");
13824
+ return checkRecordAccess({ ...skill, RBAC: rbac }, action, user);
13825
+ }
13826
+ async function filterReadableSkills(db, skills, user) {
13827
+ const out = [];
13828
+ for (const s of skills) {
13829
+ if (await canAccessSkill(db, s, "read", user)) out.push(s);
13830
+ }
13831
+ return out;
13832
+ }
13833
+
13834
+ // src/skills/bootstrap/clients.ts
13835
+ var CLIENT_MANIFEST = [
13836
+ { id: "agents", dir: ".agents/skills" },
13837
+ // cross-agent standard (symlink canonical store)
13838
+ { id: "claude", dir: ".claude/skills" },
13839
+ { id: "windsurf", dir: ".windsurf/skills" },
13840
+ { id: "continue", dir: ".continue/skills" },
13841
+ { id: "roo", dir: ".roo/skills" },
13842
+ { id: "kilocode", dir: ".kilocode/skills" },
13843
+ { id: "crush", dir: ".crush/skills" },
13844
+ { id: "goose", dir: ".goose/skills" },
13845
+ { id: "qwen", dir: ".qwen/skills" },
13846
+ { id: "iflow", dir: ".iflow/skills" },
13847
+ { id: "junie", dir: ".junie/skills" },
13848
+ { id: "kiro", dir: ".kiro/skills" },
13849
+ { id: "trae", dir: ".trae/skills" },
13850
+ { id: "augment", dir: ".augment/skills" },
13851
+ { id: "factory", dir: ".factory/skills" },
13852
+ { id: "devin", dir: ".devin/skills" },
13853
+ { id: "openhands", dir: ".openhands/skills" },
13854
+ { id: "pi", dir: ".pi/skills" },
13855
+ { id: "cortex", dir: ".cortex/skills" },
13856
+ { id: "zencoder", dir: ".zencoder/skills" },
13857
+ { id: "codebuddy", dir: ".codebuddy/skills" },
13858
+ { id: "codestudio", dir: ".codestudio/skills" },
13859
+ { id: "commandcode", dir: ".commandcode/skills" },
13860
+ { id: "codemaker", dir: ".codemaker/skills" },
13861
+ { id: "codeartsdoer", dir: ".codeartsdoer/skills" },
13862
+ { id: "lingma", dir: ".lingma/skills" },
13863
+ { id: "qoder", dir: ".qoder/skills" },
13864
+ { id: "rovodev", dir: ".rovodev/skills" },
13865
+ { id: "moxby", dir: ".moxby/skills" },
13866
+ { id: "mux", dir: ".mux/skills" },
13867
+ { id: "neovate", dir: ".neovate/skills" },
13868
+ { id: "ona", dir: ".ona/skills" },
13869
+ { id: "pochi", dir: ".pochi/skills" },
13870
+ { id: "reasonix", dir: ".reasonix/skills" },
13871
+ { id: "terramind", dir: ".terramind/skills" },
13872
+ { id: "tinycloud", dir: ".tinycloud/skills" },
13873
+ { id: "vibe", dir: ".vibe/skills" },
13874
+ { id: "adal", dir: ".adal/skills" },
13875
+ { id: "aider-desk", dir: ".aider-desk/skills" },
13876
+ { id: "autohand", dir: ".autohand/skills" },
13877
+ { id: "bob", dir: ".bob/skills" },
13878
+ { id: "hermes", dir: ".hermes/skills" },
13879
+ { id: "inferencesh", dir: ".inferencesh/skills" },
13880
+ { id: "jazz", dir: ".jazz/skills" },
13881
+ { id: "kode", dir: ".kode/skills" },
13882
+ { id: "mcpjam", dir: ".mcpjam/skills" },
13883
+ { id: "forge", dir: ".forge/skills" },
13884
+ { id: "tabnine", dir: ".tabnine/agent/skills" }
13885
+ // exception: nested under agent/
13886
+ ];
13887
+
13888
+ // src/skills/bootstrap/exulu-sh.generated.ts
13889
+ var EXULU_SH_B64 = "IyEvYmluL3NoCiMgZXh1bHUg4oCUIGhlbHBlciBmb3IgdGhlIEV4dWx1IGNlbnRyYWwgc2tpbGwgbGlicmFyeS4gVGhlIGFnZW50IGludm9rZXMgdGhpcwojIChuZXZlciByYXcgY3VybCk6IHRoZSB0b2tlbiBpcyByZWFkIGZyb20gdGhlIGNvbmZpZyBmaWxlIGhlcmUgYW5kIHNlbnQgdmlhCiMgYHgtYXBpLWtleTogQmVhcmVyYCwgc28gaXQgbmV2ZXIgZW50ZXJzIHRoZSBtb2RlbCBjb250ZXh0LiBDbGllbnQgZmFuLW91dAojIChjb3B5L3N5bWxpbmsgYWNyb3NzIGFnZW50IGNsaWVudHMpIGlzIGRldGVybWluaXN0aWMuCnNldCAtZXUKCkNPTkZJR19ESVI9IiRIT01FLy5jb25maWcvZXh1bHUiCkNPTkZJR19GSUxFPSIkQ09ORklHX0RJUi9za2lsbHMuanNvbiIKCmRpZSgpIHsgcHJpbnRmICdleHVsdTogJXNcbicgIiQqIiA+JjI7IGV4aXQgMTsgfQppbmZvKCkgeyBwcmludGYgJyVzXG4nICIkKiIgPiYyOyB9Cgpqc29uX3N0cigpIHsgIyBqc29uX3N0ciA8a2V5PiA8ZmlsZT4g4oCUIGZsYXQgImtleSI6InZhbHVlIgogIHNlZCAtbiAicy8uKlwiJDFcIltbOnNwYWNlOl1dKjpbWzpzcGFjZTpdXSpcIlxcKFteXCJdKlxcKVwiLiovXFwxL3AiICIkMiIgfCBoZWFkIC1uMQp9CgpbIC1mICIkQ09ORklHX0ZJTEUiIF0gfHwgZGllICJub3QgY29uZmlndXJlZCDigJQgcnVuOiBjdXJsIC1mc1NMIDxiYXNlX3VybD4vYXBpL3NraWxscy9pbnN0YWxsLnNoIHwgc2giCgpCQVNFX1VSTD0iJChqc29uX3N0ciBiYXNlX3VybCAiJENPTkZJR19GSUxFIikiCkJBQ0tFTkQ9IiQoanNvbl9zdHIgYmFja2VuZCAiJENPTkZJR19GSUxFIikiCkFQSV9LRVk9IiQoanNvbl9zdHIgYXBpX2tleSAiJENPTkZJR19GSUxFIikiCkxJTktfTU9ERT0iJChqc29uX3N0ciBsaW5rX21vZGUgIiRDT05GSUdfRklMRSIpIgpTQ09QRT0iJChqc29uX3N0ciBzY29wZSAiJENPTkZJR19GSUxFIikiCkNMSUVOVFM9IiQoc2VkIC1uICdzLy4qImNsaWVudHMiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKlxbXChbXl1dKlwpXF0uKi9cMS9wJyAiJENPTkZJR19GSUxFIiB8IHRyICcsJyAnICcgfCB0ciAtZCAnIicgfCB0ciAtcyAnICcpIgoKWyAtbiAiJENMSUVOVFMiIF0gfHwgQ0xJRU5UUz0iYWdlbnRzIgpbIC1uICIkTElOS19NT0RFIiBdIHx8IExJTktfTU9ERT0iY29weSIKWyAtbiAiJFNDT1BFIiBdIHx8IFNDT1BFPSJwcm9qZWN0IgoKaWYgWyAteiAiJEJBQ0tFTkQiIF07IHRoZW4KICBbIC1uICIkQkFTRV9VUkwiIF0gfHwgZGllICJjb25maWcgaGFzIG5vIGJhY2tlbmQgYW5kIG5vIGJhc2VfdXJsIgogIEJBQ0tFTkQ9IiQoY3VybCAtZnNTTCAiJEJBU0VfVVJML2FwaS9jb25maWciIHwgc2VkIC1uICdzLy4qImJhY2tlbmQiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKiJcKFteIl0qXCkiLiovXDEvcCcgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJEJBQ0tFTkQiIF0gfHwgZGllICJjb3VsZCBub3QgcmVzb2x2ZSBiYWNrZW5kIGZyb20gJEJBU0VfVVJML2FwaS9jb25maWciCmZpCkJBQ0tFTkQ9IiQocHJpbnRmICclcycgIiRCQUNLRU5EIiB8IHNlZCAnczovKiQ6OicpIgoKUk9PVD0iJFBXRCIKWyAiJFNDT1BFIiA9ICJob21lIiBdICYmIFJPT1Q9IiRIT01FIgoKIyBkaXJfZm9yIENMSUVOVF9JRCAtPiByZWxhdGl2ZSBza2lsbCBkaXIgKGdlbmVyYXRlZCBmcm9tIHRoZSBtYW5pZmVzdCBpbiB0aGUKIyByZWFsIGJ1aWxkOyBhIHJlcHJlc2VudGF0aXZlIHN1YnNldCBoZXJlIGZvciBsb2NhbCB0ZXN0aW5nKS4KZGlyX2ZvcigpIHsKICBjYXNlICIkMSIgaW4KX19ESVJfRk9SX0NBU0VTX18KICAgICopIHJldHVybiAxIDs7CiAgZXNhYwp9CgphcGkoKSB7ICMgYXBpIDxNRVRIT0Q+IDxwYXRoPiBbZXh0cmEgY3VybCBhcmdzLi4uXSAtPiBib2R5IG9uIHN0ZG91dAogIG09IiQxIjsgcD0iJDIiOyBzaGlmdCAyCiAgY3VybCAtZnNTIC1YICIkbSIgIiRCQUNLRU5EJHAiIC1IICJ4LWFwaS1rZXk6IEJlYXJlciAkQVBJX0tFWSIgIiRAIgp9CgptZXRhX3ZlcnNpb24oKSB7ICMgbWV0YV92ZXJzaW9uIDxuYW1lPiAtPiBjdXJyZW50X3ZlcnNpb24gZnJvbSByZWdpc3RyeQogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJDEiIDI+L2Rldi9udWxsIFwKICAgIHwgc2VkIC1uICdzLy4qImN1cnJlbnRfdmVyc2lvbiJbWzpzcGFjZTpdXSo6W1s6c3BhY2U6XV0qXChbMC05XVswLTldKlwpLiovXDEvcCcgfCBoZWFkIC1uMQp9CgpfbWFuYWdlZCgpIHsgIyBfbWFuYWdlZCA8ZGlyPiAtPiAwIGlmIHNhZmUgdG8gb3ZlcndyaXRlIChvdXJzIG9yIHN5bWxpbmsgb3IgYWJzZW50KQogIGQ9IiQxIgogIGlmIFsgLWUgIiRkIiBdICYmIFsgISAtTCAiJGQiIF0gJiYgWyAhIC1mICIkZC8uZXh1bHUtc2tpbGwuanNvbiIgXTsgdGhlbgogICAgaW5mbyAic2tpcCAkZCAoZXhpc3RzLCBub3QgbWFuYWdlZCBieSBleHVsdSkiOyByZXR1cm4gMQogIGZpCiAgcmV0dXJuIDAKfQoKX3B1dF9yZWFsKCkgeyAjIF9wdXRfcmVhbCA8ZGVzdD4gPHNyY2Rpcj4gPG1hcmtlci1qc29uPgogIF9tYW5hZ2VkICIkMSIgfHwgcmV0dXJuIDAKICBybSAtcmYgIiQxIjsgbWtkaXIgLXAgIiQxIjsgY3AgLVIgIiQyLy4iICIkMS8iCiAgcHJpbnRmICclc1xuJyAiJDMiID4gIiQxLy5leHVsdS1za2lsbC5qc29uIgp9CgpwbGFjZV9za2lsbCgpIHsgIyBwbGFjZV9za2lsbCA8bmFtZT4gPHNyY2Rpcj4gPHZlcnNpb24+CiAgcG5hbWU9IiQxIjsgcHNyYz0iJDIiOyBwdmVyPSIkezM6LTF9IgogIG1hcmtlcj0neyAibmFtZSI6ICInIiRwbmFtZSInIiwgInZlcnNpb24iOiAnIiRwdmVyIicsICJzb3VyY2UiOiAiJyIkQkFDS0VORCInIiB9JwogIGNhbm9uPSIkUk9PVC8uYWdlbnRzL3NraWxscy8kcG5hbWUiCiAgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBfcHV0X3JlYWwgIiRjYW5vbiIgIiRwc3JjIiAiJG1hcmtlciIKICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICBkPSIkKGRpcl9mb3IgIiRpZCIpIiB8fCB7IGluZm8gInVua25vd24gY2xpZW50OiAkaWQiOyBjb250aW51ZTsgfQogICAgcGFyZW50PSIkUk9PVC8kZCI7IGRlc3Q9IiRwYXJlbnQvJHBuYW1lIgogICAgaWYgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBbICIkaWQiICE9ICJhZ2VudHMiIF07IHRoZW4KICAgICAgX21hbmFnZWQgIiRkZXN0IiB8fCBjb250aW51ZQogICAgICBta2RpciAtcCAiJHBhcmVudCI7IHJtIC1yZiAiJGRlc3QiCiAgICAgIGlmIGxuIC1zICIkY2Fub24iICIkZGVzdCIgMj4vZGV2L251bGw7IHRoZW4KICAgICAgICBpbmZvICJsaW5rZWQgJGRlc3QgLT4gJGNhbm9uIgogICAgICBlbHNlCiAgICAgICAgaW5mbyAic3ltbGluayB1bnN1cHBvcnRlZCBhdCAkZGVzdDsgY29weWluZyIKICAgICAgICBfcHV0X3JlYWwgIiRkZXN0IiAiJHBzcmMiICIkbWFya2VyIgogICAgICBmaQogICAgZWxpZiBbICIkTElOS19NT0RFIiA9ICJjb3B5IiBdOyB0aGVuCiAgICAgIF9wdXRfcmVhbCAiJGRlc3QiICIkcHNyYyIgIiRtYXJrZXIiCiAgICBmaQogICAgIyBzeW1saW5rICsgYWdlbnRzOiBhbHJlYWR5IHBsYWNlZCBhcyB0aGUgY2Fub25pY2FsIHN0b3JlIGFib3ZlLgogIGRvbmUKfQoKaW5zdGFsbGVkX25hbWVzKCkgeyAjIHVuaXF1ZSBza2lsbCBuYW1lcyB0aGF0IGNhcnJ5IG91ciBtYXJrZXIgdW5kZXIgUk9PVAogIHsgZmluZCAiJFJPT1QvLmFnZW50cy9za2lsbHMiIC1tYXhkZXB0aCAyIC1uYW1lIC5leHVsdS1za2lsbC5qc29uIDI+L2Rldi9udWxsCiAgICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICAgIGQ9IiQoZGlyX2ZvciAiJGlkIikiIHx8IGNvbnRpbnVlCiAgICAgIGZpbmQgIiRST09ULyRkIiAtbWF4ZGVwdGggMiAtbmFtZSAuZXh1bHUtc2tpbGwuanNvbiAyPi9kZXYvbnVsbAogICAgZG9uZQogIH0gfCB3aGlsZSByZWFkIC1yIG07IGRvIGpzb25fc3RyIG5hbWUgIiRtIjsgZG9uZSB8IHNvcnQgLXUKfQoKbWFya2VyX3ZlcnNpb24oKSB7ICMgbWFya2VyX3ZlcnNpb24gPG5hbWU+CiAgZm9yIGJhc2UgaW4gIiRST09ULy5hZ2VudHMvc2tpbGxzIjsgZG8KICAgIFsgLWYgIiRiYXNlLyQxLy5leHVsdS1za2lsbC5qc29uIiBdICYmIHsganNvbl9zdHIgdmVyc2lvbiAiJGJhc2UvJDEvLmV4dWx1LXNraWxsLmpzb24iOyByZXR1cm47IH0KICBkb25lCiAgZm9yIGlkIGluICRDTElFTlRTOyBkbwogICAgZD0iJChkaXJfZm9yICIkaWQiKSIgfHwgY29udGludWUKICAgIGY9IiRST09ULyRkLyQxLy5leHVsdS1za2lsbC5qc29uIgogICAgWyAtZiAiJGYiIF0gJiYgeyBqc29uX3N0ciB2ZXJzaW9uICIkZiI7IHJldHVybjsgfQogIGRvbmUKfQoKZG9faW5zdGFsbCgpIHsgIyBkb19pbnN0YWxsIDxuYW1lPgogIG5hbWU9IiQxIgogIFRNUD0iJChta3RlbXAgLWQpIgogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJG5hbWUvZG93bmxvYWQiIC0tb3V0cHV0ICIkVE1QL3NraWxsLnppcCIgXAogICAgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImRvd25sb2FkIGZhaWxlZCBmb3IgJyRuYW1lJyAoNDAzID0gbm8gYWNjZXNzLCA0MDQgPSB1bmtub3duKSI7IH0KICBta2RpciAtcCAiJFRNUC94IgogIHVuemlwIC1xICIkVE1QL3NraWxsLnppcCIgLWQgIiRUTVAveCIgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImJhZCBhcmNoaXZlIGZvciAnJG5hbWUnIjsgfQogIHNyYz0iJChmaW5kICIkVE1QL3giIC1taW5kZXB0aCAxIC1tYXhkZXB0aCAxIC10eXBlIGQgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJHNyYyIgXSB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAidW5leHBlY3RlZCBhcmNoaXZlIGxheW91dCBmb3IgJyRuYW1lJyI7IH0KICB2ZXI9IiQobWV0YV92ZXJzaW9uICIkbmFtZSIpIgogIHBsYWNlX3NraWxsICIkbmFtZSIgIiRzcmMiICIke3ZlcjotMX0iCiAgcm0gLXJmICIkVE1QIgogIGluZm8gImluc3RhbGxlZCAkbmFtZSAodiR7dmVyOi0xfSkgWyRMSU5LX01PREVdIGludG86ICRDTElFTlRTIgp9Cgp1c2FnZSgpIHsKICBjYXQgPiYyIDw8RU9GCmV4dWx1IOKAlCBFeHVsdSBza2lsbCBsaWJyYXJ5IGhlbHBlcgogIGV4dWx1IGxpc3QgICAgICAgICAgICAgICAgIGxpc3Qgc2tpbGxzIHlvdSBjYW4gYWNjZXNzIChKU09OKQogIGV4dWx1IGdldCA8bmFtZT4gICAgICAgICAgIHNob3cgb25lIHNraWxsJ3MgbWV0YWRhdGEgKEpTT04pCiAgZXh1bHUgaW5zdGFsbCA8bmFtZT4gICAgICAgaW5zdGFsbC9yZWZyZXNoIGEgc2tpbGwgaW50byB5b3VyIGFnZW50IGNsaWVudHMKICBleHVsdSB1cGRhdGUgWzxuYW1lPl0gICAgICB1cGRhdGUgaW5zdGFsbGVkIHNraWxscyAoYWxsLCBvciBvbmUpIHRvIGxhdGVzdAogIGV4dWx1IHB1Ymxpc2ggPG5hbWU+IDxkaXI+IHB1Ymxpc2ggYSBsb2NhbCBza2lsbCBmb2xkZXIgYXMgPG5hbWU+CiAgZXh1bHUgY29uZmlnICAgICAgICAgICAgICAgc2hvdyByZXNvbHZlZCBiYWNrZW5kIC8gc2NvcGUgLyBjbGllbnRzIChubyBzZWNyZXRzKQpFT0YKfQoKY21kPSIkezE6LWhlbHB9IgpbICQjIC1ndCAwIF0gJiYgc2hpZnQgfHwgdHJ1ZQoKY2FzZSAiJGNtZCIgaW4KICBsaXN0KSBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5IiA7OwogIGdldCkgWyAkIyAtZ2UgMSBdIHx8IGRpZSAidXNhZ2U6IGV4dWx1IGdldCA8bmFtZT4iOyBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5LyQxIiA7OwogIGluc3RhbGwpIFsgJCMgLWdlIDEgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBpbnN0YWxsIDxuYW1lPiI7IGRvX2luc3RhbGwgIiQxIiA7OwogIHVwZGF0ZSkKICAgIGlmIFsgJCMgLWdlIDEgXTsgdGhlbiBuYW1lcz0iJDEiOyBlbHNlIG5hbWVzPSIkKGluc3RhbGxlZF9uYW1lcykiOyBmaQogICAgWyAtbiAiJG5hbWVzIiBdIHx8IHsgaW5mbyAibm8gZXh1bHUtbWFuYWdlZCBza2lsbHMgZm91bmQgdW5kZXIgJFJPT1QiOyBleGl0IDA7IH0KICAgIGZvciBuIGluICRuYW1lczsgZG8KICAgICAgY3VyPSIkKG1hcmtlcl92ZXJzaW9uICIkbiIpIgogICAgICBsYXRlc3Q9IiQobWV0YV92ZXJzaW9uICIkbiIpIgogICAgICBbIC1uICIkbGF0ZXN0IiBdIHx8IHsgaW5mbyAic2tpcCAkbiAobm90IGluIHJlZ2lzdHJ5KSI7IGNvbnRpbnVlOyB9CiAgICAgIGlmIFsgLXogIiRjdXIiIF0gfHwgWyAiJGxhdGVzdCIgLWd0ICIkY3VyIiBdIDI+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgaW5mbyAidXBkYXRpbmcgJG46IHYke2N1cjotP30gLT4gdiRsYXRlc3QiOyBkb19pbnN0YWxsICIkbiIKICAgICAgZWxzZQogICAgICAgIGluZm8gIiRuIHVwIHRvIGRhdGUgKHYkY3VyKSIKICAgICAgZmkKICAgIGRvbmUKICAgIDs7CiAgcHVibGlzaCkKICAgIFsgJCMgLWdlIDIgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBwdWJsaXNoIDxuYW1lPiA8ZGlyPiIKICAgIG5hbWU9IiQxIjsgZm9sZGVyPSIkMiIKICAgIFsgLWQgIiRmb2xkZXIiIF0gfHwgZGllICJubyBzdWNoIGZvbGRlcjogJGZvbGRlciIKICAgIFsgLWYgIiRmb2xkZXIvU0tJTEwubWQiIF0gfHwgZGllICIkZm9sZGVyIGhhcyBubyBTS0lMTC5tZCBhdCBpdHMgcm9vdCIKICAgIGNvbW1hbmQgLXYgemlwID4vZGV2L251bGwgMj4mMSB8fCBkaWUgInRoZSAnemlwJyBjb21tYW5kIGlzIHJlcXVpcmVkIHRvIHB1Ymxpc2giCiAgICBUTVA9IiQobWt0ZW1wIC1kKSIKICAgICggY2QgIiQoZGlybmFtZSAiJGZvbGRlciIpIiBcCiAgICAgICYmIHppcCAtcSAtciAtWCAiJFRNUC9za2lsbC56aXAiICIkKGJhc2VuYW1lICIkZm9sZGVyIikiIFwKICAgICAgICAgICAteCAnKi8uZXh1bHUtc2tpbGwuanNvbicgJyovLmdpdC8qJyAnKi5EU19TdG9yZScgJyovX19NQUNPU1gvKicgKSBcCiAgICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJjb3VsZCBub3QgemlwICRmb2xkZXIiOyB9CiAgICBhcGkgUE9TVCAiL3NraWxscy9yZWdpc3RyeS8kbmFtZSIgLUggIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vemlwIiBcCiAgICAgIC0tZGF0YS1iaW5hcnkgQCIkVE1QL3NraWxsLnppcCIgXAogICAgICB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAicHVibGlzaCBmYWlsZWQgKDQwMyA9IG5vIHdyaXRlIGFjY2VzcywgNDA5ID0gbmFtZSB0YWtlbikiOyB9CiAgICBybSAtcmYgIiRUTVAiCiAgICBpbmZvICJwdWJsaXNoZWQgJG5hbWUiCiAgICA7OwogIGNvbmZpZykKICAgIHByaW50ZiAnYmFja2VuZD0lc1xuc2NvcGU9JXNcbmxpbmtfbW9kZT0lc1xuY2xpZW50cz0lc1xuYXBpX2tleT0lc1xuJyBcCiAgICAgICIkQkFDS0VORCIgIiRTQ09QRSIgIiRMSU5LX01PREUiICIkQ0xJRU5UUyIgXAogICAgICAiJChbIC1uICIkQVBJX0tFWSIgXSAmJiBlY2hvIHNldCB8fCBlY2hvIE1JU1NJTkcpIgogICAgOzsKICBoZWxwfC0taGVscHwtaCkgdXNhZ2UgOzsKICAqKSBpbmZvICJ1bmtub3duIGNvbW1hbmQ6ICRjbWQiOyB1c2FnZTsgZXhpdCAyIDs7CmVzYWMK";
13890
+
13891
+ // src/skills/bootstrap/exulu-skills.ts
13892
+ var BOOTSTRAP_CLIENTS_JSON = JSON.stringify(CLIENT_MANIFEST, null, 2);
13893
+ var DIR_FOR_CASES = CLIENT_MANIFEST.map(
13894
+ (c) => ` ${c.id}) printf '%s' '${c.dir}' ;;`
13895
+ ).join("\n");
13896
+ var BOOTSTRAP_EXULU_SH = Buffer.from(EXULU_SH_B64, "base64").toString("utf8").replace("__DIR_FOR_CASES__", DIR_FOR_CASES);
13897
+ var BOOTSTRAP_SKILL_MD = `---
13898
+ name: exulu-skills
13899
+ description: Install, update, and publish skills from this Exulu instance's central skill library. Use when the user asks to install a skill, get the latest version of a skill, list available skills, or publish a skill to Exulu.
13900
+ ---
13901
+
13902
+ # Exulu Skills
13903
+
13904
+ Bridge to the Exulu central skill library. **All operations go through the
13905
+ bundled helper script \u2014 do not hand-write curl or copy files yourself.** The
13906
+ script reads the API token from config (keeping it out of this conversation) and
13907
+ handles the multi-client copy/symlink fan-out deterministically.
13908
+
13909
+ ## The helper
13910
+
13911
+ Run the script next to this file, \`scripts/exulu\`, with \`sh\` and the absolute
13912
+ path of this skill's directory:
13913
+
13914
+ \`\`\`
13915
+ sh "<this-skill-dir>/scripts/exulu" <command>
13916
+ \`\`\`
13917
+
13918
+ Commands:
13919
+ - \`list\` \u2014 skills you can access (JSON on stdout)
13920
+ - \`get <name>\` \u2014 one skill's metadata (JSON)
13921
+ - \`install <name>\` \u2014 install/refresh a skill into the user's agent clients
13922
+ - \`update [<name>]\` \u2014 update every installed skill, or just \`<name>\`, to latest
13923
+ - \`publish <name> <folder>\` \u2014 publish a local skill folder as \`<name>\`
13924
+ - \`config\` \u2014 show resolved backend / scope / clients (prints no secrets)
13925
+
13926
+ The token, backend URL, target clients, copy-vs-symlink mode, and scope all come
13927
+ from \`~/.config/exulu/skills.json\` (written by the installer). Never print the
13928
+ \`api_key\` or read it into your reply \u2014 the script uses it internally.
13929
+
13930
+ ## Requests \u2192 commands
13931
+
13932
+ - "list / search skills" \u2192 \`exulu list\`, then filter the JSON for the user.
13933
+ - "install skill X" / "add the X skill" \u2192 \`exulu install X\`.
13934
+ - "update / get the latest version [of X]" \u2192 \`exulu update [X]\`.
13935
+ - "publish / upload this skill as X" \u2192 confirm the target name with the user; for
13936
+ an existing skill run \`exulu get X\` first and confirm a new version is intended;
13937
+ then \`exulu publish X <folder>\`.
13938
+
13939
+ ## Not configured yet?
13940
+
13941
+ If \`exulu config\` reports it's not configured (or \`~/.config/exulu/skills.json\`
13942
+ is missing), tell the user to run the installer \u2014 it sets everything up
13943
+ interactively (base URL, API key, target clients, copy/symlink):
13944
+
13945
+ \`\`\`
13946
+ curl -fsSL <base_url>/api/skills/install.sh | sh
13947
+ \`\`\`
13948
+
13949
+ \`<base_url>\` is their Exulu frontend URL (e.g. https://ai.open.de). They can
13950
+ create an API key at \`<base_url>/token\`.
13951
+
13952
+ ## Errors
13953
+
13954
+ - install: \`403\` = no access to that skill; \`404\` = unknown name.
13955
+ - publish: \`403\` = you can see it but lack write access; \`409\` = the name is
13956
+ taken by a skill you can't access.
13957
+ `;
13958
+
13450
13959
  // src/exulu/routes.ts
13451
13960
  var REQUEST_SIZE_LIMIT = "50mb";
13452
13961
  var getExuluVersionNumber = async () => {
@@ -13520,6 +14029,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
13520
14029
  }
13521
14030
  next();
13522
14031
  });
14032
+ const rawZip = express2.raw({ type: ["application/zip", "application/octet-stream", "application/x-zip-compressed", "application/x-zip"], limit: "50mb" });
13523
14033
  console.log(`
13524
14034
  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
13525
14035
  \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
@@ -13745,7 +14255,7 @@ Mood: friendly and intelligent.
13745
14255
  });
13746
14256
  return;
13747
14257
  }
13748
- const uuid = randomUUID2();
14258
+ const uuid = randomUUID3();
13749
14259
  const image_url = await uploadFile(Buffer.from(image_base64, "base64"), `${uuid}.png`, config, {
13750
14260
  contentType: "image/png"
13751
14261
  }, authenticationResult.user?.id, void 0, true);
@@ -13949,6 +14459,10 @@ Mood: friendly and intelligent.
13949
14459
  const providerapikey = resolved.apiKey;
13950
14460
  const resolvedLanguageModel = resolved.languageModel;
13951
14461
  const resolvedModelId = resolved.model.id;
14462
+ const contextWindow = await resolveContextWindow({
14463
+ modelId: resolved.model.id,
14464
+ exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
14465
+ });
13952
14466
  if (!!headers.stream) {
13953
14467
  const statistics = {
13954
14468
  label: agent.name,
@@ -13967,27 +14481,46 @@ Mood: friendly and intelligent.
13967
14481
  const instructions = customInstructions ? `${agent.instructions}
13968
14482
 
13969
14483
  ${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
- });
14484
+ if (headers.session) markStreamActive(headers.session);
14485
+ let result;
14486
+ try {
14487
+ result = await provider.generateStream({
14488
+ contexts,
14489
+ agent,
14490
+ user,
14491
+ instructions,
14492
+ session: headers.session,
14493
+ message,
14494
+ previousMessages,
14495
+ currentTools: enabledTools,
14496
+ currentSkills: enabledSkills,
14497
+ approvedTools,
14498
+ allExuluTools: tools,
14499
+ languageModel: resolvedLanguageModel,
14500
+ providerapikey,
14501
+ toolConfigs: agent.tools,
14502
+ exuluConfig: config,
14503
+ req,
14504
+ contextWindow,
14505
+ disabledTools
14506
+ });
14507
+ } catch (err) {
14508
+ if (headers.session) clearStreamActive(headers.session);
14509
+ if (err instanceof ContextCompactionRequiredError) {
14510
+ res.status(413).send(err.message);
14511
+ return;
14512
+ }
14513
+ throw err;
14514
+ }
13988
14515
  result.stream.consumeStream();
13989
14516
  result.stream.pipeUIMessageStreamToResponse(res, {
13990
14517
  messageMetadata: ({ part }) => {
14518
+ if (part.type === "finish-step") {
14519
+ return {
14520
+ lastStepInputTokens: part.usage.inputTokens,
14521
+ lastStepOutputTokens: part.usage.outputTokens
14522
+ };
14523
+ }
13991
14524
  if (part.type === "finish") {
13992
14525
  return {
13993
14526
  totalTokens: part.totalUsage.totalTokens,
@@ -14004,22 +14537,20 @@ ${customInstructions}` : agent.instructions;
14004
14537
  sendSources: true,
14005
14538
  onError: (error) => {
14006
14539
  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);
14540
+ if (headers.session) clearStreamActive(headers.session);
14541
+ let message2;
14542
+ if (error == null) message2 = "unknown error";
14543
+ else if (typeof error === "string") message2 = error;
14544
+ else if (error instanceof Error) message2 = error.message;
14545
+ else message2 = JSON.stringify(error);
14546
+ return mapStreamErrorMessage(message2);
14017
14547
  },
14018
14548
  generateMessageId: createIdGenerator({
14019
14549
  prefix: "msg_",
14020
14550
  size: 16
14021
14551
  }),
14022
14552
  onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
14553
+ if (headers.session) clearStreamActive(headers.session);
14023
14554
  console.log(
14024
14555
  "[EXULU] onFinish",
14025
14556
  messages?.map((msg) => msg.parts?.map((part) => part.type === "text" ? part.text : null)).join("\n")
@@ -14081,33 +14612,129 @@ ${customInstructions}` : agent.instructions;
14081
14612
  const instructions = customInstructions ? `${agent.instructions}
14082
14613
 
14083
14614
  ${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 }) => {
14615
+ let response;
14616
+ try {
14617
+ response = await provider.generateSync({
14618
+ contexts,
14619
+ agent,
14620
+ user,
14621
+ req,
14622
+ instructions,
14623
+ session: headers.session,
14624
+ inputMessages: [req.body.message],
14625
+ currentTools: enabledTools,
14626
+ currentSkills: enabledSkills,
14627
+ allExuluTools: tools,
14628
+ languageModel: resolvedLanguageModel,
14629
+ providerapikey,
14630
+ exuluConfig: config,
14631
+ toolConfigs: agent.tools,
14632
+ contextWindow,
14633
+ disabledTools,
14634
+ statistics: {
14635
+ label: agent.name,
14636
+ trigger: "agent"
14637
+ },
14638
+ onTokenUsage: async ({ inputTokens, outputTokens }) => {
14639
+ }
14640
+ });
14641
+ } catch (err) {
14642
+ if (err instanceof ContextCompactionRequiredError) {
14643
+ res.status(413).send(err.message);
14644
+ return;
14104
14645
  }
14105
- });
14646
+ throw err;
14647
+ }
14106
14648
  res.status(200).json(response);
14107
14649
  return;
14108
14650
  }
14109
14651
  });
14110
14652
  };
14653
+ const registerAgentCompactRoute = (slug) => {
14654
+ app.post(slug + "/:instance", async (req, res) => {
14655
+ const instance = req.params.instance;
14656
+ if (!instance) {
14657
+ res.status(400).json({ message: "Missing instance in request." });
14658
+ return;
14659
+ }
14660
+ const sessionID = req.headers["session"] || null;
14661
+ if (!sessionID) {
14662
+ res.status(400).json({ message: "Missing session header." });
14663
+ return;
14664
+ }
14665
+ const agent = await exuluApp.get().agent(instance);
14666
+ if (!agent) {
14667
+ res.status(404).json({ message: "Agent with id " + instance + " not found." });
14668
+ return;
14669
+ }
14670
+ const authenticationResult = await requestValidators.authenticate(req);
14671
+ if (!authenticationResult.user?.id) {
14672
+ res.status(authenticationResult.code || 401).json({ detail: `${authenticationResult.message}` });
14673
+ return;
14674
+ }
14675
+ const user = authenticationResult.user;
14676
+ const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
14677
+ if (!hasAccessToAgent) {
14678
+ res.status(401).json({ message: "You don't have access to this agent." });
14679
+ return;
14680
+ }
14681
+ const { db } = await postgresClient();
14682
+ const sessionRow = await db.from("agent_sessions").where({ id: sessionID }).first();
14683
+ if (!sessionRow) {
14684
+ res.status(404).json({ message: "Session not found for session ID: " + sessionID });
14685
+ return;
14686
+ }
14687
+ const hasAccessToSession = await checkRecordAccess(sessionRow, "write", user);
14688
+ if (!hasAccessToSession) {
14689
+ res.status(401).json({ message: "You don't have access to this session." });
14690
+ return;
14691
+ }
14692
+ if (isStreamActive(sessionID)) {
14693
+ res.status(409).json({ message: "A response is still streaming for this session \u2014 try again when it finishes." });
14694
+ return;
14695
+ }
14696
+ const overrideModelId = req.headers["x-exulu-model-override"];
14697
+ const modelId = overrideModelId ?? agent.model;
14698
+ if (!modelId) {
14699
+ res.status(400).json({ message: `Agent ${agent.name} (${agent.id}) has no model configured.` });
14700
+ return;
14701
+ }
14702
+ let resolved;
14703
+ try {
14704
+ resolved = await resolveModel({ modelId, user, providers, agent });
14705
+ } catch (err) {
14706
+ if (err instanceof ResolveModelError) {
14707
+ const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
14708
+ res.status(status).json({ message: err.message, code: err.code });
14709
+ return;
14710
+ }
14711
+ throw err;
14712
+ }
14713
+ const contextWindow = await resolveContextWindow({
14714
+ modelId: resolved.model.id,
14715
+ exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
14716
+ });
14717
+ const steer = typeof req.body?.steer === "string" ? req.body.steer : void 0;
14718
+ try {
14719
+ const result = await compactSession({
14720
+ sessionID,
14721
+ user,
14722
+ languageModel: resolved.languageModel,
14723
+ contextWindow,
14724
+ steer,
14725
+ modelId: resolved.model.id
14726
+ });
14727
+ res.json(result);
14728
+ } catch (err) {
14729
+ if (err instanceof CompactionInsufficientError) {
14730
+ res.status(422).send(err.message);
14731
+ return;
14732
+ }
14733
+ console.error("[EXULU] compactSession failed.", err);
14734
+ res.status(500).json({ message: err instanceof Error ? err.message : "Compaction failed." });
14735
+ }
14736
+ });
14737
+ };
14111
14738
  providers.forEach((provider) => {
14112
14739
  const slug = provider.slug;
14113
14740
  if (!slug) return;
@@ -14116,6 +14743,14 @@ ${customInstructions}` : agent.instructions;
14116
14743
  if (isLiteLLMEnabled() && providers.length > 0) {
14117
14744
  registerAgentRunRoute("/agents/litellm/run", providers[0]);
14118
14745
  }
14746
+ providers.forEach((provider) => {
14747
+ const slug = provider.slug;
14748
+ if (!slug) return;
14749
+ registerAgentCompactRoute(slug.replace(/\/run$/, "/compact"));
14750
+ });
14751
+ if (isLiteLLMEnabled() && providers.length > 0) {
14752
+ registerAgentCompactRoute("/agents/litellm/compact");
14753
+ }
14119
14754
  app.post("/agents/suggestions/:agentId", async (req, res) => {
14120
14755
  const agentId = req.params.agentId;
14121
14756
  if (!agentId) {
@@ -14467,7 +15102,7 @@ ${customInstructions}` : agent.instructions;
14467
15102
  const keys = [];
14468
15103
  const revisedPrompts = [];
14469
15104
  for (const img of images) {
14470
- const filename = `${randomUUID2()}.${img.extension}`;
15105
+ const filename = `${randomUUID3()}.${img.extension}`;
14471
15106
  const key = `sessions/${sessionId}/images/${toolCallId}/${filename}`;
14472
15107
  const fullKey = await uploadFile(
14473
15108
  img.buffer,
@@ -14755,7 +15390,7 @@ ${style.markdown}` : params.prompt;
14755
15390
  (d) => `- ${d.presignedUrl} (prompt: "${d.prompt}", model: ${d.model}${d.styleName ? `, style: ${d.styleName}` : ""})`
14756
15391
  );
14757
15392
  const messageText = "The user generated and selected the following image(s) in this chat:\n" + lines.join("\n");
14758
- const messageId = randomUUID2();
15393
+ const messageId = randomUUID3();
14759
15394
  const uiMessage = {
14760
15395
  id: messageId,
14761
15396
  role: "system",
@@ -15047,7 +15682,9 @@ ${style.markdown}` : params.prompt;
15047
15682
  const budget_duration = String(body?.budget_duration ?? "");
15048
15683
  if (!Number.isFinite(max_budget) || max_budget <= 0) return null;
15049
15684
  if (!BUDGET_ALLOWED_DURATIONS.has(budget_duration)) return null;
15050
- return { max_budget, budget_duration };
15685
+ const reset = parseResetAt(body?.budget_reset_at);
15686
+ if (!reset.valid) return null;
15687
+ return { max_budget, budget_duration, budget_reset_at: reset.value };
15051
15688
  };
15052
15689
  const parseBudgetSettingsBody = (body) => {
15053
15690
  if (!body || typeof body !== "object") return null;
@@ -15117,7 +15754,7 @@ ${style.markdown}` : params.prompt;
15117
15754
  }
15118
15755
  const body = parseBudgetBody(req.body);
15119
15756
  if (!body) {
15120
- res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
15757
+ res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
15121
15758
  return;
15122
15759
  }
15123
15760
  const entityIds = Array.isArray(req.body?.entityIds) ? req.body.entityIds : [];
@@ -15129,7 +15766,7 @@ ${style.markdown}` : params.prompt;
15129
15766
  for (const id of entityIds) {
15130
15767
  const tag = budgetTagFor(entityType, id);
15131
15768
  try {
15132
- await upsertBudget(tag, body.max_budget, body.budget_duration);
15769
+ await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
15133
15770
  results.push({ entityId: String(id), ok: true });
15134
15771
  } catch (err) {
15135
15772
  results.push({
@@ -15154,12 +15791,12 @@ ${style.markdown}` : params.prompt;
15154
15791
  }
15155
15792
  const body = parseBudgetBody(req.body);
15156
15793
  if (!body) {
15157
- res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
15794
+ res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
15158
15795
  return;
15159
15796
  }
15160
15797
  const tag = budgetTagFor(entityType, req.params.entityId ?? "");
15161
15798
  try {
15162
- await upsertBudget(tag, body.max_budget, body.budget_duration);
15799
+ await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
15163
15800
  const info = await tagInfo([tag]);
15164
15801
  res.status(200).json({ budget: info[tag] ?? null });
15165
15802
  } catch (err) {
@@ -15622,6 +16259,199 @@ ${style.markdown}` : params.prompt;
15622
16259
  }
15623
16260
  return root;
15624
16261
  }
16262
+ app.get("/skills/agent/bootstrap", async (_req, res) => {
16263
+ try {
16264
+ const zip = new JSZip3();
16265
+ zip.file("exulu-skills/SKILL.md", BOOTSTRAP_SKILL_MD);
16266
+ zip.file("exulu-skills/references/clients.json", BOOTSTRAP_CLIENTS_JSON);
16267
+ zip.file("exulu-skills/scripts/exulu", BOOTSTRAP_EXULU_SH, {
16268
+ unixPermissions: 493
16269
+ });
16270
+ const buffer = await zip.generateAsync({
16271
+ type: "nodebuffer",
16272
+ platform: "UNIX"
16273
+ });
16274
+ res.setHeader("Content-Type", "application/zip");
16275
+ res.setHeader("Content-Disposition", 'attachment; filename="exulu-skills.zip"');
16276
+ res.send(buffer);
16277
+ } catch (err) {
16278
+ console.error("[SKILLS] Failed to build bootstrap zip", err);
16279
+ res.status(500).json({ detail: "Failed to build bootstrap skill." });
16280
+ }
16281
+ });
16282
+ app.get("/skills/registry", async (req, res) => {
16283
+ const authResult = await requestValidators.authenticate(req);
16284
+ if (!authResult.user?.id) {
16285
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
16286
+ return;
16287
+ }
16288
+ const { db } = await postgresClient();
16289
+ const tag = typeof req.query.tag === "string" ? req.query.tag : void 0;
16290
+ const all = await db("skills").select("*");
16291
+ const readable = await filterReadableSkills(db, all, authResult.user);
16292
+ const skills = readable.filter((s) => {
16293
+ if (!tag) return true;
16294
+ const tags = Array.isArray(s.tags) ? s.tags : [];
16295
+ return tags.includes(tag);
16296
+ }).map((s) => ({
16297
+ name: s.name,
16298
+ description: s.description ?? "",
16299
+ tags: Array.isArray(s.tags) ? s.tags : [],
16300
+ current_version: s.current_version ?? 1,
16301
+ updated_at: s.updatedAt ?? s.updated_at ?? null
16302
+ }));
16303
+ res.json({ skills });
16304
+ });
16305
+ app.get("/skills/registry/:name/download", async (req, res) => {
16306
+ const authResult = await requestValidators.authenticate(req);
16307
+ if (!authResult.user?.id) {
16308
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
16309
+ return;
16310
+ }
16311
+ const { db } = await postgresClient();
16312
+ const skill = await resolveSkillByName(db, req.params.name);
16313
+ if (!skill) {
16314
+ res.status(404).json({ detail: "Skill not found." });
16315
+ return;
16316
+ }
16317
+ if (!await canAccessSkill(db, skill, "read", authResult.user)) {
16318
+ res.status(403).json({ detail: "You don't have access to this skill." });
16319
+ return;
16320
+ }
16321
+ const vQuery = req.query.version;
16322
+ const version = !vQuery || vQuery === "latest" ? skill.current_version ?? 1 : Number(vQuery);
16323
+ if (!Number.isFinite(version) || version < 1) {
16324
+ res.status(400).json({ detail: "Invalid version." });
16325
+ return;
16326
+ }
16327
+ const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
16328
+ const versionPrefix = `skills/${skill.id}/v${version}/`;
16329
+ const files = await listS3ObjectsByPrefix(versionPrefix, config);
16330
+ if (files.length === 0) {
16331
+ res.status(404).json({ detail: `Version v${version} has no files.` });
16332
+ return;
16333
+ }
16334
+ const zip = new JSZip3();
16335
+ for (const file of files) {
16336
+ const idx = file.key.indexOf(versionPrefix);
16337
+ const rel = idx >= 0 ? file.key.slice(idx + versionPrefix.length) : file.key;
16338
+ if (!rel) continue;
16339
+ const bytes = await getS3ObjectBytes(file.key, config);
16340
+ zip.file(`${safeName}/${rel}`, bytes);
16341
+ }
16342
+ const buffer = await zip.generateAsync({ type: "nodebuffer" });
16343
+ res.setHeader("Content-Type", "application/zip");
16344
+ res.setHeader("Content-Disposition", `attachment; filename="${safeName}.skill"`);
16345
+ res.send(buffer);
16346
+ });
16347
+ app.post("/skills/registry/:name", rawZip, async (req, res) => {
16348
+ const authResult = await requestValidators.authenticate(req);
16349
+ if (!authResult.user?.id) {
16350
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
16351
+ return;
16352
+ }
16353
+ const name = req.params.name;
16354
+ const bytes = req.body;
16355
+ if (!Buffer.isBuffer(bytes) || bytes.length === 0) {
16356
+ res.status(400).json({ detail: "Empty body. Send the skill as a zip/.skill payload." });
16357
+ return;
16358
+ }
16359
+ const { db } = await postgresClient();
16360
+ const existing = await resolveSkillByName(db, name);
16361
+ if (existing) {
16362
+ const canWrite = await canAccessSkill(db, existing, "write", authResult.user);
16363
+ if (canWrite) {
16364
+ const nextVersion = (existing.current_version ?? 1) + 1;
16365
+ try {
16366
+ await extractBundleToVersion({ bytes, skillId: existing.id, version: nextVersion, config });
16367
+ } catch (err) {
16368
+ if (err instanceof BundleValidationError) {
16369
+ res.status(400).json({ detail: err.message });
16370
+ return;
16371
+ }
16372
+ console.error("[SKILLS] publish (new version) failed", err);
16373
+ res.status(500).json({ detail: "Failed to publish new version." });
16374
+ return;
16375
+ }
16376
+ const history = Array.isArray(existing.history) ? existing.history : [];
16377
+ await db("skills").where({ id: existing.id }).update({
16378
+ current_version: nextVersion,
16379
+ history: JSON.stringify([
16380
+ ...history,
16381
+ { version: nextVersion, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
16382
+ ])
16383
+ });
16384
+ res.json({ name, version: nextVersion, created: false });
16385
+ return;
16386
+ } else {
16387
+ const canRead = await canAccessSkill(db, existing, "read", authResult.user);
16388
+ if (!canRead) {
16389
+ res.status(409).json({ detail: "That name is unavailable." });
16390
+ return;
16391
+ }
16392
+ res.status(403).json({ detail: "You don't have write access to this skill." });
16393
+ return;
16394
+ }
16395
+ }
16396
+ const meta = await parseSkillFrontmatter(bytes);
16397
+ const skillId = randomUUID3();
16398
+ try {
16399
+ await extractBundleToVersion({ bytes, skillId, version: 1, config });
16400
+ } catch (err) {
16401
+ if (err instanceof BundleValidationError) {
16402
+ res.status(400).json({ detail: err.message });
16403
+ return;
16404
+ }
16405
+ console.error("[SKILLS] publish (create) failed", err);
16406
+ res.status(500).json({ detail: "Failed to publish skill." });
16407
+ return;
16408
+ }
16409
+ try {
16410
+ await db("skills").insert({
16411
+ id: skillId,
16412
+ name,
16413
+ description: meta.description ?? "",
16414
+ s3folder: `skills/${skillId}`,
16415
+ tags: JSON.stringify([]),
16416
+ usage_count: 0,
16417
+ favorite_count: 0,
16418
+ current_version: 1,
16419
+ history: JSON.stringify([
16420
+ { version: 1, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
16421
+ ]),
16422
+ rights_mode: "private",
16423
+ created_by: authResult.user.id
16424
+ });
16425
+ } catch (err) {
16426
+ res.status(409).json({ detail: "That name is unavailable." });
16427
+ return;
16428
+ }
16429
+ res.json({ name, version: 1, created: true });
16430
+ });
16431
+ app.get("/skills/registry/:name", async (req, res) => {
16432
+ const authResult = await requestValidators.authenticate(req);
16433
+ if (!authResult.user?.id) {
16434
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
16435
+ return;
16436
+ }
16437
+ const { db } = await postgresClient();
16438
+ const skill = await resolveSkillByName(db, req.params.name);
16439
+ if (!skill) {
16440
+ res.status(404).json({ detail: "Skill not found." });
16441
+ return;
16442
+ }
16443
+ if (!await canAccessSkill(db, skill, "read", authResult.user)) {
16444
+ res.status(403).json({ detail: "You don't have access to this skill." });
16445
+ return;
16446
+ }
16447
+ res.json({
16448
+ name: skill.name,
16449
+ description: skill.description ?? "",
16450
+ tags: Array.isArray(skill.tags) ? skill.tags : [],
16451
+ current_version: skill.current_version ?? 1,
16452
+ history: Array.isArray(skill.history) ? skill.history : []
16453
+ });
16454
+ });
15625
16455
  app.post("/skills/:skillId/init", async (req, res) => {
15626
16456
  const authResult = await requestValidators.authenticate(req);
15627
16457
  if (!authResult.user?.id) {
@@ -15669,8 +16499,8 @@ ${style.markdown}` : params.prompt;
15669
16499
  }
15670
16500
  const { skillId } = req.params;
15671
16501
  const { extension, contentType } = req.body ?? {};
15672
- if (extension !== ".zip" && extension !== ".md") {
15673
- res.status(400).json({ detail: 'extension must be ".zip" or ".md".' });
16502
+ if (extension !== ".zip" && extension !== ".md" && extension !== ".skill") {
16503
+ res.status(400).json({ detail: 'extension must be ".zip", ".md", or ".skill".' });
15674
16504
  return;
15675
16505
  }
15676
16506
  if (!contentType || typeof contentType !== "string") {
@@ -15683,7 +16513,7 @@ ${style.markdown}` : params.prompt;
15683
16513
  res.status(404).json({ detail: "Skill not found." });
15684
16514
  return;
15685
16515
  }
15686
- const stagingKey = `user_${authResult.user.id}/skills/_staging/${randomUUID2()}${extension}`;
16516
+ const stagingKey = `user_${authResult.user.id}/skills/_staging/${randomUUID3()}${extension}`;
15687
16517
  const fullKey = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/${stagingKey}` : stagingKey;
15688
16518
  const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
15689
16519
  res.json({ uploadUrl, stagingKey });
@@ -15809,19 +16639,22 @@ ${style.markdown}` : params.prompt;
15809
16639
  }
15810
16640
  const versionPrefix = `skills/${skillId}/v${version}/`;
15811
16641
  const files = await listS3ObjectsByPrefix(versionPrefix, config);
15812
- const zip = new JSZip2();
16642
+ const asSkill = req.query.format === "skill";
16643
+ const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
16644
+ const zip = new JSZip3();
15813
16645
  let fileCount = 0;
15814
16646
  for (const file of files) {
15815
16647
  const prefixIndex = file.key.indexOf(versionPrefix);
15816
16648
  const relativePath = prefixIndex >= 0 ? file.key.slice(prefixIndex + versionPrefix.length) : file.key;
15817
16649
  if (!relativePath) continue;
15818
16650
  const bytes = await getS3ObjectBytes(file.key, config);
15819
- zip.file(relativePath, bytes);
16651
+ const archivePath = asSkill ? `${safeName}/${relativePath}` : relativePath;
16652
+ zip.file(archivePath, bytes);
15820
16653
  fileCount += 1;
15821
16654
  }
15822
16655
  const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
15823
16656
  zip.file(
15824
- "version.txt",
16657
+ asSkill ? `${safeName}/version.txt` : "version.txt",
15825
16658
  [
15826
16659
  `Skill: ${skill.name ?? skillId}`,
15827
16660
  `Skill id: ${skillId}`,
@@ -15832,13 +16665,9 @@ ${style.markdown}` : params.prompt;
15832
16665
  ].join("\n")
15833
16666
  );
15834
16667
  const buffer = await zip.generateAsync({ type: "nodebuffer" });
15835
- const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
15836
- const filename = `${safeName}-v${version}.zip`;
16668
+ const filename = asSkill ? `${safeName}.skill` : `${safeName}-v${version}.zip`;
15837
16669
  res.setHeader("Content-Type", "application/zip");
15838
- res.setHeader(
15839
- "Content-Disposition",
15840
- `attachment; filename="${filename}"`
15841
- );
16670
+ res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
15842
16671
  res.send(buffer);
15843
16672
  });
15844
16673
  app.post("/skills/:skillId/sign", async (req, res) => {
@@ -16138,6 +16967,12 @@ ${style.markdown}` : params.prompt;
16138
16967
  const fullSessionPrefix = `${generalPrefix}${userSessionPrefix}`;
16139
16968
  return { userSessionPrefix, fullSessionPrefix };
16140
16969
  }
16970
+ const loadSessionFilesAuth = async (req, res, sessionId, rights) => {
16971
+ const authed = await loadAuthedSession(req, res, sessionId, rights);
16972
+ if (!authed) return null;
16973
+ const ownerId = authed.session.user ?? authed.user.id;
16974
+ return { ...authed, ownerId, ...buildSessionPrefixes(ownerId, sessionId) };
16975
+ };
16141
16976
  function sanitizeFilename(name) {
16142
16977
  const trimmed = name.trim();
16143
16978
  if (!trimmed) return "";
@@ -16146,11 +16981,6 @@ ${style.markdown}` : params.prompt;
16146
16981
  return trimmed.replace(/[\\/]/g, "_");
16147
16982
  }
16148
16983
  app.get("/sessions/:sessionId/files", async (req, res) => {
16149
- const authResult = await requestValidators.authenticate(req);
16150
- if (!authResult.user?.id) {
16151
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16152
- return;
16153
- }
16154
16984
  const sessionId = req.params.sessionId;
16155
16985
  if (!sessionId) {
16156
16986
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16160,10 +16990,9 @@ ${style.markdown}` : params.prompt;
16160
16990
  res.status(500).json({ detail: "File uploads are not configured." });
16161
16991
  return;
16162
16992
  }
16163
- const { userSessionPrefix } = buildSessionPrefixes(
16164
- authResult.user.id,
16165
- sessionId
16166
- );
16993
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
16994
+ if (!authed) return;
16995
+ const { userSessionPrefix } = authed;
16167
16996
  let objects;
16168
16997
  try {
16169
16998
  objects = await listS3ObjectsByPrefix(userSessionPrefix, config);
@@ -16193,11 +17022,6 @@ ${style.markdown}` : params.prompt;
16193
17022
  app.post(
16194
17023
  "/sessions/:sessionId/files/upload-sign",
16195
17024
  async (req, res) => {
16196
- const authResult = await requestValidators.authenticate(req);
16197
- if (!authResult.user?.id) {
16198
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16199
- return;
16200
- }
16201
17025
  const sessionId = req.params.sessionId;
16202
17026
  if (!sessionId) {
16203
17027
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16207,6 +17031,8 @@ ${style.markdown}` : params.prompt;
16207
17031
  res.status(500).json({ detail: "File uploads are not configured." });
16208
17032
  return;
16209
17033
  }
17034
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
17035
+ if (!authed) return;
16210
17036
  const { filename, contentType } = req.body ?? {};
16211
17037
  if (!filename || typeof filename !== "string") {
16212
17038
  res.status(400).json({ detail: "Missing filename in request body." });
@@ -16221,11 +17047,7 @@ ${style.markdown}` : params.prompt;
16221
17047
  res.status(400).json({ detail: "Missing contentType in request body." });
16222
17048
  return;
16223
17049
  }
16224
- const { userSessionPrefix, fullSessionPrefix } = buildSessionPrefixes(
16225
- authResult.user.id,
16226
- sessionId
16227
- );
16228
- const fullKey = `${fullSessionPrefix}${safeName}`;
17050
+ const fullKey = `${authed.fullSessionPrefix}${safeName}`;
16229
17051
  const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
16230
17052
  res.json({ uploadUrl, key: fullKey });
16231
17053
  }
@@ -16233,11 +17055,6 @@ ${style.markdown}` : params.prompt;
16233
17055
  app.post(
16234
17056
  "/sessions/:sessionId/files/sync-to-sandbox",
16235
17057
  async (req, res) => {
16236
- const authResult = await requestValidators.authenticate(req);
16237
- if (!authResult.user?.id) {
16238
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16239
- return;
16240
- }
16241
17058
  const sessionId = req.params.sessionId;
16242
17059
  if (!sessionId) {
16243
17060
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16248,18 +17065,16 @@ ${style.markdown}` : params.prompt;
16248
17065
  res.status(400).json({ detail: "Missing key in request body." });
16249
17066
  return;
16250
17067
  }
16251
- const { fullSessionPrefix } = buildSessionPrefixes(
16252
- authResult.user.id,
16253
- sessionId
16254
- );
16255
- if (!key.startsWith(fullSessionPrefix)) {
17068
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
17069
+ if (!authed) return;
17070
+ if (!key.startsWith(authed.fullSessionPrefix)) {
16256
17071
  res.status(403).json({ detail: "Key does not belong to this session." });
16257
17072
  return;
16258
17073
  }
16259
17074
  try {
16260
17075
  const result = await downloadKeyIntoSandbox({
16261
17076
  sessionId,
16262
- userId: authResult.user.id,
17077
+ userId: authed.ownerId,
16263
17078
  fullS3Key: key,
16264
17079
  config
16265
17080
  });
@@ -16273,11 +17088,6 @@ ${style.markdown}` : params.prompt;
16273
17088
  app.delete(
16274
17089
  "/sessions/:sessionId/files",
16275
17090
  async (req, res) => {
16276
- const authResult = await requestValidators.authenticate(req);
16277
- if (!authResult.user?.id) {
16278
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16279
- return;
16280
- }
16281
17091
  const sessionId = req.params.sessionId;
16282
17092
  if (!sessionId) {
16283
17093
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16288,11 +17098,9 @@ ${style.markdown}` : params.prompt;
16288
17098
  res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
16289
17099
  return;
16290
17100
  }
16291
- const { fullSessionPrefix } = buildSessionPrefixes(
16292
- authResult.user.id,
16293
- sessionId
16294
- );
16295
- if (!key.startsWith(fullSessionPrefix)) {
17101
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
17102
+ if (!authed) return;
17103
+ if (!key.startsWith(authed.fullSessionPrefix)) {
16296
17104
  res.status(403).json({ detail: "Key does not belong to this session." });
16297
17105
  return;
16298
17106
  }
@@ -16311,11 +17119,6 @@ ${style.markdown}` : params.prompt;
16311
17119
  if (!req.headers.authorization && typeof req.query.auth === "string") {
16312
17120
  req.headers.authorization = `Bearer ${req.query.auth}`;
16313
17121
  }
16314
- const authResult = await requestValidators.authenticate(req);
16315
- if (!authResult.user?.id) {
16316
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16317
- return;
16318
- }
16319
17122
  const sessionId = req.params.sessionId;
16320
17123
  if (!sessionId) {
16321
17124
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16326,11 +17129,9 @@ ${style.markdown}` : params.prompt;
16326
17129
  res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
16327
17130
  return;
16328
17131
  }
16329
- const { fullSessionPrefix } = buildSessionPrefixes(
16330
- authResult.user.id,
16331
- sessionId
16332
- );
16333
- if (!key.startsWith(fullSessionPrefix)) {
17132
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
17133
+ if (!authed) return;
17134
+ if (!key.startsWith(authed.fullSessionPrefix)) {
16334
17135
  res.status(403).json({ detail: "Key does not belong to this session." });
16335
17136
  return;
16336
17137
  }
@@ -16434,7 +17235,7 @@ ${style.markdown}` : params.prompt;
16434
17235
  ` - tracking.json tracking events linked to the user`,
16435
17236
  ``
16436
17237
  ].join("\n");
16437
- const zip = new JSZip2();
17238
+ const zip = new JSZip3();
16438
17239
  zip.file("README.txt", readme);
16439
17240
  zip.file("user_data.json", JSON.stringify(userExport, null, 2));
16440
17241
  zip.file("sessions.json", JSON.stringify(sessionsWithMessages, null, 2));
@@ -16714,7 +17515,7 @@ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
16714
17515
 
16715
17516
  // src/mcp/index.ts
16716
17517
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16717
- import { randomUUID as randomUUID3 } from "crypto";
17518
+ import { randomUUID as randomUUID4 } from "crypto";
16718
17519
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
16719
17520
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
16720
17521
  import "express";
@@ -16819,7 +17620,7 @@ var ExuluMCP = class {
16819
17620
  throw new Error("Tool not found in converted tools array.");
16820
17621
  }
16821
17622
  const iterator = await convertedTool.execute(inputs, {
16822
- toolCallId: tool2.id + "_" + randomUUID3(),
17623
+ toolCallId: tool2.id + "_" + randomUUID4(),
16823
17624
  messages: []
16824
17625
  });
16825
17626
  let result;
@@ -17011,7 +17812,7 @@ var ExuluMCP = class {
17011
17812
  transport = this.transports[sessionId];
17012
17813
  } else if (!sessionId && isInitializeRequest(req.body)) {
17013
17814
  transport = new StreamableHTTPServerTransport({
17014
- sessionIdGenerator: () => randomUUID3(),
17815
+ sessionIdGenerator: () => randomUUID4(),
17015
17816
  onsessioninitialized: (sessionId2) => {
17016
17817
  this.transports[sessionId2] = transport;
17017
17818
  }
@@ -17992,7 +18793,7 @@ var ExuluEval = class {
17992
18793
 
17993
18794
  // src/templates/evals/index.ts
17994
18795
  import { z as z5 } from "zod";
17995
- import { generateText as generateText7, Output as Output2 } from "ai";
18796
+ import { generateText as generateText8, Output as Output2 } from "ai";
17996
18797
  var llmAsJudgeEval = () => {
17997
18798
  if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
17998
18799
  return new ExuluEval({
@@ -18037,7 +18838,7 @@ var llmAsJudgeEval = () => {
18037
18838
  rbacBypass: true
18038
18839
  });
18039
18840
  console.log("[EXULU] prompt", prompt);
18040
- const { output } = await generateText7({
18841
+ const { output } = await generateText8({
18041
18842
  temperature: 0,
18042
18843
  model: resolved.languageModel,
18043
18844
  system: "",
@@ -18460,7 +19261,7 @@ After asking a question, use the Question Read tool to check if the user has ans
18460
19261
 
18461
19262
  // src/templates/tools/question/question-ask.ts
18462
19263
  import z7 from "zod";
18463
- import { randomUUID as randomUUID4 } from "crypto";
19264
+ import { randomUUID as randomUUID5 } from "crypto";
18464
19265
  var AnswerOptionSchema = z7.object({
18465
19266
  id: z7.string().describe("Unique identifier for the answer option"),
18466
19267
  text: z7.string().describe("The text of the answer option")
@@ -18514,15 +19315,15 @@ var QuestionAskTool = new ExuluTool({
18514
19315
  throw new Error("You don't have access to this session " + session.id + ".");
18515
19316
  }
18516
19317
  const answerOptionsWithIds = answerOptions.map((text) => ({
18517
- id: randomUUID4(),
19318
+ id: randomUUID5(),
18518
19319
  text
18519
19320
  }));
18520
19321
  answerOptionsWithIds.push({
18521
- id: randomUUID4(),
19322
+ id: randomUUID5(),
18522
19323
  text: "None of the above..."
18523
19324
  });
18524
19325
  const newQuestion = {
18525
- id: randomUUID4(),
19326
+ id: randomUUID5(),
18526
19327
  question,
18527
19328
  answerOptions: answerOptionsWithIds,
18528
19329
  status: "pending"
@@ -21303,10 +22104,10 @@ var MarkdownChunker = class {
21303
22104
  // ee/python/documents/processing/doc_processor.ts
21304
22105
  import * as fs3 from "fs";
21305
22106
  import * as path from "path";
21306
- import { generateText as generateText8, Output as Output3 } from "ai";
22107
+ import { generateText as generateText9, Output as Output3 } from "ai";
21307
22108
  import { z as z12 } from "zod";
21308
22109
  import pLimit from "p-limit";
21309
- import { randomUUID as randomUUID5 } from "crypto";
22110
+ import { randomUUID as randomUUID6 } from "crypto";
21310
22111
  import * as mammoth from "mammoth";
21311
22112
  import TurndownService from "turndown";
21312
22113
  import WordExtractor from "word-extractor";
@@ -21760,7 +22561,7 @@ If the page contains a flow-chart, schematic, technical drawing or control board
21760
22561
 
21761
22562
  ### 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
22563
  `;
21763
- const result = await generateText8({
22564
+ const result = await generateText9({
21764
22565
  model,
21765
22566
  output: Output3.object({
21766
22567
  schema: z12.object({
@@ -22181,7 +22982,7 @@ var loadFile = async (file, name, tempDir) => {
22181
22982
  if (!fileType) {
22182
22983
  throw new Error("[EXULU] File name does not include extension, extension is required for document processing.");
22183
22984
  }
22184
- const UUID = randomUUID5();
22985
+ const UUID = randomUUID6();
22185
22986
  let buffer;
22186
22987
  if (Buffer.isBuffer(file)) {
22187
22988
  filePath = path.join(tempDir, `${UUID}.${fileType}`);
@@ -22211,7 +23012,7 @@ async function documentProcessor({
22211
23012
  if (!license["advanced-document-processing"]) {
22212
23013
  throw new Error("Advanced document processing is an enterprise feature, please add a valid Exulu enterprise license key to use it.");
22213
23014
  }
22214
- const uuid = randomUUID5();
23015
+ const uuid = randomUUID6();
22215
23016
  const tempDir = path.join(process.cwd(), "temp", uuid);
22216
23017
  const localFilesAndFoldersToDelete = [tempDir];
22217
23018
  console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);