@effect-agent/testing 0.1.0-beta.6 → 0.1.0-beta.60

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 (54) hide show
  1. package/dist/Certification.d.mts +105 -0
  2. package/dist/Certification.mjs +590 -0
  3. package/dist/Certification.mjs.map +1 -0
  4. package/dist/Chaos.d.mts +126 -0
  5. package/dist/Chaos.mjs +755 -0
  6. package/dist/Chaos.mjs.map +1 -0
  7. package/dist/CodeExecutorConformance.d.mts +33 -0
  8. package/dist/CodeExecutorConformance.mjs +231 -0
  9. package/dist/CodeExecutorConformance.mjs.map +1 -0
  10. package/dist/CodeExecutorSubstitute.d.mts +21 -0
  11. package/dist/CodeExecutorSubstitute.mjs +364 -0
  12. package/dist/CodeExecutorSubstitute.mjs.map +1 -0
  13. package/dist/DocsResearcher.d.mts +270 -0
  14. package/dist/DocsResearcher.mjs +490 -0
  15. package/dist/DocsResearcher.mjs.map +1 -0
  16. package/dist/ScriptedModel-DAvxIiud.d.mts +220 -0
  17. package/dist/ScriptedModel.d.mts +2 -0
  18. package/dist/ScriptedModel.mjs +155 -0
  19. package/dist/ScriptedModel.mjs.map +1 -0
  20. package/dist/TravelPlanner.d.mts +1665 -0
  21. package/dist/TravelPlanner.mjs +1958 -0
  22. package/dist/TravelPlanner.mjs.map +1 -0
  23. package/dist/deterministic-layers-Eka0fMZq.mjs +358 -0
  24. package/dist/deterministic-layers-Eka0fMZq.mjs.map +1 -0
  25. package/dist/index.d.mts +2 -3378
  26. package/dist/index.mjs +2 -4766
  27. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  28. package/package.json +1 -48
  29. package/src/{certification.ts → Certification.ts} +251 -115
  30. package/src/{chaos.ts → Chaos.ts} +246 -122
  31. package/src/{code-executor-conformance.ts → CodeExecutorConformance.ts} +29 -6
  32. package/src/{code-executor-substitute.ts → CodeExecutorSubstitute.ts} +104 -42
  33. package/src/{fixtures/docs-researcher/index.ts → DocsResearcher.ts} +68 -5
  34. package/src/{scripted-model.ts → ScriptedModel.ts} +22 -25
  35. package/src/TravelPlanner.ts +232 -0
  36. package/src/fixtures/docs-researcher/definition.ts +19 -10
  37. package/src/fixtures/docs-researcher/harness.ts +41 -30
  38. package/src/fixtures/docs-researcher/mcp.ts +49 -3
  39. package/src/fixtures/travel-planner/definition.ts +15 -2
  40. package/src/fixtures/travel-planner/deterministic-layers.ts +47 -4
  41. package/src/fixtures/travel-planner/phase2.ts +4 -3
  42. package/src/fixtures/travel-planner/phase3.ts +18 -37
  43. package/src/fixtures/travel-planner/phase4.ts +23 -36
  44. package/src/fixtures/travel-planner/phase5.ts +38 -41
  45. package/src/fixtures/travel-planner/phase6.ts +182 -84
  46. package/src/fixtures/travel-planner/phase7.ts +4 -102
  47. package/src/fixtures/travel-planner/scenarios.ts +3 -4
  48. package/src/fixtures/travel-planner/subagents-durable.ts +33 -57
  49. package/src/fixtures/travel-planner/subagents.ts +35 -13
  50. package/src/index.ts +1 -11
  51. package/dist/index.mjs.map +0 -1
  52. package/src/code-executor-conformance.d.ts +0 -30
  53. package/src/fixtures/travel-planner/index.ts +0 -11
  54. package/src/fixtures/warehouse/index.ts +0 -412
