@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.
- package/dist/Certification.d.mts +105 -0
- package/dist/Certification.mjs +592 -0
- package/dist/Certification.mjs.map +1 -0
- package/dist/Chaos.d.mts +126 -0
- package/dist/Chaos.mjs +755 -0
- package/dist/Chaos.mjs.map +1 -0
- package/dist/CodeExecutorConformance.d.mts +33 -0
- package/dist/CodeExecutorConformance.mjs +231 -0
- package/dist/CodeExecutorConformance.mjs.map +1 -0
- package/dist/CodeExecutorSubstitute.d.mts +21 -0
- package/dist/CodeExecutorSubstitute.mjs +368 -0
- package/dist/CodeExecutorSubstitute.mjs.map +1 -0
- package/dist/DocsResearcher.d.mts +270 -0
- package/dist/DocsResearcher.mjs +490 -0
- package/dist/DocsResearcher.mjs.map +1 -0
- package/dist/ScriptedModel-DAvxIiud.d.mts +220 -0
- package/dist/ScriptedModel.d.mts +2 -0
- package/dist/ScriptedModel.mjs +155 -0
- package/dist/ScriptedModel.mjs.map +1 -0
- package/dist/TravelPlanner.d.mts +1665 -0
- package/dist/TravelPlanner.mjs +1963 -0
- package/dist/TravelPlanner.mjs.map +1 -0
- package/dist/deterministic-layers-Eka0fMZq.mjs +358 -0
- package/dist/deterministic-layers-Eka0fMZq.mjs.map +1 -0
- package/dist/index.d.mts +2 -3406
- package/dist/index.mjs +2 -4771
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -48
- package/src/{certification.ts → Certification.ts} +251 -117
- package/src/{chaos.ts → Chaos.ts} +246 -122
- package/src/{code-executor-conformance.ts → CodeExecutorConformance.ts} +29 -6
- package/src/{code-executor-substitute.ts → CodeExecutorSubstitute.ts} +112 -45
- package/src/{fixtures/docs-researcher/index.ts → DocsResearcher.ts} +68 -5
- package/src/{scripted-model.ts → ScriptedModel.ts} +22 -25
- package/src/TravelPlanner.ts +232 -0
- package/src/fixtures/docs-researcher/definition.ts +19 -10
- package/src/fixtures/docs-researcher/harness.ts +41 -30
- package/src/fixtures/docs-researcher/mcp.ts +49 -3
- package/src/fixtures/travel-planner/definition.ts +15 -2
- package/src/fixtures/travel-planner/deterministic-layers.ts +47 -4
- package/src/fixtures/travel-planner/phase2.ts +4 -3
- package/src/fixtures/travel-planner/phase3.ts +18 -41
- package/src/fixtures/travel-planner/phase4.ts +23 -36
- package/src/fixtures/travel-planner/phase5.ts +38 -41
- package/src/fixtures/travel-planner/phase6.ts +187 -84
- package/src/fixtures/travel-planner/phase7.ts +4 -102
- package/src/fixtures/travel-planner/scenarios.ts +3 -4
- package/src/fixtures/travel-planner/subagents-durable.ts +33 -57
- package/src/fixtures/travel-planner/subagents.ts +35 -13
- package/src/index.ts +1 -11
- package/dist/index.mjs.map +0 -1
- package/src/code-executor-conformance.d.ts +0 -30
- package/src/fixtures/travel-planner/index.ts +0 -11
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
21
|
+
type DurableSubmitFailure,
|
|
22
|
+
type DurableSubmitOptions,
|
|
23
|
+
type Receipt,
|
|
24
|
+
} from "@effect-agent/thread/DurableAgentRuntime";
|
|
25
|
+
import {
|
|
35
26
|
DurableRuntimeFailpointError,
|
|
36
27
|
DurableRuntimeFailpointLocation,
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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<
|
|
467
|
+
readonly lanes: (receipts: ReadonlyArray<Receipt>) => ReadonlyArray<ThreadId>;
|
|
416
468
|
}
|
|
417
469
|
|
|
418
|
-
const submitOptionsFor = (slug: string,
|
|
419
|
-
|
|
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
|
|
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,
|
|
491
|
+
submitOptionsFor(slug, threadId),
|
|
438
492
|
);
|
|
493
|
+
|
|
439
494
|
return [receipt];
|
|
440
495
|
});
|
|
496
|
+
|
|
441
497
|
return {
|
|
442
|
-
|
|
498
|
+
bindings: [resolved],
|
|
443
499
|
submit,
|
|
444
|
-
lanes: () => [
|
|
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
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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: () => [
|
|
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
|
|
668
|
+
const threadId = decodeThreadId(`certify-${slug}`);
|
|
669
|
+
|
|
586
670
|
const cell: CertificationCell = {
|
|
587
|
-
|
|
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,
|
|
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
|
-
? [
|
|
601
|
-
: [
|
|
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:
|
|
707
|
+
lane: ThreadId,
|
|
620
708
|
batchProducers: ReadonlyMap<BatchId, ProducerId>,
|
|
621
709
|
) {
|
|
622
|
-
const store = yield*
|
|
710
|
+
const store = yield* ThreadStore;
|
|
623
711
|
const ledger = yield* SubmissionLedger;
|
|
624
|
-
const exported = yield* store.export(
|
|
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.
|
|
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
|
-
|
|
735
|
+
|
|
736
|
+
if (Option.isSome(found) && found.value.threadId === lane) {
|
|
645
737
|
rows.set(submissionId, found.value);
|
|
646
738
|
}
|
|
647
739
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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 `
|
|
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.
|
|
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
|
|
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(
|
|
1035
|
+
const capturingStore = Layer.effect(ThreadStore)(
|
|
912
1036
|
Effect.gen(function* () {
|
|
913
|
-
const inner = yield*
|
|
914
|
-
|
|
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.
|
|
1047
|
+
).pipe(Layer.provide(options.threadStore));
|
|
923
1048
|
|
|
924
|
-
|
|
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
|
-
|
|
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@
|
|
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
|
|