@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.cjs
CHANGED
|
@@ -1007,6 +1007,7 @@ var OpenAIResponsesProvider = class {
|
|
|
1007
1007
|
// src/providers/anthropic.ts
|
|
1008
1008
|
var ANTHROPIC_API_VERSION = "2023-06-01";
|
|
1009
1009
|
var ANTHROPIC_PRICING = {
|
|
1010
|
+
"claude-opus-4-7": [5e-6, 25e-6],
|
|
1010
1011
|
"claude-opus-4-6": [5e-6, 25e-6],
|
|
1011
1012
|
"claude-sonnet-4-6": [3e-6, 15e-6],
|
|
1012
1013
|
"claude-opus-4-5": [5e-6, 25e-6],
|
|
@@ -1051,13 +1052,21 @@ var THINKING_BUDGETS = {
|
|
|
1051
1052
|
max: 3e4
|
|
1052
1053
|
};
|
|
1053
1054
|
function supportsAdaptiveThinking(model) {
|
|
1054
|
-
return model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6");
|
|
1055
|
+
return model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6");
|
|
1055
1056
|
}
|
|
1056
1057
|
function supportsMaxEffort(model) {
|
|
1057
|
-
return model.startsWith("claude-opus-4-6");
|
|
1058
|
+
return model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6");
|
|
1059
|
+
}
|
|
1060
|
+
function supportsXhigh2(model) {
|
|
1061
|
+
return model.startsWith("claude-opus-4-7");
|
|
1058
1062
|
}
|
|
1059
1063
|
function supportsEffort(model) {
|
|
1060
|
-
return model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6") || model.startsWith("claude-opus-4-5");
|
|
1064
|
+
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");
|
|
1065
|
+
}
|
|
1066
|
+
function clampAnthropicEffort(model, effort) {
|
|
1067
|
+
if (effort === "max" && !supportsMaxEffort(model)) return "high";
|
|
1068
|
+
if (effort === "xhigh" && !supportsXhigh2(model)) return "high";
|
|
1069
|
+
return effort;
|
|
1061
1070
|
}
|
|
1062
1071
|
var AnthropicProvider = class {
|
|
1063
1072
|
name = "anthropic";
|
|
@@ -1158,10 +1167,7 @@ var AnthropicProvider = class {
|
|
|
1158
1167
|
body.tool_choice = this.mapToolChoice(options.toolChoice);
|
|
1159
1168
|
}
|
|
1160
1169
|
const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolveThinkingOptions(options);
|
|
1161
|
-
|
|
1162
|
-
if (resolvedEffort === "max" && !supportsMaxEffort(options.model)) {
|
|
1163
|
-
resolvedEffort = "high";
|
|
1164
|
-
}
|
|
1170
|
+
const resolvedEffort = activeEffort ? clampAnthropicEffort(options.model, activeEffort) : void 0;
|
|
1165
1171
|
if (hasBudgetOverride) {
|
|
1166
1172
|
body.thinking = { type: "enabled", budget_tokens: thinkingBudget };
|
|
1167
1173
|
const currentMax = body.max_tokens;
|
|
@@ -1528,6 +1534,7 @@ var GEMINI_PRICING = {
|
|
|
1528
1534
|
"gemini-3-pro-preview": [2e-6, 12e-6],
|
|
1529
1535
|
"gemini-3-flash-preview": [5e-7, 3e-6],
|
|
1530
1536
|
"gemini-3.1-pro-preview": [2e-6, 12e-6],
|
|
1537
|
+
"gemini-3.1-flash-lite": [25e-8, 15e-7],
|
|
1531
1538
|
"gemini-3.1-flash-lite-preview": [25e-8, 15e-7]
|
|
1532
1539
|
};
|
|
1533
1540
|
var GEMINI_PRICING_KEYS_BY_LENGTH = Object.keys(GEMINI_PRICING).sort(
|
|
@@ -1553,12 +1560,16 @@ var THINKING_BUDGETS2 = {
|
|
|
1553
1560
|
low: 1024,
|
|
1554
1561
|
medium: 5e3,
|
|
1555
1562
|
high: 1e4,
|
|
1563
|
+
xhigh: 16384,
|
|
1564
|
+
// between high (10000) and max (24576)
|
|
1556
1565
|
max: 24576
|
|
1557
1566
|
};
|
|
1558
1567
|
var THINKING_LEVELS = {
|
|
1559
1568
|
low: "low",
|
|
1560
1569
|
medium: "medium",
|
|
1561
1570
|
high: "high",
|
|
1571
|
+
xhigh: "high",
|
|
1572
|
+
// 3.x has no xhigh tier; clamp to 'high'
|
|
1562
1573
|
max: "high"
|
|
1563
1574
|
// 3.x caps at 'high'
|
|
1564
1575
|
};
|
|
@@ -6071,8 +6082,31 @@ ${summaryResponse.content}`
|
|
|
6071
6082
|
type: "await_human",
|
|
6072
6083
|
data: { channel: options.channel, prompt: options.prompt }
|
|
6073
6084
|
});
|
|
6074
|
-
const
|
|
6085
|
+
const makeAbortError = (reason) => {
|
|
6086
|
+
if (typeof DOMException !== "undefined") {
|
|
6087
|
+
return new DOMException(
|
|
6088
|
+
typeof reason === "string" ? reason : "awaitHuman aborted",
|
|
6089
|
+
"AbortError"
|
|
6090
|
+
);
|
|
6091
|
+
}
|
|
6092
|
+
const err = new Error(typeof reason === "string" ? reason : "awaitHuman aborted");
|
|
6093
|
+
err.name = "AbortError";
|
|
6094
|
+
return err;
|
|
6095
|
+
};
|
|
6096
|
+
const decision = await new Promise((resolve2, reject) => {
|
|
6097
|
+
if (this.signal?.aborted) {
|
|
6098
|
+
reject(makeAbortError(this.signal.reason));
|
|
6099
|
+
return;
|
|
6100
|
+
}
|
|
6075
6101
|
this.pendingDecisions.set(this.executionId, resolve2);
|
|
6102
|
+
const onAbort = () => {
|
|
6103
|
+
const current = this.pendingDecisions.get(this.executionId);
|
|
6104
|
+
if (current === resolve2) {
|
|
6105
|
+
this.pendingDecisions.delete(this.executionId);
|
|
6106
|
+
}
|
|
6107
|
+
reject(makeAbortError(this.signal?.reason));
|
|
6108
|
+
};
|
|
6109
|
+
this.signal?.addEventListener("abort", onAbort, { once: true });
|
|
6076
6110
|
});
|
|
6077
6111
|
this.emitEvent({
|
|
6078
6112
|
type: "await_human_resolved",
|
|
@@ -6697,6 +6731,16 @@ var MemoryStore = class {
|
|
|
6697
6731
|
const result = limit ? sorted.slice(0, limit) : sorted;
|
|
6698
6732
|
return result.map((e) => structuredClone(e));
|
|
6699
6733
|
}
|
|
6734
|
+
async deleteExecution(executionId) {
|
|
6735
|
+
const removed = this.executionHistory.delete(executionId);
|
|
6736
|
+
this.checkpoints.delete(executionId);
|
|
6737
|
+
this.executionStates.delete(executionId);
|
|
6738
|
+
this.streamingEvents.delete(executionId);
|
|
6739
|
+
if (this.decisions.delete(executionId)) {
|
|
6740
|
+
this.persistAwaitHumanState();
|
|
6741
|
+
}
|
|
6742
|
+
return removed;
|
|
6743
|
+
}
|
|
6700
6744
|
// ── Eval History ──────────────────────────────────────────────────
|
|
6701
6745
|
async saveEvalResult(entry) {
|
|
6702
6746
|
this.evalHistory.set(entry.id, structuredClone(entry));
|
|
@@ -6713,6 +6757,30 @@ var MemoryStore = class {
|
|
|
6713
6757
|
async listSessions() {
|
|
6714
6758
|
return [...this.sessions.keys()];
|
|
6715
6759
|
}
|
|
6760
|
+
// ── Streaming events (for state.persist: 'streaming') ────────────────
|
|
6761
|
+
//
|
|
6762
|
+
// MemoryStore's raison d'être is in-process / ephemeral. We implement
|
|
6763
|
+
// the streaming methods so tests and dev workflows behave correctly,
|
|
6764
|
+
// but on a process crash these buffers are lost — same as the rest of
|
|
6765
|
+
// MemoryStore's state. For real crash-survival, use RedisStore or
|
|
6766
|
+
// SQLiteStore.
|
|
6767
|
+
streamingEvents = /* @__PURE__ */ new Map();
|
|
6768
|
+
async appendStreamingEvents(executionId, events) {
|
|
6769
|
+
if (events.length === 0) return;
|
|
6770
|
+
const existing = this.streamingEvents.get(executionId) ?? [];
|
|
6771
|
+
existing.push(...events.map((e) => structuredClone(e)));
|
|
6772
|
+
this.streamingEvents.set(executionId, existing);
|
|
6773
|
+
}
|
|
6774
|
+
async finalizeStreamingEvents(executionId) {
|
|
6775
|
+
this.streamingEvents.delete(executionId);
|
|
6776
|
+
}
|
|
6777
|
+
async listStreamingExecutions() {
|
|
6778
|
+
return [...this.streamingEvents.keys()];
|
|
6779
|
+
}
|
|
6780
|
+
async getStreamingEvents(executionId) {
|
|
6781
|
+
const events = this.streamingEvents.get(executionId) ?? [];
|
|
6782
|
+
return events.map((e) => structuredClone(e));
|
|
6783
|
+
}
|
|
6716
6784
|
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
6717
6785
|
async close() {
|
|
6718
6786
|
}
|
|
@@ -6769,7 +6837,7 @@ function safeJsonParse(data) {
|
|
|
6769
6837
|
}
|
|
6770
6838
|
}
|
|
6771
6839
|
function rowToExecutionInfo(row) {
|
|
6772
|
-
|
|
6840
|
+
const info = {
|
|
6773
6841
|
executionId: row.execution_id,
|
|
6774
6842
|
workflow: row.workflow,
|
|
6775
6843
|
status: row.status,
|
|
@@ -6780,6 +6848,13 @@ function rowToExecutionInfo(row) {
|
|
|
6780
6848
|
error: row.error ?? void 0,
|
|
6781
6849
|
events: safeJsonParse(row.events) ?? []
|
|
6782
6850
|
};
|
|
6851
|
+
if (row.metadata != null) {
|
|
6852
|
+
const parsed = safeJsonParse(row.metadata);
|
|
6853
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
6854
|
+
info.metadata = parsed;
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
return info;
|
|
6783
6858
|
}
|
|
6784
6859
|
var SQLiteStore = class {
|
|
6785
6860
|
db;
|
|
@@ -6815,6 +6890,10 @@ var SQLiteStore = class {
|
|
|
6815
6890
|
* numeric step values become stringified names ("0", "1", …) to
|
|
6816
6891
|
* preserve replay continuity for in-flight executions across the
|
|
6817
6892
|
* upgrade boundary.
|
|
6893
|
+
* - v3 (ExecutionInfo.metadata): add `execution_history.metadata`
|
|
6894
|
+
* column (TEXT, nullable, JSON-serialized) so caller-supplied
|
|
6895
|
+
* `ExecuteOptions.metadata` round-trips through history. Existing
|
|
6896
|
+
* rows get `NULL`, deserialized as `metadata: undefined`.
|
|
6818
6897
|
*
|
|
6819
6898
|
* Applied BEFORE `initTables()` so the subsequent `CREATE TABLE
|
|
6820
6899
|
* IF NOT EXISTS` runs against the post-migration column name. Fresh
|
|
@@ -6832,7 +6911,7 @@ var SQLiteStore = class {
|
|
|
6832
6911
|
* transactional re-read costs nothing.
|
|
6833
6912
|
*/
|
|
6834
6913
|
migrate() {
|
|
6835
|
-
const TARGET_VERSION =
|
|
6914
|
+
const TARGET_VERSION = 3;
|
|
6836
6915
|
const current = this.db.pragma("user_version", { simple: true });
|
|
6837
6916
|
if (current >= TARGET_VERSION) return;
|
|
6838
6917
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -6854,6 +6933,12 @@ var SQLiteStore = class {
|
|
|
6854
6933
|
this.db.exec("ALTER TABLE checkpoints RENAME COLUMN step TO name");
|
|
6855
6934
|
}
|
|
6856
6935
|
}
|
|
6936
|
+
if (committed < 3) {
|
|
6937
|
+
const cols = this.db.pragma("table_info(execution_history)");
|
|
6938
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "metadata")) {
|
|
6939
|
+
this.db.exec("ALTER TABLE execution_history ADD COLUMN metadata TEXT");
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6857
6942
|
this.db.pragma(`user_version = ${TARGET_VERSION}`);
|
|
6858
6943
|
this.db.exec("COMMIT");
|
|
6859
6944
|
} catch (err) {
|
|
@@ -6916,7 +7001,8 @@ var SQLiteStore = class {
|
|
|
6916
7001
|
completed_at INTEGER,
|
|
6917
7002
|
duration INTEGER NOT NULL DEFAULT 0,
|
|
6918
7003
|
error TEXT,
|
|
6919
|
-
events TEXT NOT NULL
|
|
7004
|
+
events TEXT NOT NULL,
|
|
7005
|
+
metadata TEXT
|
|
6920
7006
|
);
|
|
6921
7007
|
|
|
6922
7008
|
CREATE TABLE IF NOT EXISTS eval_history (
|
|
@@ -7037,7 +7123,7 @@ var SQLiteStore = class {
|
|
|
7037
7123
|
// ── Execution History ────────────────────────────────────────────────────
|
|
7038
7124
|
async saveExecution(execution) {
|
|
7039
7125
|
this.db.prepare(
|
|
7040
|
-
"INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, events) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
7126
|
+
"INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, events, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
7041
7127
|
).run(
|
|
7042
7128
|
execution.executionId,
|
|
7043
7129
|
execution.workflow,
|
|
@@ -7047,7 +7133,8 @@ var SQLiteStore = class {
|
|
|
7047
7133
|
execution.completedAt ?? null,
|
|
7048
7134
|
execution.duration,
|
|
7049
7135
|
execution.error ?? null,
|
|
7050
|
-
JSON.stringify(execution.events)
|
|
7136
|
+
JSON.stringify(execution.events),
|
|
7137
|
+
execution.metadata !== void 0 ? JSON.stringify(execution.metadata) : null
|
|
7051
7138
|
);
|
|
7052
7139
|
}
|
|
7053
7140
|
async getExecution(executionId) {
|
|
@@ -7059,6 +7146,16 @@ var SQLiteStore = class {
|
|
|
7059
7146
|
const rows = limit ? this.db.prepare(sql).all(limit) : this.db.prepare(sql).all();
|
|
7060
7147
|
return rows.map(rowToExecutionInfo);
|
|
7061
7148
|
}
|
|
7149
|
+
async deleteExecution(executionId) {
|
|
7150
|
+
const tx = this.db.transaction(() => {
|
|
7151
|
+
const result = this.db.prepare("DELETE FROM execution_history WHERE execution_id = ?").run(executionId);
|
|
7152
|
+
this.db.prepare("DELETE FROM checkpoints WHERE execution_id = ?").run(executionId);
|
|
7153
|
+
this.db.prepare("DELETE FROM execution_state WHERE execution_id = ?").run(executionId);
|
|
7154
|
+
this.db.prepare("DELETE FROM decisions WHERE execution_id = ?").run(executionId);
|
|
7155
|
+
return result.changes > 0;
|
|
7156
|
+
});
|
|
7157
|
+
return tx();
|
|
7158
|
+
}
|
|
7062
7159
|
// ── Eval History ────────────────────────────────────────────────────────
|
|
7063
7160
|
async saveEvalResult(entry) {
|
|
7064
7161
|
this.db.prepare(
|
|
@@ -7937,6 +8034,7 @@ var MemoryManager = class {
|
|
|
7937
8034
|
if (stateStore.saveMemory) {
|
|
7938
8035
|
await stateStore.saveMemory(storeScope, key, value);
|
|
7939
8036
|
} else {
|
|
8037
|
+
warnMissingMemoryMethodsOnce(stateStore);
|
|
7940
8038
|
const storeKey = `memory:${storeScope}:${key}`;
|
|
7941
8039
|
await stateStore.saveSessionMeta(storeKey, "value", value);
|
|
7942
8040
|
if (options?.metadata) {
|
|
@@ -8039,6 +8137,14 @@ var MemoryManager = class {
|
|
|
8039
8137
|
}
|
|
8040
8138
|
}
|
|
8041
8139
|
};
|
|
8140
|
+
var _warnedStoresMissingMemory = /* @__PURE__ */ new WeakSet();
|
|
8141
|
+
function warnMissingMemoryMethodsOnce(stateStore) {
|
|
8142
|
+
if (_warnedStoresMissingMemory.has(stateStore)) return;
|
|
8143
|
+
_warnedStoresMissingMemory.add(stateStore);
|
|
8144
|
+
console.warn(
|
|
8145
|
+
"[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."
|
|
8146
|
+
);
|
|
8147
|
+
}
|
|
8042
8148
|
|
|
8043
8149
|
// src/telemetry/noop.ts
|
|
8044
8150
|
var NOOP_SPAN = {
|
|
@@ -8080,11 +8186,141 @@ function hashInput(input) {
|
|
|
8080
8186
|
return (hash >>> 0).toString(16);
|
|
8081
8187
|
}
|
|
8082
8188
|
var DEFAULT_MAX_EVENTS_PER_EXECUTION = 5e4;
|
|
8189
|
+
var DEFAULT_STREAMING_BATCH_SIZE = 100;
|
|
8190
|
+
var DEFAULT_STREAMING_BATCH_INTERVAL = 1e3;
|
|
8191
|
+
var RECOVERED_UNKNOWN_WORKFLOW = "__axl/recovered";
|
|
8192
|
+
var STREAMING_EXCLUDED_TYPES = /* @__PURE__ */ new Set([
|
|
8193
|
+
"token",
|
|
8194
|
+
"partial_object",
|
|
8195
|
+
"string_delta"
|
|
8196
|
+
]);
|
|
8197
|
+
var INTERNAL_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
8198
|
+
"sessionId",
|
|
8199
|
+
"sessionHistory",
|
|
8200
|
+
"resumeMode"
|
|
8201
|
+
]);
|
|
8202
|
+
function liftPersistedMetadata(metadata) {
|
|
8203
|
+
if (!metadata) return void 0;
|
|
8204
|
+
const filtered = {};
|
|
8205
|
+
let hasAny = false;
|
|
8206
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
8207
|
+
if (INTERNAL_METADATA_KEYS.has(k)) continue;
|
|
8208
|
+
filtered[k] = v;
|
|
8209
|
+
hasAny = true;
|
|
8210
|
+
}
|
|
8211
|
+
if (!hasAny) return void 0;
|
|
8212
|
+
try {
|
|
8213
|
+
return structuredClone(filtered);
|
|
8214
|
+
} catch {
|
|
8215
|
+
return { ...filtered };
|
|
8216
|
+
}
|
|
8217
|
+
}
|
|
8218
|
+
var StreamingFlusher = class {
|
|
8219
|
+
constructor(store, batchSize, batchInterval) {
|
|
8220
|
+
this.store = store;
|
|
8221
|
+
this.batchSize = batchSize;
|
|
8222
|
+
this.batchInterval = batchInterval;
|
|
8223
|
+
}
|
|
8224
|
+
buffers = /* @__PURE__ */ new Map();
|
|
8225
|
+
timers = /* @__PURE__ */ new Map();
|
|
8226
|
+
/**
|
|
8227
|
+
* Tracks the most recent in-flight `appendStreamingEvents` per execution
|
|
8228
|
+
* so `finalize` can await it. Without this, a size-triggered fire-and-
|
|
8229
|
+
* forget flush whose RPUSH lands AFTER `finalize`'s DEL would resurrect
|
|
8230
|
+
* the buffer + re-register the execution in the streaming-exec-ids set
|
|
8231
|
+
* — a phantom orphan that recoverIncompleteStreams would later mis-recover.
|
|
8232
|
+
*/
|
|
8233
|
+
inflight = /* @__PURE__ */ new Map();
|
|
8234
|
+
append(executionId, event) {
|
|
8235
|
+
if (STREAMING_EXCLUDED_TYPES.has(event.type)) return;
|
|
8236
|
+
let buffer = this.buffers.get(executionId);
|
|
8237
|
+
if (!buffer) {
|
|
8238
|
+
buffer = [];
|
|
8239
|
+
this.buffers.set(executionId, buffer);
|
|
8240
|
+
}
|
|
8241
|
+
buffer.push(event);
|
|
8242
|
+
if (buffer.length >= this.batchSize) {
|
|
8243
|
+
void this.flush(executionId);
|
|
8244
|
+
return;
|
|
8245
|
+
}
|
|
8246
|
+
if (!this.timers.has(executionId)) {
|
|
8247
|
+
const timer = setTimeout(() => {
|
|
8248
|
+
this.timers.delete(executionId);
|
|
8249
|
+
void this.flush(executionId);
|
|
8250
|
+
}, this.batchInterval);
|
|
8251
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
8252
|
+
timer.unref?.();
|
|
8253
|
+
}
|
|
8254
|
+
this.timers.set(executionId, timer);
|
|
8255
|
+
}
|
|
8256
|
+
}
|
|
8257
|
+
/** Flush this execution's buffer immediately. Awaits any prior in-flight
|
|
8258
|
+
* flush for the same execution so writes land in order. */
|
|
8259
|
+
async flush(executionId) {
|
|
8260
|
+
const buffer = this.buffers.get(executionId);
|
|
8261
|
+
if (!buffer || buffer.length === 0) return;
|
|
8262
|
+
this.buffers.set(executionId, []);
|
|
8263
|
+
const timer = this.timers.get(executionId);
|
|
8264
|
+
if (timer) {
|
|
8265
|
+
clearTimeout(timer);
|
|
8266
|
+
this.timers.delete(executionId);
|
|
8267
|
+
}
|
|
8268
|
+
const prior = this.inflight.get(executionId) ?? Promise.resolve();
|
|
8269
|
+
const next = prior.then(async () => {
|
|
8270
|
+
try {
|
|
8271
|
+
await this.store.appendStreamingEvents?.(executionId, buffer);
|
|
8272
|
+
} catch (err) {
|
|
8273
|
+
console.error(
|
|
8274
|
+
`[axl] StreamingFlusher: failed to append ${buffer.length} events for executionId=${executionId}: ${err instanceof Error ? err.message : String(err)}`
|
|
8275
|
+
);
|
|
8276
|
+
}
|
|
8277
|
+
});
|
|
8278
|
+
this.inflight.set(executionId, next);
|
|
8279
|
+
next.finally(() => {
|
|
8280
|
+
if (this.inflight.get(executionId) === next) {
|
|
8281
|
+
this.inflight.delete(executionId);
|
|
8282
|
+
}
|
|
8283
|
+
});
|
|
8284
|
+
return next;
|
|
8285
|
+
}
|
|
8286
|
+
/** Finalize an execution — flush remaining buffer + release store-side buffer.
|
|
8287
|
+
* Critically, awaits the most recent in-flight `appendStreamingEvents` BEFORE
|
|
8288
|
+
* calling `finalizeStreamingEvents`, so a late-landing RPUSH can't resurrect
|
|
8289
|
+
* the buffer after we've deleted it. */
|
|
8290
|
+
async finalize(executionId) {
|
|
8291
|
+
await this.flush(executionId);
|
|
8292
|
+
const pending = this.inflight.get(executionId);
|
|
8293
|
+
if (pending) {
|
|
8294
|
+
await pending;
|
|
8295
|
+
this.inflight.delete(executionId);
|
|
8296
|
+
}
|
|
8297
|
+
this.buffers.delete(executionId);
|
|
8298
|
+
const timer = this.timers.get(executionId);
|
|
8299
|
+
if (timer) {
|
|
8300
|
+
clearTimeout(timer);
|
|
8301
|
+
this.timers.delete(executionId);
|
|
8302
|
+
}
|
|
8303
|
+
try {
|
|
8304
|
+
await this.store.finalizeStreamingEvents?.(executionId);
|
|
8305
|
+
} catch (err) {
|
|
8306
|
+
console.error(
|
|
8307
|
+
`[axl] StreamingFlusher: failed to finalize executionId=${executionId}: ` + (err instanceof Error ? err.message : String(err))
|
|
8308
|
+
);
|
|
8309
|
+
}
|
|
8310
|
+
}
|
|
8311
|
+
/** Best-effort drain of every pending buffer. Called from `runtime.shutdown()`. */
|
|
8312
|
+
async flushAll() {
|
|
8313
|
+
const ids = [...this.buffers.keys()];
|
|
8314
|
+
await Promise.all(ids.map((id) => this.flush(id)));
|
|
8315
|
+
const pendings = [...this.inflight.values()];
|
|
8316
|
+
await Promise.allSettled(pendings);
|
|
8317
|
+
}
|
|
8318
|
+
};
|
|
8083
8319
|
function isAbortError(err) {
|
|
8084
8320
|
return typeof err === "object" && err !== null && err.name === "AbortError";
|
|
8085
8321
|
}
|
|
8086
8322
|
function pushEventBounded(execInfo, event, cap) {
|
|
8087
|
-
if (
|
|
8323
|
+
if (STREAMING_EXCLUDED_TYPES.has(event.type)) return;
|
|
8088
8324
|
if (cap === Infinity || execInfo.events.length < cap) {
|
|
8089
8325
|
execInfo.events.push(event);
|
|
8090
8326
|
return;
|
|
@@ -8105,6 +8341,36 @@ function pushEventBounded(execInfo, event, cap) {
|
|
|
8105
8341
|
};
|
|
8106
8342
|
}
|
|
8107
8343
|
}
|
|
8344
|
+
function sanitizeMetadataForPersist(metadata) {
|
|
8345
|
+
const cleaned = {};
|
|
8346
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
8347
|
+
try {
|
|
8348
|
+
cleaned[k] = structuredClone(v);
|
|
8349
|
+
} catch {
|
|
8350
|
+
}
|
|
8351
|
+
}
|
|
8352
|
+
return cleaned;
|
|
8353
|
+
}
|
|
8354
|
+
function boundRecoveredEvents(events, cap) {
|
|
8355
|
+
if (cap === Infinity || events.length <= cap) return events;
|
|
8356
|
+
const truncated = events.slice(0, cap);
|
|
8357
|
+
const last = truncated[truncated.length - 1];
|
|
8358
|
+
const alreadyTruncated = last?.type === "log" && typeof last.data === "object" && last.data !== null && last.data.event === "events_truncated";
|
|
8359
|
+
if (!alreadyTruncated) {
|
|
8360
|
+
truncated[truncated.length - 1] = {
|
|
8361
|
+
type: "log",
|
|
8362
|
+
executionId: events[0]?.executionId ?? "",
|
|
8363
|
+
step: last?.step ?? 0,
|
|
8364
|
+
timestamp: Date.now(),
|
|
8365
|
+
data: {
|
|
8366
|
+
event: "events_truncated",
|
|
8367
|
+
cap,
|
|
8368
|
+
message: `Recovered ExecutionInfo.events truncated at ${cap} entries. Original streaming buffer had ${events.length} events. Raise via config.state.maxEventsPerExecution.`
|
|
8369
|
+
}
|
|
8370
|
+
};
|
|
8371
|
+
}
|
|
8372
|
+
return truncated;
|
|
8373
|
+
}
|
|
8108
8374
|
var costScopeStorage = new import_node_async_hooks2.AsyncLocalStorage();
|
|
8109
8375
|
function forwardAbortSignal(external, internal) {
|
|
8110
8376
|
if (!external) return () => {
|
|
@@ -8151,6 +8417,30 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8151
8417
|
* `events` field — keeps the log to one line per row instead of one per
|
|
8152
8418
|
* rebuild tick. */
|
|
8153
8419
|
warnedMalformedExecutions = /* @__PURE__ */ new Set();
|
|
8420
|
+
/** Streaming-mode flusher. `undefined` when persist === 'terminal'
|
|
8421
|
+
* (back-compat default — no buffering overhead, no batching timer). */
|
|
8422
|
+
streamingFlusher;
|
|
8423
|
+
/** Execution IDs that are eligible for streaming-buffer durability —
|
|
8424
|
+
* populated by `execute()` / `stream()`. `createContext()` flows are
|
|
8425
|
+
* deliberately NOT added: ad-hoc contexts (tool tests, Studio playground,
|
|
8426
|
+
* evals) have no terminal `persistExecution` path that would finalize the
|
|
8427
|
+
* streaming buffer, so allowing them to write would leave phantom orphan
|
|
8428
|
+
* buffers that `recoverIncompleteStreams()` later mis-recovers as failed
|
|
8429
|
+
* executions. Membership is removed in `persistExecution` once the
|
|
8430
|
+
* canonical save + finalize chain has been scheduled. */
|
|
8431
|
+
streamableExecutionIds = /* @__PURE__ */ new Set();
|
|
8432
|
+
/** Execution IDs deleted by `runtime.deleteExecution()` while still in
|
|
8433
|
+
* flight. `persistExecution` skips its save + cache write when the
|
|
8434
|
+
* executionId is in this set — preventing a workflow that completes after
|
|
8435
|
+
* a delete from resurrecting the (intentionally-removed) row. Entries are
|
|
8436
|
+
* removed once the would-be persist has been short-circuited. */
|
|
8437
|
+
pendingDeletedExecutions = /* @__PURE__ */ new Set();
|
|
8438
|
+
/** In-flight `persistExecution` chains (save → finalize). `shutdown()`
|
|
8439
|
+
* awaits these before closing the StateStore so a workflow whose abort
|
|
8440
|
+
* triggers a persist-during-shutdown isn't racing a closed connection.
|
|
8441
|
+
* The Set is mutated by the chain's own `.finally` so entries clear as
|
|
8442
|
+
* soon as the work settles. */
|
|
8443
|
+
persistInflight = /* @__PURE__ */ new Set();
|
|
8154
8444
|
constructor(config) {
|
|
8155
8445
|
super();
|
|
8156
8446
|
this.config = resolveConfig(config ?? {});
|
|
@@ -8174,6 +8464,19 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8174
8464
|
embedder: this.config.memory.embedder
|
|
8175
8465
|
});
|
|
8176
8466
|
}
|
|
8467
|
+
if (this.config.state?.persist === "streaming") {
|
|
8468
|
+
if (this.stateStore.appendStreamingEvents) {
|
|
8469
|
+
this.streamingFlusher = new StreamingFlusher(
|
|
8470
|
+
this.stateStore,
|
|
8471
|
+
this.config.state.streamingBatchSize ?? DEFAULT_STREAMING_BATCH_SIZE,
|
|
8472
|
+
this.config.state.streamingBatchInterval ?? DEFAULT_STREAMING_BATCH_INTERVAL
|
|
8473
|
+
);
|
|
8474
|
+
} else {
|
|
8475
|
+
console.warn(
|
|
8476
|
+
`[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.`
|
|
8477
|
+
);
|
|
8478
|
+
}
|
|
8479
|
+
}
|
|
8177
8480
|
}
|
|
8178
8481
|
/**
|
|
8179
8482
|
* Whether `config.trace.redact` is enabled on this runtime. A narrow
|
|
@@ -8297,14 +8600,49 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8297
8600
|
/**
|
|
8298
8601
|
* Persist a completed/failed execution to the state store (fire-and-forget)
|
|
8299
8602
|
* and move it from the active executions map to the historical cache.
|
|
8603
|
+
*
|
|
8604
|
+
* Resurrection guard: if `runtime.deleteExecution(id)` was called while
|
|
8605
|
+
* this execution was still running, the id will be in
|
|
8606
|
+
* `pendingDeletedExecutions`. In that case we skip the save AND the
|
|
8607
|
+
* historical-cache write, and finalize the streaming buffer (if any) so
|
|
8608
|
+
* the buffer doesn't outlive the delete.
|
|
8300
8609
|
*/
|
|
8301
8610
|
persistExecution(execInfo) {
|
|
8302
|
-
const
|
|
8611
|
+
const id = execInfo.executionId;
|
|
8612
|
+
this.streamableExecutionIds.delete(id);
|
|
8613
|
+
if (this.pendingDeletedExecutions.has(id)) {
|
|
8614
|
+
this.pendingDeletedExecutions.delete(id);
|
|
8615
|
+
this.executions.delete(id);
|
|
8616
|
+
if (this.streamingFlusher) {
|
|
8617
|
+
this.streamingFlusher.finalize(id).catch(() => {
|
|
8618
|
+
});
|
|
8619
|
+
}
|
|
8620
|
+
return;
|
|
8621
|
+
}
|
|
8622
|
+
let snapshot;
|
|
8623
|
+
try {
|
|
8624
|
+
snapshot = structuredClone(execInfo);
|
|
8625
|
+
} catch {
|
|
8626
|
+
snapshot = {
|
|
8627
|
+
...execInfo,
|
|
8628
|
+
events: [...execInfo.events],
|
|
8629
|
+
metadata: execInfo.metadata ? sanitizeMetadataForPersist(execInfo.metadata) : void 0
|
|
8630
|
+
};
|
|
8631
|
+
}
|
|
8303
8632
|
if (this.stateStore.saveExecution) {
|
|
8304
|
-
this.stateStore.saveExecution(snapshot).
|
|
8633
|
+
const chain = this.stateStore.saveExecution(snapshot).then(() => {
|
|
8634
|
+
return this.streamingFlusher?.finalize(id).catch(() => {
|
|
8635
|
+
});
|
|
8636
|
+
}).catch(() => {
|
|
8637
|
+
}).then(() => void 0);
|
|
8638
|
+
this.persistInflight.add(chain);
|
|
8639
|
+
chain.finally(() => this.persistInflight.delete(chain));
|
|
8640
|
+
} else if (this.streamingFlusher) {
|
|
8641
|
+
const chain = this.streamingFlusher.finalize(id).catch(() => {
|
|
8305
8642
|
});
|
|
8643
|
+
this.persistInflight.add(chain);
|
|
8644
|
+
chain.finally(() => this.persistInflight.delete(chain));
|
|
8306
8645
|
}
|
|
8307
|
-
const id = execInfo.executionId;
|
|
8308
8646
|
this.historicalExecutions.set(id, snapshot);
|
|
8309
8647
|
this.executions.delete(id);
|
|
8310
8648
|
}
|
|
@@ -8544,6 +8882,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8544
8882
|
this.abortControllers.set(executionId, controller);
|
|
8545
8883
|
const cleanupAbortForwarder = forwardAbortSignal(options?.signal, controller);
|
|
8546
8884
|
this.registerWithCostScope(executionId);
|
|
8885
|
+
if (this.streamingFlusher) {
|
|
8886
|
+
this.streamableExecutionIds.add(executionId);
|
|
8887
|
+
}
|
|
8547
8888
|
const execInfo = {
|
|
8548
8889
|
executionId,
|
|
8549
8890
|
workflow: name,
|
|
@@ -8551,7 +8892,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8551
8892
|
events: [],
|
|
8552
8893
|
totalCost: 0,
|
|
8553
8894
|
startedAt: Date.now(),
|
|
8554
|
-
duration: 0
|
|
8895
|
+
duration: 0,
|
|
8896
|
+
metadata: liftPersistedMetadata(options?.metadata)
|
|
8555
8897
|
};
|
|
8556
8898
|
this.executions.set(executionId, execInfo);
|
|
8557
8899
|
const sessionHistory = options?.metadata?.sessionHistory ?? void 0;
|
|
@@ -8567,6 +8909,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8567
8909
|
onTrace: (event) => {
|
|
8568
8910
|
pushEventBounded(execInfo, event, this.maxEventsPerExecution);
|
|
8569
8911
|
execInfo.totalCost += eventCostContribution(event);
|
|
8912
|
+
if (this.streamableExecutionIds.has(executionId)) {
|
|
8913
|
+
this.streamingFlusher?.append(executionId, event);
|
|
8914
|
+
}
|
|
8570
8915
|
this.emit("trace", event);
|
|
8571
8916
|
this.outputAxlEvent(event);
|
|
8572
8917
|
if (event.type === "handoff_start") {
|
|
@@ -8645,6 +8990,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8645
8990
|
const validated = workflow2.inputSchema.parse(input);
|
|
8646
8991
|
const sessionHistory = options?.metadata?.sessionHistory ?? void 0;
|
|
8647
8992
|
this.registerWithCostScope(executionId);
|
|
8993
|
+
if (this.streamingFlusher) {
|
|
8994
|
+
this.streamableExecutionIds.add(executionId);
|
|
8995
|
+
}
|
|
8648
8996
|
execInfo = {
|
|
8649
8997
|
executionId,
|
|
8650
8998
|
workflow: name,
|
|
@@ -8652,7 +9000,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8652
9000
|
events: [],
|
|
8653
9001
|
totalCost: 0,
|
|
8654
9002
|
startedAt: Date.now(),
|
|
8655
|
-
duration: 0
|
|
9003
|
+
duration: 0,
|
|
9004
|
+
metadata: liftPersistedMetadata(options?.metadata)
|
|
8656
9005
|
};
|
|
8657
9006
|
this.executions.set(executionId, execInfo);
|
|
8658
9007
|
const wfCtx = new WorkflowContext({
|
|
@@ -8673,6 +9022,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8673
9022
|
onTrace: (event) => {
|
|
8674
9023
|
pushEventBounded(execInfo, event, this.maxEventsPerExecution);
|
|
8675
9024
|
execInfo.totalCost += eventCostContribution(event);
|
|
9025
|
+
if (this.streamableExecutionIds.has(executionId)) {
|
|
9026
|
+
this.streamingFlusher?.append(executionId, event);
|
|
9027
|
+
}
|
|
8676
9028
|
this.emit("trace", event);
|
|
8677
9029
|
this.outputAxlEvent(event);
|
|
8678
9030
|
axlStream._push(event);
|
|
@@ -8812,6 +9164,15 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8812
9164
|
errors.push(new Error(`${label}: ${err instanceof Error ? err.message : String(err)}`));
|
|
8813
9165
|
}
|
|
8814
9166
|
};
|
|
9167
|
+
if (this.streamingFlusher) {
|
|
9168
|
+
await safeClose("streamingFlusher", () => this.streamingFlusher.flushAll());
|
|
9169
|
+
}
|
|
9170
|
+
if (this.persistInflight.size > 0) {
|
|
9171
|
+
await safeClose(
|
|
9172
|
+
"persistInflight",
|
|
9173
|
+
() => Promise.allSettled([...this.persistInflight]).then(() => void 0)
|
|
9174
|
+
);
|
|
9175
|
+
}
|
|
8815
9176
|
if (this.mcpManager) await safeClose("mcpManager", () => this.mcpManager.shutdown());
|
|
8816
9177
|
if (this.memoryManager) await safeClose("memoryManager", () => this.memoryManager.close());
|
|
8817
9178
|
if (this.stateStore.close) await safeClose("stateStore", () => this.stateStore.close());
|
|
@@ -8822,6 +9183,81 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8822
9183
|
);
|
|
8823
9184
|
}
|
|
8824
9185
|
}
|
|
9186
|
+
/**
|
|
9187
|
+
* Recover executions whose process died mid-run when `state.persist:
|
|
9188
|
+
* 'streaming'` was configured. Synthesizes a partial `ExecutionInfo`
|
|
9189
|
+
* from the streaming buffer for each — `status: 'failed'`, error
|
|
9190
|
+
* `'process terminated'` — registers them in the historical cache so
|
|
9191
|
+
* they show up in `getExecutions()`, persists them via the store's
|
|
9192
|
+
* `saveExecution`, and finalizes the streaming buffer.
|
|
9193
|
+
*
|
|
9194
|
+
* Idempotent — if the streaming buffer is already gone (or
|
|
9195
|
+
* `listStreamingExecutions` is empty), this is a no-op. Returns the
|
|
9196
|
+
* list of recovered `ExecutionInfo`s for caller introspection.
|
|
9197
|
+
*
|
|
9198
|
+
* Wire this into your process startup AFTER `runtime.getExecutions()`
|
|
9199
|
+
* has lazy-loaded the historical cache. Order matters: completed
|
|
9200
|
+
* executions already in the store should not be overwritten by a
|
|
9201
|
+
* stale streaming buffer that the previous process didn't get to
|
|
9202
|
+
* finalize. The implementation checks the historical store before
|
|
9203
|
+
* synthesizing, so a re-run is safe.
|
|
9204
|
+
*/
|
|
9205
|
+
async recoverIncompleteStreams() {
|
|
9206
|
+
if (!this.stateStore.listStreamingExecutions || !this.stateStore.getStreamingEvents) {
|
|
9207
|
+
return [];
|
|
9208
|
+
}
|
|
9209
|
+
const ids = await this.stateStore.listStreamingExecutions();
|
|
9210
|
+
if (ids.length === 0) return [];
|
|
9211
|
+
await this.getExecutions();
|
|
9212
|
+
const recovered = [];
|
|
9213
|
+
for (const id of ids) {
|
|
9214
|
+
if (this.executions.has(id) || this.streamableExecutionIds.has(id)) {
|
|
9215
|
+
continue;
|
|
9216
|
+
}
|
|
9217
|
+
const existing = this.historicalExecutions.get(id);
|
|
9218
|
+
if (existing) {
|
|
9219
|
+
await this.stateStore.finalizeStreamingEvents?.(id);
|
|
9220
|
+
continue;
|
|
9221
|
+
}
|
|
9222
|
+
const events = await this.stateStore.getStreamingEvents(id);
|
|
9223
|
+
if (events.length === 0) {
|
|
9224
|
+
await this.stateStore.finalizeStreamingEvents?.(id);
|
|
9225
|
+
continue;
|
|
9226
|
+
}
|
|
9227
|
+
const boundedEvents = boundRecoveredEvents(events, this.maxEventsPerExecution);
|
|
9228
|
+
const workflowStartEvent = boundedEvents.find((e) => e.type === "workflow_start");
|
|
9229
|
+
const lastEvent = boundedEvents[boundedEvents.length - 1];
|
|
9230
|
+
const startedAt = workflowStartEvent?.timestamp ?? boundedEvents[0]?.timestamp ?? Date.now();
|
|
9231
|
+
const completedAt = lastEvent.timestamp ?? startedAt;
|
|
9232
|
+
const workflowName = workflowStartEvent?.workflow ?? RECOVERED_UNKNOWN_WORKFLOW;
|
|
9233
|
+
const totalCost = boundedEvents.reduce((sum, e) => sum + eventCostContribution(e), 0);
|
|
9234
|
+
const synthesized = {
|
|
9235
|
+
executionId: id,
|
|
9236
|
+
workflow: workflowName,
|
|
9237
|
+
status: "failed",
|
|
9238
|
+
events: boundedEvents,
|
|
9239
|
+
totalCost,
|
|
9240
|
+
startedAt,
|
|
9241
|
+
completedAt,
|
|
9242
|
+
duration: completedAt - startedAt,
|
|
9243
|
+
error: "process terminated (recovered from streaming buffer)"
|
|
9244
|
+
};
|
|
9245
|
+
if (this.stateStore.saveExecution) {
|
|
9246
|
+
try {
|
|
9247
|
+
await this.stateStore.saveExecution(synthesized);
|
|
9248
|
+
} catch (err) {
|
|
9249
|
+
console.error(
|
|
9250
|
+
`[axl] recoverIncompleteStreams: failed to save synthesized execution ${id}: ${err instanceof Error ? err.message : String(err)}. Streaming buffer left in place for next recovery attempt.`
|
|
9251
|
+
);
|
|
9252
|
+
continue;
|
|
9253
|
+
}
|
|
9254
|
+
}
|
|
9255
|
+
this.historicalExecutions.set(id, synthesized);
|
|
9256
|
+
await this.stateStore.finalizeStreamingEvents?.(id);
|
|
9257
|
+
recovered.push(synthesized);
|
|
9258
|
+
}
|
|
9259
|
+
return recovered;
|
|
9260
|
+
}
|
|
8825
9261
|
/** Abort a running execution by its ID. */
|
|
8826
9262
|
abort(executionId) {
|
|
8827
9263
|
const controller = this.abortControllers.get(executionId);
|
|
@@ -8854,15 +9290,85 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8854
9290
|
}
|
|
8855
9291
|
this.emit("eval_result", entry);
|
|
8856
9292
|
}
|
|
9293
|
+
/**
|
|
9294
|
+
* Delete an execution from history by id. Removes from the active map (if
|
|
9295
|
+
* still running), the historical cache, the configured StateStore, AND
|
|
9296
|
+
* any execution-scoped side state — pending awaitHuman decisions,
|
|
9297
|
+
* resolver maps, streaming buffer registration, in-flight abort controller.
|
|
9298
|
+
* Returns true if an entry was actually removed from any of the three
|
|
9299
|
+
* primary surfaces (active map, historical cache, store).
|
|
9300
|
+
*
|
|
9301
|
+
* In-flight handling: if the execution is still running, this method
|
|
9302
|
+
* **aborts** it (via the registered abort controller) AND marks the id for
|
|
9303
|
+
* skip-on-persist so the workflow's eventual `workflow_end` doesn't
|
|
9304
|
+
* resurrect the row. The workflow itself terminates normally — callers
|
|
9305
|
+
* iterating the returned `AxlStream` will see the abort flow through. If
|
|
9306
|
+
* you need a hard hand-off (delete completes before the workflow tears
|
|
9307
|
+
* down), call `runtime.abort(id)` and `await` the stream/execute promise
|
|
9308
|
+
* before calling this. `streamableExecutionIds` is cleared eagerly so
|
|
9309
|
+
* events emitted during the wind-down don't re-create the streaming
|
|
9310
|
+
* buffer in the store after the deleteExecution-issued DEL.
|
|
9311
|
+
*
|
|
9312
|
+
* Audit trail: emits `execution_deleted` on the runtime's `EventEmitter`
|
|
9313
|
+
* with `{ executionId, wasActive, hadPendingDecision }` so compliance
|
|
9314
|
+
* apps can subscribe via `runtime.on('execution_deleted', ...)` for a
|
|
9315
|
+
* "who deleted what when" log without wrapping this method. Fires
|
|
9316
|
+
* regardless of whether the id was actually present.
|
|
9317
|
+
*
|
|
9318
|
+
* Use cases: GDPR right-to-be-forgotten, operator-driven cleanup of
|
|
9319
|
+
* specific runs (e.g. a workflow that recorded PII the user requested
|
|
9320
|
+
* scrubbed). For bulk eviction by age, use a `RedisStore` TTL instead
|
|
9321
|
+
* (`defaultTtl` / `ttls.executionHistory`).
|
|
9322
|
+
*/
|
|
9323
|
+
async deleteExecution(id) {
|
|
9324
|
+
const wasActive = this.executions.has(id);
|
|
9325
|
+
const hadPendingDecision = this.pendingDecisionResolvers.has(id);
|
|
9326
|
+
if (wasActive) {
|
|
9327
|
+
this.pendingDeletedExecutions.add(id);
|
|
9328
|
+
this.streamableExecutionIds.delete(id);
|
|
9329
|
+
const controller = this.abortControllers.get(id);
|
|
9330
|
+
if (controller) {
|
|
9331
|
+
controller.abort();
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9334
|
+
if (hadPendingDecision) {
|
|
9335
|
+
this.pendingDecisionResolvers.delete(id);
|
|
9336
|
+
}
|
|
9337
|
+
const existing = await this.getExecution(id);
|
|
9338
|
+
const workflow2 = existing?.workflow;
|
|
9339
|
+
const removedFromActive = this.executions.delete(id);
|
|
9340
|
+
const removedFromHistorical = this.historicalExecutions.delete(id);
|
|
9341
|
+
let removedFromStore = false;
|
|
9342
|
+
if (this.stateStore.deleteExecution) {
|
|
9343
|
+
removedFromStore = await this.stateStore.deleteExecution(id);
|
|
9344
|
+
}
|
|
9345
|
+
this.emit("execution_deleted", {
|
|
9346
|
+
executionId: id,
|
|
9347
|
+
workflow: workflow2,
|
|
9348
|
+
wasActive,
|
|
9349
|
+
hadPendingDecision,
|
|
9350
|
+
removed: removedFromActive || removedFromHistorical || removedFromStore
|
|
9351
|
+
});
|
|
9352
|
+
return removedFromActive || removedFromHistorical || removedFromStore;
|
|
9353
|
+
}
|
|
8857
9354
|
/**
|
|
8858
9355
|
* Delete an eval history entry by id. Removes from in-memory cache and
|
|
8859
9356
|
* the configured StateStore. Returns true if an entry was actually removed.
|
|
8860
9357
|
*
|
|
8861
9358
|
* Ensures lazy-loaded history is loaded first so the in-memory cache and
|
|
8862
9359
|
* the store can't drift apart on the deletion path.
|
|
9360
|
+
*
|
|
9361
|
+
* Emits `eval_deleted` on the runtime's `EventEmitter` with
|
|
9362
|
+
* `{ id, eval, removed }`. Fires on every call — including attempts
|
|
9363
|
+
* against unknown ids (`removed: false`) — symmetric to `execution_deleted`.
|
|
9364
|
+
* Studio's eval-trends aggregator subscribes to this event so deleted
|
|
9365
|
+
* runs don't linger in the trends panel for up to 5 minutes (until the
|
|
9366
|
+
* next periodic rebuild).
|
|
8863
9367
|
*/
|
|
8864
9368
|
async deleteEvalResult(id) {
|
|
8865
9369
|
await this.getEvalHistory();
|
|
9370
|
+
const existing = this.evalHistory.find((e) => e.id === id);
|
|
9371
|
+
const evalName = existing?.eval;
|
|
8866
9372
|
const beforeLength = this.evalHistory.length;
|
|
8867
9373
|
this.evalHistory = this.evalHistory.filter((e) => e.id !== id);
|
|
8868
9374
|
const removedFromMemory = this.evalHistory.length < beforeLength;
|
|
@@ -8870,7 +9376,13 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
8870
9376
|
if (this.stateStore.deleteEvalResult) {
|
|
8871
9377
|
removedFromStore = await this.stateStore.deleteEvalResult(id);
|
|
8872
9378
|
}
|
|
8873
|
-
|
|
9379
|
+
const removed = removedFromMemory || removedFromStore;
|
|
9380
|
+
this.emit("eval_deleted", {
|
|
9381
|
+
id,
|
|
9382
|
+
eval: evalName,
|
|
9383
|
+
removed
|
|
9384
|
+
});
|
|
9385
|
+
return removed;
|
|
8874
9386
|
}
|
|
8875
9387
|
/** Get eval result history (most recent first). */
|
|
8876
9388
|
async getEvalHistory() {
|
|
@@ -9460,15 +9972,53 @@ async function* stringStreamFromEvents(source, opts) {
|
|
|
9460
9972
|
|
|
9461
9973
|
// src/state/redis.ts
|
|
9462
9974
|
var RedisStore = class _RedisStore {
|
|
9463
|
-
constructor(client) {
|
|
9975
|
+
constructor(client, keyPrefix, ttls) {
|
|
9464
9976
|
this.client = client;
|
|
9977
|
+
this.keyPrefix = keyPrefix;
|
|
9978
|
+
this.ttls = ttls;
|
|
9979
|
+
}
|
|
9980
|
+
/**
|
|
9981
|
+
* Resolve the TTL for a given storage category. Returns `undefined` when
|
|
9982
|
+
* no TTL applies (category is `null` in resolved config, the value is
|
|
9983
|
+
* non-positive — Redis EXPIRE 0 immediately deletes, which is almost
|
|
9984
|
+
* never intended — or the value is non-finite).
|
|
9985
|
+
*
|
|
9986
|
+
* Non-finite values (`NaN`, `±Infinity`) defensively map to `undefined`
|
|
9987
|
+
* rather than being passed to Redis, where they'd surface as a wire-level
|
|
9988
|
+
* protocol error at the worst possible time (first write under load).
|
|
9989
|
+
*/
|
|
9990
|
+
ttlFor(category) {
|
|
9991
|
+
const resolved = this.ttls[category];
|
|
9992
|
+
if (resolved === null) return void 0;
|
|
9993
|
+
if (!Number.isFinite(resolved) || resolved <= 0) return void 0;
|
|
9994
|
+
return Math.floor(resolved);
|
|
9465
9995
|
}
|
|
9466
9996
|
/**
|
|
9467
9997
|
* Create a connected RedisStore instance.
|
|
9468
9998
|
*
|
|
9469
|
-
*
|
|
9999
|
+
* Accepts either a URL string (back-compat) or a {@link RedisStoreOptions}
|
|
10000
|
+
* object. The object form is required to set a custom `keyPrefix`.
|
|
10001
|
+
*
|
|
10002
|
+
* @example
|
|
10003
|
+
* ```ts
|
|
10004
|
+
* // Default prefix 'axl:'
|
|
10005
|
+
* const store = await RedisStore.create('redis://localhost:6379');
|
|
10006
|
+
*
|
|
10007
|
+
* // Custom prefix for shared-cluster deployments
|
|
10008
|
+
* const store = await RedisStore.create({
|
|
10009
|
+
* url: 'redis://localhost:6379',
|
|
10010
|
+
* keyPrefix: 'axl:prod:',
|
|
10011
|
+
* });
|
|
10012
|
+
* ```
|
|
9470
10013
|
*/
|
|
9471
|
-
static async create(
|
|
10014
|
+
static async create(options) {
|
|
10015
|
+
const opts = typeof options === "string" ? { url: options } : options ?? {};
|
|
10016
|
+
const keyPrefix = opts.keyPrefix ?? "axl:";
|
|
10017
|
+
if (keyPrefix === "") {
|
|
10018
|
+
throw new Error(
|
|
10019
|
+
'RedisStore: keyPrefix cannot be empty string. Omit it for the default "axl:" prefix.'
|
|
10020
|
+
);
|
|
10021
|
+
}
|
|
9472
10022
|
let createClient;
|
|
9473
10023
|
try {
|
|
9474
10024
|
const mod = require("redis");
|
|
@@ -9482,44 +10032,197 @@ var RedisStore = class _RedisStore {
|
|
|
9482
10032
|
if (err instanceof Error && err.message.includes("createClient")) throw err;
|
|
9483
10033
|
throw new Error("redis is required for RedisStore. Install it with: npm install redis");
|
|
9484
10034
|
}
|
|
9485
|
-
const client = url ? createClient({ url }) : createClient();
|
|
10035
|
+
const client = opts.url ? createClient({ url: opts.url }) : createClient();
|
|
9486
10036
|
await client.connect();
|
|
9487
|
-
|
|
10037
|
+
const resolveCategory = (override) => {
|
|
10038
|
+
if (override === null) return null;
|
|
10039
|
+
if (typeof override === "number") return override;
|
|
10040
|
+
return typeof opts.defaultTtl === "number" ? opts.defaultTtl : null;
|
|
10041
|
+
};
|
|
10042
|
+
const resolveStreamingEvents = (override) => {
|
|
10043
|
+
if (override === null || override === void 0) return null;
|
|
10044
|
+
if (typeof override === "number") return override;
|
|
10045
|
+
return null;
|
|
10046
|
+
};
|
|
10047
|
+
const effectiveTtls = {
|
|
10048
|
+
session: resolveCategory(opts.ttls?.session),
|
|
10049
|
+
sessionMeta: resolveCategory(opts.ttls?.sessionMeta),
|
|
10050
|
+
checkpoint: resolveCategory(opts.ttls?.checkpoint),
|
|
10051
|
+
executionState: resolveCategory(opts.ttls?.executionState),
|
|
10052
|
+
executionHistory: resolveCategory(opts.ttls?.executionHistory),
|
|
10053
|
+
evalHistory: resolveCategory(opts.ttls?.evalHistory),
|
|
10054
|
+
memory: resolveCategory(opts.ttls?.memory),
|
|
10055
|
+
streamingEvents: resolveStreamingEvents(opts.ttls?.streamingEvents)
|
|
10056
|
+
};
|
|
10057
|
+
const store = new _RedisStore(client, keyPrefix, effectiveTtls);
|
|
10058
|
+
if (!opts.skipMigration) {
|
|
10059
|
+
await store.backfillExecutionIndex();
|
|
10060
|
+
await store.backfillEvalIndex();
|
|
10061
|
+
}
|
|
10062
|
+
return store;
|
|
10063
|
+
}
|
|
10064
|
+
/**
|
|
10065
|
+
* Backfill the execution-history sorted-set from the legacy ID set.
|
|
10066
|
+
* Idempotent — runs only if the ZSET is missing entries the SET has.
|
|
10067
|
+
* Safe to call manually if you constructed the store with
|
|
10068
|
+
* `skipMigration: true`.
|
|
10069
|
+
*/
|
|
10070
|
+
async backfillExecutionIndex() {
|
|
10071
|
+
await this.backfillHistoryIndex(
|
|
10072
|
+
this.execHistorySetKey(),
|
|
10073
|
+
this.execHistoryZsetKey(),
|
|
10074
|
+
(id) => this.execHistoryKey(id),
|
|
10075
|
+
(raw) => {
|
|
10076
|
+
const exec = JSON.parse(raw);
|
|
10077
|
+
return typeof exec.startedAt === "number" ? { score: exec.startedAt, value: exec.executionId } : null;
|
|
10078
|
+
},
|
|
10079
|
+
"execution-history"
|
|
10080
|
+
);
|
|
10081
|
+
}
|
|
10082
|
+
/** Backfill the eval-history sorted-set. Symmetric to `backfillExecutionIndex`. */
|
|
10083
|
+
async backfillEvalIndex() {
|
|
10084
|
+
await this.backfillHistoryIndex(
|
|
10085
|
+
this.evalHistorySetKey(),
|
|
10086
|
+
this.evalHistoryZsetKey(),
|
|
10087
|
+
(id) => this.evalHistoryKey(id),
|
|
10088
|
+
(raw) => {
|
|
10089
|
+
const entry = JSON.parse(raw);
|
|
10090
|
+
return typeof entry.timestamp === "number" ? { score: entry.timestamp, value: entry.id } : null;
|
|
10091
|
+
},
|
|
10092
|
+
"eval-history"
|
|
10093
|
+
);
|
|
10094
|
+
}
|
|
10095
|
+
/**
|
|
10096
|
+
* Shared implementation for both backfill paths. Reads SET members,
|
|
10097
|
+
* batch-MGETs their data blobs in chunks, and ZADDs each into the new
|
|
10098
|
+
* sorted-set inside a single MULTI per chunk.
|
|
10099
|
+
*/
|
|
10100
|
+
async backfillHistoryIndex(setKey, zsetKey, dataKey, scoreOf, label) {
|
|
10101
|
+
const [setSize, zsetSize] = await Promise.all([
|
|
10102
|
+
this.client.sCard(setKey),
|
|
10103
|
+
this.client.zCard(zsetKey)
|
|
10104
|
+
]);
|
|
10105
|
+
if (zsetSize >= setSize) return;
|
|
10106
|
+
const allIds = await this.client.sMembers(setKey);
|
|
10107
|
+
if (allIds.length === 0) return;
|
|
10108
|
+
const CHUNK_SIZE = 500;
|
|
10109
|
+
let migrated = 0;
|
|
10110
|
+
let skipped = 0;
|
|
10111
|
+
for (let i = 0; i < allIds.length; i += CHUNK_SIZE) {
|
|
10112
|
+
const chunk = allIds.slice(i, i + CHUNK_SIZE);
|
|
10113
|
+
const dataKeys = chunk.map((id) => dataKey(id));
|
|
10114
|
+
const raws = await this.client.mGet(dataKeys);
|
|
10115
|
+
const tx = this.client.multi();
|
|
10116
|
+
let queuedInChunk = 0;
|
|
10117
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
10118
|
+
const raw = raws[j];
|
|
10119
|
+
if (raw == null) {
|
|
10120
|
+
skipped++;
|
|
10121
|
+
continue;
|
|
10122
|
+
}
|
|
10123
|
+
try {
|
|
10124
|
+
const entry = scoreOf(raw);
|
|
10125
|
+
if (entry == null) {
|
|
10126
|
+
skipped++;
|
|
10127
|
+
continue;
|
|
10128
|
+
}
|
|
10129
|
+
tx.zAdd(zsetKey, entry);
|
|
10130
|
+
queuedInChunk++;
|
|
10131
|
+
migrated++;
|
|
10132
|
+
} catch {
|
|
10133
|
+
skipped++;
|
|
10134
|
+
}
|
|
10135
|
+
}
|
|
10136
|
+
if (queuedInChunk > 0) await tx.exec();
|
|
10137
|
+
}
|
|
10138
|
+
if (migrated > 0 || skipped > 0) {
|
|
10139
|
+
console.log(
|
|
10140
|
+
`[axl] RedisStore: backfilled ${migrated} ${label} entries into sorted-set index` + (skipped > 0 ? ` (${skipped} skipped \u2014 missing or malformed)` : "")
|
|
10141
|
+
);
|
|
10142
|
+
}
|
|
9488
10143
|
}
|
|
9489
10144
|
// ── Key helpers ──────────────────────────────────────────────────────
|
|
9490
10145
|
checkpointKey(executionId) {
|
|
9491
|
-
return
|
|
10146
|
+
return `${this.keyPrefix}checkpoint:${executionId}`;
|
|
9492
10147
|
}
|
|
9493
10148
|
sessionKey(sessionId) {
|
|
9494
|
-
return
|
|
10149
|
+
return `${this.keyPrefix}session:${sessionId}`;
|
|
9495
10150
|
}
|
|
9496
10151
|
sessionMetaKey(sessionId) {
|
|
9497
|
-
return
|
|
10152
|
+
return `${this.keyPrefix}session-meta:${sessionId}`;
|
|
10153
|
+
}
|
|
10154
|
+
sessionIdsKey() {
|
|
10155
|
+
return `${this.keyPrefix}session-ids`;
|
|
9498
10156
|
}
|
|
9499
10157
|
decisionsKey() {
|
|
9500
|
-
return
|
|
10158
|
+
return `${this.keyPrefix}decisions`;
|
|
9501
10159
|
}
|
|
9502
10160
|
executionStateKey(executionId) {
|
|
9503
|
-
return
|
|
10161
|
+
return `${this.keyPrefix}exec-state:${executionId}`;
|
|
9504
10162
|
}
|
|
9505
10163
|
pendingExecSetKey() {
|
|
9506
|
-
return
|
|
10164
|
+
return `${this.keyPrefix}pending-executions`;
|
|
9507
10165
|
}
|
|
9508
10166
|
execHistoryKey(executionId) {
|
|
9509
|
-
return
|
|
9510
|
-
}
|
|
10167
|
+
return `${this.keyPrefix}exec-history:${executionId}`;
|
|
10168
|
+
}
|
|
10169
|
+
// NOTE for future implementer of #3 (TTL config): when the executionHistory
|
|
10170
|
+
// and evalHistory data blobs gain TTLs, eviction MUST be driven by
|
|
10171
|
+
// ZREMRANGEBYSCORE on the ZSET below — NOT by SREM on these legacy ID sets.
|
|
10172
|
+
// The slow-path fallback (`listHistoryByZset`) reads from the SET when
|
|
10173
|
+
// `zsetCard < setCard`, so SET-based eviction would race with the fallback
|
|
10174
|
+
// and surface dead IDs whose data has been TTL'd away. ZSET eviction +
|
|
10175
|
+
// dual-write `SREM` is the clean path; doc the assumption then.
|
|
9511
10176
|
execHistorySetKey() {
|
|
9512
|
-
return
|
|
10177
|
+
return `${this.keyPrefix}exec-history-ids`;
|
|
10178
|
+
}
|
|
10179
|
+
/**
|
|
10180
|
+
* Sorted-set index for execution history, scored by `startedAt`. Drives
|
|
10181
|
+
* the O(log N) `listExecutions` fast path; the legacy SET above is kept
|
|
10182
|
+
* as a fallback for users running with `skipMigration: true` before
|
|
10183
|
+
* they've called `backfillExecutionIndex()` manually.
|
|
10184
|
+
*/
|
|
10185
|
+
execHistoryZsetKey() {
|
|
10186
|
+
return `${this.keyPrefix}exec-history-z`;
|
|
9513
10187
|
}
|
|
9514
10188
|
evalHistoryKey(id) {
|
|
9515
|
-
return
|
|
10189
|
+
return `${this.keyPrefix}eval-history:${id}`;
|
|
9516
10190
|
}
|
|
9517
10191
|
evalHistorySetKey() {
|
|
9518
|
-
return
|
|
10192
|
+
return `${this.keyPrefix}eval-history-ids`;
|
|
10193
|
+
}
|
|
10194
|
+
/** Sorted-set index for eval history, scored by `timestamp`. See above. */
|
|
10195
|
+
evalHistoryZsetKey() {
|
|
10196
|
+
return `${this.keyPrefix}eval-history-z`;
|
|
10197
|
+
}
|
|
10198
|
+
memoryKey(scope) {
|
|
10199
|
+
return `${this.keyPrefix}memory:${scope}`;
|
|
10200
|
+
}
|
|
10201
|
+
// ── Streaming event buffer (for `state.persist: 'streaming'`) ─────────
|
|
10202
|
+
// Layout:
|
|
10203
|
+
// `{prefix}exec-events:{id}` — a Redis list. Events are RPUSH'd as
|
|
10204
|
+
// JSON-serialized AxlEvent objects, in batches. The runtime LRANGEs
|
|
10205
|
+
// the whole list on recovery and deletes it via `finalizeStreamingEvents`
|
|
10206
|
+
// once the canonical `executionHistory` blob has been written at
|
|
10207
|
+
// terminal exit.
|
|
10208
|
+
// `{prefix}streaming-exec-ids` — a Redis set tracking which executions
|
|
10209
|
+
// have an active streaming buffer. Used by `listStreamingExecutions`
|
|
10210
|
+
// to find runs whose process died mid-flight (no corresponding
|
|
10211
|
+
// `executionHistory` blob).
|
|
10212
|
+
streamingEventsKey(executionId) {
|
|
10213
|
+
return `${this.keyPrefix}exec-events:${executionId}`;
|
|
10214
|
+
}
|
|
10215
|
+
streamingIdsKey() {
|
|
10216
|
+
return `${this.keyPrefix}streaming-exec-ids`;
|
|
9519
10217
|
}
|
|
9520
10218
|
// ── Checkpoints ──────────────────────────────────────────────────────
|
|
9521
10219
|
async saveCheckpoint(executionId, name, data) {
|
|
9522
|
-
|
|
10220
|
+
const ttl = this.ttlFor("checkpoint");
|
|
10221
|
+
if (ttl === void 0) {
|
|
10222
|
+
await this.client.hSet(this.checkpointKey(executionId), name, JSON.stringify(data));
|
|
10223
|
+
return;
|
|
10224
|
+
}
|
|
10225
|
+
await this.client.multi().hSet(this.checkpointKey(executionId), name, JSON.stringify(data)).expire(this.checkpointKey(executionId), ttl, "NX").exec();
|
|
9523
10226
|
}
|
|
9524
10227
|
async getCheckpoint(executionId, name) {
|
|
9525
10228
|
const raw = await this.client.hGet(this.checkpointKey(executionId), name);
|
|
@@ -9527,20 +10230,24 @@ var RedisStore = class _RedisStore {
|
|
|
9527
10230
|
}
|
|
9528
10231
|
// ── Sessions ─────────────────────────────────────────────────────────
|
|
9529
10232
|
async saveSession(sessionId, history) {
|
|
9530
|
-
|
|
9531
|
-
|
|
10233
|
+
const ttl = this.ttlFor("session");
|
|
10234
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10235
|
+
await this.client.multi().set(this.sessionKey(sessionId), JSON.stringify(history), setOptions).sAdd(this.sessionIdsKey(), sessionId).exec();
|
|
9532
10236
|
}
|
|
9533
10237
|
async getSession(sessionId) {
|
|
9534
10238
|
const raw = await this.client.get(this.sessionKey(sessionId));
|
|
9535
10239
|
return raw ? JSON.parse(raw) : [];
|
|
9536
10240
|
}
|
|
9537
10241
|
async deleteSession(sessionId) {
|
|
9538
|
-
await this.client.del(this.sessionKey(sessionId));
|
|
9539
|
-
await this.client.del(this.sessionMetaKey(sessionId));
|
|
9540
|
-
await this.client.sRem("axl:session-ids", sessionId);
|
|
10242
|
+
await this.client.multi().del(this.sessionKey(sessionId)).del(this.sessionMetaKey(sessionId)).sRem(this.sessionIdsKey(), sessionId).exec();
|
|
9541
10243
|
}
|
|
9542
10244
|
async saveSessionMeta(sessionId, key, value) {
|
|
9543
|
-
|
|
10245
|
+
const ttl = this.ttlFor("sessionMeta");
|
|
10246
|
+
if (ttl === void 0) {
|
|
10247
|
+
await this.client.hSet(this.sessionMetaKey(sessionId), key, JSON.stringify(value));
|
|
10248
|
+
return;
|
|
10249
|
+
}
|
|
10250
|
+
await this.client.multi().hSet(this.sessionMetaKey(sessionId), key, JSON.stringify(value)).expire(this.sessionMetaKey(sessionId), ttl).exec();
|
|
9544
10251
|
}
|
|
9545
10252
|
async getSessionMeta(sessionId, key) {
|
|
9546
10253
|
const raw = await this.client.hGet(this.sessionMetaKey(sessionId), key);
|
|
@@ -9560,64 +10267,222 @@ var RedisStore = class _RedisStore {
|
|
|
9560
10267
|
}
|
|
9561
10268
|
// ── Execution State ──────────────────────────────────────────────────
|
|
9562
10269
|
async saveExecutionState(executionId, state) {
|
|
9563
|
-
|
|
10270
|
+
const ttl = this.ttlFor("executionState");
|
|
10271
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10272
|
+
const tx = this.client.multi().set(this.executionStateKey(executionId), JSON.stringify(state), setOptions);
|
|
9564
10273
|
if (state.status === "waiting") {
|
|
9565
|
-
|
|
10274
|
+
tx.sAdd(this.pendingExecSetKey(), executionId);
|
|
9566
10275
|
} else {
|
|
9567
|
-
|
|
10276
|
+
tx.sRem(this.pendingExecSetKey(), executionId);
|
|
9568
10277
|
}
|
|
10278
|
+
await tx.exec();
|
|
9569
10279
|
}
|
|
9570
10280
|
async getExecutionState(executionId) {
|
|
9571
10281
|
const raw = await this.client.get(this.executionStateKey(executionId));
|
|
9572
10282
|
return raw ? JSON.parse(raw) : null;
|
|
9573
10283
|
}
|
|
9574
10284
|
async listPendingExecutions() {
|
|
9575
|
-
|
|
10285
|
+
const ids = await this.client.sMembers(this.pendingExecSetKey());
|
|
10286
|
+
if (ids.length === 0) return ids;
|
|
10287
|
+
const keys = ids.map((id) => this.executionStateKey(id));
|
|
10288
|
+
const raws = await this.client.mGet(keys);
|
|
10289
|
+
const live = [];
|
|
10290
|
+
const dead = [];
|
|
10291
|
+
for (let i = 0; i < ids.length; i++) {
|
|
10292
|
+
if (raws[i] != null) {
|
|
10293
|
+
live.push(ids[i]);
|
|
10294
|
+
} else {
|
|
10295
|
+
dead.push(ids[i]);
|
|
10296
|
+
}
|
|
10297
|
+
}
|
|
10298
|
+
if (dead.length > 0) {
|
|
10299
|
+
this.client.sRem(this.pendingExecSetKey(), dead).catch((err) => {
|
|
10300
|
+
console.error(
|
|
10301
|
+
`[axl] RedisStore: failed to prune ${dead.length} stale pending-exec ids: ` + (err instanceof Error ? err.message : String(err))
|
|
10302
|
+
);
|
|
10303
|
+
});
|
|
10304
|
+
}
|
|
10305
|
+
return live;
|
|
10306
|
+
}
|
|
10307
|
+
// ── Memory ──────────────────────────────────────────────────────────
|
|
10308
|
+
//
|
|
10309
|
+
// Layout: `{prefix}memory:{scope}` is a hash. Fields are user-supplied
|
|
10310
|
+
// keys; values are JSON-serialized. Hash-per-scope keeps related entries
|
|
10311
|
+
// together (good cohesion for TTL eviction once #3 lands) and bounds the
|
|
10312
|
+
// keyspace to O(scopes) instead of O(scopes × keys).
|
|
10313
|
+
//
|
|
10314
|
+
// Pre-this-patch, RedisStore had no memory methods, so MemoryManager fell
|
|
10315
|
+
// back to writing memory at synthetic sessionMeta keys
|
|
10316
|
+
// (`{prefix}session-meta:memory:{scope}:{key}` hash field `value`). To
|
|
10317
|
+
// preserve user data on upgrade, `getMemory` checks the legacy location on
|
|
10318
|
+
// miss and migrates forward; `deleteMemory` also cleans the legacy entry.
|
|
10319
|
+
// No legacy support on `getAllMemory` — that method didn't exist before,
|
|
10320
|
+
// so no user can depend on it returning legacy data.
|
|
10321
|
+
async saveMemory(scope, key, value) {
|
|
10322
|
+
const ttl = this.ttlFor("memory");
|
|
10323
|
+
if (ttl === void 0) {
|
|
10324
|
+
await this.client.hSet(this.memoryKey(scope), key, JSON.stringify(value));
|
|
10325
|
+
return;
|
|
10326
|
+
}
|
|
10327
|
+
await this.client.multi().hSet(this.memoryKey(scope), key, JSON.stringify(value)).expire(this.memoryKey(scope), ttl).exec();
|
|
10328
|
+
}
|
|
10329
|
+
async getMemory(scope, key) {
|
|
10330
|
+
const raw = await this.client.hGet(this.memoryKey(scope), key);
|
|
10331
|
+
if (raw != null) return JSON.parse(raw);
|
|
10332
|
+
const legacyRaw = await this.client.hGet(
|
|
10333
|
+
this.sessionMetaKey(`memory:${scope}:${key}`),
|
|
10334
|
+
"value"
|
|
10335
|
+
);
|
|
10336
|
+
if (legacyRaw == null) return null;
|
|
10337
|
+
const inserted = await this.client.hSetNX(this.memoryKey(scope), key, legacyRaw);
|
|
10338
|
+
if (inserted === 0 || inserted === false) {
|
|
10339
|
+
const winner = await this.client.hGet(this.memoryKey(scope), key);
|
|
10340
|
+
if (winner != null) return JSON.parse(winner);
|
|
10341
|
+
return null;
|
|
10342
|
+
}
|
|
10343
|
+
const ttl = this.ttlFor("memory");
|
|
10344
|
+
if (ttl !== void 0) {
|
|
10345
|
+
this.client.expire(this.memoryKey(scope), ttl).catch((err) => {
|
|
10346
|
+
console.error(
|
|
10347
|
+
`[axl] RedisStore: failed to set TTL on migrated memory key ${this.memoryKey(scope)}: ` + (err instanceof Error ? err.message : String(err))
|
|
10348
|
+
);
|
|
10349
|
+
});
|
|
10350
|
+
}
|
|
10351
|
+
return JSON.parse(legacyRaw);
|
|
10352
|
+
}
|
|
10353
|
+
async getAllMemory(scope) {
|
|
10354
|
+
const all = await this.client.hGetAll(this.memoryKey(scope));
|
|
10355
|
+
if (!all) return [];
|
|
10356
|
+
return Object.entries(all).map(([key, raw]) => ({ key, value: JSON.parse(raw) }));
|
|
10357
|
+
}
|
|
10358
|
+
async deleteMemory(scope, key) {
|
|
10359
|
+
await this.client.multi().hDel(this.memoryKey(scope), key).hDel(this.sessionMetaKey(`memory:${scope}:${key}`), "value").exec();
|
|
9576
10360
|
}
|
|
9577
10361
|
// ── Execution History ────────────────────────────────────────────────
|
|
9578
10362
|
async saveExecution(execution) {
|
|
9579
|
-
|
|
9580
|
-
|
|
10363
|
+
const ttl = this.ttlFor("executionHistory");
|
|
10364
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10365
|
+
await this.client.multi().sAdd(this.execHistorySetKey(), execution.executionId).set(this.execHistoryKey(execution.executionId), JSON.stringify(execution), setOptions).zAdd(this.execHistoryZsetKey(), {
|
|
10366
|
+
score: execution.startedAt,
|
|
10367
|
+
value: execution.executionId
|
|
10368
|
+
}).exec();
|
|
9581
10369
|
}
|
|
9582
10370
|
async getExecution(executionId) {
|
|
9583
10371
|
const raw = await this.client.get(this.execHistoryKey(executionId));
|
|
9584
10372
|
return raw ? JSON.parse(raw) : null;
|
|
9585
10373
|
}
|
|
9586
10374
|
async listExecutions(limit) {
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
10375
|
+
return this.listHistoryByZset(
|
|
10376
|
+
this.execHistorySetKey(),
|
|
10377
|
+
this.execHistoryZsetKey(),
|
|
10378
|
+
(id) => this.execHistoryKey(id),
|
|
10379
|
+
(a, b) => b.startedAt - a.startedAt,
|
|
10380
|
+
limit
|
|
10381
|
+
);
|
|
10382
|
+
}
|
|
10383
|
+
async deleteExecution(executionId) {
|
|
10384
|
+
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();
|
|
10385
|
+
const deleted = typeof deletedCount === "number" ? deletedCount : 0;
|
|
10386
|
+
return deleted > 0;
|
|
9596
10387
|
}
|
|
9597
10388
|
// ── Eval History ────────────────────────────────────────────────────
|
|
9598
10389
|
async saveEvalResult(entry) {
|
|
9599
|
-
|
|
9600
|
-
|
|
10390
|
+
const ttl = this.ttlFor("evalHistory");
|
|
10391
|
+
const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
|
|
10392
|
+
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();
|
|
9601
10393
|
}
|
|
9602
10394
|
async listEvalResults(limit) {
|
|
9603
|
-
|
|
10395
|
+
return this.listHistoryByZset(
|
|
10396
|
+
this.evalHistorySetKey(),
|
|
10397
|
+
this.evalHistoryZsetKey(),
|
|
10398
|
+
(id) => this.evalHistoryKey(id),
|
|
10399
|
+
(a, b) => b.timestamp - a.timestamp,
|
|
10400
|
+
limit
|
|
10401
|
+
);
|
|
10402
|
+
}
|
|
10403
|
+
async deleteEvalResult(id) {
|
|
10404
|
+
const [, deletedCount] = await this.client.multi().sRem(this.evalHistorySetKey(), id).del(this.evalHistoryKey(id)).zRem(this.evalHistoryZsetKey(), id).exec();
|
|
10405
|
+
const deleted = typeof deletedCount === "number" ? deletedCount : 0;
|
|
10406
|
+
return deleted > 0;
|
|
10407
|
+
}
|
|
10408
|
+
/**
|
|
10409
|
+
* Shared list-by-sorted-set implementation. Uses ZREVRANGEBYSCORE +
|
|
10410
|
+
* MGET when the ZSET is populated (post-migration). Falls back to the
|
|
10411
|
+
* legacy SET + N×GET + JS-sort path when the ZSET is empty or smaller
|
|
10412
|
+
* than the SET (covers users that constructed with `skipMigration: true`
|
|
10413
|
+
* before calling backfill).
|
|
10414
|
+
*/
|
|
10415
|
+
async listHistoryByZset(setKey, zsetKey, dataKey, sortCmp, limit) {
|
|
10416
|
+
const setSize = await this.client.sCard(setKey);
|
|
10417
|
+
if (setSize === 0) return [];
|
|
10418
|
+
const zsetSize = await this.client.zCard(zsetKey);
|
|
10419
|
+
if (zsetSize >= setSize) {
|
|
10420
|
+
const fetchCount = limit !== void 0 ? Math.max(limit * 2, limit + 5) : void 0;
|
|
10421
|
+
const ids2 = await this.client.zRange(zsetKey, "+inf", "-inf", {
|
|
10422
|
+
BY: "SCORE",
|
|
10423
|
+
REV: true,
|
|
10424
|
+
...fetchCount !== void 0 ? { LIMIT: { offset: 0, count: fetchCount } } : {}
|
|
10425
|
+
});
|
|
10426
|
+
if (ids2.length === 0) return [];
|
|
10427
|
+
const keys = ids2.map(dataKey);
|
|
10428
|
+
const raws = await this.client.mGet(keys);
|
|
10429
|
+
const entries2 = [];
|
|
10430
|
+
for (const raw of raws) {
|
|
10431
|
+
if (raw == null) continue;
|
|
10432
|
+
try {
|
|
10433
|
+
entries2.push(JSON.parse(raw));
|
|
10434
|
+
} catch {
|
|
10435
|
+
}
|
|
10436
|
+
if (limit !== void 0 && entries2.length >= limit) break;
|
|
10437
|
+
}
|
|
10438
|
+
return entries2;
|
|
10439
|
+
}
|
|
10440
|
+
const ids = await this.client.sMembers(setKey);
|
|
9604
10441
|
if (ids.length === 0) return [];
|
|
9605
10442
|
const entries = [];
|
|
9606
10443
|
for (const id of ids) {
|
|
9607
|
-
const raw = await this.client.get(
|
|
9608
|
-
if (raw)
|
|
10444
|
+
const raw = await this.client.get(dataKey(id));
|
|
10445
|
+
if (raw == null) continue;
|
|
10446
|
+
try {
|
|
10447
|
+
entries.push(JSON.parse(raw));
|
|
10448
|
+
} catch {
|
|
10449
|
+
}
|
|
9609
10450
|
}
|
|
9610
|
-
entries.sort(
|
|
10451
|
+
entries.sort(sortCmp);
|
|
9611
10452
|
return limit ? entries.slice(0, limit) : entries;
|
|
9612
10453
|
}
|
|
9613
|
-
async deleteEvalResult(id) {
|
|
9614
|
-
await this.client.sRem(this.evalHistorySetKey(), id);
|
|
9615
|
-
const deleted = await this.client.del(this.evalHistoryKey(id));
|
|
9616
|
-
return deleted > 0;
|
|
9617
|
-
}
|
|
9618
10454
|
// ── Sessions (Studio introspection) ────────────────────────────────────
|
|
9619
10455
|
async listSessions() {
|
|
9620
|
-
return this.client.sMembers(
|
|
10456
|
+
return this.client.sMembers(this.sessionIdsKey());
|
|
10457
|
+
}
|
|
10458
|
+
// ── Streaming events (for state.persist: 'streaming') ──────────────────
|
|
10459
|
+
async appendStreamingEvents(executionId, events) {
|
|
10460
|
+
if (events.length === 0) return;
|
|
10461
|
+
const serialized = events.map((e) => JSON.stringify(e));
|
|
10462
|
+
const ttl = this.ttlFor("streamingEvents");
|
|
10463
|
+
const tx = this.client.multi().rPush(this.streamingEventsKey(executionId), serialized).sAdd(this.streamingIdsKey(), executionId);
|
|
10464
|
+
if (ttl !== void 0) {
|
|
10465
|
+
tx.expire(this.streamingEventsKey(executionId), ttl, "NX");
|
|
10466
|
+
}
|
|
10467
|
+
await tx.exec();
|
|
10468
|
+
}
|
|
10469
|
+
async finalizeStreamingEvents(executionId) {
|
|
10470
|
+
await this.client.multi().del(this.streamingEventsKey(executionId)).sRem(this.streamingIdsKey(), executionId).exec();
|
|
10471
|
+
}
|
|
10472
|
+
async listStreamingExecutions() {
|
|
10473
|
+
return this.client.sMembers(this.streamingIdsKey());
|
|
10474
|
+
}
|
|
10475
|
+
async getStreamingEvents(executionId) {
|
|
10476
|
+
const raws = await this.client.lRange(this.streamingEventsKey(executionId), 0, -1);
|
|
10477
|
+
if (raws.length === 0) return [];
|
|
10478
|
+
const events = [];
|
|
10479
|
+
for (const raw of raws) {
|
|
10480
|
+
try {
|
|
10481
|
+
events.push(JSON.parse(raw));
|
|
10482
|
+
} catch {
|
|
10483
|
+
}
|
|
10484
|
+
}
|
|
10485
|
+
return events;
|
|
9621
10486
|
}
|
|
9622
10487
|
/** Close the Redis connection. */
|
|
9623
10488
|
async close() {
|