@effect-agent/testing 0.1.0-beta.24 → 0.1.0-beta.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-agent/testing",
3
- "version": "0.1.0-beta.24",
3
+ "version": "0.1.0-beta.25",
4
4
  "exports": {
5
5
  ".": {
6
6
  "types": "./dist/index.d.mts",
@@ -8,14 +8,14 @@
8
8
  }
9
9
  },
10
10
  "dependencies": {
11
- "@effect-agent/capabilities": "0.1.0-beta.24",
12
- "@effect-agent/core": "0.1.0-beta.24",
13
- "@effect-agent/engine": "0.1.0-beta.24",
14
- "@effect-agent/platform-node": "0.1.0-beta.24",
15
- "@effect-agent/sandbox": "0.1.0-beta.24",
16
- "@effect-agent/session": "0.1.0-beta.24",
17
- "@effect-agent/storage-memory": "0.1.0-beta.24",
18
- "@effect-agent/storage-sqlite": "0.1.0-beta.24",
11
+ "@effect-agent/capabilities": "0.1.0-beta.25",
12
+ "@effect-agent/core": "0.1.0-beta.25",
13
+ "@effect-agent/engine": "0.1.0-beta.25",
14
+ "@effect-agent/platform-node": "0.1.0-beta.25",
15
+ "@effect-agent/sandbox": "0.1.0-beta.25",
16
+ "@effect-agent/session": "0.1.0-beta.25",
17
+ "@effect-agent/storage-memory": "0.1.0-beta.25",
18
+ "@effect-agent/storage-sqlite": "0.1.0-beta.25",
19
19
  "effect": "4.0.0-rc.110"
20
20
  },
21
21
  "description": "Scripted models, deterministic fixtures, and adapter conformance kits for testing Effect Agent applications.",
@@ -51,8 +51,8 @@ import {
51
51
  type ResolvedBinding,
52
52
  type SubmissionSnapshot,
53
53
  } from "@effect-agent/session";
