@gjermundgaraba/effect-actions 0.9.0 → 0.10.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/README.md +57 -50
- package/dist/Action.d.mts +285 -79
- package/dist/Action.mjs +124 -23
- package/dist/ActionCli.d.mts +102 -31
- package/dist/ActionCli.mjs +114 -16
- package/dist/ActionHttp.d.mts +170 -51
- package/dist/ActionHttp.mjs +130 -79
- package/dist/ActionMcp.d.mts +71 -21
- package/dist/ActionMcp.mjs +108 -81
- package/dist/ActionToolkit.d.mts +56 -27
- package/dist/ActionToolkit.mjs +16 -5
- package/dist/Authentication.d.mts +129 -33
- package/dist/Authentication.mjs +231 -38
- package/dist/Testing.d.mts +82 -57
- package/dist/Testing.mjs +89 -79
- package/dist/internal/actions.d.mts +5 -25
- package/dist/internal/actions.mjs +75 -27
- package/dist/internal/authentication.d.mts +102 -0
- package/dist/internal/authentication.mjs +11 -0
- package/dist/internal/call.d.mts +12 -0
- package/dist/internal/call.mjs +12 -0
- package/dist/internal/cli.d.mts +36 -22
- package/dist/internal/cli.mjs +311 -33
- package/dist/internal/client.d.mts +43 -0
- package/dist/internal/client.mjs +25 -0
- package/dist/internal/console.mjs +77 -0
- package/dist/internal/declared.d.mts +13 -0
- package/dist/internal/declared.mjs +34 -0
- package/dist/internal/errors.d.mts +77 -0
- package/dist/internal/errors.mjs +79 -0
- package/dist/internal/implementation.d.mts +171 -41
- package/dist/internal/implementation.mjs +147 -29
- package/dist/internal/mcp.d.mts +11 -0
- package/dist/internal/mcp.mjs +52 -0
- package/dist/internal/memory.d.mts +6 -0
- package/dist/internal/memory.mjs +38 -0
- package/dist/internal/refusal.mjs +67 -0
- package/dist/internal/schema-errors.mjs +20 -0
- package/dist/internal/tools.mjs +27 -57
- package/docs/Action.md +401 -73
- package/docs/ActionCli.md +280 -51
- package/docs/ActionHttp.md +266 -82
- package/docs/ActionMcp.md +149 -104
- package/docs/ActionToolkit.md +97 -35
- package/docs/Authentication.md +369 -62
- package/docs/CONTEXT.md +23 -36
- package/docs/README.md +29 -73
- package/docs/Testing.md +154 -77
- package/docs/guarantees.md +81 -43
- package/docs/setup.md +90 -27
- package/package.json +7 -15
- package/dist/ActionCatalog.d.mts +0 -30
- package/dist/ActionCatalog.mjs +0 -39
- package/dist/ActionCliClient.d.mts +0 -48
- package/dist/ActionCliClient.mjs +0 -43
- package/dist/ActionGroup.d.mts +0 -44
- package/dist/ActionGroup.mjs +0 -35
- package/dist/ActionHttpClient.d.mts +0 -33
- package/dist/ActionHttpClient.mjs +0 -16
- package/dist/TestingClient.d.mts +0 -17
- package/dist/TestingClient.mjs +0 -22
- package/dist/internal/fetchClient.d.mts +0 -6
- package/dist/internal/fetchClient.mjs +0 -8
- package/dist/internal/tools.d.mts +0 -22
- package/docs/ActionCatalog.md +0 -54
- package/docs/ActionCliClient.md +0 -63
- package/docs/ActionGroup.md +0 -128
- package/docs/ActionHttpClient.md +0 -80
package/docs/ActionMcp.md
CHANGED
|
@@ -1,69 +1,70 @@
|
|
|
1
1
|
# ActionMcp
|
|
2
2
|
|
|
3
|
-
MCP tools from implementations, on Effect's native `McpServer`. One `Tool` per
|
|
4
|
-
|
|
5
|
-
JSON-RPC on standard I/O for a subprocess.
|
|
3
|
+
MCP tools from implementations, on Effect's native `McpServer`. One `Tool` per served action,
|
|
4
|
+
named after it. Two transports: a Streamable HTTP endpoint mounted on the router, or newline-delimited
|
|
5
|
+
JSON-RPC on standard I/O for a subprocess. HTTP speaks MCP 2026-07-28 only; stdio also speaks
|
|
6
|
+
2025-11-25 and 2025-06-18, as the host negotiates.
|
|
6
7
|
|
|
7
8
|
## API
|
|
8
9
|
|
|
9
10
|
Import `@gjermundgaraba/effect-actions/ActionMcp`.
|
|
10
11
|
|
|
11
|
-
| API
|
|
12
|
-
|
|
|
13
|
-
| `layerHttp(
|
|
14
|
-
| `
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`
|
|
33
|
-
|
|
34
|
-
is
|
|
12
|
+
| API | Purpose |
|
|
13
|
+
| ------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| `layerHttp(implementations, options)` | Serve implementations at a Streamable HTTP endpoint. |
|
|
15
|
+
| `runStdio(implementations, options)` | Serve implementations as a subprocess's program on standard I/O; succeeds when the host closes it. |
|
|
16
|
+
|
|
17
|
+
The options, exported as `LayerHttpOptions` and `Options` (the server's, which `LayerHttpOptions` extends), are the native
|
|
18
|
+
`McpServer.layerHttp` / `McpServer.layerStdio` options, except `protocols`, plus `actions` and `features`, and for `layerHttp`, `authentication`. The native ones pass through unchanged; `path` gains a default. Each implementation brings its
|
|
19
|
+
authorization ([Action.md](Action.md#implementations)); an endpoint serving protected actions
|
|
20
|
+
names their authentication descriptor and requires its provider
|
|
21
|
+
([Authentication.md](Authentication.md)).
|
|
22
|
+
|
|
23
|
+
| Option | Meaning |
|
|
24
|
+
| ------------------------------------ | ------------------------------------------------------------------------------ |
|
|
25
|
+
| `name`, `version` | Required native server information. |
|
|
26
|
+
| `description`, `websiteUrl`, `icons` | Optional native server information, sent to clients with `name` and `version`. |
|
|
27
|
+
| `instructions` | Optional native server instructions. |
|
|
28
|
+
| `extensions` | Optional native server capability extensions. |
|
|
29
|
+
| `path` | HTTP endpoint path; HTTP only, defaults to `/mcp`. |
|
|
30
|
+
| `allowedOrigins` | Optional exact Origin allowlist; HTTP only, not CORS configuration. |
|
|
31
|
+
| `authentication` | HTTP only: the protected tools' descriptor, required where one is served. |
|
|
32
|
+
| `actions` | Optional actions that are tools, among the implementations'; defaults to all. |
|
|
33
|
+
| `features` | Optional native resources, prompts and tools served beside the actions' tools. |
|
|
34
|
+
|
|
35
|
+
`implementations` is one implementation or a list. Every tool declares its action's errors plus the
|
|
36
|
+
built-in `InvalidInput`, `Unauthenticated` and `Forbidden`. `layerHttp`'s layer and `runStdio`'s
|
|
37
|
+
program retain the build failures and requirements of their builders, plus native
|
|
38
|
+
`IllegalArgumentError`. HTTP needs the router, the descriptor's provider where it serves a
|
|
39
|
+
protected action, and wraps handler and authorizer services as request requirements until middleware provided around it provides them, the
|
|
40
|
+
identity excepted, which the provider supplies. stdio needs `Stdio` and the caller's request
|
|
41
|
+
services, the identity of its protected tools included. Native `McpRequestContext`
|
|
42
|
+
is supplied by the server, not required of the host. The server also fails as its `features` do
|
|
43
|
+
and needs what they need, except the registry, `McpServer.McpServer`, which the endpoint provides,
|
|
44
|
+
such as to a feature registering with `McpServer.registerResource`.
|
|
35
45
|
|
|
36
46
|
## Canonical
|
|
37
47
|
|
|
38
|
-
|
|
48
|
+
One endpoint serves the public and the protected tools: discovery and `status` answer anyone,
|
|
49
|
+
and every other request authenticates before the endpoint decodes it.
|
|
50
|
+
|
|
51
|
+
```ts example=mcp.ts
|
|
39
52
|
import { Layer } from "effect";
|
|
40
53
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
54
|
+
import { authenticate } from "./authentication.js";
|
|
55
|
+
import { Login } from "./binding.js";
|
|
56
|
+
import { double, status, userActions } from "./handlers.js";
|
|
43
57
|
|
|
44
|
-
const allowedOrigins = ["http://localhost:3000"];
|
|
58
|
+
const allowedOrigins = ["http://localhost:3000", "http://127.0.0.1:3000"];
|
|
45
59
|
|
|
46
|
-
// One
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
name: "
|
|
50
|
-
version: "
|
|
51
|
-
path: "/mcp/public",
|
|
52
|
-
allowedOrigins,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const mcp = ActionMcp.layerHttp([UserApp, AuditApp], {
|
|
56
|
-
name: "app",
|
|
57
|
-
version: "1.0.0",
|
|
58
|
-
path: "/mcp",
|
|
60
|
+
// One URL: discovery and `status` answer anyone; a protected tool's call authenticates
|
|
61
|
+
// before its arguments are decoded, with the 401 an MCP client signs in on.
|
|
62
|
+
export const layer = ActionMcp.layerHttp([status, userActions, double], {
|
|
63
|
+
name: "effect-actions",
|
|
64
|
+
version: "0.0.0",
|
|
59
65
|
allowedOrigins,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
errors: [Forbidden],
|
|
63
|
-
before: authorize,
|
|
64
|
-
}).pipe(Layer.provide(authentication.layer));
|
|
65
|
-
|
|
66
|
-
export const layer = Layer.mergeAll(publicMcp, mcp);
|
|
66
|
+
authentication: Login,
|
|
67
|
+
}).pipe(Layer.provide(authenticate));
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
### Cross-origin browsers
|
|
@@ -72,25 +73,21 @@ export const layer = Layer.mergeAll(publicMcp, mcp);
|
|
|
72
73
|
native router CORS outside the route middleware. This CORS layer is global to the router;
|
|
73
74
|
choose its policy for every route it covers.
|
|
74
75
|
|
|
75
|
-
```ts
|
|
76
|
+
```ts example=mcp-browser.ts
|
|
76
77
|
import { Effect, Layer } from "effect";
|
|
77
78
|
import { HttpRouter } from "effect/http";
|
|
79
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
78
80
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
79
|
-
import {
|
|
81
|
+
import { Greet } from "./quickstart.js";
|
|
80
82
|
|
|
81
83
|
const allowedOrigins = ["https://ui.example.com"];
|
|
82
84
|
|
|
83
|
-
const
|
|
85
|
+
const actions = Action.implement(Greet, ({ name }) => Effect.succeed(`Hello, ${name}!`));
|
|
84
86
|
|
|
85
|
-
const mcp = ActionMcp.layerHttp(
|
|
86
|
-
name: "greetings",
|
|
87
|
-
version: "1.0.0",
|
|
88
|
-
path: "/mcp",
|
|
89
|
-
allowedOrigins,
|
|
90
|
-
});
|
|
87
|
+
const mcp = ActionMcp.layerHttp(actions, { name: "greetings", version: "1.0.0", allowedOrigins });
|
|
91
88
|
|
|
92
89
|
// Global router CORS handles preflight outside route-level authentication.
|
|
93
|
-
// This example is public;
|
|
90
|
+
// This example is public; an endpoint serving protected actions takes their `authentication`.
|
|
94
91
|
export const routes = Layer.mergeAll(
|
|
95
92
|
mcp,
|
|
96
93
|
HttpRouter.cors({
|
|
@@ -110,69 +107,117 @@ export const routes = Layer.mergeAll(
|
|
|
110
107
|
|
|
111
108
|
### Subprocess
|
|
112
109
|
|
|
113
|
-
```ts
|
|
110
|
+
```ts example=mcp-stdio.ts
|
|
114
111
|
import { NodeRuntime, NodeStdio } from "@effect/platform-node";
|
|
115
|
-
import {
|
|
112
|
+
import { Effect, Schema } from "effect";
|
|
116
113
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
117
|
-
import * as
|
|
114
|
+
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
118
115
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
119
116
|
|
|
120
117
|
const Status = Action.make("status", {
|
|
121
118
|
description: "Report whether the subprocess is ready.",
|
|
122
|
-
success:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const app = ActionGroup.make({ name: "stdio" }, Status).implement({
|
|
127
|
-
status: () => Effect.log("status called").pipe(Effect.as({ ready: true })),
|
|
119
|
+
success: { ready: Schema.Boolean },
|
|
120
|
+
readOnly: true,
|
|
121
|
+
caller: Action.Anyone,
|
|
128
122
|
});
|
|
129
123
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}).pipe(Layer.provide(NodeStdio.layer));
|
|
124
|
+
const status = Action.implement(Status, () =>
|
|
125
|
+
Effect.log("status called").pipe(Effect.as({ ready: true })),
|
|
126
|
+
);
|
|
134
127
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
128
|
+
// Serves until the host closes stdin, then exits 0. Protocol messages use stdout
|
|
129
|
+
// exclusively: runStdio writes its program's Effect logs and `Console` output to stderr, and
|
|
130
|
+
// `logToStderr`, applied last, does so for the layers provided around it, and reports a failure
|
|
131
|
+
// there rather than as runMain would, on stdout.
|
|
132
|
+
ActionMcp.runStdio(status, { name: "effect-actions-stdio", version: "0.1.0" }).pipe(
|
|
133
|
+
Effect.provide(NodeStdio.layer),
|
|
134
|
+
ActionCli.logToStderr,
|
|
135
|
+
NodeRuntime.runMain,
|
|
140
136
|
);
|
|
141
137
|
```
|
|
142
138
|
|
|
139
|
+
### Native features
|
|
140
|
+
|
|
141
|
+
Effect's own resources, prompts and tools join an endpoint's tools as its `features`, one layer
|
|
142
|
+
merging them:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
ActionMcp.layerHttp(app, {
|
|
146
|
+
name: "acme",
|
|
147
|
+
version: "1.0.0",
|
|
148
|
+
features: Layer.mergeAll(
|
|
149
|
+
McpServer.resource({ uri: "docs://readme", name: "README", content: Effect.succeed("# Acme") }),
|
|
150
|
+
McpServer.prompt({ name: "triage", content: () => Effect.succeed("Triage the issue.") }),
|
|
151
|
+
),
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
They are native: no implementation's authorization runs for them. On an endpoint
|
|
156
|
+
naming a descriptor, every request a feature answers authenticates, its listings, completions
|
|
157
|
+
and subscriptions included, so a public resource or prompt beside protected tools takes an
|
|
158
|
+
endpoint of its own. The features register before the actions, and a native tool named as an
|
|
159
|
+
action is refused when the endpoint builds: Effect's registry keeps one tool per name, and the
|
|
160
|
+
endpoint's authentication decides by that name. A service they need, provided around the endpoint, is shared with the rest of the graph; one
|
|
161
|
+
provided to the `features` layer itself is built once per endpoint.
|
|
162
|
+
|
|
163
|
+
A feature runs with the services it was built with, never a request's: Effect's `resource` and
|
|
164
|
+
`prompt` run their content in their build context alone, so a feature cannot read the caller.
|
|
165
|
+
Content that depends on the caller is an action. Never provide an identity at startup to
|
|
166
|
+
satisfy a feature.
|
|
167
|
+
|
|
143
168
|
## Rules
|
|
144
169
|
|
|
145
|
-
-
|
|
146
|
-
- `path`
|
|
147
|
-
- Requests reaching the native MCP handler with an `Origin` header receive **403** unless that exact origin is listed in `allowedOrigins`. Requests without `Origin` pass this check.
|
|
170
|
+
- HTTP serves MCP 2026-07-28 and no other revision: it is stateless, every request standing alone. The stateful revisions keep a session per `initialize`, which Effect's HTTP runtime never expires and which no identity owns. Stdio serves 2026-07-28, 2025-11-25 and 2025-06-18, whichever the host negotiates. There is no `protocols` option; Effect owns version checks and negotiation.
|
|
171
|
+
- `path` defaults to `/mcp`. `layerHttp` uses the single-endpoint Streamable HTTP transport, never the two-endpoint HTTP+SSE form.
|
|
172
|
+
- Requests reaching the native MCP handler with an `Origin` header receive **403** unless that exact origin is listed in `allowedOrigins`. Requests without `Origin` pass this check. The endpoint's authentication runs first and may reject the request before native Origin validation; the allowlist does not protect authentication from untrusted-origin requests: a check that must run before it is global middleware ([Authentication.md](Authentication.md#rules)).
|
|
148
173
|
- `allowedOrigins` is an Origin allowlist, not CORS configuration. Cross-origin browser clients also need outer CORS middleware or a proxy to handle preflight and add response headers. Without it, an allowed-origin `OPTIONS` request receives **405** and even a successful `POST` has no `Access-Control-Allow-Origin`. Keep preflight outside authentication and apply CORS headers to refusals too.
|
|
149
|
-
- An endpoint
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
-
|
|
155
|
-
-
|
|
174
|
+
- An endpoint serving a protected action names its descriptor, `authentication: Login`, and requires its provider, `Layer.provide(authenticate)`; the types refuse one without it, and `layerHttp` throws for plain JavaScript. One serving only public actions names none, and authenticates nothing.
|
|
175
|
+
- An endpoint whose every tool is protected authenticates every request before reading its body, so a host signs in when it connects.
|
|
176
|
+
- A mixed endpoint, serving public and protected tools on one URL, decides from the request's MCP routing headers before the native server reads its body, as the native server refuses a request whose headers disagree with its body. Discovery, `server/discover` and `tools/list`, `notifications/cancelled`, and a call of a public tool pass without a credential; a credential presented there is verified, and refused with 401 if it does not verify, while one its scheme decodes as empty passes as none. Every other request authenticates: a protected tool's call, before its arguments are decoded, and every native feature's request, `prompts/get`, `resources/read`, their listings, `completion/complete` and `subscriptions/listen` included, as does a method a later revision adds. The `Mcp-Name` header is compared as it is: a `tools/call` naming no public tool by it, a Base64-encoded name (`=?base64?...?=`) included, authenticates, and so does a request whose headers name no method: it fails closed.
|
|
177
|
+
- So signed-out callers of a mixed endpoint list every tool, protected ones included, and call the public ones; a protected tool's call answers the 401 an MCP client signs in on, then retries. To keep protected tools unlisted to signed-out callers, serve the public tools on an endpoint of their own, with separate `layerHttp` calls on different paths.
|
|
178
|
+
- A public tool never gets the identity, signed in or not; an action whose answer depends on the caller is protected. Middleware provided around `layerHttp` covers every request to the endpoint, before its authentication; an endpoint takes no layer middleware of its own, so middleware that reads the identity is not available over MCP ([ActionHttp.md](ActionHttp.md#rules)).
|
|
179
|
+
- Every action of the implementations passed becomes a tool, named after the action, with the action's `mcp` hints, its `title` and `_meta` included, unless `actions` lists the tools: then only those, each behind its implementation's authorizer and from its builder's one run, and an implementation holding none of them is not built. To keep an action off MCP, leave it out of `actions`: `ActionMcp.layerHttp([users, pages], { name, version, actions: Tools })` ([Action.md](Action.md#contracts)). Only a listed action's input must suit MCP, and only listed actions' names must be unique. The types take only actions of the implementations, and owe only what the listed actions' handlers and authorizers, and the builders holding them, need. Whether the endpoint authenticates follows the listed actions alone.
|
|
180
|
+
- Builders and request services follow the [dependency lifetimes](guarantees.md#dependency-lifetimes), and authorization the [authorization rules](guarantees.md#authorization). Only the tool registry is fresh per endpoint.
|
|
181
|
+
- A handler may yield `McpServer.McpServer`, its endpoint's registry, to send notifications such as progress: the endpoint provides it, and no host owes it. Served on another surface too, where nothing provides it, read it with `Effect.serviceOption`.
|
|
182
|
+
- Every served action's input must be one object with keys, as a tool's arguments are: fields, a struct or a class, identified, recursive or suspended, a record, or a declared type whose JSON Schema is an object. `layerHttp` and `runStdio` read the JSON Schema the native server reads, and throw for any other when they are called, naming the actions: a union, an array, a scalar, or an object without keys such as `Schema.Struct({})`. Omit `input`, or give `{}`, for a tool with no arguments.
|
|
183
|
+
- A tool's `inputSchema` is its input's JSON Schema, on every revision, closed with `additionalProperties: false` where the input declares its fields; a record's lists its value schema there instead. Undeclared arguments are refused on every revision, as invalid arguments.
|
|
184
|
+
- A success is sent as it is. On 2026-07-28, over HTTP and stdio, it is `structuredContent: <encoded success>`, of any JSON type (`null` for an action that returns nothing), and one text block holding the same JSON, and the tool's `outputSchema` describes the encoded success. Stdio's earlier revisions, 2025-11-25 and 2025-06-18, structure less: they carry only an object as `structuredContent`, and list only an object-rooted `outputSchema`. A success they do not structure is text alone: its JSON, or a string success the string itself. A declared error is an `isError` result whose text content is the error's JSON encoding, the same bytes HTTP sends as the body, and no `structuredContent`.
|
|
185
|
+
- On 2026-07-28, over HTTP and over stdio, the native server adds `_meta["io.modelcontextprotocol/serverInfo"]` and `resultType: "complete"` to every result, beside a tool result's own fields (`isError: false` on a success); the earlier revisions stdio speaks add neither. `serverInfo` is the options' `name`, `version`, `description`, `websiteUrl` and `icons`, as given, so a 2026-07-28 result's encoded size is the size of its own fields plus a fixed overhead per endpoint or subprocess.
|
|
186
|
+
- Invalid arguments are answered by the native `McpServer`: from 2025-11-25 on, an `isError` result with a message for the model, such as `Invalid parameters for tool 'greet': Expected string\n at ["name"]`; on earlier stdio revisions, a JSON-RPC error. HTTP's `InvalidInput` does not apply. A protected tool's call from a caller without a credential that verifies gets the 401 first, before its arguments are decoded, malformed ones included, on a mixed endpoint as on one of protected tools alone.
|
|
156
187
|
- Defects and encoding failures produce the generic `isError` text `Tool execution failed due to an internal server error.`; the cause is logged, not sent.
|
|
157
|
-
-
|
|
158
|
-
- Cancellation is Effect's native RPC interruption. Over HTTP there is no session, so `notifications/cancelled` interrupts nothing
|
|
188
|
+
- Every caller sees every tool of the endpoint ([guarantees.md](guarantees.md#scope)); authorization happens in each implementation's `authorize`.
|
|
189
|
+
- Cancellation is Effect's native RPC interruption. Over HTTP there is no session, so `notifications/cancelled` interrupts nothing. A tool call ends with its HTTP request, whose lifetime (for example, whether a client disconnect interrupts it) is the host's.
|
|
159
190
|
- Handlers may yield `McpSchema.McpRequestContext` for the client's declared information; the native server supplies it to every tool call, so it is never a router or host requirement.
|
|
160
|
-
-
|
|
191
|
+
- `runStdio` is the subprocess's whole program: it serves until the host closes stdin, then succeeds, so the process exits 0. Closing stdin interrupts every call in flight; `runStdio` succeeds once they have stopped, after any uninterruptible region has completed. An interrupted call gets no result: no answer, or a JSON-RPC error. MCP hosts close stdin to shut a server down; a script piping requests keeps stdin open until it has read every answer. A signal interrupts it, as any program. Provide `Stdio` and its services to it and run it, `NodeRuntime.runMain`.
|
|
192
|
+
- stdio is a local surface and takes no `authentication`: the host supplies `Stdio` (`NodeStdio.layer`) and any request-time services explicitly around `runStdio`, the identity of every protected tool included, `Effect.provideService(CurrentActor, actor)`. Each implementation's `authorize` runs for its protected tools. Tool arguments never establish identity.
|
|
193
|
+
- `runStdio` gives its program a `Console` that writes every method through the `error` of the console it runs with, to stderr, since stdout carries the protocol: every console logger, the default one included, `Console.log`, and the counters, timers and group labels Node's console prints on stdout. A counter or a timer prints its label and its count or the milliseconds since it started, a group prints its label without indenting what follows, and `clear` does nothing. Layers provided around `runStdio` run outside its program: apply `ActionCli.logToStderr` last, before `runMain`, as the example does, which moves their default logger to stderr and reports there what `runMain` would report on stdout, a failure of such a layer or a defect, with the exit code `runMain` gives ([ActionCli.md](ActionCli.md#rules)). It does not move their `Console` output or a logger writing through `Console.log`, such as `Logger.consoleJson`; log JSON with `Logger.withConsoleError(Logger.formatJson)` instead. Keep the global `console.log` and other direct writes off stdout.
|
|
161
194
|
- Each endpoint or subprocess owns a fresh native tool registry. That isolates tool names, not application context.
|
|
162
|
-
-
|
|
163
|
-
-
|
|
195
|
+
- Every tool declares the built-in errors ([guarantees.md](guarantees.md#wire-behavior)). An authorizer's refusal is an `isError` result whose text is its JSON, `{"_tag":"Forbidden","message":"Not allowed."}`, exactly like an action's own error.
|
|
196
|
+
- The authorizer runs after the native server decodes the tool's arguments.
|
|
197
|
+
- On an endpoint naming a Bearer descriptor, a protected tool's step-up refusal is an HTTP 401 or 403 instead of a tool result ([guarantees.md](guarantees.md#authorization)); a public tool's is its `isError` result, signed in or not; without one, or under a descriptor of another scheme, a tool's refusal is an `isError` tool result, and only the authentication's own refusal is a 401 or 403. On a `Forbidden`'s `insufficient_scope` challenge, an MCP client re-authorizes with those scopes and retries.
|
|
198
|
+
- That holds only while nothing of the response has been sent. Once a handler's notification, such as progress, has started a 200 event stream, a later refusal is the tool's `isError` result in it. An authorizer runs before its handler, so its refusal is always the status.
|
|
199
|
+
- Authentication refuses before the MCP handler too: an HTTP 401 or 403. An MCP client reads the 401's `WWW-Authenticate` challenge and finds its authorization server through the discovery `Authentication.layer` publishes for a protected resource ([Authentication.md](Authentication.md)).
|
|
164
200
|
|
|
165
201
|
## Failure modes
|
|
166
202
|
|
|
167
|
-
-
|
|
168
|
-
- `
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
203
|
+
- `MCP tool input must be one object with keys, such as a struct: <name>, ...` thrown by `layerHttp` or `runStdio`: those actions' input is a union, an array, a scalar, or an object without keys such as `Schema.Struct({})`. Wrap a union in a field, `input: { notification: Schema.Union([Email, Sms]) }`, omit `input` (or give `{}`) for no arguments, or leave the action out of `actions`.
|
|
204
|
+
- `Type 'CurrentActor' is not assignable to type 'never'` where the server is launched, with `CurrentActor` among the endpoint's startup requirements rather than its `Request<"Requires", ...>`: a feature reads a request service, which it never receives ([native features](#native-features)). Serve that content as an action.
|
|
205
|
+
- A native `McpServer.resource`, `McpServer.prompt` or `McpServer.toolkit` layer merged beside `layerHttp` builds without error and is never served: `resources/list` is empty, `prompts/list` is not found, and `tools/list` lists only the actions. Each endpoint's registry is its own: pass them as its `features`.
|
|
206
|
+
- An MCP client gets an `isError` refusal instead of the 401 or 403 it re-authorizes on: the handler sent a notification before refusing, so the response had already started. Refuse in the implementation's `authorize`, before the handler runs.
|
|
207
|
+
- `Duplicate MCP tool: <name>` thrown at the `layerHttp` or `runStdio` call: two implementations on one endpoint serve actions of the same name. Split the endpoint, or rename one action. `actions` tells apart only separate contracts sharing a name, listing one; two implementations of the same contract stay both listed: pass one implementation.
|
|
208
|
+
- `Duplicate MCP tool: <name>, claimed by an action and a native feature`, a defect when the endpoint builds: a native tool among its `features` has an action's name. Rename the native tool, or leave the action out of `actions`.
|
|
209
|
+
- `Listed in actions, but no implementation holds it: <names>` thrown at the `layerHttp` or `runStdio` call: those actions are not among the implementations' actions, by identity. `(another contract)` marks one whose name an implementation holds, as a second copy of the contracts module makes. Pass the implementation holding it, or list the contract it implements.
|
|
210
|
+
- `No overload matches this call` at `layerHttp`, ending `Type 'string' is not assignable to type 'never'` on its options: the endpoint serves a protected action and names no `authentication`, or one of another identity. Give its descriptor. `Protected action '<name>' requires its matching authentication descriptor` thrown by `layerHttp`: the same, from plain JavaScript.
|
|
211
|
+
- `Provider<CurrentActor, "example.Login">` among the endpoint's requirements, or `Type 'Provider<...>' is not assignable to type 'never'` where the server is launched: the descriptor's provider is not provided. `Layer.provide(authenticate)` on that `layerHttp` ([Authentication.md](Authentication.md#failure-modes)).
|
|
212
|
+
- `Type 'X' is not assignable to type 'never'` where the server is launched, or `Request<"Requires", X>` in the endpoint's type: a handler or an authorizer yields a request service that no middleware around the endpoint provides. Provide it with router middleware on that `layerHttp`; never an identity at startup. `X` the identity, on an endpoint serving public tools: a public handler reads it, which no public tool gets. Make that action protected.
|
|
213
|
+
- A public tool's call answers 401: it presented a credential that does not verify. Send a valid one, or none.
|
|
214
|
+
- A protected tool's call answers 401 to a signed-in host after a while: its token expired; the host refreshes it, or signs in again, on the challenge's `invalid_token`.
|
|
215
|
+
- A prompt or resource demands credentials: it is served on an endpoint naming a descriptor, where every feature request authenticates. Serve it on an endpoint without protected tools.
|
|
216
|
+
- A stdio program owes `CurrentActor`, `Type 'CurrentActor' is not assignable to type 'never'` at `runMain`: it serves a protected tool, whose caller the host supplies. Provide it around `runStdio`; stdio takes no `authentication`.
|
|
217
|
+
- Client reports a broken transport from a stdio subprocess: something wrote to stdout, such as `console.log`, or a layer provided around `runStdio` did, through the default logger without `Logger.LogToStderr` outermost, or through `Console` or a logger such as `Logger.consoleJson`, which `LogToStderr` does not move. Remove the write, apply `ActionCli.logToStderr` last, or log JSON with `Logger.withConsoleError(Logger.formatJson)`.
|
|
218
|
+
- A stdio host asking for 2025-03-26 or 2024-11-05 is offered 2025-11-25 in the `initialize` result, as MCP negotiates, and disconnects if it cannot speak it. Update the host.
|
|
219
|
+
- An MCP client cannot connect over HTTP: a request answers `400` with JSON-RPC error `-32020`. The client opens with `initialize`, as the 2025 revisions do, and HTTP serves only 2026-07-28. Pin the client to it (the official client: `versionNegotiation: { mode: { pin: "2026-07-28" } }`), or serve that host over stdio. Codex is such a client: it opens HTTP with `initialize` (measured on 0.159.0), so serve it over stdio.
|
|
173
220
|
- An Origin-bearing request reaches the native handler and gets an empty 403: its `Origin` is not in `allowedOrigins`. Add the exact origin only if the deployment trusts it.
|
|
174
|
-
- A disallowed Origin receives 401 instead:
|
|
221
|
+
- A disallowed Origin receives 401 instead: the endpoint's authentication rejected it before the native Origin check. A Host or Origin check that must run before authentication is global middleware ([Authentication.md](Authentication.md#rules)).
|
|
175
222
|
- Browser calls fail despite an allowed Origin: configure CORS outside authentication and the MCP handler (see the browser example above). The native allowlist alone neither handles preflight nor adds CORS response headers.
|
|
176
|
-
- `Object literal may only specify known properties, and 'protocols'`: the
|
|
177
|
-
- Layer build dies with `MCP tool '<name>' cannot send '<field>' as text: it is not a top-level property of its success`: the success's JSON Schema has no such top-level property, as for a union of structs. Make the success one struct.
|
|
178
|
-
- A refusal arrives as a generic internal-error result: the hook failed with an error this transport does not declare, which is a defect. Add its schema to `errors`.
|
|
223
|
+
- `Object literal may only specify known properties, and 'protocols'`: the revisions are fixed. Delete the option.
|
package/docs/ActionToolkit.md
CHANGED
|
@@ -1,71 +1,133 @@
|
|
|
1
1
|
# ActionToolkit
|
|
2
2
|
|
|
3
3
|
Implementations as Effect's native AI `Toolkit`, for programs that call tools in-process
|
|
4
|
-
with `LanguageModel` or by hand. No server,
|
|
4
|
+
with `LanguageModel` or by hand. No server. A tool takes and gives JSON, as a model does; code
|
|
5
|
+
of your own calls the implementations typed, with `Action.client` ([Action.md](Action.md#clients)).
|
|
5
6
|
|
|
6
7
|
## API
|
|
7
8
|
|
|
8
9
|
Import `@gjermundgaraba/effect-actions/ActionToolkit`.
|
|
9
10
|
|
|
10
|
-
| API
|
|
11
|
-
|
|
|
12
|
-
| `make(
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `Options`, `Binding` | Public configuration and binding types. |
|
|
11
|
+
| API | Purpose |
|
|
12
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `make(implementations, options?)` | Project an implementation or a list; returns `{ toolkit, layer }`. |
|
|
14
|
+
| `toolkit` | A native `Toolkit`: `toolkit.tools` holds the tool definitions by name, with typed schemas, MCP hints and approval. |
|
|
15
|
+
| `layer` | The handler layer: builds handlers and built authorizers in its scope; requires build-time services. |
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
| Option | Meaning |
|
|
18
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
19
|
+
| `actions` | The actions that are tools, among the implementations': `[GetUser, RenameUser]`. Defaults to every action of them. |
|
|
20
|
+
| `needsApproval` | `(call) =>` a boolean, or an `Effect` of one: the calls `LanguageModel` asks approval for instead of running them ([Approval](#approval)). |
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
Exported types: `Tools`, what `make` returns; `Options`, what it takes; `ToolCall<A>`, the call `needsApproval` receives, `A` the union of its actions, for a rule written apart from `make`.
|
|
23
|
+
|
|
24
|
+
A local surface: each implementation's `authorize` runs before the handlers of its protected
|
|
25
|
+
actions, and the caller provides the identity.
|
|
26
|
+
|
|
27
|
+
Each tool takes and gives the JSON encoding of the action's schemas, as a model speaks and
|
|
28
|
+
as an MCP tool does, combines the action's errors with the built-in `InvalidInput`,
|
|
29
|
+
`Unauthenticated` and `Forbidden` in a native union, and uses `failureMode: "return"`. Its request requirements are its handler's and its authorizer's, and a protected
|
|
30
|
+
action's identity, whether or not anything reads it; build failures and services belong to the
|
|
31
|
+
handler layer.
|
|
23
32
|
|
|
24
33
|
## Canonical
|
|
25
34
|
|
|
26
|
-
```ts
|
|
35
|
+
```ts example=toolkit-authorized.ts
|
|
27
36
|
import { NodeRuntime } from "@effect/platform-node";
|
|
28
37
|
import { Console, Effect, Layer, Stream } from "effect";
|
|
29
38
|
import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
|
|
30
|
-
import { actors,
|
|
31
|
-
import {
|
|
39
|
+
import { actors, CurrentActor } from "./authorization.js";
|
|
40
|
+
import { double, userActions } from "./handlers.js";
|
|
32
41
|
import { Users } from "./users.js";
|
|
33
42
|
|
|
34
|
-
// The
|
|
35
|
-
const
|
|
43
|
+
// The implementations' `authorize` runs for the in-process caller as for the servers.
|
|
44
|
+
const { toolkit, layer } = ActionToolkit.make([userActions, double]);
|
|
36
45
|
|
|
37
46
|
const program = Effect.gen(function* () {
|
|
38
|
-
const tools = yield*
|
|
39
|
-
const calls = yield* tools.handle("
|
|
47
|
+
const tools = yield* toolkit;
|
|
48
|
+
const calls = yield* tools.handle("getUser", { id: "1" }); // encoded arguments
|
|
40
49
|
const results = yield* Stream.runCollect(calls);
|
|
41
50
|
yield* Console.log(results);
|
|
42
51
|
}).pipe(
|
|
43
52
|
Effect.provideService(CurrentActor, actors.alice), // identity around the whole invocation
|
|
44
|
-
Effect.provide(
|
|
53
|
+
Effect.provide(layer.pipe(Layer.provide(Users.layerMemory))), // startup services only
|
|
45
54
|
);
|
|
46
55
|
|
|
47
56
|
program.pipe(NodeRuntime.runMain);
|
|
48
57
|
```
|
|
49
58
|
|
|
50
|
-
With a model: pass `
|
|
59
|
+
With a model: pass `toolkit` as `toolkit` to `LanguageModel.generateText` and provide `layer`.
|
|
60
|
+
Beside tools of your own, or provider-defined ones such as a web search, merge the toolkits
|
|
61
|
+
natively and provide every handler layer:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
LanguageModel.generateText({ prompt, toolkit: Toolkit.merge(toolkit, WebKit) }).pipe(
|
|
65
|
+
Effect.provide(Layer.mergeAll(layer, WebKitHandlers)),
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Approval
|
|
70
|
+
|
|
71
|
+
```ts example=toolkit-approval.ts
|
|
72
|
+
import { Effect, Option } from "effect";
|
|
73
|
+
import { LanguageModel } from "effect/ai";
|
|
74
|
+
import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
|
|
75
|
+
import { type Actor, CurrentActor } from "./authorization.js";
|
|
76
|
+
import { double, userActions } from "./handlers.js";
|
|
77
|
+
|
|
78
|
+
// A model's writes wait for their caller's approval, except a rename of the caller itself:
|
|
79
|
+
// `call.name` narrows `call.input` across both implementations. Without a caller, it asks.
|
|
80
|
+
export const { toolkit, layer } = ActionToolkit.make([userActions, double], {
|
|
81
|
+
needsApproval: (call) =>
|
|
82
|
+
!call.action.readOnly &&
|
|
83
|
+
Effect.map(
|
|
84
|
+
Effect.serviceOption(CurrentActor),
|
|
85
|
+
Option.match({
|
|
86
|
+
onNone: () => true,
|
|
87
|
+
onSome: ({ id }) => call.name !== "renameUser" || call.input.id !== id,
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// One turn: the caller is provided around it, for the approval, `authorize` and the handlers.
|
|
93
|
+
export const chat = (actor: Actor, prompt: string) =>
|
|
94
|
+
LanguageModel.generateText({ prompt, toolkit }).pipe(Effect.provideService(CurrentActor, actor));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A call that needs approval ends the turn with a `tool-approval-request` part instead of a
|
|
98
|
+
result; the host answers it with a native `tool-approval-response` prompt part in the next turn.
|
|
99
|
+
Every write, whoever calls: `needsApproval: (call) => !call.action.readOnly`. A
|
|
100
|
+
toolkit made for another agent or policy from the same implementations runs with this `layer`:
|
|
101
|
+
provide it once. So does one of fewer tools, such as an agent's
|
|
102
|
+
`ActionToolkit.make(userActions, { actions: [GetUser] }).toolkit`.
|
|
51
103
|
|
|
52
104
|
## Rules
|
|
53
105
|
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
- `tools.handle(name, encodedInput)` takes
|
|
57
|
-
- `
|
|
58
|
-
-
|
|
59
|
-
-
|
|
106
|
+
- Every action of the implementations passed becomes a tool, named after the action, with its `mcp` hints, unless `actions` lists the tools: then only those, each behind its implementation's authorizer and from its builder's one run, and an implementation holding none of them is not built. `make(implementations)` accepts one implementation or a list, such as `[userActions, double]`. The types take only actions of the implementations, and type `toolkit` and `layer`'s build services by the listed actions alone; `needsApproval`'s `call` is typed by every action of the implementations. Options whose `actions` may be absent, such as a value typed `ActionToolkit.Options<A>`, serve every action when it is absent and the listed ones when given, so their types owe what every action owes, and hold only the tools present either way ([guarantees.md](guarantees.md#names)).
|
|
107
|
+
- A result's `result` is the action's decoded success or declared failure, and its `encodedResult` the JSON a model reads, such as `"42"` for a `BigInt`. Declared failures are returned as native tool results (`failureMode: "return"`), not raised.
|
|
108
|
+
- `tools.handle(name, encodedInput)` takes JSON arguments, as a model sends them: an ISO string for a `Schema.Date`, `null` for an absent `Schema.optional` field. It returns an Effect producing a result stream. The handler starts while that stream is constructed, so request services must be provided around the entire `handle(...).pipe(Effect.flatMap(Stream.runCollect))`, not only around the stream.
|
|
109
|
+
- Builders, calls and identity follow the [dependency lifetimes](guarantees.md#dependency-lifetimes), and authorization the [authorization rules](guarantees.md#authorization): `layer` builds, a call releases what it acquired when it ends, whether a model's turn or `tools.handle` makes it, and identity is supplied at invocation, never when building the layer. A protected tool's call owes its identity in its type; one call without it anyway, as from plain JavaScript, fails with `Unauthenticated` before `authorize` runs. The innermost caller provided wins: a call made under a narrower identity runs as that one.
|
|
110
|
+
- A model loop in a route yields `toolkit` in its implementation's builder, `Action.implement(Chat, Effect.map(toolkit, (tools) => ({ prompt }) => ...), { authorize })`, and passes `tools` to `LanguageModel.generateText`; the route's layer takes the toolkit's `layer`, `ActionHttp.layer(ChatHttp, chat).pipe(Layer.provide(layer))`. That is one build, in the routes' layer graph, shared with every surface serving the same implementations. `Effect.provide(layer)` in the handler builds it for every call unless a surface of the graph serves the same implementations, and `HttpRouter.provideRequest(layer)` once, but in a layer graph of its own: a second build when another surface serves them and has not built them first.
|
|
111
|
+
- Each tool carries only its own handler's request requirements, plus its authorizer's, not those of sibling actions.
|
|
112
|
+
- `needsApproval` is one function over every call a model makes, before the call runs: `(call, context) =>`, where `call` is `{ name, action, input }`, the input decoded, and `context` is Effect's native approval context. `call` is typed by every action of the implementations, whatever `actions` lists, and checking `call.name` narrows `call.action` and `call.input` to that action's, across implementations too: `(call) => call.name === "erase" && call.input.id !== "draft"`. It gives a boolean, or an `Effect` of one that requires nothing: it runs in the caller's context, so it reads the caller with `Effect.serviceOption`. Default: no tool needs approval.
|
|
113
|
+
- A `needsApproval` Effect that fails means no approval is needed, as `LanguageModel` decides natively. Its `Effect` cannot fail in its type: decide in `needsApproval` what a failure means, `Effect.orElseSucceed(() => true)` to ask when a policy cannot be read.
|
|
114
|
+
- Tools belong to what runs them, their implementation's handlers behind its authorizer: the `layer` of a `make` call serves the tools it selected in any `toolkit` of the same implementations, a toolkit of fewer `actions` included, so toolkits made per agent or per approval policy run with one handler layer, built once. Two implementations never run each other's handlers, even with tools of one name, such as two implementations of one action behind different authorizers, whatever order their layers are provided in.
|
|
115
|
+
- `toolkit` is a native `Toolkit`: `Toolkit.merge` combines it with other tools, and `yield* toolkit` gives the handled toolkit once the handler layers of all its tools are provided.
|
|
116
|
+
- `LanguageModel` enforces approval; `tools.handle` ignores it, like any caller that is not a model's turn. It is not authorization, which stays the implementation's `authorize`. MCP has no such field, so `ActionMcp` takes no such option.
|
|
60
117
|
- This is not an MCP server. Use `ActionMcp` to expose the same actions to external clients.
|
|
61
|
-
-
|
|
62
|
-
- `before` runs after successful argument decoding and before the selected handler, with its action contract. Invalid arguments skip both. Its services join each tool's requirements, supplied by the caller.
|
|
118
|
+
- Every tool declares the built-in errors ([guarantees.md](guarantees.md#wire-behavior)), so an authorizer's refusal, or a handler's built-in failure, is an ordinary returned tool failure.
|
|
63
119
|
|
|
64
120
|
## Failure modes
|
|
65
121
|
|
|
66
|
-
- Tool missing from the toolkit:
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
- Type error on `
|
|
122
|
+
- Tool missing from the toolkit: its implementation was not passed to `make`, or `actions` leaves it out.
|
|
123
|
+
- `Listed in actions, but no implementation holds it: <names>` thrown at `make`: those actions are not among the implementations' actions, by identity. `(another contract)` marks one whose name an implementation holds, as a second copy of the contracts module makes. Pass the implementation holding it, or list the contract it implements.
|
|
124
|
+
- A defect when a tool handles a call: no `layer` of its implementation is provided, only one of another implementation with tools of the same names, such as another implementation of the same actions, which the types accept. Provide a `layer` made from the implementations the `toolkit` was made from.
|
|
125
|
+
- A job or an agent beside `HttpRouter.serve`, or a model loop in a route given `layer` with `HttpRouter.provideRequest`, reads other state than the routes, or its builder runs twice: its `layer` builds in another layer graph than the routes. Put it inside the layer the server serves, a route's toolkit in its builder ([Rules](#rules)), or provide `Action.layer` and the services they share above both ([dependency lifetimes](guarantees.md#dependency-lifetimes)).
|
|
126
|
+
- Every toolkit of an implementation answering with one set of startup services: two layers of it, built apart with different services, were provided together, and one of them serves its tools. Use separate implementations for separate services.
|
|
127
|
+
- Type error on `needsApproval`'s `Effect`, which requires a service such as the caller: `needsApproval` requires nothing. Read the caller with `Effect.serviceOption(CurrentActor)`, which gives `Option.none()` without one.
|
|
128
|
+
- `Property '<field>' does not exist` on `call.input`: `call` is a union over every action of the toolkit, and another action's input lacks the field. Check `call.name` first.
|
|
129
|
+
- `Duplicate tool: <name>` thrown at `make`: two implementations serve actions of the same name. Rename one action. `actions` tells apart only separate contracts sharing a name, listing one; two implementations of the same contract stay both listed: pass one implementation.
|
|
130
|
+
- `Service not found` for a request tag at call time: it was provided only to the stream. Provide it around the whole call effect. Providing a service to `layer` does not fail: that startup value fills in for every call that lacks its own ([guarantees.md](guarantees.md#dependency-lifetimes)), so never provide an identity there; a protected tool's call still owes it in its type.
|
|
131
|
+
- `Type 'CurrentActor' is not assignable to type 'never'` where a program calling a tool runs: the tool's action is protected, and its call owes the caller, whatever the handler reads. Provide it around the call, or around the turn.
|
|
132
|
+
- A returned failure whose `result` is an `AiError` with reason `ToolParameterValidationError` (`failureOrigin: "parameters"`), not a declared error: the arguments did not decode. Authorization and the handler did not run.
|
|
133
|
+
- Type error on `layer` requirements: a build-time service is missing. Provide its Layer before `layer`.
|