@exulu/backend 1.68.0 → 1.69.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.
@@ -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
  });
@@ -883,11 +883,20 @@ function durationToDays(duration) {
883
883
  }
884
884
  function windowStartYmd(reset_at, duration) {
885
885
  const days = durationToDays(duration);
886
+ const windowMs = days * DAY_MS;
887
+ const now = Date.now();
886
888
  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);
889
+ const resetMs = reset && !Number.isNaN(reset.getTime()) ? reset.getTime() : null;
890
+ const trailingStart = new Date(now - windowMs);
891
+ if (resetMs !== null) {
892
+ if (resetMs > now) {
893
+ const periodStart = new Date(resetMs - windowMs);
894
+ return ymd(periodStart > trailingStart ? periodStart : trailingStart);
895
+ } else {
896
+ return ymd(reset > trailingStart ? reset : trailingStart);
897
+ }
898
+ }
899
+ return ymd(trailingStart);
891
900
  }
892
901
  async function enrichSpendFromActivity(map) {
893
902
  const names = Object.keys(map);
@@ -898,7 +907,10 @@ async function enrichSpendFromActivity(map) {
898
907
  windows[name] = windowStartYmd(ti.budget_reset_at, ti.budget_duration);
899
908
  }
900
909
  try {
901
- const spendByTag = await getTagSpendByWindow(windows, ymd(/* @__PURE__ */ new Date()));
910
+ const spendByTag = await getTagSpendByWindow(
911
+ windows,
912
+ ymd(new Date(Date.now() + DAY_MS))
913
+ );
902
914
  for (const name of names) {
903
915
  const spend = spendByTag[name];
904
916
  if (typeof spend === "number" && Number.isFinite(spend)) {
@@ -1034,6 +1046,7 @@ async function getUserBudgetView(userId) {
1034
1046
  readCache.set(tag, { expiry: Date.now() + READ_TTL_MS, view: null });
1035
1047
  return null;
1036
1048
  }
1049
+ await provisionDefaultUserBudget(userId);
1037
1050
  const info = await tagInfo([tag]);
1038
1051
  const ti = info[tag];
1039
1052
  if (ti?.max_budget != null) {
@@ -1122,7 +1135,12 @@ var getLiteLLMProvider = ({
1122
1135
  // supports — including Vertex Gemini, which translates it into
1123
1136
  // responseSchema/responseMimeType — so enabling this matches the actual
1124
1137
  // proxy contract.
1125
- supportsStructuredOutputs: true
1138
+ supportsStructuredOutputs: true,
1139
+ // Request token usage on STREAMED responses. Without this the openai-compatible
1140
+ // provider omits `stream_options: { include_usage: true }`, so LiteLLM returns no
1141
+ // usage for streaming calls — which zeroes out the per-request token metrics and
1142
+ // the message-footer token count (both read the AI SDK `totalUsage`/finish-part usage).
1143
+ includeUsage: true
1126
1144
  });
1127
1145
  };
1128
1146
  async function resolveModel(input) {
@@ -1603,7 +1621,7 @@ var ExuluTool = class _ExuluTool {
1603
1621
  });
1604
1622
  providerapikey = resolved.apiKey;
1605
1623
  }
1606
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-CHQF36XW.js");
1624
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-ET6UI7YG.js");
1607
1625
  const tools = await convertExuluToolsToAiSdkTools2(
1608
1626
  [this],
1609
1627
  [],
@@ -1878,7 +1896,7 @@ import { z as z9 } from "zod";
1878
1896
  import { createBashTool } from "bash-tool";
1879
1897
 
1880
1898
  // src/exulu/resolve-reranker.ts
1881
- import fs from "fs";
1899
+ import "fs";
1882
1900
  var ResolveRerankerError = class extends Error {
1883
1901
  constructor(code, message) {
1884
1902
  super(message);
@@ -1973,11 +1991,9 @@ async function resolveReranker(input) {
1973
1991
  rerank_score: r.relevance_score ?? 0
1974
1992
  }));
1975
1993
  reranked.sort((a, b) => b.rerank_score - a.rerank_score);
1976
- fs.writeFileSync("reranked.json", JSON.stringify(reranked, null, 2));
1977
1994
  return reranked;
1978
1995
  } catch (err) {
1979
1996
  console.error("[EXULU] Error reranking:", err);
1980
- fs.writeFileSync("reranked.json", JSON.stringify(err, null, 2));
1981
1997
  return [];
1982
1998
  }
1983
1999
  };
@@ -3150,6 +3166,10 @@ var ExuluStorage = class {
3150
3166
  // todo add upload and delete methods
3151
3167
  };
3152
3168
 
3169
+ // src/exulu/table-names.ts
3170
+ var getTableName = (id) => sanitizeName(id) + "_items";
3171
+ var getChunksTableName = (id) => sanitizeName(id) + "_chunks";
3172
+
3153
3173
  // src/exulu/context.ts
3154
3174
  import pgvector2 from "pgvector/knex";
3155
3175
 
@@ -4943,6 +4963,11 @@ var agentsSchema = {
4943
4963
  {
4944
4964
  name: "animation_responding",
4945
4965
  type: "text"
4966
+ },
4967
+ {
4968
+ name: "sandbox_enabled",
4969
+ type: "boolean",
4970
+ default: false
4946
4971
  }
4947
4972
  ]
4948
4973
  };
@@ -5348,6 +5373,26 @@ var oauthTokensSchema = {
5348
5373
  // null = non-expiring
5349
5374
  ]
5350
5375
  };
