@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 +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +167 -8
- package/dist/index.mjs +167 -8
- package/package.json +1 -1
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.
|
|
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)
|
|
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:
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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.
|
|
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)
|
|
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:
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
*/
|