@effect-agent/testing 0.1.0-beta.42 → 0.1.0-beta.45

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.
@@ -91,6 +91,7 @@ const formatLogValue = (value: unknown): string => {
91
91
  if (typeof value === "string") {
92
92
  return value;
93
93
  }
94
+
94
95
  return JSON.stringify(value) ?? String(value);
95
96
  } catch {
96
97
  try {
@@ -104,17 +105,21 @@ const formatLogValue = (value: unknown): string => {
104
105
  const makeConsole = (capture: LogCapture, limits: CodeExecutionLimits) => {
105
106
  const write = (...values: ReadonlyArray<unknown>): void => {
106
107
  const joined = values.map(formatLogValue).join(" ");
108
+
107
109
  const line =
108
110
  joined.length > MAX_LOG_LINE_CHARACTERS
109
111
  ? `${joined.slice(0, MAX_LOG_LINE_CHARACTERS - 1)}…`
110
112
  : joined;
113
+
111
114
  const bytes = utf8ByteLength(line);
115
+
112
116
  if (capture.lines.length >= MAX_LOG_LINES || capture.bytes + bytes > limits.maxLogBytes) {
113
117
  throw new LogLimitSignal(capture.bytes + bytes);
114
118
  }
115
119
  capture.lines.push(line);
116
120
  capture.bytes += bytes;
117
121
  };
122
+
118
123
  return { debug: write, error: write, info: write, log: write, warn: write };
119
124
  };
120
125
 
@@ -131,18 +136,21 @@ const buildNamespaceObject = (
131
136
  offer: (pending: PendingHostCall) => void,
132
137
  ): Record<string, unknown> => {
133
138
  const methods: Record<string, unknown> = {};
139
+
134
140
  for (const method of namespace.methods) {
135
141
  methods[method] = (argument: unknown) =>
136
142
  new Promise((resolve, reject) => {
137
143
  offer({ namespace: namespace.name, method, argument, resolve, reject });
138
144
  });
139
145
  }
146
+
140
147
  return methods;
141
148
  };
142
149
 
143
150
  const boundedText = (value: unknown): string => {
144
151
  try {
145
152
  const text = value instanceof Error ? `${value.name}: ${value.message}` : formatLogValue(value);
153
+
146
154
  return text.slice(0, MAX_THROWN_CHARACTERS);
147
155
  } catch {
148
156
  return "[unserializable thrown value]";
@@ -160,9 +168,11 @@ const safeDecodeJson = (value: unknown): Option.Option<Schema.Json> => {
160
168
 
161
169
  const boundedThrown = (value: unknown): Schema.Json => {
162
170
  const decoded = safeDecodeJson(value);
171
+
163
172
  if (Option.isSome(decoded)) {
164
173
  try {
165
174
  const encoded = JSON.stringify(decoded.value);
175
+
166
176
  if (encoded !== undefined && encoded.length <= MAX_THROWN_CHARACTERS) {
167
177
  return decoded.value;
168
178
  }
@@ -170,12 +180,14 @@ const boundedThrown = (value: unknown): Schema.Json => {
170
180
  // fall through to the bounded string form
171
181
  }
172
182
  }
183
+
173
184
  return boundedText(value);
174
185
  };
175
186
 
176
187
  const encodedJsonByteLength = (value: Schema.Json): number | undefined => {
177
188
  try {
178
189
  const encoded = JSON.stringify(value);
190
+
179
191
  return encoded === undefined ? undefined : utf8ByteLength(encoded);
180
192
  } catch {
181
193
  return undefined;
@@ -213,6 +225,7 @@ const validateRequest = (
213
225
  }
214
226
  const reservedNames = new Set<string>([...shadowedGlobals, "console"]);
215
227
  const seen = new Set<string>();
228
+
216
229
  for (const namespace of request.namespaces) {
217
230
  if (reservedNames.has(namespace.name) || seen.has(namespace.name)) {
218
231
  return yield* CodeExecutorUnsupportedError.make({
@@ -224,6 +237,7 @@ const validateRequest = (
224
237
  seen.add(namespace.name);
225
238
  }
226
239
  const sourceBytes = utf8ByteLength(request.source);
240
+
227
241
  if (sourceBytes > request.limits.maxSourceBytes) {
228
242
  return yield* CodeSourceError.make({
229
243
  implementation: inProcessCodeExecutorImplementation,
@@ -246,6 +260,7 @@ const serveHostCalls = (
246
260
  Effect.gen(function* () {
247
261
  while (true) {
248
262
  const pending = yield* Queue.take(queue);
263
+
249
264
  counter.calls += 1;
250
265
  if (counter.calls > limits.maxHostCalls) {
251
266
  return yield* CodeHostCallLimitError.make({
@@ -255,11 +270,13 @@ const serveHostCalls = (
255
270
  });
256
271
  }
257
272
  const argument = safeDecodeJson(pending.argument);
273
+
258
274
  if (Option.isNone(argument)) {
259
275
  pending.reject(new TypeError("host call arguments must be JSON values"));
260
276
  continue;
261
277
  }
262
278
  const argumentBytes = encodedJsonByteLength(argument.value);
279
+
263
280
  if (argumentBytes === undefined || argumentBytes > limits.maxHostCallArgumentBytes) {
264
281
  return yield* CodeOutputLimitError.make({
265
282
  implementation: inProcessCodeExecutorImplementation,
@@ -269,6 +286,7 @@ const serveHostCalls = (
269
286
  logs: [...capture.lines],
270
287
  });
271
288
  }
289
+
272
290
  const rawOutcome = yield* host.call(
273
291
  CodeHostCall.make({
274
292
  namespace: pending.namespace,
@@ -276,7 +294,9 @@ const serveHostCalls = (
276
294
  argument: argument.value,
277
295
  }),
278
296
  );
297
+
279
298
  const outcome = decodeHostOutcome(rawOutcome);
299
+
280
300
  if (Option.isNone(outcome)) {
281
301
  return yield* CodeExecutionProtocolError.make({
282
302
  implementation: inProcessCodeExecutorImplementation,
@@ -288,6 +308,7 @@ const serveHostCalls = (
288
308
  continue;
289
309
  }
290
310
  const resultBytes = encodedJsonByteLength(outcome.value.value);
311
+
291
312
  if (resultBytes === undefined || resultBytes > limits.maxHostCallResultBytes) {
292
313
  return yield* CodeOutputLimitError.make({
293
314
  implementation: inProcessCodeExecutorImplementation,
@@ -307,6 +328,7 @@ const classifyProgramFailure = (
307
328
  capture: LogCapture,
308
329
  ): CodeOutputLimitError | CodeSourceError | CodeProgramFailedError => {
309
330
  const inner = thrown instanceof EvaluationThrew ? thrown.inner : thrown;
331
+
310
332
  if (inner instanceof LogLimitSignal) {
311
333
  return CodeOutputLimitError.make({
312
334
  implementation: inProcessCodeExecutorImplementation,
@@ -327,6 +349,7 @@ const classifyProgramFailure = (
327
349
  // split is by value shape: exception-like values read as `threw`, plain
328
350
  // rejection values (an uncaught host failure envelope) read as `rejected`.
329
351
  const reason = thrown instanceof EvaluationThrew || inner instanceof Error ? "threw" : "rejected";
352
+
330
353
  return CodeProgramFailedError.make({
331
354
  implementation: inProcessCodeExecutorImplementation,
332
355
  reason,
@@ -370,11 +393,13 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
370
393
  // rejected synchronously, so the queue stays bounded against hostile
371
394
  // programs.
372
395
  let issuedHostCalls = 0;
396
+
373
397
  const namespaceObjects = request.namespaces.map((namespace) =>
374
398
  buildNamespaceObject(namespace, (pending) => {
375
399
  issuedHostCalls += 1;
376
400
  if (issuedHostCalls > request.limits.maxHostCalls + 1) {
377
401
  pending.reject(new Error(`host-call limit of ${request.limits.maxHostCalls} exceeded`));
402
+
378
403
  return;
379
404
  }
380
405
  Queue.offerUnsafe(queue, pending);
@@ -388,6 +413,7 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
388
413
  const program = Effect.tryPromise({
389
414
  try: async () => {
390
415
  let candidate: unknown;
416
+
391
417
  try {
392
418
  candidate = factory(
393
419
  ...shadowedGlobals.map(() => undefined),
@@ -401,17 +427,20 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
401
427
  throw new EvaluationThrew(new NotAFunction(typeof candidate));
402
428
  }
403
429
  let outcome: unknown;
430
+
404
431
  try {
405
432
  outcome = candidate();
406
433
  } catch (cause) {
407
434
  throw new EvaluationThrew(cause);
408
435
  }
436
+
409
437
  return await Promise.resolve(outcome);
410
438
  },
411
439
  catch: (thrown) => classifyProgramFailure(thrown, request.limits, capture),
412
440
  });
413
441
 
414
442
  const startedAt = yield* Clock.currentTimeMillis;
443
+
415
444
  // The wall-clock deadline interrupts only at asynchronous suspension
416
445
  // points: a synchronous runaway shares the host thread and cannot be
417
446
  // stopped in-process — exactly why the platform CPU enforcement cases
@@ -431,6 +460,7 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
431
460
  }),
432
461
  Effect.ensuring(Fiber.interrupt(server)),
433
462
  );
463
+
434
464
  const finishedAt = yield* Clock.currentTimeMillis;
435
465
 
436
466
  // An unawaited burst can outrun the server: the program may return before
@@ -456,7 +486,9 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
456
486
  }),
457
487
  ),
458
488
  );
489
+
459
490
  const resultBytes = encodedJsonByteLength(value);
491
+
460
492
  if (resultBytes === undefined || resultBytes > request.limits.maxResultBytes) {
461
493
  return yield* CodeOutputLimitError.make({
462
494
  implementation: inProcessCodeExecutorImplementation,
@@ -16,10 +16,12 @@ import { Tool, Toolkit } from "effect/unstable/ai";
16
16
  export const ResearchDocumentId = Schema.NonEmptyString.check(Schema.isMaxLength(64)).pipe(
17
17
  Schema.brand("@effect-agent/testing/docs-researcher/ResearchDocumentId"),
18
18
  );
19
+
19
20
  export type ResearchDocumentId = typeof ResearchDocumentId.Type;
20
21
 
21
22
  const BoundedTitle = Schema.NonEmptyString.check(Schema.isMaxLength(120));
22
23
  const BoundedBody = Schema.NonEmptyString.check(Schema.isMaxLength(16 * 1024));
24
+
23
25
  /** Bounded summary text: the ONLY child-derived text that may cross to the parent. */
24
26
  export const BoundedSummary = Schema.NonEmptyString.check(Schema.isMaxLength(240));
25
27
 
@@ -66,6 +68,7 @@ export const FetchDocument = Tool.make("fetch_document", {
66
68
  });
67
69
 
68
70
  export const DocContentToolkit = Toolkit.make(FetchDocument);
71
+
69
72
  export const docContentToolkitLayer = DocContentToolkit.toLayer({
70
73
  fetch_document: (query) => Effect.flatMap(DocumentLibrary, (library) => library.fetch(query)),
71
74
  });
@@ -125,9 +128,11 @@ export const researchCorpusDocumentIds: ReadonlyArray<ResearchDocumentId> = [
125
128
 
126
129
  const requireCorpusEntry = (documentId: string): CorpusEntry => {
127
130
  const entry = corpusEntries.get(documentId);
131
+
128
132
  if (entry === undefined) {
129
133
  throw new Error(`No deterministic corpus entry exists for document ${documentId}`);
130
134
  }
135
+
131
136
  return entry;
132
137
  };
133
138
 
@@ -136,6 +141,7 @@ export const researchDocumentLookup = (
136
141
  query: DocumentQuery,
137
142
  ): Effect.Effect<ResearchDocument, DocumentUnavailable> => {
138
143
  const entry = corpusEntries.get(query.documentId);
144
+
139
145
  return entry === undefined
140
146
  ? Effect.fail(
141
147
  DocumentUnavailable.make({
@@ -333,6 +339,7 @@ export const expectedResearchDigest = (
333
339
  ResearchDigest.make({
334
340
  findings: documentIds.map((documentId) => {
335
341
  const summary = documentSummaryFor(documentId);
342
+
336
343
  return SummaryFinding.make({
337
344
  documentId: summary.documentId,
338
345
  summary: summary.summary,
@@ -53,9 +53,11 @@ import {
53
53
  export const docsResearcherDeploymentId = Schema.decodeSync(DeploymentId)(
54
54
  "docs-researcher-p7-deployment",
55
55
  );
56
+
56
57
  export const docsResearcherProducerId = Schema.decodeSync(ProducerId)(
57
58
  "docs-researcher-p7-producer",
58
59
  );
60
+
59
61
  export const docsResearcherPrincipal = Schema.decodeSync(Principal)("docs-researcher-p7-principal");
60
62
 
61
63
  const digestOf = (pair: string) => Schema.decodeSync(Digest)(pair.repeat(32));
@@ -154,6 +156,7 @@ const makeCountingModel = (
154
156
  Effect.gen(function* () {
155
157
  const calls = yield* Ref.make(0);
156
158
  const prompts = yield* Ref.make<ReadonlyArray<string>>([]);
159
+
157
160
  const model = Model.make(
158
161
  "scripted",
159
162
  name,
@@ -166,13 +169,16 @@ const makeCountingModel = (
166
169
  Effect.gen(function* () {
167
170
  yield* Ref.update(calls, (value) => value + 1);
168
171
  const promptJson = JSON.stringify(request.prompt);
172
+
169
173
  yield* Ref.update(prompts, (previous) => [...previous, promptJson]);
174
+
170
175
  return Stream.fromIterable(yield* decide(promptJson));
171
176
  }),
172
177
  ),
173
178
  }),
174
179
  ),
175
180
  );
181
+
176
182
  return { model, calls: Ref.get(calls), prompts: Ref.get(prompts) };
177
183
  });
178
184
 
@@ -184,7 +190,7 @@ export interface DocsResearcherHarnessOptions {
184
190
 
185
191
  /** One durable coordinator/summarizer pair with observable counters and MCP evidence. */
186
192
  export interface DocsResearcherHarness {
187
- /** Host registrations for `NodeDurableRuntimeOptions.bindings` (parent + child). */
193
+ /** Resolved registrations for the parent and child workers. */
188
194
  readonly bindings: ReadonlyArray<ResolvedBinding>;
189
195
  /** The validated MCP discovery the child toolkit registration was gated on. */
190
196
  readonly discovery: McpDiscovery;
@@ -217,12 +223,15 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
217
223
  const discovery = yield* Effect.scoped(
218
224
  Effect.gen(function* () {
219
225
  const connection = yield* connectMcp(docsMcpRequest);
226
+
220
227
  yield* assertDiscoveryMatchesAuthoredToolkit(connection);
228
+
221
229
  return connection.discovery;
222
230
  }),
223
231
  ).pipe(Effect.provide(docsMcpConnectorLayer));
224
232
 
225
233
  const fetchCounts = yield* Ref.make<ReadonlyMap<string, number>>(new Map());
234
+
226
235
  const libraryLayer = Layer.succeed(
227
236
  DocumentLibrary,
228
237
  DocumentLibrary.of({
@@ -232,14 +241,17 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
232
241
  ).pipe(Effect.andThen(researchDocumentLookup(query))),
233
242
  }),
234
243
  );
244
+
235
245
  const childToolkitLayer = docContentToolkitLayer.pipe(Layer.provideMerge(libraryLayer));
236
246
 
237
247
  const childModel = yield* makeCountingModel("doc-summarizer-p7", (promptJson) =>
238
248
  Effect.suspend(() => {
239
249
  const documentId = documentIds.find((candidate) => promptJson.includes(candidate));
250
+
240
251
  if (documentId === undefined) {
241
252
  return Effect.die(new Error("The summarizer prompt names no corpus document"));
242
253
  }
254
+
243
255
  return Effect.succeed(
244
256
  promptJson.includes(fetchCallId(documentId))
245
257
  ? summaryParts(documentId)
@@ -247,9 +259,11 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
247
259
  );
248
260
  }),
249
261
  );
262
+
250
263
  const childBinding = Agent.withModel(DocSummarizer, childModel.model);
251
264
 
252
265
  const firstCallId = summarizeCallId(documentIds[0] ?? "durability-notes");
266
+
253
267
  const parentModel = yield* makeCountingModel("docs-researcher-p7", (promptJson) =>
254
268
  Effect.succeed(
255
269
  promptJson.includes(firstCallId)
@@ -257,6 +271,7 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
257
271
  : summaryDelegationParts(documentIds),
258
272
  ),
259
273
  );
274
+
260
275
  const parentBinding = Agent.withModel(DocsResearcher, parentModel.model);
261
276
 
262
277
  const delegationLayer = docsSummaryHandlersLayer(childBinding).pipe(
@@ -273,6 +288,7 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
273
288
  parentBinding,
274
289
  docsCoordinatorDigests,
275
290
  ).pipe(Effect.provide(delegationLayer));
291
+
276
292
  const childResolved: ResolvedBinding = yield* DurableWorkerBinding.make(
277
293
  childBinding,
278
294
  docsSummarizerDigests,
@@ -288,6 +304,7 @@ export const makeDocsResearcherHarness = (options?: DocsResearcherHarnessOptions
288
304
  fetchInvocations: (documentId) =>
289
305
  Ref.get(fetchCounts).pipe(Effect.map((current) => current.get(documentId) ?? 0)),
290
306
  };
307
+
291
308
  return harness;
292
309
  });
293
310
 
@@ -302,9 +319,11 @@ const encodeResearchDocument = Schema.encodeEffect(ResearchDocument);
302
319
  export const redactedDocumentPreview = Effect.fn("DocsResearcher.redactedDocumentPreview")(
303
320
  function* (documentId: string): Effect.fn.Return<RedactedPreview, RedactionError, Redactor> {
304
321
  const redactor = yield* Redactor;
322
+
305
323
  const encoded = yield* encodeResearchDocument(researchDocumentFor(documentId)).pipe(
306
324
  Effect.orDie,
307
325
  );
326
+
308
327
  return yield* redactor.redact(encoded);
309
328
  },
310
329
  );
@@ -5,7 +5,8 @@ import {
5
5
  McpToolkitMismatch,
6
6
  type McpConnection,
7
7
  } from "@effect-agent/capabilities";
8
- import { Effect, JsonSchema, Layer, Schema } from "effect";
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
 
@@ -48,17 +49,25 @@ const JsonSchemaDefinitions = Schema.Record(
48
49
  Schema.String,
49
50
  Schema.Record(Schema.String, Schema.Unknown),
50
51
  );
52
+
51
53
  const decodeJsonSchemaDefinitions = Schema.decodeUnknownSync(JsonSchemaDefinitions);
52
54
  const decodeToolJsonSchema = Schema.decodeUnknownSync(McpSchema.ToolJsonSchema);
53
55
 
54
56
  const flattenTopLevelRef = (schema: JsonSchema.JsonSchema): McpSchema.ToolJsonSchema => {
55
57
  const ref = schema["$ref"];
58
+
56
59
  if (typeof ref !== "string") {
57
60
  return decodeToolJsonSchema(schema);
58
61
  }
59
62
 
60
63
  const defs = decodeJsonSchemaDefinitions(schema["$defs"]);
61
- const resolved = JsonSchema.resolve$ref(ref, 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
+
62
71
  return decodeToolJsonSchema(resolved ?? schema);
63
72
  };
64
73
 
@@ -132,9 +141,11 @@ export const assertDiscoveryMatchesAuthoredToolkit = Effect.fn(
132
141
  const authored = Object.values(DocContentToolkit.tools)
133
142
  .map((tool) => ({ name: tool.name, inputSchema: Tool.getJsonSchema(tool) }))
134
143
  .sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
144
+
135
145
  const discovered = Object.values(connection.toolkit.tools)
136
146
  .map((tool) => ({ name: tool.name, inputSchema: Tool.getJsonSchema(tool) }))
137
147
  .sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
148
+
138
149
  const matches =
139
150
  authored.length === discovered.length &&
140
151
  authored.every(
@@ -142,6 +153,7 @@ export const assertDiscoveryMatchesAuthoredToolkit = Effect.fn(
142
153
  tool.name === discovered[index]?.name &&
143
154
  isJsonEqual(tool.inputSchema, discovered[index]?.inputSchema),
144
155
  );
156
+
145
157
  if (!matches) {
146
158
  return yield* McpToolkitMismatch.make({
147
159
  serverId: connection.discovery.identity.serverId,
@@ -5,11 +5,13 @@ import { Tool, Toolkit } from "effect/unstable/ai";
5
5
  export const AirportCode = Schema.NonEmptyString.pipe(
6
6
  Schema.brand("@effect-agent/testing/travel-planner/AirportCode"),
7
7
  );
8
+
8
9
  export type AirportCode = typeof AirportCode.Type;
9
10
 
10
11
  export const QuoteId = Schema.NonEmptyString.pipe(
11
12
  Schema.brand("@effect-agent/testing/travel-planner/QuoteId"),
12
13
  );
14
+
13
15
  export type QuoteId = typeof QuoteId.Type;
14
16
 
15
17
  export class TripRequest extends Schema.Class<TripRequest>("TripRequest")({
@@ -84,18 +86,22 @@ export class TravelPlan extends Schema.Class<TravelPlan>("TravelPlan")({
84
86
  }) {}
85
87
 
86
88
  const unavailableFields = { query: Schema.String, message: Schema.String };
89
+
87
90
  export class FlightUnavailable extends Schema.TaggedError<FlightUnavailable>()(
88
91
  "FlightUnavailable",
89
92
  unavailableFields,
90
93
  ) {}
94
+
91
95
  export class LodgingUnavailable extends Schema.TaggedError<LodgingUnavailable>()(
92
96
  "LodgingUnavailable",
93
97
  unavailableFields,
94
98
  ) {}
99
+
95
100
  export class ActivityUnavailable extends Schema.TaggedError<ActivityUnavailable>()(
96
101
  "ActivityUnavailable",
97
102
  unavailableFields,
98
103
  ) {}
104
+
99
105
  export class GuidanceFailure extends Schema.TaggedError<GuidanceFailure>()("GuidanceFailure", {
100
106
  message: Schema.String,
101
107
  }) {}
@@ -104,10 +110,12 @@ export class FlightCatalog extends Context.Service<
104
110
  FlightCatalog,
105
111
  { readonly search: (query: FlightQuery) => Effect.Effect<FlightOption, FlightUnavailable> }
106
112
  >()("@effect-agent/testing/travel-planner/FlightCatalog") {}
113
+
107
114
  export class LodgingCatalog extends Context.Service<
108
115
  LodgingCatalog,
109
116
  { readonly search: (query: LodgingQuery) => Effect.Effect<LodgingOption, LodgingUnavailable> }
110
117
  >()("@effect-agent/testing/travel-planner/LodgingCatalog") {}
118
+
111
119
  export class ActivityCatalog extends Context.Service<
112
120
  ActivityCatalog,
113
121
  {
@@ -116,6 +124,7 @@ export class ActivityCatalog extends Context.Service<
116
124
  ) => Effect.Effect<ActivitySearchResult, ActivityUnavailable>;
117
125
  }
118
126
  >()("@effect-agent/testing/travel-planner/ActivityCatalog") {}
127
+
119
128
  export class TravelGuidance extends Context.Service<
120
129
  TravelGuidance,
121
130
  { readonly instructions: (input: TripRequest) => Effect.Effect<string, GuidanceFailure> }
@@ -128,6 +137,7 @@ export const SearchFlights = Tool.make("search_flights", {
128
137
  failureMode: "error",
129
138
  dependencies: [FlightCatalog],
130
139
  });
140
+
131
141
  export const SearchLodging = Tool.make("search_lodging", {
132
142
  parameters: LodgingQuery,
133
143
  success: LodgingOption,
@@ -135,6 +145,7 @@ export const SearchLodging = Tool.make("search_lodging", {
135
145
  failureMode: "error",
136
146
  dependencies: [LodgingCatalog],
137
147
  });
148
+
138
149
  export const SearchActivities = Tool.make("search_activities", {
139
150
  parameters: ActivityQuery,
140
151
  success: ActivitySearchResult,
@@ -144,6 +155,7 @@ export const SearchActivities = Tool.make("search_activities", {
144
155
  });
145
156
 
146
157
  export const TravelPlannerToolkit = Toolkit.make(SearchFlights, SearchLodging, SearchActivities);
158
+
147
159
  export const TravelPlannerToolkitLayer = TravelPlannerToolkit.toLayer({
148
160
  search_flights: (query) => Effect.flatMap(FlightCatalog, (catalog) => catalog.search(query)),
149
161
  search_lodging: (query) => Effect.flatMap(LodgingCatalog, (catalog) => catalog.search(query)),