@exulu/backend 1.68.0 → 1.69.1

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.
@@ -119,9 +119,9 @@ async function postgresClient() {
119
119
  // Log pool events to help debug connection issues
120
120
  afterCreate: (conn, done) => {
121
121
  console.log("[EXULU] New database connection created");
122
- conn.query("SET statement_timeout = 1800000", (err) => {
122
+ conn.query("SET statement_timeout = 1800000; SET hnsw.ef_search = 20", (err) => {
123
123
  if (err) {
124
- console.error("[EXULU] Error setting statement_timeout:", err);
124
+ console.error("[EXULU] Error setting connection parameters:", err);
125
125
  }
126
126
  done(err, conn);
127
127
  });
@@ -881,13 +881,31 @@ function durationToDays(duration) {
881
881
  return n;
882
882
  }
883
883
  }
884
+ function subtractDuration(date, duration) {
885
+ const m = /^\s*(\d+(?:\.\d+)?)\s*(mo|[a-z]+)?\s*$/i.exec(String(duration ?? ""));
886
+ const unit = m ? (m[2] ?? "d").toLowerCase() : "d";
887
+ const n = m ? parseFloat(m[1]) : NaN;
888
+ if (unit === "mo" && Number.isFinite(n) && n > 0) {
889
+ const result = new Date(date);
890
+ result.setUTCMonth(result.getUTCMonth() - Math.round(n));
891
+ return result;
892
+ }
893
+ return new Date(date.getTime() - durationToDays(duration) * DAY_MS);
894
+ }
884
895
  function windowStartYmd(reset_at, duration) {
885
- const days = durationToDays(duration);
896
+ const now = Date.now();
886
897
  const reset = reset_at ? new Date(reset_at) : null;
887
- const periodStart = reset && !Number.isNaN(reset.getTime()) ? new Date(reset.getTime() - days * DAY_MS) : null;
888
- const trailingStart = new Date(Date.now() - days * DAY_MS);
889
- const chosen = periodStart && periodStart > trailingStart ? periodStart : trailingStart;
890
- return ymd(chosen);
898
+ const resetMs = reset && !Number.isNaN(reset.getTime()) ? reset.getTime() : null;
899
+ const trailingStart = subtractDuration(new Date(now), duration);
900
+ if (resetMs !== null) {
901
+ if (resetMs > now) {
902
+ const periodStart = subtractDuration(reset, duration);
903
+ return ymd(periodStart > trailingStart ? periodStart : trailingStart);
904
+ } else {
905
+ return ymd(reset > trailingStart ? reset : trailingStart);
906
+ }
907
+ }
908
+ return ymd(trailingStart);
891
909
  }
892
910
  async function enrichSpendFromActivity(map) {
893
911
  const names = Object.keys(map);
@@ -898,7 +916,10 @@ async function enrichSpendFromActivity(map) {
898
916
  windows[name] = windowStartYmd(ti.budget_reset_at, ti.budget_duration);
899
917
  }
900
918
  try {
901
- const spendByTag = await getTagSpendByWindow(windows, ymd(/* @__PURE__ */ new Date()));
919
+ const spendByTag = await getTagSpendByWindow(
920
+ windows,
921
+ ymd(new Date(Date.now() + DAY_MS))
922
+ );
902
923
  for (const name of names) {
903
924
  const spend = spendByTag[name];
904
925
  if (typeof spend === "number" && Number.isFinite(spend)) {
@@ -1034,6 +1055,7 @@ async function getUserBudgetView(userId) {
1034
1055
  readCache.set(tag, { expiry: Date.now() + READ_TTL_MS, view: null });
1035
1056
  return null;
1036
1057
  }
1058
+ await provisionDefaultUserBudget(userId);
1037
1059
  const info = await tagInfo([tag]);
1038
1060
  const ti = info[tag];
1039
1061
  if (ti?.max_budget != null) {
@@ -1122,7 +1144,12 @@ var getLiteLLMProvider = ({
1122
1144
  // supports — including Vertex Gemini, which translates it into
1123
1145
  // responseSchema/responseMimeType — so enabling this matches the actual
1124
1146
  // proxy contract.
1125
- supportsStructuredOutputs: true
1147
+ supportsStructuredOutputs: true,
1148
+ // Request token usage on STREAMED responses. Without this the openai-compatible
1149
+ // provider omits `stream_options: { include_usage: true }`, so LiteLLM returns no
1150
+ // usage for streaming calls — which zeroes out the per-request token metrics and
1151
+ // the message-footer token count (both read the AI SDK `totalUsage`/finish-part usage).
1152
+ includeUsage: true
1126
1153
  });
1127
1154
  };
1128
1155
  async function resolveModel(input) {
@@ -1603,7 +1630,7 @@ var ExuluTool = class _ExuluTool {
1603
1630
  });
1604
1631
  providerapikey = resolved.apiKey;
1605
1632
  }
1606
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-CHQF36XW.js");
1633
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-MXLGIOCT.js");
1607
1634
  const tools = await convertExuluToolsToAiSdkTools2(
1608
1635
  [this],
1609
1636
  [],
@@ -1878,7 +1905,7 @@ import { z as z9 } from "zod";
1878
1905
  import { createBashTool } from "bash-tool";
1879
1906
 
1880
1907
  // src/exulu/resolve-reranker.ts
1881
- import fs from "fs";
1908
+ import "fs";
1882
1909
  var ResolveRerankerError = class extends Error {
1883
1910
  constructor(code, message) {
1884
1911
  super(message);
@@ -1973,11 +2000,9 @@ async function resolveReranker(input) {
1973
2000
  rerank_score: r.relevance_score ?? 0
1974
2001
  }));
1975
2002
  reranked.sort((a, b) => b.rerank_score - a.rerank_score);
1976
- fs.writeFileSync("reranked.json", JSON.stringify(reranked, null, 2));
1977
2003
  return reranked;
1978
2004
  } catch (err) {
1979
2005
  console.error("[EXULU] Error reranking:", err);
1980
- fs.writeFileSync("reranked.json", JSON.stringify(err, null, 2));
1981
2006
  return [];
1982
2007
  }
1983
2008
  };
@@ -3150,6 +3175,10 @@ var ExuluStorage = class {
3150
3175
  // todo add upload and delete methods
3151
3176
  };
3152
3177
 
3178
+ // src/exulu/table-names.ts
3179
+ var getTableName = (id) => sanitizeName(id) + "_items";
3180
+ var getChunksTableName = (id) => sanitizeName(id) + "_chunks";
3181
+
3153
3182
  // src/exulu/context.ts
3154
3183
  import pgvector2 from "pgvector/knex";
3155
3184
 
@@ -4943,6 +4972,11 @@ var agentsSchema = {
4943
4972
  {
4944
4973
  name: "animation_responding",
4945
4974
  type: "text"
4975
+ },
4976
+ {
4977
+ name: "sandbox_enabled",
4978
+ type: "boolean",
4979
+ default: false
4946
4980
  }
4947
4981
  ]
4948
4982
  };
@@ -5348,6 +5382,26 @@ var oauthTokensSchema = {
5348
5382
  // null = non-expiring
5349
5383
  ]
5350
5384
  };
5385
+ var sharedArtifactsSchema = {
5386
+ type: "shared_artifacts",
5387
+ name: {
5388
+ plural: "shared_artifacts",
5389
+ singular: "shared_artifact"
5390
+ },
5391
+ // RBAC drives the "regular" auth_mode: rights_mode + the rbac table scope
5392
+ // who may view. public/password modes ignore rights_mode.
5393
+ RBAC: true,
5394
+ fields: [
5395
+ { name: "name", type: "text", index: true, unique: true, required: true },
5396
+ { name: "s3key", type: "text", required: true },
5397
+ { name: "auth_mode", type: "text", default: "regular" },
5398
+ { name: "password_hash", type: "text", required: false },
5399
+ // bcrypt; password mode only
5400
+ { name: "expires_at", type: "date", required: false },
5401
+ // null = no expiry
5402
+ { name: "content_type", type: "text", required: false }
5403
+ ]
5404
+ };
5351
5405
  var contextPresetsSchema = {
5352
5406
  type: "context_presets",
5353
5407
  name: {
@@ -5440,6 +5494,7 @@ var coreSchemas = {
5440
5494
  promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
5441
5495
  contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
5442
5496
  oauthTokensSchema: () => addCoreFields(oauthTokensSchema),
5497
+ sharedArtifactsSchema: () => addCoreFields(sharedArtifactsSchema),
5443
5498
  transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
5444
5499
  imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
5445
5500
  };
@@ -6213,7 +6268,8 @@ var vectorSearch = async ({
6213
6268
  trigger,
6214
6269
  cutoffs,
6215
6270
  expand,
6216
- entityFilter
6271
+ entityFilter,
6272
+ queryEmbedding
6217
6273
  }) => {
6218
6274
  const table = convertContextToTableDefinition(context);
6219
6275
  console.log("[EXULU] Called vector search.", {
@@ -6295,36 +6351,53 @@ var vectorSearch = async ({
6295
6351
  let vector = [];
6296
6352
  let vectorStr = "";
6297
6353
  let vectorExpr = "";
6354
+ const _tBody = Date.now();
6355
+ let _preMs = 0, _statMs = 0, _resolveMs = 0, _embedMs = 0;
6356
+ let _embedSource = "none";
6298
6357
  if (query) {
6358
+ const _tp = Date.now();
6299
6359
  const { processed: stemmedQuery } = preprocessQuery(query, {
6300
6360
  enableStemming: true,
6301
6361
  detectLanguage: true
6302
6362
  });
6363
+ _preMs = Date.now() - _tp;
6303
6364
  console.log("[EXULU] Stemmed query:", stemmedQuery);
6304
6365
  if (stemmedQuery) {
6305
6366
  query = stemmedQuery;
6306
6367
  }
6307
- await updateStatistic({
6308
- name: "count",
6309
- label: table.name.singular,
6310
- type: STATISTICS_TYPE_ENUM.EMBEDDER_GENERATE,
6311
- trigger,
6312
- count: 1,
6313
- user: user?.id,
6314
- role
6315
- });
6316
- const resolved = await resolveEmbedder({
6317
- model: embedder.model,
6318
- contextId: context.id,
6319
- contextName: context.name,
6320
- user,
6321
- roleId: role
6322
- });
6323
- const [queryVector] = await resolved.embed([query], { inputType: "query" });
6324
- if (!queryVector?.length) {
6325
- throw new Error("No vector generated for query.");
6368
+ if (queryEmbedding && queryEmbedding.length) {
6369
+ vector = queryEmbedding;
6370
+ _embedSource = "reused";
6371
+ } else {
6372
+ const _ts = Date.now();
6373
+ await updateStatistic({
6374
+ name: "count",
6375
+ label: table.name.singular,
6376
+ type: STATISTICS_TYPE_ENUM.EMBEDDER_GENERATE,
6377
+ trigger,
6378
+ count: 1,
6379
+ user: user?.id,
6380
+ role
6381
+ });
6382
+ _statMs = Date.now() - _ts;
6383
+ const _tr = Date.now();
6384
+ const resolved = await resolveEmbedder({
6385
+ model: embedder.model,
6386
+ contextId: context.id,
6387
+ contextName: context.name,
6388
+ user,
6389
+ roleId: role
6390
+ });
6391
+ _resolveMs = Date.now() - _tr;
6392
+ const _te = Date.now();
6393
+ const [queryVector] = await resolved.embed([query], { inputType: "query" });
6394
+ _embedMs = Date.now() - _te;
6395
+ if (!queryVector?.length) {
6396
+ throw new Error("No vector generated for query.");
6397
+ }
6398
+ vector = queryVector;
6399
+ _embedSource = "computed";
6326
6400
  }
6327
- vector = queryVector;
6328
6401
  vectorStr = `ARRAY[${vector.join(",")}]`;
6329
6402
  vectorExpr = `${vectorStr}::vector`;
6330
6403
  }
@@ -6351,6 +6424,7 @@ var vectorSearch = async ({
6351
6424
  const languages = configuration.languages?.length ? configuration.languages : ["english"];
6352
6425
  console.log("[EXULU] Vector search params:", { method, query, cutoffs, languages });
6353
6426
  let resultChunks = [];
6427
+ const _tSql = Date.now();
6354
6428
  switch (method) {
6355
6429
  case "tsvector":
6356
6430
  chunksQuery.limit(limit * 2);
@@ -6464,6 +6538,11 @@ var vectorSearch = async ({
6464
6538
  ]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
6465
6539
  resultChunks = await hybridQuery;
6466
6540
  }
6541
+ if (process.env.EXULU_VS_TIMING) {
6542
+ console.log(
6543
+ `[EXULU-VS] ctx=${context.id} method=${method} embed=${_embedSource} pre=${_preMs}ms stat=${_statMs}ms resolve=${_resolveMs}ms embedApi=${_embedMs}ms sql=${Date.now() - _tSql}ms total=${Date.now() - _tBody}ms`
6544
+ );
6545
+ }
6467
6546
  console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
6468
6547
  let results = resultChunks.map((chunk) => ({
6469
6548
  chunk_content: chunk.content,
@@ -6902,12 +6981,6 @@ var bullmqDecorator = async ({
6902
6981
  };
6903
6982
 
6904
6983
  // src/exulu/context.ts
6905
- var getTableName = (id) => {
6906
- return sanitizeName(id) + "_items";
6907
- };
6908
- var getChunksTableName = (id) => {
6909
- return sanitizeName(id) + "_chunks";
6910
- };
6911
6984
  var ExuluContext2 = class {
6912
6985
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
6913
6986
  // underscores and be a max length of 80 characters and at least 5 characters long.
@@ -7401,12 +7474,18 @@ var ExuluContext2 = class {
7401
7474
  };
7402
7475
  getItems = async ({
7403
7476
  filters,
7404
- fields
7477
+ fields,
7478
+ user,
7479
+ role
7405
7480
  }) => {
7406
7481
  const { db: db2 } = await postgresClient();
7407
- let query = db2.from(getTableName(this.id)).select(fields || "*");
7408
7482
  const tableDefinition = convertContextToTableDefinition(this);
7483
+ let query = db2.from(getTableName(this.id)).select(fields || "*");
7409
7484
  query = applyFilters(query, filters || [], tableDefinition);
7485
+ if (user) {
7486
+ const acUser = role && (!user.role || user.role.id !== role) ? { ...user, role: { ...user.role ?? {}, id: role } } : user;
7487
+ query = applyAccessControl(tableDefinition, query, acUser);
7488
+ }
7410
7489
  const items = await query;
7411
7490
  return items;
7412
7491
  };
@@ -9123,9 +9202,21 @@ var createNewMemoryItemTool = (agent, context) => {
9123
9202
  case "longText":
9124
9203
  case "shortText":
9125
9204
  case "code":
9126
- case "enum":
9127
9205
  fields[field.name] = z10.string().describe("The " + field.name + " of the item to create");
9128
9206
  break;
9207
+ case "enum":
9208
+ if (field.enumValues && field.enumValues.length > 0) {
9209
+ const enumValues = field.enumValues;
9210
+ fields[field.name] = z10.preprocess(
9211
+ (v) => typeof v === "string" ? v.toUpperCase() : v,
9212
+ z10.enum(enumValues)
9213
+ ).describe(
9214
+ "The " + field.name + " of the item to create. Must be one of: " + field.enumValues.join(", ")
9215
+ );
9216
+ } else {
9217
+ fields[field.name] = z10.string().describe("The " + field.name + " of the item to create");
9218
+ }
9219
+ break;
9129
9220
  case "json":
9130
9221
  fields[field.name] = z10.string({}).describe(
9131
9222
  "The " + field.name + " of the item to create, it should be a valid JSON string."
@@ -9151,6 +9242,9 @@ var createNewMemoryItemTool = (agent, context) => {
9151
9242
  break;
9152
9243
  }
9153
9244
  }
9245
+ fields["visibility"] = z10.enum(["private", "public"]).optional().describe(
9246
+ "Whether this memory is private to the user or shared (public). Ask the user if unknown."
9247
+ );
9154
9248
  const toolName = "create_" + sanitizeName(context.name) + "_memory_item";
9155
9249
  return new ExuluTool({
9156
9250
  id: toolName,
@@ -9160,14 +9254,36 @@ var createNewMemoryItemTool = (agent, context) => {
9160
9254
  type: "function",
9161
9255
  inputSchema: z10.object(fields),
9162
9256
  config: [],
9163
- execute: async ({ name, description, surroundingContext, mode, information, exuluConfig, user }) => {
9257
+ execute: async (params) => {
9258
+ const { name, description, surroundingContext, information, visibility, exuluConfig, user } = params;
9164
9259
  let result = { result: "" };
9260
+ if (!visibility) {
9261
+ return {
9262
+ result: `Before saving this memory, ask the user whether it should be PRIVATE (visible only to them) or PUBLIC (shared with the team), then call \`${toolName}\` again with \`visibility\` set.`
9263
+ };
9264
+ }
9165
9265
  try {
9266
+ const extraFields = {};
9267
+ for (const field of context.fields ?? []) {
9268
+ if (field.type === "enum" && field.enumValues && field.enumValues.length > 0) {
9269
+ const raw = params[field.name];
9270
+ if (raw !== void 0 && raw !== null && raw !== "") {
9271
+ const rawStr = String(raw);
9272
+ const canonical = field.enumValues.find(
9273
+ (v) => v.toUpperCase() === rawStr.toUpperCase()
9274
+ );
9275
+ if (canonical !== void 0) {
9276
+ extraFields[field.name] = canonical;
9277
+ }
9278
+ }
9279
+ }
9280
+ }
9166
9281
  const newItem = {
9167
9282
  name,
9168
9283
  description: "Description: " + description + "\n\nSurrounding Context: " + surroundingContext,
9169
9284
  information: "Information: " + information,
9170
- rights_mode: "public"
9285
+ rights_mode: visibility === "private" ? "private" : "public",
9286
+ ...extraFields
9171
9287
  };
9172
9288
  const { item: createdItem, job: createdJob } = await context.createItem(
9173
9289
  newItem,
@@ -9935,7 +10051,7 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
9935
10051
  contexts = [];
9936
10052
  }
9937
10053
  let sharedSessionSandbox;
9938
- if (sessionID && exuluConfig) {
10054
+ if (sessionID && exuluConfig && agent?.sandbox_enabled === true) {
9939
10055
  try {
9940
10056
  sharedSessionSandbox = await createSessionSandbox(
9941
10057
  sessionID,
@@ -10202,6 +10318,8 @@ export {
10202
10318
  createUppyRoutes,
10203
10319
  ExuluStorage,
10204
10320
  sanitizeName,
10321
+ getTableName,
10322
+ getChunksTableName,
10205
10323
  ExuluTokenizer,
10206
10324
  Chunk,
10207
10325
  BaseChunker,
@@ -10228,6 +10346,7 @@ export {
10228
10346
  getTagBudgetMap,
10229
10347
  provisionDefaultUserBudget,
10230
10348
  getUserBudgetView,
10349
+ resolveEmbedder,
10231
10350
  updateStatistic,
10232
10351
  applySorting,
10233
10352
  applyAccessControl,
@@ -10241,13 +10360,13 @@ export {
10241
10360
  ResolveModelError,
10242
10361
  resolveModel,
10243
10362
  exuluApp,
10363
+ getEntitiesTableName,
10364
+ getChunkEntitiesTableName,
10244
10365
  getEntitiesForItem,
10245
10366
  ensureEntityTables,
10246
10367
  vectorSearch,
10247
10368
  mapType,
10248
10369
  maybePruneJobResults,
10249
- getTableName,
10250
- getChunksTableName,
10251
10370
  ExuluContext2 as ExuluContext,
10252
10371
  resolveReranker,
10253
10372
  oauthRegistry,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  convertExuluToolsToAiSdkTools
3
- } from "./chunk-VPSLTGZF.js";
3
+ } from "./chunk-PJSDLFXL.js";
4
4
  export {
5
5
  convertExuluToolsToAiSdkTools
6
6
  };