@fevex/core 0.1.0-alpha.1 → 0.1.0-alpha.10
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 +771 -13
- package/dist/agents/index.d.mts +2 -2
- package/dist/agents/index.mjs +1 -1
- package/dist/artifacts/index.d.mts +2 -0
- package/dist/artifacts/index.mjs +2 -0
- package/dist/artifacts-gUzemET7.mjs +440 -0
- package/dist/channels/index.d.mts +2 -0
- package/dist/channels/index.mjs +192 -0
- package/dist/evals/index.d.mts +19 -11
- package/dist/evals/index.mjs +125 -122
- package/dist/fevex-_KOsOsB8.d.mts +129 -0
- package/dist/http/index.d.mts +150 -0
- package/dist/http/index.mjs +692 -0
- package/dist/index-26ilbzAs.d.mts +50 -0
- package/dist/index-B8EcvWeN.d.mts +103 -0
- package/dist/index-BEqYWean.d.mts +118 -0
- package/dist/index-Btth7rdL.d.mts +58 -0
- package/dist/{index-QTkd6a7C.d.mts → index-CFzrvDQ0.d.mts} +4 -3
- package/dist/index-CIHWc5V1.d.mts +116 -0
- package/dist/index-D7r0puBi.d.mts +64 -0
- package/dist/index-DHcfIfh-.d.mts +224 -0
- package/dist/index-DUhNa7IR.d.mts +73 -0
- package/dist/index-lCCFokc6.d.mts +617 -0
- package/dist/index.d.mts +22 -10
- package/dist/index.mjs +4178 -671
- package/dist/knowledge/index.d.mts +2 -0
- package/dist/knowledge/index.mjs +289 -0
- package/dist/model-stream-JQdHDDcj.mjs +131 -0
- package/dist/models/index.d.mts +2 -46
- package/dist/observability/index.d.mts +2 -2
- package/dist/policies/index.d.mts +1 -1
- package/dist/policies/index.mjs +1 -1
- package/dist/run-error-Cr17wPGH.mjs +23 -0
- package/dist/{model-stream-u2xMsEaf.mjs → run-support-CmZ6Xo6H.mjs} +41 -130
- package/dist/runtime/index.d.mts +2 -2
- package/dist/runtime/index.mjs +271 -9
- package/dist/sandbox/index.d.mts +2 -0
- package/dist/sandbox/index.mjs +147 -0
- package/dist/teams/index.d.mts +2 -0
- package/dist/teams/index.mjs +2 -0
- package/dist/teams-Ctz676G4.mjs +82 -0
- package/dist/testing/index.d.mts +39 -9
- package/dist/testing/index.mjs +512 -28
- package/dist/tools/index.d.mts +3 -3
- package/dist/tools/index.mjs +442 -2
- package/dist/types-CckDlhYo.d.mts +575 -0
- package/dist/workflows/index.d.mts +2 -0
- package/dist/workflows/index.mjs +6 -0
- package/package.json +31 -3
- package/dist/exact-definition-NHBBFlFR.d.mts +0 -4
- package/dist/fevex-Dz6GBz9o.d.mts +0 -32
- package/dist/index-BzxHfyYV.d.mts +0 -23
- package/dist/index-CZGyhNIQ.d.mts +0 -47
- package/dist/index-C_Zpr13f.d.mts +0 -124
- package/dist/index-DGWOGXzc.d.mts +0 -181
- package/dist/index-ojCY3lwm.d.mts +0 -79
package/dist/testing/index.mjs
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return {
|
|
6
|
-
calls,
|
|
7
|
-
async *stream(input) {
|
|
8
|
-
input.signal?.throwIfAborted();
|
|
9
|
-
const response = responses[calls.length];
|
|
10
|
-
calls.push(input);
|
|
11
|
-
if (!response) throw new Error(`fakeModel has no response for call ${calls.length}`);
|
|
12
|
-
if (response.output !== void 0) {
|
|
13
|
-
const delta = typeof response.output === "string" ? response.output : JSON.stringify(response.output);
|
|
14
|
-
if (delta) yield {
|
|
15
|
-
type: "output.delta",
|
|
16
|
-
delta
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
yield {
|
|
20
|
-
type: "completed",
|
|
21
|
-
result: response
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
}
|
|
1
|
+
import { isRetainableRunStore } from "../runtime/index.mjs";
|
|
2
|
+
import { IntegrationError } from "../tools/index.mjs";
|
|
3
|
+
import { t as readModelStream } from "../model-stream-JQdHDDcj.mjs";
|
|
4
|
+
//#region src/testing/contracts.ts
|
|
26
5
|
const contractOutputSchema = {
|
|
27
6
|
type: "object",
|
|
28
7
|
properties: { answer: { type: "string" } },
|
|
@@ -38,6 +17,121 @@ const contractToolInputSchema = {
|
|
|
38
17
|
function assert(condition, message) {
|
|
39
18
|
if (!condition) throw new TypeError(message);
|
|
40
19
|
}
|
|
20
|
+
async function assertRejects(operation, message, code) {
|
|
21
|
+
try {
|
|
22
|
+
await operation();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (code !== void 0) assert(error instanceof Error && "code" in error && error.code === code, message);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
throw new TypeError(message);
|
|
28
|
+
}
|
|
29
|
+
async function testMemoryStore(store) {
|
|
30
|
+
const actor = { id: `actor-${crypto.randomUUID()}` };
|
|
31
|
+
const sessionId = `session-${crypto.randomUUID()}`;
|
|
32
|
+
const context = {
|
|
33
|
+
agentName: "assistant",
|
|
34
|
+
input: "refund status",
|
|
35
|
+
sessionId,
|
|
36
|
+
context: {
|
|
37
|
+
namespace: "tenant-a",
|
|
38
|
+
actor
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const saved = await store.write({
|
|
42
|
+
content: "Refund status is approved.",
|
|
43
|
+
agentName: "assistant",
|
|
44
|
+
sessionId,
|
|
45
|
+
namespace: "tenant-a",
|
|
46
|
+
actor,
|
|
47
|
+
metadata: { source: "contract" }
|
|
48
|
+
}, context);
|
|
49
|
+
assert(saved.id.trim(), "MemoryRecord id cannot be empty");
|
|
50
|
+
assert(saved.createdAt.trim(), "MemoryRecord createdAt cannot be empty");
|
|
51
|
+
saved.content = "mutated";
|
|
52
|
+
assert((await store.search({
|
|
53
|
+
query: "refund",
|
|
54
|
+
sessionId,
|
|
55
|
+
namespace: "tenant-a",
|
|
56
|
+
actor
|
|
57
|
+
}, context))[0]?.content === "Refund status is approved.", "MemoryStore must not expose mutable record references");
|
|
58
|
+
assert((await store.search({
|
|
59
|
+
query: "refund",
|
|
60
|
+
sessionId: "other"
|
|
61
|
+
}, context)).length === 0, "MemoryStore must isolate sessions");
|
|
62
|
+
assert((await store.search({
|
|
63
|
+
query: "refund",
|
|
64
|
+
namespace: "tenant-b"
|
|
65
|
+
}, context)).length === 0, "MemoryStore must isolate namespaces");
|
|
66
|
+
assert((await store.search({
|
|
67
|
+
query: "refund",
|
|
68
|
+
actor: { id: "other" }
|
|
69
|
+
}, context)).length === 0, "MemoryStore must isolate actors");
|
|
70
|
+
await store.write({
|
|
71
|
+
content: "Refund backup note.",
|
|
72
|
+
sessionId,
|
|
73
|
+
namespace: "tenant-a",
|
|
74
|
+
actor
|
|
75
|
+
}, context);
|
|
76
|
+
assert((await store.search({
|
|
77
|
+
query: "refund",
|
|
78
|
+
sessionId,
|
|
79
|
+
namespace: "tenant-a",
|
|
80
|
+
actor,
|
|
81
|
+
limit: 1
|
|
82
|
+
}, context)).length === 1, "MemoryStore must respect limits");
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
controller.abort();
|
|
85
|
+
await store.search({ query: "refund" }, {
|
|
86
|
+
...context,
|
|
87
|
+
signal: controller.signal
|
|
88
|
+
}).then(() => {
|
|
89
|
+
throw new TypeError("MemoryStore must reject aborted searches");
|
|
90
|
+
}, () => {});
|
|
91
|
+
}
|
|
92
|
+
async function testToolProvider(provider, contract) {
|
|
93
|
+
const tools = await provider.listTools({});
|
|
94
|
+
assert(tools.some((tool) => tool.name === contract.allowedTool), "ToolProvider must list the allowed contract tool");
|
|
95
|
+
if (contract.disallowedTool !== void 0) assert(!tools.some((tool) => tool.name === contract.disallowedTool), "ToolProvider must not list disallowed tools");
|
|
96
|
+
const output = await provider.callTool(contract.allowedTool, contract.input ?? { query: "value" }, {});
|
|
97
|
+
assert(JSON.stringify(output) === JSON.stringify(contract.output ?? { answer: "ok" }), "ToolProvider returned an unexpected output");
|
|
98
|
+
if (contract.safeError) await Promise.resolve(provider.callTool("contract_error", {}, {})).then(() => {
|
|
99
|
+
throw new TypeError("ToolProvider must reject configured error calls");
|
|
100
|
+
}, (error) => {
|
|
101
|
+
assert(error instanceof IntegrationError, "ToolProvider errors must be IntegrationError");
|
|
102
|
+
assert(error.message === error.safeMessage, "IntegrationError message must be safe");
|
|
103
|
+
});
|
|
104
|
+
if (contract.timeout) {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
controller.abort();
|
|
107
|
+
await Promise.resolve(provider.callTool(contract.allowedTool, {}, { signal: controller.signal })).then(() => {
|
|
108
|
+
throw new TypeError("ToolProvider must reject aborted calls");
|
|
109
|
+
}, () => {});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function testChannelAdapter(adapter, contract) {
|
|
113
|
+
assert(adapter.name.trim(), "ChannelAdapter name cannot be empty");
|
|
114
|
+
const message = await adapter.parse(contract.input, {});
|
|
115
|
+
assert(message !== null, "ChannelAdapter must parse the contract input");
|
|
116
|
+
for (const [name, value] of [
|
|
117
|
+
["id", message.id],
|
|
118
|
+
["deliveryId", message.deliveryId],
|
|
119
|
+
["conversationId", message.conversationId],
|
|
120
|
+
["content", message.content]
|
|
121
|
+
]) assert(value.trim(), `ChannelMessage ${name} cannot be empty`);
|
|
122
|
+
if (message.threadId !== void 0) assert(message.threadId.trim(), "ChannelMessage threadId cannot be empty");
|
|
123
|
+
if (message.actor !== void 0) assert(message.actor.id.trim(), "ChannelMessage actor id cannot be empty");
|
|
124
|
+
if (message.metadata !== void 0) JSON.stringify(message.metadata);
|
|
125
|
+
assert(JSON.stringify(message) === JSON.stringify(contract.message), "ChannelAdapter returned an unexpected message");
|
|
126
|
+
if (contract.ignoredInput !== void 0) assert(await adapter.parse(contract.ignoredInput, {}) === null, "ChannelAdapter must ignore the configured input");
|
|
127
|
+
if (contract.output.threadId !== void 0) assert(contract.output.threadId.trim(), "ChannelOutput threadId cannot be empty");
|
|
128
|
+
if (contract.output.metadata !== void 0) JSON.stringify(contract.output.metadata);
|
|
129
|
+
assert(JSON.stringify(await adapter.deliver(contract.output, {})) === JSON.stringify(contract.delivered), "ChannelAdapter returned an unexpected delivery");
|
|
130
|
+
if (contract.elicitation) {
|
|
131
|
+
assert(adapter.deliverElicitation, "ChannelAdapter must support elicitation delivery");
|
|
132
|
+
for (let attempt = 0; attempt < 2; attempt += 1) assert(JSON.stringify(await adapter.deliverElicitation(structuredClone(contract.elicitation.request), { ...contract.elicitation.context })) === JSON.stringify(contract.elicitation.delivered), "ChannelAdapter returned an unexpected elicitation delivery or retry receipt");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
41
135
|
async function testModelGateway(model, contract = {}) {
|
|
42
136
|
const output = contract.output ?? { answer: "ok" };
|
|
43
137
|
const toolCall = contract.toolCall ?? {
|
|
@@ -109,6 +203,126 @@ async function testRunStore(store) {
|
|
|
109
203
|
const runId = `contract-run-${suffix}`;
|
|
110
204
|
const sessionId = `contract-session-${suffix}`;
|
|
111
205
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
206
|
+
const atomicRunId = `contract-atomic-run-${suffix}`;
|
|
207
|
+
const atomicSessionId = `contract-atomic-session-${suffix}`;
|
|
208
|
+
const atomicRun = {
|
|
209
|
+
id: atomicRunId,
|
|
210
|
+
sessionId: atomicSessionId,
|
|
211
|
+
agentName: "contract-agent",
|
|
212
|
+
status: "running",
|
|
213
|
+
revision: 0,
|
|
214
|
+
createdAt: now,
|
|
215
|
+
updatedAt: now
|
|
216
|
+
};
|
|
217
|
+
const atomicSession = {
|
|
218
|
+
id: atomicSessionId,
|
|
219
|
+
history: [],
|
|
220
|
+
createdAt: now,
|
|
221
|
+
updatedAt: now
|
|
222
|
+
};
|
|
223
|
+
const atomicCheckpoint = {
|
|
224
|
+
version: 2,
|
|
225
|
+
runId: atomicRunId,
|
|
226
|
+
definitionHash: "atomic-definition",
|
|
227
|
+
messages: [{
|
|
228
|
+
role: "user",
|
|
229
|
+
content: "atomic input"
|
|
230
|
+
}],
|
|
231
|
+
inputContent: "atomic input",
|
|
232
|
+
step: 1,
|
|
233
|
+
toolCallCount: 0,
|
|
234
|
+
seenToolCallIds: [],
|
|
235
|
+
pendingTools: [],
|
|
236
|
+
pendingIndex: 0
|
|
237
|
+
};
|
|
238
|
+
const atomicLease = {
|
|
239
|
+
generation: 0,
|
|
240
|
+
runId: atomicRunId,
|
|
241
|
+
ownerId: `atomic-owner-${suffix}`,
|
|
242
|
+
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
243
|
+
};
|
|
244
|
+
const atomicStarted = {
|
|
245
|
+
id: `atomic-event-${suffix}`,
|
|
246
|
+
sequence: 1,
|
|
247
|
+
type: "run.started",
|
|
248
|
+
runId: atomicRunId,
|
|
249
|
+
timestamp: now
|
|
250
|
+
};
|
|
251
|
+
assert(await store.createExecution({
|
|
252
|
+
run: atomicRun,
|
|
253
|
+
session: atomicSession,
|
|
254
|
+
checkpoint: atomicCheckpoint,
|
|
255
|
+
lease: atomicLease,
|
|
256
|
+
events: [atomicStarted]
|
|
257
|
+
}), "RunStore must atomically create a new execution");
|
|
258
|
+
assert(atomicRun.revision === 1 && (await store.getRun(atomicRunId))?.revision === 1 && (await store.getSession(atomicSessionId))?.id === atomicSessionId && (await store.getCheckpoint(atomicRunId))?.version === 2 && (await store.listEvents(atomicRunId))[0]?.id === atomicStarted.id, "RunStore createExecution must include run, session, checkpoint, lease, and started event");
|
|
259
|
+
assert(!await store.acquireLease({
|
|
260
|
+
...atomicLease,
|
|
261
|
+
ownerId: `atomic-competitor-${suffix}`
|
|
262
|
+
}), "RunStore createExecution must acquire the initial lease");
|
|
263
|
+
assert(!await store.createExecution({
|
|
264
|
+
run: {
|
|
265
|
+
...atomicRun,
|
|
266
|
+
revision: 0
|
|
267
|
+
},
|
|
268
|
+
session: await store.getSession(atomicRun.sessionId),
|
|
269
|
+
checkpoint: atomicCheckpoint,
|
|
270
|
+
lease: atomicLease,
|
|
271
|
+
events: [atomicStarted]
|
|
272
|
+
}), "RunStore createExecution must reject duplicate run ids");
|
|
273
|
+
const ownedSession = await store.getSession(atomicRun.sessionId);
|
|
274
|
+
const competitorId = `competitor-${suffix}`;
|
|
275
|
+
const competitor = () => ({
|
|
276
|
+
run: {
|
|
277
|
+
...atomicRun,
|
|
278
|
+
id: competitorId,
|
|
279
|
+
revision: 0,
|
|
280
|
+
status: "running"
|
|
281
|
+
},
|
|
282
|
+
session: structuredClone(ownedSession),
|
|
283
|
+
checkpoint: {
|
|
284
|
+
...atomicCheckpoint,
|
|
285
|
+
runId: competitorId
|
|
286
|
+
},
|
|
287
|
+
lease: {
|
|
288
|
+
...atomicLease,
|
|
289
|
+
runId: competitorId
|
|
290
|
+
},
|
|
291
|
+
events: []
|
|
292
|
+
});
|
|
293
|
+
assert(!await store.createExecution(competitor()), "Active sessions must reject another run");
|
|
294
|
+
await assertRejects(() => store.saveSession(structuredClone(ownedSession)), "Compaction must reject an active session");
|
|
295
|
+
assert(await store.commitExecution({
|
|
296
|
+
lease: atomicLease,
|
|
297
|
+
expectedRevision: atomicRun.revision,
|
|
298
|
+
run: Object.assign(atomicRun, { status: "paused" })
|
|
299
|
+
}), "Owner must be able to pause");
|
|
300
|
+
await store.releaseLease(atomicRunId, atomicLease.ownerId, atomicLease.generation);
|
|
301
|
+
assert(!await store.createExecution(competitor()), "Paused sessions remain reserved without a worker lease");
|
|
302
|
+
await assertRejects(() => store.saveSession(structuredClone(ownedSession)), "Compaction must reject a paused session");
|
|
303
|
+
assert(await store.acquireLease(atomicLease), "Resume must acquire a new lease generation");
|
|
304
|
+
assert(await store.commitExecution({
|
|
305
|
+
lease: atomicLease,
|
|
306
|
+
expectedRevision: atomicRun.revision,
|
|
307
|
+
run: Object.assign(atomicRun, { status: "completed" }),
|
|
308
|
+
session: ownedSession
|
|
309
|
+
}), "Terminal commit must release the session");
|
|
310
|
+
const staleSession = structuredClone(ownedSession);
|
|
311
|
+
await store.saveSession(ownedSession);
|
|
312
|
+
await assertRejects(() => store.saveSession(staleSession), "Stale compaction must not overwrite history");
|
|
313
|
+
assert(!await store.createExecution({
|
|
314
|
+
...competitor(),
|
|
315
|
+
session: staleSession
|
|
316
|
+
}), "Run creation must reject history read before compaction");
|
|
317
|
+
const next = competitor();
|
|
318
|
+
assert(await store.createExecution(next), "Terminal sessions can be reserved again");
|
|
319
|
+
assert(!await store.commitExecution({
|
|
320
|
+
lease: next.lease,
|
|
321
|
+
expectedRevision: next.run.revision,
|
|
322
|
+
run: next.run,
|
|
323
|
+
session: staleSession
|
|
324
|
+
}), "A stale history commit must fail atomically");
|
|
325
|
+
assert((await store.getRun(next.run.id))?.revision === next.run.revision, "Rejected history commits must not advance the run");
|
|
112
326
|
const session = {
|
|
113
327
|
id: sessionId,
|
|
114
328
|
history: [],
|
|
@@ -126,10 +340,23 @@ async function testRunStore(store) {
|
|
|
126
340
|
};
|
|
127
341
|
await store.saveSession(session);
|
|
128
342
|
await store.saveRun(run);
|
|
343
|
+
const commitLease = {
|
|
344
|
+
generation: 0,
|
|
345
|
+
runId,
|
|
346
|
+
ownerId: `writer-${suffix}`,
|
|
347
|
+
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
348
|
+
};
|
|
349
|
+
assert(await store.acquireLease(commitLease), "Writer must acquire a lease");
|
|
129
350
|
const snapshot = await store.getRun(runId);
|
|
130
351
|
assert(snapshot?.status === "running", "RunStore must return saved runs");
|
|
131
352
|
snapshot.status = "failed";
|
|
132
353
|
assert((await store.getRun(runId))?.status === "running", "RunStore must not expose mutable run references");
|
|
354
|
+
await store.saveRun({
|
|
355
|
+
...run,
|
|
356
|
+
status: "completed"
|
|
357
|
+
});
|
|
358
|
+
assert((await store.getRun(runId))?.status === "completed", "RunStore must overwrite an existing run on save");
|
|
359
|
+
await store.saveRun(run);
|
|
133
360
|
const firstEvent = {
|
|
134
361
|
id: `event-1-${suffix}`,
|
|
135
362
|
sequence: 1,
|
|
@@ -139,6 +366,7 @@ async function testRunStore(store) {
|
|
|
139
366
|
};
|
|
140
367
|
await store.appendEvent(firstEvent);
|
|
141
368
|
const checkpoint = {
|
|
369
|
+
version: 2,
|
|
142
370
|
runId,
|
|
143
371
|
definitionHash: "definition",
|
|
144
372
|
messages: [{
|
|
@@ -189,6 +417,7 @@ async function testRunStore(store) {
|
|
|
189
417
|
}
|
|
190
418
|
};
|
|
191
419
|
assert(await store.commitExecution({
|
|
420
|
+
lease: commitLease,
|
|
192
421
|
expectedRevision: 0,
|
|
193
422
|
run,
|
|
194
423
|
session,
|
|
@@ -199,6 +428,7 @@ async function testRunStore(store) {
|
|
|
199
428
|
assert(run.revision === 1, "RunStore must advance the committed run revision");
|
|
200
429
|
assert((await store.getSession(sessionId))?.history.length === 1 && (await store.getCheckpoint(runId))?.definitionHash === "definition" && (await store.getToolExecution(runId, "tool-call"))?.status === "completed", "RunStore atomic commits must include session, checkpoint, and tool ledger");
|
|
201
430
|
assert(!await store.commitExecution({
|
|
431
|
+
lease: commitLease,
|
|
202
432
|
expectedRevision: 0,
|
|
203
433
|
run: {
|
|
204
434
|
...run,
|
|
@@ -213,14 +443,64 @@ async function testRunStore(store) {
|
|
|
213
443
|
payload: { error: "stale" }
|
|
214
444
|
}]
|
|
215
445
|
}), "RunStore must reject stale revisions");
|
|
446
|
+
const firstPage = await store.listEvents(runId, { limit: 1 });
|
|
447
|
+
assert(firstPage.length === 1 && firstPage[0]?.id === firstEvent.id, "Event pagination must honor limit and start at the first sequence");
|
|
448
|
+
assert((await store.listEvents(runId, {
|
|
449
|
+
after: firstEvent.id,
|
|
450
|
+
limit: 1
|
|
451
|
+
}))[0]?.id === secondEvent.id, "Event pagination must continue after the cursor");
|
|
452
|
+
assert((await store.listEvents(runId, {
|
|
453
|
+
after: secondEvent.id,
|
|
454
|
+
limit: 1
|
|
455
|
+
})).length === 0, "Event pagination after the last event must be empty");
|
|
456
|
+
const latestPage = await store.listEvents(runId, {
|
|
457
|
+
order: "desc",
|
|
458
|
+
limit: 1
|
|
459
|
+
});
|
|
460
|
+
assert(latestPage.length === 1 && latestPage[0]?.id === secondEvent.id, "Descending pagination must honor limit and find the latest event");
|
|
461
|
+
const descending = await store.listEvents(runId, {
|
|
462
|
+
order: "desc",
|
|
463
|
+
limit: 2
|
|
464
|
+
});
|
|
465
|
+
assert(descending.length === 2 && descending[0]?.id === secondEvent.id && descending[1]?.id === firstEvent.id, "Event pagination must order by sequence");
|
|
466
|
+
const descendingAfter = await store.listEvents(runId, {
|
|
467
|
+
after: firstEvent.id,
|
|
468
|
+
order: "desc",
|
|
469
|
+
limit: 2
|
|
470
|
+
});
|
|
471
|
+
assert(descendingAfter.length === 1 && descendingAfter[0]?.id === secondEvent.id, "Event cursors must select strictly newer sequences even in descending order");
|
|
472
|
+
await assertRejects(() => store.listEvents(runId, {
|
|
473
|
+
after: atomicStarted.id,
|
|
474
|
+
limit: 1
|
|
475
|
+
}), "Event pagination must reject a cursor from another run with INVALID_CURSOR", "INVALID_CURSOR");
|
|
476
|
+
for (const limit of [
|
|
477
|
+
0,
|
|
478
|
+
-1,
|
|
479
|
+
NaN,
|
|
480
|
+
Infinity,
|
|
481
|
+
1.5
|
|
482
|
+
]) await assertRejects(() => store.listEvents(runId, { limit }), "Event pagination must validate limits");
|
|
216
483
|
const events = await store.listEvents(runId, { after: firstEvent.id });
|
|
217
484
|
assert(events.length === 1 && events[0]?.id === secondEvent.id, "RunStore cursors must return stable ordered suffixes");
|
|
485
|
+
await assertRejects(() => store.listEvents(`missing-run-${suffix}`), "RunStore listEvents must reject unknown runs");
|
|
486
|
+
await assertRejects(() => store.listEvents(runId, { after: `missing-cursor-${suffix}` }), "RunStore listEvents must reject unknown cursors with INVALID_CURSOR", "INVALID_CURSOR");
|
|
487
|
+
assert(await store.commitExecution({
|
|
488
|
+
lease: commitLease,
|
|
489
|
+
expectedRevision: 1,
|
|
490
|
+
run,
|
|
491
|
+
checkpoint: null
|
|
492
|
+
}), "RunStore must commit a checkpoint deletion");
|
|
493
|
+
assert(await store.getCheckpoint(runId) === void 0, "RunStore must delete the checkpoint when a commit sets it to null");
|
|
494
|
+
assert((await store.getToolExecution(runId, "tool-call"))?.status === "completed", "RunStore must keep the tool ledger when a checkpoint is deleted");
|
|
495
|
+
await store.releaseLease(runId, commitLease.ownerId, commitLease.generation);
|
|
218
496
|
const lease1 = {
|
|
497
|
+
generation: 0,
|
|
219
498
|
runId,
|
|
220
499
|
ownerId: `owner-1-${suffix}`,
|
|
221
500
|
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
222
501
|
};
|
|
223
502
|
const lease2 = {
|
|
503
|
+
generation: 0,
|
|
224
504
|
runId,
|
|
225
505
|
ownerId: `owner-2-${suffix}`,
|
|
226
506
|
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
@@ -229,9 +509,213 @@ async function testRunStore(store) {
|
|
|
229
509
|
assert(!await store.acquireLease(lease2), "RunStore must reject a competing lease");
|
|
230
510
|
assert(!await store.renewLease(lease2), "RunStore must reject renewal by another owner");
|
|
231
511
|
assert(await store.renewLease(lease1), "RunStore must renew a matching lease");
|
|
232
|
-
await store.releaseLease(runId, lease1.ownerId);
|
|
512
|
+
await store.releaseLease(runId, lease1.ownerId, lease1.generation);
|
|
233
513
|
assert(await store.acquireLease(lease2), "RunStore must release a lease for another owner");
|
|
234
|
-
await store.releaseLease(runId, lease2.ownerId);
|
|
514
|
+
await store.releaseLease(runId, lease2.ownerId, lease2.generation);
|
|
515
|
+
assert(!await store.renewLease(lease1), "RunStore must reject renewal of a released lease");
|
|
516
|
+
const expiredLease = {
|
|
517
|
+
generation: 0,
|
|
518
|
+
runId,
|
|
519
|
+
ownerId: `owner-3-${suffix}`,
|
|
520
|
+
expiresAt: (/* @__PURE__ */ new Date(Date.now() - 1e3)).toISOString()
|
|
521
|
+
};
|
|
522
|
+
const takeoverLease = {
|
|
523
|
+
generation: 0,
|
|
524
|
+
runId,
|
|
525
|
+
ownerId: `owner-4-${suffix}`,
|
|
526
|
+
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
527
|
+
};
|
|
528
|
+
assert(await store.acquireLease(expiredLease), "RunStore must acquire a free lease");
|
|
529
|
+
const beforeTakeover = await store.getRun(runId);
|
|
530
|
+
assert(!await store.commitExecution({
|
|
531
|
+
lease: expiredLease,
|
|
532
|
+
expectedRevision: beforeTakeover.revision,
|
|
533
|
+
run: {
|
|
534
|
+
...beforeTakeover,
|
|
535
|
+
status: "failed"
|
|
536
|
+
}
|
|
537
|
+
}), "An expired lease must not commit even before takeover");
|
|
538
|
+
assert(!await store.renewLease({
|
|
539
|
+
...expiredLease,
|
|
540
|
+
expiresAt: takeoverLease.expiresAt
|
|
541
|
+
}), "An expired lease cannot be revived by renewal");
|
|
542
|
+
assert(await store.acquireLease(takeoverLease), "RunStore must let another owner take over an expired lease");
|
|
543
|
+
await store.releaseLease(runId, takeoverLease.ownerId, takeoverLease.generation);
|
|
544
|
+
const oldToken = { ...takeoverLease };
|
|
545
|
+
assert(await store.acquireLease(takeoverLease), "A released owner can acquire a new generation");
|
|
546
|
+
assert(takeoverLease.generation > oldToken.generation, "Generation must increase even for the same owner");
|
|
547
|
+
const currentRun = await store.getRun(runId);
|
|
548
|
+
const currentSession = await store.getSession(sessionId);
|
|
549
|
+
const currentEvents = await store.listEvents(runId);
|
|
550
|
+
const currentTool = await store.getToolExecution(runId, "tool-call");
|
|
551
|
+
assert(currentRun.revision === beforeTakeover.revision, "Takeover must be tested before revision changes");
|
|
552
|
+
const staleCommit = {
|
|
553
|
+
lease: oldToken,
|
|
554
|
+
expectedRevision: currentRun.revision,
|
|
555
|
+
run: {
|
|
556
|
+
...currentRun,
|
|
557
|
+
status: "failed"
|
|
558
|
+
},
|
|
559
|
+
session: {
|
|
560
|
+
...currentSession,
|
|
561
|
+
history: []
|
|
562
|
+
},
|
|
563
|
+
checkpoint,
|
|
564
|
+
toolExecution: {
|
|
565
|
+
...toolExecution,
|
|
566
|
+
output: "stale"
|
|
567
|
+
},
|
|
568
|
+
events: [{
|
|
569
|
+
...firstEvent,
|
|
570
|
+
id: `stale-owner-${suffix}`,
|
|
571
|
+
sequence: 3
|
|
572
|
+
}]
|
|
573
|
+
};
|
|
574
|
+
assert(!await store.commitExecution(staleCommit), "A previous generation must not commit with a still-current run revision");
|
|
575
|
+
assert(!await store.commitExecution({
|
|
576
|
+
...staleCommit,
|
|
577
|
+
lease: {
|
|
578
|
+
...takeoverLease,
|
|
579
|
+
ownerId: "wrong-owner"
|
|
580
|
+
}
|
|
581
|
+
}), "Generation alone must not authorize a commit");
|
|
582
|
+
assert(!await store.renewLease({
|
|
583
|
+
...oldToken,
|
|
584
|
+
expiresAt: takeoverLease.expiresAt
|
|
585
|
+
}), "A stale generation must not renew a new lease owned by the same owner");
|
|
586
|
+
await store.releaseLease(runId, oldToken.ownerId, oldToken.generation);
|
|
587
|
+
assert(await store.renewLease(takeoverLease), "A stale release must not revoke the new generation");
|
|
588
|
+
assert(JSON.stringify(await store.getRun(runId)) === JSON.stringify(currentRun), "Rejected lease commits must not change the run");
|
|
589
|
+
assert(JSON.stringify(await store.getSession(sessionId)) === JSON.stringify(currentSession), "Rejected lease commits must not change history");
|
|
590
|
+
assert(JSON.stringify(await store.listEvents(runId)) === JSON.stringify(currentEvents), "Rejected lease commits must not append events");
|
|
591
|
+
assert(JSON.stringify(await store.getToolExecution(runId, "tool-call")) === JSON.stringify(currentTool), "Rejected lease commits must not change the tool ledger");
|
|
592
|
+
assert(await store.getCheckpoint(runId) === void 0, "Rejected lease commits must not create checkpoints");
|
|
593
|
+
assert(await store.commitExecution({
|
|
594
|
+
lease: takeoverLease,
|
|
595
|
+
expectedRevision: currentRun.revision,
|
|
596
|
+
run: {
|
|
597
|
+
...currentRun,
|
|
598
|
+
status: "completed"
|
|
599
|
+
}
|
|
600
|
+
}), "The current generation must be able to commit");
|
|
601
|
+
await store.releaseLease(runId, takeoverLease.ownerId, takeoverLease.generation);
|
|
602
|
+
}
|
|
603
|
+
async function testRetentionStore(store) {
|
|
604
|
+
assert(isRetainableRunStore(store), "RunStore must implement purgeRuns");
|
|
605
|
+
const suffix = crypto.randomUUID();
|
|
606
|
+
const now = Date.now();
|
|
607
|
+
const old = (/* @__PURE__ */ new Date(now - 2 * 864e5)).toISOString();
|
|
608
|
+
const before = (/* @__PURE__ */ new Date(now - 864e5)).toISOString();
|
|
609
|
+
const recent = new Date(now).toISOString();
|
|
610
|
+
const entries = [
|
|
611
|
+
{
|
|
612
|
+
id: `retention-old-completed-${suffix}`,
|
|
613
|
+
status: "completed",
|
|
614
|
+
createdAt: old
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
id: `retention-old-failed-${suffix}`,
|
|
618
|
+
status: "failed",
|
|
619
|
+
createdAt: old
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
id: `retention-recent-completed-${suffix}`,
|
|
623
|
+
status: "completed",
|
|
624
|
+
createdAt: recent
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
id: `retention-old-running-${suffix}`,
|
|
628
|
+
status: "running",
|
|
629
|
+
createdAt: old
|
|
630
|
+
}
|
|
631
|
+
];
|
|
632
|
+
for (const entry of entries) {
|
|
633
|
+
const sessionId = `retention-session-${entry.id}`;
|
|
634
|
+
await store.saveSession({
|
|
635
|
+
id: sessionId,
|
|
636
|
+
history: [],
|
|
637
|
+
createdAt: entry.createdAt,
|
|
638
|
+
updatedAt: entry.createdAt
|
|
639
|
+
});
|
|
640
|
+
await store.saveRun({
|
|
641
|
+
id: entry.id,
|
|
642
|
+
sessionId,
|
|
643
|
+
agentName: "retention-agent",
|
|
644
|
+
status: entry.status,
|
|
645
|
+
revision: 1,
|
|
646
|
+
createdAt: entry.createdAt,
|
|
647
|
+
updatedAt: entry.createdAt
|
|
648
|
+
});
|
|
649
|
+
await store.appendEvent({
|
|
650
|
+
id: `retention-event-${entry.id}`,
|
|
651
|
+
sequence: 1,
|
|
652
|
+
type: "run.started",
|
|
653
|
+
runId: entry.id,
|
|
654
|
+
timestamp: entry.createdAt
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
const purged = await store.purgeRuns({ before });
|
|
658
|
+
assert(purged.runs === 2, "Retention must delete only old terminal runs");
|
|
659
|
+
assert(purged.sessions === 2, "Retention must delete sessions left without runs");
|
|
660
|
+
assert(!await store.getRun(entries[0].id), "Retention must delete the completed run");
|
|
661
|
+
assert(!await store.getRun(entries[1].id), "Retention must delete the failed run");
|
|
662
|
+
assert(await store.getRun(entries[2].id), "Retention must keep recent terminal runs");
|
|
663
|
+
assert(await store.getRun(entries[3].id), "Retention must keep active runs");
|
|
664
|
+
assert(!await store.getSession(`retention-session-${entries[0].id}`), "Retention must delete empty sessions");
|
|
665
|
+
assert(await store.getSession(`retention-session-${entries[2].id}`), "Retention must keep sessions with runs");
|
|
666
|
+
assert((await store.purgeRuns({ limit: 1 })).runs === 1, "Retention must honor the limit");
|
|
667
|
+
await store.purgeRuns({ before: new Date(Date.now() + 864e5).toISOString() });
|
|
668
|
+
assert(!await store.getRun(entries[2].id), "Retention must purge remaining terminal runs");
|
|
669
|
+
assert(await store.getRun(entries[3].id), "Retention must keep active runs after a full purge");
|
|
670
|
+
const boundaryId = `retention-boundary-${suffix}`;
|
|
671
|
+
const boundarySessionId = `retention-boundary-session-${suffix}`;
|
|
672
|
+
const boundaryCreatedAt = "2020-01-01T00:00:00.100Z";
|
|
673
|
+
await store.saveSession({
|
|
674
|
+
id: boundarySessionId,
|
|
675
|
+
history: [],
|
|
676
|
+
createdAt: boundaryCreatedAt,
|
|
677
|
+
updatedAt: boundaryCreatedAt
|
|
678
|
+
});
|
|
679
|
+
await store.saveRun({
|
|
680
|
+
id: boundaryId,
|
|
681
|
+
sessionId: boundarySessionId,
|
|
682
|
+
agentName: "retention-agent",
|
|
683
|
+
status: "completed",
|
|
684
|
+
revision: 1,
|
|
685
|
+
createdAt: boundaryCreatedAt,
|
|
686
|
+
updatedAt: boundaryCreatedAt
|
|
687
|
+
});
|
|
688
|
+
assert((await store.purgeRuns({
|
|
689
|
+
before: "2020-01-01T00:00:00.500Z",
|
|
690
|
+
statuses: ["completed"]
|
|
691
|
+
})).runs === 1, "Retention must compare subsecond timestamps");
|
|
692
|
+
assert(!await store.getRun(boundaryId), "Retention must delete the boundary run");
|
|
693
|
+
}
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region src/testing/fake-model.ts
|
|
696
|
+
/** Creates a deterministic model gateway for unit tests. */
|
|
697
|
+
function fakeModel(...responses) {
|
|
698
|
+
const calls = [];
|
|
699
|
+
return {
|
|
700
|
+
calls,
|
|
701
|
+
async *stream(input) {
|
|
702
|
+
input.signal?.throwIfAborted();
|
|
703
|
+
const response = responses[calls.length];
|
|
704
|
+
calls.push(input);
|
|
705
|
+
if (!response) throw new Error(`fakeModel has no response for call ${calls.length}`);
|
|
706
|
+
if (response.output !== void 0) {
|
|
707
|
+
const delta = typeof response.output === "string" ? response.output : JSON.stringify(response.output);
|
|
708
|
+
if (delta) yield {
|
|
709
|
+
type: "output.delta",
|
|
710
|
+
delta
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
yield {
|
|
714
|
+
type: "completed",
|
|
715
|
+
result: response
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
};
|
|
235
719
|
}
|
|
236
720
|
//#endregion
|
|
237
|
-
export { fakeModel, testModelGateway, testRunStore };
|
|
721
|
+
export { fakeModel, testChannelAdapter, testMemoryStore, testModelGateway, testRetentionStore, testRunStore, testToolProvider };
|
package/dist/tools/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { h as
|
|
2
|
-
import {
|
|
3
|
-
export { ToolApproval, type ToolCall, ToolDefinition, ToolExecutionContext, ToolIdempotency, ToolResult, ToolRetryPolicy, ToolRisk, type ToolSpec, defineTool, toToolSpec };
|
|
1
|
+
import { A as ToolApproval, C as ConnectionToolPolicy, D as ToolProviderContext, E as ToolProvider, F as ToolRetryPolicy, I as ToolRisk, L as defineTool, M as ToolExecutionContext, N as ToolIdempotency, O as ToolProviderTool, P as ToolResult, R as toToolSpec, S as ConnectionDefinition, T as IntegrationErrorCategory, _ as FevexJsonSchemaProfileLimits, a as InMemoryToolEmbeddingCacheOptions, b as compileFevexJsonSchema, c as ToolEmbeddingIndexEntry, d as cosineSimilarity, f as schemaKeywords, g as FEVEX_JSON_SCHEMA_PROFILE_VERSION, h as toolEmbeddingText, i as InMemoryToolEmbeddingCache, j as ToolDefinition, k as defineConnection, l as ToolEmbeddingMatch, m as toolEmbeddingKey, n as ToolSearchCandidate, o as ToolEmbeddingCache, p as searchToolEmbeddingIndex, r as BuildToolEmbeddingIndexOptions, s as ToolEmbeddingIndex, t as ToolReranker, u as buildToolEmbeddingIndex, v as FevexJsonSchemaProfileOptions, w as IntegrationError, x as validateFevexJsonSchemaProfile, y as FevexJsonSchemaValidator } from "../index-DHcfIfh-.mjs";
|
|
2
|
+
import { g as ToolSpec, m as ToolCall } from "../types-CckDlhYo.mjs";
|
|
3
|
+
export { BuildToolEmbeddingIndexOptions, ConnectionDefinition, ConnectionToolPolicy, FEVEX_JSON_SCHEMA_PROFILE_VERSION, FevexJsonSchemaProfileLimits, FevexJsonSchemaProfileOptions, FevexJsonSchemaValidator, InMemoryToolEmbeddingCache, InMemoryToolEmbeddingCacheOptions, IntegrationError, IntegrationErrorCategory, ToolApproval, type ToolCall, ToolDefinition, ToolEmbeddingCache, ToolEmbeddingIndex, ToolEmbeddingIndexEntry, ToolEmbeddingMatch, ToolExecutionContext, ToolIdempotency, ToolProvider, ToolProviderContext, ToolProviderTool, ToolReranker, ToolResult, ToolRetryPolicy, ToolRisk, ToolSearchCandidate, type ToolSpec, buildToolEmbeddingIndex, compileFevexJsonSchema, cosineSimilarity, defineConnection, defineTool, schemaKeywords, searchToolEmbeddingIndex, toToolSpec, toolEmbeddingKey, toolEmbeddingText, validateFevexJsonSchemaProfile };
|