@effect-agent/testing 0.1.0-beta.9 → 0.1.0-beta.91

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 (55) hide show
  1. package/dist/Certification.d.mts +106 -0
  2. package/dist/Certification.mjs +633 -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 +276 -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 +1680 -0
  21. package/dist/TravelPlanner.mjs +1963 -0
  22. package/dist/TravelPlanner.mjs.map +1 -0
  23. package/dist/deterministic-layers-D5owIoke.mjs +358 -0
  24. package/dist/deterministic-layers-D5owIoke.mjs.map +1 -0
  25. package/dist/index.d.mts +2 -3408
  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} +366 -171
  30. package/src/{chaos.ts → Chaos.ts} +241 -127
  31. package/src/{code-executor-conformance.ts → CodeExecutorConformance.ts} +30 -7
  32. package/src/{code-executor-substitute.ts → CodeExecutorSubstitute.ts} +113 -46
  33. package/src/{fixtures/docs-researcher/index.ts → DocsResearcher.ts} +68 -5
  34. package/src/{scripted-model.ts → ScriptedModel.ts} +25 -29
  35. package/src/TravelPlanner.ts +232 -0
  36. package/src/fixtures/docs-researcher/definition.ts +20 -11
  37. package/src/fixtures/docs-researcher/harness.ts +41 -34
  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 +19 -42
  43. package/src/fixtures/travel-planner/phase4.ts +24 -37
  44. package/src/fixtures/travel-planner/phase5.ts +35 -42
  45. package/src/fixtures/travel-planner/phase6.ts +191 -88
  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 +35 -61
  49. package/src/fixtures/travel-planner/subagents.ts +36 -14
  50. package/src/index.ts +1 -11
  51. package/src/internal/certification-report.ts +25 -0
  52. package/dist/index.mjs.map +0 -1
  53. package/src/code-executor-conformance.d.ts +0 -30
  54. package/src/fixtures/travel-planner/index.ts +0 -11
  55. package/src/fixtures/warehouse/index.ts +0 -412
