@effect-agent/testing 0.1.0-beta.8 → 0.1.0-beta.80
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 +592 -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 +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 +1963 -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 -3406
- 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} +251 -117
- 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} +112 -45
- 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 -41
- 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 +187 -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,24 +1,18 @@
|
|
|
1
|
+
import { connectMcp, type McpDiscovery } from "@effect-agent/capabilities/Mcp";
|
|
1
2
|
import {
|
|
2
|
-
connectMcp,
|
|
3
3
|
Redactor,
|
|
4
|
-
SubagentReservationsMemoryLive,
|
|
5
|
-
type McpDiscovery,
|
|
6
4
|
type RedactedPreview,
|
|
7
5
|
type RedactionError,
|
|
8
|
-
} from "@effect-agent/capabilities";
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type IdempotencyKey,
|
|
19
|
-
type ResolvedBinding,
|
|
20
|
-
} from "@effect-agent/session";
|
|
21
|
-
import { Crypto, Effect, Layer, Ref, Schema, Stream } from "effect";
|
|
6
|
+
} from "@effect-agent/capabilities/Redaction";
|
|
7
|
+
import { SubagentReservationsMemoryLive } from "@effect-agent/capabilities/SubagentReservations";
|
|
8
|
+
import * as Agent from "@effect-agent/core/Agent";
|
|
9
|
+
import { type ThreadId } from "@effect-agent/core/Identifiers";
|
|
10
|
+
import { DurableWorkerBinding, type ResolvedBinding } from "@effect-agent/thread/AgentRegistration";
|
|
11
|
+
import { type DurableSubmitOptions } from "@effect-agent/thread/DurableAgentRuntime";
|
|
12
|
+
import { DefinitionDigests, DeploymentId, Digest, ProducerId } from "@effect-agent/thread/Records";
|
|
13
|
+
import { Principal, type IdempotencyKey } from "@effect-agent/thread/SubmissionLedger";
|
|
14
|
+
import type { Crypto } from "effect";
|
|
15
|
+
import { Effect, Layer, Ref, Schema, Stream } from "effect";
|
|
22
16
|
import { LanguageModel, Model, type Response } from "effect/unstable/ai";
|
|
23
17
|
|
|
24
18
|
import { DeterministicIdGeneratorLayer } from "../travel-planner/deterministic-layers.ts";
|
|
@@ -52,9 +46,11 @@ import {
|
|
|
52
46
|
export const docsResearcherDeploymentId = Schema.decodeSync(DeploymentId)(
|
|
53
47
|
"docs-researcher-p7-deployment",
|
|
54
48
|
);
|
|
49
|
+
|
|
55
50
|
export const docsResearcherProducerId = Schema.decodeSync(ProducerId)(
|
|
56
51
|
"docs-researcher-p7-producer",
|
|
57
52
|
);
|
|
53
|
+
|
|
58
54
|
export const docsResearcherPrincipal = Schema.decodeSync(Principal)("docs-researcher-p7-principal");
|
|
59
55
|
|
|
60
56
|
const digestOf = (pair: string) => Schema.decodeSync(Digest)(pair.repeat(32));
|
|
@@ -75,10 +71,10 @@ export const docsSummarizerDigests = DefinitionDigests.make({
|
|
|
75
71
|
|
|
76
72
|
/** Durable admission options for one docs-researcher Submission on one mission lane. */
|
|
77
73
|
export const docsResearcherSubmitOptions = (
|
|
78
|
-
|
|
74
|
+
threadId: ThreadId,
|
|
79
75
|
idempotencyKey: IdempotencyKey,
|
|
80
76
|
): DurableSubmitOptions => ({
|
|
81
|
-
|
|
77
|
+
threadId,
|
|
82
78
|
principal: docsResearcherPrincipal,
|
|
83
79
|
idempotencyKey,
|
|
84
80
|
definitions: docsCoordinatorDigests,
|
|
@@ -100,15 +96,13 @@ const scriptedUsage = { inputTokens: { total: 96 }, outputTokens: { total: 64 }
|
|
|
100
96
|
const summaryDelegationParts = (
|
|
101
97
|
documentIds: ReadonlyArray<string>,
|
|
102
98
|
): ReadonlyArray<Response.StreamPartEncoded> => [
|
|
103
|
-
...documentIds.map(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}),
|
|
111
|
-
),
|
|
99
|
+
...documentIds.map((documentId): Response.StreamPartEncoded => ({
|
|
100
|
+
type: "tool-call",
|
|
101
|
+
id: summarizeCallId(documentId),
|
|
102
|
+
name: "delegate_document_summary",
|
|
103
|
+
params: { documentId },
|
|
104
|
+
providerExecuted: false,
|
|
105
|
+
})),
|
|
112
106
|
{ type: "finish", reason: "tool-calls", usage: scriptedUsage },
|
|
113
107
|
];
|
|
114
108
|
|
|
@@ -155,6 +149,7 @@ const makeCountingModel = (
|
|
|
155
149
|
Effect.gen(function* () {
|
|
156
150
|
const calls = yield* Ref.make(0);
|
|
157
151
|
const prompts = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
152
|
+
|
|
158
153
|
const model = Model.make(
|
|
159
154
|
"scripted",
|
|
160
155
|
name,
|
|
@@ -167,13 +162,16 @@ const makeCountingModel = (
|
|
|
167
162
|
Effect.gen(function* () {
|
|
168
163
|
yield* Ref.update(calls, (value) => value + 1);
|
|
169
164
|
const promptJson = JSON.stringify(request.prompt);
|
|
165
|
+
|
|
170
166
|
yield* Ref.update(prompts, (previous) => [...previous, promptJson]);
|
|
167
|
+
|
|
171
168
|
return Stream.fromIterable(yield* decide(promptJson));
|
|
172
169
|
}),
|
|
173
170
|
),
|
|
174
171
|
}),
|
|
175
172
|
),
|
|
176
173
|
);
|
|
174
|
+
|
|
177
175
|
return { model, calls: Ref.get(calls), prompts: Ref.get(prompts) };
|
|
178
176
|
});
|
|
179
177
|
|
|
@@ -185,7 +183,7 @@ export interface DocsResearcherHarnessOptions {
|
|
|
185
183
|
|
|
186
184
|
/** One durable coordinator/summarizer pair with observable counters and MCP evidence. */
|
|
187
185
|
export interface DocsResearcherHarness {
|
|
188
|
-
/**
|
|
186
|
+
/** Resolved registrations for the parent and child workers. */
|
|
189
187
|
readonly bindings: ReadonlyArray<ResolvedBinding>;
|
|
190
188
|
/** The validated MCP discovery the child toolkit registration was gated on. */
|
|
191
189
|
readonly discovery: McpDiscovery;
|
|
@@ -218,12 +216,15 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
218
216
|
const discovery = yield* Effect.scoped(
|
|
219
217
|
Effect.gen(function* () {
|
|
220
218
|
const connection = yield* connectMcp(docsMcpRequest);
|
|
219
|
+
|
|
221
220
|
yield* assertDiscoveryMatchesAuthoredToolkit(connection);
|
|
221
|
+
|
|
222
222
|
return connection.discovery;
|
|
223
223
|
}),
|
|
224
224
|
).pipe(Effect.provide(docsMcpConnectorLayer));
|
|
225
225
|
|
|
226
226
|
const fetchCounts = yield* Ref.make<ReadonlyMap<string, number>>(new Map());
|
|
227
|
+
|
|
227
228
|
const libraryLayer = Layer.succeed(
|
|
228
229
|
DocumentLibrary,
|
|
229
230
|
DocumentLibrary.of({
|
|
@@ -233,14 +234,17 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
233
234
|
).pipe(Effect.andThen(researchDocumentLookup(query))),
|
|
234
235
|
}),
|
|
235
236
|
);
|
|
237
|
+
|
|
236
238
|
const childToolkitLayer = docContentToolkitLayer.pipe(Layer.provideMerge(libraryLayer));
|
|
237
239
|
|
|
238
240
|
const childModel = yield* makeCountingModel("doc-summarizer-p7", (promptJson) =>
|
|
239
241
|
Effect.suspend(() => {
|
|
240
242
|
const documentId = documentIds.find((candidate) => promptJson.includes(candidate));
|
|
243
|
+
|
|
241
244
|
if (documentId === undefined) {
|
|
242
245
|
return Effect.die(new Error("The summarizer prompt names no corpus document"));
|
|
243
246
|
}
|
|
247
|
+
|
|
244
248
|
return Effect.succeed(
|
|
245
249
|
promptJson.includes(fetchCallId(documentId))
|
|
246
250
|
? summaryParts(documentId)
|
|
@@ -248,9 +252,11 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
248
252
|
);
|
|
249
253
|
}),
|
|
250
254
|
);
|
|
255
|
+
|
|
251
256
|
const childBinding = Agent.withModel(DocSummarizer, childModel.model);
|
|
252
257
|
|
|
253
258
|
const firstCallId = summarizeCallId(documentIds[0] ?? "durability-notes");
|
|
259
|
+
|
|
254
260
|
const parentModel = yield* makeCountingModel("docs-researcher-p7", (promptJson) =>
|
|
255
261
|
Effect.succeed(
|
|
256
262
|
promptJson.includes(firstCallId)
|
|
@@ -258,6 +264,7 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
258
264
|
: summaryDelegationParts(documentIds),
|
|
259
265
|
),
|
|
260
266
|
);
|
|
267
|
+
|
|
261
268
|
const parentBinding = Agent.withModel(DocsResearcher, parentModel.model);
|
|
262
269
|
|
|
263
270
|
const delegationLayer = docsSummaryHandlersLayer(childBinding).pipe(
|
|
@@ -274,6 +281,7 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
274
281
|
parentBinding,
|
|
275
282
|
docsCoordinatorDigests,
|
|
276
283
|
).pipe(Effect.provide(delegationLayer));
|
|
284
|
+
|
|
277
285
|
const childResolved: ResolvedBinding = yield* DurableWorkerBinding.make(
|
|
278
286
|
childBinding,
|
|
279
287
|
docsSummarizerDigests,
|
|
@@ -289,6 +297,7 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
289
297
|
fetchInvocations: (documentId) =>
|
|
290
298
|
Ref.get(fetchCounts).pipe(Effect.map((current) => current.get(documentId) ?? 0)),
|
|
291
299
|
};
|
|
300
|
+
|
|
292
301
|
return harness;
|
|
293
302
|
});
|
|
294
303
|
|
|
@@ -297,15 +306,17 @@ const encodeResearchDocument = Schema.encodeEffect(ResearchDocument);
|
|
|
297
306
|
/**
|
|
298
307
|
* The audit-surface preview of one fetched document: the raw document —
|
|
299
308
|
* secret marker and all — passes through the configured structural `Redactor`
|
|
300
|
-
* before anything may quote it outside the child
|
|
309
|
+
* before anything may quote it outside the child Thread (SEC-008,
|
|
301
310
|
* CAP-013). Tests assert the preview keeps shape but no scalar content.
|
|
302
311
|
*/
|
|
303
312
|
export const redactedDocumentPreview = Effect.fn("DocsResearcher.redactedDocumentPreview")(
|
|
304
313
|
function* (documentId: string): Effect.fn.Return<RedactedPreview, RedactionError, Redactor> {
|
|
305
314
|
const redactor = yield* Redactor;
|
|
315
|
+
|
|
306
316
|
const encoded = yield* encodeResearchDocument(researchDocumentFor(documentId)).pipe(
|
|
307
317
|
Effect.orDie,
|
|
308
318
|
);
|
|
319
|
+
|
|
309
320
|
return yield* redactor.redact(encoded);
|
|
310
321
|
},
|
|
311
322
|
);
|
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
McpServerIdentity,
|
|
5
5
|
McpToolkitMismatch,
|
|
6
6
|
type McpConnection,
|
|
7
|
-
} from "@effect-agent/capabilities";
|
|
8
|
-
import {
|
|
7
|
+
} from "@effect-agent/capabilities/Mcp";
|
|
8
|
+
import type { JsonSchema } from "effect";
|
|
9
|
+
import { Effect, JsonPointer, Layer, Schema } from "effect";
|
|
9
10
|
import { Tool } from "effect/unstable/ai";
|
|
10
11
|
import * as McpSchema from "effect/unstable/ai/McpSchema";
|
|
11
12
|
|
|
@@ -37,10 +38,52 @@ export const docsMcpIdentity = McpServerIdentity.make({
|
|
|
37
38
|
}),
|
|
38
39
|
});
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* `Tool.getJsonSchema` produces a `$ref`/`$defs`-shaped schema for
|
|
43
|
+
* `FetchDocument`'s named, refined parameters type, but `McpSchema.Tool`'s
|
|
44
|
+
* `inputSchema` requires a flat `{ type: "object", ... }` root — the shape a
|
|
45
|
+
* real MCP server advertises on the wire. This inlines the single top-level
|
|
46
|
+
* `$ref` so the derivation described above still holds byte-for-byte.
|
|
47
|
+
*/
|
|
48
|
+
const JsonSchemaDefinitions = Schema.Record(
|
|
49
|
+
Schema.String,
|
|
50
|
+
Schema.Record(Schema.String, Schema.Unknown),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const decodeJsonSchemaDefinitions = Schema.decodeUnknownSync(JsonSchemaDefinitions);
|
|
54
|
+
const decodeToolJsonSchema = Schema.decodeUnknownSync(McpSchema.ToolJsonSchema);
|
|
55
|
+
|
|
56
|
+
const flattenTopLevelRef = (schema: JsonSchema.JsonSchema): McpSchema.ToolJsonSchema => {
|
|
57
|
+
const ref = schema["$ref"];
|
|
58
|
+
|
|
59
|
+
if (typeof ref !== "string") {
|
|
60
|
+
return decodeToolJsonSchema(schema);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const defs = decodeJsonSchemaDefinitions(schema["$defs"]);
|
|
64
|
+
|
|
65
|
+
const key = ref.startsWith("#/$defs/")
|
|
66
|
+
? JsonPointer.unescapeToken(ref.slice("#/$defs/".length))
|
|
67
|
+
: undefined;
|
|
68
|
+
|
|
69
|
+
const resolved = key !== undefined && Object.hasOwn(defs, key) ? defs[key] : undefined;
|
|
70
|
+
|
|
71
|
+
return decodeToolJsonSchema(resolved ?? schema);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const fetchDocumentOutputSchema = flattenTopLevelRef(
|
|
75
|
+
Tool.getJsonSchemaFromSchema(FetchDocument.successSchema),
|
|
76
|
+
);
|
|
77
|
+
|
|
40
78
|
const discoveredFetchDocument = McpSchema.Tool.make({
|
|
41
79
|
name: FetchDocument.name,
|
|
42
80
|
description: "Fetch one bounded research document by its identifier.",
|
|
43
|
-
inputSchema: Tool.getJsonSchema(FetchDocument),
|
|
81
|
+
inputSchema: flattenTopLevelRef(Tool.getJsonSchema(FetchDocument)),
|
|
82
|
+
// `validateMcpDiscovery` only compares an `outputSchema` derived down to an
|
|
83
|
+
// object type; mirror that so this fixture stays a real round-trip check.
|
|
84
|
+
...(fetchDocumentOutputSchema.type === "object"
|
|
85
|
+
? { outputSchema: fetchDocumentOutputSchema }
|
|
86
|
+
: {}),
|
|
44
87
|
});
|
|
45
88
|
|
|
46
89
|
const scriptedConnector = (tools: ReadonlyArray<McpSchema.Tool>): Layer.Layer<McpConnector> =>
|
|
@@ -98,9 +141,11 @@ export const assertDiscoveryMatchesAuthoredToolkit = Effect.fn(
|
|
|
98
141
|
const authored = Object.values(DocContentToolkit.tools)
|
|
99
142
|
.map((tool) => ({ name: tool.name, inputSchema: Tool.getJsonSchema(tool) }))
|
|
100
143
|
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
144
|
+
|
|
101
145
|
const discovered = Object.values(connection.toolkit.tools)
|
|
102
146
|
.map((tool) => ({ name: tool.name, inputSchema: Tool.getJsonSchema(tool) }))
|
|
103
147
|
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
148
|
+
|
|
104
149
|
const matches =
|
|
105
150
|
authored.length === discovered.length &&
|
|
106
151
|
authored.every(
|
|
@@ -108,6 +153,7 @@ export const assertDiscoveryMatchesAuthoredToolkit = Effect.fn(
|
|
|
108
153
|
tool.name === discovered[index]?.name &&
|
|
109
154
|
isJsonEqual(tool.inputSchema, discovered[index]?.inputSchema),
|
|
110
155
|
);
|
|
156
|
+
|
|
111
157
|
if (!matches) {
|
|
112
158
|
return yield* McpToolkitMismatch.make({
|
|
113
159
|
serverId: connection.discovery.identity.serverId,
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Agent from "@effect-agent/core/Agent";
|
|
2
|
+
import { AgentPolicy } from "@effect-agent/core/AgentPolicy";
|
|
2
3
|
import { Context, Effect, Schema } from "effect";
|
|
3
4
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
4
5
|
|
|
5
6
|
export const AirportCode = Schema.NonEmptyString.pipe(
|
|
6
7
|
Schema.brand("@effect-agent/testing/travel-planner/AirportCode"),
|
|
7
8
|
);
|
|
9
|
+
|
|
8
10
|
export type AirportCode = typeof AirportCode.Type;
|
|
9
11
|
|
|
10
12
|
export const QuoteId = Schema.NonEmptyString.pipe(
|
|
11
13
|
Schema.brand("@effect-agent/testing/travel-planner/QuoteId"),
|
|
12
14
|
);
|
|
15
|
+
|
|
13
16
|
export type QuoteId = typeof QuoteId.Type;
|
|
14
17
|
|
|
15
18
|
export class TripRequest extends Schema.Class<TripRequest>("TripRequest")({
|
|
@@ -84,18 +87,22 @@ export class TravelPlan extends Schema.Class<TravelPlan>("TravelPlan")({
|
|
|
84
87
|
}) {}
|
|
85
88
|
|
|
86
89
|
const unavailableFields = { query: Schema.String, message: Schema.String };
|
|
90
|
+
|
|
87
91
|
export class FlightUnavailable extends Schema.TaggedError<FlightUnavailable>()(
|
|
88
92
|
"FlightUnavailable",
|
|
89
93
|
unavailableFields,
|
|
90
94
|
) {}
|
|
95
|
+
|
|
91
96
|
export class LodgingUnavailable extends Schema.TaggedError<LodgingUnavailable>()(
|
|
92
97
|
"LodgingUnavailable",
|
|
93
98
|
unavailableFields,
|
|
94
99
|
) {}
|
|
100
|
+
|
|
95
101
|
export class ActivityUnavailable extends Schema.TaggedError<ActivityUnavailable>()(
|
|
96
102
|
"ActivityUnavailable",
|
|
97
103
|
unavailableFields,
|
|
98
104
|
) {}
|
|
105
|
+
|
|
99
106
|
export class GuidanceFailure extends Schema.TaggedError<GuidanceFailure>()("GuidanceFailure", {
|
|
100
107
|
message: Schema.String,
|
|
101
108
|
}) {}
|
|
@@ -104,10 +111,12 @@ export class FlightCatalog extends Context.Service<
|
|
|
104
111
|
FlightCatalog,
|
|
105
112
|
{ readonly search: (query: FlightQuery) => Effect.Effect<FlightOption, FlightUnavailable> }
|
|
106
113
|
>()("@effect-agent/testing/travel-planner/FlightCatalog") {}
|
|
114
|
+
|
|
107
115
|
export class LodgingCatalog extends Context.Service<
|
|
108
116
|
LodgingCatalog,
|
|
109
117
|
{ readonly search: (query: LodgingQuery) => Effect.Effect<LodgingOption, LodgingUnavailable> }
|
|
110
118
|
>()("@effect-agent/testing/travel-planner/LodgingCatalog") {}
|
|
119
|
+
|
|
111
120
|
export class ActivityCatalog extends Context.Service<
|
|
112
121
|
ActivityCatalog,
|
|
113
122
|
{
|
|
@@ -116,6 +125,7 @@ export class ActivityCatalog extends Context.Service<
|
|
|
116
125
|
) => Effect.Effect<ActivitySearchResult, ActivityUnavailable>;
|
|
117
126
|
}
|
|
118
127
|
>()("@effect-agent/testing/travel-planner/ActivityCatalog") {}
|
|
128
|
+
|
|
119
129
|
export class TravelGuidance extends Context.Service<
|
|
120
130
|
TravelGuidance,
|
|
121
131
|
{ readonly instructions: (input: TripRequest) => Effect.Effect<string, GuidanceFailure> }
|
|
@@ -128,6 +138,7 @@ export const SearchFlights = Tool.make("search_flights", {
|
|
|
128
138
|
failureMode: "error",
|
|
129
139
|
dependencies: [FlightCatalog],
|
|
130
140
|
});
|
|
141
|
+
|
|
131
142
|
export const SearchLodging = Tool.make("search_lodging", {
|
|
132
143
|
parameters: LodgingQuery,
|
|
133
144
|
success: LodgingOption,
|
|
@@ -135,6 +146,7 @@ export const SearchLodging = Tool.make("search_lodging", {
|
|
|
135
146
|
failureMode: "error",
|
|
136
147
|
dependencies: [LodgingCatalog],
|
|
137
148
|
});
|
|
149
|
+
|
|
138
150
|
export const SearchActivities = Tool.make("search_activities", {
|
|
139
151
|
parameters: ActivityQuery,
|
|
140
152
|
success: ActivitySearchResult,
|
|
@@ -144,13 +156,14 @@ export const SearchActivities = Tool.make("search_activities", {
|
|
|
144
156
|
});
|
|
145
157
|
|
|
146
158
|
export const TravelPlannerToolkit = Toolkit.make(SearchFlights, SearchLodging, SearchActivities);
|
|
159
|
+
|
|
147
160
|
export const TravelPlannerToolkitLayer = TravelPlannerToolkit.toLayer({
|
|
148
161
|
search_flights: (query) => Effect.flatMap(FlightCatalog, (catalog) => catalog.search(query)),
|
|
149
162
|
search_lodging: (query) => Effect.flatMap(LodgingCatalog, (catalog) => catalog.search(query)),
|
|
150
163
|
search_activities: (query) => Effect.flatMap(ActivityCatalog, (catalog) => catalog.search(query)),
|
|
151
164
|
});
|
|
152
165
|
|
|
153
|
-
export const TravelPlanner = Agent.
|
|
166
|
+
export const TravelPlanner = Agent.make("travel-planner", {
|
|
154
167
|
input: TripRequest,
|
|
155
168
|
output: TravelPlan,
|
|
156
169
|
instructions: (input) =>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ThreadId, RunId, TurnId } from "@effect-agent/core/Identifiers";
|
|
2
|
+
import { IdGenerator } from "@effect-agent/core/IdGenerator";
|
|
3
|
+
import { RunContextPreparationPassthrough } from "@effect-agent/engine/RunOptions";
|
|
4
|
+
import { ThreadHistory } from "@effect-agent/engine/ThreadHistory";
|
|
2
5
|
import { Context, Deferred, Effect, Layer, Option, Ref, Schema } from "effect";
|
|
3
6
|
|
|
4
7
|
import {
|
|
@@ -23,6 +26,7 @@ export class CatalogLifecycleCounts extends Schema.Class<CatalogLifecycleCounts>
|
|
|
23
26
|
acquired: Schema.Natural,
|
|
24
27
|
finalized: Schema.Natural,
|
|
25
28
|
}) {}
|
|
29
|
+
|
|
26
30
|
export class CatalogLifecycle extends Context.Service<
|
|
27
31
|
CatalogLifecycle,
|
|
28
32
|
{
|
|
@@ -36,6 +40,7 @@ export class CatalogLifecycle extends Context.Service<
|
|
|
36
40
|
Effect.gen(function* () {
|
|
37
41
|
const acquired = yield* Ref.make(0);
|
|
38
42
|
const finalized = yield* Ref.make(0);
|
|
43
|
+
|
|
39
44
|
return CatalogLifecycle.of({
|
|
40
45
|
markAcquired: Ref.update(acquired, (n) => n + 1),
|
|
41
46
|
markFinalized: Ref.update(finalized, (n) => n + 1),
|
|
@@ -53,11 +58,13 @@ const flight = FlightOption.make({
|
|
|
53
58
|
estimatedCents: 180_000,
|
|
54
59
|
currency: "USD",
|
|
55
60
|
});
|
|
61
|
+
|
|
56
62
|
const lodging = LodgingOption.make({
|
|
57
63
|
lodging: "Bloomsbury House · refundable studio · 4 nights",
|
|
58
64
|
estimatedCents: 104_000,
|
|
59
65
|
currency: "USD",
|
|
60
66
|
});
|
|
67
|
+
|
|
61
68
|
const activities = ActivitySearchResult.make({
|
|
62
69
|
activities: ["British Museum timed entry", "Thames evening walk"],
|
|
63
70
|
});
|
|
@@ -84,6 +91,7 @@ export const ReverseCompletionToolkitLayer = Effect.gen(function* () {
|
|
|
84
91
|
const releaseFlight = yield* Deferred.make<void>();
|
|
85
92
|
const releaseLodging = yield* Deferred.make<void>();
|
|
86
93
|
const releaseActivity = yield* Deferred.make<void>();
|
|
94
|
+
|
|
87
95
|
const awaitRelease = <A>(
|
|
88
96
|
started: Deferred.Deferred<void>,
|
|
89
97
|
release: Deferred.Deferred<void>,
|
|
@@ -93,6 +101,7 @@ export const ReverseCompletionToolkitLayer = Effect.gen(function* () {
|
|
|
93
101
|
Effect.andThen(Deferred.await(release)),
|
|
94
102
|
Effect.as(value),
|
|
95
103
|
);
|
|
104
|
+
|
|
96
105
|
return {
|
|
97
106
|
controls: {
|
|
98
107
|
flightStarted: Deferred.await(flightStarted),
|
|
@@ -114,7 +123,9 @@ export const FlightCatalogLayer = Layer.effect(
|
|
|
114
123
|
FlightCatalog,
|
|
115
124
|
Effect.gen(function* () {
|
|
116
125
|
const lifecycle = yield* CatalogLifecycle;
|
|
126
|
+
|
|
117
127
|
yield* Effect.acquireRelease(lifecycle.markAcquired, () => lifecycle.markFinalized);
|
|
128
|
+
|
|
118
129
|
return FlightCatalog.of({
|
|
119
130
|
search: (query) =>
|
|
120
131
|
query.origin === query.destination
|
|
@@ -128,11 +139,14 @@ export const FlightCatalogLayer = Layer.effect(
|
|
|
128
139
|
});
|
|
129
140
|
}),
|
|
130
141
|
);
|
|
142
|
+
|
|
131
143
|
export const LodgingCatalogLayer = Layer.effect(
|
|
132
144
|
LodgingCatalog,
|
|
133
145
|
Effect.gen(function* () {
|
|
134
146
|
const lifecycle = yield* CatalogLifecycle;
|
|
147
|
+
|
|
135
148
|
yield* Effect.acquireRelease(lifecycle.markAcquired, () => lifecycle.markFinalized);
|
|
149
|
+
|
|
136
150
|
return LodgingCatalog.of({
|
|
137
151
|
search: (query) =>
|
|
138
152
|
query.nights < 1
|
|
@@ -146,11 +160,14 @@ export const LodgingCatalogLayer = Layer.effect(
|
|
|
146
160
|
});
|
|
147
161
|
}),
|
|
148
162
|
);
|
|
163
|
+
|
|
149
164
|
export const ActivityCatalogLayer = Layer.effect(
|
|
150
165
|
ActivityCatalog,
|
|
151
166
|
Effect.gen(function* () {
|
|
152
167
|
const lifecycle = yield* CatalogLifecycle;
|
|
168
|
+
|
|
153
169
|
yield* Effect.acquireRelease(lifecycle.markAcquired, () => lifecycle.markFinalized);
|
|
170
|
+
|
|
154
171
|
return ActivityCatalog.of({
|
|
155
172
|
search: (query) =>
|
|
156
173
|
query.destination === ""
|
|
@@ -164,10 +181,12 @@ export const ActivityCatalogLayer = Layer.effect(
|
|
|
164
181
|
});
|
|
165
182
|
}),
|
|
166
183
|
);
|
|
184
|
+
|
|
167
185
|
/** Stable supplier-side booking identity, minted deterministically from the idempotency key. */
|
|
168
186
|
export const BookingRef = Schema.NonEmptyString.pipe(
|
|
169
187
|
Schema.brand("@effect-agent/testing/travel-planner/BookingRef"),
|
|
170
188
|
);
|
|
189
|
+
|
|
171
190
|
export type BookingRef = typeof BookingRef.Type;
|
|
172
191
|
|
|
173
192
|
/** The supplier desk operations the P5 booking Tools and Steps invoke. */
|
|
@@ -178,6 +197,7 @@ export const SupplierOperation = Schema.Literals([
|
|
|
178
197
|
"reserve-lodging",
|
|
179
198
|
"issue-confirmation",
|
|
180
199
|
]);
|
|
200
|
+
|
|
181
201
|
export type SupplierOperation = typeof SupplierOperation.Type;
|
|
182
202
|
|
|
183
203
|
/**
|
|
@@ -286,7 +306,9 @@ export class SupplierBookingDesk extends Context.Service<
|
|
|
286
306
|
request.idempotencyKey,
|
|
287
307
|
(current.counts.get(request.idempotencyKey) ?? 0) + 1,
|
|
288
308
|
);
|
|
309
|
+
|
|
289
310
|
const existing = current.bookings.get(request.idempotencyKey);
|
|
311
|
+
|
|
290
312
|
const record =
|
|
291
313
|
existing ??
|
|
292
314
|
SupplierBookingRecord.make({
|
|
@@ -296,18 +318,24 @@ export class SupplierBookingDesk extends Context.Service<
|
|
|
296
318
|
detail: request.detail,
|
|
297
319
|
status: "confirmed",
|
|
298
320
|
});
|
|
321
|
+
|
|
299
322
|
const bookings =
|
|
300
323
|
existing === undefined
|
|
301
324
|
? new Map(current.bookings).set(request.idempotencyKey, record)
|
|
302
325
|
: current.bookings;
|
|
326
|
+
|
|
303
327
|
const hold = Option.fromNullishOr(current.holds.get(request.idempotencyKey));
|
|
328
|
+
|
|
304
329
|
const holds = Option.isSome(hold)
|
|
305
330
|
? (() => {
|
|
306
331
|
const next = new Map(current.holds);
|
|
332
|
+
|
|
307
333
|
next.delete(request.idempotencyKey);
|
|
334
|
+
|
|
308
335
|
return next;
|
|
309
336
|
})()
|
|
310
337
|
: current.holds;
|
|
338
|
+
|
|
311
339
|
return [
|
|
312
340
|
{ record, hold },
|
|
313
341
|
{ bookings, counts, holds },
|
|
@@ -318,9 +346,11 @@ export class SupplierBookingDesk extends Context.Service<
|
|
|
318
346
|
Ref.modify(state, (current) => {
|
|
319
347
|
const key = cancelBookingIdempotencyKey(bookingRef);
|
|
320
348
|
const counts = new Map(current.counts).set(key, (current.counts.get(key) ?? 0) + 1);
|
|
349
|
+
|
|
321
350
|
const existingEntry = [...current.bookings.entries()].find(
|
|
322
351
|
([, record]) => record.bookingRef === bookingRef,
|
|
323
352
|
);
|
|
353
|
+
|
|
324
354
|
if (existingEntry === undefined) {
|
|
325
355
|
return [
|
|
326
356
|
{ record: Option.none<SupplierBookingRecord>(), hold: Option.none() },
|
|
@@ -328,19 +358,25 @@ export class SupplierBookingDesk extends Context.Service<
|
|
|
328
358
|
] as const;
|
|
329
359
|
}
|
|
330
360
|
const [storeKey, existing] = existingEntry;
|
|
361
|
+
|
|
331
362
|
const cancelled =
|
|
332
363
|
existing.status === "cancelled"
|
|
333
364
|
? existing
|
|
334
365
|
: SupplierBookingRecord.make({ ...existing, status: "cancelled" });
|
|
366
|
+
|
|
335
367
|
const bookings = new Map(current.bookings).set(storeKey, cancelled);
|
|
336
368
|
const hold = Option.fromNullishOr(current.holds.get(key));
|
|
369
|
+
|
|
337
370
|
const holds = Option.isSome(hold)
|
|
338
371
|
? (() => {
|
|
339
372
|
const next = new Map(current.holds);
|
|
373
|
+
|
|
340
374
|
next.delete(key);
|
|
375
|
+
|
|
341
376
|
return next;
|
|
342
377
|
})()
|
|
343
378
|
: current.holds;
|
|
379
|
+
|
|
344
380
|
return [
|
|
345
381
|
{ record: Option.some(cancelled), hold },
|
|
346
382
|
{ bookings, counts, holds },
|
|
@@ -371,10 +407,12 @@ export class SupplierBookingDesk extends Context.Service<
|
|
|
371
407
|
Effect.gen(function* () {
|
|
372
408
|
const held = yield* Deferred.make<void>();
|
|
373
409
|
const release = yield* Deferred.make<void>();
|
|
410
|
+
|
|
374
411
|
yield* Ref.update(state, (current) => ({
|
|
375
412
|
...current,
|
|
376
413
|
holds: new Map(current.holds).set(idempotencyKey, { held, release }),
|
|
377
414
|
}));
|
|
415
|
+
|
|
378
416
|
return {
|
|
379
417
|
held: Deferred.await(held),
|
|
380
418
|
release: Deferred.succeed(release, undefined).pipe(Effect.asVoid),
|
|
@@ -402,15 +440,17 @@ export const TravelGuidanceLayer = Layer.succeed(
|
|
|
402
440
|
),
|
|
403
441
|
}),
|
|
404
442
|
);
|
|
443
|
+
|
|
405
444
|
export const DeterministicIdGeneratorLayer = Layer.effect(
|
|
406
445
|
IdGenerator,
|
|
407
446
|
Effect.gen(function* () {
|
|
408
|
-
const
|
|
447
|
+
const thread = yield* Ref.make(0);
|
|
409
448
|
const run = yield* Ref.make(0);
|
|
410
449
|
const turn = yield* Ref.make(0);
|
|
450
|
+
|
|
411
451
|
return IdGenerator.of({
|
|
412
|
-
|
|
413
|
-
Effect.map((n) => Schema.decodeSync(
|
|
452
|
+
nextThreadId: Ref.updateAndGet(thread, (n) => n + 1).pipe(
|
|
453
|
+
Effect.map((n) => Schema.decodeSync(ThreadId)(`thread-${n}`)),
|
|
414
454
|
),
|
|
415
455
|
nextRunId: Ref.updateAndGet(run, (n) => n + 1).pipe(
|
|
416
456
|
Effect.map((n) => Schema.decodeSync(RunId)(`run-${n}`)),
|
|
@@ -421,7 +461,10 @@ export const DeterministicIdGeneratorLayer = Layer.effect(
|
|
|
421
461
|
});
|
|
422
462
|
}),
|
|
423
463
|
);
|
|
464
|
+
|
|
424
465
|
export const TravelPlannerRuntimeLayer = Layer.mergeAll(
|
|
466
|
+
RunContextPreparationPassthrough,
|
|
467
|
+
ThreadHistory.layerTransient,
|
|
425
468
|
TravelPlannerToolkitLayer,
|
|
426
469
|
FlightCatalogLayer,
|
|
427
470
|
LodgingCatalogLayer,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Agent from "@effect-agent/core/Agent";
|
|
2
|
+
import { AgentPolicy } from "@effect-agent/core/AgentPolicy";
|
|
2
3
|
import { Context, Effect, Schema } from "effect";
|
|
3
4
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
4
5
|
|
|
@@ -75,7 +76,7 @@ export const TravelPlannerPhase2ToolkitLayer = TravelPlannerPhase2Toolkit.toLaye
|
|
|
75
76
|
Effect.flatMap(ItineraryHoldGateway, (gateway) => gateway.hold(request)),
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
export const TravelPlannerPhase2 = Agent.
|
|
79
|
+
export const TravelPlannerPhase2 = Agent.make("travel-planner-phase-2", {
|
|
79
80
|
input: TripRequest,
|
|
80
81
|
output: TravelPlan,
|
|
81
82
|
instructions: (input) =>
|
|
@@ -86,7 +87,7 @@ export const TravelPlannerPhase2 = Agent.define("travel-planner-phase-2", {
|
|
|
86
87
|
maxToolCalls: 4,
|
|
87
88
|
maxDuration: "30 seconds",
|
|
88
89
|
toolConcurrency: 3,
|
|
89
|
-
tokenBudget:
|
|
90
|
+
tokenBudget: 4_096,
|
|
90
91
|
}),
|
|
91
92
|
description:
|
|
92
93
|
"Build a review-only itinerary and require approval before creating a temporary hold.",
|