@effect-agent/testing 0.1.0-beta.6 → 0.1.0-beta.60
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/Certification.d.mts +105 -0
- package/dist/Certification.mjs +590 -0
- package/dist/Certification.mjs.map +1 -0
- package/dist/Chaos.d.mts +126 -0
- package/dist/Chaos.mjs +755 -0
- package/dist/Chaos.mjs.map +1 -0
- package/dist/CodeExecutorConformance.d.mts +33 -0
- package/dist/CodeExecutorConformance.mjs +231 -0
- package/dist/CodeExecutorConformance.mjs.map +1 -0
- package/dist/CodeExecutorSubstitute.d.mts +21 -0
- package/dist/CodeExecutorSubstitute.mjs +364 -0
- package/dist/CodeExecutorSubstitute.mjs.map +1 -0
- package/dist/DocsResearcher.d.mts +270 -0
- package/dist/DocsResearcher.mjs +490 -0
- package/dist/DocsResearcher.mjs.map +1 -0
- package/dist/ScriptedModel-DAvxIiud.d.mts +220 -0
- package/dist/ScriptedModel.d.mts +2 -0
- package/dist/ScriptedModel.mjs +155 -0
- package/dist/ScriptedModel.mjs.map +1 -0
- package/dist/TravelPlanner.d.mts +1665 -0
- package/dist/TravelPlanner.mjs +1958 -0
- package/dist/TravelPlanner.mjs.map +1 -0
- package/dist/deterministic-layers-Eka0fMZq.mjs +358 -0
- package/dist/deterministic-layers-Eka0fMZq.mjs.map +1 -0
- package/dist/index.d.mts +2 -3378
- package/dist/index.mjs +2 -4766
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -48
- package/src/{certification.ts → Certification.ts} +251 -115
- package/src/{chaos.ts → Chaos.ts} +246 -122
- package/src/{code-executor-conformance.ts → CodeExecutorConformance.ts} +29 -6
- package/src/{code-executor-substitute.ts → CodeExecutorSubstitute.ts} +104 -42
- package/src/{fixtures/docs-researcher/index.ts → DocsResearcher.ts} +68 -5
- package/src/{scripted-model.ts → ScriptedModel.ts} +22 -25
- package/src/TravelPlanner.ts +232 -0
- package/src/fixtures/docs-researcher/definition.ts +19 -10
- package/src/fixtures/docs-researcher/harness.ts +41 -30
- package/src/fixtures/docs-researcher/mcp.ts +49 -3
- package/src/fixtures/travel-planner/definition.ts +15 -2
- package/src/fixtures/travel-planner/deterministic-layers.ts +47 -4
- package/src/fixtures/travel-planner/phase2.ts +4 -3
- package/src/fixtures/travel-planner/phase3.ts +18 -37
- package/src/fixtures/travel-planner/phase4.ts +23 -36
- package/src/fixtures/travel-planner/phase5.ts +38 -41
- package/src/fixtures/travel-planner/phase6.ts +182 -84
- package/src/fixtures/travel-planner/phase7.ts +4 -102
- package/src/fixtures/travel-planner/scenarios.ts +3 -4
- package/src/fixtures/travel-planner/subagents-durable.ts +33 -57
- package/src/fixtures/travel-planner/subagents.ts +35 -13
- package/src/index.ts +1 -11
- package/dist/index.mjs.map +0 -1
- package/src/code-executor-conformance.d.ts +0 -30
- package/src/fixtures/travel-planner/index.ts +0 -11
- package/src/fixtures/warehouse/index.ts +0 -412
|
@@ -1,45 +1,27 @@
|
|
|
1
|
-
import { AgentId,
|
|
1
|
+
import { AgentId, ThreadId, RunId, SubmissionId } from "@effect-agent/core/Identifiers";
|
|
2
2
|
import {
|
|
3
3
|
BatchId,
|
|
4
4
|
CanonicalBatch,
|
|
5
|
-
ConversationCheckpoint,
|
|
6
|
-
ConversationProjection,
|
|
7
5
|
DefinitionDigests,
|
|
8
6
|
DeploymentId,
|
|
9
7
|
Digest,
|
|
10
8
|
ProducerId,
|
|
11
9
|
RecordEnvelope,
|
|
12
10
|
RecordId,
|
|
13
|
-
} from "@effect-agent/
|
|
11
|
+
} from "@effect-agent/thread/Records";
|
|
12
|
+
import { ThreadProjection } from "@effect-agent/thread/ThreadProjection";
|
|
13
|
+
import { ThreadCheckpoint } from "@effect-agent/thread/ThreadStore";
|
|
14
14
|
import { Effect, Schema } from "effect";
|
|
15
15
|
|
|
16
16
|
import { TravelPlan, TripRequest } from "./definition.ts";
|
|
17
17
|
import { expectedTravelPlan, phase1Trip } from "./scenarios.ts";
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
* The Phase 3 profile persists Conversation history but deliberately does not
|
|
21
|
-
* claim durable admission or recovery of accepted work.
|
|
22
|
-
*/
|
|
23
|
-
export class TravelPlannerPersistenceProfile extends Schema.Class<TravelPlannerPersistenceProfile>(
|
|
24
|
-
"@effect-agent/testing/travel-planner/TravelPlannerPersistenceProfile",
|
|
25
|
-
)({
|
|
26
|
-
deploymentClass: Schema.Literal("P"),
|
|
27
|
-
durableAcceptedWork: Schema.Literal(false),
|
|
28
|
-
canonicalSchemaVersion: Schema.Literal(1),
|
|
29
|
-
}) {}
|
|
30
|
-
|
|
31
|
-
export const phase3TravelPlannerProfile = TravelPlannerPersistenceProfile.make({
|
|
32
|
-
deploymentClass: "P",
|
|
33
|
-
durableAcceptedWork: false,
|
|
34
|
-
canonicalSchemaVersion: 1,
|
|
35
|
-
});
|
|
19
|
+
export const phase3TravelPlannerThreadId = Schema.decodeSync(ThreadId)("travel-planner-p3-thread");
|
|
36
20
|
|
|
37
|
-
export const phase3TravelPlannerConversationId = Schema.decodeSync(ConversationId)(
|
|
38
|
-
"travel-planner-p3-conversation",
|
|
39
|
-
);
|
|
40
21
|
export const phase3TravelPlannerProducerId = Schema.decodeSync(ProducerId)(
|
|
41
22
|
"travel-planner-p3-producer",
|
|
42
23
|
);
|
|
24
|
+
|
|
43
25
|
export const phase3TravelPlannerRunId = Schema.decodeSync(RunId)("travel-planner-p3-run");
|
|
44
26
|
|
|
45
27
|
const deploymentId = Schema.decodeSync(DeploymentId)("travel-planner-p3-scripted");
|
|
@@ -61,7 +43,7 @@ const travelPlanOutput = Schema.encodeSync(TravelPlan)(expectedTravelPlan);
|
|
|
61
43
|
const record = (recordId: string, createdAt: string, payload: unknown) =>
|
|
62
44
|
Schema.decodeUnknownSync(RecordEnvelope)({
|
|
63
45
|
recordId: Schema.decodeSync(RecordId)(recordId),
|
|
64
|
-
family: "
|
|
46
|
+
family: "thread",
|
|
65
47
|
schemaVersion: 1,
|
|
66
48
|
createdAt,
|
|
67
49
|
deploymentId,
|
|
@@ -69,7 +51,7 @@ const record = (recordId: string, createdAt: string, payload: unknown) =>
|
|
|
69
51
|
});
|
|
70
52
|
|
|
71
53
|
/**
|
|
72
|
-
* The first atomic append establishes the
|
|
54
|
+
* The first atomic append establishes the Thread and records its input.
|
|
73
55
|
* Its encoded value is the redacted current-version persistence fixture.
|
|
74
56
|
*/
|
|
75
57
|
export const phase3TravelPlannerInitialBatch = CanonicalBatch.make({
|
|
@@ -77,7 +59,7 @@ export const phase3TravelPlannerInitialBatch = CanonicalBatch.make({
|
|
|
77
59
|
producerId: phase3TravelPlannerProducerId,
|
|
78
60
|
records: [
|
|
79
61
|
record("travel-planner-p3-created", "2026-09-01T00:00:00.000Z", {
|
|
80
|
-
_tag: "
|
|
62
|
+
_tag: "ThreadCreated",
|
|
81
63
|
agentId,
|
|
82
64
|
definitions: phase3TravelPlannerDefinitionDigests,
|
|
83
65
|
}),
|
|
@@ -115,9 +97,8 @@ export const phase3TravelPlannerBatches = [
|
|
|
115
97
|
] as const;
|
|
116
98
|
|
|
117
99
|
/** Portable current-version fixture; it contains no passenger identity or credentials. */
|
|
118
|
-
export const phase3TravelPlannerEncodedFixture
|
|
119
|
-
phase3TravelPlannerBatches
|
|
120
|
-
);
|
|
100
|
+
export const phase3TravelPlannerEncodedFixture: ReadonlyArray<typeof CanonicalBatch.Encoded> =
|
|
101
|
+
Schema.encodeSync(Schema.Array(CanonicalBatch))(phase3TravelPlannerBatches);
|
|
121
102
|
|
|
122
103
|
export class TravelPlannerProjectionError extends Schema.TaggedError<TravelPlannerProjectionError>()(
|
|
123
104
|
"TravelPlannerProjectionError",
|
|
@@ -126,9 +107,10 @@ export class TravelPlannerProjectionError extends Schema.TaggedError<TravelPlann
|
|
|
126
107
|
|
|
127
108
|
/** Decode the itinerary projection rebuilt from canonical model-completion records. */
|
|
128
109
|
export const travelPlanFromProjection = (
|
|
129
|
-
projection:
|
|
110
|
+
projection: ThreadProjection,
|
|
130
111
|
): Effect.Effect<TravelPlan, TravelPlannerProjectionError> => {
|
|
131
112
|
const output = projection.modelOutputs.at(-1);
|
|
113
|
+
|
|
132
114
|
if (output === undefined) {
|
|
133
115
|
return Effect.fail(
|
|
134
116
|
TravelPlannerProjectionError.make({
|
|
@@ -136,24 +118,23 @@ export const travelPlanFromProjection = (
|
|
|
136
118
|
}),
|
|
137
119
|
);
|
|
138
120
|
}
|
|
121
|
+
|
|
139
122
|
return Schema.decodeUnknownEffect(TravelPlan)(output).pipe(
|
|
140
123
|
Effect.mapError((error) => TravelPlannerProjectionError.make({ message: error.message })),
|
|
141
124
|
);
|
|
142
125
|
};
|
|
143
126
|
|
|
144
127
|
/** Build a disposable checkpoint bound to a validated canonical prefix. */
|
|
145
|
-
export const makePhase3TravelPlannerCheckpoint = (
|
|
146
|
-
|
|
147
|
-
): ConversationCheckpoint =>
|
|
148
|
-
Schema.decodeSync(ConversationCheckpoint)({
|
|
128
|
+
export const makePhase3TravelPlannerCheckpoint = (projection: ThreadProjection): ThreadCheckpoint =>
|
|
129
|
+
Schema.decodeSync(ThreadCheckpoint)({
|
|
149
130
|
schemaVersion: 1,
|
|
150
|
-
|
|
131
|
+
threadId: projection.threadId,
|
|
151
132
|
throughSequence: projection.throughSequence,
|
|
152
133
|
tailDigest: projection.tailDigest,
|
|
153
134
|
engineVersion: "phase-3-test-runtime",
|
|
154
135
|
agentDefinitionDigest: phase3TravelPlannerDefinitionDigests.agent,
|
|
155
136
|
modelDigest: phase3TravelPlannerDefinitionDigests.model,
|
|
156
137
|
toolDigest: phase3TravelPlannerDefinitionDigests.tools,
|
|
157
|
-
state: Schema.encodeSync(
|
|
138
|
+
state: Schema.encodeSync(ThreadProjection)(projection),
|
|
158
139
|
createdAt: "2026-09-01T00:00:04.000Z",
|
|
159
140
|
});
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import * as Agent from "@effect-agent/core/Agent";
|
|
2
|
+
import { AgentPolicy } from "@effect-agent/core/AgentPolicy";
|
|
3
|
+
import { type ThreadId } from "@effect-agent/core/Identifiers";
|
|
4
|
+
import { ToolExecutionClass } from "@effect-agent/engine/DurableStep";
|
|
5
|
+
import { type DurableSubmitOptions, type Receipt } from "@effect-agent/thread/DurableAgentRuntime";
|
|
3
6
|
import {
|
|
4
7
|
CanonicalRecordEnvelope,
|
|
5
8
|
DefinitionDigests,
|
|
6
9
|
DeploymentId,
|
|
7
10
|
Digest,
|
|
8
|
-
Principal,
|
|
9
11
|
ProducerId,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type Receipt,
|
|
13
|
-
} from "@effect-agent/session";
|
|
12
|
+
} from "@effect-agent/thread/Records";
|
|
13
|
+
import { Principal, type IdempotencyKey } from "@effect-agent/thread/SubmissionLedger";
|
|
14
14
|
import { Effect, Layer, Schema } from "effect";
|
|
15
15
|
import { Model, Tool, Toolkit } from "effect/unstable/ai";
|
|
16
16
|
|
|
17
|
-
import { ScriptedModel, type ScriptedTurnInput } from "../../
|
|
17
|
+
import { ScriptedModel, type ScriptedTurnInput } from "../../ScriptedModel.ts";
|
|
18
18
|
import {
|
|
19
19
|
ActivityCatalog,
|
|
20
20
|
ActivityQuery,
|
|
@@ -41,35 +41,14 @@ import {
|
|
|
41
41
|
} from "./deterministic-layers.ts";
|
|
42
42
|
import { phase1HappyPathTurns } from "./scenarios.ts";
|
|
43
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
44
|
export const phase4TravelPlannerDeploymentId = Schema.decodeSync(DeploymentId)(
|
|
68
45
|
"travel-planner-p4-deployment",
|
|
69
46
|
);
|
|
47
|
+
|
|
70
48
|
export const phase4TravelPlannerProducerId = Schema.decodeSync(ProducerId)(
|
|
71
49
|
"travel-planner-p4-producer",
|
|
72
50
|
);
|
|
51
|
+
|
|
73
52
|
export const phase4TravelPlannerPrincipal = Schema.decodeSync(Principal)(
|
|
74
53
|
"travel-planner-p4-principal",
|
|
75
54
|
);
|
|
@@ -85,10 +64,10 @@ export const phase4TravelPlannerDefinitionDigests = DefinitionDigests.make({
|
|
|
85
64
|
|
|
86
65
|
/** Durable admission options for one Travel Planner Submission on one trip lane. */
|
|
87
66
|
export const phase4TravelPlannerSubmitOptions = (
|
|
88
|
-
|
|
67
|
+
threadId: ThreadId,
|
|
89
68
|
idempotencyKey: IdempotencyKey,
|
|
90
69
|
): DurableSubmitOptions => ({
|
|
91
|
-
|
|
70
|
+
threadId,
|
|
92
71
|
principal: phase4TravelPlannerPrincipal,
|
|
93
72
|
idempotencyKey,
|
|
94
73
|
definitions: phase4TravelPlannerDefinitionDigests,
|
|
@@ -112,6 +91,7 @@ export const DurableSearchFlights = Tool.make("search_flights", {
|
|
|
112
91
|
failureMode: "error",
|
|
113
92
|
dependencies: [FlightCatalog],
|
|
114
93
|
}).annotate(ToolExecutionClass, "readonly");
|
|
94
|
+
|
|
115
95
|
export const DurableSearchLodging = Tool.make("search_lodging", {
|
|
116
96
|
parameters: Schema.Struct(LodgingQuery.fields),
|
|
117
97
|
success: LodgingOption,
|
|
@@ -119,6 +99,7 @@ export const DurableSearchLodging = Tool.make("search_lodging", {
|
|
|
119
99
|
failureMode: "error",
|
|
120
100
|
dependencies: [LodgingCatalog],
|
|
121
101
|
}).annotate(ToolExecutionClass, "readonly");
|
|
102
|
+
|
|
122
103
|
export const DurableSearchActivities = Tool.make("search_activities", {
|
|
123
104
|
parameters: Schema.Struct(ActivityQuery.fields),
|
|
124
105
|
success: ActivitySearchResult,
|
|
@@ -147,7 +128,7 @@ export const TravelPlannerPhase4ToolkitLayer = TravelPlannerPhase4Toolkit.toLaye
|
|
|
147
128
|
* runtime. The searches are read-only and safe to repeat across Attempts (D6); supplier booking
|
|
148
129
|
* is deliberately absent because DN does NOT claim replay-safe external mutation (P5 scope).
|
|
149
130
|
*/
|
|
150
|
-
export const TravelPlannerPhase4 = Agent.
|
|
131
|
+
export const TravelPlannerPhase4 = Agent.make("travel-planner-phase-4", {
|
|
151
132
|
input: TripRequest,
|
|
152
133
|
output: TravelPlan,
|
|
153
134
|
instructions: (input) =>
|
|
@@ -208,10 +189,12 @@ export const travelPlanFromDurableSettlement = Effect.fn(
|
|
|
208
189
|
const settlements = records.flatMap((envelope) =>
|
|
209
190
|
envelope.record.payload._tag === "SubmissionSettled" ? [envelope.record.payload] : [],
|
|
210
191
|
);
|
|
192
|
+
|
|
211
193
|
const settled = settlements.at(0);
|
|
194
|
+
|
|
212
195
|
if (settled === undefined) {
|
|
213
196
|
return yield* TravelPlannerDurableEvidenceError.make({
|
|
214
|
-
message: "The canonical
|
|
197
|
+
message: "The canonical Thread Log has no SubmissionSettled record.",
|
|
215
198
|
});
|
|
216
199
|
}
|
|
217
200
|
if (settled.outcome !== "completed" || settled.result === undefined) {
|
|
@@ -219,6 +202,7 @@ export const travelPlanFromDurableSettlement = Effect.fn(
|
|
|
219
202
|
message: `The Submission settled ${settled.outcome} without a completed itinerary result.`,
|
|
220
203
|
});
|
|
221
204
|
}
|
|
205
|
+
|
|
222
206
|
return yield* Schema.decodeUnknownEffect(TravelPlan)(settled.result).pipe(
|
|
223
207
|
Effect.mapError((error) =>
|
|
224
208
|
TravelPlannerDurableEvidenceError.make({
|
|
@@ -235,7 +219,7 @@ const decodeComparableJson = Schema.decodeUnknownEffect(Schema.Json);
|
|
|
235
219
|
* Project canonical evidence into a Submission-identity-independent comparable form: batch
|
|
236
220
|
* identity, canonical sequence, and the full encoded record, with the ledger-minted
|
|
237
221
|
* `submissionId`/`receiptId` (and every identity derived from them: run, turn, batch, record,
|
|
238
|
-
* and settlement ids) replaced by stable placeholders. Two
|
|
222
|
+
* and settlement ids) replaced by stable placeholders. Two Threads whose normalized
|
|
239
223
|
* evidence is equal took byte-equivalent canonical histories, so restart-equivalence can compare
|
|
240
224
|
* a recovered run against an uninterrupted control run on a separate database.
|
|
241
225
|
*/
|
|
@@ -252,16 +236,19 @@ export const normalizeDurableTravelPlannerEvidence = Effect.fn(
|
|
|
252
236
|
}),
|
|
253
237
|
),
|
|
254
238
|
);
|
|
239
|
+
|
|
255
240
|
const comparable = encoded.map((envelope) => ({
|
|
256
241
|
batchId: envelope.batchId,
|
|
257
242
|
sequence: envelope.sequence,
|
|
258
243
|
record: envelope.record,
|
|
259
244
|
}));
|
|
245
|
+
|
|
260
246
|
const substituted: unknown = JSON.parse(
|
|
261
247
|
JSON.stringify(comparable)
|
|
262
248
|
.replaceAll(receipt.submissionId, "{submissionId}")
|
|
263
249
|
.replaceAll(receipt.receiptId, "{receiptId}"),
|
|
264
250
|
);
|
|
251
|
+
|
|
265
252
|
return yield* decodeComparableJson(substituted).pipe(
|
|
266
253
|
Effect.mapError((error) =>
|
|
267
254
|
TravelPlannerDurableEvidenceError.make({
|
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import * as Agent from "@effect-agent/core/Agent";
|
|
2
|
+
import { AgentPolicy } from "@effect-agent/core/AgentPolicy";
|
|
3
|
+
import { type ThreadId } from "@effect-agent/core/Identifiers";
|
|
4
|
+
import {
|
|
5
|
+
DurableStep,
|
|
6
|
+
DurableStepError,
|
|
7
|
+
ToolExecutionClass,
|
|
8
|
+
} from "@effect-agent/engine/DurableStep";
|
|
9
|
+
import { type DurableSubmitOptions } from "@effect-agent/thread/DurableAgentRuntime";
|
|
3
10
|
import {
|
|
4
11
|
DefinitionDigests,
|
|
5
12
|
DeploymentId,
|
|
6
13
|
Digest,
|
|
7
14
|
PersistedJson,
|
|
8
|
-
Principal,
|
|
9
15
|
ProducerId,
|
|
16
|
+
type CanonicalRecordEnvelope,
|
|
17
|
+
} from "@effect-agent/thread/Records";
|
|
18
|
+
import { Principal, type IdempotencyKey } from "@effect-agent/thread/SubmissionLedger";
|
|
19
|
+
import {
|
|
10
20
|
ReconciliationCompleted,
|
|
11
21
|
ReconciliationSafeToRetry,
|
|
12
22
|
ReconciliationUncertain,
|
|
13
23
|
ToolReconciler,
|
|
14
24
|
ToolReconcilerError,
|
|
15
|
-
|
|
16
|
-
type DurableSubmitOptions,
|
|
17
|
-
type IdempotencyKey,
|
|
18
|
-
} from "@effect-agent/session";
|
|
25
|
+
} from "@effect-agent/thread/ToolReconciler";
|
|
19
26
|
import { Effect, Layer, Option, Schema } from "effect";
|
|
20
27
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
21
28
|
|
|
@@ -42,42 +49,14 @@ import {
|
|
|
42
49
|
} from "./deterministic-layers.ts";
|
|
43
50
|
import { DurableSearchActivities, DurableSearchFlights, DurableSearchLodging } from "./phase4.ts";
|
|
44
51
|
|
|
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
52
|
export const phase5TravelPlannerDeploymentId = Schema.decodeSync(DeploymentId)(
|
|
76
53
|
"travel-planner-p5-deployment",
|
|
77
54
|
);
|
|
55
|
+
|
|
78
56
|
export const phase5TravelPlannerProducerId = Schema.decodeSync(ProducerId)(
|
|
79
57
|
"travel-planner-p5-producer",
|
|
80
58
|
);
|
|
59
|
+
|
|
81
60
|
export const phase5TravelPlannerPrincipal = Schema.decodeSync(Principal)(
|
|
82
61
|
"travel-planner-p5-principal",
|
|
83
62
|
);
|
|
@@ -93,10 +72,10 @@ export const phase5TravelPlannerDefinitionDigests = DefinitionDigests.make({
|
|
|
93
72
|
|
|
94
73
|
/** Durable admission options for one Travel Planner Submission on one trip lane. */
|
|
95
74
|
export const phase5TravelPlannerSubmitOptions = (
|
|
96
|
-
|
|
75
|
+
threadId: ThreadId,
|
|
97
76
|
idempotencyKey: IdempotencyKey,
|
|
98
77
|
): DurableSubmitOptions => ({
|
|
99
|
-
|
|
78
|
+
threadId,
|
|
100
79
|
principal: phase5TravelPlannerPrincipal,
|
|
101
80
|
idempotencyKey,
|
|
102
81
|
definitions: phase5TravelPlannerDefinitionDigests,
|
|
@@ -105,6 +84,7 @@ export const phase5TravelPlannerSubmitOptions = (
|
|
|
105
84
|
export const TravelerRef = Schema.NonEmptyString.pipe(
|
|
106
85
|
Schema.brand("@effect-agent/testing/travel-planner/TravelerRef"),
|
|
107
86
|
);
|
|
87
|
+
|
|
108
88
|
export type TravelerRef = typeof TravelerRef.Type;
|
|
109
89
|
|
|
110
90
|
/**
|
|
@@ -171,6 +151,7 @@ export class TravelBookingReport extends Schema.Class<TravelBookingReport>("Trav
|
|
|
171
151
|
* to query external truth — both sides are exported so they cannot drift.
|
|
172
152
|
*/
|
|
173
153
|
export const bookFlightIdempotencyKey = (toolCallId: string): string => `book-flight:${toolCallId}`;
|
|
154
|
+
|
|
174
155
|
export const itineraryStepIdempotencyKey = (toolCallId: string, stepName: string): string =>
|
|
175
156
|
`${toolCallId}:${stepName}`;
|
|
176
157
|
|
|
@@ -205,7 +186,7 @@ export const CancelBooking = Tool.make("cancel_booking", {
|
|
|
205
186
|
|
|
206
187
|
/**
|
|
207
188
|
* The Durable Tool: declaring `DurableStep` in `dependencies` is what makes it durable
|
|
208
|
-
* (
|
|
189
|
+
* (GLOSSARY.md). Its handler divides supplier mutation into three named Steps — `reserve-flight`,
|
|
209
190
|
* `reserve-lodging`, `issue-confirmation` — each deriving its supplier idempotency key from
|
|
210
191
|
* `(toolCallId, stepName)`, so re-entry after interruption replays recorded Step results and the
|
|
211
192
|
* supplier dedupes any honestly-repeated call. The Tool itself carries no execution-class
|
|
@@ -252,11 +233,13 @@ export const TravelPlannerPhase5ToolkitLayer = TravelPlannerPhase5Toolkit.toLaye
|
|
|
252
233
|
Effect.gen(function* () {
|
|
253
234
|
const desk = yield* SupplierBookingDesk;
|
|
254
235
|
const toolCallId = yield* requireToolCallId("book_flight", context.toolCallId);
|
|
236
|
+
|
|
255
237
|
const record = yield* desk.book({
|
|
256
238
|
operation: "book-flight",
|
|
257
239
|
idempotencyKey: bookFlightIdempotencyKey(toolCallId),
|
|
258
240
|
detail: `flight ${request.quoteId} for ${request.travelerRef} on ${request.departOn}`,
|
|
259
241
|
});
|
|
242
|
+
|
|
260
243
|
return SupplierBookingConfirmation.make({
|
|
261
244
|
bookingRef: record.bookingRef,
|
|
262
245
|
status: "confirmed",
|
|
@@ -267,6 +250,7 @@ export const TravelPlannerPhase5ToolkitLayer = TravelPlannerPhase5Toolkit.toLaye
|
|
|
267
250
|
Effect.gen(function* () {
|
|
268
251
|
const desk = yield* SupplierBookingDesk;
|
|
269
252
|
const record = yield* desk.cancel(request.bookingRef);
|
|
253
|
+
|
|
270
254
|
return CancellationConfirmation.make({
|
|
271
255
|
bookingRef: record.bookingRef,
|
|
272
256
|
status: "cancelled",
|
|
@@ -277,6 +261,7 @@ export const TravelPlannerPhase5ToolkitLayer = TravelPlannerPhase5Toolkit.toLaye
|
|
|
277
261
|
const desk = yield* SupplierBookingDesk;
|
|
278
262
|
const step = yield* DurableStep;
|
|
279
263
|
const toolCallId = yield* requireToolCallId("book_itinerary", context.toolCallId);
|
|
264
|
+
|
|
280
265
|
const bookStep = (
|
|
281
266
|
stepName: "reserve-flight" | "reserve-lodging" | "issue-confirmation",
|
|
282
267
|
detail: string,
|
|
@@ -290,18 +275,22 @@ export const TravelPlannerPhase5ToolkitLayer = TravelPlannerPhase5Toolkit.toLaye
|
|
|
290
275
|
detail,
|
|
291
276
|
}),
|
|
292
277
|
);
|
|
278
|
+
|
|
293
279
|
const flight = yield* bookStep(
|
|
294
280
|
"reserve-flight",
|
|
295
281
|
`flight ${request.quoteId} for ${request.travelerRef}`,
|
|
296
282
|
);
|
|
283
|
+
|
|
297
284
|
const lodging = yield* bookStep(
|
|
298
285
|
"reserve-lodging",
|
|
299
286
|
`lodging ${request.destination} for ${request.nights} nights (${request.travelerRef})`,
|
|
300
287
|
);
|
|
288
|
+
|
|
301
289
|
const confirmation = yield* bookStep(
|
|
302
290
|
"issue-confirmation",
|
|
303
291
|
`itinerary confirmation for ${request.travelerRef}`,
|
|
304
292
|
);
|
|
293
|
+
|
|
305
294
|
return ItineraryConfirmation.make({
|
|
306
295
|
flightBookingRef: flight.bookingRef,
|
|
307
296
|
lodgingBookingRef: lodging.bookingRef,
|
|
@@ -336,6 +325,7 @@ export const TravelSupplierReconcilerLayer: Layer.Layer<
|
|
|
336
325
|
ToolReconciler,
|
|
337
326
|
Effect.gen(function* () {
|
|
338
327
|
const desk = yield* SupplierBookingDesk;
|
|
328
|
+
|
|
339
329
|
return ToolReconciler.of({
|
|
340
330
|
reconcile: (evidence) =>
|
|
341
331
|
Effect.gen(function* () {
|
|
@@ -343,6 +333,7 @@ export const TravelSupplierReconcilerLayer: Layer.Layer<
|
|
|
343
333
|
case "book_flight": {
|
|
344
334
|
const key = bookFlightIdempotencyKey(evidence.toolCallId);
|
|
345
335
|
const booking = yield* desk.lookup(key);
|
|
336
|
+
|
|
346
337
|
if (Option.isSome(booking) && booking.value.status === "confirmed") {
|
|
347
338
|
const confirmation = yield* encodeConfirmation(
|
|
348
339
|
SupplierBookingConfirmation.make({
|
|
@@ -351,8 +342,10 @@ export const TravelSupplierReconcilerLayer: Layer.Layer<
|
|
|
351
342
|
detail: booking.value.detail,
|
|
352
343
|
}),
|
|
353
344
|
).pipe(Effect.flatMap(decodePersistedJson));
|
|
345
|
+
|
|
354
346
|
return ReconciliationCompleted.make({ result: confirmation, isFailure: false });
|
|
355
347
|
}
|
|
348
|
+
|
|
356
349
|
return ReconciliationUncertain.make({
|
|
357
350
|
reason: `The supplier desk shows no confirmed booking under ${key}; a write may still be in flight.`,
|
|
358
351
|
});
|
|
@@ -384,7 +377,7 @@ export const TravelSupplierReconcilerLayer: Layer.Layer<
|
|
|
384
377
|
* idempotent-by-contract cancellation, and one Durable Tool whose Steps carry supplier
|
|
385
378
|
* idempotency keys.
|
|
386
379
|
*/
|
|
387
|
-
export const TravelPlannerPhase5 = Agent.
|
|
380
|
+
export const TravelPlannerPhase5 = Agent.make("travel-planner-phase-5", {
|
|
388
381
|
input: TripRequest,
|
|
389
382
|
output: TravelBookingReport,
|
|
390
383
|
instructions: [
|
|
@@ -413,6 +406,7 @@ export class TravelPlannerBookingEvidenceError extends Schema.TaggedError<Travel
|
|
|
413
406
|
const bookingResultRefs = (result: unknown): ReadonlyArray<string> => {
|
|
414
407
|
if (typeof result !== "object" || result === null) return [];
|
|
415
408
|
const refs: Array<string> = [];
|
|
409
|
+
|
|
416
410
|
for (const [field, value] of Object.entries(result)) {
|
|
417
411
|
if (
|
|
418
412
|
typeof value === "string" &&
|
|
@@ -424,6 +418,7 @@ const bookingResultRefs = (result: unknown): ReadonlyArray<string> => {
|
|
|
424
418
|
refs.push(value);
|
|
425
419
|
}
|
|
426
420
|
}
|
|
421
|
+
|
|
427
422
|
return refs;
|
|
428
423
|
};
|
|
429
424
|
|
|
@@ -443,8 +438,10 @@ export const assertSettledBookingsExistAtSupplier = Effect.fn(
|
|
|
443
438
|
const desk = yield* SupplierBookingDesk;
|
|
444
439
|
const bookings = yield* desk.bookings;
|
|
445
440
|
const knownRefs = new Set<string>(bookings.map((booking) => booking.bookingRef));
|
|
441
|
+
|
|
446
442
|
for (const envelope of records) {
|
|
447
443
|
const payload = envelope.record.payload;
|
|
444
|
+
|
|
448
445
|
if (
|
|
449
446
|
payload._tag !== "ToolCallSettled" ||
|
|
450
447
|
payload.isFailure ||
|