@exulu/backend 3.7.3 → 4.0.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.
Files changed (41) hide show
  1. package/dist/{chunk-T6JVFT7L.js → chunk-QMN6MVHQ.js} +6 -1
  2. package/dist/{chunk-BNTL6LYY.js → chunk-RBEWHG7I.js} +404 -59
  3. package/dist/cli/start-whisper.js +1 -1
  4. package/dist/{convert-exulu-tools-to-ai-sdk-tools-UQSLJDXE.js → convert-exulu-tools-to-ai-sdk-tools-6RU4IZMI.js} +1 -1
  5. package/dist/index.cjs +957 -474
  6. package/dist/index.d.cts +3 -6
  7. package/dist/index.d.ts +3 -6
  8. package/dist/index.js +258 -182
  9. package/dist/python-setup-DRJ3QX5F.js +17 -0
  10. package/ee/LICENSE.md +2 -2
  11. package/ee/agentic-retrieval/pipeline/config.test.ts +18 -1
  12. package/ee/agentic-retrieval/pipeline/config.ts +15 -0
  13. package/ee/agentic-retrieval/pipeline/index.test.ts +73 -0
  14. package/ee/agentic-retrieval/pipeline/index.ts +67 -13
  15. package/ee/agentic-retrieval/pipeline/memory.test.ts +59 -0
  16. package/ee/agentic-retrieval/pipeline/memory.ts +181 -11
  17. package/ee/agentic-retrieval/pipeline/pin-rerun.test.ts +17 -0
  18. package/ee/agentic-retrieval/pipeline/pin-rerun.ts +29 -0
  19. package/ee/agentic-retrieval/pipeline/routing.test.ts +34 -0
  20. package/ee/agentic-retrieval/pipeline/routing.ts +96 -5
  21. package/ee/agentic-retrieval/pipeline/search.ts +9 -6
  22. package/ee/agentic-retrieval/pipeline/timing.test.ts +24 -0
  23. package/ee/agentic-retrieval/pipeline/timing.ts +26 -0
  24. package/ee/agentic-retrieval/pipeline/types.ts +2 -0
  25. package/ee/invoke-skills/artifact-filter.test.ts +49 -0
  26. package/ee/invoke-skills/artifact-filter.ts +38 -0
  27. package/ee/invoke-skills/create-sandbox.ts +56 -4
  28. package/ee/python/documents/processing/README.md +2 -3
  29. package/ee/python/documents/processing/doc_processor.ts +21 -61
  30. package/ee/python/documents/processing/split_pdf.py +25 -30
  31. package/ee/python/documents/processing/tests/__init__.py +0 -0
  32. package/ee/python/documents/processing/tests/test_split_pdf.py +230 -0
  33. package/ee/python/requirements.txt +17 -2
  34. package/ee/python/setup.sh +40 -1
  35. package/ee/python/transcription/pipeline.py +109 -15
  36. package/ee/python/transcription/tests/test_align_model_licensing.py +184 -0
  37. package/ee/workers.ts +2 -7
  38. package/license.md +2 -2
  39. package/package.json +3 -4
  40. package/scripts/postinstall.cjs +52 -1
  41. package/ee/python/documents/processing/document_to_markdown.py +0 -413
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  isPythonEnvironmentSetup,
6
6
  setupPythonEnvironment,
7
7
  validatePythonEnvironment
