@exulu/backend 2.0.1 → 2.2.0

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