@effect-agent/testing 0.0.1-beta.0
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/index.d.mts +3331 -0
- package/dist/index.mjs +4205 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
- package/src/certification.ts +979 -0
- package/src/chaos.ts +1187 -0
- package/src/fixtures/docs-researcher/definition.ts +342 -0
- package/src/fixtures/docs-researcher/harness.ts +316 -0
- package/src/fixtures/docs-researcher/index.ts +34 -0
- package/src/fixtures/docs-researcher/mcp.ts +126 -0
- package/src/fixtures/travel-planner/definition.ts +167 -0
- package/src/fixtures/travel-planner/deterministic-layers.ts +431 -0
- package/src/fixtures/travel-planner/index.ts +11 -0
- package/src/fixtures/travel-planner/phase2.ts +94 -0
- package/src/fixtures/travel-planner/phase3.ts +159 -0
- package/src/fixtures/travel-planner/phase4.ts +272 -0
- package/src/fixtures/travel-planner/phase5.ts +476 -0
- package/src/fixtures/travel-planner/phase6.ts +923 -0
- package/src/fixtures/travel-planner/phase7.ts +112 -0
- package/src/fixtures/travel-planner/scenarios.ts +85 -0
- package/src/fixtures/travel-planner/subagents-durable.ts +391 -0
- package/src/fixtures/travel-planner/subagents.ts +525 -0
- package/src/index.ts +5 -0
- package/src/scripted-model.ts +303 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
McpConnectionRequest,
|
|
3
|
+
McpConnector,
|
|
4
|
+
McpServerIdentity,
|
|
5
|
+
McpToolkitMismatch,
|
|
6
|
+
type McpConnection,
|
|
7
|
+
} from "@effect-agent/capabilities";
|
|
8
|
+
import { Effect, Layer, Schema } from "effect";
|
|
9
|
+
import { Tool } from "effect/unstable/ai";
|
|
10
|
+
import * as McpSchema from "effect/unstable/ai/McpSchema";
|
|
11
|
+
|
|
12
|
+
import { DocContentToolkit, FetchDocument } from "./definition.ts";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Scripted MCP fixture: a deterministic `McpConnector` adapter that serves the
|
|
16
|
+
// doc-summarizer's content tool. Discovery entries are DERIVED from the
|
|
17
|
+
// authored Tool (`Tool.getJsonSchema`), so `validateMcpDiscovery` digesting
|
|
18
|
+
// both sides is a real check, not a tautology; the mismatch and over-limit
|
|
19
|
+
// connectors below serve deliberately wrong contracts so tests can pin the
|
|
20
|
+
// fail-closed paths (CAP-009, SEC-013).
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Framework-side hard bounds one docs-researcher assembly requests. */
|
|
24
|
+
export const docsMcpRequest = McpConnectionRequest.make({
|
|
25
|
+
serverId: "docs-content-mcp",
|
|
26
|
+
maxToolCount: 4,
|
|
27
|
+
maxToolDescriptionBytes: 256,
|
|
28
|
+
maxDiscoveryBytes: 16_384,
|
|
29
|
+
connectTimeoutMillis: 1_000,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const docsMcpIdentity = McpServerIdentity.make({
|
|
33
|
+
serverId: docsMcpRequest.serverId,
|
|
34
|
+
implementation: McpSchema.Implementation.make({
|
|
35
|
+
name: "docs-researcher-content-fixture",
|
|
36
|
+
version: "1.0.0",
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const discoveredFetchDocument = McpSchema.Tool.make({
|
|
41
|
+
name: FetchDocument.name,
|
|
42
|
+
description: "Fetch one bounded research document by its identifier.",
|
|
43
|
+
inputSchema: Tool.getJsonSchema(FetchDocument),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const scriptedConnector = (tools: ReadonlyArray<McpSchema.Tool>): Layer.Layer<McpConnector> =>
|
|
47
|
+
Layer.succeed(McpConnector)({
|
|
48
|
+
connect: () =>
|
|
49
|
+
Effect.acquireRelease(
|
|
50
|
+
Effect.succeed({
|
|
51
|
+
identity: docsMcpIdentity,
|
|
52
|
+
capabilities: McpSchema.ServerCapabilities.make({}),
|
|
53
|
+
tools,
|
|
54
|
+
toolkit: DocContentToolkit,
|
|
55
|
+
}),
|
|
56
|
+
() => Effect.void,
|
|
57
|
+
),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** The well-behaved scripted content server. */
|
|
61
|
+
export const docsMcpConnectorLayer: Layer.Layer<McpConnector> = scriptedConnector([
|
|
62
|
+
discoveredFetchDocument,
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/** Serves a tool description exceeding `maxToolDescriptionBytes` (SEC-013 bound). */
|
|
66
|
+
export const docsMcpOversizedConnectorLayer: Layer.Layer<McpConnector> = scriptedConnector([
|
|
67
|
+
McpSchema.Tool.make({
|
|
68
|
+
name: discoveredFetchDocument.name,
|
|
69
|
+
description: "x".repeat(1_024),
|
|
70
|
+
inputSchema: discoveredFetchDocument.inputSchema,
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
/** Serves a discovery schema that disagrees with the authored toolkit (drift fails closed). */
|
|
75
|
+
export const docsMcpMismatchedConnectorLayer: Layer.Layer<McpConnector> = scriptedConnector([
|
|
76
|
+
McpSchema.Tool.make({
|
|
77
|
+
name: discoveredFetchDocument.name,
|
|
78
|
+
description: discoveredFetchDocument.description,
|
|
79
|
+
inputSchema: { type: "object", properties: { url: { type: "string" } } },
|
|
80
|
+
}),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const isJsonEqual = (left: unknown, right: unknown): boolean =>
|
|
84
|
+
JSON.stringify(left) === JSON.stringify(right);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Bind DISCOVERY to AUTHORING: `validateMcpDiscovery` (inside `connectMcp`)
|
|
88
|
+
* already proved the served discovery matches the connection's own Toolkit;
|
|
89
|
+
* this check additionally proves that Toolkit is the exact toolkit the
|
|
90
|
+
* doc-summarizer was AUTHORED against — same tool names, same derived JSON
|
|
91
|
+
* schemas — so a connector cannot substitute a look-alike toolkit. The
|
|
92
|
+
* docs-researcher harness runs it before any worker Binding registration and
|
|
93
|
+
* fails closed on any drift.
|
|
94
|
+
*/
|
|
95
|
+
export const assertDiscoveryMatchesAuthoredToolkit = Effect.fn(
|
|
96
|
+
"DocsResearcher.assertDiscoveryMatchesAuthoredToolkit",
|
|
97
|
+
)(function* (connection: McpConnection): Effect.fn.Return<void, McpToolkitMismatch> {
|
|
98
|
+
const authored = Object.values(DocContentToolkit.tools)
|
|
99
|
+
.map((tool) => ({ name: tool.name, inputSchema: Tool.getJsonSchema(tool) }))
|
|
100
|
+
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
101
|
+
const discovered = Object.values(connection.toolkit.tools)
|
|
102
|
+
.map((tool) => ({ name: tool.name, inputSchema: Tool.getJsonSchema(tool) }))
|
|
103
|
+
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
104
|
+
const matches =
|
|
105
|
+
authored.length === discovered.length &&
|
|
106
|
+
authored.every(
|
|
107
|
+
(tool, index) =>
|
|
108
|
+
tool.name === discovered[index]?.name &&
|
|
109
|
+
isJsonEqual(tool.inputSchema, discovered[index]?.inputSchema),
|
|
110
|
+
);
|
|
111
|
+
if (!matches) {
|
|
112
|
+
return yield* McpToolkitMismatch.make({
|
|
113
|
+
serverId: connection.discovery.identity.serverId,
|
|
114
|
+
message:
|
|
115
|
+
"The MCP-discovered toolkit does not match the doc-summarizer's authored content toolkit",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/** Round-trip guard for encoded discovery values persisted as fixture evidence. */
|
|
121
|
+
export const DocsMcpDiscoveryEvidence = Schema.Struct({
|
|
122
|
+
serverId: Schema.NonEmptyString,
|
|
123
|
+
toolCount: Schema.Natural,
|
|
124
|
+
encodedBytes: Schema.Natural,
|
|
125
|
+
toolkitSchemaDigest: Schema.String,
|
|
126
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Agent, AgentPolicy } from "@effect-agent/core";
|
|
2
|
+
import { Context, Effect, Schema } from "effect";
|
|
3
|
+
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
4
|
+
|
|
5
|
+
export const AirportCode = Schema.NonEmptyString.pipe(
|
|
6
|
+
Schema.brand("@effect-agent/testing/travel-planner/AirportCode"),
|
|
7
|
+
);
|
|
8
|
+
export type AirportCode = typeof AirportCode.Type;
|
|
9
|
+
|
|
10
|
+
export const QuoteId = Schema.NonEmptyString.pipe(
|
|
11
|
+
Schema.brand("@effect-agent/testing/travel-planner/QuoteId"),
|
|
12
|
+
);
|
|
13
|
+
export type QuoteId = typeof QuoteId.Type;
|
|
14
|
+
|
|
15
|
+
export class TripRequest extends Schema.Class<TripRequest>("TripRequest")({
|
|
16
|
+
request: Schema.NonEmptyString,
|
|
17
|
+
origin: AirportCode,
|
|
18
|
+
destination: AirportCode,
|
|
19
|
+
departOn: Schema.String,
|
|
20
|
+
nights: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
21
|
+
travelers: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
22
|
+
budgetCents: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
23
|
+
currency: Schema.Literal("USD"),
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
export class FlightQuery extends Schema.Class<FlightQuery>("FlightQuery")({
|
|
27
|
+
origin: AirportCode,
|
|
28
|
+
destination: AirportCode,
|
|
29
|
+
departOn: Schema.String,
|
|
30
|
+
travelers: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
31
|
+
}) {}
|
|
32
|
+
|
|
33
|
+
export class LodgingQuery extends Schema.Class<LodgingQuery>("LodgingQuery")({
|
|
34
|
+
destination: AirportCode,
|
|
35
|
+
departOn: Schema.String,
|
|
36
|
+
nights: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
37
|
+
travelers: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
38
|
+
}) {}
|
|
39
|
+
|
|
40
|
+
export class ActivityQuery extends Schema.Class<ActivityQuery>("ActivityQuery")({
|
|
41
|
+
destination: AirportCode,
|
|
42
|
+
departOn: Schema.String,
|
|
43
|
+
nights: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
44
|
+
travelers: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
45
|
+
}) {}
|
|
46
|
+
|
|
47
|
+
export class FlightOption extends Schema.Class<FlightOption>("FlightOption")({
|
|
48
|
+
quoteId: QuoteId,
|
|
49
|
+
flight: Schema.String,
|
|
50
|
+
estimatedCents: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
51
|
+
currency: Schema.Literal("USD"),
|
|
52
|
+
}) {}
|
|
53
|
+
|
|
54
|
+
export class LodgingOption extends Schema.Class<LodgingOption>("LodgingOption")({
|
|
55
|
+
lodging: Schema.String,
|
|
56
|
+
estimatedCents: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
57
|
+
currency: Schema.Literal("USD"),
|
|
58
|
+
}) {}
|
|
59
|
+
|
|
60
|
+
/** A successful empty activity search is distinct from supplier unavailability. */
|
|
61
|
+
export class ActivitySearchResult extends Schema.Class<ActivitySearchResult>(
|
|
62
|
+
"ActivitySearchResult",
|
|
63
|
+
)({
|
|
64
|
+
activities: Schema.Array(Schema.String),
|
|
65
|
+
}) {}
|
|
66
|
+
|
|
67
|
+
export class Itinerary extends Schema.Class<Itinerary>("Itinerary")({
|
|
68
|
+
title: Schema.String,
|
|
69
|
+
route: Schema.String,
|
|
70
|
+
dates: Schema.String,
|
|
71
|
+
flight: Schema.String,
|
|
72
|
+
lodging: Schema.String,
|
|
73
|
+
activities: Schema.Array(Schema.String),
|
|
74
|
+
estimatedTotalCents: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
75
|
+
currency: Schema.Literal("USD"),
|
|
76
|
+
quoteId: QuoteId,
|
|
77
|
+
assumptions: Schema.Array(Schema.String),
|
|
78
|
+
unresolvedConstraints: Schema.Array(Schema.String),
|
|
79
|
+
nextAction: Schema.Literal("review"),
|
|
80
|
+
}) {}
|
|
81
|
+
|
|
82
|
+
export class TravelPlan extends Schema.Class<TravelPlan>("TravelPlan")({
|
|
83
|
+
itineraries: Schema.Array(Itinerary),
|
|
84
|
+
}) {}
|
|
85
|
+
|
|
86
|
+
const unavailableFields = { query: Schema.String, message: Schema.String };
|
|
87
|
+
export class FlightUnavailable extends Schema.TaggedErrorClass<FlightUnavailable>()(
|
|
88
|
+
"FlightUnavailable",
|
|
89
|
+
unavailableFields,
|
|
90
|
+
) {}
|
|
91
|
+
export class LodgingUnavailable extends Schema.TaggedErrorClass<LodgingUnavailable>()(
|
|
92
|
+
"LodgingUnavailable",
|
|
93
|
+
unavailableFields,
|
|
94
|
+
) {}
|
|
95
|
+
export class ActivityUnavailable extends Schema.TaggedErrorClass<ActivityUnavailable>()(
|
|
96
|
+
"ActivityUnavailable",
|
|
97
|
+
unavailableFields,
|
|
98
|
+
) {}
|
|
99
|
+
export class GuidanceFailure extends Schema.TaggedErrorClass<GuidanceFailure>()("GuidanceFailure", {
|
|
100
|
+
message: Schema.String,
|
|
101
|
+
}) {}
|
|
102
|
+
|
|
103
|
+
export class FlightCatalog extends Context.Service<
|
|
104
|
+
FlightCatalog,
|
|
105
|
+
{ readonly search: (query: FlightQuery) => Effect.Effect<FlightOption, FlightUnavailable> }
|
|
106
|
+
>()("@effect-agent/testing/travel-planner/FlightCatalog") {}
|
|
107
|
+
export class LodgingCatalog extends Context.Service<
|
|
108
|
+
LodgingCatalog,
|
|
109
|
+
{ readonly search: (query: LodgingQuery) => Effect.Effect<LodgingOption, LodgingUnavailable> }
|
|
110
|
+
>()("@effect-agent/testing/travel-planner/LodgingCatalog") {}
|
|
111
|
+
export class ActivityCatalog extends Context.Service<
|
|
112
|
+
ActivityCatalog,
|
|
113
|
+
{
|
|
114
|
+
readonly search: (
|
|
115
|
+
query: ActivityQuery,
|
|
116
|
+
) => Effect.Effect<ActivitySearchResult, ActivityUnavailable>;
|
|
117
|
+
}
|
|
118
|
+
>()("@effect-agent/testing/travel-planner/ActivityCatalog") {}
|
|
119
|
+
export class TravelGuidance extends Context.Service<
|
|
120
|
+
TravelGuidance,
|
|
121
|
+
{ readonly instructions: (input: TripRequest) => Effect.Effect<string, GuidanceFailure> }
|
|
122
|
+
>()("@effect-agent/testing/travel-planner/TravelGuidance") {}
|
|
123
|
+
|
|
124
|
+
export const SearchFlights = Tool.make("search_flights", {
|
|
125
|
+
parameters: FlightQuery,
|
|
126
|
+
success: FlightOption,
|
|
127
|
+
failure: FlightUnavailable,
|
|
128
|
+
failureMode: "error",
|
|
129
|
+
dependencies: [FlightCatalog],
|
|
130
|
+
});
|
|
131
|
+
export const SearchLodging = Tool.make("search_lodging", {
|
|
132
|
+
parameters: LodgingQuery,
|
|
133
|
+
success: LodgingOption,
|
|
134
|
+
failure: LodgingUnavailable,
|
|
135
|
+
failureMode: "error",
|
|
136
|
+
dependencies: [LodgingCatalog],
|
|
137
|
+
});
|
|
138
|
+
export const SearchActivities = Tool.make("search_activities", {
|
|
139
|
+
parameters: ActivityQuery,
|
|
140
|
+
success: ActivitySearchResult,
|
|
141
|
+
failure: ActivityUnavailable,
|
|
142
|
+
failureMode: "error",
|
|
143
|
+
dependencies: [ActivityCatalog],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export const TravelPlannerToolkit = Toolkit.make(SearchFlights, SearchLodging, SearchActivities);
|
|
147
|
+
export const TravelPlannerToolkitLayer = TravelPlannerToolkit.toLayer({
|
|
148
|
+
search_flights: (query) => Effect.flatMap(FlightCatalog, (catalog) => catalog.search(query)),
|
|
149
|
+
search_lodging: (query) => Effect.flatMap(LodgingCatalog, (catalog) => catalog.search(query)),
|
|
150
|
+
search_activities: (query) => Effect.flatMap(ActivityCatalog, (catalog) => catalog.search(query)),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export const TravelPlanner = Agent.define("travel-planner", {
|
|
154
|
+
input: TripRequest,
|
|
155
|
+
output: TravelPlan,
|
|
156
|
+
instructions: (input) =>
|
|
157
|
+
Effect.flatMap(TravelGuidance, (guidance) => guidance.instructions(input)),
|
|
158
|
+
toolkit: TravelPlannerToolkit,
|
|
159
|
+
policy: AgentPolicy.make({
|
|
160
|
+
maxTurns: 2,
|
|
161
|
+
maxToolCalls: 3,
|
|
162
|
+
maxDuration: "30 seconds",
|
|
163
|
+
toolConcurrency: 3,
|
|
164
|
+
}),
|
|
165
|
+
description: "Build one review-only itinerary from bounded parallel deterministic searches.",
|
|
166
|
+
metadata: { deploymentClass: "E", phase: "P1" },
|
|
167
|
+
});
|