8
- } from "./chunk-T6JVFT7L.js";
8
+ } from "./chunk-QMN6MVHQ.js";
9
9
  import {
10
10
  COMPACTION_INSUFFICIENT,
11
11
  ContextCompactionRequiredError,
@@ -62,6 +62,7 @@ import {
62
62
  imageAttachmentGuard,
63
63
  initAudit,
64
64
  invalidateBudgetCaches,
65
+ isIgnoredArtifactPath,
65
66
  isLiteLLMEnabled,
66
67
  listS3ObjectsByPrefix,
67
68
  listTagsByPrefix,
@@ -75,6 +76,7 @@ import {
75
76
  resolveReranker,
76
77
  sanitizeName,
77
78
  sanitizeToolName,
79
+ sessionFilePrefix,
78
80
  setBudgetSettings,
79
81
  setLiteLLMPackageRoot,
80
82
  sliceHistoryAtCheckpoint,
@@ -88,7 +90,7 @@ import {
88
90
  verifyCredentialNonce,
89
91
  waitForLiteLLMReady,
90
92
  withRetry
91
- } from "./chunk-BNTL6LYY.js";
93
+ } from "./chunk-RBEWHG7I.js";
92
94
  import {
93
95
  LiteLLMAdminError,
94
96
  findLiteLLMModel,
@@ -1418,6 +1420,67 @@ function resolveSearchQueryTexts(query) {
1418
1420
  hybridOrQuery: buildFullTextOrQuery(query, ftsText)
1419
1421
  };
1420
1422
  }
1423
+ function chooseFullTextQuery(opts) {
1424
+ if (opts.strictMatches || countOrTerms(opts.orText) > MAX_OR_TERMS) {
1425
+ return { fn: "plainto_tsquery", text: opts.strictText };
1426
+ }
1427
+ return { fn: "websearch_to_tsquery", text: opts.orText };
1428
+ }
1429
+ var MAX_OR_TERMS = 12;
1430
+ function countOrTerms(orText) {
1431
+ return orText.trim() ? orText.split(/\s+or\s+/i).length : 0;
1432
+ }
1433
+
1434
+ // src/graphql/resolvers/expand-neighbours.ts
1435
+ function planNeighbourFetch(results, expand) {
1436
+ const before = Math.max(0, expand.before ?? 0);
1437
+ const after = Math.max(0, expand.after ?? 0);
1438
+ const plan = /* @__PURE__ */ new Map();
1439
+ if (before === 0 && after === 0) return plan;
1440
+ const present = new Set(results.map((r) => `${r.item_id}-${r.chunk_index}`));
1441
+ for (const r of results) {
1442
+ for (let i = r.chunk_index - before; i <= r.chunk_index + after; i++) {
1443
+ if (i < 0 || i === r.chunk_index || present.has(`${r.item_id}-${i}`)) continue;
1444
+ if (!plan.has(r.item_id)) plan.set(r.item_id, /* @__PURE__ */ new Set());
1445
+ plan.get(r.item_id).add(i);
1446
+ }
1447
+ }
1448
+ return plan;
1449
+ }
1450
+ function mergeNeighbours(results, rows, plan, context) {
1451
+ const byItem = /* @__PURE__ */ new Map();
1452
+ for (const r of results) if (!byItem.has(r.item_id)) byItem.set(r.item_id, r);
1453
+ const merged = /* @__PURE__ */ new Map();
1454
+ for (const r of results) merged.set(`${r.item_id}-${r.chunk_index}`, r);
1455
+ for (const row of rows) {
1456
+ if (!plan.get(row.source)?.has(row.chunk_index)) continue;
1457
+ const key = `${row.source}-${row.chunk_index}`;
1458
+ if (merged.has(key)) continue;
1459
+ const parent = byItem.get(row.source);
1460
+ if (!parent) continue;
1461
+ merged.set(key, {
1462
+ chunk_content: row.content,
1463
+ chunk_index: row.chunk_index,
1464
+ chunk_id: row.id,
1465
+ chunk_source: row.source,
1466
+ chunk_metadata: row.metadata,
1467
+ chunk_created_at: row.createdAt,
1468
+ chunk_updated_at: row.updatedAt,
1469
+ item_updated_at: parent.item_updated_at,
1470
+ item_created_at: parent.item_created_at,
1471
+ item_id: parent.item_id,
1472
+ item_external_id: parent.item_external_id,
1473
+ item_name: parent.item_name,
1474
+ chunk_cosine_distance: 0,
1475
+ chunk_fts_rank: 0,
1476
+ chunk_hybrid_score: 0,
1477
+ context
1478
+ });
1479
+ }
1480
+ return Array.from(merged.values()).sort(
1481
+ (a, b) => a.item_id === b.item_id ? a.chunk_index - b.chunk_index : 0
1482
+ );
1483
+ }
1421
1484
 
1422
1485
  // src/graphql/resolvers/field-allow-list.ts
1423
1486
  var ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
@@ -2385,6 +2448,14 @@ var agentsSchema = {
2385
2448
  name: "max_tool_steps",
2386
2449
  type: "number"
2387
2450
  },
2451
+ {
2452
+ // Thinking budget of the answer model, forwarded as LiteLLM's
2453
+ // reasoning_effort ("none" | "disable" | "minimal" | "low" | "medium" |
2454
+ // "high"). null = provider default. See resolve-reasoning-effort.ts.
2455
+ // Auto-ALTERed on boot.
2456
+ name: "reasoning_effort",
2457
+ type: "text"
2458
+ },
2388
2459
  {
2389
2460
  name: "guest_access",
2390
2461
  type: "boolean",
@@ -3099,6 +3170,14 @@ var convertContextToTableDefinition = (context) => {
3099
3170
  return addCoreFields(definition);
3100
3171
  };
3101
3172
 
3173
+ // src/graphql/resolvers/query-embedding-policy.ts
3174
+ function needsQueryEmbedding(method) {
3175
+ return method !== "tsvector";
3176
+ }
3177
+ function boostsWithQueryEntities(method) {
3178
+ return method !== "tsvector";
3179
+ }
3180
+
3102
3181
  // src/exulu/entities/config.ts
3103
3182
  var hydrateEntityTypes = async (context) => {
3104
3183
  const byName = /* @__PURE__ */ new Map();
@@ -3863,7 +3942,9 @@ var vectorSearch = async ({
3863
3942
  const embedText = texts.embedText;
3864
3943
  hybridOrQuery = texts.hybridOrQuery;
3865
3944
  query = texts.ftsText;
3866
- if (queryEmbedding && queryEmbedding.length) {
3945
+ if (!needsQueryEmbedding(method)) {
3946
+ _embedSource = "none";
3947
+ } else if (queryEmbedding && queryEmbedding.length) {
3867
3948
  vector = queryEmbedding;
3868
3949
  _embedSource = "reused";
3869
3950
  } else {
@@ -3896,8 +3977,10 @@ var vectorSearch = async ({
3896
3977
  vector = queryVector;
3897
3978
  _embedSource = "computed";
3898
3979
  }
3899
- vectorStr = `ARRAY[${vector.join(",")}]`;
3900
- vectorExpr = `${vectorStr}::vector`;
3980
+ if (vector.length) {
3981
+ vectorStr = `ARRAY[${vector.join(",")}]`;
3982
+ vectorExpr = `${vectorStr}::vector`;
3983
+ }
3901
3984
  }
3902
3985
  let keywordsQuery = [];
3903
3986
  if (keywords) {
@@ -3955,14 +4038,22 @@ var vectorSearch = async ({
3955
4038
  ]);
3956
4039
  resultChunks = await chunksQuery;
3957
4040
  break;
3958
- case "hybridSearch":
4041
+ case "hybridSearch": {
4042
+ let strictMatches = false;
4043
+ if (query && hybridOrQuery) {
4044
+ const probe = await db(chunksTable + " as chunks").select(db.raw("1")).whereRaw(`(${languages.map((lang) => `chunks.fts @@ plainto_tsquery('${lang}', ?)`).join(" OR ")})`, languages.map(() => query)).first();
4045
+ strictMatches = Boolean(probe);
4046
+ }
4047
+ const fullText = chooseFullTextQuery({ strictMatches, strictText: query ?? "", orText: hybridOrQuery });
4048
+ const ftsFn = fullText.fn;
4049
+ hybridOrQuery = fullText.text;
3959
4050
  const matchCount = Math.min(limit * 2);
3960
4051
  const fullTextWeight = 2;
3961
4052
  const semanticWeight = 1;
3962
4053
  const rrfK = 50;
3963
- const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ");
4054
+ const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ");
3964
4055
  const ftRankParams = languages.map(() => hybridOrQuery);
3965
- const ftMatchExpression = languages.map((lang) => `chunks.fts @@ websearch_to_tsquery('${lang}', ?)`).join(" OR ");
4056
+ const ftMatchExpression = languages.map((lang) => `chunks.fts @@ ${ftsFn}('${lang}', ?)`).join(" OR ");
3966
4057
  const ftMatchParams = languages.map(() => hybridOrQuery);
3967
4058
  let fullTextQuery = db(chunksTable + " as chunks").select([
3968
4059
  "chunks.id",
@@ -4003,7 +4094,7 @@ var vectorSearch = async ({
4003
4094
  db.raw('items."updatedAt" as item_updated_at'),
4004
4095
  db.raw('items."createdAt" as item_created_at'),
4005
4096
  db.raw(
4006
- `GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) AS fts_rank`,
4097
+ `GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) AS fts_rank`,
4007
4098
  languages.map(() => hybridOrQuery)
4008
4099
  ),
4009
4100
  db.raw(`(1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance`),
@@ -4029,12 +4120,14 @@ var vectorSearch = async ({
4029
4120
  `,
4030
4121
  [rrfK, fullTextWeight, rrfK, semanticWeight, cutoffs?.hybrid || 0]
4031
4122
  ).whereRaw(
4032
- `(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) > ?)`,
4123
+ `(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) > ?)`,
4033
4124
  [...languages.map(() => hybridOrQuery), cutoffs?.tsvector || 0]
4034
4125
  ).whereRaw(`(chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)`, [
4035
4126
  cutoffs?.cosineDistance || 0
4036
4127
  ]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
4037
4128
  resultChunks = await hybridQuery;
4129
+ break;
4130
+ }
4038
4131
  }
4039
4132
  if (process.env.EXULU_VS_TIMING) {
4040
4133
  console.log(
@@ -4086,7 +4179,7 @@ var vectorSearch = async ({
4086
4179
  }
4087
4180
  let queryEntities = [];
4088
4181
  let entityInsights;
4089
- if (entitiesOn && rawQuery) {
4182
+ if (entitiesOn && rawQuery && boostsWithQueryEntities(method)) {
4090
4183
  try {
4091
4184
  const types = await hydrateEntityTypes(context);
4092
4185
  const { mentions: queryMentions } = await extractEntitiesForItem({
@@ -4118,104 +4211,18 @@ var vectorSearch = async ({
4118
4211
  }
4119
4212
  results = results.slice(0, limit);
4120
4213
  if (expand?.before || expand?.after) {
4121
- const expandedMap = /* @__PURE__ */ new Map();
4122
- for (const chunk of results) {
4123
- expandedMap.set(`${chunk.item_id}-${chunk.chunk_index}`, chunk);
4124
- }
4125
- if (expand?.before) {
4126
- for (const chunk of results) {
4127
- const indicesToFetch = Array.from(
4128
- { length: expand.before },
4129
- (_, i) => chunk.chunk_index - expand.before + i
4130
- ).filter((index) => index >= 0);
4131
- await Promise.all(
4132
- indicesToFetch.map(async (index) => {
4133
- if (expandedMap.has(`${chunk.item_id}-${index}`)) {
4134
- return;
4135
- }
4136
- const expandedChunk = await db(chunksTable).where({
4137
- source: chunk.item_id,
4138
- chunk_index: index
4139
- }).first();
4140
- if (expandedChunk) {
4141
- if (expandedChunk) {
4142
- expandedMap.set(`${chunk.item_id}-${index}`, {
4143
- chunk_content: expandedChunk.content,
4144
- chunk_index: expandedChunk.chunk_index,
4145
- chunk_id: expandedChunk.id,
4146
- chunk_source: expandedChunk.source,
4147
- chunk_metadata: expandedChunk.metadata,
4148
- chunk_created_at: expandedChunk.createdAt,
4149
- chunk_updated_at: expandedChunk.updatedAt,
4150
- item_updated_at: chunk.item_updated_at,
4151
- item_created_at: chunk.item_created_at,
4152
- item_id: chunk.item_id,
4153
- item_external_id: chunk.item_external_id,
4154
- item_name: chunk.item_name,
4155
- chunk_cosine_distance: 0,
4156
- chunk_fts_rank: 0,
4157
- chunk_hybrid_score: 0,
4158
- context: {
4159
- name: table.name.singular,
4160
- id: table.id || ""
4161
- }
4162
- });
4163
- }
4164
- }
4165
- })
4166
- );
4167
- }
4168
- }
4169
- if (expand?.after) {
4170
- for (const chunk of results) {
4171
- const indicesToFetch = Array.from(
4172
- { length: expand.after },
4173
- (_, i) => chunk.chunk_index + i + 1
4174
- );
4175
- await Promise.all(
4176
- indicesToFetch.map(async (index) => {
4177
- if (expandedMap.has(`${chunk.item_id}-${index}`)) {
4178
- return;
4179
- }
4180
- const expandedChunk = await db(chunksTable).where({
4181
- source: chunk.item_id,
4182
- chunk_index: index
4183
- }).first();
4184
- if (expandedChunk) {
4185
- expandedMap.set(`${chunk.item_id}-${index}`, {
4186
- chunk_content: expandedChunk.content,
4187
- chunk_index: expandedChunk.chunk_index,
4188
- chunk_id: expandedChunk.id,
4189
- chunk_source: expandedChunk.source,
4190
- chunk_metadata: expandedChunk.metadata,
4191
- chunk_created_at: expandedChunk.createdAt,
4192
- chunk_updated_at: expandedChunk.updatedAt,
4193
- item_updated_at: chunk.item_updated_at,
4194
- item_created_at: chunk.item_created_at,
4195
- item_id: chunk.item_id,
4196
- item_external_id: chunk.item_external_id,
4197
- item_name: chunk.item_name,
4198
- chunk_cosine_distance: 0,
4199
- chunk_fts_rank: 0,
4200
- chunk_hybrid_score: 0,
4201
- context: {
4202
- name: table.name.singular,
4203
- id: table.id || ""
4204
- }
4205
- });
4206
- }
4207
- })
4208
- );
4209
- }
4214
+ const plan = planNeighbourFetch(results, expand);
4215
+ if (plan.size > 0) {
4216
+ const itemIds = Array.from(plan.keys());
4217
+ const indices = Array.from(new Set(Array.from(plan.values()).flatMap((s) => Array.from(s))));
4218
+ const rows = await db(chunksTable).select(["id", "source", "chunk_index", "content", "metadata", "createdAt", "updatedAt"]).whereIn("source", itemIds).whereIn("chunk_index", indices);
4219
+ results = mergeNeighbours(results, rows, plan, { name: table.name.singular, id: table.id || "" });
4210
4220
  }
4211
- results = Array.from(expandedMap.values());
4212
4221
  results = results.sort((a, b) => {
4213
4222
  if (a.item_id !== b.item_id) {
4214
4223
  return a.item_id.localeCompare(b.item_id);
4215
4224
  }
4216
- const aIndex = Number(a.chunk_index);
4217
- const bIndex = Number(b.chunk_index);
4218
- return aIndex - bIndex;
4225
+ return Number(a.chunk_index) - Number(b.chunk_index);
4219
4226
  });
4220
4227
  }
4221
4228
  if (entitiesOn) {
@@ -7741,6 +7748,19 @@ function serializeError(err, depth = 0) {
7741
7748
  return { message: String(err) };
7742
7749
  }
7743
7750
 
7751
+ // src/exulu/turn-metadata.ts
7752
+ function finishTurnMetadata(opts) {
7753
+ const now = opts.now ?? Date.now();
7754
+ return {
7755
+ totalTokens: opts.totalUsage.totalTokens,
7756
+ reasoningTokens: opts.totalUsage.reasoningTokens,
7757
+ inputTokens: opts.totalUsage.inputTokens,
7758
+ outputTokens: opts.totalUsage.outputTokens,
7759
+ cachedInputTokens: opts.totalUsage.cachedInputTokens,
7760
+ durationMs: Math.max(0, now - opts.startedAt)
7761
+ };
7762
+ }
7763
+
7744
7764
  // src/exulu/agent-as-tool.ts
7745
7765
  import { z as z2 } from "zod";
7746
7766
 
@@ -7846,6 +7866,54 @@ async function resolveFreshFileUrl(url, opts) {
7846
7866
  }
7847
7867
  }
7848
7868
 
7869
+ // src/exulu/session-file-listing.ts
7870
+ var DEFAULT_MAX = 25;
7871
+ function formatSize(bytes) {
7872
+ if (bytes < 1024) return `${bytes} B`;
7873
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
7874
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
7875
+ }
7876
+ function formatAge(then, now) {
7877
+ const minutes = Math.max(0, Math.round((now.getTime() - then.getTime()) / 6e4));
7878
+ if (minutes < 1) return "just now";
7879
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
7880
+ const hours = Math.round(minutes / 60);
7881
+ if (hours < 48) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
7882
+ const days = Math.round(hours / 24);
7883
+ return `${days} day${days === 1 ? "" : "s"} ago`;
7884
+ }
7885
+ function describeSessionFiles(files, opts = {}) {
7886
+ const now = opts.now ?? /* @__PURE__ */ new Date();
7887
+ const max = opts.max ?? DEFAULT_MAX;
7888
+ const usable = files.filter((f) => f.name && !f.name.endsWith("/") && !isIgnoredArtifactPath(f.name)).sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
7889
+ if (usable.length === 0) return "";
7890
+ const shown = usable.slice(0, max);
7891
+ const lines = shown.map((f) => {
7892
+ const isNew = opts.lastTurnAt ? f.lastModified.getTime() > opts.lastTurnAt.getTime() : false;
7893
+ return `- ${f.name} (${formatSize(f.size)}, ${formatAge(f.lastModified, now)})${isNew ? " [NEW since your last answer]" : ""}`;
7894
+ });
7895
+ const omitted = usable.length - shown.length;
7896
+ if (omitted > 0) lines.push(`\u2026 and ${omitted} more file${omitted === 1 ? "" : "s"} (list them with \`ls\`).`);
7897
+ return "Files currently in this session (newest first):\n" + lines.join("\n") + '\nThese files are available to you. When the user refers to one of these files, to "the document" or "the attachment", or asks something only such a file can answer, read it with parse_document, view_document_page or read_session_file. Otherwise proceed as usual, e.g. with the knowledge bases.';
7898
+ }
7899
+ async function loadSessionFileListing(opts) {
7900
+ const uploads = opts.exuluConfig?.fileUploads;
7901
+ if (!uploads?.s3Bucket) return "";
7902
+ const prefix = sessionFilePrefix(opts.ownerId, opts.sessionID, uploads.s3prefix);
7903
+ try {
7904
+ const objects = await listS3ObjectsByPrefix(prefix, opts.exuluConfig);
7905
+ const files = objects.map((o) => ({
7906
+ name: o.key.slice(o.key.indexOf(prefix) + prefix.length),
7907
+ size: o.size,
7908
+ lastModified: new Date(o.lastModified)
7909
+ }));
7910
+ return describeSessionFiles(files, { lastTurnAt: opts.lastTurnAt });
7911
+ } catch (err) {
7912
+ console.warn(`[EXULU] could not list session files for prompt (session ${opts.sessionID}):`, err);
7913
+ return "";
7914
+ }
7915
+ }
7916
+
7849
7917
  // src/exulu/generate-stream.ts
7850
7918
  import {
7851
7919
  convertToModelMessages,
@@ -8085,11 +8153,12 @@ function resolveTurnStepBudget(maxStepCount, agent) {
8085
8153
  }
8086
8154
  return DEFAULT_MAX_STEPS;
8087
8155
  }
8156
+ var TOOL_INPUT_FLATTEN_CHARS = 6e3;
8088
8157
  function flattenPart(part) {
8089
8158
  const p = part;
8090
8159
  if (p?.type === "text") return p.text ?? "";
8091
8160
  if (p?.type === "tool-call") {
8092
- return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
8161
+ return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, TOOL_INPUT_FLATTEN_CHARS)}`;
8093
8162
  }
8094
8163
  if (p?.type === "tool-result") {
8095
8164
  const out = p.output?.value ?? p.output;
@@ -8111,7 +8180,7 @@ function flattenToolHistory(messages) {
8111
8180
  return m;
8112
8181
  });
8113
8182
  }
8114
- 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.`;
8183
+ 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. Do not invent, estimate or "fill in" values that were not actually gathered: report only what the tools returned or what you wrote down, and name explicitly what is missing. 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.`;
8115
8184
  function finalAnswerGuard(maxSteps) {
8116
8185
  return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
8117
8186
  toolChoice: "none",
@@ -8136,6 +8205,35 @@ function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
8136
8205
  };
8137
8206
  }
8138
8207
 
8208
+ // src/exulu/resolve-reasoning-effort.ts
8209
+ var REASONING_EFFORTS = ["none", "disable", "minimal", "low", "medium", "high"];
8210
+ function resolveReasoningEffort(agent) {
8211
+ const raw = agent?.reasoning_effort;
8212
+ if (typeof raw !== "string") return void 0;
8213
+ const normalized = raw.trim().toLowerCase();
8214
+ return REASONING_EFFORTS.includes(normalized) ? normalized : void 0;
8215
+ }
8216
+ function resolveProviderOptions(agent) {
8217
+ const effort = resolveReasoningEffort(agent);
8218
+ return {
8219
+ openai: { reasoningSummary: "auto" },
8220
+ ...effort ? { litellm: { reasoningEffort: effort } } : {}
8221
+ };
8222
+ }
8223
+
8224
+ // src/exulu/stream-error.ts
8225
+ function onChatStreamError({ error }) {
8226
+ const detail = error instanceof Error ? error.message : error === void 0 ? "unknown error" : safeStringify(error);
8227
+ console.error("[EXULU] chat stream error.", detail);
8228
+ }
8229
+ function safeStringify(value) {
8230
+ try {
8231
+ return JSON.stringify(value) ?? String(value);
8232
+ } catch {
8233
+ return String(value);
8234
+ }
8235
+ }
8236
+
8139
8237
  // src/exulu/generate-stream.ts
8140
8238
  var processFilePartsInMessages = async (messages, offloadCtx) => {
8141
8239
  const processedMessages = await Promise.all(
@@ -8229,6 +8327,15 @@ var saveChat = async ({
8229
8327
  await mutation;
8230
8328
  }
8231
8329
  };
8330
+ var lastMessageTime = (rows) => {
8331
+ let latest;
8332
+ for (const row of rows) {
8333
+ if (!row.createdAt) continue;
8334
+ const d = new Date(row.createdAt);
8335
+ if (!latest || d > latest) latest = d;
8336
+ }
8337
+ return latest;
8338
+ };
8232
8339
  var getAgentMessages = async ({
8233
8340
  session,
8234
8341
  user,
@@ -8285,10 +8392,13 @@ var generateSync = async ({
8285
8392
  }
8286
8393
  let project;
8287
8394
  let sessionItems;
8395
+ let sessionOwnerId;
8396
+ let lastTurnAt;
8288
8397
  if (session) {
8289
8398
  const sessionData = await getSession({ sessionID: session });
8290
8399
  sessionItems = sessionData.session_items;
8291
8400
  project = sessionData.project;
8401
+ sessionOwnerId = sessionData.user ?? void 0;
8292
8402
  }
8293
8403
  const model = languageModel;
8294
8404
  console.log("[EXULU] Model created for generating sync.");
@@ -8298,6 +8408,7 @@ var generateSync = async ({
8298
8408
  session,
8299
8409
  user: user.id
8300
8410
  });
8411
+ lastTurnAt = lastMessageTime(previousMessages);
8301
8412
  const previousMessagesContent = previousMessages.map(
8302
8413
  (message) => JSON.parse(message.content)
8303
8414
  );
@@ -8392,7 +8503,8 @@ var generateSync = async ({
8392
8503
  agent,
8393
8504
  memoryItems,
8394
8505
  contextWindow,
8395
- disabledTools
8506
+ disabledTools,
8507
+ sessionOwnerId
8396
8508
  );
8397
8509
  const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
8398
8510
  const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
@@ -8459,6 +8571,15 @@ var generateSync = async ({
8459
8571
  commands like \`node create_doc.js\`) live in the same place. These files are scoped to
8460
8572
  this single session; they are NOT visible in other sessions, projects, or knowledge bases.
8461
8573
  `;
8574
+ if (session) {
8575
+ const listing = await loadSessionFileListing({
8576
+ sessionID: session,
8577
+ ownerId: sessionOwnerId ?? user?.id ?? "api",
8578
+ exuluConfig,
8579
+ lastTurnAt
8580
+ });
8581
+ if (listing) system += "\n\n" + listing;
8582
+ }
8462
8583
  system += `
8463
8584
 
8464
8585
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
@@ -8627,10 +8748,13 @@ var generateStream = async ({
8627
8748
  let previousMessagesContent = previousMessages || [];
8628
8749
  let project;
8629
8750
  let sessionItems;
8751
+ let sessionOwnerId;
8752
+ let lastTurnAt;
8630
8753
  if (session) {
8631
8754
  const sessionData = await getSession({ sessionID: session });
8632
8755
  project = sessionData.project;
8633
8756
  sessionItems = sessionData.session_items;
8757
+ sessionOwnerId = sessionData.user ?? void 0;
8634
8758
  console.log("[EXULU] loading previous messages from session: " + session);
8635
8759
  const previousMessages2 = await getAgentMessages({
8636
8760
  session,
@@ -8641,6 +8765,7 @@ var generateStream = async ({
8641
8765
  includeAllUsers: isRunSessionMetadata(sessionData.metadata)
8642
8766
  });
8643
8767
  previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
8768
+ lastTurnAt = lastMessageTime(previousMessages2);
8644
8769
  }
8645
8770
  const model = languageModel;
8646
8771
  messages = await validateUIMessages({
@@ -8783,6 +8908,15 @@ ${skillsList}
8783
8908
  truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
8784
8909
  to page through it \u2014 do not ask the user to re-upload.
8785
8910
  `;
8911
+ if (session) {
8912
+ const listing = await loadSessionFileListing({
8913
+ sessionID: session,
8914
+ ownerId: sessionOwnerId ?? user?.id ?? "api",
8915
+ exuluConfig,
8916
+ lastTurnAt
8917
+ });
8918
+ if (listing) system += "\n\n" + listing;
8919
+ }
8786
8920
  system += `
8787
8921
 
8788
8922
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
@@ -8810,7 +8944,8 @@ When a tool execution is not approved by the user, do not retry it unless explic
8810
8944
  agent,
8811
8945
  memoryItems,
8812
8946
  contextWindow,
8813
- disabledTools
8947
+ disabledTools,
8948
+ sessionOwnerId
8814
8949
  );
8815
8950
  console.log("[EXULU] Converted tools", Object.keys(tools));
8816
8951
  const includesContextSearchTool = currentTools?.some(
@@ -8884,18 +9019,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
8884
9019
  // for the first step or change other parameters.
8885
9020
  system,
8886
9021
  maxRetries: 2,
8887
- providerOptions: {
8888
- openai: {
8889
- reasoningSummary: "auto"
8890
- }
8891
- },
9022
+ // OpenAI reasoning summaries + the agent's optional thinking budget
9023
+ // (agents.reasoning_effort → LiteLLM reasoning_effort).
9024
+ providerOptions: resolveProviderOptions(agent),
8892
9025
  tools,
8893
- onError: (error) => {
8894
- console.error("[EXULU] chat stream error.", error);
8895
- throw new Error(
8896
- `Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
8897
- );
8898
- },
9026
+ // Log only — throwing here crashed the process (see stream-error.ts).
9027
+ onError: onChatStreamError,
8899
9028
  // todo allow configuring the step budget per skill
8900
9029
  prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
8901
9030
  stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
@@ -10738,13 +10867,7 @@ var processUiMessagesFlow = async ({
10738
10867
  messageMetadata: ({ part }) => {
10739
10868
  console.log("[EXULU] part", part.type);
10740
10869
  if (part.type === "finish") {
10741
- return {
10742
- totalTokens: part.totalUsage.totalTokens,
10743
- reasoningTokens: part.totalUsage.reasoningTokens,
10744
- inputTokens: part.totalUsage.inputTokens,
10745
- outputTokens: part.totalUsage.outputTokens,
10746
- cachedInputTokens: part.totalUsage.cachedInputTokens
10747
- };
10870
+ return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: startTime });
10748
10871
  }
10749
10872
  return void 0;
10750
10873
  },
@@ -16679,6 +16802,7 @@ var createExpressRoutes = async (app, tools, contexts, config, evals, tracer) =>
16679
16802
 
16680
16803
  ${customInstructions}` : agent.instructions;
16681
16804
  if (headers.session) markStreamActive(headers.session);
16805
+ const turnStartedAt = Date.now();
16682
16806
  let result;
16683
16807
  try {
16684
16808
  result = await generateStream({
@@ -16720,13 +16844,7 @@ ${customInstructions}` : agent.instructions;
16720
16844
  };
16721
16845
  }
16722
16846
  if (part.type === "finish") {
16723
- return {
16724
- totalTokens: part.totalUsage.totalTokens,
16725
- reasoningTokens: part.totalUsage.reasoningTokens,
16726
- inputTokens: part.totalUsage.inputTokens,
16727
- outputTokens: part.totalUsage.outputTokens,
16728
- cachedInputTokens: part.totalUsage.cachedInputTokens
16729
- };
16847
+ return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: turnStartedAt });
16730
16848
  }
16731
16849
  return void 0;
16732
16850
  },
@@ -24145,7 +24263,7 @@ function reconstructTableHeaders(document, validationResults, verbose = false) {
24145
24263
  }
24146
24264
  }
24147
24265
  async function validateWithVLM(document, model, verbose = false, concurrency = 10) {
24148
- console.log(`[EXULU] Starting VLM validation for docling output, ${document.length} pages...`);
24266
+ console.log(`[EXULU] Starting VLM validation for processor output, ${document.length} pages...`);
24149
24267
  console.log(`[EXULU] Concurrency limit: ${concurrency}`);
24150
24268
  const limit = pLimit(concurrency);
24151
24269
  const validationResults = /* @__PURE__ */ new Map();
@@ -24280,48 +24398,7 @@ async function processDocument(filePath, fileType, buffer, tempDir, config, verb
24280
24398
  async function processPdf(buffer, paths, config, verbose = false) {
24281
24399
  try {
24282
24400
  let json = [];
24283
- if (config?.processor.name === "docling") {
24284
- console.log(`[EXULU] Validating Python environment...`);
24285
- const validation = await validatePythonEnvironment(void 0, true);
24286
- if (!validation.valid) {
24287
- console.log(`[EXULU] Python environment not ready, setting up automatically...`);
24288
- console.log(`[EXULU] Reason: ${validation.message}`);
24289
- const setupResult = await setupPythonEnvironment({
24290
- verbose: true,
24291
- force: false
24292
- // Only setup if not already done
24293
- });
24294
- if (!setupResult.success) {
24295
- throw new Error(`Failed to setup Python environment: ${setupResult.message}
24296
-
24297
- ${setupResult.output || ""}`);
24298
- }
24299
- console.log(`[EXULU] Python environment setup completed successfully`);
24300
- } else {
24301
- console.log(`[EXULU] Python environment is valid`);
24302
- }
24303
- console.log(`[EXULU] Processing document with document_to_markdown.py`);
24304
- const result = await executePythonScript({
24305
- scriptPath: "ee/python/documents/processing/document_to_markdown.py",
24306
- args: [
24307
- paths.source,
24308
- "-o",
24309
- paths.json,
24310
- "--images-dir",
24311
- paths.images
24312
- ],
24313
- timeout: 30 * 60 * 1e3
24314
- // 30 minutes for large documents
24315
- });
24316
- if (result.stderr) {
24317
- console.log("Processing info:", result.stderr.trim());
24318
- }
24319
- if (!result.success) {
24320
- throw new Error(`Document processing failed: ${result.stderr}`);
24321
- }
24322
- const jsonContent = await fs3.promises.readFile(paths.json, "utf-8");
24323
- json = JSON.parse(jsonContent);
24324
- } else if (config?.processor.name === "officeparser") {
24401
+ if (config?.processor.name === "officeparser") {
24325
24402
  const text = await parseOfficeAsync2(buffer, {
24326
24403
  outputErrorToConsole: false,
24327
24404
  newlineDelimiter: "\n"
@@ -24412,14 +24489,16 @@ stderr: ${splitResult.stderr.slice(-1e3)}`
24412
24489
  image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
24413
24490
  }));
24414
24491
  fs3.writeFileSync(paths.json, JSON.stringify(json, null, 2));
24492
+ } else {
24493
+ const configured = String(config?.processor?.name ?? "");
24494
+ throw new Error(
24495
+ configured === "" ? "[EXULU] No document processor configured. Set processor.name to one of: mistral, liteparse, officeparser." : `[EXULU] Unknown document processor "${configured}". Supported processors are: mistral, liteparse, officeparser.` + (configured === "docling" ? ' The "docling" processor was removed: it depended on PyMuPDF, which is AGPL-licensed. Use "mistral" for PDF OCR.' : "")
24496
+ );
24415
24497
  }
24416
24498
  console.log(`[EXULU]
24417
24499
  \u2713 Document processing completed successfully`);
24418
24500
  console.log(`[EXULU] Total pages: ${json.length}`);
24419
24501
  console.log(`[EXULU] Output file: ${paths.json}`);
24420
- if (config?.vlm?.model) {
24421
- console.error("[EXULU] VLM validation is only supported when docling is enabled, skipping validation.");
24422
- }
24423
24502
  const vlmModel = config?.vlm?.model ? await resolveVlmModel(config) : void 0;
24424
24503
  if (vlmModel && json.length > 0) {
24425
24504
  json = await validateWithVLM(
@@ -24537,9 +24616,6 @@ async function documentProcessor({
24537
24616
  } = await loadFile(file, name, tempDir);
24538
24617
  let supportedTypes = [];
24539
24618
  switch (config?.processor.name) {
24540
- case "docling":
24541
- supportedTypes = ["pdf", "docx", "doc", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"];
24542
- break;
24543
24619
  case "officeparser":
24544
24620
  supportedTypes = ["docx", "pptx", "xlsx", "odt", "odp", "ods", "pdf", "rtf", "csv", "md", "html"];
24545
24621
  break;