@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.
Files changed (56) hide show
  1. package/README.md +771 -13
  2. package/dist/agents/index.d.mts +2 -2
  3. package/dist/agents/index.mjs +1 -1
  4. package/dist/artifacts/index.d.mts +2 -0
  5. package/dist/artifacts/index.mjs +2 -0
  6. package/dist/artifacts-gUzemET7.mjs +440 -0
  7. package/dist/channels/index.d.mts +2 -0
  8. package/dist/channels/index.mjs +192 -0
  9. package/dist/evals/index.d.mts +19 -11
  10. package/dist/evals/index.mjs +125 -122
  11. package/dist/fevex-_KOsOsB8.d.mts +129 -0
  12. package/dist/http/index.d.mts +150 -0
  13. package/dist/http/index.mjs +692 -0
  14. package/dist/index-26ilbzAs.d.mts +50 -0
  15. package/dist/index-B8EcvWeN.d.mts +103 -0
  16. package/dist/index-BEqYWean.d.mts +118 -0
  17. package/dist/index-Btth7rdL.d.mts +58 -0
  18. package/dist/{index-QTkd6a7C.d.mts → index-CFzrvDQ0.d.mts} +4 -3
  19. package/dist/index-CIHWc5V1.d.mts +116 -0
  20. package/dist/index-D7r0puBi.d.mts +64 -0
  21. package/dist/index-DHcfIfh-.d.mts +224 -0
  22. package/dist/index-DUhNa7IR.d.mts +73 -0
  23. package/dist/index-lCCFokc6.d.mts +617 -0
  24. package/dist/index.d.mts +22 -10
  25. package/dist/index.mjs +4178 -671
  26. package/dist/knowledge/index.d.mts +2 -0
  27. package/dist/knowledge/index.mjs +289 -0
  28. package/dist/model-stream-JQdHDDcj.mjs +131 -0
  29. package/dist/models/index.d.mts +2 -46
  30. package/dist/observability/index.d.mts +2 -2
  31. package/dist/policies/index.d.mts +1 -1
  32. package/dist/policies/index.mjs +1 -1
  33. package/dist/run-error-Cr17wPGH.mjs +23 -0
  34. package/dist/{model-stream-u2xMsEaf.mjs → run-support-CmZ6Xo6H.mjs} +41 -130
  35. package/dist/runtime/index.d.mts +2 -2
  36. package/dist/runtime/index.mjs +271 -9
  37. package/dist/sandbox/index.d.mts +2 -0
  38. package/dist/sandbox/index.mjs +147 -0
  39. package/dist/teams/index.d.mts +2 -0
  40. package/dist/teams/index.mjs +2 -0
  41. package/dist/teams-Ctz676G4.mjs +82 -0
  42. package/dist/testing/index.d.mts +39 -9
  43. package/dist/testing/index.mjs +512 -28
  44. package/dist/tools/index.d.mts +3 -3
  45. package/dist/tools/index.mjs +442 -2
  46. package/dist/types-CckDlhYo.d.mts +575 -0
  47. package/dist/workflows/index.d.mts +2 -0
  48. package/dist/workflows/index.mjs +6 -0
  49. package/package.json +31 -3
  50. package/dist/exact-definition-NHBBFlFR.d.mts +0 -4
  51. package/dist/fevex-Dz6GBz9o.d.mts +0 -32
  52. package/dist/index-BzxHfyYV.d.mts +0 -23
  53. package/dist/index-CZGyhNIQ.d.mts +0 -47
  54. package/dist/index-C_Zpr13f.d.mts +0 -124
  55. package/dist/index-DGWOGXzc.d.mts +0 -181
  56. package/dist/index-ojCY3lwm.d.mts +0 -79
@@ -1,28 +1,7 @@
1
- import { t as readModelStream } from "../model-stream-u2xMsEaf.mjs";
2
- //#region src/testing/index.ts
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
- }
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 };
@@ -1,3 +1,3 @@
1
- import { h as ToolSpec, m as ToolCall } from "../index-C_Zpr13f.mjs";
2
- import { a as ToolResult, c as defineTool, i as ToolIdempotency, l as toToolSpec, n as ToolDefinition, o as ToolRetryPolicy, r as ToolExecutionContext, s as ToolRisk, t as ToolApproval } from "../index-CZGyhNIQ.mjs";
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 };