54
- import type { CertificationCaseResult } from "@effect-agent/session/testing";
55
54
  import {
55
+ type CertificationCaseResult,
56
56
  CertificationReport,
57
57
  CertificationSweepResult,
58
58
  CertificationTierThreeReport,
@@ -670,7 +670,7 @@ const failureTagOf = <E>(cause: Cause.Cause<E>): string => {
670
670
  if (Option.isSome(failure)) {
671
671
  const error: unknown = failure.value;
672
672
  if (typeof error === "object" && error !== null && "_tag" in error) {
673
- return String((error as { _tag: unknown })._tag);
673
+ return String(error._tag);
674
674
  }
675
675
  return String(error).slice(0, 256);
676
676
  }
package/src/chaos.ts CHANGED
@@ -10,9 +10,9 @@ import {
10
10
  ConversationId,
11
11
  IdGenerator,
12
12
  RunId,
13
- SubmissionId,
14
13
  ToolCallId,
15
14
  TurnId,
15
+ type SubmissionId,
16
16
  } from "@effect-agent/core";
17
17
  import { DurableStep, DurableStepError, ToolExecutionClass } from "@effect-agent/engine";
18
18
  import {
@@ -42,6 +42,9 @@ import {
42
42
  verifyConversationInvariants,
43
43
  type CanonicalRecordEnvelope,
44
44
  type BatchId,
45
+ type DurableBindingFailure,
46
+ type DurableSubmitFailure,
47
+ type DurableWorkerFailure,
45
48
  type ProducerId,
46
49
  type Receipt,
47
50
  type ResolvedBinding,
@@ -51,7 +54,14 @@ import {
51
54
  } from "@effect-agent/session";
52
55
  import { Cause, Effect, Exit, Layer, Option, Ref, Schema, Stream } from "effect";
53
56
  import { FastCheck } from "effect/testing";
54
- import { LanguageModel, Model, Prompt, Tool, Toolkit, type Response } from "effect/unstable/ai";
57
+ import {
58
+ LanguageModel,
59
+ Model,
60
+ Tool,
61
+ Toolkit,
62
+ type Prompt,
63
+ type Response,
64
+ } from "effect/unstable/ai";
55
65
 
56
66
  /**
57
67
  * P7 WP4 chaos machinery (plan §5): a Schema-first `ChaosPlan`, a seeded generator over
@@ -176,12 +186,14 @@ export class ChaosConvergenceFailure extends Schema.TaggedError<ChaosConvergence
176
186
  /** Default root seed for chaos suites; override with the `CHAOS_SEED` environment variable. */
177
187
  export const DEFAULT_CHAOS_SEED = 20260813;
178
188
 
189
+ const ChaosSeedFromEnvironment = Schema.FiniteFromString.check(Schema.isInt());
190
+ const decodeChaosSeedFromEnvironment = Schema.decodeUnknownOption(ChaosSeedFromEnvironment);
191
+
179
192
  /** The root seed for this run: `CHAOS_SEED` when set to an integer, the default otherwise. */
180
193
  export const chaosSeedFromEnv = (env: Record<string, string | undefined>): number => {
181
194
  const raw = env["CHAOS_SEED"];
182
195
  if (raw === undefined || raw === "") return DEFAULT_CHAOS_SEED;
183
- const parsed = Number.parseInt(raw, 10);
184
- return Number.isSafeInteger(parsed) ? parsed : DEFAULT_CHAOS_SEED;
196
+ return Option.getOrElse(decodeChaosSeedFromEnvironment(raw), () => DEFAULT_CHAOS_SEED);
185
197
  };
186
198
 
187
199
  export interface ChaosGeneratorOptions {
@@ -458,11 +470,11 @@ const DELEGATE_CALL_ID = "chaos-delegate-1";
458
470
  const HEX = "0123456789abcdef";
459
471
  const decodeDigest = Schema.decodeSync(Digest);
460
472
  const laneDigests = (lane: number): DefinitionDigests => {
461
- const digest = decodeDigest(HEX[lane % 8]!.repeat(64));
473
+ const digest = decodeDigest(HEX.charAt(lane % 8).repeat(64));
462
474
  return DefinitionDigests.make({ agent: digest, model: digest, tools: digest });
463
475
  };
464
476
  const childDigestStrings = (lane: number) => {
465
- const char = HEX[8 + (lane % 8)]!;
477
+ const char = HEX.charAt(8 + (lane % 8));
466
478
  return { agent: char.repeat(64), model: char.repeat(64), tools: char.repeat(64) } as const;
467
479
  };
468
480
  const childLaneDigests = (lane: number): DefinitionDigests => {
@@ -565,10 +577,12 @@ interface LaneFixture {
565
577
  readonly ref: string;
566
578
  readonly deskInPlay: boolean;
567
579
  readonly submissionIndexes: ReadonlyArray<number>;
568
- readonly submitOne: (flatIndex: number) => Effect.Effect<Receipt, unknown>;
580
+ readonly submitOne: (flatIndex: number) => Effect.Effect<Receipt, DurableSubmitFailure>;
569
581
  readonly drives: (
570
582
  firstReceipt: Receipt | undefined,
571
- ) => ReadonlyArray<Effect.Effect<ReadonlyArray<Settlement>, unknown>>;
583
+ ) => ReadonlyArray<
584
+ Effect.Effect<ReadonlyArray<Settlement>, DurableWorkerFailure | DurableBindingFailure>
585
+ >;
572
586
  readonly childConversationOf: (firstReceipt: Receipt) => ConversationId | undefined;
573
587
  }
574
588
 
@@ -636,8 +650,8 @@ const makeLaneFixture = Effect.fn("Chaos.makeLaneFixture")(function* (
636
650
 
637
651
  const plainLaneFixture = (
638
652
  deskInPlay: boolean,
639
- drive: Effect.Effect<ReadonlyArray<Settlement>, unknown>,
640
- submitOne: (flatIndex: number) => Effect.Effect<Receipt, unknown>,
653
+ drive: Effect.Effect<ReadonlyArray<Settlement>, DurableWorkerFailure | DurableBindingFailure>,
654
+ submitOne: (flatIndex: number) => Effect.Effect<Receipt, DurableSubmitFailure>,
641
655
  ): LaneFixture => ({
642
656
  index: laneIndex,
643
657
  kind,
@@ -746,9 +760,9 @@ const makeLaneFixture = Effect.fn("Chaos.makeLaneFixture")(function* (
746
760
  submitOptionsFor(flatIndex),
747
761
  ),
748
762
  drives: (firstReceipt) => {
749
- const drives: Array<Effect.Effect<ReadonlyArray<Settlement>, unknown>> = [
750
- driveResolved(conversationId),
751
- ];
763
+ const drives: Array<
764
+ Effect.Effect<ReadonlyArray<Settlement>, DurableWorkerFailure | DurableBindingFailure>
765
+ > = [driveResolved(conversationId)];
752
766
  if (firstReceipt !== undefined) {
753
767
  drives.push(
754
768
  driveResolved(
@@ -838,9 +852,9 @@ const resolutionPass = Effect.fn("Chaos.resolutionPass")(function* (
838
852
  const kind =
839
853
  plan.resolutionInjections.length === 0
840
854
  ? "never-happened"
841
- : plan.resolutionInjections[
842
- injectionIndex(flatIndex, call.toolCallId, plan.resolutionInjections.length)
843
- ]!;
855
+ : (plan.resolutionInjections.at(
856
+ injectionIndex(flatIndex, call.toolCallId, plan.resolutionInjections.length),
857
+ ) ?? "never-happened");
844
858
  yield* tolerateTyped(
845
859
  runtime.resolveUnknown(
846
860
  UnknownResolutionCommand.make({
@@ -858,9 +872,9 @@ const resolutionPass = Effect.fn("Chaos.resolutionPass")(function* (
858
872
  const decision =
859
873
  plan.approvalDecisions.length === 0
860
874
  ? "approved"
861
- : plan.approvalDecisions[
862
- injectionIndex(flatIndex, pending.toolCallId, plan.approvalDecisions.length)
863
- ]!;
875
+ : (plan.approvalDecisions.at(
876
+ injectionIndex(flatIndex, pending.toolCallId, plan.approvalDecisions.length),
877
+ ) ?? "approved");
864
878
  yield* tolerateTyped(
865
879
  runtime.resolveApproval(
866
880
  ApprovalDecisionCommand.make({
@@ -975,11 +989,19 @@ export const runChaosPlan = Effect.fn("Chaos.runChaosPlan")(function* (
975
989
  lanes.push(yield* makeLaneFixture(plan, lane, kind, laneSubmissions.get(lane) ?? [], desk));
976
990
  }
977
991
 
978
- const states: Array<SubmissionState> = plan.submissions.map((spec, flatIndex) => ({
979
- flatIndex,
980
- lane: lanes.find((fixture) => fixture.index === spec.lane % plan.lanes)!,
981
- receipt: undefined,
982
- }));
992
+ const lanesByIndex = new Map(lanes.map((lane) => [lane.index, lane]));
993
+ const states: Array<SubmissionState> = [];
994
+ for (const [flatIndex, spec] of plan.submissions.entries()) {
995
+ const laneIndex = spec.lane % plan.lanes;
996
+ const lane = lanesByIndex.get(laneIndex);
997
+ if (lane === undefined) {
998
+ return yield* ChaosConvergenceFailure.make({
999
+ seed: plan.seed,
1000
+ message: `submission ${flatIndex} addresses missing lane ${laneIndex}`,
1001
+ });
1002
+ }
1003
+ states.push({ flatIndex, lane, receipt: undefined });
1004
+ }
983
1005
  const appliedAborts = new Set<number>();
984
1006
 
985
1007
  type ArmEntry =
@@ -1022,7 +1044,10 @@ export const runChaosPlan = Effect.fn("Chaos.runChaosPlan")(function* (
1022
1044
  }
1023
1045
 
1024
1046
  // Drive every lane (and discovered child lanes) in seeded order under the active arm.
1025
- const order = [...lanes].sort(() => random() - 0.5);
1047
+ const order = lanes
1048
+ .map((lane) => ({ lane, rank: random() }))
1049
+ .sort((left, right) => left.rank - right.rank || left.lane.index - right.lane.index)
1050
+ .map(({ lane }) => lane);
1026
1051
  for (const lane of order) {
1027
1052
  const firstFlat = lane.submissionIndexes[0];
1028
1053
  const firstReceipt = firstFlat === undefined ? undefined : states[firstFlat]?.receipt;
@@ -1,4 +1,4 @@
1
- import { CodeExecutor, SandboxImplementation } from "@effect-agent/sandbox";
1
+ import { CodeExecutor, type SandboxImplementation } from "@effect-agent/sandbox";
2
2
  import { Effect, Schema } from "effect";
3
3
  declare const CodeExecutorConformanceViolation_base: Schema.Class<CodeExecutorConformanceViolation, Schema.TaggedStruct<"CodeExecutorConformanceViolation", {
4
4
  readonly caseName: Schema.String;
@@ -1,18 +1,18 @@
1
1
  import {
2
- CodeExecutionError,
3
2
  CodeExecutionHost,
4
3
  CodeExecutionLimits,
5
4
  CodeExecutionNamespace,
6
5
  CodeExecutionRequest,
7
- CodeExecutionResult,
8
6
  CodeExecutor,
9
- CodeHostCall,
10
7
  CodeHostCallFailure,
11
- CodeHostCallResult,
12
8
  CodeHostCallSuccess,
13
9
  NetworkAllowlist,
14
10
  NetworkDisabled,
15
- SandboxImplementation,
11
+ type CodeExecutionError,
12
+ type CodeExecutionResult,
13
+ type CodeHostCall,
14
+ type CodeHostCallResult,
15
+ type SandboxImplementation,
16
16
  } from "@effect-agent/sandbox";
17
17
  import { Deferred, Duration, Effect, Fiber, Schema } from "effect";
18
18
 
@@ -404,6 +404,8 @@ export const codeExecutorConformanceCases = (
404
404
  "TEST-015 fails typed on a host outcome outside the protocol schema",
405
405
  makeRequest("async () => warehouse.query({})", { namespaces: [warehouseNamespace] }),
406
406
  {
407
+ // Deliberately violate the compile-time host contract to verify that an adapter
408
+ // independently decodes the runtime protocol boundary.
407
409
  call: () => Effect.succeed({ bogus: true } as unknown as CodeHostCallResult),
408
410
  },
409
411
  "CodeExecutionProtocolError",
@@ -1,9 +1,6 @@
1
1
  import {
2
2
  CodeExecutionHost,
3
- CodeExecutionLimits,
4
- CodeExecutionNamespace,
5
3
  CodeExecutionProtocolError,
6
- CodeExecutionRequest,
7
4
  CodeExecutionResourceUse,
8
5
  CodeExecutionResult,
9
6
  CodeExecutionTimeoutError,
@@ -17,6 +14,9 @@ import {
17
14
  CodeProgramFailedError,
18
15
  CodeSourceError,
19
16
  SandboxImplementation,
17
+ type CodeExecutionLimits,
18
+ type CodeExecutionNamespace,
19
+ type CodeExecutionRequest,
20
20
  } from "@effect-agent/sandbox";
21
21
  import { Clock, Duration, Effect, Fiber, Layer, Option, Queue, Schema } from "effect";
22
22
 
@@ -346,6 +346,9 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
346
346
 
347
347
  const factory = yield* Effect.try({
348
348
  try: () =>
349
+ // This substitute intentionally evaluates authored test programs in-process and reports
350
+ // an `unisolated` posture. Real adapters own the security boundary and never use this path.
351
+ // oxlint-disable-next-line typescript/no-implied-eval
349
352
  new Function(
350
353
  ...shadowedGlobals,
351
354
  "console",
@@ -399,7 +402,7 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
399
402
  }
400
403
  let outcome: unknown;
401
404
  try {
402
- outcome = (candidate as () => unknown)();
405
+ outcome = candidate();
403
406
  } catch (cause) {
404
407
  throw new EvaluationThrew(cause);
405
408
  }
@@ -18,7 +18,8 @@ import {
18
18
  type IdempotencyKey,
19
19
  type ResolvedBinding,
20
20
  } from "@effect-agent/session";
21
- import { Crypto, Effect, Layer, Ref, Schema, Stream } from "effect";
21
+ import type { Crypto } from "effect";
22
+ import { Effect, Layer, Ref, Schema, Stream } from "effect";
22
23
  import { LanguageModel, Model, type Response } from "effect/unstable/ai";
23
24
 
24
25
  import { DeterministicIdGeneratorLayer } from "../travel-planner/deterministic-layers.ts";
@@ -44,13 +44,22 @@ export const docsMcpIdentity = McpServerIdentity.make({
44
44
  * real MCP server advertises on the wire. This inlines the single top-level
45
45
  * `$ref` so the derivation described above still holds byte-for-byte.
46
46
  */
47
+ const JsonSchemaDefinitions = Schema.Record(
48
+ Schema.String,
49
+ Schema.Record(Schema.String, Schema.Unknown),
50
+ );
51
+ const decodeJsonSchemaDefinitions = Schema.decodeUnknownSync(JsonSchemaDefinitions);
52
+ const decodeToolJsonSchema = Schema.decodeUnknownSync(McpSchema.ToolJsonSchema);
53
+
47
54
  const flattenTopLevelRef = (schema: JsonSchema.JsonSchema): McpSchema.ToolJsonSchema => {
48
55
  const ref = schema["$ref"];
49
- const defs = schema["$defs"] as JsonSchema.Definitions | undefined;
50
- if (typeof ref !== "string" || defs === undefined) return schema as McpSchema.ToolJsonSchema;
56
+ if (typeof ref !== "string") {
57
+ return decodeToolJsonSchema(schema);
58
+ }
51
59
 
60
+ const defs = decodeJsonSchemaDefinitions(schema["$defs"]);
52
61
  const resolved = JsonSchema.resolve$ref(ref, defs);
53
- return (resolved ?? schema) as McpSchema.ToolJsonSchema;
62
+ return decodeToolJsonSchema(resolved ?? schema);
54
63
  };
55
64
 
56
65
  const fetchDocumentOutputSchema = flattenTopLevelRef(
@@ -15,8 +15,6 @@ export const phase1Trip = Schema.decodeSync(TripRequest)({
15
15
  budgetCents: 350_000,
16
16
  currency: "USD",
17
17
  });
18
- /** Backward-compatible fixture alias while consumers transition to the P1 name. */
19
- export const phase0Trip = phase1Trip;
20
18
  export const expectedTravelPlan: TravelPlanValue = Schema.decodeSync(TravelPlan)({
21
19
  itineraries: [
22
20
  {
@@ -82,4 +80,3 @@ export const phase1HappyPathTurns = [
82
80
  termination: { _tag: "Complete" },
83
81
  },
84
82
  ] satisfies readonly [ScriptedTurnInput, ScriptedTurnInput];
85
- export const phase0HappyPathTurns = phase1HappyPathTurns;
@@ -20,14 +20,16 @@ import { Effect, Layer, Ref, Schema, Stream } from "effect";
20
20
  import { LanguageModel, Model, type Response, type Toolkit } from "effect/unstable/ai";
21
21
 
22
22
  import { DeterministicIdGeneratorLayer } from "./deterministic-layers.ts";
23
- import {
23
+ import type {
24
24
  DestinationBrief,
25
+ DestinationReport,
26
+ DestinationResearcherToolkit,
27
+ } from "./subagents.ts";
28
+ import {
25
29
  DestinationFacts,
26
30
  DestinationGuide,
27
31
  DestinationRecommendation,
28
- DestinationReport,
29
32
  DestinationResearcher,
30
- DestinationResearcherToolkit,
31
33
  DestinationResearcherToolkitLayer,
32
34
  destinationLookup,
33
35
  destinationReportFor,
@@ -1,9 +1,9 @@
1
1
  import { ToolExecutionClass } from "@effect-agent/engine";
2
2
  import { SqliteClient } from "@effect/sql-sqlite-node";
3
- import { Context, Duration, Effect, Layer, Option, Predicate, Schema } from "effect";
3
+ import { Context, Effect, Layer, Option, Predicate, Schema, type Duration } from "effect";
4
4
  import { Tool, Toolkit } from "effect/unstable/ai";
5
5
  import * as SqlClient from "effect/unstable/sql/SqlClient";
6
- import { SqlError } from "effect/unstable/sql/SqlError";
6
+ import type { SqlError } from "effect/unstable/sql/SqlError";
7
7
 
8
8
  /**
9
9
  * The C3 read-only SQL reference integration (plan §8, SEC-015): application
@@ -196,7 +196,7 @@ const decodeRow = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema
196
196
  * on the reason's cause. No error-text matching.
197
197
  */
198
198
  const isWriteDenial = (error: SqlError): boolean => {
199
- const cause = (error.reason as { readonly cause?: unknown }).cause;
199
+ const cause = error.reason.cause;
200
200
  return (
201
201
  Predicate.isObject(cause) &&
202
202
  "errcode" in cause &&
@@ -206,7 +206,7 @@ const isWriteDenial = (error: SqlError): boolean => {
206
206
  };
207
207
 
208
208
  const sqlFailureMessage = (error: SqlError): string => {
209
- const cause = (error.reason as { readonly cause?: unknown }).cause;
209
+ const cause = error.reason.cause;
210
210
  const detail =
211
211
  Predicate.isObject(cause) && "errstr" in cause && typeof cause.errstr === "string"
212
212
  ? `: ${cause.errstr}`
@@ -275,7 +275,7 @@ export const warehouseDbLayer = (
275
275
  return yield* denied;
276
276
  }
277
277
  const rawRows = yield* sql
278
- .unsafe<Record<string, unknown>>(text, parameters as Array<unknown>)
278
+ .unsafe<Record<string, unknown>>(text, [...parameters])
279
279
  .withoutTransform.pipe(
280
280
  Effect.catchTag("SqlError", (error) =>
281
281
  Effect.fail<WarehouseQueryDenied | WarehouseQueryFailed>(