@effect-agent/testing 0.1.0-beta.42 → 0.1.0-beta.45
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 +8 -8
- package/dist/certification.mjs.map +1 -1
- package/dist/chaos.d.mts +1 -1
- package/dist/chaos.mjs +5 -3
- 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.d.mts +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.d.mts +2 -2
- package/dist/travel-planner.mjs +1 -1
- package/dist/travel-planner.mjs.map +1 -1
- package/package.json +1 -71
- package/src/certification.ts +110 -23
- package/src/chaos.ts +111 -3
- 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 +20 -1
- 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 +15 -2
- 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.42",
|
|
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.42",
|
|
32
|
-
"@effect-agent/core": "0.1.0-beta.42",
|
|
33
|
-
"@effect-agent/engine": "0.1.0-beta.42",
|
|
34
|
-
"@effect-agent/sandbox": "0.1.0-beta.42",
|
|
35
|
-
"@effect-agent/thread": "0.1.0-beta.42"
|
|
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.45","dependencies":{"@effect-agent/capabilities":"0.1.0-beta.45","@effect-agent/core":"0.1.0-beta.45","@effect-agent/engine":"0.1.0-beta.45","@effect-agent/sandbox":"0.1.0-beta.45","@effect-agent/thread":"0.1.0-beta.45"},"devDependencies":{"@effect-agent/platform-node":"0.1.0-beta.45","@effect-agent/storage-memory":"0.1.0-beta.45","@effect-agent/storage-sqlite":"0.1.0-beta.45","@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
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type AgentId,
|
|
16
16
|
type SubmissionId,
|
|
17
17
|
} from "@effect-agent/core";
|
|
18
|
-
import { DurableStep, DurableStepError } from "@effect-agent/engine";
|
|
18
|
+
import { DurableStep, DurableStepError, RunToolAuthorization } from "@effect-agent/engine";
|
|
19
19
|
import {
|
|
20
20
|
ApprovalDecisionCommand,
|
|
21
21
|
ThreadExportRequest,
|
|
@@ -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,31 +571,39 @@ 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}`);
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
577
|
+
|
|
578
|
+
const submitOne = Effect.fn("Certification.submitOne")(function* (
|
|
579
|
+
key: string,
|
|
580
|
+
question: string,
|
|
581
|
+
) {
|
|
582
|
+
const runtime = yield* DurableAgentRuntime;
|
|
583
|
+
|
|
584
|
+
return yield* runtime.submit(
|
|
585
|
+
{ definition: { id: plainDefinition.id, input: plainDefinition.input } },
|
|
586
|
+
{ question },
|
|
587
|
+
{
|
|
588
|
+
threadId,
|
|
589
|
+
principal: PRINCIPAL,
|
|
590
|
+
idempotencyKey: decodeIdempotencyKey(key),
|
|
591
|
+
definitions: DIGESTS,
|
|
592
|
+
},
|
|
593
|
+
);
|
|
594
|
+
});
|
|
595
|
+
|
|
557
596
|
const cell: CertificationCell = {
|
|
558
597
|
bindings: [resolved],
|
|
559
598
|
submit: Effect.gen(function* () {
|
|
560
599
|
const host = yield* submitOne(`certify-key-${slug}-host`, "host question");
|
|
561
600
|
const queued = yield* submitOne(`certify-key-${slug}-queued`, "queued question");
|
|
601
|
+
|
|
562
602
|
return [host, queued];
|
|
563
603
|
}),
|
|
564
604
|
lanes: () => [threadId],
|
|
565
605
|
};
|
|
606
|
+
|
|
566
607
|
return cell;
|
|
567
608
|
}
|
|
568
609
|
case "delegation": {
|
|
@@ -570,6 +611,7 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
570
611
|
childDefinition,
|
|
571
612
|
promptShapeModel("certify-child", '{"answer":"child-answer"}'),
|
|
572
613
|
);
|
|
614
|
+
|
|
573
615
|
const parentBinding = Agent.withModel(
|
|
574
616
|
coordinatorDefinition,
|
|
575
617
|
promptShapeModel(
|
|
@@ -581,36 +623,45 @@ const makeCell = Effect.fn("Certification.makeCell")(function* (
|
|
|
581
623
|
),
|
|
582
624
|
),
|
|
583
625
|
);
|
|
626
|
+
|
|
584
627
|
const delegationLayer = SubagentRuntime.layer(researchDelegation, childBinding, {
|
|
585
628
|
mapChildFailure,
|
|
586
629
|
durable: { targetDigests: CHILD_DIGEST_STRINGS },
|
|
587
630
|
}).pipe(Layer.provide(delegationSupport));
|
|
631
|
+
|
|
588
632
|
const lookupLayer = Toolkit.make(Lookup).toLayer({
|
|
589
633
|
lookup: ({ key }) => Effect.succeed({ value: `found-${key}` }),
|
|
590
634
|
});
|
|
635
|
+
|
|
591
636
|
const parentResolved = yield* DurableWorkerBinding.make(parentBinding, DIGESTS).pipe(
|
|
592
637
|
Effect.provide(Layer.mergeAll(delegationLayer, lookupLayer)),
|
|
593
638
|
);
|
|
639
|
+
|
|
594
640
|
const childResolved = yield* DurableWorkerBinding.make(childBinding, CHILD_DIGESTS);
|
|
595
641
|
const threadId = decodeThreadId(`certify-${slug}`);
|
|
642
|
+
|
|
596
643
|
const cell: CertificationCell = {
|
|
597
644
|
bindings: [parentResolved, childResolved],
|
|
598
645
|
submit: Effect.gen(function* () {
|
|
599
646
|
const runtime = yield* DurableAgentRuntime;
|
|
647
|
+
|
|
600
648
|
const receipt = yield* runtime.submit(
|
|
601
649
|
{ definition: { id: coordinatorDefinition.id, input: coordinatorDefinition.input } },
|
|
602
650
|
{ mission: "plan" },
|
|
603
651
|
submitOptionsFor(slug, threadId),
|
|
604
652
|
);
|
|
653
|
+
|
|
605
654
|
return [receipt];
|
|
606
655
|
}),
|
|
607
656
|
lanes: (receipts) => {
|
|
608
657
|
const parent = receipts.at(0);
|
|
658
|
+
|
|
609
659
|
return parent === undefined
|
|
610
660
|
? [threadId]
|
|
611
661
|
: [threadId, childThreadIdFor(parent.submissionId, DELEGATE_CALL)];
|
|
612
662
|
},
|
|
613
663
|
};
|
|
664
|
+
|
|
614
665
|
return cell;
|
|
615
666
|
}
|
|
616
667
|
}
|
|
@@ -634,12 +685,15 @@ const verifyLane = Effect.fn("Certification.verifyLane")(function* (
|
|
|
634
685
|
const exported = yield* store.export(ThreadExportRequest.make({ threadId: lane }));
|
|
635
686
|
const rows = new Map<SubmissionId, SubmissionSnapshot>();
|
|
636
687
|
const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
688
|
+
|
|
637
689
|
for (const submission of nonterminal) {
|
|
638
690
|
if (submission.threadId === lane) rows.set(submission.submissionId, submission);
|
|
639
691
|
}
|
|
640
692
|
const named = new Set<SubmissionId>();
|
|
693
|
+
|
|
641
694
|
for (const envelope of exported.records) {
|
|
642
695
|
const payload = envelope.record.payload;
|
|
696
|
+
|
|
643
697
|
if (
|
|
644
698
|
payload._tag === "UserInputRecorded" ||
|
|
645
699
|
payload._tag === "SubmissionSettled" ||
|
|
@@ -651,14 +705,17 @@ const verifyLane = Effect.fn("Certification.verifyLane")(function* (
|
|
|
651
705
|
for (const submissionId of named) {
|
|
652
706
|
if (rows.has(submissionId)) continue;
|
|
653
707
|
const found = yield* ledger.lookup(SubmissionLookupById.make({ submissionId }));
|
|
708
|
+
|
|
654
709
|
if (Option.isSome(found) && found.value.threadId === lane) {
|
|
655
710
|
rows.set(submissionId, found.value);
|
|
656
711
|
}
|
|
657
712
|
}
|
|
713
|
+
|
|
658
714
|
const checkpoint =
|
|
659
715
|
store.checkpoints === undefined
|
|
660
716
|
? Option.none()
|
|
661
717
|
: yield* store.checkpoints.load(LoadCheckpointRequest.make({ threadId: lane }));
|
|
718
|
+
|
|
662
719
|
return yield* verifyThreadInvariants({
|
|
663
720
|
export: exported,
|
|
664
721
|
submissions: [...rows.values()],
|
|
@@ -671,13 +728,17 @@ const verifyLane = Effect.fn("Certification.verifyLane")(function* (
|
|
|
671
728
|
|
|
672
729
|
const failureTagOf = <E>(cause: Cause.Cause<E>): string => {
|
|
673
730
|
const failure = Cause.findErrorOption(cause);
|
|
731
|
+
|
|
674
732
|
if (Option.isSome(failure)) {
|
|
675
733
|
const error: unknown = failure.value;
|
|
734
|
+
|
|
676
735
|
if (typeof error === "object" && error !== null && "_tag" in error) {
|
|
677
736
|
return String(error._tag);
|
|
678
737
|
}
|
|
738
|
+
|
|
679
739
|
return String(error).slice(0, 256);
|
|
680
740
|
}
|
|
741
|
+
|
|
681
742
|
return "defect";
|
|
682
743
|
};
|
|
683
744
|
|
|
@@ -688,7 +749,6 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
688
749
|
batchProducers: ReadonlyMap<BatchId, ProducerId>,
|
|
689
750
|
leaseAdvance: Duration.Duration,
|
|
690
751
|
) {
|
|
691
|
-
const runtime = yield* DurableAgentRuntime;
|
|
692
752
|
const ledger = yield* SubmissionLedger;
|
|
693
753
|
const control = yield* DurableRuntimeFailpointTestControl;
|
|
694
754
|
const slug = `${scenario}-${location.replaceAll(":", "-")}`;
|
|
@@ -705,9 +765,20 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
705
765
|
|
|
706
766
|
const cell = yield* makeCell(scenario, slug);
|
|
707
767
|
|
|
768
|
+
const runtime = yield* DurableAgentRuntime.pipe(
|
|
769
|
+
Effect.provide(
|
|
770
|
+
DurableAgentRuntime.layerWithBindings(cell.bindings).pipe(
|
|
771
|
+
Layer.provide(RunToolAuthorization.allowAll),
|
|
772
|
+
),
|
|
773
|
+
),
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
const submit = cell.submit.pipe(Effect.provideService(DurableAgentRuntime, runtime));
|
|
777
|
+
|
|
708
778
|
// One-shot arm: the fault fires at most once anywhere in the cell (initial drive OR a
|
|
709
779
|
// re-drive round's public unblocking operation), modelling one crash at this boundary.
|
|
710
780
|
const fired = yield* Ref.make(false);
|
|
781
|
+
|
|
711
782
|
yield* control.setHandler((hit) =>
|
|
712
783
|
hit !== location
|
|
713
784
|
? Effect.void
|
|
@@ -722,13 +793,16 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
722
793
|
|
|
723
794
|
// Submissions are idempotent (DUR-001): one replay recovers a submit-boundary fault.
|
|
724
795
|
let receipts: ReadonlyArray<Receipt>;
|
|
725
|
-
const firstSubmit = yield* Effect.exit(
|
|
796
|
+
const firstSubmit = yield* Effect.exit(submit);
|
|
797
|
+
|
|
726
798
|
if (Exit.isSuccess(firstSubmit)) {
|
|
727
799
|
receipts = firstSubmit.value;
|
|
728
800
|
} else {
|
|
729
|
-
const secondSubmit = yield* Effect.exit(
|
|
801
|
+
const secondSubmit = yield* Effect.exit(submit);
|
|
802
|
+
|
|
730
803
|
if (Exit.isFailure(secondSubmit)) {
|
|
731
804
|
yield* control.clear;
|
|
805
|
+
|
|
732
806
|
return failed(
|
|
733
807
|
`submission replay did not recover: ${failureTagOf(secondSubmit.cause)}`,
|
|
734
808
|
yield* Ref.get(fired),
|
|
@@ -738,21 +812,24 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
738
812
|
}
|
|
739
813
|
const lanes = cell.lanes(receipts);
|
|
740
814
|
|
|
741
|
-
const driveLane = (lane: ThreadId) => runtime.processThreadResolved(lane
|
|
815
|
+
const driveLane = (lane: ThreadId) => runtime.processThreadResolved(lane);
|
|
742
816
|
|
|
743
817
|
const allSettled = Effect.gen(function* () {
|
|
744
818
|
for (const receipt of receipts) {
|
|
745
819
|
const snapshot = yield* ledger.lookup(
|
|
746
820
|
SubmissionLookupById.make({ submissionId: receipt.submissionId }),
|
|
747
821
|
);
|
|
822
|
+
|
|
748
823
|
if (Option.isNone(snapshot) || snapshot.value.state !== "settled") return false;
|
|
749
824
|
}
|
|
825
|
+
|
|
750
826
|
return true;
|
|
751
827
|
});
|
|
752
828
|
|
|
753
829
|
// Re-drive to convergence using ONLY public operations: worker drives, recovery passes,
|
|
754
830
|
// and the authorized DUR-017/approval unblocking paths chosen from `explainThread`.
|
|
755
831
|
let converged = false;
|
|
832
|
+
|
|
756
833
|
for (let round = 0; round < MAX_REDRIVE_ROUNDS && !converged; round++) {
|
|
757
834
|
// Expire any lease a faulted Attempt left behind (D5): virtual time is the
|
|
758
835
|
// adapter-neutral reclaim lever — a live lease may block every new claim.
|
|
@@ -763,6 +840,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
763
840
|
}
|
|
764
841
|
for (const lane of lanes) {
|
|
765
842
|
const explains = yield* Effect.exit(runtime.explainThread(lane));
|
|
843
|
+
|
|
766
844
|
if (Exit.isFailure(explains)) continue;
|
|
767
845
|
for (const explanation of explains.value) {
|
|
768
846
|
for (const unknown of explanation.evidence.unknownCalls) {
|
|
@@ -783,6 +861,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
783
861
|
const decided = explanation.evidence.approvalDecisions.some(
|
|
784
862
|
(decision) => decision.toolCallId === pending.toolCallId,
|
|
785
863
|
);
|
|
864
|
+
|
|
786
865
|
if (decided) continue;
|
|
787
866
|
yield* Effect.exit(
|
|
788
867
|
runtime.resolveApproval(
|
|
@@ -799,6 +878,7 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
799
878
|
}
|
|
800
879
|
}
|
|
801
880
|
const settled = yield* Effect.exit(allSettled);
|
|
881
|
+
|
|
802
882
|
converged = Exit.isSuccess(settled) && settled.value;
|
|
803
883
|
}
|
|
804
884
|
yield* control.clear;
|
|
@@ -811,8 +891,10 @@ const runSweepCell = Effect.fn("Certification.runSweepCell")(function* (
|
|
|
811
891
|
// Every lane of the cell must verify in convergence mode with a recomputed digest chain.
|
|
812
892
|
let digestChainVerified = true;
|
|
813
893
|
const failedChecks: Array<string> = [];
|
|
894
|
+
|
|
814
895
|
for (const lane of lanes) {
|
|
815
896
|
const verdict = yield* Effect.exit(verifyLane(lane, batchProducers));
|
|
897
|
+
|
|
816
898
|
if (Exit.isFailure(verdict)) {
|
|
817
899
|
return failed(`lane ${lane} could not be verified: ${failureTagOf(verdict.cause)}`, wasFired);
|
|
818
900
|
}
|
|
@@ -873,6 +955,7 @@ export const resolveTierThree = Effect.fn("Certification.resolveTierThree")(func
|
|
|
873
955
|
}
|
|
874
956
|
if (options.crashLever !== undefined) {
|
|
875
957
|
const cases = yield* options.crashLever;
|
|
958
|
+
|
|
876
959
|
return CertificationTierThreeReport.make({
|
|
877
960
|
status: cases.length === 0 ? "not-exercised" : "exercised",
|
|
878
961
|
evidence: options.tierThreeEvidence ?? [],
|
|
@@ -889,6 +972,7 @@ export const resolveTierThree = Effect.fn("Certification.resolveTierThree")(func
|
|
|
889
972
|
cases: [],
|
|
890
973
|
});
|
|
891
974
|
}
|
|
975
|
+
|
|
892
976
|
return CertificationTierThreeReport.make({
|
|
893
977
|
status: "not-exercised",
|
|
894
978
|
evidence: [],
|
|
@@ -917,12 +1001,14 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
|
|
|
917
1001
|
options: CertifyDurableAdaptersOptions<LedgerE, StoreE>,
|
|
918
1002
|
): Effect.Effect<CertificationReport, LedgerE | StoreE, Crypto.Crypto> => {
|
|
919
1003
|
const batchProducers = new Map<BatchId, ProducerId>();
|
|
1004
|
+
|
|
920
1005
|
// Interpose the candidate store with an append-time capture of each batch's producer
|
|
921
1006
|
// identity — the one value the ThreadStore port deliberately does not export — so
|
|
922
1007
|
// Tier 2's invariant verification recomputes the FULL digest chain instead of skipping it.
|
|
923
1008
|
const capturingStore = Layer.effect(ThreadStore)(
|
|
924
1009
|
Effect.gen(function* () {
|
|
925
1010
|
const inner = yield* ThreadStore;
|
|
1011
|
+
|
|
926
1012
|
return ThreadStore.of({
|
|
927
1013
|
...inner,
|
|
928
1014
|
append: (request) =>
|
|
@@ -933,7 +1019,9 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
|
|
|
933
1019
|
}),
|
|
934
1020
|
).pipe(Layer.provide(options.threadStore));
|
|
935
1021
|
|
|
936
|
-
|
|
1022
|
+
// RUN-036: certification uses the default-none Tool failure observer. Trusted application
|
|
1023
|
+
// reporting adds no durable transition and is verified separately from adapter certification.
|
|
1024
|
+
const environment = Layer.mergeAll(
|
|
937
1025
|
options.submissionLedger,
|
|
938
1026
|
capturingStore,
|
|
939
1027
|
options.wakeScheduler ?? WakeScheduler.layerNoop,
|
|
@@ -947,9 +1035,6 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
|
|
|
947
1035
|
abortPollInterval: Duration.millis(50),
|
|
948
1036
|
}),
|
|
949
1037
|
);
|
|
950
|
-
// RUN-036: certification uses the default-none Tool failure observer. Trusted application
|
|
951
|
-
// reporting adds no durable transition and is verified separately from adapter certification.
|
|
952
|
-
const environment = DurableAgentRuntime.layer.pipe(Layer.provideMerge(support));
|
|
953
1038
|
|
|
954
1039
|
const leaseAdvance = Duration.millis(
|
|
955
1040
|
Duration.toMillis(options.ownershipLeaseDuration ?? DEFAULT_OWNERSHIP_LEASE_DURATION) + 1_000,
|
|
@@ -960,6 +1045,7 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
|
|
|
960
1045
|
|
|
961
1046
|
// Tier 2 — coordinator failpoint convergence sweep.
|
|
962
1047
|
const tier2: Array<CertificationSweepResult> = [];
|
|
1048
|
+
|
|
963
1049
|
for (const scenario of CERTIFICATION_SCENARIOS) {
|
|
964
1050
|
for (const location of DurableRuntimeFailpointLocation.literals) {
|
|
965
1051
|
tier2.push(yield* runSweepCell(scenario, location, batchProducers, leaseAdvance));
|
|
@@ -974,6 +1060,7 @@ export const certifyDurableAdapters = <LedgerE = never, StoreE = never>(
|
|
|
974
1060
|
const tier3 = yield* resolveTierThree(capabilities.durability, options);
|
|
975
1061
|
|
|
976
1062
|
const generatedAt = yield* nowUtc;
|
|
1063
|
+
|
|
977
1064
|
const ok =
|
|
978
1065
|
tier1.every((result) => result.status === "passed") &&
|
|
979
1066
|
tier2.every((result) => result.status !== "failed") &&
|