@statelyai/agent 2.0.0-alpha.11 → 2.0.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-sdk.cjs +4 -5
- package/dist/ai-sdk.d.cts +7 -4
- package/dist/ai-sdk.d.mts +7 -4
- package/dist/ai-sdk.mjs +1 -2
- package/dist/{events-JiVPYrct.mjs → decision-BezSD_YC.mjs} +327 -20
- package/dist/{events-CRQj3VtP.cjs → decision-dWGhBh0P.cjs} +401 -28
- package/dist/errors-BQRk9eiZ.d.cts +19 -0
- package/dist/errors-C9rxnWbX.d.mts +19 -0
- package/dist/errors-CeSXQx0v.mjs +23 -0
- package/dist/errors-DUBBzRLP.cjs +28 -0
- package/dist/event-log-store-CNT_7F0V.cjs +452 -0
- package/dist/event-log-store-CriMgX1D.d.mts +144 -0
- package/dist/event-log-store-D7pWtIhb.mjs +411 -0
- package/dist/event-log-store-Ruq18mGp.d.cts +144 -0
- package/dist/index.cjs +1050 -705
- package/dist/index.d.cts +538 -565
- package/dist/index.d.mts +538 -565
- package/dist/index.mjs +950 -644
- package/dist/machines.cjs +752 -0
- package/dist/machines.d.cts +372 -0
- package/dist/machines.d.mts +372 -0
- package/dist/machines.mjs +741 -0
- package/dist/otel.cjs +268 -0
- package/dist/otel.d.cts +67 -0
- package/dist/otel.d.mts +67 -0
- package/dist/otel.mjs +267 -0
- package/dist/run-agent-C3mFDGTf.d.mts +1111 -0
- package/dist/run-agent-DnvtcnTZ.d.cts +1111 -0
- package/dist/setup-agent-DAZZSjDS.mjs +1711 -0
- package/dist/setup-agent-DP95MFrI.cjs +1836 -0
- package/dist/sqlite.cjs +135 -0
- package/dist/sqlite.d.cts +57 -0
- package/dist/sqlite.d.mts +57 -0
- package/dist/sqlite.mjs +133 -0
- package/dist/{text-logic-CaKqgX4Y.d.mts → text-logic-BDxwQNsD.d.cts} +155 -72
- package/dist/{text-logic-Ckhr2kKC.d.cts → text-logic-TkKPw8Aq.d.mts} +155 -72
- package/dist/{types-qm00QF91.d.mts → types-QbEfCVny.d.cts} +1 -1
- package/dist/{types-C9QiMjre.d.cts → types-_FXoFBGO.d.mts} +1 -1
- package/package.json +47 -39
- package/readme.md +49 -12
- package/schemas/agent-workflow.json +40 -21
- package/skills/generate-machine/SKILL.md +267 -0
- package/dist/adapter.cjs +0 -15
- package/dist/adapter.d.cts +0 -4
- package/dist/adapter.d.mts +0 -4
- package/dist/adapter.mjs +0 -2
- package/dist/decision-C3k4ve51.mjs +0 -227
- package/dist/decision-D8wJrM8W.cjs +0 -286
- package/dist/openai-compat.cjs +0 -309
- package/dist/openai-compat.d.cts +0 -59
- package/dist/openai-compat.d.mts +0 -59
- package/dist/openai-compat.mjs +0 -308
- package/dist/steps-BALp1eZo.d.mts +0 -198
- package/dist/steps-CVe54GPP.cjs +0 -420
- package/dist/steps-CkyyyuHd.mjs +0 -379
- package/dist/steps-MjnQI4aB.d.cts +0 -198
- package/dist/steps.cjs +0 -12
- package/dist/steps.d.cts +0 -3
- package/dist/steps.d.mts +0 -3
- package/dist/steps.mjs +0 -3
- package/dist/utils-BYqT_Dyv.d.cts +0 -108
- package/dist/utils-Do5wIJrh.d.mts +0 -108
- package/dist/zod.cjs +0 -31
- package/dist/zod.d.cts +0 -30
- package/dist/zod.d.mts +0 -30
- package/dist/zod.mjs +0 -30
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { t as AgentError } from "./errors-CeSXQx0v.mjs";
|
|
2
|
+
//#region src/event-log-store.ts
|
|
3
|
+
/** The durable replay-entry envelope version. */
|
|
4
|
+
const AGENT_EVENT_SCHEMA_VERSION = 1;
|
|
5
|
+
/** A precise failure when an entry would not survive a JSON round-trip. */
|
|
6
|
+
var NonSerializableAgentEventError = class extends AgentError {
|
|
7
|
+
path;
|
|
8
|
+
valueType;
|
|
9
|
+
constructor(path, valueType) {
|
|
10
|
+
super("non-serializable-event", `Agent event field '${path}' is not JSON-serializable (${valueType}).`);
|
|
11
|
+
this.name = "NonSerializableAgentEventError";
|
|
12
|
+
this.path = path;
|
|
13
|
+
this.valueType = valueType;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Rejects values JSON would drop or coerce. Unlike `JSON.stringify`, this does
|
|
18
|
+
* not silently erase `undefined`/functions or turn non-finite numbers into
|
|
19
|
+
* `null`; durable entries contain only plain JSON values.
|
|
20
|
+
*/
|
|
21
|
+
function assertJsonSerializable(value, path = "entry", ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
22
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
23
|
+
if (typeof value === "number") {
|
|
24
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) throw new NonSerializableAgentEventError(path, Object.is(value, -0) ? "-0" : String(value));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value !== "object") throw new NonSerializableAgentEventError(path, typeof value);
|
|
28
|
+
if (ancestors.has(value)) throw new NonSerializableAgentEventError(path, "circular reference");
|
|
29
|
+
ancestors.add(value);
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
for (let index = 0; index < value.length; index++) {
|
|
32
|
+
if (!Object.hasOwn(value, index)) throw new NonSerializableAgentEventError(`${path}[${index}]`, "array hole");
|
|
33
|
+
assertJsonSerializable(value[index], `${path}[${index}]`, ancestors);
|
|
34
|
+
}
|
|
35
|
+
const extraKey = Object.keys(value).find((key) => !/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= value.length);
|
|
36
|
+
if (extraKey !== void 0) throw new NonSerializableAgentEventError(`${path}.${extraKey}`, "array property");
|
|
37
|
+
const symbols = Object.getOwnPropertySymbols(value);
|
|
38
|
+
if (symbols.length > 0) throw new NonSerializableAgentEventError(`${path}.[${String(symbols[0])}]`, "symbol key");
|
|
39
|
+
const hiddenKey = Object.getOwnPropertyNames(value).find((key) => key !== "length" && !/^(?:0|[1-9]\d*)$/.test(key) && !Object.getOwnPropertyDescriptor(value, key)?.enumerable);
|
|
40
|
+
if (hiddenKey !== void 0) throw new NonSerializableAgentEventError(`${path}.${hiddenKey}`, "non-enumerable property");
|
|
41
|
+
ancestors.delete(value);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const prototype = Object.getPrototypeOf(value);
|
|
45
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
46
|
+
const type = value.constructor?.name ?? "object";
|
|
47
|
+
ancestors.delete(value);
|
|
48
|
+
throw new NonSerializableAgentEventError(path, type);
|
|
49
|
+
}
|
|
50
|
+
const symbols = Object.getOwnPropertySymbols(value);
|
|
51
|
+
if (symbols.length > 0) {
|
|
52
|
+
ancestors.delete(value);
|
|
53
|
+
throw new NonSerializableAgentEventError(`${path}.[${String(symbols[0])}]`, "symbol key");
|
|
54
|
+
}
|
|
55
|
+
const hiddenKey = Object.getOwnPropertyNames(value).find((key) => !Object.getOwnPropertyDescriptor(value, key)?.enumerable);
|
|
56
|
+
if (hiddenKey !== void 0) {
|
|
57
|
+
ancestors.delete(value);
|
|
58
|
+
throw new NonSerializableAgentEventError(`${path}.${hiddenKey}`, "non-enumerable property");
|
|
59
|
+
}
|
|
60
|
+
for (const [key, child] of Object.entries(value)) assertJsonSerializable(child, `${path}.${key}`, ancestors);
|
|
61
|
+
ancestors.delete(value);
|
|
62
|
+
}
|
|
63
|
+
/** Validates the complete durable envelope before append/export/replay. */
|
|
64
|
+
function assertAgentLogEntry(entry) {
|
|
65
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) throw new Error("Agent event entry must be an object.");
|
|
66
|
+
assertJsonSerializable(entry);
|
|
67
|
+
const candidate = entry;
|
|
68
|
+
if (candidate.schemaVersion !== 1) throw new Error(`Unsupported agent event schema version '${String(candidate.schemaVersion)}'; expected '1'.`);
|
|
69
|
+
if (!Number.isInteger(candidate.index) || candidate.index < 0) throw new Error(`Agent event entry.index must be a non-negative integer; got ${String(candidate.index)}.`);
|
|
70
|
+
if (typeof candidate.id !== "string" || !candidate.id || typeof candidate.machineId !== "string" || !candidate.machineId || typeof candidate.machineVersion !== "string" || !candidate.machineVersion) throw new Error("Agent event entry requires non-empty id, machineId, and machineVersion.");
|
|
71
|
+
if (typeof candidate.recordedAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(candidate.recordedAt) || Number.isNaN(Date.parse(candidate.recordedAt))) throw new Error(`Agent event entry.recordedAt is not RFC 3339: '${String(candidate.recordedAt)}'.`);
|
|
72
|
+
if (candidate.event === null || typeof candidate.event !== "object" || Array.isArray(candidate.event) || typeof candidate.event.type !== "string" || !candidate.event.type) throw new Error("Agent event entry.event requires a non-empty string type.");
|
|
73
|
+
if (candidate.causationId !== void 0 && (typeof candidate.causationId !== "string" || !candidate.causationId)) throw new Error("Agent event entry.causationId must be a non-empty string when present.");
|
|
74
|
+
if (candidate.correlationId !== void 0 && (typeof candidate.correlationId !== "string" || !candidate.correlationId)) throw new Error("Agent event entry.correlationId must be a non-empty string when present.");
|
|
75
|
+
if (candidate.metadata !== void 0 && (candidate.metadata === null || typeof candidate.metadata !== "object" || Array.isArray(candidate.metadata))) throw new Error("Agent event entry.metadata must be an object when present.");
|
|
76
|
+
if (candidate.verification !== void 0 && (candidate.verification === null || typeof candidate.verification !== "object" || typeof candidate.verification.stateHash !== "string" || !candidate.verification.stateHash || typeof candidate.verification.effectsHash !== "string" || !candidate.verification.effectsHash)) throw new Error("Agent event entry.verification requires non-empty stateHash and effectsHash strings.");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Rejection from {@link AgentEventLogStore.append} when the thread's stored
|
|
80
|
+
* length is not the `expectedIndex` the writer held — a concurrent writer
|
|
81
|
+
* appended first. `actualLength` is the length core found.
|
|
82
|
+
*/
|
|
83
|
+
var AgentEventLogConflictError = class extends AgentError {
|
|
84
|
+
threadId;
|
|
85
|
+
expectedIndex;
|
|
86
|
+
actualLength;
|
|
87
|
+
constructor(threadId, expectedIndex, actualLength) {
|
|
88
|
+
super("event-log-conflict", `AgentEventLogStore.append: length conflict on thread "${threadId}": expected length ${expectedIndex} but found ${actualLength} — a concurrent writer won.`);
|
|
89
|
+
this.name = "AgentEventLogConflictError";
|
|
90
|
+
this.threadId = threadId;
|
|
91
|
+
this.expectedIndex = expectedIndex;
|
|
92
|
+
this.actualLength = actualLength;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* In-memory reference store, and the baseline the conformance suite runs
|
|
97
|
+
* against. Per-thread entries are held in a contiguous, index-ordered list;
|
|
98
|
+
* append's check-and-push runs synchronously (no `await` between reading the
|
|
99
|
+
* length and writing), so two appends racing on the same `expectedIndex`
|
|
100
|
+
* resolve to exactly one winner. Every stored and returned entry is
|
|
101
|
+
* `structuredClone`d, so a caller can neither mutate stored state through a
|
|
102
|
+
* value it appended nor through a value it read.
|
|
103
|
+
*/
|
|
104
|
+
function createInMemoryEventLogStore() {
|
|
105
|
+
const threads = /* @__PURE__ */ new Map();
|
|
106
|
+
const clone = (value) => structuredClone(value);
|
|
107
|
+
return {
|
|
108
|
+
async append({ threadId, expectedIndex, entries }) {
|
|
109
|
+
for (const entry of entries) assertAgentLogEntry(entry);
|
|
110
|
+
for (let i = 0; i < entries.length; i++) if (entries[i].index !== expectedIndex + i) throw new Error(`AgentEventLogStore.append: entry.index (${entries[i].index}) must be contiguous from expectedIndex (${expectedIndex}); expected ${expectedIndex + i} at position ${i} on thread "${threadId}".`);
|
|
111
|
+
const list = threads.get(threadId);
|
|
112
|
+
const length = list ? list.length : 0;
|
|
113
|
+
if (length !== expectedIndex) throw new AgentEventLogConflictError(threadId, expectedIndex, length);
|
|
114
|
+
const ids = new Set((list ?? []).map((entry) => entry.id));
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (ids.has(entry.id)) throw new Error(`AgentEventLogStore.append: duplicate event id "${entry.id}" in thread "${threadId}".`);
|
|
117
|
+
ids.add(entry.id);
|
|
118
|
+
}
|
|
119
|
+
const cloned = entries.map((entry) => clone(entry));
|
|
120
|
+
if (list) for (const entry of cloned) list.push(entry);
|
|
121
|
+
else threads.set(threadId, cloned);
|
|
122
|
+
},
|
|
123
|
+
async read(threadId, options) {
|
|
124
|
+
const list = threads.get(threadId) ?? [];
|
|
125
|
+
const from = options?.from ?? 0;
|
|
126
|
+
return list.slice(from).map((entry) => clone(entry));
|
|
127
|
+
},
|
|
128
|
+
async length(threadId) {
|
|
129
|
+
return threads.get(threadId)?.length ?? 0;
|
|
130
|
+
},
|
|
131
|
+
async fork({ threadId, newThreadId, upToIndex, atEventId }) {
|
|
132
|
+
if ((threads.get(newThreadId)?.length ?? 0) > 0) throw new Error(`AgentEventLogStore.fork: newThreadId "${newThreadId}" already has entries.`);
|
|
133
|
+
const source = threads.get(threadId);
|
|
134
|
+
if (!source) throw new Error(`AgentEventLogStore.fork: unknown source thread "${threadId}".`);
|
|
135
|
+
if (upToIndex !== void 0 && atEventId !== void 0) throw new Error("AgentEventLogStore.fork: pass either upToIndex or atEventId, not both.");
|
|
136
|
+
const eventIndex = atEventId === void 0 ? void 0 : source.findIndex((entry) => entry.id === atEventId);
|
|
137
|
+
if (atEventId !== void 0 && eventIndex === -1) throw new Error(`AgentEventLogStore.fork: thread "${threadId}" has no event id "${atEventId}".`);
|
|
138
|
+
const upTo = eventIndex === void 0 ? upToIndex ?? source.length : eventIndex + 1;
|
|
139
|
+
if (upTo < 0 || upTo > source.length) throw new Error(`AgentEventLogStore.fork: thread "${threadId}" (length ${source.length}) has no index ${upTo} to fork up to.`);
|
|
140
|
+
threads.set(newThreadId, source.slice(0, upTo).map((entry) => clone(entry)));
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function fail(message) {
|
|
145
|
+
throw new Error(`event-log-store conformance: ${message}`);
|
|
146
|
+
}
|
|
147
|
+
function entry(index, type, metadata) {
|
|
148
|
+
return {
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
id: `evt_${index}`,
|
|
151
|
+
index,
|
|
152
|
+
recordedAt: "2026-01-01T00:00:00.000Z",
|
|
153
|
+
machineId: "conformance",
|
|
154
|
+
machineVersion: "v1",
|
|
155
|
+
event: {
|
|
156
|
+
type,
|
|
157
|
+
seq: index
|
|
158
|
+
},
|
|
159
|
+
...metadata !== void 0 ? { metadata } : {}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function entriesFrom(start, count) {
|
|
163
|
+
return Array.from({ length: count }, (_unused, i) => entry(start + i, `e${start + i}`));
|
|
164
|
+
}
|
|
165
|
+
function assertJsonEqual(actual, expected, message) {
|
|
166
|
+
const a = JSON.stringify(actual);
|
|
167
|
+
const b = JSON.stringify(expected);
|
|
168
|
+
if (a !== b) fail(`${message} (expected ${b}, got ${a})`);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Validates a store against the reference's semantics: empty read + zero length
|
|
172
|
+
* for unknown threads; single and multi-entry append; contiguity misuse guard;
|
|
173
|
+
* stale-`expectedIndex` conflict with correct fields; an interleaved concurrent
|
|
174
|
+
* append race (exactly one winner); event-id uniqueness; `read({ from })`
|
|
175
|
+
* incremental correctness; thread isolation; metadata round-trip; deep-copy
|
|
176
|
+
* isolation on append and read; and the full fork contract.
|
|
177
|
+
*/
|
|
178
|
+
async function assertEventLogStoreConformance(create) {
|
|
179
|
+
{
|
|
180
|
+
const store = await create();
|
|
181
|
+
assertJsonEqual(await store.read("missing"), [], "read of an unknown thread must be empty");
|
|
182
|
+
if (await store.length("missing") !== 0) fail("length of an unknown thread must be 0");
|
|
183
|
+
}
|
|
184
|
+
{
|
|
185
|
+
const store = await create();
|
|
186
|
+
const e = entry(0, "start", { label: "first" });
|
|
187
|
+
await store.append({
|
|
188
|
+
threadId: "t",
|
|
189
|
+
expectedIndex: 0,
|
|
190
|
+
entries: [e]
|
|
191
|
+
});
|
|
192
|
+
assertJsonEqual(await store.read("t"), [e], "read must return the appended entry");
|
|
193
|
+
if (await store.length("t") !== 1) fail("length after a single append must be 1");
|
|
194
|
+
}
|
|
195
|
+
{
|
|
196
|
+
const store = await create();
|
|
197
|
+
await store.append({
|
|
198
|
+
threadId: "t",
|
|
199
|
+
expectedIndex: 0,
|
|
200
|
+
entries: entriesFrom(0, 3)
|
|
201
|
+
});
|
|
202
|
+
await store.append({
|
|
203
|
+
threadId: "t",
|
|
204
|
+
expectedIndex: 3,
|
|
205
|
+
entries: entriesFrom(3, 2)
|
|
206
|
+
});
|
|
207
|
+
if (await store.length("t") !== 5) fail("length after appending 3 then 2 entries must be 5");
|
|
208
|
+
assertJsonEqual((await store.read("t")).map((e) => e.index), [
|
|
209
|
+
0,
|
|
210
|
+
1,
|
|
211
|
+
2,
|
|
212
|
+
3,
|
|
213
|
+
4
|
|
214
|
+
], "reads must return entries in contiguous log order");
|
|
215
|
+
}
|
|
216
|
+
{
|
|
217
|
+
const store = await create();
|
|
218
|
+
let caught;
|
|
219
|
+
try {
|
|
220
|
+
await store.append({
|
|
221
|
+
threadId: "t",
|
|
222
|
+
expectedIndex: 0,
|
|
223
|
+
entries: [entry(1, "gap")]
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
caught = error;
|
|
227
|
+
}
|
|
228
|
+
if (!(caught instanceof Error) || caught instanceof AgentEventLogConflictError) fail("a non-contiguous entry.index must throw a plain Error, not a conflict");
|
|
229
|
+
}
|
|
230
|
+
{
|
|
231
|
+
const store = await create();
|
|
232
|
+
await store.append({
|
|
233
|
+
threadId: "t",
|
|
234
|
+
expectedIndex: 0,
|
|
235
|
+
entries: [entry(0, "a")]
|
|
236
|
+
});
|
|
237
|
+
let caught;
|
|
238
|
+
try {
|
|
239
|
+
await store.append({
|
|
240
|
+
threadId: "t",
|
|
241
|
+
expectedIndex: 0,
|
|
242
|
+
entries: [entry(0, "b")]
|
|
243
|
+
});
|
|
244
|
+
} catch (error) {
|
|
245
|
+
caught = error;
|
|
246
|
+
}
|
|
247
|
+
if (!(caught instanceof AgentEventLogConflictError)) fail("a stale expectedIndex must throw AgentEventLogConflictError");
|
|
248
|
+
if (caught.threadId !== "t" || caught.expectedIndex !== 0 || caught.actualLength !== 1) fail("conflict error must carry threadId, expectedIndex, and the actual length");
|
|
249
|
+
}
|
|
250
|
+
{
|
|
251
|
+
const store = await create();
|
|
252
|
+
await store.append({
|
|
253
|
+
threadId: "t",
|
|
254
|
+
expectedIndex: 0,
|
|
255
|
+
entries: [entry(0, "a")]
|
|
256
|
+
});
|
|
257
|
+
let caught;
|
|
258
|
+
try {
|
|
259
|
+
await store.append({
|
|
260
|
+
threadId: "t",
|
|
261
|
+
expectedIndex: 1,
|
|
262
|
+
entries: [{
|
|
263
|
+
...entry(1, "b"),
|
|
264
|
+
id: "evt_0"
|
|
265
|
+
}]
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
caught = error;
|
|
269
|
+
}
|
|
270
|
+
if (!(caught instanceof Error) || caught instanceof AgentEventLogConflictError) fail("a duplicate event id within a thread must throw a plain Error");
|
|
271
|
+
}
|
|
272
|
+
{
|
|
273
|
+
const store = await create();
|
|
274
|
+
await store.append({
|
|
275
|
+
threadId: "t",
|
|
276
|
+
expectedIndex: 0,
|
|
277
|
+
entries: [entry(0, "a")]
|
|
278
|
+
});
|
|
279
|
+
const results = await Promise.allSettled([store.append({
|
|
280
|
+
threadId: "t",
|
|
281
|
+
expectedIndex: 1,
|
|
282
|
+
entries: [entry(1, "w-a")]
|
|
283
|
+
}), store.append({
|
|
284
|
+
threadId: "t",
|
|
285
|
+
expectedIndex: 1,
|
|
286
|
+
entries: [entry(1, "w-b")]
|
|
287
|
+
})]);
|
|
288
|
+
const winners = results.filter((r) => r.status === "fulfilled");
|
|
289
|
+
const conflicts = results.filter((r) => r.status === "rejected" && r.reason instanceof AgentEventLogConflictError);
|
|
290
|
+
if (winners.length !== 1) fail(`exactly one racing append must win, got ${winners.length}`);
|
|
291
|
+
if (conflicts.length !== 1) fail(`exactly one racing append must reject with a conflict, got ${conflicts.length}`);
|
|
292
|
+
if (await store.length("t") !== 2) fail("after the race the log length must be 2");
|
|
293
|
+
}
|
|
294
|
+
{
|
|
295
|
+
const store = await create();
|
|
296
|
+
await store.append({
|
|
297
|
+
threadId: "t",
|
|
298
|
+
expectedIndex: 0,
|
|
299
|
+
entries: entriesFrom(0, 5)
|
|
300
|
+
});
|
|
301
|
+
assertJsonEqual((await store.read("t", { from: 2 })).map((e) => e.index), [
|
|
302
|
+
2,
|
|
303
|
+
3,
|
|
304
|
+
4
|
|
305
|
+
], "read({ from }) must skip entries below `from`");
|
|
306
|
+
assertJsonEqual(await store.read("t", { from: 5 }), [], "read({ from }) at the log length must be empty");
|
|
307
|
+
}
|
|
308
|
+
{
|
|
309
|
+
const store = await create();
|
|
310
|
+
await store.append({
|
|
311
|
+
threadId: "a",
|
|
312
|
+
expectedIndex: 0,
|
|
313
|
+
entries: [entry(0, "x")]
|
|
314
|
+
});
|
|
315
|
+
if (await store.length("b") !== 0) fail("an append to one thread must not create another");
|
|
316
|
+
await store.append({
|
|
317
|
+
threadId: "b",
|
|
318
|
+
expectedIndex: 0,
|
|
319
|
+
entries: entriesFrom(0, 2)
|
|
320
|
+
});
|
|
321
|
+
if (await store.length("a") !== 1 || await store.length("b") !== 2) fail("threads must grow independently");
|
|
322
|
+
}
|
|
323
|
+
{
|
|
324
|
+
const store = await create();
|
|
325
|
+
const metadata = {
|
|
326
|
+
source: "user",
|
|
327
|
+
nested: { attempt: 2 },
|
|
328
|
+
tags: ["x", "y"]
|
|
329
|
+
};
|
|
330
|
+
await store.append({
|
|
331
|
+
threadId: "t",
|
|
332
|
+
expectedIndex: 0,
|
|
333
|
+
entries: [entry(0, "e", metadata)]
|
|
334
|
+
});
|
|
335
|
+
assertJsonEqual((await store.read("t"))[0].metadata, metadata, "metadata must round-trip verbatim");
|
|
336
|
+
}
|
|
337
|
+
{
|
|
338
|
+
const store = await create();
|
|
339
|
+
const e = entry(0, "e", { tags: ["a"] });
|
|
340
|
+
await store.append({
|
|
341
|
+
threadId: "t",
|
|
342
|
+
expectedIndex: 0,
|
|
343
|
+
entries: [e]
|
|
344
|
+
});
|
|
345
|
+
e.metadata.tags.push("mutated-after-append");
|
|
346
|
+
e.event.seq = 999;
|
|
347
|
+
const read = await store.read("t");
|
|
348
|
+
read[0].metadata.tags.push("mutated-after-read");
|
|
349
|
+
read[0].event.seq = 888;
|
|
350
|
+
const reread = await store.read("t");
|
|
351
|
+
assertJsonEqual(reread[0].metadata, { tags: ["a"] }, "stored entry must be isolated from post-append and post-read mutation");
|
|
352
|
+
assertJsonEqual(reread[0].event, {
|
|
353
|
+
type: "e",
|
|
354
|
+
seq: 0
|
|
355
|
+
}, "stored event must be isolated from mutation of an appended or read copy");
|
|
356
|
+
}
|
|
357
|
+
{
|
|
358
|
+
const store = await create();
|
|
359
|
+
await store.append({
|
|
360
|
+
threadId: "src",
|
|
361
|
+
expectedIndex: 0,
|
|
362
|
+
entries: entriesFrom(0, 3)
|
|
363
|
+
});
|
|
364
|
+
await store.fork({
|
|
365
|
+
threadId: "src",
|
|
366
|
+
newThreadId: "fork-full"
|
|
367
|
+
});
|
|
368
|
+
if (await store.length("fork-full") !== 3) fail("fork with the default upToIndex must copy the full log");
|
|
369
|
+
await store.fork({
|
|
370
|
+
threadId: "src",
|
|
371
|
+
newThreadId: "fork-1",
|
|
372
|
+
upToIndex: 1
|
|
373
|
+
});
|
|
374
|
+
assertJsonEqual((await store.read("fork-1")).map((e) => e.index), [0], "fork with upToIndex 1 must copy only entry 0");
|
|
375
|
+
await store.fork({
|
|
376
|
+
threadId: "src",
|
|
377
|
+
newThreadId: "fork-id",
|
|
378
|
+
atEventId: "evt_1"
|
|
379
|
+
});
|
|
380
|
+
assertJsonEqual((await store.read("fork-id")).map((e) => e.id), ["evt_0", "evt_1"], "fork with atEventId must include the named entry");
|
|
381
|
+
await store.append({
|
|
382
|
+
threadId: "fork-1",
|
|
383
|
+
expectedIndex: 1,
|
|
384
|
+
entries: [entry(1, "branch")]
|
|
385
|
+
});
|
|
386
|
+
assertJsonEqual((await store.read("fork-1")).map((e) => e.event.type), ["e0", "branch"], "a forked thread must append independently from its copied length");
|
|
387
|
+
if (await store.length("src") !== 3) fail("appending to a fork must not touch the source thread");
|
|
388
|
+
let caughtExisting;
|
|
389
|
+
try {
|
|
390
|
+
await store.fork({
|
|
391
|
+
threadId: "src",
|
|
392
|
+
newThreadId: "fork-full"
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
caughtExisting = error;
|
|
396
|
+
}
|
|
397
|
+
if (!(caughtExisting instanceof Error) || caughtExisting instanceof AgentEventLogConflictError) fail("forking onto a non-empty thread must reject with a plain Error");
|
|
398
|
+
let caughtUnknown;
|
|
399
|
+
try {
|
|
400
|
+
await store.fork({
|
|
401
|
+
threadId: "nope",
|
|
402
|
+
newThreadId: "fork-nope"
|
|
403
|
+
});
|
|
404
|
+
} catch (error) {
|
|
405
|
+
caughtUnknown = error;
|
|
406
|
+
}
|
|
407
|
+
if (!(caughtUnknown instanceof Error) || caughtUnknown instanceof AgentEventLogConflictError) fail("forking an unknown source thread must reject with a plain Error");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
export { assertEventLogStoreConformance as a, assertAgentLogEntry as i, AgentEventLogConflictError as n, assertJsonSerializable as o, NonSerializableAgentEventError as r, createInMemoryEventLogStore as s, AGENT_EVENT_SCHEMA_VERSION as t };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { t as AgentError } from "./errors-BQRk9eiZ.cjs";
|
|
2
|
+
import { EventObject } from "xstate";
|
|
3
|
+
|
|
4
|
+
//#region src/event-log-store.d.ts
|
|
5
|
+
/** The durable replay-entry envelope version. */
|
|
6
|
+
declare const AGENT_EVENT_SCHEMA_VERSION: 1;
|
|
7
|
+
/** Values that round-trip through JSON without adapters or silent coercion. */
|
|
8
|
+
type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
9
|
+
[key: string]: JsonValue;
|
|
10
|
+
};
|
|
11
|
+
interface AgentLogVerification {
|
|
12
|
+
/** Hash of the logical machine projection after this entry is applied. */
|
|
13
|
+
stateHash: string;
|
|
14
|
+
/** Hash of the serializable effects owed at that frontier. */
|
|
15
|
+
effectsHash: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One journaled entry: an EXTERNAL input to the machine — an effect completion
|
|
19
|
+
* (a `done`/`error` event carrying its output inline), a user-sent event, or a
|
|
20
|
+
* timer firing. Never a raised/internal event: deterministic replay re-derives
|
|
21
|
+
* those from the machine's own logic, so journaling them would double-apply.
|
|
22
|
+
* JSON-safe, stored verbatim.
|
|
23
|
+
*/
|
|
24
|
+
interface AgentLogEntry {
|
|
25
|
+
/** Version of this outer envelope, independent of the machine event type. */
|
|
26
|
+
schemaVersion: typeof AGENT_EVENT_SCHEMA_VERSION;
|
|
27
|
+
/** Stable identity within the thread. Forked prefixes retain their ids. */
|
|
28
|
+
id: string;
|
|
29
|
+
/** 0-based position in the thread's log. */
|
|
30
|
+
index: number;
|
|
31
|
+
/** RFC 3339 wall-clock time when the host accepted the entry. Metadata only. */
|
|
32
|
+
recordedAt: string;
|
|
33
|
+
/** The authored XState machine id. */
|
|
34
|
+
machineId: string;
|
|
35
|
+
/** Explicit version or structural hash of the machine that accepted the event. */
|
|
36
|
+
machineVersion: string;
|
|
37
|
+
event: EventObject;
|
|
38
|
+
/** Optional causal parent entry id, scoped to the same thread. */
|
|
39
|
+
causationId?: string;
|
|
40
|
+
/** Optional host-owned correlation id spanning threads/runs. */
|
|
41
|
+
correlationId?: string;
|
|
42
|
+
/** Recorded projection hashes used by strict replay verification. */
|
|
43
|
+
verification?: AgentLogVerification;
|
|
44
|
+
/** Host-owned, JSON-safe; stored verbatim, never interpreted. */
|
|
45
|
+
metadata?: Record<string, JsonValue>;
|
|
46
|
+
}
|
|
47
|
+
/** A precise failure when an entry would not survive a JSON round-trip. */
|
|
48
|
+
declare class NonSerializableAgentEventError extends AgentError {
|
|
49
|
+
readonly path: string;
|
|
50
|
+
readonly valueType: string;
|
|
51
|
+
constructor(path: string, valueType: string);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Rejects values JSON would drop or coerce. Unlike `JSON.stringify`, this does
|
|
55
|
+
* not silently erase `undefined`/functions or turn non-finite numbers into
|
|
56
|
+
* `null`; durable entries contain only plain JSON values.
|
|
57
|
+
*/
|
|
58
|
+
declare function assertJsonSerializable(value: unknown, path?: string, ancestors?: WeakSet<object>): asserts value is JsonValue;
|
|
59
|
+
/** Validates the complete durable envelope before append/export/replay. */
|
|
60
|
+
declare function assertAgentLogEntry(entry: unknown): asserts entry is AgentLogEntry;
|
|
61
|
+
/**
|
|
62
|
+
* An append-only event log: the authoritative durability artifact of a thread.
|
|
63
|
+
* The journal of external inputs IS the source of truth — deterministic machine
|
|
64
|
+
* replay derives every snapshot from it, a fork is a copied log prefix, and the
|
|
65
|
+
* type-only {@link AgentSnapshotStore} is a mere idle-point compaction cache
|
|
66
|
+
* over what the log already implies.
|
|
67
|
+
*
|
|
68
|
+
* Userland stores (a Postgres table with a `(thread_id, index)` primary key, an
|
|
69
|
+
* append-only object in blob storage, …) implement this one shape so they
|
|
70
|
+
* interoperate; {@link createInMemoryEventLogStore} is the in-memory reference.
|
|
71
|
+
*/
|
|
72
|
+
interface AgentEventLogStore {
|
|
73
|
+
/**
|
|
74
|
+
* Append `entries` starting at `expectedIndex` (the current length of the
|
|
75
|
+
* thread's log; 0 for a new thread). Atomic — all entries land or none do.
|
|
76
|
+
* Rejects with {@link AgentEventLogConflictError} when the thread's length
|
|
77
|
+
* differs from `expectedIndex`: a concurrent writer appended first. Each
|
|
78
|
+
* entry's `index` must be contiguous from `expectedIndex` (a plain `Error`
|
|
79
|
+
* otherwise: that is caller misuse, not a race).
|
|
80
|
+
*/
|
|
81
|
+
append(input: {
|
|
82
|
+
threadId: string;
|
|
83
|
+
expectedIndex: number;
|
|
84
|
+
entries: AgentLogEntry[];
|
|
85
|
+
}): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Read a thread's entries in log order. `from` (default 0) skips entries
|
|
88
|
+
* below that index for incremental catch-up. An unknown thread reads as an
|
|
89
|
+
* empty array.
|
|
90
|
+
*/
|
|
91
|
+
read(threadId: string, options?: {
|
|
92
|
+
from?: number;
|
|
93
|
+
}): Promise<AgentLogEntry[]>;
|
|
94
|
+
/** The thread's current log length (0 for an unknown thread) — the next `expectedIndex`. */
|
|
95
|
+
length(threadId: string): Promise<number>;
|
|
96
|
+
/**
|
|
97
|
+
* Copy a prefix onto a fresh, empty `newThreadId` — either `[0, upToIndex)`
|
|
98
|
+
* or through the inclusive `atEventId`. With neither cutoff the full source
|
|
99
|
+
* is copied. The fork then appends independently. Rejects (plain `Error`) if
|
|
100
|
+
* `newThreadId` already has entries, the source/id is unknown, or both cutoff
|
|
101
|
+
* forms are supplied. Implementations may copy-on-write or physically copy;
|
|
102
|
+
* observable behavior must match a full copy.
|
|
103
|
+
*/
|
|
104
|
+
fork(input: {
|
|
105
|
+
threadId: string;
|
|
106
|
+
newThreadId: string;
|
|
107
|
+
upToIndex?: number; /** Inclusive event-id cutoff. Mutually exclusive with `upToIndex`. */
|
|
108
|
+
atEventId?: string;
|
|
109
|
+
}): Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Rejection from {@link AgentEventLogStore.append} when the thread's stored
|
|
113
|
+
* length is not the `expectedIndex` the writer held — a concurrent writer
|
|
114
|
+
* appended first. `actualLength` is the length core found.
|
|
115
|
+
*/
|
|
116
|
+
declare class AgentEventLogConflictError extends AgentError {
|
|
117
|
+
readonly threadId: string;
|
|
118
|
+
readonly expectedIndex: number;
|
|
119
|
+
readonly actualLength: number;
|
|
120
|
+
constructor(threadId: string, expectedIndex: number, actualLength: number);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* In-memory reference store, and the baseline the conformance suite runs
|
|
124
|
+
* against. Per-thread entries are held in a contiguous, index-ordered list;
|
|
125
|
+
* append's check-and-push runs synchronously (no `await` between reading the
|
|
126
|
+
* length and writing), so two appends racing on the same `expectedIndex`
|
|
127
|
+
* resolve to exactly one winner. Every stored and returned entry is
|
|
128
|
+
* `structuredClone`d, so a caller can neither mutate stored state through a
|
|
129
|
+
* value it appended nor through a value it read.
|
|
130
|
+
*/
|
|
131
|
+
declare function createInMemoryEventLogStore(): AgentEventLogStore;
|
|
132
|
+
/** Produces a fresh, empty store for one assertion. */
|
|
133
|
+
type CreateStore = () => Promise<AgentEventLogStore> | AgentEventLogStore;
|
|
134
|
+
/**
|
|
135
|
+
* Validates a store against the reference's semantics: empty read + zero length
|
|
136
|
+
* for unknown threads; single and multi-entry append; contiguity misuse guard;
|
|
137
|
+
* stale-`expectedIndex` conflict with correct fields; an interleaved concurrent
|
|
138
|
+
* append race (exactly one winner); event-id uniqueness; `read({ from })`
|
|
139
|
+
* incremental correctness; thread isolation; metadata round-trip; deep-copy
|
|
140
|
+
* isolation on append and read; and the full fork contract.
|
|
141
|
+
*/
|
|
142
|
+
declare function assertEventLogStoreConformance(create: CreateStore): Promise<void>;
|
|
143
|
+
//#endregion
|
|
144
|
+
export { AgentLogVerification as a, assertAgentLogEntry as c, createInMemoryEventLogStore as d, AgentLogEntry as i, assertEventLogStoreConformance as l, AgentEventLogConflictError as n, JsonValue as o, AgentEventLogStore as r, NonSerializableAgentEventError as s, AGENT_EVENT_SCHEMA_VERSION as t, assertJsonSerializable as u };
|