@gjermundgaraba/effect-actions 0.9.0 → 0.11.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.
Files changed (70) hide show
  1. package/README.md +56 -50
  2. package/dist/Action.d.mts +286 -79
  3. package/dist/Action.mjs +125 -23
  4. package/dist/ActionCli.d.mts +102 -31
  5. package/dist/ActionCli.mjs +114 -16
  6. package/dist/ActionHttp.d.mts +170 -51
  7. package/dist/ActionHttp.mjs +130 -79
  8. package/dist/ActionMcp.d.mts +71 -21
  9. package/dist/ActionMcp.mjs +231 -80
  10. package/dist/ActionToolkit.d.mts +56 -27
  11. package/dist/ActionToolkit.mjs +16 -5
  12. package/dist/Authentication.d.mts +129 -33
  13. package/dist/Authentication.mjs +231 -38
  14. package/dist/Testing.d.mts +84 -57
  15. package/dist/Testing.mjs +121 -79
  16. package/dist/internal/actions.d.mts +5 -25
  17. package/dist/internal/actions.mjs +75 -27
  18. package/dist/internal/authentication.d.mts +102 -0
  19. package/dist/internal/authentication.mjs +11 -0
  20. package/dist/internal/call.d.mts +12 -0
  21. package/dist/internal/call.mjs +12 -0
  22. package/dist/internal/cli.d.mts +36 -22
  23. package/dist/internal/cli.mjs +311 -33
  24. package/dist/internal/client.d.mts +43 -0
  25. package/dist/internal/client.mjs +25 -0
  26. package/dist/internal/console.mjs +77 -0
  27. package/dist/internal/declared.d.mts +13 -0
  28. package/dist/internal/declared.mjs +34 -0
  29. package/dist/internal/errors.d.mts +77 -0
  30. package/dist/internal/errors.mjs +79 -0
  31. package/dist/internal/implementation.d.mts +171 -41
  32. package/dist/internal/implementation.mjs +147 -29
  33. package/dist/internal/mcp.d.mts +14 -0
  34. package/dist/internal/mcp.mjs +182 -0
  35. package/dist/internal/media.d.mts +8 -0
  36. package/dist/internal/media.mjs +26 -0
  37. package/dist/internal/memory.d.mts +6 -0
  38. package/dist/internal/memory.mjs +38 -0
  39. package/dist/internal/refusal.mjs +67 -0
  40. package/dist/internal/schema-errors.mjs +20 -0
  41. package/dist/internal/tools.mjs +27 -57
  42. package/docs/Action.md +403 -73
  43. package/docs/ActionCli.md +281 -51
  44. package/docs/ActionHttp.md +267 -82
  45. package/docs/ActionMcp.md +232 -102
  46. package/docs/ActionToolkit.md +99 -35
  47. package/docs/Authentication.md +369 -62
  48. package/docs/CONTEXT.md +24 -36
  49. package/docs/README.md +29 -73
  50. package/docs/Testing.md +154 -77
  51. package/docs/guarantees.md +82 -43
  52. package/docs/setup.md +95 -27
  53. package/package.json +12 -20
  54. package/dist/ActionCatalog.d.mts +0 -30
  55. package/dist/ActionCatalog.mjs +0 -39
  56. package/dist/ActionCliClient.d.mts +0 -48
  57. package/dist/ActionCliClient.mjs +0 -43
  58. package/dist/ActionGroup.d.mts +0 -44
  59. package/dist/ActionGroup.mjs +0 -35
  60. package/dist/ActionHttpClient.d.mts +0 -33
  61. package/dist/ActionHttpClient.mjs +0 -16
  62. package/dist/TestingClient.d.mts +0 -17
  63. package/dist/TestingClient.mjs +0 -22
  64. package/dist/internal/fetchClient.d.mts +0 -6
  65. package/dist/internal/fetchClient.mjs +0 -8
  66. package/dist/internal/tools.d.mts +0 -22
  67. package/docs/ActionCatalog.md +0 -54
  68. package/docs/ActionCliClient.md +0 -63
  69. package/docs/ActionGroup.md +0 -128
  70. package/docs/ActionHttpClient.md +0 -80
