@effect-agent/testing 0.0.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +3331 -0
- package/dist/index.mjs +4205 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
- package/src/certification.ts +979 -0
- package/src/chaos.ts +1187 -0
- package/src/fixtures/docs-researcher/definition.ts +342 -0
- package/src/fixtures/docs-researcher/harness.ts +316 -0
- package/src/fixtures/docs-researcher/index.ts +34 -0
- package/src/fixtures/docs-researcher/mcp.ts +126 -0
- package/src/fixtures/travel-planner/definition.ts +167 -0
- package/src/fixtures/travel-planner/deterministic-layers.ts +431 -0
- package/src/fixtures/travel-planner/index.ts +11 -0
- package/src/fixtures/travel-planner/phase2.ts +94 -0
- package/src/fixtures/travel-planner/phase3.ts +159 -0
- package/src/fixtures/travel-planner/phase4.ts +272 -0
- package/src/fixtures/travel-planner/phase5.ts +476 -0
- package/src/fixtures/travel-planner/phase6.ts +923 -0
- package/src/fixtures/travel-planner/phase7.ts +112 -0
- package/src/fixtures/travel-planner/scenarios.ts +85 -0
- package/src/fixtures/travel-planner/subagents-durable.ts +391 -0
- package/src/fixtures/travel-planner/subagents.ts +525 -0
- package/src/index.ts +5 -0
- package/src/scripted-model.ts +303 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Agent, AgentPolicy, type ConversationId } from "@effect-agent/core";
|
|
2
|
+
import { ToolExecutionClass } from "@effect-agent/engine";
|
|
3
|
+
import {
|
|
4
|
+
CanonicalRecordEnvelope,
|
|
5
|
+
DefinitionDigests,
|
|
6
|
+
DeploymentId,
|
|
7
|
+
Digest,
|
|
8
|
+
Principal,
|
|
9
|
+
ProducerId,
|
|
10
|
+
type DurableSubmitOptions,
|
|
11
|
+
type IdempotencyKey,
|
|
12
|
+
type Receipt,
|
|
13
|
+
} from "@effect-agent/session";
|
|
14
|
+
import { Effect, Layer, Schema } from "effect";
|
|
15
|
+
import { Model, Tool, Toolkit } from "effect/unstable/ai";
|
|
16
|
+
|
|
17
|
+
import { ScriptedModel, type ScriptedTurnInput } from "../../scripted-model.ts";
|
|
18
|
+
import {
|
|
19
|
+
ActivityCatalog,
|
|
20
|
+
ActivityQuery,
|
|
21
|
+
ActivitySearchResult,
|
|
22
|
+
ActivityUnavailable,
|
|
23
|
+
FlightCatalog,
|
|
24
|
+
FlightOption,
|
|
25
|
+
FlightQuery,
|
|
26
|
+
FlightUnavailable,
|
|
27
|
+
LodgingCatalog,
|
|
28
|
+
LodgingOption,
|
|
29
|
+
LodgingQuery,
|
|
30
|
+
LodgingUnavailable,
|
|
31
|
+
TravelGuidance,
|
|
32
|
+
TravelPlan,
|
|
33
|
+
TripRequest,
|
|
34
|
+
} from "./definition.ts";
|
|
35
|
+
import {
|
|
36
|
+
ActivityCatalogLayer,
|
|
37
|
+
CatalogLifecycle,
|
|
38
|
+
FlightCatalogLayer,
|
|
39
|
+
LodgingCatalogLayer,
|
|
40
|
+
TravelGuidanceLayer,
|
|
41
|
+
} from "./deterministic-layers.ts";
|
|
42
|
+
import { phase1HappyPathTurns } from "./scenarios.ts";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The Phase 4 profile claims durable accepted work on the Node/SQLite runtime (deployment class
|
|
46
|
+
* DN): once `submit` returns a Receipt, the Submission settles exactly once even across process
|
|
47
|
+
* loss. The claim is limited to safe-to-repeat toolkits (D6): supplier booking is explicitly NOT
|
|
48
|
+
* claimed safely replayable — replay-safe external mutation is P5 (Durable Tools) scope.
|
|
49
|
+
*/
|
|
50
|
+
export class TravelPlannerDurabilityProfile extends Schema.Class<TravelPlannerDurabilityProfile>(
|
|
51
|
+
"@effect-agent/testing/travel-planner/TravelPlannerDurabilityProfile",
|
|
52
|
+
)({
|
|
53
|
+
deploymentClass: Schema.Literal("DN"),
|
|
54
|
+
durableAcceptedWork: Schema.Literal(true),
|
|
55
|
+
canonicalSchemaVersion: Schema.Literal(1),
|
|
56
|
+
/** Supplier booking replay safety is P5 (Durable Tools) scope; DN does not claim it. */
|
|
57
|
+
supplierBookingReplaySafe: Schema.Literal(false),
|
|
58
|
+
}) {}
|
|
59
|
+
|
|
60
|
+
export const phase4TravelPlannerProfile = TravelPlannerDurabilityProfile.make({
|
|
61
|
+
deploymentClass: "DN",
|
|
62
|
+
durableAcceptedWork: true,
|
|
63
|
+
canonicalSchemaVersion: 1,
|
|
64
|
+
supplierBookingReplaySafe: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const phase4TravelPlannerDeploymentId = Schema.decodeSync(DeploymentId)(
|
|
68
|
+
"travel-planner-p4-deployment",
|
|
69
|
+
);
|
|
70
|
+
export const phase4TravelPlannerProducerId = Schema.decodeSync(ProducerId)(
|
|
71
|
+
"travel-planner-p4-producer",
|
|
72
|
+
);
|
|
73
|
+
export const phase4TravelPlannerPrincipal = Schema.decodeSync(Principal)(
|
|
74
|
+
"travel-planner-p4-principal",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const digest = (character: string) => Schema.decodeSync(Digest)(character.repeat(64));
|
|
78
|
+
|
|
79
|
+
/** Redacted, deterministic definition identities for the current fixture version. */
|
|
80
|
+
export const phase4TravelPlannerDefinitionDigests = DefinitionDigests.make({
|
|
81
|
+
agent: digest("d"),
|
|
82
|
+
model: digest("e"),
|
|
83
|
+
tools: digest("f"),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** Durable admission options for one Travel Planner Submission on one trip lane. */
|
|
87
|
+
export const phase4TravelPlannerSubmitOptions = (
|
|
88
|
+
conversationId: ConversationId,
|
|
89
|
+
idempotencyKey: IdempotencyKey,
|
|
90
|
+
): DurableSubmitOptions => ({
|
|
91
|
+
conversationId,
|
|
92
|
+
principal: phase4TravelPlannerPrincipal,
|
|
93
|
+
idempotencyKey,
|
|
94
|
+
definitions: phase4TravelPlannerDefinitionDigests,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The P4 read-only search Tools, calling the same deterministic catalogs as the P1 toolkit.
|
|
99
|
+
*
|
|
100
|
+
* The plain-Struct parameter shape is a historical remnant of the P4 carry-in workaround: since
|
|
101
|
+
* the P5 engine fix, official history carries Schema-ENCODED Tool-call parameters, so class-typed
|
|
102
|
+
* parameter codecs persist canonically too — the Structs simply need no change here. The
|
|
103
|
+
* `ToolExecutionClass` `readonly` annotation is the deliberate P5 migration (plan §4.3): these
|
|
104
|
+
* Tools perform no external mutation, so a crash between start and settlement is a free re-run
|
|
105
|
+
* and they never enter the prepared/settled uncertainty protocol — keeping the P4 canonical
|
|
106
|
+
* history byte-stable (an unannotated Tool fails closed to `uncertain`).
|
|
107
|
+
*/
|
|
108
|
+
export const DurableSearchFlights = Tool.make("search_flights", {
|
|
109
|
+
parameters: Schema.Struct(FlightQuery.fields),
|
|
110
|
+
success: FlightOption,
|
|
111
|
+
failure: FlightUnavailable,
|
|
112
|
+
failureMode: "error",
|
|
113
|
+
dependencies: [FlightCatalog],
|
|
114
|
+
}).annotate(ToolExecutionClass, "readonly");
|
|
115
|
+
export const DurableSearchLodging = Tool.make("search_lodging", {
|
|
116
|
+
parameters: Schema.Struct(LodgingQuery.fields),
|
|
117
|
+
success: LodgingOption,
|
|
118
|
+
failure: LodgingUnavailable,
|
|
119
|
+
failureMode: "error",
|
|
120
|
+
dependencies: [LodgingCatalog],
|
|
121
|
+
}).annotate(ToolExecutionClass, "readonly");
|
|
122
|
+
export const DurableSearchActivities = Tool.make("search_activities", {
|
|
123
|
+
parameters: Schema.Struct(ActivityQuery.fields),
|
|
124
|
+
success: ActivitySearchResult,
|
|
125
|
+
failure: ActivityUnavailable,
|
|
126
|
+
failureMode: "error",
|
|
127
|
+
dependencies: [ActivityCatalog],
|
|
128
|
+
}).annotate(ToolExecutionClass, "readonly");
|
|
129
|
+
|
|
130
|
+
export const TravelPlannerPhase4Toolkit = Toolkit.make(
|
|
131
|
+
DurableSearchFlights,
|
|
132
|
+
DurableSearchLodging,
|
|
133
|
+
DurableSearchActivities,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
export const TravelPlannerPhase4ToolkitLayer = TravelPlannerPhase4Toolkit.toLayer({
|
|
137
|
+
search_flights: (query) =>
|
|
138
|
+
Effect.flatMap(FlightCatalog, (catalog) => catalog.search(FlightQuery.make(query))),
|
|
139
|
+
search_lodging: (query) =>
|
|
140
|
+
Effect.flatMap(LodgingCatalog, (catalog) => catalog.search(LodgingQuery.make(query))),
|
|
141
|
+
search_activities: (query) =>
|
|
142
|
+
Effect.flatMap(ActivityCatalog, (catalog) => catalog.search(ActivityQuery.make(query))),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The cumulative Travel Planner, Phase 4: the P1 planning behavior on the durable Node/SQLite
|
|
147
|
+
* runtime. The searches are read-only and safe to repeat across Attempts (D6); supplier booking
|
|
148
|
+
* is deliberately absent because DN does NOT claim replay-safe external mutation (P5 scope).
|
|
149
|
+
*/
|
|
150
|
+
export const TravelPlannerPhase4 = Agent.define("travel-planner-phase-4", {
|
|
151
|
+
input: TripRequest,
|
|
152
|
+
output: TravelPlan,
|
|
153
|
+
instructions: (input) =>
|
|
154
|
+
Effect.flatMap(TravelGuidance, (guidance) => guidance.instructions(input)),
|
|
155
|
+
toolkit: TravelPlannerPhase4Toolkit,
|
|
156
|
+
policy: AgentPolicy.make({
|
|
157
|
+
maxTurns: 2,
|
|
158
|
+
maxToolCalls: 3,
|
|
159
|
+
maxDuration: "30 seconds",
|
|
160
|
+
toolConcurrency: 3,
|
|
161
|
+
}),
|
|
162
|
+
description:
|
|
163
|
+
"Durably plan one review-only itinerary from safe-to-repeat deterministic searches; supplier booking is not claimed safely replayable.",
|
|
164
|
+
metadata: { deploymentClass: "DN", phase: "P4" },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The P4 Agent Binding: the durable Travel Planner definition bound to a finite scripted model.
|
|
169
|
+
* The scripted Layer is rebuilt per Run, so every Run of one Binding replays the same
|
|
170
|
+
* deterministic script.
|
|
171
|
+
*/
|
|
172
|
+
export const makePhase4TravelPlannerAgent = (
|
|
173
|
+
turns: ReadonlyArray<ScriptedTurnInput> = phase1HappyPathTurns,
|
|
174
|
+
) =>
|
|
175
|
+
Agent.withModel(
|
|
176
|
+
TravelPlannerPhase4,
|
|
177
|
+
Model.make("scripted", "travel-planner-phase-4", ScriptedModel.layer(turns)),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Everything a durable worker needs beyond the runtime stack, reusing the deterministic P1
|
|
182
|
+
* travel-service Layers. The durable coordinator supplies its own deterministic `IdGenerator`,
|
|
183
|
+
* so this Layer deliberately provides none.
|
|
184
|
+
*/
|
|
185
|
+
export const phase4TravelPlannerWorkerLayer = Layer.mergeAll(
|
|
186
|
+
TravelPlannerPhase4ToolkitLayer,
|
|
187
|
+
FlightCatalogLayer,
|
|
188
|
+
LodgingCatalogLayer,
|
|
189
|
+
ActivityCatalogLayer,
|
|
190
|
+
TravelGuidanceLayer,
|
|
191
|
+
).pipe(Layer.provide(CatalogLifecycle.layerNoDeps));
|
|
192
|
+
|
|
193
|
+
export class TravelPlannerDurableEvidenceError extends Schema.TaggedErrorClass<TravelPlannerDurableEvidenceError>()(
|
|
194
|
+
"TravelPlannerDurableEvidenceError",
|
|
195
|
+
{ message: Schema.String },
|
|
196
|
+
) {}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Decode the completed itinerary from the canonical `SubmissionSettled` record. Canonical history
|
|
200
|
+
* is the outcome authority (DUR-015): the settled result — not any ledger cache — must decode
|
|
201
|
+
* through the trip output schema.
|
|
202
|
+
*/
|
|
203
|
+
export const travelPlanFromDurableSettlement = Effect.fn(
|
|
204
|
+
"TravelPlannerPhase4.travelPlanFromDurableSettlement",
|
|
205
|
+
)(function* (
|
|
206
|
+
records: ReadonlyArray<CanonicalRecordEnvelope>,
|
|
207
|
+
): Effect.fn.Return<TravelPlan, TravelPlannerDurableEvidenceError> {
|
|
208
|
+
const settlements = records.flatMap((envelope) =>
|
|
209
|
+
envelope.record.payload._tag === "SubmissionSettled" ? [envelope.record.payload] : [],
|
|
210
|
+
);
|
|
211
|
+
const settled = settlements.at(0);
|
|
212
|
+
if (settled === undefined) {
|
|
213
|
+
return yield* TravelPlannerDurableEvidenceError.make({
|
|
214
|
+
message: "The canonical Conversation Log has no SubmissionSettled record.",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (settled.outcome !== "completed" || settled.result === undefined) {
|
|
218
|
+
return yield* TravelPlannerDurableEvidenceError.make({
|
|
219
|
+
message: `The Submission settled ${settled.outcome} without a completed itinerary result.`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return yield* Schema.decodeUnknownEffect(TravelPlan)(settled.result).pipe(
|
|
223
|
+
Effect.mapError((error) =>
|
|
224
|
+
TravelPlannerDurableEvidenceError.make({
|
|
225
|
+
message: `The settled result does not decode through the TravelPlan schema: ${error.message}`,
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const encodeEvidence = Schema.encodeEffect(Schema.Array(CanonicalRecordEnvelope));
|
|
232
|
+
const decodeComparableJson = Schema.decodeUnknownEffect(Schema.Json);
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Project canonical evidence into a Submission-identity-independent comparable form: batch
|
|
236
|
+
* identity, canonical sequence, and the full encoded record, with the ledger-minted
|
|
237
|
+
* `submissionId`/`receiptId` (and every identity derived from them: run, turn, batch, record,
|
|
238
|
+
* and settlement ids) replaced by stable placeholders. Two Conversations whose normalized
|
|
239
|
+
* evidence is equal took byte-equivalent canonical histories, so restart-equivalence can compare
|
|
240
|
+
* a recovered run against an uninterrupted control run on a separate database.
|
|
241
|
+
*/
|
|
242
|
+
export const normalizeDurableTravelPlannerEvidence = Effect.fn(
|
|
243
|
+
"TravelPlannerPhase4.normalizeDurableTravelPlannerEvidence",
|
|
244
|
+
)(function* (
|
|
245
|
+
records: ReadonlyArray<CanonicalRecordEnvelope>,
|
|
246
|
+
receipt: Receipt,
|
|
247
|
+
): Effect.fn.Return<Schema.Json, TravelPlannerDurableEvidenceError> {
|
|
248
|
+
const encoded = yield* encodeEvidence(records).pipe(
|
|
249
|
+
Effect.mapError((error) =>
|
|
250
|
+
TravelPlannerDurableEvidenceError.make({
|
|
251
|
+
message: `Canonical evidence failed to encode: ${error.message}`,
|
|
252
|
+
}),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
const comparable = encoded.map((envelope) => ({
|
|
256
|
+
batchId: envelope.batchId,
|
|
257
|
+
sequence: envelope.sequence,
|
|
258
|
+
record: envelope.record,
|
|
259
|
+
}));
|
|
260
|
+
const substituted: unknown = JSON.parse(
|
|
261
|
+
JSON.stringify(comparable)
|
|
262
|
+
.replaceAll(receipt.submissionId, "{submissionId}")
|
|
263
|
+
.replaceAll(receipt.receiptId, "{receiptId}"),
|
|
264
|
+
);
|
|
265
|
+
return yield* decodeComparableJson(substituted).pipe(
|
|
266
|
+
Effect.mapError((error) =>
|
|
267
|
+
TravelPlannerDurableEvidenceError.make({
|
|
268
|
+
message: `Normalized evidence is not comparable JSON: ${error.message}`,
|
|
269
|
+
}),
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
});
|