@axlsdk/axl 0.17.6 → 0.17.8
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/README.md +62 -0
- package/dist/index.cjs +938 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +379 -2
- package/dist/index.d.ts +379 -2
- package/dist/index.js +938 -74
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1367,7 +1367,8 @@ var GEMINI_PRICING = {
|
|
|
1367
1367
|
"gemini-3-flash-preview": [5e-7, 3e-6],
|
|
1368
1368
|
"gemini-3.1-pro-preview": [2e-6, 12e-6],
|
|
1369
1369
|
"gemini-3.1-flash-lite": [25e-8, 15e-7],
|
|
1370
|
-
"gemini-3.1-flash-lite-preview": [25e-8, 15e-7]
|
|
1370
|
+
"gemini-3.1-flash-lite-preview": [25e-8, 15e-7],
|
|
1371
|
+
"gemini-3.5-flash": [15e-7, 9e-6]
|
|
1371
1372
|
};
|
|
1372
1373
|
var GEMINI_PRICING_KEYS_BY_LENGTH = Object.keys(GEMINI_PRICING).sort(
|
|
1373
1374
|
(a, b) => b.length - a.length
|
|
@@ -1607,12 +1608,21 @@ var GeminiProvider = class {
|
|
|
1607
1608
|
*/
|
|
1608
1609
|
mapMessages(messages) {
|
|
1609
1610
|
const toolCallIdToName = /* @__PURE__ */ new Map();
|
|
1611
|
+
const geminiNativeIds = /* @__PURE__ */ new Set();
|
|
1610
1612
|
for (const msg of messages) {
|
|
1611
1613
|
if (msg.role === "assistant" && msg.tool_calls) {
|
|
1612
1614
|
for (const tc of msg.tool_calls) {
|
|
1613
1615
|
toolCallIdToName.set(tc.id, tc.function.name);
|
|
1614
1616
|
}
|
|
1615
1617
|
}
|
|
1618
|
+
if (msg.role === "assistant") {
|
|
1619
|
+
const rawParts = msg.providerMetadata?.geminiParts;
|
|
1620
|
+
if (rawParts) {
|
|
1621
|
+
for (const p of rawParts) {
|
|
1622
|
+
if (p.functionCall?.id) geminiNativeIds.add(p.functionCall.id);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1616
1626
|
}
|
|
1617
1627
|
const result = [];
|
|
1618
1628
|
for (const msg of messages) {
|
|
@@ -1653,16 +1663,16 @@ var GeminiProvider = class {
|
|
|
1653
1663
|
} catch {
|
|
1654
1664
|
responseData = { result: msg.content };
|
|
1655
1665
|
}
|
|
1666
|
+
const functionResponse = {
|
|
1667
|
+
name: functionName,
|
|
1668
|
+
response: responseData
|
|
1669
|
+
};
|
|
1670
|
+
if (msg.tool_call_id && geminiNativeIds.has(msg.tool_call_id)) {
|
|
1671
|
+
functionResponse.id = msg.tool_call_id;
|
|
1672
|
+
}
|
|
1656
1673
|
result.push({
|
|
1657
1674
|
role: "user",
|
|
1658
|
-
parts: [
|
|
1659
|
-
{
|
|
1660
|
-
functionResponse: {
|
|
1661
|
-
name: functionName,
|
|
1662
|
-
response: responseData
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
]
|
|
1675
|
+
parts: [{ functionResponse }]
|
|
1666
1676
|
});
|
|
1667
1677
|
} else if (msg.role === "user") {
|
|
1668
1678
|
result.push({ role: "user", parts: [{ text: msg.content }] });
|
|
@@ -1730,7 +1740,7 @@ var GeminiProvider = class {
|
|
|
1730
1740
|
content += part.text;
|
|
1731
1741
|
} else if (part.functionCall) {
|
|
1732
1742
|
toolCalls.push({
|
|
1733
|
-
id: `call_${this.callCounter++}`,
|
|
1743
|
+
id: part.functionCall.id || `call_${this.callCounter++}`,
|
|
1734
1744
|
type: "function",
|
|
1735
1745
|
function: {
|
|
1736
1746
|
name: part.functionCall.name,
|
|
@@ -1810,7 +1820,7 @@ var GeminiProvider = class {
|
|
|
1810
1820
|
} else if (part.functionCall) {
|
|
1811
1821
|
yield {
|
|
1812
1822
|
type: "tool_call_delta",
|
|
1813
|
-
id: `call_${this.callCounter++}`,
|
|
1823
|
+
id: part.functionCall.id || `call_${this.callCounter++}`,
|
|
1814
1824
|
name: part.functionCall.name,
|
|
1815
1825
|
arguments: JSON.stringify(part.functionCall.args)
|
|
1816
1826
|
};
|
|
@@ -5914,8 +5924,31 @@ ${summaryResponse.content}`
|
|
|
5914
5924
|
type: "await_human",
|
|
5915
5925
|
data: { channel: options.channel, prompt: options.prompt }
|
|
5916
5926
|
});
|
|
5917
|
-
const
|
|
5927
|
+
const makeAbortError = (reason) => {
|
|
5928
|
+
if (typeof DOMException !== "undefined") {
|
|
5929
|
+
return new DOMException(
|
|
5930
|
+
typeof reason === "string" ? reason : "awaitHuman aborted",
|
|
5931
|
+
"AbortError"
|
|
5932
|
+
);
|
|
5933
|
+
}
|
|
5934
|
+
const err = new Error(typeof reason === "string" ? reason : "awaitHuman aborted");
|
|
5935
|
+
err.name = "AbortError";
|
|
5936
|
+
return err;
|
|
5937
|
+
};
|
|
5938
|
+
const decision = await new Promise((resolve2, reject) => {
|
|
5939
|
+
if (this.signal?.aborted) {
|
|
5940
|
+
reject(makeAbortError(this.signal.reason));
|
|
5941
|
+
return;
|
|
5942
|
+
}
|
|
5918
5943
|
this.pendingDecisions.set(this.executionId, resolve2);
|
|
5944
|
+
const onAbort = () => {
|
|
5945
|
+
const current = this.pendingDecisions.get(this.executionId);
|
|
5946
|
+
if (current === resolve2) {
|
|
5947
|
+
this.pendingDecisions.delete(this.executionId);
|
|
5948
|
+
}
|
|
5949
|
+
reject(makeAbortError(this.signal?.reason));
|
|
5950
|
+
};
|
|
5951
|
+
this.signal?.addEventListener("abort", onAbort, { once: true });
|
|
5919
5952
|
});
|
|
5920
5953
|
this.emitEvent({
|
|
5921
5954
|
type: "await_human_resolved",
|
|
@@ -6540,6 +6573,16 @@ var MemoryStore = class {
|
|
|
6540
6573
|
const result = limit ? sorted.slice(0, limit) : sorted;
|
|
6541
6574
|
return result.map((e) => structuredClone(e));
|
|
6542
6575
|
}
|
|
6576
|
+
async deleteExecution(executionId) {
|
|
6577
|
+
const removed = this.executionHistory.delete(executionId);
|
|
6578
|
+
this.checkpoints.delete(executionId);
|
|
6579
|
+
this.executionStates.delete(executionId);
|
|
6580
|
+
this.streamingEvents.delete(executionId);
|
|
6581
|
+
if (this.decisions.delete(executionId)) {
|
|
6582
|
+
this.persistAwaitHumanState();
|
|
6583
|
+
}
|
|
6584
|
+
return removed;
|
|
6585
|
+
}
|
|
6543
6586
|
// ── Eval History ──────────────────────────────────────────────────
|
|
6544
6587
|
async saveEvalResult(entry) {
|
|
6545
6588
|
this.evalHistory.set(entry.id, structuredClone(entry));
|
|
@@ -6556,6 +6599,30 @@ var MemoryStore = class {
|
|
|
6556
6599
|
async listSessions() {
|
|
6557
6600
|
return [...this.sessions.keys()];
|
|
6558
6601
|
}
|
|
6602
|
+
// ── Streaming events (for state.persist: 'streaming') ────────────────
|
|
6603
|
+
//
|
|
6604
|
+
// MemoryStore's raison d'être is in-process / ephemeral. We implement
|
|
6605
|
+
// the streaming methods so tests and dev workflows behave correctly,
|
|
6606
|
+
// but on a process crash these buffers are lost — same as the rest of
|
|
6607
|
+
// MemoryStore's state. For real crash-survival, use RedisStore or
|
|
6608
|
+
// SQLiteStore.
|
|
6609
|
+
streamingEvents = /* @__PURE__ */ new Map();
|
|
6610
|
+
async appendStreamingEvents(executionId, events) {
|
|
6611
|
+
if (events.length === 0) return;
|
|
6612
|
+
const existing = this.streamingEvents.get(executionId) ?? [];
|
|
6613
|
+
existing.push(...events.map((e) => structuredClone(e)));
|
|
6614
|
+
this.streamingEvents.set(executionId, existing);
|
|
6615
|
+
}
|
|
6616
|
+
async finalizeStreamingEvents(executionId) {
|
|
6617
|
+
this.streamingEvents.delete(executionId);
|
|
6618
|
+
}
|
|
6619
|
+
async listStreamingExecutions() {
|
|
6620
|
+
return [...this.streamingEvents.keys()];
|
|
6621
|
+
}
|
|
6622
|
+
async getStreamingEvents(executionId) {
|
|
6623
|
+
const events = this.streamingEvents.get(executionId) ?? [];
|
|
6624
|
+
return events.map((e) => structuredClone(e));
|
|
6625
|
+
}
|
|
6559
6626
|
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
6560
6627
|
async close() {
|
|
6561
6628
|
}
|
|
@@ -6612,7 +6679,7 @@ function safeJsonParse(data) {
|
|
|
6612
6679
|
}
|
|
6613
6680
|
}
|
|
6614
6681
|
function rowToExecutionInfo(row) {
|
|
6615
|
-
|
|
6682
|
+
const info = {
|
|
6616
6683
|
executionId: row.execution_id,
|
|
6617
6684
|
workflow: row.workflow,
|
|
6618
6685
|
status: row.status,
|
|
@@ -6623,6 +6690,13 @@ function rowToExecutionInfo(row) {
|
|
|
6623
6690
|
error: row.error ?? void 0,
|
|
6624
6691
|
events: safeJsonParse(row.events) ?? []
|
|
6625
6692
|
};
|
|
6693
|
+
if (row.metadata != null) {
|
|
6694
|
+
const parsed = safeJsonParse(row.metadata);
|
|
6695
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
6696
|
+
info.metadata = parsed;
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
return info;
|
|
6626
6700
|
}
|
|
6627
6701
|
var SQLiteStore = class {
|
|
6628
6702
|
db;
|
|
@@ -6658,6 +6732,10 @@ var SQLiteStore = class {
|
|
|
6658
6732
|
* numeric step values become stringified names ("0", "1", …) to
|
|
6659
6733
|
* preserve replay continuity for in-flight executions across the
|
|
6660
6734
|
* upgrade boundary.
|
|
6735
|
+
* - v3 (ExecutionInfo.metadata): add `execution_history.metadata`
|
|
6736
|
+
* column (TEXT, nullable, JSON-serialized) so caller-supplied
|
|
6737
|
+
* `ExecuteOptions.metadata` round-trips through history. Existing
|
|
6738
|
+
* rows get `NULL`, deserialized as `metadata: undefined`.
|
|
6661
6739
|
*
|
|
6662
6740
|
* Applied BEFORE `initTables()` so the subsequent `CREATE TABLE
|
|
6663
6741
|
* IF NOT EXISTS` runs against the post-migration column name. Fresh
|
|
@@ -6675,7 +6753,7 @@ var SQLiteStore = class {
|
|
|
6675
6753
|
* transactional re-read costs nothing.
|
|
6676
6754
|
*/
|
|
6677
6755
|
migrate() {
|
|
6678
|
-
const TARGET_VERSION =
|
|
6756
|
+
const TARGET_VERSION = 3;
|
|
6679
6757
|
const current = this.db.pragma("user_version", { simple: true });
|
|
6680
6758
|
if (current >= TARGET_VERSION) return;
|
|
6681
6759
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -6697,6 +6775,12 @@ var SQLiteStore = class {
|
|
|
6697
6775
|
this.db.exec("ALTER TABLE checkpoints RENAME COLUMN step TO name");
|
|
6698
6776
|
}
|
|
6699
6777
|
}
|
|
6778
|
+
if (committed < 3) {
|
|
6779
|
+
const cols = this.db.pragma("table_info(execution_history)");
|
|
6780
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "metadata")) {
|
|
6781
|
+
this.db.exec("ALTER TABLE execution_history ADD COLUMN metadata TEXT");
|
|
6782
|
+
}
|
|
6783
|
+
}
|
|
6700
6784
|
this.db.pragma(`user_version = ${TARGET_VERSION}`);
|
|
6701
6785
|
this.db.exec("COMMIT");
|
|
6702
6786
|
} catch (err) {
|
|
@@ -6759,7 +6843,8 @@ var SQLiteStore = class {
|
|
|
6759
6843
|
completed_at INTEGER,
|
|
6760
6844
|
duration INTEGER NOT NULL DEFAULT 0,
|
|
6761
6845
|
error TEXT,
|
|
6762
|
-
events TEXT NOT NULL
|
|
6846
|
+
events TEXT NOT NULL,
|
|
6847
|
+
metadata TEXT
|
|
6763
6848
|
);
|
|
6764
6849
|
|
|
6765
6850
|
CREATE TABLE IF NOT EXISTS eval_history (
|
|
@@ -6880,7 +6965,7 @@ var SQLiteStore = class {
|
|
|
6880
6965
|
// ── Execution History ────────────────────────────────────────────────────
|
|
6881
6966
|
async saveExecution(execution) {
|
|
6882
6967
|
this.db.prepare(
|
|
6883
|
-
"INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, events) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
6968
|
+
"INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, events, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
6884
6969
|
).run(
|
|
6885
6970
|
execution.executionId,
|
|
6886
6971
|
execution.workflow,
|
|
@@ -6890,7 +6975,8 @@ var SQLiteStore = class {
|
|
|
6890
6975
|
execution.completedAt ?? null,
|
|
6891
6976
|
execution.duration,
|
|
6892
6977
|
execution.error ?? null,
|
|
6893
|
-
JSON.stringify(execution.events)
|
|
6978
|
+
JSON.stringify(execution.events),
|
|
6979
|
+
execution.metadata !== void 0 ? JSON.stringify(execution.metadata) : null
|
|
6894
6980
|
);
|
|
6895
6981
|
}
|
|
6896
6982
|
async getExecution(executionId) {
|
|
@@ -6902,6 +6988,16 @@ var SQLiteStore = class {
|
|
|
6902
6988
|
const rows = limit ? this.db.prepare(sql).all(limit) : this.db.prepare(sql).all();
|
|
6903
6989
|
return rows.map(rowToExecutionInfo);
|
|
6904
6990
|
}
|
|
6991
|
+
async deleteExecution(executionId) {
|
|
6992
|
+
const tx = this.db.transaction(() => {
|
|
6993
|
+
const result = this.db.prepare("DELETE FROM execution_history WHERE execution_id = ?").run(executionId);
|
|
6994
|
+
this.db.prepare("DELETE FROM checkpoints WHERE execution_id = ?").run(executionId);
|
|
6995
|
+
this.db.prepare("DELETE FROM execution_state WHERE execution_id = ?").run(executionId);
|
|
6996
|
+
this.db.prepare("DELETE FROM decisions WHERE execution_id = ?").run(executionId);
|
|
6997
|
+
return result.changes > 0;
|
|
6998
|
+
});
|
|
6999
|
+
return tx();
|
|
7000
|
+
}
|
|
6905
7001
|
// ── Eval History ────────────────────────────────────────────────────────
|
|
6906
7002
|
async saveEvalResult(entry) {
|
|
6907
7003
|
this.db.prepare(
|
|
@@ -7780,6 +7876,7 @@ var MemoryManager = class {
|
|
|
7780
7876
|
if (stateStore.saveMemory) {
|
|
7781
7877
|
await stateStore.saveMemory(storeScope, key, value);
|
|
7782
7878
|
} else {
|
|
7879
|
+
warnMissingMemoryMethodsOnce(stateStore);
|
|
7783
7880
|
const storeKey = `memory:${storeScope}:${key}`;
|
|
7784
7881
|
await stateStore.saveSessionMeta(storeKey, "value", value);
|
|
7785
7882
|
if (options?.metadata) {
|
|
@@ -7882,6 +7979,14 @@ var MemoryManager = class {
|
|
|
7882
7979
|
}
|
|
7883
7980
|
}
|
|
7884
7981
|
};
|
|
7982
|
+
var _warnedStoresMissingMemory = /* @__PURE__ */ new WeakSet();
|
|
7983
|
+
function warnMissingMemoryMethodsOnce(stateStore) {
|
|
7984
|
+
if (_warnedStoresMissingMemory.has(stateStore)) return;
|
|
7985
|
+
_warnedStoresMissingMemory.add(stateStore);
|
|
7986
|
+
console.warn(
|
|
7987
|
+
"[axl] ctx.remember() against a StateStore without saveMemory/getMemory/getAllMemory/deleteMemory \u2014 falling back to a sessionMeta-based path that drops metadata and is not enumerable. Implement the four optional memory methods on your StateStore for full fidelity. This warning fires once per offending store."
|
|
7988
|
+
);
|
|
7989
|
+
}
|
|
7885
7990
|
|
|
7886
7991
|
// src/telemetry/noop.ts
|
|
7887
7992
|
var NOOP_SPAN = {
|
|
@@ -7923,11 +8028,141 @@ function hashInput(input) {
|
|
|
7923
8028
|
return (hash >>> 0).toString(16);
|
|
7924
8029
|
}
|
|
7925
8030
|
var DEFAULT_MAX_EVENTS_PER_EXECUTION = 5e4;
|
|
8031
|
+
var DEFAULT_STREAMING_BATCH_SIZE = 100;
|
|
8032
|
+
var DEFAULT_STREAMING_BATCH_INTERVAL = 1e3;
|
|
8033
|
+
var RECOVERED_UNKNOWN_WORKFLOW = "__axl/recovered";
|
|
8034
|
+
var STREAMING_EXCLUDED_TYPES = /* @__PURE__ */ new Set([
|
|
8035
|
+
"token",
|
|
8036
|
+
"partial_object",
|
|
8037
|
+
"string_delta"
|
|
8038
|
+
]);
|
|
8039
|
+
var INTERNAL_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
8040
|
+
"sessionId",
|
|
8041
|
+
"sessionHistory",
|
|
8042
|
+
"resumeMode"
|
|
8043
|
+
]);
|
|
8044
|
+
function liftPersistedMetadata(metadata) {
|
|
8045
|
+
if (!metadata) return void 0;
|
|
8046
|
+
const filtered = {};
|
|
8047
|
+
let hasAny = false;
|
|
8048
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
8049
|
+
if (INTERNAL_METADATA_KEYS.has(k)) continue;
|
|
8050
|
+
filtered[k] = v;
|
|
8051
|
+
hasAny = true;
|
|
8052
|
+
}
|
|
8053
|
+
if (!hasAny) return void 0;
|
|
8054
|
+
try {
|
|
8055
|
+
return structuredClone(filtered);
|
|
8056
|
+
} catch {
|
|
8057
|
+
return { ...filtered };
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
var StreamingFlusher = class {
|
|
8061
|
+
constructor(store, batchSize, batchInterval) {
|
|
8062
|
+
this.store = store;
|
|
8063
|
+
this.batchSize = batchSize;
|
|
8064
|
+
this.batchInterval = batchInterval;
|
|
8065
|
+
}
|
|
8066
|
+
buffers = /* @__PURE__ */ new Map();
|
|
8067
|
+
timers = /* @__PURE__ */ new Map();
|
|
8068
|
+
/**
|
|
8069
|
+
* Tracks the most recent in-flight `appendStreamingEvents` per execution
|
|
8070
|
+
* so `finalize` can await it. Without this, a size-triggered fire-and-
|
|
8071
|
+
* forget flush whose RPUSH lands AFTER `finalize`'s DEL would resurrect
|
|
8072
|
+
* the buffer + re-register the execution in the streaming-exec-ids set
|
|
8073
|
+
* — a phantom orphan that recoverIncompleteStreams would later mis-recover.
|
|
8074
|
+
*/
|
|
8075
|
+
inflight = /* @__PURE__ */ new Map();
|
|
8076
|
+
append(executionId, event) {
|
|
8077
|
+
if (STREAMING_EXCLUDED_TYPES.has(event.type)) return;
|
|
8078
|
+
let buffer = this.buffers.get(executionId);
|
|
8079
|
+
if (!buffer) {
|
|
8080
|
+
buffer = [];
|
|
8081
|
+
this.buffers.set(executionId, buffer);
|
|
8082
|
+
}
|
|
8083
|
+
buffer.push(event);
|
|
8084
|
+
if (buffer.length >= this.batchSize) {
|
|
8085
|
+
void this.flush(executionId);
|
|
8086
|
+
return;
|
|
8087
|
+
}
|
|
8088
|
+
if (!this.timers.has(executionId)) {
|
|
8089
|
+
const timer = setTimeout(() => {
|
|
8090
|
+
this.timers.delete(executionId);
|
|
8091
|
+
void this.flush(executionId);
|
|
8092
|
+
}, this.batchInterval);
|
|
8093
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
8094
|
+
timer.unref?.();
|
|
8095
|
+
}
|
|
8096
|
+
this.timers.set(executionId, timer);
|
|
8097
|
+
}
|
|
8098
|
+
}
|
|
8099
|
+
/** Flush this execution's buffer immediately. Awaits any prior in-flight
|
|
8100
|
+
* flush for the same execution so writes land in order. */
|
|
8101
|
+
async flush(executionId) {
|
|
8102
|
+
const buffer = this.buffers.get(executionId);
|
|
8103
|
+
if (!buffer || buffer.length === 0) return;
|
|
8104
|
+
this.buffers.set(executionId, []);
|
|
8105
|
+
const timer = this.timers.get(executionId);
|
|
8106
|
+
if (timer) {
|
|
8107
|
+
clearTimeout(timer);
|
|
8108
|
+
this.timers.delete(executionId);
|
|
8109
|
+
}
|
|
8110
|
+
const prior = this.inflight.get(executionId) ?? Promise.resolve();
|
|
8111
|
+
const next = prior.then(async () => {
|
|
8112
|
+
try {
|
|
8113
|
+
await this.store.appendStreamingEvents?.(executionId, buffer);
|
|
8114
|
+
} catch (err) {
|
|
8115
|
+
console.error(
|
|
8116
|
+
`[axl] StreamingFlusher: failed to append ${buffer.length} events for executionId=${executionId}: ${err instanceof Error ? err.message : String(err)}`
|
|
8117
|
+
);
|
|
8118
|
+
}
|
|
8119
|
+
});
|
|
8120
|
+
this.inflight.set(executionId, next);
|
|
8121
|
+
next.finally(() => {
|
|
8122
|
+
if (this.inflight.get(executionId) === next) {
|
|
8123
|
+
this.inflight.delete(executionId);
|
|
8124
|
+
}
|
|
8125
|
+
});
|
|
8126
|
+
return next;
|
|
8127
|
+
}
|
|
8128
|
+
/** Finalize an execution — flush remaining buffer + release store-side buffer.
|
|
8129
|
+
* Critically, awaits the most recent in-flight `appendStreamingEvents` BEFORE
|
|
8130
|
+
* calling `finalizeStreamingEvents`, so a late-landing RPUSH can't resurrect
|
|
8131
|
+
* the buffer after we've deleted it. */
|
|
8132
|
+
async finalize(executionId) {
|
|
8133
|
+
await this.flush(executionId);
|
|
8134
|
+
const pending = this.inflight.get(executionId);
|
|
8135
|
+
if (pending) {
|
|
8136
|
+
await pending;
|
|
8137
|
+
this.inflight.delete(executionId);
|
|
8138
|
+
}
|
|
8139
|
+
this.buffers.delete(executionId);
|
|
8140
|
+
const timer = this.timers.get(executionId);
|
|
8141
|
+
if (timer) {
|
|
8142
|
+
clearTimeout(timer);
|
|
8143
|
+
this.timers.delete(executionId);
|
|
8144
|
+
}
|
|
8145
|
+
try {
|
|
8146
|
+
await this.store.finalizeStreamingEvents?.(executionId);
|
|
8147
|
+
} catch (err) {
|
|
8148
|
+
console.error(
|
|
8149
|
+
`[axl] StreamingFlusher: failed to finalize executionId=${executionId}: ` + (err instanceof Error ? err.message : String(err))
|
|
8150
|
+
);
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
/** Best-effort drain of every pending buffer. Called from `runtime.shutdown()`. */
|
|
8154
|
+
async flushAll() {
|
|
8155
|
+
const ids = [...this.buffers.keys()];
|
|
8156
|
+
await Promise.all(ids.map((id) => this.flush(id)));
|
|
8157
|
+
const pendings = [...this.inflight.values()];
|
|
8158
|
+
await Promise.allSettled(pendings);
|
|
8159
|
+
}
|
|
8160
|
+
};
|
|
7926
8161
|
function isAbortError(err) {
|
|
7927
8162
|
return typeof err === "object" && err !== null && err.name === "AbortError";
|
|
7928
8163
|
}
|
|
7929
8164
|
function pushEventBounded(execInfo, event, cap) {
|
|
7930
|
-
if (
|
|
8165
|
+
if (STREAMING_EXCLUDED_TYPES.has(event.type)) return;
|
|
7931
8166
|
if (cap === Infinity || execInfo.events.length < cap) {
|
|
7932
8167
|
execInfo.events.push(event);
|
|
7933
8168
|
return;
|
|
@@ -7948,6 +8183,36 @@ function pushEventBounded(execInfo, event, cap) {
|
|
|
7948
8183
|
};
|
|
7949
8184
|
}
|
|
7950
8185
|
}
|
|
8186
|
+
function sanitizeMetadataForPersist(metadata) {
|
|
8187
|
+
const cleaned = {};
|
|
8188
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
8189
|
+
try {
|
|
8190
|
+
cleaned[k] = structuredClone(v);
|
|
8191
|
+
} catch {
|
|
8192
|
+
}
|
|
8193
|
+
}
|
|
8194
|
+
return cleaned;
|
|
8195
|
+
}
|
|
8196
|
+
function boundRecoveredEvents(events, cap) {
|
|
8197
|
+
if (cap === Infinity || events.length <= cap) return events;
|
|
8198
|
+
const truncated = events.slice(0, cap);
|
|
8199
|
+
const last = truncated[truncated.length - 1];
|
|
8200
|
+
const alreadyTruncated = last?.type === "log" && typeof last.data === "object" && last.data !== null && last.data.event === "events_truncated";
|
|
8201
|
+
if (!alreadyTruncated) {
|
|
8202
|
+
truncated[truncated.length - 1] = {
|
|
8203
|
+
type: "log",
|
|
8204
|
+
executionId: events[0]?.executionId ?? "",
|
|
8205
|
+
step: last?.step ?? 0,
|
|
8206
|
+
timestamp: Date.now(),
|
|
8207
|
+
data: {
|
|
8208
|
+
event: "events_truncated",
|
|
8209
|
+
cap,
|
|
8210
|
+
message: `Recovered ExecutionInfo.events truncated at ${cap} entries. Original streaming buffer had ${events.length} events. Raise via config.state.maxEventsPerExecution.`
|
|
8211
|
+
}
|
|
8212
|
+
};
|
|
8213
|
+
}
|
|
8214
|
+
return truncated;
|
|
8215
|
+
}
|
|
7951
8216
|
var costScopeStorage = new AsyncLocalStorage2();
|
|
7952
8217
|
function forwardAbortSignal(external, internal) {
|
|
7953
8218
|
if (!external) return () => {
|
|
@@ -7994,6 +8259,30 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
7994
8259
|
* `events` field — keeps the log to one line per row instead of one per
|
|
7995
8260
|
* rebuild tick. */
|
|
7996
8261
|
warnedMalformedExecutions = /* @__PURE__ */ new Set();
|
|
8262
|
+
/** Streaming-mode flusher. `undefined` when persist === 'terminal'
|
|
8263
|
+
* (back-compat default — no buffering overhead, no batching timer). */
|
|
8264
|
+
streamingFlusher;
|
|
8265
|
+
/** Execution IDs that are eligible for streaming-buffer durability —
|
|
8266
|
+
* populated by `execute()` / `stream()`. `createContext()` flows are
|
|
8267
|
+
* deliberately NOT added: ad-hoc contexts (tool tests, Studio playground,
|
|
8268
|
+
* evals) have no terminal `persistExecution` path that would finalize the
|
|
8269
|
+
* streaming buffer, so allowing them to write would leave phantom orphan
|
|
8270
|
+
* buffers that `recoverIncompleteStreams()` later mis-recovers as failed
|
|
8271
|
+
* executions. Membership is removed in `persistExecution` once the
|
|
8272
|
+
* canonical save + finalize chain has been scheduled. */
|
|
8273
|
+
streamableExecutionIds = /* @__PURE__ */ new Set();
|
|
8274
|
+
/** Execution IDs deleted by `runtime.deleteExecution()` while still in
|
|
8275
|
+
* flight. `persistExecution` skips its save + cache write when the
|
|
8276
|
+
* executionId is in this set — preventing a workflow that completes after
|
|
8277
|
+
* a delete from resurrecting the (intentionally-removed) row. Entries are
|
|
8278
|
+
* removed once the would-be persist has been short-circuited. */
|
|
8279
|
+
pendingDeletedExecutions = /* @__PURE__ */ new Set();
|
|
8280
|
+
/** In-flight `persistExecution` chains (save → finalize). `shutdown()`
|
|
8281
|
+
* awaits these before closing the StateStore so a workflow whose abort
|
|
8282
|
+
* triggers a persist-during-shutdown isn't racing a closed connection.
|
|
8283
|
+
* The Set is mutated by the chain's own `.finally` so entries clear as
|
|
8284
|
+
* soon as the work settles. */
|
|
8285
|
+
persistInflight = /* @__PURE__ */ new Set();
|
|
7997
8286
|
constructor(config) {
|
|
7998
8287
|
super();
|
|
7999
8288
|
this.config = resolveConfig(config ?? {});
|
|
@@ -8017,6 +8306,19 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8017
8306
|
embedder: this.config.memory.embedder
|
|
8018
8307
|
});
|
|
8019
8308
|
}
|
|
8309
|
+
if (this.config.state?.persist === "streaming") {
|
|
8310
|
+
if (this.stateStore.appendStreamingEvents) {
|
|
8311
|
+
this.streamingFlusher = new StreamingFlusher(
|
|
8312
|
+
this.stateStore,
|
|
8313
|
+
this.config.state.streamingBatchSize ?? DEFAULT_STREAMING_BATCH_SIZE,
|
|
8314
|
+
this.config.state.streamingBatchInterval ?? DEFAULT_STREAMING_BATCH_INTERVAL
|
|
8315
|
+
);
|
|
8316
|
+
} else {
|
|
8317
|
+
console.warn(
|
|
8318
|
+
`[axl] config.state.persist === 'streaming' but the configured StateStore does not implement appendStreamingEvents. Streaming durability is disabled. Use RedisStore for crash-survival, or implement the optional streaming methods on your custom StateStore.`
|
|
8319
|
+
);
|
|
8320
|
+
}
|
|
8321
|
+
}
|
|
8020
8322
|
}
|
|
8021
8323
|
/**
|
|
8022
8324
|
* Whether `config.trace.redact` is enabled on this runtime. A narrow
|
|
@@ -8140,14 +8442,49 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8140
8442
|
/**
|
|
8141
8443
|
* Persist a completed/failed execution to the state store (fire-and-forget)
|
|
8142
8444
|
* and move it from the active executions map to the historical cache.
|
|
8445
|
+
*
|
|
8446
|
+
* Resurrection guard: if `runtime.deleteExecution(id)` was called while
|
|
8447
|
+
* this execution was still running, the id will be in
|
|
8448
|
+
* `pendingDeletedExecutions`. In that case we skip the save AND the
|
|
8449
|
+
* historical-cache write, and finalize the streaming buffer (if any) so
|
|
8450
|
+
* the buffer doesn't outlive the delete.
|
|
8143
8451
|
*/
|
|
8144
8452
|
persistExecution(execInfo) {
|
|
8145
|
-
const
|
|
8453
|
+
const id = execInfo.executionId;
|
|
8454
|
+
this.streamableExecutionIds.delete(id);
|
|
8455
|
+
if (this.pendingDeletedExecutions.has(id)) {
|
|
8456
|
+
this.pendingDeletedExecutions.delete(id);
|
|
8457
|
+
this.executions.delete(id);
|
|
8458
|
+
if (this.streamingFlusher) {
|
|
8459
|
+
this.streamingFlusher.finalize(id).catch(() => {
|
|
8460
|
+
});
|
|
8461
|
+
}
|
|
8462
|
+
return;
|
|
8463
|
+
}
|
|
8464
|
+
let snapshot;
|
|
8465
|
+
try {
|
|
8466
|
+
snapshot = structuredClone(execInfo);
|
|
8467
|
+
} catch {
|
|
8468
|
+
snapshot = {
|
|
8469
|
+
...execInfo,
|
|
8470
|
+
events: [...execInfo.events],
|
|
8471
|
+
metadata: execInfo.metadata ? sanitizeMetadataForPersist(execInfo.metadata) : void 0
|
|
8472
|
+
};
|
|
8473
|
+
}
|
|
8146
8474
|
if (this.stateStore.saveExecution) {
|
|
8147
|
-
this.stateStore.saveExecution(snapshot).
|
|
8475
|
+
const chain = this.stateStore.saveExecution(snapshot).then(() => {
|
|
8476
|
+
return this.streamingFlusher?.finalize(id).catch(() => {
|
|
8477
|
+
});
|
|
8478
|
+
}).catch(() => {
|
|
8479
|
+
}).then(() => void 0);
|
|
8480
|
+
this.persistInflight.add(chain);
|
|
8481
|
+
chain.finally(() => this.persistInflight.delete(chain));
|
|
8482
|
+
} else if (this.streamingFlusher) {
|
|
8483
|
+
const chain = this.streamingFlusher.finalize(id).catch(() => {
|
|
8148
8484
|
});
|
|
8485
|
+
this.persistInflight.add(chain);
|
|
8486
|
+
chain.finally(() => this.persistInflight.delete(chain));
|
|
8149
8487
|
}
|
|
8150
|
-
const id = execInfo.executionId;
|
|
8151
8488
|
this.historicalExecutions.set(id, snapshot);
|
|
8152
8489
|
this.executions.delete(id);
|
|
8153
8490
|
}
|
|
@@ -8387,6 +8724,9 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8387
8724
|
this.abortControllers.set(executionId, controller);
|
|
8388
8725
|
const cleanupAbortForwarder = forwardAbortSignal(options?.signal, controller);
|
|
8389
8726
|
this.registerWithCostScope(executionId);
|
|
8727
|
+
if (this.streamingFlusher) {
|
|
8728
|
+
this.streamableExecutionIds.add(executionId);
|
|
8729
|
+
}
|
|
8390
8730
|
const execInfo = {
|
|
8391
8731
|
executionId,
|
|
8392
8732
|
workflow: name,
|
|
@@ -8394,7 +8734,8 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8394
8734
|
events: [],
|
|
8395
8735
|
totalCost: 0,
|
|
8396
8736
|
startedAt: Date.now(),
|
|
8397
|
-
duration: 0
|
|
8737
|
+
duration: 0,
|
|
8738
|
+
metadata: liftPersistedMetadata(options?.metadata)
|
|
8398
8739
|
};
|
|
8399
8740
|
this.executions.set(executionId, execInfo);
|
|
8400
8741
|
const sessionHistory = options?.metadata?.sessionHistory ?? void 0;
|
|
@@ -8410,6 +8751,9 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8410
8751
|
onTrace: (event) => {
|
|
8411
8752
|
pushEventBounded(execInfo, event, this.maxEventsPerExecution);
|
|
8412
8753
|
execInfo.totalCost += eventCostContribution(event);
|
|
8754
|
+
if (this.streamableExecutionIds.has(executionId)) {
|
|
8755
|
+
this.streamingFlusher?.append(executionId, event);
|
|
8756
|
+
}
|
|
8413
8757
|
this.emit("trace", event);
|
|
8414
8758
|
this.outputAxlEvent(event);
|
|
8415
8759
|
if (event.type === "handoff_start") {
|
|
@@ -8488,6 +8832,9 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8488
8832
|
const validated = workflow2.inputSchema.parse(input);
|
|
8489
8833
|
const sessionHistory = options?.metadata?.sessionHistory ?? void 0;
|
|
8490
8834
|
this.registerWithCostScope(executionId);
|
|
8835
|
+
if (this.streamingFlusher) {
|
|
8836
|
+
this.streamableExecutionIds.add(executionId);
|
|
8837
|
+
}
|
|
8491
8838
|
execInfo = {
|
|
8492
8839
|
executionId,
|
|
8493
8840
|
workflow: name,
|
|
@@ -8495,7 +8842,8 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8495
8842
|
events: [],
|
|
8496
8843
|
totalCost: 0,
|
|
8497
8844
|
startedAt: Date.now(),
|
|
8498
|
-
duration: 0
|
|
8845
|
+
duration: 0,
|
|
8846
|
+
metadata: liftPersistedMetadata(options?.metadata)
|
|
8499
8847
|
};
|
|
8500
8848
|
this.executions.set(executionId, execInfo);
|
|
8501
8849
|
const wfCtx = new WorkflowContext({
|
|
@@ -8516,6 +8864,9 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8516
8864
|
onTrace: (event) => {
|
|
8517
8865
|
pushEventBounded(execInfo, event, this.maxEventsPerExecution);
|
|
8518
8866
|
execInfo.totalCost += eventCostContribution(event);
|
|
8867
|
+
if (this.streamableExecutionIds.has(executionId)) {
|
|
8868
|
+
this.streamingFlusher?.append(executionId, event);
|
|
8869
|
+
}
|
|
8519
8870
|
this.emit("trace", event);
|
|
8520
8871
|
this.outputAxlEvent(event);
|
|
8521
8872
|
axlStream._push(event);
|
|
@@ -8655,6 +9006,15 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8655
9006
|
errors.push(new Error(`${label}: ${err instanceof Error ? err.message : String(err)}`));
|
|
8656
9007
|
}
|
|
8657
9008
|
};
|
|
9009
|
+
if (this.streamingFlusher) {
|
|
9010
|
+
await safeClose("streamingFlusher", () => this.streamingFlusher.flushAll());
|
|
9011
|
+
}
|
|
9012
|
+
if (this.persistInflight.size > 0) {
|
|
9013
|
+
await safeClose(
|
|
9014
|
+
"persistInflight",
|
|
9015
|
+
() => Promise.allSettled([...this.persistInflight]).then(() => void 0)
|
|
9016
|
+
);
|
|
9017
|
+
}
|
|
8658
9018
|
if (this.mcpManager) await safeClose("mcpManager", () => this.mcpManager.shutdown());
|
|
8659
9019
|
if (this.memoryManager) await safeClose("memoryManager", () => this.memoryManager.close());
|
|
8660
9020
|
if (this.stateStore.close) await safeClose("stateStore", () => this.stateStore.close());
|
|
@@ -8665,6 +9025,81 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8665
9025
|
);
|
|
8666
9026
|
}
|
|
8667
9027
|
}
|
|
9028
|
+
/**
|
|
9029
|
+
* Recover executions whose process died mid-run when `state.persist:
|
|
9030
|
+
* 'streaming'` was configured. Synthesizes a partial `ExecutionInfo`
|
|
9031
|
+
* from the streaming buffer for each — `status: 'failed'`, error
|
|
9032
|
+
* `'process terminated'` — registers them in the historical cache so
|
|
9033
|
+
* they show up in `getExecutions()`, persists them via the store's
|
|
9034
|
+
* `saveExecution`, and finalizes the streaming buffer.
|
|
9035
|
+
*
|
|
9036
|
+
* Idempotent — if the streaming buffer is already gone (or
|
|
9037
|
+
* `listStreamingExecutions` is empty), this is a no-op. Returns the
|
|
9038
|
+
* list of recovered `ExecutionInfo`s for caller introspection.
|
|
9039
|
+
*
|
|
9040
|
+
* Wire this into your process startup AFTER `runtime.getExecutions()`
|
|
9041
|
+
* has lazy-loaded the historical cache. Order matters: completed
|
|
9042
|
+
* executions already in the store should not be overwritten by a
|
|
9043
|
+
* stale streaming buffer that the previous process didn't get to
|
|
9044
|
+
* finalize. The implementation checks the historical store before
|
|
9045
|
+
* synthesizing, so a re-run is safe.
|
|
9046
|
+
*/
|
|
9047
|
+
async recoverIncompleteStreams() {
|
|
9048
|
+
if (!this.stateStore.listStreamingExecutions || !this.stateStore.getStreamingEvents) {
|
|
9049
|
+
return [];
|
|
9050
|
+
}
|
|
9051
|
+
const ids = await this.stateStore.listStreamingExecutions();
|
|
9052
|
+
if (ids.length === 0) return [];
|
|
9053
|
+
await this.getExecutions();
|
|
9054
|
+
const recovered = [];
|
|
9055
|
+
for (const id of ids) {
|
|
9056
|
+
if (this.executions.has(id) || this.streamableExecutionIds.has(id)) {
|
|
9057
|
+
continue;
|
|
9058
|
+
}
|
|
9059
|
+
const existing = this.historicalExecutions.get(id);
|
|
9060
|
+
if (existing) {
|
|
9061
|
+
await this.stateStore.finalizeStreamingEvents?.(id);
|
|
9062
|
+
continue;
|
|
9063
|
+
}
|
|
9064
|
+
const events = await this.stateStore.getStreamingEvents(id);
|
|
9065
|
+
if (events.length === 0) {
|
|
9066
|
+
await this.stateStore.finalizeStreamingEvents?.(id);
|
|
9067
|
+
continue;
|
|
9068
|
+
}
|
|
9069
|
+
const boundedEvents = boundRecoveredEvents(events, this.maxEventsPerExecution);
|
|
9070
|
+
const workflowStartEvent = boundedEvents.find((e) => e.type === "workflow_start");
|
|
9071
|
+
const lastEvent = boundedEvents[boundedEvents.length - 1];
|
|
9072
|
+
const startedAt = workflowStartEvent?.timestamp ?? boundedEvents[0]?.timestamp ?? Date.now();
|
|
9073
|
+
const completedAt = lastEvent.timestamp ?? startedAt;
|
|
9074
|
+
const workflowName = workflowStartEvent?.workflow ?? RECOVERED_UNKNOWN_WORKFLOW;
|
|
9075
|
+
const totalCost = boundedEvents.reduce((sum, e) => sum + eventCostContribution(e), 0);
|
|
9076
|
+
const synthesized = {
|
|
9077
|
+
executionId: id,
|
|
9078
|
+
workflow: workflowName,
|
|
9079
|
+
status: "failed",
|
|
9080
|
+
events: boundedEvents,
|
|
9081
|
+
totalCost,
|
|
9082
|
+
startedAt,
|
|
9083
|
+
completedAt,
|
|
9084
|
+
duration: completedAt - startedAt,
|
|
9085
|
+
error: "process terminated (recovered from streaming buffer)"
|
|
9086
|
+
};
|
|
9087
|
+
if (this.stateStore.saveExecution) {
|
|
9088
|
+
try {
|
|
9089
|
+
await this.stateStore.saveExecution(synthesized);
|
|
9090
|
+
} catch (err) {
|
|
9091
|
+
console.error(
|
|
9092
|
+
`[axl] recoverIncompleteStreams: failed to save synthesized execution ${id}: ${err instanceof Error ? err.message : String(err)}. Streaming buffer left in place for next recovery attempt.`
|
|
9093
|
+
);
|
|
9094
|
+
continue;
|
|
9095
|
+
}
|
|
9096
|
+
}
|
|
9097
|
+
this.historicalExecutions.set(id, synthesized);
|
|
9098
|
+
await this.stateStore.finalizeStreamingEvents?.(id);
|
|
9099
|
+
recovered.push(synthesized);
|
|
9100
|
+
}
|
|
9101
|
+
return recovered;
|
|
9102
|
+
}
|
|
8668
9103
|
/** Abort a running execution by its ID. */
|
|
8669
9104
|
abort(executionId) {
|
|
8670
9105
|
const controller = this.abortControllers.get(executionId);
|
|
@@ -8697,15 +9132,85 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8697
9132
|
}
|
|
8698
9133
|
this.emit("eval_result", entry);
|
|
8699
9134
|
}
|
|
9135
|
+
/**
|
|
9136
|
+
* Delete an execution from history by id. Removes from the active map (if
|
|
9137
|
+
* still running), the historical cache, the configured StateStore, AND
|
|
9138
|
+
* any execution-scoped side state — pending awaitHuman decisions,
|
|
9139
|
+
* resolver maps, streaming buffer registration, in-flight abort controller.
|
|
9140
|
+
* Returns true if an entry was actually removed from any of the three
|
|
9141
|
+
* primary surfaces (active map, historical cache, store).
|
|
9142
|
+
*
|
|
9143
|
+
* In-flight handling: if the execution is still running, this method
|
|
9144
|
+
* **aborts** it (via the registered abort controller) AND marks the id for
|
|
9145
|
+
* skip-on-persist so the workflow's eventual `workflow_end` doesn't
|
|
9146
|
+
* resurrect the row. The workflow itself terminates normally — callers
|
|
9147
|
+
* iterating the returned `AxlStream` will see the abort flow through. If
|
|
9148
|
+
* you need a hard hand-off (delete completes before the workflow tears
|
|
9149
|
+
* down), call `runtime.abort(id)` and `await` the stream/execute promise
|
|
9150
|
+
* before calling this. `streamableExecutionIds` is cleared eagerly so
|
|
9151
|
+
* events emitted during the wind-down don't re-create the streaming
|
|
9152
|
+
* buffer in the store after the deleteExecution-issued DEL.
|
|
9153
|
+
*
|
|
9154
|
+
* Audit trail: emits `execution_deleted` on the runtime's `EventEmitter`
|
|
9155
|
+
* with `{ executionId, wasActive, hadPendingDecision }` so compliance
|
|
9156
|
+
* apps can subscribe via `runtime.on('execution_deleted', ...)` for a
|
|
9157
|
+
* "who deleted what when" log without wrapping this method. Fires
|
|
9158
|
+
* regardless of whether the id was actually present.
|
|
9159
|
+
*
|
|
9160
|
+
* Use cases: GDPR right-to-be-forgotten, operator-driven cleanup of
|
|
9161
|
+
* specific runs (e.g. a workflow that recorded PII the user requested
|
|
9162
|
+
* scrubbed). For bulk eviction by age, use a `RedisStore` TTL instead
|
|
9163
|
+
* (`defaultTtl` / `ttls.executionHistory`).
|
|
9164
|
+
*/
|
|
9165
|
+
async deleteExecution(id) {
|
|
9166
|
+
const wasActive = this.executions.has(id);
|
|
9167
|
+
const hadPendingDecision = this.pendingDecisionResolvers.has(id);
|
|
9168
|
+
if (wasActive) {
|
|
9169
|
+
this.pendingDeletedExecutions.add(id);
|
|
9170
|
+
this.streamableExecutionIds.delete(id);
|
|
9171
|
+
const controller = this.abortControllers.get(id);
|
|
9172
|
+
if (controller) {
|
|
9173
|
+
controller.abort();
|
|
9174
|
+
}
|
|
9175
|
+
}
|
|
9176
|
+
if (hadPendingDecision) {
|
|
9177
|
+
this.pendingDecisionResolvers.delete(id);
|
|
9178
|
+
}
|
|
9179
|
+
const existing = await this.getExecution(id);
|
|
9180
|
+
const workflow2 = existing?.workflow;
|
|
9181
|
+
const removedFromActive = this.executions.delete(id);
|
|
9182
|
+
const removedFromHistorical = this.historicalExecutions.delete(id);
|
|
9183
|
+
let removedFromStore = false;
|
|
9184
|
+
if (this.stateStore.deleteExecution) {
|
|
9185
|
+
removedFromStore = await this.stateStore.deleteExecution(id);
|
|
9186
|
+
}
|
|
9187
|
+
this.emit("execution_deleted", {
|
|
9188
|
+
executionId: id,
|
|
9189
|
+
workflow: workflow2,
|
|
9190
|
+
wasActive,
|
|
9191
|
+
hadPendingDecision,
|
|
9192
|
+
removed: removedFromActive || removedFromHistorical || removedFromStore
|
|
9193
|
+
});
|
|
9194
|
+
return removedFromActive || removedFromHistorical || removedFromStore;
|
|
9195
|
+
}
|
|
8700
9196
|
/**
|
|
8701
9197
|
* Delete an eval history entry by id. Removes from in-memory cache and
|
|
8702
9198
|
* the configured StateStore. Returns true if an entry was actually removed.
|
|
8703
9199
|
*
|
|
8704
9200
|
* Ensures lazy-loaded history is loaded first so the in-memory cache and
|
|
8705
9201
|
* the store can't drift apart on the deletion path.
|
|
9202
|
+
*
|
|
9203
|
+
* Emits `eval_deleted` on the runtime's `EventEmitter` with
|
|
9204
|
+
* `{ id, eval, removed }`. Fires on every call — including attempts
|
|
9205
|
+
* against unknown ids (`removed: false`) — symmetric to `execution_deleted`.
|
|
9206
|
+
* Studio's eval-trends aggregator subscribes to this event so deleted
|
|
9207
|
+
* runs don't linger in the trends panel for up to 5 minutes (until the
|
|
9208
|
+
* next periodic rebuild).
|
|
8706
9209
|
*/
|
|
8707
9210
|
async deleteEvalResult(id) {
|
|
8708
9211
|
await this.getEvalHistory();
|
|
9212
|
+
const existing = this.evalHistory.find((e) => e.id === id);
|
|
9213
|
+
const evalName = existing?.eval;
|
|
8709
9214
|
const beforeLength = this.evalHistory.length;
|
|
8710
9215
|
this.evalHistory = this.evalHistory.filter((e) => e.id !== id);
|
|
8711
9216
|
const removedFromMemory = this.evalHistory.length < beforeLength;
|
|
@@ -8713,7 +9218,13 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8713
9218
|
if (this.stateStore.deleteEvalResult) {
|
|
8714
9219
|
removedFromStore = await this.stateStore.deleteEvalResult(id);
|
|
8715
9220
|
}
|
|
8716
|
-
|
|
9221
|
+
const removed = removedFromMemory || removedFromStore;
|
|
9222
|
+
this.emit("eval_deleted", {
|
|
9223
|
+
id,
|
|
9224
|
+
eval: evalName,
|
|
9225
|
+
removed
|
|
9226
|
+
});
|
|
9227
|
+
return removed;
|
|
8717
9228
|
}
|
|
8718
9229
|
/** Get eval result history (most recent first). */
|
|
8719
9230
|
async getEvalHistory() {
|
|
@@ -9303,15 +9814,53 @@ async function* stringStreamFromEvents(source, opts) {
|
|
|
9303
9814
|
|
|
9304
9815
|
// src/state/redis.ts
|
|
9305
9816
|
var RedisStore = class _RedisStore {
|
|
9306
|
-
constructor(client) {
|
|
9817
|
+
constructor(client, keyPrefix, ttls) {
|
|
9307
9818
|
this.client = client;
|
|
9819
|
+
this.keyPrefix = keyPrefix;
|
|
9820
|
+
this.ttls = ttls;
|
|
9821
|
+
}
|
|
9822
|
+
/**
|
|
9823
|
+
* Resolve the TTL for a given storage category. Returns `undefined` when
|
|
9824
|
+
* no TTL applies (category is `null` in resolved config, the value is
|
|
9825
|
+
* non-positive — Redis EXPIRE 0 immediately deletes, which is almost
|
|
9826
|
+
* never intended — or the value is non-finite).
|
|
9827
|
+
*
|
|
9828
|
+
* Non-finite values (`NaN`, `±Infinity`) defensively map to `undefined`
|
|
9829
|
+
* rather than being passed to Redis, where they'd surface as a wire-level
|
|
9830
|
+
* protocol error at the worst possible time (first write under load).
|
|
9831
|
+
*/
|
|
9832
|
+
ttlFor(category) {
|
|
9833
|
+
const resolved = this.ttls[category];
|
|
9834
|
+
if (resolved === null) return void 0;
|
|
9835
|
+
if (!Number.isFinite(resolved) || resolved <= 0) return void 0;
|
|
9836
|
+
return Math.floor(resolved);
|
|
9308
9837
|
}
|
|
9309
9838
|
/**
|
|
9310
9839
|
* Create a connected RedisStore instance.
|
|
9311
9840
|
*
|
|
9312
|
-
*
|
|
9841
|
+
* Accepts either a URL string (back-compat) or a {@link RedisStoreOptions}
|
|
9842
|
+
* object. The object form is required to set a custom `keyPrefix`.
|
|
9843
|
+
*
|
|
9844
|
+
* @example
|
|
9845
|
+
* ```ts
|
|
9846
|
+
* // Default prefix 'axl:'
|
|
9847
|
+
* const store = await RedisStore.create('redis://localhost:6379');
|
|
9848
|
+
*
|
|
9849
|
+
* // Custom prefix for shared-cluster deployments
|
|
9850
|
+
* const store = await RedisStore.create({
|
|
9851
|
+
* url: 'redis://localhost:6379',
|
|
9852
|
+
* keyPrefix: 'axl:prod:',
|
|
9853
|
+
* });
|
|
9854
|
+
* ```
|
|
9313
9855
|
*/
|
|
9314
|
-
static async create(
|
|
9856
|
+
static async create(options) {
|
|
9857
|
+
const opts = typeof options === "string" ? { url: options } : options ?? {};
|
|
9858
|
+
const keyPrefix = opts.keyPrefix ?? "axl:";
|
|
9859
|
+
if (keyPrefix === "") {
|
|
9860
|
+
throw new Error(
|
|
9861
|
+
'RedisStore: keyPrefix cannot be empty string. Omit it for the default "axl:" prefix.'
|
|
9862
|
+
);
|
|
9863
|
+
}
|
|
9315
9864
|
let createClient;
|
|
9316
9865
|
try {
|
|
9317
9866
|
const mod = __require("redis");
|
|
@@ -9325,44 +9874,197 @@ var RedisStore = class _RedisStore {
|
|
|
9325
9874
|
if (err instanceof Error && err.message.includes("createClient")) throw err;
|
|
9326
9875
|
throw new Error("redis is required for RedisStore. Install it with: npm install redis");
|
|
9327
9876
|
}
|
|
9328
|
-
const client = url ? createClient({ url }) : createClient();
|
|
9877
|
+
const client = opts.url ? createClient({ url: opts.url }) : createClient();
|
|
9329
9878
|
await client.connect();
|
|
9330
|
-
|
|
9879
|
+
const resolveCategory = (override) => {
|
|
9880
|
+
if (override === null) return null;
|
|
9881
|
+
if (typeof override === "number") return override;
|
|
9882
|
+
return typeof opts.defaultTtl === "number" ? opts.defaultTtl : null;
|
|
9883
|
+
};
|
|
9884
|
+
const resolveStreamingEvents = (override) => {
|
|
9885
|
+
if (override === null || override === void 0) return null;
|
|
9886
|
+
if (typeof override === "number") return override;
|
|
9887
|
+
return null;
|
|
9888
|
+
};
|
|
9889
|
+
const effectiveTtls = {
|
|
9890
|
+
session: resolveCategory(opts.ttls?.session),
|
|
9891
|
+
sessionMeta: resolveCategory(opts.ttls?.sessionMeta),
|
|
9892
|
+
checkpoint: resolveCategory(opts.ttls?.checkpoint),
|
|
9893
|
+
executionState: resolveCategory(opts.ttls?.executionState),
|
|
9894
|
+
executionHistory: resolveCategory(opts.ttls?.executionHistory),
|
|
9895
|
+
evalHistory: resolveCategory(opts.ttls?.evalHistory),
|
|
9896
|
+
memory: resolveCategory(opts.ttls?.memory),
|
|
9897
|
+
streamingEvents: resolveStreamingEvents(opts.ttls?.streamingEvents)
|
|
9898
|
+
};
|
|
9899
|
+
const store = new _RedisStore(client, keyPrefix, effectiveTtls);
|
|
9900
|
+
if (!opts.skipMigration) {
|
|
9901
|
+
await store.backfillExecutionIndex();
|
|
9902
|
+
await store.backfillEvalIndex();
|
|
9903
|
+
}
|
|
9904
|
+
return store;
|
|
9905
|
+
}
|
|
9906
|
+
/**
|
|
9907
|
+
* Backfill the execution-history sorted-set from the legacy ID set.
|
|
9908
|
+
* Idempotent — runs only if the ZSET is missing entries the SET has.
|
|
9909
|
+
* Safe to call manually if you constructed the store with
|
|
9910
|
+
* `skipMigration: true`.
|
|
9911
|
+
*/
|
|
9912
|
+
async backfillExecutionIndex() {
|
|
9913
|
+
await this.backfillHistoryIndex(
|
|
9914
|
+
this.execHistorySetKey(),
|
|
9915
|
+
this.execHistoryZsetKey(),
|
|
9916
|
+
(id) => this.execHistoryKey(id),
|
|
9917
|
+
(raw) => {
|
|
9918
|
+
const exec = JSON.parse(raw);
|
|
9919
|
+
return typeof exec.startedAt === "number" ? { score: exec.startedAt, value: exec.executionId } : null;
|
|
9920
|
+
},
|
|
9921
|
+
"execution-history"
|
|
9922
|
+
);
|
|
9923
|
+
}
|
|
9924
|
+
/** Backfill the eval-history sorted-set. Symmetric to `backfillExecutionIndex`. */
|
|
9925
|
+
async backfillEvalIndex() {
|
|
9926
|
+
await this.backfillHistoryIndex(
|
|
9927
|
+
this.evalHistorySetKey(),
|
|
9928
|
+
this.evalHistoryZsetKey(),
|
|
9929
|
+
(id) => this.evalHistoryKey(id),
|
|
9930
|
+
(raw) => {
|
|
9931
|
+
const entry = JSON.parse(raw);
|
|
9932
|
+
return typeof entry.timestamp === "number" ? { score: entry.timestamp, value: entry.id } : null;
|
|
9933
|
+
},
|
|
9934
|
+
"eval-history"
|
|
9935
|
+
);
|
|
9936
|
+
}
|
|
9937
|
+
/**
|
|
9938
|
+
* Shared implementation for both backfill paths. Reads SET members,
|
|
9939
|
+
* batch-MGETs their data blobs in chunks, and ZADDs each into the new
|
|
9940
|
+
* sorted-set inside a single MULTI per chunk.
|
|
9941
|
+
*/
|
|
9942
|
+
async backfillHistoryIndex(setKey, zsetKey, dataKey, scoreOf, label) {
|
|
9943
|
+
const [setSize, zsetSize] = await Promise.all([
|
|
9944
|
+
this.client.sCard(setKey),
|
|
9945
|
+
this.client.zCard(zsetKey)
|
|
9946
|
+
]);
|
|
9947
|
+
if (zsetSize >= setSize) return;
|
|
9948
|
+
const allIds = await this.client.sMembers(setKey);
|
|
9949
|
+
if (allIds.length === 0) return;
|
|
9950
|
+
const CHUNK_SIZE = 500;
|
|
9951
|
+
let migrated = 0;
|
|
9952
|
+
let skipped = 0;
|
|
9953
|
+
for (let i = 0; i < allIds.length; i += CHUNK_SIZE) {
|
|
9954
|
+
const chunk = allIds.slice(i, i + CHUNK_SIZE);
|
|
9955
|
+
const dataKeys = chunk.map((id) => dataKey(id));
|
|
9956
|
+
const raws = await this.client.mGet(dataKeys);
|
|
9957
|
+
const tx = this.client.multi();
|
|
9958
|
+
let queuedInChunk = 0;
|
|
9959
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
9960
|
+
const raw = raws[j];
|
|
9961
|
+
if (raw == null) {
|
|
9962
|
+
skipped++;
|
|
9963
|
+
continue;
|
|
9964
|
+
}
|
|
9965
|
+
try {
|
|
9966
|
+
const entry = scoreOf(raw);
|
|
9967
|
+
if (entry == null) {
|
|
9968
|
+
skipped++;
|
|
9969
|
+
continue;
|
|
9970
|
+
}
|
|
9971
|
+
tx.zAdd(zsetKey, entry);
|
|
9972
|
+
queuedInChunk++;
|
|
9973
|
+
migrated++;
|
|
9974
|
+
} catch {
|
|
9975
|
+
skipped++;
|
|
9976
|
+
}
|
|
9977
|
+
}
|
|
9978
|
+
if (queuedInChunk > 0) await tx.exec();
|
|
9979
|
+
}
|
|
9980
|
+
if (migrated > 0 || skipped > 0) {
|
|
9981
|
+
console.log(
|
|
9982
|
+
`[axl] RedisStore: backfilled ${migrated} ${label} entries into sorted-set index` + (skipped > 0 ? ` (${skipped} skipped \u2014 missing or malformed)` : "")
|
|
9983
|
+
);
|
|
9984
|
+
}
|
|
9331
9985
|
}
|
|
9332
9986
|
// ── Key helpers ──────────────────────────────────────────────────────
|
|
9333
9987
|
checkpointKey(executionId) {
|
|
9334
|
-
return
|
|
9988
|
+
return `${this.keyPrefix}checkpoint:${executionId}`;
|
|
9335
9989
|
}
|
|
9336
9990
|
sessionKey(sessionId) {
|
|
9337
|
-
return
|
|
9991
|
+
return `${this.keyPrefix}session:${sessionId}`;
|
|
9338
9992
|
}
|
|
9339
9993
|
sessionMetaKey(sessionId) {
|
|
9340
|
-
return
|
|
9994
|
+
return `${this.keyPrefix}session-meta:${sessionId}`;
|
|
9995
|
+
}
|
|
9996
|
+
sessionIdsKey() {
|
|
9997
|
+
return `${this.keyPrefix}session-ids`;
|
|
9341
9998
|
}
|
|
9342
9999
|
decisionsKey() {
|
|
9343
|
-
return
|
|
10000
|
+
return `${this.keyPrefix}decisions`;
|
|
9344
10001
|
}
|
|
9345
10002
|
executionStateKey(executionId) {
|
|
9346
|
-
return
|
|
10003
|
+
return `${this.keyPrefix}exec-state:${executionId}`;
|
|
9347
10004
|
}
|
|
9348
10005
|
pendingExecSetKey() {
|
|
9349
|
-
return
|
|
10006
|
+
return `${this.keyPrefix}pending-executions`;
|
|
9350
10007
|
}
|
|
9351
10008
|
execHistoryKey(executionId) {
|
|
9352
|
-
return
|
|
9353
|
-
}
|
|
10009
|
+
return `${this.keyPrefix}exec-history:${executionId}`;
|
|
10010
|
+
}
|
|
10011
|
+
// NOTE for future implementer of #3 (TTL config): when the executionHistory
|
|
10012
|
+
// and evalHistory data blobs gain TTLs, eviction MUST be driven by
|
|
10013
|
+
// ZREMRANGEBYSCORE on the ZSET below — NOT by SREM on these legacy ID sets.
|
|
10014
|
+
// The slow-path fallback (`listHistoryByZset`) reads from the SET when
|
|
10015
|
+
// `zsetCard < setCard`, so SET-based eviction would race with the fallback
|
|
10016
|
+
// and surface dead IDs whose data has been TTL'd away. ZSET eviction +
|
|
10017
|
+
// dual-write `SREM` is the clean path; doc the assumption then.
|
|
9354
10018
|
execHistorySetKey() {
|
|
9355
|
-
return
|
|
10019
|
+
return `${this.keyPrefix}exec-history-ids`;
|
|
10020
|
+
}
|
|
10021
|
+
/**
|
|
10022
|
+
* Sorted-set index for execution history, scored by `startedAt`. Drives
|
|
10023
|
+
* the O(log N) `listExecutions` fast path; the legacy SET above is kept
|
|
10024
|
+
* as a fallback for users running with `skipMigration: true` before
|
|
10025
|
+
* they've called `backfillExecutionIndex()` manually.
|
|
10026
|
+
*/
|
|
10027
|
+
execHistoryZsetKey() {
|
|
10028
|
+
return `${this.keyPrefix}exec-history-z`;
|
|
9356
10029
|
}
|
|
9357
10030
|
evalHistoryKey(id) {
|
|
9358
|
-
return
|
|
10031
|
+
return `${this.keyPrefix}eval-history:${id}`;
|
|
9359
10032
|
}
|
|
9360
10033
|
evalHistorySetKey() {
|
|
9361
|
-
return
|
|
10034
|
+
return `${this.keyPrefix}eval-history-ids`;
|
|
10035
|
+
}
|
|
10036
|
+
/** Sorted-set index for eval history, scored by `timestamp`. See above. */
|
|
10037
|
+
evalHistoryZsetKey() {
|
|
10038
|
+
return `${this.keyPrefix}eval-history-z`;
|
|
10039
|
+
}
|
|
10040
|
+
memoryKey(scope) {
|
|
10041
|
+
return `${this.keyPrefix}memory:${scope}`;
|
|
10042
|
+
}
|
|
10043
|
+
// ── Streaming event buffer (for `state.persist: 'streaming'`) ─────────
|
|
10044
|
+
// Layout:
|
|
10045
|
+
// `{prefix}exec-events:{id}` — a Redis list. Events are RPUSH'd as
|
|
10046
|
+
// JSON-serialized AxlEvent objects, in batches. The runtime LRANGEs
|
|
10047
|
+
// the whole list on recovery and deletes it via `finalizeStreamingEvents`
|
|
10048
|
+
// once the canonical `executionHistory` blob has been written at
|
|
10049
|
+
// terminal exit.
|
|
10050
|
+
// `{prefix}streaming-exec-ids` — a Redis set tracking which executions
|
|
10051
|
+
// have an active streaming buffer. Used by `listStreamingExecutions`
|
|
10052
|
+
// to find runs whose process died mid-flight (no corresponding
|
|
10053
|
+
// `executionHistory` blob).
|
|
10054
|
+
streamingEventsKey(executionId) {
|
|
10055
|
+
return `${this.keyPrefix}exec-events:${executionId}`;
|
|
10056
|
+
}
|
|
10057
|
+
streamingIdsKey() {
|
|
10058
|
+
return `${this.keyPrefix}streaming-exec-ids`;
|
|
9362
10059
|
}
|
|
9363
10060
|
// ── Checkpoints ──────────────────────────────────────────────────────
|
|
9364
10061
|
async saveCheckpoint(executionId, name, data) {
|
|
9365
|
-
|
|
10062
|
+
const ttl = this.ttlFor("checkpoint");
|
|
10063
|
+
if (ttl === void 0) {
|
|
10064
|
+
await this.client.hSet(this.checkpointKey(executionId), name, JSON.stringify(data));
|
|
10065
|
+
return;
|
|
10066
|
+
}
|
|
10067
|
+
await this.client.multi().hSet(this.checkpointKey(executionId), name, JSON.stringify(data)).expire(this.checkpointKey(executionId), ttl, "NX").exec();
|
|
9366
10068
|
}
|
|
9367
10069
|
async getCheckpoint(executionId, name) {
|
|
9368
10070
|
const raw = await this.client.hGet(this.checkpointKey(executionId), name);
|
|
@@ -9370,20 +10072,24 @@ var RedisStore = class _RedisStore {
|
|
|
9370
10072
|
}
|
|
9371
10073
|
// ── Sessions ─────────────────────────────────────────────────────────
|
|
9372
10074
|
async saveSession(sessionId, history) {
|
|
9373
|
-
|
|
9374
|
-
|
|
10075
|
+
const ttl = this.ttlFor("session");
|
|
10076
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10077
|
+
await this.client.multi().set(this.sessionKey(sessionId), JSON.stringify(history), setOptions).sAdd(this.sessionIdsKey(), sessionId).exec();
|
|
9375
10078
|
}
|
|
9376
10079
|
async getSession(sessionId) {
|
|
9377
10080
|
const raw = await this.client.get(this.sessionKey(sessionId));
|
|
9378
10081
|
return raw ? JSON.parse(raw) : [];
|
|
9379
10082
|
}
|
|
9380
10083
|
async deleteSession(sessionId) {
|
|
9381
|
-
await this.client.del(this.sessionKey(sessionId));
|
|
9382
|
-
await this.client.del(this.sessionMetaKey(sessionId));
|
|
9383
|
-
await this.client.sRem("axl:session-ids", sessionId);
|
|
10084
|
+
await this.client.multi().del(this.sessionKey(sessionId)).del(this.sessionMetaKey(sessionId)).sRem(this.sessionIdsKey(), sessionId).exec();
|
|
9384
10085
|
}
|
|
9385
10086
|
async saveSessionMeta(sessionId, key, value) {
|
|
9386
|
-
|
|
10087
|
+
const ttl = this.ttlFor("sessionMeta");
|
|
10088
|
+
if (ttl === void 0) {
|
|
10089
|
+
await this.client.hSet(this.sessionMetaKey(sessionId), key, JSON.stringify(value));
|
|
10090
|
+
return;
|
|
10091
|
+
}
|
|
10092
|
+
await this.client.multi().hSet(this.sessionMetaKey(sessionId), key, JSON.stringify(value)).expire(this.sessionMetaKey(sessionId), ttl).exec();
|
|
9387
10093
|
}
|
|
9388
10094
|
async getSessionMeta(sessionId, key) {
|
|
9389
10095
|
const raw = await this.client.hGet(this.sessionMetaKey(sessionId), key);
|
|
@@ -9403,64 +10109,222 @@ var RedisStore = class _RedisStore {
|
|
|
9403
10109
|
}
|
|
9404
10110
|
// ── Execution State ──────────────────────────────────────────────────
|
|
9405
10111
|
async saveExecutionState(executionId, state) {
|
|
9406
|
-
|
|
10112
|
+
const ttl = this.ttlFor("executionState");
|
|
10113
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10114
|
+
const tx = this.client.multi().set(this.executionStateKey(executionId), JSON.stringify(state), setOptions);
|
|
9407
10115
|
if (state.status === "waiting") {
|
|
9408
|
-
|
|
10116
|
+
tx.sAdd(this.pendingExecSetKey(), executionId);
|
|
9409
10117
|
} else {
|
|
9410
|
-
|
|
10118
|
+
tx.sRem(this.pendingExecSetKey(), executionId);
|
|
9411
10119
|
}
|
|
10120
|
+
await tx.exec();
|
|
9412
10121
|
}
|
|
9413
10122
|
async getExecutionState(executionId) {
|
|
9414
10123
|
const raw = await this.client.get(this.executionStateKey(executionId));
|
|
9415
10124
|
return raw ? JSON.parse(raw) : null;
|
|
9416
10125
|
}
|
|
9417
10126
|
async listPendingExecutions() {
|
|
9418
|
-
|
|
10127
|
+
const ids = await this.client.sMembers(this.pendingExecSetKey());
|
|
10128
|
+
if (ids.length === 0) return ids;
|
|
10129
|
+
const keys = ids.map((id) => this.executionStateKey(id));
|
|
10130
|
+
const raws = await this.client.mGet(keys);
|
|
10131
|
+
const live = [];
|
|
10132
|
+
const dead = [];
|
|
10133
|
+
for (let i = 0; i < ids.length; i++) {
|
|
10134
|
+
if (raws[i] != null) {
|
|
10135
|
+
live.push(ids[i]);
|
|
10136
|
+
} else {
|
|
10137
|
+
dead.push(ids[i]);
|
|
10138
|
+
}
|
|
10139
|
+
}
|
|
10140
|
+
if (dead.length > 0) {
|
|
10141
|
+
this.client.sRem(this.pendingExecSetKey(), dead).catch((err) => {
|
|
10142
|
+
console.error(
|
|
10143
|
+
`[axl] RedisStore: failed to prune ${dead.length} stale pending-exec ids: ` + (err instanceof Error ? err.message : String(err))
|
|
10144
|
+
);
|
|
10145
|
+
});
|
|
10146
|
+
}
|
|
10147
|
+
return live;
|
|
10148
|
+
}
|
|
10149
|
+
// ── Memory ──────────────────────────────────────────────────────────
|
|
10150
|
+
//
|
|
10151
|
+
// Layout: `{prefix}memory:{scope}` is a hash. Fields are user-supplied
|
|
10152
|
+
// keys; values are JSON-serialized. Hash-per-scope keeps related entries
|
|
10153
|
+
// together (good cohesion for TTL eviction once #3 lands) and bounds the
|
|
10154
|
+
// keyspace to O(scopes) instead of O(scopes × keys).
|
|
10155
|
+
//
|
|
10156
|
+
// Pre-this-patch, RedisStore had no memory methods, so MemoryManager fell
|
|
10157
|
+
// back to writing memory at synthetic sessionMeta keys
|
|
10158
|
+
// (`{prefix}session-meta:memory:{scope}:{key}` hash field `value`). To
|
|
10159
|
+
// preserve user data on upgrade, `getMemory` checks the legacy location on
|
|
10160
|
+
// miss and migrates forward; `deleteMemory` also cleans the legacy entry.
|
|
10161
|
+
// No legacy support on `getAllMemory` — that method didn't exist before,
|
|
10162
|
+
// so no user can depend on it returning legacy data.
|
|
10163
|
+
async saveMemory(scope, key, value) {
|
|
10164
|
+
const ttl = this.ttlFor("memory");
|
|
10165
|
+
if (ttl === void 0) {
|
|
10166
|
+
await this.client.hSet(this.memoryKey(scope), key, JSON.stringify(value));
|
|
10167
|
+
return;
|
|
10168
|
+
}
|
|
10169
|
+
await this.client.multi().hSet(this.memoryKey(scope), key, JSON.stringify(value)).expire(this.memoryKey(scope), ttl).exec();
|
|
10170
|
+
}
|
|
10171
|
+
async getMemory(scope, key) {
|
|
10172
|
+
const raw = await this.client.hGet(this.memoryKey(scope), key);
|
|
10173
|
+
if (raw != null) return JSON.parse(raw);
|
|
10174
|
+
const legacyRaw = await this.client.hGet(
|
|
10175
|
+
this.sessionMetaKey(`memory:${scope}:${key}`),
|
|
10176
|
+
"value"
|
|
10177
|
+
);
|
|
10178
|
+
if (legacyRaw == null) return null;
|
|
10179
|
+
const inserted = await this.client.hSetNX(this.memoryKey(scope), key, legacyRaw);
|
|
10180
|
+
if (inserted === 0 || inserted === false) {
|
|
10181
|
+
const winner = await this.client.hGet(this.memoryKey(scope), key);
|
|
10182
|
+
if (winner != null) return JSON.parse(winner);
|
|
10183
|
+
return null;
|
|
10184
|
+
}
|
|
10185
|
+
const ttl = this.ttlFor("memory");
|
|
10186
|
+
if (ttl !== void 0) {
|
|
10187
|
+
this.client.expire(this.memoryKey(scope), ttl).catch((err) => {
|
|
10188
|
+
console.error(
|
|
10189
|
+
`[axl] RedisStore: failed to set TTL on migrated memory key ${this.memoryKey(scope)}: ` + (err instanceof Error ? err.message : String(err))
|
|
10190
|
+
);
|
|
10191
|
+
});
|
|
10192
|
+
}
|
|
10193
|
+
return JSON.parse(legacyRaw);
|
|
10194
|
+
}
|
|
10195
|
+
async getAllMemory(scope) {
|
|
10196
|
+
const all = await this.client.hGetAll(this.memoryKey(scope));
|
|
10197
|
+
if (!all) return [];
|
|
10198
|
+
return Object.entries(all).map(([key, raw]) => ({ key, value: JSON.parse(raw) }));
|
|
10199
|
+
}
|
|
10200
|
+
async deleteMemory(scope, key) {
|
|
10201
|
+
await this.client.multi().hDel(this.memoryKey(scope), key).hDel(this.sessionMetaKey(`memory:${scope}:${key}`), "value").exec();
|
|
9419
10202
|
}
|
|
9420
10203
|
// ── Execution History ────────────────────────────────────────────────
|
|
9421
10204
|
async saveExecution(execution) {
|
|
9422
|
-
|
|
9423
|
-
|
|
10205
|
+
const ttl = this.ttlFor("executionHistory");
|
|
10206
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10207
|
+
await this.client.multi().sAdd(this.execHistorySetKey(), execution.executionId).set(this.execHistoryKey(execution.executionId), JSON.stringify(execution), setOptions).zAdd(this.execHistoryZsetKey(), {
|
|
10208
|
+
score: execution.startedAt,
|
|
10209
|
+
value: execution.executionId
|
|
10210
|
+
}).exec();
|
|
9424
10211
|
}
|
|
9425
10212
|
async getExecution(executionId) {
|
|
9426
10213
|
const raw = await this.client.get(this.execHistoryKey(executionId));
|
|
9427
10214
|
return raw ? JSON.parse(raw) : null;
|
|
9428
10215
|
}
|
|
9429
10216
|
async listExecutions(limit) {
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
10217
|
+
return this.listHistoryByZset(
|
|
10218
|
+
this.execHistorySetKey(),
|
|
10219
|
+
this.execHistoryZsetKey(),
|
|
10220
|
+
(id) => this.execHistoryKey(id),
|
|
10221
|
+
(a, b) => b.startedAt - a.startedAt,
|
|
10222
|
+
limit
|
|
10223
|
+
);
|
|
10224
|
+
}
|
|
10225
|
+
async deleteExecution(executionId) {
|
|
10226
|
+
const [, deletedCount] = await this.client.multi().sRem(this.execHistorySetKey(), executionId).del(this.execHistoryKey(executionId)).zRem(this.execHistoryZsetKey(), executionId).del(this.checkpointKey(executionId)).del(this.executionStateKey(executionId)).sRem(this.pendingExecSetKey(), executionId).del(this.streamingEventsKey(executionId)).sRem(this.streamingIdsKey(), executionId).hDel(this.decisionsKey(), executionId).exec();
|
|
10227
|
+
const deleted = typeof deletedCount === "number" ? deletedCount : 0;
|
|
10228
|
+
return deleted > 0;
|
|
9439
10229
|
}
|
|
9440
10230
|
// ── Eval History ────────────────────────────────────────────────────
|
|
9441
10231
|
async saveEvalResult(entry) {
|
|
9442
|
-
|
|
9443
|
-
|
|
10232
|
+
const ttl = this.ttlFor("evalHistory");
|
|
10233
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10234
|
+
await this.client.multi().sAdd(this.evalHistorySetKey(), entry.id).set(this.evalHistoryKey(entry.id), JSON.stringify(entry), setOptions).zAdd(this.evalHistoryZsetKey(), { score: entry.timestamp, value: entry.id }).exec();
|
|
9444
10235
|
}
|
|
9445
10236
|
async listEvalResults(limit) {
|
|
9446
|
-
|
|
10237
|
+
return this.listHistoryByZset(
|
|
10238
|
+
this.evalHistorySetKey(),
|
|
10239
|
+
this.evalHistoryZsetKey(),
|
|
10240
|
+
(id) => this.evalHistoryKey(id),
|
|
10241
|
+
(a, b) => b.timestamp - a.timestamp,
|
|
10242
|
+
limit
|
|
10243
|
+
);
|
|
10244
|
+
}
|
|
10245
|
+
async deleteEvalResult(id) {
|
|
10246
|
+
const [, deletedCount] = await this.client.multi().sRem(this.evalHistorySetKey(), id).del(this.evalHistoryKey(id)).zRem(this.evalHistoryZsetKey(), id).exec();
|
|
10247
|
+
const deleted = typeof deletedCount === "number" ? deletedCount : 0;
|
|
10248
|
+
return deleted > 0;
|
|
10249
|
+
}
|
|
10250
|
+
/**
|
|
10251
|
+
* Shared list-by-sorted-set implementation. Uses ZREVRANGEBYSCORE +
|
|
10252
|
+
* MGET when the ZSET is populated (post-migration). Falls back to the
|
|
10253
|
+
* legacy SET + N×GET + JS-sort path when the ZSET is empty or smaller
|
|
10254
|
+
* than the SET (covers users that constructed with `skipMigration: true`
|
|
10255
|
+
* before calling backfill).
|
|
10256
|
+
*/
|
|
10257
|
+
async listHistoryByZset(setKey, zsetKey, dataKey, sortCmp, limit) {
|
|
10258
|
+
const setSize = await this.client.sCard(setKey);
|
|
10259
|
+
if (setSize === 0) return [];
|
|
10260
|
+
const zsetSize = await this.client.zCard(zsetKey);
|
|
10261
|
+
if (zsetSize >= setSize) {
|
|
10262
|
+
const fetchCount = limit !== void 0 ? Math.max(limit * 2, limit + 5) : void 0;
|
|
10263
|
+
const ids2 = await this.client.zRange(zsetKey, "+inf", "-inf", {
|
|
10264
|
+
BY: "SCORE",
|
|
10265
|
+
REV: true,
|
|
10266
|
+
...fetchCount !== void 0 ? { LIMIT: { offset: 0, count: fetchCount } } : {}
|
|
10267
|
+
});
|
|
10268
|
+
if (ids2.length === 0) return [];
|
|
10269
|
+
const keys = ids2.map(dataKey);
|
|
10270
|
+
const raws = await this.client.mGet(keys);
|
|
10271
|
+
const entries2 = [];
|
|
10272
|
+
for (const raw of raws) {
|
|
10273
|
+
if (raw == null) continue;
|
|
10274
|
+
try {
|
|
10275
|
+
entries2.push(JSON.parse(raw));
|
|
10276
|
+
} catch {
|
|
10277
|
+
}
|
|
10278
|
+
if (limit !== void 0 && entries2.length >= limit) break;
|
|
10279
|
+
}
|
|
10280
|
+
return entries2;
|
|
10281
|
+
}
|
|
10282
|
+
const ids = await this.client.sMembers(setKey);
|
|
9447
10283
|
if (ids.length === 0) return [];
|
|
9448
10284
|
const entries = [];
|
|
9449
10285
|
for (const id of ids) {
|
|
9450
|
-
const raw = await this.client.get(
|
|
9451
|
-
if (raw)
|
|
10286
|
+
const raw = await this.client.get(dataKey(id));
|
|
10287
|
+
if (raw == null) continue;
|
|
10288
|
+
try {
|
|
10289
|
+
entries.push(JSON.parse(raw));
|
|
10290
|
+
} catch {
|
|
10291
|
+
}
|
|
9452
10292
|
}
|
|
9453
|
-
entries.sort(
|
|
10293
|
+
entries.sort(sortCmp);
|
|
9454
10294
|
return limit ? entries.slice(0, limit) : entries;
|
|
9455
10295
|
}
|
|
9456
|
-
async deleteEvalResult(id) {
|
|
9457
|
-
await this.client.sRem(this.evalHistorySetKey(), id);
|
|
9458
|
-
const deleted = await this.client.del(this.evalHistoryKey(id));
|
|
9459
|
-
return deleted > 0;
|
|
9460
|
-
}
|
|
9461
10296
|
// ── Sessions (Studio introspection) ────────────────────────────────────
|
|
9462
10297
|
async listSessions() {
|
|
9463
|
-
return this.client.sMembers(
|
|
10298
|
+
return this.client.sMembers(this.sessionIdsKey());
|
|
10299
|
+
}
|
|
10300
|
+
// ── Streaming events (for state.persist: 'streaming') ──────────────────
|
|
10301
|
+
async appendStreamingEvents(executionId, events) {
|
|
10302
|
+
if (events.length === 0) return;
|
|
10303
|
+
const serialized = events.map((e) => JSON.stringify(e));
|
|
10304
|
+
const ttl = this.ttlFor("streamingEvents");
|
|
10305
|
+
const tx = this.client.multi().rPush(this.streamingEventsKey(executionId), serialized).sAdd(this.streamingIdsKey(), executionId);
|
|
10306
|
+
if (ttl !== void 0) {
|
|
10307
|
+
tx.expire(this.streamingEventsKey(executionId), ttl, "NX");
|
|
10308
|
+
}
|
|
10309
|
+
await tx.exec();
|
|
10310
|
+
}
|
|
10311
|
+
async finalizeStreamingEvents(executionId) {
|
|
10312
|
+
await this.client.multi().del(this.streamingEventsKey(executionId)).sRem(this.streamingIdsKey(), executionId).exec();
|
|
10313
|
+
}
|
|
10314
|
+
async listStreamingExecutions() {
|
|
10315
|
+
return this.client.sMembers(this.streamingIdsKey());
|
|
10316
|
+
}
|
|
10317
|
+
async getStreamingEvents(executionId) {
|
|
10318
|
+
const raws = await this.client.lRange(this.streamingEventsKey(executionId), 0, -1);
|
|
10319
|
+
if (raws.length === 0) return [];
|
|
10320
|
+
const events = [];
|
|
10321
|
+
for (const raw of raws) {
|
|
10322
|
+
try {
|
|
10323
|
+
events.push(JSON.parse(raw));
|
|
10324
|
+
} catch {
|
|
10325
|
+
}
|
|
10326
|
+
}
|
|
10327
|
+
return events;
|
|
9464
10328
|
}
|
|
9465
10329
|
/** Close the Redis connection. */
|
|
9466
10330
|
async close() {
|