@gjermundgaraba/effect-actions 0.12.0 → 0.13.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 +4 -4
- package/dist/Action.d.mts +3 -1
- package/dist/ActionToolkit.d.mts +23 -1
- package/dist/ActionToolkit.mjs +28 -1
- package/dist/Testing.d.mts +3 -1
- package/dist/Testing.mjs +2 -2
- package/dist/mcp/protocol.d.mts +2 -1
- package/docs/Action.md +11 -6
- package/docs/ActionCli-failures.md +2 -0
- package/docs/ActionCli.md +7 -5
- package/docs/ActionHttp-failures.md +2 -2
- package/docs/ActionHttp.md +6 -3
- package/docs/ActionMcp.md +20 -20
- package/docs/ActionRpc.md +3 -2
- package/docs/ActionToolkit.md +15 -12
- package/docs/Authentication.md +3 -2
- package/docs/CONTEXT.md +3 -2
- package/docs/README.md +9 -5
- package/docs/Testing-failures.md +2 -1
- package/docs/Testing.md +10 -3
- package/docs/setup-failures.md +5 -2
- package/docs/setup.md +9 -7
- package/examples/kitchen-sink/README.md +5 -4
- package/examples/kitchen-sink/src/assistant/handlers.ts +18 -38
- package/examples/kitchen-sink/src/issues/Issues.ts +4 -2
- package/examples/kitchen-sink/src/issues/write-limit.ts +2 -1
- package/examples/kitchen-sink/src/server.ts +3 -3
- package/examples/live-rpc/README.md +1 -1
- package/examples/live-rpc/src/chat/Chat.ts +3 -1
- package/examples/live-rpc/src/chat/handlers.ts +2 -2
- package/examples/live-rpc/src/chat/store.ts +1 -1
- package/examples/live-rpc/src/server.ts +3 -3
- package/examples/live-rpc/test/chat/chat.test.ts +1 -1
- package/examples/mcp-tools/README.md +9 -4
- package/examples/mcp-tools/src/printers/Printers.ts +6 -4
- package/examples/mcp-tools/src/server.ts +3 -2
- package/examples/mcp-tools/test/routes.test.ts +2 -2
- package/examples/notes-service/README.md +6 -3
- package/examples/notes-service/src/commands.ts +50 -2
- package/examples/notes-service/src/notes/Notes.ts +32 -4
- package/examples/notes-service/src/notes/handlers.ts +8 -2
- package/examples/notes-service/src/notes/store.ts +11 -0
- package/examples/notes-service/src/server.ts +3 -3
- package/examples/notes-service/test/commands.test.ts +156 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ native Toolkit, and run it from a CLI. Same schemas, same handler, Effect's own
|
|
|
5
5
|
underneath.
|
|
6
6
|
|
|
7
7
|
**If you are a coding agent**, start at [docs/README.md](docs/README.md), not here, and read
|
|
8
|
-
one page per tool call
|
|
9
|
-
action and limiting request bodies included, to its page and its example.
|
|
8
|
+
one page per tool call, again alone if its output says it was truncated. It routes every task,
|
|
9
|
+
protecting an action and limiting request bodies included, to its page and its example.
|
|
10
10
|
|
|
11
11
|
## Looks like this
|
|
12
12
|
|
|
@@ -100,11 +100,11 @@ const greeting = Effect.gen(function* () {
|
|
|
100
100
|
## Install
|
|
101
101
|
|
|
102
102
|
```sh
|
|
103
|
-
pnpm add @gjermundgaraba/effect-actions effect@~4.0.
|
|
103
|
+
pnpm add @gjermundgaraba/effect-actions effect@~4.0.3
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
It needs TypeScript 7 or newer and is ESM only
|
|
107
|
-
([docs/setup.md](docs/setup.md#install)). Add `@effect/platform-node@~4.0.
|
|
107
|
+
([docs/setup.md](docs/setup.md#install)). Add `@effect/platform-node@~4.0.3` to serve or run a CLI from Node. Every module is a
|
|
108
108
|
subpath import (`.../Action`, `.../ActionHttp`, ...); there is no package root, so a
|
|
109
109
|
contracts-only bundle never loads a server.
|
|
110
110
|
|
package/dist/Action.d.mts
CHANGED
|
@@ -24,7 +24,9 @@ export interface Mcp {
|
|
|
24
24
|
readonly title?: string | undefined;
|
|
25
25
|
/**
|
|
26
26
|
* `annotations.destructiveHint`, meaningful for a write only. Left out, a write is listed as
|
|
27
|
-
* destructive, MCP's default: state `false` for one that only adds
|
|
27
|
+
* destructive, MCP's default: state `false` for one that only adds, MCP's "only additive
|
|
28
|
+
* updates". One that replaces, removes or interrupts, such as an edit or a restart, stays
|
|
29
|
+
* destructive.
|
|
28
30
|
*/
|
|
29
31
|
readonly destructiveHint?: boolean | undefined;
|
|
30
32
|
/** `annotations.idempotentHint`, meaningful for a write only. */
|
package/dist/ActionToolkit.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { BuiltIns } from "./contract/errors.mjs";
|
|
|
2
2
|
import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Offered, OptionalUnless, RequestOf, SelectedOf, Served, Serving } from "./contract/implementation.mjs";
|
|
3
3
|
import { Action_d_exports, Any } from "./Action.mjs";
|
|
4
4
|
import { Effect, Layer, Schema } from "effect";
|
|
5
|
-
import { Tool, Toolkit } from "effect/ai";
|
|
5
|
+
import { Prompt, Tool, Toolkit } from "effect/ai";
|
|
6
6
|
//#region src/toolkit/ActionToolkit.d.ts
|
|
7
7
|
type NativeTool<A extends Any, R> = Tool.Tool<A["name"], {
|
|
8
8
|
readonly parameters: Schema.toCodecJson<A["input"]>;
|
|
@@ -63,4 +63,26 @@ export interface Options<A extends Any> {
|
|
|
63
63
|
* `examples/kitchen-sink/src/assistant/`.
|
|
64
64
|
*/
|
|
65
65
|
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>>>;
|
|
66
|
+
/** A tool call a model asked to have approved, which no answer has resolved yet. */
|
|
67
|
+
export interface Approval {
|
|
68
|
+
/** What a `tool-approval-response` names to answer it. */
|
|
69
|
+
readonly approvalId: string;
|
|
70
|
+
readonly toolCallId: string;
|
|
71
|
+
/** The tool's name, the action's. */
|
|
72
|
+
readonly name: string;
|
|
73
|
+
/** The call's arguments as the model sent them: show them as JSON. */
|
|
74
|
+
readonly params: unknown;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The approval requests still open in a conversation's history, such as a `Chat`'s, in the
|
|
78
|
+
* order they were asked: those with neither an answer nor a tool result. An answer recorded
|
|
79
|
+
* without a result, as a `streamText` turn that failed while its call ran leaves it, is not
|
|
80
|
+
* open: the next turn acts on it unasked, and another answer would run the call twice. A turn's
|
|
81
|
+
* response lists only the requests that turn made, and the answering turn's none, so read every
|
|
82
|
+
* open one here. Each names the call an answer would run, found as Effect finds it: the last
|
|
83
|
+
* request with its `approvalId`, listed where it was asked, and the last call with its id.
|
|
84
|
+
* Guide: `docs/ActionToolkit.md` in this package; in an application:
|
|
85
|
+
* `examples/kitchen-sink/src/assistant/handlers.ts`.
|
|
86
|
+
*/
|
|
87
|
+
export declare const openApprovals: ({ content: messages }: Prompt.Prompt) => ReadonlyArray<Approval>;
|
|
66
88
|
//#endregion
|
package/dist/ActionToolkit.mjs
CHANGED
|
@@ -17,5 +17,32 @@ function make(apps, options) {
|
|
|
17
17
|
layer: layer.pipe(provideHandlers(served))
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* The approval requests still open in a conversation's history, such as a `Chat`'s, in the
|
|
22
|
+
* order they were asked: those with neither an answer nor a tool result. An answer recorded
|
|
23
|
+
* without a result, as a `streamText` turn that failed while its call ran leaves it, is not
|
|
24
|
+
* open: the next turn acts on it unasked, and another answer would run the call twice. A turn's
|
|
25
|
+
* response lists only the requests that turn made, and the answering turn's none, so read every
|
|
26
|
+
* open one here. Each names the call an answer would run, found as Effect finds it: the last
|
|
27
|
+
* request with its `approvalId`, listed where it was asked, and the last call with its id.
|
|
28
|
+
* Guide: `docs/ActionToolkit.md` in this package; in an application:
|
|
29
|
+
* `examples/kitchen-sink/src/assistant/handlers.ts`.
|
|
30
|
+
*/
|
|
31
|
+
const openApprovals = ({ content: messages }) => {
|
|
32
|
+
const said = messages.flatMap((message) => message.role === "assistant" ? message.content : []);
|
|
33
|
+
const told = messages.flatMap((message) => message.role === "tool" ? message.content : []);
|
|
34
|
+
const answered = new Set(told.flatMap((part) => part.type === "tool-approval-response" ? [part.approvalId] : []));
|
|
35
|
+
const ran = new Set(told.flatMap((part) => part.type === "tool-result" ? [part.id] : []));
|
|
36
|
+
const calls = new Map(said.flatMap((part) => part.type === "tool-call" ? [[part.id, part]] : []));
|
|
37
|
+
return said.filter((part) => part.type === "tool-approval-request").filter(({ approvalId }, place, all) => all.findLastIndex((later) => later.approvalId === approvalId) === place).flatMap(({ approvalId, toolCallId }) => {
|
|
38
|
+
const call = calls.get(toolCallId);
|
|
39
|
+
return call === void 0 || answered.has(approvalId) || ran.has(toolCallId) ? [] : [{
|
|
40
|
+
approvalId,
|
|
41
|
+
toolCallId,
|
|
42
|
+
name: call.name,
|
|
43
|
+
params: call.params
|
|
44
|
+
}];
|
|
45
|
+
});
|
|
46
|
+
};
|
|
20
47
|
//#endregion
|
|
21
|
-
export { make };
|
|
48
|
+
export { make, openApprovals };
|
package/dist/Testing.d.mts
CHANGED
|
@@ -21,7 +21,8 @@ export declare function layer(handler: (request: Request) => Promise<Response>):
|
|
|
21
21
|
* it to `ActionHttp.client` and to `mcpClient`. The routes are built with this layer and
|
|
22
22
|
* released with its scope, without request logs: only the cause of a defect, of a result that
|
|
23
23
|
* does not encode or of a failure a route of your own leaves unhandled is logged, as an error,
|
|
24
|
-
* where a server's request logger would show it
|
|
24
|
+
* where a server's request logger would show it, on the process console even under `it.effect`
|
|
25
|
+
* and its `TestConsole`. What they still require is this layer's, as
|
|
25
26
|
* under `HttpRouter.serve`: a builder's services, and a per-request service no middleware of
|
|
26
27
|
* theirs provides, including one a global middleware reads. The routes keep their real
|
|
27
28
|
* authentication: a client sends its caller's credential. Provided around it, the test
|
|
@@ -32,6 +33,7 @@ export declare function layer(handler: (request: Request) => Promise<Response>):
|
|
|
32
33
|
* The client is the layer's own: the program's other HTTP clients get none of its requests,
|
|
33
34
|
* and it none of theirs.
|
|
34
35
|
* Guide: `docs/Testing.md` in this package; in an application: `examples/notes-service/test/`.
|
|
36
|
+
* A type error at `it.effect`, such as one naming `Scope`: `docs/Testing-failures.md`.
|
|
35
37
|
*/
|
|
36
38
|
export declare function layer<A, E, R>(routes: Layer.Layer<A, E, R>): Layer.Layer<HttpClient.HttpClient, E, Exclude<HttpRouter.Request.Without<R> | HttpRouter.Request.Only<"Requires", R> | HttpRouter.Request.Only<"GlobalRequires", R>, Served>>;
|
|
37
39
|
/** Where `mcpClient` sends, and through what client. */
|
package/dist/Testing.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { assertOnce, projectedErrors } from "./contract/rules.mjs";
|
|
|
3
3
|
import { checked } from "./contract/call.mjs";
|
|
4
4
|
import { defaultPath, liftOf, statelessRequest } from "./mcp/protocol.mjs";
|
|
5
5
|
import { clientOf } from "./testing/memory.mjs";
|
|
6
|
-
import { Cause, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path, Predicate, Schema, identity } from "effect";
|
|
6
|
+
import { Cause, Console, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path, Predicate, Schema, identity } from "effect";
|
|
7
7
|
import { McpSchema } from "effect/ai";
|
|
8
8
|
import { Etag, HttpClient, HttpClientRequest, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
|
|
9
9
|
import { Base64, Sse } from "effect/encoding";
|
|
@@ -15,7 +15,7 @@ function layer(routes) {
|
|
|
15
15
|
const platform = yield* Layer.build(Layer.fresh(Layer.mergeAll(HttpPlatform.layer, Path.layer, Etag.layerWeak).pipe(Layer.provideMerge(Layer.succeed(FileSystem.FileSystem, fileSystem)))));
|
|
16
16
|
const context = Context.merge(platform, yield* Effect.context());
|
|
17
17
|
const app = yield* HttpRouter.toHttpEffect(routes).pipe(Effect.provideContext(context));
|
|
18
|
-
return clientOf(HttpEffect.toWebHandlerWith(context)(Effect.tapCause(app, (cause) => isReported(cause) ? Effect.logError(cause) : Effect.void)));
|
|
18
|
+
return clientOf(HttpEffect.toWebHandlerWith(context)(Effect.tapCause(app, (cause) => isReported(cause) ? Effect.logError(cause).pipe(Effect.provideService(Console.Console, globalThis.console)) : Effect.void)));
|
|
19
19
|
}));
|
|
20
20
|
}
|
|
21
21
|
const isReported = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) && !ErrorReporter.isIgnored(reason.error) || Cause.isDieReason(reason) && !ErrorReporter.isIgnored(reason.defect));
|
package/dist/mcp/protocol.d.mts
CHANGED
|
@@ -6,7 +6,8 @@ import { McpProtocol } from "effect/ai";
|
|
|
6
6
|
//#region src/mcp/protocol.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* An MCP request's parameters, as `Testing.mcpRequest` takes them: JSON, with any `_meta`, leaving
|
|
9
|
-
* out an `undefined` one, as a caller may.
|
|
9
|
+
* out an `undefined` one, as a caller may. A helper passing on whole params types them as this,
|
|
10
|
+
* and a tool's `arguments` as `Schema.Json`, never `unknown`.
|
|
10
11
|
*/
|
|
11
12
|
export interface Params {
|
|
12
13
|
readonly _meta?: {
|
package/docs/Action.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
`Action.make` defines an action, and `Action.implement` binds handlers and an authorizer to
|
|
4
4
|
contracts: every surface takes the implementation it returns, or a list of them.
|
|
5
5
|
|
|
6
|
-
Read this page alone, in one tool call
|
|
7
|
-
search [Action-failures.md](Action-failures.md). Reading order:
|
|
6
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
7
|
+
When something fails, search [Action-failures.md](Action-failures.md). Reading order:
|
|
8
|
+
[README.md](README.md).
|
|
8
9
|
|
|
9
10
|
## API
|
|
10
11
|
|
|
@@ -121,8 +122,9 @@ and a public action in [notes-service/src/status/Status.ts](../examples/notes-se
|
|
|
121
122
|
## Implement actions
|
|
122
123
|
|
|
123
124
|
`Action.implement` binds a handler to one action, or a record of handlers keyed by name to a
|
|
124
|
-
list. Either may instead be a builder: an Effect that returns the handler
|
|
125
|
-
is how a handler gets a service
|
|
125
|
+
list. Either may instead be a builder: an Effect, run once at startup, that returns the handler
|
|
126
|
+
or the record. It is how a handler gets a startup service; what differs per call, the caller
|
|
127
|
+
first, the handler reads itself.
|
|
126
128
|
|
|
127
129
|
```ts
|
|
128
130
|
export const notes = Action.implement(
|
|
@@ -183,7 +185,9 @@ export const authorize = Effect.fn("authorize")(function* (action: Action.Any) {
|
|
|
183
185
|
[notes-service/src/identity/authorize.ts](../examples/notes-service/src/identity/authorize.ts)
|
|
184
186
|
is that rule with `scopes: [scope]` added, the OAuth variant: it publishes discovery.
|
|
185
187
|
Derive the rule from `action.readOnly`, never from an `mcp` hint: changing an action's
|
|
186
|
-
`readOnly` then changes who may call it.
|
|
188
|
+
`readOnly` then changes who may call it. Actions under another rule go in another
|
|
189
|
+
`Action.implement` with its own `authorize`, or `Action.allowAll`, not a branch on
|
|
190
|
+
`action.name`.
|
|
187
191
|
|
|
188
192
|
- **A call runs authentication, then over HTTP and RPC the layer's `middleware`, input
|
|
189
193
|
decoding, the authorizer, then the handler**, each refusing before the next. So a caller the
|
|
@@ -202,7 +206,8 @@ Derive the rule from `action.readOnly`, never from an `mcp` hint: changing an ac
|
|
|
202
206
|
is built once for each implementation stating it: keep state they share in a service it
|
|
203
207
|
yields.
|
|
204
208
|
- **A limit, such as a rate limit, is the handler's, not the authorizer's.** It fails with an
|
|
205
|
-
error the action declares, and its limiter is a service the builder yields
|
|
209
|
+
error the action declares, and its limiter is a service the builder yields, apart from the
|
|
210
|
+
store
|
|
206
211
|
([kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts)):
|
|
207
212
|
a service is shared wherever it is provided
|
|
208
213
|
([kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts)), state kept in a
|
|
@@ -100,3 +100,5 @@ The tasks are in [ActionCli.md](ActionCli.md).
|
|
|
100
100
|
with `Command.make`: a command of your own that calls `Action.client` or `ActionHttp.client`
|
|
101
101
|
fails with the action's own error. Pipe the command's whole Effect through
|
|
102
102
|
`ActionCli.toUserError`, given the binding or the actions it calls, or derive the command with `ActionCli.command`.
|
|
103
|
+
- `Argument of type 'string | undefined' is not assignable` at a `UserError`'s `userMessage`: it
|
|
104
|
+
is optional on the type. Read it with `?? ""`, or assert it with `toHaveProperty`.
|
package/docs/ActionCli.md
CHANGED
|
@@ -8,8 +8,9 @@ service you already serve. `make` and `remote` derive a command for every action
|
|
|
8
8
|
implementations or the binding, so a new action is a new command: list `actions` to leave some
|
|
9
9
|
out.
|
|
10
10
|
|
|
11
|
-
Read this page alone, in one tool call
|
|
12
|
-
search [ActionCli-failures.md](ActionCli-failures.md). Reading order:
|
|
11
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
12
|
+
When something fails, search [ActionCli-failures.md](ActionCli-failures.md). Reading order:
|
|
13
|
+
[README.md](README.md).
|
|
13
14
|
|
|
14
15
|
## API
|
|
15
16
|
|
|
@@ -147,9 +148,10 @@ In an aggregate, a command's options go under `commands`, keyed by the action's
|
|
|
147
148
|
### Add a command of your own
|
|
148
149
|
|
|
149
150
|
Work every surface should offer, a bulk import for one, is an action first: a command of your
|
|
150
|
-
own then only reads the file and calls it
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
own then only reads the file and calls it, as `notes import <file>` reads `Argument.FileText`
|
|
152
|
+
and calls `importNotes`. For a syntax the options cannot express, such as a flag named apart
|
|
153
|
+
from its field, build the command with native `Command.make`, `Flag` and `Argument`, and pass it
|
|
154
|
+
in `subcommands`: `notes export <file>` takes `Argument.String("file")`
|
|
153
155
|
([notes-service/src/commands.ts](../examples/notes-service/src/commands.ts)):
|
|
154
156
|
|
|
155
157
|
```ts
|
|
@@ -68,8 +68,8 @@ The tasks are in [ActionHttp.md](ActionHttp.md).
|
|
|
68
68
|
|
|
69
69
|
## Client methods
|
|
70
70
|
|
|
71
|
-
- `HttpClientError` whose `reason._tag` is `DecodeError`,
|
|
72
|
-
declares that status. Declare the error on the binding when middleware answers with it, or on
|
|
71
|
+
- `HttpClientError` whose `reason._tag` is `DecodeError`, printed `Decode error (<status> …)`,
|
|
72
|
+
for a status such as 429: no schema declares that status. For a 500, see the empty 500 above. Declare the error on the binding when middleware answers with it, or on
|
|
73
73
|
the action when its handler does.
|
|
74
74
|
- `HttpClientError` whose `reason._tag` is `InvalidUrlError` outside a browser: relative routes
|
|
75
75
|
have no page to resolve against. Set `baseUrl`.
|
package/docs/ActionHttp.md
CHANGED
|
@@ -4,8 +4,9 @@ JSON `POST` routes on Effect's `HttpApi`, one per action. `ActionHttp.make` bind
|
|
|
4
4
|
mount path; the server mounts that one binding, every client calls it and the OpenAPI document is
|
|
5
5
|
generated from it.
|
|
6
6
|
|
|
7
|
-
Read this page alone, in one tool call
|
|
8
|
-
search [ActionHttp-failures.md](ActionHttp-failures.md). Reading order:
|
|
7
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
8
|
+
When something fails, search [ActionHttp-failures.md](ActionHttp-failures.md). Reading order:
|
|
9
|
+
[README.md](README.md).
|
|
9
10
|
|
|
10
11
|
## API
|
|
11
12
|
|
|
@@ -89,7 +90,9 @@ Files: [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts) and
|
|
|
89
90
|
[notes-service/src/server.ts](../examples/notes-service/src/server.ts).
|
|
90
91
|
|
|
91
92
|
- Pass every surface the same list of implementations: the layer serves the binding's actions
|
|
92
|
-
among them.
|
|
93
|
+
among them. Beside rpcs, one provider and one header decoding serve both
|
|
94
|
+
([ActionRpc.md](ActionRpc.md#serve-the-rpcs), "The same actions as JSON too";
|
|
95
|
+
[kitchen-sink/src/server.ts](../examples/kitchen-sink/src/server.ts)).
|
|
93
96
|
- A service a handler or an authorizer reads per request stays owed by the layer until router
|
|
94
97
|
middleware around it, `HttpRouter.provideRequest` or the layer's own `middleware` provides it.
|
|
95
98
|
Router middleware is the one that reads the request, such as a header:
|
package/docs/ActionMcp.md
CHANGED
|
@@ -5,19 +5,20 @@ after it. `layerHttp` mounts a Streamable HTTP endpoint, for clients that connec
|
|
|
5
5
|
`runStdio` is the whole program of a subprocess a local agent starts. For a model your own code
|
|
6
6
|
calls in process, use [ActionToolkit.md](ActionToolkit.md).
|
|
7
7
|
|
|
8
|
-
Read this page alone, in one tool call
|
|
9
|
-
search [ActionMcp-failures.md](ActionMcp-failures.md). Reading order:
|
|
8
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
9
|
+
When something fails, search [ActionMcp-failures.md](ActionMcp-failures.md). Reading order:
|
|
10
|
+
[README.md](README.md).
|
|
10
11
|
|
|
11
12
|
## API
|
|
12
13
|
|
|
13
14
|
Import `@gjermundgaraba/effect-actions/ActionMcp`.
|
|
14
15
|
|
|
15
|
-
| API | Purpose
|
|
16
|
-
| ------------------------------------- |
|
|
17
|
-
| `layerHttp(implementations, options)` | A Layer serving the actions as tools at one HTTP endpoint.
|
|
18
|
-
| `runStdio(implementations, options)` | An Effect serving them on standard I/O until stdin closes.
|
|
19
|
-
| `Options` | Type of `runStdio`'s options.
|
|
20
|
-
| `LayerHttpOptions` | Type of `layerHttp`'s options: `Options` and
|
|
16
|
+
| API | Purpose |
|
|
17
|
+
| ------------------------------------- | ----------------------------------------------------------- |
|
|
18
|
+
| `layerHttp(implementations, options)` | A Layer serving the actions as tools at one HTTP endpoint. |
|
|
19
|
+
| `runStdio(implementations, options)` | An Effect serving them on standard I/O until stdin closes. |
|
|
20
|
+
| `Options` | Type of `runStdio`'s options. |
|
|
21
|
+
| `LayerHttpOptions` | Type of `layerHttp`'s options: `Options` and four for HTTP. |
|
|
21
22
|
|
|
22
23
|
`implementations` is one implementation or a list.
|
|
23
24
|
|
|
@@ -31,6 +32,7 @@ Import `@gjermundgaraba/effect-actions/ActionMcp`.
|
|
|
31
32
|
| `features` | One Layer of native resources, prompts and tools to serve. |
|
|
32
33
|
| `path` | HTTP only: the endpoint's path; defaults to `/mcp`. |
|
|
33
34
|
| `allowedOrigins` | HTTP only: the exact origins allowed; none by default. |
|
|
35
|
+
| `allowSubscriptions` | HTTP only: `false` turns off 2026-07-28 subscriptions. |
|
|
34
36
|
| `authentication` | HTTP only: the protected tools' authentication descriptor. |
|
|
35
37
|
|
|
36
38
|
All but `actions`, `features` and `authentication` are Effect's own `McpServer` options, passed
|
|
@@ -48,16 +50,16 @@ export const routes = Layer.mergeAll(
|
|
|
48
50
|
);
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
See [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts) and
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
servers, see [Action-in-process.md](Action-in-process.md#share-one-build-across-servers).
|
|
53
|
+
See [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts) and `server.ts` beside it.
|
|
54
|
+
Surfaces in one set of routes run each builder once, so a note written over HTTP is read as a
|
|
55
|
+
tool: [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts). For two servers,
|
|
56
|
+
see [Action-in-process.md](Action-in-process.md#share-one-build-across-servers).
|
|
56
57
|
|
|
57
58
|
- The endpoint speaks MCP 2026-07-28, 2025-11-25 and 2025-06-18, as the client negotiates.
|
|
58
|
-
Claude Code 2.1 and Codex CLI 0.162
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
Claude Code 2.1 and Codex CLI 0.162 connect (October 2026):
|
|
60
|
+
`claude mcp add --transport http <name> <url>/mcp --header "Authorization: Bearer <token>"`,
|
|
61
|
+
`codex mcp add <name> --url <url>/mcp --bearer-token-env-var <VAR>`;
|
|
62
|
+
[mcp-tools](../examples/mcp-tools/README.md#connect-a-client) has stdio and Claude Desktop.
|
|
61
63
|
- **A client of a 2025 revision opens a session** with `initialize`; a 2026-07-28 request stands
|
|
62
64
|
alone. A session holds what its client sent there and no identity: each request authenticates,
|
|
63
65
|
as its own caller. It lasts until its client ends it with a `DELETE`, answered 204, or the process stops, as
|
|
@@ -109,9 +111,7 @@ curl http://127.0.0.1:3000/mcp -d "$BODY" -H 'Content-Type: application/json' \
|
|
|
109
111
|
- **No token arrives, so the host names the caller.** stdio takes no `authentication`: provide the
|
|
110
112
|
protected tools' identity around `runStdio`, whose one caller is whoever started it. Each
|
|
111
113
|
implementation's `authorize` still runs, so give that caller what it checks. Tools served
|
|
112
|
-
behind a token over HTTP keep their one protected contract here: never a public copy.
|
|
113
|
-
server has many callers: never provide an identity at its startup, where it would stand for
|
|
114
|
-
every request.
|
|
114
|
+
behind a token over HTTP keep their one protected contract here: never a public copy.
|
|
115
115
|
- **Test the tools without a process**: the implementations in process over a fake upstream
|
|
116
116
|
([mcp-tools/test/printers/printers.test.ts](../examples/mcp-tools/test/printers/printers.test.ts)),
|
|
117
117
|
an endpoint in memory
|
|
@@ -126,7 +126,7 @@ tool's read-only hint, and the contract's `mcp` option holds its `title`, its ot
|
|
|
126
126
|
[mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts) and
|
|
127
127
|
[mcp-tools/src/instructions.ts](../examples/mcp-tools/src/instructions.ts).
|
|
128
128
|
|
|
129
|
-
- MCP takes a write as destructive unless it states `destructiveHint: false
|
|
129
|
+
- MCP takes a write as destructive unless it states `destructiveHint: false`: one that only adds.
|
|
130
130
|
- A tool's input is one object with keys. Omit `input` for a tool without arguments, and wrap a
|
|
131
131
|
union, an array or a scalar in a field.
|
|
132
132
|
- An argument the schema refuses, an undeclared one included, is answered by the native server
|
package/docs/ActionRpc.md
CHANGED
|
@@ -4,8 +4,9 @@ Actions as Effect RPC, on Effect's own `RpcServer` and `RpcClient`: one unary rp
|
|
|
4
4
|
Reach for it when a client keeps a connection open and calls often, such as a page on a
|
|
5
5
|
WebSocket, or when the application already speaks Effect RPC.
|
|
6
6
|
|
|
7
|
-
Read this page alone, in one tool call
|
|
8
|
-
search [ActionRpc-failures.md](ActionRpc-failures.md). Reading order:
|
|
7
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
8
|
+
When something fails, search [ActionRpc-failures.md](ActionRpc-failures.md). Reading order:
|
|
9
|
+
[README.md](README.md).
|
|
9
10
|
|
|
10
11
|
## API
|
|
11
12
|
|
package/docs/ActionToolkit.md
CHANGED
|
@@ -6,25 +6,27 @@ calls your actions. Nothing authenticates: the host names the caller. Code of yo
|
|
|
6
6
|
the same implementations typed, with `Action.client`
|
|
7
7
|
([Action-in-process.md](Action-in-process.md#call-actions-in-process)).
|
|
8
8
|
|
|
9
|
-
Read this page alone, in one tool call
|
|
10
|
-
search [ActionToolkit-failures.md](ActionToolkit-failures.md). Reading order:
|
|
9
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
10
|
+
When something fails, search [ActionToolkit-failures.md](ActionToolkit-failures.md). Reading order:
|
|
11
|
+
[README.md](README.md).
|
|
11
12
|
|
|
12
13
|
## API
|
|
13
14
|
|
|
14
15
|
Import `@gjermundgaraba/effect-actions/ActionToolkit`.
|
|
15
16
|
|
|
16
|
-
| API | Purpose
|
|
17
|
-
| --------------------------------- |
|
|
18
|
-
| `make(implementations, options?)` | Project an implementation or a list; returns `{ toolkit, layer }`.
|
|
19
|
-
| `toolkit` | A native `Toolkit`, one tool per action, named after it.
|
|
20
|
-
| `layer` | The tools' handlers. Requires the startup services their builders read.
|
|
17
|
+
| API | Purpose |
|
|
18
|
+
| --------------------------------- | ------------------------------------------------------------------------------ |
|
|
19
|
+
| `make(implementations, options?)` | Project an implementation or a list; returns `{ toolkit, layer }`. |
|
|
20
|
+
| `toolkit` | A native `Toolkit`, one tool per action, named after it. |
|
|
21
|
+
| `layer` | The tools' handlers. Requires the startup services their builders read. |
|
|
22
|
+
| `openApprovals(history)` | The approval requests a `Chat`'s history still holds open, each with its call. |
|
|
21
23
|
|
|
22
24
|
| Option | Meaning |
|
|
23
25
|
| --------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
24
26
|
| `actions` | The actions that are tools: `[GetUser, RenameUser]`. Defaults to every action of the implementations. |
|
|
25
27
|
| `needsApproval` | `(call, context) =>` a boolean, or an `Effect` of one: the calls a model must have approved. Defaults to none. |
|
|
26
28
|
|
|
27
|
-
Exported types: `Tools`, what `make` returns; `Options`, what it takes; `ToolCall<A>`, the call `needsApproval` receives, for a rule written apart from `make
|
|
29
|
+
Exported types: `Tools`, what `make` returns; `Options`, what it takes; `ToolCall<A>`, the call `needsApproval` receives, for a rule written apart from `make`; `Approval`, what `openApprovals` lists.
|
|
28
30
|
|
|
29
31
|
## Give a model tools
|
|
30
32
|
|
|
@@ -52,7 +54,8 @@ conversations.pipe(
|
|
|
52
54
|
- A turn's tool calls, the approved calls of an answering turn included, run concurrently unless `generateText` is given a `concurrency`. A service they share must tolerate it: [assistant/src/tasks/store.ts](../examples/assistant/src/tasks/store.ts) locks each change.
|
|
53
55
|
- Arguments the input does not declare are dropped, where HTTP and a CLI refuse them ([guarantees.md](guarantees.md#wire-behavior)): a misspelled optional argument is ignored silently.
|
|
54
56
|
- Beside other tools, merge natively and provide every handler layer: `Toolkit.merge(toolkit, others)` with `Layer.mergeAll(layer, othersHandlers)`.
|
|
55
|
-
- **A model loop behind a route.** Yield `toolkit` and the model in the implementation's builder, `Action.implement(Ask, Effect.map(toolkit, (tools) => (input) => ...))`: what the handler itself yields is owed per request. Provide `layer` to the route's layer, `ActionHttp.layer(Http, ask).pipe(Layer.provide(layer))`, not in the handler, so the tools share the routes' build ([Action-in-process.md](Action-in-process.md#share-one-build-across-servers)). A tool call runs as the request's caller, unless the handler provides another around it. Keep each caller's `Chat` in a service between requests ([kitchen-sink/src/assistant/conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts)), and answer an approval in a later one: [kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts), with a scripted model in [demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts),
|
|
57
|
+
- **A model loop behind a route.** Yield `toolkit` and the model in the implementation's builder, `Action.implement(Ask, Effect.map(toolkit, (tools) => (input) => ...))`: what the handler itself yields is owed per request. Provide `layer` to the route's layer, `ActionHttp.layer(Http, ask).pipe(Layer.provide(layer))`, not in the handler, so the tools share the routes' build ([Action-in-process.md](Action-in-process.md#share-one-build-across-servers)). A tool call runs as the request's caller, unless the handler provides another around it. Keep each caller's `Chat` in a service between requests ([kitchen-sink/src/assistant/conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts)), and answer an approval in a later one: [kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts), with a scripted model in [demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts), which the entrypoint provides through [kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts), and tested over HTTP in [kitchen-sink/test/assistant/assistant.test.ts](../examples/kitchen-sink/test/assistant/assistant.test.ts).
|
|
58
|
+
- **The real model comes later?** Provide a scripted one from the entrypoint until then, as the kitchen-sink does. Reading the model with `Effect.serviceOption` hides the requirement: the server builds without one and every request fails.
|
|
56
59
|
|
|
57
60
|
### Call a tool by hand
|
|
58
61
|
|
|
@@ -109,8 +112,8 @@ chat
|
|
|
109
112
|
|
|
110
113
|
- The answer needs the prompt history, where its request is found by `approvalId`. Sent alone through a bare `LanguageModel.generateText`, it runs nothing and raises nothing. Use a `Chat`, or send again the assistant message holding the call and its request, before the answer.
|
|
111
114
|
- An approved call runs in the answering turn, before the model is asked again, as whoever that turn runs as: provide the caller who was asked. `authorize` runs for it too ([assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts)).
|
|
112
|
-
- **Each request is answered on its own.** Of a turn's several requests, an approved call runs, a denied one (`approved: false`) does not and gives the model a failed result (`{ type: "execution-denied", reason }`, whose `reason` is `undefined` unless the answer gives one: not JSON
|
|
115
|
+
- **Each request is answered on its own.** Of a turn's several requests, an approved call runs, a denied one (`approved: false`) does not and gives the model a failed result (`{ type: "execution-denied", reason }`, whose `reason` is `undefined` unless the answer gives one: not JSON: a route answering with it as a `Schema.Unknown` success answers an empty 500), and one left unanswered stays open for a later turn. To require every answer at once, check it in the host. Refuse an `approvalId` answered twice in one request: every answer is acted on, so its call would run twice. The answering turn's response does not repeat a request left open: list every open one with `ActionToolkit.openApprovals(yield* Ref.get(chat.history))`, as [kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts) does. Effect finds an approval's call by its tool-call id, which a provider makes unique: where a model of your own repeats one, the last call with it runs, which `openApprovals` names and `response.toolCalls` may not.
|
|
113
116
|
- **Continue on `toolCalls`, never on `toolResults`.** The answering turn's response holds the approved call's result, no tool call, and the model's reply to it: a loop that continues while a response has tool results asks the model again and loses that reply.
|
|
114
|
-
- **If a turn fails or is interrupted
|
|
117
|
+
- **If a turn fails or is interrupted once an approved call has started**, the model failing or the client going away, the call may have run, and may run again. A failed `generateText` records nothing of its turn, so the same answer sent again runs the call again. A failed `streamText` records its prompt and the parts it streamed: a call that returned has its result recorded, but one that failed or was cut off while running leaves its answer without a result, which the next turn acts on unasked; `openApprovals` leaves that request out, as another answer would run the call twice. End the conversation where a turn does not succeed, or record an answer in your own store before its turn, and say in the error that an approved call may have run. Run one turn of a conversation at a time, and look it up again once the lock is held, so a turn that waited does not run on a conversation just ended: [kitchen-sink/src/assistant/conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts) does both.
|
|
115
118
|
- **Show the person what is asked.** A request names its call by `toolCallId`; that call, in `response.toolCalls`, holds the model's `params`, typed `unknown`. Show them as JSON, as [assistant/src/agent.ts](../examples/assistant/src/agent.ts) does, which joins the two; decode them with `Schema.toCodecJson(AddTask.input)`, the tool's parameters, only where you read a field.
|
|
116
|
-
- **Only the person asked answers.** A `tool-approval-response` whose `approvalId` matches
|
|
119
|
+
- **Only the person asked answers.** A `tool-approval-response` whose `approvalId` matches a request in the history without a result is acted on, as the turn's caller, an answered one included: take an `approvalId` only from that caller, among those `openApprovals` lists, and keep the `Chat` where its owner alone reaches it.
|
package/docs/Authentication.md
CHANGED
|
@@ -4,8 +4,9 @@ How a remote caller proves the identity a protected action declares. `Authentica
|
|
|
4
4
|
declares it, as a browser-safe descriptor; `Authentication.layer` is that descriptor's provider,
|
|
5
5
|
running the verifier you write. Verifying a token, login and consent stay in the application.
|
|
6
6
|
|
|
7
|
-
Read this page alone, in one tool call
|
|
8
|
-
search [Authentication-failures.md](Authentication-failures.md). Reading
|
|
7
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
8
|
+
When something fails, search [Authentication-failures.md](Authentication-failures.md). Reading
|
|
9
|
+
order: [README.md](README.md).
|
|
9
10
|
|
|
10
11
|
## API
|
|
11
12
|
|
package/docs/CONTEXT.md
CHANGED
|
@@ -4,8 +4,9 @@ Vocabulary for `@gjermundgaraba/effect-actions`. The docs, the examples, the cod
|
|
|
4
4
|
use these words with these meanings. Module names and Effect's own terms (`HttpApi`, `Tool`,
|
|
5
5
|
`Toolkit`, `McpServer`, `Rpc`, `RpcGroup`, `RpcServer`, `Command`) mean what their modules say.
|
|
6
6
|
|
|
7
|
-
Read this page alone, in one tool call
|
|
8
|
-
[README.md](README.md), and each module's failures from its `-failures.md`
|
|
7
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
8
|
+
Tasks are routed from [README.md](README.md), and each module's failures from its `-failures.md`
|
|
9
|
+
page.
|
|
9
10
|
|
|
10
11
|
## Terms
|
|
11
12
|
|
package/docs/README.md
CHANGED
|
@@ -11,15 +11,19 @@ same schemas, handler and authorization.
|
|
|
11
11
|
first (step 5), before the examples and the pages.
|
|
12
12
|
|
|
13
13
|
1. **Open one page per tool call, with your file-reading tool.** A page fits one read. Several
|
|
14
|
-
in one call, however many commands it runs,
|
|
15
|
-
|
|
14
|
+
in one call, however many commands it runs, can be cut by its output limit, with no mark or
|
|
15
|
+
with one that understates what is lost. If an output says it was truncated, read that page
|
|
16
|
+
again alone.
|
|
16
17
|
2. **Start from the example closest to what you are building** ([Examples](#examples)). Its
|
|
17
18
|
README has a layout tree and a "Where to look" table naming the file for each thing. Copy its
|
|
18
|
-
layout and idioms: the examples are the reference for how code is written.
|
|
19
|
+
layout and idioms: the examples are the reference for how code is written. An example shows
|
|
20
|
+
one way, not the rules, the alternatives or the traps: read steps 3 and 4 even when it fits.
|
|
19
21
|
3. **Read [Action.md](Action.md) completely.** Every surface builds on it.
|
|
20
22
|
4. **Read the page of each surface you serve** ([Pages](#pages)), and the example files its task
|
|
21
23
|
sections link.
|
|
22
|
-
5. **When something fails, search the `*-failures.md` pages** for a fragment of the message
|
|
24
|
+
5. **When something fails, search the `*-failures.md` pages** for a fragment of the message
|
|
25
|
+
without quotes, your own code's first compile error included, before reasoning it out:
|
|
26
|
+
`grep -rnF "<fragment>" node_modules/@gjermundgaraba/effect-actions/docs/*-failures.md`. Each
|
|
23
27
|
module page has one beside it, such as [Action-failures.md](Action-failures.md).
|
|
24
28
|
|
|
25
29
|
A service others call is [notes-service](../examples/notes-service/) plus
|
|
@@ -37,7 +41,7 @@ there is no package root.
|
|
|
37
41
|
|
|
38
42
|
| Read | When you need to |
|
|
39
43
|
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
-
| [setup.md](setup.md) | install, import the modules, write the minimal program, lay out an application, bundle a browser client
|
|
44
|
+
| [setup.md](setup.md) | install, write `tsconfig.json`, import the modules, write the minimal program, lay out an application, bundle a browser client |
|
|
41
45
|
| [Action.md](Action.md) | define a contract and who may call it, implement it, authorize calls, fail with declared and built-in errors |
|
|
42
46
|
| [Action-in-process.md](Action-in-process.md) | call actions from your own code, such as a job; share one build of the implementations across servers and jobs |
|
|
43
47
|
| [ActionHttp.md](ActionHttp.md) | bind actions to JSON routes, serve them, limit request bodies, publish OpenAPI, call them from Effect or a browser |
|
package/docs/Testing-failures.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
What you see when something is wrong, and the fix. Search for a fragment of the message.
|
|
4
4
|
The tasks are in [Testing.md](Testing.md).
|
|
5
5
|
|
|
6
|
-
- A route answers an empty 500: a defect, or a result that does not encode. `Testing.layer` logs the cause as an error, and nothing for a request that succeeds or is refused: read the test's output, or collect it with a `Logger` or an `ErrorReporter` provided around the layer.
|
|
6
|
+
- A route answers an empty 500, which a client method reports as `HttpClientError: Decode error (500 …)`: a defect, or a result that does not encode. `Testing.layer` logs the cause as an error on the process console, under `it.effect` too, and nothing for a request that succeeds or is refused: read the test's output, or collect it with a `Logger` or an `ErrorReporter` provided around the layer.
|
|
7
7
|
- A client method fails with `Action.Unauthenticated`: routes under `Testing.layer` keep their real authentication. Give the client a credential in `transformClient` ([Test as a signed-in caller](Testing.md#test-as-a-signed-in-caller)).
|
|
8
8
|
- A protected route answers 401 where the test expects 415 or 400: authentication runs before the body is read. Send a valid credential.
|
|
9
9
|
- `Type 'Provider<CurrentUser, "notes-service.Login">' is missing the following properties from type 'Scope'` at `it.effect`, or `… is not assignable to type 'never'` at `Effect.runPromise`: the routes serve a protected action without a provider of its descriptor. Provide the real one or a test verifier to the routes; a caller provided around `Testing.layer` does not satisfy them.
|
|
@@ -11,6 +11,7 @@ The tasks are in [Testing.md](Testing.md).
|
|
|
11
11
|
- `Type '<Service>' is missing the following properties from type 'Scope'` at every `it.effect`, for a service that is no identity, such as a store, a limiter or a `LanguageModel`: a builder now yields it, and the routes require it. Provide its layer to `Testing.layer(routes)`, as the entrypoint provides it to the server.
|
|
12
12
|
- `Type 'unknown' is not assignable to type 'Scope'` at `it.effect`, or `Argument of type 'Effect<…>' is not assignable` at an `Effect.flip` or `Effect.exit`, around a loop over an array of calls to different actions: their successes and errors differ, so the array has no one element type. Annotate it, `ReadonlyArray<Effect.Effect<unknown, unknown>>`, with the caller as a third parameter for `Action.client` calls of protected actions, or assert each call on its own.
|
|
13
13
|
- An rpc call never answers under `Testing.layer`, and nothing is logged: the layer holds the rpc layer twice, under two protocols, and built it for the other one, a WebSocket. Give the rpc layer its WebSocket in the entrypoint, so the routes a test serves hold it once, under the test's protocol ([Serve the rpcs](ActionRpc.md#serve-the-rpcs)).
|
|
14
|
+
- `Property '<field>' does not exist on type 'MethodError<Action<…>>'`, or on a built-in error, after `expect(error).toBeInstanceOf(…)`: `toBeInstanceOf` does not narrow the union of the action's errors. Compare with `toEqual(new X({ … }))`, or narrow with `instanceof` ([Assert on errors](Testing.md#assert-on-errors)).
|
|
14
15
|
- A test passes whatever message an error carries: `toEqual` never compares a tagged error's `message`. Assert it separately ([Assert on errors](Testing.md#assert-on-errors)).
|
|
15
16
|
- The test reads a service the handlers never changed: it was provided inside the routes and again to the test, two instances. Provide it once, around `Testing.layer`, with `Layer.provideMerge`.
|
|
16
17
|
- `MCP tools/call "<name>" answered 404`: the endpoint is not at `/mcp`, the default. Pass `mcpClient` its `url`.
|
package/docs/Testing.md
CHANGED
|
@@ -8,8 +8,9 @@ tests of the surfaces beside them, in `it.effect` from `@effect/vitest`, which s
|
|
|
8
8
|
`Scope` that `Action.client` and `ActionRpc.client` require, a `TestClock` and a `TestConsole`
|
|
9
9
|
(both imported from `effect/testing`).
|
|
10
10
|
|
|
11
|
-
Read this page alone, in one tool call
|
|
12
|
-
search [Testing-failures.md](Testing-failures.md). Reading order:
|
|
11
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
12
|
+
When something fails, search [Testing-failures.md](Testing-failures.md). Reading order:
|
|
13
|
+
[README.md](README.md).
|
|
13
14
|
|
|
14
15
|
## API
|
|
15
16
|
|
|
@@ -30,7 +31,8 @@ Import `@gjermundgaraba/effect-actions/Testing`.
|
|
|
30
31
|
| `mcpRequest`: `headers` | Request headers. |
|
|
31
32
|
|
|
32
33
|
Exported types: `McpClient<Actions>`, `McpClientOptions`, `McpRequestOptions`, and `McpParams`,
|
|
33
|
-
the `params` of `mcpRequest`.
|
|
34
|
+
the `params` of `mcpRequest`. A helper passing on a tool's `arguments` types them `Schema.Json`,
|
|
35
|
+
and whole `params` `McpParams`, never `unknown`.
|
|
34
36
|
|
|
35
37
|
## Test an implementation in process
|
|
36
38
|
|
|
@@ -204,6 +206,11 @@ and so does `toStrictEqual`. Other fields are compared, so `toEqual(new NoteNotF
|
|
|
204
206
|
asserts the `id`, and `toEqual(new Action.Forbidden({ scopes: ["issues:write"] }))` still needs the
|
|
205
207
|
message asserted beside it. The message is all a model reads of a tool's failure.
|
|
206
208
|
|
|
209
|
+
`toBeInstanceOf` narrows nothing: the flipped failure stays the union of the action's declared
|
|
210
|
+
and built-in errors, so reading a field only one of them has, such as `retryAfterSeconds`, does
|
|
211
|
+
not compile. Compare with `toEqual(new RateLimited({ retryAfterSeconds: 10 }))`, or narrow with
|
|
212
|
+
`instanceof` first.
|
|
213
|
+
|
|
207
214
|
A Toolkit raises neither a refusal nor a declared error: each is a result the model reads, so
|
|
208
215
|
assert `isFailure` and `result.message`
|
|
209
216
|
([assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts)).
|
package/docs/setup-failures.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
What you see when something is wrong, and the fix. Search for a fragment of the message.
|
|
4
4
|
The tasks are in [setup.md](setup.md).
|
|
5
5
|
|
|
6
|
+
- `TS1149: File name '…/assistant.ts' differs from already included file name '…/Assistant.ts' only in casing`:
|
|
7
|
+
a lowercase file is named for its domain beside the domain's contract. Name lowercase files for
|
|
8
|
+
their role, such as `handlers.ts` or `turns.ts`: on a disk that ignores case the two are one file.
|
|
6
9
|
- `Cannot find module '@gjermundgaraba/effect-actions'`: there is no root export. Import a
|
|
7
10
|
subpath from [Entry points](setup.md#entry-points), never the root or a `dist/` path.
|
|
8
11
|
- A browser build fails with `Could not resolve "node:…"`, or warns
|
|
@@ -28,7 +31,7 @@ The tasks are in [setup.md](setup.md).
|
|
|
28
31
|
such as `found 4.1.0`, or a start failing with `Cannot find module '…/effect/dist/…'` under an
|
|
29
32
|
`@effect/*` package: more than one Effect release is installed, or one outside the peer
|
|
30
33
|
range. Install the `effect` range [Install](setup.md#install) names, with every `@effect/*`
|
|
31
|
-
package at the version `effect` resolved to, such as `@effect/platform-node@4.0.
|
|
32
|
-
`effect@4.0.
|
|
34
|
+
package at the version `effect` resolved to, such as `@effect/platform-node@4.0.3` beside
|
|
35
|
+
`effect@4.0.3`.
|
|
33
36
|
- `Cannot find module 'effect/unstable/http'` (or `…/httpapi`, `…/cli`, `…/ai`): these modules
|
|
34
37
|
are now `effect/http`, `effect/http-api`, `effect/cli` and `effect/ai`.
|