@@ -1,98 +1,249 @@
1
- import "./internal/implementation.mjs";
2
- import { TextField, bindTools } from "./internal/tools.mjs";
3
- import { Context, Effect, Layer, Option, Predicate, Schema } from "effect";
4
- import { McpProtocol, McpSchema, McpServer } from "effect/ai";
1
+ import { Anyone } from "./internal/actions.mjs";
2
+ import { provideHandlers, select, toList } from "./internal/implementation.mjs";
3
+ import { assertAuthentication, promote } from "./internal/authentication.mjs";
4
+ import { fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom } from "./internal/mcp.mjs";
5
+ import { recordStepUp } from "./internal/refusal.mjs";
6
+ import { logToStderr } from "./internal/console.mjs";
7
+ import { bindTools } from "./internal/tools.mjs";
8
+ import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Option, Predicate, References, Result, Schema, Stream } from "effect";
9
+ import { HttpRouter, HttpServerRequest } from "effect/http";
10
+ import { AiError, McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
5
11
  //#region src/ActionMcp.ts
6
12
  /**
7
- * The one protocol revision served. 2026-07-28 is stateless over HTTP: no
8
- * initialize handshake and no session, so every request stands alone.
13
+ * The revisions served over stdio: 2026-07-28 and the stateful revisions hosts open stdio
14
+ * with, newest first; a host asking for another is offered 2025-11-25. A success is sent as it
15
+ * is on each. 2026-07-28 carries any success as `structuredContent`; the adapters of 2025-11-25
16
+ * and 2025-06-18 carry only an object there, and list only an object-rooted `outputSchema`. A
17
+ * success they do not structure is text alone, a string as itself.
9
18
  */
10
- const protocols = [McpProtocol.v2026_07_28];
11
- /** The parts of a listed output schema a text field is removed from. */
12
- const decodeOutput = Schema.decodeUnknownOption(Schema.Struct({
13
- properties: Schema.Record(Schema.String, Schema.Json),
14
- required: Schema.optionalKey(Schema.Array(Schema.String))
15
- }));
16
- /** A JSON value that is an object: neither null, a scalar nor an array. */
17
- const isJsonObject = (value) => Predicate.isObject(value);
18
- /** Split a JSON object into one field and the rest. */
19
- const detach = (value, field) => {
20
- const { [field]: detached, ...rest } = value;
21
- return {
22
- detached,
23
- rest
19
+ const stdioProtocols = [
20
+ McpProtocol.v2026_07_28,
21
+ McpProtocol.v2025_11_25,
22
+ McpProtocol.v2025_06_18
23
+ ];
24
+ /**
25
+ * MCP has a JSON-only wire contract. Success is the encoded success as structured content,
26
+ * and its JSON as text, but for its media, each lifted into a block of its own; declared
27
+ * failures are returned as JSON text. The native server refuses undeclared arguments and
28
+ * publishes closed input schemas.
29
+ */
30
+ const projection = {
31
+ label: "MCP tool",
32
+ tool: (tool) => tool.annotate(Tool.Strict, true)
33
+ };
34
+ /** Whether a JSON Schema is one the native server takes for a tool's arguments. */
35
+ const isToolJson = Schema.is(McpSchema.ToolJson);
36
+ /**
37
+ * The JSON Schema of `schema`, a JSON codec, as the native server lists it for a tool: a
38
+ * `$ref` at its root resolved, as MCP requires an object root, and its definitions as `$defs`.
39
+ * A strict one refuses undeclared keys.
40
+ */
41
+ const toolJsonSchema = (schema, strict) => {
42
+ const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
43
+ const [scope, key] = Predicate.isString(document.schema.$ref) ? JsonPointer.parseUriFragment(document.schema.$ref) ?? [] : [];
44
+ const root = scope === "$defs" && key !== void 0 ? document.definitions[key] : document.schema;
45
+ return root === void 0 || Object.keys(document.definitions).length === 0 ? root : {
46
+ ...root,
47
+ $defs: document.definitions
24
48
  };
25
49
  };
26
50
  /**
27
- * The listed output schema without the text field. Only a top-level property can be
28
- * detached; anything else fails the layer build rather than publish a schema the
29
- * structured content would not satisfy.
51
+ * Refuse, when the server is made, the actions whose input is not one object, as the root of a
52
+ * tool's arguments must be, naming every one, reading the JSON Schema the native
53
+ * server reads: not a union, an array, a scalar, nor `Schema.Struct({})`, which takes any value
54
+ * but `null`, where the native server dies when the layer builds, suggesting
55
+ * `Tool.EmptyParams`. Then refuse those holding media where no tool lifts it, as each action's
56
+ * `Lift` says.
30
57
  */
31
- const outputWithout = (tool, field) => {
32
- const { properties: _, required: __, ...output } = tool.outputSchema ?? {};
33
- const listed = Option.getOrUndefined(decodeOutput(tool.outputSchema));
34
- if (listed === void 0 || !Object.hasOwn(listed.properties, field)) return Effect.die(`MCP tool '${tool.name}' cannot send '${field}' as text: it is not a top-level property of its success`);
35
- const required = (listed.required ?? []).filter((key) => key !== field);
36
- return Effect.succeed({
37
- ...output,
38
- properties: detach(listed.properties, field).rest,
39
- ...required.length === 0 ? {} : { required }
40
- });
58
+ const assertShapes = (lifted) => {
59
+ const inputs = lifted.map(([action]) => action).filter((action) => !isToolJson(toolJsonSchema(Schema.toCodecJson(action.input), true))).map(({ name }) => name);
60
+ if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")}`);
61
+ const misplaced = lifted.filter(([action, lift]) => isMisplaced(action, lift)).map(([{ name }]) => name);
62
+ if (misplaced.length > 0) throw new Error(`MCP media must be the success or an array of it, or a top-level field of a struct success, one, optional or a required array: ${misplaced.join(", ")}`);
41
63
  };
42
64
  /**
43
- * A successful result with a string text field sent once, raw, before the structured
44
- * rest and its JSON copy. Any other result, such as a failure or a success without
45
- * the field, is the native one.
65
+ * What the native server answers a call that fails other than as its contract declares: a
66
+ * defect, an invalid success or error, or an unknown tool.
46
67
  */
47
- const textResult = (result, field) => {
48
- const structured = result.structuredContent;
49
- if (result.isError === true || !isJsonObject(structured)) return result;
50
- const { detached, rest } = detach(structured, field);
51
- if (!Predicate.isString(detached)) return result;
52
- return new McpSchema.CallToolResult({
53
- ...result,
54
- structuredContent: rest,
55
- content: [{
56
- type: "text",
57
- text: detached
58
- }, {
59
- type: "text",
60
- text: JSON.stringify(rest)
61
- }]
68
+ const internalError = "Tool execution failed due to an internal server error.";
69
+ /** A tool's error result, `text` its one text block. */
70
+ const errorResult = (text) => new McpSchema.CallToolResult({
71
+ isError: true,
72
+ content: [{
73
+ type: "text",
74
+ text
75
+ }]
76
+ });
77
+ /** The one text block holding `encoded`'s JSON, none for `undefined`, as the native server sends it. */
78
+ const textOf = (encoded) => encoded === void 0 ? [] : [{
79
+ type: "text",
80
+ text: JSON.stringify(encoded)
81
+ }];
82
+ /** The content block a lifted media value is sent as, naming its field where it has one. */
83
+ const blockOf = ({ kind, field, value }) => ({
84
+ type: kind,
85
+ data: value.data,
86
+ mimeType: value.mimeType,
87
+ ...field === void 0 ? {} : { _meta: { [fieldKey]: field } }
88
+ });
89
+ /** Why a tool call's arguments failed to decode, as the native server tells it, if they did. */
90
+ const invalidParameters = (error) => AiError.isAiError(error) && Predicate.isTagged(error.reason, "ToolParameterValidationError") ? error.reason : void 0;
91
+ const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, References.CurrentLogLevel);
92
+ /**
93
+ * Register the tools of `toolkit` on the endpoint's registry, as `McpServer.registerToolkit`
94
+ * does, lifting the media `lifts` finds in a tool's success into blocks of their own. The native
95
+ * registration sends every success as structured content and one JSON text block, and takes
96
+ * no option, so this one follows it: its decode options, failure classification, defect
97
+ * scrubbing and listing, and the request services it leaves out of what it retains. A tool
98
+ * whose action holds no media is registered as the native one would be: a test compares their
99
+ * listings and results, not the services each call runs with.
100
+ */
101
+ const registerTools = Effect.fnUntraced(function* (toolkit, lifts) {
102
+ const registry = yield* McpServer.McpServer;
103
+ const built = yield* toolkit.pipe(Effect.updateContext((context) => Context.merge(context, Context.mergeAll(...Object.values(toolkit.tools).flatMap((tool) => {
104
+ const key = Context.Service(tool.id);
105
+ return Option.toArray(Context.getOption(context, key)).map((handler) => Context.make(key, {
106
+ ...handler,
107
+ context: omitRequestServices(handler.context)
108
+ }));
109
+ })))));
110
+ const services = omitRequestServices(yield* Effect.context());
111
+ const internalToolError = (cause) => {
112
+ const failure = Cause.findFail(cause);
113
+ return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(cause).pipe(Effect.andThen(Effect.provideContext(ErrorReporter.report(cause), services)), Effect.as(errorResult(internalError)));
114
+ };
115
+ const handleCause = (cause) => {
116
+ const failure = Cause.findFail(cause);
117
+ if (Result.isSuccess(failure)) {
118
+ const { error } = failure.success;
119
+ const invalid = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin) === "parameters" ? invalidParameters(error) : void 0;
120
+ if (invalid !== void 0) return Effect.fail(new McpSchema.InvalidParams({ message: invalid.message }));
121
+ }
122
+ return internalToolError(cause);
123
+ };
124
+ const registrations = yield* Effect.forEach(Object.entries(built.tools), ([name, tool]) => Effect.gen(function* () {
125
+ const strict = Tool.getStrictMode(tool) === true;
126
+ const decodeOptions = {
127
+ onExcessProperty: strict ? "error" : "ignore",
128
+ errors: "all"
129
+ };
130
+ const lift = lifts.get(name);
131
+ const media = lift === void 0 || lift.fields.length === 0 ? void 0 : lift;
132
+ const structured = media === void 0 ? tool.successSchema : media.rest === void 0 ? void 0 : Schema.toCodecJson(media.rest);
133
+ const outputSchema = structured === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(toolJsonSchema(structured, false)).pipe(Effect.orDie);
134
+ const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(toolJsonSchema(tool.parametersSchema, strict)).pipe(Effect.orDie);
135
+ const lifted = new Set(media?.fields.map(({ name }) => name));
136
+ const succeeded = ({ encodedResult, result }) => {
137
+ if (media === void 0) return new McpSchema.CallToolResult({
138
+ isError: false,
139
+ structuredContent: encodedResult,
140
+ content: textOf(encodedResult)
141
+ });
142
+ const blocks = liftedFrom(media, result).map(blockOf);
143
+ if (structured === void 0) return new McpSchema.CallToolResult({
144
+ isError: false,
145
+ content: blocks
146
+ });
147
+ const rest = Predicate.isObject(encodedResult) ? Object.fromEntries(Object.entries(encodedResult).filter(([key]) => !lifted.has(key))) : encodedResult;
148
+ return new McpSchema.CallToolResult({
149
+ isError: false,
150
+ structuredContent: rest,
151
+ content: [...textOf(rest), ...blocks]
152
+ });
153
+ };
154
+ return {
155
+ tool: new McpSchema.Tool({
156
+ name,
157
+ description: Tool.getDescription(tool),
158
+ inputSchema,
159
+ outputSchema,
160
+ annotations: {
161
+ ...Context.getOption(tool.annotations, Tool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
162
+ readOnlyHint: Context.get(tool.annotations, Tool.Readonly),
163
+ destructiveHint: Context.get(tool.annotations, Tool.Destructive),
164
+ idempotentHint: Context.get(tool.annotations, Tool.Idempotent),
165
+ openWorldHint: Context.get(tool.annotations, Tool.OpenWorld)
166
+ },
167
+ _meta: Context.getOrUndefined(tool.annotations, Tool.Meta)
168
+ }),
169
+ annotations: tool.annotations,
170
+ handle: (payload) => built.handle(name, payload ?? {}, void 0, decodeOptions).pipe(Stream.unwrap, Stream.runLast, Effect.flatMap(Effect.fromOption), Effect.flatMap((result) => !result.isFailure ? Effect.succeed(succeeded(result)) : result.failureOrigin === "handler" ? Effect.succeed(new McpSchema.CallToolResult({
171
+ isError: true,
172
+ content: textOf(result.encodedResult)
173
+ })) : Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result")))), Effect.catchCause(handleCause), Effect.provideContext(services))
174
+ };
175
+ }));
176
+ for (const registration of registrations) yield* registry.addTool(registration);
177
+ });
178
+ const server = (apps, transport, features = Layer.empty, authentication) => {
179
+ const binding = bindTools(apps, authentication === void 0 ? projection : {
180
+ ...projection,
181
+ handler: (run, action) => (input) => action.caller === Anyone ? run(input) : recordStepUp(promote(authentication, run(input)))
182
+ });
183
+ const lifted = apps.flatMap((app) => app.actions.map((action) => [action, liftOf(action.success)]));
184
+ assertShapes(lifted);
185
+ const lifts = new Map(lifted.map(([{ name }, lift]) => [name, lift]));
186
+ const register = Effect.gen(function* () {
187
+ const registry = yield* McpServer.McpServer;
188
+ const claimed = registry.tools.flatMap(({ tool }) => lifts.has(tool.name) ? [tool.name] : []);
189
+ if (claimed.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${claimed.join(", ")}, claimed by an action and a native feature`));
190
+ const handlers = yield* Layer.build(binding.layer);
191
+ yield* registerTools(binding.toolkit, lifts).pipe(Effect.setContext(Context.add(handlers, McpServer.McpServer, registry)));
62
192
  });
193
+ return Layer.effectDiscard(register).pipe(Layer.provide(features), Layer.provide(transport), Layer.fresh, provideHandlers(apps));
63
194
  };
64
- /** A native registration whose success sends `field` as text. */
65
- const withText = (registration, field) => Effect.map(outputWithout(registration.tool, field), (outputSchema) => ({
66
- ...registration,
67
- tool: new McpSchema.Tool({
68
- ...registration.tool,
69
- outputSchema
70
- }),
71
- handle: (payload) => Effect.map(registration.handle(payload), (result) => Predicate.isTagged(result, "InputRequired") ? result : textResult(result, field))
72
- }));
73
- const registration = (apps, options) => {
74
- const binding = bindTools(apps, "mcp", options);
75
- const registered = McpServer.registerToolkit(binding.toolkit).pipe(Effect.updateService(McpServer.McpServer, (server) => ({
76
- ...server,
77
- addTool: (tool) => {
78
- const field = Context.get(tool.annotations, TextField);
79
- return field === void 0 ? server.addTool(tool) : Effect.flatMap(withText(tool, field), server.addTool);
80
- }
81
- })));
82
- return Layer.effectDiscard(registered).pipe(Layer.provide(binding.layer));
195
+ /**
196
+ * What an anonymous caller of a mixed endpoint may send besides a public tool's call:
197
+ * discovering the server and its tools, which the contracts describe, and cancelling.
198
+ * Everything else, a native feature's listing, completion or subscription among them, and
199
+ * any method a later revision adds, authenticates.
200
+ */
201
+ const anonymous = /* @__PURE__ */ new Set([
202
+ "server/discover",
203
+ "tools/list",
204
+ "notifications/cancelled"
205
+ ]);
206
+ /**
207
+ * Whether a request to an endpoint serving `actions` must authenticate before the native
208
+ * server reads its body. An endpoint whose every tool is protected authenticates every
209
+ * request, so a host signs in when it connects. Otherwise the request's routing headers
210
+ * decide, which the native server refuses when they disagree with the body: discovery and a
211
+ * call of a public tool authenticate only a caller presenting credentials, and everything
212
+ * else authenticates, so native features are protected too. A request whose headers name
213
+ * no public tool as it is authenticates: it fails closed.
214
+ */
215
+ const requiresAuthentication = (actions) => {
216
+ const open = new Set(actions.filter(({ caller }) => caller === Anyone).map(({ name }) => name));
217
+ return (headers) => {
218
+ if (open.size === 0) return true;
219
+ const method = headers["mcp-method"];
220
+ if (method === void 0) return true;
221
+ if (anonymous.has(method)) return false;
222
+ if (method !== "tools/call") return true;
223
+ const name = headers["mcp-name"];
224
+ return name === void 0 || !open.has(name);
225
+ };
83
226
  };
84
- const server = (apps, options, transport) => registration(apps, options).pipe(Layer.provide(transport), Layer.fresh);
85
- function layerHttp(apps, options) {
86
- return server(apps, options, McpServer.layerHttp({
227
+ function layerHttp(apps, { actions, features, path, authentication, ...options }) {
228
+ const selected = select(toList(apps), actions);
229
+ const served = selected.flatMap((app) => app.actions);
230
+ assertAuthentication(served, authentication);
231
+ const transport = server(selected, McpServer.layerHttp({
87
232
  ...options,
88
- protocols
89
- }));
233
+ path: path ?? "/mcp",
234
+ protocols: [httpProtocol]
235
+ }), features, authentication);
236
+ if (authentication === void 0) return transport;
237
+ const required = requiresAuthentication(served);
238
+ const middleware = HttpRouter.middleware(Effect.map(authentication["~provider"], (runtime) => (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => runtime.http(route, !required(request.headers)))));
239
+ return transport.pipe(Layer.provide(middleware.layer));
90
240
  }
91
- function layerStdio(apps, options) {
92
- return server(apps, options, McpServer.layerStdio({
241
+ function runStdio(apps, { actions, features, ...options }) {
242
+ const transport = server(select(toList(apps), actions), McpServer.layerStdio({
93
243
  ...options,
94
- protocols
95
- }));
244
+ protocols: stdioProtocols
245
+ }), features);
246
+ return Layer.launch(transport).pipe(logToStderr, Effect.forkChild, Effect.flatMap(Fiber.await), Effect.flatMap((exit) => Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause) ? Effect.failCause(exit.cause) : Effect.void));
96
247
  }
97
248
  //#endregion
98
- export { layerHttp, layerStdio };
249
+ export { layerHttp, runStdio };
@@ -1,40 +1,69 @@
1
- import { Action_d_exports, Any, Codec } from "./Action.mjs";
2
- import { AnyImplementation, BuildContext, BuildError, HandlerContext, HiddenFromMcp, Implementation } from "./internal/implementation.mjs";
3
- import { SurfaceOptions } from "./internal/tools.mjs";
4
- import { Layer, Schema } from "effect";
1
+ import { BuiltIns } from "./internal/errors.mjs";
2
+ import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Offered, OptionalUnless, RequestOf, SelectedOf, Served, Serving } from "./internal/implementation.mjs";
3
+ import { Action_d_exports, Any } from "./Action.mjs";
4
+ import { Effect, Layer, Schema } from "effect";
5
5
  import { Tool, Toolkit } from "effect/ai";
6
6
  //#region src/ActionToolkit.d.ts
7
- /** What the in-process caller binds around the implementations it projects. */
8
- export type Options<Errors extends ReadonlyArray<Codec> = [], R = never> = SurfaceOptions<Errors, R>;
9
- /** A native tool corresponding to an action MCP may serve, named by its tool metadata. */
10
- type NativeTool<A extends Any, E extends Codec, R> = Extract<A["mcp"], object> extends {
11
- readonly name: infer Name extends string;
12
- } ? Tool.Tool<Name, {
13
- readonly parameters: A["input"];
14
- readonly success: A["success"];
15
- readonly failure: Schema.Union<ReadonlyArray<A["errors"][number] | E>>;
7
+ /** A native tool named after its action, taking and giving JSON. */
8
+ type NativeTool<A extends Any, R> = Tool.Tool<A["name"], {
9
+ readonly parameters: Schema.toCodecJson<A["input"]>;
10
+ readonly success: Schema.toCodecJson<A["success"]>;
11
+ readonly failure: Schema.toCodecJson<Schema.Union<ReadonlyArray<A["error"][number] | BuiltIns>>>;
16
12
  readonly failureMode: "return";
17
- }, R> : never;
18
- type HandlersOf<App extends AnyImplementation> = App extends Implementation<any, infer H, any, any> ? H : never;
13
+ }, R>;
19
14
  /**
20
- * A tool needs what its handler needs, plus what the binding's `before` hook needs. The
21
- * rule is `ActionMcp`'s: only an action certainly hidden from MCP has no tool.
15
+ * The tool of each action of `App` among `Listed`. A tool needs what its handler and its
16
+ * implementation's authorization need.
22
17
  */
23
- type ToolForAction<App extends AnyImplementation, A extends Any, E extends Codec, RB> = A extends HiddenFromMcp ? never : NativeTool<A, E, HandlerContext<HandlersOf<App>, Extract<A["name"], keyof HandlersOf<App>>> | RB>;
24
- type ToolsFor<App extends AnyImplementation, E extends Codec, RB> = App extends AnyImplementation ? ToolForAction<App, App["group"]["actions"][number], E, RB> : never;
25
- type ToolkitTools<Apps extends ReadonlyArray<AnyImplementation>, E extends Codec, RB> = { readonly [T in ToolsFor<Apps[number], E, RB> as T["name"]]: T; };
26
- /** Native tools and the layer that binds their action implementations. */
27
- export interface Binding<Tools extends Record<string, Tool.Any>, E = never, R = never> {
28
- readonly toolkit: Toolkit.Toolkit<Tools>;
18
+ type ToolFor<App, Listed extends Any> = App extends unknown ? Serving<App, Listed> extends (infer A extends Any) ? A extends Any ? NativeTool<A, RequestOf<App, A>> : never : never : never;
19
+ /**
20
+ * Native tools bound to their action implementations: a native `Toolkit` and the layer of
21
+ * its handlers, for `LanguageModel`, `Toolkit.merge` or `handle`. Tools belong to their
22
+ * implementations: the `layer` of a `make` call serves the tools it selected in any `toolkit`
23
+ * of the same implementations, while two implementations never run each other's handlers,
24
+ * even with tools of one name.
25
+ */
26
+ export interface Tools<T extends Record<string, Tool.Any>, E, R> {
27
+ /**
28
+ * The native toolkit: its `tools` are the definitions, by name, with schemas, hints and approval.
29
+ */
30
+ readonly toolkit: Toolkit.Toolkit<T>;
29
31
  /** Acquires handlers once in the layer scope; handler requirements remain at invocation. */
30
- readonly layer: Layer.Layer<Tool.HandlersFor<Tools>, E, R>;
32
+ readonly layer: Layer.Layer<Tool.HandlersFor<T>, E, R>;
33
+ }
34
+ /**
35
+ * One call of a tool, as `needsApproval` receives it: its action's name, the action, and
36
+ * the call's decoded input. Checking `name` narrows the other two to that action's.
37
+ */
38
+ export type ToolCall<A extends Any = Any> = A extends Any ? {
39
+ readonly name: A["name"];
40
+ readonly action: A;
41
+ readonly input: A["input"]["Type"];
42
+ } : never;
43
+ /** How `make` projects its tools, the actions `A`. */
44
+ export interface Options<A extends Any> {
45
+ /**
46
+ * The actions that are tools, among the implementations' actions: `[GetUser, RenameUser]`.
47
+ * Each keeps its implementation's authorization and builder. Defaults to every action of them.
48
+ */
49
+ readonly actions?: ReadonlyArray<A> | undefined;
50
+ /**
51
+ * Whether a model's call needs approval before it runs: a boolean, or an Effect of one, for
52
+ * each call, with Effect's native approval context. `LanguageModel` asks for approval
53
+ * instead of calling the tool when it is `true`. The Effect runs in the caller's context and
54
+ * requires nothing, so it reads the caller with `Effect.serviceOption`. Defaults to none.
55
+ */
56
+ readonly needsApproval?: (call: ToolCall<A>, context: Tool.NeedsApprovalContext) => boolean | Effect.Effect<boolean>;
31
57
  }
32
58
  /**
33
- * Project implementations into Effect's native AI toolkit.
59
+ * Project implementations into Effect's native AI toolkit: one tool per action, keyed by its
60
+ * name, of the listed `actions` or, without them, of every action of the implementations.
34
61
  *
35
62
  * Unlike MCP, calls return the action's native success/failure values directly.
36
63
  * Build services are needed to construct `layer`; request services are needed
37
- * when the resulting toolkit handles a call.
64
+ * when the resulting toolkit handles a call, the identity an implementation's `authorize`
65
+ * reads included: the caller provides it. `needsApproval` marks the calls a model must have
66
+ * approved; it authorizes nothing, which stays `authorize`'s.
38
67
  */
39
- export declare function make<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options?: Options<Errors, RB>): Binding<ToolkitTools<Apps, Errors[number], RB>, BuildError<Apps[number], HiddenFromMcp>, BuildContext<Apps[number], HiddenFromMcp>>;
68
+ export declare function make<const Apps extends Served, const O extends Options<ActionOf<Member<Apps>>> = {}>(implementations: Apps, ...options: OptionalUnless<O, Options<ActionOf<Member<Apps>>> & O & NoInfer<Known<O, Options<Any>>>>): Tools<{ readonly [T in ToolFor<Member<Apps>, Offered<O, ActionOf<Member<Apps>>>> as T["name"]]: T; }, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>;
40
69
  //#endregion
@@ -1,10 +1,21 @@
1
- import "./internal/implementation.mjs";
1
+ import { Implementation, provideHandlers, select, toList } from "./internal/implementation.mjs";
2
2
  import { bindTools } from "./internal/tools.mjs";
3
- import "effect";
4
- import "effect/ai";
5
3
  //#region src/ActionToolkit.ts
6
- function make(apps, options = {}) {
7
- return bindTools(apps, "native", options);
4
+ function make(apps, options) {
5
+ const served = select(toList(apps), options?.actions);
6
+ const needsApproval = options?.needsApproval;
7
+ const { toolkit, layer } = bindTools(served, {
8
+ label: "tool",
9
+ tool: (tool, action, app) => Object.assign(needsApproval === void 0 ? tool : tool.setNeedsApproval((input, context) => needsApproval({
10
+ name: action.name,
11
+ action,
12
+ input
13
+ }, context)), { id: `${Implementation.runKey(app)}/${action.name}` })
14
+ });
15
+ return {
16
+ toolkit,
17
+ layer: layer.pipe(provideHandlers(served))
18
+ };
8
19
  }
9
20
  //#endregion
10
21
  export { make };