@effect-agent/testing 0.1.0-beta.38 → 0.1.0-beta.40

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