@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,923 @@
|
|
|
1
|
+
import { SubagentReservationsMemoryLive } from "@effect-agent/capabilities";
|
|
2
|
+
import { Agent } from "@effect-agent/core";
|
|
3
|
+
import {
|
|
4
|
+
DefinitionDigests,
|
|
5
|
+
DeploymentId,
|
|
6
|
+
Digest,
|
|
7
|
+
DurableWorkerBinding,
|
|
8
|
+
ProducerId,
|
|
9
|
+
type CanonicalRecordEnvelope,
|
|
10
|
+
type Receipt,
|
|
11
|
+
type ResolvedBinding,
|
|
12
|
+
type ToolReconciler,
|
|
13
|
+
} from "@effect-agent/session";
|
|
14
|
+
import { Duration, Effect, Layer, Schema, Stream } from "effect";
|
|
15
|
+
import { LanguageModel, Model, type Response } from "effect/unstable/ai";
|
|
16
|
+
|
|
17
|
+
import { TravelPlan, TripRequest } from "./definition.ts";
|
|
18
|
+
import {
|
|
19
|
+
DeterministicIdGeneratorLayer,
|
|
20
|
+
SupplierBookingDesk,
|
|
21
|
+
supplierBookingRefFor,
|
|
22
|
+
} from "./deterministic-layers.ts";
|
|
23
|
+
import {
|
|
24
|
+
TravelPlannerDurableEvidenceError,
|
|
25
|
+
TravelPlannerPhase4,
|
|
26
|
+
normalizeDurableTravelPlannerEvidence,
|
|
27
|
+
phase4TravelPlannerDefinitionDigests,
|
|
28
|
+
phase4TravelPlannerWorkerLayer,
|
|
29
|
+
} from "./phase4.ts";
|
|
30
|
+
import {
|
|
31
|
+
TravelPlannerPhase5,
|
|
32
|
+
TravelSupplierReconcilerLayer,
|
|
33
|
+
bookFlightIdempotencyKey,
|
|
34
|
+
phase5TravelPlannerDefinitionDigests,
|
|
35
|
+
phase5TravelPlannerWorkerLayer,
|
|
36
|
+
} from "./phase5.ts";
|
|
37
|
+
import { expectedTravelPlan } from "./scenarios.ts";
|
|
38
|
+
import {
|
|
39
|
+
durableDestinationResearchHandlersLayer,
|
|
40
|
+
durableResearchCallId,
|
|
41
|
+
durableResearchShortlist,
|
|
42
|
+
s2CoordinatorDigests,
|
|
43
|
+
s2ResearcherDigests,
|
|
44
|
+
} from "./subagents-durable.ts";
|
|
45
|
+
import {
|
|
46
|
+
DestinationGuide,
|
|
47
|
+
DestinationResearcher,
|
|
48
|
+
DestinationResearcherToolkitLayer,
|
|
49
|
+
DestinationShortlist,
|
|
50
|
+
ResearchDispatchGate,
|
|
51
|
+
ResearchMission,
|
|
52
|
+
TravelCoordinator,
|
|
53
|
+
destinationLookup,
|
|
54
|
+
encodedDestinationReport,
|
|
55
|
+
} from "./subagents.ts";
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Phase 6 (P6): the SAME cumulative Travel Planner on the Cloudflare Durable
|
|
59
|
+
// Object runtime, deployment class DC. This module is deliberately
|
|
60
|
+
// platform-neutral (it imports no Cloudflare types): the worker Bindings it
|
|
61
|
+
// builds are plain `ResolvedBinding` values a Conversation Object registers,
|
|
62
|
+
// and the cross-platform evidence normal form is shared by the DN and DC
|
|
63
|
+
// halves of the equivalence suite (plan §1.8, D-P6-6).
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The Phase 6 profile: the P4/P5/S2 Travel Planner claims re-earned on the Cloudflare Durable
|
|
68
|
+
* Object runtime (deployment class `DC`), where eviction and alarm redelivery replace process
|
|
69
|
+
* kill and restart as the exercised recovery path. `cloudflareEquivalence` is the claim the S2
|
|
70
|
+
* fixture explicitly deferred to P6 (`TravelPlannerSubagentDurabilityProfile` pins it `false`
|
|
71
|
+
* for `DN`): it flips to `true` here ONLY because the phase-6 suites assert byte-equal
|
|
72
|
+
* cross-platform normalized canonical evidence against one committed golden. Exactly-once
|
|
73
|
+
* EXTERNAL effects remain — deliberately — unclaimed on every platform (DUR-003).
|
|
74
|
+
*/
|
|
75
|
+
export class TravelPlannerCloudflareProfile extends Schema.Class<TravelPlannerCloudflareProfile>(
|
|
76
|
+
"@effect-agent/testing/travel-planner/TravelPlannerCloudflareProfile",
|
|
77
|
+
)({
|
|
78
|
+
deploymentClass: Schema.Literal("DC"),
|
|
79
|
+
durableAcceptedWork: Schema.Literal(true),
|
|
80
|
+
canonicalSchemaVersion: Schema.Literal(1),
|
|
81
|
+
/** P5 semantics under DC recovery: prepared/settled records, Unknown Outcomes, approvals. */
|
|
82
|
+
supplierBookingUncertaintyProtocol: Schema.Literal(true),
|
|
83
|
+
/** S2 semantics under DC recovery: cross-Object establishment/join, completed child never re-runs. */
|
|
84
|
+
durableAttachedSubagents: Schema.Literal(true),
|
|
85
|
+
/** DN and DC produce byte-equal cross-platform normalized canonical evidence (one golden). */
|
|
86
|
+
cloudflareEquivalence: Schema.Literal(true),
|
|
87
|
+
/** Never claimed at any phase on any platform (DUR-003). */
|
|
88
|
+
exactlyOnceExternalEffects: Schema.Literal(false),
|
|
89
|
+
}) {}
|
|
90
|
+
|
|
91
|
+
export const phase6TravelPlannerProfile = TravelPlannerCloudflareProfile.make({
|
|
92
|
+
deploymentClass: "DC",
|
|
93
|
+
durableAcceptedWork: true,
|
|
94
|
+
canonicalSchemaVersion: 1,
|
|
95
|
+
supplierBookingUncertaintyProtocol: true,
|
|
96
|
+
durableAttachedSubagents: true,
|
|
97
|
+
cloudflareEquivalence: true,
|
|
98
|
+
exactlyOnceExternalEffects: false,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const phase6TravelPlannerDeploymentId = Schema.decodeSync(DeploymentId)(
|
|
102
|
+
"travel-planner-p6-deployment",
|
|
103
|
+
);
|
|
104
|
+
/** Producer prefix of the DC host; each Object mints `{prefix}:{conversationId}`. */
|
|
105
|
+
export const phase6TravelPlannerProducerPrefix = "travel-planner-p6-producer";
|
|
106
|
+
/** The full producer identity one DC Conversation Object mints for itself. */
|
|
107
|
+
export const phase6TravelPlannerProducerId = (conversationId: string): ProducerId =>
|
|
108
|
+
Schema.decodeSync(ProducerId)(`${phase6TravelPlannerProducerPrefix}:${conversationId}`);
|
|
109
|
+
|
|
110
|
+
const digestOf = (character: string) => Schema.decodeSync(Digest)(character.repeat(64));
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Registration digests of the GATED planner Binding: the same `TravelPlannerPhase4` definition
|
|
114
|
+
* bound to a model whose first response waits on a test gate, addressable separately so the
|
|
115
|
+
* admission-limits rows can hold a lane busy deterministically without touching the ordinary
|
|
116
|
+
* planner registration.
|
|
117
|
+
*/
|
|
118
|
+
export const phase6GatedPlannerDefinitionDigests = DefinitionDigests.make({
|
|
119
|
+
agent: digestOf("9"),
|
|
120
|
+
model: digestOf("8"),
|
|
121
|
+
tools: digestOf("7"),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Cross-platform evidence normal form (plan §1.8, D-P6-6)
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/** The run-specific identities the cross-platform normal form scrubs. */
|
|
129
|
+
export interface CrossPlatformEvidenceIdentity {
|
|
130
|
+
/** The Conversation lane the evidence came from. */
|
|
131
|
+
readonly conversationId: string;
|
|
132
|
+
/** The host's deployment identity (`DeploymentId` on every record envelope). */
|
|
133
|
+
readonly deploymentId: string;
|
|
134
|
+
/** The full producer identity of the run (DN: configured; DC: `{prefix}:{conversationId}`). */
|
|
135
|
+
readonly producerId: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const decodeComparableJson = Schema.decodeUnknownEffect(Schema.Json);
|
|
139
|
+
|
|
140
|
+
/** The base normal form's element shape, re-decoded so sequences can be renumbered. */
|
|
141
|
+
const ComparableEnvelope = Schema.Struct({
|
|
142
|
+
batchId: Schema.String,
|
|
143
|
+
sequence: Schema.Number,
|
|
144
|
+
record: Schema.Json,
|
|
145
|
+
});
|
|
146
|
+
const decodeComparableEnvelopes = Schema.decodeUnknownEffect(Schema.Array(ComparableEnvelope));
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The CROSS-PLATFORM extension of `normalizeDurableTravelPlannerEvidence` (D-P6-6): after the
|
|
150
|
+
* base normalization replaces the two ledger-minted identities (which also normalizes the
|
|
151
|
+
* DC-format routable `{uuidv7}:{conversationId}` Submission identities and everything derived
|
|
152
|
+
* from them), this form additionally scrubs everything that legitimately differs between a DN
|
|
153
|
+
* process and a DC Durable Object over the same scenario:
|
|
154
|
+
*
|
|
155
|
+
* - `RepairAnnotated` audit records are dropped BEFORE normalization and the canonical
|
|
156
|
+
* sequence is renumbered to the surviving order: repairs are DUR-013 evidence of recovery
|
|
157
|
+
* itself, legally present in a recovered run and legally absent from an uninterrupted
|
|
158
|
+
* control (on DC even a CLEAN run carries one, because every pass reconciles before it
|
|
159
|
+
* claims, so the ready lane's input is applied through the recovery path). Canonical ORDER
|
|
160
|
+
* is the durability §5 claim; sequence contiguity is a platform artifact of who appended;
|
|
161
|
+
* - the Conversation identity (DC lanes mint unique names per test run);
|
|
162
|
+
* - the deployment and producer identities (host configuration, not canonical semantics);
|
|
163
|
+
* - `createdAt` commit timestamps (wall clock);
|
|
164
|
+
* - 64-hex digests (they hash RAW content that legally embeds run-specific identity, so they
|
|
165
|
+
* can never be byte-equal across runs; chain integrity is asserted separately by the
|
|
166
|
+
* adapters and the convergence helpers).
|
|
167
|
+
*
|
|
168
|
+
* Two runs whose cross-platform normalized evidence is byte-equal took canonically equivalent
|
|
169
|
+
* histories — the exact sense in which durability §5 permits storage differences while
|
|
170
|
+
* requiring the same observable ordering. Both the DN and DC suites assert equality against
|
|
171
|
+
* the one committed `phase6TravelPlannerGoldenEvidence`, so DN ≡ DC transitively.
|
|
172
|
+
*/
|
|
173
|
+
export const normalizeCrossPlatformTravelPlannerEvidence = Effect.fn(
|
|
174
|
+
"TravelPlannerPhase6.normalizeCrossPlatformTravelPlannerEvidence",
|
|
175
|
+
)(function* (
|
|
176
|
+
records: ReadonlyArray<CanonicalRecordEnvelope>,
|
|
177
|
+
receipt: Receipt,
|
|
178
|
+
identity: CrossPlatformEvidenceIdentity,
|
|
179
|
+
): Effect.fn.Return<Schema.Json, TravelPlannerDurableEvidenceError> {
|
|
180
|
+
const canonical = records.filter(
|
|
181
|
+
(envelope) => envelope.record.payload._tag !== "RepairAnnotated",
|
|
182
|
+
);
|
|
183
|
+
const base = yield* normalizeDurableTravelPlannerEvidence(canonical, receipt);
|
|
184
|
+
const scrubbed: unknown = JSON.parse(
|
|
185
|
+
JSON.stringify(base)
|
|
186
|
+
.replaceAll(identity.producerId, "{producerId}")
|
|
187
|
+
.replaceAll(identity.deploymentId, "{deploymentId}")
|
|
188
|
+
.replaceAll(identity.conversationId, "{conversationId}")
|
|
189
|
+
.replaceAll(/\d{4}-\d{2}-\d{2}T[0-9:.]+Z/g, "{timestamp}")
|
|
190
|
+
.replaceAll(/"[0-9a-f]{64}"/g, '"{digest}"'),
|
|
191
|
+
);
|
|
192
|
+
const comparable = yield* decodeComparableEnvelopes(scrubbed).pipe(
|
|
193
|
+
Effect.mapError((error) =>
|
|
194
|
+
TravelPlannerDurableEvidenceError.make({
|
|
195
|
+
message: `Cross-platform normalized evidence lost the comparable shape: ${error.message}`,
|
|
196
|
+
}),
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
const renumbered = comparable.map((entry, index) => ({
|
|
200
|
+
batchId: entry.batchId,
|
|
201
|
+
sequence: index + 1,
|
|
202
|
+
record: entry.record,
|
|
203
|
+
}));
|
|
204
|
+
return yield* decodeComparableJson(renumbered).pipe(
|
|
205
|
+
Effect.mapError((error) =>
|
|
206
|
+
TravelPlannerDurableEvidenceError.make({
|
|
207
|
+
message: `Cross-platform normalized evidence is not comparable JSON: ${error.message}`,
|
|
208
|
+
}),
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Prompt-aware scripted models. A DC Attempt may resume on a FRESH Object
|
|
215
|
+
// incarnation whose Layers (and any in-memory turn counter) were rebuilt, so
|
|
216
|
+
// every phase-6 model derives its response purely from the committed history
|
|
217
|
+
// in its prompt — the way a real model would — instead of from call order.
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
const scriptedUsage = { inputTokens: { total: 128 }, outputTokens: { total: 96 } };
|
|
221
|
+
|
|
222
|
+
const promptAwareModel = (
|
|
223
|
+
name: string,
|
|
224
|
+
decide: (promptJson: string) => Stream.Stream<Response.StreamPartEncoded>,
|
|
225
|
+
) =>
|
|
226
|
+
Model.make(
|
|
227
|
+
"scripted",
|
|
228
|
+
name,
|
|
229
|
+
Layer.effect(
|
|
230
|
+
LanguageModel.LanguageModel,
|
|
231
|
+
LanguageModel.make({
|
|
232
|
+
generateText: () => Effect.succeed([]),
|
|
233
|
+
streamText: (options) =>
|
|
234
|
+
Stream.unwrap(Effect.sync(() => decide(JSON.stringify(options.prompt)))),
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
/** The P1/P4 happy-path Tool Call identities (scenarios.ts, byte-stable since P1). */
|
|
240
|
+
export const phase6FlightCallId = "flight-call-1";
|
|
241
|
+
export const phase6LodgingCallId = "lodging-call-1";
|
|
242
|
+
export const phase6ActivityCallId = "activity-call-1";
|
|
243
|
+
|
|
244
|
+
/** Turn 1 of the planner: the SAME three search declarations as `phase1HappyPathTurns`. */
|
|
245
|
+
const plannerSearchTurnParts: ReadonlyArray<Response.StreamPartEncoded> = [
|
|
246
|
+
{
|
|
247
|
+
type: "tool-call",
|
|
248
|
+
id: phase6FlightCallId,
|
|
249
|
+
name: "search_flights",
|
|
250
|
+
params: { origin: "SFO", destination: "LHR", departOn: "2026-09-14", travelers: 2 },
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: "tool-call",
|
|
254
|
+
id: phase6LodgingCallId,
|
|
255
|
+
name: "search_lodging",
|
|
256
|
+
params: { destination: "LHR", departOn: "2026-09-14", nights: 4, travelers: 2 },
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
type: "tool-call",
|
|
260
|
+
id: phase6ActivityCallId,
|
|
261
|
+
name: "search_activities",
|
|
262
|
+
params: { destination: "LHR", departOn: "2026-09-14", nights: 4, travelers: 2 },
|
|
263
|
+
},
|
|
264
|
+
{ type: "finish", reason: "tool-calls", usage: scriptedUsage },
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
/** Turn 2 of the planner: the SAME itinerary text as `phase1HappyPathTurns`. */
|
|
268
|
+
const plannerPlanTurnParts: ReadonlyArray<Response.StreamPartEncoded> = [
|
|
269
|
+
{ type: "text-start", id: "itinerary-json" },
|
|
270
|
+
{
|
|
271
|
+
type: "text-delta",
|
|
272
|
+
id: "itinerary-json",
|
|
273
|
+
delta: JSON.stringify(Schema.encodeSync(TravelPlan)(expectedTravelPlan)),
|
|
274
|
+
},
|
|
275
|
+
{ type: "text-end", id: "itinerary-json" },
|
|
276
|
+
{ type: "finish", reason: "stop", usage: scriptedUsage },
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const plannerDecide = (promptJson: string): Stream.Stream<Response.StreamPartEncoded> =>
|
|
280
|
+
promptJson.includes(phase6FlightCallId)
|
|
281
|
+
? Stream.fromIterable(plannerPlanTurnParts)
|
|
282
|
+
: Stream.fromIterable(plannerSearchTurnParts);
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The P4 planner script (`phase1HappyPathTurns`) as a prompt-aware model: once the search
|
|
286
|
+
* batch is committed history, every later request gets the plan — identical parts, so the DC
|
|
287
|
+
* canonical evidence is byte-equivalent to the DN ScriptedModel run after normalization.
|
|
288
|
+
*/
|
|
289
|
+
export const phase6PlannerModel = promptAwareModel("travel-planner-phase-4", plannerDecide);
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Deterministic test gate for the admission-limits rows. Module state is
|
|
293
|
+
// intentionally NOT durable: it plays the external world's role (a slow
|
|
294
|
+
// upstream model), never Conversation state.
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
const releasedPlannerGates = new Set<string>();
|
|
298
|
+
|
|
299
|
+
/** Release the gated planner model for one `[gate:...]` marker. */
|
|
300
|
+
export const releasePhase6PlannerGate = (marker: string): void => {
|
|
301
|
+
releasedPlannerGates.add(marker);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/** Re-close one gate marker (fresh suites reuse markers safely). */
|
|
305
|
+
export const resetPhase6PlannerGate = (marker: string): void => {
|
|
306
|
+
releasedPlannerGates.delete(marker);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const awaitPlannerGate = (marker: string): Effect.Effect<void> =>
|
|
310
|
+
Effect.gen(function* () {
|
|
311
|
+
while (!releasedPlannerGates.has(marker)) {
|
|
312
|
+
yield* Effect.sleep(Duration.millis(10));
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const gateMarkerFromPrompt = (promptJson: string): string =>
|
|
317
|
+
/\[gate:([^\]]+)\]/.exec(promptJson)?.[1] ?? "unknown-gate";
|
|
318
|
+
|
|
319
|
+
/** A trip whose request text carries the gate marker the gated model waits on. */
|
|
320
|
+
export const phase6GatedTrip = (marker: string): TripRequest =>
|
|
321
|
+
Schema.decodeUnknownSync(TripRequest)({
|
|
322
|
+
request: `Plan a review-only London trip, but wait for the concierge. [gate:${marker}]`,
|
|
323
|
+
origin: "SFO",
|
|
324
|
+
destination: "LHR",
|
|
325
|
+
departOn: "2026-09-14",
|
|
326
|
+
nights: 4,
|
|
327
|
+
travelers: 2,
|
|
328
|
+
budgetCents: 350_000,
|
|
329
|
+
currency: "USD",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The SAME planner behavior with a hanging first response: the model waits on the released
|
|
334
|
+
* gate before answering, keeping its lane durably busy so queue-depth admission limits can be
|
|
335
|
+
* exercised deterministically.
|
|
336
|
+
*/
|
|
337
|
+
export const phase6GatedPlannerModel = Model.make(
|
|
338
|
+
"scripted",
|
|
339
|
+
"travel-planner-phase-4-gated",
|
|
340
|
+
Layer.effect(
|
|
341
|
+
LanguageModel.LanguageModel,
|
|
342
|
+
LanguageModel.make({
|
|
343
|
+
generateText: () => Effect.succeed([]),
|
|
344
|
+
streamText: (options) =>
|
|
345
|
+
Stream.unwrap(
|
|
346
|
+
Effect.sync(() => {
|
|
347
|
+
const promptJson = JSON.stringify(options.prompt);
|
|
348
|
+
return promptJson.includes(phase6FlightCallId)
|
|
349
|
+
? plannerDecide(promptJson)
|
|
350
|
+
: Stream.fromEffectDrain(awaitPlannerGate(gateMarkerFromPrompt(promptJson))).pipe(
|
|
351
|
+
Stream.concat(plannerDecide(promptJson)),
|
|
352
|
+
);
|
|
353
|
+
}),
|
|
354
|
+
),
|
|
355
|
+
}),
|
|
356
|
+
),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// P5 booking slice: the SAME phase-5 booking agent and supplier desk. The desk
|
|
361
|
+
// is a module-level singleton because it IS the external supplier: like a real
|
|
362
|
+
// supplier's ledger, its bookings and call counters survive `ctx.abort()` and
|
|
363
|
+
// incarnation loss, which is exactly what the never-fabricate and
|
|
364
|
+
// executed-once assertions measure.
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
const sharedSupplierDesk = Effect.runSync(
|
|
368
|
+
Effect.flatMap(SupplierBookingDesk, Effect.succeed).pipe(
|
|
369
|
+
Effect.provide(SupplierBookingDesk.layer),
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
/** The shared external supplier desk instance (module-level external truth). */
|
|
374
|
+
export const phase6SupplierDesk = sharedSupplierDesk;
|
|
375
|
+
|
|
376
|
+
/** Layer handing the shared desk to Bindings, reconcilers, and assertions. */
|
|
377
|
+
export const phase6SupplierDeskLayer: Layer.Layer<SupplierBookingDesk> = Layer.succeed(
|
|
378
|
+
SupplierBookingDesk,
|
|
379
|
+
sharedSupplierDesk,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The REAL P5 supplier reconciliation policy over the shared desk, closed to no requirements
|
|
384
|
+
* so a Conversation Object can install it directly: `book_flight` recovers only from supplier
|
|
385
|
+
* truth (absence stays fail-closed `Uncertain` → durable Unknown Outcome), keyed Steps are
|
|
386
|
+
* provably re-enterable.
|
|
387
|
+
*/
|
|
388
|
+
export const phase6SupplierReconcilerLayer: Layer.Layer<ToolReconciler> =
|
|
389
|
+
TravelSupplierReconcilerLayer.pipe(Layer.provide(phase6SupplierDeskLayer));
|
|
390
|
+
|
|
391
|
+
const bookingMarkerFromPrompt = (promptJson: string): string =>
|
|
392
|
+
/\[case:([^\]]+)\]/.exec(promptJson)?.[1] ?? "unknown-case";
|
|
393
|
+
|
|
394
|
+
/** The deterministic booking Tool Call identity for one `[case:...]` marker. */
|
|
395
|
+
export const phase6BookingToolCallId = (marker: string): string => `book-${marker}`;
|
|
396
|
+
|
|
397
|
+
/** The bookingRef the supplier desk mints for one marker's approved booking. */
|
|
398
|
+
export const phase6BookingRef = (marker: string): string =>
|
|
399
|
+
supplierBookingRefFor(bookFlightIdempotencyKey(phase6BookingToolCallId(marker)));
|
|
400
|
+
|
|
401
|
+
/** A trip whose request text carries the per-lane booking case marker. */
|
|
402
|
+
export const phase6BookingTrip = (marker: string): TripRequest =>
|
|
403
|
+
Schema.decodeUnknownSync(TripRequest)({
|
|
404
|
+
request: `Book the approved London flight for the traveler. [case:${marker}]`,
|
|
405
|
+
origin: "SFO",
|
|
406
|
+
destination: "LHR",
|
|
407
|
+
departOn: "2026-09-14",
|
|
408
|
+
nights: 4,
|
|
409
|
+
travelers: 2,
|
|
410
|
+
budgetCents: 350_000,
|
|
411
|
+
currency: "USD",
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const bookingCallParts = (marker: string): ReadonlyArray<Response.StreamPartEncoded> => [
|
|
415
|
+
{
|
|
416
|
+
type: "tool-call",
|
|
417
|
+
id: phase6BookingToolCallId(marker),
|
|
418
|
+
name: "book_flight",
|
|
419
|
+
params: {
|
|
420
|
+
quoteId: "quote-sfo-lhr-001",
|
|
421
|
+
travelerRef: `traveler-${marker}`,
|
|
422
|
+
departOn: "2026-09-14",
|
|
423
|
+
},
|
|
424
|
+
providerExecuted: false,
|
|
425
|
+
},
|
|
426
|
+
{ type: "finish", reason: "tool-calls", usage: scriptedUsage },
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
const bookingReportParts = (marker: string): ReadonlyArray<Response.StreamPartEncoded> => [
|
|
430
|
+
{ type: "text-start", id: "booking-report" },
|
|
431
|
+
{
|
|
432
|
+
type: "text-delta",
|
|
433
|
+
id: "booking-report",
|
|
434
|
+
delta: JSON.stringify({
|
|
435
|
+
summary: "trip booked",
|
|
436
|
+
bookingRefs: [phase6BookingRef(marker)],
|
|
437
|
+
}),
|
|
438
|
+
},
|
|
439
|
+
{ type: "text-end", id: "booking-report" },
|
|
440
|
+
{ type: "finish", reason: "stop", usage: scriptedUsage },
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The P5 booking script as a prompt-aware model: request 1 declares the approval-gated
|
|
445
|
+
* `book_flight` call (identity derived from the lane's `[case:...]` marker so supplier
|
|
446
|
+
* idempotency keys never collide across lanes); once that call is committed history, the model
|
|
447
|
+
* writes the booking report.
|
|
448
|
+
*/
|
|
449
|
+
export const phase6BookingModel = promptAwareModel("travel-planner-phase-5", (promptJson) => {
|
|
450
|
+
const marker = bookingMarkerFromPrompt(promptJson);
|
|
451
|
+
return promptJson.includes(phase6BookingToolCallId(marker))
|
|
452
|
+
? Stream.fromIterable(bookingReportParts(marker))
|
|
453
|
+
: Stream.fromIterable(bookingCallParts(marker));
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
// S2 delegation slice: the SAME coordinator → destination-researcher pair.
|
|
458
|
+
// The guide invocation counter is module state for the same reason as the
|
|
459
|
+
// desk: it is the child's external side-effect record, and "the completed
|
|
460
|
+
// child never re-executes" is asserted against it across evictions.
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
let guideInvocations = 0;
|
|
464
|
+
|
|
465
|
+
/** Deterministic guide-lookup handler executions across every incarnation. */
|
|
466
|
+
export const phase6GuideInvocationCount = (): number => guideInvocations;
|
|
467
|
+
|
|
468
|
+
const countingGuideLayer = Layer.succeed(
|
|
469
|
+
DestinationGuide,
|
|
470
|
+
DestinationGuide.of({
|
|
471
|
+
lookup: (query) =>
|
|
472
|
+
Effect.suspend(() => {
|
|
473
|
+
guideInvocations += 1;
|
|
474
|
+
return destinationLookup(query);
|
|
475
|
+
}),
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
/** The one-candidate research mission of the DC delegation slice. */
|
|
480
|
+
export const phase6ResearchMission = Schema.decodeUnknownSync(ResearchMission)({
|
|
481
|
+
request: "Shortlist one September culture city for the DC delegation slice.",
|
|
482
|
+
candidates: ["LHR"],
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
export const phase6ResearchDestination = "LHR";
|
|
486
|
+
|
|
487
|
+
/** The child's scripted guide-lookup Tool Call identity. */
|
|
488
|
+
export const phase6ChildLookupCallId = `lookup-${phase6ResearchDestination}`;
|
|
489
|
+
|
|
490
|
+
const coordinatorDelegationParts: ReadonlyArray<Response.StreamPartEncoded> = [
|
|
491
|
+
{
|
|
492
|
+
type: "tool-call",
|
|
493
|
+
id: durableResearchCallId,
|
|
494
|
+
name: "delegate_destination_research",
|
|
495
|
+
params: { destination: phase6ResearchDestination, focus: "museums" },
|
|
496
|
+
providerExecuted: false,
|
|
497
|
+
},
|
|
498
|
+
{ type: "finish", reason: "tool-calls", usage: scriptedUsage },
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
const coordinatorShortlistParts: ReadonlyArray<Response.StreamPartEncoded> = [
|
|
502
|
+
{ type: "text-start", id: "shortlist" },
|
|
503
|
+
{
|
|
504
|
+
type: "text-delta",
|
|
505
|
+
id: "shortlist",
|
|
506
|
+
delta: JSON.stringify(
|
|
507
|
+
Schema.encodeSync(DestinationShortlist)(durableResearchShortlist(phase6ResearchDestination)),
|
|
508
|
+
),
|
|
509
|
+
},
|
|
510
|
+
{ type: "text-end", id: "shortlist" },
|
|
511
|
+
{ type: "finish", reason: "stop", usage: scriptedUsage },
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
const researcherLookupParts: ReadonlyArray<Response.StreamPartEncoded> = [
|
|
515
|
+
{
|
|
516
|
+
type: "tool-call",
|
|
517
|
+
id: phase6ChildLookupCallId,
|
|
518
|
+
name: "lookup_destination",
|
|
519
|
+
params: { destination: phase6ResearchDestination },
|
|
520
|
+
providerExecuted: false,
|
|
521
|
+
},
|
|
522
|
+
{ type: "finish", reason: "tool-calls", usage: scriptedUsage },
|
|
523
|
+
];
|
|
524
|
+
|
|
525
|
+
const researcherReportParts: ReadonlyArray<Response.StreamPartEncoded> = [
|
|
526
|
+
{ type: "text-start", id: "destination-report" },
|
|
527
|
+
{
|
|
528
|
+
type: "text-delta",
|
|
529
|
+
id: "destination-report",
|
|
530
|
+
delta: encodedDestinationReport(phase6ResearchDestination),
|
|
531
|
+
},
|
|
532
|
+
{ type: "text-end", id: "destination-report" },
|
|
533
|
+
{ type: "finish", reason: "stop", usage: scriptedUsage },
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
/** Prompt-aware S2 coordinator: delegation call first, shortlist once it is history. */
|
|
537
|
+
export const phase6CoordinatorModel = promptAwareModel("travel-coordinator-p6", (promptJson) =>
|
|
538
|
+
promptJson.includes(durableResearchCallId)
|
|
539
|
+
? Stream.fromIterable(coordinatorShortlistParts)
|
|
540
|
+
: Stream.fromIterable(coordinatorDelegationParts),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
let researcherGateReleased = false;
|
|
544
|
+
|
|
545
|
+
/** Allow the researcher's FIRST model response to proceed (sticky across incarnations). */
|
|
546
|
+
export const releasePhase6ResearcherGate = (): void => {
|
|
547
|
+
researcherGateReleased = true;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
/** Re-close the researcher gate (each delegation scenario starts gated). */
|
|
551
|
+
export const resetPhase6ResearcherGate = (): void => {
|
|
552
|
+
researcherGateReleased = false;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const awaitResearcherGate: Effect.Effect<void> = Effect.gen(function* () {
|
|
556
|
+
while (!researcherGateReleased) {
|
|
557
|
+
yield* Effect.sleep(Duration.millis(10));
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Prompt-aware S2 researcher: guide lookup first, report once it is history. The FIRST
|
|
563
|
+
* response waits on the researcher gate — a stand-in for real model latency. The child's own
|
|
564
|
+
* Object may legally start its Attempt the moment its routed admission commits, while the
|
|
565
|
+
* parent is still appending the lineage record into the child's log; a child whose first
|
|
566
|
+
* batch commits during that window races the parent's append on one tail. Real models answer
|
|
567
|
+
* in seconds, so establishment always wins that race in production; the gate reproduces that
|
|
568
|
+
* timing deterministically instead of relying on scheduler luck.
|
|
569
|
+
*/
|
|
570
|
+
export const phase6ResearcherModel = promptAwareModel("destination-researcher-p6", (promptJson) =>
|
|
571
|
+
promptJson.includes(phase6ChildLookupCallId)
|
|
572
|
+
? Stream.fromIterable(researcherReportParts)
|
|
573
|
+
: Stream.fromEffectDrain(awaitResearcherGate).pipe(
|
|
574
|
+
Stream.concat(Stream.fromIterable(researcherLookupParts)),
|
|
575
|
+
),
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// Worker Binding registrations for the DC Conversation Object
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Every phase-6 Travel Planner worker Binding, captured with its requirement Contexts
|
|
584
|
+
* (spec/subagents.md §11): the P4 planner and its gated twin, the P5 booking agent over the
|
|
585
|
+
* shared supplier desk, and the S2 coordinator/researcher pair wired through the durable
|
|
586
|
+
* delegation Layer. A Conversation Object registers these via its `bindings` option; the
|
|
587
|
+
* capture runs once per incarnation, and everything stateful the assertions rely on (desk,
|
|
588
|
+
* guide counter, gates) lives at module level so it survives incarnation loss.
|
|
589
|
+
*/
|
|
590
|
+
export const makePhase6TravelPlannerBindings: Effect.Effect<ReadonlyArray<ResolvedBinding>> =
|
|
591
|
+
Effect.gen(function* () {
|
|
592
|
+
const planner: ResolvedBinding = yield* DurableWorkerBinding.make(
|
|
593
|
+
Agent.withModel(TravelPlannerPhase4, phase6PlannerModel),
|
|
594
|
+
phase4TravelPlannerDefinitionDigests,
|
|
595
|
+
).pipe(Effect.provide(phase4TravelPlannerWorkerLayer));
|
|
596
|
+
|
|
597
|
+
const gatedPlanner: ResolvedBinding = yield* DurableWorkerBinding.make(
|
|
598
|
+
Agent.withModel(TravelPlannerPhase4, phase6GatedPlannerModel),
|
|
599
|
+
phase6GatedPlannerDefinitionDigests,
|
|
600
|
+
).pipe(Effect.provide(phase4TravelPlannerWorkerLayer));
|
|
601
|
+
|
|
602
|
+
const booking: ResolvedBinding = yield* DurableWorkerBinding.make(
|
|
603
|
+
Agent.withModel(TravelPlannerPhase5, phase6BookingModel),
|
|
604
|
+
phase5TravelPlannerDefinitionDigests,
|
|
605
|
+
).pipe(
|
|
606
|
+
Effect.provide(
|
|
607
|
+
phase5TravelPlannerWorkerLayer.pipe(Layer.provideMerge(phase6SupplierDeskLayer)),
|
|
608
|
+
),
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
const researcherBinding = Agent.withModel(DestinationResearcher, phase6ResearcherModel);
|
|
612
|
+
const childToolkitLayer = DestinationResearcherToolkitLayer.pipe(
|
|
613
|
+
Layer.provideMerge(countingGuideLayer),
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
const coordinator: ResolvedBinding = yield* DurableWorkerBinding.make(
|
|
617
|
+
Agent.withModel(TravelCoordinator, phase6CoordinatorModel),
|
|
618
|
+
s2CoordinatorDigests,
|
|
619
|
+
).pipe(
|
|
620
|
+
Effect.provide(
|
|
621
|
+
durableDestinationResearchHandlersLayer(researcherBinding).pipe(
|
|
622
|
+
Layer.provide(
|
|
623
|
+
Layer.mergeAll(
|
|
624
|
+
childToolkitLayer,
|
|
625
|
+
SubagentReservationsMemoryLive,
|
|
626
|
+
DeterministicIdGeneratorLayer,
|
|
627
|
+
ResearchDispatchGate.layerOpen,
|
|
628
|
+
),
|
|
629
|
+
),
|
|
630
|
+
),
|
|
631
|
+
),
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
const researcher: ResolvedBinding = yield* DurableWorkerBinding.make(
|
|
635
|
+
researcherBinding,
|
|
636
|
+
s2ResearcherDigests,
|
|
637
|
+
).pipe(Effect.provide(childToolkitLayer));
|
|
638
|
+
|
|
639
|
+
return [planner, gatedPlanner, booking, coordinator, researcher];
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
// The committed golden normalized-evidence fixture (D-P6-6)
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* The committed cross-platform normalized canonical evidence of ONE uninterrupted Travel
|
|
648
|
+
* Planner planning Submission (the P1/P4 happy path: canonical input, the search Turn, three
|
|
649
|
+
* Tool settlements, the plan Turn, one Settlement). `travel-planner-phase6.test.ts` asserts
|
|
650
|
+
* the DN run equals this value and `travel-planner-dc.test.ts` asserts the DC run equals this
|
|
651
|
+
* value, so the two platforms' canonical outcomes are byte-equivalent transitively — the P6
|
|
652
|
+
* exit gate "Travel Planner produces equivalent canonical outcomes under DN and DC".
|
|
653
|
+
*
|
|
654
|
+
* Regenerate ONLY when the Travel Planner scenario itself changes, by printing either suite's
|
|
655
|
+
* normalized value; both suites must then agree on the new golden.
|
|
656
|
+
*/
|
|
657
|
+
export const phase6TravelPlannerGoldenEvidence: Schema.Json = [
|
|
658
|
+
{
|
|
659
|
+
batchId: "conversation-created:{conversationId}",
|
|
660
|
+
sequence: 1,
|
|
661
|
+
record: {
|
|
662
|
+
recordId: "conversation-created:{conversationId}",
|
|
663
|
+
family: "conversation",
|
|
664
|
+
schemaVersion: 1,
|
|
665
|
+
createdAt: "{timestamp}",
|
|
666
|
+
deploymentId: "{deploymentId}",
|
|
667
|
+
payload: {
|
|
668
|
+
_tag: "ConversationCreated",
|
|
669
|
+
agentId: "travel-planner-phase-4",
|
|
670
|
+
definitions: {
|
|
671
|
+
agent: "{digest}",
|
|
672
|
+
model: "{digest}",
|
|
673
|
+
tools: "{digest}",
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
batchId: "submission-input:{submissionId}",
|
|
680
|
+
sequence: 2,
|
|
681
|
+
record: {
|
|
682
|
+
recordId: "input:{submissionId}",
|
|
683
|
+
family: "conversation",
|
|
684
|
+
schemaVersion: 1,
|
|
685
|
+
createdAt: "{timestamp}",
|
|
686
|
+
deploymentId: "{deploymentId}",
|
|
687
|
+
payload: {
|
|
688
|
+
_tag: "UserInputRecorded",
|
|
689
|
+
submissionId: "{submissionId}",
|
|
690
|
+
kind: "user",
|
|
691
|
+
runId: "run:{submissionId}",
|
|
692
|
+
input: {
|
|
693
|
+
request:
|
|
694
|
+
"Plan a review-only London trip using the deterministic flight, lodging, and activity searches.",
|
|
695
|
+
origin: "SFO",
|
|
696
|
+
destination: "LHR",
|
|
697
|
+
departOn: "2026-09-14",
|
|
698
|
+
nights: 4,
|
|
699
|
+
travelers: 2,
|
|
700
|
+
budgetCents: 350000,
|
|
701
|
+
currency: "USD",
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
batchId: "turn-response:run:{submissionId}:1",
|
|
708
|
+
sequence: 3,
|
|
709
|
+
record: {
|
|
710
|
+
recordId: "model-response:run:{submissionId}:1",
|
|
711
|
+
family: "conversation",
|
|
712
|
+
schemaVersion: 1,
|
|
713
|
+
createdAt: "{timestamp}",
|
|
714
|
+
deploymentId: "{deploymentId}",
|
|
715
|
+
payload: {
|
|
716
|
+
_tag: "ModelResponseRecorded",
|
|
717
|
+
runId: "run:{submissionId}",
|
|
718
|
+
turnId: "turn:run:{submissionId}:1",
|
|
719
|
+
turn: 1,
|
|
720
|
+
messages: {
|
|
721
|
+
content: [
|
|
722
|
+
{
|
|
723
|
+
options: {},
|
|
724
|
+
role: "system",
|
|
725
|
+
content:
|
|
726
|
+
'You are the Effect Agent Travel Planner P1 interpreter fixture.\nThe user asked: Plan a review-only London trip using the deterministic flight, lodging, and activity searches.\nCall search_flights, search_lodging, and search_activities exactly once in one Tool batch.\nThen return only a JSON object of exactly this shape, no prose:\n{"itineraries": [{"title": "<short itinerary name>", "route": "<origin-destination>", "dates": "<date range>", "flight": "<flight description from the Tool result>", "lodging": "<lodging description from the Tool result>", "activities": ["<activity>", "..."], "estimatedTotalCents": <positive integer total in cents>, "currency": "USD", "quoteId": "<quoteId from the flight Tool result>", "assumptions": ["<assumption>", "..."], "unresolvedConstraints": [], "nextAction": "review"}]}\nUse the Tool results verbatim; activity results may legitimately be an empty array.\nThis is read-only planning. Require review before any mutation.',
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
options: {},
|
|
730
|
+
role: "user",
|
|
731
|
+
content:
|
|
732
|
+
'{"request":"Plan a review-only London trip using the deterministic flight, lodging, and activity searches.","origin":"SFO","destination":"LHR","departOn":"2026-09-14","nights":4,"travelers":2,"budgetCents":350000,"currency":"USD"}',
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
options: {},
|
|
736
|
+
role: "assistant",
|
|
737
|
+
content: [
|
|
738
|
+
{
|
|
739
|
+
options: {},
|
|
740
|
+
type: "tool-call",
|
|
741
|
+
id: "flight-call-1",
|
|
742
|
+
name: "search_flights",
|
|
743
|
+
params: {
|
|
744
|
+
origin: "SFO",
|
|
745
|
+
destination: "LHR",
|
|
746
|
+
departOn: "2026-09-14",
|
|
747
|
+
travelers: 2,
|
|
748
|
+
},
|
|
749
|
+
providerExecuted: false,
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
options: {},
|
|
753
|
+
type: "tool-call",
|
|
754
|
+
id: "lodging-call-1",
|
|
755
|
+
name: "search_lodging",
|
|
756
|
+
params: {
|
|
757
|
+
destination: "LHR",
|
|
758
|
+
departOn: "2026-09-14",
|
|
759
|
+
nights: 4,
|
|
760
|
+
travelers: 2,
|
|
761
|
+
},
|
|
762
|
+
providerExecuted: false,
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
options: {},
|
|
766
|
+
type: "tool-call",
|
|
767
|
+
id: "activity-call-1",
|
|
768
|
+
name: "search_activities",
|
|
769
|
+
params: {
|
|
770
|
+
destination: "LHR",
|
|
771
|
+
departOn: "2026-09-14",
|
|
772
|
+
nights: 4,
|
|
773
|
+
travelers: 2,
|
|
774
|
+
},
|
|
775
|
+
providerExecuted: false,
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
},
|
|
779
|
+
],
|
|
780
|
+
},
|
|
781
|
+
messagesDigest: "{digest}",
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
batchId: "turn-results:run:{submissionId}:1",
|
|
787
|
+
sequence: 4,
|
|
788
|
+
record: {
|
|
789
|
+
recordId: "tool-settled:run:{submissionId}:1:flight-call-1",
|
|
790
|
+
family: "conversation",
|
|
791
|
+
schemaVersion: 1,
|
|
792
|
+
createdAt: "{timestamp}",
|
|
793
|
+
deploymentId: "{deploymentId}",
|
|
794
|
+
payload: {
|
|
795
|
+
_tag: "ToolCallSettled",
|
|
796
|
+
runId: "run:{submissionId}",
|
|
797
|
+
toolCallId: "flight-call-1",
|
|
798
|
+
toolName: "search_flights",
|
|
799
|
+
result: {
|
|
800
|
+
quoteId: "quote-sfo-lhr-001",
|
|
801
|
+
flight: "EA 218 · nonstop · SFO 18:40 → LHR 13:05+1",
|
|
802
|
+
estimatedCents: 180000,
|
|
803
|
+
currency: "USD",
|
|
804
|
+
},
|
|
805
|
+
isFailure: false,
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
batchId: "turn-results:run:{submissionId}:1",
|
|
811
|
+
sequence: 5,
|
|
812
|
+
record: {
|
|
813
|
+
recordId: "tool-settled:run:{submissionId}:1:lodging-call-1",
|
|
814
|
+
family: "conversation",
|
|
815
|
+
schemaVersion: 1,
|
|
816
|
+
createdAt: "{timestamp}",
|
|
817
|
+
deploymentId: "{deploymentId}",
|
|
818
|
+
payload: {
|
|
819
|
+
_tag: "ToolCallSettled",
|
|
820
|
+
runId: "run:{submissionId}",
|
|
821
|
+
toolCallId: "lodging-call-1",
|
|
822
|
+
toolName: "search_lodging",
|
|
823
|
+
result: {
|
|
824
|
+
lodging: "Bloomsbury House · refundable studio · 4 nights",
|
|
825
|
+
estimatedCents: 104000,
|
|
826
|
+
currency: "USD",
|
|
827
|
+
},
|
|
828
|
+
isFailure: false,
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
batchId: "turn-results:run:{submissionId}:1",
|
|
834
|
+
sequence: 6,
|
|
835
|
+
record: {
|
|
836
|
+
recordId: "tool-settled:run:{submissionId}:1:activity-call-1",
|
|
837
|
+
family: "conversation",
|
|
838
|
+
schemaVersion: 1,
|
|
839
|
+
createdAt: "{timestamp}",
|
|
840
|
+
deploymentId: "{deploymentId}",
|
|
841
|
+
payload: {
|
|
842
|
+
_tag: "ToolCallSettled",
|
|
843
|
+
runId: "run:{submissionId}",
|
|
844
|
+
toolCallId: "activity-call-1",
|
|
845
|
+
toolName: "search_activities",
|
|
846
|
+
result: {
|
|
847
|
+
activities: ["British Museum timed entry", "Thames evening walk"],
|
|
848
|
+
},
|
|
849
|
+
isFailure: false,
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
batchId: "turn:run:{submissionId}:2",
|
|
855
|
+
sequence: 7,
|
|
856
|
+
record: {
|
|
857
|
+
recordId: "model-response:run:{submissionId}:2",
|
|
858
|
+
family: "conversation",
|
|
859
|
+
schemaVersion: 1,
|
|
860
|
+
createdAt: "{timestamp}",
|
|
861
|
+
deploymentId: "{deploymentId}",
|
|
862
|
+
payload: {
|
|
863
|
+
_tag: "ModelResponseRecorded",
|
|
864
|
+
runId: "run:{submissionId}",
|
|
865
|
+
turnId: "turn:run:{submissionId}:2",
|
|
866
|
+
turn: 2,
|
|
867
|
+
messages: {
|
|
868
|
+
content: [
|
|
869
|
+
{
|
|
870
|
+
options: {},
|
|
871
|
+
role: "assistant",
|
|
872
|
+
content:
|
|
873
|
+
'{"itineraries":[{"title":"Westward light, eastbound overnight","route":"San Francisco → London","dates":"14–19 September 2026","flight":"EA 218 · nonstop · SFO 18:40 → LHR 13:05+1","lodging":"Bloomsbury House · refundable studio · 4 nights","activities":["British Museum timed entry","Thames evening walk"],"estimatedTotalCents":284000,"currency":"USD","quoteId":"quote-sfo-lhr-001","assumptions":["Two travelers sharing one studio","Quote is read-only availability, not a reservation"],"unresolvedConstraints":["Traveler names and accessibility requests are intentionally omitted"],"nextAction":"review"}]}',
|
|
874
|
+
},
|
|
875
|
+
],
|
|
876
|
+
},
|
|
877
|
+
messagesDigest: "{digest}",
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
batchId: "submission-settlement:{submissionId}",
|
|
883
|
+
sequence: 8,
|
|
884
|
+
record: {
|
|
885
|
+
recordId: "settlement:{submissionId}",
|
|
886
|
+
family: "conversation",
|
|
887
|
+
schemaVersion: 1,
|
|
888
|
+
createdAt: "{timestamp}",
|
|
889
|
+
deploymentId: "{deploymentId}",
|
|
890
|
+
payload: {
|
|
891
|
+
_tag: "SubmissionSettled",
|
|
892
|
+
submissionId: "{submissionId}",
|
|
893
|
+
settlementId: "settlement:{submissionId}",
|
|
894
|
+
receiptId: "{receiptId}",
|
|
895
|
+
outcome: "completed",
|
|
896
|
+
runId: "run:{submissionId}",
|
|
897
|
+
result: {
|
|
898
|
+
itineraries: [
|
|
899
|
+
{
|
|
900
|
+
title: "Westward light, eastbound overnight",
|
|
901
|
+
route: "San Francisco → London",
|
|
902
|
+
dates: "14–19 September 2026",
|
|
903
|
+
flight: "EA 218 · nonstop · SFO 18:40 → LHR 13:05+1",
|
|
904
|
+
lodging: "Bloomsbury House · refundable studio · 4 nights",
|
|
905
|
+
activities: ["British Museum timed entry", "Thames evening walk"],
|
|
906
|
+
estimatedTotalCents: 284000,
|
|
907
|
+
currency: "USD",
|
|
908
|
+
quoteId: "quote-sfo-lhr-001",
|
|
909
|
+
assumptions: [
|
|
910
|
+
"Two travelers sharing one studio",
|
|
911
|
+
"Quote is read-only availability, not a reservation",
|
|
912
|
+
],
|
|
913
|
+
unresolvedConstraints: [
|
|
914
|
+
"Traveler names and accessibility requests are intentionally omitted",
|
|
915
|
+
],
|
|
916
|
+
nextAction: "review",
|
|
917
|
+
},
|
|
918
|
+
],
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
];
|