@exulu/backend 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-IJ4HNHOT.js → chunk-RVZWZNWG.js} +568 -274
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-2PEDFZ2X.js → convert-exulu-tools-to-ai-sdk-tools-K3RHHLN6.js} +1 -1
- package/dist/index.cjs +1274 -437
- package/dist/index.d.cts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +720 -211
- package/ee/agentic-retrieval/pipeline/config.test.ts +15 -0
- package/ee/agentic-retrieval/pipeline/config.ts +6 -0
- package/ee/agentic-retrieval/pipeline/global-ids.ts +30 -0
- package/ee/agentic-retrieval/pipeline/index.test.ts +86 -1
- package/ee/agentic-retrieval/pipeline/index.ts +96 -46
- package/ee/agentic-retrieval/pipeline/project-scope.test.ts +73 -0
- package/ee/agentic-retrieval/pipeline/project-scope.ts +77 -0
- package/ee/agentic-retrieval/pipeline/search.test.ts +27 -0
- package/ee/agentic-retrieval/pipeline/search.ts +7 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
validatePythonEnvironment
|
|
8
8
|
} from "./chunk-T6JVFT7L.js";
|
|
9
9
|
import {
|
|
10
|
+
COMPACTION_INSUFFICIENT,
|
|
11
|
+
ContextCompactionRequiredError,
|
|
12
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
10
13
|
ExuluTool,
|
|
11
14
|
LITELLM_UI_PATH,
|
|
12
15
|
LiteLLMAdminError,
|
|
@@ -18,16 +21,19 @@ import {
|
|
|
18
21
|
buildTags,
|
|
19
22
|
checkLicense,
|
|
20
23
|
checkRecordAccess,
|
|
24
|
+
contextOccupancy,
|
|
21
25
|
convertExuluToolsToAiSdkTools,
|
|
22
26
|
copyS3Object,
|
|
23
27
|
createAgenticRetrievalTool,
|
|
24
|
-
createProjectItemsRetrievalTool,
|
|
25
28
|
createTaggedFetch,
|
|
26
29
|
createUppyRoutes,
|
|
27
30
|
decryptOauthState,
|
|
28
31
|
deleteS3Object,
|
|
32
|
+
deriveContextBudget,
|
|
29
33
|
downloadKeyIntoSandbox,
|
|
30
34
|
enableLiteLLMClientMode,
|
|
35
|
+
estimateMessageTokens,
|
|
36
|
+
estimateTokens,
|
|
31
37
|
exchangeCodeForTokens,
|
|
32
38
|
exuluApp,
|
|
33
39
|
getBudgetSettings,
|
|
@@ -40,10 +46,12 @@ import {
|
|
|
40
46
|
getTagDailyActivity,
|
|
41
47
|
getToken,
|
|
42
48
|
getUserBudgetView,
|
|
49
|
+
guardExtractedFileText,
|
|
43
50
|
invalidateBudgetCaches,
|
|
44
51
|
isLiteLLMEnabled,
|
|
45
52
|
listS3ObjectsByPrefix,
|
|
46
53
|
listTagsByPrefix,
|
|
54
|
+
mapStreamErrorMessage,
|
|
47
55
|
oauthRegistry,
|
|
48
56
|
oauthTokenStore,
|
|
49
57
|
postgresClient,
|
|
@@ -55,15 +63,17 @@ import {
|
|
|
55
63
|
sanitizeToolName,
|
|
56
64
|
setBudgetSettings,
|
|
57
65
|
setLiteLLMPackageRoot,
|
|
66
|
+
sliceHistoryAtCheckpoint,
|
|
58
67
|
startLiteLLMSupervisor,
|
|
59
68
|
tagDelete,
|
|
60
69
|
tagInfo,
|
|
70
|
+
truncateToolOutput,
|
|
61
71
|
updateStatistic,
|
|
62
72
|
uploadFile,
|
|
63
73
|
upsertBudget,
|
|
64
74
|
waitForLiteLLMReady,
|
|
65
75
|
withRetry
|
|
66
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-RVZWZNWG.js";
|
|
67
77
|
import {
|
|
68
78
|
findLiteLLMModel
|
|
69
79
|
} from "./chunk-7CCMW3IW.js";
|
|
@@ -2213,6 +2223,13 @@ var agentsSchema = {
|
|
|
2213
2223
|
name: "sandbox_enabled",
|
|
2214
2224
|
type: "boolean",
|
|
2215
2225
|
default: false
|
|
2226
|
+
},
|
|
2227
|
+
{
|
|
2228
|
+
// Per-turn budget for ALL tool steps on one chat message (bash, files,
|
|
2229
|
+
// knowledge search, integrations). 0/null = platform default
|
|
2230
|
+
// (DEFAULT_MAX_STEPS in resolve-max-steps.ts). Auto-ALTERed on boot.
|
|
2231
|
+
name: "max_tool_steps",
|
|
2232
|
+
type: "number"
|
|
2216
2233
|
}
|
|
2217
2234
|
]
|
|
2218
2235
|
};
|
|
@@ -4252,7 +4269,7 @@ var ExuluContext2 = class {
|
|
|
4252
4269
|
embedder,
|
|
4253
4270
|
chunker,
|
|
4254
4271
|
processor,
|
|
4255
|
-
active,
|
|
4272
|
+
active: active2,
|
|
4256
4273
|
fields,
|
|
4257
4274
|
queryRewriter,
|
|
4258
4275
|
resultReranker,
|
|
@@ -4283,7 +4300,7 @@ var ExuluContext2 = class {
|
|
|
4283
4300
|
this.description = description;
|
|
4284
4301
|
this.embedder = embedder;
|
|
4285
4302
|
this.chunker = chunker;
|
|
4286
|
-
this.active =
|
|
4303
|
+
this.active = active2;
|
|
4287
4304
|
this.queryRewriter = queryRewriter;
|
|
4288
4305
|
this.resultReranker = resultReranker;
|
|
4289
4306
|
this.entities = entities;
|
|
@@ -5398,14 +5415,26 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
|
|
|
5398
5415
|
)
|
|
5399
5416
|
);
|
|
5400
5417
|
if (args.project) {
|
|
5401
|
-
const
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5418
|
+
const hasAgentic = result.tools.some(
|
|
5419
|
+
(tool2) => tool2?.id === "agentic_context_search"
|
|
5420
|
+
);
|
|
5421
|
+
if (!hasAgentic) {
|
|
5422
|
+
const instance = createAgenticRetrievalTool({
|
|
5423
|
+
contexts: [],
|
|
5424
|
+
user,
|
|
5425
|
+
role: user.role?.id,
|
|
5426
|
+
model: void 0
|
|
5427
|
+
});
|
|
5428
|
+
if (instance) {
|
|
5429
|
+
result.tools.unshift({
|
|
5430
|
+
id: instance.id,
|
|
5431
|
+
name: instance.name,
|
|
5432
|
+
description: instance.description,
|
|
5433
|
+
category: instance.category,
|
|
5434
|
+
type: instance.type,
|
|
5435
|
+
config: []
|
|
5436
|
+
});
|
|
5437
|
+
}
|
|
5409
5438
|
}
|
|
5410
5439
|
}
|
|
5411
5440
|
result.tools = result.tools.filter((tool2) => tool2 !== null);
|
|
@@ -8852,7 +8881,7 @@ var durationFromSegments = (segments) => {
|
|
|
8852
8881
|
|
|
8853
8882
|
// src/exulu/recall/service.ts
|
|
8854
8883
|
var TABLE2 = "transcription_jobs";
|
|
8855
|
-
var DEFAULT_BOT_NAME = "
|
|
8884
|
+
var DEFAULT_BOT_NAME = "Company Notetaker";
|
|
8856
8885
|
var log3 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
|
|
8857
8886
|
var parseJson = (v) => {
|
|
8858
8887
|
if (v == null) return null;
|
|
@@ -11275,14 +11304,15 @@ import bodyParser from "body-parser";
|
|
|
11275
11304
|
import CryptoJS4 from "crypto-js";
|
|
11276
11305
|
import OpenAI from "openai";
|
|
11277
11306
|
import fs2 from "fs";
|
|
11278
|
-
import { randomUUID as
|
|
11307
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
11279
11308
|
import "@opentelemetry/api";
|
|
11280
11309
|
import JSZip2 from "jszip";
|
|
11281
11310
|
import { createIdGenerator } from "ai";
|
|
11282
11311
|
import cookieParser from "cookie-parser";
|
|
11283
11312
|
|
|
11284
11313
|
// src/exulu/resolve-max-steps.ts
|
|
11285
|
-
|
|
11314
|
+
var DEFAULT_MAX_STEPS = 10;
|
|
11315
|
+
function resolveRetrievalCallBudget(toolConfigs) {
|
|
11286
11316
|
const agentic = toolConfigs?.find((t) => t.id === "agentic_context_search");
|
|
11287
11317
|
if (!agentic?.config) return void 0;
|
|
11288
11318
|
const entry = agentic.config.find((c) => c.name === "max_steps");
|
|
@@ -11291,10 +11321,146 @@ function resolveMaxStepsFromToolConfigs(toolConfigs) {
|
|
|
11291
11321
|
const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
|
|
11292
11322
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
|
|
11293
11323
|
}
|
|
11324
|
+
function resolveTurnStepBudget(maxStepCount, agent) {
|
|
11325
|
+
if (typeof maxStepCount === "number" && Number.isFinite(maxStepCount) && maxStepCount > 0) {
|
|
11326
|
+
return Math.floor(maxStepCount);
|
|
11327
|
+
}
|
|
11328
|
+
const raw = agent?.max_tool_steps;
|
|
11329
|
+
const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
|
|
11330
|
+
if (Number.isFinite(n) && n > 0) {
|
|
11331
|
+
return Math.floor(n);
|
|
11332
|
+
}
|
|
11333
|
+
return DEFAULT_MAX_STEPS;
|
|
11334
|
+
}
|
|
11335
|
+
function flattenPart(part) {
|
|
11336
|
+
const p = part;
|
|
11337
|
+
if (p?.type === "text") return p.text ?? "";
|
|
11338
|
+
if (p?.type === "tool-call") {
|
|
11339
|
+
return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
|
|
11340
|
+
}
|
|
11341
|
+
if (p?.type === "tool-result") {
|
|
11342
|
+
const out = p.output?.value ?? p.output;
|
|
11343
|
+
return `The "${p.toolName}" tool returned: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
|
|
11344
|
+
}
|
|
11345
|
+
return "";
|
|
11346
|
+
}
|
|
11347
|
+
function flattenToolHistory(messages) {
|
|
11348
|
+
return messages.map((m) => {
|
|
11349
|
+
const msg = m;
|
|
11350
|
+
if (msg.role === "tool") {
|
|
11351
|
+
const text = (Array.isArray(msg.content) ? msg.content : []).map(flattenPart).filter(Boolean).join("\n");
|
|
11352
|
+
return { role: "user", content: text || "(tool results)" };
|
|
11353
|
+
}
|
|
11354
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
11355
|
+
const text = msg.content.map(flattenPart).filter(Boolean).join("\n");
|
|
11356
|
+
return { role: "assistant", content: text || "(searching)" };
|
|
11357
|
+
}
|
|
11358
|
+
return m;
|
|
11359
|
+
});
|
|
11360
|
+
}
|
|
11361
|
+
var FINAL_ANSWER_INSTRUCTION = `This is your last step for this turn. Answer the user's original question now, in plain text, using only the information gathered above. If you could not finish the task, tell the user you reached the maximum number of tool steps, summarize what you found and did so far, and say what remains \u2014 they can ask you to continue. Do not attempt any further tool calls. Write your answer as normal prose for the user: do not output tool-call syntax, JSON commands, or bracketed lines such as "[called tool ...]" \u2014 describe anything you did or still plan to do in plain language.`;
|
|
11294
11362
|
function finalAnswerGuard(maxSteps) {
|
|
11295
|
-
return ({ stepNumber }) => stepNumber >= maxSteps - 1 ? {
|
|
11363
|
+
return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
|
|
11364
|
+
toolChoice: "none",
|
|
11365
|
+
activeTools: [],
|
|
11366
|
+
...Array.isArray(messages) ? {
|
|
11367
|
+
messages: [
|
|
11368
|
+
...flattenToolHistory(messages),
|
|
11369
|
+
{ role: "user", content: FINAL_ANSWER_INSTRUCTION }
|
|
11370
|
+
]
|
|
11371
|
+
} : {}
|
|
11372
|
+
} : void 0;
|
|
11373
|
+
}
|
|
11374
|
+
function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
|
|
11375
|
+
if (limit == null || limit <= 0 || !agenticToolKey || !allToolKeys.includes(agenticToolKey)) {
|
|
11376
|
+
return () => void 0;
|
|
11377
|
+
}
|
|
11378
|
+
const remainingTools = allToolKeys.filter((k) => k !== agenticToolKey);
|
|
11379
|
+
return ({ steps }) => {
|
|
11380
|
+
const calls = (steps ?? []).flatMap((s) => s?.toolCalls ?? []).filter((c) => c?.toolName === agenticToolKey).length;
|
|
11381
|
+
if (calls < limit) return void 0;
|
|
11382
|
+
return { activeTools: remainingTools };
|
|
11383
|
+
};
|
|
11384
|
+
}
|
|
11385
|
+
|
|
11386
|
+
// src/exulu/context-guard.ts
|
|
11387
|
+
var KEEP_RECENT_TOOL_MESSAGES = 2;
|
|
11388
|
+
var COLLAPSE_KEEP_CHARS = 400;
|
|
11389
|
+
var COLLAPSE_MARKER = " \u2026[older tool output collapsed mid-response to fit the context window \u2014 the full output is in the session file named in the notice above, if one was saved]";
|
|
11390
|
+
function contextGuard(contextWindow) {
|
|
11391
|
+
const budget = deriveContextBudget(contextWindow);
|
|
11392
|
+
return async ({ messages }) => {
|
|
11393
|
+
if (!Array.isArray(messages) || messages.length === 0) return void 0;
|
|
11394
|
+
const tokens = estimateTokens(JSON.stringify(messages));
|
|
11395
|
+
if (tokens < budget.usableWindow) return void 0;
|
|
11396
|
+
const toolIndices = messages.map((m, i) => m?.role === "tool" ? i : -1).filter((i) => i !== -1);
|
|
11397
|
+
const collapsible = new Set(toolIndices.slice(0, Math.max(0, toolIndices.length - KEEP_RECENT_TOOL_MESSAGES)));
|
|
11398
|
+
if (collapsible.size === 0) return void 0;
|
|
11399
|
+
let changed = false;
|
|
11400
|
+
const next = messages.map((m, i) => {
|
|
11401
|
+
if (!collapsible.has(i)) return m;
|
|
11402
|
+
const msg = m;
|
|
11403
|
+
if (!Array.isArray(msg.content)) return m;
|
|
11404
|
+
const content = msg.content.map((part) => {
|
|
11405
|
+
const p = part;
|
|
11406
|
+
if (p?.type !== "tool-result") return part;
|
|
11407
|
+
const out = p.output?.value ?? p.output;
|
|
11408
|
+
const asText = typeof out === "string" ? out : JSON.stringify(out ?? "");
|
|
11409
|
+
if (asText.length <= COLLAPSE_KEEP_CHARS + COLLAPSE_MARKER.length) return part;
|
|
11410
|
+
changed = true;
|
|
11411
|
+
return { ...part, output: { type: "text", value: asText.slice(0, COLLAPSE_KEEP_CHARS) + COLLAPSE_MARKER } };
|
|
11412
|
+
});
|
|
11413
|
+
return { ...m, content };
|
|
11414
|
+
});
|
|
11415
|
+
return changed ? { messages: next } : void 0;
|
|
11416
|
+
};
|
|
11417
|
+
}
|
|
11418
|
+
function composePrepareSteps(...guards) {
|
|
11419
|
+
return async (opts) => {
|
|
11420
|
+
let merged;
|
|
11421
|
+
let messages = opts.messages;
|
|
11422
|
+
for (const guard of guards) {
|
|
11423
|
+
const result = await guard({ ...opts, messages });
|
|
11424
|
+
if (!result) continue;
|
|
11425
|
+
merged = { ...merged ?? {}, ...result };
|
|
11426
|
+
if (Array.isArray(result.messages)) {
|
|
11427
|
+
messages = result.messages;
|
|
11428
|
+
}
|
|
11429
|
+
}
|
|
11430
|
+
if (merged && messages && !("messages" in merged)) {
|
|
11431
|
+
merged.messages = messages;
|
|
11432
|
+
}
|
|
11433
|
+
return merged;
|
|
11434
|
+
};
|
|
11296
11435
|
}
|
|
11297
11436
|
|
|
11437
|
+
// src/exulu/auto-decline-stale-approvals.ts
|
|
11438
|
+
var AUTO_DECLINE_REASON = "Automatically declined because the user sent a new message instead of responding to the approval request.";
|
|
11439
|
+
var isPendingApprovalToolPart = (part) => {
|
|
11440
|
+
const candidate = part;
|
|
11441
|
+
return (candidate?.type === "dynamic-tool" || typeof candidate?.type === "string" && candidate.type.startsWith("tool-")) && candidate?.state === "approval-requested" && typeof candidate?.approval?.id === "string";
|
|
11442
|
+
};
|
|
11443
|
+
var autoDeclineStaleApprovals = (messages) => {
|
|
11444
|
+
const declined = [];
|
|
11445
|
+
const reconciled = messages.map((message, index) => {
|
|
11446
|
+
if (index === messages.length - 1 || message.role !== "assistant") return message;
|
|
11447
|
+
if (!message.parts?.some(isPendingApprovalToolPart)) return message;
|
|
11448
|
+
const updated = {
|
|
11449
|
+
...message,
|
|
11450
|
+
parts: message.parts.map(
|
|
11451
|
+
(part) => isPendingApprovalToolPart(part) ? {
|
|
11452
|
+
...part,
|
|
11453
|
+
state: "output-denied",
|
|
11454
|
+
approval: { id: part.approval.id, approved: false, reason: AUTO_DECLINE_REASON }
|
|
11455
|
+
} : part
|
|
11456
|
+
)
|
|
11457
|
+
};
|
|
11458
|
+
declined.push(updated);
|
|
11459
|
+
return updated;
|
|
11460
|
+
});
|
|
11461
|
+
return { messages: reconciled, declined };
|
|
11462
|
+
};
|
|
11463
|
+
|
|
11298
11464
|
// src/exulu/provider.ts
|
|
11299
11465
|
import { z as z2 } from "zod";
|
|
11300
11466
|
import {
|
|
@@ -11530,7 +11696,9 @@ var ExuluProvider = class {
|
|
|
11530
11696
|
agent,
|
|
11531
11697
|
instructions,
|
|
11532
11698
|
maxStepCount,
|
|
11533
|
-
onTokenUsage
|
|
11699
|
+
onTokenUsage,
|
|
11700
|
+
contextWindow,
|
|
11701
|
+
disabledTools
|
|
11534
11702
|
}) => {
|
|
11535
11703
|
console.log(
|
|
11536
11704
|
"[EXULU] Called generate sync for agent: " + this.name,
|
|
@@ -11558,9 +11726,7 @@ var ExuluProvider = class {
|
|
|
11558
11726
|
if (messages && session && user) {
|
|
11559
11727
|
const previousMessages = await getAgentMessages({
|
|
11560
11728
|
session,
|
|
11561
|
-
user: user.id
|
|
11562
|
-
limit: 50,
|
|
11563
|
-
page: 1
|
|
11729
|
+
user: user.id
|
|
11564
11730
|
});
|
|
11565
11731
|
const previousMessagesContent = previousMessages.map(
|
|
11566
11732
|
(message) => JSON.parse(message.content)
|
|
@@ -11569,6 +11735,12 @@ var ExuluProvider = class {
|
|
|
11569
11735
|
// append the new message to the previous messages:
|
|
11570
11736
|
messages: [...previousMessagesContent, ...messages]
|
|
11571
11737
|
});
|
|
11738
|
+
const contextBudget = deriveContextBudget(contextWindow);
|
|
11739
|
+
const occupancy = contextOccupancy(messages);
|
|
11740
|
+
if (occupancy >= contextBudget.blockThreshold) {
|
|
11741
|
+
throw new ContextCompactionRequiredError(occupancy, contextBudget);
|
|
11742
|
+
}
|
|
11743
|
+
messages = sliceHistoryAtCheckpoint(messages);
|
|
11572
11744
|
}
|
|
11573
11745
|
console.log(
|
|
11574
11746
|
"[EXULU] Message count for agent: " + this.name,
|
|
@@ -11633,6 +11805,34 @@ var ExuluProvider = class {
|
|
|
11633
11805
|
if (memoryContext) {
|
|
11634
11806
|
system += "\n\n" + memoryContext;
|
|
11635
11807
|
}
|
|
11808
|
+
const tools = await convertExuluToolsToAiSdkTools(
|
|
11809
|
+
currentTools,
|
|
11810
|
+
currentSkills,
|
|
11811
|
+
approvedTools,
|
|
11812
|
+
allExuluTools,
|
|
11813
|
+
toolConfigs,
|
|
11814
|
+
providerapikey,
|
|
11815
|
+
contexts,
|
|
11816
|
+
user,
|
|
11817
|
+
exuluConfig,
|
|
11818
|
+
session,
|
|
11819
|
+
req,
|
|
11820
|
+
project,
|
|
11821
|
+
sessionItems,
|
|
11822
|
+
model,
|
|
11823
|
+
agent,
|
|
11824
|
+
memoryItems,
|
|
11825
|
+
contextWindow,
|
|
11826
|
+
disabledTools
|
|
11827
|
+
);
|
|
11828
|
+
const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
|
|
11829
|
+
const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
|
|
11830
|
+
const retrievalGuard = retrievalBudgetGuard(
|
|
11831
|
+
resolveRetrievalCallBudget(toolConfigs),
|
|
11832
|
+
agenticToolKey,
|
|
11833
|
+
Object.keys(tools)
|
|
11834
|
+
);
|
|
11835
|
+
const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
|
|
11636
11836
|
const includesContextSearchTool = currentTools?.some(
|
|
11637
11837
|
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
11638
11838
|
);
|
|
@@ -11645,12 +11845,12 @@ var ExuluProvider = class {
|
|
|
11645
11845
|
system += `
|
|
11646
11846
|
|
|
11647
11847
|
|
|
11648
|
-
|
|
11848
|
+
|
|
11649
11849
|
When you use a context search tool, you will include references to the items
|
|
11650
11850
|
retrieved from the tool call result inline in the response using this exact JSON format
|
|
11651
11851
|
(all on one line, no line breaks):
|
|
11652
11852
|
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
11653
|
-
|
|
11853
|
+
|
|
11654
11854
|
IMPORTANT formatting rules:
|
|
11655
11855
|
- Do NOT reference just chunks like "Looking at chunk_index 5 and chunk_index 0 from the search result", always use the JSON format above.
|
|
11656
11856
|
- Use the exact format shown above, all on ONE line
|
|
@@ -11658,9 +11858,9 @@ var ExuluProvider = class {
|
|
|
11658
11858
|
- Use the context ID from the tool result
|
|
11659
11859
|
- Include the file/item name, not the full path
|
|
11660
11860
|
- Separate multiple citations with spaces
|
|
11661
|
-
|
|
11861
|
+
|
|
11662
11862
|
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
11663
|
-
|
|
11863
|
+
|
|
11664
11864
|
The citations will be rendered as interactive badges in the UI.
|
|
11665
11865
|
`;
|
|
11666
11866
|
}
|
|
@@ -11671,12 +11871,12 @@ var ExuluProvider = class {
|
|
|
11671
11871
|
When you use a web search tool, you will include references to the results of the tool call result inline in the response using this exact JSON format
|
|
11672
11872
|
(all on one line, no line breaks):
|
|
11673
11873
|
{url: <url>, title: <title>, snippet: <snippet>}
|
|
11674
|
-
|
|
11874
|
+
|
|
11675
11875
|
IMPORTANT formatting rules:
|
|
11676
11876
|
- Use the exact format shown above, all on ONE line
|
|
11677
11877
|
- Do NOT use quotes around field names or values
|
|
11678
11878
|
- Separate multiple results with spaces
|
|
11679
|
-
|
|
11879
|
+
|
|
11680
11880
|
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
11681
11881
|
`;
|
|
11682
11882
|
}
|
|
@@ -11709,29 +11909,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11709
11909
|
system,
|
|
11710
11910
|
prompt,
|
|
11711
11911
|
maxRetries: 2,
|
|
11712
|
-
tools
|
|
11713
|
-
currentTools,
|
|
11714
|
-
currentSkills,
|
|
11715
|
-
approvedTools,
|
|
11716
|
-
allExuluTools,
|
|
11717
|
-
toolConfigs,
|
|
11718
|
-
providerapikey,
|
|
11719
|
-
contexts,
|
|
11720
|
-
user,
|
|
11721
|
-
exuluConfig,
|
|
11722
|
-
session,
|
|
11723
|
-
req,
|
|
11724
|
-
project,
|
|
11725
|
-
sessionItems,
|
|
11726
|
-
model,
|
|
11727
|
-
agent,
|
|
11728
|
-
memoryItems
|
|
11729
|
-
),
|
|
11912
|
+
tools,
|
|
11730
11913
|
// Stop after the image_generation tool fires — the widget IS the
|
|
11731
11914
|
// assistant's response, no follow-up text turn is wanted (same
|
|
11732
11915
|
// reasoning as question_ask: the UI artifact is the message).
|
|
11733
|
-
prepareStep:
|
|
11734
|
-
stopWhen: [stepCountIs(
|
|
11916
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
11917
|
+
stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
|
|
11735
11918
|
});
|
|
11736
11919
|
console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
|
|
11737
11920
|
const {
|
|
@@ -11792,26 +11975,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11792
11975
|
ignoreIncompleteToolCalls: true
|
|
11793
11976
|
}),
|
|
11794
11977
|
maxRetries: 2,
|
|
11795
|
-
tools
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
approvedTools,
|
|
11799
|
-
allExuluTools,
|
|
11800
|
-
toolConfigs,
|
|
11801
|
-
providerapikey,
|
|
11802
|
-
contexts,
|
|
11803
|
-
user,
|
|
11804
|
-
exuluConfig,
|
|
11805
|
-
session,
|
|
11806
|
-
req,
|
|
11807
|
-
project,
|
|
11808
|
-
sessionItems,
|
|
11809
|
-
model,
|
|
11810
|
-
agent,
|
|
11811
|
-
memoryItems
|
|
11812
|
-
),
|
|
11813
|
-
prepareStep: finalAnswerGuard(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5),
|
|
11814
|
-
stopWhen: [stepCountIs(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5), hasToolCall("image_generation")]
|
|
11978
|
+
tools,
|
|
11979
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
11980
|
+
stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
|
|
11815
11981
|
});
|
|
11816
11982
|
if (statistics) {
|
|
11817
11983
|
await Promise.all([
|
|
@@ -11863,7 +12029,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11863
12029
|
* - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
|
|
11864
12030
|
* - Image files -> image parts (which ARE supported by Responses API)
|
|
11865
12031
|
*/
|
|
11866
|
-
async processFilePartsInMessages(messages) {
|
|
12032
|
+
async processFilePartsInMessages(messages, offloadCtx) {
|
|
11867
12033
|
const processedMessages = await Promise.all(
|
|
11868
12034
|
messages.map(async (message) => {
|
|
11869
12035
|
if (message.role !== "user" || !Array.isArray(message.parts)) {
|
|
@@ -11908,10 +12074,11 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11908
12074
|
outputErrorToConsole: false,
|
|
11909
12075
|
newlineDelimiter: "\n"
|
|
11910
12076
|
});
|
|
12077
|
+
const guardedText = await guardExtractedFileText(filename, String(extractedText), offloadCtx);
|
|
11911
12078
|
return {
|
|
11912
12079
|
type: "text",
|
|
11913
12080
|
text: `<file file name = "${filename}" >
|
|
11914
|
-
${
|
|
12081
|
+
${guardedText}
|
|
11915
12082
|
</file>`
|
|
11916
12083
|
};
|
|
11917
12084
|
} catch (error) {
|
|
@@ -11927,7 +12094,6 @@ ${extractedText}
|
|
|
11927
12094
|
...message,
|
|
11928
12095
|
parts: processedParts
|
|
11929
12096
|
};
|
|
11930
|
-
console.log("[EXULU] Result: " + JSON.stringify(result, null, 2));
|
|
11931
12097
|
return result;
|
|
11932
12098
|
})
|
|
11933
12099
|
);
|
|
@@ -11950,7 +12116,9 @@ ${extractedText}
|
|
|
11950
12116
|
exuluConfig,
|
|
11951
12117
|
instructions,
|
|
11952
12118
|
req,
|
|
11953
|
-
maxStepCount
|
|
12119
|
+
maxStepCount,
|
|
12120
|
+
contextWindow,
|
|
12121
|
+
disabledTools
|
|
11954
12122
|
}) => {
|
|
11955
12123
|
if (!this.config) {
|
|
11956
12124
|
console.error("[EXULU] Config is required for streaming.");
|
|
@@ -11971,9 +12139,7 @@ ${extractedText}
|
|
|
11971
12139
|
console.log("[EXULU] loading previous messages from session: " + session);
|
|
11972
12140
|
const previousMessages2 = await getAgentMessages({
|
|
11973
12141
|
session,
|
|
11974
|
-
user: user?.id
|
|
11975
|
-
limit: 50,
|
|
11976
|
-
page: 1
|
|
12142
|
+
user: user?.id
|
|
11977
12143
|
});
|
|
11978
12144
|
previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
|
|
11979
12145
|
}
|
|
@@ -12031,60 +12197,33 @@ ${extractedText}
|
|
|
12031
12197
|
messages = messages.filter(
|
|
12032
12198
|
(message2, index, self) => index === self.findLastIndex((t) => t.id === message2.id)
|
|
12033
12199
|
);
|
|
12034
|
-
messages =
|
|
12200
|
+
const { messages: reconciledMessages, declined } = autoDeclineStaleApprovals(messages);
|
|
12201
|
+
messages = reconciledMessages;
|
|
12202
|
+
if (declined.length && session && user) {
|
|
12203
|
+
await saveChat({ session, user: user.id, messages: declined });
|
|
12204
|
+
}
|
|
12205
|
+
messages = await this.processFilePartsInMessages(messages, {
|
|
12206
|
+
contextWindow,
|
|
12207
|
+
sessionID: session,
|
|
12208
|
+
user,
|
|
12209
|
+
exuluConfig
|
|
12210
|
+
});
|
|
12211
|
+
const chronologicalMessages = messages;
|
|
12212
|
+
const contextBudget = deriveContextBudget(contextWindow);
|
|
12213
|
+
const occupancy = contextOccupancy(chronologicalMessages);
|
|
12214
|
+
if (occupancy >= contextBudget.blockThreshold) {
|
|
12215
|
+
console.warn(
|
|
12216
|
+
`[EXULU] Blocking request: occupancy ${occupancy} >= blockThreshold ${contextBudget.blockThreshold} (window ${contextBudget.contextWindow}).`
|
|
12217
|
+
);
|
|
12218
|
+
throw new ContextCompactionRequiredError(occupancy, contextBudget);
|
|
12219
|
+
}
|
|
12220
|
+
messages = sliceHistoryAtCheckpoint(chronologicalMessages);
|
|
12035
12221
|
const genericContext = "IMPORTANT: \n\n The current date is " + (/* @__PURE__ */ new Date()).toLocaleDateString() + " and the current time is " + (/* @__PURE__ */ new Date()).toLocaleTimeString() + ". If the user does not explicitly provide the current date, for examle when saying ' this weekend', you should assume they are talking with the current date in mind as a reference.";
|
|
12036
12222
|
let system = instructions || "You are a helpful assistant. When you use a tool to answer a question do not explicitly comment on the result of the tool call unless the user has explicitly you to do something with the result.";
|
|
12037
12223
|
if (user?.personal_system_prompt?.trim()) {
|
|
12038
12224
|
system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
|
|
12039
12225
|
}
|
|
12040
12226
|
system += "\n\n" + genericContext;
|
|
12041
|
-
const includesContextSearchTool = currentTools?.some(
|
|
12042
|
-
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
12043
|
-
);
|
|
12044
|
-
const includesWebSearchTool = currentTools?.some(
|
|
12045
|
-
(tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
|
|
12046
|
-
);
|
|
12047
|
-
console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
|
|
12048
|
-
console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
|
|
12049
|
-
console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
|
|
12050
|
-
if (includesContextSearchTool) {
|
|
12051
|
-
system += `
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
When you use a context search tool, you will include references to the items
|
|
12056
|
-
retrieved from the tool call result inline in the response using this exact JSON format
|
|
12057
|
-
(all on one line, no line breaks):
|
|
12058
|
-
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
12059
|
-
|
|
12060
|
-
IMPORTANT formatting rules:
|
|
12061
|
-
- Use the exact format shown above, all on ONE line
|
|
12062
|
-
- Do NOT use quotes around field names or values
|
|
12063
|
-
- Use the context ID from the tool result
|
|
12064
|
-
- Include the file/item name, not the full path
|
|
12065
|
-
- Separate multiple citations with spaces
|
|
12066
|
-
|
|
12067
|
-
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
12068
|
-
|
|
12069
|
-
The citations will be rendered as interactive badges in the UI.
|
|
12070
|
-
`;
|
|
12071
|
-
}
|
|
12072
|
-
if (includesWebSearchTool) {
|
|
12073
|
-
system += `
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
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
|
|
12077
|
-
(all on one line, no line breaks):
|
|
12078
|
-
{url: <url>, title: <title>, snippet: <snippet>}
|
|
12079
|
-
|
|
12080
|
-
IMPORTANT formatting rules:
|
|
12081
|
-
- Use the exact format shown above, all on ONE line
|
|
12082
|
-
- Do NOT use quotes around field names or values
|
|
12083
|
-
- Separate multiple results with spaces
|
|
12084
|
-
|
|
12085
|
-
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
12086
|
-
`;
|
|
12087
|
-
}
|
|
12088
12227
|
if (currentSkills?.length) {
|
|
12089
12228
|
const skillsList = currentSkills.map((skill) => {
|
|
12090
12229
|
const description = (skill.description ?? "").trim();
|
|
@@ -12139,6 +12278,11 @@ ${skillsList}
|
|
|
12139
12278
|
read them with the readFile tool. Files you produce yourself (via writeFile or via shell
|
|
12140
12279
|
commands like \`node create_doc.js\`) live in the same place. These files are scoped to
|
|
12141
12280
|
this single session; they are NOT visible in other sessions, projects, or knowledge bases.
|
|
12281
|
+
|
|
12282
|
+
Note on large outputs: oversized tool outputs and large uploaded documents are automatically
|
|
12283
|
+
truncated in the conversation; the FULL content is saved as a session file (named in the
|
|
12284
|
+
truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
|
|
12285
|
+
to page through it \u2014 do not ask the user to re-upload.
|
|
12142
12286
|
`;
|
|
12143
12287
|
system += `
|
|
12144
12288
|
|
|
@@ -12162,9 +12306,66 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
12162
12306
|
sessionItems,
|
|
12163
12307
|
model,
|
|
12164
12308
|
agent,
|
|
12165
|
-
memoryItems
|
|
12309
|
+
memoryItems,
|
|
12310
|
+
contextWindow,
|
|
12311
|
+
disabledTools
|
|
12166
12312
|
);
|
|
12167
12313
|
console.log("[EXULU] Converted tools", Object.keys(tools));
|
|
12314
|
+
const includesContextSearchTool = currentTools?.some(
|
|
12315
|
+
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
12316
|
+
);
|
|
12317
|
+
const includesWebSearchTool = currentTools?.some(
|
|
12318
|
+
(tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
|
|
12319
|
+
);
|
|
12320
|
+
console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
|
|
12321
|
+
console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
|
|
12322
|
+
console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
|
|
12323
|
+
if (includesContextSearchTool) {
|
|
12324
|
+
system += `
|
|
12325
|
+
|
|
12326
|
+
|
|
12327
|
+
|
|
12328
|
+
When you use a context search tool, you will include references to the items
|
|
12329
|
+
retrieved from the tool call result inline in the response using this exact JSON format
|
|
12330
|
+
(all on one line, no line breaks):
|
|
12331
|
+
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
12332
|
+
|
|
12333
|
+
IMPORTANT formatting rules:
|
|
12334
|
+
- Use the exact format shown above, all on ONE line
|
|
12335
|
+
- Do NOT use quotes around field names or values
|
|
12336
|
+
- Use the context ID from the tool result
|
|
12337
|
+
- Include the file/item name, not the full path
|
|
12338
|
+
- Separate multiple citations with spaces
|
|
12339
|
+
|
|
12340
|
+
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
12341
|
+
|
|
12342
|
+
The citations will be rendered as interactive badges in the UI.
|
|
12343
|
+
`;
|
|
12344
|
+
}
|
|
12345
|
+
if (includesWebSearchTool) {
|
|
12346
|
+
system += `
|
|
12347
|
+
|
|
12348
|
+
|
|
12349
|
+
When you use a web search tool, you will include references to the results of the tool call result inline in the response using this exact JSON format
|
|
12350
|
+
(all on one line, no line breaks):
|
|
12351
|
+
{url: <url>, title: <title>, snippet: <snippet>}
|
|
12352
|
+
|
|
12353
|
+
IMPORTANT formatting rules:
|
|
12354
|
+
- Use the exact format shown above, all on ONE line
|
|
12355
|
+
- Do NOT use quotes around field names or values
|
|
12356
|
+
- Separate multiple results with spaces
|
|
12357
|
+
|
|
12358
|
+
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
12359
|
+
`;
|
|
12360
|
+
}
|
|
12361
|
+
const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
|
|
12362
|
+
const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
|
|
12363
|
+
const retrievalGuard = retrievalBudgetGuard(
|
|
12364
|
+
resolveRetrievalCallBudget(toolConfigs),
|
|
12365
|
+
agenticToolKey,
|
|
12366
|
+
Object.keys(tools)
|
|
12367
|
+
);
|
|
12368
|
+
const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
|
|
12168
12369
|
const result = streamText({
|
|
12169
12370
|
temperature: 0,
|
|
12170
12371
|
// TODO Make this configurable
|
|
@@ -12189,10 +12390,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
12189
12390
|
`Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
|
|
12190
12391
|
);
|
|
12191
12392
|
},
|
|
12192
|
-
//
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
stopWhen: [stepCountIs(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? (currentSkills?.length ? 10 : 5)), hasToolCall("image_generation")]
|
|
12393
|
+
// todo allow configuring the step budget per skill
|
|
12394
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
12395
|
+
stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
|
|
12196
12396
|
});
|
|
12197
12397
|
return {
|
|
12198
12398
|
stream: result,
|
|
@@ -12203,19 +12403,14 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
12203
12403
|
};
|
|
12204
12404
|
var getAgentMessages = async ({
|
|
12205
12405
|
session,
|
|
12206
|
-
user
|
|
12207
|
-
limit,
|
|
12208
|
-
page
|
|
12406
|
+
user
|
|
12209
12407
|
}) => {
|
|
12210
12408
|
const { db } = await postgresClient();
|
|
12211
|
-
console.log(
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
query.offset((page - 1) * limit);
|
|
12217
|
-
}
|
|
12218
|
-
const messages = await query;
|
|
12409
|
+
console.log("[EXULU] getting agent messages for session: " + session + " and user: " + user);
|
|
12410
|
+
const messages = await db.from("agent_messages").where({ session, user: user || null }).orderBy([
|
|
12411
|
+
{ column: "createdAt", order: "asc" },
|
|
12412
|
+
{ column: "id", order: "asc" }
|
|
12413
|
+
]);
|
|
12219
12414
|
return messages;
|
|
12220
12415
|
};
|
|
12221
12416
|
var getSession = async ({ sessionID }) => {
|
|
@@ -12318,6 +12513,151 @@ ${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
|
|
|
12318
12513
|
};
|
|
12319
12514
|
};
|
|
12320
12515
|
|
|
12516
|
+
// src/exulu/resolve-context-window.ts
|
|
12517
|
+
var resolveContextWindow = async ({
|
|
12518
|
+
modelId,
|
|
12519
|
+
exuluProvider
|
|
12520
|
+
}) => {
|
|
12521
|
+
if (process.env.EXULU_USE_LITELLM === "true") {
|
|
12522
|
+
const entry = await findLiteLLMModel(modelId);
|
|
12523
|
+
const fromCatalog = entry?.max_input_tokens ?? entry?.max_tokens;
|
|
12524
|
+
if (fromCatalog != null && fromCatalog > 0) return fromCatalog;
|
|
12525
|
+
} else if (exuluProvider) {
|
|
12526
|
+
try {
|
|
12527
|
+
const fromProvider = exuluProvider.maxContextLength;
|
|
12528
|
+
if (fromProvider != null && fromProvider > 0) return fromProvider;
|
|
12529
|
+
} catch {
|
|
12530
|
+
}
|
|
12531
|
+
}
|
|
12532
|
+
console.warn(
|
|
12533
|
+
`[EXULU] Unknown context window for model "${modelId}" \u2014 assuming ${DEFAULT_CONTEXT_WINDOW}. Check the LiteLLM catalog / provider template metadata.`
|
|
12534
|
+
);
|
|
12535
|
+
return DEFAULT_CONTEXT_WINDOW;
|
|
12536
|
+
};
|
|
12537
|
+
|
|
12538
|
+
// src/exulu/active-streams.ts
|
|
12539
|
+
var active = /* @__PURE__ */ new Set();
|
|
12540
|
+
var markStreamActive = (sessionID) => {
|
|
12541
|
+
active.add(sessionID);
|
|
12542
|
+
};
|
|
12543
|
+
var clearStreamActive = (sessionID) => {
|
|
12544
|
+
active.delete(sessionID);
|
|
12545
|
+
};
|
|
12546
|
+
var isStreamActive = (sessionID) => active.has(sessionID);
|
|
12547
|
+
|
|
12548
|
+
// src/exulu/compact-session.ts
|
|
12549
|
+
import { randomUUID } from "crypto";
|
|
12550
|
+
import { generateText as generateText6, validateUIMessages as validateUIMessages2 } from "ai";
|
|
12551
|
+
var CompactionInsufficientError = class extends Error {
|
|
12552
|
+
constructor(reason) {
|
|
12553
|
+
super(JSON.stringify({ code: COMPACTION_INSUFFICIENT, message: reason }));
|
|
12554
|
+
this.name = "CompactionInsufficientError";
|
|
12555
|
+
}
|
|
12556
|
+
};
|
|
12557
|
+
var MIN_TAIL_MESSAGES = 2;
|
|
12558
|
+
var SUMMARY_TOOL_OUTPUT_SLICE = 1500;
|
|
12559
|
+
var SUMMARY_TOOL_INPUT_SLICE = 200;
|
|
12560
|
+
var SUMMARY_SYSTEM = `You compress chat histories for an AI assistant. Produce a dense, factual summary of the conversation below. Preserve:
|
|
12561
|
+
- the user's intent and any outstanding requests
|
|
12562
|
+
- key facts, decisions, and constraints
|
|
12563
|
+
- files, artifacts, and session files touched \u2014 ALWAYS keep exact file and item names so they stay retrievable
|
|
12564
|
+
- errors encountered and how they were resolved
|
|
12565
|
+
- pending tasks and the current state of the work
|
|
12566
|
+
Do not invent information. Do not include pleasantries. Write compact prose or bullet points.`;
|
|
12567
|
+
var splitTail = (messages, tailTokenBudget) => {
|
|
12568
|
+
const minTail = Math.min(MIN_TAIL_MESSAGES, messages.length);
|
|
12569
|
+
let cut = messages.length;
|
|
12570
|
+
let tokens = 0;
|
|
12571
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
12572
|
+
const t = estimateMessageTokens(messages[i]);
|
|
12573
|
+
const tailCount = messages.length - i;
|
|
12574
|
+
if (tailCount > minTail && tokens + t > tailTokenBudget) break;
|
|
12575
|
+
tokens += t;
|
|
12576
|
+
cut = i;
|
|
12577
|
+
}
|
|
12578
|
+
return { head: messages.slice(0, cut), tail: messages.slice(cut) };
|
|
12579
|
+
};
|
|
12580
|
+
var serializeForSummary = (messages) => messages.map((m) => {
|
|
12581
|
+
const parts = (m.parts ?? []).map((part) => {
|
|
12582
|
+
const p = part;
|
|
12583
|
+
if (p.type === "text") return p.text ?? "";
|
|
12584
|
+
if (p.type === "file") return `[file: ${p.filename ?? p.url ?? "attachment"}]`;
|
|
12585
|
+
if (p.type === "reasoning" || p.type === "step-start") return "";
|
|
12586
|
+
if (p.type?.startsWith("tool-") || p.type === "dynamic-tool") {
|
|
12587
|
+
const out = p.output?.value ?? p.output;
|
|
12588
|
+
const outText = typeof out === "string" ? out : JSON.stringify(out ?? "");
|
|
12589
|
+
return `[tool ${p.type}: ${JSON.stringify(p.input ?? {}).slice(0, SUMMARY_TOOL_INPUT_SLICE)}] \u2192 ${outText.slice(0, SUMMARY_TOOL_OUTPUT_SLICE)}`;
|
|
12590
|
+
}
|
|
12591
|
+
return "";
|
|
12592
|
+
}).filter(Boolean).join("\n");
|
|
12593
|
+
return `${m.role.toUpperCase()}:
|
|
12594
|
+
${parts}`;
|
|
12595
|
+
}).join("\n\n");
|
|
12596
|
+
var compactSession = async ({
|
|
12597
|
+
sessionID,
|
|
12598
|
+
user,
|
|
12599
|
+
languageModel,
|
|
12600
|
+
contextWindow,
|
|
12601
|
+
steer,
|
|
12602
|
+
modelId,
|
|
12603
|
+
summarize
|
|
12604
|
+
}) => {
|
|
12605
|
+
const budget = deriveContextBudget(contextWindow);
|
|
12606
|
+
const rows = await getAgentMessages({ session: sessionID, user: user.id });
|
|
12607
|
+
const all = await validateUIMessages2({ messages: rows.map((r) => JSON.parse(r.content)) });
|
|
12608
|
+
const history = sliceHistoryAtCheckpoint(all);
|
|
12609
|
+
const { head, tail } = splitTail(history, budget.compactionTailTokens);
|
|
12610
|
+
if (head.length === 0) {
|
|
12611
|
+
throw new CompactionInsufficientError(
|
|
12612
|
+
"There is nothing left to compact \u2014 the recent messages already form the whole context. Start a new chat instead."
|
|
12613
|
+
);
|
|
12614
|
+
}
|
|
12615
|
+
let corpus = serializeForSummary(head);
|
|
12616
|
+
const originalTokens = estimateTokens(corpus);
|
|
12617
|
+
corpus = truncateToolOutput(corpus, contextWindow, "history", 0.3, Math.floor(budget.usableWindow * 0.8) * 4);
|
|
12618
|
+
const system = steer?.trim() ? `${SUMMARY_SYSTEM}
|
|
12619
|
+
|
|
12620
|
+
Focus especially on: ${steer.trim()}` : SUMMARY_SYSTEM;
|
|
12621
|
+
const doSummarize = summarize ?? (async ({ system: sys, prompt, maxOutputTokens }) => {
|
|
12622
|
+
const { text } = await generateText6({
|
|
12623
|
+
model: languageModel,
|
|
12624
|
+
system: sys,
|
|
12625
|
+
prompt,
|
|
12626
|
+
temperature: 0,
|
|
12627
|
+
maxRetries: 2,
|
|
12628
|
+
maxOutputTokens
|
|
12629
|
+
});
|
|
12630
|
+
return text;
|
|
12631
|
+
});
|
|
12632
|
+
const summary = await doSummarize({ system, prompt: corpus, maxOutputTokens: budget.summaryBudgetTokens });
|
|
12633
|
+
const summaryTokens = estimateTokens(summary);
|
|
12634
|
+
let tailTokens = 0;
|
|
12635
|
+
for (const m of tail) tailTokens += estimateMessageTokens(m);
|
|
12636
|
+
const occupancyEstimate = summaryTokens + tailTokens;
|
|
12637
|
+
if (occupancyEstimate >= budget.blockThreshold) {
|
|
12638
|
+
throw new CompactionInsufficientError(
|
|
12639
|
+
"Compacting cannot shrink this conversation below the context limit \u2014 a recent message or output is too large by itself. Start a new chat."
|
|
12640
|
+
);
|
|
12641
|
+
}
|
|
12642
|
+
const compaction = {
|
|
12643
|
+
coversUpTo: head[head.length - 1].id,
|
|
12644
|
+
originalTokens,
|
|
12645
|
+
summaryTokens,
|
|
12646
|
+
occupancyEstimate,
|
|
12647
|
+
...steer?.trim() ? { steer: steer.trim() } : {}
|
|
12648
|
+
};
|
|
12649
|
+
const checkpoint = {
|
|
12650
|
+
id: `compaction_${randomUUID()}`,
|
|
12651
|
+
role: "user",
|
|
12652
|
+
parts: [{ type: "text", text: `[Conversation summary \u2014 earlier messages were compacted]
|
|
12653
|
+
|
|
12654
|
+
${summary}` }],
|
|
12655
|
+
metadata: { compaction }
|
|
12656
|
+
};
|
|
12657
|
+
await saveChat({ session: sessionID, user: user.id, messages: [checkpoint], ...modelId ? { model: modelId } : {} });
|
|
12658
|
+
return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
|
|
12659
|
+
};
|
|
12660
|
+
|
|
12321
12661
|
// src/exulu/transcribe.ts
|
|
12322
12662
|
var TranscriptionError = class extends Error {
|
|
12323
12663
|
constructor(upstreamStatus, message) {
|
|
@@ -12683,7 +13023,7 @@ function checkApiKeyScope(user, agentId) {
|
|
|
12683
13023
|
import "express";
|
|
12684
13024
|
import {
|
|
12685
13025
|
streamText as streamText2,
|
|
12686
|
-
generateText as
|
|
13026
|
+
generateText as generateText7,
|
|
12687
13027
|
stepCountIs as stepCountIs2,
|
|
12688
13028
|
jsonSchema
|
|
12689
13029
|
} from "ai";
|
|
@@ -12773,7 +13113,7 @@ function transformCompletion(text, inputTokens, outputTokens, ctx) {
|
|
|
12773
13113
|
}
|
|
12774
13114
|
|
|
12775
13115
|
// src/exulu/openai-gateway.ts
|
|
12776
|
-
import { randomUUID } from "crypto";
|
|
13116
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
12777
13117
|
import "crypto-js";
|
|
12778
13118
|
import express from "express";
|
|
12779
13119
|
function convertOpenAIToolsToAiSdkTools(tools) {
|
|
@@ -13087,6 +13427,10 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
|
|
|
13087
13427
|
}
|
|
13088
13428
|
const providerapikey = resolved.apiKey;
|
|
13089
13429
|
const languageModel = resolved.languageModel;
|
|
13430
|
+
const contextWindow = await resolveContextWindow({
|
|
13431
|
+
modelId: resolved.model.id,
|
|
13432
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
13433
|
+
});
|
|
13090
13434
|
const disabledTools = req.body.disabledTools ?? [];
|
|
13091
13435
|
const enabledTools = await getEnabledTools(
|
|
13092
13436
|
agent,
|
|
@@ -13111,12 +13455,50 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
|
|
|
13111
13455
|
project?.id,
|
|
13112
13456
|
void 0,
|
|
13113
13457
|
languageModel,
|
|
13114
|
-
agent
|
|
13458
|
+
agent,
|
|
13459
|
+
void 0,
|
|
13460
|
+
contextWindow,
|
|
13461
|
+
disabledTools
|
|
13462
|
+
);
|
|
13463
|
+
const gatewayAgenticEntry = enabledTools?.find((t) => t.id === "agentic_context_search");
|
|
13464
|
+
const gatewayAgenticKey = gatewayAgenticEntry ? sanitizeToolName(gatewayAgenticEntry.name) : void 0;
|
|
13465
|
+
const gatewayRetrievalGuard = retrievalBudgetGuard(
|
|
13466
|
+
resolveRetrievalCallBudget(agent.tools),
|
|
13467
|
+
gatewayAgenticKey,
|
|
13468
|
+
Object.keys(convertedTools)
|
|
13115
13469
|
);
|
|
13470
|
+
const turnBudget = resolveTurnStepBudget(void 0, agent);
|
|
13116
13471
|
const clientTools = Array.isArray(req.body.tools) ? req.body.tools : [];
|
|
13117
13472
|
const activeTools = clientTools.length > 0 ? convertOpenAIToolsToAiSdkTools(clientTools) : convertedTools;
|
|
13118
13473
|
const openaiMessages = req.body.messages ?? [];
|
|
13119
13474
|
const { systemPrompt: requestSystemPrompt, coreMessages } = convertOpenAIMessagesToModelMessages(openaiMessages);
|
|
13475
|
+
const gatewayBudget = deriveContextBudget(contextWindow);
|
|
13476
|
+
const IMAGE_TOKEN_ALLOWANCE = 1e3;
|
|
13477
|
+
let imageCount = 0;
|
|
13478
|
+
const textOnlyMessages = openaiMessages.map((m) => {
|
|
13479
|
+
if (!Array.isArray(m.content)) return m;
|
|
13480
|
+
return {
|
|
13481
|
+
...m,
|
|
13482
|
+
content: m.content.map((p) => {
|
|
13483
|
+
if (p && typeof p === "object" && "image_url" in p && p.image_url) {
|
|
13484
|
+
imageCount += 1;
|
|
13485
|
+
return { ...p, image_url: { url: "[image]" } };
|
|
13486
|
+
}
|
|
13487
|
+
return p;
|
|
13488
|
+
})
|
|
13489
|
+
};
|
|
13490
|
+
});
|
|
13491
|
+
const promptTokens = estimateTokens(JSON.stringify(textOnlyMessages)) + imageCount * IMAGE_TOKEN_ALLOWANCE;
|
|
13492
|
+
if (promptTokens >= gatewayBudget.blockThreshold) {
|
|
13493
|
+
res.status(400).json({
|
|
13494
|
+
error: {
|
|
13495
|
+
message: `This request is ~${promptTokens.toLocaleString("en-US")} tokens, which exceeds the model's usable context window (${gatewayBudget.usableWindow.toLocaleString("en-US")} tokens). Reduce the conversation history.`,
|
|
13496
|
+
type: "invalid_request_error",
|
|
13497
|
+
code: "context_length_exceeded"
|
|
13498
|
+
}
|
|
13499
|
+
});
|
|
13500
|
+
return;
|
|
13501
|
+
}
|
|
13120
13502
|
const agentInstructions = agent.instructions ?? "";
|
|
13121
13503
|
const systemParts = [
|
|
13122
13504
|
agentInstructions ? `You are an agent named: ${agent.name}
|
|
@@ -13126,7 +13508,7 @@ ${project.description}` : ""}` : "",
|
|
|
13126
13508
|
requestSystemPrompt
|
|
13127
13509
|
].filter(Boolean);
|
|
13128
13510
|
const systemPrompt = systemParts.join("\n\n");
|
|
13129
|
-
const completionId = `chatcmpl-${
|
|
13511
|
+
const completionId = `chatcmpl-${randomUUID2()}`;
|
|
13130
13512
|
const created = Math.floor(Date.now() / 1e3);
|
|
13131
13513
|
const hasTools = Object.keys(activeTools).length > 0;
|
|
13132
13514
|
const ctx = { completionId, created, modelId };
|
|
@@ -13140,7 +13522,8 @@ ${project.description}` : ""}` : "",
|
|
|
13140
13522
|
messages: coreMessages,
|
|
13141
13523
|
tools: hasTools ? activeTools : void 0,
|
|
13142
13524
|
maxRetries: 2,
|
|
13143
|
-
|
|
13525
|
+
prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
|
|
13526
|
+
stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)],
|
|
13144
13527
|
onError: (error) => {
|
|
13145
13528
|
console.error("[OPENAI GATEWAY] stream error:", error);
|
|
13146
13529
|
}
|
|
@@ -13173,13 +13556,14 @@ ${project.description}` : ""}` : "",
|
|
|
13173
13556
|
const usage = await result.usage;
|
|
13174
13557
|
await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
|
|
13175
13558
|
} else {
|
|
13176
|
-
const { text, usage } = await
|
|
13559
|
+
const { text, usage } = await generateText7({
|
|
13177
13560
|
model: languageModel,
|
|
13178
13561
|
system: systemPrompt || void 0,
|
|
13179
13562
|
messages: coreMessages,
|
|
13180
13563
|
tools: hasTools ? activeTools : void 0,
|
|
13181
13564
|
maxRetries: 2,
|
|
13182
|
-
|
|
13565
|
+
prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
|
|
13566
|
+
stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)]
|
|
13183
13567
|
});
|
|
13184
13568
|
res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
|
|
13185
13569
|
await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
|
|
@@ -13677,7 +14061,7 @@ Mood: friendly and intelligent.
|
|
|
13677
14061
|
});
|
|
13678
14062
|
return;
|
|
13679
14063
|
}
|
|
13680
|
-
const uuid =
|
|
14064
|
+
const uuid = randomUUID3();
|
|
13681
14065
|
const image_url = await uploadFile(Buffer.from(image_base64, "base64"), `${uuid}.png`, config, {
|
|
13682
14066
|
contentType: "image/png"
|
|
13683
14067
|
}, authenticationResult.user?.id, void 0, true);
|
|
@@ -13881,6 +14265,10 @@ Mood: friendly and intelligent.
|
|
|
13881
14265
|
const providerapikey = resolved.apiKey;
|
|
13882
14266
|
const resolvedLanguageModel = resolved.languageModel;
|
|
13883
14267
|
const resolvedModelId = resolved.model.id;
|
|
14268
|
+
const contextWindow = await resolveContextWindow({
|
|
14269
|
+
modelId: resolved.model.id,
|
|
14270
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
14271
|
+
});
|
|
13884
14272
|
if (!!headers.stream) {
|
|
13885
14273
|
const statistics = {
|
|
13886
14274
|
label: agent.name,
|
|
@@ -13899,27 +14287,46 @@ Mood: friendly and intelligent.
|
|
|
13899
14287
|
const instructions = customInstructions ? `${agent.instructions}
|
|
13900
14288
|
|
|
13901
14289
|
${customInstructions}` : agent.instructions;
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
|
|
13907
|
-
|
|
13908
|
-
|
|
13909
|
-
|
|
13910
|
-
|
|
13911
|
-
|
|
13912
|
-
|
|
13913
|
-
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
14290
|
+
if (headers.session) markStreamActive(headers.session);
|
|
14291
|
+
let result;
|
|
14292
|
+
try {
|
|
14293
|
+
result = await provider.generateStream({
|
|
14294
|
+
contexts,
|
|
14295
|
+
agent,
|
|
14296
|
+
user,
|
|
14297
|
+
instructions,
|
|
14298
|
+
session: headers.session,
|
|
14299
|
+
message,
|
|
14300
|
+
previousMessages,
|
|
14301
|
+
currentTools: enabledTools,
|
|
14302
|
+
currentSkills: enabledSkills,
|
|
14303
|
+
approvedTools,
|
|
14304
|
+
allExuluTools: tools,
|
|
14305
|
+
languageModel: resolvedLanguageModel,
|
|
14306
|
+
providerapikey,
|
|
14307
|
+
toolConfigs: agent.tools,
|
|
14308
|
+
exuluConfig: config,
|
|
14309
|
+
req,
|
|
14310
|
+
contextWindow,
|
|
14311
|
+
disabledTools
|
|
14312
|
+
});
|
|
14313
|
+
} catch (err) {
|
|
14314
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
14315
|
+
if (err instanceof ContextCompactionRequiredError) {
|
|
14316
|
+
res.status(413).send(err.message);
|
|
14317
|
+
return;
|
|
14318
|
+
}
|
|
14319
|
+
throw err;
|
|
14320
|
+
}
|
|
13920
14321
|
result.stream.consumeStream();
|
|
13921
14322
|
result.stream.pipeUIMessageStreamToResponse(res, {
|
|
13922
14323
|
messageMetadata: ({ part }) => {
|
|
14324
|
+
if (part.type === "finish-step") {
|
|
14325
|
+
return {
|
|
14326
|
+
lastStepInputTokens: part.usage.inputTokens,
|
|
14327
|
+
lastStepOutputTokens: part.usage.outputTokens
|
|
14328
|
+
};
|
|
14329
|
+
}
|
|
13923
14330
|
if (part.type === "finish") {
|
|
13924
14331
|
return {
|
|
13925
14332
|
totalTokens: part.totalUsage.totalTokens,
|
|
@@ -13936,22 +14343,20 @@ ${customInstructions}` : agent.instructions;
|
|
|
13936
14343
|
sendSources: true,
|
|
13937
14344
|
onError: (error) => {
|
|
13938
14345
|
console.error("[EXULU] chat response error.", error);
|
|
13939
|
-
if (
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
if (typeof error === "string")
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
return error.message;
|
|
13947
|
-
}
|
|
13948
|
-
return JSON.stringify(error);
|
|
14346
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
14347
|
+
let message2;
|
|
14348
|
+
if (error == null) message2 = "unknown error";
|
|
14349
|
+
else if (typeof error === "string") message2 = error;
|
|
14350
|
+
else if (error instanceof Error) message2 = error.message;
|
|
14351
|
+
else message2 = JSON.stringify(error);
|
|
14352
|
+
return mapStreamErrorMessage(message2);
|
|
13949
14353
|
},
|
|
13950
14354
|
generateMessageId: createIdGenerator({
|
|
13951
14355
|
prefix: "msg_",
|
|
13952
14356
|
size: 16
|
|
13953
14357
|
}),
|
|
13954
14358
|
onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
|
|
14359
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
13955
14360
|
console.log(
|
|
13956
14361
|
"[EXULU] onFinish",
|
|
13957
14362
|
messages?.map((msg) => msg.parts?.map((part) => part.type === "text" ? part.text : null)).join("\n")
|
|
@@ -14013,33 +14418,129 @@ ${customInstructions}` : agent.instructions;
|
|
|
14013
14418
|
const instructions = customInstructions ? `${agent.instructions}
|
|
14014
14419
|
|
|
14015
14420
|
${customInstructions}` : agent.instructions;
|
|
14016
|
-
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
14421
|
+
let response;
|
|
14422
|
+
try {
|
|
14423
|
+
response = await provider.generateSync({
|
|
14424
|
+
contexts,
|
|
14425
|
+
agent,
|
|
14426
|
+
user,
|
|
14427
|
+
req,
|
|
14428
|
+
instructions,
|
|
14429
|
+
session: headers.session,
|
|
14430
|
+
inputMessages: [req.body.message],
|
|
14431
|
+
currentTools: enabledTools,
|
|
14432
|
+
currentSkills: enabledSkills,
|
|
14433
|
+
allExuluTools: tools,
|
|
14434
|
+
languageModel: resolvedLanguageModel,
|
|
14435
|
+
providerapikey,
|
|
14436
|
+
exuluConfig: config,
|
|
14437
|
+
toolConfigs: agent.tools,
|
|
14438
|
+
contextWindow,
|
|
14439
|
+
disabledTools,
|
|
14440
|
+
statistics: {
|
|
14441
|
+
label: agent.name,
|
|
14442
|
+
trigger: "agent"
|
|
14443
|
+
},
|
|
14444
|
+
onTokenUsage: async ({ inputTokens, outputTokens }) => {
|
|
14445
|
+
}
|
|
14446
|
+
});
|
|
14447
|
+
} catch (err) {
|
|
14448
|
+
if (err instanceof ContextCompactionRequiredError) {
|
|
14449
|
+
res.status(413).send(err.message);
|
|
14450
|
+
return;
|
|
14036
14451
|
}
|
|
14037
|
-
|
|
14452
|
+
throw err;
|
|
14453
|
+
}
|
|
14038
14454
|
res.status(200).json(response);
|
|
14039
14455
|
return;
|
|
14040
14456
|
}
|
|
14041
14457
|
});
|
|
14042
14458
|
};
|
|
14459
|
+
const registerAgentCompactRoute = (slug) => {
|
|
14460
|
+
app.post(slug + "/:instance", async (req, res) => {
|
|
14461
|
+
const instance = req.params.instance;
|
|
14462
|
+
if (!instance) {
|
|
14463
|
+
res.status(400).json({ message: "Missing instance in request." });
|
|
14464
|
+
return;
|
|
14465
|
+
}
|
|
14466
|
+
const sessionID = req.headers["session"] || null;
|
|
14467
|
+
if (!sessionID) {
|
|
14468
|
+
res.status(400).json({ message: "Missing session header." });
|
|
14469
|
+
return;
|
|
14470
|
+
}
|
|
14471
|
+
const agent = await exuluApp.get().agent(instance);
|
|
14472
|
+
if (!agent) {
|
|
14473
|
+
res.status(404).json({ message: "Agent with id " + instance + " not found." });
|
|
14474
|
+
return;
|
|
14475
|
+
}
|
|
14476
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
14477
|
+
if (!authenticationResult.user?.id) {
|
|
14478
|
+
res.status(authenticationResult.code || 401).json({ detail: `${authenticationResult.message}` });
|
|
14479
|
+
return;
|
|
14480
|
+
}
|
|
14481
|
+
const user = authenticationResult.user;
|
|
14482
|
+
const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
|
|
14483
|
+
if (!hasAccessToAgent) {
|
|
14484
|
+
res.status(401).json({ message: "You don't have access to this agent." });
|
|
14485
|
+
return;
|
|
14486
|
+
}
|
|
14487
|
+
const { db } = await postgresClient();
|
|
14488
|
+
const sessionRow = await db.from("agent_sessions").where({ id: sessionID }).first();
|
|
14489
|
+
if (!sessionRow) {
|
|
14490
|
+
res.status(404).json({ message: "Session not found for session ID: " + sessionID });
|
|
14491
|
+
return;
|
|
14492
|
+
}
|
|
14493
|
+
const hasAccessToSession = await checkRecordAccess(sessionRow, "write", user);
|
|
14494
|
+
if (!hasAccessToSession) {
|
|
14495
|
+
res.status(401).json({ message: "You don't have access to this session." });
|
|
14496
|
+
return;
|
|
14497
|
+
}
|
|
14498
|
+
if (isStreamActive(sessionID)) {
|
|
14499
|
+
res.status(409).json({ message: "A response is still streaming for this session \u2014 try again when it finishes." });
|
|
14500
|
+
return;
|
|
14501
|
+
}
|
|
14502
|
+
const overrideModelId = req.headers["x-exulu-model-override"];
|
|
14503
|
+
const modelId = overrideModelId ?? agent.model;
|
|
14504
|
+
if (!modelId) {
|
|
14505
|
+
res.status(400).json({ message: `Agent ${agent.name} (${agent.id}) has no model configured.` });
|
|
14506
|
+
return;
|
|
14507
|
+
}
|
|
14508
|
+
let resolved;
|
|
14509
|
+
try {
|
|
14510
|
+
resolved = await resolveModel({ modelId, user, providers, agent });
|
|
14511
|
+
} catch (err) {
|
|
14512
|
+
if (err instanceof ResolveModelError) {
|
|
14513
|
+
const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
|
|
14514
|
+
res.status(status).json({ message: err.message, code: err.code });
|
|
14515
|
+
return;
|
|
14516
|
+
}
|
|
14517
|
+
throw err;
|
|
14518
|
+
}
|
|
14519
|
+
const contextWindow = await resolveContextWindow({
|
|
14520
|
+
modelId: resolved.model.id,
|
|
14521
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
14522
|
+
});
|
|
14523
|
+
const steer = typeof req.body?.steer === "string" ? req.body.steer : void 0;
|
|
14524
|
+
try {
|
|
14525
|
+
const result = await compactSession({
|
|
14526
|
+
sessionID,
|
|
14527
|
+
user,
|
|
14528
|
+
languageModel: resolved.languageModel,
|
|
14529
|
+
contextWindow,
|
|
14530
|
+
steer,
|
|
14531
|
+
modelId: resolved.model.id
|
|
14532
|
+
});
|
|
14533
|
+
res.json(result);
|
|
14534
|
+
} catch (err) {
|
|
14535
|
+
if (err instanceof CompactionInsufficientError) {
|
|
14536
|
+
res.status(422).send(err.message);
|
|
14537
|
+
return;
|
|
14538
|
+
}
|
|
14539
|
+
console.error("[EXULU] compactSession failed.", err);
|
|
14540
|
+
res.status(500).json({ message: err instanceof Error ? err.message : "Compaction failed." });
|
|
14541
|
+
}
|
|
14542
|
+
});
|
|
14543
|
+
};
|
|
14043
14544
|
providers.forEach((provider) => {
|
|
14044
14545
|
const slug = provider.slug;
|
|
14045
14546
|
if (!slug) return;
|
|
@@ -14048,6 +14549,14 @@ ${customInstructions}` : agent.instructions;
|
|
|
14048
14549
|
if (isLiteLLMEnabled() && providers.length > 0) {
|
|
14049
14550
|
registerAgentRunRoute("/agents/litellm/run", providers[0]);
|
|
14050
14551
|
}
|
|
14552
|
+
providers.forEach((provider) => {
|
|
14553
|
+
const slug = provider.slug;
|
|
14554
|
+
if (!slug) return;
|
|
14555
|
+
registerAgentCompactRoute(slug.replace(/\/run$/, "/compact"));
|
|
14556
|
+
});
|
|
14557
|
+
if (isLiteLLMEnabled() && providers.length > 0) {
|
|
14558
|
+
registerAgentCompactRoute("/agents/litellm/compact");
|
|
14559
|
+
}
|
|
14051
14560
|
app.post("/agents/suggestions/:agentId", async (req, res) => {
|
|
14052
14561
|
const agentId = req.params.agentId;
|
|
14053
14562
|
if (!agentId) {
|
|
@@ -14399,7 +14908,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
14399
14908
|
const keys = [];
|
|
14400
14909
|
const revisedPrompts = [];
|
|
14401
14910
|
for (const img of images) {
|
|
14402
|
-
const filename = `${
|
|
14911
|
+
const filename = `${randomUUID3()}.${img.extension}`;
|
|
14403
14912
|
const key = `sessions/${sessionId}/images/${toolCallId}/${filename}`;
|
|
14404
14913
|
const fullKey = await uploadFile(
|
|
14405
14914
|
img.buffer,
|
|
@@ -14687,7 +15196,7 @@ ${style.markdown}` : params.prompt;
|
|
|
14687
15196
|
(d) => `- ${d.presignedUrl} (prompt: "${d.prompt}", model: ${d.model}${d.styleName ? `, style: ${d.styleName}` : ""})`
|
|
14688
15197
|
);
|
|
14689
15198
|
const messageText = "The user generated and selected the following image(s) in this chat:\n" + lines.join("\n");
|
|
14690
|
-
const messageId =
|
|
15199
|
+
const messageId = randomUUID3();
|
|
14691
15200
|
const uiMessage = {
|
|
14692
15201
|
id: messageId,
|
|
14693
15202
|
role: "system",
|
|
@@ -15615,7 +16124,7 @@ ${style.markdown}` : params.prompt;
|
|
|
15615
16124
|
res.status(404).json({ detail: "Skill not found." });
|
|
15616
16125
|
return;
|
|
15617
16126
|
}
|
|
15618
|
-
const stagingKey = `user_${authResult.user.id}/skills/_staging/${
|
|
16127
|
+
const stagingKey = `user_${authResult.user.id}/skills/_staging/${randomUUID3()}${extension}`;
|
|
15619
16128
|
const fullKey = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/${stagingKey}` : stagingKey;
|
|
15620
16129
|
const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
|
|
15621
16130
|
res.json({ uploadUrl, stagingKey });
|
|
@@ -16646,7 +17155,7 @@ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
|
|
|
16646
17155
|
|
|
16647
17156
|
// src/mcp/index.ts
|
|
16648
17157
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16649
|
-
import { randomUUID as
|
|
17158
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
16650
17159
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
16651
17160
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
16652
17161
|
import "express";
|
|
@@ -16751,7 +17260,7 @@ var ExuluMCP = class {
|
|
|
16751
17260
|
throw new Error("Tool not found in converted tools array.");
|
|
16752
17261
|
}
|
|
16753
17262
|
const iterator = await convertedTool.execute(inputs, {
|
|
16754
|
-
toolCallId: tool2.id + "_" +
|
|
17263
|
+
toolCallId: tool2.id + "_" + randomUUID4(),
|
|
16755
17264
|
messages: []
|
|
16756
17265
|
});
|
|
16757
17266
|
let result;
|
|
@@ -16943,7 +17452,7 @@ var ExuluMCP = class {
|
|
|
16943
17452
|
transport = this.transports[sessionId];
|
|
16944
17453
|
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
16945
17454
|
transport = new StreamableHTTPServerTransport({
|
|
16946
|
-
sessionIdGenerator: () =>
|
|
17455
|
+
sessionIdGenerator: () => randomUUID4(),
|
|
16947
17456
|
onsessioninitialized: (sessionId2) => {
|
|
16948
17457
|
this.transports[sessionId2] = transport;
|
|
16949
17458
|
}
|
|
@@ -17924,7 +18433,7 @@ var ExuluEval = class {
|
|
|
17924
18433
|
|
|
17925
18434
|
// src/templates/evals/index.ts
|
|
17926
18435
|
import { z as z5 } from "zod";
|
|
17927
|
-
import { generateText as
|
|
18436
|
+
import { generateText as generateText8, Output as Output2 } from "ai";
|
|
17928
18437
|
var llmAsJudgeEval = () => {
|
|
17929
18438
|
if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
|
|
17930
18439
|
return new ExuluEval({
|
|
@@ -17969,7 +18478,7 @@ var llmAsJudgeEval = () => {
|
|
|
17969
18478
|
rbacBypass: true
|
|
17970
18479
|
});
|
|
17971
18480
|
console.log("[EXULU] prompt", prompt);
|
|
17972
|
-
const { output } = await
|
|
18481
|
+
const { output } = await generateText8({
|
|
17973
18482
|
temperature: 0,
|
|
17974
18483
|
model: resolved.languageModel,
|
|
17975
18484
|
system: "",
|
|
@@ -18392,7 +18901,7 @@ After asking a question, use the Question Read tool to check if the user has ans
|
|
|
18392
18901
|
|
|
18393
18902
|
// src/templates/tools/question/question-ask.ts
|
|
18394
18903
|
import z7 from "zod";
|
|
18395
|
-
import { randomUUID as
|
|
18904
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
18396
18905
|
var AnswerOptionSchema = z7.object({
|
|
18397
18906
|
id: z7.string().describe("Unique identifier for the answer option"),
|
|
18398
18907
|
text: z7.string().describe("The text of the answer option")
|
|
@@ -18446,15 +18955,15 @@ var QuestionAskTool = new ExuluTool({
|
|
|
18446
18955
|
throw new Error("You don't have access to this session " + session.id + ".");
|
|
18447
18956
|
}
|
|
18448
18957
|
const answerOptionsWithIds = answerOptions.map((text) => ({
|
|
18449
|
-
id:
|
|
18958
|
+
id: randomUUID5(),
|
|
18450
18959
|
text
|
|
18451
18960
|
}));
|
|
18452
18961
|
answerOptionsWithIds.push({
|
|
18453
|
-
id:
|
|
18962
|
+
id: randomUUID5(),
|
|
18454
18963
|
text: "None of the above..."
|
|
18455
18964
|
});
|
|
18456
18965
|
const newQuestion = {
|
|
18457
|
-
id:
|
|
18966
|
+
id: randomUUID5(),
|
|
18458
18967
|
question,
|
|
18459
18968
|
answerOptions: answerOptionsWithIds,
|
|
18460
18969
|
status: "pending"
|
|
@@ -21235,10 +21744,10 @@ var MarkdownChunker = class {
|
|
|
21235
21744
|
// ee/python/documents/processing/doc_processor.ts
|
|
21236
21745
|
import * as fs3 from "fs";
|
|
21237
21746
|
import * as path from "path";
|
|
21238
|
-
import { generateText as
|
|
21747
|
+
import { generateText as generateText9, Output as Output3 } from "ai";
|
|
21239
21748
|
import { z as z12 } from "zod";
|
|
21240
21749
|
import pLimit from "p-limit";
|
|
21241
|
-
import { randomUUID as
|
|
21750
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
21242
21751
|
import * as mammoth from "mammoth";
|
|
21243
21752
|
import TurndownService from "turndown";
|
|
21244
21753
|
import WordExtractor from "word-extractor";
|
|
@@ -21692,7 +22201,7 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
21692
22201
|
|
|
21693
22202
|
### 7. Only populate \`corrected_text\` when \`needs_correction\` is true. If the OCR output is accurate, return \`needs_correction: false\` and \`corrected_content: null\`.
|
|
21694
22203
|
`;
|
|
21695
|
-
const result = await
|
|
22204
|
+
const result = await generateText9({
|
|
21696
22205
|
model,
|
|
21697
22206
|
output: Output3.object({
|
|
21698
22207
|
schema: z12.object({
|
|
@@ -22113,7 +22622,7 @@ var loadFile = async (file, name, tempDir) => {
|
|
|
22113
22622
|
if (!fileType) {
|
|
22114
22623
|
throw new Error("[EXULU] File name does not include extension, extension is required for document processing.");
|
|
22115
22624
|
}
|
|
22116
|
-
const UUID =
|
|
22625
|
+
const UUID = randomUUID6();
|
|
22117
22626
|
let buffer;
|
|
22118
22627
|
if (Buffer.isBuffer(file)) {
|
|
22119
22628
|
filePath = path.join(tempDir, `${UUID}.${fileType}`);
|
|
@@ -22143,7 +22652,7 @@ async function documentProcessor({
|
|
|
22143
22652
|
if (!license["advanced-document-processing"]) {
|
|
22144
22653
|
throw new Error("Advanced document processing is an enterprise feature, please add a valid Exulu enterprise license key to use it.");
|
|
22145
22654
|
}
|
|
22146
|
-
const uuid =
|
|
22655
|
+
const uuid = randomUUID6();
|
|
22147
22656
|
const tempDir = path.join(process.cwd(), "temp", uuid);
|
|
22148
22657
|
const localFilesAndFoldersToDelete = [tempDir];
|
|
22149
22658
|
console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);
|