@effect-agent/testing 0.1.0-beta.42 → 0.1.0-beta.44
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.mjs.map +1 -1
- package/dist/chaos.mjs.map +1 -1
- package/dist/code-executor.mjs.map +1 -1
- package/dist/deterministic-layers-CKyYxBhN.mjs.map +1 -1
- package/dist/docs-researcher.mjs +3 -2
- package/dist/docs-researcher.mjs.map +1 -1
- package/dist/scripted-model-C2y0ztuj.mjs.map +1 -1
- package/dist/travel-planner.mjs.map +1 -1
- package/package.json +1 -71
- package/src/certification.ts +78 -0
- package/src/chaos.ts +96 -0
- package/src/code-executor-conformance.ts +19 -0
- package/src/code-executor-substitute.ts +32 -0
- package/src/fixtures/docs-researcher/definition.ts +7 -0
- package/src/fixtures/docs-researcher/harness.ts +19 -0
- package/src/fixtures/docs-researcher/mcp.ts +14 -2
- package/src/fixtures/travel-planner/definition.ts +12 -0
- package/src/fixtures/travel-planner/deterministic-layers.ts +38 -0
- package/src/fixtures/travel-planner/phase3.ts +4 -0
- package/src/fixtures/travel-planner/phase4.ts +10 -0
- package/src/fixtures/travel-planner/phase5.ts +20 -0
- package/src/fixtures/travel-planner/phase6.ts +12 -0
- package/src/fixtures/travel-planner/scenarios.ts +2 -0
- package/src/fixtures/travel-planner/subagents-durable.ts +13 -0
- package/src/fixtures/travel-planner/subagents.ts +21 -0
- package/src/scripted-model.ts +18 -0
|
@@ -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
|
|
|
@@ -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 {
|
|
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
|
-
|
|
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)),
|