@@ -1,67 +1,72 @@
1
+ import * as Subagent from "@effect-agent/capabilities/Subagent";
2
+ import { SubagentPolicy, SubagentRuntime } from "@effect-agent/capabilities/Subagent";
3
+ import { SubagentReservationsMemoryLive } from "@effect-agent/capabilities/SubagentReservations";
4
+ import * as Agent from "@effect-agent/core/Agent";
5
+ import { AgentPolicy } from "@effect-agent/core/AgentPolicy";
1
6
  import {
2
- Subagent,
3
- SubagentPolicy,
4
- SubagentReservationsMemoryLive,
5
- SubagentRuntime,
6
- } from "@effect-agent/capabilities";
7
- import {
8
- Agent,
9
- AgentPolicy,
10
- ConversationId,
11
- IdGenerator,
7
+ ThreadId,
12
8
  RunId,
13
9
  ToolCallId,
14
10
  TurnId,
15
11
  type AgentId,
16
12
  type SubmissionId,
17
- } from "@effect-agent/core";
18
- import { DurableStep, DurableStepError } from "@effect-agent/engine";
13
+ } from "@effect-agent/core/Identifiers";
14
+ import { IdGenerator } from "@effect-agent/core/IdGenerator";
15
+ import { DurableStep, DurableStepError } from "@effect-agent/engine/DurableStep";
16
+ import { RunToolAuthorization } from "@effect-agent/engine/RunOptions";
17
+ import { DurableWorkerBinding, type ResolvedBinding } from "@effect-agent/thread/AgentRegistration";
19
18
  import {
20
- AgentBindingResolver,
21
- ApprovalDecisionCommand,
22
- CertificationCaseResult,
23
- CertificationReport,
24
- CertificationSweepResult,
25
- CertificationTierThreeReport,
26
- CertifiedAdapterIdentity,
27
- ConversationExportRequest,
28
- ConversationStore,
29
- DefinitionDigests,
30
- DeploymentId,
31
- Digest,
32
19
  DurableAgentRuntime,
33
20
  DurableRuntimeConfig,
34
- DurableRuntimeFailpoint,
21
+ type DurableSubmitFailure,
22
+ type DurableSubmitOptions,
23
+ type Receipt,
24
+ } from "@effect-agent/thread/DurableAgentRuntime";
25
+ import {
35
26
  DurableRuntimeFailpointError,
36
27
  DurableRuntimeFailpointLocation,
37
- DurableRuntimeFailpointTestControl,
38
- DurableWorkerBinding,
28
+ } from "@effect-agent/thread/DurableFailpoint";
29
+ import {
30
+ DefinitionDigests,
31
+ DeploymentId,
32
+ Digest,
33
+ ProducerId,
34
+ type BatchId,
35
+ } from "@effect-agent/thread/Records";
36
+ import { childThreadIdFor } from "@effect-agent/thread/RunJournal";
37
+ import {
38
+ ApprovalDecisionCommand,
39
39
  IdempotencyKey,
40
- LoadCheckpointRequest,
41
40
  Principal,
42
- ProducerId,
43
41
  ResolutionSafeToRetry,
44
42
  SubmissionLedger,
45
43
  SubmissionLookupById,
46
- ToolReconciler,
47
44
  UnknownResolutionCommand,
48
- WakeScheduler,
49
45
  DEFAULT_OWNERSHIP_LEASE_DURATION,
46
+ type SubmissionSnapshot,
47
+ } from "@effect-agent/thread/SubmissionLedger";
48
+ import {
49
+ type CertificationCaseResult,
50
+ CertificationReport,
51
+ CertificationSweepResult,
52
+ CertificationTierThreeReport,
53
+ CertifiedAdapterIdentity,
50
54
  certifyPorts,
51
- childConversationIdFor,
52
- verifyConversationInvariants,
53
- type BatchId,
54
55
  type CertificationScenario,
55
- type DurableSubmitFailure,
56
- type DurableSubmitOptions,
57
- type Receipt,
58
- type ResolvedBinding,
59
- type SubmissionSnapshot,
60
- } from "@effect-agent/session";
56
+ } from "@effect-agent/thread/testing/Certification";
57
+ import { DurableRuntimeFailpointTestControl } from "@effect-agent/thread/testing/DurableFailpointTestControl";
58
+ import { verifyThreadInvariants } from "@effect-agent/thread/ThreadInvariants";
59
+ import {
60
+ ThreadExportRequest,
61
+ ThreadStore,
62
+ LoadCheckpointRequest,
63
+ } from "@effect-agent/thread/ThreadStore";
64
+ import { ToolReconciler } from "@effect-agent/thread/ToolReconciler";
65
+ import { WakeScheduler } from "@effect-agent/thread/WakeScheduler";
66
+ import type { Crypto } from "effect";
61
67
  import {
62
68
  Cause,
63
69
  Clock,
64
- Crypto,
65
70
  DateTime,
66
71
  Duration,
67
72
  Effect,
@@ -87,7 +92,7 @@ import { LanguageModel, Model, Tool, Toolkit, type Response } from "effect/unsta
87
92
  * (plain / uncertain-tool / durable-steps / approval / join / delegation). After the injected
88
93
  * fault the runner asserts the state stays CLASSIFIABLE (recovery + the public unblocking
89
94
  * operations `resolveUnknown`/`resolveApproval` are the only levers used) and that the
90
- * re-drive CONVERGES to `verifyConversationInvariants` with `requireAllSettled` — including a
95
+ * re-drive CONVERGES to `verifyThreadInvariants` with `requireAllSettled` — including a
91
96
  * fully discharged digest-chain check, because the runner captures per-batch producer
92
97
  * identity at append time.
93
98
  * - **Tier 3 — real loss lever**: recorded honestly. A durable adapter either supplies a
@@ -126,7 +131,7 @@ export interface CertifyDurableAdaptersOptions<LedgerE = never, StoreE = never>
126
131
  * does); the certification's own environment supplies nothing else.
127
132
  */
128
133
  readonly submissionLedger: Layer.Layer<SubmissionLedger, LedgerE, Crypto.Crypto>;
129
- readonly conversationStore: Layer.Layer<ConversationStore, StoreE, Crypto.Crypto>;
134
+ readonly threadStore: Layer.Layer<ThreadStore, StoreE, Crypto.Crypto>;
130
135
  /** Defaults to `WakeScheduler.layerNoop`; the runner re-drives lanes explicitly. */
131
136
  readonly wakeScheduler?: Layer.Layer<WakeScheduler> | undefined;
132
137
  /** Executes Tier 3 in this run; takes precedence over `tierThreeEvidence`. */
