@effect-agent/testing 0.1.0-beta.9 → 0.1.0-beta.90
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 +106 -0
- package/dist/Certification.mjs +633 -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 +368 -0
- package/dist/CodeExecutorSubstitute.mjs.map +1 -0
- package/dist/DocsResearcher.d.mts +276 -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 +1680 -0
- package/dist/TravelPlanner.mjs +1963 -0
- package/dist/TravelPlanner.mjs.map +1 -0
- package/dist/deterministic-layers-D5owIoke.mjs +358 -0
- package/dist/deterministic-layers-D5owIoke.mjs.map +1 -0
- package/dist/index.d.mts +2 -3408
- package/dist/index.mjs +2 -4771
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -48
- package/src/{certification.ts → Certification.ts} +366 -171
- package/src/{chaos.ts → Chaos.ts} +241 -127
- package/src/{code-executor-conformance.ts → CodeExecutorConformance.ts} +30 -7
- package/src/{code-executor-substitute.ts → CodeExecutorSubstitute.ts} +113 -46
- package/src/{fixtures/docs-researcher/index.ts → DocsResearcher.ts} +68 -5
- package/src/{scripted-model.ts → ScriptedModel.ts} +25 -29
- package/src/TravelPlanner.ts +232 -0
- package/src/fixtures/docs-researcher/definition.ts +20 -11
- package/src/fixtures/docs-researcher/harness.ts +41 -34
- 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 +19 -42
- package/src/fixtures/travel-planner/phase4.ts +24 -37
- package/src/fixtures/travel-planner/phase5.ts +35 -42
- package/src/fixtures/travel-planner/phase6.ts +191 -88
- 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 +35 -61
- package/src/fixtures/travel-planner/subagents.ts +36 -14
- package/src/index.ts +1 -11
- package/src/internal/certification-report.ts +25 -0
- 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 {
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { AgentId, ThreadId, RunId, SubmissionId } from "effect-agent/identifiers";
|
|
2
3
|
import {
|
|
3
4
|
BatchId,
|
|
4
5
|
CanonicalBatch,
|
|
5
|
-
ConversationCheckpoint,
|
|
6
|
-
ConversationProjection,
|
|
7
6
|
DefinitionDigests,
|
|
8
7
|
DeploymentId,
|
|
9
8
|
Digest,
|
|
10
9
|
ProducerId,
|
|
11
10
|
RecordEnvelope,
|
|
12
11
|
RecordId,
|
|
13
|
-
} from "
|
|
14
|
-
import {
|
|
12
|
+
} from "effect-agent/records";
|
|
13
|
+
import { ThreadProjection } from "effect-agent/thread-projection";
|
|
14
|
+
import { ThreadCheckpoint } from "effect-agent/thread-store";
|
|
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,19 @@ 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
|
-
|
|
154
|
-
agentDefinitionDigest: phase3TravelPlannerDefinitionDigests.agent,
|
|
155
|
-
modelDigest: phase3TravelPlannerDefinitionDigests.model,
|
|
156
|
-
toolDigest: phase3TravelPlannerDefinitionDigests.tools,
|
|
157
|
-
state: Schema.encodeSync(ConversationProjection)(projection),
|
|
134
|
+
state: Schema.encodeSync(ThreadProjection)(projection),
|
|
158
135
|
createdAt: "2026-09-01T00:00:04.000Z",
|
|
159
136
|
});
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { Effect, Layer, Schema } from "effect";
|
|
2
|
+
import * as Agent from "effect-agent/agent";
|
|
3
|
+
import { AgentPolicy } from "effect-agent/agent-policy";
|
|
4
|
+
import { type DurableSubmitOptions, type Receipt } from "effect-agent/durable-agent-runtime";
|
|
5
|
+
import { ToolExecutionClass } from "effect-agent/durable-step";
|
|
6
|
+
import { type ThreadId } from "effect-agent/identifiers";
|
|
3
7
|
import {
|
|
4
8
|
CanonicalRecordEnvelope,
|
|
5
9
|
DefinitionDigests,
|
|
6
10
|
DeploymentId,
|
|
7
11
|
Digest,
|
|
8
|
-
Principal,
|
|
9
12
|
ProducerId,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type Receipt,
|
|
13
|
-
} from "@effect-agent/session";
|
|
14
|
-
import { Effect, Layer, Schema } from "effect";
|
|
13
|
+
} from "effect-agent/records";
|
|
14
|
+
import { Principal, type IdempotencyKey } from "effect-agent/submission-ledger";
|
|
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,22 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { Effect, Layer, Option, Schema } from "effect";
|
|
2
|
+
import * as Agent from "effect-agent/agent";
|
|
3
|
+
import { AgentPolicy } from "effect-agent/agent-policy";
|
|
4
|
+
import { type DurableSubmitOptions } from "effect-agent/durable-agent-runtime";
|
|
5
|
+
import { DurableStep, DurableStepError, ToolExecutionClass } from "effect-agent/durable-step";
|
|
6
|
+
import { type ThreadId } from "effect-agent/identifiers";
|
|
3
7
|
import {
|
|
4
8
|
DefinitionDigests,
|
|
5
9
|
DeploymentId,
|
|
6
10
|
Digest,
|
|
7
11
|
PersistedJson,
|
|
8
|
-
Principal,
|
|
9
12
|
ProducerId,
|
|
13
|
+
type CanonicalRecordEnvelope,
|
|
14
|
+
} from "effect-agent/records";
|
|
15
|
+
import { Principal, type IdempotencyKey } from "effect-agent/submission-ledger";
|
|
16
|
+
import {
|
|
10
17
|
ReconciliationCompleted,
|
|
11
18
|
ReconciliationSafeToRetry,
|
|
12
19
|
ReconciliationUncertain,
|
|
13
20
|
ToolReconciler,
|
|
14
21
|
ToolReconcilerError,
|
|
15
|
-
|
|
16
|
-
type DurableSubmitOptions,
|
|
17
|
-
type IdempotencyKey,
|
|
18
|
-
} from "@effect-agent/session";
|
|
19
|
-
import { Effect, Layer, Option, Schema } from "effect";
|
|
22
|
+
} from "effect-agent/tool-reconciler";
|
|
20
23
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
21
24
|
|
|
22
25
|
import {
|
|
@@ -42,42 +45,14 @@ import {
|
|
|
42
45
|
} from "./deterministic-layers.ts";
|
|
43
46
|
import { DurableSearchActivities, DurableSearchFlights, DurableSearchLodging } from "./phase4.ts";
|
|
44
47
|
|
|
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
48
|
export const phase5TravelPlannerDeploymentId = Schema.decodeSync(DeploymentId)(
|
|
76
49
|
"travel-planner-p5-deployment",
|
|
77
50
|
);
|
|
51
|
+
|
|
78
52
|
export const phase5TravelPlannerProducerId = Schema.decodeSync(ProducerId)(
|
|
79
53
|
"travel-planner-p5-producer",
|
|
80
54
|
);
|
|
55
|
+
|
|
81
56
|
export const phase5TravelPlannerPrincipal = Schema.decodeSync(Principal)(
|
|
82
57
|
"travel-planner-p5-principal",
|
|
83
58
|
);
|
|
@@ -93,10 +68,10 @@ export const phase5TravelPlannerDefinitionDigests = DefinitionDigests.make({
|
|
|
93
68
|
|
|
94
69
|
/** Durable admission options for one Travel Planner Submission on one trip lane. */
|
|
95
70
|
export const phase5TravelPlannerSubmitOptions = (
|
|
96
|
-
|
|
71
|
+
threadId: ThreadId,
|
|
97
72
|
idempotencyKey: IdempotencyKey,
|
|
98
73
|
): DurableSubmitOptions => ({
|
|
99
|
-
|
|
74
|
+
threadId,
|
|
100
75
|
principal: phase5TravelPlannerPrincipal,
|
|
101
76
|
idempotencyKey,
|
|
102
77
|
definitions: phase5TravelPlannerDefinitionDigests,
|
|
@@ -105,6 +80,7 @@ export const phase5TravelPlannerSubmitOptions = (
|
|
|
105
80
|
export const TravelerRef = Schema.NonEmptyString.pipe(
|
|
106
81
|
Schema.brand("@effect-agent/testing/travel-planner/TravelerRef"),
|
|
107
82
|
);
|
|
83
|
+
|
|
108
84
|
export type TravelerRef = typeof TravelerRef.Type;
|
|
109
85
|
|
|
110
86
|
/**
|
|
@@ -171,6 +147,7 @@ export class TravelBookingReport extends Schema.Class<TravelBookingReport>("Trav
|
|
|
171
147
|
* to query external truth — both sides are exported so they cannot drift.
|
|
172
148
|
*/
|
|
173
149
|
export const bookFlightIdempotencyKey = (toolCallId: string): string => `book-flight:${toolCallId}`;
|
|
150
|
+
|
|
174
151
|
export const itineraryStepIdempotencyKey = (toolCallId: string, stepName: string): string =>
|
|
175
152
|
`${toolCallId}:${stepName}`;
|
|
176
153
|
|
|
@@ -205,7 +182,7 @@ export const CancelBooking = Tool.make("cancel_booking", {
|
|
|
205
182
|
|
|
206
183
|
/**
|
|
207
184
|
* The Durable Tool: declaring `DurableStep` in `dependencies` is what makes it durable
|
|
208
|
-
* (
|
|
185
|
+
* (GLOSSARY.md). Its handler divides supplier mutation into three named Steps — `reserve-flight`,
|
|
209
186
|
* `reserve-lodging`, `issue-confirmation` — each deriving its supplier idempotency key from
|
|
210
187
|
* `(toolCallId, stepName)`, so re-entry after interruption replays recorded Step results and the
|
|
211
188
|
* supplier dedupes any honestly-repeated call. The Tool itself carries no execution-class
|
|
@@ -252,11 +229,13 @@ export const TravelPlannerPhase5ToolkitLayer = TravelPlannerPhase5Toolkit.toLaye
|
|
|
252
229
|
Effect.gen(function* () {
|
|
253
230
|
const desk = yield* SupplierBookingDesk;
|
|
254
231
|
const toolCallId = yield* requireToolCallId("book_flight", context.toolCallId);
|
|
232
|
+
|
|
255
233
|
const record = yield* desk.book({
|
|
256
234
|
operation: "book-flight",
|
|
257
235
|
idempotencyKey: bookFlightIdempotencyKey(toolCallId),
|
|
258
236
|
detail: `flight ${request.quoteId} for ${request.travelerRef} on ${request.departOn}`,
|
|
259
237
|
});
|
|
238
|
+
|
|
260
239
|
return SupplierBookingConfirmation.make({
|
|
261
240
|
bookingRef: record.bookingRef,
|
|
262
241
|
status: "confirmed",
|
|
@@ -267,6 +246,7 @@ export const TravelPlannerPhase5ToolkitLayer = TravelPlannerPhase5Toolkit.toLaye
|
|
|
267
246
|
Effect.gen(function* () {
|
|
268
247
|
const desk = yield* SupplierBookingDesk;
|
|
269
248
|
const record = yield* desk.cancel(request.bookingRef);
|
|
249
|
+
|
|
270
250
|
return CancellationConfirmation.make({
|
|
271
251
|
bookingRef: record.bookingRef,
|
|
272
252
|
status: "cancelled",
|
|
@@ -277,6 +257,7 @@ export const TravelPlannerPhase5ToolkitLayer = TravelPlannerPhase5Toolkit.toLaye
|
|
|
277
257
|
const desk = yield* SupplierBookingDesk;
|
|
278
258
|
const step = yield* DurableStep;
|
|
279
259
|
const toolCallId = yield* requireToolCallId("book_itinerary", context.toolCallId);
|
|
260
|
+
|
|
280
261
|
const bookStep = (
|
|
281
262
|
stepName: "reserve-flight" | "reserve-lodging" | "issue-confirmation",
|
|
282
263
|
detail: string,
|
|
@@ -290,18 +271,22 @@ export const TravelPlannerPhase5ToolkitLayer = TravelPlannerPhase5Toolkit.toLaye
|
|
|
290
271
|
detail,
|
|
291
272
|
}),
|
|
292
273
|
);
|
|
274
|
+
|
|
293
275
|
const flight = yield* bookStep(
|
|
294
276
|
"reserve-flight",
|
|
295
277
|
`flight ${request.quoteId} for ${request.travelerRef}`,
|
|
296
278
|
);
|
|
279
|
+
|
|
297
280
|
const lodging = yield* bookStep(
|
|
298
281
|
"reserve-lodging",
|
|
299
282
|
`lodging ${request.destination} for ${request.nights} nights (${request.travelerRef})`,
|
|
300
283
|
);
|
|
284
|
+
|
|
301
285
|
const confirmation = yield* bookStep(
|
|
302
286
|
"issue-confirmation",
|
|
303
287
|
`itinerary confirmation for ${request.travelerRef}`,
|
|
304
288
|
);
|
|
289
|
+
|
|
305
290
|
return ItineraryConfirmation.make({
|
|
306
291
|
flightBookingRef: flight.bookingRef,
|
|
307
292
|
lodgingBookingRef: lodging.bookingRef,
|
|
@@ -336,6 +321,7 @@ export const TravelSupplierReconcilerLayer: Layer.Layer<
|
|
|
336
321
|
ToolReconciler,
|
|
337
322
|
Effect.gen(function* () {
|
|
338
323
|
const desk = yield* SupplierBookingDesk;
|
|
324
|
+
|
|
339
325
|
return ToolReconciler.of({
|
|
340
326
|
reconcile: (evidence) =>
|
|
341
327
|
Effect.gen(function* () {
|
|
@@ -343,6 +329,7 @@ export const TravelSupplierReconcilerLayer: Layer.Layer<
|
|
|
343
329
|
case "book_flight": {
|
|
344
330
|
const key = bookFlightIdempotencyKey(evidence.toolCallId);
|
|
345
331
|
const booking = yield* desk.lookup(key);
|
|
332
|
+
|
|
346
333
|
if (Option.isSome(booking) && booking.value.status === "confirmed") {
|
|
347
334
|
const confirmation = yield* encodeConfirmation(
|
|
348
335
|
SupplierBookingConfirmation.make({
|
|
@@ -351,8 +338,10 @@ export const TravelSupplierReconcilerLayer: Layer.Layer<
|
|
|
351
338
|
detail: booking.value.detail,
|
|
352
339
|
}),
|
|
353
340
|
).pipe(Effect.flatMap(decodePersistedJson));
|
|
341
|
+
|
|
354
342
|
return ReconciliationCompleted.make({ result: confirmation, isFailure: false });
|
|
355
343
|
}
|
|
344
|
+
|
|
356
345
|
return ReconciliationUncertain.make({
|
|
357
346
|
reason: `The supplier desk shows no confirmed booking under ${key}; a write may still be in flight.`,
|
|
358
347
|
});
|
|
@@ -384,7 +373,7 @@ export const TravelSupplierReconcilerLayer: Layer.Layer<
|
|
|
384
373
|
* idempotent-by-contract cancellation, and one Durable Tool whose Steps carry supplier
|
|
385
374
|
* idempotency keys.
|
|
386
375
|
*/
|
|
387
|
-
export const TravelPlannerPhase5 = Agent.
|
|
376
|
+
export const TravelPlannerPhase5 = Agent.make("travel-planner-phase-5", {
|
|
388
377
|
input: TripRequest,
|
|
389
378
|
output: TravelBookingReport,
|
|
390
379
|
instructions: [
|
|
@@ -413,6 +402,7 @@ export class TravelPlannerBookingEvidenceError extends Schema.TaggedError<Travel
|
|
|
413
402
|
const bookingResultRefs = (result: unknown): ReadonlyArray<string> => {
|
|
414
403
|
if (typeof result !== "object" || result === null) return [];
|
|
415
404
|
const refs: Array<string> = [];
|
|
405
|
+
|
|
416
406
|
for (const [field, value] of Object.entries(result)) {
|
|
417
407
|
if (
|
|
418
408
|
typeof value === "string" &&
|
|
@@ -424,6 +414,7 @@ const bookingResultRefs = (result: unknown): ReadonlyArray<string> => {
|
|
|
424
414
|
refs.push(value);
|
|
425
415
|
}
|
|
426
416
|
}
|
|
417
|
+
|
|
427
418
|
return refs;
|
|
428
419
|
};
|
|
429
420
|
|
|
@@ -443,8 +434,10 @@ export const assertSettledBookingsExistAtSupplier = Effect.fn(
|
|
|
443
434
|
const desk = yield* SupplierBookingDesk;
|
|
444
435
|
const bookings = yield* desk.bookings;
|
|
445
436
|
const knownRefs = new Set<string>(bookings.map((booking) => booking.bookingRef));
|
|
437
|
+
|
|
446
438
|
for (const envelope of records) {
|
|
447
439
|
const payload = envelope.record.payload;
|
|
440
|
+
|
|
448
441
|
if (
|
|
449
442
|
payload._tag !== "ToolCallSettled" ||
|
|
450
443
|
payload.isFailure ||
|