@superatomai/sdk-node 0.0.51-mds → 0.0.52-mds

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.d.mts CHANGED
@@ -904,6 +904,13 @@ interface SuperatomSDKConfig {
904
904
  * Default: 5 minutes
905
905
  */
906
906
  queryCacheTTL?: number;
907
+ /**
908
+ * Byte budget for cached query results, in MB.
909
+ * The cache evicts least-recently-used entries to stay under it, so a long TTL
910
+ * cannot grow the process until the host kills it. Keep it well below the
911
+ * process memory limit. Default: 1024 MB
912
+ */
913
+ queryCacheMaxMB?: number;
907
914
  /**
908
915
  * Dashboard conversation history TTL (Time To Live) in minutes
909
916
  * Per-dashboard conversation histories expire after this duration
@@ -2780,10 +2787,33 @@ declare class QueryCache {
2780
2787
  private cache;
2781
2788
  private ttlMs;
2782
2789
  private maxCacheSize;
2790
+ private maxCacheBytes;
2791
+ private currentBytes;
2783
2792
  private cleanupInterval;
2784
2793
  private readonly algorithm;
2785
2794
  private encryptionKey;
2786
2795
  constructor();
2796
+ /**
2797
+ * Set the byte budget for cached result data.
2798
+ * @param megabytes - Budget in MB. Keep it well under the process memory limit.
2799
+ */
2800
+ setMaxBytes(megabytes: number): void;
2801
+ /** Current estimated bytes held by the data cache. */
2802
+ getCurrentBytes(): number;
2803
+ /**
2804
+ * Rough retained size of a result, in bytes.
2805
+ *
2806
+ * Sampled rather than exact: stringifying a multi-megabyte result on every set()
2807
+ * would allocate the very memory this budget exists to protect. Estimated from a
2808
+ * few rows of the largest array in the payload, at 2 bytes per UTF-16 char.
2809
+ * Both error directions are safe — under-estimating evicts less than ideal,
2810
+ * over-estimating evicts more, and a miss only re-runs the query.
2811
+ */
2812
+ private estimateBytes;
2813
+ /** Largest array at the top level or one level in — where result rows live. */
2814
+ private largestArray;
2815
+ /** Drop least-recently-used entries until the budget is satisfied. */
2816
+ private evictUntilUnderBudget;
2787
2817
  /**
2788
2818
  * Set the cache TTL (Time To Live)
2789
2819
  * @param minutes - TTL in minutes (default: 10)
@@ -2926,6 +2956,7 @@ declare class SuperatomSDK {
2926
2956
  private components;
2927
2957
  private tools;
2928
2958
  private workflows;
2959
+ private registryWaiters;
2929
2960
  private anthropicApiKey;
2930
2961
  private groqApiKey;
2931
2962
  private geminiApiKey;
@@ -3044,6 +3075,19 @@ declare class SuperatomSDK {
3044
3075
  * the workflow component is returned directly, with the LLM-extracted props.
3045
3076
  */
3046
3077
  setWorkflows(workflows: WorkflowDescriptor[]): void;
3078
+ /**
3079
+ * Make sure the agent has a tool surface before answering a prompt.
3080
+ *
3081
+ * Components and workflows are pushed by the browser once per page load and
3082
+ * kept only in memory, so a backend restart leaves both lists empty until
3083
+ * someone reloads a tab. The agent still answers — with no workflows to route
3084
+ * to and no components to render — which looks like a model regression rather
3085
+ * than lost state. Ask the client to re-send instead, and proceed regardless
3086
+ * so a stale or silent client degrades no worse than it does today.
3087
+ */
3088
+ private ensureRegistry;
3089
+ /** Release anyone blocked in ensureRegistry() now that the list has arrived. */
3090
+ private resolveRegistryWaiters;
3047
3091
  /**
3048
3092
  * Get the registered workflow components.
3049
3093
  */
package/dist/index.d.ts CHANGED
@@ -904,6 +904,13 @@ interface SuperatomSDKConfig {
904
904
  * Default: 5 minutes
905
905
  */
906
906
  queryCacheTTL?: number;
907
+ /**
908
+ * Byte budget for cached query results, in MB.
909
+ * The cache evicts least-recently-used entries to stay under it, so a long TTL
910
+ * cannot grow the process until the host kills it. Keep it well below the
911
+ * process memory limit. Default: 1024 MB
912
+ */
913
+ queryCacheMaxMB?: number;
907
914
  /**
908
915
  * Dashboard conversation history TTL (Time To Live) in minutes
909
916
  * Per-dashboard conversation histories expire after this duration
@@ -2780,10 +2787,33 @@ declare class QueryCache {
2780
2787
  private cache;
2781
2788
  private ttlMs;
2782
2789
  private maxCacheSize;
2790
+ private maxCacheBytes;
2791
+ private currentBytes;
2783
2792
  private cleanupInterval;
2784
2793
  private readonly algorithm;
2785
2794
  private encryptionKey;
2786
2795
  constructor();
2796
+ /**
2797
+ * Set the byte budget for cached result data.
2798
+ * @param megabytes - Budget in MB. Keep it well under the process memory limit.
2799
+ */
2800
+ setMaxBytes(megabytes: number): void;
2801
+ /** Current estimated bytes held by the data cache. */
2802
+ getCurrentBytes(): number;
2803
+ /**
2804
+ * Rough retained size of a result, in bytes.
2805
+ *
2806
+ * Sampled rather than exact: stringifying a multi-megabyte result on every set()
2807
+ * would allocate the very memory this budget exists to protect. Estimated from a
2808
+ * few rows of the largest array in the payload, at 2 bytes per UTF-16 char.
2809
+ * Both error directions are safe — under-estimating evicts less than ideal,
2810
+ * over-estimating evicts more, and a miss only re-runs the query.
2811
+ */
2812
+ private estimateBytes;
2813
+ /** Largest array at the top level or one level in — where result rows live. */
2814
+ private largestArray;
2815
+ /** Drop least-recently-used entries until the budget is satisfied. */
2816
+ private evictUntilUnderBudget;
2787
2817
  /**
2788
2818
  * Set the cache TTL (Time To Live)
2789
2819
  * @param minutes - TTL in minutes (default: 10)
@@ -2926,6 +2956,7 @@ declare class SuperatomSDK {
2926
2956
  private components;
2927
2957
  private tools;
2928
2958
  private workflows;
2959
+ private registryWaiters;
2929
2960
  private anthropicApiKey;
2930
2961
  private groqApiKey;
2931
2962
  private geminiApiKey;
@@ -3044,6 +3075,19 @@ declare class SuperatomSDK {
3044
3075
  * the workflow component is returned directly, with the LLM-extracted props.
3045
3076
  */
3046
3077
  setWorkflows(workflows: WorkflowDescriptor[]): void;
3078
+ /**
3079
+ * Make sure the agent has a tool surface before answering a prompt.
3080
+ *
3081
+ * Components and workflows are pushed by the browser once per page load and
3082
+ * kept only in memory, so a backend restart leaves both lists empty until
3083
+ * someone reloads a tab. The agent still answers — with no workflows to route
3084
+ * to and no components to render — which looks like a model regression rather
3085
+ * than lost state. Ask the client to re-send instead, and proceed regardless
3086
+ * so a stale or silent client degrades no worse than it does today.
3087
+ */
3088
+ private ensureRegistry;
3089
+ /** Release anyone blocked in ensureRegistry() now that the list has arrived. */
3090
+ private resolveRegistryWaiters;
3047
3091
  /**
3048
3092
  * Get the registered workflow components.
3049
3093
  */
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ module.exports = __toCommonJS(index_exports);
61
61
  // src/websocket.ts
62
62
  var import_ws = __toESM(require("ws"));
63
63
  function createWebSocket(url) {
64
- return new import_ws.default(url);
64
+ return new import_ws.default(url, { handshakeTimeout: 15e3 });
65
65
  }
66
66
 
67
67
  // src/types.ts
@@ -1572,6 +1572,13 @@ var QueryCache = class {
1572
1572
  // Default: 10 minutes
1573
1573
  this.maxCacheSize = 5e3;
1574
1574
  // Max data cache entries
1575
+ // A count cap cannot bound memory when entries range from 1KB to 10MB, so the
1576
+ // real limit is in bytes. Without it the process grows until pm2 kills it, which
1577
+ // evicts 100% of the cache — the worst possible eviction policy. LRU sheds the
1578
+ // coldest entries instead and keeps the expensive, frequently-asked queries warm.
1579
+ this.maxCacheBytes = 1024 * 1024 * 1024;
1580
+ // Default: 1 GiB
1581
+ this.currentBytes = 0;
1575
1582
  this.cleanupInterval = null;
1576
1583
  // Encryption for queryId tokens
1577
1584
  this.algorithm = "aes-256-gcm";
@@ -1582,6 +1589,74 @@ var QueryCache = class {
1582
1589
  // ============================================
1583
1590
  // Data Cache (TTL-based, max size)
1584
1591
  // ============================================
1592
+ /**
1593
+ * Set the byte budget for cached result data.
1594
+ * @param megabytes - Budget in MB. Keep it well under the process memory limit.
1595
+ */
1596
+ setMaxBytes(megabytes) {
1597
+ this.maxCacheBytes = Math.max(1, megabytes) * 1024 * 1024;
1598
+ logger.info(`[QueryCache] Byte budget set to ${megabytes} MB`);
1599
+ this.evictUntilUnderBudget();
1600
+ }
1601
+ /** Current estimated bytes held by the data cache. */
1602
+ getCurrentBytes() {
1603
+ return this.currentBytes;
1604
+ }
1605
+ /**
1606
+ * Rough retained size of a result, in bytes.
1607
+ *
1608
+ * Sampled rather than exact: stringifying a multi-megabyte result on every set()
1609
+ * would allocate the very memory this budget exists to protect. Estimated from a
1610
+ * few rows of the largest array in the payload, at 2 bytes per UTF-16 char.
1611
+ * Both error directions are safe — under-estimating evicts less than ideal,
1612
+ * over-estimating evicts more, and a miss only re-runs the query.
1613
+ */
1614
+ estimateBytes(data) {
1615
+ if (data == null) return 0;
1616
+ const rows = this.largestArray(data);
1617
+ if (rows && rows.length > 0) {
1618
+ const sample = Math.min(5, rows.length);
1619
+ let chars = 0;
1620
+ for (let i = 0; i < sample; i++) {
1621
+ try {
1622
+ chars += JSON.stringify(rows[i])?.length ?? 0;
1623
+ } catch {
1624
+ chars += 200;
1625
+ }
1626
+ }
1627
+ return Math.round(chars / sample * rows.length * 2);
1628
+ }
1629
+ try {
1630
+ return (JSON.stringify(data)?.length ?? 0) * 2;
1631
+ } catch {
1632
+ return 1024;
1633
+ }
1634
+ }
1635
+ /** Largest array at the top level or one level in — where result rows live. */
1636
+ largestArray(data) {
1637
+ if (Array.isArray(data)) return data;
1638
+ if (typeof data !== "object") return null;
1639
+ let best = null;
1640
+ for (const value of Object.values(data)) {
1641
+ if (Array.isArray(value) && (!best || value.length > best.length)) best = value;
1642
+ }
1643
+ return best;
1644
+ }
1645
+ /** Drop least-recently-used entries until the budget is satisfied. */
1646
+ evictUntilUnderBudget() {
1647
+ let evicted = 0;
1648
+ while (this.currentBytes > this.maxCacheBytes && this.cache.size > 1) {
1649
+ const oldestKey = this.cache.keys().next().value;
1650
+ if (!oldestKey) break;
1651
+ const entry = this.cache.get(oldestKey);
1652
+ this.cache.delete(oldestKey);
1653
+ if (entry) this.currentBytes -= entry.bytes;
1654
+ evicted++;
1655
+ }
1656
+ if (evicted > 0) {
1657
+ logger.info(`[QueryCache] Evicted ${evicted} LRU entr${evicted === 1 ? "y" : "ies"} for the ${Math.round(this.maxCacheBytes / 1048576)}MB budget (now ${Math.round(this.currentBytes / 1048576)}MB over ${this.cache.size} entries)`);
1658
+ }
1659
+ }
1585
1660
  /**
1586
1661
  * Set the cache TTL (Time To Live)
1587
1662
  * @param minutes - TTL in minutes (default: 10)
@@ -1603,16 +1678,32 @@ var QueryCache = class {
1603
1678
  * a genuinely new key past the size limit.
1604
1679
  */
1605
1680
  set(query, data) {
1606
- this.cache.delete(query);
1681
+ const previous = this.cache.get(query);
1682
+ if (previous) {
1683
+ this.cache.delete(query);
1684
+ this.currentBytes -= previous.bytes;
1685
+ }
1686
+ const bytes = this.estimateBytes(data);
1687
+ if (bytes > this.maxCacheBytes / 4) {
1688
+ logger.warn(`[QueryCache] Not caching a ${Math.round(bytes / 1048576)}MB result \u2014 over a quarter of the ${Math.round(this.maxCacheBytes / 1048576)}MB budget`);
1689
+ return;
1690
+ }
1607
1691
  if (this.cache.size >= this.maxCacheSize) {
1608
1692
  const oldestKey = this.cache.keys().next().value;
1609
- if (oldestKey) this.cache.delete(oldestKey);
1693
+ if (oldestKey) {
1694
+ const oldest = this.cache.get(oldestKey);
1695
+ this.cache.delete(oldestKey);
1696
+ if (oldest) this.currentBytes -= oldest.bytes;
1697
+ }
1610
1698
  }
1611
1699
  this.cache.set(query, {
1612
1700
  query,
1613
1701
  data,
1614
- timestamp: Date.now()
1702
+ timestamp: Date.now(),
1703
+ bytes
1615
1704
  });
1705
+ this.currentBytes += bytes;
1706
+ this.evictUntilUnderBudget();
1616
1707
  logger.debug(`[QueryCache] Stored result for query (${query.substring(0, 50)}...)`);
1617
1708
  }
1618
1709
  /**
@@ -1625,6 +1716,7 @@ var QueryCache = class {
1625
1716
  if (!entry) return null;
1626
1717
  if (Date.now() - entry.timestamp > this.ttlMs) {
1627
1718
  this.cache.delete(query);
1719
+ this.currentBytes -= entry.bytes;
1628
1720
  return null;
1629
1721
  }
1630
1722
  this.cache.delete(query);
@@ -1642,13 +1734,16 @@ var QueryCache = class {
1642
1734
  * Remove a specific query from cache
1643
1735
  */
1644
1736
  delete(query) {
1737
+ const entry = this.cache.get(query);
1645
1738
  this.cache.delete(query);
1739
+ if (entry) this.currentBytes -= entry.bytes;
1646
1740
  }
1647
1741
  /**
1648
1742
  * Clear all cached entries
1649
1743
  */
1650
1744
  clear() {
1651
1745
  this.cache.clear();
1746
+ this.currentBytes = 0;
1652
1747
  logger.info("[QueryCache] Cache cleared");
1653
1748
  }
1654
1749
  /**
@@ -1678,6 +1773,7 @@ var QueryCache = class {
1678
1773
  for (const [key, entry] of this.cache.entries()) {
1679
1774
  if (now - entry.timestamp > this.ttlMs) {
1680
1775
  this.cache.delete(key);
1776
+ this.currentBytes -= entry.bytes;
1681
1777
  expiredCount++;
1682
1778
  }
1683
1779
  }
@@ -6136,6 +6232,7 @@ var LLM = class {
6136
6232
  try {
6137
6233
  currentToolUse.input = currentToolUse.inputJson ? JSON.parse(currentToolUse.inputJson) : {};
6138
6234
  } catch (error) {
6235
+ logger.error(`[LLM] Tool "${currentToolUse.name}" input was not valid JSON, falling back to {}: ${String(currentToolUse.inputJson).substring(0, 500)}`);
6139
6236
  currentToolUse.input = {};
6140
6237
  }
6141
6238
  delete currentToolUse.inputJson;
@@ -6182,6 +6279,7 @@ var LLM = class {
6182
6279
  if (stopReason === "tool_use") {
6183
6280
  const toolUses = contentBlocks.filter((block) => block.type === "tool_use");
6184
6281
  if (toolUses.length === 0) {
6282
+ logger.error(`[LLM] stop_reason=tool_use but no tool_use block survived parsing (${contentBlocks.length} content block(s))`);
6185
6283
  break;
6186
6284
  }
6187
6285
  conversationMessages.push({
@@ -6208,10 +6306,12 @@ var LLM = class {
6208
6306
  content: stripLoneSurrogates(resultContent)
6209
6307
  };
6210
6308
  } catch (error) {
6309
+ const message = error instanceof Error ? error.message : String(error);
6310
+ logger.error(`[LLM] Tool "${toolUse.name}" threw: ${message} | input: ${JSON.stringify(toolUse.input).substring(0, 300)}`);
6211
6311
  return {
6212
6312
  type: "tool_result",
6213
6313
  tool_use_id: toolUse.id,
6214
- content: error instanceof Error ? error.message : String(error),
6314
+ content: message,
6215
6315
  is_error: true
6216
6316
  };
6217
6317
  }
@@ -17546,6 +17646,8 @@ var SuperatomSDK = class {
17546
17646
  this.components = [];
17547
17647
  this.tools = [];
17548
17648
  this.workflows = [];
17649
+ // Resolvers waiting on a registry re-send, keyed by the RES type expected.
17650
+ this.registryWaiters = /* @__PURE__ */ new Map();
17549
17651
  // Heartbeat properties for keeping WebSocket connection alive.
17550
17652
  // 25s ping + 10s grace stays under common NAT/LB idle thresholds (~60-100s)
17551
17653
  // so we detect dead sockets within seconds instead of minutes.
@@ -17577,6 +17679,9 @@ var SuperatomSDK = class {
17577
17679
  if (config.queryCacheTTL !== void 0) {
17578
17680
  queryCache.setTTL(config.queryCacheTTL);
17579
17681
  }
17682
+ if (config.queryCacheMaxMB !== void 0) {
17683
+ queryCache.setMaxBytes(config.queryCacheMaxMB);
17684
+ }
17580
17685
  if (config.dashboardHistoryTTL !== void 0) {
17581
17686
  dashboardConversationHistory.setTTL(config.dashboardHistoryTTL);
17582
17687
  }
@@ -17728,7 +17833,9 @@ var SuperatomSDK = class {
17728
17833
  });
17729
17834
  break;
17730
17835
  case "USER_PROMPT_REQ":
17731
- 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) => {
17836
+ this.ensureRegistry().then(
17837
+ () => 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)
17838
+ ).catch((error) => {
17732
17839
  logger.error("Failed to handle user prompt request:", error);
17733
17840
  });
17734
17841
  break;
@@ -17743,13 +17850,21 @@ var SuperatomSDK = class {
17743
17850
  });
17744
17851
  break;
17745
17852
  case "COMPONENT_LIST_RES":
17746
- handleComponentListResponse(parsed, (com) => this.storeComponents(com), this.collections).catch((error) => {
17853
+ handleComponentListResponse(parsed, (com) => {
17854
+ this.storeComponents(com);
17855
+ this.resolveRegistryWaiters("COMPONENT_LIST_RES");
17856
+ }, this.collections).catch((error) => {
17747
17857
  logger.error("Failed to handle component list request:", error);
17858
+ this.resolveRegistryWaiters("COMPONENT_LIST_RES");
17748
17859
  });
17749
17860
  break;
17750
17861
  case "WORKFLOW_LIST_RES":
17751
- handleWorkflowListResponse(parsed, (wf) => this.setWorkflows(wf)).catch((error) => {
17862
+ handleWorkflowListResponse(parsed, (wf) => {
17863
+ this.setWorkflows(wf);
17864
+ this.resolveRegistryWaiters("WORKFLOW_LIST_RES");
17865
+ }).catch((error) => {
17752
17866
  logger.error("Failed to handle workflow list request:", error);
17867
+ this.resolveRegistryWaiters("WORKFLOW_LIST_RES");
17753
17868
  });
17754
17869
  break;
17755
17870
  case "USERS":
@@ -18003,6 +18118,50 @@ var SuperatomSDK = class {
18003
18118
  this.workflows = workflows;
18004
18119
  logger.info(`Workflows stored in SDK: ${workflows.length} workflow(s)`);
18005
18120
  }
18121
+ /**
18122
+ * Make sure the agent has a tool surface before answering a prompt.
18123
+ *
18124
+ * Components and workflows are pushed by the browser once per page load and
18125
+ * kept only in memory, so a backend restart leaves both lists empty until
18126
+ * someone reloads a tab. The agent still answers — with no workflows to route
18127
+ * to and no components to render — which looks like a model regression rather
18128
+ * than lost state. Ask the client to re-send instead, and proceed regardless
18129
+ * so a stale or silent client degrades no worse than it does today.
18130
+ */
18131
+ async ensureRegistry(timeoutMs = 4e3) {
18132
+ const missing = [];
18133
+ if (this.workflows.length === 0) missing.push(["WORKFLOW_LIST_REQ", "WORKFLOW_LIST_RES"]);
18134
+ if (this.components.length === 0) missing.push(["COMPONENT_LIST_REQ", "COMPONENT_LIST_RES"]);
18135
+ if (missing.length === 0) return;
18136
+ logger.warn(`Registry empty, requesting from client: ${missing.map(([req]) => req).join(", ")}`);
18137
+ await Promise.all(missing.map(([reqType, resType]) => new Promise((resolve) => {
18138
+ let settled = false;
18139
+ const finish = () => {
18140
+ if (!settled) {
18141
+ settled = true;
18142
+ resolve();
18143
+ }
18144
+ };
18145
+ const waiters = this.registryWaiters.get(resType) ?? [];
18146
+ waiters.push(finish);
18147
+ this.registryWaiters.set(resType, waiters);
18148
+ const sent = this.send({
18149
+ type: reqType,
18150
+ from: { type: this.type },
18151
+ to: { type: "runtime" },
18152
+ payload: {}
18153
+ });
18154
+ if (!sent) finish();
18155
+ else setTimeout(finish, timeoutMs);
18156
+ })));
18157
+ }
18158
+ /** Release anyone blocked in ensureRegistry() now that the list has arrived. */
18159
+ resolveRegistryWaiters(resType) {
18160
+ const waiters = this.registryWaiters.get(resType);
18161
+ if (!waiters || waiters.length === 0) return;
18162
+ this.registryWaiters.delete(resType);
18163
+ waiters.forEach((resolve) => resolve());
18164
+ }
18006
18165
  /**
18007
18166
  * Get the registered workflow components.
18008
18167
  */
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/websocket.ts
2
2
  import WebSocket from "ws";
3
3
  function createWebSocket(url) {
4
- return new WebSocket(url);
4
+ return new WebSocket(url, { handshakeTimeout: 15e3 });
5
5
  }
6
6
 
7
7
  // src/types.ts
@@ -1512,6 +1512,13 @@ var QueryCache = class {
1512
1512
  // Default: 10 minutes
1513
1513
  this.maxCacheSize = 5e3;
1514
1514
  // Max data cache entries
1515
+ // A count cap cannot bound memory when entries range from 1KB to 10MB, so the
1516
+ // real limit is in bytes. Without it the process grows until pm2 kills it, which
1517
+ // evicts 100% of the cache — the worst possible eviction policy. LRU sheds the
1518
+ // coldest entries instead and keeps the expensive, frequently-asked queries warm.
1519
+ this.maxCacheBytes = 1024 * 1024 * 1024;
1520
+ // Default: 1 GiB
1521
+ this.currentBytes = 0;
1515
1522
  this.cleanupInterval = null;
1516
1523
  // Encryption for queryId tokens
1517
1524
  this.algorithm = "aes-256-gcm";
@@ -1522,6 +1529,74 @@ var QueryCache = class {
1522
1529
  // ============================================
1523
1530
  // Data Cache (TTL-based, max size)
1524
1531
  // ============================================
1532
+ /**
1533
+ * Set the byte budget for cached result data.
1534
+ * @param megabytes - Budget in MB. Keep it well under the process memory limit.
1535
+ */
1536
+ setMaxBytes(megabytes) {
1537
+ this.maxCacheBytes = Math.max(1, megabytes) * 1024 * 1024;
1538
+ logger.info(`[QueryCache] Byte budget set to ${megabytes} MB`);
1539
+ this.evictUntilUnderBudget();
1540
+ }
1541
+ /** Current estimated bytes held by the data cache. */
1542
+ getCurrentBytes() {
1543
+ return this.currentBytes;
1544
+ }
1545
+ /**
1546
+ * Rough retained size of a result, in bytes.
1547
+ *
1548
+ * Sampled rather than exact: stringifying a multi-megabyte result on every set()
1549
+ * would allocate the very memory this budget exists to protect. Estimated from a
1550
+ * few rows of the largest array in the payload, at 2 bytes per UTF-16 char.
1551
+ * Both error directions are safe — under-estimating evicts less than ideal,
1552
+ * over-estimating evicts more, and a miss only re-runs the query.
1553
+ */
1554
+ estimateBytes(data) {
1555
+ if (data == null) return 0;
1556
+ const rows = this.largestArray(data);
1557
+ if (rows && rows.length > 0) {
1558
+ const sample = Math.min(5, rows.length);
1559
+ let chars = 0;
1560
+ for (let i = 0; i < sample; i++) {
1561
+ try {
1562
+ chars += JSON.stringify(rows[i])?.length ?? 0;
1563
+ } catch {
1564
+ chars += 200;
1565
+ }
1566
+ }
1567
+ return Math.round(chars / sample * rows.length * 2);
1568
+ }
1569
+ try {
1570
+ return (JSON.stringify(data)?.length ?? 0) * 2;
1571
+ } catch {
1572
+ return 1024;
1573
+ }
1574
+ }
1575
+ /** Largest array at the top level or one level in — where result rows live. */
1576
+ largestArray(data) {
1577
+ if (Array.isArray(data)) return data;
1578
+ if (typeof data !== "object") return null;
1579
+ let best = null;
1580
+ for (const value of Object.values(data)) {
1581
+ if (Array.isArray(value) && (!best || value.length > best.length)) best = value;
1582
+ }
1583
+ return best;
1584
+ }
1585
+ /** Drop least-recently-used entries until the budget is satisfied. */
1586
+ evictUntilUnderBudget() {
1587
+ let evicted = 0;
1588
+ while (this.currentBytes > this.maxCacheBytes && this.cache.size > 1) {
1589
+ const oldestKey = this.cache.keys().next().value;
1590
+ if (!oldestKey) break;
1591
+ const entry = this.cache.get(oldestKey);
1592
+ this.cache.delete(oldestKey);
1593
+ if (entry) this.currentBytes -= entry.bytes;
1594
+ evicted++;
1595
+ }
1596
+ if (evicted > 0) {
1597
+ logger.info(`[QueryCache] Evicted ${evicted} LRU entr${evicted === 1 ? "y" : "ies"} for the ${Math.round(this.maxCacheBytes / 1048576)}MB budget (now ${Math.round(this.currentBytes / 1048576)}MB over ${this.cache.size} entries)`);
1598
+ }
1599
+ }
1525
1600
  /**
1526
1601
  * Set the cache TTL (Time To Live)
1527
1602
  * @param minutes - TTL in minutes (default: 10)
@@ -1543,16 +1618,32 @@ var QueryCache = class {
1543
1618
  * a genuinely new key past the size limit.
1544
1619
  */
1545
1620
  set(query, data) {
1546
- this.cache.delete(query);
1621
+ const previous = this.cache.get(query);
1622
+ if (previous) {
1623
+ this.cache.delete(query);
1624
+ this.currentBytes -= previous.bytes;
1625
+ }
1626
+ const bytes = this.estimateBytes(data);
1627
+ if (bytes > this.maxCacheBytes / 4) {
1628
+ logger.warn(`[QueryCache] Not caching a ${Math.round(bytes / 1048576)}MB result \u2014 over a quarter of the ${Math.round(this.maxCacheBytes / 1048576)}MB budget`);
1629
+ return;
1630
+ }
1547
1631
  if (this.cache.size >= this.maxCacheSize) {
1548
1632
  const oldestKey = this.cache.keys().next().value;
1549
- if (oldestKey) this.cache.delete(oldestKey);
1633
+ if (oldestKey) {
1634
+ const oldest = this.cache.get(oldestKey);
1635
+ this.cache.delete(oldestKey);
1636
+ if (oldest) this.currentBytes -= oldest.bytes;
1637
+ }
1550
1638
  }
1551
1639
  this.cache.set(query, {
1552
1640
  query,
1553
1641
  data,
1554
- timestamp: Date.now()
1642
+ timestamp: Date.now(),
1643
+ bytes
1555
1644
  });
1645
+ this.currentBytes += bytes;
1646
+ this.evictUntilUnderBudget();
1556
1647
  logger.debug(`[QueryCache] Stored result for query (${query.substring(0, 50)}...)`);
1557
1648
  }
1558
1649
  /**
@@ -1565,6 +1656,7 @@ var QueryCache = class {
1565
1656
  if (!entry) return null;
1566
1657
  if (Date.now() - entry.timestamp > this.ttlMs) {
1567
1658
  this.cache.delete(query);
1659
+ this.currentBytes -= entry.bytes;
1568
1660
  return null;
1569
1661
  }
1570
1662
  this.cache.delete(query);
@@ -1582,13 +1674,16 @@ var QueryCache = class {
1582
1674
  * Remove a specific query from cache
1583
1675
  */
1584
1676
  delete(query) {
1677
+ const entry = this.cache.get(query);
1585
1678
  this.cache.delete(query);
1679
+ if (entry) this.currentBytes -= entry.bytes;
1586
1680
  }
1587
1681
  /**
1588
1682
  * Clear all cached entries
1589
1683
  */
1590
1684
  clear() {
1591
1685
  this.cache.clear();
1686
+ this.currentBytes = 0;
1592
1687
  logger.info("[QueryCache] Cache cleared");
1593
1688
  }
1594
1689
  /**
@@ -1618,6 +1713,7 @@ var QueryCache = class {
1618
1713
  for (const [key, entry] of this.cache.entries()) {
1619
1714
  if (now - entry.timestamp > this.ttlMs) {
1620
1715
  this.cache.delete(key);
1716
+ this.currentBytes -= entry.bytes;
1621
1717
  expiredCount++;
1622
1718
  }
1623
1719
  }
@@ -6076,6 +6172,7 @@ var LLM = class {
6076
6172
  try {
6077
6173
  currentToolUse.input = currentToolUse.inputJson ? JSON.parse(currentToolUse.inputJson) : {};
6078
6174
  } catch (error) {
6175
+ logger.error(`[LLM] Tool "${currentToolUse.name}" input was not valid JSON, falling back to {}: ${String(currentToolUse.inputJson).substring(0, 500)}`);
6079
6176
  currentToolUse.input = {};
6080
6177
  }
6081
6178
  delete currentToolUse.inputJson;
@@ -6122,6 +6219,7 @@ var LLM = class {
6122
6219
  if (stopReason === "tool_use") {
6123
6220
  const toolUses = contentBlocks.filter((block) => block.type === "tool_use");
6124
6221
  if (toolUses.length === 0) {
6222
+ logger.error(`[LLM] stop_reason=tool_use but no tool_use block survived parsing (${contentBlocks.length} content block(s))`);
6125
6223
  break;
6126
6224
  }
6127
6225
  conversationMessages.push({
@@ -6148,10 +6246,12 @@ var LLM = class {
6148
6246
  content: stripLoneSurrogates(resultContent)
6149
6247
  };
6150
6248
  } catch (error) {
6249
+ const message = error instanceof Error ? error.message : String(error);
6250
+ logger.error(`[LLM] Tool "${toolUse.name}" threw: ${message} | input: ${JSON.stringify(toolUse.input).substring(0, 300)}`);
6151
6251
  return {
6152
6252
  type: "tool_result",
6153
6253
  tool_use_id: toolUse.id,
6154
- content: error instanceof Error ? error.message : String(error),
6254
+ content: message,
6155
6255
  is_error: true
6156
6256
  };
6157
6257
  }
@@ -17486,6 +17586,8 @@ var SuperatomSDK = class {
17486
17586
  this.components = [];
17487
17587
  this.tools = [];
17488
17588
  this.workflows = [];
17589
+ // Resolvers waiting on a registry re-send, keyed by the RES type expected.
17590
+ this.registryWaiters = /* @__PURE__ */ new Map();
17489
17591
  // Heartbeat properties for keeping WebSocket connection alive.
17490
17592
  // 25s ping + 10s grace stays under common NAT/LB idle thresholds (~60-100s)
17491
17593
  // so we detect dead sockets within seconds instead of minutes.
@@ -17517,6 +17619,9 @@ var SuperatomSDK = class {
17517
17619
  if (config.queryCacheTTL !== void 0) {
17518
17620
  queryCache.setTTL(config.queryCacheTTL);
17519
17621
  }
17622
+ if (config.queryCacheMaxMB !== void 0) {
17623
+ queryCache.setMaxBytes(config.queryCacheMaxMB);
17624
+ }
17520
17625
  if (config.dashboardHistoryTTL !== void 0) {
17521
17626
  dashboardConversationHistory.setTTL(config.dashboardHistoryTTL);
17522
17627
  }
@@ -17668,7 +17773,9 @@ var SuperatomSDK = class {
17668
17773
  });
17669
17774
  break;
17670
17775
  case "USER_PROMPT_REQ":
17671
- 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) => {
17776
+ this.ensureRegistry().then(
17777
+ () => 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)
17778
+ ).catch((error) => {
17672
17779
  logger.error("Failed to handle user prompt request:", error);
17673
17780
  });
17674
17781
  break;
@@ -17683,13 +17790,21 @@ var SuperatomSDK = class {
17683
17790
  });
17684
17791
  break;
17685
17792
  case "COMPONENT_LIST_RES":
17686
- handleComponentListResponse(parsed, (com) => this.storeComponents(com), this.collections).catch((error) => {
17793
+ handleComponentListResponse(parsed, (com) => {
17794
+ this.storeComponents(com);
17795
+ this.resolveRegistryWaiters("COMPONENT_LIST_RES");
17796
+ }, this.collections).catch((error) => {
17687
17797
  logger.error("Failed to handle component list request:", error);
17798
+ this.resolveRegistryWaiters("COMPONENT_LIST_RES");
17688
17799
  });
17689
17800
  break;
17690
17801
  case "WORKFLOW_LIST_RES":
17691
- handleWorkflowListResponse(parsed, (wf) => this.setWorkflows(wf)).catch((error) => {
17802
+ handleWorkflowListResponse(parsed, (wf) => {
17803
+ this.setWorkflows(wf);
17804
+ this.resolveRegistryWaiters("WORKFLOW_LIST_RES");
17805
+ }).catch((error) => {
17692
17806
  logger.error("Failed to handle workflow list request:", error);
17807
+ this.resolveRegistryWaiters("WORKFLOW_LIST_RES");
17693
17808
  });
17694
17809
  break;
17695
17810
  case "USERS":
@@ -17943,6 +18058,50 @@ var SuperatomSDK = class {
17943
18058
  this.workflows = workflows;
17944
18059
  logger.info(`Workflows stored in SDK: ${workflows.length} workflow(s)`);
17945
18060
  }
18061
+ /**
18062
+ * Make sure the agent has a tool surface before answering a prompt.
18063
+ *
18064
+ * Components and workflows are pushed by the browser once per page load and
18065
+ * kept only in memory, so a backend restart leaves both lists empty until
18066
+ * someone reloads a tab. The agent still answers — with no workflows to route
18067
+ * to and no components to render — which looks like a model regression rather
18068
+ * than lost state. Ask the client to re-send instead, and proceed regardless
18069
+ * so a stale or silent client degrades no worse than it does today.
18070
+ */
18071
+ async ensureRegistry(timeoutMs = 4e3) {
18072
+ const missing = [];
18073
+ if (this.workflows.length === 0) missing.push(["WORKFLOW_LIST_REQ", "WORKFLOW_LIST_RES"]);
18074
+ if (this.components.length === 0) missing.push(["COMPONENT_LIST_REQ", "COMPONENT_LIST_RES"]);
18075
+ if (missing.length === 0) return;
18076
+ logger.warn(`Registry empty, requesting from client: ${missing.map(([req]) => req).join(", ")}`);
18077
+ await Promise.all(missing.map(([reqType, resType]) => new Promise((resolve) => {
18078
+ let settled = false;
18079
+ const finish = () => {
18080
+ if (!settled) {
18081
+ settled = true;
18082
+ resolve();
18083
+ }
18084
+ };
18085
+ const waiters = this.registryWaiters.get(resType) ?? [];
18086
+ waiters.push(finish);
18087
+ this.registryWaiters.set(resType, waiters);
18088
+ const sent = this.send({
18089
+ type: reqType,
18090
+ from: { type: this.type },
18091
+ to: { type: "runtime" },
18092
+ payload: {}
18093
+ });
18094
+ if (!sent) finish();
18095
+ else setTimeout(finish, timeoutMs);
18096
+ })));
18097
+ }
18098
+ /** Release anyone blocked in ensureRegistry() now that the list has arrived. */
18099
+ resolveRegistryWaiters(resType) {
18100
+ const waiters = this.registryWaiters.get(resType);
18101
+ if (!waiters || waiters.length === 0) return;
18102
+ this.registryWaiters.delete(resType);
18103
+ waiters.forEach((resolve) => resolve());
18104
+ }
17946
18105
  /**
17947
18106
  * Get the registered workflow components.
17948
18107
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superatomai/sdk-node",
3
- "version": "0.0.51-mds",
3
+ "version": "0.0.52-mds",
4
4
  "description": "Node.js TypeScript SDK for Superatom",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",