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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/Certification.d.mts +105 -0
  2. package/dist/Certification.mjs +592 -0
  3. package/dist/Certification.mjs.map +1 -0
  4. package/dist/Chaos.d.mts +126 -0
  5. package/dist/Chaos.mjs +755 -0
  6. package/dist/Chaos.mjs.map +1 -0
  7. package/dist/CodeExecutorConformance.d.mts +33 -0
  8. package/dist/CodeExecutorConformance.mjs +231 -0
  9. package/dist/CodeExecutorConformance.mjs.map +1 -0
  10. package/dist/CodeExecutorSubstitute.d.mts +21 -0
  11. package/dist/CodeExecutorSubstitute.mjs +368 -0
  12. package/dist/CodeExecutorSubstitute.mjs.map +1 -0
  13. package/dist/DocsResearcher.d.mts +270 -0
  14. package/dist/DocsResearcher.mjs +490 -0
  15. package/dist/DocsResearcher.mjs.map +1 -0
  16. package/dist/ScriptedModel-DAvxIiud.d.mts +220 -0
  17. package/dist/ScriptedModel.d.mts +2 -0
  18. package/dist/ScriptedModel.mjs +155 -0
  19. package/dist/ScriptedModel.mjs.map +1 -0
  20. package/dist/TravelPlanner.d.mts +1665 -0
  21. package/dist/TravelPlanner.mjs +1963 -0
  22. package/dist/TravelPlanner.mjs.map +1 -0
  23. package/dist/deterministic-layers-Eka0fMZq.mjs +358 -0
  24. package/dist/deterministic-layers-Eka0fMZq.mjs.map +1 -0
  25. package/dist/index.d.mts +2 -3406
  26. package/dist/index.mjs +2 -4771
  27. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  28. package/package.json +1 -48
  29. package/src/{certification.ts → Certification.ts} +251 -117
  30. package/src/{chaos.ts → Chaos.ts} +246 -122
  31. package/src/{code-executor-conformance.ts → CodeExecutorConformance.ts} +29 -6
  32. package/src/{code-executor-substitute.ts → CodeExecutorSubstitute.ts} +112 -45
  33. package/src/{fixtures/docs-researcher/index.ts → DocsResearcher.ts} +68 -5
  34. package/src/{scripted-model.ts → ScriptedModel.ts} +22 -25
  35. package/src/TravelPlanner.ts +232 -0
  36. package/src/fixtures/docs-researcher/definition.ts +19 -10
  37. package/src/fixtures/docs-researcher/harness.ts +41 -30
  38. package/src/fixtures/docs-researcher/mcp.ts +49 -3
  39. package/src/fixtures/travel-planner/definition.ts +15 -2
  40. package/src/fixtures/travel-planner/deterministic-layers.ts +47 -4
  41. package/src/fixtures/travel-planner/phase2.ts +4 -3
  42. package/src/fixtures/travel-planner/phase3.ts +18 -41
  43. package/src/fixtures/travel-planner/phase4.ts +23 -36
  44. package/src/fixtures/travel-planner/phase5.ts +38 -41
  45. package/src/fixtures/travel-planner/phase6.ts +187 -84
  46. package/src/fixtures/travel-planner/phase7.ts +4 -102
  47. package/src/fixtures/travel-planner/scenarios.ts +3 -4
  48. package/src/fixtures/travel-planner/subagents-durable.ts +33 -57
  49. package/src/fixtures/travel-planner/subagents.ts +35 -13
  50. package/src/index.ts +1 -11
  51. package/dist/index.mjs.map +0 -1
  52. package/src/code-executor-conformance.d.ts +0 -30
  53. package/src/fixtures/travel-planner/index.ts +0 -11
  54. package/src/fixtures/warehouse/index.ts +0 -412
