@fevex/core 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +424 -6
- package/dist/agents/index.d.mts +1 -1
- package/dist/agents/index.mjs +1 -1
- package/dist/channels/index.d.mts +2 -0
- package/dist/channels/index.mjs +150 -0
- package/dist/evals/index.d.mts +19 -11
- package/dist/evals/index.mjs +125 -122
- package/dist/fevex-Ddyi6z6L.d.mts +60 -0
- package/dist/http/index.d.mts +90 -0
- package/dist/http/index.mjs +470 -0
- package/dist/{index-ojCY3lwm.d.mts → index-B4PIkqOU.d.mts} +5 -4
- package/dist/index-Bg9rr8Qe.d.mts +50 -0
- package/dist/index-Bgcxgy66.d.mts +82 -0
- package/dist/index-CTIwrVf9.d.mts +51 -0
- package/dist/index-CqOjvtQ9.d.mts +132 -0
- package/dist/index-CrOId2px.d.mts +66 -0
- package/dist/index-DeXUtyd9.d.mts +70 -0
- package/dist/index-DviHtsLo.d.mts +58 -0
- package/dist/index-Dx_LotWZ.d.mts +425 -0
- package/dist/{index-QTkd6a7C.d.mts → index-dkFyTNh4.d.mts} +4 -3
- package/dist/index.d.mts +22 -10
- package/dist/index.mjs +2784 -720
- package/dist/knowledge/index.d.mts +2 -0
- package/dist/knowledge/index.mjs +89 -0
- package/dist/models/index.d.mts +2 -46
- package/dist/observability/index.d.mts +1 -1
- 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/runtime/index.d.mts +2 -2
- package/dist/runtime/index.mjs +58 -9
- package/dist/sandbox/index.d.mts +2 -0
- package/dist/sandbox/index.mjs +122 -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 +33 -9
- package/dist/testing/index.mjs +415 -27
- package/dist/tools/index.d.mts +3 -3
- package/dist/tools/index.mjs +222 -2
- package/dist/types-CuoRx12Q.d.mts +333 -0
- package/dist/workflows/index.d.mts +2 -0
- package/dist/workflows/index.mjs +6 -0
- package/package.json +27 -3
- 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/testing/index.mjs
CHANGED
|
@@ -1,28 +1,6 @@
|
|
|
1
|
+
import { IntegrationError } from "../tools/index.mjs";
|
|
1
2
|
import { t as readModelStream } from "../model-stream-u2xMsEaf.mjs";
|
|
2
|
-
//#region src/testing/
|
|
3
|
-
function fakeModel(...responses) {
|
|
4
|
-
const calls = [];
|
|
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
|
-
}
|
|
3
|
+
//#region src/testing/contracts.ts
|
|
26
4
|
const contractOutputSchema = {
|
|
27
5
|
type: "object",
|
|
28
6
|
properties: { answer: { type: "string" } },
|
|
@@ -38,6 +16,117 @@ const contractToolInputSchema = {
|
|
|
38
16
|
function assert(condition, message) {
|
|
39
17
|
if (!condition) throw new TypeError(message);
|
|
40
18
|
}
|
|
19
|
+
async function assertRejects(operation, message, code) {
|
|
20
|
+
try {
|
|
21
|
+
await operation();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (code !== void 0) assert(error instanceof Error && "code" in error && error.code === code, message);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw new TypeError(message);
|
|
27
|
+
}
|
|
28
|
+
async function testMemoryStore(store) {
|
|
29
|
+
const actor = { id: `actor-${crypto.randomUUID()}` };
|
|
30
|
+
const sessionId = `session-${crypto.randomUUID()}`;
|
|
31
|
+
const context = {
|
|
32
|
+
agentName: "assistant",
|
|
33
|
+
input: "refund status",
|
|
34
|
+
sessionId,
|
|
35
|
+
context: {
|
|
36
|
+
namespace: "tenant-a",
|
|
37
|
+
actor
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const saved = await store.write({
|
|
41
|
+
content: "Refund status is approved.",
|
|
42
|
+
agentName: "assistant",
|
|
43
|
+
sessionId,
|
|
44
|
+
namespace: "tenant-a",
|
|
45
|
+
actor,
|
|
46
|
+
metadata: { source: "contract" }
|
|
47
|
+
}, context);
|
|
48
|
+
assert(saved.id.trim(), "MemoryRecord id cannot be empty");
|
|
49
|
+
assert(saved.createdAt.trim(), "MemoryRecord createdAt cannot be empty");
|
|
50
|
+
saved.content = "mutated";
|
|
51
|
+
assert((await store.search({
|
|
52
|
+
query: "refund",
|
|
53
|
+
sessionId,
|
|
54
|
+
namespace: "tenant-a",
|
|
55
|
+
actor
|
|
56
|
+
}, context))[0]?.content === "Refund status is approved.", "MemoryStore must not expose mutable record references");
|
|
57
|
+
assert((await store.search({
|
|
58
|
+
query: "refund",
|
|
59
|
+
sessionId: "other"
|
|
60
|
+
}, context)).length === 0, "MemoryStore must isolate sessions");
|
|
61
|
+
assert((await store.search({
|
|
62
|
+
query: "refund",
|
|
63
|
+
namespace: "tenant-b"
|
|
64
|
+
}, context)).length === 0, "MemoryStore must isolate namespaces");
|
|
65
|
+
assert((await store.search({
|
|
66
|
+
query: "refund",
|
|
67
|
+
actor: { id: "other" }
|
|
68
|
+
}, context)).length === 0, "MemoryStore must isolate actors");
|
|
69
|
+
await store.write({
|
|
70
|
+
content: "Refund backup note.",
|
|
71
|
+
sessionId,
|
|
72
|
+
namespace: "tenant-a",
|
|
73
|
+
actor
|
|
74
|
+
}, context);
|
|
75
|
+
assert((await store.search({
|
|
76
|
+
query: "refund",
|
|
77
|
+
sessionId,
|
|
78
|
+
namespace: "tenant-a",
|
|
79
|
+
actor,
|
|
80
|
+
limit: 1
|
|
81
|
+
}, context)).length === 1, "MemoryStore must respect limits");
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
controller.abort();
|
|
84
|
+
await store.search({ query: "refund" }, {
|
|
85
|
+
...context,
|
|
86
|
+
signal: controller.signal
|
|
87
|
+
}).then(() => {
|
|
88
|
+
throw new TypeError("MemoryStore must reject aborted searches");
|
|
89
|
+
}, () => {});
|
|
90
|
+
}
|
|
91
|
+
async function testToolProvider(provider, contract) {
|
|
92
|
+
const tools = await provider.listTools({});
|
|
93
|
+
assert(tools.some((tool) => tool.name === contract.allowedTool), "ToolProvider must list the allowed contract tool");
|
|
94
|
+
if (contract.disallowedTool !== void 0) assert(!tools.some((tool) => tool.name === contract.disallowedTool), "ToolProvider must not list disallowed tools");
|
|
95
|
+
const output = await provider.callTool(contract.allowedTool, contract.input ?? { query: "value" }, {});
|
|
96
|
+
assert(JSON.stringify(output) === JSON.stringify(contract.output ?? { answer: "ok" }), "ToolProvider returned an unexpected output");
|
|
97
|
+
if (contract.safeError) await Promise.resolve(provider.callTool("contract_error", {}, {})).then(() => {
|
|
98
|
+
throw new TypeError("ToolProvider must reject configured error calls");
|
|
99
|
+
}, (error) => {
|
|
100
|
+
assert(error instanceof IntegrationError, "ToolProvider errors must be IntegrationError");
|
|
101
|
+
assert(error.message === error.safeMessage, "IntegrationError message must be safe");
|
|
102
|
+
});
|
|
103
|
+
if (contract.timeout) {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
controller.abort();
|
|
106
|
+
await Promise.resolve(provider.callTool(contract.allowedTool, {}, { signal: controller.signal })).then(() => {
|
|
107
|
+
throw new TypeError("ToolProvider must reject aborted calls");
|
|
108
|
+
}, () => {});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function testChannelAdapter(adapter, contract) {
|
|
112
|
+
assert(adapter.name.trim(), "ChannelAdapter name cannot be empty");
|
|
113
|
+
const message = await adapter.parse(contract.input, {});
|
|
114
|
+
assert(message !== null, "ChannelAdapter must parse the contract input");
|
|
115
|
+
for (const [name, value] of [
|
|
116
|
+
["id", message.id],
|
|
117
|
+
["deliveryId", message.deliveryId],
|
|
118
|
+
["conversationId", message.conversationId],
|
|
119
|
+
["content", message.content]
|
|
120
|
+
]) assert(value.trim(), `ChannelMessage ${name} cannot be empty`);
|
|
121
|
+
if (message.threadId !== void 0) assert(message.threadId.trim(), "ChannelMessage threadId cannot be empty");
|
|
122
|
+
if (message.actor !== void 0) assert(message.actor.id.trim(), "ChannelMessage actor id cannot be empty");
|
|
123
|
+
if (message.metadata !== void 0) JSON.stringify(message.metadata);
|
|
124
|
+
assert(JSON.stringify(message) === JSON.stringify(contract.message), "ChannelAdapter returned an unexpected message");
|
|
125
|
+
if (contract.ignoredInput !== void 0) assert(await adapter.parse(contract.ignoredInput, {}) === null, "ChannelAdapter must ignore the configured input");
|
|
126
|
+
if (contract.output.threadId !== void 0) assert(contract.output.threadId.trim(), "ChannelOutput threadId cannot be empty");
|
|
127
|
+
if (contract.output.metadata !== void 0) JSON.stringify(contract.output.metadata);
|
|
128
|
+
assert(JSON.stringify(await adapter.deliver(contract.output, {})) === JSON.stringify(contract.delivered), "ChannelAdapter returned an unexpected delivery");
|
|
129
|
+
}
|
|
41
130
|
async function testModelGateway(model, contract = {}) {
|
|
42
131
|
const output = contract.output ?? { answer: "ok" };
|
|
43
132
|
const toolCall = contract.toolCall ?? {
|
|
@@ -109,6 +198,126 @@ async function testRunStore(store) {
|
|
|
109
198
|
const runId = `contract-run-${suffix}`;
|
|
110
199
|
const sessionId = `contract-session-${suffix}`;
|
|
111
200
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
201
|
+
const atomicRunId = `contract-atomic-run-${suffix}`;
|
|
202
|
+
const atomicSessionId = `contract-atomic-session-${suffix}`;
|
|
203
|
+
const atomicRun = {
|
|
204
|
+
id: atomicRunId,
|
|
205
|
+
sessionId: atomicSessionId,
|
|
206
|
+
agentName: "contract-agent",
|
|
207
|
+
status: "running",
|
|
208
|
+
revision: 0,
|
|
209
|
+
createdAt: now,
|
|
210
|
+
updatedAt: now
|
|
211
|
+
};
|
|
212
|
+
const atomicSession = {
|
|
213
|
+
id: atomicSessionId,
|
|
214
|
+
history: [],
|
|
215
|
+
createdAt: now,
|
|
216
|
+
updatedAt: now
|
|
217
|
+
};
|
|
218
|
+
const atomicCheckpoint = {
|
|
219
|
+
version: 2,
|
|
220
|
+
runId: atomicRunId,
|
|
221
|
+
definitionHash: "atomic-definition",
|
|
222
|
+
messages: [{
|
|
223
|
+
role: "user",
|
|
224
|
+
content: "atomic input"
|
|
225
|
+
}],
|
|
226
|
+
inputContent: "atomic input",
|
|
227
|
+
step: 1,
|
|
228
|
+
toolCallCount: 0,
|
|
229
|
+
seenToolCallIds: [],
|
|
230
|
+
pendingTools: [],
|
|
231
|
+
pendingIndex: 0
|
|
232
|
+
};
|
|
233
|
+
const atomicLease = {
|
|
234
|
+
generation: 0,
|
|
235
|
+
runId: atomicRunId,
|
|
236
|
+
ownerId: `atomic-owner-${suffix}`,
|
|
237
|
+
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
238
|
+
};
|
|
239
|
+
const atomicStarted = {
|
|
240
|
+
id: `atomic-event-${suffix}`,
|
|
241
|
+
sequence: 1,
|
|
242
|
+
type: "run.started",
|
|
243
|
+
runId: atomicRunId,
|
|
244
|
+
timestamp: now
|
|
245
|
+
};
|
|
246
|
+
assert(await store.createExecution({
|
|
247
|
+
run: atomicRun,
|
|
248
|
+
session: atomicSession,
|
|
249
|
+
checkpoint: atomicCheckpoint,
|
|
250
|
+
lease: atomicLease,
|
|
251
|
+
events: [atomicStarted]
|
|
252
|
+
}), "RunStore must atomically create a new execution");
|
|
253
|
+
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");
|
|
254
|
+
assert(!await store.acquireLease({
|
|
255
|
+
...atomicLease,
|
|
256
|
+
ownerId: `atomic-competitor-${suffix}`
|
|
257
|
+
}), "RunStore createExecution must acquire the initial lease");
|
|
258
|
+
assert(!await store.createExecution({
|
|
259
|
+
run: {
|
|
260
|
+
...atomicRun,
|
|
261
|
+
revision: 0
|
|
262
|
+
},
|
|
263
|
+
session: await store.getSession(atomicRun.sessionId),
|
|
264
|
+
checkpoint: atomicCheckpoint,
|
|
265
|
+
lease: atomicLease,
|
|
266
|
+
events: [atomicStarted]
|
|
267
|
+
}), "RunStore createExecution must reject duplicate run ids");
|
|
268
|
+
const ownedSession = await store.getSession(atomicRun.sessionId);
|
|
269
|
+
const competitorId = `competitor-${suffix}`;
|
|
270
|
+
const competitor = () => ({
|
|
271
|
+
run: {
|
|
272
|
+
...atomicRun,
|
|
273
|
+
id: competitorId,
|
|
274
|
+
revision: 0,
|
|
275
|
+
status: "running"
|
|
276
|
+
},
|
|
277
|
+
session: structuredClone(ownedSession),
|
|
278
|
+
checkpoint: {
|
|
279
|
+
...atomicCheckpoint,
|
|
280
|
+
runId: competitorId
|
|
281
|
+
},
|
|
282
|
+
lease: {
|
|
283
|
+
...atomicLease,
|
|
284
|
+
runId: competitorId
|
|
285
|
+
},
|
|
286
|
+
events: []
|
|
287
|
+
});
|
|
288
|
+
assert(!await store.createExecution(competitor()), "Active sessions must reject another run");
|
|
289
|
+
await assertRejects(() => store.saveSession(structuredClone(ownedSession)), "Compaction must reject an active session");
|
|
290
|
+
assert(await store.commitExecution({
|
|
291
|
+
lease: atomicLease,
|
|
292
|
+
expectedRevision: atomicRun.revision,
|
|
293
|
+
run: Object.assign(atomicRun, { status: "paused" })
|
|
294
|
+
}), "Owner must be able to pause");
|
|
295
|
+
await store.releaseLease(atomicRunId, atomicLease.ownerId, atomicLease.generation);
|
|
296
|
+
assert(!await store.createExecution(competitor()), "Paused sessions remain reserved without a worker lease");
|
|
297
|
+
await assertRejects(() => store.saveSession(structuredClone(ownedSession)), "Compaction must reject a paused session");
|
|
298
|
+
assert(await store.acquireLease(atomicLease), "Resume must acquire a new lease generation");
|
|
299
|
+
assert(await store.commitExecution({
|
|
300
|
+
lease: atomicLease,
|
|
301
|
+
expectedRevision: atomicRun.revision,
|
|
302
|
+
run: Object.assign(atomicRun, { status: "completed" }),
|
|
303
|
+
session: ownedSession
|
|
304
|
+
}), "Terminal commit must release the session");
|
|
305
|
+
const staleSession = structuredClone(ownedSession);
|
|
306
|
+
await store.saveSession(ownedSession);
|
|
307
|
+
await assertRejects(() => store.saveSession(staleSession), "Stale compaction must not overwrite history");
|
|
308
|
+
assert(!await store.createExecution({
|
|
309
|
+
...competitor(),
|
|
310
|
+
session: staleSession
|
|
311
|
+
}), "Run creation must reject history read before compaction");
|
|
312
|
+
const next = competitor();
|
|
313
|
+
assert(await store.createExecution(next), "Terminal sessions can be reserved again");
|
|
314
|
+
assert(!await store.commitExecution({
|
|
315
|
+
lease: next.lease,
|
|
316
|
+
expectedRevision: next.run.revision,
|
|
317
|
+
run: next.run,
|
|
318
|
+
session: staleSession
|
|
319
|
+
}), "A stale history commit must fail atomically");
|
|
320
|
+
assert((await store.getRun(next.run.id))?.revision === next.run.revision, "Rejected history commits must not advance the run");
|
|
112
321
|
const session = {
|
|
113
322
|
id: sessionId,
|
|
114
323
|
history: [],
|
|
@@ -126,10 +335,23 @@ async function testRunStore(store) {
|
|
|
126
335
|
};
|
|
127
336
|
await store.saveSession(session);
|
|
128
337
|
await store.saveRun(run);
|
|
338
|
+
const commitLease = {
|
|
339
|
+
generation: 0,
|
|
340
|
+
runId,
|
|
341
|
+
ownerId: `writer-${suffix}`,
|
|
342
|
+
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
343
|
+
};
|
|
344
|
+
assert(await store.acquireLease(commitLease), "Writer must acquire a lease");
|
|
129
345
|
const snapshot = await store.getRun(runId);
|
|
130
346
|
assert(snapshot?.status === "running", "RunStore must return saved runs");
|
|
131
347
|
snapshot.status = "failed";
|
|
132
348
|
assert((await store.getRun(runId))?.status === "running", "RunStore must not expose mutable run references");
|
|
349
|
+
await store.saveRun({
|
|
350
|
+
...run,
|
|
351
|
+
status: "completed"
|
|
352
|
+
});
|
|
353
|
+
assert((await store.getRun(runId))?.status === "completed", "RunStore must overwrite an existing run on save");
|
|
354
|
+
await store.saveRun(run);
|
|
133
355
|
const firstEvent = {
|
|
134
356
|
id: `event-1-${suffix}`,
|
|
135
357
|
sequence: 1,
|
|
@@ -139,6 +361,7 @@ async function testRunStore(store) {
|
|
|
139
361
|
};
|
|
140
362
|
await store.appendEvent(firstEvent);
|
|
141
363
|
const checkpoint = {
|
|
364
|
+
version: 2,
|
|
142
365
|
runId,
|
|
143
366
|
definitionHash: "definition",
|
|
144
367
|
messages: [{
|
|
@@ -189,6 +412,7 @@ async function testRunStore(store) {
|
|
|
189
412
|
}
|
|
190
413
|
};
|
|
191
414
|
assert(await store.commitExecution({
|
|
415
|
+
lease: commitLease,
|
|
192
416
|
expectedRevision: 0,
|
|
193
417
|
run,
|
|
194
418
|
session,
|
|
@@ -199,6 +423,7 @@ async function testRunStore(store) {
|
|
|
199
423
|
assert(run.revision === 1, "RunStore must advance the committed run revision");
|
|
200
424
|
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
425
|
assert(!await store.commitExecution({
|
|
426
|
+
lease: commitLease,
|
|
202
427
|
expectedRevision: 0,
|
|
203
428
|
run: {
|
|
204
429
|
...run,
|
|
@@ -213,14 +438,64 @@ async function testRunStore(store) {
|
|
|
213
438
|
payload: { error: "stale" }
|
|
214
439
|
}]
|
|
215
440
|
}), "RunStore must reject stale revisions");
|
|
441
|
+
const firstPage = await store.listEvents(runId, { limit: 1 });
|
|
442
|
+
assert(firstPage.length === 1 && firstPage[0]?.id === firstEvent.id, "Event pagination must honor limit and start at the first sequence");
|
|
443
|
+
assert((await store.listEvents(runId, {
|
|
444
|
+
after: firstEvent.id,
|
|
445
|
+
limit: 1
|
|
446
|
+
}))[0]?.id === secondEvent.id, "Event pagination must continue after the cursor");
|
|
447
|
+
assert((await store.listEvents(runId, {
|
|
448
|
+
after: secondEvent.id,
|
|
449
|
+
limit: 1
|
|
450
|
+
})).length === 0, "Event pagination after the last event must be empty");
|
|
451
|
+
const latestPage = await store.listEvents(runId, {
|
|
452
|
+
order: "desc",
|
|
453
|
+
limit: 1
|
|
454
|
+
});
|
|
455
|
+
assert(latestPage.length === 1 && latestPage[0]?.id === secondEvent.id, "Descending pagination must honor limit and find the latest event");
|
|
456
|
+
const descending = await store.listEvents(runId, {
|
|
457
|
+
order: "desc",
|
|
458
|
+
limit: 2
|
|
459
|
+
});
|
|
460
|
+
assert(descending.length === 2 && descending[0]?.id === secondEvent.id && descending[1]?.id === firstEvent.id, "Event pagination must order by sequence");
|
|
461
|
+
const descendingAfter = await store.listEvents(runId, {
|
|
462
|
+
after: firstEvent.id,
|
|
463
|
+
order: "desc",
|
|
464
|
+
limit: 2
|
|
465
|
+
});
|
|
466
|
+
assert(descendingAfter.length === 1 && descendingAfter[0]?.id === secondEvent.id, "Event cursors must select strictly newer sequences even in descending order");
|
|
467
|
+
await assertRejects(() => store.listEvents(runId, {
|
|
468
|
+
after: atomicStarted.id,
|
|
469
|
+
limit: 1
|
|
470
|
+
}), "Event pagination must reject a cursor from another run with INVALID_CURSOR", "INVALID_CURSOR");
|
|
471
|
+
for (const limit of [
|
|
472
|
+
0,
|
|
473
|
+
-1,
|
|
474
|
+
NaN,
|
|
475
|
+
Infinity,
|
|
476
|
+
1.5
|
|
477
|
+
]) await assertRejects(() => store.listEvents(runId, { limit }), "Event pagination must validate limits");
|
|
216
478
|
const events = await store.listEvents(runId, { after: firstEvent.id });
|
|
217
479
|
assert(events.length === 1 && events[0]?.id === secondEvent.id, "RunStore cursors must return stable ordered suffixes");
|
|
480
|
+
await assertRejects(() => store.listEvents(`missing-run-${suffix}`), "RunStore listEvents must reject unknown runs");
|
|
481
|
+
await assertRejects(() => store.listEvents(runId, { after: `missing-cursor-${suffix}` }), "RunStore listEvents must reject unknown cursors with INVALID_CURSOR", "INVALID_CURSOR");
|
|
482
|
+
assert(await store.commitExecution({
|
|
483
|
+
lease: commitLease,
|
|
484
|
+
expectedRevision: 1,
|
|
485
|
+
run,
|
|
486
|
+
checkpoint: null
|
|
487
|
+
}), "RunStore must commit a checkpoint deletion");
|
|
488
|
+
assert(await store.getCheckpoint(runId) === void 0, "RunStore must delete the checkpoint when a commit sets it to null");
|
|
489
|
+
assert((await store.getToolExecution(runId, "tool-call"))?.status === "completed", "RunStore must keep the tool ledger when a checkpoint is deleted");
|
|
490
|
+
await store.releaseLease(runId, commitLease.ownerId, commitLease.generation);
|
|
218
491
|
const lease1 = {
|
|
492
|
+
generation: 0,
|
|
219
493
|
runId,
|
|
220
494
|
ownerId: `owner-1-${suffix}`,
|
|
221
495
|
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
222
496
|
};
|
|
223
497
|
const lease2 = {
|
|
498
|
+
generation: 0,
|
|
224
499
|
runId,
|
|
225
500
|
ownerId: `owner-2-${suffix}`,
|
|
226
501
|
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
@@ -229,9 +504,122 @@ async function testRunStore(store) {
|
|
|
229
504
|
assert(!await store.acquireLease(lease2), "RunStore must reject a competing lease");
|
|
230
505
|
assert(!await store.renewLease(lease2), "RunStore must reject renewal by another owner");
|
|
231
506
|
assert(await store.renewLease(lease1), "RunStore must renew a matching lease");
|
|
232
|
-
await store.releaseLease(runId, lease1.ownerId);
|
|
507
|
+
await store.releaseLease(runId, lease1.ownerId, lease1.generation);
|
|
233
508
|
assert(await store.acquireLease(lease2), "RunStore must release a lease for another owner");
|
|
234
|
-
await store.releaseLease(runId, lease2.ownerId);
|
|
509
|
+
await store.releaseLease(runId, lease2.ownerId, lease2.generation);
|
|
510
|
+
assert(!await store.renewLease(lease1), "RunStore must reject renewal of a released lease");
|
|
511
|
+
const expiredLease = {
|
|
512
|
+
generation: 0,
|
|
513
|
+
runId,
|
|
514
|
+
ownerId: `owner-3-${suffix}`,
|
|
515
|
+
expiresAt: (/* @__PURE__ */ new Date(Date.now() - 1e3)).toISOString()
|
|
516
|
+
};
|
|
517
|
+
const takeoverLease = {
|
|
518
|
+
generation: 0,
|
|
519
|
+
runId,
|
|
520
|
+
ownerId: `owner-4-${suffix}`,
|
|
521
|
+
expiresAt: new Date(Date.now() + 3e4).toISOString()
|
|
522
|
+
};
|
|
523
|
+
assert(await store.acquireLease(expiredLease), "RunStore must acquire a free lease");
|
|
524
|
+
const beforeTakeover = await store.getRun(runId);
|
|
525
|
+
assert(!await store.commitExecution({
|
|
526
|
+
lease: expiredLease,
|
|
527
|
+
expectedRevision: beforeTakeover.revision,
|
|
528
|
+
run: {
|
|
529
|
+
...beforeTakeover,
|
|
530
|
+
status: "failed"
|
|
531
|
+
}
|
|
532
|
+
}), "An expired lease must not commit even before takeover");
|
|
533
|
+
assert(!await store.renewLease({
|
|
534
|
+
...expiredLease,
|
|
535
|
+
expiresAt: takeoverLease.expiresAt
|
|
536
|
+
}), "An expired lease cannot be revived by renewal");
|
|
537
|
+
assert(await store.acquireLease(takeoverLease), "RunStore must let another owner take over an expired lease");
|
|
538
|
+
await store.releaseLease(runId, takeoverLease.ownerId, takeoverLease.generation);
|
|
539
|
+
const oldToken = { ...takeoverLease };
|
|
540
|
+
assert(await store.acquireLease(takeoverLease), "A released owner can acquire a new generation");
|
|
541
|
+
assert(takeoverLease.generation > oldToken.generation, "Generation must increase even for the same owner");
|
|
542
|
+
const currentRun = await store.getRun(runId);
|
|
543
|
+
const currentSession = await store.getSession(sessionId);
|
|
544
|
+
const currentEvents = await store.listEvents(runId);
|
|
545
|
+
const currentTool = await store.getToolExecution(runId, "tool-call");
|
|
546
|
+
assert(currentRun.revision === beforeTakeover.revision, "Takeover must be tested before revision changes");
|
|
547
|
+
const staleCommit = {
|
|
548
|
+
lease: oldToken,
|
|
549
|
+
expectedRevision: currentRun.revision,
|
|
550
|
+
run: {
|
|
551
|
+
...currentRun,
|
|
552
|
+
status: "failed"
|
|
553
|
+
},
|
|
554
|
+
session: {
|
|
555
|
+
...currentSession,
|
|
556
|
+
history: []
|
|
557
|
+
},
|
|
558
|
+
checkpoint,
|
|
559
|
+
toolExecution: {
|
|
560
|
+
...toolExecution,
|
|
561
|
+
output: "stale"
|
|
562
|
+
},
|
|
563
|
+
events: [{
|
|
564
|
+
...firstEvent,
|
|
565
|
+
id: `stale-owner-${suffix}`,
|
|
566
|
+
sequence: 3
|
|
567
|
+
}]
|
|
568
|
+
};
|
|
569
|
+
assert(!await store.commitExecution(staleCommit), "A previous generation must not commit with a still-current run revision");
|
|
570
|
+
assert(!await store.commitExecution({
|
|
571
|
+
...staleCommit,
|
|
572
|
+
lease: {
|
|
573
|
+
...takeoverLease,
|
|
574
|
+
ownerId: "wrong-owner"
|
|
575
|
+
}
|
|
576
|
+
}), "Generation alone must not authorize a commit");
|
|
577
|
+
assert(!await store.renewLease({
|
|
578
|
+
...oldToken,
|
|
579
|
+
expiresAt: takeoverLease.expiresAt
|
|
580
|
+
}), "A stale generation must not renew a new lease owned by the same owner");
|
|
581
|
+
await store.releaseLease(runId, oldToken.ownerId, oldToken.generation);
|
|
582
|
+
assert(await store.renewLease(takeoverLease), "A stale release must not revoke the new generation");
|
|
583
|
+
assert(JSON.stringify(await store.getRun(runId)) === JSON.stringify(currentRun), "Rejected lease commits must not change the run");
|
|
584
|
+
assert(JSON.stringify(await store.getSession(sessionId)) === JSON.stringify(currentSession), "Rejected lease commits must not change history");
|
|
585
|
+
assert(JSON.stringify(await store.listEvents(runId)) === JSON.stringify(currentEvents), "Rejected lease commits must not append events");
|
|
586
|
+
assert(JSON.stringify(await store.getToolExecution(runId, "tool-call")) === JSON.stringify(currentTool), "Rejected lease commits must not change the tool ledger");
|
|
587
|
+
assert(await store.getCheckpoint(runId) === void 0, "Rejected lease commits must not create checkpoints");
|
|
588
|
+
assert(await store.commitExecution({
|
|
589
|
+
lease: takeoverLease,
|
|
590
|
+
expectedRevision: currentRun.revision,
|
|
591
|
+
run: {
|
|
592
|
+
...currentRun,
|
|
593
|
+
status: "completed"
|
|
594
|
+
}
|
|
595
|
+
}), "The current generation must be able to commit");
|
|
596
|
+
await store.releaseLease(runId, takeoverLease.ownerId, takeoverLease.generation);
|
|
597
|
+
}
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/testing/fake-model.ts
|
|
600
|
+
/** Creates a deterministic model gateway for unit tests. */
|
|
601
|
+
function fakeModel(...responses) {
|
|
602
|
+
const calls = [];
|
|
603
|
+
return {
|
|
604
|
+
calls,
|
|
605
|
+
async *stream(input) {
|
|
606
|
+
input.signal?.throwIfAborted();
|
|
607
|
+
const response = responses[calls.length];
|
|
608
|
+
calls.push(input);
|
|
609
|
+
if (!response) throw new Error(`fakeModel has no response for call ${calls.length}`);
|
|
610
|
+
if (response.output !== void 0) {
|
|
611
|
+
const delta = typeof response.output === "string" ? response.output : JSON.stringify(response.output);
|
|
612
|
+
if (delta) yield {
|
|
613
|
+
type: "output.delta",
|
|
614
|
+
delta
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
yield {
|
|
618
|
+
type: "completed",
|
|
619
|
+
result: response
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
};
|
|
235
623
|
}
|
|
236
624
|
//#endregion
|
|
237
|
-
export { fakeModel, testModelGateway, testRunStore };
|
|
625
|
+
export { fakeModel, testChannelAdapter, testMemoryStore, testModelGateway, testRunStore, testToolProvider };
|
package/dist/tools/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
export { ToolApproval, type ToolCall, ToolDefinition, ToolExecutionContext, ToolIdempotency, ToolResult, ToolRetryPolicy, ToolRisk, type ToolSpec, defineTool, toToolSpec };
|
|
1
|
+
import { g as ToolSpec, m as ToolCall } from "../types-CuoRx12Q.mjs";
|
|
2
|
+
import { C as toToolSpec, S as defineTool, _ as ToolExecutionContext, a as compileFevexJsonSchema, b as ToolRetryPolicy, c as ConnectionToolPolicy, d as ToolProvider, f as ToolProviderContext, g as ToolDefinition, h as ToolApproval, i as FevexJsonSchemaValidator, l as IntegrationError, m as defineConnection, n as FevexJsonSchemaProfileLimits, o as validateFevexJsonSchemaProfile, p as ToolProviderTool, r as FevexJsonSchemaProfileOptions, s as ConnectionDefinition, t as FEVEX_JSON_SCHEMA_PROFILE_VERSION, u as IntegrationErrorCategory, v as ToolIdempotency, x as ToolRisk, y as ToolResult } from "../index-CqOjvtQ9.mjs";
|
|
3
|
+
export { ConnectionDefinition, ConnectionToolPolicy, FEVEX_JSON_SCHEMA_PROFILE_VERSION, FevexJsonSchemaProfileLimits, FevexJsonSchemaProfileOptions, FevexJsonSchemaValidator, IntegrationError, IntegrationErrorCategory, ToolApproval, type ToolCall, ToolDefinition, ToolExecutionContext, ToolIdempotency, ToolProvider, ToolProviderContext, ToolProviderTool, ToolResult, ToolRetryPolicy, ToolRisk, type ToolSpec, compileFevexJsonSchema, defineConnection, defineTool, toToolSpec, validateFevexJsonSchemaProfile };
|