@@ -166,8 +171,36 @@ export const CERTIFICATION_SCENARIOS: ReadonlyArray<CertificationScenario> = [
166
171
  */
167
172
  export const TIER2_UNREACHED_LOCATIONS: ReadonlyArray<DurableRuntimeFailpointLocation> = [
168
173
  "abort:after-intent",
174
+ // Compaction requires a `contextTokenLimit` policy plus prior-Run history
175
+ // none of the six scenario shapes carries; pinned in-process by the
176
+ // RUN-026 rows in `packages/testing/test/durable-runtime.test.ts`
177
+ // (compaction failpoint idempotence across re-drive).
178
+ "compaction:before-canonical-append",
179
+ "compaction:after-canonical-append",
180
+ // These shapes use neither programmatic Tools nor grace finalization. Their reservations
181
+ // are exercised before/after append in the public durable-runtime regression suite.
182
+ "policy:before-reservation-append",
183
+ "policy:after-reservation-append",
169
184
  "resolve:after-intent",
170
185
  "subagent:after-child-abort-intent",
186
+ // Background workers use retained delivery, source capacity, and child-origin paths absent
187
+ // from these six attached/ordinary scenarios. The before/after creation and completion
188
+ // boundaries are exercised by packages/thread/test/worker-host.test.ts.
189
+ "worker:before-source-append",
190
+ "worker:after-source-append",
191
+ "worker:before-origin-append",
192
+ "worker:after-origin-append",
193
+ "worker:before-completion-append",
194
+ "worker:after-completion-append",
195
+ // Root attached declarations keep their independent pool; nested shared subtree mutations
196
+ // are covered by the same focused worker-host failpoint suite.
197
+ "worker:before-subtree-append",
198
+ "worker:after-subtree-append",
199
+ // Automatic report decisions and retained delivery insertion have dedicated worker crash tests.
200
+ "worker:before-report-append",
201
+ "worker:after-report-append",
202
+ "worker:before-report-delivery",
203
+ "worker:after-report-delivery",
171
204
  ];
172
205
 
173
206
  /** Locations of `tier2` rows whose armed fault never fired in ANY scenario, sorted. */
@@ -175,9 +208,11 @@ export const tier2NeverFiredLocations = (
175
208
  tier2: ReadonlyArray<CertificationSweepResult>,
176
209
  ): ReadonlyArray<DurableRuntimeFailpointLocation> => {
177
210
  const fired = new Set<DurableRuntimeFailpointLocation>();
211
+
178
212
  for (const row of tier2) {
179
213
  if (row.failpointFired) fired.add(row.location);
180
214
  }
215
+
181
216
  return DurableRuntimeFailpointLocation.literals.filter((location) => !fired.has(location)).sort();
182
217
  };
183
218
 
@@ -187,18 +222,21 @@ export const tier2NeverFiredLocations = (
187
222
 
188
223
  const SHA_A = Schema.decodeSync(Digest)("a".repeat(64));
189
224
  const DIGESTS = DefinitionDigests.make({ agent: SHA_A, model: SHA_A, tools: SHA_A });
225
+
190
226
  const CHILD_DIGEST_STRINGS = {
191
227
  agent: "b".repeat(64),
192
228
  model: "c".repeat(64),
193
229
  tools: "d".repeat(64),
194
230
  } as const;
231
+
195
232
  const CHILD_DIGESTS = DefinitionDigests.make({
196
233
  agent: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.agent),
197
234
  model: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.model),
198
235
  tools: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.tools),
199
236
  });
237
+
200
238
  const PRINCIPAL = Schema.decodeSync(Principal)("principal-certification");
201
- const decodeConversationId = Schema.decodeSync(ConversationId);
239
+ const decodeThreadId = Schema.decodeSync(ThreadId);
202
240
  const decodeIdempotencyKey = Schema.decodeSync(IdempotencyKey);
203
241
  const decodeToolCallId = Schema.decodeSync(ToolCallId);
204
242
 
@@ -248,18 +286,26 @@ const promptShapeModel = (
248
286
  generateText: () => Effect.succeed([]),
249
287
  streamText: (request) => {
250
288
  const hasToolResult = request.prompt.content.some((message) => message.role === "tool");
289
+
251
290
  const parts =
252
291
  toolParts === undefined || hasToolResult ? finalParts(finalText) : toolParts;
292
+
253
293
  return Stream.fromIterable(parts);
254
294
  },
255
295
  }),
256
296
  ),
257
297
  );
258
298
 
299
+ // Tier-2 intentionally advances virtual time past a 30-second ownership lease on every
300
+ // re-drive round. Keep its business-policy duration above the full eight-round sweep so this
301
+ // fixture certifies crash convergence rather than accidentally relying on a fresh duration
302
+ // allowance per replacement Attempt. RUN-030 owns the dedicated duration-expiry coverage.
303
+ const CERTIFICATION_MAX_DURATION = "10 minutes" as const;
304
+
259
305
  const policy = AgentPolicy.make({
260
306
  maxTurns: 4,
261
307
  maxToolCalls: 4,
262
- maxDuration: "30 seconds",
308
+ maxDuration: CERTIFICATION_MAX_DURATION,
263
309
  toolConcurrency: 2,
264
310
  });
265
311
 
@@ -267,7 +313,7 @@ const QuestionInput = Schema.Struct({ question: Schema.String });
267
313
  const AnswerOutput = Schema.Struct({ answer: Schema.String });
268
314
 
269
315
  /** plain / join: no tools — the pure Turn/submission/join seams. */
