@superatomai/sdk-node 0.0.1-s → 0.0.2-dsp

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.mjs CHANGED
@@ -326,6 +326,24 @@ var ComponentListResponseMessageSchema = z3.object({
326
326
  type: z3.literal("COMPONENT_LIST_RES"),
327
327
  payload: ComponentListResponsePayloadSchema
328
328
  });
329
+ var WorkflowDescriptorSchema = z3.object({
330
+ id: z3.string(),
331
+ name: z3.string(),
332
+ description: z3.string(),
333
+ whenToUse: z3.string(),
334
+ propsSchema: z3.record(z3.string()),
335
+ defaultProps: z3.record(z3.unknown()).optional()
336
+ });
337
+ var WorkflowsSchema = z3.array(WorkflowDescriptorSchema);
338
+ var WorkflowListResponsePayloadSchema = z3.object({
339
+ workflows: z3.array(WorkflowDescriptorSchema)
340
+ });
341
+ var WorkflowListResponseMessageSchema = z3.object({
342
+ id: z3.string(),
343
+ from: MessageParticipantSchema,
344
+ type: z3.literal("WORKFLOW_LIST_RES"),
345
+ payload: WorkflowListResponsePayloadSchema
346
+ });
329
347
  var OutputFieldSchema = z3.object({
330
348
  name: z3.string(),
331
349
  // Field name (column name in the result)
@@ -349,8 +367,10 @@ var ToolSchema = z3.object({
349
367
  fullSchema: z3.string().optional(),
350
368
  params: z3.record(z3.string()),
351
369
  fn: z3.function().args(z3.any()).returns(z3.any()),
352
- outputSchema: OutputSchema.optional()
370
+ outputSchema: OutputSchema.optional(),
353
371
  // Optional: describes the data structure returned by this tool
372
+ /** Cache policy. `false` = never cache (live data, write ops). Mirrors HTTP `Cache-Control: no-store`. */
373
+ cache: z3.union([z3.literal(false), z3.object({ ttlMs: z3.number().optional() })]).optional()
354
374
  });
355
375
  var UserQueryFiltersSchema = z3.object({
356
376
  username: z3.string().optional(),
@@ -1465,7 +1485,7 @@ var QueryCache = class {
1465
1485
  this.cache = /* @__PURE__ */ new Map();
1466
1486
  this.ttlMs = 10 * 60 * 1e3;
1467
1487
  // Default: 10 minutes
1468
- this.maxCacheSize = 500;
1488
+ this.maxCacheSize = 5e3;
1469
1489
  // Max data cache entries
1470
1490
  this.cleanupInterval = null;
1471
1491
  // Encryption for queryId tokens
@@ -1492,9 +1512,13 @@ var QueryCache = class {
1492
1512
  return this.ttlMs / 60 / 1e3;
1493
1513
  }
1494
1514
  /**
1495
- * Store query result in data cache
1515
+ * Store query result in data cache.
1516
+ * If the key already exists, it's removed first so the re-insert places it
1517
+ * at the back of the iteration order (LRU). Eviction only fires when adding
1518
+ * a genuinely new key past the size limit.
1496
1519
  */
1497
1520
  set(query, data) {
1521
+ this.cache.delete(query);
1498
1522
  if (this.cache.size >= this.maxCacheSize) {
1499
1523
  const oldestKey = this.cache.keys().next().value;
1500
1524
  if (oldestKey) this.cache.delete(oldestKey);
@@ -1507,7 +1531,9 @@ var QueryCache = class {
1507
1531
  logger.debug(`[QueryCache] Stored result for query (${query.substring(0, 50)}...)`);
1508
1532
  }
1509
1533
  /**
1510
- * Get cached result if exists and not expired
1534
+ * Get cached result if exists and not expired.
1535
+ * On hit, re-inserts the entry so it moves to the back of the Map's
1536
+ * iteration order — turning FIFO eviction into true LRU.
1511
1537
  */
1512
1538
  get(query) {
1513
1539
  const entry = this.cache.get(query);
@@ -1516,6 +1542,8 @@ var QueryCache = class {
1516
1542
  this.cache.delete(query);
1517
1543
  return null;
1518
1544
  }
1545
+ this.cache.delete(query);
1546
+ this.cache.set(query, entry);
1519
1547
  logger.info(`[QueryCache] Cache HIT for query (${query.substring(0, 50)}...)`);
1520
1548
  return entry.data;
1521
1549
  }
@@ -1657,6 +1685,21 @@ var QueryCache = class {
1657
1685
  };
1658
1686
  var queryCache = new QueryCache();
1659
1687
 
1688
+ // src/utils/surrogate.ts
1689
+ var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
1690
+ function stripLoneSurrogates(value) {
1691
+ if (typeof value !== "string") return value;
1692
+ if (!/[\uD800-\uDFFF]/.test(value)) return value;
1693
+ return value.replace(LONE_SURROGATE_RE, "\uFFFD");
1694
+ }
1695
+ function safeTruncate(text, maxUnits) {
1696
+ if (typeof text !== "string" || text.length <= maxUnits || maxUnits < 0) return text;
1697
+ let end = maxUnits;
1698
+ const lastCode = text.charCodeAt(end - 1);
1699
+ if (lastCode >= 55296 && lastCode <= 56319) end -= 1;
1700
+ return text.slice(0, end);
1701
+ }
1702
+
1660
1703
  // src/userResponse/llm-result-truncator.ts
1661
1704
  var DEFAULT_MAX_ROWS = 10;
1662
1705
  var DEFAULT_MAX_CHARS_PER_FIELD = 500;
@@ -1699,12 +1742,12 @@ function isDateString(value) {
1699
1742
  }
1700
1743
  function truncateTextField(value, maxLength) {
1701
1744
  if (value.length <= maxLength) {
1702
- return { text: value, wasTruncated: false };
1745
+ return { text: stripLoneSurrogates(value), wasTruncated: false };
1703
1746
  }
1704
- const truncated = value.substring(0, maxLength);
1705
- const remaining = value.length - maxLength;
1747
+ const truncated = safeTruncate(value, maxLength);
1748
+ const remaining = value.length - truncated.length;
1706
1749
  return {
1707
- text: `${truncated}... (${remaining} more chars)`,
1750
+ text: `${stripLoneSurrogates(truncated)}... (${remaining} more chars)`,
1708
1751
  wasTruncated: true
1709
1752
  };
1710
1753
  }
@@ -2002,6 +2045,21 @@ function formatResultAsString(formattedResult) {
2002
2045
  return JSON.stringify(formattedResult, null, 2);
2003
2046
  }
2004
2047
 
2048
+ // src/utils/cache-key.ts
2049
+ function stableStringify(value) {
2050
+ if (value === null || typeof value !== "object") {
2051
+ return JSON.stringify(value);
2052
+ }
2053
+ if (Array.isArray(value)) {
2054
+ return "[" + value.map(stableStringify).join(",") + "]";
2055
+ }
2056
+ const keys = Object.keys(value).sort();
2057
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}";
2058
+ }
2059
+ function buildDirectToolCacheKey(toolId, parameters) {
2060
+ return `et-direct:${toolId}:${stableStringify(parameters || {})}`;
2061
+ }
2062
+
2005
2063
  // src/handlers/data-request.ts
2006
2064
  function getQueryCacheKey(query) {
2007
2065
  if (typeof query === "string") {
@@ -2015,7 +2073,7 @@ function getQueryCacheKey(query) {
2015
2073
  }
2016
2074
  return "";
2017
2075
  }
2018
- function getCacheKey(collection, op, params) {
2076
+ function getCacheKey(collection, op, params, tools) {
2019
2077
  if (collection === "database" && op === "execute" && params?.sql) {
2020
2078
  return getQueryCacheKey(params.sql);
2021
2079
  }
@@ -2025,6 +2083,13 @@ function getCacheKey(collection, op, params) {
2025
2083
  const paramsKey = params.params ? JSON.stringify(params.params) : "";
2026
2084
  return sqlKey ? `et:${toolId}:${sqlKey}:${paramsKey}` : "";
2027
2085
  }
2086
+ if (collection === "external-tools" && op === "execute" && params?.toolId && !params?.sql) {
2087
+ const tool = tools?.find((t) => t.id === params.toolId);
2088
+ if (tool?.cache === false) return "";
2089
+ const { toolId, toolName, ...rest } = params;
2090
+ const paramsKey = stableStringify(rest);
2091
+ return `et-direct:${toolId}:${paramsKey}`;
2092
+ }
2028
2093
  if (collection === "external-tools" && op === "executeByQueryId" && params?.queryId) {
2029
2094
  const toolId = params.toolId || "";
2030
2095
  const filterKey = params.filterParams ? JSON.stringify(params.filterParams) : "";
@@ -2033,7 +2098,7 @@ function getCacheKey(collection, op, params) {
2033
2098
  }
2034
2099
  return "";
2035
2100
  }
2036
- async function handleDataRequest(data, collections, sendMessage) {
2101
+ async function handleDataRequest(data, collections, sendMessage, tools) {
2037
2102
  let requestId;
2038
2103
  let collection;
2039
2104
  let op;
@@ -2059,7 +2124,7 @@ async function handleDataRequest(data, collections, sendMessage) {
2059
2124
  const startTime = performance.now();
2060
2125
  let result;
2061
2126
  let fromCache = false;
2062
- const cacheKey = getCacheKey(collection, op, params);
2127
+ const cacheKey = getCacheKey(collection, op, params, tools);
2063
2128
  if (cacheKey) {
2064
2129
  const cachedResult = queryCache.get(cacheKey);
2065
2130
  if (cachedResult !== null) {
@@ -3724,6 +3789,12 @@ You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
3724
3789
  ---
3725
3790
 
3726
3791
  ## CONTEXT
3792
+
3793
+ ### Global Knowledge Base
3794
+ {{GLOBAL_KNOWLEDGE_BASE}}
3795
+
3796
+ ### Knowledge Base Context
3797
+ {{KNOWLEDGE_BASE_CONTEXT}}
3727
3798
  `,
3728
3799
  user: `{{USER_PROMPT}}`
3729
3800
  },
@@ -4276,13 +4347,11 @@ var PromptLoader = class {
4276
4347
  * @returns Processed string
4277
4348
  */
4278
4349
  replaceVariables(template, variables) {
4279
- let content = template;
4280
- for (const [key, value] of Object.entries(variables)) {
4281
- const pattern = new RegExp(`{{${key}}}`, "g");
4282
- const replacementValue = typeof value === "string" ? value : JSON.stringify(value);
4283
- content = content.replace(pattern, replacementValue);
4284
- }
4285
- return content;
4350
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
4351
+ if (!(key in variables)) return match;
4352
+ const value = variables[key];
4353
+ return typeof value === "string" ? value : JSON.stringify(value);
4354
+ });
4286
4355
  }
4287
4356
  /**
4288
4357
  * Load both system and user prompts from cache and replace variables
@@ -4305,8 +4374,6 @@ var PromptLoader = class {
4305
4374
  const [staticPart, contextPart] = template.system.split(contextMarker);
4306
4375
  const processedStatic = this.replaceVariables(staticPart, variables);
4307
4376
  const processedContext = this.replaceVariables(contextMarker + contextPart, variables);
4308
- const staticLength = processedStatic.length;
4309
- const contextLength = processedContext.length;
4310
4377
  return {
4311
4378
  system: [
4312
4379
  {
@@ -5236,9 +5303,36 @@ var searchConversationsWithReranking = async (options) => {
5236
5303
  return null;
5237
5304
  }
5238
5305
  };
5306
+ var findExactMatch = async ({
5307
+ userPrompt,
5308
+ collections,
5309
+ userId
5310
+ }) => {
5311
+ try {
5312
+ if (!collections || !collections["conversation-history"] || !collections["conversation-history"]["exactMatch"]) {
5313
+ logger.info("[ConversationSearch] conversation-history.exactMatch collection not registered, skipping");
5314
+ return null;
5315
+ }
5316
+ const result = await collections["conversation-history"]["exactMatch"]({
5317
+ userPrompt,
5318
+ userId
5319
+ });
5320
+ if (!result || !result.uiBlock) {
5321
+ logger.info("[ConversationSearch] No exact match found");
5322
+ return null;
5323
+ }
5324
+ logger.info(`[ConversationSearch] \u2713 Exact prompt match found for "${userPrompt.substring(0, 50)}..."`);
5325
+ return result;
5326
+ } catch (error) {
5327
+ const errorMsg = error instanceof Error ? error.message : String(error);
5328
+ logger.warn(`[ConversationSearch] Error in exact match lookup: ${errorMsg}`);
5329
+ return null;
5330
+ }
5331
+ };
5239
5332
  var ConversationSearch = {
5240
5333
  searchConversations,
5241
- searchConversationsWithReranking
5334
+ searchConversationsWithReranking,
5335
+ findExactMatch
5242
5336
  };
5243
5337
  var conversation_search_default = ConversationSearch;
5244
5338
 
@@ -5256,16 +5350,20 @@ var MAX_TOKENS_CLASSIFICATION = 1500;
5256
5350
  var MAX_TOKENS_ADAPTATION = 8192;
5257
5351
  var MAX_TOKENS_TEXT_RESPONSE = 4e3;
5258
5352
  var MAX_TOKENS_NEXT_QUESTIONS = 1200;
5259
- var DEFAULT_MAX_ROWS_FOR_LLM = 10;
5353
+ var MAX_ROWS_FETCHED = 50;
5354
+ var MAX_ROWS_TO_LLM = 10;
5355
+ var LLM_SAMPLE_ROWS = 10;
5356
+ var MAX_ROWS_RENDERED = 250;
5260
5357
  var DEFAULT_MAX_CHARS_PER_FIELD2 = 500;
5261
- var STREAM_PREVIEW_MAX_ROWS = 10;
5262
5358
  var STREAM_PREVIEW_MAX_CHARS = 200;
5263
- var TOOL_TRACKING_MAX_ROWS = 5;
5264
5359
  var TOOL_TRACKING_MAX_CHARS = 200;
5265
- var TOOL_TRACKING_SAMPLE_ROWS = 3;
5360
+ var DEFAULT_MAX_ROWS_FOR_LLM = LLM_SAMPLE_ROWS;
5361
+ var STREAM_PREVIEW_MAX_ROWS = LLM_SAMPLE_ROWS;
5362
+ var TOOL_TRACKING_SAMPLE_ROWS = LLM_SAMPLE_ROWS;
5363
+ var TOOL_TRACKING_MAX_ROWS = 5;
5266
5364
  var DEFAULT_QUERY_LIMIT = 24;
5267
- var MAX_AGENT_QUERY_LIMIT = 10;
5268
- var MAX_COMPONENT_QUERY_LIMIT = 100;
5365
+ var MAX_AGENT_QUERY_LIMIT = LLM_SAMPLE_ROWS;
5366
+ var MAX_COMPONENT_QUERY_LIMIT = MAX_ROWS_RENDERED;
5269
5367
  var EXACT_MATCH_SIMILARITY_THRESHOLD = 0.99;
5270
5368
  var DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD = 0.8;
5271
5369
  var MAX_TOOL_CALLING_ITERATIONS = 20;
@@ -5762,6 +5860,7 @@ var LLM = class {
5762
5860
  /* Get a complete text response from an LLM (Anthropic or Groq) */
5763
5861
  static async text(messages, options = {}) {
5764
5862
  const [provider, modelName] = this._parseModel(options.model);
5863
+ messages = this._sanitizeMessages(messages);
5765
5864
  if (provider === "anthropic") {
5766
5865
  return this._anthropicText(messages, modelName, options);
5767
5866
  } else if (provider === "groq") {
@@ -5777,6 +5876,7 @@ var LLM = class {
5777
5876
  /* Stream response from an LLM (Anthropic or Groq) */
5778
5877
  static async stream(messages, options = {}, json) {
5779
5878
  const [provider, modelName] = this._parseModel(options.model);
5879
+ messages = this._sanitizeMessages(messages);
5780
5880
  if (provider === "anthropic") {
5781
5881
  return this._anthropicStream(messages, modelName, options, json);
5782
5882
  } else if (provider === "groq") {
@@ -5792,6 +5892,7 @@ var LLM = class {
5792
5892
  /* Stream response with tool calling support (Anthropic and Gemini) */
5793
5893
  static async streamWithTools(messages, tools, toolHandler, options = {}, maxIterations = 3) {
5794
5894
  const [provider, modelName] = this._parseModel(options.model);
5895
+ messages = this._sanitizeMessages(messages);
5795
5896
  if (provider === "anthropic") {
5796
5897
  return this._anthropicStreamWithTools(messages, tools, toolHandler, modelName, options, maxIterations);
5797
5898
  } else if (provider === "gemini") {
@@ -5817,6 +5918,26 @@ var LLM = class {
5817
5918
  }
5818
5919
  return sys;
5819
5920
  }
5921
+ /**
5922
+ * Strip unpaired UTF-16 surrogates from every text field of a message set.
5923
+ *
5924
+ * A lone surrogate (from mid-pair string slicing or corrupt source data)
5925
+ * serializes to a bare `\udXXX` escape that strict JSON parsers — including
5926
+ * the one on Anthropic's API — reject with "no low surrogate in string",
5927
+ * failing the whole request. Sanitizing here, at the single boundary every
5928
+ * provider call flows through, guarantees no request can carry one.
5929
+ */
5930
+ static _sanitizeMessages(messages) {
5931
+ const sys = typeof messages.sys === "string" ? stripLoneSurrogates(messages.sys) : messages.sys.map(
5932
+ (block) => block?.type === "text" && typeof block.text === "string" ? { ...block, text: stripLoneSurrogates(block.text) } : block
5933
+ );
5934
+ return {
5935
+ ...messages,
5936
+ sys,
5937
+ user: stripLoneSurrogates(messages.user),
5938
+ ...messages.prefill !== void 0 && { prefill: stripLoneSurrogates(messages.prefill) }
5939
+ };
5940
+ }
5820
5941
  /**
5821
5942
  * Log cache usage metrics from Anthropic API response
5822
5943
  * Shows cache hits, costs, and savings
@@ -6196,12 +6317,14 @@ var LLM = class {
6196
6317
  let resultContent = typeof result === "string" ? result : JSON.stringify(result);
6197
6318
  const MAX_RESULT_LENGTH = 5e4;
6198
6319
  if (resultContent.length > MAX_RESULT_LENGTH) {
6199
- resultContent = resultContent.substring(0, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + resultContent.length + " total]";
6320
+ resultContent = safeTruncate(resultContent, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + resultContent.length + " total]";
6200
6321
  }
6201
6322
  return {
6202
6323
  type: "tool_result",
6203
6324
  tool_use_id: toolUse.id,
6204
- content: resultContent
6325
+ // Final safety net: tool results carry source data and are built
6326
+ // mid-loop (after entry-point sanitize), so strip lone surrogates here.
6327
+ content: stripLoneSurrogates(resultContent)
6205
6328
  };
6206
6329
  } catch (error) {
6207
6330
  return {
@@ -6716,11 +6839,12 @@ var LLM = class {
6716
6839
  let resultContent = typeof result2 === "string" ? result2 : JSON.stringify(result2);
6717
6840
  const MAX_RESULT_LENGTH = 5e4;
6718
6841
  if (resultContent.length > MAX_RESULT_LENGTH) {
6719
- resultContent = resultContent.substring(0, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + resultContent.length + " total]";
6842
+ resultContent = safeTruncate(resultContent, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + resultContent.length + " total]";
6720
6843
  }
6721
6844
  return {
6722
6845
  name: fc.name,
6723
- response: { result: resultContent }
6846
+ // Final safety net: strip lone surrogates from source-data results.
6847
+ response: { result: stripLoneSurrogates(resultContent) }
6724
6848
  };
6725
6849
  } catch (error) {
6726
6850
  return {
@@ -6993,12 +7117,12 @@ var LLM = class {
6993
7117
  result = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult);
6994
7118
  const MAX_RESULT_LENGTH = 5e4;
6995
7119
  if (result.length > MAX_RESULT_LENGTH) {
6996
- result = result.substring(0, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + result.length + " total]";
7120
+ result = safeTruncate(result, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + result.length + " total]";
6997
7121
  }
6998
7122
  } catch (error) {
6999
7123
  result = JSON.stringify({ error: error instanceof Error ? error.message : String(error) });
7000
7124
  }
7001
- return { role: "tool", tool_call_id: tc.id, content: result };
7125
+ return { role: "tool", tool_call_id: tc.id, content: stripLoneSurrogates(result) };
7002
7126
  }));
7003
7127
  toolCallResults.forEach((r) => conversationMessages.push(r));
7004
7128
  }
@@ -7137,6 +7261,130 @@ function extractObjectText(obj) {
7137
7261
  return JSON.stringify(obj, null, 2);
7138
7262
  }
7139
7263
 
7264
+ // src/userResponse/agents/data-summary.ts
7265
+ var MAIN_AGENT_COMPLETE_ROWS = MAX_ROWS_TO_LLM;
7266
+ function summarizeRows(rows) {
7267
+ if (!rows.length) return { totalRows: 0, columns: {} };
7268
+ const MAX_CATEGORIES = 20;
7269
+ const keys = Object.keys(rows[0]);
7270
+ const columns = {};
7271
+ const fmtDate = (ms) => {
7272
+ const d = new Date(ms);
7273
+ const p = (n) => String(n).padStart(2, "0");
7274
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
7275
+ };
7276
+ for (const key of keys) {
7277
+ let nonNull = 0, zeros = 0, isNumeric = true, isDateLike = true, sum = 0;
7278
+ let min, max;
7279
+ let dMin, dMax;
7280
+ const counts = /* @__PURE__ */ new Map();
7281
+ let overflow = false;
7282
+ for (const row of rows) {
7283
+ const v = row[key];
7284
+ if (v === null || v === void 0 || v === "") continue;
7285
+ nonNull++;
7286
+ const num = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : NaN;
7287
+ if (!Number.isNaN(num) && typeof v !== "boolean") {
7288
+ if (num === 0) zeros++;
7289
+ min = min === void 0 ? num : Math.min(min, num);
7290
+ max = max === void 0 ? num : Math.max(max, num);
7291
+ sum += num;
7292
+ } else {
7293
+ isNumeric = false;
7294
+ }
7295
+ const t = v instanceof Date ? v.getTime() : typeof v === "string" ? Date.parse(v) : NaN;
7296
+ if (!Number.isNaN(t) && typeof v !== "number") {
7297
+ dMin = dMin === void 0 ? t : Math.min(dMin, t);
7298
+ dMax = dMax === void 0 ? t : Math.max(dMax, t);
7299
+ } else {
7300
+ isDateLike = false;
7301
+ }
7302
+ if (!overflow) {
7303
+ const sv = String(v);
7304
+ counts.set(sv, (counts.get(sv) || 0) + 1);
7305
+ if (counts.size > MAX_CATEGORIES * 5) overflow = true;
7306
+ }
7307
+ }
7308
+ const nulls = rows.length - nonNull;
7309
+ if (nonNull === 0) {
7310
+ columns[key] = { type: "empty", nulls };
7311
+ continue;
7312
+ }
7313
+ if (isNumeric) {
7314
+ columns[key] = {
7315
+ type: "number",
7316
+ nonNull,
7317
+ nulls,
7318
+ zeros,
7319
+ min,
7320
+ max,
7321
+ avg: Math.round(sum / nonNull * 100) / 100
7322
+ };
7323
+ } else if (isDateLike) {
7324
+ columns[key] = {
7325
+ type: "date",
7326
+ nonNull,
7327
+ min: fmtDate(dMin),
7328
+ max: fmtDate(dMax),
7329
+ distinct: overflow ? `${MAX_CATEGORIES * 5}+` : counts.size
7330
+ };
7331
+ } else if (!overflow && counts.size <= MAX_CATEGORIES) {
7332
+ const dist = {};
7333
+ for (const [k, c] of [...counts.entries()].sort((a, b) => b[1] - a[1])) dist[k] = c;
7334
+ columns[key] = { type: "category", distinct: counts.size, counts: dist };
7335
+ } else {
7336
+ columns[key] = {
7337
+ type: "text",
7338
+ distinct: overflow ? `${MAX_CATEGORIES * 5}+` : counts.size,
7339
+ examples: [...counts.keys()].slice(0, 3)
7340
+ };
7341
+ }
7342
+ }
7343
+ const dims = keys.filter((k) => columns[k]?.type === "category" && columns[k].distinct <= 6).sort((a, b) => columns[a].distinct - columns[b].distinct);
7344
+ const measures = keys.filter((k) => columns[k]?.type === "number");
7345
+ let groups;
7346
+ if (dims.length && measures.length) {
7347
+ const MAX_GROUPS = 30;
7348
+ let useDims = dims.slice(0, 2);
7349
+ const keyOf = (row) => useDims.map((d) => `${d}=${row[d]}`).join(" | ");
7350
+ if (new Set(rows.map(keyOf)).size > MAX_GROUPS) useDims = dims.slice(0, 1);
7351
+ const agg = /* @__PURE__ */ new Map();
7352
+ for (const row of rows) {
7353
+ const key = useDims.map((d) => `${d}=${row[d]}`).join(" | ");
7354
+ let g = agg.get(key);
7355
+ if (!g) {
7356
+ g = { count: 0, sums: {}, ns: {} };
7357
+ agg.set(key, g);
7358
+ }
7359
+ g.count++;
7360
+ for (const m of measures) {
7361
+ const v = row[m];
7362
+ if (typeof v === "number" && !Number.isNaN(v)) {
7363
+ g.sums[m] = (g.sums[m] || 0) + v;
7364
+ g.ns[m] = (g.ns[m] || 0) + 1;
7365
+ }
7366
+ }
7367
+ }
7368
+ if (agg.size <= MAX_GROUPS) {
7369
+ const isCountLike = (name) => /count|\bobs\b|observation|qty|quantity|\bnum|total|evidential|volume|units/i.test(name);
7370
+ groups = {
7371
+ by: useDims,
7372
+ rows: [...agg.entries()].map(([key, g]) => {
7373
+ const avg = {};
7374
+ const sum = {};
7375
+ for (const m of measures) {
7376
+ if (!g.ns[m]) continue;
7377
+ avg[m] = Math.round(g.sums[m] / g.ns[m] * 100) / 100;
7378
+ if (isCountLike(m)) sum[m] = Math.round(g.sums[m] * 100) / 100;
7379
+ }
7380
+ return Object.keys(sum).length ? { key, count: g.count, avg, sum } : { key, count: g.count, avg };
7381
+ })
7382
+ };
7383
+ }
7384
+ }
7385
+ return groups ? { totalRows: rows.length, columns, groups } : { totalRows: rows.length, columns };
7386
+ }
7387
+
7140
7388
  // src/userResponse/agents/agent-prompt-builder.ts
7141
7389
  function buildSourceSummaries(externalTools) {
7142
7390
  return externalTools.map((tool) => {
@@ -7356,8 +7604,15 @@ ${result}`;
7356
7604
  await streamDelay();
7357
7605
  }
7358
7606
  const cappedInput = { ...toolInput };
7359
- if (cappedInput.limit === void 0 || cappedInput.limit > this.config.maxRowsPerSource) {
7360
- cappedInput.limit = this.config.maxRowsPerSource;
7607
+ if (cappedInput.limit === void 0 || cappedInput.limit > this.config.maxRowsFetched) {
7608
+ cappedInput.limit = this.config.maxRowsFetched;
7609
+ }
7610
+ const _st = this.extractSourceType();
7611
+ if (typeof cappedInput.sql === "string") {
7612
+ cappedInput.sql = ensureQueryLimit(cappedInput.sql, this.config.maxRowsFetched, this.config.maxRowsFetched, _st);
7613
+ }
7614
+ if (typeof cappedInput.query === "string") {
7615
+ cappedInput.query = ensureQueryLimit(cappedInput.query, this.config.maxRowsFetched, this.config.maxRowsFetched, _st);
7361
7616
  }
7362
7617
  queryExecuted = cappedInput.sql || cappedInput.query || JSON.stringify(cappedInput);
7363
7618
  if (this.streamBuffer.hasCallback() && queryExecuted) {
@@ -7408,8 +7663,11 @@ Analyze the error and try again with a corrected query.`;
7408
7663
  `);
7409
7664
  if (resultData.length > 0) {
7410
7665
  try {
7411
- const previewData = resultData.slice(0, this.config.maxRowsPerSource);
7412
- this.streamBuffer.write(`<DataTable>${JSON.stringify(previewData)}</DataTable>
7666
+ const preview = formatQueryResultForLLM(resultData, {
7667
+ maxRows: STREAM_PREVIEW_MAX_ROWS,
7668
+ maxCharsPerField: STREAM_PREVIEW_MAX_CHARS
7669
+ });
7670
+ this.streamBuffer.write(`<DataTable>${JSON.stringify(preview.data)}</DataTable>
7413
7671
 
7414
7672
  `);
7415
7673
  } catch {
@@ -7432,7 +7690,12 @@ Analyze the error and try again with a corrected query.`;
7432
7690
  _totalRecords: totalRowsMatched,
7433
7691
  _recordsShown: resultData.length,
7434
7692
  _metadata: result.metadata,
7435
- _sampleData: resultData.slice(0, 3)
7693
+ _sampleData: resultData.slice(0, TOOL_TRACKING_SAMPLE_ROWS),
7694
+ // For the main agent: a bounded summary over the FULL fetched
7695
+ // result (complete structure regardless of size) + a complete
7696
+ // slice for small results (so lookups arrive whole, not biased).
7697
+ _summary: summarizeRows(resultData),
7698
+ _mainAgentRows: resultData.slice(0, MAIN_AGENT_COMPLETE_ROWS)
7436
7699
  },
7437
7700
  outputSchema: this.tool.outputSchema,
7438
7701
  sourceSchema: this.tool.description,
@@ -7440,7 +7703,7 @@ Analyze the error and try again with a corrected query.`;
7440
7703
  };
7441
7704
  allExecutedTools.push(executedTool);
7442
7705
  const formatted = typeof formattedResult === "string" ? formattedResult : JSON.stringify(formattedResult);
7443
- const followUpNote = successfulQueries < 2 ? "You may make ONE follow-up query if this data is incomplete for the intent. Otherwise, STOP." : "Maximum queries reached. Use the data you have.";
7706
+ const followUpNote = successfulQueries < 2 ? "STOP now and return this data \u2014 it satisfies the intent in the common case. Make a second query ONLY if a column or row the intent explicitly requires is verifiably MISSING from these results (not merely to widen, re-sort, or double-check). A redundant follow-up wastes a full round-trip." : "Maximum queries reached. Use the data you have.";
7444
7707
  return `\u2705 Query executed successfully. ${resultData.length} rows returned (${totalRowsMatched} total matched). ${followUpNote}
7445
7708
 
7446
7709
  ${formatted}`;
@@ -7502,7 +7765,13 @@ Analyze the error and try again with a corrected query.`;
7502
7765
  sourceId: this.tool.id,
7503
7766
  sourceName: this.tool.name,
7504
7767
  success: true,
7505
- data: resultData,
7768
+ // Don't retain the full fetched result — only the bounded slice the
7769
+ // main agent can actually use. The complete-structure summary and the
7770
+ // complete-small rows already live on each executedTool (`_summary` /
7771
+ // `_mainAgentRows`); keeping the full resultData here would hold up to
7772
+ // MAX_ROWS_FETCHED rows × every query in memory for the whole request
7773
+ // for no functional gain.
7774
+ data: resultData.slice(0, MAIN_AGENT_COMPLETE_ROWS),
7506
7775
  metadata: {
7507
7776
  totalRowsMatched,
7508
7777
  rowsReturned: resultData.length,
@@ -7553,13 +7822,13 @@ Analyze the error and try again with a corrected query.`;
7553
7822
  const fullSchema = this.options.preResolvedSchema || this.tool.fullSchema || this.tool.description || "No schema available";
7554
7823
  const databaseRules = await promptLoader.loadDatabaseRulesForType(sourceType);
7555
7824
  const rowLimitSyntax = {
7556
- "mssql": `Use SELECT TOP ${this.config.maxRowsPerSource} in every SELECT statement`,
7557
- "postgres": `Add LIMIT ${this.config.maxRowsPerSource} at the end of every query`,
7558
- "mysql": `Add LIMIT ${this.config.maxRowsPerSource} at the end of every query`,
7559
- "excel": `Add LIMIT ${this.config.maxRowsPerSource} at the end of every query`,
7560
- "csv": `Add LIMIT ${this.config.maxRowsPerSource} at the end of every query`
7825
+ "mssql": `Use SELECT TOP ${this.config.maxRowsFetched} in every SELECT statement`,
7826
+ "postgres": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`,
7827
+ "mysql": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`,
7828
+ "excel": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`,
7829
+ "csv": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`
7561
7830
  };
7562
- const rowLimitInstruction = rowLimitSyntax[sourceType] || `Limit results to ${this.config.maxRowsPerSource} rows`;
7831
+ const rowLimitInstruction = rowLimitSyntax[sourceType] || `Limit results to ${this.config.maxRowsFetched} rows`;
7563
7832
  const hasSchemaSearch = !this.options.skipSchemaSearch && !!this.tool.schemaSearchFn;
7564
7833
  const schemaTier = this.tool.schemaTier || "";
7565
7834
  let schemaSearchInstructions = "";
@@ -7598,8 +7867,13 @@ Even if a table appears in the detailed schema above, search_schema returns samp
7598
7867
  FULL_SCHEMA: fullSchema,
7599
7868
  DATABASE_RULES: databaseRules,
7600
7869
  ROW_LIMIT_SYNTAX: rowLimitInstruction,
7870
+ // NOTE: MAX_ROWS here governs the SQL row cap (the {{MAX_ROWS}} in the
7871
+ // "limit results to N rows" line), so it MUST match the fetch cap that
7872
+ // ROW_LIMIT_SYNTAX states — not the smaller LLM-show cap — or the prompt
7873
+ // contradicts itself ("max 10 rows" + "LIMIT 50") and the LLM picks its
7874
+ // own number. See maxRowsFetched override below.
7601
7875
  SCHEMA_SEARCH_INSTRUCTIONS: schemaSearchInstructions,
7602
- MAX_ROWS: String(this.config.maxRowsPerSource),
7876
+ MAX_ROWS: String(this.config.maxRowsFetched),
7603
7877
  AGGREGATION_MODE: aggregation,
7604
7878
  GLOBAL_KNOWLEDGE_BASE: this.config.globalKnowledgeBase || "No global knowledge base available.",
7605
7879
  CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
@@ -7721,13 +7995,27 @@ Even if a table appears in the detailed schema above, search_schema returns samp
7721
7995
  // src/userResponse/scripts/script-runner.ts
7722
7996
  import { spawn } from "child_process";
7723
7997
  import * as path5 from "path";
7998
+ import * as os from "os";
7724
7999
 
7725
8000
  // src/userResponse/scripts/script-ipc.ts
7726
8001
  function encodeMessage(msg) {
7727
8002
  return JSON.stringify(msg) + "\n";
7728
8003
  }
8004
+ var LineOverflowError = class extends Error {
8005
+ constructor(buffered, limit) {
8006
+ super(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) \u2014 aborting to avoid unbounded memory.`);
8007
+ this.buffered = buffered;
8008
+ this.limit = limit;
8009
+ this.name = "LineOverflowError";
8010
+ }
8011
+ };
8012
+ var DEFAULT_MAX_IPC_BYTES = (() => {
8013
+ const envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);
8014
+ return Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;
8015
+ })();
7729
8016
  var LineSplitter = class {
7730
- constructor() {
8017
+ constructor(maxBytes = DEFAULT_MAX_IPC_BYTES) {
8018
+ this.maxBytes = maxBytes;
7731
8019
  this.buffer = "";
7732
8020
  }
7733
8021
  push(chunk) {
@@ -7739,6 +8027,11 @@ var LineSplitter = class {
7739
8027
  this.buffer = this.buffer.slice(idx + 1);
7740
8028
  if (line.length > 0) lines.push(line);
7741
8029
  }
8030
+ if (this.buffer.length > this.maxBytes) {
8031
+ const buffered = this.buffer.length;
8032
+ this.buffer = "";
8033
+ throw new LineOverflowError(buffered, this.maxBytes);
8034
+ }
7742
8035
  return lines;
7743
8036
  }
7744
8037
  /** Flush any remaining partial data (useful on stream close) */
@@ -7751,7 +8044,57 @@ var LineSplitter = class {
7751
8044
  };
7752
8045
 
7753
8046
  // src/userResponse/scripts/script-runner.ts
7754
- var SCRIPT_TIMEOUT_MS = 3e5;
8047
+ var DEFAULT_SCRIPT_TIMEOUT_MS = 6e4;
8048
+ var SCRIPT_TIMEOUT_MS = (() => {
8049
+ const envVal = Number(process.env.SCRIPT_TIMEOUT_MS);
8050
+ return Number.isFinite(envVal) && envVal > 0 ? envVal : DEFAULT_SCRIPT_TIMEOUT_MS;
8051
+ })();
8052
+ var MAX_CONCURRENT_SCRIPTS = (() => {
8053
+ const envVal = Number(process.env.SCRIPT_MAX_CONCURRENCY);
8054
+ if (Number.isFinite(envVal) && envVal > 0) return Math.floor(envVal);
8055
+ return Math.max(2, Math.min(8, (os.cpus()?.length || 4) - 1));
8056
+ })();
8057
+ var MAX_SCRIPT_QUEUE = (() => {
8058
+ const envVal = Number(process.env.SCRIPT_MAX_QUEUE);
8059
+ if (Number.isFinite(envVal) && envVal >= 0) return Math.floor(envVal);
8060
+ return 100;
8061
+ })();
8062
+ var ScriptCapacityError = class extends Error {
8063
+ constructor(message) {
8064
+ super(message);
8065
+ this.name = "ScriptCapacityError";
8066
+ }
8067
+ };
8068
+ var Semaphore = class {
8069
+ constructor(max, maxQueue) {
8070
+ this.max = max;
8071
+ this.maxQueue = maxQueue;
8072
+ this.active = 0;
8073
+ this.waiters = [];
8074
+ }
8075
+ async acquire() {
8076
+ if (this.active < this.max) {
8077
+ this.active++;
8078
+ return;
8079
+ }
8080
+ if (this.waiters.length >= this.maxQueue) {
8081
+ throw new ScriptCapacityError(
8082
+ `Script execution at capacity (${this.max} running, ${this.waiters.length} queued). Try again shortly.`
8083
+ );
8084
+ }
8085
+ return new Promise((resolve2) => this.waiters.push(resolve2));
8086
+ }
8087
+ release() {
8088
+ const next = this.waiters.shift();
8089
+ if (next) next();
8090
+ else this.active--;
8091
+ }
8092
+ get queued() {
8093
+ return this.waiters.length;
8094
+ }
8095
+ };
8096
+ var scriptSemaphore = new Semaphore(MAX_CONCURRENT_SCRIPTS, MAX_SCRIPT_QUEUE);
8097
+ logger.info(`[ScriptRunner] Concurrency cap: ${MAX_CONCURRENT_SCRIPTS} concurrent, queue ${MAX_SCRIPT_QUEUE}`);
7755
8098
  var tsxBinaryPath = null;
7756
8099
  function resolveTsxBinary() {
7757
8100
  if (tsxBinaryPath) return tsxBinaryPath;
@@ -7774,6 +8117,22 @@ async function runScript(recipe, scriptPath, params, options) {
7774
8117
  const resolvedParams = coerceParams(recipe, applyDefaults(recipe, params));
7775
8118
  const paramSchema = buildParamSchema(recipe);
7776
8119
  logger.info(`[ScriptRunner] Executing "${recipe.name}" (${recipe.id}) with params: ${JSON.stringify(resolvedParams)}`);
8120
+ try {
8121
+ if (scriptSemaphore.queued > 0) {
8122
+ logger.info(`[ScriptRunner] "${recipe.name}" queued (${scriptSemaphore.queued} ahead)`);
8123
+ }
8124
+ await scriptSemaphore.acquire();
8125
+ } catch (capErr) {
8126
+ const msg = capErr instanceof Error ? capErr.message : String(capErr);
8127
+ logger.warn(`[ScriptRunner] "${recipe.name}" shed: ${msg}`);
8128
+ return {
8129
+ success: false,
8130
+ data: [],
8131
+ executedQueries: [],
8132
+ error: msg,
8133
+ executionTimeMs: Date.now() - startedAt
8134
+ };
8135
+ }
7777
8136
  try {
7778
8137
  const result = await executeInSubprocess(
7779
8138
  scriptPath,
@@ -7815,6 +8174,8 @@ async function runScript(recipe, scriptPath, params, options) {
7815
8174
  error: msg,
7816
8175
  executionTimeMs: totalMs
7817
8176
  };
8177
+ } finally {
8178
+ scriptSemaphore.release();
7818
8179
  }
7819
8180
  }
7820
8181
  function executeInSubprocess(scriptPath, params, paramSchema, externalTools, streamBuffer, timeoutMs) {
@@ -7827,6 +8188,15 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
7827
8188
  [tsxBin, bootstrap, scriptPath],
7828
8189
  {
7829
8190
  stdio: ["pipe", "pipe", "pipe"],
8191
+ // `detached: true` makes the child a process-group LEADER. tsx 4.x
8192
+ // re-execs node to run the user script, so the script is a GRANDCHILD;
8193
+ // a plain `child.kill()` would hit only the tsx wrapper and orphan the
8194
+ // grandchild (a runaway/looping script that outlives the timeout — #4).
8195
+ // With its own group we can SIGKILL the whole tree via `-pid` in
8196
+ // cleanup(). We deliberately do NOT unref() — the parent still tracks
8197
+ // the child's exit and pipes normally. (POSIX; Windows falls back to
8198
+ // a direct kill in cleanup().)
8199
+ detached: process.platform !== "win32",
7830
8200
  env: {
7831
8201
  ...process.env,
7832
8202
  // Keep the child quiet about dotenv etc.
@@ -7834,22 +8204,23 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
7834
8204
  }
7835
8205
  }
7836
8206
  );
7837
- const toolsById = /* @__PURE__ */ new Map();
7838
- const toolsByName = /* @__PURE__ */ new Map();
7839
- for (const t of externalTools) {
7840
- toolsById.set(t.id, t);
7841
- toolsByName.set(t.name.toLowerCase(), t);
7842
- }
7843
8207
  let resolved = false;
7844
8208
  let executedQueries = [];
7845
8209
  let stderrBuffer = "";
7846
8210
  const cleanup = () => {
7847
- if (!child.killed) {
8211
+ if (child.killed) return;
8212
+ const pid = child.pid;
8213
+ if (pid && process.platform !== "win32") {
7848
8214
  try {
7849
- child.kill("SIGKILL");
8215
+ process.kill(-pid, "SIGKILL");
8216
+ return;
7850
8217
  } catch {
7851
8218
  }
7852
8219
  }
8220
+ try {
8221
+ child.kill("SIGKILL");
8222
+ } catch {
8223
+ }
7853
8224
  };
7854
8225
  const finish = (r) => {
7855
8226
  if (resolved) return;
@@ -7869,7 +8240,19 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
7869
8240
  const stdoutSplitter = new LineSplitter();
7870
8241
  child.stdout.setEncoding("utf-8");
7871
8242
  child.stdout.on("data", (chunk) => {
7872
- for (const line of stdoutSplitter.push(chunk)) {
8243
+ let lines;
8244
+ try {
8245
+ lines = stdoutSplitter.push(chunk);
8246
+ } catch (err) {
8247
+ finish({
8248
+ kind: "err",
8249
+ phase: "ipc",
8250
+ message: err instanceof Error ? err.message : String(err),
8251
+ executedQueries
8252
+ });
8253
+ return;
8254
+ }
8255
+ for (const line of lines) {
7873
8256
  let msg;
7874
8257
  try {
7875
8258
  msg = JSON.parse(line);
@@ -7913,9 +8296,6 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
7913
8296
  child.stdin.write(init);
7914
8297
  function handleChildMessage(msg) {
7915
8298
  switch (msg.type) {
7916
- case "query":
7917
- void handleQuery8(msg.id, msg.toolId, msg.sql);
7918
- return;
7919
8299
  case "stream":
7920
8300
  if (streamBuffer?.hasCallback()) {
7921
8301
  streamBuffer.write(msg.chunk);
@@ -7941,117 +8321,21 @@ ${msg.stack}` : ""),
7941
8321
  return;
7942
8322
  }
7943
8323
  }
7944
- async function handleQuery8(id, toolId, sql) {
7945
- const tool = resolveAuthorizedTool(toolId, toolsById, toolsByName);
7946
- if (!tool) {
7947
- sendToChild({
7948
- type: "query_error",
7949
- id,
7950
- error: `Tool "${toolId}" is not authorized for this request.`
7951
- });
7952
- return;
7953
- }
7954
- const startedAt = Date.now();
7955
- streamQueryStart(streamBuffer, tool.name, sql);
7956
- try {
7957
- const result = await tool.fn({ sql });
7958
- const elapsed = Date.now() - startedAt;
7959
- if (result && result.error) {
7960
- const errMsg = typeof result.error === "string" ? result.error : JSON.stringify(result.error);
7961
- streamQueryDone(streamBuffer, elapsed);
7962
- streamQueryError(streamBuffer, tool.name, errMsg);
7963
- sendToChild({ type: "query_error", id, error: errMsg });
7964
- return;
7965
- }
7966
- const data = result?.data || [];
7967
- const count = result?.count ?? data.length;
7968
- const totalCount = result?.metadata?.totalCount;
7969
- streamQueryDone(streamBuffer, elapsed);
7970
- streamQuerySuccess(streamBuffer, tool.name, data.length, totalCount ?? count);
7971
- streamDataPreview(streamBuffer, data);
7972
- sendToChild({
7973
- type: "query_result",
7974
- id,
7975
- data,
7976
- count,
7977
- metadata: { totalCount, executionTimeMs: elapsed }
7978
- });
7979
- } catch (err) {
7980
- const elapsed = Date.now() - startedAt;
7981
- const errMsg = err instanceof Error ? err.message : String(err);
7982
- streamQueryDone(streamBuffer, elapsed);
7983
- streamQueryError(streamBuffer, tool.name, errMsg);
7984
- sendToChild({ type: "query_error", id, error: errMsg });
7985
- }
7986
- }
7987
- function sendToChild(msg) {
7988
- try {
7989
- child.stdin.write(encodeMessage(msg));
7990
- } catch (err) {
7991
- logger.warn(`[ScriptRunner] Failed to write to child stdin: ${err.message}`);
7992
- }
7993
- }
7994
8324
  });
7995
8325
  }
7996
- function resolveAuthorizedTool(toolId, byId, byName) {
7997
- const exact = byId.get(toolId);
7998
- if (exact) return exact;
7999
- const exactName = byName.get(toolId.toLowerCase());
8000
- if (exactName) return exactName;
8001
- for (const [id, tool] of byId) {
8002
- if (id.includes(toolId) || toolId.includes(id)) return tool;
8003
- }
8004
- const lower = toolId.toLowerCase();
8005
- for (const [name, tool] of byName) {
8006
- if (name.includes(lower) || lower.includes(name)) return tool;
8007
- }
8008
- return null;
8009
- }
8010
- function streamQueryStart(sb, sourceName, sql) {
8011
- if (!sb?.hasCallback()) return;
8012
- sb.write(`
8013
- \u{1F4DD} **Querying ${sourceName}:**
8014
- \`\`\`sql
8015
- ${sql}
8016
- \`\`\`
8017
-
8018
- `);
8019
- sb.write(`__QUERY_TIMER_START_Executing query__`);
8020
- }
8021
- function streamQueryDone(sb, ms) {
8022
- if (!sb?.hasCallback()) return;
8023
- sb.write(`__QUERY_TIMER_DONE_${(ms / 1e3).toFixed(1)}__
8024
-
8025
- `);
8026
- }
8027
- function streamQuerySuccess(sb, sourceName, rows, total) {
8028
- if (!sb?.hasCallback()) return;
8029
- const totalInfo = total > rows ? ` of ${total} total` : "";
8030
- sb.write(`\u2705 **${rows} rows${totalInfo} from ${sourceName}**
8031
-
8032
- `);
8033
- }
8034
- function streamQueryError(sb, sourceName, msg) {
8035
- if (!sb?.hasCallback()) return;
8036
- sb.write(`\u274C **Query failed on ${sourceName}:** ${msg}
8037
-
8038
- `);
8039
- }
8040
- function streamDataPreview(sb, data) {
8041
- if (!sb?.hasCallback() || data.length === 0) return;
8042
- try {
8043
- sb.write(`<DataTable>${JSON.stringify(data.slice(0, 10))}</DataTable>
8044
-
8045
- `);
8046
- } catch {
8047
- sb.write(`_Data preview not available_
8048
-
8049
- `);
8050
- }
8051
- }
8052
8326
  function withSynthesizedFinalData(queries, finalData, finalCount) {
8053
8327
  if (!finalData || finalData.length === 0) return queries;
8054
- if (!queries || queries.length === 0) return queries;
8328
+ if (!queries || queries.length === 0) {
8329
+ return [{
8330
+ sourceId: "computed:_final",
8331
+ sourceName: "Script final dataset",
8332
+ sql: "-- script final returned data (runTool / pure JS, no ctx.query)",
8333
+ data: finalData,
8334
+ count: finalCount,
8335
+ executionTimeMs: 0,
8336
+ virtual: true
8337
+ }];
8338
+ }
8055
8339
  const firstSqlQuery = queries.find((q) => !q.virtual);
8056
8340
  const rawSample = firstSqlQuery?.data?.[0];
8057
8341
  const finalSample = finalData[0];
@@ -8191,10 +8475,11 @@ var EXECUTE_SCRIPT_TOOL_DEF = {
8191
8475
  };
8192
8476
  var MAX_SCRIPT_ATTEMPTS = 3;
8193
8477
  var MainAgent = class {
8194
- constructor(externalTools, config, scriptStore, turnId, streamBuffer) {
8478
+ constructor(externalTools, config, scriptStore, turnId, streamBuffer, workflows = []) {
8195
8479
  this.createdFromPrompt = "";
8196
8480
  this.scriptState = { recipeId: null, attempts: 0, lastSuccessfulResult: null };
8197
8481
  this.externalTools = externalTools;
8482
+ this.workflows = workflows;
8198
8483
  this.config = config;
8199
8484
  this.streamBuffer = streamBuffer || new StreamBuffer();
8200
8485
  this.scriptStore = scriptStore ?? null;
@@ -8220,22 +8505,31 @@ var MainAgent = class {
8220
8505
  this.createdFromPrompt = userPrompt;
8221
8506
  const sourceTools = this.externalTools.filter((t) => t.toolType !== "direct");
8222
8507
  const directTools = this.externalTools.filter((t) => t.toolType === "direct");
8223
- logger.info(`[MainAgent] ${sourceTools.length} source tool(s), ${directTools.length} direct tool(s)`);
8508
+ logger.info(`[MainAgent] ${sourceTools.length} source tool(s), ${directTools.length} direct tool(s), ${this.workflows.length} workflow(s)`);
8224
8509
  const summaries = buildSourceSummaries(sourceTools);
8225
- const systemPrompt = await this.buildSystemPrompt(summaries, directTools, conversationHistory);
8510
+ const systemPrompt = await this.buildSystemPrompt(summaries, directTools, this.workflows, conversationHistory);
8226
8511
  logger.logLLMPrompt("mainAgent", "system", extractPromptText(systemPrompt));
8227
8512
  logger.logLLMPrompt("mainAgent", "user", userPrompt);
8228
8513
  const sourceToolDefs = this.buildSourceToolDefinitions(summaries);
8229
8514
  const directToolDefs = this.buildDirectToolDefinitions(directTools);
8515
+ const workflowToolDefs = this.buildWorkflowToolDefinitions(this.workflows);
8230
8516
  const tools = [
8231
8517
  ...sourceToolDefs,
8232
8518
  ...directToolDefs,
8519
+ ...workflowToolDefs,
8233
8520
  ...this.scriptingEnabled ? [WRITE_SCRIPT_TOOL_DEF, EXECUTE_SCRIPT_TOOL_DEF] : []
8234
8521
  ];
8235
8522
  const sourceResults = [];
8236
8523
  const executedTools = [];
8237
8524
  let sourceCallCounter = 0;
8525
+ let selectedWorkflow;
8238
8526
  const toolHandler = async (toolName, toolInput) => {
8527
+ const workflow = this.workflows.find((w) => w.id === toolName);
8528
+ if (workflow) {
8529
+ return this.handleWorkflow(workflow, toolInput, (w) => {
8530
+ selectedWorkflow = w;
8531
+ });
8532
+ }
8239
8533
  if (toolName === "write_script") {
8240
8534
  if (!this.scriptingEnabled) return "Scripting is not enabled for this request.";
8241
8535
  return this.handleWriteScript(toolInput);
@@ -8300,24 +8594,34 @@ var MainAgent = class {
8300
8594
  this.config.maxIterations
8301
8595
  );
8302
8596
  const totalTime = Date.now() - startTime;
8303
- logger.info(`[MainAgent] Complete | ${sourceResults.length} source queries, ${executedTools.length} successful | ${totalTime}ms`);
8304
- const savedScript = this.buildSavedScript();
8597
+ logger.info(
8598
+ `[MainAgent] Complete | ${sourceResults.length} source queries, ${executedTools.length} successful${selectedWorkflow ? ` | workflow="${selectedWorkflow.name}"` : ""} | ${totalTime}ms`
8599
+ );
8600
+ const savedScript = await this.buildSavedScript();
8305
8601
  if (savedScript) {
8306
8602
  logger.info(`[MainAgent] Script authored: "${savedScript.name}" (${savedScript.parameters.length} params, ${this.scriptState.attempts} attempt${this.scriptState.attempts === 1 ? "" : "s"})`);
8307
8603
  } else if (sourceResults.some((r) => r.success)) {
8308
8604
  logger.warn(`[MainAgent] Source query succeeded but no script was authored \u2014 LLM skipped write_script/execute_script. Prompt policy may need tightening.`);
8309
8605
  }
8606
+ if (!savedScript && this.scriptStore && this.scriptState.recipeId) {
8607
+ try {
8608
+ await this.scriptStore.discardDraft(this.scriptState.recipeId);
8609
+ } catch (err) {
8610
+ logger.warn(`[MainAgent] Failed to discard failed draft ${this.scriptState.recipeId}: ${err}`);
8611
+ }
8612
+ }
8310
8613
  return {
8311
8614
  text,
8312
8615
  executedTools,
8313
8616
  sourceResults,
8314
- savedScript
8617
+ savedScript,
8618
+ workflow: selectedWorkflow
8315
8619
  };
8316
8620
  }
8317
8621
  // ============================================
8318
8622
  // Script-authoring tool handlers
8319
8623
  // ============================================
8320
- handleWriteScript(toolInput) {
8624
+ async handleWriteScript(toolInput) {
8321
8625
  const scriptBody = typeof toolInput?.scriptBody === "string" ? toolInput.scriptBody.trim() : "";
8322
8626
  if (!scriptBody) {
8323
8627
  return "write_script requires a non-empty `scriptBody` starting with `export async function getData(ctx, params)`.";
@@ -8326,7 +8630,7 @@ var MainAgent = class {
8326
8630
  const intentDescription = toolInput.description || "";
8327
8631
  const tags = Array.isArray(toolInput.tags) ? toolInput.tags : [];
8328
8632
  const parameters = Array.isArray(toolInput.parameters) ? this.normalizeParameterList(toolInput.parameters) : [];
8329
- const draft = this.scriptStore.saveDraft({
8633
+ const draft = await this.scriptStore.saveDraft({
8330
8634
  recipeId: this.scriptState.recipeId ?? void 0,
8331
8635
  turnId: this.turnId,
8332
8636
  name,
@@ -8347,7 +8651,7 @@ var MainAgent = class {
8347
8651
  if (!this.scriptState.recipeId) {
8348
8652
  return "No draft found. Call write_script first with the script you want to verify.";
8349
8653
  }
8350
- const draftRecipe = this.scriptStore.get(this.scriptState.recipeId);
8654
+ const draftRecipe = await this.scriptStore.get(this.scriptState.recipeId);
8351
8655
  if (!draftRecipe) {
8352
8656
  logger.error(`[MainAgent] execute_script: draft "${this.scriptState.recipeId}" missing from store`);
8353
8657
  return "Draft was lost from the store. Call write_script again.";
@@ -8365,10 +8669,17 @@ var MainAgent = class {
8365
8669
  );
8366
8670
  if (result.success) {
8367
8671
  this.scriptState.lastSuccessfulResult = result;
8672
+ const totalRows = result.data.length;
8368
8673
  const summary2 = {
8369
8674
  ok: true,
8370
8675
  executionTimeMs: result.executionTimeMs,
8676
+ totalRows,
8677
+ // dataSummary is computed over ALL rows — use it for any range/count/
8678
+ // min/max/total/"all-or-none" claim. sampleRows is just the first few
8679
+ // rows (head of result, NOT representative) for illustrating shape.
8680
+ dataSummary: summarizeRows(result.data),
8371
8681
  sampleRows: result.data.slice(0, 5),
8682
+ analysisGuidance: `Showing ${Math.min(totalRows, 5)} of ${totalRows} rows. The sample is the HEAD of the result and is NOT representative \u2014 do not generalize from it. Base every number, range, count, and "all/none" statement on dataSummary, which is computed over all ${totalRows} rows. The full result is rendered for the user by the on-screen component.`,
8372
8683
  executedQueries: result.executedQueries.map((q) => ({
8373
8684
  sourceId: q.sourceId,
8374
8685
  sourceName: q.sourceName,
@@ -8376,10 +8687,10 @@ var MainAgent = class {
8376
8687
  sample: q.data.slice(0, 2)
8377
8688
  }))
8378
8689
  };
8379
- logger.info(`[MainAgent] execute_script: ok \u2014 ${result.data.length} rows in ${result.executionTimeMs}ms (attempt ${this.scriptState.attempts})`);
8690
+ logger.info(`[MainAgent] execute_script: ok \u2014 ${totalRows} rows in ${result.executionTimeMs}ms (attempt ${this.scriptState.attempts})`);
8380
8691
  return JSON.stringify(summary2, null, 2);
8381
8692
  }
8382
- this.scriptStore.recordDraftError(draftRecipe.id, {
8693
+ await this.scriptStore.recordDraftError(draftRecipe.id, {
8383
8694
  phase: result.errorPhase ?? "runtime",
8384
8695
  message: result.error ?? "Unknown error",
8385
8696
  attempt: this.scriptState.attempts
@@ -8405,11 +8716,11 @@ var MainAgent = class {
8405
8716
  * `ScriptStore.promoteToVerified()`. Only returned when a verified
8406
8717
  * successful execution is on record.
8407
8718
  */
8408
- buildSavedScript() {
8719
+ async buildSavedScript() {
8409
8720
  const { recipeId, lastSuccessfulResult } = this.scriptState;
8410
8721
  if (!recipeId || !lastSuccessfulResult) return void 0;
8411
8722
  if (!this.scriptStore) return void 0;
8412
- const draft = this.scriptStore.get(recipeId);
8723
+ const draft = await this.scriptStore.get(recipeId);
8413
8724
  if (!draft) return void 0;
8414
8725
  const executedQueries = lastSuccessfulResult.executedQueries.map((q) => ({
8415
8726
  sourceId: q.sourceId,
@@ -8536,6 +8847,7 @@ ${lines.join("\n")}`;
8536
8847
  maxRows: 5,
8537
8848
  maxCharsPerField: 200
8538
8849
  });
8850
+ const sampleData = Array.isArray(resultData) ? resultData.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : Array.isArray(formattedResult.data) ? formattedResult.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : [];
8539
8851
  executedTools.push({
8540
8852
  id: tool.id,
8541
8853
  name: tool.name,
@@ -8544,7 +8856,7 @@ ${lines.join("\n")}`;
8544
8856
  _totalRecords: result.totalItems || result.count || rowCount,
8545
8857
  _recordsShown: rowCount,
8546
8858
  _metadata: result.metadata,
8547
- _sampleData: Array.isArray(resultData) ? resultData.slice(0, 3) : [resultData]
8859
+ _sampleData: sampleData
8548
8860
  },
8549
8861
  outputSchema: tool.outputSchema
8550
8862
  });
@@ -8570,9 +8882,10 @@ ${formatted}`;
8570
8882
  // System Prompt
8571
8883
  // ============================================
8572
8884
  /**
8573
- * Build the main agent's system prompt with source summaries and direct tool descriptions.
8885
+ * Build the main agent's system prompt with source summaries, direct tool descriptions,
8886
+ * and workflow component descriptions.
8574
8887
  */
8575
- async buildSystemPrompt(summaries, directTools, conversationHistory) {
8888
+ async buildSystemPrompt(summaries, directTools, workflows, conversationHistory) {
8576
8889
  const summariesText = formatSummariesForPrompt(summaries);
8577
8890
  const maxSourceCalls = Math.max(2, this.config.maxIterations - 2);
8578
8891
  let directToolsText = "";
@@ -8584,10 +8897,28 @@ ${formatted}`;
8584
8897
  ${t.description || "No description"}${paramList ? "\n Parameters:\n" + paramList : ""}`;
8585
8898
  }).join("\n\n");
8586
8899
  }
8900
+ let workflowsText = "";
8901
+ if (workflows.length > 0) {
8902
+ workflowsText = workflows.map((w, idx) => {
8903
+ const propLines = Object.entries(w.propsSchema || {}).map(([k, v]) => ` - ${k}: ${v}`).join("\n");
8904
+ return [
8905
+ `${idx + 1}. **${w.name}** (tool: ${w.id})`,
8906
+ ` ${w.description}`,
8907
+ ` When to use: ${w.whenToUse}`,
8908
+ propLines ? ` Props:
8909
+ ${propLines}` : ""
8910
+ ].filter(Boolean).join("\n");
8911
+ }).join("\n\n");
8912
+ } else {
8913
+ workflowsText = "No workflow components registered for this project.";
8914
+ }
8587
8915
  const prompts = await promptLoader.loadPrompts("agent-main", {
8588
8916
  SOURCE_SUMMARIES: summariesText,
8589
8917
  DIRECT_TOOLS: directToolsText,
8590
- MAX_ROWS: String(this.config.maxRowsPerSource),
8918
+ WORKFLOW_COMPONENTS: workflowsText,
8919
+ MAX_ROWS: String(MAIN_AGENT_COMPLETE_ROWS),
8920
+ MAX_ROWS_RENDERED: String(MAX_ROWS_RENDERED),
8921
+ MAX_ROWS_FETCHED: String(MAX_ROWS_FETCHED),
8591
8922
  MAX_SOURCE_CALLS: String(maxSourceCalls),
8592
8923
  GLOBAL_KNOWLEDGE_BASE: this.config.globalKnowledgeBase || "No global knowledge base available.",
8593
8924
  CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
@@ -8673,6 +9004,70 @@ ${formatted}`;
8673
9004
  });
8674
9005
  }
8675
9006
  // ============================================
9007
+ // Workflow Handling
9008
+ // ============================================
9009
+ /**
9010
+ * Capture a workflow selection. We do NOT execute anything — the LLM has
9011
+ * already extracted the props it wants the workflow rendered with. We
9012
+ * record the selection (via the capture callback) and return a short
9013
+ * acknowledgement so the LLM ends its turn cleanly without writing
9014
+ * analysis text or calling more tools.
9015
+ */
9016
+ async handleWorkflow(workflow, toolInput, capture) {
9017
+ const props = { ...workflow.defaultProps || {}, ...toolInput || {} };
9018
+ logger.info(
9019
+ `[MainAgent] Workflow selected: "${workflow.name}" | props: ${JSON.stringify(props).substring(0, 200)}`
9020
+ );
9021
+ if (this.streamBuffer.hasCallback()) {
9022
+ this.streamBuffer.write(`
9023
+
9024
+ \u{1F9ED} **Launching ${workflow.name} workflow...**
9025
+
9026
+ `);
9027
+ await streamDelay();
9028
+ }
9029
+ capture({ name: workflow.name, props });
9030
+ return `\u2705 Workflow "${workflow.name}" selected. The UI will render now \u2014 do NOT write analysis text or call any other tools. End your turn.`;
9031
+ }
9032
+ /**
9033
+ * Build LLM tool definitions for workflow components. The workflow's
9034
+ * propsSchema becomes the tool's input_schema so the LLM extracts props
9035
+ * directly from the prompt — same mechanic as direct tools.
9036
+ */
9037
+ buildWorkflowToolDefinitions(workflows) {
9038
+ return workflows.map((workflow) => {
9039
+ const properties = {};
9040
+ const required = [];
9041
+ Object.entries(workflow.propsSchema || {}).forEach(([key, typeOrValue]) => {
9042
+ const valueStr = String(typeOrValue).toLowerCase();
9043
+ let schemaType = "string";
9044
+ const typeMatch = valueStr.match(/^(string|number|integer|boolean|array|object)\b/);
9045
+ if (typeMatch) {
9046
+ schemaType = typeMatch[1];
9047
+ }
9048
+ const isOptional = valueStr.includes("(optional)") || valueStr.includes("optional");
9049
+ const description = typeof typeOrValue === "string" ? typeOrValue : `Prop: ${key}`;
9050
+ if (schemaType === "array") {
9051
+ properties[key] = { type: "array", items: {}, description };
9052
+ } else if (schemaType === "object") {
9053
+ properties[key] = { type: "object", description };
9054
+ } else {
9055
+ properties[key] = { type: schemaType, description };
9056
+ }
9057
+ if (!isOptional) required.push(key);
9058
+ });
9059
+ return {
9060
+ name: workflow.id,
9061
+ description: `[WORKFLOW] ${workflow.description} \u2014 When to use: ${workflow.whenToUse}`,
9062
+ input_schema: {
9063
+ type: "object",
9064
+ properties,
9065
+ required: required.length > 0 ? required : void 0
9066
+ }
9067
+ };
9068
+ });
9069
+ }
9070
+ // ============================================
8676
9071
  // Format Result for Main Agent
8677
9072
  // ============================================
8678
9073
  /**
@@ -8682,7 +9077,7 @@ ${formatted}`;
8682
9077
  if (!result.success) {
8683
9078
  return `Data source "${result.sourceName}" could not fulfill the request: ${result.error}. Try rephrasing your intent or querying a different source.`;
8684
9079
  }
8685
- const { data, metadata } = result;
9080
+ const { metadata } = result;
8686
9081
  let output = `## Data from "${result.sourceName}"
8687
9082
  `;
8688
9083
  output += `Rows returned: ${metadata.rowsReturned}`;
@@ -8693,33 +9088,68 @@ ${formatted}`;
8693
9088
  Execution time: ${metadata.executionTimeMs}ms
8694
9089
 
8695
9090
  `;
8696
- if (data.length === 0) {
9091
+ const successfulTools = result.allExecutedTools && result.allExecutedTools.length > 0 ? result.allExecutedTools : result.executedTool ? [result.executedTool] : [];
9092
+ const queries = successfulTools.map((t) => ({
9093
+ sql: t?.params?.sql || t?.params?.query,
9094
+ rows: t?.result?._mainAgentRows ?? t?.result?._sampleData ?? [],
9095
+ summary: t?.result?._summary,
9096
+ total: t?.result?._totalRecords,
9097
+ shown: t?.result?._recordsShown
9098
+ })).filter((q) => Boolean(q.sql));
9099
+ if (queries.length === 0) {
8697
9100
  output += "No data returned.";
8698
9101
  return output;
8699
9102
  }
8700
- const maxRowsForLLM = Math.min(data.length, 10);
8701
- const truncatedData = data.slice(0, maxRowsForLLM).map((row) => {
8702
- const truncatedRow = {};
9103
+ const truncRows = (rowsArr) => rowsArr.map((row) => {
9104
+ const out = {};
8703
9105
  for (const [key, value] of Object.entries(row)) {
8704
- if (typeof value === "string" && value.length > 200) {
8705
- truncatedRow[key] = value.substring(0, 200) + "...";
8706
- } else {
8707
- truncatedRow[key] = value;
8708
- }
9106
+ out[key] = typeof value === "string" && value.length > 200 ? safeTruncate(value, 200) + "..." : value;
8709
9107
  }
8710
- return truncatedRow;
9108
+ return out;
8711
9109
  });
8712
- const truncationNote = data.length > maxRowsForLLM ? `
8713
- (showing ${maxRowsForLLM} of ${data.length} rows)` : "";
8714
- output += `### Results (${data.length} rows${truncationNote})
9110
+ const jsonBlock = (obj) => {
9111
+ try {
9112
+ return "```json\n" + JSON.stringify(obj, null, 2) + "\n```\n";
9113
+ } catch {
9114
+ return "```\n[Data could not be serialized]\n```\n";
9115
+ }
9116
+ };
9117
+ const scopeWarning = `PRESERVE every filter and scope \u2014 brand, product, date range, AND geography/segment (\`State\`/\`Region\`/\`District\`). Do NOT broaden, drop, or "simplify" any predicate.`;
9118
+ output += queries.length === 1 ? `### The SQL below and its result \u2014 copy the SQL VERBATIM into \`write_script\` (inside \`ctx.query(...)\`)
9119
+ ${scopeWarning}
9120
+
9121
+ ` : `### ${queries.length} successful queries \u2014 each block pairs ONE query's SQL with ITS OWN result. Do NOT mix a query's SQL with another query's data.
9122
+ ${scopeWarning} The script re-runs them, so you can include more than one \`ctx.query(...)\`.
9123
+
8715
9124
  `;
8716
- output += "```json\n";
8717
- try {
8718
- output += JSON.stringify(truncatedData, null, 2);
8719
- } catch {
8720
- output += "[Data could not be serialized]";
8721
- }
8722
- output += "\n```\n";
9125
+ queries.forEach((q, i) => {
9126
+ const total = q.total ?? q.rows.length;
9127
+ const capped = (q.shown ?? q.rows.length) >= MAX_ROWS_FETCHED;
9128
+ output += queries.length === 1 ? `**SQL:**
9129
+ ` : `### Query ${i + 1} \u2014 ${capped ? `${MAX_ROWS_FETCHED}+ (TRUNCATED)` : `${total}`} rows
9130
+ **SQL for Query ${i + 1}:**
9131
+ `;
9132
+ output += "```sql\n" + String(q.sql) + "\n```\n";
9133
+ if (capped) {
9134
+ output += `> \u26A0\uFE0F **TRUNCATED, NON-REPRESENTATIVE SAMPLE.** This query hit the ${MAX_ROWS_FETCHED}-row exploration cap, so it returned only the FIRST ${MAX_ROWS_FETCHED} rows \u2014 and because it is \`ORDER BY\`'d, those rows are likely ALL from one group (e.g. one District/category), not a cross-section. Do NOT conclude any group "has no data" or read any count/total/min/max from this. To reason over all rows, re-query with **aggregation** (\`GROUP BY\` the dimension, e.g. \`SELECT District, COUNT(*) ... GROUP BY District\`) or compute the metric in SQL \u2014 never from this slice.
9135
+ `;
9136
+ }
9137
+ if (total <= MAIN_AGENT_COMPLETE_ROWS) {
9138
+ const rows = truncRows(q.rows);
9139
+ output += `**All ${rows.length} rows (COMPLETE result \u2014 this is the entire result set, nothing hidden):**
9140
+ `;
9141
+ output += jsonBlock(rows);
9142
+ } else {
9143
+ const samples = truncRows(q.rows.slice(0, 3));
9144
+ output += `**Result summary \u2014 computed over ALL ${total} rows. Use THIS for every count, range, total, "all/none", and per-group number; do NOT infer them from the samples:**
9145
+ `;
9146
+ output += jsonBlock(q.summary ?? { totalRows: total });
9147
+ output += `**Sample rows (first ${samples.length} of ${total} \u2014 to show row SHAPE only, NOT representative of the distribution):**
9148
+ `;
9149
+ output += jsonBlock(samples);
9150
+ }
9151
+ output += "\n";
9152
+ });
8723
9153
  return output;
8724
9154
  }
8725
9155
  /**
@@ -8745,14 +9175,15 @@ function extractTablesFromSQL(sqls) {
8745
9175
  // src/userResponse/agents/types.ts
8746
9176
  var DEFAULT_AGENT_CONFIG = {
8747
9177
  maxRowsPerSource: 10,
9178
+ maxRowsFetched: MAX_ROWS_FETCHED,
8748
9179
  mainAgentModel: "",
8749
9180
  // will use the provider's default model
8750
9181
  sourceAgentModel: "",
8751
9182
  // will use the provider's default model
8752
9183
  maxRetries: 2,
8753
9184
  // 2 retries = 3 total query attempts (1 initial + 2 retries for SQL errors)
8754
- maxIterations: 8
8755
- // schema search (2-3) + query attempts (2) + LLM responses + final
9185
+ maxIterations: 12
9186
+ // schema search (2-3) + query attempts (2) + write_script/execute_script (2-3) + LLM responses + final
8756
9187
  };
8757
9188
 
8758
9189
  // src/userResponse/utils/component-props-processor.ts
@@ -9026,6 +9457,7 @@ function formatExecutedTools(executedTools) {
9026
9457
  Fields:
9027
9458
  ${fieldsText}`;
9028
9459
  }
9460
+ const MAX_SAMPLE_BLOCK_CHARS = 8e3;
9029
9461
  let sampleDataText = "";
9030
9462
  const sampleData = tool.result?._sampleData;
9031
9463
  if (Array.isArray(sampleData) && sampleData.length > 0) {
@@ -9033,8 +9465,11 @@ ${fieldsText}`;
9033
9465
  sampleDataText = `
9034
9466
  \u{1F511} RESULT FIELDS: ${sampleFields.join(", ")}`;
9035
9467
  try {
9468
+ const stringified = JSON.stringify(sampleData, null, 2);
9469
+ const capped = stringified.length > MAX_SAMPLE_BLOCK_CHARS ? `${stringified.substring(0, MAX_SAMPLE_BLOCK_CHARS)}
9470
+ ... (truncated; ${stringified.length - MAX_SAMPLE_BLOCK_CHARS} more chars)` : stringified;
9036
9471
  sampleDataText += `
9037
- \u{1F4C4} SAMPLE ROW: ${JSON.stringify(sampleData[0])}`;
9472
+ \u{1F4C4} SAMPLE ROWS (${sampleData.length}): ${capped}`;
9038
9473
  } catch {
9039
9474
  }
9040
9475
  }
@@ -9322,11 +9757,20 @@ Fixed SQL query:`;
9322
9757
  return validatedQuery;
9323
9758
  }
9324
9759
 
9760
+ // src/userResponse/scripts/script-metadata-store.ts
9761
+ function resolveScriptRecipeStore(collections) {
9762
+ const s = collections?.["script-recipes"];
9763
+ if (s && typeof s.search === "function" && typeof s.getById === "function") {
9764
+ return s;
9765
+ }
9766
+ return null;
9767
+ }
9768
+
9325
9769
  // src/userResponse/scripts/script-store.ts
9326
9770
  import * as fs6 from "fs";
9327
9771
  import * as path6 from "path";
9772
+ import { createHash, randomBytes } from "crypto";
9328
9773
  var DEFAULT_STORE_DIR = "scripts-store";
9329
- var METADATA_SUBDIR = "metadata";
9330
9774
  function normalizeScriptBody(scriptBody) {
9331
9775
  let i = 0;
9332
9776
  const n = scriptBody.length;
@@ -9351,383 +9795,321 @@ function normalizeScriptBody(scriptBody) {
9351
9795
  } else if (/^\s*function\s+getData\b/.test(cleanBody)) {
9352
9796
  cleanBody = cleanBody.replace(/^\s*function\s+getData\b/, "export async function getData");
9353
9797
  }
9798
+ cleanBody = cleanBody.replace(/(?<!\\)\\([dwsDWS])/g, "\\\\$1");
9354
9799
  return cleanBody;
9355
9800
  }
9356
9801
  var ScriptStore = class {
9357
- constructor(baseDir) {
9358
- this.recipes = /* @__PURE__ */ new Map();
9359
- this.loaded = false;
9360
- this.storeDir = baseDir || path6.join(process.cwd(), DEFAULT_STORE_DIR);
9361
- }
9362
- metadataDir() {
9363
- return path6.join(this.storeDir, METADATA_SUBDIR);
9364
- }
9365
- /**
9366
- * Filename base for a recipe. Drafts include the per-turn suffix so two
9367
- * concurrent turns can never clobber each other's draft files; verified
9368
- * recipes use the bare slug.
9369
- */
9370
- fileBaseName(recipe) {
9371
- const slug = this.toFileName(recipe.name);
9372
- if (recipe.status === "draft" && recipe.turnId) {
9373
- return `${slug}-${recipe.turnId}`;
9802
+ constructor(opts) {
9803
+ this.store = opts?.store ?? resolveScriptRecipeStore(opts?.collections) ?? null;
9804
+ this.storeDir = opts?.baseDir || path6.join(process.cwd(), DEFAULT_STORE_DIR);
9805
+ this.projectId = opts?.projectId;
9806
+ if (!this.store) {
9807
+ logger.warn("[ScriptStore] No script-recipes metadata store injected \u2014 script reuse disabled this run.");
9374
9808
  }
9375
- return slug;
9376
9809
  }
9377
- /**
9378
- * Absolute path to the .ts file for a recipe. Callers (e.g. ScriptRunner,
9379
- * MainAgent's execute_script) use this to hand off the path to the tsx child.
9380
- */
9381
- getScriptPath(recipe) {
9382
- return path6.join(this.storeDir, `${this.fileBaseName(recipe)}.ts`);
9383
- }
9384
- /**
9385
- * Absolute path to the metadata JSON for a recipe.
9386
- */
9387
- getMetadataPath(recipe) {
9388
- return path6.join(this.metadataDir(), `${this.fileBaseName(recipe)}.json`);
9810
+ /** Whether a metadata store is wired (matcher / authoring are gated on this). */
9811
+ hasStore() {
9812
+ return this.store !== null;
9389
9813
  }
9390
- /**
9391
- * Get all VERIFIED recipes — drafts are filtered out so the matcher never
9392
- * considers an unverified script. Loads from disk on first access.
9393
- */
9394
- getAll() {
9395
- this.ensureLoaded();
9396
- return Array.from(this.recipes.values()).filter((r) => (r.status ?? "verified") === "verified");
9814
+ // ============================================
9815
+ // Read
9816
+ // ============================================
9817
+ /** Number of healthy verified recipes (gates the script-matching path). */
9818
+ async count() {
9819
+ if (!this.store) return 0;
9820
+ try {
9821
+ return await this.store.count({ projectId: this.projectId });
9822
+ } catch (err) {
9823
+ logger.warn(`[ScriptStore] count failed: ${err}`);
9824
+ return 0;
9825
+ }
9397
9826
  }
9398
9827
  /**
9399
- * Get a recipe by ID returns drafts as well as verified scripts.
9400
- * Used by MainAgent and the promotion path.
9828
+ * FTS shortlist for the matcher (metadata only bodies are loaded lazily by
9829
+ * `get()` once the LLM picks one). Returns verified, healthy recipes ranked
9830
+ * by relevance.
9401
9831
  */
9402
- get(id) {
9403
- this.ensureLoaded();
9404
- return this.recipes.get(id) || null;
9832
+ async search(prompt, limit) {
9833
+ if (!this.store) return [];
9834
+ try {
9835
+ const rows = await this.store.search({ prompt, projectId: this.projectId, limit });
9836
+ return rows.map((r) => this.rowToRecipe(r, ""));
9837
+ } catch (err) {
9838
+ logger.warn(`[ScriptStore] search failed: ${err}`);
9839
+ return [];
9840
+ }
9405
9841
  }
9406
- /**
9407
- * Number of verified recipes (matches `getAll().length`).
9408
- */
9409
- count() {
9410
- this.ensureLoaded();
9411
- return this.getAll().length;
9842
+ /** Fetch one recipe by id with its body loaded from disk. */
9843
+ async get(id) {
9844
+ if (!this.store) return null;
9845
+ try {
9846
+ const row = await this.store.getById(id);
9847
+ if (!row) return null;
9848
+ const body = this.readBody(row.fileBase);
9849
+ return this.rowToRecipe(row, body);
9850
+ } catch (err) {
9851
+ logger.warn(`[ScriptStore] get(${id}) failed: ${err}`);
9852
+ return null;
9853
+ }
9412
9854
  }
9413
- /**
9414
- * Save a recipe (create or update).
9415
- * File is named after the script: "order-status-distribution.json"
9416
- */
9417
- save(recipe) {
9418
- this.ensureLoaded();
9419
- recipe.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9420
- this.recipes.set(recipe.id, recipe);
9421
- this.saveRecipeToDisk(recipe);
9422
- logger.info(`[ScriptStore] Saved script "${recipe.name}" (${recipe.id}) \u2014 ${this.recipes.size} total`);
9855
+ // ============================================
9856
+ // Write
9857
+ // ============================================
9858
+ /** Create or update a recipe (metadata upsert + body write when changed). */
9859
+ async save(recipe) {
9860
+ if (!this.store) return;
9861
+ try {
9862
+ if (!recipe.fileBase) {
9863
+ recipe.fileBase = await this.computeFileBase(recipe.name, recipe.id);
9864
+ }
9865
+ if (recipe.scriptBody) {
9866
+ const normalized = normalizeScriptBody(recipe.scriptBody);
9867
+ const hash = this.hash(normalized);
9868
+ if (hash !== recipe.bodyHash) {
9869
+ this.writeBody(recipe.fileBase, normalized);
9870
+ recipe.bodyHash = hash;
9871
+ }
9872
+ }
9873
+ recipe.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9874
+ await this.store.upsert(this.recipeToRow(recipe));
9875
+ logger.info(`[ScriptStore] Saved "${recipe.name}" (${recipe.id})`);
9876
+ } catch (err) {
9877
+ logger.warn(`[ScriptStore] save("${recipe.name}") failed: ${err}`);
9878
+ }
9423
9879
  }
9424
9880
  /**
9425
- * Persist (or update) a draft recipe to disk. Always writes immediately so
9426
- * the `.ts` body is visible in the IDE the moment MainAgent calls
9427
- * `write_script`. Within one turn, retries that pass the same `recipeId`
9428
- * overwrite the same files (the LLM rewriting itself); a fresh `recipeId`
9429
- * is minted only on the first call of the turn.
9430
- *
9431
- * Filename includes the `turnId` suffix so concurrent turns never collide.
9881
+ * Persist (or update) a draft. Within a turn, retries that pass the same
9882
+ * `recipeId` overwrite the same row + file; a fresh `recipeId` mints a new
9883
+ * draft. The body is visible at scripts-store/<fileBase>.ts immediately.
9432
9884
  */
9433
- saveDraft(input) {
9434
- this.ensureLoaded();
9885
+ async saveDraft(input) {
9886
+ if (!this.store) {
9887
+ throw new Error("[ScriptStore] saveDraft called with no metadata store injected");
9888
+ }
9435
9889
  const now = (/* @__PURE__ */ new Date()).toISOString();
9436
- const existing = input.recipeId ? this.recipes.get(input.recipeId) : void 0;
9437
- const reuseExisting = existing && (existing.status ?? "verified") === "draft" && existing.turnId === input.turnId;
9438
- const recipe = reuseExisting ? {
9439
- ...existing,
9440
- name: input.name,
9441
- intentDescription: input.intentDescription,
9442
- tags: input.tags,
9443
- parameters: input.parameters,
9444
- scriptBody: input.scriptBody,
9445
- updatedAt: now
9446
- } : {
9447
- id: `script_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
9448
- version: 1,
9890
+ const existing = input.recipeId ? await this.store.getById(input.recipeId) : null;
9891
+ const reuse = existing && existing.status === "draft" && existing.turnId === input.turnId;
9892
+ const id = reuse ? existing.id : `script_${Date.now()}_${randomBytes(3).toString("hex")}`;
9893
+ const fileBase = reuse ? existing.fileBase : await this.computeFileBase(input.name, id);
9894
+ const normalized = normalizeScriptBody(input.scriptBody);
9895
+ const bodyHash = this.hash(normalized);
9896
+ this.writeBody(fileBase, normalized);
9897
+ const recipe = {
9898
+ id,
9899
+ version: existing?.version ?? 1,
9449
9900
  name: input.name,
9450
9901
  intentDescription: input.intentDescription,
9451
9902
  tags: input.tags,
9452
- sourceIds: [],
9453
- tables: [],
9903
+ sourceIds: existing?.sourceIds ?? [],
9904
+ tables: existing?.tables ?? [],
9454
9905
  parameters: input.parameters,
9455
- scriptBody: input.scriptBody,
9456
- successCount: 0,
9457
- failureCount: 0,
9906
+ scriptBody: normalized,
9907
+ fileBase,
9908
+ bodyHash,
9909
+ projectId: this.projectId,
9910
+ successCount: existing?.successCount ?? 0,
9911
+ failureCount: existing?.failureCount ?? 0,
9458
9912
  lastUsed: now,
9459
9913
  createdFrom: input.createdFrom,
9460
- createdAt: now,
9914
+ createdAt: existing?.createdAt ?? now,
9461
9915
  updatedAt: now,
9462
- forkDepth: 0,
9916
+ forkDepth: existing?.forkDepth ?? 0,
9463
9917
  status: "draft",
9464
9918
  turnId: input.turnId
9465
9919
  };
9466
- this.recipes.set(recipe.id, recipe);
9467
- this.saveRecipeToDisk(recipe);
9468
- logger.info(
9469
- `[ScriptStore] ${reuseExisting ? "Updated" : "Saved"} draft "${recipe.name}" (${recipe.id}) at ${this.getScriptPath(recipe)}`
9470
- );
9920
+ await this.store.upsert(this.recipeToRow(recipe));
9921
+ logger.info(`[ScriptStore] ${reuse ? "Updated" : "Saved"} draft "${recipe.name}" (${id}) at ${this.getScriptPath(recipe)}`);
9471
9922
  return recipe;
9472
9923
  }
9473
- /**
9474
- * Stamp the draft with the most recent execution failure so it is visible
9475
- * in the metadata JSON without grepping logs. No-op if the recipe doesn't
9476
- * exist or has already been promoted.
9477
- */
9478
- recordDraftError(recipeId, err) {
9479
- this.ensureLoaded();
9480
- const recipe = this.recipes.get(recipeId);
9481
- if (!recipe || (recipe.status ?? "verified") !== "draft") return;
9482
- recipe.lastError = {
9483
- phase: err.phase,
9484
- message: err.message,
9485
- attempt: err.attempt,
9486
- at: (/* @__PURE__ */ new Date()).toISOString()
9487
- };
9488
- recipe.failureCount++;
9489
- recipe.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9490
- this.saveRecipeToDisk(recipe);
9924
+ /** Stamp a draft's last execution error (metadata only). */
9925
+ async recordDraftError(recipeId, err) {
9926
+ if (!this.store) return;
9927
+ try {
9928
+ await this.store.recordDraftError(recipeId, { ...err, at: (/* @__PURE__ */ new Date()).toISOString() });
9929
+ } catch (e) {
9930
+ logger.warn(`[ScriptStore] recordDraftError failed: ${e}`);
9931
+ }
9491
9932
  }
9492
9933
  /**
9493
9934
  * Promote a successfully-executed draft into a verified script.
9494
- *
9495
- * - Renames the on-disk files from `<slug>-<turnId>.{ts,json}` to the bare
9496
- * `<slug>.{ts,json}`. If a verified file with the same slug already exists
9497
- * (concurrent turn won the race, or a prior session has it), the recipe
9498
- * keeps its turn-suffixed filename so we never overwrite verified work —
9499
- * the matcher keys on `recipe.id`, so two siblings with similar slugs is fine.
9500
- * - Flips `status: 'verified'`, clears `lastError`, applies provenance
9501
- * (`sourceIds`, `tables`) and optional fork lineage.
9502
- */
9503
- promoteToVerified(recipeId, input) {
9504
- this.ensureLoaded();
9505
- const recipe = this.recipes.get(recipeId);
9506
- if (!recipe) {
9507
- logger.warn(`[ScriptStore] promoteToVerified: recipe "${recipeId}" not found`);
9508
- return null;
9509
- }
9510
- if ((recipe.status ?? "verified") === "verified") {
9511
- logger.info(`[ScriptStore] promoteToVerified: recipe "${recipeId}" already verified \u2014 no-op`);
9512
- return recipe;
9513
- }
9514
- const slug = this.toFileName(recipe.name);
9515
- const canonicalTs = path6.join(this.storeDir, `${slug}.ts`);
9516
- const canonicalMeta = path6.join(this.metadataDir(), `${slug}.json`);
9517
- const canonicalFree = !fs6.existsSync(canonicalTs) && !fs6.existsSync(canonicalMeta);
9518
- const oldTsPath = this.getScriptPath(recipe);
9519
- const oldMetaPath = this.getMetadataPath(recipe);
9520
- recipe.status = "verified";
9521
- recipe.successCount = Math.max(1, recipe.successCount);
9522
- recipe.failureCount = 0;
9523
- recipe.lastError = void 0;
9524
- recipe.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
9525
- recipe.updatedAt = recipe.lastUsed;
9526
- recipe.sourceIds = input.sourceIds;
9527
- recipe.tables = input.tables;
9528
- if (input.parentId !== void 0) recipe.parentId = input.parentId;
9529
- if (input.forkDepth !== void 0) recipe.forkDepth = input.forkDepth;
9530
- if (input.forkReason !== void 0) recipe.forkReason = input.forkReason;
9531
- if (canonicalFree) {
9532
- recipe.turnId = void 0;
9533
- try {
9534
- if (fs6.existsSync(oldTsPath)) fs6.unlinkSync(oldTsPath);
9535
- if (fs6.existsSync(oldMetaPath)) fs6.unlinkSync(oldMetaPath);
9536
- } catch (err) {
9537
- logger.warn(`[ScriptStore] promoteToVerified cleanup warning: ${err}`);
9935
+ * The on-disk body already exists at <fileBase>.ts (written at write_script
9936
+ * time) and keeps its name — only the DB row flips status + provenance.
9937
+ */
9938
+ async promoteToVerified(recipeId, input) {
9939
+ if (!this.store) return null;
9940
+ try {
9941
+ const row = await this.store.promote(recipeId, {
9942
+ sourceIds: input.sourceIds,
9943
+ tables: input.tables,
9944
+ parentId: input.parentId,
9945
+ forkDepth: input.forkDepth,
9946
+ forkReason: input.forkReason,
9947
+ components: input.components
9948
+ });
9949
+ if (!row) {
9950
+ logger.warn(`[ScriptStore] promoteToVerified: recipe "${recipeId}" not found`);
9951
+ return null;
9538
9952
  }
9539
- this.saveRecipeToDisk(recipe);
9540
- logger.info(
9541
- `[ScriptStore] Promoted draft "${recipe.name}" (${recipe.id}) \u2192 ${this.getScriptPath(recipe)}`
9542
- );
9543
- } else {
9544
- this.saveRecipeToDisk(recipe);
9545
- logger.info(
9546
- `[ScriptStore] Promoted draft "${recipe.name}" (${recipe.id}) \u2014 canonical slug "${slug}" was taken, kept turn-suffixed filename ${this.getScriptPath(recipe)}`
9547
- );
9953
+ logger.info(`[ScriptStore] Promoted "${row.name}" (${recipeId}) \u2192 verified`);
9954
+ return this.rowToRecipe(row, this.readBody(row.fileBase));
9955
+ } catch (err) {
9956
+ logger.warn(`[ScriptStore] promoteToVerified failed: ${err}`);
9957
+ return null;
9548
9958
  }
9549
- return recipe;
9550
9959
  }
9551
9960
  /**
9552
- * Drop a draft from disk + memory (e.g. when an outer error path wants to
9553
- * clean up). Per the agreed policy, MainAgent's normal failure path does
9554
- * NOT call this failed drafts are kept on disk for the user to inspect.
9961
+ * Drop a draft (row + body file). MainAgent calls this at end-of-turn when a
9962
+ * draft was authored but never verified failed drafts are never matched, so
9963
+ * deleting them immediately avoids unbounded accumulation (#5). No-op if the
9964
+ * recipe isn't a draft (so a promoted/verified script is never removed here).
9555
9965
  */
9556
- discardDraft(recipeId) {
9557
- this.ensureLoaded();
9558
- const recipe = this.recipes.get(recipeId);
9559
- if (!recipe || (recipe.status ?? "verified") !== "draft") return;
9560
- this.recipes.delete(recipeId);
9561
- this.deleteRecipeFromDisk(recipe);
9562
- logger.info(`[ScriptStore] Discarded draft "${recipe.name}" (${recipeId})`);
9966
+ async discardDraft(recipeId) {
9967
+ await this.removeById(recipeId, "draft");
9563
9968
  }
9564
- /**
9565
- * Delete a recipe by ID.
9566
- */
9567
- delete(id) {
9568
- this.ensureLoaded();
9569
- const recipe = this.recipes.get(id);
9570
- if (recipe) {
9571
- this.recipes.delete(id);
9572
- this.deleteRecipeFromDisk(recipe);
9573
- logger.info(`[ScriptStore] Deleted script "${recipe.name}" (${id})`);
9574
- }
9969
+ /** Delete a recipe (row + body file). */
9970
+ async delete(id) {
9971
+ await this.removeById(id);
9575
9972
  }
9576
- /**
9577
- * Record a successful execution for a recipe.
9578
- */
9579
- recordSuccess(id) {
9580
- this.ensureLoaded();
9581
- const recipe = this.recipes.get(id);
9582
- if (recipe) {
9583
- recipe.successCount++;
9584
- recipe.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
9585
- this.saveRecipeToDisk(recipe);
9973
+ /** Record a successful execution (atomic counter bump). */
9974
+ async recordSuccess(id) {
9975
+ if (!this.store) return;
9976
+ try {
9977
+ await this.store.updateStats(id, { successDelta: 1, lastUsed: (/* @__PURE__ */ new Date()).toISOString() });
9978
+ } catch (err) {
9979
+ logger.warn(`[ScriptStore] recordSuccess failed: ${err}`);
9586
9980
  }
9587
9981
  }
9588
- /**
9589
- * Record a failed execution for a recipe.
9590
- */
9591
- recordFailure(id) {
9592
- this.ensureLoaded();
9593
- const recipe = this.recipes.get(id);
9594
- if (recipe) {
9595
- recipe.failureCount++;
9596
- recipe.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
9597
- this.saveRecipeToDisk(recipe);
9982
+ /** Record a failed execution (atomic counter bump). */
9983
+ async recordFailure(id) {
9984
+ if (!this.store) return;
9985
+ try {
9986
+ await this.store.updateStats(id, { failureDelta: 1, lastUsed: (/* @__PURE__ */ new Date()).toISOString() });
9987
+ } catch (err) {
9988
+ logger.warn(`[ScriptStore] recordFailure failed: ${err}`);
9598
9989
  }
9599
9990
  }
9600
- /**
9601
- * Get the failure rate for a recipe (0 to 1).
9602
- * Returns 0 if the recipe has fewer than 5 total uses.
9603
- */
9604
- getFailureRate(id) {
9605
- const recipe = this.recipes.get(id);
9606
- if (!recipe) return 0;
9607
- const total = recipe.successCount + recipe.failureCount;
9608
- if (total < 5) return 0;
9609
- return recipe.failureCount / total;
9991
+ // ============================================
9992
+ // Paths (sync body lives on disk)
9993
+ // ============================================
9994
+ /** Absolute path to the .ts body for a recipe (used by the runner/MainAgent). */
9995
+ getScriptPath(recipe) {
9996
+ const base = recipe.fileBase || this.toSlug(recipe.name);
9997
+ return path6.join(this.storeDir, `${base}.ts`);
9610
9998
  }
9611
9999
  // ============================================
9612
- // Persistence — one JSON file per script
10000
+ // Internals
9613
10001
  // ============================================
9614
- ensureLoaded() {
9615
- if (!this.loaded) {
9616
- this.loadAllFromDisk();
9617
- this.loaded = true;
10002
+ async removeById(id, requireStatus) {
10003
+ if (!this.store) return;
10004
+ try {
10005
+ const row = await this.store.getById(id);
10006
+ if (!row) return;
10007
+ if (requireStatus && row.status !== requireStatus) return;
10008
+ await this.store.remove(id);
10009
+ this.unlinkBody(row.fileBase);
10010
+ logger.info(`[ScriptStore] Removed "${row.name}" (${id})`);
10011
+ } catch (err) {
10012
+ logger.warn(`[ScriptStore] remove(${id}) failed: ${err}`);
9618
10013
  }
9619
10014
  }
9620
- /**
9621
- * Convert a script name to a safe filename.
9622
- * "Order Status Distribution" → "order-status-distribution"
9623
- */
9624
- toFileName(name) {
9625
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed-script";
10015
+ rowToRecipe(row, body) {
10016
+ return {
10017
+ id: row.id,
10018
+ version: row.version ?? 1,
10019
+ name: row.name,
10020
+ intentDescription: row.intentDescription,
10021
+ tags: row.tags ?? [],
10022
+ sourceIds: row.sourceIds ?? [],
10023
+ tables: row.tables ?? [],
10024
+ parameters: row.parameters ?? [],
10025
+ scriptBody: body,
10026
+ fileBase: row.fileBase,
10027
+ bodyHash: row.bodyHash ?? void 0,
10028
+ projectId: row.projectId ?? void 0,
10029
+ successCount: row.successCount ?? 0,
10030
+ failureCount: row.failureCount ?? 0,
10031
+ lastUsed: row.lastUsed ?? (/* @__PURE__ */ new Date()).toISOString(),
10032
+ createdFrom: row.createdFrom ?? "",
10033
+ createdAt: row.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
10034
+ updatedAt: row.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
10035
+ parentId: row.parentId ?? void 0,
10036
+ forkDepth: typeof row.forkDepth === "number" ? row.forkDepth : 0,
10037
+ forkReason: row.forkReason ?? void 0,
10038
+ components: row.components ?? void 0,
10039
+ status: row.status ?? "verified",
10040
+ turnId: row.turnId ?? void 0,
10041
+ lastError: row.lastError ?? void 0
10042
+ };
9626
10043
  }
9627
- /**
9628
- * Load all script files from disk.
9629
- *
9630
- * Supports two layouts:
9631
- * - New (preferred): metadata/<name>.json + <name>.ts
9632
- * - Legacy: <name>.json with embedded scriptBody (auto-migrated on next save)
9633
- */
9634
- loadAllFromDisk() {
9635
- try {
9636
- if (!fs6.existsSync(this.storeDir)) return;
9637
- const metaDir = this.metadataDir();
9638
- const metaExists = fs6.existsSync(metaDir);
9639
- if (metaExists) {
9640
- const metaFiles = fs6.readdirSync(metaDir).filter((f) => f.endsWith(".json"));
9641
- for (const file of metaFiles) {
9642
- try {
9643
- const metaPath = path6.join(metaDir, file);
9644
- const meta = JSON.parse(fs6.readFileSync(metaPath, "utf-8"));
9645
- if (!meta.id) continue;
9646
- const baseName = file.replace(/\.json$/, "");
9647
- const bodyPath = path6.join(this.storeDir, `${baseName}.ts`);
9648
- if (!fs6.existsSync(bodyPath)) {
9649
- logger.warn(`[ScriptStore] Metadata ${file} has no sibling ${baseName}.ts \u2014 skipping`);
9650
- continue;
9651
- }
9652
- meta.scriptBody = fs6.readFileSync(bodyPath, "utf-8");
9653
- if (typeof meta.forkDepth !== "number") meta.forkDepth = 0;
9654
- if (!meta.status) meta.status = "verified";
9655
- this.recipes.set(meta.id, meta);
9656
- } catch (err) {
9657
- logger.warn(`[ScriptStore] Failed to load metadata ${file}: ${err}`);
9658
- }
9659
- }
9660
- }
9661
- const topLevelJson = fs6.readdirSync(this.storeDir).filter((f) => f.endsWith(".json"));
9662
- for (const file of topLevelJson) {
9663
- try {
9664
- const filePath = path6.join(this.storeDir, file);
9665
- const recipe = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
9666
- if (!recipe.id || !recipe.scriptBody) continue;
9667
- if (this.recipes.has(recipe.id)) continue;
9668
- if (typeof recipe.forkDepth !== "number") recipe.forkDepth = 0;
9669
- if (!recipe.status) recipe.status = "verified";
9670
- this.recipes.set(recipe.id, recipe);
9671
- } catch (err) {
9672
- logger.warn(`[ScriptStore] Failed to load legacy script ${file}: ${err}`);
9673
- }
9674
- }
9675
- if (this.recipes.size > 0) {
9676
- logger.info(`[ScriptStore] Loaded ${this.recipes.size} scripts from ${this.storeDir}`);
10044
+ recipeToRow(recipe) {
10045
+ return {
10046
+ id: recipe.id,
10047
+ projectId: recipe.projectId ?? this.projectId ?? null,
10048
+ version: recipe.version ?? 1,
10049
+ name: recipe.name,
10050
+ intentDescription: recipe.intentDescription,
10051
+ tags: recipe.tags ?? null,
10052
+ createdFrom: recipe.createdFrom ?? null,
10053
+ sourceIds: recipe.sourceIds ?? null,
10054
+ tables: recipe.tables ?? null,
10055
+ parameters: recipe.parameters ?? null,
10056
+ components: recipe.components ?? null,
10057
+ fileBase: recipe.fileBase || this.toSlug(recipe.name),
10058
+ bodyHash: recipe.bodyHash ?? null,
10059
+ successCount: recipe.successCount ?? 0,
10060
+ failureCount: recipe.failureCount ?? 0,
10061
+ lastUsed: recipe.lastUsed ?? null,
10062
+ parentId: recipe.parentId ?? null,
10063
+ forkDepth: typeof recipe.forkDepth === "number" ? recipe.forkDepth : 0,
10064
+ forkReason: recipe.forkReason ?? null,
10065
+ status: recipe.status ?? "verified",
10066
+ turnId: recipe.turnId ?? null,
10067
+ lastError: recipe.lastError ?? null
10068
+ };
10069
+ }
10070
+ /** slug of name, with a short id suffix when the bare slug is already taken. */
10071
+ async computeFileBase(name, id) {
10072
+ const slug = this.toSlug(name);
10073
+ if (this.store) {
10074
+ try {
10075
+ const taken = await this.store.fileBaseTaken(slug, id, this.projectId);
10076
+ if (taken) return `${slug}-${id.slice(-6)}`;
10077
+ } catch {
9677
10078
  }
9678
- } catch (err) {
9679
- logger.warn(`[ScriptStore] Failed to read store directory ${this.storeDir}: ${err}`);
9680
10079
  }
10080
+ return slug;
9681
10081
  }
9682
- /**
9683
- * Save a recipe to disk in split format:
9684
- * metadata/<name>.json (metadata, no scriptBody)
9685
- * <name>.ts (just the function body)
9686
- *
9687
- * If a legacy top-level <name>.json exists for the same recipe, remove it
9688
- * so we don't end up with duplicate sources of truth.
9689
- */
9690
- saveRecipeToDisk(recipe) {
10082
+ toSlug(name) {
10083
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed-script";
10084
+ }
10085
+ hash(body) {
10086
+ return createHash("sha256").update(body).digest("hex");
10087
+ }
10088
+ bodyPath(fileBase) {
10089
+ return path6.join(this.storeDir, `${fileBase}.ts`);
10090
+ }
10091
+ readBody(fileBase) {
9691
10092
  try {
9692
- const metaDir = this.metadataDir();
9693
- if (!fs6.existsSync(metaDir)) {
9694
- fs6.mkdirSync(metaDir, { recursive: true });
9695
- }
9696
- const baseName = this.fileBaseName(recipe);
9697
- const metaPath = path6.join(metaDir, `${baseName}.json`);
9698
- const bodyPath = path6.join(this.storeDir, `${baseName}.ts`);
9699
- const { scriptBody, ...meta } = recipe;
9700
- fs6.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
9701
- fs6.writeFileSync(bodyPath, normalizeScriptBody(scriptBody));
9702
- const legacyPath = path6.join(this.storeDir, `${this.toFileName(recipe.name)}.json`);
9703
- if (fs6.existsSync(legacyPath)) {
9704
- try {
9705
- fs6.unlinkSync(legacyPath);
9706
- } catch {
9707
- }
9708
- }
10093
+ return fs6.readFileSync(this.bodyPath(fileBase), "utf-8");
9709
10094
  } catch (err) {
9710
- logger.warn(`[ScriptStore] Failed to save script "${recipe.name}": ${err}`);
10095
+ logger.warn(`[ScriptStore] body file ${fileBase}.ts missing/unreadable: ${err}`);
10096
+ return "";
9711
10097
  }
9712
10098
  }
9713
- /**
9714
- * Delete a recipe's files from disk (both metadata + body, plus any legacy file).
9715
- */
9716
- deleteRecipeFromDisk(recipe) {
9717
- const baseName = this.fileBaseName(recipe);
9718
- const slug = this.toFileName(recipe.name);
9719
- const candidates = [
9720
- path6.join(this.metadataDir(), `${baseName}.json`),
9721
- path6.join(this.storeDir, `${baseName}.ts`),
9722
- path6.join(this.storeDir, `${slug}.json`)
9723
- // legacy single-file format
9724
- ];
9725
- for (const p of candidates) {
9726
- try {
9727
- if (fs6.existsSync(p)) fs6.unlinkSync(p);
9728
- } catch (err) {
9729
- logger.warn(`[ScriptStore] Failed to delete ${p}: ${err}`);
9730
- }
10099
+ /** Atomic body write (temp + rename) so concurrent reads never see a partial file. */
10100
+ writeBody(fileBase, normalizedBody) {
10101
+ if (!fs6.existsSync(this.storeDir)) fs6.mkdirSync(this.storeDir, { recursive: true });
10102
+ const finalPath = this.bodyPath(fileBase);
10103
+ const tmpPath = `${finalPath}.tmp-${randomBytes(4).toString("hex")}`;
10104
+ fs6.writeFileSync(tmpPath, normalizedBody);
10105
+ fs6.renameSync(tmpPath, finalPath);
10106
+ }
10107
+ unlinkBody(fileBase) {
10108
+ try {
10109
+ const p = this.bodyPath(fileBase);
10110
+ if (fs6.existsSync(p)) fs6.unlinkSync(p);
10111
+ } catch (err) {
10112
+ logger.warn(`[ScriptStore] failed to delete body ${fileBase}.ts: ${err}`);
9731
10113
  }
9732
10114
  }
9733
10115
  };
@@ -9744,14 +10126,14 @@ var ScriptMatcher = class {
9744
10126
  * Returns null if no script matches.
9745
10127
  */
9746
10128
  async match(userPrompt, apiKey, model) {
9747
- const recipes = this.store.getAll();
10129
+ const recipes = await this.store.search(userPrompt);
9748
10130
  if (recipes.length === 0) return null;
9749
10131
  const healthyRecipes = recipes.filter((r) => {
9750
10132
  const total = r.successCount + r.failureCount;
9751
10133
  return total < 5 || r.failureCount / total <= MAX_FAILURE_RATE;
9752
10134
  });
9753
10135
  if (healthyRecipes.length === 0) {
9754
- logger.info(`[ScriptMatcher] All ${recipes.length} scripts have high failure rates \u2014 skipping`);
10136
+ logger.info(`[ScriptMatcher] No healthy candidates among ${recipes.length} FTS matches \u2014 skipping`);
9755
10137
  return null;
9756
10138
  }
9757
10139
  const scriptCatalog = this.buildScriptCatalog(healthyRecipes);
@@ -9787,7 +10169,7 @@ var ScriptMatcher = class {
9787
10169
  logger.info(`[ScriptMatcher] No match. Reason: ${reasoning}`);
9788
10170
  return null;
9789
10171
  }
9790
- const recipe = this.store.get(response.scriptId);
10172
+ const recipe = await this.store.get(response.scriptId);
9791
10173
  if (!recipe) {
9792
10174
  logger.warn(`[ScriptMatcher] LLM returned unknown script ID: "${response.scriptId}"`);
9793
10175
  return null;
@@ -9810,61 +10192,360 @@ var ScriptMatcher = class {
9810
10192
  reasoning
9811
10193
  };
9812
10194
  }
9813
- logger.info(`[ScriptMatcher] Matched "${recipe.name}" (tier: high). Reason: ${reasoning}`);
9814
- return {
9815
- recipe,
9816
- tier: "high",
9817
- similarity: 0.95,
9818
- confidence: "high",
9819
- extractedParams: response.params || {},
9820
- reasoning
9821
- };
9822
- } catch (error) {
9823
- const msg = error instanceof Error ? error.message : String(error);
9824
- logger.error(`[ScriptMatcher] LLM matching failed: ${msg}`);
9825
- return null;
9826
- }
10195
+ logger.info(`[ScriptMatcher] Matched "${recipe.name}" (tier: high). Reason: ${reasoning}`);
10196
+ return {
10197
+ recipe,
10198
+ tier: "high",
10199
+ similarity: 0.95,
10200
+ confidence: "high",
10201
+ extractedParams: response.params || {},
10202
+ reasoning
10203
+ };
10204
+ } catch (error) {
10205
+ const msg = error instanceof Error ? error.message : String(error);
10206
+ logger.error(`[ScriptMatcher] LLM matching failed: ${msg}`);
10207
+ return null;
10208
+ }
10209
+ }
10210
+ /**
10211
+ * Build the script catalog string for the LLM prompt.
10212
+ * Each script gets: index, ID, name, description, and parameter definitions.
10213
+ */
10214
+ buildScriptCatalog(recipes) {
10215
+ return recipes.map((r, idx) => {
10216
+ const paramList = r.parameters.length > 0 ? r.parameters.map((p) => {
10217
+ let desc = `${p.name} (${p.type}`;
10218
+ if (!p.required) desc += ", optional";
10219
+ if (p.default !== void 0) desc += `, default: ${JSON.stringify(p.default)}`;
10220
+ if (p.enumValues) desc += `, values: ${Object.keys(p.enumValues).join("/")}`;
10221
+ desc += `) \u2014 ${p.description}`;
10222
+ return ` - ${desc}`;
10223
+ }).join("\n") : " (no parameters)";
10224
+ return `${idx + 1}. [${r.id}] "${r.name}"
10225
+ ${r.intentDescription}
10226
+ Parameters:
10227
+ ${paramList}`;
10228
+ }).join("\n\n");
10229
+ }
10230
+ };
10231
+
10232
+ // src/userResponse/scripts/script-component-generator.ts
10233
+ function inferColumns(rows) {
10234
+ const cols = /* @__PURE__ */ new Map();
10235
+ if (!Array.isArray(rows) || rows.length === 0) return cols;
10236
+ const sample = rows.slice(0, 20);
10237
+ const keys = /* @__PURE__ */ new Set();
10238
+ for (const r of sample) {
10239
+ if (r && typeof r === "object") for (const k of Object.keys(r)) keys.add(k);
10240
+ }
10241
+ for (const key of keys) {
10242
+ let type = "TEXT";
10243
+ let typed = false;
10244
+ const distinct = /* @__PURE__ */ new Set();
10245
+ for (const r of sample) {
10246
+ const v = r?.[key];
10247
+ if (v === null || v === void 0) continue;
10248
+ if (!typed) {
10249
+ if (typeof v === "number") type = "NUMBER";
10250
+ else if (typeof v === "boolean") type = "BOOLEAN";
10251
+ else if (v instanceof Date) type = "DATE";
10252
+ else if (typeof v === "string" && v.trim() !== "" && !isNaN(Number(v))) type = "NUMBER";
10253
+ else type = "TEXT";
10254
+ typed = true;
10255
+ }
10256
+ distinct.add(v instanceof Date ? v.toISOString() : String(v));
10257
+ }
10258
+ cols.set(key.toLowerCase(), { name: key, type, invariant: distinct.size <= 1 });
10259
+ }
10260
+ return cols;
10261
+ }
10262
+ function resolveCol(cols, key) {
10263
+ if (key == null) return void 0;
10264
+ return cols.get(String(key).toLowerCase());
10265
+ }
10266
+ function firstOfType(cols, type) {
10267
+ for (const c of cols.values()) if (c.type === type) return c;
10268
+ return void 0;
10269
+ }
10270
+ var ALL_KEY_NAMES = ["xAxisKey", "yAxisKey", "valueKey", "nameKey", "seriesKey", "groupBy", "aggregationField", "categoryKey"];
10271
+ var AVG_KEYWORDS = /\b(average|avg|mean)\b/i;
10272
+ var COUNT_KEYWORDS = /\b(count|number of|how many|# of|no\.? of)\b/i;
10273
+ var SUM_KEYWORDS = /\b(total|sum|overall|combined|aggregate|all)\b/i;
10274
+ function validateAndRepairComponent(comp, componentType, cols, rowCount) {
10275
+ if (cols.size === 0) return comp;
10276
+ const props = comp.props || (comp.props = {});
10277
+ const cfg = props.config || (props.config = {});
10278
+ for (const k of ALL_KEY_NAMES) {
10279
+ if (cfg[k] == null) continue;
10280
+ const r = resolveCol(cols, cfg[k]);
10281
+ if (r) cfg[k] = r.name;
10282
+ else delete cfg[k];
10283
+ }
10284
+ if (Array.isArray(cfg.metricKeys)) {
10285
+ cfg.metricKeys = cfg.metricKeys.map((m) => resolveCol(cols, m)?.name).filter(Boolean);
10286
+ if (cfg.metricKeys.length === 0) delete cfg.metricKeys;
10287
+ }
10288
+ const numeric = () => firstOfType(cols, "NUMBER");
10289
+ const category = () => firstOfType(cols, "TEXT") || firstOfType(cols, "DATE");
10290
+ const hasMetrics = Array.isArray(cfg.metricKeys) && cfg.metricKeys.length > 0;
10291
+ switch (componentType) {
10292
+ case "KPICard":
10293
+ case "GaugeChart": {
10294
+ const vk = resolveCol(cols, cfg.valueKey) || numeric() || category();
10295
+ if (!vk) return null;
10296
+ cfg.valueKey = vk.name;
10297
+ const title = String(props.title || "");
10298
+ if (rowCount > 1 && !cfg.aggregation) {
10299
+ if (COUNT_KEYWORDS.test(title)) cfg.aggregation = "count";
10300
+ else if (!vk.invariant) {
10301
+ if (AVG_KEYWORDS.test(title)) cfg.aggregation = "avg";
10302
+ else if (SUM_KEYWORDS.test(title)) cfg.aggregation = "sum";
10303
+ }
10304
+ }
10305
+ if (cfg.aggregation === "sum" || cfg.aggregation === "avg") {
10306
+ const af = resolveCol(cols, cfg.aggregationField);
10307
+ if (af && af.type === "NUMBER") {
10308
+ cfg.aggregationField = af.name;
10309
+ } else {
10310
+ const n = vk.type === "NUMBER" ? vk : numeric();
10311
+ if (n) cfg.aggregationField = n.name;
10312
+ else {
10313
+ delete cfg.aggregation;
10314
+ delete cfg.aggregationField;
10315
+ }
10316
+ }
10317
+ }
10318
+ return comp;
10319
+ }
10320
+ case "PieChart":
10321
+ case "TreemapChart": {
10322
+ let nameC = resolveCol(cols, cfg.nameKey);
10323
+ let valC = resolveCol(cols, cfg.valueKey);
10324
+ if (nameC?.type === "NUMBER" && valC && valC.type !== "NUMBER") {
10325
+ const t = nameC;
10326
+ nameC = valC;
10327
+ valC = t;
10328
+ }
10329
+ if (!nameC) nameC = category();
10330
+ if (!valC || valC.type !== "NUMBER") valC = numeric() || valC;
10331
+ if (!nameC || !valC) return null;
10332
+ cfg.nameKey = nameC.name;
10333
+ cfg.valueKey = valC.name;
10334
+ return comp;
10335
+ }
10336
+ case "WaterfallChart": {
10337
+ const catC = resolveCol(cols, cfg.categoryKey) || category();
10338
+ let valC = resolveCol(cols, cfg.valueKey);
10339
+ if (!valC || valC.type !== "NUMBER") valC = numeric() || valC;
10340
+ if (!catC || !valC) return null;
10341
+ cfg.categoryKey = catC.name;
10342
+ cfg.valueKey = valC.name;
10343
+ return comp;
10344
+ }
10345
+ case "ScatterChart": {
10346
+ let xC = resolveCol(cols, cfg.xAxisKey);
10347
+ let yC = resolveCol(cols, cfg.yAxisKey);
10348
+ if (!xC || xC.type !== "NUMBER") xC = numeric() || xC;
10349
+ if (!yC || yC.type !== "NUMBER") {
10350
+ yC = [...cols.values()].find((c) => c.type === "NUMBER" && c.name !== xC?.name) || numeric() || yC;
10351
+ }
10352
+ if (!xC || !yC) return null;
10353
+ cfg.xAxisKey = xC.name;
10354
+ cfg.yAxisKey = yC.name;
10355
+ return comp;
10356
+ }
10357
+ case "HeatmapChart": {
10358
+ const xC = resolveCol(cols, cfg.xAxisKey) || category();
10359
+ const yC = resolveCol(cols, cfg.yAxisKey) || [...cols.values()].find((c) => (c.type === "TEXT" || c.type === "DATE") && c.name !== xC?.name);
10360
+ let valC = resolveCol(cols, cfg.valueKey);
10361
+ if (!valC || valC.type !== "NUMBER") valC = numeric() || valC;
10362
+ if (!xC || !yC || !valC) return null;
10363
+ cfg.xAxisKey = xC.name;
10364
+ cfg.yAxisKey = yC.name;
10365
+ cfg.valueKey = valC.name;
10366
+ return comp;
10367
+ }
10368
+ case "BarChart":
10369
+ case "LineChart": {
10370
+ let xC = resolveCol(cols, cfg.xAxisKey);
10371
+ let yC = resolveCol(cols, cfg.yAxisKey);
10372
+ if (!hasMetrics && xC?.type === "NUMBER" && yC && yC.type !== "NUMBER") {
10373
+ const t = xC;
10374
+ xC = yC;
10375
+ yC = t;
10376
+ }
10377
+ if (!xC) xC = (componentType === "LineChart" ? firstOfType(cols, "DATE") || category() : category()) || firstOfType(cols, "NUMBER");
10378
+ if (!xC) return null;
10379
+ cfg.xAxisKey = xC.name;
10380
+ if (!hasMetrics) {
10381
+ if (!yC || yC.type !== "NUMBER") yC = numeric();
10382
+ if (!yC) return null;
10383
+ cfg.yAxisKey = yC.name;
10384
+ }
10385
+ if (cfg.seriesKey) {
10386
+ const sC = resolveCol(cols, cfg.seriesKey);
10387
+ if (!sC || sC.type !== "TEXT" || sC.name === cfg.xAxisKey) delete cfg.seriesKey;
10388
+ else cfg.seriesKey = sC.name;
10389
+ }
10390
+ return comp;
10391
+ }
10392
+ case "RadarChart": {
10393
+ if (!hasMetrics) {
10394
+ const vk = resolveCol(cols, cfg.valueKey) || numeric();
10395
+ if (!vk) return null;
10396
+ cfg.valueKey = vk.name;
10397
+ }
10398
+ return comp;
10399
+ }
10400
+ default:
10401
+ return comp;
10402
+ }
10403
+ }
10404
+ function scriptCompKey(comp) {
10405
+ const id = comp?.componentId || comp?.componentName || "";
10406
+ const src = comp?.sourceIndex ?? "";
10407
+ return `${id}|${src}|${JSON.stringify(comp?.props?.config ?? comp?.config ?? {})}`;
10408
+ }
10409
+ function extractAnswerComponentObject(text) {
10410
+ const hasMatch = text.match(/"hasAnswerComponent"\s*:\s*(true|false)/);
10411
+ if (!hasMatch || hasMatch[1] !== "true") return null;
10412
+ const startMatch = text.match(/"answerComponent"\s*:\s*\{/);
10413
+ if (!startMatch) return null;
10414
+ const startPos = startMatch.index + startMatch[0].length - 1;
10415
+ let depth = 0, inString = false, escapeNext = false, endPos = -1;
10416
+ for (let i = startPos; i < text.length; i++) {
10417
+ const c = text[i];
10418
+ if (escapeNext) {
10419
+ escapeNext = false;
10420
+ continue;
10421
+ }
10422
+ if (c === "\\") {
10423
+ escapeNext = true;
10424
+ continue;
10425
+ }
10426
+ if (c === '"') {
10427
+ inString = !inString;
10428
+ continue;
10429
+ }
10430
+ if (!inString) {
10431
+ if (c === "{") depth++;
10432
+ else if (c === "}") {
10433
+ depth--;
10434
+ if (depth === 0) {
10435
+ endPos = i + 1;
10436
+ break;
10437
+ }
10438
+ }
10439
+ }
10440
+ }
10441
+ if (endPos <= startPos) return null;
10442
+ try {
10443
+ return JSON.parse(text.substring(startPos, endPos));
10444
+ } catch {
10445
+ return null;
10446
+ }
10447
+ }
10448
+ async function buildScriptComponent(comp, ctx) {
10449
+ const { queries, queryIds, columnsByIndex, availableComponents } = ctx;
10450
+ let validationCols = /* @__PURE__ */ new Map();
10451
+ let validationRowCount = 0;
10452
+ let specSourceRef = "";
10453
+ const wantedId = comp.componentId || "";
10454
+ const wantedName = comp.componentName || "";
10455
+ const template = availableComponents.find(
10456
+ (c) => c.id === wantedId || c.name === wantedId || c.id === wantedName || c.name === wantedName
10457
+ );
10458
+ if (!template) {
10459
+ logger.warn(`[ScriptComponentGen] Component "${wantedId || wantedName}" not found \u2014 skipping`);
10460
+ return null;
10461
+ }
10462
+ if (template.type === "MarkdownBlock") {
10463
+ const p = comp.props || {};
10464
+ const content = p.content ?? p.config?.content ?? p.config?.markdown ?? p.config?.text ?? "";
10465
+ return {
10466
+ component: {
10467
+ id: `comp_${Math.random().toString(36).substring(2, 8)}`,
10468
+ name: template.name,
10469
+ displayName: template.displayName,
10470
+ type: template.type,
10471
+ description: template.description,
10472
+ props: { title: p.title, description: p.description, content },
10473
+ category: template.category,
10474
+ keywords: template.keywords
10475
+ },
10476
+ // Persist a content-bearing spec so the markdown survives deterministic
10477
+ // replay (it has no data source, so its text must be stored — otherwise
10478
+ // re-running a matched script rebuilds only the source-bound components
10479
+ // and silently drops the narrative).
10480
+ spec: {
10481
+ componentType: template.name,
10482
+ sourceRef: "markdown",
10483
+ title: p.title,
10484
+ description: p.description,
10485
+ content,
10486
+ config: {}
10487
+ }
10488
+ };
9827
10489
  }
9828
- /**
9829
- * Build the script catalog string for the LLM prompt.
9830
- * Each script gets: index, ID, name, description, and parameter definitions.
9831
- */
9832
- buildScriptCatalog(recipes) {
9833
- return recipes.map((r, idx) => {
9834
- const paramList = r.parameters.length > 0 ? r.parameters.map((p) => {
9835
- let desc = `${p.name} (${p.type}`;
9836
- if (!p.required) desc += ", optional";
9837
- if (p.default !== void 0) desc += `, default: ${JSON.stringify(p.default)}`;
9838
- if (p.enumValues) desc += `, values: ${Object.keys(p.enumValues).join("/")}`;
9839
- desc += `) \u2014 ${p.description}`;
9840
- return ` - ${desc}`;
9841
- }).join("\n") : " (no parameters)";
9842
- return `${idx + 1}. [${r.id}] "${r.name}"
9843
- ${r.intentDescription}
9844
- Parameters:
9845
- ${paramList}`;
9846
- }).join("\n\n");
10490
+ const sourceIdx = typeof comp.sourceIndex === "number" ? comp.sourceIndex : 0;
10491
+ const targetQuery = queries[sourceIdx] || queries[0];
10492
+ const targetQueryId = queryIds[targetQuery.sourceId];
10493
+ if (!targetQueryId) {
10494
+ logger.warn(`[ScriptComponentGen] No queryId for source "${targetQuery.sourceName}" \u2014 skipping`);
10495
+ return null;
9847
10496
  }
9848
- };
9849
-
9850
- // src/userResponse/scripts/script-component-generator.ts
10497
+ validationCols = columnsByIndex[sourceIdx] || inferColumns(targetQuery.data);
10498
+ validationRowCount = targetQuery.count ?? (targetQuery.data?.length || 0);
10499
+ specSourceRef = targetQuery.sourceId;
10500
+ const renderedToolId = targetQuery.virtual ? "script_dataset" : targetQuery.sourceId;
10501
+ const externalToolProp = { toolId: renderedToolId, toolName: targetQuery.sourceName, parameters: { queryId: targetQueryId } };
10502
+ const repaired = validateAndRepairComponent(comp, template.type, validationCols, validationRowCount);
10503
+ if (!repaired) {
10504
+ logger.warn(`[ScriptComponentGen] Component "${template.name}" dropped \u2014 config keys could not be mapped to the data columns`);
10505
+ return null;
10506
+ }
10507
+ if (template.type === "DataTable" && Array.isArray(comp.props?.config?.columns)) {
10508
+ comp.props.config.columns = comp.props.config.columns.map(
10509
+ (c) => typeof c === "string" ? { key: c, label: c.replace(/[_-]+/g, " ").replace(/\b\w/g, (m) => m.toUpperCase()) } : c
10510
+ );
10511
+ }
10512
+ const component = {
10513
+ id: `comp_${Math.random().toString(36).substring(2, 8)}`,
10514
+ name: template.name,
10515
+ displayName: template.displayName,
10516
+ type: template.type,
10517
+ description: template.description,
10518
+ props: { ...comp.props, externalTool: externalToolProp },
10519
+ category: template.category,
10520
+ keywords: template.keywords
10521
+ };
10522
+ const spec = specSourceRef ? {
10523
+ componentType: template.name,
10524
+ sourceRef: specSourceRef,
10525
+ title: comp.props?.title,
10526
+ description: comp.props?.description,
10527
+ config: { ...comp.props?.config || {} }
10528
+ } : void 0;
10529
+ return { component, spec };
10530
+ }
9851
10531
  async function generateScriptComponents(params) {
9852
- const { userPrompt, scriptResult, queryIds, availableComponents, apiKey, model, componentStreamCallback, externalTools, collections } = params;
10532
+ const { userPrompt, scriptResult, queryIds, availableComponents, apiKey, model, componentStreamCallback, analysisText } = params;
9853
10533
  const queries = scriptResult.executedQueries;
9854
10534
  if (!queries || queries.length === 0) {
9855
10535
  logger.warn(`[ScriptComponentGen] No query data available \u2014 returning empty`);
9856
- return { components: [], layoutTitle: "", layoutDescription: "", actions: [] };
10536
+ return { components: [], layoutTitle: "", layoutDescription: "", actions: [], componentSpecs: [] };
9857
10537
  }
9858
10538
  logger.info(`[ScriptComponentGen] Starting | ${queries.length} queries | ${availableComponents.length} available components`);
9859
- const availableCompsText = availableComponents.filter((c) => c.name !== "DynamicMarkdownBlock").map((c) => `- ${c.name} (${c.type}): ${c.description || ""}`).join("\n");
10539
+ const columnsByIndex = queries.map((q) => {
10540
+ const data = q.data && q.data.length ? q.data : q.virtual ? scriptResult.data : q.data;
10541
+ return inferColumns(data || []);
10542
+ });
10543
+ const availableCompsText = availableComponents.map((c) => `- ${c.name} (${c.type}): ${c.description || ""}`).join("\n");
9860
10544
  const sourceDescriptions = queries.map((q, idx) => {
9861
10545
  if (!q.data || q.data.length === 0) return null;
9862
- const sampleRow = q.data[0];
9863
- const columns = Object.entries(sampleRow).map(([key, value]) => {
9864
- const type = typeof value === "number" ? "NUMBER" : value instanceof Date ? "DATE" : typeof value === "boolean" ? "BOOLEAN" : "TEXT";
9865
- return `${key} (${type})`;
9866
- }).join(", ");
9867
- const sampleData = JSON.stringify(q.data.slice(0, 3), null, 2);
10546
+ const cols = columnsByIndex[idx];
10547
+ const columns = cols && cols.size > 0 ? Array.from(cols.values()).map((c) => `${c.name} (${c.type})`).join(", ") : Object.keys(q.data[0]).join(", ");
10548
+ const sampleData = JSON.stringify(q.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS), null, 2);
9868
10549
  return `### Source ${idx}: "${q.sourceName}" (sourceIndex: ${idx})
9869
10550
  - Rows: ${q.count}${q.totalCount && q.totalCount > q.count ? ` of ${q.totalCount} total` : ""}
9870
10551
  - Columns: ${columns}
@@ -9873,19 +10554,6 @@ async function generateScriptComponents(params) {
9873
10554
  ${sampleData}
9874
10555
  \`\`\``;
9875
10556
  }).filter(Boolean).join("\n\n");
9876
- const isMultiSource = queries.length > 1;
9877
- const federationTool = externalTools?.find((t) => t.id === "federation_query");
9878
- let federationContext = "";
9879
- if (isMultiSource && federationTool) {
9880
- federationContext = `### Cross-Source Federation Available
9881
- You can create components that JOIN data from multiple sources using DuckDB SQL.
9882
- Set sourceIndex to "federation" and provide federationSql.
9883
- ${federationTool.fullSchema || federationTool.description || "Use schema-qualified table names."}`;
9884
- } else if (isMultiSource) {
9885
- federationContext = `### Multiple Sources Available
9886
- Each component can use data from one source (specify sourceIndex: 0, 1, etc.).
9887
- Cross-source JOINs are not available for this project.`;
9888
- }
9889
10557
  try {
9890
10558
  const prompts = await promptLoader.loadPrompts("script-components", {
9891
10559
  USER_PROMPT: userPrompt,
@@ -9894,95 +10562,98 @@ Cross-source JOINs are not available for this project.`;
9894
10562
  ROW_COUNT: queries.map((q) => `${q.sourceName}: ${q.count}`).join(", "),
9895
10563
  COLUMN_DESCRIPTIONS: sourceDescriptions,
9896
10564
  SAMPLE_DATA: "",
9897
- FEDERATION_CONTEXT: federationContext
10565
+ ANALYSIS_TEXT: analysisText && analysisText.trim() ? analysisText.trim() : "No analysis text provided \u2014 infer intent from the user question and the returned data."
9898
10566
  });
10567
+ const ctx = {
10568
+ queries,
10569
+ queryIds,
10570
+ columnsByIndex,
10571
+ availableComponents
10572
+ };
10573
+ let fullText = "";
10574
+ let answerAttempted = false;
10575
+ let streamedAnswerKey = null;
10576
+ const partial = componentStreamCallback ? (chunk) => {
10577
+ fullText += chunk;
10578
+ if (answerAttempted) return;
10579
+ const answerComp = extractAnswerComponentObject(fullText);
10580
+ if (!answerComp) return;
10581
+ answerAttempted = true;
10582
+ streamedAnswerKey = scriptCompKey(answerComp);
10583
+ buildScriptComponent(answerComp, ctx).then((built) => {
10584
+ if (built) {
10585
+ componentStreamCallback(built.component);
10586
+ logger.info("[ScriptComponentGen] Streamed answer component early");
10587
+ } else {
10588
+ streamedAnswerKey = null;
10589
+ }
10590
+ }).catch(() => {
10591
+ streamedAnswerKey = null;
10592
+ });
10593
+ } : void 0;
9899
10594
  const response = await LLM.stream(
9900
10595
  { sys: prompts.system, user: prompts.user },
9901
10596
  {
9902
10597
  model: model || void 0,
9903
- maxTokens: 1500,
10598
+ maxTokens: 5e3,
9904
10599
  temperature: 0,
9905
- apiKey
10600
+ apiKey,
10601
+ partial
9906
10602
  },
9907
10603
  true
9908
10604
  );
9909
10605
  if (!response || !response.components || !Array.isArray(response.components)) {
9910
10606
  logger.warn(`[ScriptComponentGen] LLM returned invalid response`);
9911
- return { components: [], layoutTitle: "", layoutDescription: "", actions: [] };
10607
+ return { components: [], layoutTitle: "", layoutDescription: "", actions: [], componentSpecs: [] };
9912
10608
  }
9913
10609
  const layoutTitle = response.layoutTitle || "Dashboard";
9914
10610
  const layoutDescription = response.layoutDescription || "";
9915
10611
  const matchedComponents = [];
10612
+ const componentSpecs = [];
9916
10613
  for (const comp of response.components) {
9917
- const wantedId = comp.componentId || "";
9918
- const wantedName = comp.componentName || "";
9919
- const template = availableComponents.find(
9920
- (c) => c.id === wantedId || c.name === wantedId || c.id === wantedName || c.name === wantedName
9921
- );
9922
- if (!template) {
9923
- logger.warn(`[ScriptComponentGen] Component "${wantedId || wantedName}" not found \u2014 skipping`);
9924
- continue;
9925
- }
9926
- let externalToolProp;
9927
- if (comp.sourceIndex === "federation" && comp.federationSql) {
9928
- if (!isMultiSource || !federationTool) {
9929
- logger.warn(
9930
- `[ScriptComponentGen] Component "${comp.componentName || comp.componentId}" requested federation, but ${!isMultiSource ? "only one source query exists" : "no federation_query tool is registered"} \u2014 falling back to single-source. Computed columns from script's JS post-processing will not appear in this component.`
9931
- );
9932
- comp.sourceIndex = 0;
9933
- delete comp.federationSql;
9934
- } else {
9935
- logger.info(`[ScriptComponentGen] Federation component \u2014 executing cross-source SQL`);
9936
- try {
9937
- const fedResult = await executeFederationQuery(comp.federationSql, externalTools, collections);
9938
- if (fedResult) {
9939
- const fedQueryId = queryCache.storeQuery(comp.federationSql, fedResult);
9940
- externalToolProp = {
9941
- toolId: "federation_query",
9942
- toolName: "Cross-Source Query",
9943
- parameters: { queryId: fedQueryId }
9944
- };
9945
- } else {
9946
- logger.warn(`[ScriptComponentGen] Federation query failed \u2014 skipping component`);
9947
- continue;
9948
- }
9949
- } catch (fedErr) {
9950
- logger.warn(`[ScriptComponentGen] Federation query error: ${fedErr} \u2014 skipping component`);
9951
- continue;
9952
- }
10614
+ const built = await buildScriptComponent(comp, ctx);
10615
+ if (!built) continue;
10616
+ matchedComponents.push(built.component);
10617
+ if (built.spec) componentSpecs.push(built.spec);
10618
+ if (componentStreamCallback && scriptCompKey(comp) !== streamedAnswerKey) {
10619
+ componentStreamCallback(built.component);
10620
+ }
10621
+ }
10622
+ if (response.hasAnswerComponent && response.answerComponent) {
10623
+ const aKey = scriptCompKey(response.answerComponent);
10624
+ const alreadyIncluded = response.components.some((c) => scriptCompKey(c) === aKey);
10625
+ if (!alreadyIncluded) {
10626
+ const built = await buildScriptComponent(response.answerComponent, ctx);
10627
+ if (built) {
10628
+ matchedComponents.unshift(built.component);
10629
+ if (built.spec) componentSpecs.unshift(built.spec);
10630
+ if (componentStreamCallback && aKey !== streamedAnswerKey) componentStreamCallback(built.component);
9953
10631
  }
9954
10632
  }
9955
- if (!externalToolProp) {
9956
- const sourceIdx = typeof comp.sourceIndex === "number" ? comp.sourceIndex : 0;
9957
- const targetQuery = queries[sourceIdx] || queries[0];
9958
- const targetQueryId = queryIds[targetQuery.sourceId];
9959
- if (!targetQueryId) {
9960
- logger.warn(`[ScriptComponentGen] No queryId for source "${targetQuery.sourceName}" \u2014 skipping`);
9961
- continue;
9962
- }
9963
- const renderedToolId = targetQuery.virtual ? "script_dataset" : targetQuery.sourceId;
9964
- externalToolProp = {
9965
- toolId: renderedToolId,
9966
- toolName: targetQuery.sourceName,
9967
- parameters: { queryId: targetQueryId }
10633
+ }
10634
+ const hasMarkdown = matchedComponents.some((c) => c.type === "MarkdownBlock");
10635
+ if (!hasMarkdown && analysisText && analysisText.trim()) {
10636
+ const mdTemplate = availableComponents.find((c) => c.type === "MarkdownBlock" || c.name === "DynamicMarkdownBlock");
10637
+ if (mdTemplate) {
10638
+ const mdComponent = {
10639
+ id: `comp_${Math.random().toString(36).substring(2, 8)}`,
10640
+ name: mdTemplate.name,
10641
+ displayName: mdTemplate.displayName,
10642
+ type: mdTemplate.type,
10643
+ description: mdTemplate.description,
10644
+ props: { content: analysisText.trim() },
10645
+ category: mdTemplate.category,
10646
+ keywords: mdTemplate.keywords
9968
10647
  };
9969
- }
9970
- const component = {
9971
- id: `comp_${Math.random().toString(36).substring(2, 8)}`,
9972
- name: template.name,
9973
- displayName: template.displayName,
9974
- type: template.type,
9975
- description: template.description,
9976
- props: {
9977
- ...comp.props,
9978
- externalTool: externalToolProp
9979
- },
9980
- category: template.category,
9981
- keywords: template.keywords
9982
- };
9983
- matchedComponents.push(component);
9984
- if (componentStreamCallback) {
9985
- componentStreamCallback(component);
10648
+ matchedComponents.push(mdComponent);
10649
+ componentSpecs.push({
10650
+ componentType: mdTemplate.name,
10651
+ sourceRef: "markdown",
10652
+ content: analysisText.trim(),
10653
+ config: {}
10654
+ });
10655
+ if (componentStreamCallback) componentStreamCallback(mdComponent);
10656
+ logger.info("[ScriptComponentGen] Injected fallback DynamicMarkdownBlock (LLM omitted one)");
9986
10657
  }
9987
10658
  }
9988
10659
  logger.info(`[ScriptComponentGen] Generated ${matchedComponents.length} components`);
@@ -9996,37 +10667,97 @@ Cross-source JOINs are not available for this project.`;
9996
10667
  components: matchedComponents,
9997
10668
  layoutTitle,
9998
10669
  layoutDescription,
9999
- actions
10670
+ actions,
10671
+ componentSpecs
10000
10672
  };
10001
10673
  } catch (error) {
10002
10674
  const msg = error instanceof Error ? error.message : String(error);
10003
10675
  logger.error(`[ScriptComponentGen] Failed: ${msg}`);
10004
- return { components: [], layoutTitle: "", layoutDescription: "", actions: [] };
10676
+ return { components: [], layoutTitle: "", layoutDescription: "", actions: [], componentSpecs: [] };
10005
10677
  }
10006
10678
  }
10007
- async function executeFederationQuery(sql, externalTools, collections) {
10008
- const fedTool = externalTools?.find((t) => t.id === "federation_query");
10009
- if (fedTool) {
10010
- try {
10011
- const result = await fedTool.fn({ sql });
10012
- return result;
10013
- } catch (err) {
10014
- logger.warn(`[ScriptComponentGen] Federation tool execution failed: ${err}`);
10679
+ function assembleComponentsFromSpecs(params) {
10680
+ const { specs, scriptResult, queryIds, availableComponents, componentStreamCallback, analysisText } = params;
10681
+ if (!specs || specs.length === 0) return null;
10682
+ const queries = scriptResult.executedQueries || [];
10683
+ const byId = new Map(queries.map((q) => [q.sourceId, q]));
10684
+ const out = [];
10685
+ let answerComponentStreamed = false;
10686
+ const mdTemplate = availableComponents.find((c) => c.type === "MarkdownBlock" || c.name === "DynamicMarkdownBlock");
10687
+ const buildMarkdown = (content, title, description) => {
10688
+ if (!mdTemplate || !content || !content.trim()) return null;
10689
+ return {
10690
+ id: `comp_${Math.random().toString(36).substring(2, 8)}`,
10691
+ name: mdTemplate.name,
10692
+ displayName: mdTemplate.displayName,
10693
+ type: mdTemplate.type,
10694
+ description: mdTemplate.description,
10695
+ props: { title, description, content: content.trim() },
10696
+ category: mdTemplate.category,
10697
+ keywords: mdTemplate.keywords
10698
+ };
10699
+ };
10700
+ for (const spec of specs) {
10701
+ if (spec.sourceRef === "federation") {
10702
+ logger.info("[ScriptComponentGen] Recipe has a federation spec \u2014 not replayable deterministically, falling back to LLM");
10703
+ return null;
10704
+ }
10705
+ if (spec.sourceRef === "markdown") {
10706
+ const md = buildMarkdown(spec.content || analysisText || "", spec.title, spec.description);
10707
+ if (md) {
10708
+ out.push(md);
10709
+ if (componentStreamCallback) componentStreamCallback(md);
10710
+ }
10711
+ continue;
10712
+ }
10713
+ const q = byId.get(spec.sourceRef) || queries.find((x) => x.virtual);
10714
+ if (!q || !q.data || q.data.length === 0) {
10715
+ logger.info(`[ScriptComponentGen] Recipe spec source "${spec.sourceRef}" missing/empty on replay \u2014 falling back to LLM`);
10716
+ return null;
10717
+ }
10718
+ const template = availableComponents.find((c) => c.name === spec.componentType || c.id === spec.componentType);
10719
+ if (!template) {
10720
+ logger.info(`[ScriptComponentGen] Recipe spec component "${spec.componentType}" not in library \u2014 falling back to LLM`);
10721
+ return null;
10722
+ }
10723
+ const cols = inferColumns(q.data);
10724
+ const comp = { props: { title: spec.title, description: spec.description, config: { ...spec.config || {} } } };
10725
+ const repaired = validateAndRepairComponent(comp, template.type, cols, q.count ?? q.data.length);
10726
+ if (!repaired) {
10727
+ logger.info(`[ScriptComponentGen] Recipe spec "${spec.componentType}" no longer maps to columns (shape drift) \u2014 falling back to LLM`);
10728
+ return null;
10729
+ }
10730
+ const queryId = queryIds[q.sourceId] || queryCache.storeQuery(q.sql, { data: q.data, count: q.count });
10731
+ const toolId = q.virtual ? "script_dataset" : q.sourceId;
10732
+ const component = {
10733
+ id: `comp_${Math.random().toString(36).substring(2, 8)}`,
10734
+ name: template.name,
10735
+ displayName: template.displayName,
10736
+ type: template.type,
10737
+ description: template.description,
10738
+ props: {
10739
+ ...comp.props,
10740
+ externalTool: { toolId, toolName: q.sourceName, parameters: { queryId } }
10741
+ },
10742
+ category: template.category,
10743
+ keywords: template.keywords
10744
+ };
10745
+ out.push(component);
10746
+ if (componentStreamCallback && !answerComponentStreamed) {
10747
+ componentStreamCallback(component);
10748
+ answerComponentStreamed = true;
10015
10749
  }
10016
10750
  }
10017
- if (collections?.["external-tools"]?.execute) {
10018
- try {
10019
- const result = await collections["external-tools"].execute({
10020
- toolId: "federation_query",
10021
- toolName: "Cross-Source Query",
10022
- parameters: { sql }
10023
- });
10024
- return result;
10025
- } catch (err) {
10026
- logger.warn(`[ScriptComponentGen] Federation via collections failed: ${err}`);
10751
+ if (out.length > 0 && !out.some((c) => c.type === "MarkdownBlock")) {
10752
+ const md = buildMarkdown(analysisText || "");
10753
+ if (md) {
10754
+ out.push(md);
10755
+ if (componentStreamCallback) componentStreamCallback(md);
10756
+ logger.info("[ScriptComponentGen] Injected markdown on replay (recipe had no markdown spec)");
10027
10757
  }
10028
10758
  }
10029
- return null;
10759
+ logger.info(`[ScriptComponentGen] Deterministically assembled ${out.length} component(s) from recipe \u2014 no LLM call`);
10760
+ return out.length > 0 ? out : null;
10030
10761
  }
10031
10762
 
10032
10763
  // src/userResponse/anthropic.ts
@@ -11994,6 +12725,24 @@ var OpenAILLM = class extends BaseLLM {
11994
12725
  var openaiLLM = new OpenAILLM();
11995
12726
 
11996
12727
  // src/userResponse/agent-user-response.ts
12728
+ function hasExpiredScriptDataset(component) {
12729
+ if (!component || typeof component !== "object") return false;
12730
+ const et = component?.props?.externalTool;
12731
+ if (et?.toolId === "script_dataset") {
12732
+ const qid = et?.parameters?.queryId;
12733
+ if (typeof qid === "string" && qid.length > 0) {
12734
+ const stored = queryCache.getQuery(qid);
12735
+ if (!stored || stored.data == null) return true;
12736
+ }
12737
+ }
12738
+ const children = component?.props?.config?.components;
12739
+ if (Array.isArray(children)) {
12740
+ for (const child of children) {
12741
+ if (hasExpiredScriptDataset(child)) return true;
12742
+ }
12743
+ }
12744
+ return false;
12745
+ }
11997
12746
  function rehydrateCachedComponent(component) {
11998
12747
  const qMap = component?.props?.config?._queryMap;
11999
12748
  if (!qMap || typeof qMap !== "object" || Object.keys(qMap).length === 0) return component;
@@ -12043,7 +12792,24 @@ function getLLMInstance(provider) {
12043
12792
  return anthropicLLM;
12044
12793
  }
12045
12794
  }
12046
- var get_agent_user_response = async (prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, conversationHistory, streamCallback, collections, externalTools, userId, mainAgentModel, sourceAgentModel) => {
12795
+ function storeScriptDatasetQuery(q, ctx) {
12796
+ const data = q.data.length > MAX_ROWS_RENDERED ? q.data.slice(0, MAX_ROWS_RENDERED) : q.data;
12797
+ if (q.data.length > MAX_ROWS_RENDERED) {
12798
+ logger.warn(`[AgentFlow] Script dataset "${q.sourceId}" had ${q.data.length} rows \u2014 capped to ${MAX_ROWS_RENDERED} for render/cache. Consider pre-aggregating in the script.`);
12799
+ }
12800
+ const count = data.length;
12801
+ if (q.virtual && ctx.recipeId) {
12802
+ const descriptor = {
12803
+ kind: "script_dataset",
12804
+ recipeId: ctx.recipeId,
12805
+ params: ctx.params || {},
12806
+ datasetRef: q.sourceId
12807
+ };
12808
+ return queryCache.storeQuery(JSON.stringify(descriptor), { data, count });
12809
+ }
12810
+ return queryCache.storeQuery(q.sql, { data, count });
12811
+ }
12812
+ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, conversationHistory, streamCallback, collections, externalTools, userId, mainAgentModel, sourceAgentModel, workflows) => {
12047
12813
  const startTime = Date.now();
12048
12814
  const providers = llmProviders || ["anthropic"];
12049
12815
  const provider = providers[0];
@@ -12052,19 +12818,19 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
12052
12818
  logger.logLLMPrompt("agentUserResponse", "user", `User Prompt: ${prompt}`);
12053
12819
  logger.info(`[AgentFlow] Starting | provider: ${provider} | prompt: "${prompt.substring(0, 50)}..."`);
12054
12820
  try {
12055
- const conversationMatch = await conversation_search_default.searchConversationsWithReranking({
12821
+ const conversationMatch = await conversation_search_default.findExactMatch({
12056
12822
  userPrompt: prompt,
12057
12823
  collections,
12058
- userId,
12059
- similarityThreshold: EXACT_MATCH_SIMILARITY_THRESHOLD
12824
+ userId
12060
12825
  });
12061
12826
  if (conversationMatch) {
12062
- logger.info(`[AgentFlow] Found matching conversation (${(conversationMatch.similarity * 100).toFixed(2)}% similarity)`);
12063
12827
  const rawComponent = conversationMatch.uiBlock?.component || conversationMatch.uiBlock?.generatedComponentMetadata;
12064
12828
  const isValidComponent = rawComponent && typeof rawComponent === "object" && Object.keys(rawComponent).length > 0;
12065
12829
  const component = isValidComponent ? rawComponent : null;
12066
12830
  const cachedTextResponse = conversationMatch.uiBlock?.analysis || conversationMatch.uiBlock?.textResponse || conversationMatch.uiBlock?.text || "";
12067
- if (conversationMatch.similarity >= EXACT_MATCH_SIMILARITY_THRESHOLD) {
12831
+ if (component && hasExpiredScriptDataset(component)) {
12832
+ logger.info(`[AgentFlow] Exact match found but cached UIBlock has expired script_dataset \u2014 falling through to script matcher`);
12833
+ } else {
12068
12834
  if (streamCallback && cachedTextResponse) {
12069
12835
  streamCallback(cachedTextResponse);
12070
12836
  }
@@ -12078,16 +12844,14 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
12078
12844
  component: rehydratedComponent,
12079
12845
  actions: conversationMatch.uiBlock?.actions || [],
12080
12846
  reasoning: `Exact match from previous conversation`,
12081
- method: `${provider}-agent-semantic-match`,
12082
- semanticSimilarity: conversationMatch.similarity
12847
+ method: `${provider}-agent-exact-match`,
12848
+ semanticSimilarity: 1
12083
12849
  },
12084
12850
  errors: []
12085
12851
  };
12086
12852
  }
12087
- logger.info(`[AgentFlow] Similar match but below exact threshold \u2014 proceeding with agent`);
12088
- } else {
12089
- logger.info(`[AgentFlow] No matching conversations \u2014 proceeding with agent`);
12090
12853
  }
12854
+ logger.info(`[AgentFlow] No exact match \u2014 proceeding with agent`);
12091
12855
  const apiKey = (() => {
12092
12856
  switch (provider) {
12093
12857
  case "anthropic":
@@ -12152,11 +12916,11 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
12152
12916
  projectId
12153
12917
  };
12154
12918
  const streamBuffer = new StreamBuffer(streamCallback);
12155
- const scriptStore = new ScriptStore();
12919
+ const scriptStore = new ScriptStore({ collections, projectId });
12156
12920
  const turnId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
12157
12921
  const FORK_DEPTH_CAP = 3;
12158
12922
  let forkContext;
12159
- if (scriptStore.count() > 0) {
12923
+ if (await scriptStore.count() > 0) {
12160
12924
  const scriptMatcher = new ScriptMatcher(scriptStore);
12161
12925
  const scriptMatch = await scriptMatcher.match(prompt, apiKey, sourceAgentModel);
12162
12926
  if (scriptMatch && scriptMatch.tier === "near") {
@@ -12190,7 +12954,7 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
12190
12954
  { externalTools: agentTools, streamBuffer }
12191
12955
  );
12192
12956
  if (scriptResult.success && scriptResult.data.length > 0) {
12193
- scriptStore.recordSuccess(scriptMatch.recipe.id);
12957
+ await scriptStore.recordSuccess(scriptMatch.recipe.id);
12194
12958
  const dataContext = scriptResult.executedQueries.map((q) => {
12195
12959
  const preview = q.data.slice(0, 15);
12196
12960
  return `## Data from "${q.sourceName}"
@@ -12228,7 +12992,7 @@ ${JSON.stringify(preview, null, 2)}
12228
12992
  result: {
12229
12993
  _totalRecords: q.totalCount || q.count,
12230
12994
  _recordsShown: q.data.length,
12231
- _sampleData: q.data.slice(0, 3)
12995
+ _sampleData: q.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS)
12232
12996
  }
12233
12997
  }));
12234
12998
  let matchedComponents2 = [];
@@ -12242,19 +13006,31 @@ ${JSON.stringify(preview, null, 2)}
12242
13006
  streamBuffer.write("__TEXT_COMPLETE__COMPONENT_GENERATION_START__");
12243
13007
  streamBuffer.flush();
12244
13008
  }
12245
- const componentStreamCallback = streamBuffer.hasCallback() ? (component) => {
12246
- const answerMarker = `__ANSWER_COMPONENT_START__${JSON.stringify(component)}__ANSWER_COMPONENT_END__`;
12247
- streamBuffer.write(answerMarker);
12248
- streamBuffer.flush();
12249
- } : void 0;
13009
+ const componentStreamCallback = void 0;
12250
13010
  const scriptQueryIds = {};
12251
13011
  for (const q of scriptResult.executedQueries) {
12252
- scriptQueryIds[q.sourceId] = queryCache.storeQuery(q.sql, {
12253
- data: q.data,
12254
- count: q.count
13012
+ scriptQueryIds[q.sourceId] = storeScriptDatasetQuery(q, {
13013
+ recipeId: scriptMatch.recipe.id,
13014
+ params: scriptMatch.extractedParams || {}
12255
13015
  });
12256
13016
  }
12257
- try {
13017
+ let usedRecipeComponents = false;
13018
+ if (scriptMatch.recipe.components && scriptMatch.recipe.components.length > 0) {
13019
+ const assembled = assembleComponentsFromSpecs({
13020
+ specs: scriptMatch.recipe.components,
13021
+ scriptResult,
13022
+ queryIds: scriptQueryIds,
13023
+ availableComponents: components,
13024
+ componentStreamCallback,
13025
+ analysisText: scriptTextResponse
13026
+ });
13027
+ if (assembled && assembled.length > 0) {
13028
+ matchedComponents2 = assembled;
13029
+ layoutTitle2 = scriptMatch.recipe.name || layoutTitle2;
13030
+ usedRecipeComponents = true;
13031
+ }
13032
+ }
13033
+ if (!usedRecipeComponents) try {
12258
13034
  const compResult = await generateScriptComponents({
12259
13035
  userPrompt: prompt,
12260
13036
  scriptResult,
@@ -12264,13 +13040,22 @@ ${JSON.stringify(preview, null, 2)}
12264
13040
  model: sourceAgentModel,
12265
13041
  componentStreamCallback,
12266
13042
  externalTools: agentTools,
12267
- collections
13043
+ collections,
13044
+ analysisText: scriptTextResponse
12268
13045
  });
12269
13046
  if (compResult.components.length > 0) {
12270
13047
  matchedComponents2 = compResult.components;
12271
13048
  layoutTitle2 = compResult.layoutTitle;
12272
13049
  layoutDescription2 = compResult.layoutDescription;
12273
13050
  actions2 = compResult.actions;
13051
+ if (compResult.componentSpecs.length > 0) {
13052
+ try {
13053
+ scriptMatch.recipe.components = compResult.componentSpecs;
13054
+ await scriptStore.save(scriptMatch.recipe);
13055
+ } catch (e) {
13056
+ logger.warn(`[AgentFlow] Failed to backfill component recipe: ${e}`);
13057
+ }
13058
+ }
12274
13059
  } else {
12275
13060
  logger.info(`[AgentFlow] Lightweight component gen returned no components \u2014 falling back to full generator`);
12276
13061
  const matchResult = await generateAgentComponents({
@@ -12363,12 +13148,12 @@ ${JSON.stringify(preview, null, 2)}
12363
13148
  };
12364
13149
  } else {
12365
13150
  logger.warn(`[AgentFlow] Script "${scriptMatch.recipe.name}" failed: ${scriptResult.error || "no data returned"}`);
12366
- scriptStore.recordFailure(scriptMatch.recipe.id);
13151
+ await scriptStore.recordFailure(scriptMatch.recipe.id);
12367
13152
  }
12368
13153
  } catch (scriptErr) {
12369
13154
  const errMsg = scriptErr instanceof Error ? scriptErr.message : String(scriptErr);
12370
13155
  logger.warn(`[AgentFlow] Script execution error (falling back to agent flow): ${errMsg}`);
12371
- scriptStore.recordFailure(scriptMatch.recipe.id);
13156
+ await scriptStore.recordFailure(scriptMatch.recipe.id);
12372
13157
  }
12373
13158
  }
12374
13159
  } else {
@@ -12425,13 +13210,35 @@ User question: ${prompt}`;
12425
13210
  logger.warn(`[AgentFlow] Source routing pre-filter failed (using all tools): ${msg}`);
12426
13211
  }
12427
13212
  }
12428
- const mainAgent = new MainAgent(filteredTools, agentConfig, scriptStore, turnId, streamBuffer);
13213
+ const mainAgent = new MainAgent(filteredTools, agentConfig, scriptStore, turnId, streamBuffer, workflows || []);
12429
13214
  const agentResponse = await mainAgent.handleQuestion(
12430
13215
  effectivePrompt,
12431
13216
  apiKey,
12432
13217
  conversationHistory,
12433
13218
  streamBuffer.hasCallback() ? (chunk) => streamBuffer.write(chunk) : void 0
12434
13219
  );
13220
+ if (agentResponse.workflow) {
13221
+ streamBuffer.flush();
13222
+ const workflowComponent = {
13223
+ id: `workflow_${Date.now()}`,
13224
+ name: agentResponse.workflow.name,
13225
+ type: `Workflow_${agentResponse.workflow.name}`,
13226
+ description: `Workflow: ${agentResponse.workflow.name}`,
13227
+ props: agentResponse.workflow.props
13228
+ };
13229
+ const elapsedTime2 = Date.now() - startTime;
13230
+ logger.info(`[AgentFlow] Workflow short-circuit | "${agentResponse.workflow.name}" | ${elapsedTime2}ms`);
13231
+ return {
13232
+ success: true,
13233
+ data: {
13234
+ text: "",
13235
+ component: workflowComponent,
13236
+ actions: [],
13237
+ method: `${provider}-agent-workflow`
13238
+ },
13239
+ errors: []
13240
+ };
13241
+ }
12435
13242
  const rawText = streamBuffer.getFullText() || agentResponse.text || "I apologize, but I was unable to generate a response.";
12436
13243
  const textResponse = rawText.replace(/_?_?SB_END_?_?/g, "").replace(/__SB_\w+_(?:START|MSG)_?_?/g, "").replace(/__QUERY_TIMER_START_[^_]*__/g, "").replace(/__QUERY_TIMER_DONE_[\d.]+__/g, "").replace(/__TEXT_COMPLETE__COMPONENT_GENERATION_START__/g, "").replace(/\[COMPLEXITY:\s*(?:simple|medium|complex)\]/gi, "");
12437
13244
  streamBuffer.flush();
@@ -12471,6 +13278,7 @@ User question: ${prompt}`;
12471
13278
  let layoutTitle = "Dashboard";
12472
13279
  let layoutDescription = "Multi-component dashboard";
12473
13280
  let actions = [];
13281
+ let authoredComponentSpecs = [];
12474
13282
  if (!hasExecutedTools) {
12475
13283
  logger.info(`[AgentFlow] No tools executed \u2014 general question, wrapping in DynamicMarkdownBlock`);
12476
13284
  const mainAgentText = agentResponse.text || textResponse;
@@ -12518,9 +13326,9 @@ User question: ${prompt}`;
12518
13326
  logger.info(`[AgentFlow] Using savedScript.executedQueries (${agentResponse.savedScript.executedQueries.length} entries, includes computed columns) for component gen`);
12519
13327
  const scriptQueries = agentResponse.savedScript.executedQueries;
12520
13328
  for (const q of scriptQueries) {
12521
- agentQueryIds[q.sourceId] = queryCache.storeQuery(q.sql, {
12522
- data: q.data,
12523
- count: q.count
13329
+ agentQueryIds[q.sourceId] = storeScriptDatasetQuery(q, {
13330
+ recipeId: agentResponse.savedScript.recipeId,
13331
+ params: {}
12524
13332
  });
12525
13333
  }
12526
13334
  agentScriptResult = {
@@ -12558,7 +13366,7 @@ User question: ${prompt}`;
12558
13366
  executionTimeMs: 0
12559
13367
  };
12560
13368
  }
12561
- const compResult = await generateScriptComponents({
13369
+ const compGenArgs = {
12562
13370
  userPrompt: prompt,
12563
13371
  scriptResult: agentScriptResult,
12564
13372
  queryIds: agentQueryIds,
@@ -12567,13 +13375,29 @@ User question: ${prompt}`;
12567
13375
  model: sourceAgentModel,
12568
13376
  componentStreamCallback,
12569
13377
  externalTools: agentTools,
12570
- collections
12571
- });
12572
- if (compResult.components.length > 0) {
13378
+ collections,
13379
+ analysisText: agentResponse.text
13380
+ };
13381
+ const MAX_COMP_ATTEMPTS = 3;
13382
+ let compResult;
13383
+ for (let attempt = 1; attempt <= MAX_COMP_ATTEMPTS; attempt++) {
13384
+ try {
13385
+ compResult = await generateScriptComponents(compGenArgs);
13386
+ break;
13387
+ } catch (err) {
13388
+ const msg = err instanceof Error ? err.message : String(err);
13389
+ const transient = /connection error|econnreset|etimedout|fetch failed|socket hang up|overloaded|rate.?limit|\b429\b|\b5\d\d\b|network|timeout/i.test(msg);
13390
+ if (!transient || attempt === MAX_COMP_ATTEMPTS) throw err;
13391
+ logger.warn(`[AgentFlow] Lightweight component gen transient error (attempt ${attempt}/${MAX_COMP_ATTEMPTS}) \u2014 retrying: ${msg}`);
13392
+ await new Promise((res) => setTimeout(res, 400 * attempt));
13393
+ }
13394
+ }
13395
+ if (compResult && compResult.components.length > 0) {
12573
13396
  matchedComponents = compResult.components;
12574
13397
  layoutTitle = compResult.layoutTitle;
12575
13398
  layoutDescription = compResult.layoutDescription;
12576
13399
  actions = compResult.actions;
13400
+ authoredComponentSpecs = compResult.componentSpecs;
12577
13401
  usedLightweight = true;
12578
13402
  logger.info(`[AgentFlow] Lightweight component gen succeeded \u2014 ${matchedComponents.length} components`);
12579
13403
  }
@@ -12676,10 +13500,11 @@ User question: ${prompt}`;
12676
13500
  forkDepth: Math.min(FORK_DEPTH_CAP, forkContext.parentDepth + 1),
12677
13501
  forkReason: forkContext.modificationHint
12678
13502
  } : void 0;
12679
- const promoted = scriptStore.promoteToVerified(authored.recipeId, {
13503
+ const promoted = await scriptStore.promoteToVerified(authored.recipeId, {
12680
13504
  sourceIds: authored.sourceIds,
12681
13505
  tables: authored.tables,
12682
- ...lineage
13506
+ ...lineage,
13507
+ components: authoredComponentSpecs
12683
13508
  });
12684
13509
  if (promoted && lineage?.parentId) {
12685
13510
  logger.info(
@@ -12915,7 +13740,7 @@ var CONTEXT_CONFIG = {
12915
13740
  };
12916
13741
 
12917
13742
  // src/handlers/user-prompt-request.ts
12918
- var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel) => {
13743
+ var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows) => {
12919
13744
  const errors = [];
12920
13745
  const parseResult = UserPromptRequestMessageSchema.safeParse(data);
12921
13746
  if (!parseResult.success) {
@@ -13001,7 +13826,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
13001
13826
  externalTools,
13002
13827
  userId,
13003
13828
  mainAgentModel,
13004
- sourceAgentModel
13829
+ sourceAgentModel,
13830
+ workflows
13005
13831
  );
13006
13832
  logger.info("User prompt request completed");
13007
13833
  const uiBlockId = existingUiBlockId;
@@ -13154,8 +13980,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
13154
13980
  wsId
13155
13981
  };
13156
13982
  };
13157
- async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel) {
13158
- const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel);
13983
+ async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows) {
13984
+ const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows);
13159
13985
  if (response.data?.component?.props?.config?.components) {
13160
13986
  response.data.component.props.config.components = response.data.component.props.config.components.map((comp) => ({
13161
13987
  ...comp,
@@ -13587,6 +14413,24 @@ async function handleComponentListResponse(data, storeComponents, collections) {
13587
14413
  }
13588
14414
  }
13589
14415
 
14416
+ // src/handlers/workflow-list-response.ts
14417
+ async function handleWorkflowListResponse(data, storeWorkflows) {
14418
+ try {
14419
+ const parsed = WorkflowListResponseMessageSchema.parse(data);
14420
+ const { payload } = parsed;
14421
+ const workflowsList = payload.workflows;
14422
+ if (!workflowsList) {
14423
+ logger.error("Workflows list not found in WORKFLOW_LIST_RES payload");
14424
+ return;
14425
+ }
14426
+ const workflows = WorkflowsSchema.parse(workflowsList);
14427
+ storeWorkflows(workflows);
14428
+ logger.info(`Stored ${workflows.length} workflow descriptor(s) from frontend`);
14429
+ } catch (error) {
14430
+ logger.error("Failed to handle workflow list response:", error);
14431
+ }
14432
+ }
14433
+
13590
14434
  // src/handlers/users.ts
13591
14435
  async function handleUsersRequest(data, collections, sendMessage) {
13592
14436
  const executeCollection = async (collection, op, params) => {
@@ -16403,17 +17247,45 @@ function formatComponentsForPrompt(components) {
16403
17247
  Props Structure: ${propsPreview}`;
16404
17248
  }).join("\n\n");
16405
17249
  }
17250
+ function classifyTool(tool) {
17251
+ if (tool.toolType === "direct") return "direct";
17252
+ if (tool.id.endsWith("_query")) return "sql";
17253
+ if (tool.id.endsWith("_call")) return "rest";
17254
+ if (tool.id.endsWith("_graphql")) return "graphql";
17255
+ return "direct";
17256
+ }
16406
17257
  function formatToolsForPrompt(tools) {
16407
17258
  if (!tools || tools.length === 0) {
16408
17259
  return "No external tools available.";
16409
17260
  }
16410
- return tools.map((tool, idx) => {
17261
+ const directTools = [];
17262
+ const sourceTools = [];
17263
+ for (const t of tools) {
17264
+ (classifyTool(t) === "direct" ? directTools : sourceTools).push(t);
17265
+ }
17266
+ const renderTool = (tool, idx, label) => {
16411
17267
  const paramsStr = Object.entries(tool.params || {}).map(([key, type]) => `${key}: ${type}`).join(", ");
16412
- return `${idx + 1}. ID: ${tool.id}
17268
+ return `${idx + 1}. [${label}] ID: ${tool.id}
16413
17269
  Name: ${tool.name}
16414
17270
  Description: ${tool.description}
16415
17271
  Parameters: { ${paramsStr} }`;
16416
- }).join("\n\n");
17272
+ };
17273
+ const sections = [];
17274
+ if (directTools.length > 0) {
17275
+ const body = directTools.map((t, i) => renderTool(t, i, "DIRECT \u2014 PREFER")).join("\n\n");
17276
+ sections.push(`### Direct Tools (try these first \u2014 they answer most questions without SQL)
17277
+ ${body}`);
17278
+ }
17279
+ if (sourceTools.length > 0) {
17280
+ const body = sourceTools.map((t, i) => {
17281
+ const kind = classifyTool(t);
17282
+ const label = kind === "sql" ? "SQL" : kind === "rest" ? "REST" : "GRAPHQL";
17283
+ return renderTool(t, i, label);
17284
+ }).join("\n\n");
17285
+ sections.push(`### Source Tools (fallback \u2014 use only when no direct tool fits)
17286
+ ${body}`);
17287
+ }
17288
+ return sections.join("\n\n");
16417
17289
  }
16418
17290
  function formatExistingComponentsForPrompt(existingComponents) {
16419
17291
  if (!existingComponents || existingComponents.length === 0) {
@@ -16438,7 +17310,7 @@ function sendDashCompResponse(id, res, sendMessage, clientId) {
16438
17310
  }
16439
17311
 
16440
17312
  // src/dashComp/pick-component.ts
16441
- async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, dashCompModels, conversationHistory) {
17313
+ async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, dashCompModels, conversationHistory, userId) {
16442
17314
  const errors = [];
16443
17315
  const availableComponentsText = formatComponentsForPrompt(components);
16444
17316
  const availableToolsText = formatToolsForPrompt(tools);
@@ -16454,6 +17326,25 @@ async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApi
16454
17326
  schemaDoc = schema.generateSchemaDocumentation();
16455
17327
  }
16456
17328
  const databaseRules = await promptLoader.loadDatabaseRules();
17329
+ let globalKnowledgeBase = "No global knowledge base available.";
17330
+ let knowledgeBaseContext = "No additional knowledge base context available.";
17331
+ if (collections) {
17332
+ const kbResult = await knowledge_base_default.getAllKnowledgeBase({
17333
+ prompt,
17334
+ collections,
17335
+ userId,
17336
+ topK: KNOWLEDGE_BASE_TOP_K
17337
+ });
17338
+ globalKnowledgeBase = kbResult.globalContext || globalKnowledgeBase;
17339
+ const dynamicParts = [];
17340
+ if (kbResult.userContext) {
17341
+ dynamicParts.push("## User-Specific Knowledge Base\n" + kbResult.userContext);
17342
+ }
17343
+ if (kbResult.queryContext) {
17344
+ dynamicParts.push("## Relevant Knowledge Base (Query-Matched)\n" + kbResult.queryContext);
17345
+ }
17346
+ knowledgeBaseContext = dynamicParts.join("\n\n") || knowledgeBaseContext;
17347
+ }
16457
17348
  const prompts = await promptLoader.loadPrompts("dash-comp-picker", {
16458
17349
  USER_PROMPT: prompt,
16459
17350
  AVAILABLE_COMPONENTS: availableComponentsText,
@@ -16461,8 +17352,12 @@ async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApi
16461
17352
  DATABASE_RULES: databaseRules,
16462
17353
  AVAILABLE_TOOLS: availableToolsText,
16463
17354
  CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
16464
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
17355
+ CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
17356
+ GLOBAL_KNOWLEDGE_BASE: globalKnowledgeBase,
17357
+ KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext
16465
17358
  });
17359
+ logger.logLLMPrompt("dashCompPicker", "system", extractPromptText(prompts.system));
17360
+ logger.logLLMPrompt("dashCompPicker", "user", prompts.user);
16466
17361
  logger.debug("[DASH_COMP_REQ] Loaded dash-comp-picker prompts with schema and tools");
16467
17362
  const { apiKey, model } = getApiKeyAndModel(
16468
17363
  anthropicApiKey,
@@ -16828,6 +17723,18 @@ Fixed SQL query:`;
16828
17723
  logger.info(`[DASH_COMP_REQ] Replaced direct query with queryId: ${queryId}`);
16829
17724
  }
16830
17725
  finalComponent = { ...finalComponent, props };
17726
+ const ext = finalComponent.props?.externalTool;
17727
+ if (ext?.toolId && !ext?.parameters?.sql) {
17728
+ const matchedTool = tools?.find((t) => t.id === ext.toolId);
17729
+ if (matchedTool && matchedTool.cache !== false) {
17730
+ const match = executedTools.find((t) => t.id === ext.toolId);
17731
+ if (match) {
17732
+ const cacheKey = buildDirectToolCacheKey(ext.toolId, ext.parameters);
17733
+ queryCache.set(cacheKey, { success: true, data: match.result });
17734
+ logger.info(`[DASH_COMP_REQ] Pre-populated et-direct cache for ${ext.toolId}`);
17735
+ }
17736
+ }
17737
+ }
16831
17738
  if (parsedResult.props.query) {
16832
17739
  logger.info(`[DASH_COMP_REQ] Data source: Database query`);
16833
17740
  }
@@ -16862,7 +17769,7 @@ Fixed SQL query:`;
16862
17769
  }
16863
17770
 
16864
17771
  // src/dashComp/create-filter.ts
16865
- async function createFilterWithLLM(prompt, components, existingComponents, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, tools, dashCompModels, collections) {
17772
+ async function createFilterWithLLM(prompt, components, existingComponents, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, tools, dashCompModels, collections, userId) {
16866
17773
  const errors = [];
16867
17774
  try {
16868
17775
  const filterComponents = components.filter((c) => c.type.startsWith("Filter"));
@@ -16882,6 +17789,25 @@ async function createFilterWithLLM(prompt, components, existingComponents, anthr
16882
17789
  schemaDoc = schema.generateSchemaDocumentation();
16883
17790
  }
16884
17791
  const databaseRules = await promptLoader.loadDatabaseRules();
17792
+ let globalKnowledgeBase = "No global knowledge base available.";
17793
+ let knowledgeBaseContext = "No additional knowledge base context available.";
17794
+ if (collections) {
17795
+ const kbResult = await knowledge_base_default.getAllKnowledgeBase({
17796
+ prompt,
17797
+ collections,
17798
+ userId,
17799
+ topK: KNOWLEDGE_BASE_TOP_K
17800
+ });
17801
+ globalKnowledgeBase = kbResult.globalContext || globalKnowledgeBase;
17802
+ const dynamicParts = [];
17803
+ if (kbResult.userContext) {
17804
+ dynamicParts.push("## User-Specific Knowledge Base\n" + kbResult.userContext);
17805
+ }
17806
+ if (kbResult.queryContext) {
17807
+ dynamicParts.push("## Relevant Knowledge Base (Query-Matched)\n" + kbResult.queryContext);
17808
+ }
17809
+ knowledgeBaseContext = dynamicParts.join("\n\n") || knowledgeBaseContext;
17810
+ }
16885
17811
  const prompts = await promptLoader.loadPrompts("dash-filter-picker", {
16886
17812
  USER_PROMPT: prompt,
16887
17813
  AVAILABLE_COMPONENTS: formatComponentsForPrompt(filterComponents),
@@ -16889,8 +17815,12 @@ async function createFilterWithLLM(prompt, components, existingComponents, anthr
16889
17815
  SCHEMA_DOC: schemaDoc || "No database schema available",
16890
17816
  DATABASE_RULES: databaseRules,
16891
17817
  AVAILABLE_TOOLS: formatToolsForPrompt(tools),
16892
- CURRENT_DATETIME: getCurrentDateTimeForPrompt()
17818
+ CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
17819
+ GLOBAL_KNOWLEDGE_BASE: globalKnowledgeBase,
17820
+ KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext
16893
17821
  });
17822
+ logger.logLLMPrompt("dashFilterPicker", "system", extractPromptText(prompts.system));
17823
+ logger.logLLMPrompt("dashFilterPicker", "user", prompts.user);
16894
17824
  logger.debug("[DASH_COMP_REQ:FILTER] Loaded dash-filter-picker prompts");
16895
17825
  const { apiKey, model } = getApiKeyAndModel(
16896
17826
  anthropicApiKey,
@@ -16999,7 +17929,35 @@ async function createFilterWithLLM(prompt, components, existingComponents, anthr
16999
17929
  const updatedComponents = result.updatedComponents || [];
17000
17930
  for (const comp of updatedComponents) {
17001
17931
  const extTool = comp.props?.externalTool;
17002
- if (!extTool?.parameters?.sql || !extTool?.toolId) continue;
17932
+ if (!extTool?.toolId) continue;
17933
+ const isDirectTool = !extTool?.parameters?.sql;
17934
+ if (isDirectTool) {
17935
+ const directTool = tools?.find((t) => t.id === extTool.toolId);
17936
+ if (!directTool) {
17937
+ logger.warn(`[DASH_COMP_REQ:FILTER] direct tool ${extTool.toolId} not found in tool registry`);
17938
+ continue;
17939
+ }
17940
+ const declared = new Set(Object.keys(directTool.params || {}));
17941
+ const sentKeys = Object.keys(extTool.parameters || {});
17942
+ const unknown = sentKeys.filter((k) => !declared.has(k));
17943
+ if (unknown.length > 0) {
17944
+ logger.warn(`[DASH_COMP_REQ:FILTER] dropping unknown params on ${extTool.toolId}: ${unknown.join(", ")}`);
17945
+ unknown.forEach((k) => delete extTool.parameters[k]);
17946
+ }
17947
+ if (directTool.cache !== false) {
17948
+ try {
17949
+ const directResult = await directTool.fn(extTool.parameters || {});
17950
+ const cacheKey2 = buildDirectToolCacheKey(extTool.toolId, extTool.parameters);
17951
+ queryCache.set(cacheKey2, { success: true, data: directResult });
17952
+ logger.info(`[DASH_COMP_REQ:FILTER] direct tool ${extTool.toolId} validated and cached for component: ${comp.id}`);
17953
+ } catch (err) {
17954
+ const errMsg = err instanceof Error ? err.message : String(err);
17955
+ logger.warn(`[DASH_COMP_REQ:FILTER] direct tool validation failed for ${extTool.toolId} on component ${comp.id}: ${errMsg}`);
17956
+ }
17957
+ }
17958
+ continue;
17959
+ }
17960
+ if (!extTool?.parameters?.sql) continue;
17003
17961
  let sql = extTool.parameters.sql;
17004
17962
  const defaultParams = extTool.parameters.params || {};
17005
17963
  const toolId = extTool.toolId;
@@ -17236,7 +18194,8 @@ var processDashCompRequest = async (data, components, _sendMessage, anthropicApi
17236
18194
  llmProviders,
17237
18195
  tools,
17238
18196
  dashCompModels,
17239
- collections
18197
+ collections,
18198
+ userId
17240
18199
  );
17241
18200
  } else {
17242
18201
  llmResponse = await pickComponentWithLLM(
@@ -17250,7 +18209,8 @@ var processDashCompRequest = async (data, components, _sendMessage, anthropicApi
17250
18209
  collections,
17251
18210
  tools,
17252
18211
  dashCompModels,
17253
- conversationHistory
18212
+ conversationHistory,
18213
+ userId
17254
18214
  );
17255
18215
  }
17256
18216
  if (llmResponse.success && dashboardId && prompt) {
@@ -17389,7 +18349,7 @@ function sendReportCompResponse(id, res, sendMessage, clientId) {
17389
18349
  }
17390
18350
 
17391
18351
  // src/reportComp/generate-report.ts
17392
- async function generateReportComponents(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, modelConfig, conversationHistory) {
18352
+ async function generateReportComponents(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, modelConfig, conversationHistory, userId) {
17393
18353
  const errors = [];
17394
18354
  const availableComponentsText = formatComponentsForPrompt2(components);
17395
18355
  const availableToolsText = formatToolsForPrompt2(tools);
@@ -17405,6 +18365,25 @@ async function generateReportComponents(prompt, components, anthropicApiKey, gro
17405
18365
  schemaDoc = schema.generateSchemaDocumentation();
17406
18366
  }
17407
18367
  const databaseRules = await promptLoader.loadDatabaseRules();
18368
+ let globalKnowledgeBase = "No global knowledge base available.";
18369
+ let knowledgeBaseContext = "No additional knowledge base context available.";
18370
+ if (collections) {
18371
+ const kbResult = await knowledge_base_default.getAllKnowledgeBase({
18372
+ prompt,
18373
+ collections,
18374
+ userId,
18375
+ topK: KNOWLEDGE_BASE_TOP_K
18376
+ });
18377
+ globalKnowledgeBase = kbResult.globalContext || globalKnowledgeBase;
18378
+ const dynamicParts = [];
18379
+ if (kbResult.userContext) {
18380
+ dynamicParts.push("## User-Specific Knowledge Base\n" + kbResult.userContext);
18381
+ }
18382
+ if (kbResult.queryContext) {
18383
+ dynamicParts.push("## Relevant Knowledge Base (Query-Matched)\n" + kbResult.queryContext);
18384
+ }
18385
+ knowledgeBaseContext = dynamicParts.join("\n\n") || knowledgeBaseContext;
18386
+ }
17408
18387
  const prompts = await promptLoader.loadPrompts("report-comp-picker", {
17409
18388
  USER_PROMPT: prompt,
17410
18389
  AVAILABLE_COMPONENTS: availableComponentsText,
@@ -17412,8 +18391,12 @@ async function generateReportComponents(prompt, components, anthropicApiKey, gro
17412
18391
  DATABASE_RULES: databaseRules,
17413
18392
  AVAILABLE_TOOLS: availableToolsText,
17414
18393
  CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
17415
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
18394
+ CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
18395
+ GLOBAL_KNOWLEDGE_BASE: globalKnowledgeBase,
18396
+ KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext
17416
18397
  });
18398
+ logger.logLLMPrompt("reportCompPicker", "system", extractPromptText(prompts.system));
18399
+ logger.logLLMPrompt("reportCompPicker", "user", prompts.user);
17417
18400
  logger.debug("[REPORT_COMP_REQ] Loaded report-comp-picker prompts with schema and tools");
17418
18401
  const { apiKey, model } = getApiKeyAndModel2(
17419
18402
  anthropicApiKey,
@@ -17638,13 +18621,21 @@ async function validateAllExternalToolQueries(components, collections, tools, mo
17638
18621
  data: {}
17639
18622
  });
17640
18623
  if (result?.success !== false && !result?.error) {
17641
- const resultData = result?.data?.data ?? result?.data ?? [];
17642
- const dataArray = Array.isArray(resultData) ? resultData : [resultData];
18624
+ const toolResult = result?.data ?? result;
18625
+ const valueKey = comp.props?.config?.valueKey;
18626
+ const isKpi = comp.type === "KPICard" || comp.name === "DynamicKPICard";
18627
+ let dataArray;
18628
+ if (isKpi && valueKey && toolResult && typeof toolResult === "object" && !Array.isArray(toolResult) && toolResult[valueKey] !== void 0) {
18629
+ dataArray = [toolResult];
18630
+ } else {
18631
+ const resultData = toolResult?.data ?? toolResult ?? [];
18632
+ dataArray = Array.isArray(resultData) ? resultData : [resultData];
18633
+ }
17643
18634
  if (!comp.props.config) {
17644
18635
  comp.props.config = {};
17645
18636
  }
17646
18637
  comp.props.config.data = dataArray;
17647
- logger.info(`[REPORT_COMP_REQ] \u2713 ${comp.name} prefetched ${dataArray.length} rows (non-SQL tool)`);
18638
+ logger.info(`[REPORT_COMP_REQ] \u2713 ${comp.name} prefetched ${dataArray.length} ${dataArray.length === 1 && isKpi ? "aggregate" : "rows"} (non-SQL tool)`);
17648
18639
  }
17649
18640
  } catch (err) {
17650
18641
  logger.warn(`[REPORT_COMP_REQ] \u26A0 ${comp.name} non-SQL prefetch failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -17784,7 +18775,8 @@ var processReportCompRequest = async (data, components, _sendMessage, anthropicA
17784
18775
  collections,
17785
18776
  tools,
17786
18777
  modelConfig,
17787
- conversationHistory
18778
+ conversationHistory,
18779
+ userId
17788
18780
  );
17789
18781
  if (llmResponse.success && reportId && prompt) {
17790
18782
  const comps = llmResponse.data?.components;
@@ -17914,7 +18906,7 @@ function getLLMProviders() {
17914
18906
  // src/auth/user-manager.ts
17915
18907
  import fs8 from "fs";
17916
18908
  import path8 from "path";
17917
- import os from "os";
18909
+ import os2 from "os";
17918
18910
  var UserManager = class {
17919
18911
  /**
17920
18912
  * Initialize UserManager with file path and sync interval
@@ -17926,7 +18918,7 @@ var UserManager = class {
17926
18918
  this.hasChanged = false;
17927
18919
  this.syncInterval = null;
17928
18920
  this.isInitialized = false;
17929
- this.filePath = path8.join(os.homedir(), ".superatom", "projects", projectId, "users.json");
18921
+ this.filePath = path8.join(os2.homedir(), ".superatom", "projects", projectId, "users.json");
17930
18922
  this.syncIntervalMs = syncIntervalMs;
17931
18923
  }
17932
18924
  /**
@@ -18212,7 +19204,7 @@ var UserManager = class {
18212
19204
  // src/dashboards/dashboard-manager.ts
18213
19205
  import fs9 from "fs";
18214
19206
  import path9 from "path";
18215
- import os2 from "os";
19207
+ import os3 from "os";
18216
19208
  var DashboardManager = class {
18217
19209
  /**
18218
19210
  * Initialize DashboardManager with project ID
@@ -18221,7 +19213,7 @@ var DashboardManager = class {
18221
19213
  constructor(projectId = "snowflake-dataset") {
18222
19214
  this.projectId = projectId;
18223
19215
  this.dashboardsBasePath = path9.join(
18224
- os2.homedir(),
19216
+ os3.homedir(),
18225
19217
  ".superatom",
18226
19218
  "projects",
18227
19219
  projectId,
@@ -18379,7 +19371,7 @@ var DashboardManager = class {
18379
19371
  // src/reports/report-manager.ts
18380
19372
  import fs10 from "fs";
18381
19373
  import path10 from "path";
18382
- import os3 from "os";
19374
+ import os4 from "os";
18383
19375
  var ReportManager = class {
18384
19376
  /**
18385
19377
  * Initialize ReportManager with project ID
@@ -18388,7 +19380,7 @@ var ReportManager = class {
18388
19380
  constructor(projectId = "snowflake-dataset") {
18389
19381
  this.projectId = projectId;
18390
19382
  this.reportsBasePath = path10.join(
18391
- os3.homedir(),
19383
+ os4.homedir(),
18392
19384
  ".superatom",
18393
19385
  "projects",
18394
19386
  projectId,
@@ -18909,23 +19901,27 @@ var CleanupService = class _CleanupService {
18909
19901
  // src/index.ts
18910
19902
  var DEFAULT_WS_URL = "wss://ws.superatom.ai/websocket";
18911
19903
  var SuperatomSDK = class {
18912
- // 3.5 minutes (PING_INTERVAL + 30s grace)
18913
19904
  constructor(config) {
18914
19905
  this.ws = null;
18915
19906
  this.messageHandlers = /* @__PURE__ */ new Map();
18916
19907
  this.messageTypeHandlers = /* @__PURE__ */ new Map();
18917
19908
  this.connected = false;
18918
19909
  this.reconnectAttempts = 0;
18919
- this.maxReconnectAttempts = 5;
19910
+ // Retry forever — the backend must self-heal across NAT timeouts, DO
19911
+ // redeploys, and long network outages. The previous cap (5) gave up after
19912
+ // ~30s and left the singleton SDK dead until process restart.
19913
+ this.maxReconnectAttempts = Infinity;
18920
19914
  this.collections = {};
18921
19915
  this.components = [];
18922
19916
  this.tools = [];
18923
- // Heartbeat properties for keeping WebSocket connection alive
19917
+ this.workflows = [];
19918
+ // Heartbeat properties for keeping WebSocket connection alive.
19919
+ // 25s ping + 10s grace stays under common NAT/LB idle thresholds (~60-100s)
19920
+ // so we detect dead sockets within seconds instead of minutes.
18924
19921
  this.pingInterval = null;
18925
19922
  this.lastPong = Date.now();
18926
- this.PING_INTERVAL_MS = 18e4;
18927
- // 3 minutes
18928
- this.PONG_TIMEOUT_MS = 21e4;
19923
+ this.PING_INTERVAL_MS = 25e3;
19924
+ this.PONG_TIMEOUT_MS = 35e3;
18929
19925
  if (config.logLevel) {
18930
19926
  logger.setLogLevel(config.logLevel);
18931
19927
  }
@@ -19081,7 +20077,7 @@ var SuperatomSDK = class {
19081
20077
  this.handlePong();
19082
20078
  break;
19083
20079
  case "DATA_REQ":
19084
- handleDataRequest(parsed, this.collections, (msg) => this.send(msg)).catch((error) => {
20080
+ handleDataRequest(parsed, this.collections, (msg) => this.send(msg), this.tools).catch((error) => {
19085
20081
  logger.error("Failed to handle data request:", error);
19086
20082
  });
19087
20083
  break;
@@ -19101,7 +20097,7 @@ var SuperatomSDK = class {
19101
20097
  });
19102
20098
  break;
19103
20099
  case "USER_PROMPT_REQ":
19104
- handleUserPromptRequest(parsed, this.components, (msg) => this.send(msg), this.anthropicApiKey, this.groqApiKey, this.geminiApiKey, this.openaiApiKey, this.llmProviders, this.collections, this.tools, this.mainAgentModel, this.sourceAgentModel).catch((error) => {
20100
+ handleUserPromptRequest(parsed, this.components, (msg) => this.send(msg), this.anthropicApiKey, this.groqApiKey, this.geminiApiKey, this.openaiApiKey, this.llmProviders, this.collections, this.tools, this.mainAgentModel, this.sourceAgentModel, this.workflows).catch((error) => {
19105
20101
  logger.error("Failed to handle user prompt request:", error);
19106
20102
  });
19107
20103
  break;
@@ -19120,6 +20116,11 @@ var SuperatomSDK = class {
19120
20116
  logger.error("Failed to handle component list request:", error);
19121
20117
  });
19122
20118
  break;
20119
+ case "WORKFLOW_LIST_RES":
20120
+ handleWorkflowListResponse(parsed, (wf) => this.setWorkflows(wf)).catch((error) => {
20121
+ logger.error("Failed to handle workflow list request:", error);
20122
+ });
20123
+ break;
19123
20124
  case "USERS":
19124
20125
  handleUsersRequest(parsed, this.collections, (msg) => this.send(msg)).catch((error) => {
19125
20126
  logger.error("Failed to handle users request:", error);
@@ -19279,9 +20280,9 @@ var SuperatomSDK = class {
19279
20280
  handleReconnect() {
19280
20281
  if (this.reconnectAttempts < this.maxReconnectAttempts) {
19281
20282
  this.reconnectAttempts++;
19282
- const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 1e4);
20283
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
19283
20284
  setTimeout(() => {
19284
- logger.info(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
20285
+ logger.info(`Attempting to reconnect (attempt ${this.reconnectAttempts})...`);
19285
20286
  this.connect().catch((error) => {
19286
20287
  logger.error("Reconnection failed:", error);
19287
20288
  });
@@ -19359,6 +20360,24 @@ var SuperatomSDK = class {
19359
20360
  getTools() {
19360
20361
  return this.tools;
19361
20362
  }
20363
+ /**
20364
+ * Register workflow components for the SDK instance.
20365
+ *
20366
+ * Workflows are pre-built multi-step UI flows the main agent can pick when
20367
+ * the user's prompt matches a workflow's `whenToUse` trigger. Picking a
20368
+ * workflow short-circuits analysis text + dashboard component generation —
20369
+ * the workflow component is returned directly, with the LLM-extracted props.
20370
+ */
20371
+ setWorkflows(workflows) {
20372
+ this.workflows = workflows;
20373
+ logger.info(`Workflows stored in SDK: ${workflows.length} workflow(s)`);
20374
+ }
20375
+ /**
20376
+ * Get the registered workflow components.
20377
+ */
20378
+ getWorkflows() {
20379
+ return this.workflows;
20380
+ }
19362
20381
  /**
19363
20382
  * Apply model strategy to all LLM provider singletons
19364
20383
  * @param strategy - 'best', 'fast', or 'balanced'
@@ -19418,6 +20437,8 @@ export {
19418
20437
  LLM,
19419
20438
  MainAgent,
19420
20439
  STORAGE_CONFIG,
20440
+ ScriptMatcher,
20441
+ ScriptStore,
19421
20442
  SuperatomSDK,
19422
20443
  Thread,
19423
20444
  ThreadManager,
@@ -19431,10 +20452,13 @@ export {
19431
20452
  hybridRerank,
19432
20453
  llmUsageLogger,
19433
20454
  logger,
20455
+ normalizeScriptBody,
19434
20456
  openaiLLM,
19435
20457
  queryCache,
19436
20458
  rerankChromaResults,
19437
20459
  rerankConversationResults,
20460
+ resolveScriptRecipeStore,
20461
+ runScript,
19438
20462
  userPromptErrorLogger
19439
20463
  };
19440
20464
  //# sourceMappingURL=index.mjs.map