@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,24 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
DeploymentId,
|
|
13
|
-
Digest,
|
|
14
|
-
DurableWorkerBinding,
|
|
15
|
-
Principal,
|
|
16
|
-
ProducerId,
|
|
17
|
-
type DurableSubmitOptions,
|
|
18
|
-
type IdempotencyKey,
|
|
19
|
-
type ResolvedBinding,
|
|
20
|
-
} from "@effect-agent/session";
|
|
21
|
-
import { Crypto, Effect, Layer, Ref, Schema, Stream } from "effect";
|
|
1
|
+
import type { Crypto } from "effect";
|
|
2
|
+
import { Effect, Layer, Ref, Schema, Stream } from "effect";
|
|
3
|
+
import * as Agent from "effect-agent/agent";
|
|
4
|
+
import { DurableWorkerBinding, type ResolvedBinding } from "effect-agent/agent-registration";
|
|
5
|
+
import { type DurableSubmitOptions } from "effect-agent/durable-agent-runtime";
|
|
6
|
+
import { type ThreadId } from "effect-agent/identifiers";
|
|
7
|
+
import { connectMcp, type McpDiscovery } from "effect-agent/mcp";
|
|
8
|
+
import { DefinitionDigests, DeploymentId, Digest, ProducerId } from "effect-agent/records";
|
|
9
|
+
import { Redactor, type RedactedPreview, type RedactionError } from "effect-agent/redaction";
|
|
10
|
+
import { SubagentReservationsMemoryLive } from "effect-agent/subagent-reservations";
|
|
11
|
+
import { Principal, type IdempotencyKey } from "effect-agent/submission-ledger";
|
|
22
12
|
import { LanguageModel, Model, type Response } from "effect/unstable/ai";
|
|
23
13
|
|
|
24
14
|
import { DeterministicIdGeneratorLayer } from "../travel-planner/deterministic-layers.ts";
|
|
@@ -52,9 +42,11 @@ import {
|
|
|
52
42
|
export const docsResearcherDeploymentId = Schema.decodeSync(DeploymentId)(
|
|
53
43
|
"docs-researcher-p7-deployment",
|
|
54
44
|
);
|
|
45
|
+
|
|
55
46
|
export const docsResearcherProducerId = Schema.decodeSync(ProducerId)(
|
|
56
47
|
"docs-researcher-p7-producer",
|
|
57
48
|
);
|
|
49
|
+
|
|
58
50
|
export const docsResearcherPrincipal = Schema.decodeSync(Principal)("docs-researcher-p7-principal");
|
|
59
51
|
|
|
60
52
|
const digestOf = (pair: string) => Schema.decodeSync(Digest)(pair.repeat(32));
|
|
@@ -75,10 +67,10 @@ export const docsSummarizerDigests = DefinitionDigests.make({
|
|
|
75
67
|
|
|
76
68
|
/** Durable admission options for one docs-researcher Submission on one mission lane. */
|
|
77
69
|
export const docsResearcherSubmitOptions = (
|
|
78
|
-
|
|
70
|
+
threadId: ThreadId,
|
|
79
71
|
idempotencyKey: IdempotencyKey,
|
|
80
72
|
): DurableSubmitOptions => ({
|
|
81
|
-
|
|
73
|
+
threadId,
|
|
82
74
|
principal: docsResearcherPrincipal,
|
|
83
75
|
idempotencyKey,
|
|
84
76
|
definitions: docsCoordinatorDigests,
|
|
@@ -100,15 +92,13 @@ const scriptedUsage = { inputTokens: { total: 96 }, outputTokens: { total: 64 }
|
|
|
100
92
|
const summaryDelegationParts = (
|
|
101
93
|
documentIds: ReadonlyArray<string>,
|
|
102
94
|
): ReadonlyArray<Response.StreamPartEncoded> => [
|
|
103
|
-
...documentIds.map(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}),
|
|
111
|
-
),
|
|
95
|
+
...documentIds.map((documentId): Response.StreamPartEncoded => ({
|
|
96
|
+
type: "tool-call",
|
|
97
|
+
id: summarizeCallId(documentId),
|
|
98
|
+
name: "delegate_document_summary",
|
|
99
|
+
params: { documentId },
|
|
100
|
+
providerExecuted: false,
|
|
101
|
+
})),
|
|
112
102
|
{ type: "finish", reason: "tool-calls", usage: scriptedUsage },
|
|
113
103
|
];
|
|
114
104
|
|
|
@@ -155,6 +145,7 @@ const makeCountingModel = (
|
|
|
155
145
|
Effect.gen(function* () {
|
|
156
146
|
const calls = yield* Ref.make(0);
|
|
157
147
|
const prompts = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
148
|
+
|
|
158
149
|
const model = Model.make(
|
|
159
150
|
"scripted",
|
|
160
151
|
name,
|
|
@@ -167,13 +158,16 @@ const makeCountingModel = (
|
|
|
167
158
|
Effect.gen(function* () {
|
|
168
159
|
yield* Ref.update(calls, (value) => value + 1);
|
|
169
160
|
const promptJson = JSON.stringify(request.prompt);
|
|
161
|
+
|
|
170
162
|
yield* Ref.update(prompts, (previous) => [...previous, promptJson]);
|
|
163
|
+
|
|
171
164
|
return Stream.fromIterable(yield* decide(promptJson));
|
|
172
165
|
}),
|
|
173
166
|
),
|
|
174
167
|
}),
|
|
175
168
|
),
|
|
176
169
|
);
|
|
170
|
+
|
|
177
171
|
return { model, calls: Ref.get(calls), prompts: Ref.get(prompts) };
|
|
178
172
|
});
|
|
179
173
|
|
|
@@ -185,7 +179,7 @@ export interface DocsResearcherHarnessOptions {
|
|
|
185
179
|
|
|
186
180
|
/** One durable coordinator/summarizer pair with observable counters and MCP evidence. */
|
|
187
181
|
export interface DocsResearcherHarness {
|
|
188
|
-
/**
|
|
182
|
+
/** Resolved registrations for the parent and child workers. */
|
|
189
183
|
readonly bindings: ReadonlyArray<ResolvedBinding>;
|
|
190
184
|
/** The validated MCP discovery the child toolkit registration was gated on. */
|
|
191
185
|
readonly discovery: McpDiscovery;
|
|
@@ -218,12 +212,15 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
218
212
|
const discovery = yield* Effect.scoped(
|
|
219
213
|
Effect.gen(function* () {
|
|
220
214
|
const connection = yield* connectMcp(docsMcpRequest);
|
|
215
|
+
|
|
221
216
|
yield* assertDiscoveryMatchesAuthoredToolkit(connection);
|
|
217
|
+
|
|
222
218
|
return connection.discovery;
|
|
223
219
|
}),
|
|
224
220
|
).pipe(Effect.provide(docsMcpConnectorLayer));
|
|
225
221
|
|
|
226
222
|
const fetchCounts = yield* Ref.make<ReadonlyMap<string, number>>(new Map());
|
|
223
|
+
|
|
227
224
|
const libraryLayer = Layer.succeed(
|
|
228
225
|
DocumentLibrary,
|
|
229
226
|
DocumentLibrary.of({
|
|
@@ -233,14 +230,17 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
233
230
|
).pipe(Effect.andThen(researchDocumentLookup(query))),
|
|
234
231
|
}),
|
|
235
232
|
);
|
|
233
|
+
|
|
236
234
|
const childToolkitLayer = docContentToolkitLayer.pipe(Layer.provideMerge(libraryLayer));
|
|
237
235
|
|
|
238
236
|
const childModel = yield* makeCountingModel("doc-summarizer-p7", (promptJson) =>
|
|
239
237
|
Effect.suspend(() => {
|
|
240
238
|
const documentId = documentIds.find((candidate) => promptJson.includes(candidate));
|
|
239
|
+
|
|
241
240
|
if (documentId === undefined) {
|
|
242
241
|
return Effect.die(new Error("The summarizer prompt names no corpus document"));
|
|
243
242
|
}
|
|
243
|
+
|
|
244
244
|
return Effect.succeed(
|
|
245
245
|
promptJson.includes(fetchCallId(documentId))
|
|
246
246
|
? summaryParts(documentId)
|
|
@@ -248,9 +248,11 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
248
248
|
);
|
|
249
249
|
}),
|
|
250
250
|
);
|
|
251
|
+
|
|
251
252
|
const childBinding = Agent.withModel(DocSummarizer, childModel.model);
|
|
252
253
|
|
|
253
254
|
const firstCallId = summarizeCallId(documentIds[0] ?? "durability-notes");
|
|
255
|
+
|
|
254
256
|
const parentModel = yield* makeCountingModel("docs-researcher-p7", (promptJson) =>
|
|
255
257
|
Effect.succeed(
|
|
256
258
|
promptJson.includes(firstCallId)
|
|
@@ -258,6 +260,7 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
258
260
|
: summaryDelegationParts(documentIds),
|
|
259
261
|
),
|
|
260
262
|
);
|
|
263
|
+
|
|
261
264
|
const parentBinding = Agent.withModel(DocsResearcher, parentModel.model);
|
|
262
265
|
|
|
263
266
|
const delegationLayer = docsSummaryHandlersLayer(childBinding).pipe(
|
|
@@ -274,6 +277,7 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
274
277
|
parentBinding,
|
|
275
278
|
docsCoordinatorDigests,
|
|
276
279
|
).pipe(Effect.provide(delegationLayer));
|
|
280
|
+
|
|
277
281
|
const childResolved: ResolvedBinding = yield* DurableWorkerBinding.make(
|
|
278
282
|
childBinding,
|
|
279
283
|
docsSummarizerDigests,
|
|
@@ -289,6 +293,7 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
|
|
|
289
293
|
fetchInvocations: (documentId) =>
|
|
290
294
|
Ref.get(fetchCounts).pipe(Effect.map((current) => current.get(documentId) ?? 0)),
|
|
291
295
|
};
|
|
296
|
+
|
|
292
297
|
return harness;
|
|
293
298
|
});
|
|
294
299
|
|
|
@@ -297,15 +302,17 @@ const encodeResearchDocument = Schema.encodeEffect(ResearchDocument);
|
|
|
297
302
|
/**
|
|
298
303
|
* The audit-surface preview of one fetched document: the raw document —
|
|
299
304
|
* secret marker and all — passes through the configured structural `Redactor`
|
|
300
|
-
* before anything may quote it outside the child
|
|
305
|
+
* before anything may quote it outside the child Thread (SEC-008,
|
|
301
306
|
* CAP-013). Tests assert the preview keeps shape but no scalar content.
|
|
302
307
|
*/
|
|
303
308
|
export const redactedDocumentPreview = Effect.fn("DocsResearcher.redactedDocumentPreview")(
|
|
304
309
|
function* (documentId: string): Effect.fn.Return<RedactedPreview, RedactionError, Redactor> {
|
|
305
310
|
const redactor = yield* Redactor;
|
|
311
|
+
|
|
306
312
|
const encoded = yield* encodeResearchDocument(researchDocumentFor(documentId)).pipe(
|
|
307
313
|
Effect.orDie,
|
|
308
314
|
);
|
|
315
|
+
|
|
309
316
|
return yield* redactor.redact(encoded);
|
|
310
317
|
},
|
|
311
318
|
);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import type { JsonSchema } from "effect";
|
|
2
|
+
import { Effect, JsonPointer, Layer, Schema } from "effect";
|
|
1
3
|
import {
|
|
2
4
|
McpConnectionRequest,
|
|
3
5
|
McpConnector,
|
|
4
6
|
McpServerIdentity,
|
|
5
7
|
McpToolkitMismatch,
|
|
6
8
|
type McpConnection,
|
|
7
|
-
} from "
|
|
8
|
-
import { Effect, Layer, Schema } from "effect";
|
|
9
|
+
} from "effect-agent/mcp";
|
|
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 { Agent, AgentPolicy } from "@effect-agent/core";
|
|
2
1
|
import { Context, Effect, Schema } from "effect";
|
|
2
|
+
import * as Agent from "effect-agent/agent";
|
|
3
|
+
import { AgentPolicy } from "effect-agent/agent-policy";
|
|
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,5 +1,8 @@
|
|
|
1
|
-
import { ConversationId, IdGenerator, RunId, TurnId } from "@effect-agent/core";
|
|
2
1
|
import { Context, Deferred, Effect, Layer, Option, Ref, Schema } from "effect";
|
|
2
|
+
import { IdGenerator } from "effect-agent/id-generator";
|
|
3
|
+
import { ThreadId, RunId, TurnId } from "effect-agent/identifiers";
|
|
4
|
+
import { RunContextPreparationPassthrough } from "effect-agent/run-options";
|
|
5
|
+
import { ThreadHistory } from "effect-agent/thread-history";
|
|
3
6
|
|
|
4
7
|
import {
|
|
5
8
|
ActivityCatalog,
|
|
@@ -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.layer,
|
|
425
468
|
TravelPlannerToolkitLayer,
|
|
426
469
|
FlightCatalogLayer,
|
|
427
470
|
LodgingCatalogLayer,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Agent, AgentPolicy } from "@effect-agent/core";
|
|
2
1
|
import { Context, Effect, Schema } from "effect";
|
|
2
|
+
import * as Agent from "effect-agent/agent";
|
|
3
|
+
import { AgentPolicy } from "effect-agent/agent-policy";
|
|
3
4
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
4
5
|
|
|
5
6
|
import {
|
|
@@ -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.",
|