@gjermundgaraba/effect-actions 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +4 -4
  2. package/dist/Action.d.mts +3 -1
  3. package/dist/ActionToolkit.d.mts +23 -1
  4. package/dist/ActionToolkit.mjs +28 -1
  5. package/dist/Testing.d.mts +3 -1
  6. package/dist/Testing.mjs +2 -2
  7. package/dist/mcp/protocol.d.mts +2 -1
  8. package/docs/Action.md +11 -6
  9. package/docs/ActionCli-failures.md +2 -0
  10. package/docs/ActionCli.md +7 -5
  11. package/docs/ActionHttp-failures.md +2 -2
  12. package/docs/ActionHttp.md +6 -3
  13. package/docs/ActionMcp.md +20 -20
  14. package/docs/ActionRpc.md +3 -2
  15. package/docs/ActionToolkit.md +15 -12
  16. package/docs/Authentication.md +3 -2
  17. package/docs/CONTEXT.md +3 -2
  18. package/docs/README.md +9 -5
  19. package/docs/Testing-failures.md +2 -1
  20. package/docs/Testing.md +10 -3
  21. package/docs/setup-failures.md +5 -2
  22. package/docs/setup.md +9 -7
  23. package/examples/kitchen-sink/README.md +5 -4
  24. package/examples/kitchen-sink/src/assistant/handlers.ts +18 -38
  25. package/examples/kitchen-sink/src/issues/Issues.ts +4 -2
  26. package/examples/kitchen-sink/src/issues/write-limit.ts +2 -1
  27. package/examples/kitchen-sink/src/server.ts +3 -3
  28. package/examples/live-rpc/README.md +1 -1
  29. package/examples/live-rpc/src/chat/Chat.ts +3 -1
  30. package/examples/live-rpc/src/chat/handlers.ts +2 -2
  31. package/examples/live-rpc/src/chat/store.ts +1 -1
  32. package/examples/live-rpc/src/server.ts +3 -3
  33. package/examples/live-rpc/test/chat/chat.test.ts +1 -1
  34. package/examples/mcp-tools/README.md +9 -4
  35. package/examples/mcp-tools/src/printers/Printers.ts +6 -4
  36. package/examples/mcp-tools/src/server.ts +3 -2
  37. package/examples/mcp-tools/test/routes.test.ts +2 -2
  38. package/examples/notes-service/README.md +6 -3
  39. package/examples/notes-service/src/commands.ts +50 -2
  40. package/examples/notes-service/src/notes/Notes.ts +32 -4
  41. package/examples/notes-service/src/notes/handlers.ts +8 -2
  42. package/examples/notes-service/src/notes/store.ts +11 -0
  43. package/examples/notes-service/src/server.ts +3 -3
  44. package/examples/notes-service/test/commands.test.ts +156 -2
  45. 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: pages read together are cut. It routes every task, protecting an
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.2
103
+ pnpm add @gjermundgaraba/effect-actions effect@~4.0.4
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.2` to serve or run a CLI from Node. Every module is a
107
+ ([docs/setup.md](docs/setup.md#install)). Add `@effect/platform-node@~4.0.4` 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 or updates.
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. */
@@ -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
@@ -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 };
@@ -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. What they still require is this layer's, as
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));
@@ -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: pages read together are cut. When something fails,
7
- search [Action-failures.md](Action-failures.md). Reading order: [README.md](README.md).
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 or the record, which
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: pages read together are cut. When something fails,
12
- search [ActionCli-failures.md](ActionCli-failures.md). Reading order: [README.md](README.md).
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. For a syntax the options cannot express, such as a
151
- flag named apart from its field, build the command with native `Command.make`, `Flag` and `Argument`, and
152
- pass it in `subcommands`: `notes export <file>` takes `Argument.String("file")`
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`, for a status such as 429: no schema
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`.
@@ -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: pages read together are cut. When something fails,
8
- search [ActionHttp-failures.md](ActionHttp-failures.md). Reading order: [README.md](README.md).
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: pages read together are cut. When something fails,
9
- search [ActionMcp-failures.md](ActionMcp-failures.md). Reading order: [README.md](README.md).
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 three for HTTP. |
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
- [quickstart/src/server.ts](../examples/quickstart/src/server.ts). Surfaces given the same
53
- implementations in one set of routes run each builder once, so a note written over HTTP is read
54
- as a tool: [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts). For two
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 both connect, as tested in October 2026:
59
- [notes-service](../examples/notes-service/README.md#run-it) has the commands,
60
- [mcp-tools](../examples/mcp-tools/README.md#connect-a-client) those for stdio and Claude Desktop.
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. An HTTP
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: pages read together are cut. When something fails,
8
- search [ActionRpc-failures.md](ActionRpc-failures.md). Reading order: [README.md](README.md).
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
 
@@ -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: pages read together are cut. When something fails,
10
- search [ActionToolkit-failures.md](ActionToolkit-failures.md). Reading order: [README.md](README.md).
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), provided in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) and tested over HTTP in [kitchen-sink/test/assistant/assistant.test.ts](../examples/kitchen-sink/test/assistant/assistant.test.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, so never an action's success), 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: read every open request from the `Chat`'s 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: a model of your own that repeats one in a `Chat` runs another call than was shown, or none.
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 after an approved call**, the model failing or the client going away, the call has run, the `Chat` has recorded nothing of that turn, and the same answer sent again runs the call again. End the conversation where a turn does not succeed, or record an answer 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.
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 an open request in the history is acted on, as the turn's caller: take an `approvalId` only from that caller, and keep the `Chat` where its owner alone reaches it.
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.
@@ -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: pages read together are cut. When something fails,
8
- search [Authentication-failures.md](Authentication-failures.md). Reading order: [README.md](README.md).
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: pages read together are cut. Tasks are routed from
8
- [README.md](README.md), and each module's failures from its `-failures.md` page.
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, are cut by its output limit, and what is cut goes
15
- unnoticed.
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. Each
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 |
@@ -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: pages read together are cut. When something fails,
12
- search [Testing-failures.md](Testing-failures.md). Reading order: [README.md](README.md).
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)).
@@ -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.2` beside
32
- `effect@4.0.2`.
34
+ package at the version `effect` resolved to, such as `@effect/platform-node@4.0.4` beside
35
+ `effect@4.0.4`.
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`.