@exulu/backend 2.0.1 → 2.2.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-Y7JPNBFM.js} +603 -279
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-2PEDFZ2X.js → convert-exulu-tools-to-ai-sdk-tools-M7I2TZQQ.js} +1 -1
- package/dist/index.cjs +1689 -525
- package/dist/index.d.cts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +1108 -307
- 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.cjs
CHANGED
|
@@ -415,7 +415,12 @@ function getS3Client(config) {
|
|
|
415
415
|
credentials: {
|
|
416
416
|
accessKeyId: config.fileUploads.s3key,
|
|
417
417
|
secretAccessKey: config.fileUploads.s3secret
|
|
418
|
-
}
|
|
418
|
+
},
|
|
419
|
+
// AWS SDK >= 3.729 injects x-amz-checksum-crc32 (of an empty body) into
|
|
420
|
+
// presigned PUT URLs, which S3-compatible stores like MinIO reject on
|
|
421
|
+
// upload with a checksum mismatch. WHEN_REQUIRED disables that default.
|
|
422
|
+
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
423
|
+
responseChecksumValidation: "WHEN_REQUIRED"
|
|
419
424
|
});
|
|
420
425
|
return s3Client;
|
|
421
426
|
}
|
|
@@ -1667,6 +1672,9 @@ async function tagUpdate(input) {
|
|
|
1667
1672
|
budget_duration: input.budget_duration
|
|
1668
1673
|
});
|
|
1669
1674
|
}
|
|
1675
|
+
async function budgetUpdate(budget_id, patch) {
|
|
1676
|
+
await call("/budget/update", { budget_id, ...patch });
|
|
1677
|
+
}
|
|
1670
1678
|
async function tagDelete(name) {
|
|
1671
1679
|
await call("/tag/delete", { name });
|
|
1672
1680
|
}
|
|
@@ -1675,8 +1683,9 @@ function extractBudget(raw) {
|
|
|
1675
1683
|
const max_budget = bt.max_budget ?? raw?.max_budget ?? null;
|
|
1676
1684
|
const budget_duration = bt.budget_duration ?? raw?.budget_duration ?? null;
|
|
1677
1685
|
const budget_reset_at = bt.budget_reset_at ?? raw?.budget_reset_at ?? null;
|
|
1686
|
+
const budget_id = bt.budget_id ?? raw?.budget_id ?? null;
|
|
1678
1687
|
const spend = typeof raw?.spend === "number" ? raw.spend : typeof bt.spend === "number" ? bt.spend : 0;
|
|
1679
|
-
return { max_budget, budget_duration, budget_reset_at, spend };
|
|
1688
|
+
return { max_budget, budget_duration, budget_reset_at, budget_id, spend };
|
|
1680
1689
|
}
|
|
1681
1690
|
async function listTags() {
|
|
1682
1691
|
const { url, masterKey } = litellmBase();
|
|
@@ -1709,7 +1718,8 @@ async function listTagBudgets() {
|
|
|
1709
1718
|
spend: b.spend,
|
|
1710
1719
|
max_budget: b.max_budget,
|
|
1711
1720
|
budget_duration: b.budget_duration,
|
|
1712
|
-
budget_reset_at: b.budget_reset_at
|
|
1721
|
+
budget_reset_at: b.budget_reset_at,
|
|
1722
|
+
budget_id: b.budget_id
|
|
1713
1723
|
};
|
|
1714
1724
|
}
|
|
1715
1725
|
const names = Object.keys(map);
|
|
@@ -1755,7 +1765,8 @@ async function tagInfo(names) {
|
|
|
1755
1765
|
spend: b.spend,
|
|
1756
1766
|
max_budget: b.max_budget,
|
|
1757
1767
|
budget_duration: b.budget_duration,
|
|
1758
|
-
budget_reset_at: b.budget_reset_at
|
|
1768
|
+
budget_reset_at: b.budget_reset_at,
|
|
1769
|
+
budget_id: b.budget_id
|
|
1759
1770
|
};
|
|
1760
1771
|
}
|
|
1761
1772
|
return out;
|
|
@@ -1986,7 +1997,16 @@ async function setBudgetSettings(settings) {
|
|
|
1986
1997
|
}).onConflict("config_key").merge({ config_value: JSON.stringify(settings) });
|
|
1987
1998
|
return settings;
|
|
1988
1999
|
}
|
|
1989
|
-
|
|
2000
|
+
function parseResetAt(raw) {
|
|
2001
|
+
if (raw === void 0 || raw === null || raw === "") {
|
|
2002
|
+
return { valid: true, value: void 0 };
|
|
2003
|
+
}
|
|
2004
|
+
if (typeof raw !== "string") return { valid: false };
|
|
2005
|
+
const t = Date.parse(raw);
|
|
2006
|
+
if (Number.isNaN(t)) return { valid: false };
|
|
2007
|
+
return { valid: true, value: new Date(t).toISOString() };
|
|
2008
|
+
}
|
|
2009
|
+
async function upsertBudget(tag, max_budget, budget_duration, budget_reset_at) {
|
|
1990
2010
|
const info = await tagInfo([tag]);
|
|
1991
2011
|
try {
|
|
1992
2012
|
if (info[tag]) {
|
|
@@ -2001,6 +2021,15 @@ async function upsertBudget(tag, max_budget, budget_duration) {
|
|
|
2001
2021
|
await tagUpdate({ name: tag, max_budget, budget_duration });
|
|
2002
2022
|
}
|
|
2003
2023
|
}
|
|
2024
|
+
if (budget_reset_at) {
|
|
2025
|
+
const after = await tagInfo([tag]);
|
|
2026
|
+
const budgetId = after[tag]?.budget_id ?? null;
|
|
2027
|
+
if (budgetId) {
|
|
2028
|
+
await budgetUpdate(budgetId, { budget_reset_at });
|
|
2029
|
+
} else {
|
|
2030
|
+
console.warn(`[EXULU] upsertBudget: no budget_id for ${tag}; reset date not applied`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2004
2033
|
invalidateBudgetCaches(tag);
|
|
2005
2034
|
}
|
|
2006
2035
|
function invalidateBudgetCaches(tag) {
|
|
@@ -2875,126 +2904,14 @@ var init_wrap_execute = __esm({
|
|
|
2875
2904
|
}
|
|
2876
2905
|
});
|
|
2877
2906
|
|
|
2878
|
-
// src/templates/tools/project-retrieval-tool.ts
|
|
2879
|
-
var import_zod2, createProjectItemsRetrievalTool;
|
|
2880
|
-
var init_project_retrieval_tool = __esm({
|
|
2881
|
-
"src/templates/tools/project-retrieval-tool.ts"() {
|
|
2882
|
-
"use strict";
|
|
2883
|
-
init_cjs_shims();
|
|
2884
|
-
init_tool();
|
|
2885
|
-
import_zod2 = require("zod");
|
|
2886
|
-
init_client();
|
|
2887
|
-
createProjectItemsRetrievalTool = async ({
|
|
2888
|
-
user,
|
|
2889
|
-
role,
|
|
2890
|
-
contexts,
|
|
2891
|
-
projectId
|
|
2892
|
-
}) => {
|
|
2893
|
-
let project;
|
|
2894
|
-
const { db: db2 } = await postgresClient();
|
|
2895
|
-
project = await db2.from("projects").where("id", projectId).first();
|
|
2896
|
-
if (!project) {
|
|
2897
|
-
return;
|
|
2898
|
-
}
|
|
2899
|
-
console.log("[EXULU] Project search tool created for project", project);
|
|
2900
|
-
if (!project.project_items?.length) {
|
|
2901
|
-
return;
|
|
2902
|
-
}
|
|
2903
|
-
const projectRetrievalTool = ExuluTool.internal({
|
|
2904
|
-
id: "context_search_in_knowledge_items_added_to_project_" + projectId,
|
|
2905
|
-
name: "context_search in knowledge items added to project " + project.name,
|
|
2906
|
-
description: "This tool retrieves information about a project from conversations and items that were added to the project " + project.name + ".",
|
|
2907
|
-
inputSchema: import_zod2.z.object({
|
|
2908
|
-
query: import_zod2.z.string().describe("The query to retrieve information about the project " + project.name + "."),
|
|
2909
|
-
keywords: import_zod2.z.array(import_zod2.z.string()).describe(
|
|
2910
|
-
"The most relevant keywords in the query, such as names of people, companies, products, etc. in the project " + project.name + "."
|
|
2911
|
-
)
|
|
2912
|
-
}),
|
|
2913
|
-
type: "context",
|
|
2914
|
-
category: "project",
|
|
2915
|
-
config: [],
|
|
2916
|
-
execute: async ({ query }) => {
|
|
2917
|
-
console.log("[EXULU] Project search tool searching for project", project);
|
|
2918
|
-
const items = project.project_items;
|
|
2919
|
-
const set = {};
|
|
2920
|
-
for (const item of items) {
|
|
2921
|
-
const context = item.split("/")[0];
|
|
2922
|
-
if (!context) {
|
|
2923
|
-
throw new Error(
|
|
2924
|
-
"The item added to the project does not have a valid gid with the context id as the prefix before the first slash."
|
|
2925
|
-
);
|
|
2926
|
-
}
|
|
2927
|
-
const id = item.split("/").slice(1).join("/");
|
|
2928
|
-
if (set[context]) {
|
|
2929
|
-
set[context].push(id);
|
|
2930
|
-
} else {
|
|
2931
|
-
set[context] = [id];
|
|
2932
|
-
}
|
|
2933
|
-
}
|
|
2934
|
-
console.log("[EXULU] Project search tool searching through contexts", Object.keys(set));
|
|
2935
|
-
const results = await Promise.all(
|
|
2936
|
-
Object.keys(set).map(async (contextName) => {
|
|
2937
|
-
const context = contexts.find((context2) => context2.id === contextName);
|
|
2938
|
-
if (!context) {
|
|
2939
|
-
console.error(
|
|
2940
|
-
"[EXULU] Context not found for project information retrieval tool.",
|
|
2941
|
-
contextName
|
|
2942
|
-
);
|
|
2943
|
-
return [];
|
|
2944
|
-
}
|
|
2945
|
-
const itemIds = set[contextName];
|
|
2946
|
-
console.log("[EXULU] Project search tool searching through items", itemIds);
|
|
2947
|
-
const result = await context.search({
|
|
2948
|
-
query,
|
|
2949
|
-
itemFilters: [
|
|
2950
|
-
{
|
|
2951
|
-
id: {
|
|
2952
|
-
in: itemIds
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
],
|
|
2956
|
-
chunkFilters: [],
|
|
2957
|
-
user,
|
|
2958
|
-
role,
|
|
2959
|
-
method: "hybridSearch",
|
|
2960
|
-
sort: {
|
|
2961
|
-
field: "updatedAt",
|
|
2962
|
-
direction: "desc"
|
|
2963
|
-
},
|
|
2964
|
-
trigger: "tool",
|
|
2965
|
-
limit: 10,
|
|
2966
|
-
page: 1
|
|
2967
|
-
});
|
|
2968
|
-
return {
|
|
2969
|
-
result: result.chunks.map((chunk) => ({
|
|
2970
|
-
...chunk,
|
|
2971
|
-
context: {
|
|
2972
|
-
name: context.name,
|
|
2973
|
-
id: context.id
|
|
2974
|
-
}
|
|
2975
|
-
}))
|
|
2976
|
-
};
|
|
2977
|
-
})
|
|
2978
|
-
);
|
|
2979
|
-
console.log("[EXULU] Project search tool results", results);
|
|
2980
|
-
return {
|
|
2981
|
-
result: JSON.stringify(results.flat())
|
|
2982
|
-
};
|
|
2983
|
-
}
|
|
2984
|
-
});
|
|
2985
|
-
return projectRetrievalTool;
|
|
2986
|
-
};
|
|
2987
|
-
}
|
|
2988
|
-
});
|
|
2989
|
-
|
|
2990
2907
|
// src/templates/tools/session-items-retrieval-tool.ts
|
|
2991
|
-
var
|
|
2908
|
+
var import_zod2, createSessionItemsRetrievalTool;
|
|
2992
2909
|
var init_session_items_retrieval_tool = __esm({
|
|
2993
2910
|
"src/templates/tools/session-items-retrieval-tool.ts"() {
|
|
2994
2911
|
"use strict";
|
|
2995
2912
|
init_cjs_shims();
|
|
2996
2913
|
init_tool();
|
|
2997
|
-
|
|
2914
|
+
import_zod2 = require("zod");
|
|
2998
2915
|
createSessionItemsRetrievalTool = async ({
|
|
2999
2916
|
user,
|
|
3000
2917
|
role,
|
|
@@ -3006,8 +2923,8 @@ var init_session_items_retrieval_tool = __esm({
|
|
|
3006
2923
|
id: "session_items_information_context_search",
|
|
3007
2924
|
name: "context_search in knowledge items added to session.",
|
|
3008
2925
|
description: "Context search in knowledge items added to session.",
|
|
3009
|
-
inputSchema:
|
|
3010
|
-
query:
|
|
2926
|
+
inputSchema: import_zod2.z.object({
|
|
2927
|
+
query: import_zod2.z.string().describe("The query to retrieve information from knowledge items added to the session.")
|
|
3011
2928
|
}),
|
|
3012
2929
|
type: "context",
|
|
3013
2930
|
category: "session",
|
|
@@ -3079,6 +2996,80 @@ var init_session_items_retrieval_tool = __esm({
|
|
|
3079
2996
|
}
|
|
3080
2997
|
});
|
|
3081
2998
|
|
|
2999
|
+
// ee/agentic-retrieval/pipeline/global-ids.ts
|
|
3000
|
+
function parsePreselectedItems(globalIds) {
|
|
3001
|
+
const map = /* @__PURE__ */ new Map();
|
|
3002
|
+
for (const gid of globalIds) {
|
|
3003
|
+
const slashIdx = gid.indexOf("/");
|
|
3004
|
+
if (slashIdx === -1) {
|
|
3005
|
+
if (gid) map.set(gid, null);
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
const contextId = gid.slice(0, slashIdx);
|
|
3009
|
+
const itemId = gid.slice(slashIdx + 1);
|
|
3010
|
+
if (!contextId || !itemId) continue;
|
|
3011
|
+
if (map.get(contextId) === null) continue;
|
|
3012
|
+
const existing = map.get(contextId) ?? [];
|
|
3013
|
+
existing.push(itemId);
|
|
3014
|
+
map.set(contextId, existing);
|
|
3015
|
+
}
|
|
3016
|
+
return map;
|
|
3017
|
+
}
|
|
3018
|
+
var init_global_ids = __esm({
|
|
3019
|
+
"ee/agentic-retrieval/pipeline/global-ids.ts"() {
|
|
3020
|
+
"use strict";
|
|
3021
|
+
init_cjs_shims();
|
|
3022
|
+
}
|
|
3023
|
+
});
|
|
3024
|
+
|
|
3025
|
+
// ee/agentic-retrieval/pipeline/project-scope.ts
|
|
3026
|
+
function resolveProjectScope(opts) {
|
|
3027
|
+
const { scope, enabledContextIds, availableContextIds } = opts;
|
|
3028
|
+
if (!scope || scope.items.length === 0) return void 0;
|
|
3029
|
+
const itemsByContext = parsePreselectedItems(scope.items);
|
|
3030
|
+
const pinsByContext = /* @__PURE__ */ new Map();
|
|
3031
|
+
const scopedItemsByContext = /* @__PURE__ */ new Map();
|
|
3032
|
+
const addedContextIds = [];
|
|
3033
|
+
const allProjectContextIds = [];
|
|
3034
|
+
for (const [ctxId, itemIds] of itemsByContext) {
|
|
3035
|
+
if (!availableContextIds.has(ctxId)) {
|
|
3036
|
+
console.warn(
|
|
3037
|
+
`[EXULU pipeline] project "${scope.name}" references unknown context "${ctxId}" \u2014 skipping those items.`
|
|
3038
|
+
);
|
|
3039
|
+
continue;
|
|
3040
|
+
}
|
|
3041
|
+
allProjectContextIds.push(ctxId);
|
|
3042
|
+
if (enabledContextIds.has(ctxId)) {
|
|
3043
|
+
if (itemIds && itemIds.length > 0) pinsByContext.set(ctxId, new Set(itemIds));
|
|
3044
|
+
} else {
|
|
3045
|
+
scopedItemsByContext.set(ctxId, itemIds);
|
|
3046
|
+
addedContextIds.push(ctxId);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
if (allProjectContextIds.length === 0) return void 0;
|
|
3050
|
+
return { pinsByContext, scopedItemsByContext, addedContextIds, allProjectContextIds };
|
|
3051
|
+
}
|
|
3052
|
+
function buildProjectKbProfileDefaults(items) {
|
|
3053
|
+
const defaults = {};
|
|
3054
|
+
for (const gid of items) {
|
|
3055
|
+
const slashIdx = gid.indexOf("/");
|
|
3056
|
+
const ctxId = slashIdx === -1 ? gid : gid.slice(0, slashIdx);
|
|
3057
|
+
if (ctxId === TRANSCRIPTIONS_CONTEXT_ID && !defaults[ctxId]) {
|
|
3058
|
+
defaults[ctxId] = { enabled: true, kind: "conversations", instructions: "", overrides: {} };
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
return defaults;
|
|
3062
|
+
}
|
|
3063
|
+
var TRANSCRIPTIONS_CONTEXT_ID;
|
|
3064
|
+
var init_project_scope = __esm({
|
|
3065
|
+
"ee/agentic-retrieval/pipeline/project-scope.ts"() {
|
|
3066
|
+
"use strict";
|
|
3067
|
+
init_cjs_shims();
|
|
3068
|
+
init_global_ids();
|
|
3069
|
+
TRANSCRIPTIONS_CONTEXT_ID = "transcriptions";
|
|
3070
|
+
}
|
|
3071
|
+
});
|
|
3072
|
+
|
|
3082
3073
|
// src/utils/sanitize-tool-name.ts
|
|
3083
3074
|
function sanitizeToolName(name) {
|
|
3084
3075
|
if (typeof name !== "string") return "";
|
|
@@ -3106,19 +3097,19 @@ var init_sanitize_tool_name = __esm({
|
|
|
3106
3097
|
});
|
|
3107
3098
|
|
|
3108
3099
|
// src/templates/tools/memory-tool.ts
|
|
3109
|
-
var
|
|
3100
|
+
var import_zod3, createNewMemoryItemTool;
|
|
3110
3101
|
var init_memory_tool = __esm({
|
|
3111
3102
|
"src/templates/tools/memory-tool.ts"() {
|
|
3112
3103
|
"use strict";
|
|
3113
3104
|
init_cjs_shims();
|
|
3114
3105
|
init_tool();
|
|
3115
|
-
|
|
3106
|
+
import_zod3 = require("zod");
|
|
3116
3107
|
init_sanitize_name();
|
|
3117
3108
|
createNewMemoryItemTool = (agent, context) => {
|
|
3118
3109
|
const fields = {
|
|
3119
|
-
name:
|
|
3120
|
-
description:
|
|
3121
|
-
surroundingContext:
|
|
3110
|
+
name: import_zod3.z.string().describe("The name of the item to create"),
|
|
3111
|
+
description: import_zod3.z.string().describe("The description of the item to create"),
|
|
3112
|
+
surroundingContext: import_zod3.z.string().describe("A description of the context surrounding this memory, for example if it relates to a question a user asked, a specific product, or entity etc...")
|
|
3122
3113
|
};
|
|
3123
3114
|
for (const field of context.fields) {
|
|
3124
3115
|
switch (field.type) {
|
|
@@ -3126,47 +3117,47 @@ var init_memory_tool = __esm({
|
|
|
3126
3117
|
case "longText":
|
|
3127
3118
|
case "shortText":
|
|
3128
3119
|
case "code":
|
|
3129
|
-
fields[field.name] =
|
|
3120
|
+
fields[field.name] = import_zod3.z.string().describe("The " + field.name + " of the item to create");
|
|
3130
3121
|
break;
|
|
3131
3122
|
case "enum":
|
|
3132
3123
|
if (field.enumValues && field.enumValues.length > 0) {
|
|
3133
3124
|
const enumValues = field.enumValues;
|
|
3134
|
-
fields[field.name] =
|
|
3125
|
+
fields[field.name] = import_zod3.z.preprocess(
|
|
3135
3126
|
(v) => typeof v === "string" ? v.toUpperCase() : v,
|
|
3136
|
-
|
|
3127
|
+
import_zod3.z.enum(enumValues)
|
|
3137
3128
|
).describe(
|
|
3138
3129
|
"The " + field.name + " of the item to create. Must be one of: " + field.enumValues.join(", ")
|
|
3139
3130
|
);
|
|
3140
3131
|
} else {
|
|
3141
|
-
fields[field.name] =
|
|
3132
|
+
fields[field.name] = import_zod3.z.string().describe("The " + field.name + " of the item to create");
|
|
3142
3133
|
}
|
|
3143
3134
|
break;
|
|
3144
3135
|
case "json":
|
|
3145
|
-
fields[field.name] =
|
|
3136
|
+
fields[field.name] = import_zod3.z.string({}).describe(
|
|
3146
3137
|
"The " + field.name + " of the item to create, it should be a valid JSON string."
|
|
3147
3138
|
);
|
|
3148
3139
|
break;
|
|
3149
3140
|
case "markdown":
|
|
3150
|
-
fields[field.name] =
|
|
3141
|
+
fields[field.name] = import_zod3.z.string().describe(
|
|
3151
3142
|
"The " + field.name + " of the item to create, it should be a valid Markdown string."
|
|
3152
3143
|
);
|
|
3153
3144
|
break;
|
|
3154
3145
|
case "number":
|
|
3155
|
-
fields[field.name] =
|
|
3146
|
+
fields[field.name] = import_zod3.z.number().describe("The " + field.name + " of the item to create");
|
|
3156
3147
|
break;
|
|
3157
3148
|
case "boolean":
|
|
3158
|
-
fields[field.name] =
|
|
3149
|
+
fields[field.name] = import_zod3.z.boolean().describe("The " + field.name + " of the item to create");
|
|
3159
3150
|
break;
|
|
3160
3151
|
case "file":
|
|
3161
3152
|
case "uuid":
|
|
3162
3153
|
case "date":
|
|
3163
3154
|
break;
|
|
3164
3155
|
default:
|
|
3165
|
-
fields[field.name] =
|
|
3156
|
+
fields[field.name] = import_zod3.z.string().describe("The " + field.name + " of the item to create");
|
|
3166
3157
|
break;
|
|
3167
3158
|
}
|
|
3168
3159
|
}
|
|
3169
|
-
fields["visibility"] =
|
|
3160
|
+
fields["visibility"] = import_zod3.z.enum(["private", "public"]).optional().describe(
|
|
3170
3161
|
"Whether this memory is private to the user or shared (public). Ask the user if unknown."
|
|
3171
3162
|
);
|
|
3172
3163
|
const toolName = "create_" + sanitizeName(context.name) + "_memory_item";
|
|
@@ -3176,7 +3167,7 @@ var init_memory_tool = __esm({
|
|
|
3176
3167
|
category: agent.name + "_memory",
|
|
3177
3168
|
description: "Create a new memory item in the " + agent.name + " memory context",
|
|
3178
3169
|
type: "function",
|
|
3179
|
-
inputSchema:
|
|
3170
|
+
inputSchema: import_zod3.z.object(fields),
|
|
3180
3171
|
config: [],
|
|
3181
3172
|
execute: async (params) => {
|
|
3182
3173
|
const { name, description, surroundingContext, information, visibility, exuluConfig, user } = params;
|
|
@@ -3747,9 +3738,9 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
3747
3738
|
});
|
|
3748
3739
|
const writeFileTool = (0, import_ai2.tool)({
|
|
3749
3740
|
description: 'Write content to a file in the sandbox. Creates parent directories if needed. Paths are always resolved against the session sandbox root \u2014 both relative paths ("skills/foo.md") and leading-slash paths ("/skills/foo.md") work and reach the same file. When the path is under the session artifact tree, the file is also uploaded to S3 and a short-lived presigned URL is returned in the tool output.',
|
|
3750
|
-
inputSchema:
|
|
3751
|
-
path:
|
|
3752
|
-
content:
|
|
3741
|
+
inputSchema: import_zod4.z.object({
|
|
3742
|
+
path: import_zod4.z.string().describe("The path where the file should be written. Relative paths and leading-slash paths are both resolved against the session sandbox root."),
|
|
3743
|
+
content: import_zod4.z.string().describe("The content to write to the file")
|
|
3753
3744
|
}),
|
|
3754
3745
|
execute: async ({ path: path2, content }) => {
|
|
3755
3746
|
const resolvedPath = resolveSessionPath(path2, sessionDir);
|
|
@@ -3768,8 +3759,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
3768
3759
|
});
|
|
3769
3760
|
const readFileTool = (0, import_ai2.tool)({
|
|
3770
3761
|
description: 'Read the contents of a file from the sandbox. Paths are always resolved against the session sandbox root \u2014 both relative paths ("skills/foo.md") and leading-slash paths ("/skills/foo.md") work and reach the same file. If the file does not exist, the error message is surfaced verbatim.',
|
|
3771
|
-
inputSchema:
|
|
3772
|
-
path:
|
|
3762
|
+
inputSchema: import_zod4.z.object({
|
|
3763
|
+
path: import_zod4.z.string().describe("The path of the file to read. Relative paths and leading-slash paths are both resolved against the session sandbox root.")
|
|
3773
3764
|
}),
|
|
3774
3765
|
execute: async ({ path: path2 }) => {
|
|
3775
3766
|
const resolvedPath = resolveSessionPath(path2, sessionDir);
|
|
@@ -3780,8 +3771,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
3780
3771
|
const originalBashTool = tools.bash;
|
|
3781
3772
|
const bashTool = (0, import_ai2.tool)({
|
|
3782
3773
|
description: originalBashTool.description ?? "",
|
|
3783
|
-
inputSchema:
|
|
3784
|
-
command:
|
|
3774
|
+
inputSchema: import_zod4.z.object({
|
|
3775
|
+
command: import_zod4.z.string().describe("The bash command to execute.")
|
|
3785
3776
|
}),
|
|
3786
3777
|
execute: async (args, opts) => {
|
|
3787
3778
|
const before = persistenceEnabled ? await snapshotSessionArtifacts() : null;
|
|
@@ -3851,7 +3842,7 @@ ${lines.join("\n")}`;
|
|
|
3851
3842
|
sandboxCache.set(sessionId, { handle, installedSkills });
|
|
3852
3843
|
return handle;
|
|
3853
3844
|
}
|
|
3854
|
-
var import_sandbox_runtime, import_promises, import_node_fs5, import_node_path4, import_node_child_process3, import_node_util2, import_bash_tool, import_ai2,
|
|
3845
|
+
var import_sandbox_runtime, import_promises, import_node_fs5, import_node_path4, import_node_child_process3, import_node_util2, import_bash_tool, import_ai2, import_zod4, import_crypto_js4, getAllExuluVariables, execAsync2, EXEC_MAX_BUFFER, sandboxProbePromise, SANDBOX_FALLBACK_INSTRUCTIONS, degradedModeLogged, sandboxCache;
|
|
3855
3846
|
var init_create_sandbox = __esm({
|
|
3856
3847
|
"ee/invoke-skills/create-sandbox.ts"() {
|
|
3857
3848
|
"use strict";
|
|
@@ -3866,7 +3857,7 @@ var init_create_sandbox = __esm({
|
|
|
3866
3857
|
init_system_dependencies();
|
|
3867
3858
|
import_bash_tool = require("bash-tool");
|
|
3868
3859
|
import_ai2 = require("ai");
|
|
3869
|
-
|
|
3860
|
+
import_zod4 = require("zod");
|
|
3870
3861
|
init_variable();
|
|
3871
3862
|
import_crypto_js4 = __toESM(require("crypto-js"), 1);
|
|
3872
3863
|
init_client();
|
|
@@ -3910,9 +3901,9 @@ var init_truncate_tool_output = __esm({
|
|
|
3910
3901
|
"src/utils/truncate-tool-output.ts"() {
|
|
3911
3902
|
"use strict";
|
|
3912
3903
|
init_cjs_shims();
|
|
3913
|
-
truncateToolOutput = (output, maxContextLength, toolName, tailFraction = 0.1) => {
|
|
3904
|
+
truncateToolOutput = (output, maxContextLength, toolName, tailFraction = 0.1, charLimitOverride) => {
|
|
3914
3905
|
const effectiveCtx = maxContextLength != null && maxContextLength > 0 ? maxContextLength : 128e3;
|
|
3915
|
-
const charLimit = Math.floor(effectiveCtx * 0.25 * 4);
|
|
3906
|
+
const charLimit = charLimitOverride != null && charLimitOverride > 0 ? charLimitOverride : Math.floor(effectiveCtx * 0.25 * 4);
|
|
3916
3907
|
const clampedTail = Math.min(1, Math.max(0, tailFraction));
|
|
3917
3908
|
if (output.length <= charLimit) return output;
|
|
3918
3909
|
const headChars = Math.floor(charLimit * (1 - clampedTail));
|
|
@@ -3934,13 +3925,257 @@ var init_truncate_tool_output = __esm({
|
|
|
3934
3925
|
}
|
|
3935
3926
|
});
|
|
3936
3927
|
|
|
3928
|
+
// src/exulu/context-budget.ts
|
|
3929
|
+
var DEFAULT_CONTEXT_WINDOW, deriveContextBudget, estimateTokens, estimateMessageTokens, getCompaction, sliceHistoryAtCheckpoint, contextOccupancy, CONTEXT_COMPACTION_REQUIRED, COMPACTION_INSUFFICIENT, ContextCompactionRequiredError, PROVIDER_CONTEXT_ERROR_PATTERNS, isProviderContextLengthError, mapStreamErrorMessage;
|
|
3930
|
+
var init_context_budget = __esm({
|
|
3931
|
+
"src/exulu/context-budget.ts"() {
|
|
3932
|
+
"use strict";
|
|
3933
|
+
init_cjs_shims();
|
|
3934
|
+
DEFAULT_CONTEXT_WINDOW = 128e3;
|
|
3935
|
+
deriveContextBudget = (contextWindowInput) => {
|
|
3936
|
+
const contextWindow = contextWindowInput != null && contextWindowInput > 0 ? contextWindowInput : DEFAULT_CONTEXT_WINDOW;
|
|
3937
|
+
const outputReserve = Math.min(32e3, Math.floor(contextWindow * 0.2));
|
|
3938
|
+
const usableWindow = contextWindow - outputReserve;
|
|
3939
|
+
return {
|
|
3940
|
+
contextWindow,
|
|
3941
|
+
outputReserve,
|
|
3942
|
+
usableWindow,
|
|
3943
|
+
warnThreshold: Math.floor(usableWindow * 0.8),
|
|
3944
|
+
blockThreshold: Math.floor(usableWindow * 0.95),
|
|
3945
|
+
toolOutputCapTokens: Math.min(25e3, Math.max(4e3, Math.floor(contextWindow * 0.1))),
|
|
3946
|
+
compactionTailTokens: Math.floor(usableWindow * 0.1),
|
|
3947
|
+
summaryBudgetTokens: Math.min(8e3, Math.floor(usableWindow * 0.05))
|
|
3948
|
+
};
|
|
3949
|
+
};
|
|
3950
|
+
estimateTokens = (text) => text ? Math.ceil(text.length / 4) : 0;
|
|
3951
|
+
estimateMessageTokens = (message) => estimateTokens(JSON.stringify(message));
|
|
3952
|
+
getCompaction = (message) => message.metadata?.compaction;
|
|
3953
|
+
sliceHistoryAtCheckpoint = (messages) => {
|
|
3954
|
+
let checkpointIdx = -1;
|
|
3955
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3956
|
+
if (getCompaction(messages[i])) {
|
|
3957
|
+
checkpointIdx = i;
|
|
3958
|
+
break;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
if (checkpointIdx === -1) return messages;
|
|
3962
|
+
const checkpoint = messages[checkpointIdx];
|
|
3963
|
+
const coversUpTo = getCompaction(checkpoint).coversUpTo;
|
|
3964
|
+
const coversIdx = messages.findIndex((m) => m.id === coversUpTo);
|
|
3965
|
+
const boundary = coversIdx === -1 ? checkpointIdx : coversIdx;
|
|
3966
|
+
const after = messages.filter((m, i) => i > boundary && i !== checkpointIdx);
|
|
3967
|
+
return [checkpoint, ...after];
|
|
3968
|
+
};
|
|
3969
|
+
contextOccupancy = (messages) => {
|
|
3970
|
+
let anchorIdx = -1;
|
|
3971
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3972
|
+
const m = messages[i];
|
|
3973
|
+
const meta = m.metadata;
|
|
3974
|
+
if (getCompaction(m) || m.role === "assistant" && (typeof meta?.inputTokens === "number" || typeof meta?.lastStepInputTokens === "number")) {
|
|
3975
|
+
anchorIdx = i;
|
|
3976
|
+
break;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
let total = 0;
|
|
3980
|
+
let rest = messages;
|
|
3981
|
+
if (anchorIdx !== -1) {
|
|
3982
|
+
const anchor = messages[anchorIdx];
|
|
3983
|
+
const compaction = getCompaction(anchor);
|
|
3984
|
+
if (compaction) {
|
|
3985
|
+
total = compaction.occupancyEstimate;
|
|
3986
|
+
} else {
|
|
3987
|
+
const meta = anchor.metadata;
|
|
3988
|
+
total = typeof meta.lastStepInputTokens === "number" ? meta.lastStepInputTokens + (meta.lastStepOutputTokens ?? 0) : (meta.inputTokens ?? 0) + (meta.outputTokens ?? 0);
|
|
3989
|
+
}
|
|
3990
|
+
rest = messages.slice(anchorIdx + 1);
|
|
3991
|
+
}
|
|
3992
|
+
for (const m of rest) total += estimateMessageTokens(m);
|
|
3993
|
+
return total;
|
|
3994
|
+
};
|
|
3995
|
+
CONTEXT_COMPACTION_REQUIRED = "CONTEXT_COMPACTION_REQUIRED";
|
|
3996
|
+
COMPACTION_INSUFFICIENT = "COMPACTION_INSUFFICIENT";
|
|
3997
|
+
ContextCompactionRequiredError = class extends Error {
|
|
3998
|
+
constructor(occupancy, budget) {
|
|
3999
|
+
super(
|
|
4000
|
+
JSON.stringify({
|
|
4001
|
+
code: CONTEXT_COMPACTION_REQUIRED,
|
|
4002
|
+
message: `This conversation no longer fits the model's context window (~${occupancy.toLocaleString("en-US")} of ${budget.usableWindow.toLocaleString("en-US")} usable tokens). Compact the conversation to continue.`,
|
|
4003
|
+
occupancy,
|
|
4004
|
+
usableWindow: budget.usableWindow,
|
|
4005
|
+
contextWindow: budget.contextWindow
|
|
4006
|
+
})
|
|
4007
|
+
);
|
|
4008
|
+
this.occupancy = occupancy;
|
|
4009
|
+
this.budget = budget;
|
|
4010
|
+
this.name = "ContextCompactionRequiredError";
|
|
4011
|
+
}
|
|
4012
|
+
};
|
|
4013
|
+
PROVIDER_CONTEXT_ERROR_PATTERNS = [
|
|
4014
|
+
/ContextWindowExceededError/i,
|
|
4015
|
+
/context.?window/i,
|
|
4016
|
+
/context.?length/i,
|
|
4017
|
+
/maximum context/i,
|
|
4018
|
+
/prompt is too long/i,
|
|
4019
|
+
/input is too long/i,
|
|
4020
|
+
/token count exceeds/i,
|
|
4021
|
+
/too many tokens/i
|
|
4022
|
+
];
|
|
4023
|
+
isProviderContextLengthError = (message) => PROVIDER_CONTEXT_ERROR_PATTERNS.some((re) => re.test(message));
|
|
4024
|
+
mapStreamErrorMessage = (message) => isProviderContextLengthError(message) ? JSON.stringify({
|
|
4025
|
+
code: CONTEXT_COMPACTION_REQUIRED,
|
|
4026
|
+
message: "The model rejected the request because the conversation exceeds its context window. Compact the conversation to continue.",
|
|
4027
|
+
providerMessage: message.slice(0, 500)
|
|
4028
|
+
}) : message;
|
|
4029
|
+
}
|
|
4030
|
+
});
|
|
4031
|
+
|
|
4032
|
+
// src/exulu/tool-output-offload.ts
|
|
4033
|
+
var import_node_crypto3, PREVIEW_CHARS, storeAsSessionFile, buildNotice, guardToolOutput, guardExtractedFileText;
|
|
4034
|
+
var init_tool_output_offload = __esm({
|
|
4035
|
+
"src/exulu/tool-output-offload.ts"() {
|
|
4036
|
+
"use strict";
|
|
4037
|
+
init_cjs_shims();
|
|
4038
|
+
import_node_crypto3 = require("crypto");
|
|
4039
|
+
init_uppy();
|
|
4040
|
+
init_context_budget();
|
|
4041
|
+
PREVIEW_CHARS = 4e3;
|
|
4042
|
+
storeAsSessionFile = async (serialized, ctx) => {
|
|
4043
|
+
if (!ctx.sessionID || !ctx.exuluConfig?.fileUploads) return void 0;
|
|
4044
|
+
const safeTool = ctx.toolName.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
|
|
4045
|
+
const name = `tool-output-${safeTool}-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}.txt`;
|
|
4046
|
+
try {
|
|
4047
|
+
await uploadFile(
|
|
4048
|
+
Buffer.from(serialized, "utf-8"),
|
|
4049
|
+
`sessions/${ctx.sessionID}/${name}`,
|
|
4050
|
+
ctx.exuluConfig,
|
|
4051
|
+
{ contentType: "text/plain" },
|
|
4052
|
+
ctx.user?.id
|
|
4053
|
+
);
|
|
4054
|
+
return name;
|
|
4055
|
+
} catch (err) {
|
|
4056
|
+
console.error("[EXULU] Failed to offload oversized tool output to session files.", err);
|
|
4057
|
+
return void 0;
|
|
4058
|
+
}
|
|
4059
|
+
};
|
|
4060
|
+
buildNotice = (tokens, capTokens, sessionFile) => sessionFile ? `Tool output truncated: ~${tokens.toLocaleString("en-US")} tokens (limit ${capTokens.toLocaleString("en-US")}). The FULL output is saved as session file "${sessionFile}" \u2014 call read_session_file with { filename: "${sessionFile}", offset, limit } to read specific line ranges.` : `Tool output truncated: ~${tokens.toLocaleString("en-US")} tokens (limit ${capTokens.toLocaleString("en-US")}). The remainder was discarded \u2014 re-run the tool with narrower arguments.`;
|
|
4061
|
+
guardToolOutput = async (value, ctx) => {
|
|
4062
|
+
if (value == null) return value;
|
|
4063
|
+
let serialized;
|
|
4064
|
+
try {
|
|
4065
|
+
serialized = typeof value === "string" ? value : JSON.stringify(value);
|
|
4066
|
+
} catch {
|
|
4067
|
+
return value;
|
|
4068
|
+
}
|
|
4069
|
+
if (typeof serialized !== "string") return value;
|
|
4070
|
+
const budget = deriveContextBudget(ctx.contextWindow);
|
|
4071
|
+
const tokens = estimateTokens(serialized);
|
|
4072
|
+
if (tokens <= budget.toolOutputCapTokens) return value;
|
|
4073
|
+
const sessionFile = await storeAsSessionFile(serialized, ctx);
|
|
4074
|
+
const result = {
|
|
4075
|
+
truncated: true,
|
|
4076
|
+
notice: buildNotice(tokens, budget.toolOutputCapTokens, sessionFile),
|
|
4077
|
+
...sessionFile ? { sessionFile } : {},
|
|
4078
|
+
preview: serialized.slice(0, PREVIEW_CHARS)
|
|
4079
|
+
};
|
|
4080
|
+
return result;
|
|
4081
|
+
};
|
|
4082
|
+
guardExtractedFileText = async (filename, text, ctx) => {
|
|
4083
|
+
const budget = deriveContextBudget(ctx.contextWindow);
|
|
4084
|
+
const tokens = estimateTokens(text);
|
|
4085
|
+
if (tokens <= budget.toolOutputCapTokens) return text;
|
|
4086
|
+
const sessionFile = await storeAsSessionFile(text, { ...ctx, toolName: `upload-${filename}` });
|
|
4087
|
+
const notice = sessionFile ? `[Document "${filename}" truncated: ~${tokens.toLocaleString("en-US")} tokens. The full extracted text is saved as session file "${sessionFile}" \u2014 read specific parts with read_session_file (offset/limit).]` : `[Document "${filename}" truncated: ~${tokens.toLocaleString("en-US")} tokens \u2014 the remainder is unavailable.]`;
|
|
4088
|
+
return `${text.slice(0, PREVIEW_CHARS)}
|
|
4089
|
+
|
|
4090
|
+
${notice}`;
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
|
|
4095
|
+
// src/templates/tools/session-file-read-tool.ts
|
|
4096
|
+
var import_zod5, DEFAULT_LIMIT, MAX_CONTENT_CHARS, createSessionFileReadTool;
|
|
4097
|
+
var init_session_file_read_tool = __esm({
|
|
4098
|
+
"src/templates/tools/session-file-read-tool.ts"() {
|
|
4099
|
+
"use strict";
|
|
4100
|
+
init_cjs_shims();
|
|
4101
|
+
import_zod5 = require("zod");
|
|
4102
|
+
init_tool();
|
|
4103
|
+
init_uppy();
|
|
4104
|
+
DEFAULT_LIMIT = 250;
|
|
4105
|
+
MAX_CONTENT_CHARS = 16e3;
|
|
4106
|
+
createSessionFileReadTool = ({
|
|
4107
|
+
sessionID,
|
|
4108
|
+
user,
|
|
4109
|
+
exuluConfig
|
|
4110
|
+
}) => {
|
|
4111
|
+
if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
|
|
4112
|
+
const readSessionFileExecute = async ({ filename, offset, limit }) => {
|
|
4113
|
+
const safeName = String(filename ?? "").trim();
|
|
4114
|
+
if (!safeName || safeName.includes("..") || safeName.includes("/") || safeName.includes("\\")) {
|
|
4115
|
+
return {
|
|
4116
|
+
error: "Invalid filename \u2014 pass the bare file name exactly as listed in the session files (no paths)."
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
4119
|
+
const uploads = exuluConfig.fileUploads;
|
|
4120
|
+
const generalPrefix = uploads.s3prefix ? `${uploads.s3prefix.replace(/\/$/, "")}/` : "";
|
|
4121
|
+
const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
|
|
4122
|
+
try {
|
|
4123
|
+
const url = await getPresignedUrl(uploads.s3Bucket, key, exuluConfig);
|
|
4124
|
+
const res = await fetch(url);
|
|
4125
|
+
if (!res.ok) {
|
|
4126
|
+
return { error: `Could not read session file "${safeName}" (status ${res.status}). Check the exact file name.` };
|
|
4127
|
+
}
|
|
4128
|
+
const textBody = await res.text();
|
|
4129
|
+
const lines = textBody.split("\n");
|
|
4130
|
+
const start = (offset ?? 1) - 1;
|
|
4131
|
+
const requested = limit ?? DEFAULT_LIMIT;
|
|
4132
|
+
const sliced = lines.slice(start, start + requested);
|
|
4133
|
+
let content = sliced.join("\n");
|
|
4134
|
+
let linesReturned = sliced.length;
|
|
4135
|
+
if (content.length > MAX_CONTENT_CHARS) {
|
|
4136
|
+
content = content.slice(0, MAX_CONTENT_CHARS);
|
|
4137
|
+
linesReturned = Math.max(1, content.split("\n").length - 1);
|
|
4138
|
+
content = content + "\n[slice truncated \u2014 request fewer lines]";
|
|
4139
|
+
}
|
|
4140
|
+
return {
|
|
4141
|
+
content,
|
|
4142
|
+
totalLines: lines.length,
|
|
4143
|
+
offset: start + 1,
|
|
4144
|
+
linesReturned
|
|
4145
|
+
};
|
|
4146
|
+
} catch (err) {
|
|
4147
|
+
return { error: `Failed to read session file "${safeName}": ${err instanceof Error ? err.message : "unknown error"}` };
|
|
4148
|
+
}
|
|
4149
|
+
};
|
|
4150
|
+
return ExuluTool.internal({
|
|
4151
|
+
id: "read_session_file",
|
|
4152
|
+
name: "read_session_file",
|
|
4153
|
+
needsApproval: false,
|
|
4154
|
+
description: "Read a line range from a file stored in this session's files \u2014 including offloaded tool outputs (tool-output-*.txt) and uploaded documents. Use offset (1-based line number) and limit to page through large files instead of reading everything at once.",
|
|
4155
|
+
inputSchema: import_zod5.z.object({
|
|
4156
|
+
filename: import_zod5.z.string().describe('Exact session file name as referenced in a truncation notice, e.g. "tool-output-web_search-a1b2c3d4.txt"'),
|
|
4157
|
+
offset: import_zod5.z.number().int().min(1).optional().describe("1-based first line to read (default 1)"),
|
|
4158
|
+
limit: import_zod5.z.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT})`)
|
|
4159
|
+
}),
|
|
4160
|
+
type: "function",
|
|
4161
|
+
category: "session",
|
|
4162
|
+
config: [],
|
|
4163
|
+
// ExuluTool's execute type is modeled on retrieval tools ({result/job/items});
|
|
4164
|
+
// internal utility tools return richer shapes (memory-tool has the same
|
|
4165
|
+
// mismatch). The AI SDK passes the object through verbatim, so cast.
|
|
4166
|
+
execute: readSessionFileExecute
|
|
4167
|
+
});
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
});
|
|
4171
|
+
|
|
3937
4172
|
// src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
|
|
3938
4173
|
var convert_exulu_tools_to_ai_sdk_tools_exports = {};
|
|
3939
4174
|
__export(convert_exulu_tools_to_ai_sdk_tools_exports, {
|
|
3940
4175
|
convertExuluToolsToAiSdkTools: () => convertExuluToolsToAiSdkTools,
|
|
3941
4176
|
hydrateVariables: () => hydrateVariables
|
|
3942
4177
|
});
|
|
3943
|
-
var import_client_s32, import_crypto_js5,
|
|
4178
|
+
var import_client_s32, import_crypto_js5, import_node_crypto4, OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS, generateS3Key, s3Client2, getMimeType, hydrateVariables, convertExuluToolsToAiSdkTools;
|
|
3944
4179
|
var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
3945
4180
|
"src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts"() {
|
|
3946
4181
|
"use strict";
|
|
@@ -3950,17 +4185,21 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
3950
4185
|
init_statistics2();
|
|
3951
4186
|
init_client();
|
|
3952
4187
|
import_crypto_js5 = __toESM(require("crypto-js"), 1);
|
|
3953
|
-
init_project_retrieval_tool();
|
|
3954
4188
|
init_session_items_retrieval_tool();
|
|
3955
4189
|
init_pipeline();
|
|
4190
|
+
init_project_scope();
|
|
3956
4191
|
init_sanitize_tool_name();
|
|
3957
|
-
|
|
4192
|
+
import_node_crypto4 = require("crypto");
|
|
3958
4193
|
init_statistics();
|
|
3959
4194
|
init_memory_tool();
|
|
3960
4195
|
init_create_sandbox();
|
|
3961
4196
|
init_uppy();
|
|
3962
4197
|
init_truncate_tool_output();
|
|
3963
|
-
|
|
4198
|
+
init_tool_output_offload();
|
|
4199
|
+
init_session_file_read_tool();
|
|
4200
|
+
init_context_budget();
|
|
4201
|
+
OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
|
|
4202
|
+
generateS3Key = (filename) => `${(0, import_node_crypto4.randomUUID)()}-${filename}`;
|
|
3964
4203
|
getMimeType = (type) => {
|
|
3965
4204
|
switch (type) {
|
|
3966
4205
|
case ".png":
|
|
@@ -4057,7 +4296,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
4057
4296
|
await Promise.all(promises2);
|
|
4058
4297
|
return tool4;
|
|
4059
4298
|
};
|
|
4060
|
-
convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems) => {
|
|
4299
|
+
convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems, contextWindow, disabledTools) => {
|
|
4061
4300
|
if (!currentTools) return {};
|
|
4062
4301
|
if (!allExuluTools) {
|
|
4063
4302
|
allExuluTools = [];
|
|
@@ -4065,6 +4304,8 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
4065
4304
|
if (!contexts) {
|
|
4066
4305
|
contexts = [];
|
|
4067
4306
|
}
|
|
4307
|
+
const budget = deriveContextBudget(contextWindow);
|
|
4308
|
+
const toolOutputCharLimit = budget.toolOutputCapTokens * 4;
|
|
4068
4309
|
let sharedSessionSandbox;
|
|
4069
4310
|
if (sessionID && exuluConfig && agent?.sandbox_enabled === true) {
|
|
4070
4311
|
try {
|
|
@@ -4081,16 +4322,28 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
4081
4322
|
);
|
|
4082
4323
|
}
|
|
4083
4324
|
}
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4325
|
+
const disabled = new Set(disabledTools ?? []);
|
|
4326
|
+
let projectScope;
|
|
4327
|
+
if (project && !disabled.has("agentic_context_search")) {
|
|
4328
|
+
const { db: db2 } = await postgresClient();
|
|
4329
|
+
const projectRow = await db2.from("projects").where("id", project).first();
|
|
4330
|
+
let rawItems = projectRow?.project_items;
|
|
4331
|
+
if (typeof rawItems === "string") {
|
|
4332
|
+
try {
|
|
4333
|
+
rawItems = JSON.parse(rawItems);
|
|
4334
|
+
} catch {
|
|
4335
|
+
rawItems = void 0;
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
if (projectRow && Array.isArray(rawItems) && rawItems.length > 0) {
|
|
4339
|
+
projectScope = {
|
|
4340
|
+
id: projectRow.id,
|
|
4341
|
+
name: projectRow.name,
|
|
4342
|
+
description: projectRow.description ?? void 0,
|
|
4343
|
+
customInstructions: projectRow.custom_instructions ?? void 0,
|
|
4344
|
+
items: rawItems,
|
|
4345
|
+
kbProfileDefaults: buildProjectKbProfileDefaults(rawItems)
|
|
4346
|
+
};
|
|
4094
4347
|
}
|
|
4095
4348
|
}
|
|
4096
4349
|
if (agent?.memory && contexts?.length) {
|
|
@@ -4101,7 +4354,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
4101
4354
|
);
|
|
4102
4355
|
}
|
|
4103
4356
|
const createNewMemoryTool = createNewMemoryItemTool(agent, context);
|
|
4104
|
-
if (createNewMemoryTool) {
|
|
4357
|
+
if (createNewMemoryTool && !disabled.has(createNewMemoryTool.id)) {
|
|
4105
4358
|
if (!currentTools) {
|
|
4106
4359
|
currentTools = [];
|
|
4107
4360
|
}
|
|
@@ -4116,31 +4369,62 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
4116
4369
|
contexts,
|
|
4117
4370
|
items: sessionItems
|
|
4118
4371
|
});
|
|
4119
|
-
if (sessionItemsRetrievalTool) {
|
|
4372
|
+
if (sessionItemsRetrievalTool && !disabled.has(sessionItemsRetrievalTool.id)) {
|
|
4120
4373
|
currentTools.push(sessionItemsRetrievalTool);
|
|
4121
4374
|
}
|
|
4122
4375
|
}
|
|
4376
|
+
const sessionFileReadTool = createSessionFileReadTool({ sessionID, user, exuluConfig });
|
|
4377
|
+
if (sessionFileReadTool && !disabled.has(sessionFileReadTool.id)) {
|
|
4378
|
+
currentTools.push(sessionFileReadTool);
|
|
4379
|
+
}
|
|
4123
4380
|
console.log("[EXULU] Creating agentic search tool", contexts?.length, model);
|
|
4124
|
-
if (contexts?.length && model) {
|
|
4125
|
-
const
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4381
|
+
if (contexts?.length && model && !disabled.has("agentic_context_search")) {
|
|
4382
|
+
const index = currentTools.findIndex((tool4) => tool4.id === "agentic_context_search");
|
|
4383
|
+
const memoryContext = agent?.memory ? contexts.find((c) => c.id === agent.memory) : void 0;
|
|
4384
|
+
if (index !== -1) {
|
|
4385
|
+
const agenticSearchTool = createAgenticRetrievalTool({
|
|
4386
|
+
contexts: contexts.filter((context) => context.id !== agent?.memory),
|
|
4387
|
+
// memory is searched by the memory phase, not as a KB
|
|
4388
|
+
memoryContext,
|
|
4389
|
+
user,
|
|
4390
|
+
role: user?.role?.id,
|
|
4391
|
+
model,
|
|
4392
|
+
preselected: sessionItems,
|
|
4393
|
+
memoryItems,
|
|
4394
|
+
projectScope
|
|
4395
|
+
});
|
|
4396
|
+
if (agenticSearchTool) {
|
|
4138
4397
|
currentTools[index] = {
|
|
4139
4398
|
...currentTools[index],
|
|
4140
4399
|
// important to keep the original tool config
|
|
4141
4400
|
...agenticSearchTool
|
|
4142
4401
|
};
|
|
4143
4402
|
}
|
|
4403
|
+
} else if (projectScope) {
|
|
4404
|
+
const projectContextIds = new Set(
|
|
4405
|
+
projectScope.items.map((gid) => {
|
|
4406
|
+
const i = gid.indexOf("/");
|
|
4407
|
+
return i === -1 ? gid : gid.slice(0, i);
|
|
4408
|
+
})
|
|
4409
|
+
);
|
|
4410
|
+
const scopedContexts = contexts.filter(
|
|
4411
|
+
(c) => projectContextIds.has(c.id) && c.id !== agent?.memory
|
|
4412
|
+
);
|
|
4413
|
+
if (scopedContexts.length > 0) {
|
|
4414
|
+
const projectSearchTool = createAgenticRetrievalTool({
|
|
4415
|
+
contexts: scopedContexts,
|
|
4416
|
+
memoryContext,
|
|
4417
|
+
user,
|
|
4418
|
+
role: user?.role?.id,
|
|
4419
|
+
model,
|
|
4420
|
+
preselected: [...sessionItems ?? [], ...projectScope.items],
|
|
4421
|
+
memoryItems,
|
|
4422
|
+
projectScope
|
|
4423
|
+
});
|
|
4424
|
+
if (projectSearchTool) {
|
|
4425
|
+
currentTools.push(projectSearchTool);
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4144
4428
|
}
|
|
4145
4429
|
} else {
|
|
4146
4430
|
const agenticSearchTool = currentTools.find((tool4) => tool4.id === "agentic_context_search");
|
|
@@ -4168,7 +4452,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
4168
4452
|
if (typeof result?.content === "string") {
|
|
4169
4453
|
return {
|
|
4170
4454
|
...result,
|
|
4171
|
-
content: truncateToolOutput(result.content,
|
|
4455
|
+
content: truncateToolOutput(result.content, budget.contextWindow, "readFile", 0.05, toolOutputCharLimit)
|
|
4172
4456
|
};
|
|
4173
4457
|
}
|
|
4174
4458
|
return result;
|
|
@@ -4185,10 +4469,10 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
4185
4469
|
return {
|
|
4186
4470
|
...result,
|
|
4187
4471
|
...typeof result?.stdout === "string" && {
|
|
4188
|
-
stdout: truncateToolOutput(result.stdout,
|
|
4472
|
+
stdout: truncateToolOutput(result.stdout, budget.contextWindow, "bash", 0.1, toolOutputCharLimit)
|
|
4189
4473
|
},
|
|
4190
4474
|
...typeof result?.stderr === "string" && {
|
|
4191
|
-
stderr: truncateToolOutput(result.stderr,
|
|
4475
|
+
stderr: truncateToolOutput(result.stderr, budget.contextWindow, "bash stderr", 0.4, toolOutputCharLimit)
|
|
4192
4476
|
}
|
|
4193
4477
|
};
|
|
4194
4478
|
}
|
|
@@ -4324,16 +4608,30 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
4324
4608
|
user: user?.id,
|
|
4325
4609
|
role: user?.role?.id
|
|
4326
4610
|
});
|
|
4611
|
+
const guardCtx = {
|
|
4612
|
+
toolName: cur.name,
|
|
4613
|
+
contextWindow,
|
|
4614
|
+
sessionID,
|
|
4615
|
+
user,
|
|
4616
|
+
exuluConfig
|
|
4617
|
+
};
|
|
4618
|
+
const offloadExempt = OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS.has(cur.id);
|
|
4327
4619
|
if (response && typeof response === "object" && Symbol.asyncIterator in response) {
|
|
4328
4620
|
let lastValue;
|
|
4329
4621
|
for await (const value of response) {
|
|
4330
4622
|
yield value;
|
|
4331
4623
|
lastValue = value;
|
|
4332
4624
|
}
|
|
4333
|
-
return lastValue;
|
|
4625
|
+
if (offloadExempt) return lastValue;
|
|
4626
|
+
const guarded = await guardToolOutput(lastValue, guardCtx);
|
|
4627
|
+
if (guarded !== lastValue) {
|
|
4628
|
+
yield guarded;
|
|
4629
|
+
}
|
|
4630
|
+
return guarded;
|
|
4334
4631
|
} else {
|
|
4335
|
-
|
|
4336
|
-
|
|
4632
|
+
const guarded = offloadExempt ? response : await guardToolOutput(response, guardCtx);
|
|
4633
|
+
yield guarded;
|
|
4634
|
+
return guarded;
|
|
4337
4635
|
}
|
|
4338
4636
|
}
|
|
4339
4637
|
}
|
|
@@ -4346,7 +4644,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
4346
4644
|
});
|
|
4347
4645
|
|
|
4348
4646
|
// src/exulu/tool.ts
|
|
4349
|
-
var import_ai3, import_zod6,
|
|
4647
|
+
var import_ai3, import_zod6, import_node_crypto5, PUBLIC_TOOL_TYPES, ExuluTool;
|
|
4350
4648
|
var init_tool = __esm({
|
|
4351
4649
|
"src/exulu/tool.ts"() {
|
|
4352
4650
|
"use strict";
|
|
@@ -4354,7 +4652,7 @@ var init_tool = __esm({
|
|
|
4354
4652
|
import_ai3 = require("ai");
|
|
4355
4653
|
import_zod6 = require("zod");
|
|
4356
4654
|
init_sanitize_name();
|
|
4357
|
-
|
|
4655
|
+
import_node_crypto5 = require("crypto");
|
|
4358
4656
|
init_singleton();
|
|
4359
4657
|
init_resolve_model();
|
|
4360
4658
|
init_validate();
|
|
@@ -4481,7 +4779,7 @@ var init_tool = __esm({
|
|
|
4481
4779
|
throw new Error("Tool " + sanitizeName(this.name) + " not found in " + JSON.stringify(tools));
|
|
4482
4780
|
}
|
|
4483
4781
|
console.log("[EXULU] Tool found", this.name);
|
|
4484
|
-
const toolCallId = this.id + "_" + (0,
|
|
4782
|
+
const toolCallId = this.id + "_" + (0, import_node_crypto5.randomUUID)();
|
|
4485
4783
|
console.log("[EXULU] Calling tool execute", {
|
|
4486
4784
|
inputs,
|
|
4487
4785
|
toolCallId,
|
|
@@ -4675,6 +4973,7 @@ function parsePipelineConfig(raw) {
|
|
|
4675
4973
|
managedContext: boolVal(r["managed_context"]),
|
|
4676
4974
|
requirePreselectedContexts: boolVal(r["require_preselected_contexts"]),
|
|
4677
4975
|
logging: boolVal(r["logging"]),
|
|
4976
|
+
projectSearch: r["project_search"] === void 0 || r["project_search"] === "" ? true : boolVal(r["project_search"]),
|
|
4678
4977
|
utilityModel: strVal(r["utility_model"], ""),
|
|
4679
4978
|
knowledgeBases: jsonVal("knowledge_bases", knowledgeBasesSchema, r["knowledge_bases"]),
|
|
4680
4979
|
routing: jsonVal("routing", routingSchema, r["routing"]),
|
|
@@ -6002,6 +6301,7 @@ async function searchContexts(opts) {
|
|
|
6002
6301
|
role,
|
|
6003
6302
|
model,
|
|
6004
6303
|
preselectedItems,
|
|
6304
|
+
scopedItemsByContext,
|
|
6005
6305
|
identifierPinsByContext,
|
|
6006
6306
|
memoryPinnedItemIds,
|
|
6007
6307
|
userPinnedItemIdsByContext,
|
|
@@ -6028,6 +6328,8 @@ async function searchContexts(opts) {
|
|
|
6028
6328
|
let pinnedItemIds;
|
|
6029
6329
|
if (hasPreselection) {
|
|
6030
6330
|
pinnedItemIds = preselectedItems.get(ctxId) ?? [];
|
|
6331
|
+
} else if (scopedItemsByContext?.has(ctxId)) {
|
|
6332
|
+
pinnedItemIds = scopedItemsByContext.get(ctxId) ?? [];
|
|
6031
6333
|
} else if (!skipPrefilter) {
|
|
6032
6334
|
const identifierPins = identifierPinsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
|
|
6033
6335
|
let pins = new Set(identifierPins);
|
|
@@ -6256,30 +6558,12 @@ var init_rerank = __esm({
|
|
|
6256
6558
|
});
|
|
6257
6559
|
|
|
6258
6560
|
// ee/agentic-retrieval/pipeline/index.ts
|
|
6259
|
-
function
|
|
6260
|
-
const
|
|
6261
|
-
for (const
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
continue;
|
|
6266
|
-
}
|
|
6267
|
-
const contextId = gid.slice(0, slashIdx);
|
|
6268
|
-
const itemId = gid.slice(slashIdx + 1);
|
|
6269
|
-
if (!contextId || !itemId) continue;
|
|
6270
|
-
if (map.get(contextId) === null) continue;
|
|
6271
|
-
const existing = map.get(contextId) ?? [];
|
|
6272
|
-
existing.push(itemId);
|
|
6273
|
-
map.set(contextId, existing);
|
|
6274
|
-
}
|
|
6275
|
-
return map;
|
|
6276
|
-
}
|
|
6277
|
-
function addChunks(result, chunks) {
|
|
6278
|
-
const seen = new Set(result.chunks.map((c) => c.chunk_id));
|
|
6279
|
-
for (const chunk of chunks) {
|
|
6280
|
-
if (!seen.has(chunk.chunk_id)) {
|
|
6281
|
-
seen.add(chunk.chunk_id);
|
|
6282
|
-
result.chunks.push(chunk);
|
|
6561
|
+
function addChunks(result, chunks) {
|
|
6562
|
+
const seen = new Set(result.chunks.map((c) => c.chunk_id));
|
|
6563
|
+
for (const chunk of chunks) {
|
|
6564
|
+
if (!seen.has(chunk.chunk_id)) {
|
|
6565
|
+
seen.add(chunk.chunk_id);
|
|
6566
|
+
result.chunks.push(chunk);
|
|
6283
6567
|
}
|
|
6284
6568
|
}
|
|
6285
6569
|
}
|
|
@@ -6303,7 +6587,8 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6303
6587
|
model,
|
|
6304
6588
|
instructions: adminInstructions,
|
|
6305
6589
|
preselected,
|
|
6306
|
-
memoryItems
|
|
6590
|
+
memoryItems,
|
|
6591
|
+
projectScope
|
|
6307
6592
|
} = opts;
|
|
6308
6593
|
const license = checkLicense();
|
|
6309
6594
|
if (!license["agentic-retrieval"]) {
|
|
@@ -6313,7 +6598,9 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6313
6598
|
return ExuluTool.internal({
|
|
6314
6599
|
id: "agentic_context_search",
|
|
6315
6600
|
name: "Context Search",
|
|
6316
|
-
description: `Intelligent knowledge search across the available knowledge bases: ${contexts.map((c) => c.name || c.id).join(", ")}. Routes the question to the right sources, searches them with query expansion, and returns reranked passages. Results are exhaustive for the given query: do NOT repeat the call with a rephrased version of the same question \u2014 re-call only with genuinely new information (a different product or model, an explicitly named source or document, or new details from the user)
|
|
6601
|
+
description: `Intelligent knowledge search across the available knowledge bases: ${contexts.map((c) => c.name || c.id).join(", ")}. Routes the question to the right sources, searches them with query expansion, and returns reranked passages. Results are exhaustive for the given query: do NOT repeat the call with a rephrased version of the same question \u2014 re-call only with genuinely new information (a different product or model, an explicitly named source or document, or new details from the user).` + // Note: the description suffix intentionally remains even when the per-agent project_search
|
|
6602
|
+
// config is off — the config is only known at execute time, not at factory time.
|
|
6603
|
+
(projectScope ? ` Also searches the knowledge items attached to the project "${projectScope.name}".` : ""),
|
|
6317
6604
|
category: "contexts",
|
|
6318
6605
|
needsApproval: false,
|
|
6319
6606
|
type: "context",
|
|
@@ -6356,10 +6643,16 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6356
6643
|
},
|
|
6357
6644
|
{
|
|
6358
6645
|
name: "max_steps",
|
|
6359
|
-
description: "Maximum
|
|
6646
|
+
description: "Maximum knowledge searches the agent may run for one message. Once spent, the search tool is disabled for the rest of the turn. 0 = no search-specific cap (the agent's overall tool-step budget still applies).",
|
|
6360
6647
|
type: "number",
|
|
6361
6648
|
default: 0
|
|
6362
6649
|
},
|
|
6650
|
+
{
|
|
6651
|
+
name: "project_search",
|
|
6652
|
+
description: "Automatically include items attached to the chat's project as an additional knowledge source (boosts them in shared sources, adds scoped search for others).",
|
|
6653
|
+
type: "boolean",
|
|
6654
|
+
default: true
|
|
6655
|
+
},
|
|
6363
6656
|
{
|
|
6364
6657
|
name: "knowledge_bases",
|
|
6365
6658
|
description: "Per-knowledge-base profiles: enabled, kind (documents | conversations | records), instructions, and per-KB overrides (limit, expand, multiQuery, hyde). JSON object keyed by context id.",
|
|
@@ -6469,6 +6762,23 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6469
6762
|
}
|
|
6470
6763
|
}
|
|
6471
6764
|
const preselectedItems = parsePreselectedItems(preselected ?? []);
|
|
6765
|
+
const availableContextsById = new Map(contexts.map((c) => [c.id, c]));
|
|
6766
|
+
const resolvedProject = cfg.projectSearch ? resolveProjectScope({
|
|
6767
|
+
scope: projectScope,
|
|
6768
|
+
enabledContextIds: new Set(enabledContexts.map((c) => c.id)),
|
|
6769
|
+
availableContextIds: new Set(availableContextsById.keys())
|
|
6770
|
+
}) : void 0;
|
|
6771
|
+
if (resolvedProject) {
|
|
6772
|
+
if (projectScope?.kbProfileDefaults) {
|
|
6773
|
+
for (const [ctxId, profile] of Object.entries(projectScope.kbProfileDefaults)) {
|
|
6774
|
+
if (!cfg.knowledgeBases[ctxId]) cfg.knowledgeBases[ctxId] = profile;
|
|
6775
|
+
}
|
|
6776
|
+
}
|
|
6777
|
+
enabledContexts = [
|
|
6778
|
+
...enabledContexts,
|
|
6779
|
+
...resolvedProject.addedContextIds.map((id) => availableContextsById.get(id)).filter((c) => Boolean(c))
|
|
6780
|
+
];
|
|
6781
|
+
}
|
|
6472
6782
|
const contextsById = new Map(enabledContexts.map((c) => [c.id, c]));
|
|
6473
6783
|
const kbKindById = new Map(
|
|
6474
6784
|
enabledContexts.map((c) => [
|
|
@@ -6479,7 +6789,12 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6479
6789
|
const documentContexts = enabledContexts.filter(
|
|
6480
6790
|
(c) => (cfg.knowledgeBases[c.id]?.kind ?? "documents") === "documents"
|
|
6481
6791
|
);
|
|
6482
|
-
const extraInstructions = [
|
|
6792
|
+
const extraInstructions = [
|
|
6793
|
+
cfg.instructions,
|
|
6794
|
+
adminInstructions,
|
|
6795
|
+
resolvedProject && projectScope?.customInstructions ? `Instructions for the attached project "${projectScope.name}":
|
|
6796
|
+
${projectScope.customInstructions}` : ""
|
|
6797
|
+
].filter(Boolean).join("\n");
|
|
6483
6798
|
const [memResult, routResult] = await Promise.all([
|
|
6484
6799
|
runMemoryPhase({
|
|
6485
6800
|
memoryChunks: memoryItems ?? [],
|
|
@@ -6525,6 +6840,30 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6525
6840
|
yield { result: "The user has requested to search in knowledge bases that are not part of the preselected knowledge bases: " + missing.join(", ") };
|
|
6526
6841
|
return;
|
|
6527
6842
|
}
|
|
6843
|
+
let effectiveMainContexts = mainContexts;
|
|
6844
|
+
if (resolvedProject) {
|
|
6845
|
+
const mainSet = new Set(mainContexts);
|
|
6846
|
+
const appended = resolvedProject.allProjectContextIds.filter(
|
|
6847
|
+
(id) => !mainSet.has(id) && contextsById.has(id)
|
|
6848
|
+
);
|
|
6849
|
+
if (appended.length > 0) {
|
|
6850
|
+
effectiveMainContexts = [...mainContexts, ...appended];
|
|
6851
|
+
result.steps.push({
|
|
6852
|
+
stepNumber: 1,
|
|
6853
|
+
text: `Including sources from project "${projectScope.name}": ${appended.join(", ")}`,
|
|
6854
|
+
toolCalls: [],
|
|
6855
|
+
chunks: [],
|
|
6856
|
+
tokens: 0
|
|
6857
|
+
});
|
|
6858
|
+
result.reasoning.push({
|
|
6859
|
+
text: `Including project sources: ${appended.join(", ")}`,
|
|
6860
|
+
tools: []
|
|
6861
|
+
});
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
const fallbackContextsToSearch = fallbackContexts.filter(
|
|
6865
|
+
(id) => !effectiveMainContexts.includes(id)
|
|
6866
|
+
);
|
|
6528
6867
|
const {
|
|
6529
6868
|
updatedQuestion,
|
|
6530
6869
|
updatedKeywords,
|
|
@@ -6551,7 +6890,7 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6551
6890
|
}
|
|
6552
6891
|
const [mainSearch, speculativeFallbackSearch] = await Promise.all([
|
|
6553
6892
|
searchContexts({
|
|
6554
|
-
contextIds:
|
|
6893
|
+
contextIds: effectiveMainContexts,
|
|
6555
6894
|
contextsById,
|
|
6556
6895
|
kbProfiles: cfg.knowledgeBases,
|
|
6557
6896
|
question: updatedQuestion,
|
|
@@ -6564,13 +6903,14 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6564
6903
|
identifierPinsByContext,
|
|
6565
6904
|
memoryPinnedItemIds,
|
|
6566
6905
|
userPinnedItemIdsByContext,
|
|
6906
|
+
scopedItemsByContext: resolvedProject?.scopedItemsByContext,
|
|
6567
6907
|
rewrites: cfg.vocabulary.rewrites,
|
|
6568
6908
|
styleHint: cfg.vocabulary.styleHint,
|
|
6569
6909
|
maxQueries: cfg.tuning.maxQueriesPerContext,
|
|
6570
6910
|
skipPrefilter: false
|
|
6571
6911
|
}),
|
|
6572
|
-
|
|
6573
|
-
contextIds:
|
|
6912
|
+
fallbackContextsToSearch.length > 0 && !hasExplicitDocAndPage ? searchContexts({
|
|
6913
|
+
contextIds: fallbackContextsToSearch,
|
|
6574
6914
|
contextsById,
|
|
6575
6915
|
kbProfiles: cfg.knowledgeBases,
|
|
6576
6916
|
question: updatedQuestion,
|
|
@@ -6583,6 +6923,7 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6583
6923
|
identifierPinsByContext,
|
|
6584
6924
|
memoryPinnedItemIds,
|
|
6585
6925
|
userPinnedItemIdsByContext,
|
|
6926
|
+
scopedItemsByContext: resolvedProject?.scopedItemsByContext,
|
|
6586
6927
|
rewrites: cfg.vocabulary.rewrites,
|
|
6587
6928
|
styleHint: cfg.vocabulary.styleHint,
|
|
6588
6929
|
maxQueries: cfg.tuning.maxQueriesPerContext,
|
|
@@ -6596,6 +6937,9 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6596
6937
|
})(),
|
|
6597
6938
|
...(function* () {
|
|
6598
6939
|
for (const s of userPinnedItemIdsByContext.values()) yield* s;
|
|
6940
|
+
})(),
|
|
6941
|
+
...(function* () {
|
|
6942
|
+
if (resolvedProject) for (const s of resolvedProject.pinsByContext.values()) yield* s;
|
|
6599
6943
|
})()
|
|
6600
6944
|
]);
|
|
6601
6945
|
const userPinnedItemIds = new Set(
|
|
@@ -6663,15 +7007,15 @@ function createAgenticRetrievalTool(opts) {
|
|
|
6663
7007
|
result.reasoning.push({ text: "Literal lookup satisfied; skipping fallback.", tools: [] });
|
|
6664
7008
|
yield { result: serializeOutput(result) };
|
|
6665
7009
|
}
|
|
6666
|
-
if (!literalLookupSatisfied &&
|
|
7010
|
+
if (!literalLookupSatisfied && fallbackContextsToSearch.length > 0 && (reranker ? mainRerank.rerank_score_max_genuine < cfg.tuning.fallbackThreshold : mainRerank.limited_results.length < cfg.tuning.topK)) {
|
|
6667
7011
|
result.steps.push({
|
|
6668
7012
|
stepNumber: 1,
|
|
6669
|
-
text: `Using fallback search in ${
|
|
7013
|
+
text: `Using fallback search in ${fallbackContextsToSearch.join(", ")}`,
|
|
6670
7014
|
toolCalls: [],
|
|
6671
7015
|
chunks: [],
|
|
6672
7016
|
tokens: 0
|
|
6673
7017
|
});
|
|
6674
|
-
result.reasoning.push({ text: `Fallback search in ${
|
|
7018
|
+
result.reasoning.push({ text: `Fallback search in ${fallbackContextsToSearch.join(", ")}`, tools: [] });
|
|
6675
7019
|
yield { result: serializeOutput(result) };
|
|
6676
7020
|
const fallbackRerank = await rerankResults({
|
|
6677
7021
|
chunks: speculativeFallbackSearch.chunks,
|
|
@@ -6747,11 +7091,14 @@ var init_pipeline = __esm({
|
|
|
6747
7091
|
init_resolve_model();
|
|
6748
7092
|
init_singleton();
|
|
6749
7093
|
init_config();
|
|
7094
|
+
init_project_scope();
|
|
6750
7095
|
init_routing();
|
|
6751
7096
|
init_memory();
|
|
6752
7097
|
init_prefilter();
|
|
6753
7098
|
init_search();
|
|
6754
7099
|
init_rerank();
|
|
7100
|
+
init_global_ids();
|
|
7101
|
+
init_global_ids();
|
|
6755
7102
|
}
|
|
6756
7103
|
});
|
|
6757
7104
|
|
|
@@ -9086,6 +9433,13 @@ var agentsSchema = {
|
|
|
9086
9433
|
name: "sandbox_enabled",
|
|
9087
9434
|
type: "boolean",
|
|
9088
9435
|
default: false
|
|
9436
|
+
},
|
|
9437
|
+
{
|
|
9438
|
+
// Per-turn budget for ALL tool steps on one chat message (bash, files,
|
|
9439
|
+
// knowledge search, integrations). 0/null = platform default
|
|
9440
|
+
// (DEFAULT_MAX_STEPS in resolve-max-steps.ts). Auto-ALTERed on boot.
|
|
9441
|
+
name: "max_tool_steps",
|
|
9442
|
+
type: "number"
|
|
9089
9443
|
}
|
|
9090
9444
|
]
|
|
9091
9445
|
};
|
|
@@ -11148,7 +11502,7 @@ var ExuluContext2 = class {
|
|
|
11148
11502
|
embedder,
|
|
11149
11503
|
chunker,
|
|
11150
11504
|
processor,
|
|
11151
|
-
active,
|
|
11505
|
+
active: active2,
|
|
11152
11506
|
fields,
|
|
11153
11507
|
queryRewriter,
|
|
11154
11508
|
resultReranker,
|
|
@@ -11179,7 +11533,7 @@ var ExuluContext2 = class {
|
|
|
11179
11533
|
this.description = description;
|
|
11180
11534
|
this.embedder = embedder;
|
|
11181
11535
|
this.chunker = chunker;
|
|
11182
|
-
this.active =
|
|
11536
|
+
this.active = active2;
|
|
11183
11537
|
this.queryRewriter = queryRewriter;
|
|
11184
11538
|
this.resultReranker = resultReranker;
|
|
11185
11539
|
this.entities = entities;
|
|
@@ -12179,7 +12533,6 @@ init_cjs_shims();
|
|
|
12179
12533
|
init_pipeline();
|
|
12180
12534
|
init_check_record_access();
|
|
12181
12535
|
init_client();
|
|
12182
|
-
init_project_retrieval_tool();
|
|
12183
12536
|
init_singleton();
|
|
12184
12537
|
init_supervisor();
|
|
12185
12538
|
init_catalog();
|
|
@@ -12326,14 +12679,26 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
|
|
|
12326
12679
|
)
|
|
12327
12680
|
);
|
|
12328
12681
|
if (args.project) {
|
|
12329
|
-
const
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12682
|
+
const hasAgentic = result.tools.some(
|
|
12683
|
+
(tool4) => tool4?.id === "agentic_context_search"
|
|
12684
|
+
);
|
|
12685
|
+
if (!hasAgentic) {
|
|
12686
|
+
const instance2 = createAgenticRetrievalTool({
|
|
12687
|
+
contexts: [],
|
|
12688
|
+
user,
|
|
12689
|
+
role: user.role?.id,
|
|
12690
|
+
model: void 0
|
|
12691
|
+
});
|
|
12692
|
+
if (instance2) {
|
|
12693
|
+
result.tools.unshift({
|
|
12694
|
+
id: instance2.id,
|
|
12695
|
+
name: instance2.name,
|
|
12696
|
+
description: instance2.description,
|
|
12697
|
+
category: instance2.category,
|
|
12698
|
+
type: instance2.type,
|
|
12699
|
+
config: []
|
|
12700
|
+
});
|
|
12701
|
+
}
|
|
12337
12702
|
}
|
|
12338
12703
|
}
|
|
12339
12704
|
result.tools = result.tools.filter((tool4) => tool4 !== null);
|
|
@@ -18086,21 +18451,7 @@ function isOsJunkPath(path2) {
|
|
|
18086
18451
|
const basename = path2.split("/").pop() ?? "";
|
|
18087
18452
|
return basename === ".DS_Store" || basename === "Thumbs.db" || basename === "desktop.ini";
|
|
18088
18453
|
}
|
|
18089
|
-
async function
|
|
18090
|
-
const { bytes, skillId, isZip, config } = opts;
|
|
18091
|
-
if (!isZip) {
|
|
18092
|
-
await uploadFile(
|
|
18093
|
-
bytes,
|
|
18094
|
-
`skills/${skillId}/v1/SKILL.md`,
|
|
18095
|
-
config,
|
|
18096
|
-
{ contentType: "text/markdown" },
|
|
18097
|
-
void 0,
|
|
18098
|
-
void 0,
|
|
18099
|
-
true
|
|
18100
|
-
// global=true so the key isn't user-prefixed (skill files are shared)
|
|
18101
|
-
);
|
|
18102
|
-
return { filesCount: 1 };
|
|
18103
|
-
}
|
|
18454
|
+
async function extractZipToPrefix(bytes, prefix, config) {
|
|
18104
18455
|
let zip;
|
|
18105
18456
|
try {
|
|
18106
18457
|
zip = await import_jszip.default.loadAsync(bytes);
|
|
@@ -18164,7 +18515,7 @@ async function extractBundleToS3(opts) {
|
|
|
18164
18515
|
}
|
|
18165
18516
|
let filesCount = 0;
|
|
18166
18517
|
for (const { relPath, content } of prepared) {
|
|
18167
|
-
const s3Key =
|
|
18518
|
+
const s3Key = `${prefix}${relPath}`;
|
|
18168
18519
|
await uploadFile(
|
|
18169
18520
|
content,
|
|
18170
18521
|
s3Key,
|
|
@@ -18173,12 +18524,77 @@ async function extractBundleToS3(opts) {
|
|
|
18173
18524
|
void 0,
|
|
18174
18525
|
void 0,
|
|
18175
18526
|
true
|
|
18176
|
-
// global=true —
|
|
18527
|
+
// global=true — skill files are shared across users
|
|
18177
18528
|
);
|
|
18178
18529
|
filesCount += 1;
|
|
18179
18530
|
}
|
|
18180
18531
|
return { filesCount };
|
|
18181
18532
|
}
|
|
18533
|
+
async function extractBundleToS3(opts) {
|
|
18534
|
+
const { bytes, skillId, isZip, config } = opts;
|
|
18535
|
+
if (!isZip) {
|
|
18536
|
+
await uploadFile(
|
|
18537
|
+
bytes,
|
|
18538
|
+
`skills/${skillId}/v1/SKILL.md`,
|
|
18539
|
+
config,
|
|
18540
|
+
{ contentType: "text/markdown" },
|
|
18541
|
+
void 0,
|
|
18542
|
+
void 0,
|
|
18543
|
+
true
|
|
18544
|
+
// global=true so the key isn't user-prefixed (skill files are shared)
|
|
18545
|
+
);
|
|
18546
|
+
return { filesCount: 1 };
|
|
18547
|
+
}
|
|
18548
|
+
return extractZipToPrefix(bytes, `skills/${skillId}/v1/`, config);
|
|
18549
|
+
}
|
|
18550
|
+
async function extractBundleToVersion(opts) {
|
|
18551
|
+
const { bytes, skillId, version, config } = opts;
|
|
18552
|
+
return extractZipToPrefix(bytes, `skills/${skillId}/v${version}/`, config);
|
|
18553
|
+
}
|
|
18554
|
+
|
|
18555
|
+
// src/skills/frontmatter.ts
|
|
18556
|
+
init_cjs_shims();
|
|
18557
|
+
var import_jszip2 = __toESM(require("jszip"), 1);
|
|
18558
|
+
function parseFrontmatter(md) {
|
|
18559
|
+
const match = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
18560
|
+
if (!match) return {};
|
|
18561
|
+
const block = match[1];
|
|
18562
|
+
const out = {};
|
|
18563
|
+
for (const line of block.split(/\r?\n/)) {
|
|
18564
|
+
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
18565
|
+
if (!m) continue;
|
|
18566
|
+
const key = m[1];
|
|
18567
|
+
let v = m[2].trim();
|
|
18568
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
|
|
18569
|
+
v = v.slice(1, -1);
|
|
18570
|
+
}
|
|
18571
|
+
out[key] = v;
|
|
18572
|
+
}
|
|
18573
|
+
return out;
|
|
18574
|
+
}
|
|
18575
|
+
async function parseSkillFrontmatter(zipBytes) {
|
|
18576
|
+
let zip;
|
|
18577
|
+
try {
|
|
18578
|
+
zip = await import_jszip2.default.loadAsync(zipBytes);
|
|
18579
|
+
} catch {
|
|
18580
|
+
return {};
|
|
18581
|
+
}
|
|
18582
|
+
const paths = [];
|
|
18583
|
+
zip.forEach((p, entry) => {
|
|
18584
|
+
if (!entry.dir) paths.push(p);
|
|
18585
|
+
});
|
|
18586
|
+
const heads = new Set(paths.map((p) => p.split("/")[0]).filter(Boolean));
|
|
18587
|
+
let strip = (p) => p;
|
|
18588
|
+
if (heads.size === 1) {
|
|
18589
|
+
const head = [...heads][0] + "/";
|
|
18590
|
+
if (paths.every((p) => p.startsWith(head))) strip = (p) => p.slice(head.length);
|
|
18591
|
+
}
|
|
18592
|
+
const skillPath = paths.find((p) => strip(p) === "SKILL.md");
|
|
18593
|
+
if (!skillPath) return {};
|
|
18594
|
+
const md = await zip.file(skillPath).async("string");
|
|
18595
|
+
const fm = parseFrontmatter(md);
|
|
18596
|
+
return { name: fm.name, description: fm.description };
|
|
18597
|
+
}
|
|
18182
18598
|
|
|
18183
18599
|
// src/sessions/pdf-preview-cache.ts
|
|
18184
18600
|
init_cjs_shims();
|
|
@@ -18259,11 +18675,11 @@ var import_body_parser = __toESM(require("body-parser"), 1);
|
|
|
18259
18675
|
var import_crypto_js9 = __toESM(require("crypto-js"), 1);
|
|
18260
18676
|
var import_openai = __toESM(require("openai"), 1);
|
|
18261
18677
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
18262
|
-
var
|
|
18678
|
+
var import_node_crypto9 = require("crypto");
|
|
18263
18679
|
var import_api2 = require("@opentelemetry/api");
|
|
18264
18680
|
init_check_record_access();
|
|
18265
|
-
var
|
|
18266
|
-
var
|
|
18681
|
+
var import_jszip3 = __toESM(require("jszip"), 1);
|
|
18682
|
+
var import_ai15 = require("ai");
|
|
18267
18683
|
var import_cookie_parser = __toESM(require("cookie-parser"), 1);
|
|
18268
18684
|
init_statistics2();
|
|
18269
18685
|
|
|
@@ -18272,7 +18688,8 @@ init_cjs_shims();
|
|
|
18272
18688
|
|
|
18273
18689
|
// src/exulu/resolve-max-steps.ts
|
|
18274
18690
|
init_cjs_shims();
|
|
18275
|
-
|
|
18691
|
+
var DEFAULT_MAX_STEPS = 10;
|
|
18692
|
+
function resolveRetrievalCallBudget(toolConfigs) {
|
|
18276
18693
|
const agentic = toolConfigs?.find((t) => t.id === "agentic_context_search");
|
|
18277
18694
|
if (!agentic?.config) return void 0;
|
|
18278
18695
|
const entry = agentic.config.find((c) => c.name === "max_steps");
|
|
@@ -18281,15 +18698,26 @@ function resolveMaxStepsFromToolConfigs(toolConfigs) {
|
|
|
18281
18698
|
const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
|
|
18282
18699
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
|
|
18283
18700
|
}
|
|
18701
|
+
function resolveTurnStepBudget(maxStepCount, agent) {
|
|
18702
|
+
if (typeof maxStepCount === "number" && Number.isFinite(maxStepCount) && maxStepCount > 0) {
|
|
18703
|
+
return Math.floor(maxStepCount);
|
|
18704
|
+
}
|
|
18705
|
+
const raw = agent?.max_tool_steps;
|
|
18706
|
+
const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
|
|
18707
|
+
if (Number.isFinite(n) && n > 0) {
|
|
18708
|
+
return Math.floor(n);
|
|
18709
|
+
}
|
|
18710
|
+
return DEFAULT_MAX_STEPS;
|
|
18711
|
+
}
|
|
18284
18712
|
function flattenPart(part) {
|
|
18285
18713
|
const p = part;
|
|
18286
18714
|
if (p?.type === "text") return p.text ?? "";
|
|
18287
18715
|
if (p?.type === "tool-call") {
|
|
18288
|
-
return `
|
|
18716
|
+
return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
|
|
18289
18717
|
}
|
|
18290
18718
|
if (p?.type === "tool-result") {
|
|
18291
18719
|
const out = p.output?.value ?? p.output;
|
|
18292
|
-
return `
|
|
18720
|
+
return `The "${p.toolName}" tool returned: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
|
|
18293
18721
|
}
|
|
18294
18722
|
return "";
|
|
18295
18723
|
}
|
|
@@ -18307,7 +18735,7 @@ function flattenToolHistory(messages) {
|
|
|
18307
18735
|
return m;
|
|
18308
18736
|
});
|
|
18309
18737
|
}
|
|
18310
|
-
var FINAL_ANSWER_INSTRUCTION =
|
|
18738
|
+
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.`;
|
|
18311
18739
|
function finalAnswerGuard(maxSteps) {
|
|
18312
18740
|
return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
|
|
18313
18741
|
toolChoice: "none",
|
|
@@ -18320,6 +18748,73 @@ function finalAnswerGuard(maxSteps) {
|
|
|
18320
18748
|
} : {}
|
|
18321
18749
|
} : void 0;
|
|
18322
18750
|
}
|
|
18751
|
+
function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
|
|
18752
|
+
if (limit == null || limit <= 0 || !agenticToolKey || !allToolKeys.includes(agenticToolKey)) {
|
|
18753
|
+
return () => void 0;
|
|
18754
|
+
}
|
|
18755
|
+
const remainingTools = allToolKeys.filter((k) => k !== agenticToolKey);
|
|
18756
|
+
return ({ steps }) => {
|
|
18757
|
+
const calls = (steps ?? []).flatMap((s) => s?.toolCalls ?? []).filter((c) => c?.toolName === agenticToolKey).length;
|
|
18758
|
+
if (calls < limit) return void 0;
|
|
18759
|
+
return { activeTools: remainingTools };
|
|
18760
|
+
};
|
|
18761
|
+
}
|
|
18762
|
+
|
|
18763
|
+
// src/exulu/context-guard.ts
|
|
18764
|
+
init_cjs_shims();
|
|
18765
|
+
init_context_budget();
|
|
18766
|
+
var KEEP_RECENT_TOOL_MESSAGES = 2;
|
|
18767
|
+
var COLLAPSE_KEEP_CHARS = 400;
|
|
18768
|
+
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]";
|
|
18769
|
+
function contextGuard(contextWindow) {
|
|
18770
|
+
const budget = deriveContextBudget(contextWindow);
|
|
18771
|
+
return async ({ messages }) => {
|
|
18772
|
+
if (!Array.isArray(messages) || messages.length === 0) return void 0;
|
|
18773
|
+
const tokens = estimateTokens(JSON.stringify(messages));
|
|
18774
|
+
if (tokens < budget.usableWindow) return void 0;
|
|
18775
|
+
const toolIndices = messages.map((m, i) => m?.role === "tool" ? i : -1).filter((i) => i !== -1);
|
|
18776
|
+
const collapsible = new Set(toolIndices.slice(0, Math.max(0, toolIndices.length - KEEP_RECENT_TOOL_MESSAGES)));
|
|
18777
|
+
if (collapsible.size === 0) return void 0;
|
|
18778
|
+
let changed = false;
|
|
18779
|
+
const next = messages.map((m, i) => {
|
|
18780
|
+
if (!collapsible.has(i)) return m;
|
|
18781
|
+
const msg = m;
|
|
18782
|
+
if (!Array.isArray(msg.content)) return m;
|
|
18783
|
+
const content = msg.content.map((part) => {
|
|
18784
|
+
const p = part;
|
|
18785
|
+
if (p?.type !== "tool-result") return part;
|
|
18786
|
+
const out = p.output?.value ?? p.output;
|
|
18787
|
+
const asText = typeof out === "string" ? out : JSON.stringify(out ?? "");
|
|
18788
|
+
if (asText.length <= COLLAPSE_KEEP_CHARS + COLLAPSE_MARKER.length) return part;
|
|
18789
|
+
changed = true;
|
|
18790
|
+
return { ...part, output: { type: "text", value: asText.slice(0, COLLAPSE_KEEP_CHARS) + COLLAPSE_MARKER } };
|
|
18791
|
+
});
|
|
18792
|
+
return { ...m, content };
|
|
18793
|
+
});
|
|
18794
|
+
return changed ? { messages: next } : void 0;
|
|
18795
|
+
};
|
|
18796
|
+
}
|
|
18797
|
+
function composePrepareSteps(...guards) {
|
|
18798
|
+
return async (opts) => {
|
|
18799
|
+
let merged;
|
|
18800
|
+
let messages = opts.messages;
|
|
18801
|
+
for (const guard of guards) {
|
|
18802
|
+
const result = await guard({ ...opts, messages });
|
|
18803
|
+
if (!result) continue;
|
|
18804
|
+
merged = { ...merged ?? {}, ...result };
|
|
18805
|
+
if (Array.isArray(result.messages)) {
|
|
18806
|
+
messages = result.messages;
|
|
18807
|
+
}
|
|
18808
|
+
}
|
|
18809
|
+
if (merged && messages && !("messages" in merged)) {
|
|
18810
|
+
merged.messages = messages;
|
|
18811
|
+
}
|
|
18812
|
+
return merged;
|
|
18813
|
+
};
|
|
18814
|
+
}
|
|
18815
|
+
|
|
18816
|
+
// src/exulu/provider.ts
|
|
18817
|
+
init_sanitize_tool_name();
|
|
18323
18818
|
|
|
18324
18819
|
// src/exulu/auto-decline-stale-approvals.ts
|
|
18325
18820
|
init_cjs_shims();
|
|
@@ -18419,6 +18914,8 @@ async function clearSessionCurrentTask(session) {
|
|
|
18419
18914
|
}
|
|
18420
18915
|
|
|
18421
18916
|
// src/exulu/provider.ts
|
|
18917
|
+
init_context_budget();
|
|
18918
|
+
init_tool_output_offload();
|
|
18422
18919
|
var ExuluProvider = class {
|
|
18423
18920
|
// Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
|
|
18424
18921
|
// underscores and be a max length of 80 characters and at least 5 characters long.
|
|
@@ -18589,7 +19086,9 @@ var ExuluProvider = class {
|
|
|
18589
19086
|
agent,
|
|
18590
19087
|
instructions,
|
|
18591
19088
|
maxStepCount,
|
|
18592
|
-
onTokenUsage
|
|
19089
|
+
onTokenUsage,
|
|
19090
|
+
contextWindow,
|
|
19091
|
+
disabledTools
|
|
18593
19092
|
}) => {
|
|
18594
19093
|
console.log(
|
|
18595
19094
|
"[EXULU] Called generate sync for agent: " + this.name,
|
|
@@ -18617,9 +19116,7 @@ var ExuluProvider = class {
|
|
|
18617
19116
|
if (messages && session && user) {
|
|
18618
19117
|
const previousMessages = await getAgentMessages({
|
|
18619
19118
|
session,
|
|
18620
|
-
user: user.id
|
|
18621
|
-
limit: 50,
|
|
18622
|
-
page: 1
|
|
19119
|
+
user: user.id
|
|
18623
19120
|
});
|
|
18624
19121
|
const previousMessagesContent = previousMessages.map(
|
|
18625
19122
|
(message) => JSON.parse(message.content)
|
|
@@ -18628,6 +19125,12 @@ var ExuluProvider = class {
|
|
|
18628
19125
|
// append the new message to the previous messages:
|
|
18629
19126
|
messages: [...previousMessagesContent, ...messages]
|
|
18630
19127
|
});
|
|
19128
|
+
const contextBudget = deriveContextBudget(contextWindow);
|
|
19129
|
+
const occupancy = contextOccupancy(messages);
|
|
19130
|
+
if (occupancy >= contextBudget.blockThreshold) {
|
|
19131
|
+
throw new ContextCompactionRequiredError(occupancy, contextBudget);
|
|
19132
|
+
}
|
|
19133
|
+
messages = sliceHistoryAtCheckpoint(messages);
|
|
18631
19134
|
}
|
|
18632
19135
|
console.log(
|
|
18633
19136
|
"[EXULU] Message count for agent: " + this.name,
|
|
@@ -18692,6 +19195,34 @@ var ExuluProvider = class {
|
|
|
18692
19195
|
if (memoryContext) {
|
|
18693
19196
|
system += "\n\n" + memoryContext;
|
|
18694
19197
|
}
|
|
19198
|
+
const tools = await convertExuluToolsToAiSdkTools(
|
|
19199
|
+
currentTools,
|
|
19200
|
+
currentSkills,
|
|
19201
|
+
approvedTools,
|
|
19202
|
+
allExuluTools,
|
|
19203
|
+
toolConfigs,
|
|
19204
|
+
providerapikey,
|
|
19205
|
+
contexts,
|
|
19206
|
+
user,
|
|
19207
|
+
exuluConfig,
|
|
19208
|
+
session,
|
|
19209
|
+
req,
|
|
19210
|
+
project,
|
|
19211
|
+
sessionItems,
|
|
19212
|
+
model,
|
|
19213
|
+
agent,
|
|
19214
|
+
memoryItems,
|
|
19215
|
+
contextWindow,
|
|
19216
|
+
disabledTools
|
|
19217
|
+
);
|
|
19218
|
+
const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
|
|
19219
|
+
const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
|
|
19220
|
+
const retrievalGuard = retrievalBudgetGuard(
|
|
19221
|
+
resolveRetrievalCallBudget(toolConfigs),
|
|
19222
|
+
agenticToolKey,
|
|
19223
|
+
Object.keys(tools)
|
|
19224
|
+
);
|
|
19225
|
+
const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
|
|
18695
19226
|
const includesContextSearchTool = currentTools?.some(
|
|
18696
19227
|
(tool4) => tool4.name.toLowerCase().includes("context_search") || tool4.id.includes("context_search") || tool4.type === "context"
|
|
18697
19228
|
);
|
|
@@ -18704,12 +19235,12 @@ var ExuluProvider = class {
|
|
|
18704
19235
|
system += `
|
|
18705
19236
|
|
|
18706
19237
|
|
|
18707
|
-
|
|
19238
|
+
|
|
18708
19239
|
When you use a context search tool, you will include references to the items
|
|
18709
19240
|
retrieved from the tool call result inline in the response using this exact JSON format
|
|
18710
19241
|
(all on one line, no line breaks):
|
|
18711
19242
|
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
18712
|
-
|
|
19243
|
+
|
|
18713
19244
|
IMPORTANT formatting rules:
|
|
18714
19245
|
- 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.
|
|
18715
19246
|
- Use the exact format shown above, all on ONE line
|
|
@@ -18717,9 +19248,9 @@ var ExuluProvider = class {
|
|
|
18717
19248
|
- Use the context ID from the tool result
|
|
18718
19249
|
- Include the file/item name, not the full path
|
|
18719
19250
|
- Separate multiple citations with spaces
|
|
18720
|
-
|
|
19251
|
+
|
|
18721
19252
|
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
18722
|
-
|
|
19253
|
+
|
|
18723
19254
|
The citations will be rendered as interactive badges in the UI.
|
|
18724
19255
|
`;
|
|
18725
19256
|
}
|
|
@@ -18730,12 +19261,12 @@ var ExuluProvider = class {
|
|
|
18730
19261
|
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
|
|
18731
19262
|
(all on one line, no line breaks):
|
|
18732
19263
|
{url: <url>, title: <title>, snippet: <snippet>}
|
|
18733
|
-
|
|
19264
|
+
|
|
18734
19265
|
IMPORTANT formatting rules:
|
|
18735
19266
|
- Use the exact format shown above, all on ONE line
|
|
18736
19267
|
- Do NOT use quotes around field names or values
|
|
18737
19268
|
- Separate multiple results with spaces
|
|
18738
|
-
|
|
19269
|
+
|
|
18739
19270
|
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
18740
19271
|
`;
|
|
18741
19272
|
}
|
|
@@ -18768,29 +19299,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
18768
19299
|
system,
|
|
18769
19300
|
prompt,
|
|
18770
19301
|
maxRetries: 2,
|
|
18771
|
-
tools
|
|
18772
|
-
currentTools,
|
|
18773
|
-
currentSkills,
|
|
18774
|
-
approvedTools,
|
|
18775
|
-
allExuluTools,
|
|
18776
|
-
toolConfigs,
|
|
18777
|
-
providerapikey,
|
|
18778
|
-
contexts,
|
|
18779
|
-
user,
|
|
18780
|
-
exuluConfig,
|
|
18781
|
-
session,
|
|
18782
|
-
req,
|
|
18783
|
-
project,
|
|
18784
|
-
sessionItems,
|
|
18785
|
-
model,
|
|
18786
|
-
agent,
|
|
18787
|
-
memoryItems
|
|
18788
|
-
),
|
|
19302
|
+
tools,
|
|
18789
19303
|
// Stop after the image_generation tool fires — the widget IS the
|
|
18790
19304
|
// assistant's response, no follow-up text turn is wanted (same
|
|
18791
19305
|
// reasoning as question_ask: the UI artifact is the message).
|
|
18792
|
-
prepareStep:
|
|
18793
|
-
stopWhen: [(0, import_ai11.stepCountIs)(
|
|
19306
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
19307
|
+
stopWhen: [(0, import_ai11.stepCountIs)(turnBudget), (0, import_ai11.hasToolCall)("image_generation")]
|
|
18794
19308
|
});
|
|
18795
19309
|
console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
|
|
18796
19310
|
const {
|
|
@@ -18851,26 +19365,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
18851
19365
|
ignoreIncompleteToolCalls: true
|
|
18852
19366
|
}),
|
|
18853
19367
|
maxRetries: 2,
|
|
18854
|
-
tools
|
|
18855
|
-
|
|
18856
|
-
|
|
18857
|
-
approvedTools,
|
|
18858
|
-
allExuluTools,
|
|
18859
|
-
toolConfigs,
|
|
18860
|
-
providerapikey,
|
|
18861
|
-
contexts,
|
|
18862
|
-
user,
|
|
18863
|
-
exuluConfig,
|
|
18864
|
-
session,
|
|
18865
|
-
req,
|
|
18866
|
-
project,
|
|
18867
|
-
sessionItems,
|
|
18868
|
-
model,
|
|
18869
|
-
agent,
|
|
18870
|
-
memoryItems
|
|
18871
|
-
),
|
|
18872
|
-
prepareStep: finalAnswerGuard(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5),
|
|
18873
|
-
stopWhen: [(0, import_ai11.stepCountIs)(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5), (0, import_ai11.hasToolCall)("image_generation")]
|
|
19368
|
+
tools,
|
|
19369
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
19370
|
+
stopWhen: [(0, import_ai11.stepCountIs)(turnBudget), (0, import_ai11.hasToolCall)("image_generation")]
|
|
18874
19371
|
});
|
|
18875
19372
|
if (statistics) {
|
|
18876
19373
|
await Promise.all([
|
|
@@ -18922,7 +19419,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
18922
19419
|
* - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
|
|
18923
19420
|
* - Image files -> image parts (which ARE supported by Responses API)
|
|
18924
19421
|
*/
|
|
18925
|
-
async processFilePartsInMessages(messages) {
|
|
19422
|
+
async processFilePartsInMessages(messages, offloadCtx) {
|
|
18926
19423
|
const processedMessages = await Promise.all(
|
|
18927
19424
|
messages.map(async (message) => {
|
|
18928
19425
|
if (message.role !== "user" || !Array.isArray(message.parts)) {
|
|
@@ -18967,10 +19464,11 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
18967
19464
|
outputErrorToConsole: false,
|
|
18968
19465
|
newlineDelimiter: "\n"
|
|
18969
19466
|
});
|
|
19467
|
+
const guardedText = await guardExtractedFileText(filename, String(extractedText), offloadCtx);
|
|
18970
19468
|
return {
|
|
18971
19469
|
type: "text",
|
|
18972
19470
|
text: `<file file name = "${filename}" >
|
|
18973
|
-
${
|
|
19471
|
+
${guardedText}
|
|
18974
19472
|
</file>`
|
|
18975
19473
|
};
|
|
18976
19474
|
} catch (error) {
|
|
@@ -18986,7 +19484,6 @@ ${extractedText}
|
|
|
18986
19484
|
...message,
|
|
18987
19485
|
parts: processedParts
|
|
18988
19486
|
};
|
|
18989
|
-
console.log("[EXULU] Result: " + JSON.stringify(result, null, 2));
|
|
18990
19487
|
return result;
|
|
18991
19488
|
})
|
|
18992
19489
|
);
|
|
@@ -19009,7 +19506,9 @@ ${extractedText}
|
|
|
19009
19506
|
exuluConfig,
|
|
19010
19507
|
instructions,
|
|
19011
19508
|
req,
|
|
19012
|
-
maxStepCount
|
|
19509
|
+
maxStepCount,
|
|
19510
|
+
contextWindow,
|
|
19511
|
+
disabledTools
|
|
19013
19512
|
}) => {
|
|
19014
19513
|
if (!this.config) {
|
|
19015
19514
|
console.error("[EXULU] Config is required for streaming.");
|
|
@@ -19030,9 +19529,7 @@ ${extractedText}
|
|
|
19030
19529
|
console.log("[EXULU] loading previous messages from session: " + session);
|
|
19031
19530
|
const previousMessages2 = await getAgentMessages({
|
|
19032
19531
|
session,
|
|
19033
|
-
user: user?.id
|
|
19034
|
-
limit: 50,
|
|
19035
|
-
page: 1
|
|
19532
|
+
user: user?.id
|
|
19036
19533
|
});
|
|
19037
19534
|
previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
|
|
19038
19535
|
}
|
|
@@ -19095,60 +19592,28 @@ ${extractedText}
|
|
|
19095
19592
|
if (declined.length && session && user) {
|
|
19096
19593
|
await saveChat({ session, user: user.id, messages: declined });
|
|
19097
19594
|
}
|
|
19098
|
-
messages = await this.processFilePartsInMessages(messages
|
|
19595
|
+
messages = await this.processFilePartsInMessages(messages, {
|
|
19596
|
+
contextWindow,
|
|
19597
|
+
sessionID: session,
|
|
19598
|
+
user,
|
|
19599
|
+
exuluConfig
|
|
19600
|
+
});
|
|
19601
|
+
const chronologicalMessages = messages;
|
|
19602
|
+
const contextBudget = deriveContextBudget(contextWindow);
|
|
19603
|
+
const occupancy = contextOccupancy(chronologicalMessages);
|
|
19604
|
+
if (occupancy >= contextBudget.blockThreshold) {
|
|
19605
|
+
console.warn(
|
|
19606
|
+
`[EXULU] Blocking request: occupancy ${occupancy} >= blockThreshold ${contextBudget.blockThreshold} (window ${contextBudget.contextWindow}).`
|
|
19607
|
+
);
|
|
19608
|
+
throw new ContextCompactionRequiredError(occupancy, contextBudget);
|
|
19609
|
+
}
|
|
19610
|
+
messages = sliceHistoryAtCheckpoint(chronologicalMessages);
|
|
19099
19611
|
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.";
|
|
19100
19612
|
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.";
|
|
19101
19613
|
if (user?.personal_system_prompt?.trim()) {
|
|
19102
19614
|
system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
|
|
19103
19615
|
}
|
|
19104
19616
|
system += "\n\n" + genericContext;
|
|
19105
|
-
const includesContextSearchTool = currentTools?.some(
|
|
19106
|
-
(tool4) => tool4.name.toLowerCase().includes("context_search") || tool4.id.includes("context_search") || tool4.type === "context"
|
|
19107
|
-
);
|
|
19108
|
-
const includesWebSearchTool = currentTools?.some(
|
|
19109
|
-
(tool4) => tool4.name.toLowerCase().includes("web_search") || tool4.id.includes("web_search") || tool4.type === "web_search"
|
|
19110
|
-
);
|
|
19111
|
-
console.log("[EXULU] Current tools: " + currentTools?.map((tool4) => tool4.name).join("\n"));
|
|
19112
|
-
console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
|
|
19113
|
-
console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
|
|
19114
|
-
if (includesContextSearchTool) {
|
|
19115
|
-
system += `
|
|
19116
|
-
|
|
19117
|
-
|
|
19118
|
-
|
|
19119
|
-
When you use a context search tool, you will include references to the items
|
|
19120
|
-
retrieved from the tool call result inline in the response using this exact JSON format
|
|
19121
|
-
(all on one line, no line breaks):
|
|
19122
|
-
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
19123
|
-
|
|
19124
|
-
IMPORTANT formatting rules:
|
|
19125
|
-
- Use the exact format shown above, all on ONE line
|
|
19126
|
-
- Do NOT use quotes around field names or values
|
|
19127
|
-
- Use the context ID from the tool result
|
|
19128
|
-
- Include the file/item name, not the full path
|
|
19129
|
-
- Separate multiple citations with spaces
|
|
19130
|
-
|
|
19131
|
-
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
19132
|
-
|
|
19133
|
-
The citations will be rendered as interactive badges in the UI.
|
|
19134
|
-
`;
|
|
19135
|
-
}
|
|
19136
|
-
if (includesWebSearchTool) {
|
|
19137
|
-
system += `
|
|
19138
|
-
|
|
19139
|
-
|
|
19140
|
-
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
|
|
19141
|
-
(all on one line, no line breaks):
|
|
19142
|
-
{url: <url>, title: <title>, snippet: <snippet>}
|
|
19143
|
-
|
|
19144
|
-
IMPORTANT formatting rules:
|
|
19145
|
-
- Use the exact format shown above, all on ONE line
|
|
19146
|
-
- Do NOT use quotes around field names or values
|
|
19147
|
-
- Separate multiple results with spaces
|
|
19148
|
-
|
|
19149
|
-
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
19150
|
-
`;
|
|
19151
|
-
}
|
|
19152
19617
|
if (currentSkills?.length) {
|
|
19153
19618
|
const skillsList = currentSkills.map((skill) => {
|
|
19154
19619
|
const description = (skill.description ?? "").trim();
|
|
@@ -19203,6 +19668,11 @@ ${skillsList}
|
|
|
19203
19668
|
read them with the readFile tool. Files you produce yourself (via writeFile or via shell
|
|
19204
19669
|
commands like \`node create_doc.js\`) live in the same place. These files are scoped to
|
|
19205
19670
|
this single session; they are NOT visible in other sessions, projects, or knowledge bases.
|
|
19671
|
+
|
|
19672
|
+
Note on large outputs: oversized tool outputs and large uploaded documents are automatically
|
|
19673
|
+
truncated in the conversation; the FULL content is saved as a session file (named in the
|
|
19674
|
+
truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
|
|
19675
|
+
to page through it \u2014 do not ask the user to re-upload.
|
|
19206
19676
|
`;
|
|
19207
19677
|
system += `
|
|
19208
19678
|
|
|
@@ -19226,9 +19696,66 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
19226
19696
|
sessionItems,
|
|
19227
19697
|
model,
|
|
19228
19698
|
agent,
|
|
19229
|
-
memoryItems
|
|
19699
|
+
memoryItems,
|
|
19700
|
+
contextWindow,
|
|
19701
|
+
disabledTools
|
|
19230
19702
|
);
|
|
19231
19703
|
console.log("[EXULU] Converted tools", Object.keys(tools));
|
|
19704
|
+
const includesContextSearchTool = currentTools?.some(
|
|
19705
|
+
(tool4) => tool4.name.toLowerCase().includes("context_search") || tool4.id.includes("context_search") || tool4.type === "context"
|
|
19706
|
+
);
|
|
19707
|
+
const includesWebSearchTool = currentTools?.some(
|
|
19708
|
+
(tool4) => tool4.name.toLowerCase().includes("web_search") || tool4.id.includes("web_search") || tool4.type === "web_search"
|
|
19709
|
+
);
|
|
19710
|
+
console.log("[EXULU] Current tools: " + currentTools?.map((tool4) => tool4.name).join("\n"));
|
|
19711
|
+
console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
|
|
19712
|
+
console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
|
|
19713
|
+
if (includesContextSearchTool) {
|
|
19714
|
+
system += `
|
|
19715
|
+
|
|
19716
|
+
|
|
19717
|
+
|
|
19718
|
+
When you use a context search tool, you will include references to the items
|
|
19719
|
+
retrieved from the tool call result inline in the response using this exact JSON format
|
|
19720
|
+
(all on one line, no line breaks):
|
|
19721
|
+
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
19722
|
+
|
|
19723
|
+
IMPORTANT formatting rules:
|
|
19724
|
+
- Use the exact format shown above, all on ONE line
|
|
19725
|
+
- Do NOT use quotes around field names or values
|
|
19726
|
+
- Use the context ID from the tool result
|
|
19727
|
+
- Include the file/item name, not the full path
|
|
19728
|
+
- Separate multiple citations with spaces
|
|
19729
|
+
|
|
19730
|
+
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
19731
|
+
|
|
19732
|
+
The citations will be rendered as interactive badges in the UI.
|
|
19733
|
+
`;
|
|
19734
|
+
}
|
|
19735
|
+
if (includesWebSearchTool) {
|
|
19736
|
+
system += `
|
|
19737
|
+
|
|
19738
|
+
|
|
19739
|
+
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
|
|
19740
|
+
(all on one line, no line breaks):
|
|
19741
|
+
{url: <url>, title: <title>, snippet: <snippet>}
|
|
19742
|
+
|
|
19743
|
+
IMPORTANT formatting rules:
|
|
19744
|
+
- Use the exact format shown above, all on ONE line
|
|
19745
|
+
- Do NOT use quotes around field names or values
|
|
19746
|
+
- Separate multiple results with spaces
|
|
19747
|
+
|
|
19748
|
+
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
19749
|
+
`;
|
|
19750
|
+
}
|
|
19751
|
+
const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
|
|
19752
|
+
const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
|
|
19753
|
+
const retrievalGuard = retrievalBudgetGuard(
|
|
19754
|
+
resolveRetrievalCallBudget(toolConfigs),
|
|
19755
|
+
agenticToolKey,
|
|
19756
|
+
Object.keys(tools)
|
|
19757
|
+
);
|
|
19758
|
+
const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
|
|
19232
19759
|
const result = (0, import_ai11.streamText)({
|
|
19233
19760
|
temperature: 0,
|
|
19234
19761
|
// TODO Make this configurable
|
|
@@ -19253,10 +19780,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
19253
19780
|
`Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
|
|
19254
19781
|
);
|
|
19255
19782
|
},
|
|
19256
|
-
//
|
|
19257
|
-
|
|
19258
|
-
|
|
19259
|
-
stopWhen: [(0, import_ai11.stepCountIs)(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? (currentSkills?.length ? 10 : 5)), (0, import_ai11.hasToolCall)("image_generation")]
|
|
19783
|
+
// todo allow configuring the step budget per skill
|
|
19784
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
19785
|
+
stopWhen: [(0, import_ai11.stepCountIs)(turnBudget), (0, import_ai11.hasToolCall)("image_generation")]
|
|
19260
19786
|
});
|
|
19261
19787
|
return {
|
|
19262
19788
|
stream: result,
|
|
@@ -19267,19 +19793,14 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
19267
19793
|
};
|
|
19268
19794
|
var getAgentMessages = async ({
|
|
19269
19795
|
session,
|
|
19270
|
-
user
|
|
19271
|
-
limit,
|
|
19272
|
-
page
|
|
19796
|
+
user
|
|
19273
19797
|
}) => {
|
|
19274
19798
|
const { db: db2 } = await postgresClient();
|
|
19275
|
-
console.log(
|
|
19276
|
-
|
|
19277
|
-
|
|
19278
|
-
|
|
19279
|
-
|
|
19280
|
-
query.offset((page - 1) * limit);
|
|
19281
|
-
}
|
|
19282
|
-
const messages = await query;
|
|
19799
|
+
console.log("[EXULU] getting agent messages for session: " + session + " and user: " + user);
|
|
19800
|
+
const messages = await db2.from("agent_messages").where({ session, user: user || null }).orderBy([
|
|
19801
|
+
{ column: "createdAt", order: "asc" },
|
|
19802
|
+
{ column: "id", order: "asc" }
|
|
19803
|
+
]);
|
|
19283
19804
|
return messages;
|
|
19284
19805
|
};
|
|
19285
19806
|
var getSession = async ({ sessionID }) => {
|
|
@@ -19383,6 +19904,161 @@ ${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
|
|
|
19383
19904
|
init_resolve_model();
|
|
19384
19905
|
init_supervisor();
|
|
19385
19906
|
|
|
19907
|
+
// src/exulu/resolve-context-window.ts
|
|
19908
|
+
init_cjs_shims();
|
|
19909
|
+
init_catalog();
|
|
19910
|
+
init_context_budget();
|
|
19911
|
+
var resolveContextWindow = async ({
|
|
19912
|
+
modelId,
|
|
19913
|
+
exuluProvider
|
|
19914
|
+
}) => {
|
|
19915
|
+
if (process.env.EXULU_USE_LITELLM === "true") {
|
|
19916
|
+
const entry = await findLiteLLMModel(modelId);
|
|
19917
|
+
const fromCatalog = entry?.max_input_tokens ?? entry?.max_tokens;
|
|
19918
|
+
if (fromCatalog != null && fromCatalog > 0) return fromCatalog;
|
|
19919
|
+
} else if (exuluProvider) {
|
|
19920
|
+
try {
|
|
19921
|
+
const fromProvider = exuluProvider.maxContextLength;
|
|
19922
|
+
if (fromProvider != null && fromProvider > 0) return fromProvider;
|
|
19923
|
+
} catch {
|
|
19924
|
+
}
|
|
19925
|
+
}
|
|
19926
|
+
console.warn(
|
|
19927
|
+
`[EXULU] Unknown context window for model "${modelId}" \u2014 assuming ${DEFAULT_CONTEXT_WINDOW}. Check the LiteLLM catalog / provider template metadata.`
|
|
19928
|
+
);
|
|
19929
|
+
return DEFAULT_CONTEXT_WINDOW;
|
|
19930
|
+
};
|
|
19931
|
+
|
|
19932
|
+
// src/exulu/routes.ts
|
|
19933
|
+
init_context_budget();
|
|
19934
|
+
|
|
19935
|
+
// src/exulu/active-streams.ts
|
|
19936
|
+
init_cjs_shims();
|
|
19937
|
+
var active = /* @__PURE__ */ new Set();
|
|
19938
|
+
var markStreamActive = (sessionID) => {
|
|
19939
|
+
active.add(sessionID);
|
|
19940
|
+
};
|
|
19941
|
+
var clearStreamActive = (sessionID) => {
|
|
19942
|
+
active.delete(sessionID);
|
|
19943
|
+
};
|
|
19944
|
+
var isStreamActive = (sessionID) => active.has(sessionID);
|
|
19945
|
+
|
|
19946
|
+
// src/exulu/compact-session.ts
|
|
19947
|
+
init_cjs_shims();
|
|
19948
|
+
var import_node_crypto6 = require("crypto");
|
|
19949
|
+
var import_ai13 = require("ai");
|
|
19950
|
+
init_truncate_tool_output();
|
|
19951
|
+
init_context_budget();
|
|
19952
|
+
var CompactionInsufficientError = class extends Error {
|
|
19953
|
+
constructor(reason) {
|
|
19954
|
+
super(JSON.stringify({ code: COMPACTION_INSUFFICIENT, message: reason }));
|
|
19955
|
+
this.name = "CompactionInsufficientError";
|
|
19956
|
+
}
|
|
19957
|
+
};
|
|
19958
|
+
var MIN_TAIL_MESSAGES = 2;
|
|
19959
|
+
var SUMMARY_TOOL_OUTPUT_SLICE = 1500;
|
|
19960
|
+
var SUMMARY_TOOL_INPUT_SLICE = 200;
|
|
19961
|
+
var SUMMARY_SYSTEM = `You compress chat histories for an AI assistant. Produce a dense, factual summary of the conversation below. Preserve:
|
|
19962
|
+
- the user's intent and any outstanding requests
|
|
19963
|
+
- key facts, decisions, and constraints
|
|
19964
|
+
- files, artifacts, and session files touched \u2014 ALWAYS keep exact file and item names so they stay retrievable
|
|
19965
|
+
- errors encountered and how they were resolved
|
|
19966
|
+
- pending tasks and the current state of the work
|
|
19967
|
+
Do not invent information. Do not include pleasantries. Write compact prose or bullet points.`;
|
|
19968
|
+
var splitTail = (messages, tailTokenBudget) => {
|
|
19969
|
+
const minTail = Math.min(MIN_TAIL_MESSAGES, messages.length);
|
|
19970
|
+
let cut = messages.length;
|
|
19971
|
+
let tokens = 0;
|
|
19972
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
19973
|
+
const t = estimateMessageTokens(messages[i]);
|
|
19974
|
+
const tailCount = messages.length - i;
|
|
19975
|
+
if (tailCount > minTail && tokens + t > tailTokenBudget) break;
|
|
19976
|
+
tokens += t;
|
|
19977
|
+
cut = i;
|
|
19978
|
+
}
|
|
19979
|
+
return { head: messages.slice(0, cut), tail: messages.slice(cut) };
|
|
19980
|
+
};
|
|
19981
|
+
var serializeForSummary = (messages) => messages.map((m) => {
|
|
19982
|
+
const parts = (m.parts ?? []).map((part) => {
|
|
19983
|
+
const p = part;
|
|
19984
|
+
if (p.type === "text") return p.text ?? "";
|
|
19985
|
+
if (p.type === "file") return `[file: ${p.filename ?? p.url ?? "attachment"}]`;
|
|
19986
|
+
if (p.type === "reasoning" || p.type === "step-start") return "";
|
|
19987
|
+
if (p.type?.startsWith("tool-") || p.type === "dynamic-tool") {
|
|
19988
|
+
const out = p.output?.value ?? p.output;
|
|
19989
|
+
const outText = typeof out === "string" ? out : JSON.stringify(out ?? "");
|
|
19990
|
+
return `[tool ${p.type}: ${JSON.stringify(p.input ?? {}).slice(0, SUMMARY_TOOL_INPUT_SLICE)}] \u2192 ${outText.slice(0, SUMMARY_TOOL_OUTPUT_SLICE)}`;
|
|
19991
|
+
}
|
|
19992
|
+
return "";
|
|
19993
|
+
}).filter(Boolean).join("\n");
|
|
19994
|
+
return `${m.role.toUpperCase()}:
|
|
19995
|
+
${parts}`;
|
|
19996
|
+
}).join("\n\n");
|
|
19997
|
+
var compactSession = async ({
|
|
19998
|
+
sessionID,
|
|
19999
|
+
user,
|
|
20000
|
+
languageModel,
|
|
20001
|
+
contextWindow,
|
|
20002
|
+
steer,
|
|
20003
|
+
modelId,
|
|
20004
|
+
summarize
|
|
20005
|
+
}) => {
|
|
20006
|
+
const budget = deriveContextBudget(contextWindow);
|
|
20007
|
+
const rows = await getAgentMessages({ session: sessionID, user: user.id });
|
|
20008
|
+
const all = await (0, import_ai13.validateUIMessages)({ messages: rows.map((r) => JSON.parse(r.content)) });
|
|
20009
|
+
const history = sliceHistoryAtCheckpoint(all);
|
|
20010
|
+
const { head, tail } = splitTail(history, budget.compactionTailTokens);
|
|
20011
|
+
if (head.length === 0) {
|
|
20012
|
+
throw new CompactionInsufficientError(
|
|
20013
|
+
"There is nothing left to compact \u2014 the recent messages already form the whole context. Start a new chat instead."
|
|
20014
|
+
);
|
|
20015
|
+
}
|
|
20016
|
+
let corpus = serializeForSummary(head);
|
|
20017
|
+
const originalTokens = estimateTokens(corpus);
|
|
20018
|
+
corpus = truncateToolOutput(corpus, contextWindow, "history", 0.3, Math.floor(budget.usableWindow * 0.8) * 4);
|
|
20019
|
+
const system = steer?.trim() ? `${SUMMARY_SYSTEM}
|
|
20020
|
+
|
|
20021
|
+
Focus especially on: ${steer.trim()}` : SUMMARY_SYSTEM;
|
|
20022
|
+
const doSummarize = summarize ?? (async ({ system: sys, prompt, maxOutputTokens }) => {
|
|
20023
|
+
const { text } = await (0, import_ai13.generateText)({
|
|
20024
|
+
model: languageModel,
|
|
20025
|
+
system: sys,
|
|
20026
|
+
prompt,
|
|
20027
|
+
temperature: 0,
|
|
20028
|
+
maxRetries: 2,
|
|
20029
|
+
maxOutputTokens
|
|
20030
|
+
});
|
|
20031
|
+
return text;
|
|
20032
|
+
});
|
|
20033
|
+
const summary = await doSummarize({ system, prompt: corpus, maxOutputTokens: budget.summaryBudgetTokens });
|
|
20034
|
+
const summaryTokens = estimateTokens(summary);
|
|
20035
|
+
let tailTokens = 0;
|
|
20036
|
+
for (const m of tail) tailTokens += estimateMessageTokens(m);
|
|
20037
|
+
const occupancyEstimate = summaryTokens + tailTokens;
|
|
20038
|
+
if (occupancyEstimate >= budget.blockThreshold) {
|
|
20039
|
+
throw new CompactionInsufficientError(
|
|
20040
|
+
"Compacting cannot shrink this conversation below the context limit \u2014 a recent message or output is too large by itself. Start a new chat."
|
|
20041
|
+
);
|
|
20042
|
+
}
|
|
20043
|
+
const compaction = {
|
|
20044
|
+
coversUpTo: head[head.length - 1].id,
|
|
20045
|
+
originalTokens,
|
|
20046
|
+
summaryTokens,
|
|
20047
|
+
occupancyEstimate,
|
|
20048
|
+
...steer?.trim() ? { steer: steer.trim() } : {}
|
|
20049
|
+
};
|
|
20050
|
+
const checkpoint = {
|
|
20051
|
+
id: `compaction_${(0, import_node_crypto6.randomUUID)()}`,
|
|
20052
|
+
role: "user",
|
|
20053
|
+
parts: [{ type: "text", text: `[Conversation summary \u2014 earlier messages were compacted]
|
|
20054
|
+
|
|
20055
|
+
${summary}` }],
|
|
20056
|
+
metadata: { compaction }
|
|
20057
|
+
};
|
|
20058
|
+
await saveChat({ session: sessionID, user: user.id, messages: [checkpoint], ...modelId ? { model: modelId } : {} });
|
|
20059
|
+
return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
|
|
20060
|
+
};
|
|
20061
|
+
|
|
19386
20062
|
// src/exulu/transcribe.ts
|
|
19387
20063
|
init_cjs_shims();
|
|
19388
20064
|
var TranscriptionError = class extends Error {
|
|
@@ -19757,7 +20433,7 @@ function checkApiKeyScope(user, agentId) {
|
|
|
19757
20433
|
// src/exulu/openai-gateway.ts
|
|
19758
20434
|
init_cjs_shims();
|
|
19759
20435
|
var import_express2 = require("express");
|
|
19760
|
-
var
|
|
20436
|
+
var import_ai14 = require("ai");
|
|
19761
20437
|
|
|
19762
20438
|
// src/exulu/openai-transformer.ts
|
|
19763
20439
|
init_cjs_shims();
|
|
@@ -19845,7 +20521,7 @@ function transformCompletion(text, inputTokens, outputTokens, ctx) {
|
|
|
19845
20521
|
}
|
|
19846
20522
|
|
|
19847
20523
|
// src/exulu/openai-gateway.ts
|
|
19848
|
-
var
|
|
20524
|
+
var import_node_crypto7 = require("crypto");
|
|
19849
20525
|
var import_crypto_js8 = require("crypto-js");
|
|
19850
20526
|
var import_express3 = __toESM(require("express"), 1);
|
|
19851
20527
|
init_client();
|
|
@@ -19853,6 +20529,9 @@ init_convert_exulu_tools_to_ai_sdk_tools();
|
|
|
19853
20529
|
init_statistics2();
|
|
19854
20530
|
init_statistics();
|
|
19855
20531
|
init_resolve_model();
|
|
20532
|
+
init_sanitize_tool_name();
|
|
20533
|
+
init_context_budget();
|
|
20534
|
+
init_supervisor();
|
|
19856
20535
|
function convertOpenAIToolsToAiSdkTools(tools) {
|
|
19857
20536
|
return Object.fromEntries(
|
|
19858
20537
|
tools.map((t) => {
|
|
@@ -19861,7 +20540,7 @@ function convertOpenAIToolsToAiSdkTools(tools) {
|
|
|
19861
20540
|
t.function.name,
|
|
19862
20541
|
{
|
|
19863
20542
|
description: t.function.description ?? "",
|
|
19864
|
-
inputSchema: (0,
|
|
20543
|
+
inputSchema: (0, import_ai14.jsonSchema)({
|
|
19865
20544
|
type: "object",
|
|
19866
20545
|
properties: params.properties ?? {},
|
|
19867
20546
|
...params.required ? { required: params.required } : {}
|
|
@@ -20164,6 +20843,10 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
|
|
|
20164
20843
|
}
|
|
20165
20844
|
const providerapikey = resolved.apiKey;
|
|
20166
20845
|
const languageModel = resolved.languageModel;
|
|
20846
|
+
const contextWindow = await resolveContextWindow({
|
|
20847
|
+
modelId: resolved.model.id,
|
|
20848
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
20849
|
+
});
|
|
20167
20850
|
const disabledTools = req.body.disabledTools ?? [];
|
|
20168
20851
|
const enabledTools = await getEnabledTools(
|
|
20169
20852
|
agent,
|
|
@@ -20188,12 +20871,50 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
|
|
|
20188
20871
|
project?.id,
|
|
20189
20872
|
void 0,
|
|
20190
20873
|
languageModel,
|
|
20191
|
-
agent
|
|
20874
|
+
agent,
|
|
20875
|
+
void 0,
|
|
20876
|
+
contextWindow,
|
|
20877
|
+
disabledTools
|
|
20878
|
+
);
|
|
20879
|
+
const gatewayAgenticEntry = enabledTools?.find((t) => t.id === "agentic_context_search");
|
|
20880
|
+
const gatewayAgenticKey = gatewayAgenticEntry ? sanitizeToolName(gatewayAgenticEntry.name) : void 0;
|
|
20881
|
+
const gatewayRetrievalGuard = retrievalBudgetGuard(
|
|
20882
|
+
resolveRetrievalCallBudget(agent.tools),
|
|
20883
|
+
gatewayAgenticKey,
|
|
20884
|
+
Object.keys(convertedTools)
|
|
20192
20885
|
);
|
|
20886
|
+
const turnBudget = resolveTurnStepBudget(void 0, agent);
|
|
20193
20887
|
const clientTools = Array.isArray(req.body.tools) ? req.body.tools : [];
|
|
20194
20888
|
const activeTools = clientTools.length > 0 ? convertOpenAIToolsToAiSdkTools(clientTools) : convertedTools;
|
|
20195
20889
|
const openaiMessages = req.body.messages ?? [];
|
|
20196
20890
|
const { systemPrompt: requestSystemPrompt, coreMessages } = convertOpenAIMessagesToModelMessages(openaiMessages);
|
|
20891
|
+
const gatewayBudget = deriveContextBudget(contextWindow);
|
|
20892
|
+
const IMAGE_TOKEN_ALLOWANCE = 1e3;
|
|
20893
|
+
let imageCount = 0;
|
|
20894
|
+
const textOnlyMessages = openaiMessages.map((m) => {
|
|
20895
|
+
if (!Array.isArray(m.content)) return m;
|
|
20896
|
+
return {
|
|
20897
|
+
...m,
|
|
20898
|
+
content: m.content.map((p) => {
|
|
20899
|
+
if (p && typeof p === "object" && "image_url" in p && p.image_url) {
|
|
20900
|
+
imageCount += 1;
|
|
20901
|
+
return { ...p, image_url: { url: "[image]" } };
|
|
20902
|
+
}
|
|
20903
|
+
return p;
|
|
20904
|
+
})
|
|
20905
|
+
};
|
|
20906
|
+
});
|
|
20907
|
+
const promptTokens = estimateTokens(JSON.stringify(textOnlyMessages)) + imageCount * IMAGE_TOKEN_ALLOWANCE;
|
|
20908
|
+
if (promptTokens >= gatewayBudget.blockThreshold) {
|
|
20909
|
+
res.status(400).json({
|
|
20910
|
+
error: {
|
|
20911
|
+
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.`,
|
|
20912
|
+
type: "invalid_request_error",
|
|
20913
|
+
code: "context_length_exceeded"
|
|
20914
|
+
}
|
|
20915
|
+
});
|
|
20916
|
+
return;
|
|
20917
|
+
}
|
|
20197
20918
|
const agentInstructions = agent.instructions ?? "";
|
|
20198
20919
|
const systemParts = [
|
|
20199
20920
|
agentInstructions ? `You are an agent named: ${agent.name}
|
|
@@ -20203,7 +20924,7 @@ ${project.description}` : ""}` : "",
|
|
|
20203
20924
|
requestSystemPrompt
|
|
20204
20925
|
].filter(Boolean);
|
|
20205
20926
|
const systemPrompt = systemParts.join("\n\n");
|
|
20206
|
-
const completionId = `chatcmpl-${(0,
|
|
20927
|
+
const completionId = `chatcmpl-${(0, import_node_crypto7.randomUUID)()}`;
|
|
20207
20928
|
const created = Math.floor(Date.now() / 1e3);
|
|
20208
20929
|
const hasTools = Object.keys(activeTools).length > 0;
|
|
20209
20930
|
const ctx = { completionId, created, modelId };
|
|
@@ -20211,13 +20932,14 @@ ${project.description}` : ""}` : "",
|
|
|
20211
20932
|
res.setHeader("Content-Type", "text/event-stream");
|
|
20212
20933
|
res.setHeader("Cache-Control", "no-cache");
|
|
20213
20934
|
res.setHeader("Connection", "keep-alive");
|
|
20214
|
-
const result = (0,
|
|
20935
|
+
const result = (0, import_ai14.streamText)({
|
|
20215
20936
|
model: languageModel,
|
|
20216
20937
|
system: systemPrompt || void 0,
|
|
20217
20938
|
messages: coreMessages,
|
|
20218
20939
|
tools: hasTools ? activeTools : void 0,
|
|
20219
20940
|
maxRetries: 2,
|
|
20220
|
-
|
|
20941
|
+
prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
|
|
20942
|
+
stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai14.stepCountIs)(turnBudget)],
|
|
20221
20943
|
onError: (error) => {
|
|
20222
20944
|
console.error("[OPENAI GATEWAY] stream error:", error);
|
|
20223
20945
|
}
|
|
@@ -20250,13 +20972,14 @@ ${project.description}` : ""}` : "",
|
|
|
20250
20972
|
const usage = await result.usage;
|
|
20251
20973
|
await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
|
|
20252
20974
|
} else {
|
|
20253
|
-
const { text, usage } = await (0,
|
|
20975
|
+
const { text, usage } = await (0, import_ai14.generateText)({
|
|
20254
20976
|
model: languageModel,
|
|
20255
20977
|
system: systemPrompt || void 0,
|
|
20256
20978
|
messages: coreMessages,
|
|
20257
20979
|
tools: hasTools ? activeTools : void 0,
|
|
20258
20980
|
maxRetries: 2,
|
|
20259
|
-
|
|
20981
|
+
prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
|
|
20982
|
+
stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai14.stepCountIs)(turnBudget)]
|
|
20260
20983
|
});
|
|
20261
20984
|
res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
|
|
20262
20985
|
await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
|
|
@@ -20375,7 +21098,7 @@ init_flow();
|
|
|
20375
21098
|
|
|
20376
21099
|
// src/exulu/recall/verify.ts
|
|
20377
21100
|
init_cjs_shims();
|
|
20378
|
-
var
|
|
21101
|
+
var import_node_crypto8 = require("crypto");
|
|
20379
21102
|
var TOLERANCE_SECONDS = 5 * 60;
|
|
20380
21103
|
var header = (headers, ...names) => {
|
|
20381
21104
|
for (const name of names) {
|
|
@@ -20393,7 +21116,7 @@ var safeEqual = (a, b) => {
|
|
|
20393
21116
|
const bufA = Buffer.from(a);
|
|
20394
21117
|
const bufB = Buffer.from(b);
|
|
20395
21118
|
if (bufA.length !== bufB.length) return false;
|
|
20396
|
-
return (0,
|
|
21119
|
+
return (0, import_node_crypto8.timingSafeEqual)(bufA, bufB);
|
|
20397
21120
|
};
|
|
20398
21121
|
var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()) => {
|
|
20399
21122
|
if (!secret) {
|
|
@@ -20415,7 +21138,7 @@ var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()
|
|
|
20415
21138
|
}
|
|
20416
21139
|
const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
|
|
20417
21140
|
const signedContent = `${id}.${timestamp}.${body}`;
|
|
20418
|
-
const expected = (0,
|
|
21141
|
+
const expected = (0, import_node_crypto8.createHmac)("sha256", secretKey(secret)).update(signedContent).digest("base64");
|
|
20419
21142
|
const passed = signatureHeader.split(" ").some((entry) => {
|
|
20420
21143
|
const [, sig] = entry.split(",");
|
|
20421
21144
|
return !!sig && safeEqual(sig, expected);
|
|
@@ -20470,6 +21193,155 @@ var contentHeadersFor = (key, contentType, filename) => {
|
|
|
20470
21193
|
};
|
|
20471
21194
|
var getSharedArtifactByName = (db2, name) => db2("shared_artifacts").where({ name }).first();
|
|
20472
21195
|
|
|
21196
|
+
// src/skills/skill-access.ts
|
|
21197
|
+
init_cjs_shims();
|
|
21198
|
+
init_check_record_access();
|
|
21199
|
+
async function resolveSkillByName(db2, name) {
|
|
21200
|
+
const row = await db2("skills").where({ name }).first();
|
|
21201
|
+
return row ?? null;
|
|
21202
|
+
}
|
|
21203
|
+
async function canAccessSkill(db2, skill, action, user) {
|
|
21204
|
+
const rbac = await RBACResolver(db2, "skill", skill.id, skill.rights_mode || "private");
|
|
21205
|
+
return checkRecordAccess({ ...skill, RBAC: rbac }, action, user);
|
|
21206
|
+
}
|
|
21207
|
+
async function filterReadableSkills(db2, skills, user) {
|
|
21208
|
+
const out = [];
|
|
21209
|
+
for (const s of skills) {
|
|
21210
|
+
if (await canAccessSkill(db2, s, "read", user)) out.push(s);
|
|
21211
|
+
}
|
|
21212
|
+
return out;
|
|
21213
|
+
}
|
|
21214
|
+
|
|
21215
|
+
// src/skills/bootstrap/exulu-skills.ts
|
|
21216
|
+
init_cjs_shims();
|
|
21217
|
+
|
|
21218
|
+
// src/skills/bootstrap/clients.ts
|
|
21219
|
+
init_cjs_shims();
|
|
21220
|
+
var CLIENT_MANIFEST = [
|
|
21221
|
+
{ id: "agents", dir: ".agents/skills" },
|
|
21222
|
+
// cross-agent standard (symlink canonical store)
|
|
21223
|
+
{ id: "claude", dir: ".claude/skills" },
|
|
21224
|
+
{ id: "windsurf", dir: ".windsurf/skills" },
|
|
21225
|
+
{ id: "continue", dir: ".continue/skills" },
|
|
21226
|
+
{ id: "roo", dir: ".roo/skills" },
|
|
21227
|
+
{ id: "kilocode", dir: ".kilocode/skills" },
|
|
21228
|
+
{ id: "crush", dir: ".crush/skills" },
|
|
21229
|
+
{ id: "goose", dir: ".goose/skills" },
|
|
21230
|
+
{ id: "qwen", dir: ".qwen/skills" },
|
|
21231
|
+
{ id: "iflow", dir: ".iflow/skills" },
|
|
21232
|
+
{ id: "junie", dir: ".junie/skills" },
|
|
21233
|
+
{ id: "kiro", dir: ".kiro/skills" },
|
|
21234
|
+
{ id: "trae", dir: ".trae/skills" },
|
|
21235
|
+
{ id: "augment", dir: ".augment/skills" },
|
|
21236
|
+
{ id: "factory", dir: ".factory/skills" },
|
|
21237
|
+
{ id: "devin", dir: ".devin/skills" },
|
|
21238
|
+
{ id: "openhands", dir: ".openhands/skills" },
|
|
21239
|
+
{ id: "pi", dir: ".pi/skills" },
|
|
21240
|
+
{ id: "cortex", dir: ".cortex/skills" },
|
|
21241
|
+
{ id: "zencoder", dir: ".zencoder/skills" },
|
|
21242
|
+
{ id: "codebuddy", dir: ".codebuddy/skills" },
|
|
21243
|
+
{ id: "codestudio", dir: ".codestudio/skills" },
|
|
21244
|
+
{ id: "commandcode", dir: ".commandcode/skills" },
|
|
21245
|
+
{ id: "codemaker", dir: ".codemaker/skills" },
|
|
21246
|
+
{ id: "codeartsdoer", dir: ".codeartsdoer/skills" },
|
|
21247
|
+
{ id: "lingma", dir: ".lingma/skills" },
|
|
21248
|
+
{ id: "qoder", dir: ".qoder/skills" },
|
|
21249
|
+
{ id: "rovodev", dir: ".rovodev/skills" },
|
|
21250
|
+
{ id: "moxby", dir: ".moxby/skills" },
|
|
21251
|
+
{ id: "mux", dir: ".mux/skills" },
|
|
21252
|
+
{ id: "neovate", dir: ".neovate/skills" },
|
|
21253
|
+
{ id: "ona", dir: ".ona/skills" },
|
|
21254
|
+
{ id: "pochi", dir: ".pochi/skills" },
|
|
21255
|
+
{ id: "reasonix", dir: ".reasonix/skills" },
|
|
21256
|
+
{ id: "terramind", dir: ".terramind/skills" },
|
|
21257
|
+
{ id: "tinycloud", dir: ".tinycloud/skills" },
|
|
21258
|
+
{ id: "vibe", dir: ".vibe/skills" },
|
|
21259
|
+
{ id: "adal", dir: ".adal/skills" },
|
|
21260
|
+
{ id: "aider-desk", dir: ".aider-desk/skills" },
|
|
21261
|
+
{ id: "autohand", dir: ".autohand/skills" },
|
|
21262
|
+
{ id: "bob", dir: ".bob/skills" },
|
|
21263
|
+
{ id: "hermes", dir: ".hermes/skills" },
|
|
21264
|
+
{ id: "inferencesh", dir: ".inferencesh/skills" },
|
|
21265
|
+
{ id: "jazz", dir: ".jazz/skills" },
|
|
21266
|
+
{ id: "kode", dir: ".kode/skills" },
|
|
21267
|
+
{ id: "mcpjam", dir: ".mcpjam/skills" },
|
|
21268
|
+
{ id: "forge", dir: ".forge/skills" },
|
|
21269
|
+
{ id: "tabnine", dir: ".tabnine/agent/skills" }
|
|
21270
|
+
// exception: nested under agent/
|
|
21271
|
+
];
|
|
21272
|
+
|
|
21273
|
+
// src/skills/bootstrap/exulu-sh.generated.ts
|
|
21274
|
+
init_cjs_shims();
|
|
21275
|
+
var EXULU_SH_B64 = "IyEvYmluL3NoCiMgZXh1bHUg4oCUIGhlbHBlciBmb3IgdGhlIEV4dWx1IGNlbnRyYWwgc2tpbGwgbGlicmFyeS4gVGhlIGFnZW50IGludm9rZXMgdGhpcwojIChuZXZlciByYXcgY3VybCk6IHRoZSB0b2tlbiBpcyByZWFkIGZyb20gdGhlIGNvbmZpZyBmaWxlIGhlcmUgYW5kIHNlbnQgdmlhCiMgYHgtYXBpLWtleTogQmVhcmVyYCwgc28gaXQgbmV2ZXIgZW50ZXJzIHRoZSBtb2RlbCBjb250ZXh0LiBDbGllbnQgZmFuLW91dAojIChjb3B5L3N5bWxpbmsgYWNyb3NzIGFnZW50IGNsaWVudHMpIGlzIGRldGVybWluaXN0aWMuCnNldCAtZXUKCkNPTkZJR19ESVI9IiRIT01FLy5jb25maWcvZXh1bHUiCkNPTkZJR19GSUxFPSIkQ09ORklHX0RJUi9za2lsbHMuanNvbiIKCmRpZSgpIHsgcHJpbnRmICdleHVsdTogJXNcbicgIiQqIiA+JjI7IGV4aXQgMTsgfQppbmZvKCkgeyBwcmludGYgJyVzXG4nICIkKiIgPiYyOyB9Cgpqc29uX3N0cigpIHsgIyBqc29uX3N0ciA8a2V5PiA8ZmlsZT4g4oCUIGZsYXQgImtleSI6InZhbHVlIgogIHNlZCAtbiAicy8uKlwiJDFcIltbOnNwYWNlOl1dKjpbWzpzcGFjZTpdXSpcIlxcKFteXCJdKlxcKVwiLiovXFwxL3AiICIkMiIgfCBoZWFkIC1uMQp9CgpbIC1mICIkQ09ORklHX0ZJTEUiIF0gfHwgZGllICJub3QgY29uZmlndXJlZCDigJQgcnVuOiBjdXJsIC1mc1NMIDxiYXNlX3VybD4vYXBpL3NraWxscy9pbnN0YWxsLnNoIHwgc2giCgpCQVNFX1VSTD0iJChqc29uX3N0ciBiYXNlX3VybCAiJENPTkZJR19GSUxFIikiCkJBQ0tFTkQ9IiQoanNvbl9zdHIgYmFja2VuZCAiJENPTkZJR19GSUxFIikiCkFQSV9LRVk9IiQoanNvbl9zdHIgYXBpX2tleSAiJENPTkZJR19GSUxFIikiCkxJTktfTU9ERT0iJChqc29uX3N0ciBsaW5rX21vZGUgIiRDT05GSUdfRklMRSIpIgpTQ09QRT0iJChqc29uX3N0ciBzY29wZSAiJENPTkZJR19GSUxFIikiCkNMSUVOVFM9IiQoc2VkIC1uICdzLy4qImNsaWVudHMiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKlxbXChbXl1dKlwpXF0uKi9cMS9wJyAiJENPTkZJR19GSUxFIiB8IHRyICcsJyAnICcgfCB0ciAtZCAnIicgfCB0ciAtcyAnICcpIgoKWyAtbiAiJENMSUVOVFMiIF0gfHwgQ0xJRU5UUz0iYWdlbnRzIgpbIC1uICIkTElOS19NT0RFIiBdIHx8IExJTktfTU9ERT0iY29weSIKWyAtbiAiJFNDT1BFIiBdIHx8IFNDT1BFPSJwcm9qZWN0IgoKaWYgWyAteiAiJEJBQ0tFTkQiIF07IHRoZW4KICBbIC1uICIkQkFTRV9VUkwiIF0gfHwgZGllICJjb25maWcgaGFzIG5vIGJhY2tlbmQgYW5kIG5vIGJhc2VfdXJsIgogIEJBQ0tFTkQ9IiQoY3VybCAtZnNTTCAiJEJBU0VfVVJML2FwaS9jb25maWciIHwgc2VkIC1uICdzLy4qImJhY2tlbmQiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKiJcKFteIl0qXCkiLiovXDEvcCcgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJEJBQ0tFTkQiIF0gfHwgZGllICJjb3VsZCBub3QgcmVzb2x2ZSBiYWNrZW5kIGZyb20gJEJBU0VfVVJML2FwaS9jb25maWciCmZpCkJBQ0tFTkQ9IiQocHJpbnRmICclcycgIiRCQUNLRU5EIiB8IHNlZCAnczovKiQ6OicpIgoKUk9PVD0iJFBXRCIKWyAiJFNDT1BFIiA9ICJob21lIiBdICYmIFJPT1Q9IiRIT01FIgoKIyBkaXJfZm9yIENMSUVOVF9JRCAtPiByZWxhdGl2ZSBza2lsbCBkaXIgKGdlbmVyYXRlZCBmcm9tIHRoZSBtYW5pZmVzdCBpbiB0aGUKIyByZWFsIGJ1aWxkOyBhIHJlcHJlc2VudGF0aXZlIHN1YnNldCBoZXJlIGZvciBsb2NhbCB0ZXN0aW5nKS4KZGlyX2ZvcigpIHsKICBjYXNlICIkMSIgaW4KX19ESVJfRk9SX0NBU0VTX18KICAgICopIHJldHVybiAxIDs7CiAgZXNhYwp9CgphcGkoKSB7ICMgYXBpIDxNRVRIT0Q+IDxwYXRoPiBbZXh0cmEgY3VybCBhcmdzLi4uXSAtPiBib2R5IG9uIHN0ZG91dAogIG09IiQxIjsgcD0iJDIiOyBzaGlmdCAyCiAgY3VybCAtZnNTIC1YICIkbSIgIiRCQUNLRU5EJHAiIC1IICJ4LWFwaS1rZXk6IEJlYXJlciAkQVBJX0tFWSIgIiRAIgp9CgptZXRhX3ZlcnNpb24oKSB7ICMgbWV0YV92ZXJzaW9uIDxuYW1lPiAtPiBjdXJyZW50X3ZlcnNpb24gZnJvbSByZWdpc3RyeQogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJDEiIDI+L2Rldi9udWxsIFwKICAgIHwgc2VkIC1uICdzLy4qImN1cnJlbnRfdmVyc2lvbiJbWzpzcGFjZTpdXSo6W1s6c3BhY2U6XV0qXChbMC05XVswLTldKlwpLiovXDEvcCcgfCBoZWFkIC1uMQp9CgpfbWFuYWdlZCgpIHsgIyBfbWFuYWdlZCA8ZGlyPiAtPiAwIGlmIHNhZmUgdG8gb3ZlcndyaXRlIChvdXJzIG9yIHN5bWxpbmsgb3IgYWJzZW50KQogIGQ9IiQxIgogIGlmIFsgLWUgIiRkIiBdICYmIFsgISAtTCAiJGQiIF0gJiYgWyAhIC1mICIkZC8uZXh1bHUtc2tpbGwuanNvbiIgXTsgdGhlbgogICAgaW5mbyAic2tpcCAkZCAoZXhpc3RzLCBub3QgbWFuYWdlZCBieSBleHVsdSkiOyByZXR1cm4gMQogIGZpCiAgcmV0dXJuIDAKfQoKX3B1dF9yZWFsKCkgeyAjIF9wdXRfcmVhbCA8ZGVzdD4gPHNyY2Rpcj4gPG1hcmtlci1qc29uPgogIF9tYW5hZ2VkICIkMSIgfHwgcmV0dXJuIDAKICBybSAtcmYgIiQxIjsgbWtkaXIgLXAgIiQxIjsgY3AgLVIgIiQyLy4iICIkMS8iCiAgcHJpbnRmICclc1xuJyAiJDMiID4gIiQxLy5leHVsdS1za2lsbC5qc29uIgp9CgpwbGFjZV9za2lsbCgpIHsgIyBwbGFjZV9za2lsbCA8bmFtZT4gPHNyY2Rpcj4gPHZlcnNpb24+CiAgcG5hbWU9IiQxIjsgcHNyYz0iJDIiOyBwdmVyPSIkezM6LTF9IgogIG1hcmtlcj0neyAibmFtZSI6ICInIiRwbmFtZSInIiwgInZlcnNpb24iOiAnIiRwdmVyIicsICJzb3VyY2UiOiAiJyIkQkFDS0VORCInIiB9JwogIGNhbm9uPSIkUk9PVC8uYWdlbnRzL3NraWxscy8kcG5hbWUiCiAgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBfcHV0X3JlYWwgIiRjYW5vbiIgIiRwc3JjIiAiJG1hcmtlciIKICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICBkPSIkKGRpcl9mb3IgIiRpZCIpIiB8fCB7IGluZm8gInVua25vd24gY2xpZW50OiAkaWQiOyBjb250aW51ZTsgfQogICAgcGFyZW50PSIkUk9PVC8kZCI7IGRlc3Q9IiRwYXJlbnQvJHBuYW1lIgogICAgaWYgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBbICIkaWQiICE9ICJhZ2VudHMiIF07IHRoZW4KICAgICAgX21hbmFnZWQgIiRkZXN0IiB8fCBjb250aW51ZQogICAgICBta2RpciAtcCAiJHBhcmVudCI7IHJtIC1yZiAiJGRlc3QiCiAgICAgIGlmIGxuIC1zICIkY2Fub24iICIkZGVzdCIgMj4vZGV2L251bGw7IHRoZW4KICAgICAgICBpbmZvICJsaW5rZWQgJGRlc3QgLT4gJGNhbm9uIgogICAgICBlbHNlCiAgICAgICAgaW5mbyAic3ltbGluayB1bnN1cHBvcnRlZCBhdCAkZGVzdDsgY29weWluZyIKICAgICAgICBfcHV0X3JlYWwgIiRkZXN0IiAiJHBzcmMiICIkbWFya2VyIgogICAgICBmaQogICAgZWxpZiBbICIkTElOS19NT0RFIiA9ICJjb3B5IiBdOyB0aGVuCiAgICAgIF9wdXRfcmVhbCAiJGRlc3QiICIkcHNyYyIgIiRtYXJrZXIiCiAgICBmaQogICAgIyBzeW1saW5rICsgYWdlbnRzOiBhbHJlYWR5IHBsYWNlZCBhcyB0aGUgY2Fub25pY2FsIHN0b3JlIGFib3ZlLgogIGRvbmUKfQoKaW5zdGFsbGVkX25hbWVzKCkgeyAjIHVuaXF1ZSBza2lsbCBuYW1lcyB0aGF0IGNhcnJ5IG91ciBtYXJrZXIgdW5kZXIgUk9PVAogIHsgZmluZCAiJFJPT1QvLmFnZW50cy9za2lsbHMiIC1tYXhkZXB0aCAyIC1uYW1lIC5leHVsdS1za2lsbC5qc29uIDI+L2Rldi9udWxsCiAgICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICAgIGQ9IiQoZGlyX2ZvciAiJGlkIikiIHx8IGNvbnRpbnVlCiAgICAgIGZpbmQgIiRST09ULyRkIiAtbWF4ZGVwdGggMiAtbmFtZSAuZXh1bHUtc2tpbGwuanNvbiAyPi9kZXYvbnVsbAogICAgZG9uZQogIH0gfCB3aGlsZSByZWFkIC1yIG07IGRvIGpzb25fc3RyIG5hbWUgIiRtIjsgZG9uZSB8IHNvcnQgLXUKfQoKbWFya2VyX3ZlcnNpb24oKSB7ICMgbWFya2VyX3ZlcnNpb24gPG5hbWU+CiAgZm9yIGJhc2UgaW4gIiRST09ULy5hZ2VudHMvc2tpbGxzIjsgZG8KICAgIFsgLWYgIiRiYXNlLyQxLy5leHVsdS1za2lsbC5qc29uIiBdICYmIHsganNvbl9zdHIgdmVyc2lvbiAiJGJhc2UvJDEvLmV4dWx1LXNraWxsLmpzb24iOyByZXR1cm47IH0KICBkb25lCiAgZm9yIGlkIGluICRDTElFTlRTOyBkbwogICAgZD0iJChkaXJfZm9yICIkaWQiKSIgfHwgY29udGludWUKICAgIGY9IiRST09ULyRkLyQxLy5leHVsdS1za2lsbC5qc29uIgogICAgWyAtZiAiJGYiIF0gJiYgeyBqc29uX3N0ciB2ZXJzaW9uICIkZiI7IHJldHVybjsgfQogIGRvbmUKfQoKZG9faW5zdGFsbCgpIHsgIyBkb19pbnN0YWxsIDxuYW1lPgogIG5hbWU9IiQxIgogIFRNUD0iJChta3RlbXAgLWQpIgogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJG5hbWUvZG93bmxvYWQiIC0tb3V0cHV0ICIkVE1QL3NraWxsLnppcCIgXAogICAgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImRvd25sb2FkIGZhaWxlZCBmb3IgJyRuYW1lJyAoNDAzID0gbm8gYWNjZXNzLCA0MDQgPSB1bmtub3duKSI7IH0KICBta2RpciAtcCAiJFRNUC94IgogIHVuemlwIC1xICIkVE1QL3NraWxsLnppcCIgLWQgIiRUTVAveCIgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImJhZCBhcmNoaXZlIGZvciAnJG5hbWUnIjsgfQogIHNyYz0iJChmaW5kICIkVE1QL3giIC1taW5kZXB0aCAxIC1tYXhkZXB0aCAxIC10eXBlIGQgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJHNyYyIgXSB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAidW5leHBlY3RlZCBhcmNoaXZlIGxheW91dCBmb3IgJyRuYW1lJyI7IH0KICB2ZXI9IiQobWV0YV92ZXJzaW9uICIkbmFtZSIpIgogIHBsYWNlX3NraWxsICIkbmFtZSIgIiRzcmMiICIke3ZlcjotMX0iCiAgcm0gLXJmICIkVE1QIgogIGluZm8gImluc3RhbGxlZCAkbmFtZSAodiR7dmVyOi0xfSkgWyRMSU5LX01PREVdIGludG86ICRDTElFTlRTIgp9Cgp1c2FnZSgpIHsKICBjYXQgPiYyIDw8RU9GCmV4dWx1IOKAlCBFeHVsdSBza2lsbCBsaWJyYXJ5IGhlbHBlcgogIGV4dWx1IGxpc3QgICAgICAgICAgICAgICAgIGxpc3Qgc2tpbGxzIHlvdSBjYW4gYWNjZXNzIChKU09OKQogIGV4dWx1IGdldCA8bmFtZT4gICAgICAgICAgIHNob3cgb25lIHNraWxsJ3MgbWV0YWRhdGEgKEpTT04pCiAgZXh1bHUgaW5zdGFsbCA8bmFtZT4gICAgICAgaW5zdGFsbC9yZWZyZXNoIGEgc2tpbGwgaW50byB5b3VyIGFnZW50IGNsaWVudHMKICBleHVsdSB1cGRhdGUgWzxuYW1lPl0gICAgICB1cGRhdGUgaW5zdGFsbGVkIHNraWxscyAoYWxsLCBvciBvbmUpIHRvIGxhdGVzdAogIGV4dWx1IHB1Ymxpc2ggPG5hbWU+IDxkaXI+IHB1Ymxpc2ggYSBsb2NhbCBza2lsbCBmb2xkZXIgYXMgPG5hbWU+CiAgZXh1bHUgY29uZmlnICAgICAgICAgICAgICAgc2hvdyByZXNvbHZlZCBiYWNrZW5kIC8gc2NvcGUgLyBjbGllbnRzIChubyBzZWNyZXRzKQpFT0YKfQoKY21kPSIkezE6LWhlbHB9IgpbICQjIC1ndCAwIF0gJiYgc2hpZnQgfHwgdHJ1ZQoKY2FzZSAiJGNtZCIgaW4KICBsaXN0KSBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5IiA7OwogIGdldCkgWyAkIyAtZ2UgMSBdIHx8IGRpZSAidXNhZ2U6IGV4dWx1IGdldCA8bmFtZT4iOyBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5LyQxIiA7OwogIGluc3RhbGwpIFsgJCMgLWdlIDEgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBpbnN0YWxsIDxuYW1lPiI7IGRvX2luc3RhbGwgIiQxIiA7OwogIHVwZGF0ZSkKICAgIGlmIFsgJCMgLWdlIDEgXTsgdGhlbiBuYW1lcz0iJDEiOyBlbHNlIG5hbWVzPSIkKGluc3RhbGxlZF9uYW1lcykiOyBmaQogICAgWyAtbiAiJG5hbWVzIiBdIHx8IHsgaW5mbyAibm8gZXh1bHUtbWFuYWdlZCBza2lsbHMgZm91bmQgdW5kZXIgJFJPT1QiOyBleGl0IDA7IH0KICAgIGZvciBuIGluICRuYW1lczsgZG8KICAgICAgY3VyPSIkKG1hcmtlcl92ZXJzaW9uICIkbiIpIgogICAgICBsYXRlc3Q9IiQobWV0YV92ZXJzaW9uICIkbiIpIgogICAgICBbIC1uICIkbGF0ZXN0IiBdIHx8IHsgaW5mbyAic2tpcCAkbiAobm90IGluIHJlZ2lzdHJ5KSI7IGNvbnRpbnVlOyB9CiAgICAgIGlmIFsgLXogIiRjdXIiIF0gfHwgWyAiJGxhdGVzdCIgLWd0ICIkY3VyIiBdIDI+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgaW5mbyAidXBkYXRpbmcgJG46IHYke2N1cjotP30gLT4gdiRsYXRlc3QiOyBkb19pbnN0YWxsICIkbiIKICAgICAgZWxzZQogICAgICAgIGluZm8gIiRuIHVwIHRvIGRhdGUgKHYkY3VyKSIKICAgICAgZmkKICAgIGRvbmUKICAgIDs7CiAgcHVibGlzaCkKICAgIFsgJCMgLWdlIDIgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBwdWJsaXNoIDxuYW1lPiA8ZGlyPiIKICAgIG5hbWU9IiQxIjsgZm9sZGVyPSIkMiIKICAgIFsgLWQgIiRmb2xkZXIiIF0gfHwgZGllICJubyBzdWNoIGZvbGRlcjogJGZvbGRlciIKICAgIFsgLWYgIiRmb2xkZXIvU0tJTEwubWQiIF0gfHwgZGllICIkZm9sZGVyIGhhcyBubyBTS0lMTC5tZCBhdCBpdHMgcm9vdCIKICAgIGNvbW1hbmQgLXYgemlwID4vZGV2L251bGwgMj4mMSB8fCBkaWUgInRoZSAnemlwJyBjb21tYW5kIGlzIHJlcXVpcmVkIHRvIHB1Ymxpc2giCiAgICBUTVA9IiQobWt0ZW1wIC1kKSIKICAgICggY2QgIiQoZGlybmFtZSAiJGZvbGRlciIpIiBcCiAgICAgICYmIHppcCAtcSAtciAtWCAiJFRNUC9za2lsbC56aXAiICIkKGJhc2VuYW1lICIkZm9sZGVyIikiIFwKICAgICAgICAgICAteCAnKi8uZXh1bHUtc2tpbGwuanNvbicgJyovLmdpdC8qJyAnKi5EU19TdG9yZScgJyovX19NQUNPU1gvKicgKSBcCiAgICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJjb3VsZCBub3QgemlwICRmb2xkZXIiOyB9CiAgICBhcGkgUE9TVCAiL3NraWxscy9yZWdpc3RyeS8kbmFtZSIgLUggIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vemlwIiBcCiAgICAgIC0tZGF0YS1iaW5hcnkgQCIkVE1QL3NraWxsLnppcCIgXAogICAgICB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAicHVibGlzaCBmYWlsZWQgKDQwMyA9IG5vIHdyaXRlIGFjY2VzcywgNDA5ID0gbmFtZSB0YWtlbikiOyB9CiAgICBybSAtcmYgIiRUTVAiCiAgICBpbmZvICJwdWJsaXNoZWQgJG5hbWUiCiAgICA7OwogIGNvbmZpZykKICAgIHByaW50ZiAnYmFja2VuZD0lc1xuc2NvcGU9JXNcbmxpbmtfbW9kZT0lc1xuY2xpZW50cz0lc1xuYXBpX2tleT0lc1xuJyBcCiAgICAgICIkQkFDS0VORCIgIiRTQ09QRSIgIiRMSU5LX01PREUiICIkQ0xJRU5UUyIgXAogICAgICAiJChbIC1uICIkQVBJX0tFWSIgXSAmJiBlY2hvIHNldCB8fCBlY2hvIE1JU1NJTkcpIgogICAgOzsKICBoZWxwfC0taGVscHwtaCkgdXNhZ2UgOzsKICAqKSBpbmZvICJ1bmtub3duIGNvbW1hbmQ6ICRjbWQiOyB1c2FnZTsgZXhpdCAyIDs7CmVzYWMK";
|
|
21276
|
+
|
|
21277
|
+
// src/skills/bootstrap/exulu-skills.ts
|
|
21278
|
+
var BOOTSTRAP_CLIENTS_JSON = JSON.stringify(CLIENT_MANIFEST, null, 2);
|
|
21279
|
+
var DIR_FOR_CASES = CLIENT_MANIFEST.map(
|
|
21280
|
+
(c) => ` ${c.id}) printf '%s' '${c.dir}' ;;`
|
|
21281
|
+
).join("\n");
|
|
21282
|
+
var BOOTSTRAP_EXULU_SH = Buffer.from(EXULU_SH_B64, "base64").toString("utf8").replace("__DIR_FOR_CASES__", DIR_FOR_CASES);
|
|
21283
|
+
var BOOTSTRAP_SKILL_MD = `---
|
|
21284
|
+
name: exulu-skills
|
|
21285
|
+
description: Install, update, and publish skills from this Exulu instance's central skill library. Use when the user asks to install a skill, get the latest version of a skill, list available skills, or publish a skill to Exulu.
|
|
21286
|
+
---
|
|
21287
|
+
|
|
21288
|
+
# Exulu Skills
|
|
21289
|
+
|
|
21290
|
+
Bridge to the Exulu central skill library. **All operations go through the
|
|
21291
|
+
bundled helper script \u2014 do not hand-write curl or copy files yourself.** The
|
|
21292
|
+
script reads the API token from config (keeping it out of this conversation) and
|
|
21293
|
+
handles the multi-client copy/symlink fan-out deterministically.
|
|
21294
|
+
|
|
21295
|
+
## The helper
|
|
21296
|
+
|
|
21297
|
+
Run the script next to this file, \`scripts/exulu\`, with \`sh\` and the absolute
|
|
21298
|
+
path of this skill's directory:
|
|
21299
|
+
|
|
21300
|
+
\`\`\`
|
|
21301
|
+
sh "<this-skill-dir>/scripts/exulu" <command>
|
|
21302
|
+
\`\`\`
|
|
21303
|
+
|
|
21304
|
+
Commands:
|
|
21305
|
+
- \`list\` \u2014 skills you can access (JSON on stdout)
|
|
21306
|
+
- \`get <name>\` \u2014 one skill's metadata (JSON)
|
|
21307
|
+
- \`install <name>\` \u2014 install/refresh a skill into the user's agent clients
|
|
21308
|
+
- \`update [<name>]\` \u2014 update every installed skill, or just \`<name>\`, to latest
|
|
21309
|
+
- \`publish <name> <folder>\` \u2014 publish a local skill folder as \`<name>\`
|
|
21310
|
+
- \`config\` \u2014 show resolved backend / scope / clients (prints no secrets)
|
|
21311
|
+
|
|
21312
|
+
The token, backend URL, target clients, copy-vs-symlink mode, and scope all come
|
|
21313
|
+
from \`~/.config/exulu/skills.json\` (written by the installer). Never print the
|
|
21314
|
+
\`api_key\` or read it into your reply \u2014 the script uses it internally.
|
|
21315
|
+
|
|
21316
|
+
## Requests \u2192 commands
|
|
21317
|
+
|
|
21318
|
+
- "list / search skills" \u2192 \`exulu list\`, then filter the JSON for the user.
|
|
21319
|
+
- "install skill X" / "add the X skill" \u2192 \`exulu install X\`.
|
|
21320
|
+
- "update / get the latest version [of X]" \u2192 \`exulu update [X]\`.
|
|
21321
|
+
- "publish / upload this skill as X" \u2192 confirm the target name with the user; for
|
|
21322
|
+
an existing skill run \`exulu get X\` first and confirm a new version is intended;
|
|
21323
|
+
then \`exulu publish X <folder>\`.
|
|
21324
|
+
|
|
21325
|
+
## Not configured yet?
|
|
21326
|
+
|
|
21327
|
+
If \`exulu config\` reports it's not configured (or \`~/.config/exulu/skills.json\`
|
|
21328
|
+
is missing), tell the user to run the installer \u2014 it sets everything up
|
|
21329
|
+
interactively (base URL, API key, target clients, copy/symlink):
|
|
21330
|
+
|
|
21331
|
+
\`\`\`
|
|
21332
|
+
curl -fsSL <base_url>/api/skills/install.sh | sh
|
|
21333
|
+
\`\`\`
|
|
21334
|
+
|
|
21335
|
+
\`<base_url>\` is their Exulu frontend URL (e.g. https://ai.open.de). They can
|
|
21336
|
+
create an API key at \`<base_url>/token\`.
|
|
21337
|
+
|
|
21338
|
+
## Errors
|
|
21339
|
+
|
|
21340
|
+
- install: \`403\` = no access to that skill; \`404\` = unknown name.
|
|
21341
|
+
- publish: \`403\` = you can see it but lack write access; \`409\` = the name is
|
|
21342
|
+
taken by a skill you can't access.
|
|
21343
|
+
`;
|
|
21344
|
+
|
|
20473
21345
|
// src/exulu/routes.ts
|
|
20474
21346
|
var REQUEST_SIZE_LIMIT = "50mb";
|
|
20475
21347
|
var getExuluVersionNumber = async () => {
|
|
@@ -20543,6 +21415,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
|
|
|
20543
21415
|
}
|
|
20544
21416
|
next();
|
|
20545
21417
|
});
|
|
21418
|
+
const rawZip = import_express5.default.raw({ type: ["application/zip", "application/octet-stream", "application/x-zip-compressed", "application/x-zip"], limit: "50mb" });
|
|
20546
21419
|
console.log(`
|
|
20547
21420
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
|
|
20548
21421
|
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
|
|
@@ -20768,7 +21641,7 @@ Mood: friendly and intelligent.
|
|
|
20768
21641
|
});
|
|
20769
21642
|
return;
|
|
20770
21643
|
}
|
|
20771
|
-
const uuid = (0,
|
|
21644
|
+
const uuid = (0, import_node_crypto9.randomUUID)();
|
|
20772
21645
|
const image_url = await uploadFile(Buffer.from(image_base64, "base64"), `${uuid}.png`, config, {
|
|
20773
21646
|
contentType: "image/png"
|
|
20774
21647
|
}, authenticationResult.user?.id, void 0, true);
|
|
@@ -20972,6 +21845,10 @@ Mood: friendly and intelligent.
|
|
|
20972
21845
|
const providerapikey = resolved.apiKey;
|
|
20973
21846
|
const resolvedLanguageModel = resolved.languageModel;
|
|
20974
21847
|
const resolvedModelId = resolved.model.id;
|
|
21848
|
+
const contextWindow = await resolveContextWindow({
|
|
21849
|
+
modelId: resolved.model.id,
|
|
21850
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
21851
|
+
});
|
|
20975
21852
|
if (!!headers.stream) {
|
|
20976
21853
|
const statistics = {
|
|
20977
21854
|
label: agent.name,
|
|
@@ -20990,27 +21867,46 @@ Mood: friendly and intelligent.
|
|
|
20990
21867
|
const instructions = customInstructions ? `${agent.instructions}
|
|
20991
21868
|
|
|
20992
21869
|
${customInstructions}` : agent.instructions;
|
|
20993
|
-
|
|
20994
|
-
|
|
20995
|
-
|
|
20996
|
-
|
|
20997
|
-
|
|
20998
|
-
|
|
20999
|
-
|
|
21000
|
-
|
|
21001
|
-
|
|
21002
|
-
|
|
21003
|
-
|
|
21004
|
-
|
|
21005
|
-
|
|
21006
|
-
|
|
21007
|
-
|
|
21008
|
-
|
|
21009
|
-
|
|
21010
|
-
|
|
21870
|
+
if (headers.session) markStreamActive(headers.session);
|
|
21871
|
+
let result;
|
|
21872
|
+
try {
|
|
21873
|
+
result = await provider.generateStream({
|
|
21874
|
+
contexts,
|
|
21875
|
+
agent,
|
|
21876
|
+
user,
|
|
21877
|
+
instructions,
|
|
21878
|
+
session: headers.session,
|
|
21879
|
+
message,
|
|
21880
|
+
previousMessages,
|
|
21881
|
+
currentTools: enabledTools,
|
|
21882
|
+
currentSkills: enabledSkills,
|
|
21883
|
+
approvedTools,
|
|
21884
|
+
allExuluTools: tools,
|
|
21885
|
+
languageModel: resolvedLanguageModel,
|
|
21886
|
+
providerapikey,
|
|
21887
|
+
toolConfigs: agent.tools,
|
|
21888
|
+
exuluConfig: config,
|
|
21889
|
+
req,
|
|
21890
|
+
contextWindow,
|
|
21891
|
+
disabledTools
|
|
21892
|
+
});
|
|
21893
|
+
} catch (err) {
|
|
21894
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
21895
|
+
if (err instanceof ContextCompactionRequiredError) {
|
|
21896
|
+
res.status(413).send(err.message);
|
|
21897
|
+
return;
|
|
21898
|
+
}
|
|
21899
|
+
throw err;
|
|
21900
|
+
}
|
|
21011
21901
|
result.stream.consumeStream();
|
|
21012
21902
|
result.stream.pipeUIMessageStreamToResponse(res, {
|
|
21013
21903
|
messageMetadata: ({ part }) => {
|
|
21904
|
+
if (part.type === "finish-step") {
|
|
21905
|
+
return {
|
|
21906
|
+
lastStepInputTokens: part.usage.inputTokens,
|
|
21907
|
+
lastStepOutputTokens: part.usage.outputTokens
|
|
21908
|
+
};
|
|
21909
|
+
}
|
|
21014
21910
|
if (part.type === "finish") {
|
|
21015
21911
|
return {
|
|
21016
21912
|
totalTokens: part.totalUsage.totalTokens,
|
|
@@ -21027,22 +21923,20 @@ ${customInstructions}` : agent.instructions;
|
|
|
21027
21923
|
sendSources: true,
|
|
21028
21924
|
onError: (error) => {
|
|
21029
21925
|
console.error("[EXULU] chat response error.", error);
|
|
21030
|
-
if (
|
|
21031
|
-
|
|
21032
|
-
|
|
21033
|
-
if (typeof error === "string")
|
|
21034
|
-
|
|
21035
|
-
|
|
21036
|
-
|
|
21037
|
-
return error.message;
|
|
21038
|
-
}
|
|
21039
|
-
return JSON.stringify(error);
|
|
21926
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
21927
|
+
let message2;
|
|
21928
|
+
if (error == null) message2 = "unknown error";
|
|
21929
|
+
else if (typeof error === "string") message2 = error;
|
|
21930
|
+
else if (error instanceof Error) message2 = error.message;
|
|
21931
|
+
else message2 = JSON.stringify(error);
|
|
21932
|
+
return mapStreamErrorMessage(message2);
|
|
21040
21933
|
},
|
|
21041
|
-
generateMessageId: (0,
|
|
21934
|
+
generateMessageId: (0, import_ai15.createIdGenerator)({
|
|
21042
21935
|
prefix: "msg_",
|
|
21043
21936
|
size: 16
|
|
21044
21937
|
}),
|
|
21045
21938
|
onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
|
|
21939
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
21046
21940
|
console.log(
|
|
21047
21941
|
"[EXULU] onFinish",
|
|
21048
21942
|
messages?.map((msg) => msg.parts?.map((part) => part.type === "text" ? part.text : null)).join("\n")
|
|
@@ -21104,33 +21998,129 @@ ${customInstructions}` : agent.instructions;
|
|
|
21104
21998
|
const instructions = customInstructions ? `${agent.instructions}
|
|
21105
21999
|
|
|
21106
22000
|
${customInstructions}` : agent.instructions;
|
|
21107
|
-
|
|
21108
|
-
|
|
21109
|
-
|
|
21110
|
-
|
|
21111
|
-
|
|
21112
|
-
|
|
21113
|
-
|
|
21114
|
-
|
|
21115
|
-
|
|
21116
|
-
|
|
21117
|
-
|
|
21118
|
-
|
|
21119
|
-
|
|
21120
|
-
|
|
21121
|
-
|
|
21122
|
-
|
|
21123
|
-
|
|
21124
|
-
|
|
21125
|
-
|
|
21126
|
-
|
|
22001
|
+
let response;
|
|
22002
|
+
try {
|
|
22003
|
+
response = await provider.generateSync({
|
|
22004
|
+
contexts,
|
|
22005
|
+
agent,
|
|
22006
|
+
user,
|
|
22007
|
+
req,
|
|
22008
|
+
instructions,
|
|
22009
|
+
session: headers.session,
|
|
22010
|
+
inputMessages: [req.body.message],
|
|
22011
|
+
currentTools: enabledTools,
|
|
22012
|
+
currentSkills: enabledSkills,
|
|
22013
|
+
allExuluTools: tools,
|
|
22014
|
+
languageModel: resolvedLanguageModel,
|
|
22015
|
+
providerapikey,
|
|
22016
|
+
exuluConfig: config,
|
|
22017
|
+
toolConfigs: agent.tools,
|
|
22018
|
+
contextWindow,
|
|
22019
|
+
disabledTools,
|
|
22020
|
+
statistics: {
|
|
22021
|
+
label: agent.name,
|
|
22022
|
+
trigger: "agent"
|
|
22023
|
+
},
|
|
22024
|
+
onTokenUsage: async ({ inputTokens, outputTokens }) => {
|
|
22025
|
+
}
|
|
22026
|
+
});
|
|
22027
|
+
} catch (err) {
|
|
22028
|
+
if (err instanceof ContextCompactionRequiredError) {
|
|
22029
|
+
res.status(413).send(err.message);
|
|
22030
|
+
return;
|
|
21127
22031
|
}
|
|
21128
|
-
|
|
22032
|
+
throw err;
|
|
22033
|
+
}
|
|
21129
22034
|
res.status(200).json(response);
|
|
21130
22035
|
return;
|
|
21131
22036
|
}
|
|
21132
22037
|
});
|
|
21133
22038
|
};
|
|
22039
|
+
const registerAgentCompactRoute = (slug) => {
|
|
22040
|
+
app.post(slug + "/:instance", async (req, res) => {
|
|
22041
|
+
const instance2 = req.params.instance;
|
|
22042
|
+
if (!instance2) {
|
|
22043
|
+
res.status(400).json({ message: "Missing instance in request." });
|
|
22044
|
+
return;
|
|
22045
|
+
}
|
|
22046
|
+
const sessionID = req.headers["session"] || null;
|
|
22047
|
+
if (!sessionID) {
|
|
22048
|
+
res.status(400).json({ message: "Missing session header." });
|
|
22049
|
+
return;
|
|
22050
|
+
}
|
|
22051
|
+
const agent = await exuluApp.get().agent(instance2);
|
|
22052
|
+
if (!agent) {
|
|
22053
|
+
res.status(404).json({ message: "Agent with id " + instance2 + " not found." });
|
|
22054
|
+
return;
|
|
22055
|
+
}
|
|
22056
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
22057
|
+
if (!authenticationResult.user?.id) {
|
|
22058
|
+
res.status(authenticationResult.code || 401).json({ detail: `${authenticationResult.message}` });
|
|
22059
|
+
return;
|
|
22060
|
+
}
|
|
22061
|
+
const user = authenticationResult.user;
|
|
22062
|
+
const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
|
|
22063
|
+
if (!hasAccessToAgent) {
|
|
22064
|
+
res.status(401).json({ message: "You don't have access to this agent." });
|
|
22065
|
+
return;
|
|
22066
|
+
}
|
|
22067
|
+
const { db: db2 } = await postgresClient();
|
|
22068
|
+
const sessionRow = await db2.from("agent_sessions").where({ id: sessionID }).first();
|
|
22069
|
+
if (!sessionRow) {
|
|
22070
|
+
res.status(404).json({ message: "Session not found for session ID: " + sessionID });
|
|
22071
|
+
return;
|
|
22072
|
+
}
|
|
22073
|
+
const hasAccessToSession = await checkRecordAccess(sessionRow, "write", user);
|
|
22074
|
+
if (!hasAccessToSession) {
|
|
22075
|
+
res.status(401).json({ message: "You don't have access to this session." });
|
|
22076
|
+
return;
|
|
22077
|
+
}
|
|
22078
|
+
if (isStreamActive(sessionID)) {
|
|
22079
|
+
res.status(409).json({ message: "A response is still streaming for this session \u2014 try again when it finishes." });
|
|
22080
|
+
return;
|
|
22081
|
+
}
|
|
22082
|
+
const overrideModelId = req.headers["x-exulu-model-override"];
|
|
22083
|
+
const modelId = overrideModelId ?? agent.model;
|
|
22084
|
+
if (!modelId) {
|
|
22085
|
+
res.status(400).json({ message: `Agent ${agent.name} (${agent.id}) has no model configured.` });
|
|
22086
|
+
return;
|
|
22087
|
+
}
|
|
22088
|
+
let resolved;
|
|
22089
|
+
try {
|
|
22090
|
+
resolved = await resolveModel({ modelId, user, providers, agent });
|
|
22091
|
+
} catch (err) {
|
|
22092
|
+
if (err instanceof ResolveModelError) {
|
|
22093
|
+
const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
|
|
22094
|
+
res.status(status).json({ message: err.message, code: err.code });
|
|
22095
|
+
return;
|
|
22096
|
+
}
|
|
22097
|
+
throw err;
|
|
22098
|
+
}
|
|
22099
|
+
const contextWindow = await resolveContextWindow({
|
|
22100
|
+
modelId: resolved.model.id,
|
|
22101
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
22102
|
+
});
|
|
22103
|
+
const steer = typeof req.body?.steer === "string" ? req.body.steer : void 0;
|
|
22104
|
+
try {
|
|
22105
|
+
const result = await compactSession({
|
|
22106
|
+
sessionID,
|
|
22107
|
+
user,
|
|
22108
|
+
languageModel: resolved.languageModel,
|
|
22109
|
+
contextWindow,
|
|
22110
|
+
steer,
|
|
22111
|
+
modelId: resolved.model.id
|
|
22112
|
+
});
|
|
22113
|
+
res.json(result);
|
|
22114
|
+
} catch (err) {
|
|
22115
|
+
if (err instanceof CompactionInsufficientError) {
|
|
22116
|
+
res.status(422).send(err.message);
|
|
22117
|
+
return;
|
|
22118
|
+
}
|
|
22119
|
+
console.error("[EXULU] compactSession failed.", err);
|
|
22120
|
+
res.status(500).json({ message: err instanceof Error ? err.message : "Compaction failed." });
|
|
22121
|
+
}
|
|
22122
|
+
});
|
|
22123
|
+
};
|
|
21134
22124
|
providers.forEach((provider) => {
|
|
21135
22125
|
const slug = provider.slug;
|
|
21136
22126
|
if (!slug) return;
|
|
@@ -21139,6 +22129,14 @@ ${customInstructions}` : agent.instructions;
|
|
|
21139
22129
|
if (isLiteLLMEnabled() && providers.length > 0) {
|
|
21140
22130
|
registerAgentRunRoute("/agents/litellm/run", providers[0]);
|
|
21141
22131
|
}
|
|
22132
|
+
providers.forEach((provider) => {
|
|
22133
|
+
const slug = provider.slug;
|
|
22134
|
+
if (!slug) return;
|
|
22135
|
+
registerAgentCompactRoute(slug.replace(/\/run$/, "/compact"));
|
|
22136
|
+
});
|
|
22137
|
+
if (isLiteLLMEnabled() && providers.length > 0) {
|
|
22138
|
+
registerAgentCompactRoute("/agents/litellm/compact");
|
|
22139
|
+
}
|
|
21142
22140
|
app.post("/agents/suggestions/:agentId", async (req, res) => {
|
|
21143
22141
|
const agentId = req.params.agentId;
|
|
21144
22142
|
if (!agentId) {
|
|
@@ -21490,7 +22488,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
21490
22488
|
const keys = [];
|
|
21491
22489
|
const revisedPrompts = [];
|
|
21492
22490
|
for (const img of images) {
|
|
21493
|
-
const filename = `${(0,
|
|
22491
|
+
const filename = `${(0, import_node_crypto9.randomUUID)()}.${img.extension}`;
|
|
21494
22492
|
const key = `sessions/${sessionId}/images/${toolCallId}/${filename}`;
|
|
21495
22493
|
const fullKey = await uploadFile(
|
|
21496
22494
|
img.buffer,
|
|
@@ -21778,7 +22776,7 @@ ${style.markdown}` : params.prompt;
|
|
|
21778
22776
|
(d) => `- ${d.presignedUrl} (prompt: "${d.prompt}", model: ${d.model}${d.styleName ? `, style: ${d.styleName}` : ""})`
|
|
21779
22777
|
);
|
|
21780
22778
|
const messageText = "The user generated and selected the following image(s) in this chat:\n" + lines.join("\n");
|
|
21781
|
-
const messageId = (0,
|
|
22779
|
+
const messageId = (0, import_node_crypto9.randomUUID)();
|
|
21782
22780
|
const uiMessage = {
|
|
21783
22781
|
id: messageId,
|
|
21784
22782
|
role: "system",
|
|
@@ -22070,7 +23068,9 @@ ${style.markdown}` : params.prompt;
|
|
|
22070
23068
|
const budget_duration = String(body?.budget_duration ?? "");
|
|
22071
23069
|
if (!Number.isFinite(max_budget) || max_budget <= 0) return null;
|
|
22072
23070
|
if (!BUDGET_ALLOWED_DURATIONS.has(budget_duration)) return null;
|
|
22073
|
-
|
|
23071
|
+
const reset = parseResetAt(body?.budget_reset_at);
|
|
23072
|
+
if (!reset.valid) return null;
|
|
23073
|
+
return { max_budget, budget_duration, budget_reset_at: reset.value };
|
|
22074
23074
|
};
|
|
22075
23075
|
const parseBudgetSettingsBody = (body) => {
|
|
22076
23076
|
if (!body || typeof body !== "object") return null;
|
|
@@ -22140,7 +23140,7 @@ ${style.markdown}` : params.prompt;
|
|
|
22140
23140
|
}
|
|
22141
23141
|
const body = parseBudgetBody(req.body);
|
|
22142
23142
|
if (!body) {
|
|
22143
|
-
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
|
|
23143
|
+
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
|
|
22144
23144
|
return;
|
|
22145
23145
|
}
|
|
22146
23146
|
const entityIds = Array.isArray(req.body?.entityIds) ? req.body.entityIds : [];
|
|
@@ -22152,7 +23152,7 @@ ${style.markdown}` : params.prompt;
|
|
|
22152
23152
|
for (const id of entityIds) {
|
|
22153
23153
|
const tag = budgetTagFor(entityType, id);
|
|
22154
23154
|
try {
|
|
22155
|
-
await upsertBudget(tag, body.max_budget, body.budget_duration);
|
|
23155
|
+
await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
|
|
22156
23156
|
results.push({ entityId: String(id), ok: true });
|
|
22157
23157
|
} catch (err) {
|
|
22158
23158
|
results.push({
|
|
@@ -22177,12 +23177,12 @@ ${style.markdown}` : params.prompt;
|
|
|
22177
23177
|
}
|
|
22178
23178
|
const body = parseBudgetBody(req.body);
|
|
22179
23179
|
if (!body) {
|
|
22180
|
-
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
|
|
23180
|
+
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
|
|
22181
23181
|
return;
|
|
22182
23182
|
}
|
|
22183
23183
|
const tag = budgetTagFor(entityType, req.params.entityId ?? "");
|
|
22184
23184
|
try {
|
|
22185
|
-
await upsertBudget(tag, body.max_budget, body.budget_duration);
|
|
23185
|
+
await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
|
|
22186
23186
|
const info = await tagInfo([tag]);
|
|
22187
23187
|
res.status(200).json({ budget: info[tag] ?? null });
|
|
22188
23188
|
} catch (err) {
|
|
@@ -22645,6 +23645,199 @@ ${style.markdown}` : params.prompt;
|
|
|
22645
23645
|
}
|
|
22646
23646
|
return root;
|
|
22647
23647
|
}
|
|
23648
|
+
app.get("/skills/agent/bootstrap", async (_req, res) => {
|
|
23649
|
+
try {
|
|
23650
|
+
const zip = new import_jszip3.default();
|
|
23651
|
+
zip.file("exulu-skills/SKILL.md", BOOTSTRAP_SKILL_MD);
|
|
23652
|
+
zip.file("exulu-skills/references/clients.json", BOOTSTRAP_CLIENTS_JSON);
|
|
23653
|
+
zip.file("exulu-skills/scripts/exulu", BOOTSTRAP_EXULU_SH, {
|
|
23654
|
+
unixPermissions: 493
|
|
23655
|
+
});
|
|
23656
|
+
const buffer = await zip.generateAsync({
|
|
23657
|
+
type: "nodebuffer",
|
|
23658
|
+
platform: "UNIX"
|
|
23659
|
+
});
|
|
23660
|
+
res.setHeader("Content-Type", "application/zip");
|
|
23661
|
+
res.setHeader("Content-Disposition", 'attachment; filename="exulu-skills.zip"');
|
|
23662
|
+
res.send(buffer);
|
|
23663
|
+
} catch (err) {
|
|
23664
|
+
console.error("[SKILLS] Failed to build bootstrap zip", err);
|
|
23665
|
+
res.status(500).json({ detail: "Failed to build bootstrap skill." });
|
|
23666
|
+
}
|
|
23667
|
+
});
|
|
23668
|
+
app.get("/skills/registry", async (req, res) => {
|
|
23669
|
+
const authResult = await requestValidators.authenticate(req);
|
|
23670
|
+
if (!authResult.user?.id) {
|
|
23671
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23672
|
+
return;
|
|
23673
|
+
}
|
|
23674
|
+
const { db: db2 } = await postgresClient();
|
|
23675
|
+
const tag = typeof req.query.tag === "string" ? req.query.tag : void 0;
|
|
23676
|
+
const all = await db2("skills").select("*");
|
|
23677
|
+
const readable = await filterReadableSkills(db2, all, authResult.user);
|
|
23678
|
+
const skills = readable.filter((s) => {
|
|
23679
|
+
if (!tag) return true;
|
|
23680
|
+
const tags = Array.isArray(s.tags) ? s.tags : [];
|
|
23681
|
+
return tags.includes(tag);
|
|
23682
|
+
}).map((s) => ({
|
|
23683
|
+
name: s.name,
|
|
23684
|
+
description: s.description ?? "",
|
|
23685
|
+
tags: Array.isArray(s.tags) ? s.tags : [],
|
|
23686
|
+
current_version: s.current_version ?? 1,
|
|
23687
|
+
updated_at: s.updatedAt ?? s.updated_at ?? null
|
|
23688
|
+
}));
|
|
23689
|
+
res.json({ skills });
|
|
23690
|
+
});
|
|
23691
|
+
app.get("/skills/registry/:name/download", async (req, res) => {
|
|
23692
|
+
const authResult = await requestValidators.authenticate(req);
|
|
23693
|
+
if (!authResult.user?.id) {
|
|
23694
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23695
|
+
return;
|
|
23696
|
+
}
|
|
23697
|
+
const { db: db2 } = await postgresClient();
|
|
23698
|
+
const skill = await resolveSkillByName(db2, req.params.name);
|
|
23699
|
+
if (!skill) {
|
|
23700
|
+
res.status(404).json({ detail: "Skill not found." });
|
|
23701
|
+
return;
|
|
23702
|
+
}
|
|
23703
|
+
if (!await canAccessSkill(db2, skill, "read", authResult.user)) {
|
|
23704
|
+
res.status(403).json({ detail: "You don't have access to this skill." });
|
|
23705
|
+
return;
|
|
23706
|
+
}
|
|
23707
|
+
const vQuery = req.query.version;
|
|
23708
|
+
const version = !vQuery || vQuery === "latest" ? skill.current_version ?? 1 : Number(vQuery);
|
|
23709
|
+
if (!Number.isFinite(version) || version < 1) {
|
|
23710
|
+
res.status(400).json({ detail: "Invalid version." });
|
|
23711
|
+
return;
|
|
23712
|
+
}
|
|
23713
|
+
const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
|
|
23714
|
+
const versionPrefix = `skills/${skill.id}/v${version}/`;
|
|
23715
|
+
const files = await listS3ObjectsByPrefix(versionPrefix, config);
|
|
23716
|
+
if (files.length === 0) {
|
|
23717
|
+
res.status(404).json({ detail: `Version v${version} has no files.` });
|
|
23718
|
+
return;
|
|
23719
|
+
}
|
|
23720
|
+
const zip = new import_jszip3.default();
|
|
23721
|
+
for (const file of files) {
|
|
23722
|
+
const idx = file.key.indexOf(versionPrefix);
|
|
23723
|
+
const rel = idx >= 0 ? file.key.slice(idx + versionPrefix.length) : file.key;
|
|
23724
|
+
if (!rel) continue;
|
|
23725
|
+
const bytes = await getS3ObjectBytes(file.key, config);
|
|
23726
|
+
zip.file(`${safeName}/${rel}`, bytes);
|
|
23727
|
+
}
|
|
23728
|
+
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
23729
|
+
res.setHeader("Content-Type", "application/zip");
|
|
23730
|
+
res.setHeader("Content-Disposition", `attachment; filename="${safeName}.skill"`);
|
|
23731
|
+
res.send(buffer);
|
|
23732
|
+
});
|
|
23733
|
+
app.post("/skills/registry/:name", rawZip, async (req, res) => {
|
|
23734
|
+
const authResult = await requestValidators.authenticate(req);
|
|
23735
|
+
if (!authResult.user?.id) {
|
|
23736
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23737
|
+
return;
|
|
23738
|
+
}
|
|
23739
|
+
const name = req.params.name;
|
|
23740
|
+
const bytes = req.body;
|
|
23741
|
+
if (!Buffer.isBuffer(bytes) || bytes.length === 0) {
|
|
23742
|
+
res.status(400).json({ detail: "Empty body. Send the skill as a zip/.skill payload." });
|
|
23743
|
+
return;
|
|
23744
|
+
}
|
|
23745
|
+
const { db: db2 } = await postgresClient();
|
|
23746
|
+
const existing = await resolveSkillByName(db2, name);
|
|
23747
|
+
if (existing) {
|
|
23748
|
+
const canWrite = await canAccessSkill(db2, existing, "write", authResult.user);
|
|
23749
|
+
if (canWrite) {
|
|
23750
|
+
const nextVersion = (existing.current_version ?? 1) + 1;
|
|
23751
|
+
try {
|
|
23752
|
+
await extractBundleToVersion({ bytes, skillId: existing.id, version: nextVersion, config });
|
|
23753
|
+
} catch (err) {
|
|
23754
|
+
if (err instanceof BundleValidationError) {
|
|
23755
|
+
res.status(400).json({ detail: err.message });
|
|
23756
|
+
return;
|
|
23757
|
+
}
|
|
23758
|
+
console.error("[SKILLS] publish (new version) failed", err);
|
|
23759
|
+
res.status(500).json({ detail: "Failed to publish new version." });
|
|
23760
|
+
return;
|
|
23761
|
+
}
|
|
23762
|
+
const history = Array.isArray(existing.history) ? existing.history : [];
|
|
23763
|
+
await db2("skills").where({ id: existing.id }).update({
|
|
23764
|
+
current_version: nextVersion,
|
|
23765
|
+
history: JSON.stringify([
|
|
23766
|
+
...history,
|
|
23767
|
+
{ version: nextVersion, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
|
|
23768
|
+
])
|
|
23769
|
+
});
|
|
23770
|
+
res.json({ name, version: nextVersion, created: false });
|
|
23771
|
+
return;
|
|
23772
|
+
} else {
|
|
23773
|
+
const canRead = await canAccessSkill(db2, existing, "read", authResult.user);
|
|
23774
|
+
if (!canRead) {
|
|
23775
|
+
res.status(409).json({ detail: "That name is unavailable." });
|
|
23776
|
+
return;
|
|
23777
|
+
}
|
|
23778
|
+
res.status(403).json({ detail: "You don't have write access to this skill." });
|
|
23779
|
+
return;
|
|
23780
|
+
}
|
|
23781
|
+
}
|
|
23782
|
+
const meta = await parseSkillFrontmatter(bytes);
|
|
23783
|
+
const skillId = (0, import_node_crypto9.randomUUID)();
|
|
23784
|
+
try {
|
|
23785
|
+
await extractBundleToVersion({ bytes, skillId, version: 1, config });
|
|
23786
|
+
} catch (err) {
|
|
23787
|
+
if (err instanceof BundleValidationError) {
|
|
23788
|
+
res.status(400).json({ detail: err.message });
|
|
23789
|
+
return;
|
|
23790
|
+
}
|
|
23791
|
+
console.error("[SKILLS] publish (create) failed", err);
|
|
23792
|
+
res.status(500).json({ detail: "Failed to publish skill." });
|
|
23793
|
+
return;
|
|
23794
|
+
}
|
|
23795
|
+
try {
|
|
23796
|
+
await db2("skills").insert({
|
|
23797
|
+
id: skillId,
|
|
23798
|
+
name,
|
|
23799
|
+
description: meta.description ?? "",
|
|
23800
|
+
s3folder: `skills/${skillId}`,
|
|
23801
|
+
tags: JSON.stringify([]),
|
|
23802
|
+
usage_count: 0,
|
|
23803
|
+
favorite_count: 0,
|
|
23804
|
+
current_version: 1,
|
|
23805
|
+
history: JSON.stringify([
|
|
23806
|
+
{ version: 1, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
|
|
23807
|
+
]),
|
|
23808
|
+
rights_mode: "private",
|
|
23809
|
+
created_by: authResult.user.id
|
|
23810
|
+
});
|
|
23811
|
+
} catch (err) {
|
|
23812
|
+
res.status(409).json({ detail: "That name is unavailable." });
|
|
23813
|
+
return;
|
|
23814
|
+
}
|
|
23815
|
+
res.json({ name, version: 1, created: true });
|
|
23816
|
+
});
|
|
23817
|
+
app.get("/skills/registry/:name", async (req, res) => {
|
|
23818
|
+
const authResult = await requestValidators.authenticate(req);
|
|
23819
|
+
if (!authResult.user?.id) {
|
|
23820
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23821
|
+
return;
|
|
23822
|
+
}
|
|
23823
|
+
const { db: db2 } = await postgresClient();
|
|
23824
|
+
const skill = await resolveSkillByName(db2, req.params.name);
|
|
23825
|
+
if (!skill) {
|
|
23826
|
+
res.status(404).json({ detail: "Skill not found." });
|
|
23827
|
+
return;
|
|
23828
|
+
}
|
|
23829
|
+
if (!await canAccessSkill(db2, skill, "read", authResult.user)) {
|
|
23830
|
+
res.status(403).json({ detail: "You don't have access to this skill." });
|
|
23831
|
+
return;
|
|
23832
|
+
}
|
|
23833
|
+
res.json({
|
|
23834
|
+
name: skill.name,
|
|
23835
|
+
description: skill.description ?? "",
|
|
23836
|
+
tags: Array.isArray(skill.tags) ? skill.tags : [],
|
|
23837
|
+
current_version: skill.current_version ?? 1,
|
|
23838
|
+
history: Array.isArray(skill.history) ? skill.history : []
|
|
23839
|
+
});
|
|
23840
|
+
});
|
|
22648
23841
|
app.post("/skills/:skillId/init", async (req, res) => {
|
|
22649
23842
|
const authResult = await requestValidators.authenticate(req);
|
|
22650
23843
|
if (!authResult.user?.id) {
|
|
@@ -22692,8 +23885,8 @@ ${style.markdown}` : params.prompt;
|
|
|
22692
23885
|
}
|
|
22693
23886
|
const { skillId } = req.params;
|
|
22694
23887
|
const { extension, contentType } = req.body ?? {};
|
|
22695
|
-
if (extension !== ".zip" && extension !== ".md") {
|
|
22696
|
-
res.status(400).json({ detail: 'extension must be ".zip" or ".
|
|
23888
|
+
if (extension !== ".zip" && extension !== ".md" && extension !== ".skill") {
|
|
23889
|
+
res.status(400).json({ detail: 'extension must be ".zip", ".md", or ".skill".' });
|
|
22697
23890
|
return;
|
|
22698
23891
|
}
|
|
22699
23892
|
if (!contentType || typeof contentType !== "string") {
|
|
@@ -22706,7 +23899,7 @@ ${style.markdown}` : params.prompt;
|
|
|
22706
23899
|
res.status(404).json({ detail: "Skill not found." });
|
|
22707
23900
|
return;
|
|
22708
23901
|
}
|
|
22709
|
-
const stagingKey = `user_${authResult.user.id}/skills/_staging/${(0,
|
|
23902
|
+
const stagingKey = `user_${authResult.user.id}/skills/_staging/${(0, import_node_crypto9.randomUUID)()}${extension}`;
|
|
22710
23903
|
const fullKey = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/${stagingKey}` : stagingKey;
|
|
22711
23904
|
const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
|
|
22712
23905
|
res.json({ uploadUrl, stagingKey });
|
|
@@ -22832,19 +24025,22 @@ ${style.markdown}` : params.prompt;
|
|
|
22832
24025
|
}
|
|
22833
24026
|
const versionPrefix = `skills/${skillId}/v${version}/`;
|
|
22834
24027
|
const files = await listS3ObjectsByPrefix(versionPrefix, config);
|
|
22835
|
-
const
|
|
24028
|
+
const asSkill = req.query.format === "skill";
|
|
24029
|
+
const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
|
|
24030
|
+
const zip = new import_jszip3.default();
|
|
22836
24031
|
let fileCount = 0;
|
|
22837
24032
|
for (const file of files) {
|
|
22838
24033
|
const prefixIndex = file.key.indexOf(versionPrefix);
|
|
22839
24034
|
const relativePath = prefixIndex >= 0 ? file.key.slice(prefixIndex + versionPrefix.length) : file.key;
|
|
22840
24035
|
if (!relativePath) continue;
|
|
22841
24036
|
const bytes = await getS3ObjectBytes(file.key, config);
|
|
22842
|
-
|
|
24037
|
+
const archivePath = asSkill ? `${safeName}/${relativePath}` : relativePath;
|
|
24038
|
+
zip.file(archivePath, bytes);
|
|
22843
24039
|
fileCount += 1;
|
|
22844
24040
|
}
|
|
22845
24041
|
const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
22846
24042
|
zip.file(
|
|
22847
|
-
"version.txt",
|
|
24043
|
+
asSkill ? `${safeName}/version.txt` : "version.txt",
|
|
22848
24044
|
[
|
|
22849
24045
|
`Skill: ${skill.name ?? skillId}`,
|
|
22850
24046
|
`Skill id: ${skillId}`,
|
|
@@ -22855,13 +24051,9 @@ ${style.markdown}` : params.prompt;
|
|
|
22855
24051
|
].join("\n")
|
|
22856
24052
|
);
|
|
22857
24053
|
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
22858
|
-
const
|
|
22859
|
-
const filename = `${safeName}-v${version}.zip`;
|
|
24054
|
+
const filename = asSkill ? `${safeName}.skill` : `${safeName}-v${version}.zip`;
|
|
22860
24055
|
res.setHeader("Content-Type", "application/zip");
|
|
22861
|
-
res.setHeader(
|
|
22862
|
-
"Content-Disposition",
|
|
22863
|
-
`attachment; filename="${filename}"`
|
|
22864
|
-
);
|
|
24056
|
+
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
|
22865
24057
|
res.send(buffer);
|
|
22866
24058
|
});
|
|
22867
24059
|
app.post("/skills/:skillId/sign", async (req, res) => {
|
|
@@ -23161,6 +24353,12 @@ ${style.markdown}` : params.prompt;
|
|
|
23161
24353
|
const fullSessionPrefix = `${generalPrefix}${userSessionPrefix}`;
|
|
23162
24354
|
return { userSessionPrefix, fullSessionPrefix };
|
|
23163
24355
|
}
|
|
24356
|
+
const loadSessionFilesAuth = async (req, res, sessionId, rights) => {
|
|
24357
|
+
const authed = await loadAuthedSession(req, res, sessionId, rights);
|
|
24358
|
+
if (!authed) return null;
|
|
24359
|
+
const ownerId = authed.session.user ?? authed.user.id;
|
|
24360
|
+
return { ...authed, ownerId, ...buildSessionPrefixes(ownerId, sessionId) };
|
|
24361
|
+
};
|
|
23164
24362
|
function sanitizeFilename(name) {
|
|
23165
24363
|
const trimmed = name.trim();
|
|
23166
24364
|
if (!trimmed) return "";
|
|
@@ -23169,11 +24367,6 @@ ${style.markdown}` : params.prompt;
|
|
|
23169
24367
|
return trimmed.replace(/[\\/]/g, "_");
|
|
23170
24368
|
}
|
|
23171
24369
|
app.get("/sessions/:sessionId/files", async (req, res) => {
|
|
23172
|
-
const authResult = await requestValidators.authenticate(req);
|
|
23173
|
-
if (!authResult.user?.id) {
|
|
23174
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23175
|
-
return;
|
|
23176
|
-
}
|
|
23177
24370
|
const sessionId = req.params.sessionId;
|
|
23178
24371
|
if (!sessionId) {
|
|
23179
24372
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -23183,10 +24376,9 @@ ${style.markdown}` : params.prompt;
|
|
|
23183
24376
|
res.status(500).json({ detail: "File uploads are not configured." });
|
|
23184
24377
|
return;
|
|
23185
24378
|
}
|
|
23186
|
-
const
|
|
23187
|
-
|
|
23188
|
-
|
|
23189
|
-
);
|
|
24379
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
|
|
24380
|
+
if (!authed) return;
|
|
24381
|
+
const { userSessionPrefix } = authed;
|
|
23190
24382
|
let objects;
|
|
23191
24383
|
try {
|
|
23192
24384
|
objects = await listS3ObjectsByPrefix(userSessionPrefix, config);
|
|
@@ -23216,11 +24408,6 @@ ${style.markdown}` : params.prompt;
|
|
|
23216
24408
|
app.post(
|
|
23217
24409
|
"/sessions/:sessionId/files/upload-sign",
|
|
23218
24410
|
async (req, res) => {
|
|
23219
|
-
const authResult = await requestValidators.authenticate(req);
|
|
23220
|
-
if (!authResult.user?.id) {
|
|
23221
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23222
|
-
return;
|
|
23223
|
-
}
|
|
23224
24411
|
const sessionId = req.params.sessionId;
|
|
23225
24412
|
if (!sessionId) {
|
|
23226
24413
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -23230,6 +24417,8 @@ ${style.markdown}` : params.prompt;
|
|
|
23230
24417
|
res.status(500).json({ detail: "File uploads are not configured." });
|
|
23231
24418
|
return;
|
|
23232
24419
|
}
|
|
24420
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
24421
|
+
if (!authed) return;
|
|
23233
24422
|
const { filename, contentType } = req.body ?? {};
|
|
23234
24423
|
if (!filename || typeof filename !== "string") {
|
|
23235
24424
|
res.status(400).json({ detail: "Missing filename in request body." });
|
|
@@ -23244,11 +24433,7 @@ ${style.markdown}` : params.prompt;
|
|
|
23244
24433
|
res.status(400).json({ detail: "Missing contentType in request body." });
|
|
23245
24434
|
return;
|
|
23246
24435
|
}
|
|
23247
|
-
const
|
|
23248
|
-
authResult.user.id,
|
|
23249
|
-
sessionId
|
|
23250
|
-
);
|
|
23251
|
-
const fullKey = `${fullSessionPrefix}${safeName}`;
|
|
24436
|
+
const fullKey = `${authed.fullSessionPrefix}${safeName}`;
|
|
23252
24437
|
const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
|
|
23253
24438
|
res.json({ uploadUrl, key: fullKey });
|
|
23254
24439
|
}
|
|
@@ -23256,11 +24441,6 @@ ${style.markdown}` : params.prompt;
|
|
|
23256
24441
|
app.post(
|
|
23257
24442
|
"/sessions/:sessionId/files/sync-to-sandbox",
|
|
23258
24443
|
async (req, res) => {
|
|
23259
|
-
const authResult = await requestValidators.authenticate(req);
|
|
23260
|
-
if (!authResult.user?.id) {
|
|
23261
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23262
|
-
return;
|
|
23263
|
-
}
|
|
23264
24444
|
const sessionId = req.params.sessionId;
|
|
23265
24445
|
if (!sessionId) {
|
|
23266
24446
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -23271,18 +24451,16 @@ ${style.markdown}` : params.prompt;
|
|
|
23271
24451
|
res.status(400).json({ detail: "Missing key in request body." });
|
|
23272
24452
|
return;
|
|
23273
24453
|
}
|
|
23274
|
-
const
|
|
23275
|
-
|
|
23276
|
-
|
|
23277
|
-
);
|
|
23278
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
24454
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
24455
|
+
if (!authed) return;
|
|
24456
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
23279
24457
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
23280
24458
|
return;
|
|
23281
24459
|
}
|
|
23282
24460
|
try {
|
|
23283
24461
|
const result = await downloadKeyIntoSandbox({
|
|
23284
24462
|
sessionId,
|
|
23285
|
-
userId:
|
|
24463
|
+
userId: authed.ownerId,
|
|
23286
24464
|
fullS3Key: key,
|
|
23287
24465
|
config
|
|
23288
24466
|
});
|
|
@@ -23296,11 +24474,6 @@ ${style.markdown}` : params.prompt;
|
|
|
23296
24474
|
app.delete(
|
|
23297
24475
|
"/sessions/:sessionId/files",
|
|
23298
24476
|
async (req, res) => {
|
|
23299
|
-
const authResult = await requestValidators.authenticate(req);
|
|
23300
|
-
if (!authResult.user?.id) {
|
|
23301
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23302
|
-
return;
|
|
23303
|
-
}
|
|
23304
24477
|
const sessionId = req.params.sessionId;
|
|
23305
24478
|
if (!sessionId) {
|
|
23306
24479
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -23311,11 +24484,9 @@ ${style.markdown}` : params.prompt;
|
|
|
23311
24484
|
res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
|
|
23312
24485
|
return;
|
|
23313
24486
|
}
|
|
23314
|
-
const
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
);
|
|
23318
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
24487
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
24488
|
+
if (!authed) return;
|
|
24489
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
23319
24490
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
23320
24491
|
return;
|
|
23321
24492
|
}
|
|
@@ -23334,11 +24505,6 @@ ${style.markdown}` : params.prompt;
|
|
|
23334
24505
|
if (!req.headers.authorization && typeof req.query.auth === "string") {
|
|
23335
24506
|
req.headers.authorization = `Bearer ${req.query.auth}`;
|
|
23336
24507
|
}
|
|
23337
|
-
const authResult = await requestValidators.authenticate(req);
|
|
23338
|
-
if (!authResult.user?.id) {
|
|
23339
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23340
|
-
return;
|
|
23341
|
-
}
|
|
23342
24508
|
const sessionId = req.params.sessionId;
|
|
23343
24509
|
if (!sessionId) {
|
|
23344
24510
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -23349,11 +24515,9 @@ ${style.markdown}` : params.prompt;
|
|
|
23349
24515
|
res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
|
|
23350
24516
|
return;
|
|
23351
24517
|
}
|
|
23352
|
-
const
|
|
23353
|
-
|
|
23354
|
-
|
|
23355
|
-
);
|
|
23356
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
24518
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
|
|
24519
|
+
if (!authed) return;
|
|
24520
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
23357
24521
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
23358
24522
|
return;
|
|
23359
24523
|
}
|
|
@@ -23457,7 +24621,7 @@ ${style.markdown}` : params.prompt;
|
|
|
23457
24621
|
` - tracking.json tracking events linked to the user`,
|
|
23458
24622
|
``
|
|
23459
24623
|
].join("\n");
|
|
23460
|
-
const zip = new
|
|
24624
|
+
const zip = new import_jszip3.default();
|
|
23461
24625
|
zip.file("README.txt", readme);
|
|
23462
24626
|
zip.file("user_data.json", JSON.stringify(userExport, null, 2));
|
|
23463
24627
|
zip.file("sessions.json", JSON.stringify(sessionsWithMessages, null, 2));
|
|
@@ -23738,7 +24902,7 @@ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
|
|
|
23738
24902
|
// src/mcp/index.ts
|
|
23739
24903
|
init_cjs_shims();
|
|
23740
24904
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
23741
|
-
var
|
|
24905
|
+
var import_node_crypto10 = require("crypto");
|
|
23742
24906
|
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
23743
24907
|
var import_types3 = require("@modelcontextprotocol/sdk/types.js");
|
|
23744
24908
|
init_sanitize_tool_name();
|
|
@@ -23850,7 +25014,7 @@ var ExuluMCP = class {
|
|
|
23850
25014
|
throw new Error("Tool not found in converted tools array.");
|
|
23851
25015
|
}
|
|
23852
25016
|
const iterator = await convertedTool.execute(inputs, {
|
|
23853
|
-
toolCallId: tool4.id + "_" + (0,
|
|
25017
|
+
toolCallId: tool4.id + "_" + (0, import_node_crypto10.randomUUID)(),
|
|
23854
25018
|
messages: []
|
|
23855
25019
|
});
|
|
23856
25020
|
let result;
|
|
@@ -24042,7 +25206,7 @@ var ExuluMCP = class {
|
|
|
24042
25206
|
transport = this.transports[sessionId];
|
|
24043
25207
|
} else if (!sessionId && (0, import_types3.isInitializeRequest)(req.body)) {
|
|
24044
25208
|
transport = new import_streamableHttp.StreamableHTTPServerTransport({
|
|
24045
|
-
sessionIdGenerator: () => (0,
|
|
25209
|
+
sessionIdGenerator: () => (0, import_node_crypto10.randomUUID)(),
|
|
24046
25210
|
onsessioninitialized: (sessionId2) => {
|
|
24047
25211
|
this.transports[sessionId2] = transport;
|
|
24048
25212
|
}
|
|
@@ -24996,7 +26160,7 @@ init_cjs_shims();
|
|
|
24996
26160
|
|
|
24997
26161
|
// src/exulu/evals.ts
|
|
24998
26162
|
init_cjs_shims();
|
|
24999
|
-
var
|
|
26163
|
+
var import_ai16 = require("ai");
|
|
25000
26164
|
init_entitlements();
|
|
25001
26165
|
var ExuluEval = class {
|
|
25002
26166
|
id;
|
|
@@ -25037,7 +26201,7 @@ var ExuluEval = class {
|
|
|
25037
26201
|
init_resolve_model();
|
|
25038
26202
|
init_singleton();
|
|
25039
26203
|
var import_zod15 = require("zod");
|
|
25040
|
-
var
|
|
26204
|
+
var import_ai17 = require("ai");
|
|
25041
26205
|
var llmAsJudgeEval = () => {
|
|
25042
26206
|
if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
|
|
25043
26207
|
return new ExuluEval({
|
|
@@ -25082,13 +26246,13 @@ var llmAsJudgeEval = () => {
|
|
|
25082
26246
|
rbacBypass: true
|
|
25083
26247
|
});
|
|
25084
26248
|
console.log("[EXULU] prompt", prompt);
|
|
25085
|
-
const { output } = await (0,
|
|
26249
|
+
const { output } = await (0, import_ai17.generateText)({
|
|
25086
26250
|
temperature: 0,
|
|
25087
26251
|
model: resolved.languageModel,
|
|
25088
26252
|
system: "",
|
|
25089
26253
|
prompt,
|
|
25090
26254
|
maxRetries: 2,
|
|
25091
|
-
output:
|
|
26255
|
+
output: import_ai17.Output.object({
|
|
25092
26256
|
schema: import_zod15.z.object({
|
|
25093
26257
|
score: import_zod15.z.number().min(0).max(100).describe("The score between 0 and 100.")
|
|
25094
26258
|
})
|
|
@@ -25522,7 +26686,7 @@ var import_zod17 = __toESM(require("zod"), 1);
|
|
|
25522
26686
|
init_tool();
|
|
25523
26687
|
init_check_record_access();
|
|
25524
26688
|
init_client();
|
|
25525
|
-
var
|
|
26689
|
+
var import_node_crypto11 = require("crypto");
|
|
25526
26690
|
var AnswerOptionSchema = import_zod17.default.object({
|
|
25527
26691
|
id: import_zod17.default.string().describe("Unique identifier for the answer option"),
|
|
25528
26692
|
text: import_zod17.default.string().describe("The text of the answer option")
|
|
@@ -25576,15 +26740,15 @@ var QuestionAskTool = new ExuluTool({
|
|
|
25576
26740
|
throw new Error("You don't have access to this session " + session.id + ".");
|
|
25577
26741
|
}
|
|
25578
26742
|
const answerOptionsWithIds = answerOptions.map((text) => ({
|
|
25579
|
-
id: (0,
|
|
26743
|
+
id: (0, import_node_crypto11.randomUUID)(),
|
|
25580
26744
|
text
|
|
25581
26745
|
}));
|
|
25582
26746
|
answerOptionsWithIds.push({
|
|
25583
|
-
id: (0,
|
|
26747
|
+
id: (0, import_node_crypto11.randomUUID)(),
|
|
25584
26748
|
text: "None of the above..."
|
|
25585
26749
|
});
|
|
25586
26750
|
const newQuestion = {
|
|
25587
|
-
id: (0,
|
|
26751
|
+
id: (0, import_node_crypto11.randomUUID)(),
|
|
25588
26752
|
question,
|
|
25589
26753
|
answerOptions: answerOptionsWithIds,
|
|
25590
26754
|
status: "pending"
|
|
@@ -28618,7 +29782,7 @@ var MarkdownChunker = class {
|
|
|
28618
29782
|
init_cjs_shims();
|
|
28619
29783
|
var fs4 = __toESM(require("fs"), 1);
|
|
28620
29784
|
var path = __toESM(require("path"), 1);
|
|
28621
|
-
var
|
|
29785
|
+
var import_ai18 = require("ai");
|
|
28622
29786
|
var import_zod22 = require("zod");
|
|
28623
29787
|
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
28624
29788
|
var import_crypto2 = require("crypto");
|
|
@@ -29083,9 +30247,9 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
29083
30247
|
|
|
29084
30248
|
### 7. Only populate \`corrected_text\` when \`needs_correction\` is true. If the OCR output is accurate, return \`needs_correction: false\` and \`corrected_content: null\`.
|
|
29085
30249
|
`;
|
|
29086
|
-
const result = await (0,
|
|
30250
|
+
const result = await (0, import_ai18.generateText)({
|
|
29087
30251
|
model,
|
|
29088
|
-
output:
|
|
30252
|
+
output: import_ai18.Output.object({
|
|
29089
30253
|
schema: import_zod22.z.object({
|
|
29090
30254
|
needs_correction: import_zod22.z.boolean(),
|
|
29091
30255
|
corrected_text: import_zod22.z.string().nullable(),
|