@effect-agent/testing 0.1.0-beta.8 → 0.1.0-beta.80

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 +592 -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 +368 -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 +1963 -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 -3406
  26. package/dist/index.mjs +2 -4771
  27. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  28. package/package.json +1 -48
  29. package/src/{certification.ts → Certification.ts} +251 -117
  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} +112 -45
  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 -41
  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 +187 -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`. */
@@ -156,8 +161,8 @@ export const CERTIFICATION_SCENARIOS: ReadonlyArray<CertificationScenario> = [
156
161
 
157
162
  /**
158
163
  * Coordinator failpoint locations that none of the six scenario shapes can reach, recorded
159
- * honestly instead of silently claimed: all three sit on operator/abort paths the shapes do
160
- * not take. They are pinned in-process by the P5/S2 suites
164
+ * honestly instead of silently claimed. These require operator, compaction, reservation, or
165
+ * background-worker paths the shapes do not take. They are pinned in-process by the P5/S2 suites
161
166
  * (`packages/testing/test/durable-tools.test.ts` "resolveUnknown is idempotent across the
162
167
  * intent failpoint", `durable-runtime.test.ts` abort rows,
163
168
  * `durable-subagents.test.ts` abort propagation) and by the process-kill/eviction crash
@@ -165,14 +170,40 @@ export const CERTIFICATION_SCENARIOS: ReadonlyArray<CertificationScenario> = [
165
170
  * protocol change that silently stops exercising a location fails the certification.
166
171
  */
167
172
  export const TIER2_UNREACHED_LOCATIONS: ReadonlyArray<DurableRuntimeFailpointLocation> = [
173
+ // Tier 2 has no native compaction; dedicated checkpoint process-loss tests cover these.
174
+ "checkpoint:before-save",
175
+ "checkpoint:after-save",
168
176
  "abort:after-intent",
169
177
  // Compaction requires a `contextTokenLimit` policy plus prior-Run history
170
178
  // none of the six scenario shapes carries; pinned in-process by the
171
179
  // RUN-026 rows in `packages/testing/test/durable-runtime.test.ts`
172
180
  // (compaction failpoint idempotence across re-drive).
181
+ "compaction:before-canonical-append",
173
182
  "compaction:after-canonical-append",
183
+ // These shapes use neither programmatic Tools nor grace finalization. Their reservations
184
+ // are exercised before/after append in the public durable-runtime regression suite.
185
+ "policy:before-reservation-append",
186
+ "policy:after-reservation-append",
174
187
  "resolve:after-intent",
175
188
  "subagent:after-child-abort-intent",
189
+ // Background workers use retained delivery, source capacity, and child-origin paths absent
190
+ // from these six attached/ordinary scenarios. The before/after creation and completion
191
+ // boundaries are exercised by packages/thread/test/worker-host.test.ts.
192
+ "worker:before-source-append",
193
+ "worker:after-source-append",
194
+ "worker:before-origin-append",
195
+ "worker:after-origin-append",
196
+ "worker:before-completion-append",
197
+ "worker:after-completion-append",
198
+ // Root attached declarations keep their independent pool; nested shared subtree mutations
199
+ // are covered by the same focused worker-host failpoint suite.
200
+ "worker:before-subtree-append",
201
+ "worker:after-subtree-append",
202
+ // Automatic report decisions and retained delivery insertion have dedicated worker crash tests.
203
+ "worker:before-report-append",
204
+ "worker:after-report-append",
205
+ "worker:before-report-delivery",
206
+ "worker:after-report-delivery",
176
207
  ];
177
208
 
178
209
  /** Locations of `tier2` rows whose armed fault never fired in ANY scenario, sorted. */
@@ -180,9 +211,11 @@ export const tier2NeverFiredLocations = (
180
211
  tier2: ReadonlyArray<CertificationSweepResult>,
181
212
  ): ReadonlyArray<DurableRuntimeFailpointLocation> => {
182
213
  const fired = new Set<DurableRuntimeFailpointLocation>();
214
+
183
215
  for (const row of tier2) {
184
216
  if (row.failpointFired) fired.add(row.location);
185
217
  }
218
+
186
219
  return DurableRuntimeFailpointLocation.literals.filter((location) => !fired.has(location)).sort();
187
220
  };
188
221
 
@@ -192,18 +225,21 @@ export const tier2NeverFiredLocations = (
192
225
 
193
226
  const SHA_A = Schema.decodeSync(Digest)("a".repeat(64));
194
227
  const DIGESTS = DefinitionDigests.make({ agent: SHA_A, model: SHA_A, tools: SHA_A });
228
+
195
229
  const CHILD_DIGEST_STRINGS = {
196
230
  agent: "b".repeat(64),
197
231
  model: "c".repeat(64),
198
232
  tools: "d".repeat(64),
199
233
  } as const;
234
+
200
235
  const CHILD_DIGESTS = DefinitionDigests.make({
201
236
  agent: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.agent),
202
237
  model: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.model),
203
238
  tools: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.tools),
204
239
  });
240
+
205
241
  const PRINCIPAL = Schema.decodeSync(Principal)("principal-certification");
206
- const decodeConversationId = Schema.decodeSync(ConversationId);
242
+ const decodeThreadId = Schema.decodeSync(ThreadId);
207
243
  const decodeIdempotencyKey = Schema.decodeSync(IdempotencyKey);
208
244
  const decodeToolCallId = Schema.decodeSync(ToolCallId);
209
245
 
@@ -253,18 +289,26 @@ const promptShapeModel = (
253
289
  generateText: () => Effect.succeed([]),
254
290
  streamText: (request) => {
255
291
  const hasToolResult = request.prompt.content.some((message) => message.role === "tool");
292
+
256
293
  const parts =
257
294
  toolParts === undefined || hasToolResult ? finalParts(finalText) : toolParts;
295
+
258
296
  return Stream.fromIterable(parts);
259
297
  },
260
298
  }),
261
299
  ),
262
300
  );
263
301
 
302
+ // Tier-2 intentionally advances virtual time past a 30-second ownership lease on every
303
+ // re-drive round. Keep its business-policy duration above the full eight-round sweep so this
304
+ // fixture certifies crash convergence rather than accidentally relying on a fresh duration
305
+ // allowance per replacement Attempt. RUN-030 owns the dedicated duration-expiry coverage.
306
+ const CERTIFICATION_MAX_DURATION = "10 minutes" as const;
307
+
264
308
  const policy = AgentPolicy.make({
265
309
  maxTurns: 4,
266
310
  maxToolCalls: 4,
267
- maxDuration: "30 seconds",
311
+ maxDuration: CERTIFICATION_MAX_DURATION,
268
312
  toolConcurrency: 2,
269
313
  });
270
314
 
@@ -272,7 +316,7 @@ const QuestionInput = Schema.Struct({ question: Schema.String });
272
316
  const AnswerOutput = Schema.Struct({ answer: Schema.String });
273
317
 
274
318
  /** plain / join: no tools — the pure Turn/submission/join seams. */
275
- const plainDefinition = Agent.define("certify-plain", {
319
+ const plainDefinition = Agent.make("certify-plain", {
276
320
  input: QuestionInput,
277
321
  output: AnswerOutput,
278
322
  instructions: "Answer as JSON.",
@@ -285,8 +329,10 @@ const Book = Tool.make("book", {
285
329
  parameters: Schema.Struct({ ref: Schema.String }),
286
330
  success: Schema.Struct({ confirmation: Schema.String }),
287
331
  });
332
+
288
333
  const bookToolkit = Toolkit.make(Book);
289
- const uncertainDefinition = Agent.define("certify-uncertain", {
334
+
335
+ const uncertainDefinition = Agent.make("certify-uncertain", {
290
336
  input: QuestionInput,
291
337
  output: AnswerOutput,
292
338
  instructions: "Book it.",
@@ -301,8 +347,10 @@ const Itinerary = Tool.make("itinerary", {
301
347
  failure: DurableStepError,
302
348
  dependencies: [DurableStep],
303
349
  });
350
+
304
351
  const itineraryToolkit = Toolkit.make(Itinerary);
305
- const stepsDefinition = Agent.define("certify-steps", {
352
+
353
+ const stepsDefinition = Agent.make("certify-steps", {
306
354
  input: QuestionInput,
307
355
  output: AnswerOutput,
308
356
  instructions: "Reserve the itinerary.",
@@ -316,8 +364,10 @@ const BookApproval = Tool.make("book", {
316
364
  success: Schema.Struct({ confirmation: Schema.String }),
317
365
  needsApproval: true,
318
366
  });
367
+
319
368
  const approvalToolkit = Toolkit.make(BookApproval);
320
- const approvalDefinition = Agent.define("certify-approval", {
369
+
370
+ const approvalDefinition = Agent.make("certify-approval", {
321
371
  input: QuestionInput,
322
372
  output: AnswerOutput,
323
373
  instructions: "Book after approval.",
@@ -326,7 +376,7 @@ const approvalDefinition = Agent.define("certify-approval", {
326
376
  });
327
377
 
328
378
  /** delegation: durable attached child plus an ordinary uncertain sibling in ONE batch. */
329
- const childDefinition = Agent.define("certify-child", {
379
+ const childDefinition = Agent.make("certify-child", {
330
380
  input: QuestionInput,
331
381
  output: AnswerOutput,
332
382
  instructions: "Answer as JSON.",
@@ -334,7 +384,7 @@ const childDefinition = Agent.define("certify-child", {
334
384
  policy: AgentPolicy.make({
335
385
  maxTurns: 2,
336
386
  maxToolCalls: 1,
337
- maxDuration: "30 seconds",
387
+ maxDuration: CERTIFICATION_MAX_DURATION,
338
388
  toolConcurrency: 1,
339
389
  }),
340
390
  });
@@ -357,7 +407,7 @@ const researchDelegation = Subagent.define("delegate_research", {
357
407
  maxConcurrency: 2,
358
408
  maxTurns: 4,
359
409
  maxToolCalls: 4,
360
- maxDuration: "10 seconds",
410
+ maxDuration: CERTIFICATION_MAX_DURATION,
361
411
  }),
362
412
  });
363
413
 
@@ -366,7 +416,7 @@ const Lookup = Tool.make("lookup", {
366
416
  success: Schema.Struct({ value: Schema.String }),
367
417
  });
368
418
 
369
- const coordinatorDefinition = Agent.define("certify-coordinator", {
419
+ const coordinatorDefinition = Agent.make("certify-coordinator", {
370
420
  input: Schema.Struct({ mission: Schema.String }),
371
421
  output: Schema.Struct({ report: Schema.String }),
372
422
  instructions: "Delegate and look up, then answer as JSON.",
@@ -374,7 +424,7 @@ const coordinatorDefinition = Agent.define("certify-coordinator", {
374
424
  policy: AgentPolicy.make({
375
425
  maxTurns: 4,
376
426
  maxToolCalls: 3,
377
- maxDuration: "30 seconds",
427
+ maxDuration: CERTIFICATION_MAX_DURATION,
378
428
  toolConcurrency: 2,
379
429
  }),
380
430
  });
@@ -389,12 +439,14 @@ const identifiers = Layer.effect(
389
439
  IdGenerator,
390
440
  Effect.gen(function* () {
391
441
  const counter = yield* Ref.make(0);
442
+
392
443
  const next = <A>(decode: (value: string) => A, prefix: string) =>
393
444
  Ref.getAndUpdate(counter, (value) => value + 1).pipe(
394
445
  Effect.map((value) => decode(`${prefix}-${value}`)),
395
446
  );
447
+
396
448
  return {
397
- nextConversationId: next(decodeConversationId, "certify-fixture-conversation"),
449
+ nextThreadId: next(decodeThreadId, "certify-fixture-thread"),
398
450
  nextRunId: next(Schema.decodeSync(RunId), "certify-fixture-run"),
399
451
  nextTurnId: next(Schema.decodeSync(TurnId), "certify-fixture-turn"),
400
452
  };
@@ -408,15 +460,15 @@ const delegationSupport = Layer.mergeAll(SubagentReservationsMemoryLive, identif
408
460
  // ---------------------------------------------------------------------------
409
461
 
410
462
  interface CertificationCell {
411
- readonly resolver: (typeof AgentBindingResolver)["Service"];
463
+ readonly bindings: ReadonlyArray<ResolvedBinding>;
412
464
  /** Idempotent submission batch — safe to replay verbatim after a submit-boundary fault. */
413
465
  readonly submit: Effect.Effect<ReadonlyArray<Receipt>, DurableSubmitFailure, DurableAgentRuntime>;
414
466
  /** All lanes of the cell in drive order, computed from the (possibly replayed) receipts. */
415
- readonly lanes: (receipts: ReadonlyArray<Receipt>) => ReadonlyArray<ConversationId>;
467
+ readonly lanes: (receipts: ReadonlyArray<Receipt>) => ReadonlyArray<ThreadId>;
416
468
  }
417
469
 
418
- const submitOptionsFor = (slug: string, conversationId: ConversationId): DurableSubmitOptions => ({
419
- conversationId,
470
+ const submitOptionsFor = (slug: string, threadId: ThreadId): DurableSubmitOptions => ({
471
+ threadId,
420
472
  principal: PRINCIPAL,
421
473
  idempotencyKey: decodeIdempotencyKey(`certify-key-${slug}`),
422
474
  definitions: DIGESTS,
@@ -428,20 +480,24 @@ const makeSingleAgentCell = (
428
480
  resolved: ResolvedBinding,
429
481
  slug: string,
430
482
  ): CertificationCell => {
431
- const conversationId = decodeConversationId(`certify-${slug}`);
483
+ const threadId = decodeThreadId(`certify-${slug}`);
484
+
432
485
  const submit = Effect.gen(function* () {
433
486
  const runtime = yield* DurableAgentRuntime;
487
+
434
488
  const receipt = yield* runtime.submit(
435
489
  { definition: { id: definition.id, input: definition.input } },
436
490
  { question: `certify ${slug}` },
437
- submitOptionsFor(slug, conversationId),
491
+ submitOptionsFor(slug, threadId),
438
492
  );
493
+
439
494
  return [receipt];
440
495
  });
496
+
441
497
  return {
442
- resolver: AgentBindingResolver.fromBindings([resolved]),
498
+ bindings: [resolved],
443
499
  submit,
444
- lanes: () => [conversationId],
500
+ lanes: () => [threadId],
445
501
  };
446
502
  };
447
503
 
@@ -455,7 +511,9 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
455
511
  plainDefinition,
456
512
  promptShapeModel("certify-plain", '{"answer":"done"}'),
457
513
  );
514
+
458
515
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS);
516
+
459
517
  return makeSingleAgentCell(plainDefinition, resolved, slug);
460
518
  }
461
519
  case "uncertain-tool": {
@@ -467,12 +525,15 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
467
525
  toolTurn(toolCallPart("book-1", "book", { ref: `r-${slug}` })),
468
526
  ),
469
527
  );
528
+
470
529
  const toolLayer = bookToolkit.toLayer({
471
530
  book: ({ ref }) => Effect.succeed({ confirmation: `confirmed-${ref}` }),
472
531
  });
532
+
473
533
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(
474
534
  Effect.provide(toolLayer),
475
535
  );
536
+
476
537
  return makeSingleAgentCell(uncertainDefinition, resolved, slug);
477
538
  }
478
539
  case "durable-steps": {
@@ -484,26 +545,32 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
484
545
  toolTurn(toolCallPart("itinerary-1", "itinerary", { ref: `trip-${slug}` })),
485
546
  ),
486
547
  );
548
+
487
549
  const toolLayer = itineraryToolkit.toLayer({
488
550
  itinerary: ({ ref }) =>
489
551
  Effect.gen(function* () {
490
552
  const step = yield* DurableStep;
553
+
491
554
  const flight = yield* step.do(
492
555
  "reserve-flight",
493
556
  Schema.String,
494
557
  Effect.succeed(`flight-${ref}`),
495
558
  );
559
+
496
560
  const lodging = yield* step.do(
497
561
  "reserve-lodging",
498
562
  Schema.String,
499
563
  Effect.succeed(`lodging-${ref}`),
500
564
  );
565
+
501
566
  return { state: `${flight}+${lodging}` };
502
567
  }),
503
568
  });
569
+
504
570
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(
505
571
  Effect.provide(toolLayer),
506
572
  );
573
+
507
574
  return makeSingleAgentCell(stepsDefinition, resolved, slug);
508
575
  }
509
576
  case "approval": {
@@ -515,12 +582,15 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
515
582
  toolTurn(toolCallPart("book-1", "book", { ref: `r-${slug}` })),
516
583
  ),
517
584
  );
585
+
518
586
  const toolLayer = approvalToolkit.toLayer({
519
587
  book: ({ ref }) => Effect.succeed({ confirmation: `confirmed-${ref}` }),
520
588
  });
589
+
521
590
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(
522
591
  Effect.provide(toolLayer),
523
592
  );
593
+
524
594
  return makeSingleAgentCell(approvalDefinition, resolved, slug);
525
595
  }
526
596
  case "join": {
@@ -528,31 +598,39 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
528
598
  plainDefinition,
529
599
  promptShapeModel("certify-join", '{"answer":"host answer"}'),
530
600
  );
601
+
531
602
  const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS);
532
- const conversationId = decodeConversationId(`certify-${slug}`);
533
- const submitOne = (key: string, question: string) =>
534
- Effect.gen(function* () {
535
- const runtime = yield* DurableAgentRuntime;
536
- return yield* runtime.submit(
537
- { definition: { id: plainDefinition.id, input: plainDefinition.input } },
538
- { question },
539
- {
540
- conversationId,
541
- principal: PRINCIPAL,
542
- idempotencyKey: decodeIdempotencyKey(key),
543
- definitions: DIGESTS,
544
- },
545
- );
546
- });
603
+ const threadId = decodeThreadId(`certify-${slug}`);
604
+
605
+ const submitOne = Effect.fn("Certification.submitOne")(function* (
606
+ key: string,
607
+ question: string,
608
+ ) {
609
+ const runtime = yield* DurableAgentRuntime;
610
+
611
+ return yield* runtime.submit(
612
+ { definition: { id: plainDefinition.id, input: plainDefinition.input } },
613
+ { question },
614
+ {
615
+ threadId,
616
+ principal: PRINCIPAL,
617
+ idempotencyKey: decodeIdempotencyKey(key),
618
+ definitions: DIGESTS,
619
+ },
620
+ );
621
+ });
622
+
547
623
  const cell: CertificationCell = {
548
- resolver: AgentBindingResolver.fromBindings([resolved]),
624
+ bindings: [resolved],
549
625
  submit: Effect.gen(function* () {
550
626
  const host = yield* submitOne(`certify-key-${slug}-host`, "host question");
551
627
  const queued = yield* submitOne(`certify-key-${slug}-queued`, "queued question");
628
+
552
629
  return [host, queued];
553
630
  }),
554
- lanes: () => [conversationId],
631
+ lanes: () => [threadId],
555
632
  };
633
+
556
634
  return cell;
557
635
  }
558
636
  case "delegation": {
@@ -560,6 +638,7 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
560
638
  childDefinition,
561
639
  promptShapeModel("certify-child", '{"answer":"child-answer"}'),
562
640
  );
641
+
563
642
  const parentBinding = Agent.withModel(
564
643
  coordinatorDefinition,
565
644
  promptShapeModel(
@@ -571,36 +650,45 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
571
650
  ),
572
651
  ),
573
652
  );
653
+
574
654
  const delegationLayer = SubagentRuntime.layer(researchDelegation, childBinding, {
575
655
  mapChildFailure,
576
656
  durable: { targetDigests: CHILD_DIGEST_STRINGS },
577
657
  }).pipe(Layer.provide(delegationSupport));
658
+
578
659
  const lookupLayer = Toolkit.make(Lookup).toLayer({
579
660
  lookup: ({ key }) => Effect.succeed({ value: `found-${key}` }),
580
661
  });
662
+
581
663
  const parentResolved = yield* DurableWorkerBinding.make(parentBinding, DIGESTS).pipe(
582
664
  Effect.provide(Layer.mergeAll(delegationLayer, lookupLayer)),
583
665
  );
666
+
584
667
  const childResolved = yield* DurableWorkerBinding.make(childBinding, CHILD_DIGESTS);
585
- const conversationId = decodeConversationId(`certify-${slug}`);
668
+ const threadId = decodeThreadId(`certify-${slug}`);
669
+
586
670
  const cell: CertificationCell = {
587
- resolver: AgentBindingResolver.fromBindings([parentResolved, childResolved]),
671
+ bindings: [parentResolved, childResolved],
588
672
  submit: Effect.gen(function* () {
589
673
  const runtime = yield* DurableAgentRuntime;
674
+
590
675
  const receipt = yield* runtime.submit(
591
676
  { definition: { id: coordinatorDefinition.id, input: coordinatorDefinition.input } },
592
677
  { mission: "plan" },
593
- submitOptionsFor(slug, conversationId),
678
+ submitOptionsFor(slug, threadId),
594
679
  );
680
+
595
681
  return [receipt];
596
682
  }),
597
683
  lanes: (receipts) => {
598
684
  const parent = receipts.at(0);
685
+
599
686
  return parent === undefined
600
- ? [conversationId]
601
- : [conversationId, childConversationIdFor(parent.submissionId, DELEGATE_CALL)];
687
+ ? [threadId]
688
+ : [threadId, childThreadIdFor(parent.submissionId, DELEGATE_CALL)];
602
689
  },
603
690
  };
691
+
604
692
  return cell;
605
693
  }
606
694
  }
@@ -616,56 +704,68 @@ const MAX_REDRIVE_ROUNDS = 8;
616
704
  * the digest chain is fully recomputed instead of skipped.
617
705
  */
618
706
  const verifyLane = Effect.fn("Certification.verifyLane")(function* (
619
- lane: ConversationId,
707
+ lane: ThreadId,
620
708
  batchProducers: ReadonlyMap<BatchId, ProducerId>,
621
709
  ) {
622
- const store = yield* ConversationStore;
710
+ const store = yield* ThreadStore;
623
711
  const ledger = yield* SubmissionLedger;
624
- const exported = yield* store.export(ConversationExportRequest.make({ conversationId: lane }));
712
+ const exported = yield* store.export(ThreadExportRequest.make({ threadId: lane }));
625
713
  const rows = new Map<SubmissionId, SubmissionSnapshot>();
626
714
  const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
715
+
627
716
  for (const submission of nonterminal) {
628
- if (submission.conversationId === lane) rows.set(submission.submissionId, submission);
717
+ if (submission.threadId === lane) rows.set(submission.submissionId, submission);
629
718
  }
630
719
  const named = new Set<SubmissionId>();
720
+
631
721
  for (const envelope of exported.records) {
632
722
  const payload = envelope.record.payload;
723
+
633
724
  if (
634
725
  payload._tag === "UserInputRecorded" ||
635
726
  payload._tag === "SubmissionSettled" ||
636
727
  payload._tag === "AbortRequested"
637
728
  ) {
638
- named.add(payload.submissionId);
729
+ if (payload.submissionId !== undefined) named.add(payload.submissionId);
639
730
  }
640
731
  }
641
732
  for (const submissionId of named) {
642
733
  if (rows.has(submissionId)) continue;
643
734
  const found = yield* ledger.lookup(SubmissionLookupById.make({ submissionId }));
644
- if (Option.isSome(found) && found.value.conversationId === lane) {
735
+
736
+ if (Option.isSome(found) && found.value.threadId === lane) {
645
737
  rows.set(submissionId, found.value);
646
738
  }
647
739
  }
648
- const checkpoint = yield* store.loadCheckpoint(
649
- LoadCheckpointRequest.make({ conversationId: lane }),
650
- );
651
- return yield* verifyConversationInvariants({
740
+
741
+ const checkpoint =
742
+ store.checkpoints === undefined
743
+ ? Option.none()
744
+ : yield* store.checkpoints.load(LoadCheckpointRequest.make({ threadId: lane }));
745
+
746
+ return yield* verifyThreadInvariants({
652
747
  export: exported,
653
748
  submissions: [...rows.values()],
654
749
  batchProducers,
655
750
  checkpoint: Option.getOrUndefined(checkpoint),
751
+ checkpointsSupported: store.checkpoints !== undefined,
656
752
  requireAllSettled: true,
657
753
  });
658
754
  });
659
755
 
660
756
  const failureTagOf = <E>(cause: Cause.Cause<E>): string => {
661
757
  const failure = Cause.findErrorOption(cause);
758
+
662
759
  if (Option.isSome(failure)) {
663
760
  const error: unknown = failure.value;
761
+
664
762
  if (typeof error === "object" && error !== null && "_tag" in error) {
665
- return String((error as { _tag: unknown })._tag);
763
+ return String(error._tag);
666
764
  }
765
+
667
766
  return String(error).slice(0, 256);
668
767
  }
768
+
669
769
  return "defect";
670
770
  };
671
771
 
@@ -676,7 +776,6 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
676
776
  batchProducers: ReadonlyMap<BatchId, ProducerId>,
677
777
  leaseAdvance: Duration.Duration,
678
778
  ) {
679
- const runtime = yield* DurableAgentRuntime;
680
779
  const ledger = yield* SubmissionLedger;
681
780
  const control = yield* DurableRuntimeFailpointTestControl;
682
781
  const slug = `${scenario}-${location.replaceAll(":", "-")}`;
@@ -693,9 +792,20 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
693
792
 
694
793
  const cell = yield* makeCell(scenario, slug);
695
794
 
795
+ const runtime = yield* DurableAgentRuntime.pipe(
796
+ Effect.provide(
797
+ DurableAgentRuntime.layerWithBindings(cell.bindings).pipe(
798
+ Layer.provide(RunToolAuthorization.allowAll),
799
+ ),
800
+ ),
801
+ );
802
+
803
+ const submit = cell.submit.pipe(Effect.provideService(DurableAgentRuntime, runtime));
804
+
696
805
  // One-shot arm: the fault fires at most once anywhere in the cell (initial drive OR a
697
806
  // re-drive round's public unblocking operation), modelling one crash at this boundary.
698
807
  const fired = yield* Ref.make(false);
808
+
699
809
  yield* control.setHandler((hit) =>
700
810
  hit !== location
701
811
  ? Effect.void
@@ -710,13 +820,16 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
710
820
 
711
821
  // Submissions are idempotent (DUR-001): one replay recovers a submit-boundary fault.
712
822
  let receipts: ReadonlyArray<Receipt>;
713
- const firstSubmit = yield* Effect.exit(cell.submit);
823
+ const firstSubmit = yield* Effect.exit(submit);
824
+
714
825
  if (Exit.isSuccess(firstSubmit)) {
715
826
  receipts = firstSubmit.value;
716
827
  } else {
717
- const secondSubmit = yield* Effect.exit(cell.submit);
828
+ const secondSubmit = yield* Effect.exit(submit);
829
+
718
830
  if (Exit.isFailure(secondSubmit)) {
719
831
  yield* control.clear;
832
+
720
833
  return failed(
721
834
  `submission replay did not recover: ${failureTagOf(secondSubmit.cause)}`,
722
835
  yield* Ref.get(fired),
@@ -726,24 +839,24 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
726
839
  }
727
840
  const lanes = cell.lanes(receipts);
728
841
 
729
- const driveLane = (lane: ConversationId) =>
730
- runtime
731
- .processConversationResolved(lane)
732
- .pipe(Effect.provideService(AgentBindingResolver, cell.resolver));
842
+ const driveLane = (lane: ThreadId) => runtime.processThreadResolved(lane);
733
843
 
734
844
  const allSettled = Effect.gen(function* () {
735
845
  for (const receipt of receipts) {
736
846
  const snapshot = yield* ledger.lookup(
737
847
  SubmissionLookupById.make({ submissionId: receipt.submissionId }),
738
848
  );
849
+
739
850
  if (Option.isNone(snapshot) || snapshot.value.state !== "settled") return false;
740
851
  }
852
+
741
853
  return true;
742
854
  });
743
855
 
744
856
  // Re-drive to convergence using ONLY public operations: worker drives, recovery passes,
745
- // and the authorized DUR-017/approval unblocking paths chosen from `explainConversation`.
857
+ // and the authorized DUR-017/approval unblocking paths chosen from `explainThread`.
746
858
  let converged = false;
859
+
747
860
  for (let round = 0; round < MAX_REDRIVE_ROUNDS && !converged; round++) {
748
861
  // Expire any lease a faulted Attempt left behind (D5): virtual time is the
749
862
  // adapter-neutral reclaim lever — a live lease may block every new claim.
@@ -753,7 +866,8 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
753
866
  yield* Effect.exit(driveLane(lane));
754
867
  }
755
868
  for (const lane of lanes) {
756
- const explains = yield* Effect.exit(runtime.explainConversation(lane));
869
+ const explains = yield* Effect.exit(runtime.explainThread(lane));
870
+
757
871
  if (Exit.isFailure(explains)) continue;
758
872
  for (const explanation of explains.value) {
759
873
  for (const unknown of explanation.evidence.unknownCalls) {
@@ -774,6 +888,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
774
888
  const decided = explanation.evidence.approvalDecisions.some(
775
889
  (decision) => decision.toolCallId === pending.toolCallId,
776
890
  );
891
+
777
892
  if (decided) continue;
778
893
  yield* Effect.exit(
779
894
  runtime.resolveApproval(
@@ -790,6 +905,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
790
905
  }
791
906
  }
792
907
  const settled = yield* Effect.exit(allSettled);
908
+
793
909
  converged = Exit.isSuccess(settled) && settled.value;
794
910
  }
795
911
  yield* control.clear;
@@ -802,8 +918,10 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
802
918
  // Every lane of the cell must verify in convergence mode with a recomputed digest chain.
803
919
  let digestChainVerified = true;
804
920
  const failedChecks: Array<string> = [];
921
+
805
922
  for (const lane of lanes) {
806
923
  const verdict = yield* Effect.exit(verifyLane(lane, batchProducers));
924
+
807
925
  if (Exit.isFailure(verdict)) {
808
926
  return failed(`lane ${lane} could not be verified: ${failureTagOf(verdict.cause)}`, wasFired);
809
927
  }
@@ -864,10 +982,14 @@ export const resolveTierThree = Effect.fn("Certification.resolveTierThree")(func
864
982
  }
865
983
  if (options.crashLever !== undefined) {
866
984
  const cases = yield* options.crashLever;
985
+
867
986
  return CertificationTierThreeReport.make({
868
- status: "exercised",
987
+ status: cases.length === 0 ? "not-exercised" : "exercised",
869
988
  evidence: options.tierThreeEvidence ?? [],
870
989
  cases,
990
+ ...(cases.length === 0
991
+ ? { detail: "the supplied crash lever executed no real-loss cases" }
992
+ : {}),
871
993
  });
872
994
  }
873
995
  if (options.tierThreeEvidence !== undefined && options.tierThreeEvidence.length > 0) {
@@ -877,6 +999,7 @@ export const resolveTierThree = Effect.fn("Certification.resolveTierThree")(func
877
999
  cases: [],
878
1000
  });
879
1001
  }
1002
+
880
1003
  return CertificationTierThreeReport.make({
881
1004
  status: "not-exercised",
882
1005
  evidence: [],
@@ -905,13 +1028,15 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
905
1028
  options: CertifyDurableAdaptersOptions<LedgerE, StoreE>,
906
1029
  ): Effect.Effect<CertificationReport, LedgerE | StoreE, Crypto.Crypto> => {
907
1030
  const batchProducers = new Map<BatchId, ProducerId>();
1031
+
908
1032
  // Interpose the candidate store with an append-time capture of each batch's producer
909
- // identity — the one value the ConversationStore port deliberately does not export — so
1033
+ // identity — the one value the ThreadStore port deliberately does not export — so
910
1034
  // Tier 2's invariant verification recomputes the FULL digest chain instead of skipping it.
911
- const capturingStore = Layer.effect(ConversationStore)(
1035
+ const capturingStore = Layer.effect(ThreadStore)(
912
1036
  Effect.gen(function* () {
913
- const inner = yield* ConversationStore;
914
- return ConversationStore.of({
1037
+ const inner = yield* ThreadStore;
1038
+
1039
+ return ThreadStore.of({
915
1040
  ...inner,
916
1041
  append: (request) =>
917
1042
  Effect.sync(() => {
@@ -919,13 +1044,15 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
919
1044
  }).pipe(Effect.andThen(inner.append(request))),
920
1045
  });
921
1046
  }),
922
- ).pipe(Layer.provide(options.conversationStore));
1047
+ ).pipe(Layer.provide(options.threadStore));
923
1048
 
924
- const support = Layer.mergeAll(
1049
+ // RUN-036: certification uses the default-none Tool failure observer. Trusted application
1050
+ // reporting adds no durable transition and is verified separately from adapter certification.
1051
+ const environment = Layer.mergeAll(
925
1052
  options.submissionLedger,
926
1053
  capturingStore,
927
1054
  options.wakeScheduler ?? WakeScheduler.layerNoop,
928
- DurableRuntimeFailpoint.layerTest,
1055
+ DurableRuntimeFailpointTestControl.layer,
929
1056
  ToolReconciler.uncertain,
930
1057
  DurableRuntimeConfig.layer({
931
1058
  deploymentId: Schema.decodeSync(DeploymentId)("deployment-certification"),
@@ -935,7 +1062,6 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
935
1062
  abortPollInterval: Duration.millis(50),
936
1063
  }),
937
1064
  );
938
- const environment = DurableAgentRuntime.layer.pipe(Layer.provideMerge(support));
939
1065
 
940
1066
  const leaseAdvance = Duration.millis(
941
1067
  Duration.toMillis(options.ownershipLeaseDuration ?? DEFAULT_OWNERSHIP_LEASE_DURATION) + 1_000,
@@ -946,6 +1072,7 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
946
1072
 
947
1073
  // Tier 2 — coordinator failpoint convergence sweep.
948
1074
  const tier2: Array<CertificationSweepResult> = [];
1075
+
949
1076
  for (const scenario of CERTIFICATION_SCENARIOS) {
950
1077
  for (const location of DurableRuntimeFailpointLocation.literals) {
951
1078
  tier2.push(yield* runSweepCell(scenario, location, batchProducers, leaseAdvance));
@@ -960,13 +1087,14 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
960
1087
  const tier3 = yield* resolveTierThree(capabilities.durability, options);
961
1088
 
962
1089
  const generatedAt = yield* nowUtc;
1090
+
963
1091
  const ok =
964
1092
  tier1.every((result) => result.status === "passed") &&
965
1093
  tier2.every((result) => result.status !== "failed") &&
966
1094
  tier3.cases.every((result) => result.status === "passed");
967
1095
 
968
1096
  return CertificationReport.make({
969
- format: "effect-agent/certification@1",
1097
+ format: "effect-agent/certification@2",
970
1098
  adapter: CertifiedAdapterIdentity.make({
971
1099
  name: options.adapter.name,
972
1100
  ...(options.adapter.version === undefined ? {} : { version: options.adapter.version }),
@@ -977,6 +1105,12 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
977
1105
  tier2,
978
1106
  tier3,
979
1107
  ok,
1108
+ fullyCertified:
1109
+ ok &&
1110
+ capabilities.durability !== "non-durable" &&
1111
+ tier3.status === "exercised" &&
1112
+ tier3.cases.length > 0 &&
1113
+ tier3.cases.every((result) => result.suite === "real-loss"),
980
1114
  });
981
1115
  });
982
1116