5376
+ var sharedArtifactsSchema = {
5377
+ type: "shared_artifacts",
5378
+ name: {
5379
+ plural: "shared_artifacts",
5380
+ singular: "shared_artifact"
5381
+ },
5382
+ // RBAC drives the "regular" auth_mode: rights_mode + the rbac table scope
5383
+ // who may view. public/password modes ignore rights_mode.
5384
+ RBAC: true,
5385
+ fields: [
5386
+ { name: "name", type: "text", index: true, unique: true, required: true },
5387
+ { name: "s3key", type: "text", required: true },
5388
+ { name: "auth_mode", type: "text", default: "regular" },
5389
+ { name: "password_hash", type: "text", required: false },
5390
+ // bcrypt; password mode only
5391
+ { name: "expires_at", type: "date", required: false },
5392
+ // null = no expiry
5393
+ { name: "content_type", type: "text", required: false }
5394
+ ]
5395
+ };
5351
5396
  var contextPresetsSchema = {
5352
5397
  type: "context_presets",
5353
5398
  name: {
@@ -5440,6 +5485,7 @@ var coreSchemas = {
5440
5485
  promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
5441
5486
  contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
5442
5487
  oauthTokensSchema: () => addCoreFields(oauthTokensSchema),
5488
+ sharedArtifactsSchema: () => addCoreFields(sharedArtifactsSchema),
5443
5489
  transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
5444
5490
  imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
5445
5491
  };
@@ -6213,7 +6259,8 @@ var vectorSearch = async ({
6213
6259
  trigger,
6214
6260
  cutoffs,
6215
6261
  expand,
6216
- entityFilter
6262
+ entityFilter,
6263
+ queryEmbedding
6217
6264
  }) => {
6218
6265
  const table = convertContextToTableDefinition(context);
6219
6266
  console.log("[EXULU] Called vector search.", {
@@ -6295,36 +6342,53 @@ var vectorSearch = async ({
6295
6342
  let vector = [];
6296
6343
  let vectorStr = "";
6297
6344
  let vectorExpr = "";
6345
+ const _tBody = Date.now();
6346
+ let _preMs = 0, _statMs = 0, _resolveMs = 0, _embedMs = 0;
6347
+ let _embedSource = "none";
6298
6348
  if (query) {
6349
+ const _tp = Date.now();
6299
6350
  const { processed: stemmedQuery } = preprocessQuery(query, {
6300
6351
  enableStemming: true,
6301
6352
  detectLanguage: true
6302
6353
  });
6354
+ _preMs = Date.now() - _tp;
6303
6355
  console.log("[EXULU] Stemmed query:", stemmedQuery);
6304
6356
  if (stemmedQuery) {
6305
6357
  query = stemmedQuery;
6306
6358
  }
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.");
6359
+ if (queryEmbedding && queryEmbedding.length) {
6360
+ vector = queryEmbedding;
6361
+ _embedSource = "reused";
6362
+ } else {
6363
+ const _ts = Date.now();
6364
+ await updateStatistic({
6365
+ name: "count",
6366
+ label: table.name.singular,
6367
+ type: STATISTICS_TYPE_ENUM.EMBEDDER_GENERATE,
6368
+ trigger,
6369
+ count: 1,
6370
+ user: user?.id,
6371
+ role
6372
+ });
6373
+ _statMs = Date.now() - _ts;
6374
+ const _tr = Date.now();
6375
+ const resolved = await resolveEmbedder({
6376
+ model: embedder.model,
6377
+ contextId: context.id,
6378
+ contextName: context.name,
6379
+ user,
6380
+ roleId: role
6381
+ });
6382
+ _resolveMs = Date.now() - _tr;
6383
+ const _te = Date.now();
6384
+ const [queryVector] = await resolved.embed([query], { inputType: "query" });
6385
+ _embedMs = Date.now() - _te;
6386
+ if (!queryVector?.length) {
6387
+ throw new Error("No vector generated for query.");
6388
+ }
6389
+ vector = queryVector;
6390
+ _embedSource = "computed";
6326
6391
  }
6327
- vector = queryVector;
6328
6392
  vectorStr = `ARRAY[${vector.join(",")}]`;
6329
6393
  vectorExpr = `${vectorStr}::vector`;
6330
6394
  }
@@ -6351,6 +6415,7 @@ var vectorSearch = async ({
6351
6415
  const languages = configuration.languages?.length ? configuration.languages : ["english"];
6352
6416
  console.log("[EXULU] Vector search params:", { method, query, cutoffs, languages });
6353
6417
  let resultChunks = [];
6418
+ const _tSql = Date.now();
6354
6419
  switch (method) {
6355
6420
  case "tsvector":
6356
6421
  chunksQuery.limit(limit * 2);
@@ -6464,6 +6529,11 @@ var vectorSearch = async ({
6464
6529
  ]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
6465
6530
  resultChunks = await hybridQuery;
6466
6531
  }
6532
+ if (process.env.EXULU_VS_TIMING) {
6533
+ console.log(
6534
+ `[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`
6535
+ );
6536
+ }
6467
6537
  console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
6468
6538
  let results = resultChunks.map((chunk) => ({
6469
6539
  chunk_content: chunk.content,
@@ -6902,12 +6972,6 @@ var bullmqDecorator = async ({
6902
6972
  };
6903
6973
 
6904
6974
  // 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
6975
  var ExuluContext2 = class {
6912
6976
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
6913
6977
  // underscores and be a max length of 80 characters and at least 5 characters long.
@@ -7401,12 +7465,18 @@ var ExuluContext2 = class {
7401
7465
  };
7402
7466
  getItems = async ({
7403
7467
  filters,
7404
- fields
7468
+ fields,
7469
+ user,
7470
+ role
7405
7471
  }) => {
7406
7472
  const { db: db2 } = await postgresClient();
7407
- let query = db2.from(getTableName(this.id)).select(fields || "*");
7408
7473
  const tableDefinition = convertContextToTableDefinition(this);
7474
+ let query = db2.from(getTableName(this.id)).select(fields || "*");
7409
7475
  query = applyFilters(query, filters || [], tableDefinition);
7476
+ if (user) {
7477
+ const acUser = role && (!user.role || user.role.id !== role) ? { ...user, role: { ...user.role ?? {}, id: role } } : user;
7478
+ query = applyAccessControl(tableDefinition, query, acUser);
7479
+ }
7410
7480
  const items = await query;
7411
7481
  return items;
7412
7482
  };
@@ -9123,9 +9193,21 @@ var createNewMemoryItemTool = (agent, context) => {
9123
9193
  case "longText":
9124
9194
  case "shortText":
9125
9195
  case "code":
9126
- case "enum":
9127
9196
  fields[field.name] = z10.string().describe("The " + field.name + " of the item to create");
9128
9197
  break;
9198
+ case "enum":
9199
+ if (field.enumValues && field.enumValues.length > 0) {
9200
+ const enumValues = field.enumValues;
9201
+ fields[field.name] = z10.preprocess(
9202
+ (v) => typeof v === "string" ? v.toUpperCase() : v,
9203
+ z10.enum(enumValues)
9204
+ ).describe(
9205
+ "The " + field.name + " of the item to create. Must be one of: " + field.enumValues.join(", ")
9206
+ );
9207
+ } else {
9208
+ fields[field.name] = z10.string().describe("The " + field.name + " of the item to create");
9209
+ }
9210
+ break;
9129
9211
  case "json":
9130
9212
  fields[field.name] = z10.string({}).describe(
9131
9213
  "The " + field.name + " of the item to create, it should be a valid JSON string."
@@ -9151,6 +9233,9 @@ var createNewMemoryItemTool = (agent, context) => {
9151
9233
  break;
9152
9234
  }
9153
9235
  }
9236
+ fields["visibility"] = z10.enum(["private", "public"]).optional().describe(
9237
+ "Whether this memory is private to the user or shared (public). Ask the user if unknown."
9238
+ );
9154
9239
  const toolName = "create_" + sanitizeName(context.name) + "_memory_item";
9155
9240
  return new ExuluTool({
9156
9241
  id: toolName,
@@ -9160,14 +9245,36 @@ var createNewMemoryItemTool = (agent, context) => {
9160
9245
  type: "function",
9161
9246
  inputSchema: z10.object(fields),
9162
9247
  config: [],
9163
- execute: async ({ name, description, surroundingContext, mode, information, exuluConfig, user }) => {
9248
+ execute: async (params) => {
9249
+ const { name, description, surroundingContext, information, visibility, exuluConfig, user } = params;
9164
9250
  let result = { result: "" };
9251
+ if (!visibility) {
9252
+ return {
9253
+ 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.`
9254
+ };
9255
+ }
9165
9256
  try {
9257
+ const extraFields = {};
9258
+ for (const field of context.fields ?? []) {
9259
+ if (field.type === "enum" && field.enumValues && field.enumValues.length > 0) {
9260
+ const raw = params[field.name];
9261
+ if (raw !== void 0 && raw !== null && raw !== "") {
9262
+ const rawStr = String(raw);
9263
+ const canonical = field.enumValues.find(
9264
+ (v) => v.toUpperCase() === rawStr.toUpperCase()
9265
+ );
9266
+ if (canonical !== void 0) {
9267
+ extraFields[field.name] = canonical;
9268
+ }
9269
+ }
9270
+ }
9271
+ }
9166
9272
  const newItem = {
9167
9273
  name,
9168
9274
  description: "Description: " + description + "\n\nSurrounding Context: " + surroundingContext,
9169
9275
  information: "Information: " + information,
9170
- rights_mode: "public"
9276
+ rights_mode: visibility === "private" ? "private" : "public",
9277
+ ...extraFields
9171
9278
  };
9172
9279
  const { item: createdItem, job: createdJob } = await context.createItem(
9173
9280
  newItem,
@@ -9935,7 +10042,7 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
9935
10042
  contexts = [];
9936
10043
  }
9937
10044
  let sharedSessionSandbox;
9938
- if (sessionID && exuluConfig) {
10045
+ if (sessionID && exuluConfig && agent?.sandbox_enabled === true) {
9939
10046
  try {
9940
10047
  sharedSessionSandbox = await createSessionSandbox(
9941
10048
  sessionID,
@@ -10202,6 +10309,8 @@ export {
10202
10309
  createUppyRoutes,
10203
10310
  ExuluStorage,
10204
10311
  sanitizeName,
10312
+ getTableName,
10313
+ getChunksTableName,
10205
10314
  ExuluTokenizer,
10206
10315
  Chunk,
10207
10316
  BaseChunker,
@@ -10228,6 +10337,7 @@ export {
10228
10337
  getTagBudgetMap,
10229
10338
  provisionDefaultUserBudget,
10230
10339
  getUserBudgetView,
10340
+ resolveEmbedder,
10231
10341
  updateStatistic,
10232
10342
  applySorting,
10233
10343
  applyAccessControl,
@@ -10241,13 +10351,13 @@ export {
10241
10351
  ResolveModelError,
10242
10352
  resolveModel,
10243
10353
  exuluApp,
10354
+ getEntitiesTableName,
10355
+ getChunkEntitiesTableName,
10244
10356
  getEntitiesForItem,
10245
10357
  ensureEntityTables,
10246
10358
  vectorSearch,
10247
10359
  mapType,
10248
10360
  maybePruneJobResults,
10249
- getTableName,
10250
- getChunksTableName,
10251
10361
  ExuluContext2 as ExuluContext,
10252
10362
  resolveReranker,
10253
10363
  oauthRegistry,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  convertExuluToolsToAiSdkTools
3
- } from "./chunk-VPSLTGZF.js";
3
+ } from "./chunk-IVC2M56U.js";
4
4
  export {
5
5
  convertExuluToolsToAiSdkTools
6
6
  };