@@ -0,0 +1,633 @@
1
+ import { Cause, Clock, DateTime, Duration, Effect, Exit, Layer, Option, Ref, Schema, Stream } from "effect";
2
+ import { LanguageModel, Model, Tool, Toolkit } from "effect/unstable/ai";
3
+ import * as Agent from "effect-agent/agent";
4
+ import { AgentPolicy } from "effect-agent/agent-policy";
5
+ import { DurableWorkerBinding } from "effect-agent/agent-registration";
6
+ import { DurableAgentRuntime, DurableRuntimeConfig } from "effect-agent/durable-agent-runtime";
7
+ import { DurableRuntimeFailpointError, DurableRuntimeFailpointLocation } from "effect-agent/durable-failpoint";
8
+ import { DurableStep, DurableStepError } from "effect-agent/durable-step";
9
+ import { IdGenerator } from "effect-agent/id-generator";
10
+ import { RunId, ThreadId, ToolCallId, TurnId } from "effect-agent/identifiers";
11
+ import { DefinitionDigests, DeploymentId, Digest, ProducerId } from "effect-agent/records";
12
+ import { childThreadIdFor } from "effect-agent/run-journal";
13
+ import { RunToolAuthorization } from "effect-agent/run-options";
14
+ import * as Subagent from "effect-agent/subagent";
15
+ import { SubagentPolicy } from "effect-agent/subagent";
16
+ import { SubagentReservationsMemoryLive } from "effect-agent/subagent-reservations";
17
+ import { ApprovalDecisionCommand, DEFAULT_OWNERSHIP_LEASE_DURATION, IdempotencyKey, Principal, ResolutionSafeToRetry, SubmissionLedger, SubmissionLookupById, UnknownResolutionCommand } from "effect-agent/submission-ledger";
18
+ import { CertificationReport, CertificationSweepResult, CertificationTierThreeReport, CertifiedAdapterIdentity, certifyPorts } from "effect-agent/testing/certification";
19
+ import { DurableRuntimeFailpointTestControl } from "effect-agent/testing/durable-failpoint-test-control";
20
+ import { verifyThreadInvariants } from "effect-agent/thread-invariants";
21
+ import { LoadCheckpointRequest, ThreadExportRequest, ThreadStore } from "effect-agent/thread-store";
22
+ import { ToolReconciler } from "effect-agent/tool-reconciler";
23
+ import { WakeScheduler } from "effect-agent/wake-scheduler";
24
+ import { TestClock } from "effect/testing";
25
+ //#region src/internal/certification-report.ts
26
+ /** Fold executed results without acquiring adapters or claiming coverage from citations. */
27
+ const makeCertificationReport = (results) => {
28
+ const { adapter, tier1, tier2, tier3 } = results;
29
+ const ok = tier1.every((result) => result.status === "passed") && tier2.every((result) => result.status !== "failed") && tier3.cases.every((result) => result.status === "passed");
30
+ return CertificationReport.make({
31
+ format: "effect-agent/certification@2",
32
+ ...results,
33
+ ok,
34
+ fullyCertified: ok && adapter.durability !== "non-durable" && tier3.status === "exercised" && tier3.cases.length > 0 && tier3.cases.every((result) => result.suite === "real-loss")
35
+ });
36
+ };
37
+ //#endregion
38
+ //#region src/Certification.ts
39
+ /** The six Tier-2 scenario shapes in sweep order. */
40
+ const CERTIFICATION_SCENARIOS = [
41
+ "plain",
42
+ "uncertain-tool",
43
+ "durable-steps",
44
+ "approval",
45
+ "join",
46
+ "delegation"
47
+ ];
48
+ /**
49
+ * Coordinator failpoint locations that none of the six scenario shapes can reach, recorded
50
+ * honestly instead of silently claimed. These require operator, compaction, reservation,
51
+ * background-worker, or Agent-update paths the shapes do not take. They are pinned in-process
52
+ * by the P5/S2 suites (`packages/testing/test/durable-tools.test.ts` "resolveUnknown is idempotent across the
53
+ * intent failpoint", `durable-runtime.test.ts` abort rows,
54
+ * `durable-subagents.test.ts` abort propagation) and by the process-kill/eviction crash
55
+ * matrices. Runner tests assert the observed never-fired set equals EXACTLY this list, so a
56
+ * protocol change that silently stops exercising a location fails the certification.
57
+ */
58
+ const TIER2_UNREACHED_LOCATIONS = [
59
+ "checkpoint:before-save",
60
+ "checkpoint:after-save",
61
+ "abort:after-intent",
62
+ "compaction:before-canonical-append",
63
+ "compaction:after-canonical-append",
64
+ "policy:before-reservation-append",
65
+ "policy:after-reservation-append",
66
+ "resolve:after-intent",
67
+ "subagent:after-child-abort-intent",
68
+ "worker:before-source-append",
69
+ "worker:after-source-append",
70
+ "worker:before-origin-append",
71
+ "worker:after-origin-append",
72
+ "worker:before-completion-append",
73
+ "worker:after-completion-append",
74
+ "worker:before-subtree-append",
75
+ "worker:after-subtree-append",
76
+ "worker:before-report-append",
77
+ "worker:after-report-append",
78
+ "worker:before-report-delivery",
79
+ "worker:after-report-delivery",
80
+ "update:before-canonical-append",
81
+ "update:after-canonical-append",
82
+ "update:before-delivery-insert",
83
+ "update:after-delivery-insert"
84
+ ];
85
+ /** Locations of `tier2` rows whose armed fault never fired in ANY scenario, sorted. */
86
+ const tier2NeverFiredLocations = (tier2) => {
87
+ const fired = /* @__PURE__ */ new Set();
88
+ for (const row of tier2) if (row.failpointFired) fired.add(row.location);
89
+ return DurableRuntimeFailpointLocation.literals.filter((location) => !fired.has(location)).sort();
90
+ };
91
+ const SHA_A = Schema.decodeSync(Digest)("a".repeat(64));
92
+ const DIGESTS = DefinitionDigests.make({
93
+ agent: SHA_A,
94
+ model: SHA_A,
95
+ tools: SHA_A
96
+ });
97
+ const CHILD_DIGEST_STRINGS = {
98
+ agent: "b".repeat(64),
99
+ model: "c".repeat(64),
100
+ tools: "d".repeat(64)
101
+ };
102
+ const CHILD_DIGESTS = DefinitionDigests.make({
103
+ agent: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.agent),
104
+ model: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.model),
105
+ tools: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.tools)
106
+ });
107
+ const PRINCIPAL = Schema.decodeSync(Principal)("principal-certification");
108
+ const decodeThreadId = Schema.decodeSync(ThreadId);
109
+ const decodeIdempotencyKey = Schema.decodeSync(IdempotencyKey);
110
+ const decodeToolCallId = Schema.decodeSync(ToolCallId);
111
+ const usage = {
112
+ inputTokens: {},
113
+ outputTokens: {}
114
+ };
115
+ const finalParts = (text) => [
116
+ {
117
+ type: "text-start",
118
+ id: "answer"
119
+ },
120
+ {
121
+ type: "text-delta",
122
+ id: "answer",
123
+ delta: text
124
+ },
125
+ {
126
+ type: "text-end",
127
+ id: "answer"
128
+ },
129
+ {
130
+ type: "finish",
131
+ reason: "stop",
132
+ usage
133
+ }
134
+ ];
135
+ const toolCallPart = (id, name, params) => ({
136
+ type: "tool-call",
137
+ id,
138
+ name,
139
+ params,
140
+ providerExecuted: false
141
+ });
142
+ const toolTurn = (...calls) => [...calls, {
143
+ type: "finish",
144
+ reason: "tool-calls",
145
+ usage
146
+ }];
147
+ /**
148
+ * Stateless scripted model that decides by PROMPT SHAPE instead of call count: while the
149
+ * prompt carries no committed tool result the model declares `toolParts` (when given),
150
+ * otherwise it answers with the final text. Deciding on the canonical prompt keeps every cell
151
+ * deterministic regardless of where the injected fault fell — a re-invoked Turn re-declares
152
+ * the same batch and a resumed batch flows into the final answer, so every scenario always
153
+ * exercises its tool path and always converges.
154
+ */
155
+ const promptShapeModel = (name, finalText, toolParts) => Model.make("scripted", name, Layer.effect(LanguageModel.LanguageModel, LanguageModel.make({
156
+ generateText: () => Effect.succeed([]),
157
+ streamText: (request) => {
158
+ const hasToolResult = request.prompt.content.some((message) => message.role === "tool");
159
+ const parts = toolParts === void 0 || hasToolResult ? finalParts(finalText) : toolParts;
160
+ return Stream.fromIterable(parts);
161
+ }
162
+ })));
163
+ const CERTIFICATION_MAX_DURATION = "10 minutes";
164
+ const policy = AgentPolicy.make({
165
+ maxTurns: 4,
166
+ maxToolCalls: 4,
167
+ maxDuration: CERTIFICATION_MAX_DURATION,
168
+ toolConcurrency: 2
169
+ });
170
+ const QuestionInput = Schema.Struct({ question: Schema.String });
171
+ const AnswerOutput = Schema.Struct({ answer: Schema.String });
172
+ /** plain / join: no tools — the pure Turn/submission/join seams. */
173
+ const plainDefinition = Agent.make("certify-plain", {
174
+ input: QuestionInput,
175
+ output: AnswerOutput,
176
+ instructions: "Answer as JSON.",
177
+ toolkit: Toolkit.empty,
178
+ policy
179
+ });
180
+ /** uncertain-tool: unannotated → fail-closed `uncertain`, enters the prepared/settled protocol. */
181
+ const Book = Tool.make("book", {
182
+ parameters: Schema.Struct({ ref: Schema.String }),
183
+ success: Schema.Struct({ confirmation: Schema.String })
184
+ });
185
+ const bookToolkit = Toolkit.make(Book);
186
+ const uncertainDefinition = Agent.make("certify-uncertain", {
187
+ input: QuestionInput,
188
+ output: AnswerOutput,
189
+ instructions: "Book it.",
190
+ toolkit: bookToolkit,
191
+ policy
192
+ });
193
+ /** durable-steps: declaring `DurableStep` as a dependency is what makes the Tool durable. */
194
+ const Itinerary = Tool.make("itinerary", {
195
+ parameters: Schema.Struct({ ref: Schema.String }),
196
+ success: Schema.Struct({ state: Schema.String }),
197
+ failure: DurableStepError,
198
+ dependencies: [DurableStep]
199
+ });
200
+ const itineraryToolkit = Toolkit.make(Itinerary);
201
+ const stepsDefinition = Agent.make("certify-steps", {
202
+ input: QuestionInput,
203
+ output: AnswerOutput,
204
+ instructions: "Reserve the itinerary.",
205
+ toolkit: itineraryToolkit,
206
+ policy
207
+ });
208
+ /** approval: fail-closed — no `DurableApprovalResolver` Layer, so undecided approvals suspend. */
209
+ const BookApproval = Tool.make("book", {
210
+ parameters: Schema.Struct({ ref: Schema.String }),
211
+ success: Schema.Struct({ confirmation: Schema.String }),
212
+ needsApproval: true
213
+ });
214
+ const approvalToolkit = Toolkit.make(BookApproval);
215
+ const approvalDefinition = Agent.make("certify-approval", {
216
+ input: QuestionInput,
217
+ output: AnswerOutput,
218
+ instructions: "Book after approval.",
219
+ toolkit: approvalToolkit,
220
+ policy
221
+ });
222
+ /** delegation: durable attached child plus an ordinary uncertain sibling in ONE batch. */
223
+ const childDefinition = Agent.make("certify-child", {
224
+ input: QuestionInput,
225
+ output: AnswerOutput,
226
+ instructions: "Answer as JSON.",
227
+ toolkit: Toolkit.empty,
228
+ policy: AgentPolicy.make({
229
+ maxTurns: 2,
230
+ maxToolCalls: 1,
231
+ maxDuration: CERTIFICATION_MAX_DURATION,
232
+ toolConcurrency: 1
233
+ })
234
+ });
235
+ var CertifyDelegationFailed = class extends Schema.TaggedError()("CertifyDelegationFailed", { childErrorTag: Schema.String }) {};
236
+ const researchDelegation = Subagent.define("delegate_research", {
237
+ description: "Research one bounded question and return findings.",
238
+ target: childDefinition,
239
+ parameters: Schema.Struct({ topic: Schema.String }),
240
+ success: Schema.Struct({ summary: Schema.String }),
241
+ failure: CertifyDelegationFailed,
242
+ prepareInput: ({ topic }) => Effect.succeed({ question: `research:${topic}` }),
243
+ projectResult: (output) => Effect.succeed({ summary: `finding:${output.answer}` }),
244
+ policy: SubagentPolicy.make({
245
+ maxChildren: 2,
246
+ maxConcurrency: 2,
247
+ maxTurns: 4,
248
+ maxToolCalls: 4,
249
+ maxDuration: CERTIFICATION_MAX_DURATION
250
+ })
251
+ });
252
+ const Lookup = Tool.make("lookup", {
253
+ parameters: Schema.Struct({ key: Schema.String }),
254
+ success: Schema.Struct({ value: Schema.String })
255
+ });
256
+ const coordinatorDefinition = Agent.make("certify-coordinator", {
257
+ input: Schema.Struct({ mission: Schema.String }),
258
+ output: Schema.Struct({ report: Schema.String }),
259
+ instructions: "Delegate and look up, then answer as JSON.",
260
+ toolkit: Toolkit.make(researchDelegation.tool, Lookup),
261
+ policy: AgentPolicy.make({
262
+ maxTurns: 4,
263
+ maxToolCalls: 3,
264
+ maxDuration: CERTIFICATION_MAX_DURATION,
265
+ toolConcurrency: 2
266
+ })
267
+ });
268
+ const mapChildFailure = (failure) => CertifyDelegationFailed.make({ childErrorTag: failure._tag });
269
+ const DELEGATE_CALL = decodeToolCallId("delegate-1");
270
+ /** Fixture-only identity source consumed by the delegation Layer's ephemeral capture. */
271
+ const identifiers = Layer.effect(IdGenerator, Effect.gen(function* () {
272
+ const counter = yield* Ref.make(0);
273
+ const next = (decode, prefix) => Ref.getAndUpdate(counter, (value) => value + 1).pipe(Effect.map((value) => decode(`${prefix}-${value}`)));
274
+ return {
275
+ nextThreadId: next(decodeThreadId, "certify-fixture-thread"),
276
+ nextRunId: next(Schema.decodeSync(RunId), "certify-fixture-run"),
277
+ nextTurnId: next(Schema.decodeSync(TurnId), "certify-fixture-turn")
278
+ };
279
+ }));
280
+ const delegationSupport = Layer.mergeAll(SubagentReservationsMemoryLive, identifiers);
281
+ const submitOptionsFor = (slug, threadId) => ({
282
+ threadId,
283
+ principal: PRINCIPAL,
284
+ idempotencyKey: decodeIdempotencyKey(`certify-key-${slug}`),
285
+ definitions: DIGESTS
286
+ });
287
+ /** One single-agent cell: one lane, one Submission, one registered exact-digest binding. */
288
+ const makeSingleAgentCell = (definition, resolved, slug) => {
289
+ const threadId = decodeThreadId(`certify-${slug}`);
290
+ const submit = Effect.gen(function* () {
291
+ return [yield* (yield* DurableAgentRuntime).submit({ definition: {
292
+ id: definition.id,
293
+ input: definition.input
294
+ } }, { question: `certify ${slug}` }, submitOptionsFor(slug, threadId))];
295
+ });
296
+ return {
297
+ bindings: [resolved],
298
+ submit,
299
+ lanes: () => [threadId]
300
+ };
301
+ };
302
+ const makeCell = Effect.fn("Certification.makeCell")(function* (scenario, slug) {
303
+ switch (scenario) {
304
+ case "plain": {
305
+ const binding = Agent.withModel(plainDefinition, promptShapeModel("certify-plain", "{\"answer\":\"done\"}"));
306
+ const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS);
307
+ return makeSingleAgentCell(plainDefinition, resolved, slug);
308
+ }
309
+ case "uncertain-tool": {
310
+ const binding = Agent.withModel(uncertainDefinition, promptShapeModel("certify-uncertain", "{\"answer\":\"booked\"}", toolTurn(toolCallPart("book-1", "book", { ref: `r-${slug}` }))));
311
+ const toolLayer = bookToolkit.toLayer({ book: ({ ref }) => Effect.succeed({ confirmation: `confirmed-${ref}` }) });
312
+ const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(Effect.provide(toolLayer));
313
+ return makeSingleAgentCell(uncertainDefinition, resolved, slug);
314
+ }
315
+ case "durable-steps": {
316
+ const binding = Agent.withModel(stepsDefinition, promptShapeModel("certify-steps", "{\"answer\":\"reserved\"}", toolTurn(toolCallPart("itinerary-1", "itinerary", { ref: `trip-${slug}` }))));
317
+ const toolLayer = itineraryToolkit.toLayer({ itinerary: ({ ref }) => Effect.gen(function* () {
318
+ const step = yield* DurableStep;
319
+ return { state: `${yield* step.do("reserve-flight", Schema.String, Effect.succeed(`flight-${ref}`))}+${yield* step.do("reserve-lodging", Schema.String, Effect.succeed(`lodging-${ref}`))}` };
320
+ }) });
321
+ const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(Effect.provide(toolLayer));
322
+ return makeSingleAgentCell(stepsDefinition, resolved, slug);
323
+ }
324
+ case "approval": {
325
+ const binding = Agent.withModel(approvalDefinition, promptShapeModel("certify-approval", "{\"answer\":\"approved\"}", toolTurn(toolCallPart("book-1", "book", { ref: `r-${slug}` }))));
326
+ const toolLayer = approvalToolkit.toLayer({ book: ({ ref }) => Effect.succeed({ confirmation: `confirmed-${ref}` }) });
327
+ const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(Effect.provide(toolLayer));
328
+ return makeSingleAgentCell(approvalDefinition, resolved, slug);
329
+ }
330
+ case "join": {
331
+ const binding = Agent.withModel(plainDefinition, promptShapeModel("certify-join", "{\"answer\":\"host answer\"}"));
332
+ const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS);
333
+ const threadId = decodeThreadId(`certify-${slug}`);
334
+ const submitOne = Effect.fn("Certification.submitOne")(function* (key, question) {
335
+ return yield* (yield* DurableAgentRuntime).submit({ definition: {
336
+ id: plainDefinition.id,
337
+ input: plainDefinition.input
338
+ } }, { question }, {
339
+ threadId,
340
+ principal: PRINCIPAL,
341
+ idempotencyKey: decodeIdempotencyKey(key),
342
+ definitions: DIGESTS
343
+ });
344
+ });
345
+ return {
346
+ bindings: [resolved],
347
+ submit: Effect.gen(function* () {
348
+ return [yield* submitOne(`certify-key-${slug}-host`, "host question"), yield* submitOne(`certify-key-${slug}-queued`, "queued question")];
349
+ }),
350
+ lanes: () => [threadId]
351
+ };
352
+ }
353
+ case "delegation": {
354
+ const childBinding = Agent.withModel(childDefinition, promptShapeModel("certify-child", "{\"answer\":\"child-answer\"}"));
355
+ const parentBinding = Agent.withModel(coordinatorDefinition, promptShapeModel("certify-parent", "{\"report\":\"done\"}", toolTurn(toolCallPart("delegate-1", "delegate_research", { topic: "paris" }), toolCallPart("lookup-1", "lookup", { key: "hotels" }))));
356
+ const delegationLayer = Subagent.layer(researchDelegation, childBinding, {
357
+ mapChildFailure,
358
+ durable: { targetDigests: CHILD_DIGEST_STRINGS }
359
+ }).pipe(Layer.provide(delegationSupport));
360
+ const lookupLayer = Toolkit.make(Lookup).toLayer({ lookup: ({ key }) => Effect.succeed({ value: `found-${key}` }) });
361
+ const parentResolved = yield* DurableWorkerBinding.make(parentBinding, DIGESTS).pipe(Effect.provide(Layer.mergeAll(delegationLayer, lookupLayer)));
362
+ const childResolved = yield* DurableWorkerBinding.make(childBinding, CHILD_DIGESTS);
363
+ const threadId = decodeThreadId(`certify-${slug}`);
364
+ return {
365
+ bindings: [parentResolved, childResolved],
366
+ submit: Effect.gen(function* () {
367
+ return [yield* (yield* DurableAgentRuntime).submit({ definition: {
368
+ id: coordinatorDefinition.id,
369
+ input: coordinatorDefinition.input
370
+ } }, { mission: "plan" }, submitOptionsFor(slug, threadId))];
371
+ }),
372
+ lanes: (receipts) => {
373
+ const parent = receipts.at(0);
374
+ return parent === void 0 ? [threadId] : [threadId, childThreadIdFor(parent.submissionId, DELEGATE_CALL)];
375
+ }
376
+ };
377
+ }
378
+ }
379
+ });
380
+ /** Maximum recovery/drive/unblock rounds before a cell is reported non-convergent. */
381
+ const MAX_REDRIVE_ROUNDS = 8;
382
+ /**
383
+ * Verify one lane after convergence: canonical export + every lane Submission the ledger or
384
+ * the log names (the same collection rule as the admin `verify` member), fed to the shared
385
+ * invariant checker in convergence mode WITH the captured per-batch producer directory, so
386
+ * the digest chain is fully recomputed instead of skipped.
387
+ */
388
+ const verifyLane = Effect.fn("Certification.verifyLane")(function* (lane, batchProducers) {
389
+ const store = yield* ThreadStore;
390
+ const ledger = yield* SubmissionLedger;
391
+ const exported = yield* store.export(ThreadExportRequest.make({ threadId: lane }));
392
+ const rows = /* @__PURE__ */ new Map();
393
+ const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
394
+ for (const submission of nonterminal) if (submission.threadId === lane) rows.set(submission.submissionId, submission);
395
+ const named = /* @__PURE__ */ new Set();
396
+ for (const envelope of exported.records) {
397
+ const payload = envelope.record.payload;
398
+ if (payload._tag === "UserInputRecorded" || payload._tag === "SubmissionSettled" || payload._tag === "AbortRequested") {
399
+ if (payload.submissionId !== void 0) named.add(payload.submissionId);
400
+ }
401
+ }
402
+ for (const submissionId of named) {
403
+ if (rows.has(submissionId)) continue;
404
+ const found = yield* ledger.lookup(SubmissionLookupById.make({ submissionId }));
405
+ if (Option.isSome(found) && found.value.threadId === lane) rows.set(submissionId, found.value);
406
+ }
407
+ const checkpoint = store.checkpoints === void 0 ? Option.none() : yield* store.checkpoints.load(LoadCheckpointRequest.make({ threadId: lane }));
408
+ return yield* verifyThreadInvariants({
409
+ export: exported,
410
+ submissions: [...rows.values()],
411
+ batchProducers,
412
+ checkpoint: Option.getOrUndefined(checkpoint),
413
+ checkpointsSupported: store.checkpoints !== void 0,
414
+ requireAllSettled: true
415
+ });
416
+ });
417
+ const failureTagOf = (cause) => {
418
+ const failure = Cause.findErrorOption(cause);
419
+ if (Option.isSome(failure)) {
420
+ const error = failure.value;
421
+ if (typeof error === "object" && error !== null && "_tag" in error) return String(error._tag);
422
+ return String(error).slice(0, 256);
423
+ }
424
+ return "defect";
425
+ };
426
+ /** Discover a clean path, or arm one location; both drives converge and verify real storage. */
427
+ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (scenario, location, batchProducers, leaseAdvance, reached) {
428
+ const ledger = yield* SubmissionLedger;
429
+ const control = yield* DurableRuntimeFailpointTestControl;
430
+ const slug = `${scenario}-${location?.replaceAll(":", "-") ?? "discovery"}`;
431
+ const failed = (detail, fired) => ({
432
+ failpointFired: fired,
433
+ status: "failed",
434
+ digestChainVerified: false,
435
+ detail: detail.slice(0, 4096)
436
+ });
437
+ const cell = yield* makeCell(scenario, slug);
438
+ const runtime = yield* DurableAgentRuntime.pipe(Effect.provide(DurableAgentRuntime.layerWithBindings(cell.bindings).pipe(Layer.provide(RunToolAuthorization.allowAll))));
439
+ const submit = cell.submit.pipe(Effect.provideService(DurableAgentRuntime, runtime));
440
+ const fired = yield* Ref.make(false);
441
+ yield* control.setHandler((hit) => Effect.suspend(() => {
442
+ reached.add(hit);
443
+ return hit !== location ? Effect.void : Ref.getAndSet(fired, true).pipe(Effect.flatMap((already) => already ? Effect.void : Effect.fail(DurableRuntimeFailpointError.make({ location: hit }))));
444
+ }));
445
+ let receipts;
446
+ const firstSubmit = yield* Effect.exit(submit);
447
+ if (Exit.isSuccess(firstSubmit)) receipts = firstSubmit.value;
448
+ else {
449
+ const secondSubmit = yield* Effect.exit(submit);
450
+ if (Exit.isFailure(secondSubmit)) {
451
+ yield* control.clear;
452
+ return failed(`submission replay did not recover: ${failureTagOf(secondSubmit.cause)}`, yield* Ref.get(fired));
453
+ }
454
+ receipts = secondSubmit.value;
455
+ }
456
+ const lanes = cell.lanes(receipts);
457
+ const driveLane = (lane) => runtime.processThreadResolved(lane);
458
+ const allSettled = Effect.gen(function* () {
459
+ for (const receipt of receipts) {
460
+ const snapshot = yield* ledger.lookup(SubmissionLookupById.make({ submissionId: receipt.submissionId }));
461
+ if (Option.isNone(snapshot) || snapshot.value.state !== "settled") return false;
462
+ }
463
+ return true;
464
+ });
465
+ let converged = false;
466
+ for (let round = 0; round < MAX_REDRIVE_ROUNDS && !converged; round++) {
467
+ yield* TestClock.adjust(leaseAdvance);
468
+ yield* Effect.exit(runtime.runRecovery);
469
+ for (const lane of lanes) yield* Effect.exit(driveLane(lane));
470
+ for (const lane of lanes) {
471
+ const explains = yield* Effect.exit(runtime.explainThread(lane));
472
+ if (Exit.isFailure(explains)) continue;
473
+ for (const explanation of explains.value) {
474
+ for (const unknown of explanation.evidence.unknownCalls) {
475
+ if (unknown.resolved) continue;
476
+ yield* Effect.exit(runtime.resolveUnknown(UnknownResolutionCommand.make({
477
+ submissionId: explanation.submission.submissionId,
478
+ toolCallId: unknown.toolCallId,
479
+ author: "certification-runner",
480
+ reason: `re-drive after injected fault at ${location}`,
481
+ resolution: ResolutionSafeToRetry.make()
482
+ })));
483
+ }
484
+ for (const pending of explanation.evidence.approvalsPending) {
485
+ if (explanation.evidence.approvalDecisions.some((decision) => decision.toolCallId === pending.toolCallId)) continue;
486
+ yield* Effect.exit(runtime.resolveApproval(ApprovalDecisionCommand.make({
487
+ submissionId: explanation.submission.submissionId,
488
+ toolCallId: pending.toolCallId,
489
+ decision: "approved",
490
+ resolver: "certification-runner",
491
+ reason: `re-drive after injected fault at ${location}`
492
+ })));
493
+ }
494
+ }
495
+ }
496
+ const settled = yield* Effect.exit(allSettled);
497
+ converged = Exit.isSuccess(settled) && settled.value;
498
+ }
499
+ yield* control.clear;
500
+ const wasFired = yield* Ref.get(fired);
501
+ if (!converged) return failed(`did not converge within ${MAX_REDRIVE_ROUNDS} re-drive rounds`, wasFired);
502
+ let digestChainVerified = true;
503
+ const failedChecks = [];
504
+ for (const lane of lanes) {
505
+ const verdict = yield* Effect.exit(verifyLane(lane, batchProducers));
506
+ if (Exit.isFailure(verdict)) return failed(`lane ${lane} could not be verified: ${failureTagOf(verdict.cause)}`, wasFired);
507
+ for (const check of verdict.value.checks) {
508
+ if (check.status === "failed") failedChecks.push(`${lane}:${check.name}${check.detail === void 0 ? "" : ` (${check.detail})`}`);
509
+ if (check.name === "digest-chain" && check.status !== "passed") digestChainVerified = false;
510
+ }
511
+ }
512
+ if (failedChecks.length > 0 || !digestChainVerified) return failed(failedChecks.length > 0 ? `invariant checks failed: ${failedChecks.join("; ")}` : "the digest chain was not fully recomputed", wasFired);
513
+ return {
514
+ failpointFired: wasFired,
515
+ status: wasFired ? "converged" : "not-triggered",
516
+ digestChainVerified
517
+ };
518
+ });
519
+ /**
520
+ * Resolve the Tier-3 record honestly (plan §1): a non-durable reference adapter has no real
521
+ * loss to exercise (`not-applicable`); a supplied lever runs NOW (`exercised`); committed
522
+ * real-loss citations are recorded (`recorded-evidence`); otherwise the certificate says
523
+ * `not-exercised` — a scoped statement, never a silent claim.
524
+ */
525
+ const resolveTierThree = Effect.fn("Certification.resolveTierThree")(function* (durability, options) {
526
+ if (durability === "non-durable") return CertificationTierThreeReport.make({
527
+ status: "not-applicable",
528
+ evidence: [],
529
+ cases: [],
530
+ detail: "the adapter declares non-durable state (reference/conformance adapter); there is no real loss to exercise"
531
+ });
532
+ if (options.crashLever !== void 0) {
533
+ const cases = yield* options.crashLever;
534
+ return CertificationTierThreeReport.make({
535
+ status: cases.length === 0 ? "not-exercised" : "exercised",
536
+ evidence: options.tierThreeEvidence ?? [],
537
+ cases,
538
+ ...cases.length === 0 ? { detail: "the supplied crash lever executed no real-loss cases" } : {}
539
+ });
540
+ }
541
+ if (options.tierThreeEvidence !== void 0 && options.tierThreeEvidence.length > 0) return CertificationTierThreeReport.make({
542
+ status: "recorded-evidence",
543
+ evidence: options.tierThreeEvidence,
544
+ cases: []
545
+ });
546
+ return CertificationTierThreeReport.make({
547
+ status: "not-exercised",
548
+ evidence: [],
549
+ cases: [],
550
+ detail: "no crash lever was supplied and no committed real-loss evidence was cited; Tier 3 is NOT discharged for this adapter"
551
+ });
552
+ });
553
+ const nowUtc = Effect.map(Clock.currentTimeMillis, (millis) => DateTime.toUtc(DateTime.makeUnsafe(millis)));
554
+ /**
555
+ * Certify one durable adapter pair (plan §1, §8 WP2). Runs Tier 2 FIRST over pristine
556
+ * storage (each cell converges to all-settled before the next starts, so the recovery scan
557
+ * never sees foreign leftovers), then Tier 1's port contract cases (whose lanes deliberately
558
+ * end in every nonterminal shape), then records Tier 3. Requires `Crypto.Crypto` and a
559
+ * TestClock-backed environment; the candidate Layers are built exactly once.
560
+ */
561
+ const certifyDurableAdapters = (options) => {
562
+ const batchProducers = /* @__PURE__ */ new Map();
563
+ const capturingStore = Layer.effect(ThreadStore)(Effect.gen(function* () {
564
+ const inner = yield* ThreadStore;
565
+ return ThreadStore.of({
566
+ ...inner,
567
+ append: (request) => Effect.sync(() => {
568
+ batchProducers.set(request.batch.batchId, request.batch.producerId);
569
+ }).pipe(Effect.andThen(inner.append(request)))
570
+ });
571
+ })).pipe(Layer.provide(options.threadStore));
572
+ const environment = Layer.mergeAll(options.submissionLedger, capturingStore, options.wakeScheduler ?? WakeScheduler.layerNoop, DurableRuntimeFailpointTestControl.layer, ToolReconciler.uncertain, DurableRuntimeConfig.layer({
573
+ deploymentId: Schema.decodeSync(DeploymentId)("deployment-certification"),
574
+ producerId: Schema.decodeSync(ProducerId)("producer-certification"),
575
+ settlementPollInterval: Duration.millis(50),
576
+ leaseRenewalInterval: Duration.seconds(5),
577
+ abortPollInterval: Duration.millis(50)
578
+ }));
579
+ const leaseAdvance = Duration.millis(Duration.toMillis(options.ownershipLeaseDuration ?? DEFAULT_OWNERSHIP_LEASE_DURATION) + 1e3);
580
+ return Effect.gen(function* () {
581
+ const ledger = yield* SubmissionLedger;
582
+ const tier2 = [];
583
+ const discoveries = [];
584
+ for (const scenario of CERTIFICATION_SCENARIOS) {
585
+ const reached = /* @__PURE__ */ new Set();
586
+ const outcome = yield* runSweepCell(scenario, void 0, batchProducers, leaseAdvance, reached);
587
+ discoveries.push({
588
+ scenario,
589
+ outcome,
590
+ reached
591
+ });
592
+ }
593
+ const observed = new Set(discoveries.flatMap(({ reached }) => [...reached]));
594
+ const unaccounted = new Set(DurableRuntimeFailpointLocation.literals.filter((location) => !observed.has(location) && !TIER2_UNREACHED_LOCATIONS.includes(location)));
595
+ for (const discovery of discoveries) {
596
+ const { scenario, reached } = discovery;
597
+ const outcomes = /* @__PURE__ */ new Map();
598
+ if (discovery.outcome.status !== "failed") while (true) {
599
+ const location = DurableRuntimeFailpointLocation.literals.find((candidate) => !outcomes.has(candidate) && (reached.has(candidate) || unaccounted.has(candidate)));
600
+ if (location === void 0) break;
601
+ outcomes.set(location, yield* runSweepCell(scenario, location, batchProducers, leaseAdvance, reached));
602
+ }
603
+ for (const location of DurableRuntimeFailpointLocation.literals) {
604
+ const outcome = outcomes.get(location) ?? discovery.outcome;
605
+ tier2.push(CertificationSweepResult.make({
606
+ scenario,
607
+ location,
608
+ ...outcome,
609
+ ...outcomes.has(location) || outcome.status === "failed" ? {} : { detail: "verified clean scenario did not reach this location" }
610
+ }));
611
+ }
612
+ }
613
+ const tier1 = yield* certifyPorts();
614
+ const capabilities = yield* ledger.capabilities;
615
+ const tier3 = yield* resolveTierThree(capabilities.durability, options);
616
+ const generatedAt = yield* nowUtc;
617
+ return makeCertificationReport({
618
+ adapter: CertifiedAdapterIdentity.make({
619
+ name: options.adapter.name,
620
+ ...options.adapter.version === void 0 ? {} : { version: options.adapter.version },
621
+ durability: capabilities.durability
622
+ }),
623
+ generatedAt,
624
+ tier1,
625
+ tier2,
626
+ tier3
627
+ });
628
+ }).pipe(Effect.provide(environment));
629
+ };
630
+ //#endregion
631
+ export { CERTIFICATION_SCENARIOS, TIER2_UNREACHED_LOCATIONS, certifyDurableAdapters, resolveTierThree, tier2NeverFiredLocations };
632
+
633
+ //# sourceMappingURL=Certification.mjs.map