package/dist/Chaos.mjs ADDED
@@ -0,0 +1,755 @@
1
+ import { Cause, Effect, Exit, Layer, Option, Ref, Schema, Stream } from "effect";
2
+ import { LanguageModel, Model, Tool, Toolkit } from "effect/unstable/ai";
3
+ import * as Subagent from "@effect-agent/capabilities/Subagent";
4
+ import { SubagentPolicy, SubagentRuntime } from "@effect-agent/capabilities/Subagent";
5
+ import { SubagentReservationsMemoryLive } from "@effect-agent/capabilities/SubagentReservations";
6
+ import * as Agent from "@effect-agent/core/Agent";
7
+ import { AgentPolicy } from "@effect-agent/core/AgentPolicy";
8
+ import { RunId, ThreadId, ToolCallId, TurnId } from "@effect-agent/core/Identifiers";
9
+ import { IdGenerator } from "@effect-agent/core/IdGenerator";
10
+ import { DurableStep, DurableStepError, ToolExecutionClass } from "@effect-agent/engine/DurableStep";
11
+ import { RunToolAuthorization } from "@effect-agent/engine/RunOptions";
12
+ import { DurableWorkerBinding } from "@effect-agent/thread/AgentRegistration";
13
+ import { DurableAgentRuntime, DurableRuntimeConfig } from "@effect-agent/thread/DurableAgentRuntime";
14
+ import { DurableRuntimeFailpointError, DurableRuntimeFailpointLocation } from "@effect-agent/thread/DurableFailpoint";
15
+ import { DefinitionDigests, Digest } from "@effect-agent/thread/Records";
16
+ import { childThreadIdFor } from "@effect-agent/thread/RunJournal";
17
+ import { AbortCommand, ApprovalDecisionCommand, IdempotencyKey, Principal, ResolutionAbortSubmission, ResolutionCompletedWithResult, ResolutionNeverHappened, SubmissionLedger, SubmissionLookupById, UnknownResolutionCommand } from "@effect-agent/thread/SubmissionLedger";
18
+ import { DurableRuntimeFailpointTestControl } from "@effect-agent/thread/testing/DurableFailpointTestControl";
19
+ import { verifyThreadInvariants } from "@effect-agent/thread/ThreadInvariants";
20
+ import { ThreadExportRequest, ThreadStore } from "@effect-agent/thread/ThreadStore";
21
+ import { FastCheck } from "effect/testing";
22
+ import { ObligationThresholds } from "@effect-agent/thread/Admin";
23
+ //#region src/Chaos.ts
24
+ /**
25
+ * P7 WP4 chaos machinery (plan §5): a Schema-first `ChaosPlan`, a seeded generator over
26
+ * `effect/testing/FastCheck` (already inside the pinned Effect — no new dependency), and a
27
+ * deterministic runner that drives the durable coordinator over whatever adapter pair the test
28
+ * provides. Every plan ends in the SAME claims the crash matrices make:
29
+ *
30
+ * 1. `verifyThreadInvariants` in convergence mode over every touched Thread (the
31
+ * shared WP1 checker — one set of claims for admin verify, certification, chaos, and soak);
32
+ * 2. `scanObligations` returning ZERO entries (everything settled; nothing invisibly stuck);
33
+ * 3. supplier non-fabrication wherever the deterministic desk was in play (durability §10: no
34
+ * canonical Tool success exists that the external store did not actually produce).
35
+ *
36
+ * Replay contract: the memory/SQLite chaos tests derive every plan from one root seed
37
+ * (`CHAOS_SEED` env override; see `chaosSeedFromEnv`) and print that seed plus the failing
38
+ * plan's own seed in the failure output, so any red run is replayable byte-for-byte.
39
+ */
40
+ /** The six durable scenario flavors a chaos lane can exercise (plan §5). */
41
+ const ChaosScenarioKind = Schema.Literals([
42
+ "plain",
43
+ "uncertain-tool",
44
+ "durable-steps",
45
+ "approval",
46
+ "join",
47
+ "delegation"
48
+ ]);
49
+ const LaneIndex = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(7));
50
+ /** One Submission of a plan: which lane it queues into and that lane's scenario flavor. */
51
+ var ChaosSubmissionSpec = class extends Schema.Class("@effect-agent/testing/ChaosSubmissionSpec")({
52
+ lane: LaneIndex,
53
+ /** The lane's flavor; the FIRST spec of a lane fixes the lane's agent. */
54
+ kind: ChaosScenarioKind
55
+ }) {};
56
+ /** How the runner resolves a durable Unknown Outcome it encounters (DUR-017 driver). */
57
+ const ChaosResolutionKind = Schema.Literals([
58
+ "never-happened",
59
+ "completed-from-supplier",
60
+ "abort-submission"
61
+ ]);
62
+ const ChaosApprovalDecision = Schema.Literals(["approved", "denied"]);
63
+ const BoundedAdapterArm = Schema.String.check(Schema.isMaxLength(128));
64
+ /**
65
+ * One seeded chaos plan (plan §5): the full fault schedule is data, so a failing run replays
66
+ * from the plan alone. `failpointArms` are coordinator locations; `adapterArms` are
67
+ * adapter-owned location names the adapter test validates (the memory runner has none).
68
+ */
69
+ var ChaosPlan = class extends Schema.Class("@effect-agent/testing/ChaosPlan")({
70
+ /** Identifies this plan in failure output; derived from the root seed plus the plan index. */
71
+ seed: Schema.Int,
72
+ /** Lane count; submissions address lanes `0..lanes-1`. */
73
+ lanes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(8)),
74
+ submissions: Schema.NonEmptyArray(ChaosSubmissionSpec),
75
+ /** Coordinator failpoint arms, consumed one per round (each fails every hit that round). */
76
+ failpointArms: Schema.Array(DurableRuntimeFailpointLocation),
77
+ /** Adapter-owned failpoint arms (e.g. SQLite `ledger:*`/`append:*` locations). */
78
+ adapterArms: Schema.Array(BoundedAdapterArm),
79
+ /** Flattened submission indices to abort mid-plan (modulo the submission count). */
80
+ abortInjections: Schema.Array(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
81
+ /** Resolution choices for Unknown Outcomes, indexed deterministically per open call. */
82
+ resolutionInjections: Schema.Array(ChaosResolutionKind),
83
+ /** Approval decisions for suspended approval lanes, indexed deterministically per call. */
84
+ approvalDecisions: Schema.Array(ChaosApprovalDecision)
85
+ }) {};
86
+ /** Per-lane verification result inside a plan report. */
87
+ var ChaosLaneReport = class extends Schema.Class("@effect-agent/testing/ChaosLaneReport")({
88
+ threadId: ThreadId,
89
+ kind: ChaosScenarioKind,
90
+ submissionCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
91
+ /** Verdict of `verifyThreadInvariants` in convergence mode. */
92
+ verified: Schema.Boolean
93
+ }) {};
94
+ /** The Schema-first outcome of one executed chaos plan. */
95
+ var ChaosPlanReport = class extends Schema.Class("@effect-agent/testing/ChaosPlanReport")({
96
+ seed: Schema.Int,
97
+ rounds: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
98
+ lanes: Schema.Array(ChaosLaneReport),
99
+ /** `scanObligations` entries after convergence — MUST be zero. */
100
+ openObligations: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
101
+ }) {};
102
+ /** Typed convergence/verification failure of one chaos plan (never a bare defect). */
103
+ var ChaosConvergenceFailure = class extends Schema.TaggedError()("ChaosConvergenceFailure", {
104
+ seed: Schema.Int,
105
+ message: Schema.String.check(Schema.isMaxLength(16384))
106
+ }) {};
107
+ /** Default root seed for chaos suites; override with the `CHAOS_SEED` environment variable. */
108
+ const DEFAULT_CHAOS_SEED = 20260813;
109
+ const ChaosSeedFromEnvironment = Schema.FiniteFromString.check(Schema.isInt());
110
+ const decodeChaosSeedFromEnvironment = Schema.decodeUnknownOption(ChaosSeedFromEnvironment);
111
+ /** The root seed for this run: `CHAOS_SEED` when set to an integer, the default otherwise. */
112
+ const chaosSeedFromEnv = (env) => {
113
+ const raw = env["CHAOS_SEED"];
114
+ if (raw === void 0 || raw === "") return DEFAULT_CHAOS_SEED;
115
+ return Option.getOrElse(decodeChaosSeedFromEnvironment(raw), () => DEFAULT_CHAOS_SEED);
116
+ };
117
+ const laneArbitrary = FastCheck.constantFrom("plain", "uncertain-tool", "durable-steps", "approval", "join", "delegation").chain((kind) => kind === "join" ? FastCheck.integer({
118
+ min: 2,
119
+ max: 3
120
+ }).map((depth) => ({
121
+ kind,
122
+ depth
123
+ })) : kind === "plain" ? FastCheck.integer({
124
+ min: 1,
125
+ max: 2
126
+ }).map((depth) => ({
127
+ kind,
128
+ depth
129
+ })) : FastCheck.constant({
130
+ kind,
131
+ depth: 1
132
+ }));
133
+ const planShapeArbitrary = (adapterArms) => FastCheck.record({
134
+ lanes: FastCheck.array(laneArbitrary, {
135
+ minLength: 1,
136
+ maxLength: 3
137
+ }),
138
+ failpointArms: FastCheck.uniqueArray(FastCheck.constantFrom(...DurableRuntimeFailpointLocation.literals), { maxLength: 3 }),
139
+ adapterArms: adapterArms.length === 0 ? FastCheck.constant([]) : FastCheck.uniqueArray(FastCheck.constantFrom(...adapterArms), { maxLength: 2 }),
140
+ abortInjections: FastCheck.uniqueArray(FastCheck.integer({
141
+ min: 0,
142
+ max: 15
143
+ }), { maxLength: 2 }),
144
+ resolutionInjections: FastCheck.array(FastCheck.constantFrom("never-happened", "completed-from-supplier", "abort-submission"), { maxLength: 4 }),
145
+ approvalDecisions: FastCheck.array(FastCheck.constantFrom("approved", "denied"), { maxLength: 2 })
146
+ }).map((shape) => {
147
+ const [first, ...rest] = shape.lanes.flatMap((lane, index) => Array.from({ length: lane.depth }, () => ChaosSubmissionSpec.make({
148
+ lane: index,
149
+ kind: lane.kind
150
+ })));
151
+ if (first === void 0) throw new Error("chaos generator produced an empty plan");
152
+ return {
153
+ lanes: shape.lanes.length,
154
+ submissions: [first, ...rest],
155
+ failpointArms: shape.failpointArms,
156
+ adapterArms: shape.adapterArms,
157
+ abortInjections: shape.abortInjections,
158
+ resolutionInjections: shape.resolutionInjections,
159
+ approvalDecisions: shape.approvalDecisions
160
+ };
161
+ });
162
+ /**
163
+ * Derive `count` chaos plans deterministically from one root seed. The same
164
+ * `{seed, count, adapterArms}` triple always yields byte-identical plans, so a failure line
165
+ * `CHAOS_SEED=<seed>` replays the exact schedule.
166
+ */
167
+ const generateChaosPlans = (options) => {
168
+ return FastCheck.sample(planShapeArbitrary(options.adapterArms ?? []), {
169
+ seed: options.seed,
170
+ numRuns: options.count
171
+ }).map((shape, index) => ChaosPlan.make({
172
+ ...shape,
173
+ seed: Math.imul(options.seed, 31) + index | 0
174
+ }));
175
+ };
176
+ /** Deterministic PRNG for the runner's small ordering choices (lane drive order). */
177
+ const mulberry32 = (seed) => {
178
+ let state = seed | 0;
179
+ return () => {
180
+ state = state + 1831565813 | 0;
181
+ let t = Math.imul(state ^ state >>> 15, 1 | state);
182
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
183
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
184
+ };
185
+ };
186
+ const usage = {
187
+ inputTokens: {},
188
+ outputTokens: {}
189
+ };
190
+ const finalParts = (text) => [
191
+ {
192
+ type: "text-start",
193
+ id: "answer"
194
+ },
195
+ {
196
+ type: "text-delta",
197
+ id: "answer",
198
+ delta: text
199
+ },
200
+ {
201
+ type: "text-end",
202
+ id: "answer"
203
+ },
204
+ {
205
+ type: "finish",
206
+ reason: "stop",
207
+ usage
208
+ }
209
+ ];
210
+ const toolTurn = (...calls) => [...calls, {
211
+ type: "finish",
212
+ reason: "tool-calls",
213
+ usage
214
+ }];
215
+ const toolCallPart = (id, name, params) => ({
216
+ type: "tool-call",
217
+ id,
218
+ name,
219
+ params,
220
+ providerExecuted: false
221
+ });
222
+ /**
223
+ * Prompt-shaped scripted model: the response depends ONLY on the request prompt, so it stays
224
+ * deterministic across Attempt re-invocations, batch resumes, and joined steering — no counter
225
+ * to drift when chaos re-enters a Turn.
226
+ */
227
+ const promptScriptedModel = (label, script) => Model.make("scripted", label, Layer.effect(LanguageModel.LanguageModel, LanguageModel.make({
228
+ generateText: () => Effect.succeed([]),
229
+ streamText: (request) => Stream.fromIterable(script(request.prompt))
230
+ })));
231
+ const lastRole = (prompt) => prompt.content.at(-1)?.role;
232
+ const policy = AgentPolicy.make({
233
+ maxTurns: 3,
234
+ maxToolCalls: 4,
235
+ maxDuration: "30 seconds",
236
+ toolConcurrency: 2
237
+ });
238
+ const PlainInput = Schema.Struct({ question: Schema.String });
239
+ const PlainOutput = Schema.Struct({ answer: Schema.String });
240
+ const plainDefinition = Agent.make("chaos-plain", {
241
+ input: PlainInput,
242
+ output: PlainOutput,
243
+ instructions: "Answer as JSON.",
244
+ toolkit: Toolkit.empty,
245
+ policy
246
+ });
247
+ /** Unannotated → fail-closed `uncertain`: enters the prepared/settled protocol (DUR-009). */
248
+ const BookUncertain = Tool.make("book", {
249
+ parameters: Schema.Struct({ ref: Schema.String }),
250
+ success: Schema.Struct({ confirmation: Schema.String })
251
+ });
252
+ const bookTools = Toolkit.make(BookUncertain);
253
+ const bookDefinition = Agent.make("chaos-book", {
254
+ input: PlainInput,
255
+ output: PlainOutput,
256
+ instructions: "Book it.",
257
+ toolkit: bookTools,
258
+ policy
259
+ });
260
+ const BookApproval = Tool.make("book", {
261
+ parameters: Schema.Struct({ ref: Schema.String }),
262
+ success: Schema.Struct({ confirmation: Schema.String }),
263
+ needsApproval: true
264
+ });
265
+ const approvalTools = Toolkit.make(BookApproval);
266
+ const approvalDefinition = Agent.make("chaos-approval", {
267
+ input: PlainInput,
268
+ output: PlainOutput,
269
+ instructions: "Book after approval.",
270
+ toolkit: approvalTools,
271
+ policy
272
+ });
273
+ const Itinerary = Tool.make("itinerary", {
274
+ parameters: Schema.Struct({ ref: Schema.String }),
275
+ success: Schema.Struct({ state: Schema.String }),
276
+ failure: DurableStepError,
277
+ dependencies: [DurableStep]
278
+ }).annotate(ToolExecutionClass, "uncertain");
279
+ const itineraryTools = Toolkit.make(Itinerary);
280
+ const itineraryDefinition = Agent.make("chaos-itinerary", {
281
+ input: PlainInput,
282
+ output: PlainOutput,
283
+ instructions: "Reserve the itinerary.",
284
+ toolkit: itineraryTools,
285
+ policy
286
+ });
287
+ const childDefinition = Agent.make("chaos-child", {
288
+ input: PlainInput,
289
+ output: PlainOutput,
290
+ instructions: "Answer as JSON.",
291
+ toolkit: Toolkit.empty,
292
+ policy: AgentPolicy.make({
293
+ maxTurns: 2,
294
+ maxToolCalls: 1,
295
+ maxDuration: "30 seconds",
296
+ toolConcurrency: 1
297
+ })
298
+ });
299
+ var ChaosDelegationFailed = class extends Schema.TaggedError()("ChaosDelegationFailed", { childErrorTag: Schema.String }) {};
300
+ const chaosDelegation = Subagent.define("delegate_chaos", {
301
+ description: "Delegate one bounded chaos question.",
302
+ target: childDefinition,
303
+ parameters: Schema.Struct({ topic: Schema.String }),
304
+ success: Schema.Struct({ summary: Schema.String }),
305
+ failure: ChaosDelegationFailed,
306
+ prepareInput: ({ topic }) => Effect.succeed({ question: `chaos:${topic}` }),
307
+ projectResult: (output) => Effect.succeed({ summary: `finding:${output.answer}` }),
308
+ policy: SubagentPolicy.make({
309
+ maxChildren: 2,
310
+ maxConcurrency: 2,
311
+ maxTurns: 4,
312
+ maxToolCalls: 4,
313
+ maxDuration: "30 seconds"
314
+ })
315
+ });
316
+ const coordinatorDefinition = Agent.make("chaos-coordinator", {
317
+ input: Schema.Struct({ mission: Schema.String }),
318
+ output: Schema.Struct({ report: Schema.String }),
319
+ instructions: "Delegate, then report as JSON.",
320
+ toolkit: Toolkit.make(chaosDelegation.tool),
321
+ policy
322
+ });
323
+ const DELEGATE_CALL_ID = "chaos-delegate-1";
324
+ const HEX = "0123456789abcdef";
325
+ const decodeDigest = Schema.decodeSync(Digest);
326
+ const laneDigests = (lane) => {
327
+ const digest = decodeDigest(HEX.charAt(lane % 8).repeat(64));
328
+ return DefinitionDigests.make({
329
+ agent: digest,
330
+ model: digest,
331
+ tools: digest
332
+ });
333
+ };
334
+ const childDigestStrings = (lane) => {
335
+ const char = HEX.charAt(8 + lane % 8);
336
+ return {
337
+ agent: char.repeat(64),
338
+ model: char.repeat(64),
339
+ tools: char.repeat(64)
340
+ };
341
+ };
342
+ const childLaneDigests = (lane) => {
343
+ const strings = childDigestStrings(lane);
344
+ return DefinitionDigests.make({
345
+ agent: decodeDigest(strings.agent),
346
+ model: decodeDigest(strings.model),
347
+ tools: decodeDigest(strings.tools)
348
+ });
349
+ };
350
+ const CHAOS_PRINCIPAL = Schema.decodeSync(Principal)("principal-chaos");
351
+ const decodeThreadId = Schema.decodeSync(ThreadId);
352
+ const decodeIdempotencyKey = Schema.decodeSync(IdempotencyKey);
353
+ const decodeToolCallId = Schema.decodeSync(ToolCallId);
354
+ const decodeRunId = Schema.decodeSync(RunId);
355
+ const decodeTurnId = Schema.decodeSync(TurnId);
356
+ /** Fixture-only identity source consumed by the delegation Layer's ephemeral capture. */
357
+ const chaosIdentifiers = Layer.effect(IdGenerator, Effect.gen(function* () {
358
+ const counter = yield* Ref.make(0);
359
+ const next = (decode, prefix) => Ref.getAndUpdate(counter, (value) => value + 1).pipe(Effect.map((value) => decode(`${prefix}-${value}`)));
360
+ return {
361
+ nextThreadId: next(decodeThreadId, "chaos-fixture-thread"),
362
+ nextRunId: next(decodeRunId, "chaos-fixture-run"),
363
+ nextTurnId: next(decodeTurnId, "chaos-fixture-turn")
364
+ };
365
+ }));
366
+ const delegationSupport = Layer.mergeAll(SubagentReservationsMemoryLive, chaosIdentifiers);
367
+ const makeChaosDesk = Effect.gen(function* () {
368
+ const produced = yield* Ref.make(/* @__PURE__ */ new Set());
369
+ return {
370
+ produced: Ref.get(produced),
371
+ record: (value) => Ref.update(produced, (current) => new Set(current).add(value))
372
+ };
373
+ });
374
+ const bookConfirmation = (ref) => `confirmed-${ref}`;
375
+ const flightValue = (ref) => `flight-${ref}`;
376
+ const lodgingValue = (ref) => `lodging-${ref}`;
377
+ /** Tolerate typed failures while preserving every defect and interruption reason. */
378
+ const tolerateTyped = (effect) => effect.pipe(Effect.exit, Effect.flatMap((exit) => {
379
+ if (Exit.isSuccess(exit)) return Effect.succeed(Option.some(exit.value));
380
+ const unexpected = exit.cause.reasons.filter((reason) => reason._tag !== "Fail");
381
+ return unexpected.length === 0 ? Effect.succeed(Option.none()) : Effect.failCause(Cause.fromReasons(unexpected));
382
+ }));
383
+ const scriptFor = (kind, ref) => {
384
+ switch (kind) {
385
+ case "plain":
386
+ case "join": return () => finalParts("{\"answer\":\"chaos\"}");
387
+ case "uncertain-tool":
388
+ case "approval": return (prompt) => lastRole(prompt) === "tool" ? finalParts("{\"answer\":\"booked\"}") : toolTurn(toolCallPart(`book-${ref}`, "book", { ref }));
389
+ case "durable-steps": return (prompt) => lastRole(prompt) === "tool" ? finalParts("{\"answer\":\"reserved\"}") : toolTurn(toolCallPart(`itinerary-${ref}`, "itinerary", { ref }));
390
+ case "delegation": return (prompt) => lastRole(prompt) === "tool" ? finalParts("{\"report\":\"done\"}") : toolTurn(toolCallPart(DELEGATE_CALL_ID, "delegate_chaos", { topic: ref }));
391
+ }
392
+ };
393
+ const makeLaneFixture = Effect.fn("Chaos.makeLaneFixture")(function* (plan, laneIndex, kind, submissionIndexes, desk) {
394
+ const runtime = yield* DurableAgentRuntime;
395
+ const threadId = decodeThreadId(`chaos-${plan.seed}-lane-${laneIndex}`);
396
+ const ref = `ref-l${laneIndex}`;
397
+ const script = scriptFor(kind, ref);
398
+ const model = promptScriptedModel(`chaos-${kind}-${laneIndex}`, script);
399
+ const digests = laneDigests(laneIndex);
400
+ const submitOptionsFor = (flatIndex) => ({
401
+ threadId,
402
+ principal: CHAOS_PRINCIPAL,
403
+ idempotencyKey: decodeIdempotencyKey(`chaos-${plan.seed}-s${flatIndex}`),
404
+ definitions: digests
405
+ });
406
+ const bookToolLayerFor = (tools) => tools.toLayer({ book: ({ ref: called }) => desk.record(bookConfirmation(called)).pipe(Effect.as({ confirmation: bookConfirmation(called) })) });
407
+ const plainLaneFixture = (deskInPlay, drive, submitOne) => ({
408
+ index: laneIndex,
409
+ kind,
410
+ threadId,
411
+ ref,
412
+ deskInPlay,
413
+ submissionIndexes,
414
+ submitOne,
415
+ drives: () => [drive],
416
+ childThreadOf: () => void 0
417
+ });
418
+ switch (kind) {
419
+ case "plain":
420
+ case "join": {
421
+ const agent = Agent.withModel(plainDefinition, model);
422
+ return plainLaneFixture(false, runtime.processThread(agent, threadId), (flatIndex) => runtime.submit(agent, { question: `chaos ${flatIndex}` }, submitOptionsFor(flatIndex)));
423
+ }
424
+ case "uncertain-tool": {
425
+ const agent = Agent.withModel(bookDefinition, model);
426
+ return plainLaneFixture(true, runtime.processThread(agent, threadId).pipe(Effect.provide(bookToolLayerFor(bookTools))), (flatIndex) => runtime.submit(agent, { question: `chaos ${flatIndex}` }, submitOptionsFor(flatIndex)));
427
+ }
428
+ case "approval": {
429
+ const agent = Agent.withModel(approvalDefinition, model);
430
+ return plainLaneFixture(true, runtime.processThread(agent, threadId).pipe(Effect.provide(bookToolLayerFor(approvalTools))), (flatIndex) => runtime.submit(agent, { question: `chaos ${flatIndex}` }, submitOptionsFor(flatIndex)));
431
+ }
432
+ case "durable-steps": {
433
+ const agent = Agent.withModel(itineraryDefinition, model);
434
+ const toolLayer = itineraryTools.toLayer({ itinerary: ({ ref: called }) => Effect.gen(function* () {
435
+ const step = yield* DurableStep;
436
+ return { state: `${yield* step.do("reserve-flight", Schema.String, desk.record(flightValue(called)).pipe(Effect.as(flightValue(called))))}+${yield* step.do("reserve-lodging", Schema.String, desk.record(lodgingValue(called)).pipe(Effect.as(lodgingValue(called))))}` };
437
+ }) });
438
+ return plainLaneFixture(true, runtime.processThread(agent, threadId).pipe(Effect.provide(toolLayer)), (flatIndex) => runtime.submit(agent, { question: `chaos ${flatIndex}` }, submitOptionsFor(flatIndex)));
439
+ }
440
+ case "delegation": {
441
+ const parentBinding = Agent.withModel(coordinatorDefinition, model);
442
+ const childModel = promptScriptedModel(`chaos-child-${laneIndex}`, () => finalParts("{\"answer\":\"child\"}"));
443
+ const childBinding = Agent.withModel(childDefinition, childModel);
444
+ const delegationLayer = SubagentRuntime.layer(chaosDelegation, childBinding, {
445
+ mapChildFailure: (failure) => ChaosDelegationFailed.make({ childErrorTag: failure._tag }),
446
+ durable: { targetDigests: childDigestStrings(laneIndex) }
447
+ }).pipe(Layer.provide(delegationSupport));
448
+ const parentResolved = yield* DurableWorkerBinding.make(parentBinding, digests).pipe(Effect.provide(delegationLayer));
449
+ const childResolved = yield* DurableWorkerBinding.make(childBinding, childLaneDigests(laneIndex));
450
+ const registeredRuntime = yield* DurableAgentRuntime.pipe(Effect.provide(DurableAgentRuntime.layerWithBindings([parentResolved, childResolved]).pipe(Layer.provide(RunToolAuthorization.allowAll))));
451
+ const driveResolved = (thread) => registeredRuntime.processThreadResolved(thread);
452
+ return {
453
+ index: laneIndex,
454
+ kind,
455
+ threadId,
456
+ ref,
457
+ deskInPlay: false,
458
+ submissionIndexes,
459
+ submitOne: (flatIndex) => runtime.submit({ definition: {
460
+ id: coordinatorDefinition.id,
461
+ input: coordinatorDefinition.input
462
+ } }, { mission: `chaos ${flatIndex}` }, submitOptionsFor(flatIndex)),
463
+ drives: (firstReceipt) => {
464
+ const drives = [driveResolved(threadId)];
465
+ if (firstReceipt !== void 0) drives.push(driveResolved(childThreadIdFor(firstReceipt.submissionId, decodeToolCallId(DELEGATE_CALL_ID))));
466
+ return drives;
467
+ },
468
+ childThreadOf: (firstReceipt) => childThreadIdFor(firstReceipt.submissionId, decodeToolCallId(DELEGATE_CALL_ID))
469
+ };
470
+ }
471
+ }
472
+ });
473
+ /** Stable per-call index into an injection list (identical across resolution passes). */
474
+ const injectionIndex = (submissionFlatIndex, callId, length) => {
475
+ let hash = submissionFlatIndex + 1;
476
+ for (const char of callId) hash = Math.imul(hash, 31) + char.charCodeAt(0) | 0;
477
+ return (hash % length + length) % length;
478
+ };
479
+ const resolutionFor = (kind, toolName, ref, produced) => {
480
+ switch (kind) {
481
+ case "abort-submission": return ResolutionAbortSubmission.make();
482
+ case "completed-from-supplier":
483
+ if (toolName === "book" && produced.has(bookConfirmation(ref))) return ResolutionCompletedWithResult.make({
484
+ result: { confirmation: bookConfirmation(ref) },
485
+ isFailure: false
486
+ });
487
+ if (toolName === "itinerary" && produced.has(flightValue(ref)) && produced.has(lodgingValue(ref))) return ResolutionCompletedWithResult.make({
488
+ result: { state: `${flightValue(ref)}+${lodgingValue(ref)}` },
489
+ isFailure: false
490
+ });
491
+ return ResolutionNeverHappened.make();
492
+ case "never-happened": return ResolutionNeverHappened.make();
493
+ }
494
+ };
495
+ /** Drive one DUR-017 pass: resolve Unknown Outcomes and pending approvals from the plan. */
496
+ const resolutionPass = Effect.fn("Chaos.resolutionPass")(function* (plan, states, desk) {
497
+ const runtime = yield* DurableAgentRuntime;
498
+ const ledger = yield* SubmissionLedger;
499
+ const produced = yield* desk.produced;
500
+ const nonterminal = yield* tolerateTyped(Stream.runCollect(ledger.scanNonterminal));
501
+ if (Option.isNone(nonterminal)) return;
502
+ const byId = /* @__PURE__ */ new Map();
503
+ for (const state of states) if (state.receipt !== void 0) byId.set(state.receipt.submissionId, state);
504
+ for (const row of nonterminal.value) {
505
+ if (row.state !== "unknown" && row.state !== "suspended") continue;
506
+ const state = byId.get(row.submissionId);
507
+ const explanation = yield* tolerateTyped(runtime.explain(row.submissionId));
508
+ if (Option.isNone(explanation)) continue;
509
+ const flatIndex = state?.flatIndex ?? 0;
510
+ const ref = state?.lane.ref ?? "ref-child";
511
+ if (row.state === "unknown") for (const call of explanation.value.evidence.unknownCalls) {
512
+ if (call.resolved) continue;
513
+ const kind = plan.resolutionInjections.length === 0 ? "never-happened" : plan.resolutionInjections.at(injectionIndex(flatIndex, call.toolCallId, plan.resolutionInjections.length)) ?? "never-happened";
514
+ yield* tolerateTyped(runtime.resolveUnknown(UnknownResolutionCommand.make({
515
+ submissionId: row.submissionId,
516
+ toolCallId: call.toolCallId,
517
+ author: "chaos-runner",
518
+ reason: `chaos plan ${plan.seed} resolution (${kind})`,
519
+ resolution: resolutionFor(kind, call.toolName, ref, produced)
520
+ })));
521
+ }
522
+ else for (const pending of explanation.value.evidence.approvalsPending) {
523
+ const decision = plan.approvalDecisions.length === 0 ? "approved" : plan.approvalDecisions.at(injectionIndex(flatIndex, pending.toolCallId, plan.approvalDecisions.length)) ?? "approved";
524
+ yield* tolerateTyped(runtime.resolveApproval(ApprovalDecisionCommand.make({
525
+ submissionId: row.submissionId,
526
+ toolCallId: pending.toolCallId,
527
+ decision,
528
+ resolver: "chaos-runner",
529
+ reason: `chaos plan ${plan.seed} approval (${decision})`
530
+ })));
531
+ }
532
+ }
533
+ });
534
+ const submissionIdsNamedBy = (records) => {
535
+ const named = /* @__PURE__ */ new Set();
536
+ for (const envelope of records) {
537
+ const payload = envelope.record.payload;
538
+ if (payload._tag === "UserInputRecorded" || payload._tag === "SubmissionSettled" || payload._tag === "AbortRequested") {
539
+ if (payload.submissionId !== void 0) named.add(payload.submissionId);
540
+ }
541
+ }
542
+ return named;
543
+ };
544
+ /**
545
+ * The final non-fabrication sweep (durability §10): every canonical Tool success recorded on a
546
+ * desk-backed lane must be a value the desk actually produced.
547
+ */
548
+ const BookResult = Schema.Struct({ confirmation: Schema.String });
549
+ const ItineraryResult = Schema.Struct({ state: Schema.String });
550
+ const decodeBookResult = Schema.decodeUnknownOption(BookResult);
551
+ const decodeItineraryResult = Schema.decodeUnknownOption(ItineraryResult);
552
+ const decodeStepOutput = Schema.decodeUnknownOption(Schema.String);
553
+ const assertNoFabrication = (plan, records, produced) => {
554
+ const fabricated = [];
555
+ const requireProduced = (value, label) => {
556
+ if (!produced.has(value)) fabricated.push(`${label} "${value}"`);
557
+ };
558
+ for (const envelope of records) {
559
+ const payload = envelope.record.payload;
560
+ if (payload._tag === "ToolCallSettled" && !payload.isFailure) {
561
+ if (payload.toolName === "book") {
562
+ const result = decodeBookResult(payload.result);
563
+ if (Option.isSome(result)) requireProduced(result.value.confirmation, "book result");
564
+ }
565
+ if (payload.toolName === "itinerary") {
566
+ const result = decodeItineraryResult(payload.result);
567
+ if (Option.isSome(result)) for (const part of result.value.state.split("+")) requireProduced(part, "itinerary step result");
568
+ }
569
+ }
570
+ if (payload._tag === "ToolStepSettled") {
571
+ const output = decodeStepOutput(payload.output);
572
+ if (Option.isSome(output)) requireProduced(output.value, "step output");
573
+ }
574
+ }
575
+ return fabricated.length === 0 ? Effect.void : Effect.fail(ChaosConvergenceFailure.make({
576
+ seed: plan.seed,
577
+ message: `fabricated Tool results absent from the desk: ${fabricated.join(", ")}`
578
+ }));
579
+ };
580
+ /**
581
+ * Execute one chaos plan against whatever adapters the ambient Layer provides and end in the
582
+ * shared invariant claims. Deterministic: same plan + same adapters → same schedule.
583
+ */
584
+ const runChaosPlan = Effect.fn("Chaos.runChaosPlan")(function* (plan, options) {
585
+ const runtime = yield* DurableAgentRuntime;
586
+ const ledger = yield* SubmissionLedger;
587
+ const store = yield* ThreadStore;
588
+ const config = yield* DurableRuntimeConfig;
589
+ const failpoints = yield* DurableRuntimeFailpointTestControl;
590
+ const random = mulberry32(plan.seed);
591
+ const desk = yield* makeChaosDesk;
592
+ const laneKinds = /* @__PURE__ */ new Map();
593
+ const laneSubmissions = /* @__PURE__ */ new Map();
594
+ plan.submissions.forEach((spec, flatIndex) => {
595
+ const lane = spec.lane % plan.lanes;
596
+ if (!laneKinds.has(lane)) laneKinds.set(lane, spec.kind);
597
+ const list = laneSubmissions.get(lane) ?? [];
598
+ list.push(flatIndex);
599
+ laneSubmissions.set(lane, list);
600
+ });
601
+ const lanes = [];
602
+ for (const [lane, kind] of laneKinds) lanes.push(yield* makeLaneFixture(plan, lane, kind, laneSubmissions.get(lane) ?? [], desk));
603
+ const lanesByIndex = new Map(lanes.map((lane) => [lane.index, lane]));
604
+ const states = [];
605
+ for (const [flatIndex, spec] of plan.submissions.entries()) {
606
+ const laneIndex = spec.lane % plan.lanes;
607
+ const lane = lanesByIndex.get(laneIndex);
608
+ if (lane === void 0) return yield* ChaosConvergenceFailure.make({
609
+ seed: plan.seed,
610
+ message: `submission ${flatIndex} addresses missing lane ${laneIndex}`
611
+ });
612
+ states.push({
613
+ flatIndex,
614
+ lane,
615
+ receipt: void 0
616
+ });
617
+ }
618
+ const appliedAborts = /* @__PURE__ */ new Set();
619
+ const armQueue = [...plan.failpointArms.map((location) => ({
620
+ family: "coordinator",
621
+ location
622
+ })), ...plan.adapterArms.map((location) => ({
623
+ family: "adapter",
624
+ location
625
+ }))];
626
+ const allSettled = Effect.gen(function* () {
627
+ if (states.some((state) => state.receipt === void 0)) return false;
628
+ const nonterminal = yield* tolerateTyped(Stream.runCollect(ledger.scanNonterminal));
629
+ return Option.isSome(nonterminal) && Array.from(nonterminal.value).length === 0;
630
+ });
631
+ const maxRounds = armQueue.length + states.length * 2 + 12;
632
+ let rounds = 0;
633
+ let converged = false;
634
+ for (let round = 0; round < maxRounds; round++) {
635
+ rounds = round + 1;
636
+ const arm = armQueue[round];
637
+ if (arm?.family === "coordinator") {
638
+ const location = arm.location;
639
+ yield* failpoints.setHandler((hit) => hit === location ? Effect.fail(DurableRuntimeFailpointError.make({ location: hit })) : Effect.void);
640
+ } else if (arm?.family === "adapter" && options?.adapterFailpoints !== void 0) yield* options.adapterFailpoints.arm(arm.location);
641
+ for (const state of states) {
642
+ if (state.receipt !== void 0) continue;
643
+ const receipt = yield* tolerateTyped(state.lane.submitOne(state.flatIndex));
644
+ if (Option.isSome(receipt)) state.receipt = receipt.value;
645
+ }
646
+ const order = lanes.map((lane) => ({
647
+ lane,
648
+ rank: random()
649
+ })).sort((left, right) => left.rank - right.rank || left.lane.index - right.lane.index).map(({ lane }) => lane);
650
+ for (const lane of order) {
651
+ const firstFlat = lane.submissionIndexes[0];
652
+ const firstReceipt = firstFlat === void 0 ? void 0 : states[firstFlat]?.receipt;
653
+ for (const drive of lane.drives(firstReceipt)) yield* tolerateTyped(drive);
654
+ }
655
+ if (round >= 1) for (const rawIndex of plan.abortInjections) {
656
+ const index = rawIndex % states.length;
657
+ if (appliedAborts.has(index)) continue;
658
+ const receipt = states[index]?.receipt;
659
+ if (receipt === void 0) continue;
660
+ appliedAborts.add(index);
661
+ yield* tolerateTyped(runtime.abort(AbortCommand.make({
662
+ submissionId: receipt.submissionId,
663
+ author: "chaos-runner",
664
+ reason: `chaos plan ${plan.seed} abort injection`
665
+ })));
666
+ }
667
+ yield* resolutionPass(plan, states, desk);
668
+ yield* failpoints.clear;
669
+ if (options?.adapterFailpoints !== void 0) yield* options.adapterFailpoints.clear;
670
+ yield* tolerateTyped(runtime.runRecovery);
671
+ yield* resolutionPass(plan, states, desk);
672
+ if (yield* allSettled) {
673
+ converged = true;
674
+ break;
675
+ }
676
+ if (options?.betweenRounds !== void 0) yield* options.betweenRounds;
677
+ }
678
+ if (!converged) {
679
+ const nonterminal = yield* tolerateTyped(Stream.runCollect(ledger.scanNonterminal));
680
+ const detail = Option.isSome(nonterminal) ? Array.from(nonterminal.value).map((row) => `${row.submissionId}(${row.state})`).join(", ") : "ledger scan failed";
681
+ return yield* ChaosConvergenceFailure.make({
682
+ seed: plan.seed,
683
+ message: `plan did not converge within ${maxRounds} rounds; nonterminal: [${detail}]; pending receipts: ${states.filter((state) => state.receipt === void 0).length}`
684
+ });
685
+ }
686
+ const produced = yield* desk.produced;
687
+ const laneReports = [];
688
+ const verifyThread = Effect.fn("Chaos.verifyThread")(function* (threadId, kind, deskInPlay) {
689
+ const exported = yield* store.export(ThreadExportRequest.make({ threadId })).pipe(Effect.mapError((error) => ChaosConvergenceFailure.make({
690
+ seed: plan.seed,
691
+ message: `export of ${threadId} failed: ${String(error)}`
692
+ })));
693
+ const rows = [];
694
+ for (const submissionId of submissionIdsNamedBy(exported.records)) {
695
+ const found = yield* ledger.lookup(SubmissionLookupById.make({ submissionId })).pipe(Effect.mapError((error) => ChaosConvergenceFailure.make({
696
+ seed: plan.seed,
697
+ message: `lookup of ${submissionId} failed: ${String(error)}`
698
+ })));
699
+ if (Option.isSome(found)) rows.push(found.value);
700
+ }
701
+ const batchProducers = new Map(exported.records.map((envelope) => [envelope.batchId, config.producerId]));
702
+ const report = yield* verifyThreadInvariants({
703
+ export: exported,
704
+ submissions: rows,
705
+ batchProducers,
706
+ requireAllSettled: true
707
+ });
708
+ if (!report.ok) {
709
+ const failed = report.checks.filter((check) => check.status === "failed").map((check) => `${check.name}: ${check.detail ?? "failed"}`).join("; ");
710
+ return yield* ChaosConvergenceFailure.make({
711
+ seed: plan.seed,
712
+ message: `invariants failed for ${threadId} (${kind}): ${failed}`
713
+ });
714
+ }
715
+ if (deskInPlay) yield* assertNoFabrication(plan, exported.records, produced);
716
+ laneReports.push(ChaosLaneReport.make({
717
+ threadId,
718
+ kind,
719
+ submissionCount: rows.length,
720
+ verified: report.ok
721
+ }));
722
+ });
723
+ for (const lane of lanes) {
724
+ yield* verifyThread(lane.threadId, lane.kind, lane.deskInPlay);
725
+ for (const flatIndex of lane.submissionIndexes) {
726
+ const receipt = states[flatIndex]?.receipt;
727
+ if (receipt === void 0) continue;
728
+ const child = lane.childThreadOf(receipt);
729
+ if (child === void 0) continue;
730
+ const childExport = yield* Effect.exit(store.export(ThreadExportRequest.make({ threadId: child })));
731
+ if (Exit.isSuccess(childExport) && childExport.value.records.length > 0) yield* verifyThread(child, "plain", false);
732
+ }
733
+ }
734
+ const obligations = yield* runtime.scanObligations(ObligationThresholds.make({
735
+ agingSeconds: 0,
736
+ overdueSeconds: 0
737
+ })).pipe(Effect.mapError((error) => ChaosConvergenceFailure.make({
738
+ seed: plan.seed,
739
+ message: `scanObligations failed: ${String(error)}`
740
+ })));
741
+ if (obligations.entries.length > 0) return yield* ChaosConvergenceFailure.make({
742
+ seed: plan.seed,
743
+ message: `open obligations after convergence: ${obligations.entries.map((entry) => `${entry.submissionId}(${entry.blockedOn})`).join(", ")}`
744
+ });
745
+ return ChaosPlanReport.make({
746
+ seed: plan.seed,
747
+ rounds,
748
+ lanes: laneReports,
749
+ openObligations: obligations.entries.length
750
+ });
751
+ });
752
+ //#endregion
753
+ export { ChaosApprovalDecision, ChaosConvergenceFailure, ChaosLaneReport, ChaosPlan, ChaosPlanReport, ChaosResolutionKind, ChaosScenarioKind, ChaosSubmissionSpec, DEFAULT_CHAOS_SEED, chaosSeedFromEnv, generateChaosPlans, runChaosPlan };
754
+
755
+ //# sourceMappingURL=Chaos.mjs.map