@axlsdk/axl 0.17.5 → 0.17.7
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 +65 -3
- package/dist/index.cjs +935 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +389 -7
- package/dist/index.d.ts +389 -7
- package/dist/index.js +935 -70
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -839,6 +839,7 @@ var OpenAIResponsesProvider = class {
|
|
|
839
839
|
// src/providers/anthropic.ts
|
|
840
840
|
var ANTHROPIC_API_VERSION = "2023-06-01";
|
|
841
841
|
var ANTHROPIC_PRICING = {
|
|
842
|
+
"claude-opus-4-7": [5e-6, 25e-6],
|
|
842
843
|
"claude-opus-4-6": [5e-6, 25e-6],
|
|
843
844
|
"claude-sonnet-4-6": [3e-6, 15e-6],
|
|
844
845
|
"claude-opus-4-5": [5e-6, 25e-6],
|
|
@@ -883,13 +884,21 @@ var THINKING_BUDGETS = {
|
|
|
883
884
|
max: 3e4
|
|
884
885
|
};
|
|
885
886
|
function supportsAdaptiveThinking(model) {
|
|
886
|
-
return model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6");
|
|
887
|
+
return model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6");
|
|
887
888
|
}
|
|
888
889
|
function supportsMaxEffort(model) {
|
|
889
|
-
return model.startsWith("claude-opus-4-6");
|
|
890
|
+
return model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6");
|
|
891
|
+
}
|
|
892
|
+
function supportsXhigh2(model) {
|
|
893
|
+
return model.startsWith("claude-opus-4-7");
|
|
890
894
|
}
|
|
891
895
|
function supportsEffort(model) {
|
|
892
|
-
return model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6") || model.startsWith("claude-opus-4-5");
|
|
896
|
+
return model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6") || model.startsWith("claude-opus-4-5");
|
|
897
|
+
}
|
|
898
|
+
function clampAnthropicEffort(model, effort) {
|
|
899
|
+
if (effort === "max" && !supportsMaxEffort(model)) return "high";
|
|
900
|
+
if (effort === "xhigh" && !supportsXhigh2(model)) return "high";
|
|
901
|
+
return effort;
|
|
893
902
|
}
|
|
894
903
|
var AnthropicProvider = class {
|
|
895
904
|
name = "anthropic";
|
|
@@ -990,10 +999,7 @@ var AnthropicProvider = class {
|
|
|
990
999
|
body.tool_choice = this.mapToolChoice(options.toolChoice);
|
|
991
1000
|
}
|
|
992
1001
|
const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolveThinkingOptions(options);
|
|
993
|
-
|
|
994
|
-
if (resolvedEffort === "max" && !supportsMaxEffort(options.model)) {
|
|
995
|
-
resolvedEffort = "high";
|
|
996
|
-
}
|
|
1002
|
+
const resolvedEffort = activeEffort ? clampAnthropicEffort(options.model, activeEffort) : void 0;
|
|
997
1003
|
if (hasBudgetOverride) {
|
|
998
1004
|
body.thinking = { type: "enabled", budget_tokens: thinkingBudget };
|
|
999
1005
|
const currentMax = body.max_tokens;
|
|
@@ -1360,6 +1366,7 @@ var GEMINI_PRICING = {
|
|
|
1360
1366
|
"gemini-3-pro-preview": [2e-6, 12e-6],
|
|
1361
1367
|
"gemini-3-flash-preview": [5e-7, 3e-6],
|
|
1362
1368
|
"gemini-3.1-pro-preview": [2e-6, 12e-6],
|
|
1369
|
+
"gemini-3.1-flash-lite": [25e-8, 15e-7],
|
|
1363
1370
|
"gemini-3.1-flash-lite-preview": [25e-8, 15e-7]
|
|
1364
1371
|
};
|
|
1365
1372
|
var GEMINI_PRICING_KEYS_BY_LENGTH = Object.keys(GEMINI_PRICING).sort(
|
|
@@ -1385,12 +1392,16 @@ var THINKING_BUDGETS2 = {
|
|
|
1385
1392
|
low: 1024,
|
|
1386
1393
|
medium: 5e3,
|
|
1387
1394
|
high: 1e4,
|
|
1395
|
+
xhigh: 16384,
|
|
1396
|
+
// between high (10000) and max (24576)
|
|
1388
1397
|
max: 24576
|
|
1389
1398
|
};
|
|
1390
1399
|
var THINKING_LEVELS = {
|
|
1391
1400
|
low: "low",
|
|
1392
1401
|
medium: "medium",
|
|
1393
1402
|
high: "high",
|
|
1403
|
+
xhigh: "high",
|
|
1404
|
+
// 3.x has no xhigh tier; clamp to 'high'
|
|
1394
1405
|
max: "high"
|
|
1395
1406
|
// 3.x caps at 'high'
|
|
1396
1407
|
};
|
|
@@ -5903,8 +5914,31 @@ ${summaryResponse.content}`
|
|
|
5903
5914
|
type: "await_human",
|
|
5904
5915
|
data: { channel: options.channel, prompt: options.prompt }
|
|
5905
5916
|
});
|
|
5906
|
-
const
|
|
5917
|
+
const makeAbortError = (reason) => {
|
|
5918
|
+
if (typeof DOMException !== "undefined") {
|
|
5919
|
+
return new DOMException(
|
|
5920
|
+
typeof reason === "string" ? reason : "awaitHuman aborted",
|
|
5921
|
+
"AbortError"
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5924
|
+
const err = new Error(typeof reason === "string" ? reason : "awaitHuman aborted");
|
|
5925
|
+
err.name = "AbortError";
|
|
5926
|
+
return err;
|
|
5927
|
+
};
|
|
5928
|
+
const decision = await new Promise((resolve2, reject) => {
|
|
5929
|
+
if (this.signal?.aborted) {
|
|
5930
|
+
reject(makeAbortError(this.signal.reason));
|
|
5931
|
+
return;
|
|
5932
|
+
}
|
|
5907
5933
|
this.pendingDecisions.set(this.executionId, resolve2);
|
|
5934
|
+
const onAbort = () => {
|
|
5935
|
+
const current = this.pendingDecisions.get(this.executionId);
|
|
5936
|
+
if (current === resolve2) {
|
|
5937
|
+
this.pendingDecisions.delete(this.executionId);
|
|
5938
|
+
}
|
|
5939
|
+
reject(makeAbortError(this.signal?.reason));
|
|
5940
|
+
};
|
|
5941
|
+
this.signal?.addEventListener("abort", onAbort, { once: true });
|
|
5908
5942
|
});
|
|
5909
5943
|
this.emitEvent({
|
|
5910
5944
|
type: "await_human_resolved",
|
|
@@ -6529,6 +6563,16 @@ var MemoryStore = class {
|
|
|
6529
6563
|
const result = limit ? sorted.slice(0, limit) : sorted;
|
|
6530
6564
|
return result.map((e) => structuredClone(e));
|
|
6531
6565
|
}
|
|
6566
|
+
async deleteExecution(executionId) {
|
|
6567
|
+
const removed = this.executionHistory.delete(executionId);
|
|
6568
|
+
this.checkpoints.delete(executionId);
|
|
6569
|
+
this.executionStates.delete(executionId);
|
|
6570
|
+
this.streamingEvents.delete(executionId);
|
|
6571
|
+
if (this.decisions.delete(executionId)) {
|
|
6572
|
+
this.persistAwaitHumanState();
|
|
6573
|
+
}
|
|
6574
|
+
return removed;
|
|
6575
|
+
}
|
|
6532
6576
|
// ── Eval History ──────────────────────────────────────────────────
|
|
6533
6577
|
async saveEvalResult(entry) {
|
|
6534
6578
|
this.evalHistory.set(entry.id, structuredClone(entry));
|
|
@@ -6545,6 +6589,30 @@ var MemoryStore = class {
|
|
|
6545
6589
|
async listSessions() {
|
|
6546
6590
|
return [...this.sessions.keys()];
|
|
6547
6591
|
}
|
|
6592
|
+
// ── Streaming events (for state.persist: 'streaming') ────────────────
|
|
6593
|
+
//
|
|
6594
|
+
// MemoryStore's raison d'être is in-process / ephemeral. We implement
|
|
6595
|
+
// the streaming methods so tests and dev workflows behave correctly,
|
|
6596
|
+
// but on a process crash these buffers are lost — same as the rest of
|
|
6597
|
+
// MemoryStore's state. For real crash-survival, use RedisStore or
|
|
6598
|
+
// SQLiteStore.
|
|
6599
|
+
streamingEvents = /* @__PURE__ */ new Map();
|
|
6600
|
+
async appendStreamingEvents(executionId, events) {
|
|
6601
|
+
if (events.length === 0) return;
|
|
6602
|
+
const existing = this.streamingEvents.get(executionId) ?? [];
|
|
6603
|
+
existing.push(...events.map((e) => structuredClone(e)));
|
|
6604
|
+
this.streamingEvents.set(executionId, existing);
|
|
6605
|
+
}
|
|
6606
|
+
async finalizeStreamingEvents(executionId) {
|
|
6607
|
+
this.streamingEvents.delete(executionId);
|
|
6608
|
+
}
|
|
6609
|
+
async listStreamingExecutions() {
|
|
6610
|
+
return [...this.streamingEvents.keys()];
|
|
6611
|
+
}
|
|
6612
|
+
async getStreamingEvents(executionId) {
|
|
6613
|
+
const events = this.streamingEvents.get(executionId) ?? [];
|
|
6614
|
+
return events.map((e) => structuredClone(e));
|
|
6615
|
+
}
|
|
6548
6616
|
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
6549
6617
|
async close() {
|
|
6550
6618
|
}
|
|
@@ -6601,7 +6669,7 @@ function safeJsonParse(data) {
|
|
|
6601
6669
|
}
|
|
6602
6670
|
}
|
|
6603
6671
|
function rowToExecutionInfo(row) {
|
|
6604
|
-
|
|
6672
|
+
const info = {
|
|
6605
6673
|
executionId: row.execution_id,
|
|
6606
6674
|
workflow: row.workflow,
|
|
6607
6675
|
status: row.status,
|
|
@@ -6612,6 +6680,13 @@ function rowToExecutionInfo(row) {
|
|
|
6612
6680
|
error: row.error ?? void 0,
|
|
6613
6681
|
events: safeJsonParse(row.events) ?? []
|
|
6614
6682
|
};
|
|
6683
|
+
if (row.metadata != null) {
|
|
6684
|
+
const parsed = safeJsonParse(row.metadata);
|
|
6685
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
6686
|
+
info.metadata = parsed;
|
|
6687
|
+
}
|
|
6688
|
+
}
|
|
6689
|
+
return info;
|
|
6615
6690
|
}
|
|
6616
6691
|
var SQLiteStore = class {
|
|
6617
6692
|
db;
|
|
@@ -6647,6 +6722,10 @@ var SQLiteStore = class {
|
|
|
6647
6722
|
* numeric step values become stringified names ("0", "1", …) to
|
|
6648
6723
|
* preserve replay continuity for in-flight executions across the
|
|
6649
6724
|
* upgrade boundary.
|
|
6725
|
+
* - v3 (ExecutionInfo.metadata): add `execution_history.metadata`
|
|
6726
|
+
* column (TEXT, nullable, JSON-serialized) so caller-supplied
|
|
6727
|
+
* `ExecuteOptions.metadata` round-trips through history. Existing
|
|
6728
|
+
* rows get `NULL`, deserialized as `metadata: undefined`.
|
|
6650
6729
|
*
|
|
6651
6730
|
* Applied BEFORE `initTables()` so the subsequent `CREATE TABLE
|
|
6652
6731
|
* IF NOT EXISTS` runs against the post-migration column name. Fresh
|
|
@@ -6664,7 +6743,7 @@ var SQLiteStore = class {
|
|
|
6664
6743
|
* transactional re-read costs nothing.
|
|
6665
6744
|
*/
|
|
6666
6745
|
migrate() {
|
|
6667
|
-
const TARGET_VERSION =
|
|
6746
|
+
const TARGET_VERSION = 3;
|
|
6668
6747
|
const current = this.db.pragma("user_version", { simple: true });
|
|
6669
6748
|
if (current >= TARGET_VERSION) return;
|
|
6670
6749
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -6686,6 +6765,12 @@ var SQLiteStore = class {
|
|
|
6686
6765
|
this.db.exec("ALTER TABLE checkpoints RENAME COLUMN step TO name");
|
|
6687
6766
|
}
|
|
6688
6767
|
}
|
|
6768
|
+
if (committed < 3) {
|
|
6769
|
+
const cols = this.db.pragma("table_info(execution_history)");
|
|
6770
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "metadata")) {
|
|
6771
|
+
this.db.exec("ALTER TABLE execution_history ADD COLUMN metadata TEXT");
|
|
6772
|
+
}
|
|
6773
|
+
}
|
|
6689
6774
|
this.db.pragma(`user_version = ${TARGET_VERSION}`);
|
|
6690
6775
|
this.db.exec("COMMIT");
|
|
6691
6776
|
} catch (err) {
|
|
@@ -6748,7 +6833,8 @@ var SQLiteStore = class {
|
|
|
6748
6833
|
completed_at INTEGER,
|
|
6749
6834
|
duration INTEGER NOT NULL DEFAULT 0,
|
|
6750
6835
|
error TEXT,
|
|
6751
|
-
events TEXT NOT NULL
|
|
6836
|
+
events TEXT NOT NULL,
|
|
6837
|
+
metadata TEXT
|
|
6752
6838
|
);
|
|
6753
6839
|
|
|
6754
6840
|
CREATE TABLE IF NOT EXISTS eval_history (
|
|
@@ -6869,7 +6955,7 @@ var SQLiteStore = class {
|
|
|
6869
6955
|
// ── Execution History ────────────────────────────────────────────────────
|
|
6870
6956
|
async saveExecution(execution) {
|
|
6871
6957
|
this.db.prepare(
|
|
6872
|
-
"INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, events) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
6958
|
+
"INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, events, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
6873
6959
|
).run(
|
|
6874
6960
|
execution.executionId,
|
|
6875
6961
|
execution.workflow,
|
|
@@ -6879,7 +6965,8 @@ var SQLiteStore = class {
|
|
|
6879
6965
|
execution.completedAt ?? null,
|
|
6880
6966
|
execution.duration,
|
|
6881
6967
|
execution.error ?? null,
|
|
6882
|
-
JSON.stringify(execution.events)
|
|
6968
|
+
JSON.stringify(execution.events),
|
|
6969
|
+
execution.metadata !== void 0 ? JSON.stringify(execution.metadata) : null
|
|
6883
6970
|
);
|
|
6884
6971
|
}
|
|
6885
6972
|
async getExecution(executionId) {
|
|
@@ -6891,6 +6978,16 @@ var SQLiteStore = class {
|
|
|
6891
6978
|
const rows = limit ? this.db.prepare(sql).all(limit) : this.db.prepare(sql).all();
|
|
6892
6979
|
return rows.map(rowToExecutionInfo);
|
|
6893
6980
|
}
|
|
6981
|
+
async deleteExecution(executionId) {
|
|
6982
|
+
const tx = this.db.transaction(() => {
|
|
6983
|
+
const result = this.db.prepare("DELETE FROM execution_history WHERE execution_id = ?").run(executionId);
|
|
6984
|
+
this.db.prepare("DELETE FROM checkpoints WHERE execution_id = ?").run(executionId);
|
|
6985
|
+
this.db.prepare("DELETE FROM execution_state WHERE execution_id = ?").run(executionId);
|
|
6986
|
+
this.db.prepare("DELETE FROM decisions WHERE execution_id = ?").run(executionId);
|
|
6987
|
+
return result.changes > 0;
|
|
6988
|
+
});
|
|
6989
|
+
return tx();
|
|
6990
|
+
}
|
|
6894
6991
|
// ── Eval History ────────────────────────────────────────────────────────
|
|
6895
6992
|
async saveEvalResult(entry) {
|
|
6896
6993
|
this.db.prepare(
|
|
@@ -7769,6 +7866,7 @@ var MemoryManager = class {
|
|
|
7769
7866
|
if (stateStore.saveMemory) {
|
|
7770
7867
|
await stateStore.saveMemory(storeScope, key, value);
|
|
7771
7868
|
} else {
|
|
7869
|
+
warnMissingMemoryMethodsOnce(stateStore);
|
|
7772
7870
|
const storeKey = `memory:${storeScope}:${key}`;
|
|
7773
7871
|
await stateStore.saveSessionMeta(storeKey, "value", value);
|
|
7774
7872
|
if (options?.metadata) {
|
|
@@ -7871,6 +7969,14 @@ var MemoryManager = class {
|
|
|
7871
7969
|
}
|
|
7872
7970
|
}
|
|
7873
7971
|
};
|
|
7972
|
+
var _warnedStoresMissingMemory = /* @__PURE__ */ new WeakSet();
|
|
7973
|
+
function warnMissingMemoryMethodsOnce(stateStore) {
|
|
7974
|
+
if (_warnedStoresMissingMemory.has(stateStore)) return;
|
|
7975
|
+
_warnedStoresMissingMemory.add(stateStore);
|
|
7976
|
+
console.warn(
|
|
7977
|
+
"[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."
|
|
7978
|
+
);
|
|
7979
|
+
}
|
|
7874
7980
|
|
|
7875
7981
|
// src/telemetry/noop.ts
|
|
7876
7982
|
var NOOP_SPAN = {
|
|
@@ -7912,11 +8018,141 @@ function hashInput(input) {
|
|
|
7912
8018
|
return (hash >>> 0).toString(16);
|
|
7913
8019
|
}
|
|
7914
8020
|
var DEFAULT_MAX_EVENTS_PER_EXECUTION = 5e4;
|
|
8021
|
+
var DEFAULT_STREAMING_BATCH_SIZE = 100;
|
|
8022
|
+
var DEFAULT_STREAMING_BATCH_INTERVAL = 1e3;
|
|
8023
|
+
var RECOVERED_UNKNOWN_WORKFLOW = "__axl/recovered";
|
|
8024
|
+
var STREAMING_EXCLUDED_TYPES = /* @__PURE__ */ new Set([
|
|
8025
|
+
"token",
|
|
8026
|
+
"partial_object",
|
|
8027
|
+
"string_delta"
|
|
8028
|
+
]);
|
|
8029
|
+
var INTERNAL_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
8030
|
+
"sessionId",
|
|
8031
|
+
"sessionHistory",
|
|
8032
|
+
"resumeMode"
|
|
8033
|
+
]);
|
|
8034
|
+
function liftPersistedMetadata(metadata) {
|
|
8035
|
+
if (!metadata) return void 0;
|
|
8036
|
+
const filtered = {};
|
|
8037
|
+
let hasAny = false;
|
|
8038
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
8039
|
+
if (INTERNAL_METADATA_KEYS.has(k)) continue;
|
|
8040
|
+
filtered[k] = v;
|
|
8041
|
+
hasAny = true;
|
|
8042
|
+
}
|
|
8043
|
+
if (!hasAny) return void 0;
|
|
8044
|
+
try {
|
|
8045
|
+
return structuredClone(filtered);
|
|
8046
|
+
} catch {
|
|
8047
|
+
return { ...filtered };
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
var StreamingFlusher = class {
|
|
8051
|
+
constructor(store, batchSize, batchInterval) {
|
|
8052
|
+
this.store = store;
|
|
8053
|
+
this.batchSize = batchSize;
|
|
8054
|
+
this.batchInterval = batchInterval;
|
|
8055
|
+
}
|
|
8056
|
+
buffers = /* @__PURE__ */ new Map();
|
|
8057
|
+
timers = /* @__PURE__ */ new Map();
|
|
8058
|
+
/**
|
|
8059
|
+
* Tracks the most recent in-flight `appendStreamingEvents` per execution
|
|
8060
|
+
* so `finalize` can await it. Without this, a size-triggered fire-and-
|
|
8061
|
+
* forget flush whose RPUSH lands AFTER `finalize`'s DEL would resurrect
|
|
8062
|
+
* the buffer + re-register the execution in the streaming-exec-ids set
|
|
8063
|
+
* — a phantom orphan that recoverIncompleteStreams would later mis-recover.
|
|
8064
|
+
*/
|
|
8065
|
+
inflight = /* @__PURE__ */ new Map();
|
|
8066
|
+
append(executionId, event) {
|
|
8067
|
+
if (STREAMING_EXCLUDED_TYPES.has(event.type)) return;
|
|
8068
|
+
let buffer = this.buffers.get(executionId);
|
|
8069
|
+
if (!buffer) {
|
|
8070
|
+
buffer = [];
|
|
8071
|
+
this.buffers.set(executionId, buffer);
|
|
8072
|
+
}
|
|
8073
|
+
buffer.push(event);
|
|
8074
|
+
if (buffer.length >= this.batchSize) {
|
|
8075
|
+
void this.flush(executionId);
|
|
8076
|
+
return;
|
|
8077
|
+
}
|
|
8078
|
+
if (!this.timers.has(executionId)) {
|
|
8079
|
+
const timer = setTimeout(() => {
|
|
8080
|
+
this.timers.delete(executionId);
|
|
8081
|
+
void this.flush(executionId);
|
|
8082
|
+
}, this.batchInterval);
|
|
8083
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
8084
|
+
timer.unref?.();
|
|
8085
|
+
}
|
|
8086
|
+
this.timers.set(executionId, timer);
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
/** Flush this execution's buffer immediately. Awaits any prior in-flight
|
|
8090
|
+
* flush for the same execution so writes land in order. */
|
|
8091
|
+
async flush(executionId) {
|
|
8092
|
+
const buffer = this.buffers.get(executionId);
|
|
8093
|
+
if (!buffer || buffer.length === 0) return;
|
|
8094
|
+
this.buffers.set(executionId, []);
|
|
8095
|
+
const timer = this.timers.get(executionId);
|
|
8096
|
+
if (timer) {
|
|
8097
|
+
clearTimeout(timer);
|
|
8098
|
+
this.timers.delete(executionId);
|
|
8099
|
+
}
|
|
8100
|
+
const prior = this.inflight.get(executionId) ?? Promise.resolve();
|
|
8101
|
+
const next = prior.then(async () => {
|
|
8102
|
+
try {
|
|
8103
|
+
await this.store.appendStreamingEvents?.(executionId, buffer);
|
|
8104
|
+
} catch (err) {
|
|
8105
|
+
console.error(
|
|
8106
|
+
`[axl] StreamingFlusher: failed to append ${buffer.length} events for executionId=${executionId}: ${err instanceof Error ? err.message : String(err)}`
|
|
8107
|
+
);
|
|
8108
|
+
}
|
|
8109
|
+
});
|
|
8110
|
+
this.inflight.set(executionId, next);
|
|
8111
|
+
next.finally(() => {
|
|
8112
|
+
if (this.inflight.get(executionId) === next) {
|
|
8113
|
+
this.inflight.delete(executionId);
|
|
8114
|
+
}
|
|
8115
|
+
});
|
|
8116
|
+
return next;
|
|
8117
|
+
}
|
|
8118
|
+
/** Finalize an execution — flush remaining buffer + release store-side buffer.
|
|
8119
|
+
* Critically, awaits the most recent in-flight `appendStreamingEvents` BEFORE
|
|
8120
|
+
* calling `finalizeStreamingEvents`, so a late-landing RPUSH can't resurrect
|
|
8121
|
+
* the buffer after we've deleted it. */
|
|
8122
|
+
async finalize(executionId) {
|
|
8123
|
+
await this.flush(executionId);
|
|
8124
|
+
const pending = this.inflight.get(executionId);
|
|
8125
|
+
if (pending) {
|
|
8126
|
+
await pending;
|
|
8127
|
+
this.inflight.delete(executionId);
|
|
8128
|
+
}
|
|
8129
|
+
this.buffers.delete(executionId);
|
|
8130
|
+
const timer = this.timers.get(executionId);
|
|
8131
|
+
if (timer) {
|
|
8132
|
+
clearTimeout(timer);
|
|
8133
|
+
this.timers.delete(executionId);
|
|
8134
|
+
}
|
|
8135
|
+
try {
|
|
8136
|
+
await this.store.finalizeStreamingEvents?.(executionId);
|
|
8137
|
+
} catch (err) {
|
|
8138
|
+
console.error(
|
|
8139
|
+
`[axl] StreamingFlusher: failed to finalize executionId=${executionId}: ` + (err instanceof Error ? err.message : String(err))
|
|
8140
|
+
);
|
|
8141
|
+
}
|
|
8142
|
+
}
|
|
8143
|
+
/** Best-effort drain of every pending buffer. Called from `runtime.shutdown()`. */
|
|
8144
|
+
async flushAll() {
|
|
8145
|
+
const ids = [...this.buffers.keys()];
|
|
8146
|
+
await Promise.all(ids.map((id) => this.flush(id)));
|
|
8147
|
+
const pendings = [...this.inflight.values()];
|
|
8148
|
+
await Promise.allSettled(pendings);
|
|
8149
|
+
}
|
|
8150
|
+
};
|
|
7915
8151
|
function isAbortError(err) {
|
|
7916
8152
|
return typeof err === "object" && err !== null && err.name === "AbortError";
|
|
7917
8153
|
}
|
|
7918
8154
|
function pushEventBounded(execInfo, event, cap) {
|
|
7919
|
-
if (
|
|
8155
|
+
if (STREAMING_EXCLUDED_TYPES.has(event.type)) return;
|
|
7920
8156
|
if (cap === Infinity || execInfo.events.length < cap) {
|
|
7921
8157
|
execInfo.events.push(event);
|
|
7922
8158
|
return;
|
|
@@ -7937,6 +8173,36 @@ function pushEventBounded(execInfo, event, cap) {
|
|
|
7937
8173
|
};
|
|
7938
8174
|
}
|
|
7939
8175
|
}
|
|
8176
|
+
function sanitizeMetadataForPersist(metadata) {
|
|
8177
|
+
const cleaned = {};
|
|
8178
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
8179
|
+
try {
|
|
8180
|
+
cleaned[k] = structuredClone(v);
|
|
8181
|
+
} catch {
|
|
8182
|
+
}
|
|
8183
|
+
}
|
|
8184
|
+
return cleaned;
|
|
8185
|
+
}
|
|
8186
|
+
function boundRecoveredEvents(events, cap) {
|
|
8187
|
+
if (cap === Infinity || events.length <= cap) return events;
|
|
8188
|
+
const truncated = events.slice(0, cap);
|
|
8189
|
+
const last = truncated[truncated.length - 1];
|
|
8190
|
+
const alreadyTruncated = last?.type === "log" && typeof last.data === "object" && last.data !== null && last.data.event === "events_truncated";
|
|
8191
|
+
if (!alreadyTruncated) {
|
|
8192
|
+
truncated[truncated.length - 1] = {
|
|
8193
|
+
type: "log",
|
|
8194
|
+
executionId: events[0]?.executionId ?? "",
|
|
8195
|
+
step: last?.step ?? 0,
|
|
8196
|
+
timestamp: Date.now(),
|
|
8197
|
+
data: {
|
|
8198
|
+
event: "events_truncated",
|
|
8199
|
+
cap,
|
|
8200
|
+
message: `Recovered ExecutionInfo.events truncated at ${cap} entries. Original streaming buffer had ${events.length} events. Raise via config.state.maxEventsPerExecution.`
|
|
8201
|
+
}
|
|
8202
|
+
};
|
|
8203
|
+
}
|
|
8204
|
+
return truncated;
|
|
8205
|
+
}
|
|
7940
8206
|
var costScopeStorage = new AsyncLocalStorage2();
|
|
7941
8207
|
function forwardAbortSignal(external, internal) {
|
|
7942
8208
|
if (!external) return () => {
|
|
@@ -7983,6 +8249,30 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
7983
8249
|
* `events` field — keeps the log to one line per row instead of one per
|
|
7984
8250
|
* rebuild tick. */
|
|
7985
8251
|
warnedMalformedExecutions = /* @__PURE__ */ new Set();
|
|
8252
|
+
/** Streaming-mode flusher. `undefined` when persist === 'terminal'
|
|
8253
|
+
* (back-compat default — no buffering overhead, no batching timer). */
|
|
8254
|
+
streamingFlusher;
|
|
8255
|
+
/** Execution IDs that are eligible for streaming-buffer durability —
|
|
8256
|
+
* populated by `execute()` / `stream()`. `createContext()` flows are
|
|
8257
|
+
* deliberately NOT added: ad-hoc contexts (tool tests, Studio playground,
|
|
8258
|
+
* evals) have no terminal `persistExecution` path that would finalize the
|
|
8259
|
+
* streaming buffer, so allowing them to write would leave phantom orphan
|
|
8260
|
+
* buffers that `recoverIncompleteStreams()` later mis-recovers as failed
|
|
8261
|
+
* executions. Membership is removed in `persistExecution` once the
|
|
8262
|
+
* canonical save + finalize chain has been scheduled. */
|
|
8263
|
+
streamableExecutionIds = /* @__PURE__ */ new Set();
|
|
8264
|
+
/** Execution IDs deleted by `runtime.deleteExecution()` while still in
|
|
8265
|
+
* flight. `persistExecution` skips its save + cache write when the
|
|
8266
|
+
* executionId is in this set — preventing a workflow that completes after
|
|
8267
|
+
* a delete from resurrecting the (intentionally-removed) row. Entries are
|
|
8268
|
+
* removed once the would-be persist has been short-circuited. */
|
|
8269
|
+
pendingDeletedExecutions = /* @__PURE__ */ new Set();
|
|
8270
|
+
/** In-flight `persistExecution` chains (save → finalize). `shutdown()`
|
|
8271
|
+
* awaits these before closing the StateStore so a workflow whose abort
|
|
8272
|
+
* triggers a persist-during-shutdown isn't racing a closed connection.
|
|
8273
|
+
* The Set is mutated by the chain's own `.finally` so entries clear as
|
|
8274
|
+
* soon as the work settles. */
|
|
8275
|
+
persistInflight = /* @__PURE__ */ new Set();
|
|
7986
8276
|
constructor(config) {
|
|
7987
8277
|
super();
|
|
7988
8278
|
this.config = resolveConfig(config ?? {});
|
|
@@ -8006,6 +8296,19 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8006
8296
|
embedder: this.config.memory.embedder
|
|
8007
8297
|
});
|
|
8008
8298
|
}
|
|
8299
|
+
if (this.config.state?.persist === "streaming") {
|
|
8300
|
+
if (this.stateStore.appendStreamingEvents) {
|
|
8301
|
+
this.streamingFlusher = new StreamingFlusher(
|
|
8302
|
+
this.stateStore,
|
|
8303
|
+
this.config.state.streamingBatchSize ?? DEFAULT_STREAMING_BATCH_SIZE,
|
|
8304
|
+
this.config.state.streamingBatchInterval ?? DEFAULT_STREAMING_BATCH_INTERVAL
|
|
8305
|
+
);
|
|
8306
|
+
} else {
|
|
8307
|
+
console.warn(
|
|
8308
|
+
`[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.`
|
|
8309
|
+
);
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
8009
8312
|
}
|
|
8010
8313
|
/**
|
|
8011
8314
|
* Whether `config.trace.redact` is enabled on this runtime. A narrow
|
|
@@ -8129,14 +8432,49 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8129
8432
|
/**
|
|
8130
8433
|
* Persist a completed/failed execution to the state store (fire-and-forget)
|
|
8131
8434
|
* and move it from the active executions map to the historical cache.
|
|
8435
|
+
*
|
|
8436
|
+
* Resurrection guard: if `runtime.deleteExecution(id)` was called while
|
|
8437
|
+
* this execution was still running, the id will be in
|
|
8438
|
+
* `pendingDeletedExecutions`. In that case we skip the save AND the
|
|
8439
|
+
* historical-cache write, and finalize the streaming buffer (if any) so
|
|
8440
|
+
* the buffer doesn't outlive the delete.
|
|
8132
8441
|
*/
|
|
8133
8442
|
persistExecution(execInfo) {
|
|
8134
|
-
const
|
|
8443
|
+
const id = execInfo.executionId;
|
|
8444
|
+
this.streamableExecutionIds.delete(id);
|
|
8445
|
+
if (this.pendingDeletedExecutions.has(id)) {
|
|
8446
|
+
this.pendingDeletedExecutions.delete(id);
|
|
8447
|
+
this.executions.delete(id);
|
|
8448
|
+
if (this.streamingFlusher) {
|
|
8449
|
+
this.streamingFlusher.finalize(id).catch(() => {
|
|
8450
|
+
});
|
|
8451
|
+
}
|
|
8452
|
+
return;
|
|
8453
|
+
}
|
|
8454
|
+
let snapshot;
|
|
8455
|
+
try {
|
|
8456
|
+
snapshot = structuredClone(execInfo);
|
|
8457
|
+
} catch {
|
|
8458
|
+
snapshot = {
|
|
8459
|
+
...execInfo,
|
|
8460
|
+
events: [...execInfo.events],
|
|
8461
|
+
metadata: execInfo.metadata ? sanitizeMetadataForPersist(execInfo.metadata) : void 0
|
|
8462
|
+
};
|
|
8463
|
+
}
|
|
8135
8464
|
if (this.stateStore.saveExecution) {
|
|
8136
|
-
this.stateStore.saveExecution(snapshot).
|
|
8465
|
+
const chain = this.stateStore.saveExecution(snapshot).then(() => {
|
|
8466
|
+
return this.streamingFlusher?.finalize(id).catch(() => {
|
|
8467
|
+
});
|
|
8468
|
+
}).catch(() => {
|
|
8469
|
+
}).then(() => void 0);
|
|
8470
|
+
this.persistInflight.add(chain);
|
|
8471
|
+
chain.finally(() => this.persistInflight.delete(chain));
|
|
8472
|
+
} else if (this.streamingFlusher) {
|
|
8473
|
+
const chain = this.streamingFlusher.finalize(id).catch(() => {
|
|
8137
8474
|
});
|
|
8475
|
+
this.persistInflight.add(chain);
|
|
8476
|
+
chain.finally(() => this.persistInflight.delete(chain));
|
|
8138
8477
|
}
|
|
8139
|
-
const id = execInfo.executionId;
|
|
8140
8478
|
this.historicalExecutions.set(id, snapshot);
|
|
8141
8479
|
this.executions.delete(id);
|
|
8142
8480
|
}
|
|
@@ -8376,6 +8714,9 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8376
8714
|
this.abortControllers.set(executionId, controller);
|
|
8377
8715
|
const cleanupAbortForwarder = forwardAbortSignal(options?.signal, controller);
|
|
8378
8716
|
this.registerWithCostScope(executionId);
|
|
8717
|
+
if (this.streamingFlusher) {
|
|
8718
|
+
this.streamableExecutionIds.add(executionId);
|
|
8719
|
+
}
|
|
8379
8720
|
const execInfo = {
|
|
8380
8721
|
executionId,
|
|
8381
8722
|
workflow: name,
|
|
@@ -8383,7 +8724,8 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8383
8724
|
events: [],
|
|
8384
8725
|
totalCost: 0,
|
|
8385
8726
|
startedAt: Date.now(),
|
|
8386
|
-
duration: 0
|
|
8727
|
+
duration: 0,
|
|
8728
|
+
metadata: liftPersistedMetadata(options?.metadata)
|
|
8387
8729
|
};
|
|
8388
8730
|
this.executions.set(executionId, execInfo);
|
|
8389
8731
|
const sessionHistory = options?.metadata?.sessionHistory ?? void 0;
|
|
@@ -8399,6 +8741,9 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8399
8741
|
onTrace: (event) => {
|
|
8400
8742
|
pushEventBounded(execInfo, event, this.maxEventsPerExecution);
|
|
8401
8743
|
execInfo.totalCost += eventCostContribution(event);
|
|
8744
|
+
if (this.streamableExecutionIds.has(executionId)) {
|
|
8745
|
+
this.streamingFlusher?.append(executionId, event);
|
|
8746
|
+
}
|
|
8402
8747
|
this.emit("trace", event);
|
|
8403
8748
|
this.outputAxlEvent(event);
|
|
8404
8749
|
if (event.type === "handoff_start") {
|
|
@@ -8477,6 +8822,9 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8477
8822
|
const validated = workflow2.inputSchema.parse(input);
|
|
8478
8823
|
const sessionHistory = options?.metadata?.sessionHistory ?? void 0;
|
|
8479
8824
|
this.registerWithCostScope(executionId);
|
|
8825
|
+
if (this.streamingFlusher) {
|
|
8826
|
+
this.streamableExecutionIds.add(executionId);
|
|
8827
|
+
}
|
|
8480
8828
|
execInfo = {
|
|
8481
8829
|
executionId,
|
|
8482
8830
|
workflow: name,
|
|
@@ -8484,7 +8832,8 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8484
8832
|
events: [],
|
|
8485
8833
|
totalCost: 0,
|
|
8486
8834
|
startedAt: Date.now(),
|
|
8487
|
-
duration: 0
|
|
8835
|
+
duration: 0,
|
|
8836
|
+
metadata: liftPersistedMetadata(options?.metadata)
|
|
8488
8837
|
};
|
|
8489
8838
|
this.executions.set(executionId, execInfo);
|
|
8490
8839
|
const wfCtx = new WorkflowContext({
|
|
@@ -8505,6 +8854,9 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8505
8854
|
onTrace: (event) => {
|
|
8506
8855
|
pushEventBounded(execInfo, event, this.maxEventsPerExecution);
|
|
8507
8856
|
execInfo.totalCost += eventCostContribution(event);
|
|
8857
|
+
if (this.streamableExecutionIds.has(executionId)) {
|
|
8858
|
+
this.streamingFlusher?.append(executionId, event);
|
|
8859
|
+
}
|
|
8508
8860
|
this.emit("trace", event);
|
|
8509
8861
|
this.outputAxlEvent(event);
|
|
8510
8862
|
axlStream._push(event);
|
|
@@ -8644,6 +8996,15 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8644
8996
|
errors.push(new Error(`${label}: ${err instanceof Error ? err.message : String(err)}`));
|
|
8645
8997
|
}
|
|
8646
8998
|
};
|
|
8999
|
+
if (this.streamingFlusher) {
|
|
9000
|
+
await safeClose("streamingFlusher", () => this.streamingFlusher.flushAll());
|
|
9001
|
+
}
|
|
9002
|
+
if (this.persistInflight.size > 0) {
|
|
9003
|
+
await safeClose(
|
|
9004
|
+
"persistInflight",
|
|
9005
|
+
() => Promise.allSettled([...this.persistInflight]).then(() => void 0)
|
|
9006
|
+
);
|
|
9007
|
+
}
|
|
8647
9008
|
if (this.mcpManager) await safeClose("mcpManager", () => this.mcpManager.shutdown());
|
|
8648
9009
|
if (this.memoryManager) await safeClose("memoryManager", () => this.memoryManager.close());
|
|
8649
9010
|
if (this.stateStore.close) await safeClose("stateStore", () => this.stateStore.close());
|
|
@@ -8654,6 +9015,81 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8654
9015
|
);
|
|
8655
9016
|
}
|
|
8656
9017
|
}
|
|
9018
|
+
/**
|
|
9019
|
+
* Recover executions whose process died mid-run when `state.persist:
|
|
9020
|
+
* 'streaming'` was configured. Synthesizes a partial `ExecutionInfo`
|
|
9021
|
+
* from the streaming buffer for each — `status: 'failed'`, error
|
|
9022
|
+
* `'process terminated'` — registers them in the historical cache so
|
|
9023
|
+
* they show up in `getExecutions()`, persists them via the store's
|
|
9024
|
+
* `saveExecution`, and finalizes the streaming buffer.
|
|
9025
|
+
*
|
|
9026
|
+
* Idempotent — if the streaming buffer is already gone (or
|
|
9027
|
+
* `listStreamingExecutions` is empty), this is a no-op. Returns the
|
|
9028
|
+
* list of recovered `ExecutionInfo`s for caller introspection.
|
|
9029
|
+
*
|
|
9030
|
+
* Wire this into your process startup AFTER `runtime.getExecutions()`
|
|
9031
|
+
* has lazy-loaded the historical cache. Order matters: completed
|
|
9032
|
+
* executions already in the store should not be overwritten by a
|
|
9033
|
+
* stale streaming buffer that the previous process didn't get to
|
|
9034
|
+
* finalize. The implementation checks the historical store before
|
|
9035
|
+
* synthesizing, so a re-run is safe.
|
|
9036
|
+
*/
|
|
9037
|
+
async recoverIncompleteStreams() {
|
|
9038
|
+
if (!this.stateStore.listStreamingExecutions || !this.stateStore.getStreamingEvents) {
|
|
9039
|
+
return [];
|
|
9040
|
+
}
|
|
9041
|
+
const ids = await this.stateStore.listStreamingExecutions();
|
|
9042
|
+
if (ids.length === 0) return [];
|
|
9043
|
+
await this.getExecutions();
|
|
9044
|
+
const recovered = [];
|
|
9045
|
+
for (const id of ids) {
|
|
9046
|
+
if (this.executions.has(id) || this.streamableExecutionIds.has(id)) {
|
|
9047
|
+
continue;
|
|
9048
|
+
}
|
|
9049
|
+
const existing = this.historicalExecutions.get(id);
|
|
9050
|
+
if (existing) {
|
|
9051
|
+
await this.stateStore.finalizeStreamingEvents?.(id);
|
|
9052
|
+
continue;
|
|
9053
|
+
}
|
|
9054
|
+
const events = await this.stateStore.getStreamingEvents(id);
|
|
9055
|
+
if (events.length === 0) {
|
|
9056
|
+
await this.stateStore.finalizeStreamingEvents?.(id);
|
|
9057
|
+
continue;
|
|
9058
|
+
}
|
|
9059
|
+
const boundedEvents = boundRecoveredEvents(events, this.maxEventsPerExecution);
|
|
9060
|
+
const workflowStartEvent = boundedEvents.find((e) => e.type === "workflow_start");
|
|
9061
|
+
const lastEvent = boundedEvents[boundedEvents.length - 1];
|
|
9062
|
+
const startedAt = workflowStartEvent?.timestamp ?? boundedEvents[0]?.timestamp ?? Date.now();
|
|
9063
|
+
const completedAt = lastEvent.timestamp ?? startedAt;
|
|
9064
|
+
const workflowName = workflowStartEvent?.workflow ?? RECOVERED_UNKNOWN_WORKFLOW;
|
|
9065
|
+
const totalCost = boundedEvents.reduce((sum, e) => sum + eventCostContribution(e), 0);
|
|
9066
|
+
const synthesized = {
|
|
9067
|
+
executionId: id,
|
|
9068
|
+
workflow: workflowName,
|
|
9069
|
+
status: "failed",
|
|
9070
|
+
events: boundedEvents,
|
|
9071
|
+
totalCost,
|
|
9072
|
+
startedAt,
|
|
9073
|
+
completedAt,
|
|
9074
|
+
duration: completedAt - startedAt,
|
|
9075
|
+
error: "process terminated (recovered from streaming buffer)"
|
|
9076
|
+
};
|
|
9077
|
+
if (this.stateStore.saveExecution) {
|
|
9078
|
+
try {
|
|
9079
|
+
await this.stateStore.saveExecution(synthesized);
|
|
9080
|
+
} catch (err) {
|
|
9081
|
+
console.error(
|
|
9082
|
+
`[axl] recoverIncompleteStreams: failed to save synthesized execution ${id}: ${err instanceof Error ? err.message : String(err)}. Streaming buffer left in place for next recovery attempt.`
|
|
9083
|
+
);
|
|
9084
|
+
continue;
|
|
9085
|
+
}
|
|
9086
|
+
}
|
|
9087
|
+
this.historicalExecutions.set(id, synthesized);
|
|
9088
|
+
await this.stateStore.finalizeStreamingEvents?.(id);
|
|
9089
|
+
recovered.push(synthesized);
|
|
9090
|
+
}
|
|
9091
|
+
return recovered;
|
|
9092
|
+
}
|
|
8657
9093
|
/** Abort a running execution by its ID. */
|
|
8658
9094
|
abort(executionId) {
|
|
8659
9095
|
const controller = this.abortControllers.get(executionId);
|
|
@@ -8686,15 +9122,85 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8686
9122
|
}
|
|
8687
9123
|
this.emit("eval_result", entry);
|
|
8688
9124
|
}
|
|
9125
|
+
/**
|
|
9126
|
+
* Delete an execution from history by id. Removes from the active map (if
|
|
9127
|
+
* still running), the historical cache, the configured StateStore, AND
|
|
9128
|
+
* any execution-scoped side state — pending awaitHuman decisions,
|
|
9129
|
+
* resolver maps, streaming buffer registration, in-flight abort controller.
|
|
9130
|
+
* Returns true if an entry was actually removed from any of the three
|
|
9131
|
+
* primary surfaces (active map, historical cache, store).
|
|
9132
|
+
*
|
|
9133
|
+
* In-flight handling: if the execution is still running, this method
|
|
9134
|
+
* **aborts** it (via the registered abort controller) AND marks the id for
|
|
9135
|
+
* skip-on-persist so the workflow's eventual `workflow_end` doesn't
|
|
9136
|
+
* resurrect the row. The workflow itself terminates normally — callers
|
|
9137
|
+
* iterating the returned `AxlStream` will see the abort flow through. If
|
|
9138
|
+
* you need a hard hand-off (delete completes before the workflow tears
|
|
9139
|
+
* down), call `runtime.abort(id)` and `await` the stream/execute promise
|
|
9140
|
+
* before calling this. `streamableExecutionIds` is cleared eagerly so
|
|
9141
|
+
* events emitted during the wind-down don't re-create the streaming
|
|
9142
|
+
* buffer in the store after the deleteExecution-issued DEL.
|
|
9143
|
+
*
|
|
9144
|
+
* Audit trail: emits `execution_deleted` on the runtime's `EventEmitter`
|
|
9145
|
+
* with `{ executionId, wasActive, hadPendingDecision }` so compliance
|
|
9146
|
+
* apps can subscribe via `runtime.on('execution_deleted', ...)` for a
|
|
9147
|
+
* "who deleted what when" log without wrapping this method. Fires
|
|
9148
|
+
* regardless of whether the id was actually present.
|
|
9149
|
+
*
|
|
9150
|
+
* Use cases: GDPR right-to-be-forgotten, operator-driven cleanup of
|
|
9151
|
+
* specific runs (e.g. a workflow that recorded PII the user requested
|
|
9152
|
+
* scrubbed). For bulk eviction by age, use a `RedisStore` TTL instead
|
|
9153
|
+
* (`defaultTtl` / `ttls.executionHistory`).
|
|
9154
|
+
*/
|
|
9155
|
+
async deleteExecution(id) {
|
|
9156
|
+
const wasActive = this.executions.has(id);
|
|
9157
|
+
const hadPendingDecision = this.pendingDecisionResolvers.has(id);
|
|
9158
|
+
if (wasActive) {
|
|
9159
|
+
this.pendingDeletedExecutions.add(id);
|
|
9160
|
+
this.streamableExecutionIds.delete(id);
|
|
9161
|
+
const controller = this.abortControllers.get(id);
|
|
9162
|
+
if (controller) {
|
|
9163
|
+
controller.abort();
|
|
9164
|
+
}
|
|
9165
|
+
}
|
|
9166
|
+
if (hadPendingDecision) {
|
|
9167
|
+
this.pendingDecisionResolvers.delete(id);
|
|
9168
|
+
}
|
|
9169
|
+
const existing = await this.getExecution(id);
|
|
9170
|
+
const workflow2 = existing?.workflow;
|
|
9171
|
+
const removedFromActive = this.executions.delete(id);
|
|
9172
|
+
const removedFromHistorical = this.historicalExecutions.delete(id);
|
|
9173
|
+
let removedFromStore = false;
|
|
9174
|
+
if (this.stateStore.deleteExecution) {
|
|
9175
|
+
removedFromStore = await this.stateStore.deleteExecution(id);
|
|
9176
|
+
}
|
|
9177
|
+
this.emit("execution_deleted", {
|
|
9178
|
+
executionId: id,
|
|
9179
|
+
workflow: workflow2,
|
|
9180
|
+
wasActive,
|
|
9181
|
+
hadPendingDecision,
|
|
9182
|
+
removed: removedFromActive || removedFromHistorical || removedFromStore
|
|
9183
|
+
});
|
|
9184
|
+
return removedFromActive || removedFromHistorical || removedFromStore;
|
|
9185
|
+
}
|
|
8689
9186
|
/**
|
|
8690
9187
|
* Delete an eval history entry by id. Removes from in-memory cache and
|
|
8691
9188
|
* the configured StateStore. Returns true if an entry was actually removed.
|
|
8692
9189
|
*
|
|
8693
9190
|
* Ensures lazy-loaded history is loaded first so the in-memory cache and
|
|
8694
9191
|
* the store can't drift apart on the deletion path.
|
|
9192
|
+
*
|
|
9193
|
+
* Emits `eval_deleted` on the runtime's `EventEmitter` with
|
|
9194
|
+
* `{ id, eval, removed }`. Fires on every call — including attempts
|
|
9195
|
+
* against unknown ids (`removed: false`) — symmetric to `execution_deleted`.
|
|
9196
|
+
* Studio's eval-trends aggregator subscribes to this event so deleted
|
|
9197
|
+
* runs don't linger in the trends panel for up to 5 minutes (until the
|
|
9198
|
+
* next periodic rebuild).
|
|
8695
9199
|
*/
|
|
8696
9200
|
async deleteEvalResult(id) {
|
|
8697
9201
|
await this.getEvalHistory();
|
|
9202
|
+
const existing = this.evalHistory.find((e) => e.id === id);
|
|
9203
|
+
const evalName = existing?.eval;
|
|
8698
9204
|
const beforeLength = this.evalHistory.length;
|
|
8699
9205
|
this.evalHistory = this.evalHistory.filter((e) => e.id !== id);
|
|
8700
9206
|
const removedFromMemory = this.evalHistory.length < beforeLength;
|
|
@@ -8702,7 +9208,13 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
8702
9208
|
if (this.stateStore.deleteEvalResult) {
|
|
8703
9209
|
removedFromStore = await this.stateStore.deleteEvalResult(id);
|
|
8704
9210
|
}
|
|
8705
|
-
|
|
9211
|
+
const removed = removedFromMemory || removedFromStore;
|
|
9212
|
+
this.emit("eval_deleted", {
|
|
9213
|
+
id,
|
|
9214
|
+
eval: evalName,
|
|
9215
|
+
removed
|
|
9216
|
+
});
|
|
9217
|
+
return removed;
|
|
8706
9218
|
}
|
|
8707
9219
|
/** Get eval result history (most recent first). */
|
|
8708
9220
|
async getEvalHistory() {
|
|
@@ -9292,15 +9804,53 @@ async function* stringStreamFromEvents(source, opts) {
|
|
|
9292
9804
|
|
|
9293
9805
|
// src/state/redis.ts
|
|
9294
9806
|
var RedisStore = class _RedisStore {
|
|
9295
|
-
constructor(client) {
|
|
9807
|
+
constructor(client, keyPrefix, ttls) {
|
|
9296
9808
|
this.client = client;
|
|
9809
|
+
this.keyPrefix = keyPrefix;
|
|
9810
|
+
this.ttls = ttls;
|
|
9811
|
+
}
|
|
9812
|
+
/**
|
|
9813
|
+
* Resolve the TTL for a given storage category. Returns `undefined` when
|
|
9814
|
+
* no TTL applies (category is `null` in resolved config, the value is
|
|
9815
|
+
* non-positive — Redis EXPIRE 0 immediately deletes, which is almost
|
|
9816
|
+
* never intended — or the value is non-finite).
|
|
9817
|
+
*
|
|
9818
|
+
* Non-finite values (`NaN`, `±Infinity`) defensively map to `undefined`
|
|
9819
|
+
* rather than being passed to Redis, where they'd surface as a wire-level
|
|
9820
|
+
* protocol error at the worst possible time (first write under load).
|
|
9821
|
+
*/
|
|
9822
|
+
ttlFor(category) {
|
|
9823
|
+
const resolved = this.ttls[category];
|
|
9824
|
+
if (resolved === null) return void 0;
|
|
9825
|
+
if (!Number.isFinite(resolved) || resolved <= 0) return void 0;
|
|
9826
|
+
return Math.floor(resolved);
|
|
9297
9827
|
}
|
|
9298
9828
|
/**
|
|
9299
9829
|
* Create a connected RedisStore instance.
|
|
9300
9830
|
*
|
|
9301
|
-
*
|
|
9831
|
+
* Accepts either a URL string (back-compat) or a {@link RedisStoreOptions}
|
|
9832
|
+
* object. The object form is required to set a custom `keyPrefix`.
|
|
9833
|
+
*
|
|
9834
|
+
* @example
|
|
9835
|
+
* ```ts
|
|
9836
|
+
* // Default prefix 'axl:'
|
|
9837
|
+
* const store = await RedisStore.create('redis://localhost:6379');
|
|
9838
|
+
*
|
|
9839
|
+
* // Custom prefix for shared-cluster deployments
|
|
9840
|
+
* const store = await RedisStore.create({
|
|
9841
|
+
* url: 'redis://localhost:6379',
|
|
9842
|
+
* keyPrefix: 'axl:prod:',
|
|
9843
|
+
* });
|
|
9844
|
+
* ```
|
|
9302
9845
|
*/
|
|
9303
|
-
static async create(
|
|
9846
|
+
static async create(options) {
|
|
9847
|
+
const opts = typeof options === "string" ? { url: options } : options ?? {};
|
|
9848
|
+
const keyPrefix = opts.keyPrefix ?? "axl:";
|
|
9849
|
+
if (keyPrefix === "") {
|
|
9850
|
+
throw new Error(
|
|
9851
|
+
'RedisStore: keyPrefix cannot be empty string. Omit it for the default "axl:" prefix.'
|
|
9852
|
+
);
|
|
9853
|
+
}
|
|
9304
9854
|
let createClient;
|
|
9305
9855
|
try {
|
|
9306
9856
|
const mod = __require("redis");
|
|
@@ -9314,44 +9864,197 @@ var RedisStore = class _RedisStore {
|
|
|
9314
9864
|
if (err instanceof Error && err.message.includes("createClient")) throw err;
|
|
9315
9865
|
throw new Error("redis is required for RedisStore. Install it with: npm install redis");
|
|
9316
9866
|
}
|
|
9317
|
-
const client = url ? createClient({ url }) : createClient();
|
|
9867
|
+
const client = opts.url ? createClient({ url: opts.url }) : createClient();
|
|
9318
9868
|
await client.connect();
|
|
9319
|
-
|
|
9869
|
+
const resolveCategory = (override) => {
|
|
9870
|
+
if (override === null) return null;
|
|
9871
|
+
if (typeof override === "number") return override;
|
|
9872
|
+
return typeof opts.defaultTtl === "number" ? opts.defaultTtl : null;
|
|
9873
|
+
};
|
|
9874
|
+
const resolveStreamingEvents = (override) => {
|
|
9875
|
+
if (override === null || override === void 0) return null;
|
|
9876
|
+
if (typeof override === "number") return override;
|
|
9877
|
+
return null;
|
|
9878
|
+
};
|
|
9879
|
+
const effectiveTtls = {
|
|
9880
|
+
session: resolveCategory(opts.ttls?.session),
|
|
9881
|
+
sessionMeta: resolveCategory(opts.ttls?.sessionMeta),
|
|
9882
|
+
checkpoint: resolveCategory(opts.ttls?.checkpoint),
|
|
9883
|
+
executionState: resolveCategory(opts.ttls?.executionState),
|
|
9884
|
+
executionHistory: resolveCategory(opts.ttls?.executionHistory),
|
|
9885
|
+
evalHistory: resolveCategory(opts.ttls?.evalHistory),
|
|
9886
|
+
memory: resolveCategory(opts.ttls?.memory),
|
|
9887
|
+
streamingEvents: resolveStreamingEvents(opts.ttls?.streamingEvents)
|
|
9888
|
+
};
|
|
9889
|
+
const store = new _RedisStore(client, keyPrefix, effectiveTtls);
|
|
9890
|
+
if (!opts.skipMigration) {
|
|
9891
|
+
await store.backfillExecutionIndex();
|
|
9892
|
+
await store.backfillEvalIndex();
|
|
9893
|
+
}
|
|
9894
|
+
return store;
|
|
9895
|
+
}
|
|
9896
|
+
/**
|
|
9897
|
+
* Backfill the execution-history sorted-set from the legacy ID set.
|
|
9898
|
+
* Idempotent — runs only if the ZSET is missing entries the SET has.
|
|
9899
|
+
* Safe to call manually if you constructed the store with
|
|
9900
|
+
* `skipMigration: true`.
|
|
9901
|
+
*/
|
|
9902
|
+
async backfillExecutionIndex() {
|
|
9903
|
+
await this.backfillHistoryIndex(
|
|
9904
|
+
this.execHistorySetKey(),
|
|
9905
|
+
this.execHistoryZsetKey(),
|
|
9906
|
+
(id) => this.execHistoryKey(id),
|
|
9907
|
+
(raw) => {
|
|
9908
|
+
const exec = JSON.parse(raw);
|
|
9909
|
+
return typeof exec.startedAt === "number" ? { score: exec.startedAt, value: exec.executionId } : null;
|
|
9910
|
+
},
|
|
9911
|
+
"execution-history"
|
|
9912
|
+
);
|
|
9913
|
+
}
|
|
9914
|
+
/** Backfill the eval-history sorted-set. Symmetric to `backfillExecutionIndex`. */
|
|
9915
|
+
async backfillEvalIndex() {
|
|
9916
|
+
await this.backfillHistoryIndex(
|
|
9917
|
+
this.evalHistorySetKey(),
|
|
9918
|
+
this.evalHistoryZsetKey(),
|
|
9919
|
+
(id) => this.evalHistoryKey(id),
|
|
9920
|
+
(raw) => {
|
|
9921
|
+
const entry = JSON.parse(raw);
|
|
9922
|
+
return typeof entry.timestamp === "number" ? { score: entry.timestamp, value: entry.id } : null;
|
|
9923
|
+
},
|
|
9924
|
+
"eval-history"
|
|
9925
|
+
);
|
|
9926
|
+
}
|
|
9927
|
+
/**
|
|
9928
|
+
* Shared implementation for both backfill paths. Reads SET members,
|
|
9929
|
+
* batch-MGETs their data blobs in chunks, and ZADDs each into the new
|
|
9930
|
+
* sorted-set inside a single MULTI per chunk.
|
|
9931
|
+
*/
|
|
9932
|
+
async backfillHistoryIndex(setKey, zsetKey, dataKey, scoreOf, label) {
|
|
9933
|
+
const [setSize, zsetSize] = await Promise.all([
|
|
9934
|
+
this.client.sCard(setKey),
|
|
9935
|
+
this.client.zCard(zsetKey)
|
|
9936
|
+
]);
|
|
9937
|
+
if (zsetSize >= setSize) return;
|
|
9938
|
+
const allIds = await this.client.sMembers(setKey);
|
|
9939
|
+
if (allIds.length === 0) return;
|
|
9940
|
+
const CHUNK_SIZE = 500;
|
|
9941
|
+
let migrated = 0;
|
|
9942
|
+
let skipped = 0;
|
|
9943
|
+
for (let i = 0; i < allIds.length; i += CHUNK_SIZE) {
|
|
9944
|
+
const chunk = allIds.slice(i, i + CHUNK_SIZE);
|
|
9945
|
+
const dataKeys = chunk.map((id) => dataKey(id));
|
|
9946
|
+
const raws = await this.client.mGet(dataKeys);
|
|
9947
|
+
const tx = this.client.multi();
|
|
9948
|
+
let queuedInChunk = 0;
|
|
9949
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
9950
|
+
const raw = raws[j];
|
|
9951
|
+
if (raw == null) {
|
|
9952
|
+
skipped++;
|
|
9953
|
+
continue;
|
|
9954
|
+
}
|
|
9955
|
+
try {
|
|
9956
|
+
const entry = scoreOf(raw);
|
|
9957
|
+
if (entry == null) {
|
|
9958
|
+
skipped++;
|
|
9959
|
+
continue;
|
|
9960
|
+
}
|
|
9961
|
+
tx.zAdd(zsetKey, entry);
|
|
9962
|
+
queuedInChunk++;
|
|
9963
|
+
migrated++;
|
|
9964
|
+
} catch {
|
|
9965
|
+
skipped++;
|
|
9966
|
+
}
|
|
9967
|
+
}
|
|
9968
|
+
if (queuedInChunk > 0) await tx.exec();
|
|
9969
|
+
}
|
|
9970
|
+
if (migrated > 0 || skipped > 0) {
|
|
9971
|
+
console.log(
|
|
9972
|
+
`[axl] RedisStore: backfilled ${migrated} ${label} entries into sorted-set index` + (skipped > 0 ? ` (${skipped} skipped \u2014 missing or malformed)` : "")
|
|
9973
|
+
);
|
|
9974
|
+
}
|
|
9320
9975
|
}
|
|
9321
9976
|
// ── Key helpers ──────────────────────────────────────────────────────
|
|
9322
9977
|
checkpointKey(executionId) {
|
|
9323
|
-
return
|
|
9978
|
+
return `${this.keyPrefix}checkpoint:${executionId}`;
|
|
9324
9979
|
}
|
|
9325
9980
|
sessionKey(sessionId) {
|
|
9326
|
-
return
|
|
9981
|
+
return `${this.keyPrefix}session:${sessionId}`;
|
|
9327
9982
|
}
|
|
9328
9983
|
sessionMetaKey(sessionId) {
|
|
9329
|
-
return
|
|
9984
|
+
return `${this.keyPrefix}session-meta:${sessionId}`;
|
|
9985
|
+
}
|
|
9986
|
+
sessionIdsKey() {
|
|
9987
|
+
return `${this.keyPrefix}session-ids`;
|
|
9330
9988
|
}
|
|
9331
9989
|
decisionsKey() {
|
|
9332
|
-
return
|
|
9990
|
+
return `${this.keyPrefix}decisions`;
|
|
9333
9991
|
}
|
|
9334
9992
|
executionStateKey(executionId) {
|
|
9335
|
-
return
|
|
9993
|
+
return `${this.keyPrefix}exec-state:${executionId}`;
|
|
9336
9994
|
}
|
|
9337
9995
|
pendingExecSetKey() {
|
|
9338
|
-
return
|
|
9996
|
+
return `${this.keyPrefix}pending-executions`;
|
|
9339
9997
|
}
|
|
9340
9998
|
execHistoryKey(executionId) {
|
|
9341
|
-
return
|
|
9342
|
-
}
|
|
9999
|
+
return `${this.keyPrefix}exec-history:${executionId}`;
|
|
10000
|
+
}
|
|
10001
|
+
// NOTE for future implementer of #3 (TTL config): when the executionHistory
|
|
10002
|
+
// and evalHistory data blobs gain TTLs, eviction MUST be driven by
|
|
10003
|
+
// ZREMRANGEBYSCORE on the ZSET below — NOT by SREM on these legacy ID sets.
|
|
10004
|
+
// The slow-path fallback (`listHistoryByZset`) reads from the SET when
|
|
10005
|
+
// `zsetCard < setCard`, so SET-based eviction would race with the fallback
|
|
10006
|
+
// and surface dead IDs whose data has been TTL'd away. ZSET eviction +
|
|
10007
|
+
// dual-write `SREM` is the clean path; doc the assumption then.
|
|
9343
10008
|
execHistorySetKey() {
|
|
9344
|
-
return
|
|
10009
|
+
return `${this.keyPrefix}exec-history-ids`;
|
|
10010
|
+
}
|
|
10011
|
+
/**
|
|
10012
|
+
* Sorted-set index for execution history, scored by `startedAt`. Drives
|
|
10013
|
+
* the O(log N) `listExecutions` fast path; the legacy SET above is kept
|
|
10014
|
+
* as a fallback for users running with `skipMigration: true` before
|
|
10015
|
+
* they've called `backfillExecutionIndex()` manually.
|
|
10016
|
+
*/
|
|
10017
|
+
execHistoryZsetKey() {
|
|
10018
|
+
return `${this.keyPrefix}exec-history-z`;
|
|
9345
10019
|
}
|
|
9346
10020
|
evalHistoryKey(id) {
|
|
9347
|
-
return
|
|
10021
|
+
return `${this.keyPrefix}eval-history:${id}`;
|
|
9348
10022
|
}
|
|
9349
10023
|
evalHistorySetKey() {
|
|
9350
|
-
return
|
|
10024
|
+
return `${this.keyPrefix}eval-history-ids`;
|
|
10025
|
+
}
|
|
10026
|
+
/** Sorted-set index for eval history, scored by `timestamp`. See above. */
|
|
10027
|
+
evalHistoryZsetKey() {
|
|
10028
|
+
return `${this.keyPrefix}eval-history-z`;
|
|
10029
|
+
}
|
|
10030
|
+
memoryKey(scope) {
|
|
10031
|
+
return `${this.keyPrefix}memory:${scope}`;
|
|
10032
|
+
}
|
|
10033
|
+
// ── Streaming event buffer (for `state.persist: 'streaming'`) ─────────
|
|
10034
|
+
// Layout:
|
|
10035
|
+
// `{prefix}exec-events:{id}` — a Redis list. Events are RPUSH'd as
|
|
10036
|
+
// JSON-serialized AxlEvent objects, in batches. The runtime LRANGEs
|
|
10037
|
+
// the whole list on recovery and deletes it via `finalizeStreamingEvents`
|
|
10038
|
+
// once the canonical `executionHistory` blob has been written at
|
|
10039
|
+
// terminal exit.
|
|
10040
|
+
// `{prefix}streaming-exec-ids` — a Redis set tracking which executions
|
|
10041
|
+
// have an active streaming buffer. Used by `listStreamingExecutions`
|
|
10042
|
+
// to find runs whose process died mid-flight (no corresponding
|
|
10043
|
+
// `executionHistory` blob).
|
|
10044
|
+
streamingEventsKey(executionId) {
|
|
10045
|
+
return `${this.keyPrefix}exec-events:${executionId}`;
|
|
10046
|
+
}
|
|
10047
|
+
streamingIdsKey() {
|
|
10048
|
+
return `${this.keyPrefix}streaming-exec-ids`;
|
|
9351
10049
|
}
|
|
9352
10050
|
// ── Checkpoints ──────────────────────────────────────────────────────
|
|
9353
10051
|
async saveCheckpoint(executionId, name, data) {
|
|
9354
|
-
|
|
10052
|
+
const ttl = this.ttlFor("checkpoint");
|
|
10053
|
+
if (ttl === void 0) {
|
|
10054
|
+
await this.client.hSet(this.checkpointKey(executionId), name, JSON.stringify(data));
|
|
10055
|
+
return;
|
|
10056
|
+
}
|
|
10057
|
+
await this.client.multi().hSet(this.checkpointKey(executionId), name, JSON.stringify(data)).expire(this.checkpointKey(executionId), ttl, "NX").exec();
|
|
9355
10058
|
}
|
|
9356
10059
|
async getCheckpoint(executionId, name) {
|
|
9357
10060
|
const raw = await this.client.hGet(this.checkpointKey(executionId), name);
|
|
@@ -9359,20 +10062,24 @@ var RedisStore = class _RedisStore {
|
|
|
9359
10062
|
}
|
|
9360
10063
|
// ── Sessions ─────────────────────────────────────────────────────────
|
|
9361
10064
|
async saveSession(sessionId, history) {
|
|
9362
|
-
|
|
9363
|
-
|
|
10065
|
+
const ttl = this.ttlFor("session");
|
|
10066
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10067
|
+
await this.client.multi().set(this.sessionKey(sessionId), JSON.stringify(history), setOptions).sAdd(this.sessionIdsKey(), sessionId).exec();
|
|
9364
10068
|
}
|
|
9365
10069
|
async getSession(sessionId) {
|
|
9366
10070
|
const raw = await this.client.get(this.sessionKey(sessionId));
|
|
9367
10071
|
return raw ? JSON.parse(raw) : [];
|
|
9368
10072
|
}
|
|
9369
10073
|
async deleteSession(sessionId) {
|
|
9370
|
-
await this.client.del(this.sessionKey(sessionId));
|
|
9371
|
-
await this.client.del(this.sessionMetaKey(sessionId));
|
|
9372
|
-
await this.client.sRem("axl:session-ids", sessionId);
|
|
10074
|
+
await this.client.multi().del(this.sessionKey(sessionId)).del(this.sessionMetaKey(sessionId)).sRem(this.sessionIdsKey(), sessionId).exec();
|
|
9373
10075
|
}
|
|
9374
10076
|
async saveSessionMeta(sessionId, key, value) {
|
|
9375
|
-
|
|
10077
|
+
const ttl = this.ttlFor("sessionMeta");
|
|
10078
|
+
if (ttl === void 0) {
|
|
10079
|
+
await this.client.hSet(this.sessionMetaKey(sessionId), key, JSON.stringify(value));
|
|
10080
|
+
return;
|
|
10081
|
+
}
|
|
10082
|
+
await this.client.multi().hSet(this.sessionMetaKey(sessionId), key, JSON.stringify(value)).expire(this.sessionMetaKey(sessionId), ttl).exec();
|
|
9376
10083
|
}
|
|
9377
10084
|
async getSessionMeta(sessionId, key) {
|
|
9378
10085
|
const raw = await this.client.hGet(this.sessionMetaKey(sessionId), key);
|
|
@@ -9392,64 +10099,222 @@ var RedisStore = class _RedisStore {
|
|
|
9392
10099
|
}
|
|
9393
10100
|
// ── Execution State ──────────────────────────────────────────────────
|
|
9394
10101
|
async saveExecutionState(executionId, state) {
|
|
9395
|
-
|
|
10102
|
+
const ttl = this.ttlFor("executionState");
|
|
10103
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10104
|
+
const tx = this.client.multi().set(this.executionStateKey(executionId), JSON.stringify(state), setOptions);
|
|
9396
10105
|
if (state.status === "waiting") {
|
|
9397
|
-
|
|
10106
|
+
tx.sAdd(this.pendingExecSetKey(), executionId);
|
|
9398
10107
|
} else {
|
|
9399
|
-
|
|
10108
|
+
tx.sRem(this.pendingExecSetKey(), executionId);
|
|
9400
10109
|
}
|
|
10110
|
+
await tx.exec();
|
|
9401
10111
|
}
|
|
9402
10112
|
async getExecutionState(executionId) {
|
|
9403
10113
|
const raw = await this.client.get(this.executionStateKey(executionId));
|
|
9404
10114
|
return raw ? JSON.parse(raw) : null;
|
|
9405
10115
|
}
|
|
9406
10116
|
async listPendingExecutions() {
|
|
9407
|
-
|
|
10117
|
+
const ids = await this.client.sMembers(this.pendingExecSetKey());
|
|
10118
|
+
if (ids.length === 0) return ids;
|
|
10119
|
+
const keys = ids.map((id) => this.executionStateKey(id));
|
|
10120
|
+
const raws = await this.client.mGet(keys);
|
|
10121
|
+
const live = [];
|
|
10122
|
+
const dead = [];
|
|
10123
|
+
for (let i = 0; i < ids.length; i++) {
|
|
10124
|
+
if (raws[i] != null) {
|
|
10125
|
+
live.push(ids[i]);
|
|
10126
|
+
} else {
|
|
10127
|
+
dead.push(ids[i]);
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
10130
|
+
if (dead.length > 0) {
|
|
10131
|
+
this.client.sRem(this.pendingExecSetKey(), dead).catch((err) => {
|
|
10132
|
+
console.error(
|
|
10133
|
+
`[axl] RedisStore: failed to prune ${dead.length} stale pending-exec ids: ` + (err instanceof Error ? err.message : String(err))
|
|
10134
|
+
);
|
|
10135
|
+
});
|
|
10136
|
+
}
|
|
10137
|
+
return live;
|
|
10138
|
+
}
|
|
10139
|
+
// ── Memory ──────────────────────────────────────────────────────────
|
|
10140
|
+
//
|
|
10141
|
+
// Layout: `{prefix}memory:{scope}` is a hash. Fields are user-supplied
|
|
10142
|
+
// keys; values are JSON-serialized. Hash-per-scope keeps related entries
|
|
10143
|
+
// together (good cohesion for TTL eviction once #3 lands) and bounds the
|
|
10144
|
+
// keyspace to O(scopes) instead of O(scopes × keys).
|
|
10145
|
+
//
|
|
10146
|
+
// Pre-this-patch, RedisStore had no memory methods, so MemoryManager fell
|
|
10147
|
+
// back to writing memory at synthetic sessionMeta keys
|
|
10148
|
+
// (`{prefix}session-meta:memory:{scope}:{key}` hash field `value`). To
|
|
10149
|
+
// preserve user data on upgrade, `getMemory` checks the legacy location on
|
|
10150
|
+
// miss and migrates forward; `deleteMemory` also cleans the legacy entry.
|
|
10151
|
+
// No legacy support on `getAllMemory` — that method didn't exist before,
|
|
10152
|
+
// so no user can depend on it returning legacy data.
|
|
10153
|
+
async saveMemory(scope, key, value) {
|
|
10154
|
+
const ttl = this.ttlFor("memory");
|
|
10155
|
+
if (ttl === void 0) {
|
|
10156
|
+
await this.client.hSet(this.memoryKey(scope), key, JSON.stringify(value));
|
|
10157
|
+
return;
|
|
10158
|
+
}
|
|
10159
|
+
await this.client.multi().hSet(this.memoryKey(scope), key, JSON.stringify(value)).expire(this.memoryKey(scope), ttl).exec();
|
|
10160
|
+
}
|
|
10161
|
+
async getMemory(scope, key) {
|
|
10162
|
+
const raw = await this.client.hGet(this.memoryKey(scope), key);
|
|
10163
|
+
if (raw != null) return JSON.parse(raw);
|
|
10164
|
+
const legacyRaw = await this.client.hGet(
|
|
10165
|
+
this.sessionMetaKey(`memory:${scope}:${key}`),
|
|
10166
|
+
"value"
|
|
10167
|
+
);
|
|
10168
|
+
if (legacyRaw == null) return null;
|
|
10169
|
+
const inserted = await this.client.hSetNX(this.memoryKey(scope), key, legacyRaw);
|
|
10170
|
+
if (inserted === 0 || inserted === false) {
|
|
10171
|
+
const winner = await this.client.hGet(this.memoryKey(scope), key);
|
|
10172
|
+
if (winner != null) return JSON.parse(winner);
|
|
10173
|
+
return null;
|
|
10174
|
+
}
|
|
10175
|
+
const ttl = this.ttlFor("memory");
|
|
10176
|
+
if (ttl !== void 0) {
|
|
10177
|
+
this.client.expire(this.memoryKey(scope), ttl).catch((err) => {
|
|
10178
|
+
console.error(
|
|
10179
|
+
`[axl] RedisStore: failed to set TTL on migrated memory key ${this.memoryKey(scope)}: ` + (err instanceof Error ? err.message : String(err))
|
|
10180
|
+
);
|
|
10181
|
+
});
|
|
10182
|
+
}
|
|
10183
|
+
return JSON.parse(legacyRaw);
|
|
10184
|
+
}
|
|
10185
|
+
async getAllMemory(scope) {
|
|
10186
|
+
const all = await this.client.hGetAll(this.memoryKey(scope));
|
|
10187
|
+
if (!all) return [];
|
|
10188
|
+
return Object.entries(all).map(([key, raw]) => ({ key, value: JSON.parse(raw) }));
|
|
10189
|
+
}
|
|
10190
|
+
async deleteMemory(scope, key) {
|
|
10191
|
+
await this.client.multi().hDel(this.memoryKey(scope), key).hDel(this.sessionMetaKey(`memory:${scope}:${key}`), "value").exec();
|
|
9408
10192
|
}
|
|
9409
10193
|
// ── Execution History ────────────────────────────────────────────────
|
|
9410
10194
|
async saveExecution(execution) {
|
|
9411
|
-
|
|
9412
|
-
|
|
10195
|
+
const ttl = this.ttlFor("executionHistory");
|
|
10196
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10197
|
+
await this.client.multi().sAdd(this.execHistorySetKey(), execution.executionId).set(this.execHistoryKey(execution.executionId), JSON.stringify(execution), setOptions).zAdd(this.execHistoryZsetKey(), {
|
|
10198
|
+
score: execution.startedAt,
|
|
10199
|
+
value: execution.executionId
|
|
10200
|
+
}).exec();
|
|
9413
10201
|
}
|
|
9414
10202
|
async getExecution(executionId) {
|
|
9415
10203
|
const raw = await this.client.get(this.execHistoryKey(executionId));
|
|
9416
10204
|
return raw ? JSON.parse(raw) : null;
|
|
9417
10205
|
}
|
|
9418
10206
|
async listExecutions(limit) {
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
10207
|
+
return this.listHistoryByZset(
|
|
10208
|
+
this.execHistorySetKey(),
|
|
10209
|
+
this.execHistoryZsetKey(),
|
|
10210
|
+
(id) => this.execHistoryKey(id),
|
|
10211
|
+
(a, b) => b.startedAt - a.startedAt,
|
|
10212
|
+
limit
|
|
10213
|
+
);
|
|
10214
|
+
}
|
|
10215
|
+
async deleteExecution(executionId) {
|
|
10216
|
+
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();
|
|
10217
|
+
const deleted = typeof deletedCount === "number" ? deletedCount : 0;
|
|
10218
|
+
return deleted > 0;
|
|
9428
10219
|
}
|
|
9429
10220
|
// ── Eval History ────────────────────────────────────────────────────
|
|
9430
10221
|
async saveEvalResult(entry) {
|
|
9431
|
-
|
|
9432
|
-
|
|
10222
|
+
const ttl = this.ttlFor("evalHistory");
|
|
10223
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10224
|
+
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();
|
|
9433
10225
|
}
|
|
9434
10226
|
async listEvalResults(limit) {
|
|
9435
|
-
|
|
10227
|
+
return this.listHistoryByZset(
|
|
10228
|
+
this.evalHistorySetKey(),
|
|
10229
|
+
this.evalHistoryZsetKey(),
|
|
10230
|
+
(id) => this.evalHistoryKey(id),
|
|
10231
|
+
(a, b) => b.timestamp - a.timestamp,
|
|
10232
|
+
limit
|
|
10233
|
+
);
|
|
10234
|
+
}
|
|
10235
|
+
async deleteEvalResult(id) {
|
|
10236
|
+
const [, deletedCount] = await this.client.multi().sRem(this.evalHistorySetKey(), id).del(this.evalHistoryKey(id)).zRem(this.evalHistoryZsetKey(), id).exec();
|
|
10237
|
+
const deleted = typeof deletedCount === "number" ? deletedCount : 0;
|
|
10238
|
+
return deleted > 0;
|
|
10239
|
+
}
|
|
10240
|
+
/**
|
|
10241
|
+
* Shared list-by-sorted-set implementation. Uses ZREVRANGEBYSCORE +
|
|
10242
|
+
* MGET when the ZSET is populated (post-migration). Falls back to the
|
|
10243
|
+
* legacy SET + N×GET + JS-sort path when the ZSET is empty or smaller
|
|
10244
|
+
* than the SET (covers users that constructed with `skipMigration: true`
|
|
10245
|
+
* before calling backfill).
|
|
10246
|
+
*/
|
|
10247
|
+
async listHistoryByZset(setKey, zsetKey, dataKey, sortCmp, limit) {
|
|
10248
|
+
const setSize = await this.client.sCard(setKey);
|
|
10249
|
+
if (setSize === 0) return [];
|
|
10250
|
+
const zsetSize = await this.client.zCard(zsetKey);
|
|
10251
|
+
if (zsetSize >= setSize) {
|
|
10252
|
+
const fetchCount = limit !== void 0 ? Math.max(limit * 2, limit + 5) : void 0;
|
|
10253
|
+
const ids2 = await this.client.zRange(zsetKey, "+inf", "-inf", {
|
|
10254
|
+
BY: "SCORE",
|
|
10255
|
+
REV: true,
|
|
10256
|
+
...fetchCount !== void 0 ? { LIMIT: { offset: 0, count: fetchCount } } : {}
|
|
10257
|
+
});
|
|
10258
|
+
if (ids2.length === 0) return [];
|
|
10259
|
+
const keys = ids2.map(dataKey);
|
|
10260
|
+
const raws = await this.client.mGet(keys);
|
|
10261
|
+
const entries2 = [];
|
|
10262
|
+
for (const raw of raws) {
|
|
10263
|
+
if (raw == null) continue;
|
|
10264
|
+
try {
|
|
10265
|
+
entries2.push(JSON.parse(raw));
|
|
10266
|
+
} catch {
|
|
10267
|
+
}
|
|
10268
|
+
if (limit !== void 0 && entries2.length >= limit) break;
|
|
10269
|
+
}
|
|
10270
|
+
return entries2;
|
|
10271
|
+
}
|
|
10272
|
+
const ids = await this.client.sMembers(setKey);
|
|
9436
10273
|
if (ids.length === 0) return [];
|
|
9437
10274
|
const entries = [];
|
|
9438
10275
|
for (const id of ids) {
|
|
9439
|
-
const raw = await this.client.get(
|
|
9440
|
-
if (raw)
|
|
10276
|
+
const raw = await this.client.get(dataKey(id));
|
|
10277
|
+
if (raw == null) continue;
|
|
10278
|
+
try {
|
|
10279
|
+
entries.push(JSON.parse(raw));
|
|
10280
|
+
} catch {
|
|
10281
|
+
}
|
|
9441
10282
|
}
|
|
9442
|
-
entries.sort(
|
|
10283
|
+
entries.sort(sortCmp);
|
|
9443
10284
|
return limit ? entries.slice(0, limit) : entries;
|
|
9444
10285
|
}
|
|
9445
|
-
async deleteEvalResult(id) {
|
|
9446
|
-
await this.client.sRem(this.evalHistorySetKey(), id);
|
|
9447
|
-
const deleted = await this.client.del(this.evalHistoryKey(id));
|
|
9448
|
-
return deleted > 0;
|
|
9449
|
-
}
|
|
9450
10286
|
// ── Sessions (Studio introspection) ────────────────────────────────────
|
|
9451
10287
|
async listSessions() {
|
|
9452
|
-
return this.client.sMembers(
|
|
10288
|
+
return this.client.sMembers(this.sessionIdsKey());
|
|
10289
|
+
}
|
|
10290
|
+
// ── Streaming events (for state.persist: 'streaming') ──────────────────
|
|
10291
|
+
async appendStreamingEvents(executionId, events) {
|
|
10292
|
+
if (events.length === 0) return;
|
|
10293
|
+
const serialized = events.map((e) => JSON.stringify(e));
|
|
10294
|
+
const ttl = this.ttlFor("streamingEvents");
|
|
10295
|
+
const tx = this.client.multi().rPush(this.streamingEventsKey(executionId), serialized).sAdd(this.streamingIdsKey(), executionId);
|
|
10296
|
+
if (ttl !== void 0) {
|
|
10297
|
+
tx.expire(this.streamingEventsKey(executionId), ttl, "NX");
|
|
10298
|
+
}
|
|
10299
|
+
await tx.exec();
|
|
10300
|
+
}
|
|
10301
|
+
async finalizeStreamingEvents(executionId) {
|
|
10302
|
+
await this.client.multi().del(this.streamingEventsKey(executionId)).sRem(this.streamingIdsKey(), executionId).exec();
|
|
10303
|
+
}
|
|
10304
|
+
async listStreamingExecutions() {
|
|
10305
|
+
return this.client.sMembers(this.streamingIdsKey());
|
|
10306
|
+
}
|
|
10307
|
+
async getStreamingEvents(executionId) {
|
|
10308
|
+
const raws = await this.client.lRange(this.streamingEventsKey(executionId), 0, -1);
|
|
10309
|
+
if (raws.length === 0) return [];
|
|
10310
|
+
const events = [];
|
|
10311
|
+
for (const raw of raws) {
|
|
10312
|
+
try {
|
|
10313
|
+
events.push(JSON.parse(raw));
|
|
10314
|
+
} catch {
|
|
10315
|
+
}
|
|
10316
|
+
}
|
|
10317
|
+
return events;
|
|
9453
10318
|
}
|
|
9454
10319
|
/** Close the Redis connection. */
|
|
9455
10320
|
async close() {
|