@exulu/backend 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1691,7 +1691,7 @@ var ExuluTool = class _ExuluTool {
1691
1691
  });
1692
1692
  providerapikey = resolved.apiKey;
1693
1693
  }
1694
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-2PEDFZ2X.js");
1694
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-K3RHHLN6.js");
1695
1695
  const tools = await convertExuluToolsToAiSdkTools2(
1696
1696
  [this],
1697
1697
  [],
@@ -1774,111 +1774,8 @@ var updateStatistic = async (statistic) => {
1774
1774
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
1775
1775
  import CryptoJS5 from "crypto-js";
1776
1776
 
1777
- // src/templates/tools/project-retrieval-tool.ts
1778
- import { z as z2 } from "zod";
1779
- var createProjectItemsRetrievalTool = async ({
1780
- user,
1781
- role,
1782
- contexts,
1783
- projectId
1784
- }) => {
1785
- let project;
1786
- const { db: db2 } = await postgresClient();
1787
- project = await db2.from("projects").where("id", projectId).first();
1788
- if (!project) {
1789
- return;
1790
- }
1791
- console.log("[EXULU] Project search tool created for project", project);
1792
- if (!project.project_items?.length) {
1793
- return;
1794
- }
1795
- const projectRetrievalTool = ExuluTool.internal({
1796
- id: "context_search_in_knowledge_items_added_to_project_" + projectId,
1797
- name: "context_search in knowledge items added to project " + project.name,
1798
- description: "This tool retrieves information about a project from conversations and items that were added to the project " + project.name + ".",
1799
- inputSchema: z2.object({
1800
- query: z2.string().describe("The query to retrieve information about the project " + project.name + "."),
1801
- keywords: z2.array(z2.string()).describe(
1802
- "The most relevant keywords in the query, such as names of people, companies, products, etc. in the project " + project.name + "."
1803
- )
1804
- }),
1805
- type: "context",
1806
- category: "project",
1807
- config: [],
1808
- execute: async ({ query }) => {
1809
- console.log("[EXULU] Project search tool searching for project", project);
1810
- const items = project.project_items;
1811
- const set = {};
1812
- for (const item of items) {
1813
- const context = item.split("/")[0];
1814
- if (!context) {
1815
- throw new Error(
1816
- "The item added to the project does not have a valid gid with the context id as the prefix before the first slash."
1817
- );
1818
- }
1819
- const id = item.split("/").slice(1).join("/");
1820
- if (set[context]) {
1821
- set[context].push(id);
1822
- } else {
1823
- set[context] = [id];
1824
- }
1825
- }
1826
- console.log("[EXULU] Project search tool searching through contexts", Object.keys(set));
1827
- const results = await Promise.all(
1828
- Object.keys(set).map(async (contextName) => {
1829
- const context = contexts.find((context2) => context2.id === contextName);
1830
- if (!context) {
1831
- console.error(
1832
- "[EXULU] Context not found for project information retrieval tool.",
1833
- contextName
1834
- );
1835
- return [];
1836
- }
1837
- const itemIds = set[contextName];
1838
- console.log("[EXULU] Project search tool searching through items", itemIds);
1839
- const result = await context.search({
1840
- query,
1841
- itemFilters: [
1842
- {
1843
- id: {
1844
- in: itemIds
1845
- }
1846
- }
1847
- ],
1848
- chunkFilters: [],
1849
- user,
1850
- role,
1851
- method: "hybridSearch",
1852
- sort: {
1853
- field: "updatedAt",
1854
- direction: "desc"
1855
- },
1856
- trigger: "tool",
1857
- limit: 10,
1858
- page: 1
1859
- });
1860
- return {
1861
- result: result.chunks.map((chunk) => ({
1862
- ...chunk,
1863
- context: {
1864
- name: context.name,
1865
- id: context.id
1866
- }
1867
- }))
1868
- };
1869
- })
1870
- );
1871
- console.log("[EXULU] Project search tool results", results);
1872
- return {
1873
- result: JSON.stringify(results.flat())
1874
- };
1875
- }
1876
- });
1877
- return projectRetrievalTool;
1878
- };
1879
-
1880
1777
  // src/templates/tools/session-items-retrieval-tool.ts
1881
- import { z as z3 } from "zod";
1778
+ import { z as z2 } from "zod";
1882
1779
  var createSessionItemsRetrievalTool = async ({
1883
1780
  user,
1884
1781
  role,
@@ -1890,8 +1787,8 @@ var createSessionItemsRetrievalTool = async ({
1890
1787
  id: "session_items_information_context_search",
1891
1788
  name: "context_search in knowledge items added to session.",
1892
1789
  description: "Context search in knowledge items added to session.",
1893
- inputSchema: z3.object({
1894
- query: z3.string().describe("The query to retrieve information from knowledge items added to the session.")
1790
+ inputSchema: z2.object({
1791
+ query: z2.string().describe("The query to retrieve information from knowledge items added to the session.")
1895
1792
  }),
1896
1793
  type: "context",
1897
1794
  category: "session",
@@ -1962,7 +1859,7 @@ var createSessionItemsRetrievalTool = async ({
1962
1859
  };
1963
1860
 
1964
1861
  // ee/agentic-retrieval/pipeline/index.ts
1965
- import { z as z8 } from "zod";
1862
+ import { z as z7 } from "zod";
1966
1863
 
1967
1864
  // ee/entitlements.ts
1968
1865
  var ENTITLEMENTS = {
@@ -2106,57 +2003,57 @@ async function resolveReranker(input) {
2106
2003
  }
2107
2004
 
2108
2005
  // ee/agentic-retrieval/pipeline/config.ts
2109
- import { z as z4 } from "zod";
2006
+ import { z as z3 } from "zod";
2110
2007
  var KB_KINDS = ["documents", "conversations", "records"];
2111
2008
  var DEFAULT_PREFILTER_CUTOFF = 2.5;
2112
2009
  var RRF_K = 60;
2113
2010
  var CHUNK_GROUP_MAX = 10;
2114
- var kbProfileSchema = z4.object({
2115
- enabled: z4.boolean().default(true),
2116
- kind: z4.enum(KB_KINDS).default("documents"),
2117
- instructions: z4.string().default(""),
2118
- overrides: z4.object({
2119
- limit: z4.number().int().positive().optional(),
2120
- expand: z4.number().int().min(0).optional(),
2121
- multiQuery: z4.boolean().optional(),
2122
- hyde: z4.boolean().optional()
2011
+ var kbProfileSchema = z3.object({
2012
+ enabled: z3.boolean().default(true),
2013
+ kind: z3.enum(KB_KINDS).default("documents"),
2014
+ instructions: z3.string().default(""),
2015
+ overrides: z3.object({
2016
+ limit: z3.number().int().positive().optional(),
2017
+ expand: z3.number().int().min(0).optional(),
2018
+ multiQuery: z3.boolean().optional(),
2019
+ hyde: z3.boolean().optional()
2123
2020
  }).default({})
2124
2021
  });
2125
- var knowledgeBasesSchema = z4.record(z4.string(), kbProfileSchema);
2126
- var routingRuleSchema = z4.object({
2127
- id: z4.string(),
2128
- label: z4.string(),
2129
- description: z4.string(),
2130
- main: z4.array(z4.string()),
2131
- fallback: z4.array(z4.string()).default([])
2022
+ var knowledgeBasesSchema = z3.record(z3.string(), kbProfileSchema);
2023
+ var routingRuleSchema = z3.object({
2024
+ id: z3.string(),
2025
+ label: z3.string(),
2026
+ description: z3.string(),
2027
+ main: z3.array(z3.string()),
2028
+ fallback: z3.array(z3.string()).default([])
2132
2029
  });
2133
- var routingSchema = z4.object({ rules: z4.array(routingRuleSchema).default([]) });
2134
- var identifierSetSchema = z4.object({
2135
- name: z4.string(),
2136
- description: z4.string().default(""),
2137
- examples: z4.array(z4.string()).default([]),
2138
- strategy: z4.enum(["fuzzy", "exact"]),
2139
- contexts: z4.array(z4.string()).default([])
2030
+ var routingSchema = z3.object({ rules: z3.array(routingRuleSchema).default([]) });
2031
+ var identifierSetSchema = z3.object({
2032
+ name: z3.string(),
2033
+ description: z3.string().default(""),
2034
+ examples: z3.array(z3.string()).default([]),
2035
+ strategy: z3.enum(["fuzzy", "exact"]),
2036
+ contexts: z3.array(z3.string()).default([])
2140
2037
  });
2141
- var vocabularySchema = z4.object({
2142
- glossary: z4.array(z4.object({ term: z4.string(), meaning: z4.string() })).default([]),
2143
- identifiers: z4.array(identifierSetSchema).default([]),
2144
- rewrites: z4.array(z4.object({ find: z4.string(), replace: z4.string() })).default([]),
2145
- styleHint: z4.string().default("")
2038
+ var vocabularySchema = z3.object({
2039
+ glossary: z3.array(z3.object({ term: z3.string(), meaning: z3.string() })).default([]),
2040
+ identifiers: z3.array(identifierSetSchema).default([]),
2041
+ rewrites: z3.array(z3.object({ find: z3.string(), replace: z3.string() })).default([]),
2042
+ styleHint: z3.string().default("")
2146
2043
  });
2147
- var memorySchema = z4.object({
2148
- enabled: z4.boolean().default(true),
2149
- override: z4.boolean().default(false),
2150
- filePrioritization: z4.boolean().default(false),
2151
- queryAugmentation: z4.boolean().default(true)
2044
+ var memorySchema = z3.object({
2045
+ enabled: z3.boolean().default(true),
2046
+ override: z3.boolean().default(false),
2047
+ filePrioritization: z3.boolean().default(false),
2048
+ queryAugmentation: z3.boolean().default(true)
2152
2049
  });
2153
- var tuningSchema = z4.object({
2154
- topK: z4.number().int().positive().default(5),
2155
- fallbackThreshold: z4.number().min(0).max(1).default(0.95),
2156
- pinBoost: z4.number().min(0).max(1).default(0.15),
2157
- identifierBoost: z4.number().min(0).max(1).default(0.15),
2158
- pageWindow: z4.number().int().min(0).default(1),
2159
- maxQueriesPerContext: z4.number().int().positive().default(5)
2050
+ var tuningSchema = z3.object({
2051
+ topK: z3.number().int().positive().default(5),
2052
+ fallbackThreshold: z3.number().min(0).max(1).default(0.95),
2053
+ pinBoost: z3.number().min(0).max(1).default(0.15),
2054
+ identifierBoost: z3.number().min(0).max(1).default(0.15),
2055
+ pageWindow: z3.number().int().min(0).default(1),
2056
+ maxQueriesPerContext: z3.number().int().positive().default(5)
2160
2057
  });
2161
2058
  var boolVal = (v) => v === true || v === "true" || v === 1;
2162
2059
  var strVal = (v, fallback) => typeof v === "string" && v.length > 0 ? v : fallback;
@@ -2212,6 +2109,7 @@ function parsePipelineConfig(raw) {
2212
2109
  managedContext: boolVal(r["managed_context"]),
2213
2110
  requirePreselectedContexts: boolVal(r["require_preselected_contexts"]),
2214
2111
  logging: boolVal(r["logging"]),
2112
+ projectSearch: r["project_search"] === void 0 || r["project_search"] === "" ? true : boolVal(r["project_search"]),
2215
2113
  utilityModel: strVal(r["utility_model"], ""),
2216
2114
  knowledgeBases: jsonVal("knowledge_bases", knowledgeBasesSchema, r["knowledge_bases"]),
2217
2115
  routing: jsonVal("routing", routingSchema, r["routing"]),
@@ -2244,9 +2142,69 @@ function effectiveKbSettings(profile, ctx) {
2244
2142
  };
2245
2143
  }
2246
2144
 
2145
+ // ee/agentic-retrieval/pipeline/global-ids.ts
2146
+ function parsePreselectedItems(globalIds) {
2147
+ const map = /* @__PURE__ */ new Map();
2148
+ for (const gid of globalIds) {
2149
+ const slashIdx = gid.indexOf("/");
2150
+ if (slashIdx === -1) {
2151
+ if (gid) map.set(gid, null);
2152
+ continue;
2153
+ }
2154
+ const contextId = gid.slice(0, slashIdx);
2155
+ const itemId = gid.slice(slashIdx + 1);
2156
+ if (!contextId || !itemId) continue;
2157
+ if (map.get(contextId) === null) continue;
2158
+ const existing = map.get(contextId) ?? [];
2159
+ existing.push(itemId);
2160
+ map.set(contextId, existing);
2161
+ }
2162
+ return map;
2163
+ }
2164
+
2165
+ // ee/agentic-retrieval/pipeline/project-scope.ts
2166
+ function resolveProjectScope(opts) {
2167
+ const { scope, enabledContextIds, availableContextIds } = opts;
2168
+ if (!scope || scope.items.length === 0) return void 0;
2169
+ const itemsByContext = parsePreselectedItems(scope.items);
2170
+ const pinsByContext = /* @__PURE__ */ new Map();
2171
+ const scopedItemsByContext = /* @__PURE__ */ new Map();
2172
+ const addedContextIds = [];
2173
+ const allProjectContextIds = [];
2174
+ for (const [ctxId, itemIds] of itemsByContext) {
2175
+ if (!availableContextIds.has(ctxId)) {
2176
+ console.warn(
2177
+ `[EXULU pipeline] project "${scope.name}" references unknown context "${ctxId}" \u2014 skipping those items.`
2178
+ );
2179
+ continue;
2180
+ }
2181
+ allProjectContextIds.push(ctxId);
2182
+ if (enabledContextIds.has(ctxId)) {
2183
+ if (itemIds && itemIds.length > 0) pinsByContext.set(ctxId, new Set(itemIds));
2184
+ } else {
2185
+ scopedItemsByContext.set(ctxId, itemIds);
2186
+ addedContextIds.push(ctxId);
2187
+ }
2188
+ }
2189
+ if (allProjectContextIds.length === 0) return void 0;
2190
+ return { pinsByContext, scopedItemsByContext, addedContextIds, allProjectContextIds };
2191
+ }
2192
+ var TRANSCRIPTIONS_CONTEXT_ID = "transcriptions";
2193
+ function buildProjectKbProfileDefaults(items) {
2194
+ const defaults = {};
2195
+ for (const gid of items) {
2196
+ const slashIdx = gid.indexOf("/");
2197
+ const ctxId = slashIdx === -1 ? gid : gid.slice(0, slashIdx);
2198
+ if (ctxId === TRANSCRIPTIONS_CONTEXT_ID && !defaults[ctxId]) {
2199
+ defaults[ctxId] = { enabled: true, kind: "conversations", instructions: "", overrides: {} };
2200
+ }
2201
+ }
2202
+ return defaults;
2203
+ }
2204
+
2247
2205
  // ee/agentic-retrieval/pipeline/routing.ts
2248
2206
  import { generateText as generateText2, Output as Output2 } from "ai";
2249
- import { z as z6 } from "zod";
2207
+ import { z as z5 } from "zod";
2250
2208
 
2251
2209
  // src/utils/with-retry.ts
2252
2210
  async function withRetry(generateFn, maxRetries = 3) {
@@ -2269,7 +2227,7 @@ async function withRetry(generateFn, maxRetries = 3) {
2269
2227
  // ee/agentic-retrieval/pipeline/prefilter.ts
2270
2228
  import Fuse from "fuse.js";
2271
2229
  import { generateText, Output } from "ai";
2272
- import { z as z5 } from "zod";
2230
+ import { z as z4 } from "zod";
2273
2231
 
2274
2232
  // ee/agentic-retrieval/pipeline/text-utils.ts
2275
2233
  var normalizeFileName = (fileName) => {
@@ -2514,9 +2472,9 @@ async function resolveIdentifierPins({
2514
2472
  system: set.strategy === "exact" ? EXACT_EXTRACTION_PROMPT(set) : FUZZY_EXTRACTION_PROMPT(set),
2515
2473
  messages: [{ role: "user", content: question }],
2516
2474
  output: Output.object({
2517
- schema: z5.object({
2518
- hasMatches: z5.boolean(),
2519
- matches: z5.array(z5.string()).optional()
2475
+ schema: z4.object({
2476
+ hasMatches: z4.boolean(),
2477
+ matches: z4.array(z4.string()).optional()
2520
2478
  })
2521
2479
  }),
2522
2480
  maxOutputTokens: 300
@@ -2627,11 +2585,11 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
2627
2585
  system: buildDocPagePrompt(knownIdentifiers),
2628
2586
  messages: [{ role: "user", content: question }],
2629
2587
  output: Output2.object({
2630
- schema: z6.object({
2631
- hasFilenameHint: z6.boolean(),
2632
- filenameHints: z6.array(z6.string()).optional(),
2633
- hasPageHint: z6.boolean(),
2634
- pageNumber: z6.number().int().nullable().optional()
2588
+ schema: z5.object({
2589
+ hasFilenameHint: z5.boolean(),
2590
+ filenameHints: z5.array(z5.string()).optional(),
2591
+ hasPageHint: z5.boolean(),
2592
+ pageNumber: z5.number().int().nullable().optional()
2635
2593
  })
2636
2594
  }),
2637
2595
  maxOutputTokens: 300
@@ -2658,9 +2616,9 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
2658
2616
  temperature: 0,
2659
2617
  system: kbSystemPrompt,
2660
2618
  output: Output2.object({
2661
- schema: z6.object({
2662
- explicitlyRequestedKnowledgeBases: z6.array(
2663
- z6.enum(enabledContexts.map((c) => c.id))
2619
+ schema: z5.object({
2620
+ explicitlyRequestedKnowledgeBases: z5.array(
2621
+ z5.enum(enabledContexts.map((c) => c.id))
2664
2622
  )
2665
2623
  })
2666
2624
  }),
@@ -2758,9 +2716,9 @@ ${extraInstructions}
2758
2716
  system: classifyPrompt,
2759
2717
  messages: [{ role: "user", content: question }],
2760
2718
  output: Output2.object({
2761
- schema: z6.object({
2762
- ruleId: z6.enum(ruleIds),
2763
- reason: z6.string()
2719
+ schema: z5.object({
2720
+ ruleId: z5.enum(ruleIds),
2721
+ reason: z5.string()
2764
2722
  })
2765
2723
  }),
2766
2724
  maxOutputTokens: 200
@@ -2822,7 +2780,7 @@ ${extraInstructions}
2822
2780
 
2823
2781
  // ee/agentic-retrieval/pipeline/memory.ts
2824
2782
  import { generateText as generateText3, Output as Output3 } from "ai";
2825
- import { z as z7 } from "zod";
2783
+ import { z as z6 } from "zod";
2826
2784
 
2827
2785
  // ee/agentic-retrieval/pipeline/multi-query.ts
2828
2786
  async function singleSearch({
@@ -3063,8 +3021,8 @@ async function runMemoryPhase({
3063
3021
  }
3064
3022
  ],
3065
3023
  output: Output3.object({
3066
- schema: z7.object({
3067
- relevantChunkIds: z7.array(z7.string()).describe(
3024
+ schema: z6.object({
3025
+ relevantChunkIds: z6.array(z6.string()).describe(
3068
3026
  "The chunk_ids (UUIDs at the start of each bullet) of chunks containing information relevant to the user's question. Empty array if none are relevant."
3069
3027
  )
3070
3028
  })
@@ -3180,17 +3138,17 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
3180
3138
  }
3181
3139
  ],
3182
3140
  output: Output3.object({
3183
- schema: z7.object({
3184
- overrides: z7.boolean().describe(
3141
+ schema: z6.object({
3142
+ overrides: z6.boolean().describe(
3185
3143
  "True ONLY if a memory chunk directly and sufficiently answers the user's question and should be authoritative over the documents. Be strict; when unsure, false."
3186
3144
  ),
3187
- confidence: z7.enum(["high", "medium", "low"]).describe(
3145
+ confidence: z6.enum(["high", "medium", "low"]).describe(
3188
3146
  "Confidence that the selected memory chunk(s) fully and directly answer the question."
3189
3147
  ),
3190
- authoritativeChunkIds: z7.array(z7.string()).describe(
3148
+ authoritativeChunkIds: z6.array(z6.string()).describe(
3191
3149
  "The chunk_ids of the memory chunk(s) that directly answer the question. Empty if overrides is false."
3192
3150
  ),
3193
- reason: z7.string().describe(
3151
+ reason: z6.string().describe(
3194
3152
  "One short sentence: why this memory does or does not directly answer the question."
3195
3153
  )
3196
3154
  })
@@ -3221,9 +3179,9 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
3221
3179
  system: "You are a helpful assistant that will strictly follow the user's instructions.",
3222
3180
  messages: [{ role: "user", content: PROMPT_EXTRACT_PRIORITIZED_FILES }],
3223
3181
  output: Output3.object({
3224
- schema: z7.object({
3225
- shouldPrioritizeFiles: z7.boolean(),
3226
- fileNameHints: z7.array(z7.string()).optional()
3182
+ schema: z6.object({
3183
+ shouldPrioritizeFiles: z6.boolean(),
3184
+ fileNameHints: z6.array(z6.string()).optional()
3227
3185
  })
3228
3186
  }),
3229
3187
  maxOutputTokens: 300
@@ -3242,10 +3200,10 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
3242
3200
  system: "You are a helpful assistant that will strictly follow the user's instructions.",
3243
3201
  messages: [{ role: "user", content: QUERY_AUGMENTATION_PROMPT }],
3244
3202
  output: Output3.object({
3245
- schema: z7.object({
3246
- updatedUserQuestion: z7.string(),
3247
- updatedRelevantKeywords: z7.array(z7.string()),
3248
- updatedImportantKeyword: z7.string()
3203
+ schema: z6.object({
3204
+ updatedUserQuestion: z6.string(),
3205
+ updatedRelevantKeywords: z6.array(z6.string()),
3206
+ updatedImportantKeyword: z6.string()
3249
3207
  })
3250
3208
  }),
3251
3209
  maxOutputTokens: 600
@@ -3426,6 +3384,7 @@ async function searchContexts(opts) {
3426
3384
  role,
3427
3385
  model,
3428
3386
  preselectedItems,
3387
+ scopedItemsByContext,
3429
3388
  identifierPinsByContext,
3430
3389
  memoryPinnedItemIds,
3431
3390
  userPinnedItemIdsByContext,
@@ -3452,6 +3411,8 @@ async function searchContexts(opts) {
3452
3411
  let pinnedItemIds;
3453
3412
  if (hasPreselection) {
3454
3413
  pinnedItemIds = preselectedItems.get(ctxId) ?? [];
3414
+ } else if (scopedItemsByContext?.has(ctxId)) {
3415
+ pinnedItemIds = scopedItemsByContext.get(ctxId) ?? [];
3455
3416
  } else if (!skipPrefilter) {
3456
3417
  const identifierPins = identifierPinsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
3457
3418
  let pins = new Set(identifierPins);
@@ -3659,24 +3620,6 @@ async function rerankResults(opts) {
3659
3620
  }
3660
3621
 
3661
3622
  // ee/agentic-retrieval/pipeline/index.ts
3662
- function parsePreselectedItems(globalIds) {
3663
- const map = /* @__PURE__ */ new Map();
3664
- for (const gid of globalIds) {
3665
- const slashIdx = gid.indexOf("/");
3666
- if (slashIdx === -1) {
3667
- if (gid) map.set(gid, null);
3668
- continue;
3669
- }
3670
- const contextId = gid.slice(0, slashIdx);
3671
- const itemId = gid.slice(slashIdx + 1);
3672
- if (!contextId || !itemId) continue;
3673
- if (map.get(contextId) === null) continue;
3674
- const existing = map.get(contextId) ?? [];
3675
- existing.push(itemId);
3676
- map.set(contextId, existing);
3677
- }
3678
- return map;
3679
- }
3680
3623
  function addChunks(result, chunks) {
3681
3624
  const seen = new Set(result.chunks.map((c) => c.chunk_id));
3682
3625
  for (const chunk of chunks) {
@@ -3706,7 +3649,8 @@ function createAgenticRetrievalTool(opts) {
3706
3649
  model,
3707
3650
  instructions: adminInstructions,
3708
3651
  preselected,
3709
- memoryItems
3652
+ memoryItems,
3653
+ projectScope
3710
3654
  } = opts;
3711
3655
  const license = checkLicense();
3712
3656
  if (!license["agentic-retrieval"]) {
@@ -3716,7 +3660,9 @@ function createAgenticRetrievalTool(opts) {
3716
3660
  return ExuluTool.internal({
3717
3661
  id: "agentic_context_search",
3718
3662
  name: "Context Search",
3719
- 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).`,
3663
+ 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
3664
+ // config is off — the config is only known at execute time, not at factory time.
3665
+ (projectScope ? ` Also searches the knowledge items attached to the project "${projectScope.name}".` : ""),
3720
3666
  category: "contexts",
3721
3667
  needsApproval: false,
3722
3668
  type: "context",
@@ -3759,10 +3705,16 @@ function createAgenticRetrievalTool(opts) {
3759
3705
  },
3760
3706
  {
3761
3707
  name: "max_steps",
3762
- 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).",
3708
+ 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).",
3763
3709
  type: "number",
3764
3710
  default: 0
3765
3711
  },
3712
+ {
3713
+ name: "project_search",
3714
+ 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).",
3715
+ type: "boolean",
3716
+ default: true
3717
+ },
3766
3718
  {
3767
3719
  name: "knowledge_bases",
3768
3720
  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.",
@@ -3794,11 +3746,11 @@ function createAgenticRetrievalTool(opts) {
3794
3746
  default: '{"topK":5,"fallbackThreshold":0.95,"pinBoost":0.15,"identifierBoost":0.15,"pageWindow":1,"maxQueriesPerContext":5}'
3795
3747
  }
3796
3748
  ],
3797
- inputSchema: z8.object({
3798
- userQuery: z8.string().describe("The original unaltered question from the user"),
3799
- relevantKeywords: z8.array(z8.string()).describe("Keywords extracted from the user's question relevant to the search"),
3800
- importantKeyword: z8.string().describe("The single most important keyword from the user's question"),
3801
- confirmedContextIds: z8.array(z8.string()).optional().describe(
3749
+ inputSchema: z7.object({
3750
+ userQuery: z7.string().describe("The original unaltered question from the user"),
3751
+ relevantKeywords: z7.array(z7.string()).describe("Keywords extracted from the user's question relevant to the search"),
3752
+ importantKeyword: z7.string().describe("The single most important keyword from the user's question"),
3753
+ confirmedContextIds: z7.array(z7.string()).optional().describe(
3802
3754
  "Knowledge base IDs explicitly confirmed by the user to be used in the retrieval. When present, only searches these contexts."
3803
3755
  )
3804
3756
  }),
@@ -3872,6 +3824,23 @@ function createAgenticRetrievalTool(opts) {
3872
3824
  }
3873
3825
  }
3874
3826
  const preselectedItems = parsePreselectedItems(preselected ?? []);
3827
+ const availableContextsById = new Map(contexts.map((c) => [c.id, c]));
3828
+ const resolvedProject = cfg.projectSearch ? resolveProjectScope({
3829
+ scope: projectScope,
3830
+ enabledContextIds: new Set(enabledContexts.map((c) => c.id)),
3831
+ availableContextIds: new Set(availableContextsById.keys())
3832
+ }) : void 0;
3833
+ if (resolvedProject) {
3834
+ if (projectScope?.kbProfileDefaults) {
3835
+ for (const [ctxId, profile] of Object.entries(projectScope.kbProfileDefaults)) {
3836
+ if (!cfg.knowledgeBases[ctxId]) cfg.knowledgeBases[ctxId] = profile;
3837
+ }
3838
+ }
3839
+ enabledContexts = [
3840
+ ...enabledContexts,
3841
+ ...resolvedProject.addedContextIds.map((id) => availableContextsById.get(id)).filter((c) => Boolean(c))
3842
+ ];
3843
+ }
3875
3844
  const contextsById = new Map(enabledContexts.map((c) => [c.id, c]));
3876
3845
  const kbKindById = new Map(
3877
3846
  enabledContexts.map((c) => [
@@ -3882,7 +3851,12 @@ function createAgenticRetrievalTool(opts) {
3882
3851
  const documentContexts = enabledContexts.filter(
3883
3852
  (c) => (cfg.knowledgeBases[c.id]?.kind ?? "documents") === "documents"
3884
3853
  );
3885
- const extraInstructions = [cfg.instructions, adminInstructions].filter(Boolean).join("\n");
3854
+ const extraInstructions = [
3855
+ cfg.instructions,
3856
+ adminInstructions,
3857
+ resolvedProject && projectScope?.customInstructions ? `Instructions for the attached project "${projectScope.name}":
3858
+ ${projectScope.customInstructions}` : ""
3859
+ ].filter(Boolean).join("\n");
3886
3860
  const [memResult, routResult] = await Promise.all([
3887
3861
  runMemoryPhase({
3888
3862
  memoryChunks: memoryItems ?? [],
@@ -3928,6 +3902,30 @@ function createAgenticRetrievalTool(opts) {
3928
3902
  yield { result: "The user has requested to search in knowledge bases that are not part of the preselected knowledge bases: " + missing.join(", ") };
3929
3903
  return;
3930
3904
  }
3905
+ let effectiveMainContexts = mainContexts;
3906
+ if (resolvedProject) {
3907
+ const mainSet = new Set(mainContexts);
3908
+ const appended = resolvedProject.allProjectContextIds.filter(
3909
+ (id) => !mainSet.has(id) && contextsById.has(id)
3910
+ );
3911
+ if (appended.length > 0) {
3912
+ effectiveMainContexts = [...mainContexts, ...appended];
3913
+ result.steps.push({
3914
+ stepNumber: 1,
3915
+ text: `Including sources from project "${projectScope.name}": ${appended.join(", ")}`,
3916
+ toolCalls: [],
3917
+ chunks: [],
3918
+ tokens: 0
3919
+ });
3920
+ result.reasoning.push({
3921
+ text: `Including project sources: ${appended.join(", ")}`,
3922
+ tools: []
3923
+ });
3924
+ }
3925
+ }
3926
+ const fallbackContextsToSearch = fallbackContexts.filter(
3927
+ (id) => !effectiveMainContexts.includes(id)
3928
+ );
3931
3929
  const {
3932
3930
  updatedQuestion,
3933
3931
  updatedKeywords,
@@ -3954,7 +3952,7 @@ function createAgenticRetrievalTool(opts) {
3954
3952
  }
3955
3953
  const [mainSearch, speculativeFallbackSearch] = await Promise.all([
3956
3954
  searchContexts({
3957
- contextIds: mainContexts,
3955
+ contextIds: effectiveMainContexts,
3958
3956
  contextsById,
3959
3957
  kbProfiles: cfg.knowledgeBases,
3960
3958
  question: updatedQuestion,
@@ -3967,13 +3965,14 @@ function createAgenticRetrievalTool(opts) {
3967
3965
  identifierPinsByContext,
3968
3966
  memoryPinnedItemIds,
3969
3967
  userPinnedItemIdsByContext,
3968
+ scopedItemsByContext: resolvedProject?.scopedItemsByContext,
3970
3969
  rewrites: cfg.vocabulary.rewrites,
3971
3970
  styleHint: cfg.vocabulary.styleHint,
3972
3971
  maxQueries: cfg.tuning.maxQueriesPerContext,
3973
3972
  skipPrefilter: false
3974
3973
  }),
3975
- fallbackContexts.length > 0 && !hasExplicitDocAndPage ? searchContexts({
3976
- contextIds: fallbackContexts,
3974
+ fallbackContextsToSearch.length > 0 && !hasExplicitDocAndPage ? searchContexts({
3975
+ contextIds: fallbackContextsToSearch,
3977
3976
  contextsById,
3978
3977
  kbProfiles: cfg.knowledgeBases,
3979
3978
  question: updatedQuestion,
@@ -3986,6 +3985,7 @@ function createAgenticRetrievalTool(opts) {
3986
3985
  identifierPinsByContext,
3987
3986
  memoryPinnedItemIds,
3988
3987
  userPinnedItemIdsByContext,
3988
+ scopedItemsByContext: resolvedProject?.scopedItemsByContext,
3989
3989
  rewrites: cfg.vocabulary.rewrites,
3990
3990
  styleHint: cfg.vocabulary.styleHint,
3991
3991
  maxQueries: cfg.tuning.maxQueriesPerContext,
@@ -3999,6 +3999,9 @@ function createAgenticRetrievalTool(opts) {
3999
3999
  })(),
4000
4000
  ...(function* () {
4001
4001
  for (const s of userPinnedItemIdsByContext.values()) yield* s;
4002
+ })(),
4003
+ ...(function* () {
4004
+ if (resolvedProject) for (const s of resolvedProject.pinsByContext.values()) yield* s;
4002
4005
  })()
4003
4006
  ]);
4004
4007
  const userPinnedItemIds = new Set(
@@ -4066,15 +4069,15 @@ function createAgenticRetrievalTool(opts) {
4066
4069
  result.reasoning.push({ text: "Literal lookup satisfied; skipping fallback.", tools: [] });
4067
4070
  yield { result: serializeOutput(result) };
4068
4071
  }
4069
- if (!literalLookupSatisfied && fallbackContexts.length > 0 && (reranker ? mainRerank.rerank_score_max_genuine < cfg.tuning.fallbackThreshold : mainRerank.limited_results.length < cfg.tuning.topK)) {
4072
+ if (!literalLookupSatisfied && fallbackContextsToSearch.length > 0 && (reranker ? mainRerank.rerank_score_max_genuine < cfg.tuning.fallbackThreshold : mainRerank.limited_results.length < cfg.tuning.topK)) {
4070
4073
  result.steps.push({
4071
4074
  stepNumber: 1,
4072
- text: `Using fallback search in ${fallbackContexts.join(", ")}`,
4075
+ text: `Using fallback search in ${fallbackContextsToSearch.join(", ")}`,
4073
4076
  toolCalls: [],
4074
4077
  chunks: [],
4075
4078
  tokens: 0
4076
4079
  });
4077
- result.reasoning.push({ text: `Fallback search in ${fallbackContexts.join(", ")}`, tools: [] });
4080
+ result.reasoning.push({ text: `Fallback search in ${fallbackContextsToSearch.join(", ")}`, tools: [] });
4078
4081
  yield { result: serializeOutput(result) };
4079
4082
  const fallbackRerank = await rerankResults({
4080
4083
  chunks: speculativeFallbackSearch.chunks,
@@ -4160,7 +4163,7 @@ function sanitizeToolName(name) {
4160
4163
  }
4161
4164
 
4162
4165
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
4163
- import { randomUUID as randomUUID3 } from "crypto";
4166
+ import { randomUUID as randomUUID4 } from "crypto";
4164
4167
 
4165
4168
  // types/enums/statistics.ts
4166
4169
  var STATISTICS_TYPE_ENUM = {
@@ -4176,12 +4179,12 @@ var STATISTICS_TYPE_ENUM = {
4176
4179
  };
4177
4180
 
4178
4181
  // src/templates/tools/memory-tool.ts
4179
- import { z as z9 } from "zod";
4182
+ import { z as z8 } from "zod";
4180
4183
  var createNewMemoryItemTool = (agent, context) => {
4181
4184
  const fields = {
4182
- name: z9.string().describe("The name of the item to create"),
4183
- description: z9.string().describe("The description of the item to create"),
4184
- surroundingContext: z9.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...")
4185
+ name: z8.string().describe("The name of the item to create"),
4186
+ description: z8.string().describe("The description of the item to create"),
4187
+ surroundingContext: z8.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...")
4185
4188
  };
4186
4189
  for (const field of context.fields) {
4187
4190
  switch (field.type) {
@@ -4189,47 +4192,47 @@ var createNewMemoryItemTool = (agent, context) => {
4189
4192
  case "longText":
4190
4193
  case "shortText":
4191
4194
  case "code":
4192
- fields[field.name] = z9.string().describe("The " + field.name + " of the item to create");
4195
+ fields[field.name] = z8.string().describe("The " + field.name + " of the item to create");
4193
4196
  break;
4194
4197
  case "enum":
4195
4198
  if (field.enumValues && field.enumValues.length > 0) {
4196
4199
  const enumValues = field.enumValues;
4197
- fields[field.name] = z9.preprocess(
4200
+ fields[field.name] = z8.preprocess(
4198
4201
  (v) => typeof v === "string" ? v.toUpperCase() : v,
4199
- z9.enum(enumValues)
4202
+ z8.enum(enumValues)
4200
4203
  ).describe(
4201
4204
  "The " + field.name + " of the item to create. Must be one of: " + field.enumValues.join(", ")
4202
4205
  );
4203
4206
  } else {
4204
- fields[field.name] = z9.string().describe("The " + field.name + " of the item to create");
4207
+ fields[field.name] = z8.string().describe("The " + field.name + " of the item to create");
4205
4208
  }
4206
4209
  break;
4207
4210
  case "json":
4208
- fields[field.name] = z9.string({}).describe(
4211
+ fields[field.name] = z8.string({}).describe(
4209
4212
  "The " + field.name + " of the item to create, it should be a valid JSON string."
4210
4213
  );
4211
4214
  break;
4212
4215
  case "markdown":
4213
- fields[field.name] = z9.string().describe(
4216
+ fields[field.name] = z8.string().describe(
4214
4217
  "The " + field.name + " of the item to create, it should be a valid Markdown string."
4215
4218
  );
4216
4219
  break;
4217
4220
  case "number":
4218
- fields[field.name] = z9.number().describe("The " + field.name + " of the item to create");
4221
+ fields[field.name] = z8.number().describe("The " + field.name + " of the item to create");
4219
4222
  break;
4220
4223
  case "boolean":
4221
- fields[field.name] = z9.boolean().describe("The " + field.name + " of the item to create");
4224
+ fields[field.name] = z8.boolean().describe("The " + field.name + " of the item to create");
4222
4225
  break;
4223
4226
  case "file":
4224
4227
  case "uuid":
4225
4228
  case "date":
4226
4229
  break;
4227
4230
  default:
4228
- fields[field.name] = z9.string().describe("The " + field.name + " of the item to create");
4231
+ fields[field.name] = z8.string().describe("The " + field.name + " of the item to create");
4229
4232
  break;
4230
4233
  }
4231
4234
  }
4232
- fields["visibility"] = z9.enum(["private", "public"]).optional().describe(
4235
+ fields["visibility"] = z8.enum(["private", "public"]).optional().describe(
4233
4236
  "Whether this memory is private to the user or shared (public). Ask the user if unknown."
4234
4237
  );
4235
4238
  const toolName = "create_" + sanitizeName(context.name) + "_memory_item";
@@ -4239,7 +4242,7 @@ var createNewMemoryItemTool = (agent, context) => {
4239
4242
  category: agent.name + "_memory",
4240
4243
  description: "Create a new memory item in the " + agent.name + " memory context",
4241
4244
  type: "function",
4242
- inputSchema: z9.object(fields),
4245
+ inputSchema: z8.object(fields),
4243
4246
  config: [],
4244
4247
  execute: async (params) => {
4245
4248
  const { name, description, surroundingContext, information, visibility, exuluConfig, user } = params;
@@ -5424,7 +5427,7 @@ ${body}`
5424
5427
  // ee/invoke-skills/create-sandbox.ts
5425
5428
  import { createBashTool } from "bash-tool";
5426
5429
  import { tool as tool2 } from "ai";
5427
- import { z as z10 } from "zod";
5430
+ import { z as z9 } from "zod";
5428
5431
  import CryptoJS4 from "crypto-js";
5429
5432
  var getAllExuluVariables = async () => {
5430
5433
  const { db: db2 } = await postgresClient();
@@ -5842,9 +5845,9 @@ Probe error: ${probe.reason ?? "(no detail)"}`
5842
5845
  });
5843
5846
  const writeFileTool = tool2({
5844
5847
  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.',
5845
- inputSchema: z10.object({
5846
- path: z10.string().describe("The path where the file should be written. Relative paths and leading-slash paths are both resolved against the session sandbox root."),
5847
- content: z10.string().describe("The content to write to the file")
5848
+ inputSchema: z9.object({
5849
+ path: z9.string().describe("The path where the file should be written. Relative paths and leading-slash paths are both resolved against the session sandbox root."),
5850
+ content: z9.string().describe("The content to write to the file")
5848
5851
  }),
5849
5852
  execute: async ({ path, content }) => {
5850
5853
  const resolvedPath = resolveSessionPath(path, sessionDir);
@@ -5863,8 +5866,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
5863
5866
  });
5864
5867
  const readFileTool = tool2({
5865
5868
  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.',
5866
- inputSchema: z10.object({
5867
- path: z10.string().describe("The path of the file to read. Relative paths and leading-slash paths are both resolved against the session sandbox root.")
5869
+ inputSchema: z9.object({
5870
+ path: z9.string().describe("The path of the file to read. Relative paths and leading-slash paths are both resolved against the session sandbox root.")
5868
5871
  }),
5869
5872
  execute: async ({ path }) => {
5870
5873
  const resolvedPath = resolveSessionPath(path, sessionDir);
@@ -5875,8 +5878,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
5875
5878
  const originalBashTool = tools.bash;
5876
5879
  const bashTool = tool2({
5877
5880
  description: originalBashTool.description ?? "",
5878
- inputSchema: z10.object({
5879
- command: z10.string().describe("The bash command to execute.")
5881
+ inputSchema: z9.object({
5882
+ command: z9.string().describe("The bash command to execute.")
5880
5883
  }),
5881
5884
  execute: async (args, opts) => {
5882
5885
  const before = persistenceEnabled ? await snapshotSessionArtifacts() : null;
@@ -5948,9 +5951,9 @@ ${lines.join("\n")}`;
5948
5951
  }
5949
5952
 
5950
5953
  // src/utils/truncate-tool-output.ts
5951
- var truncateToolOutput = (output, maxContextLength, toolName, tailFraction = 0.1) => {
5954
+ var truncateToolOutput = (output, maxContextLength, toolName, tailFraction = 0.1, charLimitOverride) => {
5952
5955
  const effectiveCtx = maxContextLength != null && maxContextLength > 0 ? maxContextLength : 128e3;
5953
- const charLimit = Math.floor(effectiveCtx * 0.25 * 4);
5956
+ const charLimit = charLimitOverride != null && charLimitOverride > 0 ? charLimitOverride : Math.floor(effectiveCtx * 0.25 * 4);
5954
5957
  const clampedTail = Math.min(1, Math.max(0, tailFraction));
5955
5958
  if (output.length <= charLimit) return output;
5956
5959
  const headChars = Math.floor(charLimit * (1 - clampedTail));
@@ -5970,8 +5973,230 @@ var truncateToolOutput = (output, maxContextLength, toolName, tailFraction = 0.1
5970
5973
  return head + marker + tail;
5971
5974
  };
5972
5975
 
5976
+ // src/exulu/tool-output-offload.ts
5977
+ import { randomUUID as randomUUID3 } from "crypto";
5978
+
5979
+ // src/exulu/context-budget.ts
5980
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
5981
+ var deriveContextBudget = (contextWindowInput) => {
5982
+ const contextWindow = contextWindowInput != null && contextWindowInput > 0 ? contextWindowInput : DEFAULT_CONTEXT_WINDOW;
5983
+ const outputReserve = Math.min(32e3, Math.floor(contextWindow * 0.2));
5984
+ const usableWindow = contextWindow - outputReserve;
5985
+ return {
5986
+ contextWindow,
5987
+ outputReserve,
5988
+ usableWindow,
5989
+ warnThreshold: Math.floor(usableWindow * 0.8),
5990
+ blockThreshold: Math.floor(usableWindow * 0.95),
5991
+ toolOutputCapTokens: Math.min(25e3, Math.max(4e3, Math.floor(contextWindow * 0.1))),
5992
+ compactionTailTokens: Math.floor(usableWindow * 0.1),
5993
+ summaryBudgetTokens: Math.min(8e3, Math.floor(usableWindow * 0.05))
5994
+ };
5995
+ };
5996
+ var estimateTokens = (text) => text ? Math.ceil(text.length / 4) : 0;
5997
+ var estimateMessageTokens = (message) => estimateTokens(JSON.stringify(message));
5998
+ var getCompaction = (message) => message.metadata?.compaction;
5999
+ var sliceHistoryAtCheckpoint = (messages) => {
6000
+ let checkpointIdx = -1;
6001
+ for (let i = messages.length - 1; i >= 0; i--) {
6002
+ if (getCompaction(messages[i])) {
6003
+ checkpointIdx = i;
6004
+ break;
6005
+ }
6006
+ }
6007
+ if (checkpointIdx === -1) return messages;
6008
+ const checkpoint = messages[checkpointIdx];
6009
+ const coversUpTo = getCompaction(checkpoint).coversUpTo;
6010
+ const coversIdx = messages.findIndex((m) => m.id === coversUpTo);
6011
+ const boundary = coversIdx === -1 ? checkpointIdx : coversIdx;
6012
+ const after = messages.filter((m, i) => i > boundary && i !== checkpointIdx);
6013
+ return [checkpoint, ...after];
6014
+ };
6015
+ var contextOccupancy = (messages) => {
6016
+ let anchorIdx = -1;
6017
+ for (let i = messages.length - 1; i >= 0; i--) {
6018
+ const m = messages[i];
6019
+ const meta = m.metadata;
6020
+ if (getCompaction(m) || m.role === "assistant" && (typeof meta?.inputTokens === "number" || typeof meta?.lastStepInputTokens === "number")) {
6021
+ anchorIdx = i;
6022
+ break;
6023
+ }
6024
+ }
6025
+ let total = 0;
6026
+ let rest = messages;
6027
+ if (anchorIdx !== -1) {
6028
+ const anchor = messages[anchorIdx];
6029
+ const compaction = getCompaction(anchor);
6030
+ if (compaction) {
6031
+ total = compaction.occupancyEstimate;
6032
+ } else {
6033
+ const meta = anchor.metadata;
6034
+ total = typeof meta.lastStepInputTokens === "number" ? meta.lastStepInputTokens + (meta.lastStepOutputTokens ?? 0) : (meta.inputTokens ?? 0) + (meta.outputTokens ?? 0);
6035
+ }
6036
+ rest = messages.slice(anchorIdx + 1);
6037
+ }
6038
+ for (const m of rest) total += estimateMessageTokens(m);
6039
+ return total;
6040
+ };
6041
+ var CONTEXT_COMPACTION_REQUIRED = "CONTEXT_COMPACTION_REQUIRED";
6042
+ var COMPACTION_INSUFFICIENT = "COMPACTION_INSUFFICIENT";
6043
+ var ContextCompactionRequiredError = class extends Error {
6044
+ constructor(occupancy, budget) {
6045
+ super(
6046
+ JSON.stringify({
6047
+ code: CONTEXT_COMPACTION_REQUIRED,
6048
+ 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.`,
6049
+ occupancy,
6050
+ usableWindow: budget.usableWindow,
6051
+ contextWindow: budget.contextWindow
6052
+ })
6053
+ );
6054
+ this.occupancy = occupancy;
6055
+ this.budget = budget;
6056
+ this.name = "ContextCompactionRequiredError";
6057
+ }
6058
+ };
6059
+ var PROVIDER_CONTEXT_ERROR_PATTERNS = [
6060
+ /ContextWindowExceededError/i,
6061
+ /context.?window/i,
6062
+ /context.?length/i,
6063
+ /maximum context/i,
6064
+ /prompt is too long/i,
6065
+ /input is too long/i,
6066
+ /token count exceeds/i,
6067
+ /too many tokens/i
6068
+ ];
6069
+ var isProviderContextLengthError = (message) => PROVIDER_CONTEXT_ERROR_PATTERNS.some((re) => re.test(message));
6070
+ var mapStreamErrorMessage = (message) => isProviderContextLengthError(message) ? JSON.stringify({
6071
+ code: CONTEXT_COMPACTION_REQUIRED,
6072
+ message: "The model rejected the request because the conversation exceeds its context window. Compact the conversation to continue.",
6073
+ providerMessage: message.slice(0, 500)
6074
+ }) : message;
6075
+
6076
+ // src/exulu/tool-output-offload.ts
6077
+ var PREVIEW_CHARS = 4e3;
6078
+ var storeAsSessionFile = async (serialized, ctx) => {
6079
+ if (!ctx.sessionID || !ctx.exuluConfig?.fileUploads) return void 0;
6080
+ const safeTool = ctx.toolName.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
6081
+ const name = `tool-output-${safeTool}-${randomUUID3().slice(0, 8)}.txt`;
6082
+ try {
6083
+ await uploadFile(
6084
+ Buffer.from(serialized, "utf-8"),
6085
+ `sessions/${ctx.sessionID}/${name}`,
6086
+ ctx.exuluConfig,
6087
+ { contentType: "text/plain" },
6088
+ ctx.user?.id
6089
+ );
6090
+ return name;
6091
+ } catch (err) {
6092
+ console.error("[EXULU] Failed to offload oversized tool output to session files.", err);
6093
+ return void 0;
6094
+ }
6095
+ };
6096
+ var 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.`;
6097
+ var guardToolOutput = async (value, ctx) => {
6098
+ if (value == null) return value;
6099
+ let serialized;
6100
+ try {
6101
+ serialized = typeof value === "string" ? value : JSON.stringify(value);
6102
+ } catch {
6103
+ return value;
6104
+ }
6105
+ if (typeof serialized !== "string") return value;
6106
+ const budget = deriveContextBudget(ctx.contextWindow);
6107
+ const tokens = estimateTokens(serialized);
6108
+ if (tokens <= budget.toolOutputCapTokens) return value;
6109
+ const sessionFile = await storeAsSessionFile(serialized, ctx);
6110
+ const result = {
6111
+ truncated: true,
6112
+ notice: buildNotice(tokens, budget.toolOutputCapTokens, sessionFile),
6113
+ ...sessionFile ? { sessionFile } : {},
6114
+ preview: serialized.slice(0, PREVIEW_CHARS)
6115
+ };
6116
+ return result;
6117
+ };
6118
+ var guardExtractedFileText = async (filename, text, ctx) => {
6119
+ const budget = deriveContextBudget(ctx.contextWindow);
6120
+ const tokens = estimateTokens(text);
6121
+ if (tokens <= budget.toolOutputCapTokens) return text;
6122
+ const sessionFile = await storeAsSessionFile(text, { ...ctx, toolName: `upload-${filename}` });
6123
+ 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.]`;
6124
+ return `${text.slice(0, PREVIEW_CHARS)}
6125
+
6126
+ ${notice}`;
6127
+ };
6128
+
6129
+ // src/templates/tools/session-file-read-tool.ts
6130
+ import { z as z10 } from "zod";
6131
+ var DEFAULT_LIMIT = 250;
6132
+ var MAX_CONTENT_CHARS = 16e3;
6133
+ var createSessionFileReadTool = ({
6134
+ sessionID,
6135
+ user,
6136
+ exuluConfig
6137
+ }) => {
6138
+ if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
6139
+ const readSessionFileExecute = async ({ filename, offset, limit }) => {
6140
+ const safeName = String(filename ?? "").trim();
6141
+ if (!safeName || safeName.includes("..") || safeName.includes("/") || safeName.includes("\\")) {
6142
+ return {
6143
+ error: "Invalid filename \u2014 pass the bare file name exactly as listed in the session files (no paths)."
6144
+ };
6145
+ }
6146
+ const uploads = exuluConfig.fileUploads;
6147
+ const generalPrefix = uploads.s3prefix ? `${uploads.s3prefix.replace(/\/$/, "")}/` : "";
6148
+ const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
6149
+ try {
6150
+ const url = await getPresignedUrl(uploads.s3Bucket, key, exuluConfig);
6151
+ const res = await fetch(url);
6152
+ if (!res.ok) {
6153
+ return { error: `Could not read session file "${safeName}" (status ${res.status}). Check the exact file name.` };
6154
+ }
6155
+ const textBody = await res.text();
6156
+ const lines = textBody.split("\n");
6157
+ const start = (offset ?? 1) - 1;
6158
+ const requested = limit ?? DEFAULT_LIMIT;
6159
+ const sliced = lines.slice(start, start + requested);
6160
+ let content = sliced.join("\n");
6161
+ let linesReturned = sliced.length;
6162
+ if (content.length > MAX_CONTENT_CHARS) {
6163
+ content = content.slice(0, MAX_CONTENT_CHARS);
6164
+ linesReturned = Math.max(1, content.split("\n").length - 1);
6165
+ content = content + "\n[slice truncated \u2014 request fewer lines]";
6166
+ }
6167
+ return {
6168
+ content,
6169
+ totalLines: lines.length,
6170
+ offset: start + 1,
6171
+ linesReturned
6172
+ };
6173
+ } catch (err) {
6174
+ return { error: `Failed to read session file "${safeName}": ${err instanceof Error ? err.message : "unknown error"}` };
6175
+ }
6176
+ };
6177
+ return ExuluTool.internal({
6178
+ id: "read_session_file",
6179
+ name: "read_session_file",
6180
+ needsApproval: false,
6181
+ 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.",
6182
+ inputSchema: z10.object({
6183
+ filename: z10.string().describe('Exact session file name as referenced in a truncation notice, e.g. "tool-output-web_search-a1b2c3d4.txt"'),
6184
+ offset: z10.number().int().min(1).optional().describe("1-based first line to read (default 1)"),
6185
+ limit: z10.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT})`)
6186
+ }),
6187
+ type: "function",
6188
+ category: "session",
6189
+ config: [],
6190
+ // ExuluTool's execute type is modeled on retrieval tools ({result/job/items});
6191
+ // internal utility tools return richer shapes (memory-tool has the same
6192
+ // mismatch). The AI SDK passes the object through verbatim, so cast.
6193
+ execute: readSessionFileExecute
6194
+ });
6195
+ };
6196
+
5973
6197
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
5974
- var generateS3Key = (filename) => `${randomUUID3()}-${filename}`;
6198
+ var OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
6199
+ var generateS3Key = (filename) => `${randomUUID4()}-${filename}`;
5975
6200
  var s3Client2;
5976
6201
  var getMimeType = (type) => {
5977
6202
  switch (type) {
@@ -6069,7 +6294,7 @@ var hydrateVariables = async (tool3) => {
6069
6294
  await Promise.all(promises);
6070
6295
  return tool3;
6071
6296
  };
6072
- var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems) => {
6297
+ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems, contextWindow, disabledTools) => {
6073
6298
  if (!currentTools) return {};
6074
6299
  if (!allExuluTools) {
6075
6300
  allExuluTools = [];
@@ -6077,6 +6302,8 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
6077
6302
  if (!contexts) {
6078
6303
  contexts = [];
6079
6304
  }
6305
+ const budget = deriveContextBudget(contextWindow);
6306
+ const toolOutputCharLimit = budget.toolOutputCapTokens * 4;
6080
6307
  let sharedSessionSandbox;
6081
6308
  if (sessionID && exuluConfig && agent?.sandbox_enabled === true) {
6082
6309
  try {
@@ -6093,16 +6320,28 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
6093
6320
  );
6094
6321
  }
6095
6322
  }
6096
- let projectRetrievalTool;
6097
- if (project) {
6098
- projectRetrievalTool = await createProjectItemsRetrievalTool({
6099
- user,
6100
- role: user?.role?.id,
6101
- contexts,
6102
- projectId: project
6103
- });
6104
- if (projectRetrievalTool) {
6105
- currentTools.push(projectRetrievalTool);
6323
+ const disabled = new Set(disabledTools ?? []);
6324
+ let projectScope;
6325
+ if (project && !disabled.has("agentic_context_search")) {
6326
+ const { db: db2 } = await postgresClient();
6327
+ const projectRow = await db2.from("projects").where("id", project).first();
6328
+ let rawItems = projectRow?.project_items;
6329
+ if (typeof rawItems === "string") {
6330
+ try {
6331
+ rawItems = JSON.parse(rawItems);
6332
+ } catch {
6333
+ rawItems = void 0;
6334
+ }
6335
+ }
6336
+ if (projectRow && Array.isArray(rawItems) && rawItems.length > 0) {
6337
+ projectScope = {
6338
+ id: projectRow.id,
6339
+ name: projectRow.name,
6340
+ description: projectRow.description ?? void 0,
6341
+ customInstructions: projectRow.custom_instructions ?? void 0,
6342
+ items: rawItems,
6343
+ kbProfileDefaults: buildProjectKbProfileDefaults(rawItems)
6344
+ };
6106
6345
  }
6107
6346
  }
6108
6347
  if (agent?.memory && contexts?.length) {
@@ -6113,7 +6352,7 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
6113
6352
  );
6114
6353
  }
6115
6354
  const createNewMemoryTool = createNewMemoryItemTool(agent, context);
6116
- if (createNewMemoryTool) {
6355
+ if (createNewMemoryTool && !disabled.has(createNewMemoryTool.id)) {
6117
6356
  if (!currentTools) {
6118
6357
  currentTools = [];
6119
6358
  }
@@ -6128,31 +6367,62 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
6128
6367
  contexts,
6129
6368
  items: sessionItems
6130
6369
  });
6131
- if (sessionItemsRetrievalTool) {
6370
+ if (sessionItemsRetrievalTool && !disabled.has(sessionItemsRetrievalTool.id)) {
6132
6371
  currentTools.push(sessionItemsRetrievalTool);
6133
6372
  }
6134
6373
  }
6374
+ const sessionFileReadTool = createSessionFileReadTool({ sessionID, user, exuluConfig });
6375
+ if (sessionFileReadTool && !disabled.has(sessionFileReadTool.id)) {
6376
+ currentTools.push(sessionFileReadTool);
6377
+ }
6135
6378
  console.log("[EXULU] Creating agentic search tool", contexts?.length, model);
6136
- if (contexts?.length && model) {
6137
- const agenticSearchTool = createAgenticRetrievalTool({
6138
- contexts: contexts.filter((context) => context.id !== agent?.memory),
6139
- // memory is searched by the memory phase, not as a KB
6140
- memoryContext: agent?.memory ? contexts.find((c) => c.id === agent.memory) : void 0,
6141
- user,
6142
- role: user?.role?.id,
6143
- model,
6144
- preselected: sessionItems,
6145
- memoryItems
6146
- });
6147
- if (agenticSearchTool) {
6148
- const index = currentTools.findIndex((tool3) => tool3.id === "agentic_context_search");
6149
- if (index !== -1) {
6379
+ if (contexts?.length && model && !disabled.has("agentic_context_search")) {
6380
+ const index = currentTools.findIndex((tool3) => tool3.id === "agentic_context_search");
6381
+ const memoryContext = agent?.memory ? contexts.find((c) => c.id === agent.memory) : void 0;
6382
+ if (index !== -1) {
6383
+ const agenticSearchTool = createAgenticRetrievalTool({
6384
+ contexts: contexts.filter((context) => context.id !== agent?.memory),
6385
+ // memory is searched by the memory phase, not as a KB
6386
+ memoryContext,
6387
+ user,
6388
+ role: user?.role?.id,
6389
+ model,
6390
+ preselected: sessionItems,
6391
+ memoryItems,
6392
+ projectScope
6393
+ });
6394
+ if (agenticSearchTool) {
6150
6395
  currentTools[index] = {
6151
6396
  ...currentTools[index],
6152
6397
  // important to keep the original tool config
6153
6398
  ...agenticSearchTool
6154
6399
  };
6155
6400
  }
6401
+ } else if (projectScope) {
6402
+ const projectContextIds = new Set(
6403
+ projectScope.items.map((gid) => {
6404
+ const i = gid.indexOf("/");
6405
+ return i === -1 ? gid : gid.slice(0, i);
6406
+ })
6407
+ );
6408
+ const scopedContexts = contexts.filter(
6409
+ (c) => projectContextIds.has(c.id) && c.id !== agent?.memory
6410
+ );
6411
+ if (scopedContexts.length > 0) {
6412
+ const projectSearchTool = createAgenticRetrievalTool({
6413
+ contexts: scopedContexts,
6414
+ memoryContext,
6415
+ user,
6416
+ role: user?.role?.id,
6417
+ model,
6418
+ preselected: [...sessionItems ?? [], ...projectScope.items],
6419
+ memoryItems,
6420
+ projectScope
6421
+ });
6422
+ if (projectSearchTool) {
6423
+ currentTools.push(projectSearchTool);
6424
+ }
6425
+ }
6156
6426
  }
6157
6427
  } else {
6158
6428
  const agenticSearchTool = currentTools.find((tool3) => tool3.id === "agentic_context_search");
@@ -6180,7 +6450,7 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
6180
6450
  if (typeof result?.content === "string") {
6181
6451
  return {
6182
6452
  ...result,
6183
- content: truncateToolOutput(result.content, agent?.maxContextLength, "readFile", 0.05)
6453
+ content: truncateToolOutput(result.content, budget.contextWindow, "readFile", 0.05, toolOutputCharLimit)
6184
6454
  };
6185
6455
  }
6186
6456
  return result;
@@ -6197,10 +6467,10 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
6197
6467
  return {
6198
6468
  ...result,
6199
6469
  ...typeof result?.stdout === "string" && {
6200
- stdout: truncateToolOutput(result.stdout, agent?.maxContextLength, "bash", 0.1)
6470
+ stdout: truncateToolOutput(result.stdout, budget.contextWindow, "bash", 0.1, toolOutputCharLimit)
6201
6471
  },
6202
6472
  ...typeof result?.stderr === "string" && {
6203
- stderr: truncateToolOutput(result.stderr, agent?.maxContextLength, "bash stderr", 0.4)
6473
+ stderr: truncateToolOutput(result.stderr, budget.contextWindow, "bash stderr", 0.4, toolOutputCharLimit)
6204
6474
  }
6205
6475
  };
6206
6476
  }
@@ -6336,16 +6606,30 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
6336
6606
  user: user?.id,
6337
6607
  role: user?.role?.id
6338
6608
  });
6609
+ const guardCtx = {
6610
+ toolName: cur.name,
6611
+ contextWindow,
6612
+ sessionID,
6613
+ user,
6614
+ exuluConfig
6615
+ };
6616
+ const offloadExempt = OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS.has(cur.id);
6339
6617
  if (response && typeof response === "object" && Symbol.asyncIterator in response) {
6340
6618
  let lastValue;
6341
6619
  for await (const value of response) {
6342
6620
  yield value;
6343
6621
  lastValue = value;
6344
6622
  }
6345
- return lastValue;
6623
+ if (offloadExempt) return lastValue;
6624
+ const guarded = await guardToolOutput(lastValue, guardCtx);
6625
+ if (guarded !== lastValue) {
6626
+ yield guarded;
6627
+ }
6628
+ return guarded;
6346
6629
  } else {
6347
- yield response;
6348
- return response;
6630
+ const guarded = offloadExempt ? response : await guardToolOutput(response, guardCtx);
6631
+ yield guarded;
6632
+ return guarded;
6349
6633
  }
6350
6634
  }
6351
6635
  }
@@ -6403,10 +6687,20 @@ export {
6403
6687
  OAUTH_CALLBACK_PATH,
6404
6688
  decryptOauthState,
6405
6689
  exchangeCodeForTokens,
6406
- createProjectItemsRetrievalTool,
6407
6690
  sanitizeToolName,
6408
6691
  reportSystemDependencies,
6409
6692
  downloadKeyIntoSandbox,
6693
+ truncateToolOutput,
6694
+ DEFAULT_CONTEXT_WINDOW,
6695
+ deriveContextBudget,
6696
+ estimateTokens,
6697
+ estimateMessageTokens,
6698
+ sliceHistoryAtCheckpoint,
6699
+ contextOccupancy,
6700
+ COMPACTION_INSUFFICIENT,
6701
+ ContextCompactionRequiredError,
6702
+ mapStreamErrorMessage,
6703
+ guardExtractedFileText,
6410
6704
  hydrateVariables,
6411
6705
  convertExuluToolsToAiSdkTools,
6412
6706
  ExuluTool,