@exulu/backend 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2875,126 +2875,14 @@ var init_wrap_execute = __esm({
2875
2875
  }
2876
2876
  });
2877
2877
 
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
2878
  // src/templates/tools/session-items-retrieval-tool.ts
2991
- var import_zod3, createSessionItemsRetrievalTool;
2879
+ var import_zod2, createSessionItemsRetrievalTool;
2992
2880
  var init_session_items_retrieval_tool = __esm({
2993
2881
  "src/templates/tools/session-items-retrieval-tool.ts"() {
2994
2882
  "use strict";
2995
2883
  init_cjs_shims();
2996
2884
  init_tool();
2997
- import_zod3 = require("zod");
2885
+ import_zod2 = require("zod");
2998
2886
  createSessionItemsRetrievalTool = async ({
2999
2887
  user,
3000
2888
  role,
@@ -3006,8 +2894,8 @@ var init_session_items_retrieval_tool = __esm({
3006
2894
  id: "session_items_information_context_search",
3007
2895
  name: "context_search in knowledge items added to session.",
3008
2896
  description: "Context search in knowledge items added to session.",
3009
- inputSchema: import_zod3.z.object({
3010
- query: import_zod3.z.string().describe("The query to retrieve information from knowledge items added to the session.")
2897
+ inputSchema: import_zod2.z.object({
2898
+ query: import_zod2.z.string().describe("The query to retrieve information from knowledge items added to the session.")
3011
2899
  }),
3012
2900
  type: "context",
3013
2901
  category: "session",
@@ -3079,6 +2967,80 @@ var init_session_items_retrieval_tool = __esm({
3079
2967
  }
3080
2968
  });
3081
2969
 
2970
+ // ee/agentic-retrieval/pipeline/global-ids.ts
2971
+ function parsePreselectedItems(globalIds) {
2972
+ const map = /* @__PURE__ */ new Map();
2973
+ for (const gid of globalIds) {
2974
+ const slashIdx = gid.indexOf("/");
2975
+ if (slashIdx === -1) {
2976
+ if (gid) map.set(gid, null);
2977
+ continue;
2978
+ }
2979
+ const contextId = gid.slice(0, slashIdx);
2980
+ const itemId = gid.slice(slashIdx + 1);
2981
+ if (!contextId || !itemId) continue;
2982
+ if (map.get(contextId) === null) continue;
2983
+ const existing = map.get(contextId) ?? [];
2984
+ existing.push(itemId);
2985
+ map.set(contextId, existing);
2986
+ }
2987
+ return map;
2988
+ }
2989
+ var init_global_ids = __esm({
2990
+ "ee/agentic-retrieval/pipeline/global-ids.ts"() {
2991
+ "use strict";
2992
+ init_cjs_shims();
2993
+ }
2994
+ });
2995
+
2996
+ // ee/agentic-retrieval/pipeline/project-scope.ts
2997
+ function resolveProjectScope(opts) {
2998
+ const { scope, enabledContextIds, availableContextIds } = opts;
2999
+ if (!scope || scope.items.length === 0) return void 0;
3000
+ const itemsByContext = parsePreselectedItems(scope.items);
3001
+ const pinsByContext = /* @__PURE__ */ new Map();
3002
+ const scopedItemsByContext = /* @__PURE__ */ new Map();
3003
+ const addedContextIds = [];
3004
+ const allProjectContextIds = [];
3005
+ for (const [ctxId, itemIds] of itemsByContext) {
3006
+ if (!availableContextIds.has(ctxId)) {
3007
+ console.warn(
3008
+ `[EXULU pipeline] project "${scope.name}" references unknown context "${ctxId}" \u2014 skipping those items.`
3009
+ );
3010
+ continue;
3011
+ }
3012
+ allProjectContextIds.push(ctxId);
3013
+ if (enabledContextIds.has(ctxId)) {
3014
+ if (itemIds && itemIds.length > 0) pinsByContext.set(ctxId, new Set(itemIds));
3015
+ } else {
3016
+ scopedItemsByContext.set(ctxId, itemIds);
3017
+ addedContextIds.push(ctxId);
3018
+ }
3019
+ }
3020
+ if (allProjectContextIds.length === 0) return void 0;
3021
+ return { pinsByContext, scopedItemsByContext, addedContextIds, allProjectContextIds };
3022
+ }
3023
+ function buildProjectKbProfileDefaults(items) {
3024
+ const defaults = {};
3025
+ for (const gid of items) {
3026
+ const slashIdx = gid.indexOf("/");
3027
+ const ctxId = slashIdx === -1 ? gid : gid.slice(0, slashIdx);
3028
+ if (ctxId === TRANSCRIPTIONS_CONTEXT_ID && !defaults[ctxId]) {
3029
+ defaults[ctxId] = { enabled: true, kind: "conversations", instructions: "", overrides: {} };
3030
+ }
3031
+ }
3032
+ return defaults;
3033
+ }
3034
+ var TRANSCRIPTIONS_CONTEXT_ID;
3035
+ var init_project_scope = __esm({
3036
+ "ee/agentic-retrieval/pipeline/project-scope.ts"() {
3037
+ "use strict";
3038
+ init_cjs_shims();
3039
+ init_global_ids();
3040
+ TRANSCRIPTIONS_CONTEXT_ID = "transcriptions";
3041
+ }
3042
+ });
3043
+
3082
3044
  // src/utils/sanitize-tool-name.ts
3083
3045
  function sanitizeToolName(name) {
3084
3046
  if (typeof name !== "string") return "";
@@ -3106,19 +3068,19 @@ var init_sanitize_tool_name = __esm({
3106
3068
  });
3107
3069
 
3108
3070
  // src/templates/tools/memory-tool.ts
3109
- var import_zod4, createNewMemoryItemTool;
3071
+ var import_zod3, createNewMemoryItemTool;
3110
3072
  var init_memory_tool = __esm({
3111
3073
  "src/templates/tools/memory-tool.ts"() {
3112
3074
  "use strict";
3113
3075
  init_cjs_shims();
3114
3076
  init_tool();
3115
- import_zod4 = require("zod");
3077
+ import_zod3 = require("zod");
3116
3078
  init_sanitize_name();
3117
3079
  createNewMemoryItemTool = (agent, context) => {
3118
3080
  const fields = {
3119
- name: import_zod4.z.string().describe("The name of the item to create"),
3120
- description: import_zod4.z.string().describe("The description of the item to create"),
3121
- surroundingContext: import_zod4.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...")
3081
+ name: import_zod3.z.string().describe("The name of the item to create"),
3082
+ description: import_zod3.z.string().describe("The description of the item to create"),
3083
+ 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
3084
  };
3123
3085
  for (const field of context.fields) {
3124
3086
  switch (field.type) {
@@ -3126,47 +3088,47 @@ var init_memory_tool = __esm({
3126
3088
  case "longText":
3127
3089
  case "shortText":
3128
3090
  case "code":
3129
- fields[field.name] = import_zod4.z.string().describe("The " + field.name + " of the item to create");
3091
+ fields[field.name] = import_zod3.z.string().describe("The " + field.name + " of the item to create");
3130
3092
  break;
3131
3093
  case "enum":
3132
3094
  if (field.enumValues && field.enumValues.length > 0) {
3133
3095
  const enumValues = field.enumValues;
3134
- fields[field.name] = import_zod4.z.preprocess(
3096
+ fields[field.name] = import_zod3.z.preprocess(
3135
3097
  (v) => typeof v === "string" ? v.toUpperCase() : v,
3136
- import_zod4.z.enum(enumValues)
3098
+ import_zod3.z.enum(enumValues)
3137
3099
  ).describe(
3138
3100
  "The " + field.name + " of the item to create. Must be one of: " + field.enumValues.join(", ")
3139
3101
  );
3140
3102
  } else {
3141
- fields[field.name] = import_zod4.z.string().describe("The " + field.name + " of the item to create");
3103
+ fields[field.name] = import_zod3.z.string().describe("The " + field.name + " of the item to create");
3142
3104
  }
3143
3105
  break;
3144
3106
  case "json":
3145
- fields[field.name] = import_zod4.z.string({}).describe(
3107
+ fields[field.name] = import_zod3.z.string({}).describe(
3146
3108
  "The " + field.name + " of the item to create, it should be a valid JSON string."
3147
3109
  );
3148
3110
  break;
3149
3111
  case "markdown":
3150
- fields[field.name] = import_zod4.z.string().describe(
3112
+ fields[field.name] = import_zod3.z.string().describe(
3151
3113
  "The " + field.name + " of the item to create, it should be a valid Markdown string."
3152
3114
  );
3153
3115
  break;
3154
3116
  case "number":
3155
- fields[field.name] = import_zod4.z.number().describe("The " + field.name + " of the item to create");
3117
+ fields[field.name] = import_zod3.z.number().describe("The " + field.name + " of the item to create");
3156
3118
  break;
3157
3119
  case "boolean":
3158
- fields[field.name] = import_zod4.z.boolean().describe("The " + field.name + " of the item to create");
3120
+ fields[field.name] = import_zod3.z.boolean().describe("The " + field.name + " of the item to create");
3159
3121
  break;
3160
3122
  case "file":
3161
3123
  case "uuid":
3162
3124
  case "date":
3163
3125
  break;
3164
3126
  default:
3165
- fields[field.name] = import_zod4.z.string().describe("The " + field.name + " of the item to create");
3127
+ fields[field.name] = import_zod3.z.string().describe("The " + field.name + " of the item to create");
3166
3128
  break;
3167
3129
  }
3168
3130
  }
3169
- fields["visibility"] = import_zod4.z.enum(["private", "public"]).optional().describe(
3131
+ fields["visibility"] = import_zod3.z.enum(["private", "public"]).optional().describe(
3170
3132
  "Whether this memory is private to the user or shared (public). Ask the user if unknown."
3171
3133
  );
3172
3134
  const toolName = "create_" + sanitizeName(context.name) + "_memory_item";
@@ -3176,7 +3138,7 @@ var init_memory_tool = __esm({
3176
3138
  category: agent.name + "_memory",
3177
3139
  description: "Create a new memory item in the " + agent.name + " memory context",
3178
3140
  type: "function",
3179
- inputSchema: import_zod4.z.object(fields),
3141
+ inputSchema: import_zod3.z.object(fields),
3180
3142
  config: [],
3181
3143
  execute: async (params) => {
3182
3144
  const { name, description, surroundingContext, information, visibility, exuluConfig, user } = params;
@@ -3747,9 +3709,9 @@ Probe error: ${probe.reason ?? "(no detail)"}`
3747
3709
  });
3748
3710
  const writeFileTool = (0, import_ai2.tool)({
3749
3711
  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: import_zod5.z.object({
3751
- path: import_zod5.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."),
3752
- content: import_zod5.z.string().describe("The content to write to the file")
3712
+ inputSchema: import_zod4.z.object({
3713
+ 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."),
3714
+ content: import_zod4.z.string().describe("The content to write to the file")
3753
3715
  }),
3754
3716
  execute: async ({ path: path2, content }) => {
3755
3717
  const resolvedPath = resolveSessionPath(path2, sessionDir);
@@ -3768,8 +3730,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
3768
3730
  });
3769
3731
  const readFileTool = (0, import_ai2.tool)({
3770
3732
  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: import_zod5.z.object({
3772
- path: import_zod5.z.string().describe("The path of the file to read. Relative paths and leading-slash paths are both resolved against the session sandbox root.")
3733
+ inputSchema: import_zod4.z.object({
3734
+ 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
3735
  }),
3774
3736
  execute: async ({ path: path2 }) => {
3775
3737
  const resolvedPath = resolveSessionPath(path2, sessionDir);
@@ -3780,8 +3742,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
3780
3742
  const originalBashTool = tools.bash;
3781
3743
  const bashTool = (0, import_ai2.tool)({
3782
3744
  description: originalBashTool.description ?? "",
3783
- inputSchema: import_zod5.z.object({
3784
- command: import_zod5.z.string().describe("The bash command to execute.")
3745
+ inputSchema: import_zod4.z.object({
3746
+ command: import_zod4.z.string().describe("The bash command to execute.")
3785
3747
  }),
3786
3748
  execute: async (args, opts) => {
3787
3749
  const before = persistenceEnabled ? await snapshotSessionArtifacts() : null;
@@ -3851,7 +3813,7 @@ ${lines.join("\n")}`;
3851
3813
  sandboxCache.set(sessionId, { handle, installedSkills });
3852
3814
  return handle;
3853
3815
  }
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, import_zod5, import_crypto_js4, getAllExuluVariables, execAsync2, EXEC_MAX_BUFFER, sandboxProbePromise, SANDBOX_FALLBACK_INSTRUCTIONS, degradedModeLogged, sandboxCache;
3816
+ 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
3817
  var init_create_sandbox = __esm({
3856
3818
  "ee/invoke-skills/create-sandbox.ts"() {
3857
3819
  "use strict";
@@ -3866,7 +3828,7 @@ var init_create_sandbox = __esm({
3866
3828
  init_system_dependencies();
3867
3829
  import_bash_tool = require("bash-tool");
3868
3830
  import_ai2 = require("ai");
3869
- import_zod5 = require("zod");
3831
+ import_zod4 = require("zod");
3870
3832
  init_variable();
3871
3833
  import_crypto_js4 = __toESM(require("crypto-js"), 1);
3872
3834
  init_client();
@@ -3910,9 +3872,9 @@ var init_truncate_tool_output = __esm({
3910
3872
  "src/utils/truncate-tool-output.ts"() {
3911
3873
  "use strict";
3912
3874
  init_cjs_shims();
3913
- truncateToolOutput = (output, maxContextLength, toolName, tailFraction = 0.1) => {
3875
+ truncateToolOutput = (output, maxContextLength, toolName, tailFraction = 0.1, charLimitOverride) => {
3914
3876
  const effectiveCtx = maxContextLength != null && maxContextLength > 0 ? maxContextLength : 128e3;
3915
- const charLimit = Math.floor(effectiveCtx * 0.25 * 4);
3877
+ const charLimit = charLimitOverride != null && charLimitOverride > 0 ? charLimitOverride : Math.floor(effectiveCtx * 0.25 * 4);
3916
3878
  const clampedTail = Math.min(1, Math.max(0, tailFraction));
3917
3879
  if (output.length <= charLimit) return output;
3918
3880
  const headChars = Math.floor(charLimit * (1 - clampedTail));
@@ -3934,13 +3896,257 @@ var init_truncate_tool_output = __esm({
3934
3896
  }
3935
3897
  });
3936
3898
 
3899
+ // src/exulu/context-budget.ts
3900
+ var DEFAULT_CONTEXT_WINDOW, deriveContextBudget, estimateTokens, estimateMessageTokens, getCompaction, sliceHistoryAtCheckpoint, contextOccupancy, CONTEXT_COMPACTION_REQUIRED, COMPACTION_INSUFFICIENT, ContextCompactionRequiredError, PROVIDER_CONTEXT_ERROR_PATTERNS, isProviderContextLengthError, mapStreamErrorMessage;
3901
+ var init_context_budget = __esm({
3902
+ "src/exulu/context-budget.ts"() {
3903
+ "use strict";
3904
+ init_cjs_shims();
3905
+ DEFAULT_CONTEXT_WINDOW = 128e3;
3906
+ deriveContextBudget = (contextWindowInput) => {
3907
+ const contextWindow = contextWindowInput != null && contextWindowInput > 0 ? contextWindowInput : DEFAULT_CONTEXT_WINDOW;
3908
+ const outputReserve = Math.min(32e3, Math.floor(contextWindow * 0.2));
3909
+ const usableWindow = contextWindow - outputReserve;
3910
+ return {
3911
+ contextWindow,
3912
+ outputReserve,
3913
+ usableWindow,
3914
+ warnThreshold: Math.floor(usableWindow * 0.8),
3915
+ blockThreshold: Math.floor(usableWindow * 0.95),
3916
+ toolOutputCapTokens: Math.min(25e3, Math.max(4e3, Math.floor(contextWindow * 0.1))),
3917
+ compactionTailTokens: Math.floor(usableWindow * 0.1),
3918
+ summaryBudgetTokens: Math.min(8e3, Math.floor(usableWindow * 0.05))
3919
+ };
3920
+ };
3921
+ estimateTokens = (text) => text ? Math.ceil(text.length / 4) : 0;
3922
+ estimateMessageTokens = (message) => estimateTokens(JSON.stringify(message));
3923
+ getCompaction = (message) => message.metadata?.compaction;
3924
+ sliceHistoryAtCheckpoint = (messages) => {
3925
+ let checkpointIdx = -1;
3926
+ for (let i = messages.length - 1; i >= 0; i--) {
3927
+ if (getCompaction(messages[i])) {
3928
+ checkpointIdx = i;
3929
+ break;
3930
+ }
3931
+ }
3932
+ if (checkpointIdx === -1) return messages;
3933
+ const checkpoint = messages[checkpointIdx];
3934
+ const coversUpTo = getCompaction(checkpoint).coversUpTo;
3935
+ const coversIdx = messages.findIndex((m) => m.id === coversUpTo);
3936
+ const boundary = coversIdx === -1 ? checkpointIdx : coversIdx;
3937
+ const after = messages.filter((m, i) => i > boundary && i !== checkpointIdx);
3938
+ return [checkpoint, ...after];
3939
+ };
3940
+ contextOccupancy = (messages) => {
3941
+ let anchorIdx = -1;
3942
+ for (let i = messages.length - 1; i >= 0; i--) {
3943
+ const m = messages[i];
3944
+ const meta = m.metadata;
3945
+ if (getCompaction(m) || m.role === "assistant" && (typeof meta?.inputTokens === "number" || typeof meta?.lastStepInputTokens === "number")) {
3946
+ anchorIdx = i;
3947
+ break;
3948
+ }
3949
+ }
3950
+ let total = 0;
3951
+ let rest = messages;
3952
+ if (anchorIdx !== -1) {
3953
+ const anchor = messages[anchorIdx];
3954
+ const compaction = getCompaction(anchor);
3955
+ if (compaction) {
3956
+ total = compaction.occupancyEstimate;
3957
+ } else {
3958
+ const meta = anchor.metadata;
3959
+ total = typeof meta.lastStepInputTokens === "number" ? meta.lastStepInputTokens + (meta.lastStepOutputTokens ?? 0) : (meta.inputTokens ?? 0) + (meta.outputTokens ?? 0);
3960
+ }
3961
+ rest = messages.slice(anchorIdx + 1);
3962
+ }
3963
+ for (const m of rest) total += estimateMessageTokens(m);
3964
+ return total;
3965
+ };
3966
+ CONTEXT_COMPACTION_REQUIRED = "CONTEXT_COMPACTION_REQUIRED";
3967
+ COMPACTION_INSUFFICIENT = "COMPACTION_INSUFFICIENT";
3968
+ ContextCompactionRequiredError = class extends Error {
3969
+ constructor(occupancy, budget) {
3970
+ super(
3971
+ JSON.stringify({
3972
+ code: CONTEXT_COMPACTION_REQUIRED,
3973
+ 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.`,
3974
+ occupancy,
3975
+ usableWindow: budget.usableWindow,
3976
+ contextWindow: budget.contextWindow
3977
+ })
3978
+ );
3979
+ this.occupancy = occupancy;
3980
+ this.budget = budget;
3981
+ this.name = "ContextCompactionRequiredError";
3982
+ }
3983
+ };
3984
+ PROVIDER_CONTEXT_ERROR_PATTERNS = [
3985
+ /ContextWindowExceededError/i,
3986
+ /context.?window/i,
3987
+ /context.?length/i,
3988
+ /maximum context/i,
3989
+ /prompt is too long/i,
3990
+ /input is too long/i,
3991
+ /token count exceeds/i,
3992
+ /too many tokens/i
3993
+ ];
3994
+ isProviderContextLengthError = (message) => PROVIDER_CONTEXT_ERROR_PATTERNS.some((re) => re.test(message));
3995
+ mapStreamErrorMessage = (message) => isProviderContextLengthError(message) ? JSON.stringify({
3996
+ code: CONTEXT_COMPACTION_REQUIRED,
3997
+ message: "The model rejected the request because the conversation exceeds its context window. Compact the conversation to continue.",
3998
+ providerMessage: message.slice(0, 500)
3999
+ }) : message;
4000
+ }
4001
+ });
4002
+
4003
+ // src/exulu/tool-output-offload.ts
4004
+ var import_node_crypto3, PREVIEW_CHARS, storeAsSessionFile, buildNotice, guardToolOutput, guardExtractedFileText;
4005
+ var init_tool_output_offload = __esm({
4006
+ "src/exulu/tool-output-offload.ts"() {
4007
+ "use strict";
4008
+ init_cjs_shims();
4009
+ import_node_crypto3 = require("crypto");
4010
+ init_uppy();
4011
+ init_context_budget();
4012
+ PREVIEW_CHARS = 4e3;
4013
+ storeAsSessionFile = async (serialized, ctx) => {
4014
+ if (!ctx.sessionID || !ctx.exuluConfig?.fileUploads) return void 0;
4015
+ const safeTool = ctx.toolName.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
4016
+ const name = `tool-output-${safeTool}-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}.txt`;
4017
+ try {
4018
+ await uploadFile(
4019
+ Buffer.from(serialized, "utf-8"),
4020
+ `sessions/${ctx.sessionID}/${name}`,
4021
+ ctx.exuluConfig,
4022
+ { contentType: "text/plain" },
4023
+ ctx.user?.id
4024
+ );
4025
+ return name;
4026
+ } catch (err) {
4027
+ console.error("[EXULU] Failed to offload oversized tool output to session files.", err);
4028
+ return void 0;
4029
+ }
4030
+ };
4031
+ 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.`;
4032
+ guardToolOutput = async (value, ctx) => {
4033
+ if (value == null) return value;
4034
+ let serialized;
4035
+ try {
4036
+ serialized = typeof value === "string" ? value : JSON.stringify(value);
4037
+ } catch {
4038
+ return value;
4039
+ }
4040
+ if (typeof serialized !== "string") return value;
4041
+ const budget = deriveContextBudget(ctx.contextWindow);
4042
+ const tokens = estimateTokens(serialized);
4043
+ if (tokens <= budget.toolOutputCapTokens) return value;
4044
+ const sessionFile = await storeAsSessionFile(serialized, ctx);
4045
+ const result = {
4046
+ truncated: true,
4047
+ notice: buildNotice(tokens, budget.toolOutputCapTokens, sessionFile),
4048
+ ...sessionFile ? { sessionFile } : {},
4049
+ preview: serialized.slice(0, PREVIEW_CHARS)
4050
+ };
4051
+ return result;
4052
+ };
4053
+ guardExtractedFileText = async (filename, text, ctx) => {
4054
+ const budget = deriveContextBudget(ctx.contextWindow);
4055
+ const tokens = estimateTokens(text);
4056
+ if (tokens <= budget.toolOutputCapTokens) return text;
4057
+ const sessionFile = await storeAsSessionFile(text, { ...ctx, toolName: `upload-${filename}` });
4058
+ 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.]`;
4059
+ return `${text.slice(0, PREVIEW_CHARS)}
4060
+
4061
+ ${notice}`;
4062
+ };
4063
+ }
4064
+ });
4065
+
4066
+ // src/templates/tools/session-file-read-tool.ts
4067
+ var import_zod5, DEFAULT_LIMIT, MAX_CONTENT_CHARS, createSessionFileReadTool;
4068
+ var init_session_file_read_tool = __esm({
4069
+ "src/templates/tools/session-file-read-tool.ts"() {
4070
+ "use strict";
4071
+ init_cjs_shims();
4072
+ import_zod5 = require("zod");
4073
+ init_tool();
4074
+ init_uppy();
4075
+ DEFAULT_LIMIT = 250;
4076
+ MAX_CONTENT_CHARS = 16e3;
4077
+ createSessionFileReadTool = ({
4078
+ sessionID,
4079
+ user,
4080
+ exuluConfig
4081
+ }) => {
4082
+ if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
4083
+ const readSessionFileExecute = async ({ filename, offset, limit }) => {
4084
+ const safeName = String(filename ?? "").trim();
4085
+ if (!safeName || safeName.includes("..") || safeName.includes("/") || safeName.includes("\\")) {
4086
+ return {
4087
+ error: "Invalid filename \u2014 pass the bare file name exactly as listed in the session files (no paths)."
4088
+ };
4089
+ }
4090
+ const uploads = exuluConfig.fileUploads;
4091
+ const generalPrefix = uploads.s3prefix ? `${uploads.s3prefix.replace(/\/$/, "")}/` : "";
4092
+ const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
4093
+ try {
4094
+ const url = await getPresignedUrl(uploads.s3Bucket, key, exuluConfig);
4095
+ const res = await fetch(url);
4096
+ if (!res.ok) {
4097
+ return { error: `Could not read session file "${safeName}" (status ${res.status}). Check the exact file name.` };
4098
+ }
4099
+ const textBody = await res.text();
4100
+ const lines = textBody.split("\n");
4101
+ const start = (offset ?? 1) - 1;
4102
+ const requested = limit ?? DEFAULT_LIMIT;
4103
+ const sliced = lines.slice(start, start + requested);
4104
+ let content = sliced.join("\n");
4105
+ let linesReturned = sliced.length;
4106
+ if (content.length > MAX_CONTENT_CHARS) {
4107
+ content = content.slice(0, MAX_CONTENT_CHARS);
4108
+ linesReturned = Math.max(1, content.split("\n").length - 1);
4109
+ content = content + "\n[slice truncated \u2014 request fewer lines]";
4110
+ }
4111
+ return {
4112
+ content,
4113
+ totalLines: lines.length,
4114
+ offset: start + 1,
4115
+ linesReturned
4116
+ };
4117
+ } catch (err) {
4118
+ return { error: `Failed to read session file "${safeName}": ${err instanceof Error ? err.message : "unknown error"}` };
4119
+ }
4120
+ };
4121
+ return ExuluTool.internal({
4122
+ id: "read_session_file",
4123
+ name: "read_session_file",
4124
+ needsApproval: false,
4125
+ 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.",
4126
+ inputSchema: import_zod5.z.object({
4127
+ filename: import_zod5.z.string().describe('Exact session file name as referenced in a truncation notice, e.g. "tool-output-web_search-a1b2c3d4.txt"'),
4128
+ offset: import_zod5.z.number().int().min(1).optional().describe("1-based first line to read (default 1)"),
4129
+ limit: import_zod5.z.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT})`)
4130
+ }),
4131
+ type: "function",
4132
+ category: "session",
4133
+ config: [],
4134
+ // ExuluTool's execute type is modeled on retrieval tools ({result/job/items});
4135
+ // internal utility tools return richer shapes (memory-tool has the same
4136
+ // mismatch). The AI SDK passes the object through verbatim, so cast.
4137
+ execute: readSessionFileExecute
4138
+ });
4139
+ };
4140
+ }
4141
+ });
4142
+
3937
4143
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
3938
4144
  var convert_exulu_tools_to_ai_sdk_tools_exports = {};
3939
4145
  __export(convert_exulu_tools_to_ai_sdk_tools_exports, {
3940
4146
  convertExuluToolsToAiSdkTools: () => convertExuluToolsToAiSdkTools,
3941
4147
  hydrateVariables: () => hydrateVariables
3942
4148
  });
3943
- var import_client_s32, import_crypto_js5, import_node_crypto3, generateS3Key, s3Client2, getMimeType, hydrateVariables, convertExuluToolsToAiSdkTools;
4149
+ var import_client_s32, import_crypto_js5, import_node_crypto4, OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS, generateS3Key, s3Client2, getMimeType, hydrateVariables, convertExuluToolsToAiSdkTools;
3944
4150
  var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
3945
4151
  "src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts"() {
3946
4152
  "use strict";
@@ -3950,17 +4156,21 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
3950
4156
  init_statistics2();
3951
4157
  init_client();
3952
4158
  import_crypto_js5 = __toESM(require("crypto-js"), 1);
3953
- init_project_retrieval_tool();
3954
4159
  init_session_items_retrieval_tool();
3955
4160
  init_pipeline();
4161
+ init_project_scope();
3956
4162
  init_sanitize_tool_name();
3957
- import_node_crypto3 = require("crypto");
4163
+ import_node_crypto4 = require("crypto");
3958
4164
  init_statistics();
3959
4165
  init_memory_tool();
3960
4166
  init_create_sandbox();
3961
4167
  init_uppy();
3962
4168
  init_truncate_tool_output();
3963
- generateS3Key = (filename) => `${(0, import_node_crypto3.randomUUID)()}-${filename}`;
4169
+ init_tool_output_offload();
4170
+ init_session_file_read_tool();
4171
+ init_context_budget();
4172
+ OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
4173
+ generateS3Key = (filename) => `${(0, import_node_crypto4.randomUUID)()}-${filename}`;
3964
4174
  getMimeType = (type) => {
3965
4175
  switch (type) {
3966
4176
  case ".png":
@@ -4057,7 +4267,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4057
4267
  await Promise.all(promises2);
4058
4268
  return tool4;
4059
4269
  };
4060
- convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems) => {
4270
+ convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems, contextWindow, disabledTools) => {
4061
4271
  if (!currentTools) return {};
4062
4272
  if (!allExuluTools) {
4063
4273
  allExuluTools = [];
@@ -4065,6 +4275,8 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4065
4275
  if (!contexts) {
4066
4276
  contexts = [];
4067
4277
  }
4278
+ const budget = deriveContextBudget(contextWindow);
4279
+ const toolOutputCharLimit = budget.toolOutputCapTokens * 4;
4068
4280
  let sharedSessionSandbox;
4069
4281
  if (sessionID && exuluConfig && agent?.sandbox_enabled === true) {
4070
4282
  try {
@@ -4081,16 +4293,28 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4081
4293
  );
4082
4294
  }
4083
4295
  }
4084
- let projectRetrievalTool;
4085
- if (project) {
4086
- projectRetrievalTool = await createProjectItemsRetrievalTool({
4087
- user,
4088
- role: user?.role?.id,
4089
- contexts,
4090
- projectId: project
4091
- });
4092
- if (projectRetrievalTool) {
4093
- currentTools.push(projectRetrievalTool);
4296
+ const disabled = new Set(disabledTools ?? []);
4297
+ let projectScope;
4298
+ if (project && !disabled.has("agentic_context_search")) {
4299
+ const { db: db2 } = await postgresClient();
4300
+ const projectRow = await db2.from("projects").where("id", project).first();
4301
+ let rawItems = projectRow?.project_items;
4302
+ if (typeof rawItems === "string") {
4303
+ try {
4304
+ rawItems = JSON.parse(rawItems);
4305
+ } catch {
4306
+ rawItems = void 0;
4307
+ }
4308
+ }
4309
+ if (projectRow && Array.isArray(rawItems) && rawItems.length > 0) {
4310
+ projectScope = {
4311
+ id: projectRow.id,
4312
+ name: projectRow.name,
4313
+ description: projectRow.description ?? void 0,
4314
+ customInstructions: projectRow.custom_instructions ?? void 0,
4315
+ items: rawItems,
4316
+ kbProfileDefaults: buildProjectKbProfileDefaults(rawItems)
4317
+ };
4094
4318
  }
4095
4319
  }
4096
4320
  if (agent?.memory && contexts?.length) {
@@ -4101,7 +4325,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4101
4325
  );
4102
4326
  }
4103
4327
  const createNewMemoryTool = createNewMemoryItemTool(agent, context);
4104
- if (createNewMemoryTool) {
4328
+ if (createNewMemoryTool && !disabled.has(createNewMemoryTool.id)) {
4105
4329
  if (!currentTools) {
4106
4330
  currentTools = [];
4107
4331
  }
@@ -4116,31 +4340,62 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4116
4340
  contexts,
4117
4341
  items: sessionItems
4118
4342
  });
4119
- if (sessionItemsRetrievalTool) {
4343
+ if (sessionItemsRetrievalTool && !disabled.has(sessionItemsRetrievalTool.id)) {
4120
4344
  currentTools.push(sessionItemsRetrievalTool);
4121
4345
  }
4122
4346
  }
4347
+ const sessionFileReadTool = createSessionFileReadTool({ sessionID, user, exuluConfig });
4348
+ if (sessionFileReadTool && !disabled.has(sessionFileReadTool.id)) {
4349
+ currentTools.push(sessionFileReadTool);
4350
+ }
4123
4351
  console.log("[EXULU] Creating agentic search tool", contexts?.length, model);
4124
- if (contexts?.length && model) {
4125
- const agenticSearchTool = createAgenticRetrievalTool({
4126
- contexts: contexts.filter((context) => context.id !== agent?.memory),
4127
- // memory is searched by the memory phase, not as a KB
4128
- memoryContext: agent?.memory ? contexts.find((c) => c.id === agent.memory) : void 0,
4129
- user,
4130
- role: user?.role?.id,
4131
- model,
4132
- preselected: sessionItems,
4133
- memoryItems
4134
- });
4135
- if (agenticSearchTool) {
4136
- const index = currentTools.findIndex((tool4) => tool4.id === "agentic_context_search");
4137
- if (index !== -1) {
4352
+ if (contexts?.length && model && !disabled.has("agentic_context_search")) {
4353
+ const index = currentTools.findIndex((tool4) => tool4.id === "agentic_context_search");
4354
+ const memoryContext = agent?.memory ? contexts.find((c) => c.id === agent.memory) : void 0;
4355
+ if (index !== -1) {
4356
+ const agenticSearchTool = createAgenticRetrievalTool({
4357
+ contexts: contexts.filter((context) => context.id !== agent?.memory),
4358
+ // memory is searched by the memory phase, not as a KB
4359
+ memoryContext,
4360
+ user,
4361
+ role: user?.role?.id,
4362
+ model,
4363
+ preselected: sessionItems,
4364
+ memoryItems,
4365
+ projectScope
4366
+ });
4367
+ if (agenticSearchTool) {
4138
4368
  currentTools[index] = {
4139
4369
  ...currentTools[index],
4140
4370
  // important to keep the original tool config
4141
4371
  ...agenticSearchTool
4142
4372
  };
4143
4373
  }
4374
+ } else if (projectScope) {
4375
+ const projectContextIds = new Set(
4376
+ projectScope.items.map((gid) => {
4377
+ const i = gid.indexOf("/");
4378
+ return i === -1 ? gid : gid.slice(0, i);
4379
+ })
4380
+ );
4381
+ const scopedContexts = contexts.filter(
4382
+ (c) => projectContextIds.has(c.id) && c.id !== agent?.memory
4383
+ );
4384
+ if (scopedContexts.length > 0) {
4385
+ const projectSearchTool = createAgenticRetrievalTool({
4386
+ contexts: scopedContexts,
4387
+ memoryContext,
4388
+ user,
4389
+ role: user?.role?.id,
4390
+ model,
4391
+ preselected: [...sessionItems ?? [], ...projectScope.items],
4392
+ memoryItems,
4393
+ projectScope
4394
+ });
4395
+ if (projectSearchTool) {
4396
+ currentTools.push(projectSearchTool);
4397
+ }
4398
+ }
4144
4399
  }
4145
4400
  } else {
4146
4401
  const agenticSearchTool = currentTools.find((tool4) => tool4.id === "agentic_context_search");
@@ -4168,7 +4423,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4168
4423
  if (typeof result?.content === "string") {
4169
4424
  return {
4170
4425
  ...result,
4171
- content: truncateToolOutput(result.content, agent?.maxContextLength, "readFile", 0.05)
4426
+ content: truncateToolOutput(result.content, budget.contextWindow, "readFile", 0.05, toolOutputCharLimit)
4172
4427
  };
4173
4428
  }
4174
4429
  return result;
@@ -4185,10 +4440,10 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4185
4440
  return {
4186
4441
  ...result,
4187
4442
  ...typeof result?.stdout === "string" && {
4188
- stdout: truncateToolOutput(result.stdout, agent?.maxContextLength, "bash", 0.1)
4443
+ stdout: truncateToolOutput(result.stdout, budget.contextWindow, "bash", 0.1, toolOutputCharLimit)
4189
4444
  },
4190
4445
  ...typeof result?.stderr === "string" && {
4191
- stderr: truncateToolOutput(result.stderr, agent?.maxContextLength, "bash stderr", 0.4)
4446
+ stderr: truncateToolOutput(result.stderr, budget.contextWindow, "bash stderr", 0.4, toolOutputCharLimit)
4192
4447
  }
4193
4448
  };
4194
4449
  }
@@ -4324,16 +4579,30 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4324
4579
  user: user?.id,
4325
4580
  role: user?.role?.id
4326
4581
  });
4582
+ const guardCtx = {
4583
+ toolName: cur.name,
4584
+ contextWindow,
4585
+ sessionID,
4586
+ user,
4587
+ exuluConfig
4588
+ };
4589
+ const offloadExempt = OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS.has(cur.id);
4327
4590
  if (response && typeof response === "object" && Symbol.asyncIterator in response) {
4328
4591
  let lastValue;
4329
4592
  for await (const value of response) {
4330
4593
  yield value;
4331
4594
  lastValue = value;
4332
4595
  }
4333
- return lastValue;
4596
+ if (offloadExempt) return lastValue;
4597
+ const guarded = await guardToolOutput(lastValue, guardCtx);
4598
+ if (guarded !== lastValue) {
4599
+ yield guarded;
4600
+ }
4601
+ return guarded;
4334
4602
  } else {
4335
- yield response;
4336
- return response;
4603
+ const guarded = offloadExempt ? response : await guardToolOutput(response, guardCtx);
4604
+ yield guarded;
4605
+ return guarded;
4337
4606
  }
4338
4607
  }
4339
4608
  }
@@ -4346,7 +4615,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4346
4615
  });
4347
4616
 
4348
4617
  // src/exulu/tool.ts
4349
- var import_ai3, import_zod6, import_node_crypto4, PUBLIC_TOOL_TYPES, ExuluTool;
4618
+ var import_ai3, import_zod6, import_node_crypto5, PUBLIC_TOOL_TYPES, ExuluTool;
4350
4619
  var init_tool = __esm({
4351
4620
  "src/exulu/tool.ts"() {
4352
4621
  "use strict";
@@ -4354,7 +4623,7 @@ var init_tool = __esm({
4354
4623
  import_ai3 = require("ai");
4355
4624
  import_zod6 = require("zod");
4356
4625
  init_sanitize_name();
4357
- import_node_crypto4 = require("crypto");
4626
+ import_node_crypto5 = require("crypto");
4358
4627
  init_singleton();
4359
4628
  init_resolve_model();
4360
4629
  init_validate();
@@ -4481,7 +4750,7 @@ var init_tool = __esm({
4481
4750
  throw new Error("Tool " + sanitizeName(this.name) + " not found in " + JSON.stringify(tools));
4482
4751
  }
4483
4752
  console.log("[EXULU] Tool found", this.name);
4484
- const toolCallId = this.id + "_" + (0, import_node_crypto4.randomUUID)();
4753
+ const toolCallId = this.id + "_" + (0, import_node_crypto5.randomUUID)();
4485
4754
  console.log("[EXULU] Calling tool execute", {
4486
4755
  inputs,
4487
4756
  toolCallId,
@@ -4675,6 +4944,7 @@ function parsePipelineConfig(raw) {
4675
4944
  managedContext: boolVal(r["managed_context"]),
4676
4945
  requirePreselectedContexts: boolVal(r["require_preselected_contexts"]),
4677
4946
  logging: boolVal(r["logging"]),
4947
+ projectSearch: r["project_search"] === void 0 || r["project_search"] === "" ? true : boolVal(r["project_search"]),
4678
4948
  utilityModel: strVal(r["utility_model"], ""),
4679
4949
  knowledgeBases: jsonVal("knowledge_bases", knowledgeBasesSchema, r["knowledge_bases"]),
4680
4950
  routing: jsonVal("routing", routingSchema, r["routing"]),
@@ -6002,6 +6272,7 @@ async function searchContexts(opts) {
6002
6272
  role,
6003
6273
  model,
6004
6274
  preselectedItems,
6275
+ scopedItemsByContext,
6005
6276
  identifierPinsByContext,
6006
6277
  memoryPinnedItemIds,
6007
6278
  userPinnedItemIdsByContext,
@@ -6028,6 +6299,8 @@ async function searchContexts(opts) {
6028
6299
  let pinnedItemIds;
6029
6300
  if (hasPreselection) {
6030
6301
  pinnedItemIds = preselectedItems.get(ctxId) ?? [];
6302
+ } else if (scopedItemsByContext?.has(ctxId)) {
6303
+ pinnedItemIds = scopedItemsByContext.get(ctxId) ?? [];
6031
6304
  } else if (!skipPrefilter) {
6032
6305
  const identifierPins = identifierPinsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
6033
6306
  let pins = new Set(identifierPins);
@@ -6256,24 +6529,6 @@ var init_rerank = __esm({
6256
6529
  });
6257
6530
 
6258
6531
  // ee/agentic-retrieval/pipeline/index.ts
6259
- function parsePreselectedItems(globalIds) {
6260
- const map = /* @__PURE__ */ new Map();
6261
- for (const gid of globalIds) {
6262
- const slashIdx = gid.indexOf("/");
6263
- if (slashIdx === -1) {
6264
- if (gid) map.set(gid, null);
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
6532
  function addChunks(result, chunks) {
6278
6533
  const seen = new Set(result.chunks.map((c) => c.chunk_id));
6279
6534
  for (const chunk of chunks) {
@@ -6303,7 +6558,8 @@ function createAgenticRetrievalTool(opts) {
6303
6558
  model,
6304
6559
  instructions: adminInstructions,
6305
6560
  preselected,
6306
- memoryItems
6561
+ memoryItems,
6562
+ projectScope
6307
6563
  } = opts;
6308
6564
  const license = checkLicense();
6309
6565
  if (!license["agentic-retrieval"]) {
@@ -6313,7 +6569,9 @@ function createAgenticRetrievalTool(opts) {
6313
6569
  return ExuluTool.internal({
6314
6570
  id: "agentic_context_search",
6315
6571
  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).`,
6572
+ 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
6573
+ // config is off — the config is only known at execute time, not at factory time.
6574
+ (projectScope ? ` Also searches the knowledge items attached to the project "${projectScope.name}".` : ""),
6317
6575
  category: "contexts",
6318
6576
  needsApproval: false,
6319
6577
  type: "context",
@@ -6356,10 +6614,16 @@ function createAgenticRetrievalTool(opts) {
6356
6614
  },
6357
6615
  {
6358
6616
  name: "max_steps",
6359
- description: "Maximum reasoning/tool steps the CALLING agent may take on a message while this tool is enabled (bounds retry loops and token cost). 0 = platform default (5, or 10 with skills).",
6617
+ 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
6618
  type: "number",
6361
6619
  default: 0
6362
6620
  },
6621
+ {
6622
+ name: "project_search",
6623
+ 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).",
6624
+ type: "boolean",
6625
+ default: true
6626
+ },
6363
6627
  {
6364
6628
  name: "knowledge_bases",
6365
6629
  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 +6733,23 @@ function createAgenticRetrievalTool(opts) {
6469
6733
  }
6470
6734
  }
6471
6735
  const preselectedItems = parsePreselectedItems(preselected ?? []);
6736
+ const availableContextsById = new Map(contexts.map((c) => [c.id, c]));
6737
+ const resolvedProject = cfg.projectSearch ? resolveProjectScope({
6738
+ scope: projectScope,
6739
+ enabledContextIds: new Set(enabledContexts.map((c) => c.id)),
6740
+ availableContextIds: new Set(availableContextsById.keys())
6741
+ }) : void 0;
6742
+ if (resolvedProject) {
6743
+ if (projectScope?.kbProfileDefaults) {
6744
+ for (const [ctxId, profile] of Object.entries(projectScope.kbProfileDefaults)) {
6745
+ if (!cfg.knowledgeBases[ctxId]) cfg.knowledgeBases[ctxId] = profile;
6746
+ }
6747
+ }
6748
+ enabledContexts = [
6749
+ ...enabledContexts,
6750
+ ...resolvedProject.addedContextIds.map((id) => availableContextsById.get(id)).filter((c) => Boolean(c))
6751
+ ];
6752
+ }
6472
6753
  const contextsById = new Map(enabledContexts.map((c) => [c.id, c]));
6473
6754
  const kbKindById = new Map(
6474
6755
  enabledContexts.map((c) => [
@@ -6479,7 +6760,12 @@ function createAgenticRetrievalTool(opts) {
6479
6760
  const documentContexts = enabledContexts.filter(
6480
6761
  (c) => (cfg.knowledgeBases[c.id]?.kind ?? "documents") === "documents"
6481
6762
  );
6482
- const extraInstructions = [cfg.instructions, adminInstructions].filter(Boolean).join("\n");
6763
+ const extraInstructions = [
6764
+ cfg.instructions,
6765
+ adminInstructions,
6766
+ resolvedProject && projectScope?.customInstructions ? `Instructions for the attached project "${projectScope.name}":
6767
+ ${projectScope.customInstructions}` : ""
6768
+ ].filter(Boolean).join("\n");
6483
6769
  const [memResult, routResult] = await Promise.all([
6484
6770
  runMemoryPhase({
6485
6771
  memoryChunks: memoryItems ?? [],
@@ -6525,6 +6811,30 @@ function createAgenticRetrievalTool(opts) {
6525
6811
  yield { result: "The user has requested to search in knowledge bases that are not part of the preselected knowledge bases: " + missing.join(", ") };
6526
6812
  return;
6527
6813
  }
6814
+ let effectiveMainContexts = mainContexts;
6815
+ if (resolvedProject) {
6816
+ const mainSet = new Set(mainContexts);
6817
+ const appended = resolvedProject.allProjectContextIds.filter(
6818
+ (id) => !mainSet.has(id) && contextsById.has(id)
6819
+ );
6820
+ if (appended.length > 0) {
6821
+ effectiveMainContexts = [...mainContexts, ...appended];
6822
+ result.steps.push({
6823
+ stepNumber: 1,
6824
+ text: `Including sources from project "${projectScope.name}": ${appended.join(", ")}`,
6825
+ toolCalls: [],
6826
+ chunks: [],
6827
+ tokens: 0
6828
+ });
6829
+ result.reasoning.push({
6830
+ text: `Including project sources: ${appended.join(", ")}`,
6831
+ tools: []
6832
+ });
6833
+ }
6834
+ }
6835
+ const fallbackContextsToSearch = fallbackContexts.filter(
6836
+ (id) => !effectiveMainContexts.includes(id)
6837
+ );
6528
6838
  const {
6529
6839
  updatedQuestion,
6530
6840
  updatedKeywords,
@@ -6551,7 +6861,7 @@ function createAgenticRetrievalTool(opts) {
6551
6861
  }
6552
6862
  const [mainSearch, speculativeFallbackSearch] = await Promise.all([
6553
6863
  searchContexts({
6554
- contextIds: mainContexts,
6864
+ contextIds: effectiveMainContexts,
6555
6865
  contextsById,
6556
6866
  kbProfiles: cfg.knowledgeBases,
6557
6867
  question: updatedQuestion,
@@ -6564,13 +6874,14 @@ function createAgenticRetrievalTool(opts) {
6564
6874
  identifierPinsByContext,
6565
6875
  memoryPinnedItemIds,
6566
6876
  userPinnedItemIdsByContext,
6877
+ scopedItemsByContext: resolvedProject?.scopedItemsByContext,
6567
6878
  rewrites: cfg.vocabulary.rewrites,
6568
6879
  styleHint: cfg.vocabulary.styleHint,
6569
6880
  maxQueries: cfg.tuning.maxQueriesPerContext,
6570
6881
  skipPrefilter: false
6571
6882
  }),
6572
- fallbackContexts.length > 0 && !hasExplicitDocAndPage ? searchContexts({
6573
- contextIds: fallbackContexts,
6883
+ fallbackContextsToSearch.length > 0 && !hasExplicitDocAndPage ? searchContexts({
6884
+ contextIds: fallbackContextsToSearch,
6574
6885
  contextsById,
6575
6886
  kbProfiles: cfg.knowledgeBases,
6576
6887
  question: updatedQuestion,
@@ -6583,6 +6894,7 @@ function createAgenticRetrievalTool(opts) {
6583
6894
  identifierPinsByContext,
6584
6895
  memoryPinnedItemIds,
6585
6896
  userPinnedItemIdsByContext,
6897
+ scopedItemsByContext: resolvedProject?.scopedItemsByContext,
6586
6898
  rewrites: cfg.vocabulary.rewrites,
6587
6899
  styleHint: cfg.vocabulary.styleHint,
6588
6900
  maxQueries: cfg.tuning.maxQueriesPerContext,
@@ -6596,6 +6908,9 @@ function createAgenticRetrievalTool(opts) {
6596
6908
  })(),
6597
6909
  ...(function* () {
6598
6910
  for (const s of userPinnedItemIdsByContext.values()) yield* s;
6911
+ })(),
6912
+ ...(function* () {
6913
+ if (resolvedProject) for (const s of resolvedProject.pinsByContext.values()) yield* s;
6599
6914
  })()
6600
6915
  ]);
6601
6916
  const userPinnedItemIds = new Set(
@@ -6663,15 +6978,15 @@ function createAgenticRetrievalTool(opts) {
6663
6978
  result.reasoning.push({ text: "Literal lookup satisfied; skipping fallback.", tools: [] });
6664
6979
  yield { result: serializeOutput(result) };
6665
6980
  }
6666
- if (!literalLookupSatisfied && fallbackContexts.length > 0 && (reranker ? mainRerank.rerank_score_max_genuine < cfg.tuning.fallbackThreshold : mainRerank.limited_results.length < cfg.tuning.topK)) {
6981
+ if (!literalLookupSatisfied && fallbackContextsToSearch.length > 0 && (reranker ? mainRerank.rerank_score_max_genuine < cfg.tuning.fallbackThreshold : mainRerank.limited_results.length < cfg.tuning.topK)) {
6667
6982
  result.steps.push({
6668
6983
  stepNumber: 1,
6669
- text: `Using fallback search in ${fallbackContexts.join(", ")}`,
6984
+ text: `Using fallback search in ${fallbackContextsToSearch.join(", ")}`,
6670
6985
  toolCalls: [],
6671
6986
  chunks: [],
6672
6987
  tokens: 0
6673
6988
  });
6674
- result.reasoning.push({ text: `Fallback search in ${fallbackContexts.join(", ")}`, tools: [] });
6989
+ result.reasoning.push({ text: `Fallback search in ${fallbackContextsToSearch.join(", ")}`, tools: [] });
6675
6990
  yield { result: serializeOutput(result) };
6676
6991
  const fallbackRerank = await rerankResults({
6677
6992
  chunks: speculativeFallbackSearch.chunks,
@@ -6747,11 +7062,14 @@ var init_pipeline = __esm({
6747
7062
  init_resolve_model();
6748
7063
  init_singleton();
6749
7064
  init_config();
7065
+ init_project_scope();
6750
7066
  init_routing();
6751
7067
  init_memory();
6752
7068
  init_prefilter();
6753
7069
  init_search();
6754
7070
  init_rerank();
7071
+ init_global_ids();
7072
+ init_global_ids();
6755
7073
  }
6756
7074
  });
6757
7075
 
@@ -9086,6 +9404,13 @@ var agentsSchema = {
9086
9404
  name: "sandbox_enabled",
9087
9405
  type: "boolean",
9088
9406
  default: false
9407
+ },
9408
+ {
9409
+ // Per-turn budget for ALL tool steps on one chat message (bash, files,
9410
+ // knowledge search, integrations). 0/null = platform default
9411
+ // (DEFAULT_MAX_STEPS in resolve-max-steps.ts). Auto-ALTERed on boot.
9412
+ name: "max_tool_steps",
9413
+ type: "number"
9089
9414
  }
9090
9415
  ]
9091
9416
  };
@@ -11148,7 +11473,7 @@ var ExuluContext2 = class {
11148
11473
  embedder,
11149
11474
  chunker,
11150
11475
  processor,
11151
- active,
11476
+ active: active2,
11152
11477
  fields,
11153
11478
  queryRewriter,
11154
11479
  resultReranker,
@@ -11179,7 +11504,7 @@ var ExuluContext2 = class {
11179
11504
  this.description = description;
11180
11505
  this.embedder = embedder;
11181
11506
  this.chunker = chunker;
11182
- this.active = active;
11507
+ this.active = active2;
11183
11508
  this.queryRewriter = queryRewriter;
11184
11509
  this.resultReranker = resultReranker;
11185
11510
  this.entities = entities;
@@ -12179,7 +12504,6 @@ init_cjs_shims();
12179
12504
  init_pipeline();
12180
12505
  init_check_record_access();
12181
12506
  init_client();
12182
- init_project_retrieval_tool();
12183
12507
  init_singleton();
12184
12508
  init_supervisor();
12185
12509
  init_catalog();
@@ -12326,14 +12650,26 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
12326
12650
  )
12327
12651
  );
12328
12652
  if (args.project) {
12329
- const projectTool = await createProjectItemsRetrievalTool({
12330
- projectId: args.project,
12331
- user,
12332
- role: user.role?.id,
12333
- contexts
12334
- });
12335
- if (projectTool) {
12336
- result.tools.unshift(projectTool);
12653
+ const hasAgentic = result.tools.some(
12654
+ (tool4) => tool4?.id === "agentic_context_search"
12655
+ );
12656
+ if (!hasAgentic) {
12657
+ const instance2 = createAgenticRetrievalTool({
12658
+ contexts: [],
12659
+ user,
12660
+ role: user.role?.id,
12661
+ model: void 0
12662
+ });
12663
+ if (instance2) {
12664
+ result.tools.unshift({
12665
+ id: instance2.id,
12666
+ name: instance2.name,
12667
+ description: instance2.description,
12668
+ category: instance2.category,
12669
+ type: instance2.type,
12670
+ config: []
12671
+ });
12672
+ }
12337
12673
  }
12338
12674
  }
12339
12675
  result.tools = result.tools.filter((tool4) => tool4 !== null);
@@ -18259,11 +18595,11 @@ var import_body_parser = __toESM(require("body-parser"), 1);
18259
18595
  var import_crypto_js9 = __toESM(require("crypto-js"), 1);
18260
18596
  var import_openai = __toESM(require("openai"), 1);
18261
18597
  var import_fs3 = __toESM(require("fs"), 1);
18262
- var import_node_crypto7 = require("crypto");
18598
+ var import_node_crypto9 = require("crypto");
18263
18599
  var import_api2 = require("@opentelemetry/api");
18264
18600
  init_check_record_access();
18265
18601
  var import_jszip2 = __toESM(require("jszip"), 1);
18266
- var import_ai14 = require("ai");
18602
+ var import_ai15 = require("ai");
18267
18603
  var import_cookie_parser = __toESM(require("cookie-parser"), 1);
18268
18604
  init_statistics2();
18269
18605
 
@@ -18272,7 +18608,8 @@ init_cjs_shims();
18272
18608
 
18273
18609
  // src/exulu/resolve-max-steps.ts
18274
18610
  init_cjs_shims();
18275
- function resolveMaxStepsFromToolConfigs(toolConfigs) {
18611
+ var DEFAULT_MAX_STEPS = 10;
18612
+ function resolveRetrievalCallBudget(toolConfigs) {
18276
18613
  const agentic = toolConfigs?.find((t) => t.id === "agentic_context_search");
18277
18614
  if (!agentic?.config) return void 0;
18278
18615
  const entry = agentic.config.find((c) => c.name === "max_steps");
@@ -18281,15 +18618,26 @@ function resolveMaxStepsFromToolConfigs(toolConfigs) {
18281
18618
  const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
18282
18619
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
18283
18620
  }
18621
+ function resolveTurnStepBudget(maxStepCount, agent) {
18622
+ if (typeof maxStepCount === "number" && Number.isFinite(maxStepCount) && maxStepCount > 0) {
18623
+ return Math.floor(maxStepCount);
18624
+ }
18625
+ const raw = agent?.max_tool_steps;
18626
+ const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
18627
+ if (Number.isFinite(n) && n > 0) {
18628
+ return Math.floor(n);
18629
+ }
18630
+ return DEFAULT_MAX_STEPS;
18631
+ }
18284
18632
  function flattenPart(part) {
18285
18633
  const p = part;
18286
18634
  if (p?.type === "text") return p.text ?? "";
18287
18635
  if (p?.type === "tool-call") {
18288
- return `[searched ${p.toolName}: ${JSON.stringify(p.input ?? {}).slice(0, 300)}]`;
18636
+ return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
18289
18637
  }
18290
18638
  if (p?.type === "tool-result") {
18291
18639
  const out = p.output?.value ?? p.output;
18292
- return `[results from ${p.toolName}]: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
18640
+ return `The "${p.toolName}" tool returned: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
18293
18641
  }
18294
18642
  return "";
18295
18643
  }
@@ -18307,7 +18655,7 @@ function flattenToolHistory(messages) {
18307
18655
  return m;
18308
18656
  });
18309
18657
  }
18310
- var FINAL_ANSWER_INSTRUCTION = "Answer the user's original question now, in plain text, using only the material already retrieved above. Do not attempt any further tool calls.";
18658
+ 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
18659
  function finalAnswerGuard(maxSteps) {
18312
18660
  return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
18313
18661
  toolChoice: "none",
@@ -18320,6 +18668,73 @@ function finalAnswerGuard(maxSteps) {
18320
18668
  } : {}
18321
18669
  } : void 0;
18322
18670
  }
18671
+ function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
18672
+ if (limit == null || limit <= 0 || !agenticToolKey || !allToolKeys.includes(agenticToolKey)) {
18673
+ return () => void 0;
18674
+ }
18675
+ const remainingTools = allToolKeys.filter((k) => k !== agenticToolKey);
18676
+ return ({ steps }) => {
18677
+ const calls = (steps ?? []).flatMap((s) => s?.toolCalls ?? []).filter((c) => c?.toolName === agenticToolKey).length;
18678
+ if (calls < limit) return void 0;
18679
+ return { activeTools: remainingTools };
18680
+ };
18681
+ }
18682
+
18683
+ // src/exulu/context-guard.ts
18684
+ init_cjs_shims();
18685
+ init_context_budget();
18686
+ var KEEP_RECENT_TOOL_MESSAGES = 2;
18687
+ var COLLAPSE_KEEP_CHARS = 400;
18688
+ 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]";
18689
+ function contextGuard(contextWindow) {
18690
+ const budget = deriveContextBudget(contextWindow);
18691
+ return async ({ messages }) => {
18692
+ if (!Array.isArray(messages) || messages.length === 0) return void 0;
18693
+ const tokens = estimateTokens(JSON.stringify(messages));
18694
+ if (tokens < budget.usableWindow) return void 0;
18695
+ const toolIndices = messages.map((m, i) => m?.role === "tool" ? i : -1).filter((i) => i !== -1);
18696
+ const collapsible = new Set(toolIndices.slice(0, Math.max(0, toolIndices.length - KEEP_RECENT_TOOL_MESSAGES)));
18697
+ if (collapsible.size === 0) return void 0;
18698
+ let changed = false;
18699
+ const next = messages.map((m, i) => {
18700
+ if (!collapsible.has(i)) return m;
18701
+ const msg = m;
18702
+ if (!Array.isArray(msg.content)) return m;
18703
+ const content = msg.content.map((part) => {
18704
+ const p = part;
18705
+ if (p?.type !== "tool-result") return part;
18706
+ const out = p.output?.value ?? p.output;
18707
+ const asText = typeof out === "string" ? out : JSON.stringify(out ?? "");
18708
+ if (asText.length <= COLLAPSE_KEEP_CHARS + COLLAPSE_MARKER.length) return part;
18709
+ changed = true;
18710
+ return { ...part, output: { type: "text", value: asText.slice(0, COLLAPSE_KEEP_CHARS) + COLLAPSE_MARKER } };
18711
+ });
18712
+ return { ...m, content };
18713
+ });
18714
+ return changed ? { messages: next } : void 0;
18715
+ };
18716
+ }
18717
+ function composePrepareSteps(...guards) {
18718
+ return async (opts) => {
18719
+ let merged;
18720
+ let messages = opts.messages;
18721
+ for (const guard of guards) {
18722
+ const result = await guard({ ...opts, messages });
18723
+ if (!result) continue;
18724
+ merged = { ...merged ?? {}, ...result };
18725
+ if (Array.isArray(result.messages)) {
18726
+ messages = result.messages;
18727
+ }
18728
+ }
18729
+ if (merged && messages && !("messages" in merged)) {
18730
+ merged.messages = messages;
18731
+ }
18732
+ return merged;
18733
+ };
18734
+ }
18735
+
18736
+ // src/exulu/provider.ts
18737
+ init_sanitize_tool_name();
18323
18738
 
18324
18739
  // src/exulu/auto-decline-stale-approvals.ts
18325
18740
  init_cjs_shims();
@@ -18419,6 +18834,8 @@ async function clearSessionCurrentTask(session) {
18419
18834
  }
18420
18835
 
18421
18836
  // src/exulu/provider.ts
18837
+ init_context_budget();
18838
+ init_tool_output_offload();
18422
18839
  var ExuluProvider = class {
18423
18840
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
18424
18841
  // underscores and be a max length of 80 characters and at least 5 characters long.
@@ -18589,7 +19006,9 @@ var ExuluProvider = class {
18589
19006
  agent,
18590
19007
  instructions,
18591
19008
  maxStepCount,
18592
- onTokenUsage
19009
+ onTokenUsage,
19010
+ contextWindow,
19011
+ disabledTools
18593
19012
  }) => {
18594
19013
  console.log(
18595
19014
  "[EXULU] Called generate sync for agent: " + this.name,
@@ -18617,9 +19036,7 @@ var ExuluProvider = class {
18617
19036
  if (messages && session && user) {
18618
19037
  const previousMessages = await getAgentMessages({
18619
19038
  session,
18620
- user: user.id,
18621
- limit: 50,
18622
- page: 1
19039
+ user: user.id
18623
19040
  });
18624
19041
  const previousMessagesContent = previousMessages.map(
18625
19042
  (message) => JSON.parse(message.content)
@@ -18628,6 +19045,12 @@ var ExuluProvider = class {
18628
19045
  // append the new message to the previous messages:
18629
19046
  messages: [...previousMessagesContent, ...messages]
18630
19047
  });
19048
+ const contextBudget = deriveContextBudget(contextWindow);
19049
+ const occupancy = contextOccupancy(messages);
19050
+ if (occupancy >= contextBudget.blockThreshold) {
19051
+ throw new ContextCompactionRequiredError(occupancy, contextBudget);
19052
+ }
19053
+ messages = sliceHistoryAtCheckpoint(messages);
18631
19054
  }
18632
19055
  console.log(
18633
19056
  "[EXULU] Message count for agent: " + this.name,
@@ -18692,6 +19115,34 @@ var ExuluProvider = class {
18692
19115
  if (memoryContext) {
18693
19116
  system += "\n\n" + memoryContext;
18694
19117
  }
19118
+ const tools = await convertExuluToolsToAiSdkTools(
19119
+ currentTools,
19120
+ currentSkills,
19121
+ approvedTools,
19122
+ allExuluTools,
19123
+ toolConfigs,
19124
+ providerapikey,
19125
+ contexts,
19126
+ user,
19127
+ exuluConfig,
19128
+ session,
19129
+ req,
19130
+ project,
19131
+ sessionItems,
19132
+ model,
19133
+ agent,
19134
+ memoryItems,
19135
+ contextWindow,
19136
+ disabledTools
19137
+ );
19138
+ const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
19139
+ const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
19140
+ const retrievalGuard = retrievalBudgetGuard(
19141
+ resolveRetrievalCallBudget(toolConfigs),
19142
+ agenticToolKey,
19143
+ Object.keys(tools)
19144
+ );
19145
+ const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
18695
19146
  const includesContextSearchTool = currentTools?.some(
18696
19147
  (tool4) => tool4.name.toLowerCase().includes("context_search") || tool4.id.includes("context_search") || tool4.type === "context"
18697
19148
  );
@@ -18704,12 +19155,12 @@ var ExuluProvider = class {
18704
19155
  system += `
18705
19156
 
18706
19157
 
18707
-
19158
+
18708
19159
  When you use a context search tool, you will include references to the items
18709
19160
  retrieved from the tool call result inline in the response using this exact JSON format
18710
19161
  (all on one line, no line breaks):
18711
19162
  {item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
18712
-
19163
+
18713
19164
  IMPORTANT formatting rules:
18714
19165
  - 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
19166
  - Use the exact format shown above, all on ONE line
@@ -18717,9 +19168,9 @@ var ExuluProvider = class {
18717
19168
  - Use the context ID from the tool result
18718
19169
  - Include the file/item name, not the full path
18719
19170
  - Separate multiple citations with spaces
18720
-
19171
+
18721
19172
  Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
18722
-
19173
+
18723
19174
  The citations will be rendered as interactive badges in the UI.
18724
19175
  `;
18725
19176
  }
@@ -18730,12 +19181,12 @@ var ExuluProvider = class {
18730
19181
  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
19182
  (all on one line, no line breaks):
18732
19183
  {url: <url>, title: <title>, snippet: <snippet>}
18733
-
19184
+
18734
19185
  IMPORTANT formatting rules:
18735
19186
  - Use the exact format shown above, all on ONE line
18736
19187
  - Do NOT use quotes around field names or values
18737
19188
  - Separate multiple results with spaces
18738
-
19189
+
18739
19190
  Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
18740
19191
  `;
18741
19192
  }
@@ -18768,29 +19219,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
18768
19219
  system,
18769
19220
  prompt,
18770
19221
  maxRetries: 2,
18771
- tools: await convertExuluToolsToAiSdkTools(
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
- ),
19222
+ tools,
18789
19223
  // Stop after the image_generation tool fires — the widget IS the
18790
19224
  // assistant's response, no follow-up text turn is wanted (same
18791
19225
  // reasoning as question_ask: the UI artifact is the message).
18792
- prepareStep: finalAnswerGuard(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5),
18793
- stopWhen: [(0, import_ai11.stepCountIs)(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5), (0, import_ai11.hasToolCall)("image_generation")]
19226
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
19227
+ stopWhen: [(0, import_ai11.stepCountIs)(turnBudget), (0, import_ai11.hasToolCall)("image_generation")]
18794
19228
  });
18795
19229
  console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
18796
19230
  const {
@@ -18851,26 +19285,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
18851
19285
  ignoreIncompleteToolCalls: true
18852
19286
  }),
18853
19287
  maxRetries: 2,
18854
- tools: await convertExuluToolsToAiSdkTools(
18855
- currentTools,
18856
- currentSkills,
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")]
19288
+ tools,
19289
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
19290
+ stopWhen: [(0, import_ai11.stepCountIs)(turnBudget), (0, import_ai11.hasToolCall)("image_generation")]
18874
19291
  });
18875
19292
  if (statistics) {
18876
19293
  await Promise.all([
@@ -18922,7 +19339,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
18922
19339
  * - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
18923
19340
  * - Image files -> image parts (which ARE supported by Responses API)
18924
19341
  */
18925
- async processFilePartsInMessages(messages) {
19342
+ async processFilePartsInMessages(messages, offloadCtx) {
18926
19343
  const processedMessages = await Promise.all(
18927
19344
  messages.map(async (message) => {
18928
19345
  if (message.role !== "user" || !Array.isArray(message.parts)) {
@@ -18967,10 +19384,11 @@ When a tool execution is not approved by the user, do not retry it unless explic
18967
19384
  outputErrorToConsole: false,
18968
19385
  newlineDelimiter: "\n"
18969
19386
  });
19387
+ const guardedText = await guardExtractedFileText(filename, String(extractedText), offloadCtx);
18970
19388
  return {
18971
19389
  type: "text",
18972
19390
  text: `<file file name = "${filename}" >
18973
- ${extractedText}
19391
+ ${guardedText}
18974
19392
  </file>`
18975
19393
  };
18976
19394
  } catch (error) {
@@ -18986,7 +19404,6 @@ ${extractedText}
18986
19404
  ...message,
18987
19405
  parts: processedParts
18988
19406
  };
18989
- console.log("[EXULU] Result: " + JSON.stringify(result, null, 2));
18990
19407
  return result;
18991
19408
  })
18992
19409
  );
@@ -19009,7 +19426,9 @@ ${extractedText}
19009
19426
  exuluConfig,
19010
19427
  instructions,
19011
19428
  req,
19012
- maxStepCount
19429
+ maxStepCount,
19430
+ contextWindow,
19431
+ disabledTools
19013
19432
  }) => {
19014
19433
  if (!this.config) {
19015
19434
  console.error("[EXULU] Config is required for streaming.");
@@ -19030,9 +19449,7 @@ ${extractedText}
19030
19449
  console.log("[EXULU] loading previous messages from session: " + session);
19031
19450
  const previousMessages2 = await getAgentMessages({
19032
19451
  session,
19033
- user: user?.id,
19034
- limit: 50,
19035
- page: 1
19452
+ user: user?.id
19036
19453
  });
19037
19454
  previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
19038
19455
  }
@@ -19091,64 +19508,32 @@ ${extractedText}
19091
19508
  (message2, index, self) => index === self.findLastIndex((t) => t.id === message2.id)
19092
19509
  );
19093
19510
  const { messages: reconciledMessages, declined } = autoDeclineStaleApprovals(messages);
19094
- messages = reconciledMessages;
19095
- if (declined.length && session && user) {
19096
- await saveChat({ session, user: user.id, messages: declined });
19097
- }
19098
- messages = await this.processFilePartsInMessages(messages);
19099
- 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
- 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
- if (user?.personal_system_prompt?.trim()) {
19102
- system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
19103
- }
19104
- 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
- `;
19511
+ messages = reconciledMessages;
19512
+ if (declined.length && session && user) {
19513
+ await saveChat({ session, user: user.id, messages: declined });
19514
+ }
19515
+ messages = await this.processFilePartsInMessages(messages, {
19516
+ contextWindow,
19517
+ sessionID: session,
19518
+ user,
19519
+ exuluConfig
19520
+ });
19521
+ const chronologicalMessages = messages;
19522
+ const contextBudget = deriveContextBudget(contextWindow);
19523
+ const occupancy = contextOccupancy(chronologicalMessages);
19524
+ if (occupancy >= contextBudget.blockThreshold) {
19525
+ console.warn(
19526
+ `[EXULU] Blocking request: occupancy ${occupancy} >= blockThreshold ${contextBudget.blockThreshold} (window ${contextBudget.contextWindow}).`
19527
+ );
19528
+ throw new ContextCompactionRequiredError(occupancy, contextBudget);
19529
+ }
19530
+ messages = sliceHistoryAtCheckpoint(chronologicalMessages);
19531
+ 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.";
19532
+ 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.";
19533
+ if (user?.personal_system_prompt?.trim()) {
19534
+ system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
19151
19535
  }
19536
+ system += "\n\n" + genericContext;
19152
19537
  if (currentSkills?.length) {
19153
19538
  const skillsList = currentSkills.map((skill) => {
19154
19539
  const description = (skill.description ?? "").trim();
@@ -19203,6 +19588,11 @@ ${skillsList}
19203
19588
  read them with the readFile tool. Files you produce yourself (via writeFile or via shell
19204
19589
  commands like \`node create_doc.js\`) live in the same place. These files are scoped to
19205
19590
  this single session; they are NOT visible in other sessions, projects, or knowledge bases.
19591
+
19592
+ Note on large outputs: oversized tool outputs and large uploaded documents are automatically
19593
+ truncated in the conversation; the FULL content is saved as a session file (named in the
19594
+ truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
19595
+ to page through it \u2014 do not ask the user to re-upload.
19206
19596
  `;
19207
19597
  system += `
19208
19598
 
@@ -19226,9 +19616,66 @@ When a tool execution is not approved by the user, do not retry it unless explic
19226
19616
  sessionItems,
19227
19617
  model,
19228
19618
  agent,
19229
- memoryItems
19619
+ memoryItems,
19620
+ contextWindow,
19621
+ disabledTools
19230
19622
  );
19231
19623
  console.log("[EXULU] Converted tools", Object.keys(tools));
19624
+ const includesContextSearchTool = currentTools?.some(
19625
+ (tool4) => tool4.name.toLowerCase().includes("context_search") || tool4.id.includes("context_search") || tool4.type === "context"
19626
+ );
19627
+ const includesWebSearchTool = currentTools?.some(
19628
+ (tool4) => tool4.name.toLowerCase().includes("web_search") || tool4.id.includes("web_search") || tool4.type === "web_search"
19629
+ );
19630
+ console.log("[EXULU] Current tools: " + currentTools?.map((tool4) => tool4.name).join("\n"));
19631
+ console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
19632
+ console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
19633
+ if (includesContextSearchTool) {
19634
+ system += `
19635
+
19636
+
19637
+
19638
+ When you use a context search tool, you will include references to the items
19639
+ retrieved from the tool call result inline in the response using this exact JSON format
19640
+ (all on one line, no line breaks):
19641
+ {item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
19642
+
19643
+ IMPORTANT formatting rules:
19644
+ - Use the exact format shown above, all on ONE line
19645
+ - Do NOT use quotes around field names or values
19646
+ - Use the context ID from the tool result
19647
+ - Include the file/item name, not the full path
19648
+ - Separate multiple citations with spaces
19649
+
19650
+ Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
19651
+
19652
+ The citations will be rendered as interactive badges in the UI.
19653
+ `;
19654
+ }
19655
+ if (includesWebSearchTool) {
19656
+ system += `
19657
+
19658
+
19659
+ 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
19660
+ (all on one line, no line breaks):
19661
+ {url: <url>, title: <title>, snippet: <snippet>}
19662
+
19663
+ IMPORTANT formatting rules:
19664
+ - Use the exact format shown above, all on ONE line
19665
+ - Do NOT use quotes around field names or values
19666
+ - Separate multiple results with spaces
19667
+
19668
+ Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
19669
+ `;
19670
+ }
19671
+ const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
19672
+ const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
19673
+ const retrievalGuard = retrievalBudgetGuard(
19674
+ resolveRetrievalCallBudget(toolConfigs),
19675
+ agenticToolKey,
19676
+ Object.keys(tools)
19677
+ );
19678
+ const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
19232
19679
  const result = (0, import_ai11.streamText)({
19233
19680
  temperature: 0,
19234
19681
  // TODO Make this configurable
@@ -19253,10 +19700,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
19253
19700
  `Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
19254
19701
  );
19255
19702
  },
19256
- // provide more loops for skills because they are more complex to execute
19257
- // todo allow configuring this per skill
19258
- prepareStep: finalAnswerGuard(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? (currentSkills?.length ? 10 : 5)),
19259
- stopWhen: [(0, import_ai11.stepCountIs)(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? (currentSkills?.length ? 10 : 5)), (0, import_ai11.hasToolCall)("image_generation")]
19703
+ // todo allow configuring the step budget per skill
19704
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
19705
+ stopWhen: [(0, import_ai11.stepCountIs)(turnBudget), (0, import_ai11.hasToolCall)("image_generation")]
19260
19706
  });
19261
19707
  return {
19262
19708
  stream: result,
@@ -19267,19 +19713,14 @@ When a tool execution is not approved by the user, do not retry it unless explic
19267
19713
  };
19268
19714
  var getAgentMessages = async ({
19269
19715
  session,
19270
- user,
19271
- limit,
19272
- page
19716
+ user
19273
19717
  }) => {
19274
19718
  const { db: db2 } = await postgresClient();
19275
- console.log(
19276
- "[EXULU] getting agent messages for session: " + session + " and user: " + user + " and page: " + page
19277
- );
19278
- const query = db2.from("agent_messages").where({ session, user: user || null }).limit(limit);
19279
- if (page > 0) {
19280
- query.offset((page - 1) * limit);
19281
- }
19282
- const messages = await query;
19719
+ console.log("[EXULU] getting agent messages for session: " + session + " and user: " + user);
19720
+ const messages = await db2.from("agent_messages").where({ session, user: user || null }).orderBy([
19721
+ { column: "createdAt", order: "asc" },
19722
+ { column: "id", order: "asc" }
19723
+ ]);
19283
19724
  return messages;
19284
19725
  };
19285
19726
  var getSession = async ({ sessionID }) => {
@@ -19383,6 +19824,161 @@ ${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
19383
19824
  init_resolve_model();
19384
19825
  init_supervisor();
19385
19826
 
19827
+ // src/exulu/resolve-context-window.ts
19828
+ init_cjs_shims();
19829
+ init_catalog();
19830
+ init_context_budget();
19831
+ var resolveContextWindow = async ({
19832
+ modelId,
19833
+ exuluProvider
19834
+ }) => {
19835
+ if (process.env.EXULU_USE_LITELLM === "true") {
19836
+ const entry = await findLiteLLMModel(modelId);
19837
+ const fromCatalog = entry?.max_input_tokens ?? entry?.max_tokens;
19838
+ if (fromCatalog != null && fromCatalog > 0) return fromCatalog;
19839
+ } else if (exuluProvider) {
19840
+ try {
19841
+ const fromProvider = exuluProvider.maxContextLength;
19842
+ if (fromProvider != null && fromProvider > 0) return fromProvider;
19843
+ } catch {
19844
+ }
19845
+ }
19846
+ console.warn(
19847
+ `[EXULU] Unknown context window for model "${modelId}" \u2014 assuming ${DEFAULT_CONTEXT_WINDOW}. Check the LiteLLM catalog / provider template metadata.`
19848
+ );
19849
+ return DEFAULT_CONTEXT_WINDOW;
19850
+ };
19851
+
19852
+ // src/exulu/routes.ts
19853
+ init_context_budget();
19854
+
19855
+ // src/exulu/active-streams.ts
19856
+ init_cjs_shims();
19857
+ var active = /* @__PURE__ */ new Set();
19858
+ var markStreamActive = (sessionID) => {
19859
+ active.add(sessionID);
19860
+ };
19861
+ var clearStreamActive = (sessionID) => {
19862
+ active.delete(sessionID);
19863
+ };
19864
+ var isStreamActive = (sessionID) => active.has(sessionID);
19865
+
19866
+ // src/exulu/compact-session.ts
19867
+ init_cjs_shims();
19868
+ var import_node_crypto6 = require("crypto");
19869
+ var import_ai13 = require("ai");
19870
+ init_truncate_tool_output();
19871
+ init_context_budget();
19872
+ var CompactionInsufficientError = class extends Error {
19873
+ constructor(reason) {
19874
+ super(JSON.stringify({ code: COMPACTION_INSUFFICIENT, message: reason }));
19875
+ this.name = "CompactionInsufficientError";
19876
+ }
19877
+ };
19878
+ var MIN_TAIL_MESSAGES = 2;
19879
+ var SUMMARY_TOOL_OUTPUT_SLICE = 1500;
19880
+ var SUMMARY_TOOL_INPUT_SLICE = 200;
19881
+ var SUMMARY_SYSTEM = `You compress chat histories for an AI assistant. Produce a dense, factual summary of the conversation below. Preserve:
19882
+ - the user's intent and any outstanding requests
19883
+ - key facts, decisions, and constraints
19884
+ - files, artifacts, and session files touched \u2014 ALWAYS keep exact file and item names so they stay retrievable
19885
+ - errors encountered and how they were resolved
19886
+ - pending tasks and the current state of the work
19887
+ Do not invent information. Do not include pleasantries. Write compact prose or bullet points.`;
19888
+ var splitTail = (messages, tailTokenBudget) => {
19889
+ const minTail = Math.min(MIN_TAIL_MESSAGES, messages.length);
19890
+ let cut = messages.length;
19891
+ let tokens = 0;
19892
+ for (let i = messages.length - 1; i >= 0; i--) {
19893
+ const t = estimateMessageTokens(messages[i]);
19894
+ const tailCount = messages.length - i;
19895
+ if (tailCount > minTail && tokens + t > tailTokenBudget) break;
19896
+ tokens += t;
19897
+ cut = i;
19898
+ }
19899
+ return { head: messages.slice(0, cut), tail: messages.slice(cut) };
19900
+ };
19901
+ var serializeForSummary = (messages) => messages.map((m) => {
19902
+ const parts = (m.parts ?? []).map((part) => {
19903
+ const p = part;
19904
+ if (p.type === "text") return p.text ?? "";
19905
+ if (p.type === "file") return `[file: ${p.filename ?? p.url ?? "attachment"}]`;
19906
+ if (p.type === "reasoning" || p.type === "step-start") return "";
19907
+ if (p.type?.startsWith("tool-") || p.type === "dynamic-tool") {
19908
+ const out = p.output?.value ?? p.output;
19909
+ const outText = typeof out === "string" ? out : JSON.stringify(out ?? "");
19910
+ return `[tool ${p.type}: ${JSON.stringify(p.input ?? {}).slice(0, SUMMARY_TOOL_INPUT_SLICE)}] \u2192 ${outText.slice(0, SUMMARY_TOOL_OUTPUT_SLICE)}`;
19911
+ }
19912
+ return "";
19913
+ }).filter(Boolean).join("\n");
19914
+ return `${m.role.toUpperCase()}:
19915
+ ${parts}`;
19916
+ }).join("\n\n");
19917
+ var compactSession = async ({
19918
+ sessionID,
19919
+ user,
19920
+ languageModel,
19921
+ contextWindow,
19922
+ steer,
19923
+ modelId,
19924
+ summarize
19925
+ }) => {
19926
+ const budget = deriveContextBudget(contextWindow);
19927
+ const rows = await getAgentMessages({ session: sessionID, user: user.id });
19928
+ const all = await (0, import_ai13.validateUIMessages)({ messages: rows.map((r) => JSON.parse(r.content)) });
19929
+ const history = sliceHistoryAtCheckpoint(all);
19930
+ const { head, tail } = splitTail(history, budget.compactionTailTokens);
19931
+ if (head.length === 0) {
19932
+ throw new CompactionInsufficientError(
19933
+ "There is nothing left to compact \u2014 the recent messages already form the whole context. Start a new chat instead."
19934
+ );
19935
+ }
19936
+ let corpus = serializeForSummary(head);
19937
+ const originalTokens = estimateTokens(corpus);
19938
+ corpus = truncateToolOutput(corpus, contextWindow, "history", 0.3, Math.floor(budget.usableWindow * 0.8) * 4);
19939
+ const system = steer?.trim() ? `${SUMMARY_SYSTEM}
19940
+
19941
+ Focus especially on: ${steer.trim()}` : SUMMARY_SYSTEM;
19942
+ const doSummarize = summarize ?? (async ({ system: sys, prompt, maxOutputTokens }) => {
19943
+ const { text } = await (0, import_ai13.generateText)({
19944
+ model: languageModel,
19945
+ system: sys,
19946
+ prompt,
19947
+ temperature: 0,
19948
+ maxRetries: 2,
19949
+ maxOutputTokens
19950
+ });
19951
+ return text;
19952
+ });
19953
+ const summary = await doSummarize({ system, prompt: corpus, maxOutputTokens: budget.summaryBudgetTokens });
19954
+ const summaryTokens = estimateTokens(summary);
19955
+ let tailTokens = 0;
19956
+ for (const m of tail) tailTokens += estimateMessageTokens(m);
19957
+ const occupancyEstimate = summaryTokens + tailTokens;
19958
+ if (occupancyEstimate >= budget.blockThreshold) {
19959
+ throw new CompactionInsufficientError(
19960
+ "Compacting cannot shrink this conversation below the context limit \u2014 a recent message or output is too large by itself. Start a new chat."
19961
+ );
19962
+ }
19963
+ const compaction = {
19964
+ coversUpTo: head[head.length - 1].id,
19965
+ originalTokens,
19966
+ summaryTokens,
19967
+ occupancyEstimate,
19968
+ ...steer?.trim() ? { steer: steer.trim() } : {}
19969
+ };
19970
+ const checkpoint = {
19971
+ id: `compaction_${(0, import_node_crypto6.randomUUID)()}`,
19972
+ role: "user",
19973
+ parts: [{ type: "text", text: `[Conversation summary \u2014 earlier messages were compacted]
19974
+
19975
+ ${summary}` }],
19976
+ metadata: { compaction }
19977
+ };
19978
+ await saveChat({ session: sessionID, user: user.id, messages: [checkpoint], ...modelId ? { model: modelId } : {} });
19979
+ return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
19980
+ };
19981
+
19386
19982
  // src/exulu/transcribe.ts
19387
19983
  init_cjs_shims();
19388
19984
  var TranscriptionError = class extends Error {
@@ -19757,7 +20353,7 @@ function checkApiKeyScope(user, agentId) {
19757
20353
  // src/exulu/openai-gateway.ts
19758
20354
  init_cjs_shims();
19759
20355
  var import_express2 = require("express");
19760
- var import_ai13 = require("ai");
20356
+ var import_ai14 = require("ai");
19761
20357
 
19762
20358
  // src/exulu/openai-transformer.ts
19763
20359
  init_cjs_shims();
@@ -19845,7 +20441,7 @@ function transformCompletion(text, inputTokens, outputTokens, ctx) {
19845
20441
  }
19846
20442
 
19847
20443
  // src/exulu/openai-gateway.ts
19848
- var import_node_crypto5 = require("crypto");
20444
+ var import_node_crypto7 = require("crypto");
19849
20445
  var import_crypto_js8 = require("crypto-js");
19850
20446
  var import_express3 = __toESM(require("express"), 1);
19851
20447
  init_client();
@@ -19853,6 +20449,9 @@ init_convert_exulu_tools_to_ai_sdk_tools();
19853
20449
  init_statistics2();
19854
20450
  init_statistics();
19855
20451
  init_resolve_model();
20452
+ init_sanitize_tool_name();
20453
+ init_context_budget();
20454
+ init_supervisor();
19856
20455
  function convertOpenAIToolsToAiSdkTools(tools) {
19857
20456
  return Object.fromEntries(
19858
20457
  tools.map((t) => {
@@ -19861,7 +20460,7 @@ function convertOpenAIToolsToAiSdkTools(tools) {
19861
20460
  t.function.name,
19862
20461
  {
19863
20462
  description: t.function.description ?? "",
19864
- inputSchema: (0, import_ai13.jsonSchema)({
20463
+ inputSchema: (0, import_ai14.jsonSchema)({
19865
20464
  type: "object",
19866
20465
  properties: params.properties ?? {},
19867
20466
  ...params.required ? { required: params.required } : {}
@@ -20164,6 +20763,10 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
20164
20763
  }
20165
20764
  const providerapikey = resolved.apiKey;
20166
20765
  const languageModel = resolved.languageModel;
20766
+ const contextWindow = await resolveContextWindow({
20767
+ modelId: resolved.model.id,
20768
+ exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
20769
+ });
20167
20770
  const disabledTools = req.body.disabledTools ?? [];
20168
20771
  const enabledTools = await getEnabledTools(
20169
20772
  agent,
@@ -20188,12 +20791,50 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
20188
20791
  project?.id,
20189
20792
  void 0,
20190
20793
  languageModel,
20191
- agent
20794
+ agent,
20795
+ void 0,
20796
+ contextWindow,
20797
+ disabledTools
20798
+ );
20799
+ const gatewayAgenticEntry = enabledTools?.find((t) => t.id === "agentic_context_search");
20800
+ const gatewayAgenticKey = gatewayAgenticEntry ? sanitizeToolName(gatewayAgenticEntry.name) : void 0;
20801
+ const gatewayRetrievalGuard = retrievalBudgetGuard(
20802
+ resolveRetrievalCallBudget(agent.tools),
20803
+ gatewayAgenticKey,
20804
+ Object.keys(convertedTools)
20192
20805
  );
20806
+ const turnBudget = resolveTurnStepBudget(void 0, agent);
20193
20807
  const clientTools = Array.isArray(req.body.tools) ? req.body.tools : [];
20194
20808
  const activeTools = clientTools.length > 0 ? convertOpenAIToolsToAiSdkTools(clientTools) : convertedTools;
20195
20809
  const openaiMessages = req.body.messages ?? [];
20196
20810
  const { systemPrompt: requestSystemPrompt, coreMessages } = convertOpenAIMessagesToModelMessages(openaiMessages);
20811
+ const gatewayBudget = deriveContextBudget(contextWindow);
20812
+ const IMAGE_TOKEN_ALLOWANCE = 1e3;
20813
+ let imageCount = 0;
20814
+ const textOnlyMessages = openaiMessages.map((m) => {
20815
+ if (!Array.isArray(m.content)) return m;
20816
+ return {
20817
+ ...m,
20818
+ content: m.content.map((p) => {
20819
+ if (p && typeof p === "object" && "image_url" in p && p.image_url) {
20820
+ imageCount += 1;
20821
+ return { ...p, image_url: { url: "[image]" } };
20822
+ }
20823
+ return p;
20824
+ })
20825
+ };
20826
+ });
20827
+ const promptTokens = estimateTokens(JSON.stringify(textOnlyMessages)) + imageCount * IMAGE_TOKEN_ALLOWANCE;
20828
+ if (promptTokens >= gatewayBudget.blockThreshold) {
20829
+ res.status(400).json({
20830
+ error: {
20831
+ 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.`,
20832
+ type: "invalid_request_error",
20833
+ code: "context_length_exceeded"
20834
+ }
20835
+ });
20836
+ return;
20837
+ }
20197
20838
  const agentInstructions = agent.instructions ?? "";
20198
20839
  const systemParts = [
20199
20840
  agentInstructions ? `You are an agent named: ${agent.name}
@@ -20203,7 +20844,7 @@ ${project.description}` : ""}` : "",
20203
20844
  requestSystemPrompt
20204
20845
  ].filter(Boolean);
20205
20846
  const systemPrompt = systemParts.join("\n\n");
20206
- const completionId = `chatcmpl-${(0, import_node_crypto5.randomUUID)()}`;
20847
+ const completionId = `chatcmpl-${(0, import_node_crypto7.randomUUID)()}`;
20207
20848
  const created = Math.floor(Date.now() / 1e3);
20208
20849
  const hasTools = Object.keys(activeTools).length > 0;
20209
20850
  const ctx = { completionId, created, modelId };
@@ -20211,13 +20852,14 @@ ${project.description}` : ""}` : "",
20211
20852
  res.setHeader("Content-Type", "text/event-stream");
20212
20853
  res.setHeader("Cache-Control", "no-cache");
20213
20854
  res.setHeader("Connection", "keep-alive");
20214
- const result = (0, import_ai13.streamText)({
20855
+ const result = (0, import_ai14.streamText)({
20215
20856
  model: languageModel,
20216
20857
  system: systemPrompt || void 0,
20217
20858
  messages: coreMessages,
20218
20859
  tools: hasTools ? activeTools : void 0,
20219
20860
  maxRetries: 2,
20220
- stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai13.stepCountIs)(5)],
20861
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
20862
+ stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai14.stepCountIs)(turnBudget)],
20221
20863
  onError: (error) => {
20222
20864
  console.error("[OPENAI GATEWAY] stream error:", error);
20223
20865
  }
@@ -20250,13 +20892,14 @@ ${project.description}` : ""}` : "",
20250
20892
  const usage = await result.usage;
20251
20893
  await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
20252
20894
  } else {
20253
- const { text, usage } = await (0, import_ai13.generateText)({
20895
+ const { text, usage } = await (0, import_ai14.generateText)({
20254
20896
  model: languageModel,
20255
20897
  system: systemPrompt || void 0,
20256
20898
  messages: coreMessages,
20257
20899
  tools: hasTools ? activeTools : void 0,
20258
20900
  maxRetries: 2,
20259
- stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai13.stepCountIs)(5)]
20901
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
20902
+ stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai14.stepCountIs)(turnBudget)]
20260
20903
  });
20261
20904
  res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
20262
20905
  await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
@@ -20375,7 +21018,7 @@ init_flow();
20375
21018
 
20376
21019
  // src/exulu/recall/verify.ts
20377
21020
  init_cjs_shims();
20378
- var import_node_crypto6 = require("crypto");
21021
+ var import_node_crypto8 = require("crypto");
20379
21022
  var TOLERANCE_SECONDS = 5 * 60;
20380
21023
  var header = (headers, ...names) => {
20381
21024
  for (const name of names) {
@@ -20393,7 +21036,7 @@ var safeEqual = (a, b) => {
20393
21036
  const bufA = Buffer.from(a);
20394
21037
  const bufB = Buffer.from(b);
20395
21038
  if (bufA.length !== bufB.length) return false;
20396
- return (0, import_node_crypto6.timingSafeEqual)(bufA, bufB);
21039
+ return (0, import_node_crypto8.timingSafeEqual)(bufA, bufB);
20397
21040
  };
20398
21041
  var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()) => {
20399
21042
  if (!secret) {
@@ -20415,7 +21058,7 @@ var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()
20415
21058
  }
20416
21059
  const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
20417
21060
  const signedContent = `${id}.${timestamp}.${body}`;
20418
- const expected = (0, import_node_crypto6.createHmac)("sha256", secretKey(secret)).update(signedContent).digest("base64");
21061
+ const expected = (0, import_node_crypto8.createHmac)("sha256", secretKey(secret)).update(signedContent).digest("base64");
20419
21062
  const passed = signatureHeader.split(" ").some((entry) => {
20420
21063
  const [, sig] = entry.split(",");
20421
21064
  return !!sig && safeEqual(sig, expected);
@@ -20768,7 +21411,7 @@ Mood: friendly and intelligent.
20768
21411
  });
20769
21412
  return;
20770
21413
  }
20771
- const uuid = (0, import_node_crypto7.randomUUID)();
21414
+ const uuid = (0, import_node_crypto9.randomUUID)();
20772
21415
  const image_url = await uploadFile(Buffer.from(image_base64, "base64"), `${uuid}.png`, config, {
20773
21416
  contentType: "image/png"
20774
21417
  }, authenticationResult.user?.id, void 0, true);
@@ -20972,6 +21615,10 @@ Mood: friendly and intelligent.
20972
21615
  const providerapikey = resolved.apiKey;
20973
21616
  const resolvedLanguageModel = resolved.languageModel;
20974
21617
  const resolvedModelId = resolved.model.id;
21618
+ const contextWindow = await resolveContextWindow({
21619
+ modelId: resolved.model.id,
21620
+ exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
21621
+ });
20975
21622
  if (!!headers.stream) {
20976
21623
  const statistics = {
20977
21624
  label: agent.name,
@@ -20990,27 +21637,46 @@ Mood: friendly and intelligent.
20990
21637
  const instructions = customInstructions ? `${agent.instructions}
20991
21638
 
20992
21639
  ${customInstructions}` : agent.instructions;
20993
- const result = await provider.generateStream({
20994
- contexts,
20995
- agent,
20996
- user,
20997
- instructions,
20998
- session: headers.session,
20999
- message,
21000
- previousMessages,
21001
- currentTools: enabledTools,
21002
- currentSkills: enabledSkills,
21003
- approvedTools,
21004
- allExuluTools: tools,
21005
- languageModel: resolvedLanguageModel,
21006
- providerapikey,
21007
- toolConfigs: agent.tools,
21008
- exuluConfig: config,
21009
- req
21010
- });
21640
+ if (headers.session) markStreamActive(headers.session);
21641
+ let result;
21642
+ try {
21643
+ result = await provider.generateStream({
21644
+ contexts,
21645
+ agent,
21646
+ user,
21647
+ instructions,
21648
+ session: headers.session,
21649
+ message,
21650
+ previousMessages,
21651
+ currentTools: enabledTools,
21652
+ currentSkills: enabledSkills,
21653
+ approvedTools,
21654
+ allExuluTools: tools,
21655
+ languageModel: resolvedLanguageModel,
21656
+ providerapikey,
21657
+ toolConfigs: agent.tools,
21658
+ exuluConfig: config,
21659
+ req,
21660
+ contextWindow,
21661
+ disabledTools
21662
+ });
21663
+ } catch (err) {
21664
+ if (headers.session) clearStreamActive(headers.session);
21665
+ if (err instanceof ContextCompactionRequiredError) {
21666
+ res.status(413).send(err.message);
21667
+ return;
21668
+ }
21669
+ throw err;
21670
+ }
21011
21671
  result.stream.consumeStream();
21012
21672
  result.stream.pipeUIMessageStreamToResponse(res, {
21013
21673
  messageMetadata: ({ part }) => {
21674
+ if (part.type === "finish-step") {
21675
+ return {
21676
+ lastStepInputTokens: part.usage.inputTokens,
21677
+ lastStepOutputTokens: part.usage.outputTokens
21678
+ };
21679
+ }
21014
21680
  if (part.type === "finish") {
21015
21681
  return {
21016
21682
  totalTokens: part.totalUsage.totalTokens,
@@ -21027,22 +21693,20 @@ ${customInstructions}` : agent.instructions;
21027
21693
  sendSources: true,
21028
21694
  onError: (error) => {
21029
21695
  console.error("[EXULU] chat response error.", error);
21030
- if (error == null) {
21031
- return "unknown error";
21032
- }
21033
- if (typeof error === "string") {
21034
- return error;
21035
- }
21036
- if (error instanceof Error) {
21037
- return error.message;
21038
- }
21039
- return JSON.stringify(error);
21696
+ if (headers.session) clearStreamActive(headers.session);
21697
+ let message2;
21698
+ if (error == null) message2 = "unknown error";
21699
+ else if (typeof error === "string") message2 = error;
21700
+ else if (error instanceof Error) message2 = error.message;
21701
+ else message2 = JSON.stringify(error);
21702
+ return mapStreamErrorMessage(message2);
21040
21703
  },
21041
- generateMessageId: (0, import_ai14.createIdGenerator)({
21704
+ generateMessageId: (0, import_ai15.createIdGenerator)({
21042
21705
  prefix: "msg_",
21043
21706
  size: 16
21044
21707
  }),
21045
21708
  onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
21709
+ if (headers.session) clearStreamActive(headers.session);
21046
21710
  console.log(
21047
21711
  "[EXULU] onFinish",
21048
21712
  messages?.map((msg) => msg.parts?.map((part) => part.type === "text" ? part.text : null)).join("\n")
@@ -21104,33 +21768,129 @@ ${customInstructions}` : agent.instructions;
21104
21768
  const instructions = customInstructions ? `${agent.instructions}
21105
21769
 
21106
21770
  ${customInstructions}` : agent.instructions;
21107
- const response = await provider.generateSync({
21108
- contexts,
21109
- agent,
21110
- user,
21111
- req,
21112
- instructions,
21113
- session: headers.session,
21114
- inputMessages: [req.body.message],
21115
- currentTools: enabledTools,
21116
- currentSkills: enabledSkills,
21117
- allExuluTools: tools,
21118
- languageModel: resolvedLanguageModel,
21119
- providerapikey,
21120
- exuluConfig: config,
21121
- toolConfigs: agent.tools,
21122
- statistics: {
21123
- label: agent.name,
21124
- trigger: "agent"
21125
- },
21126
- onTokenUsage: async ({ inputTokens, outputTokens }) => {
21771
+ let response;
21772
+ try {
21773
+ response = await provider.generateSync({
21774
+ contexts,
21775
+ agent,
21776
+ user,
21777
+ req,
21778
+ instructions,
21779
+ session: headers.session,
21780
+ inputMessages: [req.body.message],
21781
+ currentTools: enabledTools,
21782
+ currentSkills: enabledSkills,
21783
+ allExuluTools: tools,
21784
+ languageModel: resolvedLanguageModel,
21785
+ providerapikey,
21786
+ exuluConfig: config,
21787
+ toolConfigs: agent.tools,
21788
+ contextWindow,
21789
+ disabledTools,
21790
+ statistics: {
21791
+ label: agent.name,
21792
+ trigger: "agent"
21793
+ },
21794
+ onTokenUsage: async ({ inputTokens, outputTokens }) => {
21795
+ }
21796
+ });
21797
+ } catch (err) {
21798
+ if (err instanceof ContextCompactionRequiredError) {
21799
+ res.status(413).send(err.message);
21800
+ return;
21127
21801
  }
21128
- });
21802
+ throw err;
21803
+ }
21129
21804
  res.status(200).json(response);
21130
21805
  return;
21131
21806
  }
21132
21807
  });
21133
21808
  };
21809
+ const registerAgentCompactRoute = (slug) => {
21810
+ app.post(slug + "/:instance", async (req, res) => {
21811
+ const instance2 = req.params.instance;
21812
+ if (!instance2) {
21813
+ res.status(400).json({ message: "Missing instance in request." });
21814
+ return;
21815
+ }
21816
+ const sessionID = req.headers["session"] || null;
21817
+ if (!sessionID) {
21818
+ res.status(400).json({ message: "Missing session header." });
21819
+ return;
21820
+ }
21821
+ const agent = await exuluApp.get().agent(instance2);
21822
+ if (!agent) {
21823
+ res.status(404).json({ message: "Agent with id " + instance2 + " not found." });
21824
+ return;
21825
+ }
21826
+ const authenticationResult = await requestValidators.authenticate(req);
21827
+ if (!authenticationResult.user?.id) {
21828
+ res.status(authenticationResult.code || 401).json({ detail: `${authenticationResult.message}` });
21829
+ return;
21830
+ }
21831
+ const user = authenticationResult.user;
21832
+ const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
21833
+ if (!hasAccessToAgent) {
21834
+ res.status(401).json({ message: "You don't have access to this agent." });
21835
+ return;
21836
+ }
21837
+ const { db: db2 } = await postgresClient();
21838
+ const sessionRow = await db2.from("agent_sessions").where({ id: sessionID }).first();
21839
+ if (!sessionRow) {
21840
+ res.status(404).json({ message: "Session not found for session ID: " + sessionID });
21841
+ return;
21842
+ }
21843
+ const hasAccessToSession = await checkRecordAccess(sessionRow, "write", user);
21844
+ if (!hasAccessToSession) {
21845
+ res.status(401).json({ message: "You don't have access to this session." });
21846
+ return;
21847
+ }
21848
+ if (isStreamActive(sessionID)) {
21849
+ res.status(409).json({ message: "A response is still streaming for this session \u2014 try again when it finishes." });
21850
+ return;
21851
+ }
21852
+ const overrideModelId = req.headers["x-exulu-model-override"];
21853
+ const modelId = overrideModelId ?? agent.model;
21854
+ if (!modelId) {
21855
+ res.status(400).json({ message: `Agent ${agent.name} (${agent.id}) has no model configured.` });
21856
+ return;
21857
+ }
21858
+ let resolved;
21859
+ try {
21860
+ resolved = await resolveModel({ modelId, user, providers, agent });
21861
+ } catch (err) {
21862
+ if (err instanceof ResolveModelError) {
21863
+ const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
21864
+ res.status(status).json({ message: err.message, code: err.code });
21865
+ return;
21866
+ }
21867
+ throw err;
21868
+ }
21869
+ const contextWindow = await resolveContextWindow({
21870
+ modelId: resolved.model.id,
21871
+ exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
21872
+ });
21873
+ const steer = typeof req.body?.steer === "string" ? req.body.steer : void 0;
21874
+ try {
21875
+ const result = await compactSession({
21876
+ sessionID,
21877
+ user,
21878
+ languageModel: resolved.languageModel,
21879
+ contextWindow,
21880
+ steer,
21881
+ modelId: resolved.model.id
21882
+ });
21883
+ res.json(result);
21884
+ } catch (err) {
21885
+ if (err instanceof CompactionInsufficientError) {
21886
+ res.status(422).send(err.message);
21887
+ return;
21888
+ }
21889
+ console.error("[EXULU] compactSession failed.", err);
21890
+ res.status(500).json({ message: err instanceof Error ? err.message : "Compaction failed." });
21891
+ }
21892
+ });
21893
+ };
21134
21894
  providers.forEach((provider) => {
21135
21895
  const slug = provider.slug;
21136
21896
  if (!slug) return;
@@ -21139,6 +21899,14 @@ ${customInstructions}` : agent.instructions;
21139
21899
  if (isLiteLLMEnabled() && providers.length > 0) {
21140
21900
  registerAgentRunRoute("/agents/litellm/run", providers[0]);
21141
21901
  }
21902
+ providers.forEach((provider) => {
21903
+ const slug = provider.slug;
21904
+ if (!slug) return;
21905
+ registerAgentCompactRoute(slug.replace(/\/run$/, "/compact"));
21906
+ });
21907
+ if (isLiteLLMEnabled() && providers.length > 0) {
21908
+ registerAgentCompactRoute("/agents/litellm/compact");
21909
+ }
21142
21910
  app.post("/agents/suggestions/:agentId", async (req, res) => {
21143
21911
  const agentId = req.params.agentId;
21144
21912
  if (!agentId) {
@@ -21490,7 +22258,7 @@ ${customInstructions}` : agent.instructions;
21490
22258
  const keys = [];
21491
22259
  const revisedPrompts = [];
21492
22260
  for (const img of images) {
21493
- const filename = `${(0, import_node_crypto7.randomUUID)()}.${img.extension}`;
22261
+ const filename = `${(0, import_node_crypto9.randomUUID)()}.${img.extension}`;
21494
22262
  const key = `sessions/${sessionId}/images/${toolCallId}/${filename}`;
21495
22263
  const fullKey = await uploadFile(
21496
22264
  img.buffer,
@@ -21778,7 +22546,7 @@ ${style.markdown}` : params.prompt;
21778
22546
  (d) => `- ${d.presignedUrl} (prompt: "${d.prompt}", model: ${d.model}${d.styleName ? `, style: ${d.styleName}` : ""})`
21779
22547
  );
21780
22548
  const messageText = "The user generated and selected the following image(s) in this chat:\n" + lines.join("\n");
21781
- const messageId = (0, import_node_crypto7.randomUUID)();
22549
+ const messageId = (0, import_node_crypto9.randomUUID)();
21782
22550
  const uiMessage = {
21783
22551
  id: messageId,
21784
22552
  role: "system",
@@ -22706,7 +23474,7 @@ ${style.markdown}` : params.prompt;
22706
23474
  res.status(404).json({ detail: "Skill not found." });
22707
23475
  return;
22708
23476
  }
22709
- const stagingKey = `user_${authResult.user.id}/skills/_staging/${(0, import_node_crypto7.randomUUID)()}${extension}`;
23477
+ const stagingKey = `user_${authResult.user.id}/skills/_staging/${(0, import_node_crypto9.randomUUID)()}${extension}`;
22710
23478
  const fullKey = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/${stagingKey}` : stagingKey;
22711
23479
  const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
22712
23480
  res.json({ uploadUrl, stagingKey });
@@ -23738,7 +24506,7 @@ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
23738
24506
  // src/mcp/index.ts
23739
24507
  init_cjs_shims();
23740
24508
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
23741
- var import_node_crypto8 = require("crypto");
24509
+ var import_node_crypto10 = require("crypto");
23742
24510
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
23743
24511
  var import_types3 = require("@modelcontextprotocol/sdk/types.js");
23744
24512
  init_sanitize_tool_name();
@@ -23850,7 +24618,7 @@ var ExuluMCP = class {
23850
24618
  throw new Error("Tool not found in converted tools array.");
23851
24619
  }
23852
24620
  const iterator = await convertedTool.execute(inputs, {
23853
- toolCallId: tool4.id + "_" + (0, import_node_crypto8.randomUUID)(),
24621
+ toolCallId: tool4.id + "_" + (0, import_node_crypto10.randomUUID)(),
23854
24622
  messages: []
23855
24623
  });
23856
24624
  let result;
@@ -24042,7 +24810,7 @@ var ExuluMCP = class {
24042
24810
  transport = this.transports[sessionId];
24043
24811
  } else if (!sessionId && (0, import_types3.isInitializeRequest)(req.body)) {
24044
24812
  transport = new import_streamableHttp.StreamableHTTPServerTransport({
24045
- sessionIdGenerator: () => (0, import_node_crypto8.randomUUID)(),
24813
+ sessionIdGenerator: () => (0, import_node_crypto10.randomUUID)(),
24046
24814
  onsessioninitialized: (sessionId2) => {
24047
24815
  this.transports[sessionId2] = transport;
24048
24816
  }
@@ -24996,7 +25764,7 @@ init_cjs_shims();
24996
25764
 
24997
25765
  // src/exulu/evals.ts
24998
25766
  init_cjs_shims();
24999
- var import_ai15 = require("ai");
25767
+ var import_ai16 = require("ai");
25000
25768
  init_entitlements();
25001
25769
  var ExuluEval = class {
25002
25770
  id;
@@ -25037,7 +25805,7 @@ var ExuluEval = class {
25037
25805
  init_resolve_model();
25038
25806
  init_singleton();
25039
25807
  var import_zod15 = require("zod");
25040
- var import_ai16 = require("ai");
25808
+ var import_ai17 = require("ai");
25041
25809
  var llmAsJudgeEval = () => {
25042
25810
  if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
25043
25811
  return new ExuluEval({
@@ -25082,13 +25850,13 @@ var llmAsJudgeEval = () => {
25082
25850
  rbacBypass: true
25083
25851
  });
25084
25852
  console.log("[EXULU] prompt", prompt);
25085
- const { output } = await (0, import_ai16.generateText)({
25853
+ const { output } = await (0, import_ai17.generateText)({
25086
25854
  temperature: 0,
25087
25855
  model: resolved.languageModel,
25088
25856
  system: "",
25089
25857
  prompt,
25090
25858
  maxRetries: 2,
25091
- output: import_ai16.Output.object({
25859
+ output: import_ai17.Output.object({
25092
25860
  schema: import_zod15.z.object({
25093
25861
  score: import_zod15.z.number().min(0).max(100).describe("The score between 0 and 100.")
25094
25862
  })
@@ -25522,7 +26290,7 @@ var import_zod17 = __toESM(require("zod"), 1);
25522
26290
  init_tool();
25523
26291
  init_check_record_access();
25524
26292
  init_client();
25525
- var import_node_crypto9 = require("crypto");
26293
+ var import_node_crypto11 = require("crypto");
25526
26294
  var AnswerOptionSchema = import_zod17.default.object({
25527
26295
  id: import_zod17.default.string().describe("Unique identifier for the answer option"),
25528
26296
  text: import_zod17.default.string().describe("The text of the answer option")
@@ -25576,15 +26344,15 @@ var QuestionAskTool = new ExuluTool({
25576
26344
  throw new Error("You don't have access to this session " + session.id + ".");
25577
26345
  }
25578
26346
  const answerOptionsWithIds = answerOptions.map((text) => ({
25579
- id: (0, import_node_crypto9.randomUUID)(),
26347
+ id: (0, import_node_crypto11.randomUUID)(),
25580
26348
  text
25581
26349
  }));
25582
26350
  answerOptionsWithIds.push({
25583
- id: (0, import_node_crypto9.randomUUID)(),
26351
+ id: (0, import_node_crypto11.randomUUID)(),
25584
26352
  text: "None of the above..."
25585
26353
  });
25586
26354
  const newQuestion = {
25587
- id: (0, import_node_crypto9.randomUUID)(),
26355
+ id: (0, import_node_crypto11.randomUUID)(),
25588
26356
  question,
25589
26357
  answerOptions: answerOptionsWithIds,
25590
26358
  status: "pending"
@@ -28618,7 +29386,7 @@ var MarkdownChunker = class {
28618
29386
  init_cjs_shims();
28619
29387
  var fs4 = __toESM(require("fs"), 1);
28620
29388
  var path = __toESM(require("path"), 1);
28621
- var import_ai17 = require("ai");
29389
+ var import_ai18 = require("ai");
28622
29390
  var import_zod22 = require("zod");
28623
29391
  var import_p_limit = __toESM(require("p-limit"), 1);
28624
29392
  var import_crypto2 = require("crypto");
@@ -29083,9 +29851,9 @@ If the page contains a flow-chart, schematic, technical drawing or control board
29083
29851
 
29084
29852
  ### 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
29853
  `;
29086
- const result = await (0, import_ai17.generateText)({
29854
+ const result = await (0, import_ai18.generateText)({
29087
29855
  model,
29088
- output: import_ai17.Output.object({
29856
+ output: import_ai18.Output.object({
29089
29857
  schema: import_zod22.z.object({
29090
29858
  needs_correction: import_zod22.z.boolean(),
29091
29859
  corrected_text: import_zod22.z.string().nullable(),