@effect-agent/testing 0.1.0-beta.41 → 0.1.0-beta.44
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.
- package/dist/certification.mjs.map +1 -1
- package/dist/chaos.mjs.map +1 -1
- package/dist/code-executor.mjs.map +1 -1
- package/dist/deterministic-layers-CKyYxBhN.mjs.map +1 -1
- package/dist/docs-researcher.mjs +3 -2
- package/dist/docs-researcher.mjs.map +1 -1
- package/dist/scripted-model-C2y0ztuj.mjs.map +1 -1
- package/dist/travel-planner.mjs.map +1 -1
- package/package.json +1 -71
- package/src/certification.ts +78 -0
- package/src/chaos.ts +96 -0
- package/src/code-executor-conformance.ts +19 -0
- package/src/code-executor-substitute.ts +32 -0
- package/src/fixtures/docs-researcher/definition.ts +7 -0
- package/src/fixtures/docs-researcher/harness.ts +19 -0
- package/src/fixtures/docs-researcher/mcp.ts +14 -2
- package/src/fixtures/travel-planner/definition.ts +12 -0
- package/src/fixtures/travel-planner/deterministic-layers.ts +38 -0
- package/src/fixtures/travel-planner/phase3.ts +4 -0
- package/src/fixtures/travel-planner/phase4.ts +10 -0
- package/src/fixtures/travel-planner/phase5.ts +20 -0
- package/src/fixtures/travel-planner/phase6.ts +12 -0
- package/src/fixtures/travel-planner/scenarios.ts +2 -0
- package/src/fixtures/travel-planner/subagents-durable.ts +13 -0
- package/src/fixtures/travel-planner/subagents.ts +21 -0
- package/src/scripted-model.ts +18 -0
package/package.json
CHANGED
|
@@ -1,71 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@effect-agent/testing",
|
|
3
|
-
"version": "0.1.0-beta.41",
|
|
4
|
-
"exports": {
|
|
5
|
-
".": {
|
|
6
|
-
"types": "./dist/index.d.mts",
|
|
7
|
-
"default": "./dist/index.mjs"
|
|
8
|
-
},
|
|
9
|
-
"./certification": {
|
|
10
|
-
"types": "./dist/certification.d.mts",
|
|
11
|
-
"default": "./dist/certification.mjs"
|
|
12
|
-
},
|
|
13
|
-
"./chaos": {
|
|
14
|
-
"types": "./dist/chaos.d.mts",
|
|
15
|
-
"default": "./dist/chaos.mjs"
|
|
16
|
-
},
|
|
17
|
-
"./code-executor": {
|
|
18
|
-
"types": "./dist/code-executor.d.mts",
|
|
19
|
-
"default": "./dist/code-executor.mjs"
|
|
20
|
-
},
|
|
21
|
-
"./fixtures/docs-researcher": {
|
|
22
|
-
"types": "./dist/docs-researcher.d.mts",
|
|
23
|
-
"default": "./dist/docs-researcher.mjs"
|
|
24
|
-
},
|
|
25
|
-
"./fixtures/travel-planner": {
|
|
26
|
-
"types": "./dist/travel-planner.d.mts",
|
|
27
|
-
"default": "./dist/travel-planner.mjs"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"@effect-agent/capabilities": "0.1.0-beta.41",
|
|
32
|
-
"@effect-agent/core": "0.1.0-beta.41",
|
|
33
|
-
"@effect-agent/engine": "0.1.0-beta.41",
|
|
34
|
-
"@effect-agent/sandbox": "0.1.0-beta.41",
|
|
35
|
-
"@effect-agent/thread": "0.1.0-beta.41"
|
|
36
|
-
},
|
|
37
|
-
"peerDependencies": {
|
|
38
|
-
"effect": "^4.0.0-rc.111"
|
|
39
|
-
},
|
|
40
|
-
"description": "Scripted models, deterministic fixtures, and adapter conformance kits for testing Effect Agent applications.",
|
|
41
|
-
"license": "MIT",
|
|
42
|
-
"repository": {
|
|
43
|
-
"type": "git",
|
|
44
|
-
"url": "git+https://github.com/danieljvdm/effect-agent.git",
|
|
45
|
-
"directory": "packages/testing"
|
|
46
|
-
},
|
|
47
|
-
"files": [
|
|
48
|
-
"dist",
|
|
49
|
-
"src"
|
|
50
|
-
],
|
|
51
|
-
"type": "module",
|
|
52
|
-
"publishConfig": {
|
|
53
|
-
"access": "public"
|
|
54
|
-
},
|
|
55
|
-
"scripts": {
|
|
56
|
-
"build": "vp pack",
|
|
57
|
-
"check": "tsc --noEmit -p tsconfig.json",
|
|
58
|
-
"test": "vp test --passWithNoTests"
|
|
59
|
-
},
|
|
60
|
-
"devDependencies": {
|
|
61
|
-
"@effect-agent/platform-node": "0.1.0-beta.40",
|
|
62
|
-
"@effect-agent/storage-memory": "0.1.0-beta.40",
|
|
63
|
-
"@effect-agent/storage-sqlite": "0.1.0-beta.40",
|
|
64
|
-
"@effect/platform-node": "4.0.0-rc.111",
|
|
65
|
-
"@effect/sql-sqlite-node": "4.0.0-rc.111",
|
|
66
|
-
"@effect/vitest": "4.0.0-rc.111",
|
|
67
|
-
"effect": "4.0.0-rc.111",
|
|
68
|
-
"typescript": "7.0.2",
|
|
69
|
-
"vite-plus": "0.3.0"
|
|
70
|
-
}
|
|
71
|
-
}
|
|
1
|
+
{"name":"@effect-agent/testing","version":"0.1.0-beta.44","dependencies":{"@effect-agent/capabilities":"0.1.0-beta.44","@effect-agent/core":"0.1.0-beta.44","@effect-agent/engine":"0.1.0-beta.44","@effect-agent/sandbox":"0.1.0-beta.44","@effect-agent/thread":"0.1.0-beta.44"},"devDependencies":{"@effect-agent/platform-node":"0.1.0-beta.44","@effect-agent/storage-memory":"0.1.0-beta.44","@effect-agent/storage-sqlite":"0.1.0-beta.44","@effect/platform-node":"4.0.0-rc.112","@effect/sql-sqlite-node":"4.0.0-rc.112","@effect/vitest":"4.0.0-rc.112","effect":"4.0.0-rc.112","typescript":"7.0.2","vite-plus":"0.3.0"},"peerDependencies":{"effect":"^4.0.0-rc.112"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./certification":{"types":"./dist/certification.d.mts","default":"./dist/certification.mjs"},"./chaos":{"types":"./dist/chaos.d.mts","default":"./dist/chaos.mjs"},"./code-executor":{"types":"./dist/code-executor.d.mts","default":"./dist/code-executor.mjs"},"./fixtures/docs-researcher":{"types":"./dist/docs-researcher.d.mts","default":"./dist/docs-researcher.mjs"},"./fixtures/travel-planner":{"types":"./dist/travel-planner.d.mts","default":"./dist/travel-planner.mjs"}},"description":"Scripted models, deterministic fixtures, and adapter conformance kits for testing Effect Agent applications.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/testing"},"files":["dist","src"],"type":"module","publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests"}}
|
package/src/certification.ts
CHANGED
|
@@ -184,9 +184,11 @@ export const tier2NeverFiredLocations = (
|
|
|
184
184
|
tier2: ReadonlyArray<CertificationSweepResult>,
|
|
185
185
|
): ReadonlyArray<DurableRuntimeFailpointLocation> => {
|
|
186
186
|
const fired = new Set<DurableRuntimeFailpointLocation>();
|
|
187
|
+
|
|
187
188
|
for (const row of tier2) {
|
|
188
189
|
if (row.failpointFired) fired.add(row.location);
|
|
189
190
|
}
|
|
191
|
+
|
|
190
192
|
return DurableRuntimeFailpointLocation.literals.filter((location) => !fired.has(location)).sort();
|
|
191
193
|
};
|
|
192
194
|
|
|
@@ -196,16 +198,19 @@ export const tier2NeverFiredLocations = (
|
|
|
196
198
|
|
|
197
199
|
const SHA_A = Schema.decodeSync(Digest)("a".repeat(64));
|
|
198
200
|
const DIGESTS = DefinitionDigests.make({ agent: SHA_A, model: SHA_A, tools: SHA_A });
|
|
201
|
+
|
|
199
202
|
const CHILD_DIGEST_STRINGS = {
|
|
200
203
|
agent: "b".repeat(64),
|
|
201
204
|
model: "c".repeat(64),
|
|
202
205
|
tools: "d".repeat(64),
|
|
203
206
|
} as const;
|
|
207
|
+
|
|
204
208
|
const CHILD_DIGESTS = DefinitionDigests.make({
|
|
205
209
|
agent: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.agent),
|
|
206
210
|
model: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.model),
|
|
207
211
|
tools: Schema.decodeSync(Digest)(CHILD_DIGEST_STRINGS.tools),
|
|
208
212
|
});
|
|
213
|
+
|
|
209
214
|
const PRINCIPAL = Schema.decodeSync(Principal)("principal-certification");
|
|
210
215
|
const decodeThreadId = Schema.decodeSync(ThreadId);
|
|
211
216
|
const decodeIdempotencyKey = Schema.decodeSync(IdempotencyKey);
|
|
@@ -257,8 +262,10 @@ const promptShapeModel = (
|
|
|
257
262
|
generateText: () => Effect.succeed([]),
|
|
258
263
|
streamText: (request) => {
|
|
259
264
|
const hasToolResult = request.prompt.content.some((message) => message.role === "tool");
|
|
265
|
+
|
|
260
266
|
const parts =
|
|
261
267
|
toolParts === undefined || hasToolResult ? finalParts(finalText) : toolParts;
|
|
268
|
+
|
|
262
269
|
return Stream.fromIterable(parts);
|
|
263
270
|
},
|
|
264
271
|
}),
|
|
@@ -295,7 +302,9 @@ const Book = Tool.make("book", {
|
|
|
295
302
|
parameters: Schema.Struct({ ref: Schema.String }),
|
|
296
303
|
success: Schema.Struct({ confirmation: Schema.String }),
|
|
297
304
|
});
|
|
305
|
+
|
|
298
306
|
const bookToolkit = Toolkit.make(Book);
|
|
307
|
+
|
|
299
308
|
const uncertainDefinition = Agent.make("certify-uncertain", {
|
|
300
309
|
input: QuestionInput,
|
|
301
310
|
output: AnswerOutput,
|
|
@@ -311,7 +320,9 @@ const Itinerary = Tool.make("itinerary", {
|
|
|
311
320
|
failure: DurableStepError,
|
|
312
321
|
dependencies: [DurableStep],
|
|
313
322
|
});
|
|
323
|
+
|
|
314
324
|
const itineraryToolkit = Toolkit.make(Itinerary);
|
|
325
|
+
|
|
315
326
|
const stepsDefinition = Agent.make("certify-steps", {
|
|
316
327
|
input: QuestionInput,
|
|
317
328
|
output: AnswerOutput,
|
|
@@ -326,7 +337,9 @@ const BookApproval = Tool.make("book", {
|
|
|
326
337
|
success: Schema.Struct({ confirmation: Schema.String }),
|
|
327
338
|
needsApproval: true,
|
|
328
339
|
});
|
|
340
|
+
|
|
329
341
|
const approvalToolkit = Toolkit.make(BookApproval);
|
|
342
|
+
|
|
330
343
|
const approvalDefinition = Agent.make("certify-approval", {
|
|
331
344
|
input: QuestionInput,
|
|
332
345
|
output: AnswerOutput,
|
|
@@ -399,10 +412,12 @@ const identifiers = Layer.effect(
|
|
|
399
412
|
IdGenerator,
|
|
400
413
|
Effect.gen(function* () {
|
|
401
414
|
const counter = yield* Ref.make(0);
|
|
415
|
+
|
|
402
416
|
const next = <A>(decode: (value: string) => A, prefix: string) =>
|
|
403
417
|
Ref.getAndUpdate(counter, (value) => value + 1).pipe(
|
|
404
418
|
Effect.map((value) => decode(`${prefix}-${value}`)),
|
|
405
419
|
);
|
|
420
|
+
|
|
406
421
|
return {
|
|
407
422
|
nextThreadId: next(decodeThreadId, "certify-fixture-thread"),
|
|
408
423
|
nextRunId: next(Schema.decodeSync(RunId), "certify-fixture-run"),
|
|
@@ -439,15 +454,19 @@ const makeSingleAgentCell = (
|
|
|
439
454
|
slug: string,
|
|
440
455
|
): CertificationCell => {
|
|
441
456
|
const threadId = decodeThreadId(`certify-${slug}`);
|
|
457
|
+
|
|
442
458
|
const submit = Effect.gen(function* () {
|
|
443
459
|
const runtime = yield* DurableAgentRuntime;
|
|
460
|
+
|
|
444
461
|
const receipt = yield* runtime.submit(
|
|
445
462
|
{ definition: { id: definition.id, input: definition.input } },
|
|
446
463
|
{ question: `certify ${slug}` },
|
|
447
464
|
submitOptionsFor(slug, threadId),
|
|
448
465
|
);
|
|
466
|
+
|
|
449
467
|
return [receipt];
|
|
450
468
|
});
|
|
469
|
+
|
|
451
470
|
return {
|
|
452
471
|
bindings: [resolved],
|
|
453
472
|
submit,
|
|
@@ -465,7 +484,9 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
465
484
|
plainDefinition,
|
|
466
485
|
promptShapeModel("certify-plain", '{"answer":"done"}'),
|
|
467
486
|
);
|
|
487
|
+
|
|
468
488
|
const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS);
|
|
489
|
+
|
|
469
490
|
return makeSingleAgentCell(plainDefinition, resolved, slug);
|
|
470
491
|
}
|
|
471
492
|
case "uncertain-tool": {
|
|
@@ -477,12 +498,15 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
477
498
|
toolTurn(toolCallPart("book-1", "book", { ref: `r-${slug}` })),
|
|
478
499
|
),
|
|
479
500
|
);
|
|
501
|
+
|
|
480
502
|
const toolLayer = bookToolkit.toLayer({
|
|
481
503
|
book: ({ ref }) => Effect.succeed({ confirmation: `confirmed-${ref}` }),
|
|
482
504
|
});
|
|
505
|
+
|
|
483
506
|
const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(
|
|
484
507
|
Effect.provide(toolLayer),
|
|
485
508
|
);
|
|
509
|
+
|
|
486
510
|
return makeSingleAgentCell(uncertainDefinition, resolved, slug);
|
|
487
511
|
}
|
|
488
512
|
case "durable-steps": {
|
|
@@ -494,26 +518,32 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
494
518
|
toolTurn(toolCallPart("itinerary-1", "itinerary", { ref: `trip-${slug}` })),
|
|
495
519
|
),
|
|
496
520
|
);
|
|
521
|
+
|
|
497
522
|
const toolLayer = itineraryToolkit.toLayer({
|
|
498
523
|
itinerary: ({ ref }) =>
|
|
499
524
|
Effect.gen(function* () {
|
|
500
525
|
const step = yield* DurableStep;
|
|
526
|
+
|
|
501
527
|
const flight = yield* step.do(
|
|
502
528
|
"reserve-flight",
|
|
503
529
|
Schema.String,
|
|
504
530
|
Effect.succeed(`flight-${ref}`),
|
|
505
531
|
);
|
|
532
|
+
|
|
506
533
|
const lodging = yield* step.do(
|
|
507
534
|
"reserve-lodging",
|
|
508
535
|
Schema.String,
|
|
509
536
|
Effect.succeed(`lodging-${ref}`),
|
|
510
537
|
);
|
|
538
|
+
|
|
511
539
|
return { state: `${flight}+${lodging}` };
|
|
512
540
|
}),
|
|
513
541
|
});
|
|
542
|
+
|
|
514
543
|
const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(
|
|
515
544
|
Effect.provide(toolLayer),
|
|
516
545
|
);
|
|
546
|
+
|
|
517
547
|
return makeSingleAgentCell(stepsDefinition, resolved, slug);
|
|
518
548
|
}
|
|
519
549
|
case "approval": {
|
|
@@ -525,12 +555,15 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
525
555
|
toolTurn(toolCallPart("book-1", "book", { ref: `r-${slug}` })),
|
|
526
556
|
),
|
|
527
557
|
);
|
|
558
|
+
|
|
528
559
|
const toolLayer = approvalToolkit.toLayer({
|
|
529
560
|
book: ({ ref }) => Effect.succeed({ confirmation: `confirmed-${ref}` }),
|
|
530
561
|
});
|
|
562
|
+
|
|
531
563
|
const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS).pipe(
|
|
532
564
|
Effect.provide(toolLayer),
|
|
533
565
|
);
|
|
566
|
+
|
|
534
567
|
return makeSingleAgentCell(approvalDefinition, resolved, slug);
|
|
535
568
|
}
|
|
536
569
|
case "join": {
|
|
@@ -538,11 +571,14 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
538
571
|
plainDefinition,
|
|
539
572
|
promptShapeModel("certify-join", '{"answer":"host answer"}'),
|
|
540
573
|
);
|
|
574
|
+
|
|
541
575
|
const resolved = yield* DurableWorkerBinding.make(binding, DIGESTS);
|
|
542
576
|
const threadId = decodeThreadId(`certify-${slug}`);
|
|
577
|
+
|
|
543
578
|
const submitOne = (key: string, question: string) =>
|
|
544
579
|
Effect.gen(function* () {
|
|
545
580
|
const runtime = yield* DurableAgentRuntime;
|
|
581
|
+
|
|
546
582
|
return yield* runtime.submit(
|
|
547
583
|
{ definition: { id: plainDefinition.id, input: plainDefinition.input } },
|
|
548
584
|
{ question },
|
|
@@ -554,15 +590,18 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
554
590
|
},
|
|
555
591
|
);
|
|
556
592
|
});
|
|
593
|
+
|
|
557
594
|
const cell: CertificationCell = {
|
|
558
595
|
bindings: [resolved],
|
|
559
596
|
submit: Effect.gen(function* () {
|
|
560
597
|
const host = yield* submitOne(`certify-key-${slug}-host`, "host question");
|
|
561
598
|
const queued = yield* submitOne(`certify-key-${slug}-queued`, "queued question");
|
|
599
|
+
|
|
562
600
|
return [host, queued];
|
|
563
601
|
}),
|
|
564
602
|
lanes: () => [threadId],
|
|
565
603
|
};
|
|
604
|
+
|
|
566
605
|
return cell;
|
|
567
606
|
}
|
|
568
607
|
case "delegation": {
|
|
@@ -570,6 +609,7 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
570
609
|
childDefinition,
|
|
571
610
|
promptShapeModel("certify-child", '{"answer":"child-answer"}'),
|
|
572
611
|
);
|
|
612
|
+
|
|
573
613
|
const parentBinding = Agent.withModel(
|
|
574
614
|
coordinatorDefinition,
|
|
575
615
|
promptShapeModel(
|
|
@@ -581,36 +621,45 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
581
621
|
),
|
|
582
622
|
),
|
|
583
623
|
);
|
|
624
|
+
|
|
584
625
|
const delegationLayer = SubagentRuntime.layer(researchDelegation, childBinding, {
|
|
585
626
|
mapChildFailure,
|
|
586
627
|
durable: { targetDigests: CHILD_DIGEST_STRINGS },
|
|
587
628
|
}).pipe(Layer.provide(delegationSupport));
|
|
629
|
+
|
|
588
630
|
const lookupLayer = Toolkit.make(Lookup).toLayer({
|
|
589
631
|
lookup: ({ key }) => Effect.succeed({ value: `found-${key}` }),
|
|
590
632
|
});
|
|
633
|
+
|
|
591
634
|
const parentResolved = yield* DurableWorkerBinding.make(parentBinding, DIGESTS).pipe(
|
|
592
635
|
Effect.provide(Layer.mergeAll(delegationLayer, lookupLayer)),
|
|
593
636
|
);
|
|
637
|
+
|
|
594
638
|
const childResolved = yield* DurableWorkerBinding.make(childBinding, CHILD_DIGESTS);
|
|
595
639
|
const threadId = decodeThreadId(`certify-${slug}`);
|
|
640
|
+
|
|
596
641
|
const cell: CertificationCell = {
|
|
597
642
|
bindings: [parentResolved, childResolved],
|
|
598
643
|
submit: Effect.gen(function* () {
|
|
599
644
|
const runtime = yield* DurableAgentRuntime;
|
|
645
|
+
|
|
600
646
|
const receipt = yield* runtime.submit(
|
|
601
647
|
{ definition: { id: coordinatorDefinition.id, input: coordinatorDefinition.input } },
|
|
602
648
|
{ mission: "plan" },
|
|
603
649
|
submitOptionsFor(slug, threadId),
|
|
604
650
|
);
|
|
651
|
+
|
|
605
652
|
return [receipt];
|
|
606
653
|
}),
|
|
607
654
|
lanes: (receipts) => {
|
|
608
655
|
const parent = receipts.at(0);
|
|
656
|
+
|
|
609
657
|
return parent === undefined
|
|
610
658
|
? [threadId]
|
|
611
659
|
: [threadId, childThreadIdFor(parent.submissionId, DELEGATE_CALL)];
|
|
612
660
|
},
|
|
613
661
|
};
|
|
662
|
+
|
|
614
663
|
return cell;
|
|
615
664
|
}
|
|
616
665
|
}
|
|
@@ -634,12 +683,15 @@ const verifyLane = Effect.fn("Certification.verifyLane")(function* (
|
|
|
634
683
|
const exported = yield* store.export(ThreadExportRequest.make({ threadId: lane }));
|
|
635
684
|
const rows = new Map<SubmissionId, SubmissionSnapshot>();
|
|
636
685
|
const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
686
|
+
|
|
637
687
|
for (const submission of nonterminal) {
|
|
638
688
|
if (submission.threadId === lane) rows.set(submission.submissionId, submission);
|
|
639
689
|
}
|
|
640
690
|
const named = new Set<SubmissionId>();
|
|
691
|
+
|
|
641
692
|
for (const envelope of exported.records) {
|
|
642
693
|
const payload = envelope.record.payload;
|
|
694
|
+
|
|
643
695
|
if (
|
|
644
696
|
payload._tag === "UserInputRecorded" ||
|
|
645
697
|
payload._tag === "SubmissionSettled" ||
|
|
@@ -651,14 +703,17 @@ const verifyLane = Effect.fn("Certification.verifyLane")(function* (
|
|
|
651
703
|
for (const submissionId of named) {
|
|
652
704
|
if (rows.has(submissionId)) continue;
|
|
653
705
|
const found = yield* ledger.lookup(SubmissionLookupById.make({ submissionId }));
|
|
706
|
+
|
|
654
707
|
if (Option.isSome(found) && found.value.threadId === lane) {
|
|
655
708
|
rows.set(submissionId, found.value);
|
|
656
709
|
}
|
|
657
710
|
}
|
|
711
|
+
|
|
658
712
|
const checkpoint =
|
|
659
713
|
store.checkpoints === undefined
|
|
660
714
|
? Option.none()
|
|
661
715
|
: yield* store.checkpoints.load(LoadCheckpointRequest.make({ threadId: lane }));
|
|
716
|
+
|
|
662
717
|
return yield* verifyThreadInvariants({
|
|
663
718
|
export: exported,
|
|
664
719
|
submissions: [...rows.values()],
|
|
@@ -671,13 +726,17 @@ const verifyLane = Effect.fn("Certification.verifyLane")(function* (
|
|
|
671
726
|
|
|
672
727
|
const failureTagOf = <E>(cause: Cause.Cause<E>): string => {
|
|
673
728
|
const failure = Cause.findErrorOption(cause);
|
|
729
|
+
|
|
674
730
|
if (Option.isSome(failure)) {
|
|
675
731
|
const error: unknown = failure.value;
|
|
732
|
+
|
|
676
733
|
if (typeof error === "object" && error !== null && "_tag" in error) {
|
|
677
734
|
return String(error._tag);
|
|
678
735
|
}
|
|
736
|
+
|
|
679
737
|
return String(error).slice(0, 256);
|
|
680
738
|
}
|
|
739
|
+
|
|
681
740
|
return "defect";
|
|
682
741
|
};
|
|
683
742
|
|
|
@@ -708,6 +767,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
708
767
|
// One-shot arm: the fault fires at most once anywhere in the cell (initial drive OR a
|
|
709
768
|
// re-drive round's public unblocking operation), modelling one crash at this boundary.
|
|
710
769
|
const fired = yield* Ref.make(false);
|
|
770
|
+
|
|
711
771
|
yield* control.setHandler((hit) =>
|
|
712
772
|
hit !== location
|
|
713
773
|
? Effect.void
|
|
@@ -723,12 +783,15 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
723
783
|
// Submissions are idempotent (DUR-001): one replay recovers a submit-boundary fault.
|
|
724
784
|
let receipts: ReadonlyArray<Receipt>;
|
|
725
785
|
const firstSubmit = yield* Effect.exit(cell.submit);
|
|
786
|
+
|
|
726
787
|
if (Exit.isSuccess(firstSubmit)) {
|
|
727
788
|
receipts = firstSubmit.value;
|
|
728
789
|
} else {
|
|
729
790
|
const secondSubmit = yield* Effect.exit(cell.submit);
|
|
791
|
+
|
|
730
792
|
if (Exit.isFailure(secondSubmit)) {
|
|
731
793
|
yield* control.clear;
|
|
794
|
+
|
|
732
795
|
return failed(
|
|
733
796
|
`submission replay did not recover: ${failureTagOf(secondSubmit.cause)}`,
|
|
734
797
|
yield* Ref.get(fired),
|
|
@@ -745,14 +808,17 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
745
808
|
const snapshot = yield* ledger.lookup(
|
|
746
809
|
SubmissionLookupById.make({ submissionId: receipt.submissionId }),
|
|
747
810
|
);
|
|
811
|
+
|
|
748
812
|
if (Option.isNone(snapshot) || snapshot.value.state !== "settled") return false;
|
|
749
813
|
}
|
|
814
|
+
|
|
750
815
|
return true;
|
|
751
816
|
});
|
|
752
817
|
|
|
753
818
|
// Re-drive to convergence using ONLY public operations: worker drives, recovery passes,
|
|
754
819
|
// and the authorized DUR-017/approval unblocking paths chosen from `explainThread`.
|
|
755
820
|
let converged = false;
|
|
821
|
+
|
|
756
822
|
for (let round = 0; round < MAX_REDRIVE_ROUNDS && !converged; round++) {
|
|
757
823
|
// Expire any lease a faulted Attempt left behind (D5): virtual time is the
|
|
758
824
|
// adapter-neutral reclaim lever — a live lease may block every new claim.
|
|
@@ -763,6 +829,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
763
829
|
}
|
|
764
830
|
for (const lane of lanes) {
|
|
765
831
|
const explains = yield* Effect.exit(runtime.explainThread(lane));
|
|
832
|
+
|
|
766
833
|
if (Exit.isFailure(explains)) continue;
|
|
767
834
|
for (const explanation of explains.value) {
|
|
768
835
|
for (const unknown of explanation.evidence.unknownCalls) {
|
|
@@ -783,6 +850,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
783
850
|
const decided = explanation.evidence.approvalDecisions.some(
|
|
784
851
|
(decision) => decision.toolCallId === pending.toolCallId,
|
|
785
852
|
);
|
|
853
|
+
|
|
786
854
|
if (decided) continue;
|
|
787
855
|
yield* Effect.exit(
|
|
788
856
|
runtime.resolveApproval(
|
|
@@ -799,6 +867,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
799
867
|
}
|
|
800
868
|
}
|
|
801
869
|
const settled = yield* Effect.exit(allSettled);
|
|
870
|
+
|
|
802
871
|
converged = Exit.isSuccess(settled) && settled.value;
|
|
803
872
|
}
|
|
804
873
|
yield* control.clear;
|
|
@@ -811,8 +880,10 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
811
880
|
// Every lane of the cell must verify in convergence mode with a recomputed digest chain.
|
|
812
881
|
let digestChainVerified = true;
|
|
813
882
|
const failedChecks: Array<string> = [];
|
|
883
|
+
|
|
814
884
|
for (const lane of lanes) {
|
|
815
885
|
const verdict = yield* Effect.exit(verifyLane(lane, batchProducers));
|
|
886
|
+
|
|
816
887
|
if (Exit.isFailure(verdict)) {
|
|
817
888
|
return failed(`lane ${lane} could not be verified: ${failureTagOf(verdict.cause)}`, wasFired);
|
|
818
889
|
}
|
|
@@ -873,6 +944,7 @@ export const resolveTierThree = Effect.fn("Certification.resolveTierThree")(func
|
|
|
873
944
|
}
|
|
874
945
|
if (options.crashLever !== undefined) {
|
|
875
946
|
const cases = yield* options.crashLever;
|
|
947
|
+
|
|
876
948
|
return CertificationTierThreeReport.make({
|
|
877
949
|
status: cases.length === 0 ? "not-exercised" : "exercised",
|
|
878
950
|
evidence: options.tierThreeEvidence ?? [],
|
|
@@ -889,6 +961,7 @@ export const resolveTierThree = Effect.fn("Certification.resolveTierThree")(func
|
|
|
889
961
|
cases: [],
|
|
890
962
|
});
|
|
891
963
|
}
|
|
964
|
+
|
|
892
965
|
return CertificationTierThreeReport.make({
|
|
893
966
|
status: "not-exercised",
|
|
894
967
|
evidence: [],
|
|
@@ -917,12 +990,14 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
|
|
|
917
990
|
options: CertifyDurableAdaptersOptions<LedgerE, StoreE>,
|
|
918
991
|
): Effect.Effect<CertificationReport, LedgerE | StoreE, Crypto.Crypto> => {
|
|
919
992
|
const batchProducers = new Map<BatchId, ProducerId>();
|
|
993
|
+
|
|
920
994
|
// Interpose the candidate store with an append-time capture of each batch's producer
|
|
921
995
|
// identity — the one value the ThreadStore port deliberately does not export — so
|
|
922
996
|
// Tier 2's invariant verification recomputes the FULL digest chain instead of skipping it.
|
|
923
997
|
const capturingStore = Layer.effect(ThreadStore)(
|
|
924
998
|
Effect.gen(function* () {
|
|
925
999
|
const inner = yield* ThreadStore;
|
|
1000
|
+
|
|
926
1001
|
return ThreadStore.of({
|
|
927
1002
|
...inner,
|
|
928
1003
|
append: (request) =>
|
|
@@ -947,6 +1022,7 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
|
|
|
947
1022
|
abortPollInterval: Duration.millis(50),
|
|
948
1023
|
}),
|
|
949
1024
|
);
|
|
1025
|
+
|
|
950
1026
|
// RUN-036: certification uses the default-none Tool failure observer. Trusted application
|
|
951
1027
|
// reporting adds no durable transition and is verified separately from adapter certification.
|
|
952
1028
|
const environment = DurableAgentRuntime.layer.pipe(Layer.provideMerge(support));
|
|
@@ -960,6 +1036,7 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
|
|
|
960
1036
|
|
|
961
1037
|
// Tier 2 — coordinator failpoint convergence sweep.
|
|
962
1038
|
const tier2: Array<CertificationSweepResult> = [];
|
|
1039
|
+
|
|
963
1040
|
for (const scenario of CERTIFICATION_SCENARIOS) {
|
|
964
1041
|
for (const location of DurableRuntimeFailpointLocation.literals) {
|
|
965
1042
|
tier2.push(yield* runSweepCell(scenario, location, batchProducers, leaseAdvance));
|
|
@@ -974,6 +1051,7 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
|
|
|
974
1051
|
const tier3 = yield* resolveTierThree(capabilities.durability, options);
|
|
975
1052
|
|
|
976
1053
|
const generatedAt = yield* nowUtc;
|
|
1054
|
+
|
|
977
1055
|
const ok =
|
|
978
1056
|
tier1.every((result) => result.status === "passed") &&
|
|
979
1057
|
tier2.every((result) => result.status !== "failed") &&
|