@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,476 @@
|
|
|
1
|
+
import { Agent, AgentPolicy, type ConversationId } from "@effect-agent/core";
|
|
2
|
+
import { DurableStep, DurableStepError, ToolExecutionClass } from "@effect-agent/engine";
|
|
3
|
+
import {
|
|
4
|
+
DefinitionDigests,
|
|
5
|
+
DeploymentId,
|
|
6
|
+
Digest,
|
|
7
|
+
PersistedJson,
|
|
8
|
+
Principal,
|
|
9
|
+
ProducerId,
|
|
10
|
+
ReconciliationCompleted,
|
|
11
|
+
ReconciliationSafeToRetry,
|
|
12
|
+
ReconciliationUncertain,
|
|
13
|
+
ToolReconciler,
|
|
14
|
+
ToolReconcilerError,
|
|
15
|
+
type CanonicalRecordEnvelope,
|
|
16
|
+
type DurableSubmitOptions,
|
|
17
|
+
type IdempotencyKey,
|
|
18
|
+
} from "@effect-agent/session";
|
|
19
|
+
import { Effect, Layer, Option, Schema } from "effect";
|
|
20
|
+
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
ActivityCatalog,
|
|
24
|
+
ActivityQuery,
|
|
25
|
+
AirportCode,
|
|
26
|
+
FlightCatalog,
|
|
27
|
+
FlightQuery,
|
|
28
|
+
LodgingCatalog,
|
|
29
|
+
LodgingQuery,
|
|
30
|
+
QuoteId,
|
|
31
|
+
TripRequest,
|
|
32
|
+
} from "./definition.ts";
|
|
33
|
+
import {
|
|
34
|
+
ActivityCatalogLayer,
|
|
35
|
+
BookingRef,
|
|
36
|
+
CatalogLifecycle,
|
|
37
|
+
FlightCatalogLayer,
|
|
38
|
+
LodgingCatalogLayer,
|
|
39
|
+
SupplierBookingDesk,
|
|
40
|
+
SupplierBookingRecord,
|
|
41
|
+
SupplierUnavailable,
|
|
42
|
+
} from "./deterministic-layers.ts";
|
|
43
|
+
import { DurableSearchActivities, DurableSearchFlights, DurableSearchLodging } from "./phase4.ts";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The Phase 5 profile extends the P4 `DN` claim to consequential supplier mutation: booking
|
|
47
|
+
* Tools enter the prepared/settled uncertainty protocol, unresolved external effects stop at
|
|
48
|
+
* Unknown Outcomes instead of replaying, Durable Steps replay recorded results, and queued
|
|
49
|
+
* traveler input joins the active Run. Exactly-once EXTERNAL execution is still — deliberately —
|
|
50
|
+
* not claimed (DUR-003): the supplier's own idempotency keys are what dedupe repeats.
|
|
51
|
+
*/
|
|
52
|
+
export class TravelPlannerBookingProfile extends Schema.Class<TravelPlannerBookingProfile>(
|
|
53
|
+
"@effect-agent/testing/travel-planner/TravelPlannerBookingProfile",
|
|
54
|
+
)({
|
|
55
|
+
deploymentClass: Schema.Literal("DN"),
|
|
56
|
+
durableAcceptedWork: Schema.Literal(true),
|
|
57
|
+
canonicalSchemaVersion: Schema.Literal(1),
|
|
58
|
+
/** P5: supplier mutations get prepared/settled records, Unknown Outcomes, and reconciliation. */
|
|
59
|
+
supplierBookingUncertaintyProtocol: Schema.Literal(true),
|
|
60
|
+
/** P5: Durable Steps are exactly-once-RECORDED; their side effects stay at-least-once. */
|
|
61
|
+
durableStepsRecorded: Schema.Literal(true),
|
|
62
|
+
/** Never claimed at any phase (DUR-003). */
|
|
63
|
+
exactlyOnceExternalEffects: Schema.Literal(false),
|
|
64
|
+
}) {}
|
|
65
|
+
|
|
66
|
+
export const phase5TravelPlannerProfile = TravelPlannerBookingProfile.make({
|
|
67
|
+
deploymentClass: "DN",
|
|
68
|
+
durableAcceptedWork: true,
|
|
69
|
+
canonicalSchemaVersion: 1,
|
|
70
|
+
supplierBookingUncertaintyProtocol: true,
|
|
71
|
+
durableStepsRecorded: true,
|
|
72
|
+
exactlyOnceExternalEffects: false,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const phase5TravelPlannerDeploymentId = Schema.decodeSync(DeploymentId)(
|
|
76
|
+
"travel-planner-p5-deployment",
|
|
77
|
+
);
|
|
78
|
+
export const phase5TravelPlannerProducerId = Schema.decodeSync(ProducerId)(
|
|
79
|
+
"travel-planner-p5-producer",
|
|
80
|
+
);
|
|
81
|
+
export const phase5TravelPlannerPrincipal = Schema.decodeSync(Principal)(
|
|
82
|
+
"travel-planner-p5-principal",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const digest = (character: string) => Schema.decodeSync(Digest)(character.repeat(64));
|
|
86
|
+
|
|
87
|
+
/** Redacted, deterministic definition identities for the current fixture version. */
|
|
88
|
+
export const phase5TravelPlannerDefinitionDigests = DefinitionDigests.make({
|
|
89
|
+
agent: digest("1"),
|
|
90
|
+
model: digest("2"),
|
|
91
|
+
tools: digest("3"),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** Durable admission options for one Travel Planner Submission on one trip lane. */
|
|
95
|
+
export const phase5TravelPlannerSubmitOptions = (
|
|
96
|
+
conversationId: ConversationId,
|
|
97
|
+
idempotencyKey: IdempotencyKey,
|
|
98
|
+
): DurableSubmitOptions => ({
|
|
99
|
+
conversationId,
|
|
100
|
+
principal: phase5TravelPlannerPrincipal,
|
|
101
|
+
idempotencyKey,
|
|
102
|
+
definitions: phase5TravelPlannerDefinitionDigests,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const TravelerRef = Schema.NonEmptyString.pipe(
|
|
106
|
+
Schema.brand("@effect-agent/testing/travel-planner/TravelerRef"),
|
|
107
|
+
);
|
|
108
|
+
export type TravelerRef = typeof TravelerRef.Type;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Class-shaped booking parameters with branded fields: since the P5 engine fix, official history
|
|
112
|
+
* carries Schema-ENCODED Tool-call parameters, so class-typed parameter codecs persist
|
|
113
|
+
* canonically end-to-end (the P4 Struct workaround is gone for new Tools).
|
|
114
|
+
*/
|
|
115
|
+
export class FlightBookingRequest extends Schema.Class<FlightBookingRequest>(
|
|
116
|
+
"FlightBookingRequest",
|
|
117
|
+
)({
|
|
118
|
+
quoteId: QuoteId,
|
|
119
|
+
travelerRef: TravelerRef,
|
|
120
|
+
departOn: Schema.String,
|
|
121
|
+
}) {}
|
|
122
|
+
|
|
123
|
+
export class SupplierBookingConfirmation extends Schema.Class<SupplierBookingConfirmation>(
|
|
124
|
+
"SupplierBookingConfirmation",
|
|
125
|
+
)({
|
|
126
|
+
bookingRef: BookingRef,
|
|
127
|
+
status: Schema.Literal("confirmed"),
|
|
128
|
+
detail: Schema.String,
|
|
129
|
+
}) {}
|
|
130
|
+
|
|
131
|
+
export class CancelBookingRequest extends Schema.Class<CancelBookingRequest>(
|
|
132
|
+
"CancelBookingRequest",
|
|
133
|
+
)({
|
|
134
|
+
bookingRef: BookingRef,
|
|
135
|
+
travelerRef: TravelerRef,
|
|
136
|
+
}) {}
|
|
137
|
+
|
|
138
|
+
export class CancellationConfirmation extends Schema.Class<CancellationConfirmation>(
|
|
139
|
+
"CancellationConfirmation",
|
|
140
|
+
)({
|
|
141
|
+
bookingRef: BookingRef,
|
|
142
|
+
status: Schema.Literal("cancelled"),
|
|
143
|
+
}) {}
|
|
144
|
+
|
|
145
|
+
export class ItineraryBookingRequest extends Schema.Class<ItineraryBookingRequest>(
|
|
146
|
+
"ItineraryBookingRequest",
|
|
147
|
+
)({
|
|
148
|
+
quoteId: QuoteId,
|
|
149
|
+
destination: AirportCode,
|
|
150
|
+
nights: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
151
|
+
travelerRef: TravelerRef,
|
|
152
|
+
}) {}
|
|
153
|
+
|
|
154
|
+
export class ItineraryConfirmation extends Schema.Class<ItineraryConfirmation>(
|
|
155
|
+
"ItineraryConfirmation",
|
|
156
|
+
)({
|
|
157
|
+
flightBookingRef: BookingRef,
|
|
158
|
+
lodgingBookingRef: BookingRef,
|
|
159
|
+
confirmationCode: Schema.String,
|
|
160
|
+
}) {}
|
|
161
|
+
|
|
162
|
+
/** The P5 Run output: a booked (or explicitly not-booked) trip report. */
|
|
163
|
+
export class TravelBookingReport extends Schema.Class<TravelBookingReport>("TravelBookingReport")({
|
|
164
|
+
summary: Schema.String,
|
|
165
|
+
bookingRefs: Schema.Array(BookingRef),
|
|
166
|
+
}) {}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Supplier idempotency-key derivations. The handler owns key derivation (the `idempotent` and
|
|
170
|
+
* `uncertain` execution classes carry no key), and the reconciler MUST use the same derivations
|
|
171
|
+
* to query external truth — both sides are exported so they cannot drift.
|
|
172
|
+
*/
|
|
173
|
+
export const bookFlightIdempotencyKey = (toolCallId: string): string => `book-flight:${toolCallId}`;
|
|
174
|
+
export const itineraryStepIdempotencyKey = (toolCallId: string, stepName: string): string =>
|
|
175
|
+
`${toolCallId}:${stepName}`;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Approval-gated supplier booking, explicitly `uncertain`: a crash after the call may have
|
|
179
|
+
* mutated supplier state without a recorded outcome, so recovery must reconcile or stop at an
|
|
180
|
+
* Unknown Outcome — never replay automatically (DUR-009, ADR-0004).
|
|
181
|
+
*/
|
|
182
|
+
export const BookFlight = Tool.make("book_flight", {
|
|
183
|
+
parameters: FlightBookingRequest,
|
|
184
|
+
success: SupplierBookingConfirmation,
|
|
185
|
+
failure: SupplierUnavailable,
|
|
186
|
+
failureMode: "error",
|
|
187
|
+
needsApproval: true,
|
|
188
|
+
dependencies: [SupplierBookingDesk],
|
|
189
|
+
}).annotate(ToolExecutionClass, "uncertain");
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Approval-gated cancellation, annotated `idempotent`: the DECLARED external contract is that
|
|
193
|
+
* cancellation is idempotent by `bookingRef` (the supplier desk enforces it), so recovery may
|
|
194
|
+
* re-execute a prepared-but-unsettled cancel without reconciliation proof. Repeats stay
|
|
195
|
+
* observable in the supplier call counters — the annotation never claims exactly-once execution.
|
|
196
|
+
*/
|
|
197
|
+
export const CancelBooking = Tool.make("cancel_booking", {
|
|
198
|
+
parameters: CancelBookingRequest,
|
|
199
|
+
success: CancellationConfirmation,
|
|
200
|
+
failure: SupplierUnavailable,
|
|
201
|
+
failureMode: "error",
|
|
202
|
+
needsApproval: true,
|
|
203
|
+
dependencies: [SupplierBookingDesk],
|
|
204
|
+
}).annotate(ToolExecutionClass, "idempotent");
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The Durable Tool: declaring `DurableStep` in `dependencies` is what makes it durable
|
|
208
|
+
* (CONTEXT.md). Its handler divides supplier mutation into three named Steps — `reserve-flight`,
|
|
209
|
+
* `reserve-lodging`, `issue-confirmation` — each deriving its supplier idempotency key from
|
|
210
|
+
* `(toolCallId, stepName)`, so re-entry after interruption replays recorded Step results and the
|
|
211
|
+
* supplier dedupes any honestly-repeated call. The Tool itself carries no execution-class
|
|
212
|
+
* annotation: it stays fail-closed `uncertain`, and `TravelSupplierReconcilerLayer` proves
|
|
213
|
+
* re-entry safe from the keyed-Step construction instead.
|
|
214
|
+
*/
|
|
215
|
+
export const BookItinerary = Tool.make("book_itinerary", {
|
|
216
|
+
parameters: ItineraryBookingRequest,
|
|
217
|
+
success: ItineraryConfirmation,
|
|
218
|
+
failure: Schema.Union([SupplierUnavailable, DurableStepError]),
|
|
219
|
+
failureMode: "error",
|
|
220
|
+
dependencies: [DurableStep, SupplierBookingDesk],
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
export const TravelPlannerPhase5Toolkit = Toolkit.make(
|
|
224
|
+
DurableSearchFlights,
|
|
225
|
+
DurableSearchLodging,
|
|
226
|
+
DurableSearchActivities,
|
|
227
|
+
BookFlight,
|
|
228
|
+
CancelBooking,
|
|
229
|
+
BookItinerary,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const requireToolCallId = (
|
|
233
|
+
toolName: string,
|
|
234
|
+
toolCallId: string | undefined,
|
|
235
|
+
): Effect.Effect<string, SupplierUnavailable> =>
|
|
236
|
+
toolCallId === undefined
|
|
237
|
+
? Effect.fail(
|
|
238
|
+
SupplierUnavailable.make({
|
|
239
|
+
message: `${toolName} needs its stable Tool Call ID to derive the supplier idempotency key.`,
|
|
240
|
+
}),
|
|
241
|
+
)
|
|
242
|
+
: Effect.succeed(toolCallId);
|
|
243
|
+
|
|
244
|
+
export const TravelPlannerPhase5ToolkitLayer = TravelPlannerPhase5Toolkit.toLayer({
|
|
245
|
+
search_flights: (query) =>
|
|
246
|
+
Effect.flatMap(FlightCatalog, (catalog) => catalog.search(FlightQuery.make(query))),
|
|
247
|
+
search_lodging: (query) =>
|
|
248
|
+
Effect.flatMap(LodgingCatalog, (catalog) => catalog.search(LodgingQuery.make(query))),
|
|
249
|
+
search_activities: (query) =>
|
|
250
|
+
Effect.flatMap(ActivityCatalog, (catalog) => catalog.search(ActivityQuery.make(query))),
|
|
251
|
+
book_flight: (request, context) =>
|
|
252
|
+
Effect.gen(function* () {
|
|
253
|
+
const desk = yield* SupplierBookingDesk;
|
|
254
|
+
const toolCallId = yield* requireToolCallId("book_flight", context.toolCallId);
|
|
255
|
+
const record = yield* desk.book({
|
|
256
|
+
operation: "book-flight",
|
|
257
|
+
idempotencyKey: bookFlightIdempotencyKey(toolCallId),
|
|
258
|
+
detail: `flight ${request.quoteId} for ${request.travelerRef} on ${request.departOn}`,
|
|
259
|
+
});
|
|
260
|
+
return SupplierBookingConfirmation.make({
|
|
261
|
+
bookingRef: record.bookingRef,
|
|
262
|
+
status: "confirmed",
|
|
263
|
+
detail: record.detail,
|
|
264
|
+
});
|
|
265
|
+
}),
|
|
266
|
+
cancel_booking: (request) =>
|
|
267
|
+
Effect.gen(function* () {
|
|
268
|
+
const desk = yield* SupplierBookingDesk;
|
|
269
|
+
const record = yield* desk.cancel(request.bookingRef);
|
|
270
|
+
return CancellationConfirmation.make({
|
|
271
|
+
bookingRef: record.bookingRef,
|
|
272
|
+
status: "cancelled",
|
|
273
|
+
});
|
|
274
|
+
}),
|
|
275
|
+
book_itinerary: (request, context) =>
|
|
276
|
+
Effect.gen(function* () {
|
|
277
|
+
const desk = yield* SupplierBookingDesk;
|
|
278
|
+
const step = yield* DurableStep;
|
|
279
|
+
const toolCallId = yield* requireToolCallId("book_itinerary", context.toolCallId);
|
|
280
|
+
const bookStep = (
|
|
281
|
+
stepName: "reserve-flight" | "reserve-lodging" | "issue-confirmation",
|
|
282
|
+
detail: string,
|
|
283
|
+
) =>
|
|
284
|
+
step.do(
|
|
285
|
+
stepName,
|
|
286
|
+
SupplierBookingRecord,
|
|
287
|
+
desk.book({
|
|
288
|
+
operation: stepName,
|
|
289
|
+
idempotencyKey: itineraryStepIdempotencyKey(toolCallId, stepName),
|
|
290
|
+
detail,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
const flight = yield* bookStep(
|
|
294
|
+
"reserve-flight",
|
|
295
|
+
`flight ${request.quoteId} for ${request.travelerRef}`,
|
|
296
|
+
);
|
|
297
|
+
const lodging = yield* bookStep(
|
|
298
|
+
"reserve-lodging",
|
|
299
|
+
`lodging ${request.destination} for ${request.nights} nights (${request.travelerRef})`,
|
|
300
|
+
);
|
|
301
|
+
const confirmation = yield* bookStep(
|
|
302
|
+
"issue-confirmation",
|
|
303
|
+
`itinerary confirmation for ${request.travelerRef}`,
|
|
304
|
+
);
|
|
305
|
+
return ItineraryConfirmation.make({
|
|
306
|
+
flightBookingRef: flight.bookingRef,
|
|
307
|
+
lodgingBookingRef: lodging.bookingRef,
|
|
308
|
+
confirmationCode: confirmation.bookingRef,
|
|
309
|
+
});
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const decodePersistedJson = Schema.decodeUnknownEffect(PersistedJson);
|
|
314
|
+
const encodeConfirmation = Schema.encodeEffect(SupplierBookingConfirmation);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The application reconciliation policy (durability §10): a claim about EXTERNAL truth, queried
|
|
318
|
+
* from the supplier desk by the same idempotency-key derivations the handlers use.
|
|
319
|
+
*
|
|
320
|
+
* - `book_flight`: a confirmed booking under `book-flight:{toolCallId}` is recovered supplier
|
|
321
|
+
* truth — `CompletedWithResult` settles it canonically without executing anything. Absence is
|
|
322
|
+
* NOT proof the call never started (a real supplier write could be in flight), so the desk
|
|
323
|
+
* answer stays fail-closed `Uncertain`.
|
|
324
|
+
* - `book_itinerary`: every external mutation inside is a named Step whose supplier idempotency
|
|
325
|
+
* key derives from `(toolCallId, stepName)`, so re-entry is provably safe by construction —
|
|
326
|
+
* `SafeToRetry`. Committed Steps replay from their records; repeated calls dedupe at the desk.
|
|
327
|
+
* - `cancel_booking`: declared `idempotent`, so the coordinator re-executes without consulting
|
|
328
|
+
* this policy; if ever asked, the bookingRef contract makes `SafeToRetry` honest.
|
|
329
|
+
* - anything else: fail-closed `Uncertain` (AGENTS rule 11).
|
|
330
|
+
*/
|
|
331
|
+
export const TravelSupplierReconcilerLayer: Layer.Layer<
|
|
332
|
+
ToolReconciler,
|
|
333
|
+
never,
|
|
334
|
+
SupplierBookingDesk
|
|
335
|
+
> = Layer.effect(
|
|
336
|
+
ToolReconciler,
|
|
337
|
+
Effect.gen(function* () {
|
|
338
|
+
const desk = yield* SupplierBookingDesk;
|
|
339
|
+
return ToolReconciler.of({
|
|
340
|
+
reconcile: (evidence) =>
|
|
341
|
+
Effect.gen(function* () {
|
|
342
|
+
switch (evidence.toolName) {
|
|
343
|
+
case "book_flight": {
|
|
344
|
+
const key = bookFlightIdempotencyKey(evidence.toolCallId);
|
|
345
|
+
const booking = yield* desk.lookup(key);
|
|
346
|
+
if (Option.isSome(booking) && booking.value.status === "confirmed") {
|
|
347
|
+
const confirmation = yield* encodeConfirmation(
|
|
348
|
+
SupplierBookingConfirmation.make({
|
|
349
|
+
bookingRef: booking.value.bookingRef,
|
|
350
|
+
status: "confirmed",
|
|
351
|
+
detail: booking.value.detail,
|
|
352
|
+
}),
|
|
353
|
+
).pipe(Effect.flatMap(decodePersistedJson));
|
|
354
|
+
return ReconciliationCompleted.make({ result: confirmation, isFailure: false });
|
|
355
|
+
}
|
|
356
|
+
return ReconciliationUncertain.make({
|
|
357
|
+
reason: `The supplier desk shows no confirmed booking under ${key}; a write may still be in flight.`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
case "book_itinerary":
|
|
361
|
+
return ReconciliationSafeToRetry.make();
|
|
362
|
+
case "cancel_booking":
|
|
363
|
+
return ReconciliationSafeToRetry.make();
|
|
364
|
+
default:
|
|
365
|
+
return ReconciliationUncertain.make({
|
|
366
|
+
reason: `No supplier reconciliation exists for ${evidence.toolName}.`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}).pipe(
|
|
370
|
+
Effect.mapError((error) =>
|
|
371
|
+
ToolReconcilerError.make({
|
|
372
|
+
toolCallId: evidence.toolCallId,
|
|
373
|
+
message: `Supplier reconciliation failed: ${error.message}`,
|
|
374
|
+
}),
|
|
375
|
+
),
|
|
376
|
+
),
|
|
377
|
+
});
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The cumulative Travel Planner, Phase 5: the durable planner now performs consequential
|
|
383
|
+
* supplier mutation under the full uncertainty protocol — approval-gated uncertain booking,
|
|
384
|
+
* idempotent-by-contract cancellation, and one Durable Tool whose Steps carry supplier
|
|
385
|
+
* idempotency keys.
|
|
386
|
+
*/
|
|
387
|
+
export const TravelPlannerPhase5 = Agent.define("travel-planner-phase-5", {
|
|
388
|
+
input: TripRequest,
|
|
389
|
+
output: TravelBookingReport,
|
|
390
|
+
instructions: [
|
|
391
|
+
"You are the Effect Agent Travel Planner P5 booking fixture.",
|
|
392
|
+
"Search with the read-only tools, then book with book_flight, book_itinerary, or",
|
|
393
|
+
"cancel_booking exactly as scripted. Every consequential mutation is approval-gated",
|
|
394
|
+
"or Step-structured. Return only a JSON object with summary and bookingRefs.",
|
|
395
|
+
].join(" "),
|
|
396
|
+
toolkit: TravelPlannerPhase5Toolkit,
|
|
397
|
+
policy: AgentPolicy.make({
|
|
398
|
+
maxTurns: 4,
|
|
399
|
+
maxToolCalls: 6,
|
|
400
|
+
maxDuration: "30 seconds",
|
|
401
|
+
toolConcurrency: 2,
|
|
402
|
+
}),
|
|
403
|
+
description:
|
|
404
|
+
"Durably book one itinerary with prepared/settled supplier records, Unknown Outcomes, named Steps, and joined traveler input.",
|
|
405
|
+
metadata: { deploymentClass: "DN", phase: "P5" },
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
export class TravelPlannerBookingEvidenceError extends Schema.TaggedErrorClass<TravelPlannerBookingEvidenceError>()(
|
|
409
|
+
"TravelPlannerBookingEvidenceError",
|
|
410
|
+
{ message: Schema.String },
|
|
411
|
+
) {}
|
|
412
|
+
|
|
413
|
+
const bookingResultRefs = (result: unknown): ReadonlyArray<string> => {
|
|
414
|
+
if (typeof result !== "object" || result === null) return [];
|
|
415
|
+
const refs: Array<string> = [];
|
|
416
|
+
for (const [field, value] of Object.entries(result)) {
|
|
417
|
+
if (
|
|
418
|
+
typeof value === "string" &&
|
|
419
|
+
(field === "bookingRef" ||
|
|
420
|
+
field === "flightBookingRef" ||
|
|
421
|
+
field === "lodgingBookingRef" ||
|
|
422
|
+
field === "confirmationCode")
|
|
423
|
+
) {
|
|
424
|
+
refs.push(value);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return refs;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const bookingToolNames = new Set(["book_flight", "cancel_booking", "book_itinerary"]);
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Never-fabricate assertion (ROADMAP P5 exit gate): every successfully settled booking result in
|
|
434
|
+
* canonical history must reference a booking that actually exists in the supplier store. A
|
|
435
|
+
* `ToolCallSettled` whose bookingRef the supplier cannot produce would be a fabricated result —
|
|
436
|
+
* the exact lie the uncertainty protocol exists to prevent.
|
|
437
|
+
*/
|
|
438
|
+
export const assertSettledBookingsExistAtSupplier = Effect.fn(
|
|
439
|
+
"TravelPlannerPhase5.assertSettledBookingsExistAtSupplier",
|
|
440
|
+
)(function* (
|
|
441
|
+
records: ReadonlyArray<CanonicalRecordEnvelope>,
|
|
442
|
+
): Effect.fn.Return<void, TravelPlannerBookingEvidenceError, SupplierBookingDesk> {
|
|
443
|
+
const desk = yield* SupplierBookingDesk;
|
|
444
|
+
const bookings = yield* desk.bookings;
|
|
445
|
+
const knownRefs = new Set<string>(bookings.map((booking) => booking.bookingRef));
|
|
446
|
+
for (const envelope of records) {
|
|
447
|
+
const payload = envelope.record.payload;
|
|
448
|
+
if (
|
|
449
|
+
payload._tag !== "ToolCallSettled" ||
|
|
450
|
+
payload.isFailure ||
|
|
451
|
+
!bookingToolNames.has(payload.toolName)
|
|
452
|
+
) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
for (const ref of bookingResultRefs(payload.result)) {
|
|
456
|
+
if (!knownRefs.has(ref)) {
|
|
457
|
+
return yield* TravelPlannerBookingEvidenceError.make({
|
|
458
|
+
message: `Canonical record ${envelope.record.recordId} settled bookingRef ${ref}, which the supplier store cannot produce — a fabricated result.`,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Everything a durable P5 worker needs beyond the runtime stack and the supplier desk: the
|
|
467
|
+
* booking toolkit plus the deterministic P1 travel-service Layers. `SupplierBookingDesk` is
|
|
468
|
+
* deliberately NOT provided here — tests own the desk's lifetime so its counters, bookings, and
|
|
469
|
+
* crash windows survive Tool-Layer rebuilds across Attempts.
|
|
470
|
+
*/
|
|
471
|
+
export const phase5TravelPlannerWorkerLayer = Layer.mergeAll(
|
|
472
|
+
TravelPlannerPhase5ToolkitLayer,
|
|
473
|
+
FlightCatalogLayer,
|
|
474
|
+
LodgingCatalogLayer,
|
|
475
|
+
ActivityCatalogLayer,
|
|
476
|
+
).pipe(Layer.provide(CatalogLifecycle.layerNoDeps));
|