270
- const plainDefinition = Agent.define("certify-plain", {
316
+ const plainDefinition = Agent.make("certify-plain", {
271
317
  input: QuestionInput,
272
318
  output: AnswerOutput,
273
319
  instructions: "Answer as JSON.",
@@ -280,8 +326,10 @@ const Book = Tool.make("book", {
280
326
  parameters: Schema.Struct({ ref: Schema.String }),
281
327
  success: Schema.Struct({ confirmation: Schema.String }),
282
328
  });
329
+
283
330
  const bookToolkit = Toolkit.make(Book);
284
- const uncertainDefinition = Agent.define("certify-uncertain", {
331
+
332
+ const uncertainDefinition = Agent.make("certify-uncertain", {
285
333
  input: QuestionInput,
286
334
  output: AnswerOutput,
287
335
  instructions: "Book it.",
@@ -296,8 +344,10 @@ const Itinerary = Tool.make("itinerary", {
296
344
  failure: DurableStepError,
297
345
  dependencies: [DurableStep],
298
346
  });
347
+
299
348
  const itineraryToolkit = Toolkit.make(Itinerary);
300
- const stepsDefinition = Agent.define("certify-steps", {
349
+
350
+ const stepsDefinition = Agent.make("certify-steps", {
301
351
  input: QuestionInput,
302
352
  output: AnswerOutput,
303
353
  instructions: "Reserve the itinerary.",
@@ -311,8 +361,10 @@ const BookApproval = Tool.make("book", {
311
361
  success: Schema.Struct({ confirmation: Schema.String }),
312
362
  needsApproval: true,
313
363
  });
364
+
314
365
  const approvalToolkit = Toolkit.make(BookApproval);
315
- const approvalDefinition = Agent.define("certify-approval", {
366
+
367
+ const approvalDefinition = Agent.make("certify-approval", {
316
368
  input: QuestionInput,
317
369
  output: AnswerOutput,
318
370
  instructions: "Book after approval.",
@@ -321,7 +373,7 @@ const approvalDefinition = Agent.define("certify-approval", {
321
373
  });
322
374
 
323
375
  /** delegation: durable attached child plus an ordinary uncertain sibling in ONE batch. */
324
- const childDefinition = Agent.define("certify-child", {
376
+ const childDefinition = Agent.make("certify-child", {
325
377
  input: QuestionInput,
326
378
  output: AnswerOutput,
327
379
  instructions: "Answer as JSON.",
@@ -329,7 +381,7 @@ const childDefinition = Agent.define("certify-child", {
329
381
  policy: AgentPolicy.make({
330
382
  maxTurns: 2,
331
383
  maxToolCalls: 1,
332
- maxDuration: "30 seconds",
384
+ maxDuration: CERTIFICATION_MAX_DURATION,
333
385
  toolConcurrency: 1,
334
386
  }),
335
387
  });
@@ -352,7 +404,7 @@ const researchDelegation = Subagent.define("delegate_research", {
352
404
  maxConcurrency: 2,
353
405
  maxTurns: 4,
354
406
  maxToolCalls: 4,
355
- maxDuration: "10 seconds",
407
+ maxDuration: CERTIFICATION_MAX_DURATION,
356
408
  }),
357
409
  });
358
410
 
@@ -361,7 +413,7 @@ const Lookup = Tool.make("lookup", {
361
413
  success: Schema.Struct({ value: Schema.String }),
362
414
  });
363
415
 
364
- const coordinatorDefinition = Agent.define("certify-coordinator", {
416
+ const coordinatorDefinition = Agent.make("certify-coordinator", {
365
417
  input: Schema.Struct({ mission: Schema.String }),
366
418
  output: Schema.Struct({ report: Schema.String }),
367
419
  instructions: "Delegate and look up, then answer as JSON.",
@@ -369,7 +421,7 @@ const coordinatorDefinition = Agent.define("certify-coordinator", {
369
421
  policy: AgentPolicy.make({
370
422
  maxTurns: 4,
371
423
  maxToolCalls: 3,
372
- maxDuration: "30 seconds",
424
+ maxDuration: CERTIFICATION_MAX_DURATION,
373
425
  toolConcurrency: 2,
374
426
  }),
375
427
  });
@@ -384,12 +436,14 @@ const identifiers = Layer.effect(
384
436
  IdGenerator,
385
437
  Effect.gen(function* () {
386
438
  const counter = yield* Ref.make(0);
439
+
387
440
  const next = <A>(decode: (value: string) => A, prefix: string) =>
388
441
  Ref.getAndUpdate(counter, (value) => value + 1).pipe(
389
442
  Effect.map((value) => decode(`${prefix}-${value}`)),
390
443
  );
444
+
391
445
  return {
392
- nextConversationId: next(decodeConversationId, "certify-fixture-conversation"),
446
+ nextThreadId: next(decodeThreadId, "certify-fixture-thread"),
393
447
  nextRunId: next(Schema.decodeSync(RunId), "certify-fixture-run"),
394
448
  nextTurnId: next(Schema.decodeSync(TurnId), "certify-fixture-turn"),
395
449
  };
@@ -403,15 +457,15 @@ const delegationSupport = Layer.mergeAll(SubagentReservationsMemoryLive, identif
403
457
  // ---------------------------------------------------------------------------
404
458
 
405
459
  interface CertificationCell {
406
- readonly resolver: (typeof AgentBindingResolver)["Service"];
460
+ readonly bindings: ReadonlyArray<ResolvedBinding>;
407
461
  /** Idempotent submission batch — safe to replay verbatim after a submit-boundary fault. */
408
462
  readonly submit: Effect.Effect<ReadonlyArray<Receipt>, DurableSubmitFailure, DurableAgentRuntime>;
409
463
  /** All lanes of the cell in drive order, computed from the (possibly replayed) receipts. */
410
- readonly lanes: (receipts: ReadonlyArray<Receipt>) => ReadonlyArray<ConversationId>;
464
+ readonly lanes: (receipts: ReadonlyArray<Receipt>) => ReadonlyArray<ThreadId>;
411
465
  }
412
466
 
413
- const submitOptionsFor = (slug: string, conversationId: ConversationId): DurableSubmitOptions => ({
414
- conversationId,
467
+ const submitOptionsFor = (slug: string, threadId: ThreadId): DurableSubmitOptions => ({
468
+ threadId,
415
469
  principal: PRINCIPAL,
416
470
  idempotencyKey: decodeIdempotencyKey(`certify-key-${slug}`),
417
471
  definitions: DIGESTS,
@@ -423,20 +477,24 @@ const makeSingleAgentCell = (
423
477
  resolved: ResolvedBinding,
424
478
  slug: string,
425
479
  ): CertificationCell => {
426
- const conversationId = decodeConversationId(`certify-${slug}`);
480
+ const threadId = decodeThreadId(`certify-${slug}`);
481
+
427
482
  const submit = Effect.gen(function* () {
428
483
  const runtime = yield* DurableAgentRuntime;
484
+
429
485
  const receipt = yield* runtime.submit(
430
486
  { definition: { id: definition.id, input: definition.input } },
431
487
  { question: `certify ${slug}` },
432
- submitOptionsFor(slug, conversationId),
488
+ submitOptionsFor(slug, threadId),
433
489
  );
490
+
434
491
  return [receipt];
435
492
  });
493
+
436
494
  return {
437
- resolver: AgentBindingResolver.fromBindings([resolved]),
495
+ bindings: [resolved],
438
496
  submit,
439
- lanes: () => [conversationId],
497
+ lanes: () => [threadId],
440
498
  };
441
499
  };
442
500
 
@@ -450,7 +508,9 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
450
508
  plainDefinition,
451
509
  promptShapeModel("certify-plain", '{"answer":"done"}'),
452
510
  );
511
+
453
512
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS);
513
+
454
514
  return makeSingleAgentCell(plainDefinition, resolved, slug);
455
515
  }
456
516
  case "uncertain-tool": {
@@ -462,12 +522,15 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
462
522
  toolTurn(toolCallPart("book-1", "book", { ref: `r-${slug}` })),
463
523
  ),
464
524
  );
525
+
465
526
  const toolLayer = bookToolkit.toLayer({
466
527
  book: ({ ref }) => Effect.succeed({ confirmation: `confirmed-${ref}` }),
467
528
  });
529
+
468
530
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(
469
531
  Effect.provide(toolLayer),
470
532
  );
533
+
471
534
  return makeSingleAgentCell(uncertainDefinition, resolved, slug);
472
535
  }
473
536
  case "durable-steps": {
@@ -479,26 +542,32 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
479
542
  toolTurn(toolCallPart("itinerary-1", "itinerary", { ref: `trip-${slug}` })),
480
543
  ),
481
544
  );
545
+
482
546
  const toolLayer = itineraryToolkit.toLayer({
483
547
  itinerary: ({ ref }) =>
484
548
  Effect.gen(function* () {
485
549
  const step = yield* DurableStep;
550
+
486
551
  const flight = yield* step.do(
487
552
  "reserve-flight",
488
553
  Schema.String,
489
554
  Effect.succeed(`flight-${ref}`),
490
555
  );
556
+
491
557
  const lodging = yield* step.do(
492
558
  "reserve-lodging",
493
559
  Schema.String,
494
560
  Effect.succeed(`lodging-${ref}`),
495
561
  );
562
+
496
563
  return { state: `${flight}+${lodging}` };
497
564
  }),
498
565
  });
566
+
499
567
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(
500
568
  Effect.provide(toolLayer),
501
569
  );
570
+
502
571
  return makeSingleAgentCell(stepsDefinition, resolved, slug);
503
572
  }
504
573
  case "approval": {
@@ -510,12 +579,15 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
510
579
  toolTurn(toolCallPart("book-1", "book", { ref: `r-${slug}` })),
511
580
  ),
512
581
  );
582
+
513
583
  const toolLayer = approvalToolkit.toLayer({
514
584
  book: ({ ref }) => Effect.succeed({ confirmation: `confirmed-${ref}` }),
515
585
  });
586
+
516
587
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(
517
588
  Effect.provide(toolLayer),
518
589
  );
590
+
519
591
  return makeSingleAgentCell(approvalDefinition, resolved, slug);
520
592
  }
521
593
  case "join": {
@@ -523,31 +595,39 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
523
595
  plainDefinition,
524
596
  promptShapeModel("certify-join", '{"answer":"host answer"}'),
525
597
  );
598
+
526
599
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS);
527
- const conversationId = decodeConversationId(`certify-${slug}`);
528
- const submitOne = (key: string, question: string) =>
529
- Effect.gen(function* () {
530
- const runtime = yield* DurableAgentRuntime;
531
- return yield* runtime.submit(
532
- { definition: { id: plainDefinition.id, input: plainDefinition.input } },
533
- { question },
534
- {
535
- conversationId,
536
- principal: PRINCIPAL,
537
- idempotencyKey: decodeIdempotencyKey(key),
538
- definitions: DIGESTS,
539
- },
540
- );
541
- });
600
+ const threadId = decodeThreadId(`certify-${slug}`);
601
+
602
+ const submitOne = Effect.fn("Certification.submitOne")(function* (
603
+ key: string,
604
+ question: string,
605
+ ) {
606
+ const runtime = yield* DurableAgentRuntime;
607
+
608
+ return yield* runtime.submit(
609
+ { definition: { id: plainDefinition.id, input: plainDefinition.input } },
610
+ { question },
611
+ {
612
+ threadId,
613
+ principal: PRINCIPAL,
614
+ idempotencyKey: decodeIdempotencyKey(key),
615
+ definitions: DIGESTS,
616
+ },
617
+ );
618
+ });
619
+
542
620
  const cell: CertificationCell = {
543
- resolver: AgentBindingResolver.fromBindings([resolved]),
621
+ bindings: [resolved],
544
622
  submit: Effect.gen(function* () {
545
623
  const host = yield* submitOne(`certify-key-${slug}-host`, "host question");
546
624
  const queued = yield* submitOne(`certify-key-${slug}-queued`, "queued question");
625
+
547
626
  return [host, queued];
548
627
  }),
549
- lanes: () => [conversationId],
628
+ lanes: () => [threadId],
550
629
  };
630
+
551
631
  return cell;
552
632
  }
553
633
  case "delegation": {
@@ -555,6 +635,7 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
555
635
  childDefinition,
556
636
  promptShapeModel("certify-child", '{"answer":"child-answer"}'),
557
637
  );
638
+
558
639
  const parentBinding = Agent.withModel(
559
640
  coordinatorDefinition,
560
641
  promptShapeModel(
@@ -566,36 +647,45 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
566
647
  ),
567
648
  ),
568
649
  );
650
+
569
651
  const delegationLayer = SubagentRuntime.layer(researchDelegation, childBinding, {
570
652
  mapChildFailure,
571
653
  durable: { targetDigests: CHILD_DIGEST_STRINGS },
572
654
  }).pipe(Layer.provide(delegationSupport));
655
+
573
656
  const lookupLayer = Toolkit.make(Lookup).toLayer({
574
657
  lookup: ({ key }) => Effect.succeed({ value: `found-${key}` }),
575
658
  });
659
+
576
660
  const parentResolved = yield* DurableWorkerBinding.make(parentBinding, DIGESTS).pipe(
577
661
  Effect.provide(Layer.mergeAll(delegationLayer, lookupLayer)),
578
662
  );
663
+
579
664
  const childResolved = yield* DurableWorkerBinding.make(childBinding, CHILD_DIGESTS);
580
- const conversationId = decodeConversationId(`certify-${slug}`);
665
+ const threadId = decodeThreadId(`certify-${slug}`);
666
+
581
667
  const cell: CertificationCell = {
582
- resolver: AgentBindingResolver.fromBindings([parentResolved, childResolved]),
668
+ bindings: [parentResolved, childResolved],
583
669
  submit: Effect.gen(function* () {
584
670
  const runtime = yield* DurableAgentRuntime;
671
+
585
672
  const receipt = yield* runtime.submit(
586
673
  { definition: { id: coordinatorDefinition.id, input: coordinatorDefinition.input } },
587
674
  { mission: "plan" },
588
- submitOptionsFor(slug, conversationId),
675
+ submitOptionsFor(slug, threadId),
589
676
  );
677
+
590
678
  return [receipt];
591
679
  }),
592
680
  lanes: (receipts) => {
593
681
  const parent = receipts.at(0);
682
+
594
683
  return parent === undefined
595
- ? [conversationId]
596
- : [conversationId, childConversationIdFor(parent.submissionId, DELEGATE_CALL)];
684
+ ? [threadId]
685
+ : [threadId, childThreadIdFor(parent.submissionId, DELEGATE_CALL)];
597
686
  },
598
687
  };
688
+
599
689
  return cell;
600
690
  }
601
691
  }
@@ -611,56 +701,68 @@ const MAX_REDRIVE_ROUNDS = 8;
611
701
  * the digest chain is fully recomputed instead of skipped.
612
702
  */
613
703
  const verifyLane = Effect.fn("Certification.verifyLane")(function* (
614
- lane: ConversationId,
704
+ lane: ThreadId,
615
705
  batchProducers: ReadonlyMap<BatchId, ProducerId>,
616
706
  ) {
617
- const store = yield* ConversationStore;
707
+ const store = yield* ThreadStore;
618
708
  const ledger = yield* SubmissionLedger;
619
- const exported = yield* store.export(ConversationExportRequest.make({ conversationId: lane }));
709
+ const exported = yield* store.export(ThreadExportRequest.make({ threadId: lane }));
620
710
  const rows = new Map<SubmissionId, SubmissionSnapshot>();
621
711
  const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
712
+
622
713
  for (const submission of nonterminal) {
623
- if (submission.conversationId === lane) rows.set(submission.submissionId, submission);
714
+ if (submission.threadId === lane) rows.set(submission.submissionId, submission);
624
715
  }
625
716
  const named = new Set<SubmissionId>();
717
+
626
718
  for (const envelope of exported.records) {
627
719
  const payload = envelope.record.payload;
720
+
628
721
  if (
629
722
  payload._tag === "UserInputRecorded" ||
630
723
  payload._tag === "SubmissionSettled" ||
631
724
  payload._tag === "AbortRequested"
632
725
  ) {
633
- named.add(payload.submissionId);
726
+ if (payload.submissionId !== undefined) named.add(payload.submissionId);
634
727
  }
635
728
  }
636
729
  for (const submissionId of named) {
637
730
  if (rows.has(submissionId)) continue;
638
731
  const found = yield* ledger.lookup(SubmissionLookupById.make({ submissionId }));
639
- if (Option.isSome(found) && found.value.conversationId === lane) {
732
+
733
+ if (Option.isSome(found) && found.value.threadId === lane) {
640
734
  rows.set(submissionId, found.value);
641
735
  }
642
736
  }
643
- const checkpoint = yield* store.loadCheckpoint(
644
- LoadCheckpointRequest.make({ conversationId: lane }),
645
- );
646
- return yield* verifyConversationInvariants({
737
+
738
+ const checkpoint =
739
+ store.checkpoints === undefined
740
+ ? Option.none()
741
+ : yield* store.checkpoints.load(LoadCheckpointRequest.make({ threadId: lane }));
742
+
743
+ return yield* verifyThreadInvariants({
647
744
  export: exported,
648
745
  submissions: [...rows.values()],
649
746
  batchProducers,
650
747
  checkpoint: Option.getOrUndefined(checkpoint),
748
+ checkpointsSupported: store.checkpoints !== undefined,
651
749
  requireAllSettled: true,
652
750
  });
653
751
  });
654
752
 
655
753
  const failureTagOf = <E>(cause: Cause.Cause<E>): string => {
656
754
  const failure = Cause.findErrorOption(cause);
755
+
657
756
  if (Option.isSome(failure)) {
658
757
  const error: unknown = failure.value;
758
+
659
759
  if (typeof error === "object" && error !== null && "_tag" in error) {
660
- return String((error as { _tag: unknown })._tag);
760
+ return String(error._tag);
661
761
  }
762
+
662
763
  return String(error).slice(0, 256);
663
764
  }
765
+
664
766
  return "defect";
665
767
  };
666
768
 
@@ -671,7 +773,6 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
671
773
  batchProducers: ReadonlyMap<BatchId, ProducerId>,
672
774
  leaseAdvance: Duration.Duration,
673
775
  ) {
674
- const runtime = yield* DurableAgentRuntime;
675
776
  const ledger = yield* SubmissionLedger;
676
777
  const control = yield* DurableRuntimeFailpointTestControl;
677
778
  const slug = `${scenario}-${location.replaceAll(":", "-")}`;
@@ -688,9 +789,20 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
688
789
 
689
790
  const cell = yield* makeCell(scenario, slug);
690
791
 
792
+ const runtime = yield* DurableAgentRuntime.pipe(
793
+ Effect.provide(
794
+ DurableAgentRuntime.layerWithBindings(cell.bindings).pipe(
795
+ Layer.provide(RunToolAuthorization.allowAll),
796
+ ),
797
+ ),
798
+ );
799
+
800
+ const submit = cell.submit.pipe(Effect.provideService(DurableAgentRuntime, runtime));
801
+
691
802
  // One-shot arm: the fault fires at most once anywhere in the cell (initial drive OR a
692
803
  // re-drive round's public unblocking operation), modelling one crash at this boundary.
693
804
  const fired = yield* Ref.make(false);
805
+
694
806
  yield* control.setHandler((hit) =>
695
807
  hit !== location
696
808
  ? Effect.void
@@ -705,13 +817,16 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
705
817
 
706
818
  // Submissions are idempotent (DUR-001): one replay recovers a submit-boundary fault.
707
819
  let receipts: ReadonlyArray<Receipt>;
708
- const firstSubmit = yield* Effect.exit(cell.submit);
820
+ const firstSubmit = yield* Effect.exit(submit);
821
+
709
822
  if (Exit.isSuccess(firstSubmit)) {
710
823
  receipts = firstSubmit.value;
711
824
  } else {
712
- const secondSubmit = yield* Effect.exit(cell.submit);
825
+ const secondSubmit = yield* Effect.exit(submit);
826
+
713
827
  if (Exit.isFailure(secondSubmit)) {
714
828
  yield* control.clear;
829
+
715
830
  return failed(
716
831
  `submission replay did not recover: ${failureTagOf(secondSubmit.cause)}`,
717
832
  yield* Ref.get(fired),
@@ -721,24 +836,24 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
721
836
  }
722
837
  const lanes = cell.lanes(receipts);
723
838
 
724
- const driveLane = (lane: ConversationId) =>
725
- runtime
726
- .processConversationResolved(lane)
727
- .pipe(Effect.provideService(AgentBindingResolver, cell.resolver));
839
+ const driveLane = (lane: ThreadId) => runtime.processThreadResolved(lane);
728
840
 
729
841
  const allSettled = Effect.gen(function* () {
730
842
  for (const receipt of receipts) {
731
843
  const snapshot = yield* ledger.lookup(
732
844
  SubmissionLookupById.make({ submissionId: receipt.submissionId }),
733
845
  );
846
+
734
847
  if (Option.isNone(snapshot) || snapshot.value.state !== "settled") return false;
735
848
  }
849
+
736
850
  return true;
737
851
  });
738
852
 
739
853
  // Re-drive to convergence using ONLY public operations: worker drives, recovery passes,
740
- // and the authorized DUR-017/approval unblocking paths chosen from `explainConversation`.
854
+ // and the authorized DUR-017/approval unblocking paths chosen from `explainThread`.
741
855
  let converged = false;
856
+
742
857
  for (let round = 0; round < MAX_REDRIVE_ROUNDS && !converged; round++) {
743
858
  // Expire any lease a faulted Attempt left behind (D5): virtual time is the
744
859
  // adapter-neutral reclaim lever — a live lease may block every new claim.
@@ -748,7 +863,8 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
748
863
  yield* Effect.exit(driveLane(lane));
749
864
  }
750
865
  for (const lane of lanes) {
751
- const explains = yield* Effect.exit(runtime.explainConversation(lane));
866
+ const explains = yield* Effect.exit(runtime.explainThread(lane));
867
+
752
868
  if (Exit.isFailure(explains)) continue;
753
869
  for (const explanation of explains.value) {
754
870
  for (const unknown of explanation.evidence.unknownCalls) {
@@ -769,6 +885,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
769
885
  const decided = explanation.evidence.approvalDecisions.some(
770
886
  (decision) => decision.toolCallId === pending.toolCallId,
771
887
  );
888
+
772
889
  if (decided) continue;
773
890
  yield* Effect.exit(
774
891
  runtime.resolveApproval(
@@ -785,6 +902,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
785
902
  }
786
903
  }
787
904
  const settled = yield* Effect.exit(allSettled);
905
+
788
906
  converged = Exit.isSuccess(settled) && settled.value;
789
907
  }
790
908
  yield* control.clear;
@@ -797,8 +915,10 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
797
915
  // Every lane of the cell must verify in convergence mode with a recomputed digest chain.
798
916
  let digestChainVerified = true;
799
917
  const failedChecks: Array<string> = [];
918
+
800
919
  for (const lane of lanes) {
801
920
  const verdict = yield* Effect.exit(verifyLane(lane, batchProducers));
921
+
802
922
  if (Exit.isFailure(verdict)) {
803
923
  return failed(`lane ${lane} could not be verified: ${failureTagOf(verdict.cause)}`, wasFired);
804
924
  }
@@ -859,10 +979,14 @@ export const resolveTierThree = Effect.fn("Certification.resolveTierThree")(func
859
979
  }
860
980
  if (options.crashLever !== undefined) {
861
981
  const cases = yield* options.crashLever;
982
+
862
983
  return CertificationTierThreeReport.make({
863
- status: "exercised",
984
+ status: cases.length === 0 ? "not-exercised" : "exercised",
864
985
  evidence: options.tierThreeEvidence ?? [],
865
986
  cases,
987
+ ...(cases.length === 0
988
+ ? { detail: "the supplied crash lever executed no real-loss cases" }
989
+ : {}),
866
990
  });
867
991
  }
868
992
  if (options.tierThreeEvidence !== undefined && options.tierThreeEvidence.length > 0) {
@@ -872,6 +996,7 @@ export const resolveTierThree = Effect.fn("Certification.resolveTierThree")(func
872
996
  cases: [],
873
997
  });
874
998
  }
999
+
875
1000
  return CertificationTierThreeReport.make({
876
1001
  status: "not-exercised",
877
1002
  evidence: [],
@@ -900,13 +1025,15 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
900
1025
  options: CertifyDurableAdaptersOptions<LedgerE, StoreE>,
901
1026
  ): Effect.Effect<CertificationReport, LedgerE | StoreE, Crypto.Crypto> => {
902
1027
  const batchProducers = new Map<BatchId, ProducerId>();
1028
+
903
1029
  // Interpose the candidate store with an append-time capture of each batch's producer
904
- // identity — the one value the ConversationStore port deliberately does not export — so
1030
+ // identity — the one value the ThreadStore port deliberately does not export — so
905
1031
  // Tier 2's invariant verification recomputes the FULL digest chain instead of skipping it.
906
- const capturingStore = Layer.effect(ConversationStore)(
1032
+ const capturingStore = Layer.effect(ThreadStore)(
907
1033
  Effect.gen(function* () {
908
- const inner = yield* ConversationStore;
909
- return ConversationStore.of({
1034
+ const inner = yield* ThreadStore;
1035
+
1036
+ return ThreadStore.of({
910
1037
  ...inner,
911
1038
  append: (request) =>
912
1039
  Effect.sync(() => {
@@ -914,13 +1041,15 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
914
1041
  }).pipe(Effect.andThen(inner.append(request))),
915
1042
  });
916
1043
  }),
917
- ).pipe(Layer.provide(options.conversationStore));
1044
+ ).pipe(Layer.provide(options.threadStore));
918
1045
 
919
- const support = Layer.mergeAll(
1046
+ // RUN-036: certification uses the default-none Tool failure observer. Trusted application
1047
+ // reporting adds no durable transition and is verified separately from adapter certification.
1048
+ const environment = Layer.mergeAll(
920
1049
  options.submissionLedger,
921
1050
  capturingStore,
922
1051
  options.wakeScheduler ?? WakeScheduler.layerNoop,
923
- DurableRuntimeFailpoint.layerTest,
1052
+ DurableRuntimeFailpointTestControl.layer,
924
1053
  ToolReconciler.uncertain,
925
1054
  DurableRuntimeConfig.layer({
926
1055
  deploymentId: Schema.decodeSync(DeploymentId)("deployment-certification"),
@@ -930,7 +1059,6 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
930
1059
  abortPollInterval: Duration.millis(50),
931
1060
  }),
932
1061
  );
933
- const environment = DurableAgentRuntime.layer.pipe(Layer.provideMerge(support));
934
1062
 
935
1063
  const leaseAdvance = Duration.millis(
936
1064
  Duration.toMillis(options.ownershipLeaseDuration ?? DEFAULT_OWNERSHIP_LEASE_DURATION) + 1_000,
@@ -941,6 +1069,7 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
941
1069
 
942
1070
  // Tier 2 — coordinator failpoint convergence sweep.
943
1071
  const tier2: Array<CertificationSweepResult> = [];
1072
+
944
1073
  for (const scenario of CERTIFICATION_SCENARIOS) {
945
1074
  for (const location of DurableRuntimeFailpointLocation.literals) {
946
1075
  tier2.push(yield* runSweepCell(scenario, location, batchProducers, leaseAdvance));
@@ -955,13 +1084,14 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
955
1084
  const tier3 = yield* resolveTierThree(capabilities.durability, options);
956
1085
 
957
1086
  const generatedAt = yield* nowUtc;
1087
+
958
1088
  const ok =
959
1089
  tier1.every((result) => result.status === "passed") &&
960
1090
  tier2.every((result) => result.status !== "failed") &&
961
1091
  tier3.cases.every((result) => result.status === "passed");
962
1092
 
963
1093
  return CertificationReport.make({
964
- format: "effect-agent/certification@1",
1094
+ format: "effect-agent/certification@2",
965
1095
  adapter: CertifiedAdapterIdentity.make({
966
1096
  name: options.adapter.name,
967
1097
  ...(options.adapter.version === undefined ? {} : { version: options.adapter.version }),
@@ -972,6 +1102,12 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
972
1102
  tier2,
973
1103
  tier3,
974
1104
  ok,
1105
+ fullyCertified:
1106
+ ok &&
1107
+ capabilities.durability !== "non-durable" &&
1108
+ tier3.status === "exercised" &&
1109
+ tier3.cases.length > 0 &&
1110
+ tier3.cases.every((result) => result.suite === "real-loss"),
975
1111
  });
976
1112
  });
977
1113