@exulu/backend 2.0.1 → 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 +1206 -438
- package/dist/index.d.cts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +653 -212
- 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);
|
|
@@ -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,15 +11321,26 @@ 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
|
+
}
|
|
11294
11335
|
function flattenPart(part) {
|
|
11295
11336
|
const p = part;
|
|
11296
11337
|
if (p?.type === "text") return p.text ?? "";
|
|
11297
11338
|
if (p?.type === "tool-call") {
|
|
11298
|
-
return `
|
|
11339
|
+
return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
|
|
11299
11340
|
}
|
|
11300
11341
|
if (p?.type === "tool-result") {
|
|
11301
11342
|
const out = p.output?.value ?? p.output;
|
|
11302
|
-
return `
|
|
11343
|
+
return `The "${p.toolName}" tool returned: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
|
|
11303
11344
|
}
|
|
11304
11345
|
return "";
|
|
11305
11346
|
}
|
|
@@ -11317,7 +11358,7 @@ function flattenToolHistory(messages) {
|
|
|
11317
11358
|
return m;
|
|
11318
11359
|
});
|
|
11319
11360
|
}
|
|
11320
|
-
var FINAL_ANSWER_INSTRUCTION =
|
|
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.`;
|
|
11321
11362
|
function finalAnswerGuard(maxSteps) {
|
|
11322
11363
|
return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
|
|
11323
11364
|
toolChoice: "none",
|
|
@@ -11330,6 +11371,68 @@ function finalAnswerGuard(maxSteps) {
|
|
|
11330
11371
|
} : {}
|
|
11331
11372
|
} : void 0;
|
|
11332
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
|
+
};
|
|
11435
|
+
}
|
|
11333
11436
|
|
|
11334
11437
|
// src/exulu/auto-decline-stale-approvals.ts
|
|
11335
11438
|
var AUTO_DECLINE_REASON = "Automatically declined because the user sent a new message instead of responding to the approval request.";
|
|
@@ -11593,7 +11696,9 @@ var ExuluProvider = class {
|
|
|
11593
11696
|
agent,
|
|
11594
11697
|
instructions,
|
|
11595
11698
|
maxStepCount,
|
|
11596
|
-
onTokenUsage
|
|
11699
|
+
onTokenUsage,
|
|
11700
|
+
contextWindow,
|
|
11701
|
+
disabledTools
|
|
11597
11702
|
}) => {
|
|
11598
11703
|
console.log(
|
|
11599
11704
|
"[EXULU] Called generate sync for agent: " + this.name,
|
|
@@ -11621,9 +11726,7 @@ var ExuluProvider = class {
|
|
|
11621
11726
|
if (messages && session && user) {
|
|
11622
11727
|
const previousMessages = await getAgentMessages({
|
|
11623
11728
|
session,
|
|
11624
|
-
user: user.id
|
|
11625
|
-
limit: 50,
|
|
11626
|
-
page: 1
|
|
11729
|
+
user: user.id
|
|
11627
11730
|
});
|
|
11628
11731
|
const previousMessagesContent = previousMessages.map(
|
|
11629
11732
|
(message) => JSON.parse(message.content)
|
|
@@ -11632,6 +11735,12 @@ var ExuluProvider = class {
|
|
|
11632
11735
|
// append the new message to the previous messages:
|
|
11633
11736
|
messages: [...previousMessagesContent, ...messages]
|
|
11634
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);
|
|
11635
11744
|
}
|
|
11636
11745
|
console.log(
|
|
11637
11746
|
"[EXULU] Message count for agent: " + this.name,
|
|
@@ -11696,6 +11805,34 @@ var ExuluProvider = class {
|
|
|
11696
11805
|
if (memoryContext) {
|
|
11697
11806
|
system += "\n\n" + memoryContext;
|
|
11698
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);
|
|
11699
11836
|
const includesContextSearchTool = currentTools?.some(
|
|
11700
11837
|
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
11701
11838
|
);
|
|
@@ -11708,12 +11845,12 @@ var ExuluProvider = class {
|
|
|
11708
11845
|
system += `
|
|
11709
11846
|
|
|
11710
11847
|
|
|
11711
|
-
|
|
11848
|
+
|
|
11712
11849
|
When you use a context search tool, you will include references to the items
|
|
11713
11850
|
retrieved from the tool call result inline in the response using this exact JSON format
|
|
11714
11851
|
(all on one line, no line breaks):
|
|
11715
11852
|
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
11716
|
-
|
|
11853
|
+
|
|
11717
11854
|
IMPORTANT formatting rules:
|
|
11718
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.
|
|
11719
11856
|
- Use the exact format shown above, all on ONE line
|
|
@@ -11721,9 +11858,9 @@ var ExuluProvider = class {
|
|
|
11721
11858
|
- Use the context ID from the tool result
|
|
11722
11859
|
- Include the file/item name, not the full path
|
|
11723
11860
|
- Separate multiple citations with spaces
|
|
11724
|
-
|
|
11861
|
+
|
|
11725
11862
|
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
11726
|
-
|
|
11863
|
+
|
|
11727
11864
|
The citations will be rendered as interactive badges in the UI.
|
|
11728
11865
|
`;
|
|
11729
11866
|
}
|
|
@@ -11734,12 +11871,12 @@ var ExuluProvider = class {
|
|
|
11734
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
|
|
11735
11872
|
(all on one line, no line breaks):
|
|
11736
11873
|
{url: <url>, title: <title>, snippet: <snippet>}
|
|
11737
|
-
|
|
11874
|
+
|
|
11738
11875
|
IMPORTANT formatting rules:
|
|
11739
11876
|
- Use the exact format shown above, all on ONE line
|
|
11740
11877
|
- Do NOT use quotes around field names or values
|
|
11741
11878
|
- Separate multiple results with spaces
|
|
11742
|
-
|
|
11879
|
+
|
|
11743
11880
|
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
11744
11881
|
`;
|
|
11745
11882
|
}
|
|
@@ -11772,29 +11909,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11772
11909
|
system,
|
|
11773
11910
|
prompt,
|
|
11774
11911
|
maxRetries: 2,
|
|
11775
|
-
tools
|
|
11776
|
-
currentTools,
|
|
11777
|
-
currentSkills,
|
|
11778
|
-
approvedTools,
|
|
11779
|
-
allExuluTools,
|
|
11780
|
-
toolConfigs,
|
|
11781
|
-
providerapikey,
|
|
11782
|
-
contexts,
|
|
11783
|
-
user,
|
|
11784
|
-
exuluConfig,
|
|
11785
|
-
session,
|
|
11786
|
-
req,
|
|
11787
|
-
project,
|
|
11788
|
-
sessionItems,
|
|
11789
|
-
model,
|
|
11790
|
-
agent,
|
|
11791
|
-
memoryItems
|
|
11792
|
-
),
|
|
11912
|
+
tools,
|
|
11793
11913
|
// Stop after the image_generation tool fires — the widget IS the
|
|
11794
11914
|
// assistant's response, no follow-up text turn is wanted (same
|
|
11795
11915
|
// reasoning as question_ask: the UI artifact is the message).
|
|
11796
|
-
prepareStep:
|
|
11797
|
-
stopWhen: [stepCountIs(
|
|
11916
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
11917
|
+
stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
|
|
11798
11918
|
});
|
|
11799
11919
|
console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
|
|
11800
11920
|
const {
|
|
@@ -11855,26 +11975,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11855
11975
|
ignoreIncompleteToolCalls: true
|
|
11856
11976
|
}),
|
|
11857
11977
|
maxRetries: 2,
|
|
11858
|
-
tools
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
approvedTools,
|
|
11862
|
-
allExuluTools,
|
|
11863
|
-
toolConfigs,
|
|
11864
|
-
providerapikey,
|
|
11865
|
-
contexts,
|
|
11866
|
-
user,
|
|
11867
|
-
exuluConfig,
|
|
11868
|
-
session,
|
|
11869
|
-
req,
|
|
11870
|
-
project,
|
|
11871
|
-
sessionItems,
|
|
11872
|
-
model,
|
|
11873
|
-
agent,
|
|
11874
|
-
memoryItems
|
|
11875
|
-
),
|
|
11876
|
-
prepareStep: finalAnswerGuard(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5),
|
|
11877
|
-
stopWhen: [stepCountIs(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5), hasToolCall("image_generation")]
|
|
11978
|
+
tools,
|
|
11979
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
11980
|
+
stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
|
|
11878
11981
|
});
|
|
11879
11982
|
if (statistics) {
|
|
11880
11983
|
await Promise.all([
|
|
@@ -11926,7 +12029,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11926
12029
|
* - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
|
|
11927
12030
|
* - Image files -> image parts (which ARE supported by Responses API)
|
|
11928
12031
|
*/
|
|
11929
|
-
async processFilePartsInMessages(messages) {
|
|
12032
|
+
async processFilePartsInMessages(messages, offloadCtx) {
|
|
11930
12033
|
const processedMessages = await Promise.all(
|
|
11931
12034
|
messages.map(async (message) => {
|
|
11932
12035
|
if (message.role !== "user" || !Array.isArray(message.parts)) {
|
|
@@ -11971,10 +12074,11 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11971
12074
|
outputErrorToConsole: false,
|
|
11972
12075
|
newlineDelimiter: "\n"
|
|
11973
12076
|
});
|
|
12077
|
+
const guardedText = await guardExtractedFileText(filename, String(extractedText), offloadCtx);
|
|
11974
12078
|
return {
|
|
11975
12079
|
type: "text",
|
|
11976
12080
|
text: `<file file name = "${filename}" >
|
|
11977
|
-
${
|
|
12081
|
+
${guardedText}
|
|
11978
12082
|
</file>`
|
|
11979
12083
|
};
|
|
11980
12084
|
} catch (error) {
|
|
@@ -11990,7 +12094,6 @@ ${extractedText}
|
|
|
11990
12094
|
...message,
|
|
11991
12095
|
parts: processedParts
|
|
11992
12096
|
};
|
|
11993
|
-
console.log("[EXULU] Result: " + JSON.stringify(result, null, 2));
|
|
11994
12097
|
return result;
|
|
11995
12098
|
})
|
|
11996
12099
|
);
|
|
@@ -12013,7 +12116,9 @@ ${extractedText}
|
|
|
12013
12116
|
exuluConfig,
|
|
12014
12117
|
instructions,
|
|
12015
12118
|
req,
|
|
12016
|
-
maxStepCount
|
|
12119
|
+
maxStepCount,
|
|
12120
|
+
contextWindow,
|
|
12121
|
+
disabledTools
|
|
12017
12122
|
}) => {
|
|
12018
12123
|
if (!this.config) {
|
|
12019
12124
|
console.error("[EXULU] Config is required for streaming.");
|
|
@@ -12034,9 +12139,7 @@ ${extractedText}
|
|
|
12034
12139
|
console.log("[EXULU] loading previous messages from session: " + session);
|
|
12035
12140
|
const previousMessages2 = await getAgentMessages({
|
|
12036
12141
|
session,
|
|
12037
|
-
user: user?.id
|
|
12038
|
-
limit: 50,
|
|
12039
|
-
page: 1
|
|
12142
|
+
user: user?.id
|
|
12040
12143
|
});
|
|
12041
12144
|
previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
|
|
12042
12145
|
}
|
|
@@ -12099,60 +12202,28 @@ ${extractedText}
|
|
|
12099
12202
|
if (declined.length && session && user) {
|
|
12100
12203
|
await saveChat({ session, user: user.id, messages: declined });
|
|
12101
12204
|
}
|
|
12102
|
-
messages = await this.processFilePartsInMessages(messages
|
|
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);
|
|
12103
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.";
|
|
12104
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.";
|
|
12105
12223
|
if (user?.personal_system_prompt?.trim()) {
|
|
12106
12224
|
system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
|
|
12107
12225
|
}
|
|
12108
12226
|
system += "\n\n" + genericContext;
|
|
12109
|
-
const includesContextSearchTool = currentTools?.some(
|
|
12110
|
-
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
12111
|
-
);
|
|
12112
|
-
const includesWebSearchTool = currentTools?.some(
|
|
12113
|
-
(tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
|
|
12114
|
-
);
|
|
12115
|
-
console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
|
|
12116
|
-
console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
|
|
12117
|
-
console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
|
|
12118
|
-
if (includesContextSearchTool) {
|
|
12119
|
-
system += `
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
When you use a context search tool, you will include references to the items
|
|
12124
|
-
retrieved from the tool call result inline in the response using this exact JSON format
|
|
12125
|
-
(all on one line, no line breaks):
|
|
12126
|
-
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
12127
|
-
|
|
12128
|
-
IMPORTANT formatting rules:
|
|
12129
|
-
- Use the exact format shown above, all on ONE line
|
|
12130
|
-
- Do NOT use quotes around field names or values
|
|
12131
|
-
- Use the context ID from the tool result
|
|
12132
|
-
- Include the file/item name, not the full path
|
|
12133
|
-
- Separate multiple citations with spaces
|
|
12134
|
-
|
|
12135
|
-
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
12136
|
-
|
|
12137
|
-
The citations will be rendered as interactive badges in the UI.
|
|
12138
|
-
`;
|
|
12139
|
-
}
|
|
12140
|
-
if (includesWebSearchTool) {
|
|
12141
|
-
system += `
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
When you use a web search tool, you will include references to the results of the tool call result inline in the response using this exact JSON format
|
|
12145
|
-
(all on one line, no line breaks):
|
|
12146
|
-
{url: <url>, title: <title>, snippet: <snippet>}
|
|
12147
|
-
|
|
12148
|
-
IMPORTANT formatting rules:
|
|
12149
|
-
- Use the exact format shown above, all on ONE line
|
|
12150
|
-
- Do NOT use quotes around field names or values
|
|
12151
|
-
- Separate multiple results with spaces
|
|
12152
|
-
|
|
12153
|
-
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
12154
|
-
`;
|
|
12155
|
-
}
|
|
12156
12227
|
if (currentSkills?.length) {
|
|
12157
12228
|
const skillsList = currentSkills.map((skill) => {
|
|
12158
12229
|
const description = (skill.description ?? "").trim();
|
|
@@ -12207,6 +12278,11 @@ ${skillsList}
|
|
|
12207
12278
|
read them with the readFile tool. Files you produce yourself (via writeFile or via shell
|
|
12208
12279
|
commands like \`node create_doc.js\`) live in the same place. These files are scoped to
|
|
12209
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.
|
|
12210
12286
|
`;
|
|
12211
12287
|
system += `
|
|
12212
12288
|
|
|
@@ -12230,9 +12306,66 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
12230
12306
|
sessionItems,
|
|
12231
12307
|
model,
|
|
12232
12308
|
agent,
|
|
12233
|
-
memoryItems
|
|
12309
|
+
memoryItems,
|
|
12310
|
+
contextWindow,
|
|
12311
|
+
disabledTools
|
|
12234
12312
|
);
|
|
12235
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);
|
|
12236
12369
|
const result = streamText({
|
|
12237
12370
|
temperature: 0,
|
|
12238
12371
|
// TODO Make this configurable
|
|
@@ -12257,10 +12390,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
12257
12390
|
`Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
|
|
12258
12391
|
);
|
|
12259
12392
|
},
|
|
12260
|
-
//
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
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")]
|
|
12264
12396
|
});
|
|
12265
12397
|
return {
|
|
12266
12398
|
stream: result,
|
|
@@ -12271,19 +12403,14 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
12271
12403
|
};
|
|
12272
12404
|
var getAgentMessages = async ({
|
|
12273
12405
|
session,
|
|
12274
|
-
user
|
|
12275
|
-
limit,
|
|
12276
|
-
page
|
|
12406
|
+
user
|
|
12277
12407
|
}) => {
|
|
12278
12408
|
const { db } = await postgresClient();
|
|
12279
|
-
console.log(
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
query.offset((page - 1) * limit);
|
|
12285
|
-
}
|
|
12286
|
-
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
|
+
]);
|
|
12287
12414
|
return messages;
|
|
12288
12415
|
};
|
|
12289
12416
|
var getSession = async ({ sessionID }) => {
|
|
@@ -12386,6 +12513,151 @@ ${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
|
|
|
12386
12513
|
};
|
|
12387
12514
|
};
|
|
12388
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
|
+
|
|
12389
12661
|
// src/exulu/transcribe.ts
|
|
12390
12662
|
var TranscriptionError = class extends Error {
|
|
12391
12663
|
constructor(upstreamStatus, message) {
|
|
@@ -12751,7 +13023,7 @@ function checkApiKeyScope(user, agentId) {
|
|
|
12751
13023
|
import "express";
|
|
12752
13024
|
import {
|
|
12753
13025
|
streamText as streamText2,
|
|
12754
|
-
generateText as
|
|
13026
|
+
generateText as generateText7,
|
|
12755
13027
|
stepCountIs as stepCountIs2,
|
|
12756
13028
|
jsonSchema
|
|
12757
13029
|
} from "ai";
|
|
@@ -12841,7 +13113,7 @@ function transformCompletion(text, inputTokens, outputTokens, ctx) {
|
|
|
12841
13113
|
}
|
|
12842
13114
|
|
|
12843
13115
|
// src/exulu/openai-gateway.ts
|
|
12844
|
-
import { randomUUID } from "crypto";
|
|
13116
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
12845
13117
|
import "crypto-js";
|
|
12846
13118
|
import express from "express";
|
|
12847
13119
|
function convertOpenAIToolsToAiSdkTools(tools) {
|
|
@@ -13155,6 +13427,10 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
|
|
|
13155
13427
|
}
|
|
13156
13428
|
const providerapikey = resolved.apiKey;
|
|
13157
13429
|
const languageModel = resolved.languageModel;
|
|
13430
|
+
const contextWindow = await resolveContextWindow({
|
|
13431
|
+
modelId: resolved.model.id,
|
|
13432
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
13433
|
+
});
|
|
13158
13434
|
const disabledTools = req.body.disabledTools ?? [];
|
|
13159
13435
|
const enabledTools = await getEnabledTools(
|
|
13160
13436
|
agent,
|
|
@@ -13179,12 +13455,50 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
|
|
|
13179
13455
|
project?.id,
|
|
13180
13456
|
void 0,
|
|
13181
13457
|
languageModel,
|
|
13182
|
-
agent
|
|
13458
|
+
agent,
|
|
13459
|
+
void 0,
|
|
13460
|
+
contextWindow,
|
|
13461
|
+
disabledTools
|
|
13183
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)
|
|
13469
|
+
);
|
|
13470
|
+
const turnBudget = resolveTurnStepBudget(void 0, agent);
|
|
13184
13471
|
const clientTools = Array.isArray(req.body.tools) ? req.body.tools : [];
|
|
13185
13472
|
const activeTools = clientTools.length > 0 ? convertOpenAIToolsToAiSdkTools(clientTools) : convertedTools;
|
|
13186
13473
|
const openaiMessages = req.body.messages ?? [];
|
|
13187
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
|
+
}
|
|
13188
13502
|
const agentInstructions = agent.instructions ?? "";
|
|
13189
13503
|
const systemParts = [
|
|
13190
13504
|
agentInstructions ? `You are an agent named: ${agent.name}
|
|
@@ -13194,7 +13508,7 @@ ${project.description}` : ""}` : "",
|
|
|
13194
13508
|
requestSystemPrompt
|
|
13195
13509
|
].filter(Boolean);
|
|
13196
13510
|
const systemPrompt = systemParts.join("\n\n");
|
|
13197
|
-
const completionId = `chatcmpl-${
|
|
13511
|
+
const completionId = `chatcmpl-${randomUUID2()}`;
|
|
13198
13512
|
const created = Math.floor(Date.now() / 1e3);
|
|
13199
13513
|
const hasTools = Object.keys(activeTools).length > 0;
|
|
13200
13514
|
const ctx = { completionId, created, modelId };
|
|
@@ -13208,7 +13522,8 @@ ${project.description}` : ""}` : "",
|
|
|
13208
13522
|
messages: coreMessages,
|
|
13209
13523
|
tools: hasTools ? activeTools : void 0,
|
|
13210
13524
|
maxRetries: 2,
|
|
13211
|
-
|
|
13525
|
+
prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
|
|
13526
|
+
stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)],
|
|
13212
13527
|
onError: (error) => {
|
|
13213
13528
|
console.error("[OPENAI GATEWAY] stream error:", error);
|
|
13214
13529
|
}
|
|
@@ -13241,13 +13556,14 @@ ${project.description}` : ""}` : "",
|
|
|
13241
13556
|
const usage = await result.usage;
|
|
13242
13557
|
await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
|
|
13243
13558
|
} else {
|
|
13244
|
-
const { text, usage } = await
|
|
13559
|
+
const { text, usage } = await generateText7({
|
|
13245
13560
|
model: languageModel,
|
|
13246
13561
|
system: systemPrompt || void 0,
|
|
13247
13562
|
messages: coreMessages,
|
|
13248
13563
|
tools: hasTools ? activeTools : void 0,
|
|
13249
13564
|
maxRetries: 2,
|
|
13250
|
-
|
|
13565
|
+
prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
|
|
13566
|
+
stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)]
|
|
13251
13567
|
});
|
|
13252
13568
|
res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
|
|
13253
13569
|
await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
|
|
@@ -13745,7 +14061,7 @@ Mood: friendly and intelligent.
|
|
|
13745
14061
|
});
|
|
13746
14062
|
return;
|
|
13747
14063
|
}
|
|
13748
|
-
const uuid =
|
|
14064
|
+
const uuid = randomUUID3();
|
|
13749
14065
|
const image_url = await uploadFile(Buffer.from(image_base64, "base64"), `${uuid}.png`, config, {
|
|
13750
14066
|
contentType: "image/png"
|
|
13751
14067
|
}, authenticationResult.user?.id, void 0, true);
|
|
@@ -13949,6 +14265,10 @@ Mood: friendly and intelligent.
|
|
|
13949
14265
|
const providerapikey = resolved.apiKey;
|
|
13950
14266
|
const resolvedLanguageModel = resolved.languageModel;
|
|
13951
14267
|
const resolvedModelId = resolved.model.id;
|
|
14268
|
+
const contextWindow = await resolveContextWindow({
|
|
14269
|
+
modelId: resolved.model.id,
|
|
14270
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
14271
|
+
});
|
|
13952
14272
|
if (!!headers.stream) {
|
|
13953
14273
|
const statistics = {
|
|
13954
14274
|
label: agent.name,
|
|
@@ -13967,27 +14287,46 @@ Mood: friendly and intelligent.
|
|
|
13967
14287
|
const instructions = customInstructions ? `${agent.instructions}
|
|
13968
14288
|
|
|
13969
14289
|
${customInstructions}` : agent.instructions;
|
|
13970
|
-
|
|
13971
|
-
|
|
13972
|
-
|
|
13973
|
-
|
|
13974
|
-
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
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
|
+
}
|
|
13988
14321
|
result.stream.consumeStream();
|
|
13989
14322
|
result.stream.pipeUIMessageStreamToResponse(res, {
|
|
13990
14323
|
messageMetadata: ({ part }) => {
|
|
14324
|
+
if (part.type === "finish-step") {
|
|
14325
|
+
return {
|
|
14326
|
+
lastStepInputTokens: part.usage.inputTokens,
|
|
14327
|
+
lastStepOutputTokens: part.usage.outputTokens
|
|
14328
|
+
};
|
|
14329
|
+
}
|
|
13991
14330
|
if (part.type === "finish") {
|
|
13992
14331
|
return {
|
|
13993
14332
|
totalTokens: part.totalUsage.totalTokens,
|
|
@@ -14004,22 +14343,20 @@ ${customInstructions}` : agent.instructions;
|
|
|
14004
14343
|
sendSources: true,
|
|
14005
14344
|
onError: (error) => {
|
|
14006
14345
|
console.error("[EXULU] chat response error.", error);
|
|
14007
|
-
if (
|
|
14008
|
-
|
|
14009
|
-
|
|
14010
|
-
if (typeof error === "string")
|
|
14011
|
-
|
|
14012
|
-
|
|
14013
|
-
|
|
14014
|
-
return error.message;
|
|
14015
|
-
}
|
|
14016
|
-
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);
|
|
14017
14353
|
},
|
|
14018
14354
|
generateMessageId: createIdGenerator({
|
|
14019
14355
|
prefix: "msg_",
|
|
14020
14356
|
size: 16
|
|
14021
14357
|
}),
|
|
14022
14358
|
onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
|
|
14359
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
14023
14360
|
console.log(
|
|
14024
14361
|
"[EXULU] onFinish",
|
|
14025
14362
|
messages?.map((msg) => msg.parts?.map((part) => part.type === "text" ? part.text : null)).join("\n")
|
|
@@ -14081,33 +14418,129 @@ ${customInstructions}` : agent.instructions;
|
|
|
14081
14418
|
const instructions = customInstructions ? `${agent.instructions}
|
|
14082
14419
|
|
|
14083
14420
|
${customInstructions}` : agent.instructions;
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
|
|
14099
|
-
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
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;
|
|
14104
14451
|
}
|
|
14105
|
-
|
|
14452
|
+
throw err;
|
|
14453
|
+
}
|
|
14106
14454
|
res.status(200).json(response);
|
|
14107
14455
|
return;
|
|
14108
14456
|
}
|
|
14109
14457
|
});
|
|
14110
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
|
+
};
|
|
14111
14544
|
providers.forEach((provider) => {
|
|
14112
14545
|
const slug = provider.slug;
|
|
14113
14546
|
if (!slug) return;
|
|
@@ -14116,6 +14549,14 @@ ${customInstructions}` : agent.instructions;
|
|
|
14116
14549
|
if (isLiteLLMEnabled() && providers.length > 0) {
|
|
14117
14550
|
registerAgentRunRoute("/agents/litellm/run", providers[0]);
|
|
14118
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
|
+
}
|
|
14119
14560
|
app.post("/agents/suggestions/:agentId", async (req, res) => {
|
|
14120
14561
|
const agentId = req.params.agentId;
|
|
14121
14562
|
if (!agentId) {
|
|
@@ -14467,7 +14908,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
14467
14908
|
const keys = [];
|
|
14468
14909
|
const revisedPrompts = [];
|
|
14469
14910
|
for (const img of images) {
|
|
14470
|
-
const filename = `${
|
|
14911
|
+
const filename = `${randomUUID3()}.${img.extension}`;
|
|
14471
14912
|
const key = `sessions/${sessionId}/images/${toolCallId}/${filename}`;
|
|
14472
14913
|
const fullKey = await uploadFile(
|
|
14473
14914
|
img.buffer,
|
|
@@ -14755,7 +15196,7 @@ ${style.markdown}` : params.prompt;
|
|
|
14755
15196
|
(d) => `- ${d.presignedUrl} (prompt: "${d.prompt}", model: ${d.model}${d.styleName ? `, style: ${d.styleName}` : ""})`
|
|
14756
15197
|
);
|
|
14757
15198
|
const messageText = "The user generated and selected the following image(s) in this chat:\n" + lines.join("\n");
|
|
14758
|
-
const messageId =
|
|
15199
|
+
const messageId = randomUUID3();
|
|
14759
15200
|
const uiMessage = {
|
|
14760
15201
|
id: messageId,
|
|
14761
15202
|
role: "system",
|
|
@@ -15683,7 +16124,7 @@ ${style.markdown}` : params.prompt;
|
|
|
15683
16124
|
res.status(404).json({ detail: "Skill not found." });
|
|
15684
16125
|
return;
|
|
15685
16126
|
}
|
|
15686
|
-
const stagingKey = `user_${authResult.user.id}/skills/_staging/${
|
|
16127
|
+
const stagingKey = `user_${authResult.user.id}/skills/_staging/${randomUUID3()}${extension}`;
|
|
15687
16128
|
const fullKey = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/${stagingKey}` : stagingKey;
|
|
15688
16129
|
const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
|
|
15689
16130
|
res.json({ uploadUrl, stagingKey });
|
|
@@ -16714,7 +17155,7 @@ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
|
|
|
16714
17155
|
|
|
16715
17156
|
// src/mcp/index.ts
|
|
16716
17157
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16717
|
-
import { randomUUID as
|
|
17158
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
16718
17159
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
16719
17160
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
16720
17161
|
import "express";
|
|
@@ -16819,7 +17260,7 @@ var ExuluMCP = class {
|
|
|
16819
17260
|
throw new Error("Tool not found in converted tools array.");
|
|
16820
17261
|
}
|
|
16821
17262
|
const iterator = await convertedTool.execute(inputs, {
|
|
16822
|
-
toolCallId: tool2.id + "_" +
|
|
17263
|
+
toolCallId: tool2.id + "_" + randomUUID4(),
|
|
16823
17264
|
messages: []
|
|
16824
17265
|
});
|
|
16825
17266
|
let result;
|
|
@@ -17011,7 +17452,7 @@ var ExuluMCP = class {
|
|
|
17011
17452
|
transport = this.transports[sessionId];
|
|
17012
17453
|
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
17013
17454
|
transport = new StreamableHTTPServerTransport({
|
|
17014
|
-
sessionIdGenerator: () =>
|
|
17455
|
+
sessionIdGenerator: () => randomUUID4(),
|
|
17015
17456
|
onsessioninitialized: (sessionId2) => {
|
|
17016
17457
|
this.transports[sessionId2] = transport;
|
|
17017
17458
|
}
|
|
@@ -17992,7 +18433,7 @@ var ExuluEval = class {
|
|
|
17992
18433
|
|
|
17993
18434
|
// src/templates/evals/index.ts
|
|
17994
18435
|
import { z as z5 } from "zod";
|
|
17995
|
-
import { generateText as
|
|
18436
|
+
import { generateText as generateText8, Output as Output2 } from "ai";
|
|
17996
18437
|
var llmAsJudgeEval = () => {
|
|
17997
18438
|
if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
|
|
17998
18439
|
return new ExuluEval({
|
|
@@ -18037,7 +18478,7 @@ var llmAsJudgeEval = () => {
|
|
|
18037
18478
|
rbacBypass: true
|
|
18038
18479
|
});
|
|
18039
18480
|
console.log("[EXULU] prompt", prompt);
|
|
18040
|
-
const { output } = await
|
|
18481
|
+
const { output } = await generateText8({
|
|
18041
18482
|
temperature: 0,
|
|
18042
18483
|
model: resolved.languageModel,
|
|
18043
18484
|
system: "",
|
|
@@ -18460,7 +18901,7 @@ After asking a question, use the Question Read tool to check if the user has ans
|
|
|
18460
18901
|
|
|
18461
18902
|
// src/templates/tools/question/question-ask.ts
|
|
18462
18903
|
import z7 from "zod";
|
|
18463
|
-
import { randomUUID as
|
|
18904
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
18464
18905
|
var AnswerOptionSchema = z7.object({
|
|
18465
18906
|
id: z7.string().describe("Unique identifier for the answer option"),
|
|
18466
18907
|
text: z7.string().describe("The text of the answer option")
|
|
@@ -18514,15 +18955,15 @@ var QuestionAskTool = new ExuluTool({
|
|
|
18514
18955
|
throw new Error("You don't have access to this session " + session.id + ".");
|
|
18515
18956
|
}
|
|
18516
18957
|
const answerOptionsWithIds = answerOptions.map((text) => ({
|
|
18517
|
-
id:
|
|
18958
|
+
id: randomUUID5(),
|
|
18518
18959
|
text
|
|
18519
18960
|
}));
|
|
18520
18961
|
answerOptionsWithIds.push({
|
|
18521
|
-
id:
|
|
18962
|
+
id: randomUUID5(),
|
|
18522
18963
|
text: "None of the above..."
|
|
18523
18964
|
});
|
|
18524
18965
|
const newQuestion = {
|
|
18525
|
-
id:
|
|
18966
|
+
id: randomUUID5(),
|
|
18526
18967
|
question,
|
|
18527
18968
|
answerOptions: answerOptionsWithIds,
|
|
18528
18969
|
status: "pending"
|
|
@@ -21303,10 +21744,10 @@ var MarkdownChunker = class {
|
|
|
21303
21744
|
// ee/python/documents/processing/doc_processor.ts
|
|
21304
21745
|
import * as fs3 from "fs";
|
|
21305
21746
|
import * as path from "path";
|
|
21306
|
-
import { generateText as
|
|
21747
|
+
import { generateText as generateText9, Output as Output3 } from "ai";
|
|
21307
21748
|
import { z as z12 } from "zod";
|
|
21308
21749
|
import pLimit from "p-limit";
|
|
21309
|
-
import { randomUUID as
|
|
21750
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
21310
21751
|
import * as mammoth from "mammoth";
|
|
21311
21752
|
import TurndownService from "turndown";
|
|
21312
21753
|
import WordExtractor from "word-extractor";
|
|
@@ -21760,7 +22201,7 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
21760
22201
|
|
|
21761
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\`.
|
|
21762
22203
|
`;
|
|
21763
|
-
const result = await
|
|
22204
|
+
const result = await generateText9({
|
|
21764
22205
|
model,
|
|
21765
22206
|
output: Output3.object({
|
|
21766
22207
|
schema: z12.object({
|
|
@@ -22181,7 +22622,7 @@ var loadFile = async (file, name, tempDir) => {
|
|
|
22181
22622
|
if (!fileType) {
|
|
22182
22623
|
throw new Error("[EXULU] File name does not include extension, extension is required for document processing.");
|
|
22183
22624
|
}
|
|
22184
|
-
const UUID =
|
|
22625
|
+
const UUID = randomUUID6();
|
|
22185
22626
|
let buffer;
|
|
22186
22627
|
if (Buffer.isBuffer(file)) {
|
|
22187
22628
|
filePath = path.join(tempDir, `${UUID}.${fileType}`);
|
|
@@ -22211,7 +22652,7 @@ async function documentProcessor({
|
|
|
22211
22652
|
if (!license["advanced-document-processing"]) {
|
|
22212
22653
|
throw new Error("Advanced document processing is an enterprise feature, please add a valid Exulu enterprise license key to use it.");
|
|
22213
22654
|
}
|
|
22214
|
-
const uuid =
|
|
22655
|
+
const uuid = randomUUID6();
|
|
22215
22656
|
const tempDir = path.join(process.cwd(), "temp", uuid);
|
|
22216
22657
|
const localFilesAndFoldersToDelete = [tempDir];
|
|
22217
22658
|
console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);
|