@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.
Files changed (49) hide show
  1. package/README.md +424 -6
  2. package/dist/agents/index.d.mts +1 -1
  3. package/dist/agents/index.mjs +1 -1
  4. package/dist/channels/index.d.mts +2 -0
  5. package/dist/channels/index.mjs +150 -0
  6. package/dist/evals/index.d.mts +19 -11
  7. package/dist/evals/index.mjs +125 -122
  8. package/dist/fevex-Ddyi6z6L.d.mts +60 -0
  9. package/dist/http/index.d.mts +90 -0
  10. package/dist/http/index.mjs +470 -0
  11. package/dist/{index-ojCY3lwm.d.mts → index-B4PIkqOU.d.mts} +5 -4
  12. package/dist/index-Bg9rr8Qe.d.mts +50 -0
  13. package/dist/index-Bgcxgy66.d.mts +82 -0
  14. package/dist/index-CTIwrVf9.d.mts +51 -0
  15. package/dist/index-CqOjvtQ9.d.mts +132 -0
  16. package/dist/index-CrOId2px.d.mts +66 -0
  17. package/dist/index-DeXUtyd9.d.mts +70 -0
  18. package/dist/index-DviHtsLo.d.mts +58 -0
  19. package/dist/index-Dx_LotWZ.d.mts +425 -0
  20. package/dist/{index-QTkd6a7C.d.mts → index-dkFyTNh4.d.mts} +4 -3
  21. package/dist/index.d.mts +22 -10
  22. package/dist/index.mjs +2784 -720
  23. package/dist/knowledge/index.d.mts +2 -0
  24. package/dist/knowledge/index.mjs +89 -0
  25. package/dist/models/index.d.mts +2 -46
  26. package/dist/observability/index.d.mts +1 -1
  27. package/dist/policies/index.d.mts +1 -1
  28. package/dist/policies/index.mjs +1 -1
  29. package/dist/run-error-Cr17wPGH.mjs +23 -0
  30. package/dist/runtime/index.d.mts +2 -2
  31. package/dist/runtime/index.mjs +58 -9
  32. package/dist/sandbox/index.d.mts +2 -0
  33. package/dist/sandbox/index.mjs +122 -0
  34. package/dist/teams/index.d.mts +2 -0
  35. package/dist/teams/index.mjs +2 -0
  36. package/dist/teams-Ctz676G4.mjs +82 -0
  37. package/dist/testing/index.d.mts +33 -9
  38. package/dist/testing/index.mjs +415 -27
  39. package/dist/tools/index.d.mts +3 -3
  40. package/dist/tools/index.mjs +222 -2
  41. package/dist/types-CuoRx12Q.d.mts +333 -0
  42. package/dist/workflows/index.d.mts +2 -0
  43. package/dist/workflows/index.mjs +6 -0
  44. package/package.json +27 -3
  45. package/dist/fevex-Dz6GBz9o.d.mts +0 -32
  46. package/dist/index-BzxHfyYV.d.mts +0 -23
  47. package/dist/index-CZGyhNIQ.d.mts +0 -47
  48. package/dist/index-C_Zpr13f.d.mts +0 -124
  49. package/dist/index-DGWOGXzc.d.mts +0 -181
@@ -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/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
- }
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 };
@@ -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 { 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 };