@gjermundgaraba/effect-actions 0.18.2 → 0.20.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 (155) hide show
  1. package/README.md +20 -47
  2. package/dist/Action.d.mts +96 -49
  3. package/dist/Action.mjs +12 -16
  4. package/dist/ActionCli.d.mts +82 -44
  5. package/dist/ActionCli.mjs +54 -35
  6. package/dist/ActionHttp.d.mts +84 -45
  7. package/dist/ActionHttp.mjs +3 -4
  8. package/dist/ActionMcp.d.mts +54 -29
  9. package/dist/ActionMcp.mjs +33 -15
  10. package/dist/ActionRpc.d.mts +48 -37
  11. package/dist/ActionRpc.mjs +3 -4
  12. package/dist/ActionToolkit.d.mts +32 -15
  13. package/dist/ActionToolkit.mjs +11 -9
  14. package/dist/Authentication.d.mts +92 -42
  15. package/dist/Authentication.mjs +38 -37
  16. package/dist/Testing.d.mts +23 -27
  17. package/dist/Testing.mjs +1 -2
  18. package/dist/authentication/provider.d.mts +1 -6
  19. package/dist/authentication/provider.mjs +2 -3
  20. package/dist/authentication/refusal.mjs +3 -3
  21. package/dist/cli/commands.d.mts +34 -13
  22. package/dist/cli/commands.mjs +48 -24
  23. package/dist/contract/ast.mjs +1 -2
  24. package/dist/contract/call.d.mts +1 -3
  25. package/dist/contract/call.mjs +1 -2
  26. package/dist/contract/caller.d.mts +1 -3
  27. package/dist/contract/caller.mjs +2 -3
  28. package/dist/contract/declared.d.mts +0 -2
  29. package/dist/contract/declared.mjs +28 -16
  30. package/dist/contract/errors.d.mts +42 -19
  31. package/dist/contract/errors.mjs +43 -19
  32. package/dist/contract/hints.mjs +4 -4
  33. package/dist/contract/implementation.d.mts +7 -14
  34. package/dist/contract/implementation.mjs +31 -19
  35. package/dist/contract/media.d.mts +7 -5
  36. package/dist/contract/media.mjs +7 -4
  37. package/dist/contract/names.mjs +15 -14
  38. package/dist/contract/status.mjs +4 -12
  39. package/dist/http/client.d.mts +15 -6
  40. package/dist/http/client.mjs +2 -3
  41. package/dist/http/schema-errors.mjs +1 -2
  42. package/dist/mcp/client.d.mts +21 -10
  43. package/dist/mcp/client.mjs +8 -11
  44. package/dist/mcp/protocol.mjs +1 -2
  45. package/dist/stdio/console.mjs +1 -2
  46. package/dist/testing/memory.d.mts +1 -3
  47. package/dist/testing/memory.mjs +1 -2
  48. package/docs/Action-failures.md +6 -15
  49. package/docs/Action.md +159 -191
  50. package/docs/ActionCli-failures.md +12 -92
  51. package/docs/ActionCli.md +102 -156
  52. package/docs/ActionHttp-failures.md +14 -76
  53. package/docs/ActionHttp.md +150 -176
  54. package/docs/ActionMcp-failures.md +2 -22
  55. package/docs/ActionMcp.md +79 -170
  56. package/docs/ActionRpc-failures.md +3 -12
  57. package/docs/ActionRpc.md +183 -120
  58. package/docs/ActionToolkit-failures.md +3 -10
  59. package/docs/ActionToolkit.md +112 -109
  60. package/docs/Authentication-advanced.md +171 -108
  61. package/docs/Authentication-failures.md +11 -91
  62. package/docs/Authentication.md +114 -135
  63. package/docs/README.md +45 -30
  64. package/docs/Testing-failures.md +2 -6
  65. package/docs/Testing.md +126 -159
  66. package/docs/setup-failures.md +8 -32
  67. package/docs/setup.md +42 -38
  68. package/examples/README.md +6 -10
  69. package/examples/assistant/README.md +20 -38
  70. package/examples/assistant/src/agent.ts +2 -3
  71. package/examples/assistant/src/cli.ts +4 -9
  72. package/examples/assistant/src/commands.ts +6 -4
  73. package/examples/assistant/src/identity/authorize.ts +3 -3
  74. package/examples/assistant/src/tasks/Tasks.ts +1 -2
  75. package/examples/assistant/src/tasks/handlers.ts +3 -3
  76. package/examples/assistant/src/toolkit.ts +6 -8
  77. package/examples/assistant/test/commands.test.ts +16 -11
  78. package/examples/assistant/test/tasks/tasks.test.ts +0 -2
  79. package/examples/assistant/test/toolkit.test.ts +0 -1
  80. package/examples/kitchen-sink/README.md +30 -52
  81. package/examples/kitchen-sink/src/Actions.ts +2 -3
  82. package/examples/kitchen-sink/src/Api.ts +3 -4
  83. package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
  84. package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
  85. package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
  86. package/examples/kitchen-sink/src/agent.ts +1 -1
  87. package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
  88. package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
  89. package/examples/kitchen-sink/src/attachments/download.ts +5 -8
  90. package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
  91. package/examples/kitchen-sink/src/browser.ts +3 -4
  92. package/examples/kitchen-sink/src/cli.ts +6 -7
  93. package/examples/kitchen-sink/src/config.ts +2 -0
  94. package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
  95. package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
  96. package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
  97. package/examples/kitchen-sink/src/identity/quota.ts +1 -1
  98. package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
  99. package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
  100. package/examples/kitchen-sink/src/issues/guide.ts +2 -3
  101. package/examples/kitchen-sink/src/ops.ts +3 -6
  102. package/examples/kitchen-sink/src/request-policy.ts +8 -4
  103. package/examples/kitchen-sink/src/routes.ts +4 -6
  104. package/examples/kitchen-sink/src/seed.ts +0 -1
  105. package/examples/kitchen-sink/src/server.ts +11 -12
  106. package/examples/kitchen-sink/src/stdio.ts +3 -2
  107. package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
  108. package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
  109. package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
  110. package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
  111. package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
  112. package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
  113. package/examples/kitchen-sink/test/request-policy.test.ts +39 -3
  114. package/examples/kitchen-sink/test/routes.test.ts +3 -3
  115. package/examples/live-rpc/README.md +8 -49
  116. package/examples/live-rpc/src/browser.ts +8 -41
  117. package/examples/live-rpc/src/chat/Chat.ts +9 -3
  118. package/examples/live-rpc/src/chat/room.ts +1 -1
  119. package/examples/live-rpc/src/client.ts +2 -3
  120. package/examples/live-rpc/src/page.ts +34 -0
  121. package/examples/live-rpc/src/routes.ts +6 -7
  122. package/examples/live-rpc/src/server.ts +6 -4
  123. package/examples/live-rpc/test/browser.test.ts +6 -5
  124. package/examples/live-rpc/test/routes.test.ts +16 -15
  125. package/examples/live-rpc/test/server.test.ts +1 -1
  126. package/examples/mcp-tools/README.md +22 -45
  127. package/examples/mcp-tools/src/printers/Printers.ts +3 -7
  128. package/examples/mcp-tools/src/routes.ts +0 -7
  129. package/examples/mcp-tools/src/server.ts +1 -1
  130. package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
  131. package/examples/mcp-tools/test/routes.test.ts +2 -3
  132. package/examples/notes-service/README.md +9 -43
  133. package/examples/notes-service/src/cli.ts +0 -3
  134. package/examples/notes-service/src/commands.ts +7 -17
  135. package/examples/notes-service/src/identity/authorize.ts +2 -5
  136. package/examples/notes-service/src/identity/verifier.ts +3 -4
  137. package/examples/notes-service/src/notes/Notes.ts +2 -6
  138. package/examples/notes-service/src/notes/store.ts +5 -1
  139. package/examples/notes-service/src/routes.ts +6 -8
  140. package/examples/notes-service/src/server.ts +4 -3
  141. package/examples/notes-service/src/stdio.ts +4 -3
  142. package/examples/notes-service/test/commands.test.ts +1 -1
  143. package/examples/notes-service/test/notes/as.ts +6 -0
  144. package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
  145. package/examples/notes-service/test/notes/in-process.test.ts +50 -0
  146. package/examples/notes-service/test/notes/notes.test.ts +3 -42
  147. package/examples/notes-service/test/routes.test.ts +2 -2
  148. package/examples/quickstart/README.md +1 -1
  149. package/examples/quickstart/src/client.ts +1 -0
  150. package/examples/quickstart/src/server.ts +2 -4
  151. package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
  152. package/package.json +12 -16
  153. package/docs/Action-in-process.md +0 -93
  154. package/docs/CONTEXT.md +0 -46
  155. package/docs/guarantees.md +0 -44
@@ -5,11 +5,10 @@ import { Client, ClientOptions, McpCallError, Params, RequestOptions, client, re
5
5
  import { Cause, Effect, Layer, Stdio } from "effect";
6
6
  import { McpSchema, McpServer } from "effect/ai";
7
7
  import { HttpRouter } from "effect/http";
8
- //#region src/mcp/ActionMcp.d.ts
9
8
  /**
10
- * An MCP server, over HTTP or stdio: every native `McpServer.layerStdio` option except
11
- * `protocols`, the server information, `instructions` and `extensions`, the actions `A` that
12
- * are its tools, and the native features it serves beside them.
9
+ * An MCP server, over HTTP or stdio: every native `McpServer.layerStdio` option but `protocols`,
10
+ * the server information, `instructions` and `extensions` among them, the actions `A` that are
11
+ * its tools, and the native features it serves beside them.
13
12
  */
14
13
  export interface Options<E = never, R = never, A extends Any = Any> extends Omit<Parameters<typeof McpServer.layerStdio>[0], "protocols"> {
15
14
  /**
@@ -20,7 +19,14 @@ export interface Options<E = never, R = never, A extends Any = Any> extends Omit
20
19
  /**
21
20
  * Native MCP features served beside the actions' tools, such as `McpServer.resource`,
22
21
  * `McpServer.prompt` and `McpServer.toolkit` layers merged into one. They register on this
23
- * server's registry; merged beside it instead, they register on another and are not served.
22
+ * server's registry and run with the services they were built with, and no `authorize` runs for
23
+ * them.
24
+ *
25
+ * **Gotchas**
26
+ *
27
+ * - Merged beside the endpoint instead of passed here, they register on another registry and are never served, without an error.
28
+ * - A service provided to this layer itself is built again for the features alone, so a resource reads another store than the tools write: provide it around the endpoint.
29
+ * - They never receive a request's services, so they cannot read the caller: content that depends on the caller is an action.
24
30
  */
25
31
  readonly features?: Layer.Layer<never, E, R> | undefined;
26
32
  }
@@ -32,6 +38,21 @@ export interface Options<E = never, R = never, A extends Any = Any> extends Omit
32
38
  export interface LayerHttpOptions<E = never, R = never, A extends Any = Any> extends Options<E, R, A>, Omit<Parameters<typeof McpServer.layerHttp>[0], "protocols" | "path" | "allowSessionTermination"> {
33
39
  /** The endpoint's route; defaults to `/mcp`. */
34
40
  readonly path?: HttpRouter.PathInput;
41
+ /**
42
+ * The descriptor the protected tools' contracts name, whose provider, built by
43
+ * `Authentication.layer`, the routes provide. On an endpoint of protected tools only, every
44
+ * request authenticates before it is decoded; on a mixed one, anyone may discover the server,
45
+ * list its tools, cancel, and call a public tool in a 2026-07-28 request, and everything else
46
+ * authenticates first.
47
+ *
48
+ * **Gotchas**
49
+ *
50
+ * - A credential a request presents is still verified, on a public tool's call too: an expired or invalid one answers 401, its challenge naming `invalid_token`, on which a client refreshes it or signs in again.
51
+ * - `tools/list` names every tool, protected ones included: serve public tools on an endpoint of their own if signed-out callers must not see the rest.
52
+ * - A client of a 2025 revision opens a session, which on an endpoint serving a protected tool only a signed-in caller does: it signs in for every request there, a public tool's call and `tools/list` included.
53
+ * - Every prompt and resource request needs a credential, listings included: to offer them signed out, serve them on an endpoint without protected tools.
54
+ * - A handler that sends a notification starts the response, so a refusal after it is an `isError: true` tool result, not the 401 or 403 a client re-authorizes on: refuse in `authorize`.
55
+ */
35
56
  readonly authentication?: Any$1;
36
57
  /**
37
58
  * Whether MCP 2026-07-28 clients may subscribe, holding a stream open for list changes and
@@ -43,9 +64,10 @@ export interface LayerHttpOptions<E = never, R = never, A extends Any = Any> ext
43
64
  /**
44
65
  * `true` runs a server per request, as Effect's `McpServer.layerHttpRequestScoped` does, for a
45
66
  * host such as Cloudflare Workers where nothing may outlive a request. It speaks MCP 2026-07-28
46
- * alone, so a client that opens with `initialize` is refused, and it keeps no session. It has
47
- * no subscriptions, so `allowSubscriptions` is ignored, and native `features` send no
48
- * list-change notification and cannot use `McpServer.elicit`.
67
+ * alone, so a client that opens with `initialize` is refused; it keeps no session and takes only
68
+ * `POST`, a `DELETE` answering 405. It has no subscriptions, so `allowSubscriptions` is ignored,
69
+ * and native `features` send no list-change notification. `McpServer.elicit` reaches no client
70
+ * on it, as Effect's `McpServer.layerHttpRequestScoped` says.
49
71
  */
50
72
  readonly requestScoped?: boolean | undefined;
51
73
  }
@@ -61,35 +83,38 @@ type Inferring<D, A extends Any> = [D] extends [undefined] ? [Protected<A>] exte
61
83
  * Serve MCP tools over one Streamable HTTP endpoint, speaking MCP 2026-07-28, 2025-11-25 or
62
84
  * 2025-06-18, as the client negotiates. A 2026-07-28 request stands alone; a client of a 2025
63
85
  * revision opens a session with `initialize`, which holds what it sent there and no identity,
64
- * and lasts until that client ends it with a DELETE, answered 204, or the process stops. For a
65
- * serverless host, see `requestScoped`.
66
- *
67
- * Protected tools need the `authentication` descriptor their contracts name, and the
68
- * provider `Authentication.layer` builds from it. On an endpoint of protected tools only,
69
- * every request authenticates before it is decoded. On a mixed endpoint, anyone may discover
70
- * the server, list its tools, cancel, and call a public tool in a 2026-07-28 request;
71
- * everything else authenticates first, a session's every request included, so a protected
72
- * tool's description is listed to anyone but run only for its caller.
73
- * On a Bearer endpoint, a protected tool's call refused with `Unauthenticated`, or with
74
- * `Forbidden` naming scopes, is answered with its HTTP status and challenge, 401 or 403, as MCP
75
- * authorization requires, unless its response has started; any other failure is a tool result. Claude Code connects with
76
- * `claude mcp add --transport http <name> <url>`. In a tool call, what middleware provides per request wins over
77
- * what the endpoint was built with; native `features` read only what they were built with,
78
- * never a request's. Never provide an identity at startup: a tool call takes its identity
79
- * only from the request it authenticated.
86
+ * and lasts until that client ends it with a DELETE, answered 204, or the process stops. Protected
87
+ * tools need `authentication`; for a serverless host, see `requestScoped`. In a tool call, what
88
+ * middleware provides per request wins over what the endpoint was built with.
80
89
  * Guide: `docs/ActionMcp.md` in this package; in an application: `examples/mcp-tools/`.
90
+ *
91
+ * **Gotchas**
92
+ *
93
+ * - A success is `isError: false` with `structuredContent` its JSON and the same JSON as one text block, both `null` for an action without `success`; an `Action.Image` field is left out of `structuredContent` and its text, its block following them, and a success of images alone has no `structuredContent`.
94
+ * - A declared error is an `isError: true` tool result in a 200 holding its JSON as one text block, with no `structuredContent`; a refusal is answered as `Action.Unauthenticated` and `Action.Forbidden` say.
95
+ * - Arguments that do not decode never reach the handler: an `isError: true` result in a 200, text `Invalid parameters for tool '<name>': …`, or for a 2025-06-18 client a JSON-RPC error.
96
+ * - A defect, an undeclared failure, or a success or an error that does not encode is `isError: true`, text `Tool execution failed due to an internal server error.`: the cause goes to the log and to the call's `ErrorReporter`s, never to the client.
97
+ * - An unknown tool is a JSON-RPC error `-32602` in a 200; a body that is not JSON a JSON-RPC error `-32700` in a 200; a `Content-Type` missing or not JSON an empty 415; another HTTP method an empty 405 with `Allow: POST, DELETE`, `Allow: POST` where `requestScoped`, or the 401 of an endpoint with authentication when signed out.
98
+ * - A 2025-11-25 or 2025-06-18 client gets `structuredContent` only for a success that is an object: any other success arrives as its text block alone, a string unquoted.
99
+ * - A client asking for an earlier revision, such as 2025-03-26, is offered 2025-11-25, so a client speaking only that earlier one cannot connect.
100
+ * - A request body is read whole, however large, and a 2025 session holds what its `initialize` sent until it ends, so an oversized `initialize` costs memory for that long: limit bodies as `ActionHttp.layer` says.
81
101
  */
82
102
  export declare function layerHttp<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, E = never, R = never, const D extends Any$1 | undefined = undefined>(implementations: Apps, options: LayerHttpOptions<E, R, ActionOf<Member<Apps>>> & O & NoInfer<Known<O, LayerHttpOptions<E, R, Any>>> & NoInfer<Required<SelectedOf<O, Apps>>> & Matching<NoInfer<SelectedOf<O, Apps>>, NoInfer<D>> & Inferring<D, NoInfer<SelectedOf<O, Apps>>>): Layer.Layer<never, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Cause.IllegalArgumentError | E, ServerBuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", HttpToolRequestContext<RemoteRequest<Member<Apps>, SelectedOf<O, Apps>>>> | ProviderOf<D> | Features<R>>;
83
103
  /**
84
104
  * Serve MCP tools through newline-delimited JSON-RPC on standard I/O, speaking MCP
85
105
  * 2026-07-28, 2025-11-25 or 2025-06-18, as the host negotiates: the whole
86
106
  * program of an MCP subprocess, which succeeds once the host closes its side and every call
87
- * in flight has been answered. A signal interrupts it.
107
+ * in flight has been answered. A signal interrupts it. Its own Effect logs and `Console` output
108
+ * go to stderr, since stdout carries the protocol. The host supplies the `Stdio` service, and
109
+ * provides the identity around `runStdio`; arguments are tool input only and never establish
110
+ * request identity or authority, and each implementation's `authorize` runs for its protected
111
+ * actions. Its tool results, and how it answers each revision and an earlier one asked for, are
112
+ * `layerHttp`'s.
113
+ *
114
+ * **Gotchas**
88
115
  *
89
- * Effect logs go to stderr, since stdout carries the protocol. The host supplies the
90
- * `Stdio` service and the identity. Arguments are tool input only and never establish
91
- * request identity or authority; each implementation's `authorize` runs for its protected actions.
116
+ * - A line that is not JSON goes unanswered, with no `-32700` as over HTTP, and the lines after it are still answered.
117
+ * - The layers provided around it log to stdout, among the protocol's lines: apply `ActionCli.logToStderr` to the program, last, as its JSDoc says.
92
118
  */
93
119
  export declare function runStdio<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, E = never, R = never>(implementations: Apps, options: Options<E, R, ActionOf<Member<Apps>>> & O & NoInfer<Known<O, Options<E, R, Any>>>): Effect.Effect<void, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Cause.IllegalArgumentError | E, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Stdio.Stdio | ToolRequestContext<ServedRequest<Member<Apps>, SelectedOf<O, Apps>>> | Features<R>>;
94
- //#endregion
95
120
  export { type Client, type ClientOptions, McpCallError, type Params, type RequestOptions, client, request };
@@ -1,5 +1,5 @@
1
1
  import { Anyone, assertAuthentication } from "./contract/caller.mjs";
2
- import { assertOnce } from "./contract/names.mjs";
2
+ import { assertOnce, heldOnce } from "./contract/names.mjs";
3
3
  import { projectedErrors } from "./contract/declared.mjs";
4
4
  import { hintsOf } from "./contract/hints.mjs";
5
5
  import { acquire, provideHandlers, select, toList } from "./contract/implementation.mjs";
@@ -9,26 +9,39 @@ import { defaultPath, fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom, p
9
9
  import { recordStepUp } from "./authentication/refusal.mjs";
10
10
  import { withStderrConsole } from "./stdio/console.mjs";
11
11
  import { McpCallError, client, request } from "./mcp/client.mjs";
12
- import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Predicate, Schema } from "effect";
12
+ import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Option, Predicate, Schema, Stdio, Stream } from "effect";
13
13
  import { AiError, McpSchema, McpServer } from "effect/ai";
14
14
  import { HttpRouter, HttpServerRequest } from "effect/http";
15
15
  import { Base64 } from "effect/encoding";
16
- //#region src/mcp/ActionMcp.ts
16
+ // src/mcp/ActionMcp.ts
17
17
  const isToolArgumentsSchema = Schema.is(McpSchema.ToolJson);
18
+ const isJson = Schema.is(Schema.Json);
19
+ const definitionsReferenced = (value) => {
20
+ if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return [];
21
+ if (Array.isArray(value)) return value.flatMap(definitionsReferenced);
22
+ return Object.entries(value).flatMap(([field, nested]) => {
23
+ if (field !== "$ref") return definitionsReferenced(nested);
24
+ const [scope, key] = Predicate.isString(nested) ? JsonPointer.parseUriFragment(nested) ?? [] : [];
25
+ return scope === "$defs" && key !== void 0 ? [key] : [];
26
+ });
27
+ };
18
28
  const toolJsonSchema = (schema, strict) => {
19
29
  const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
20
- const [scope, key] = Predicate.isString(document.schema.$ref) ? JsonPointer.parseUriFragment(document.schema.$ref) ?? [] : [];
21
- const root = scope === "$defs" && key !== void 0 ? document.definitions[key] : document.schema;
22
- return root === void 0 || Object.keys(document.definitions).length === 0 ? root : {
30
+ const [scope, key] = Predicate.isString(document.schema["$ref"]) ? JsonPointer.parseUriFragment(document.schema["$ref"]) ?? [] : [];
31
+ const inlined = scope === "$defs" && key !== void 0 ? key : void 0;
32
+ const root = inlined === void 0 ? document.schema : document.definitions[inlined];
33
+ if (root === void 0) return void 0;
34
+ const definitions = inlined !== void 0 && ![root, ...Object.values(document.definitions)].flatMap((schema) => isJson(schema) ? definitionsReferenced(schema) : []).includes(inlined) ? Object.fromEntries(Object.entries(document.definitions).filter(([name]) => name !== inlined)) : document.definitions;
35
+ return Object.keys(definitions).length === 0 ? root : {
23
36
  ...root,
24
- $defs: document.definitions
37
+ $defs: definitions
25
38
  };
26
39
  };
27
40
  const assertToolShapes = (lifted) => {
28
41
  const inputs = lifted.map(([action]) => action).filter((action) => !isToolArgumentsSchema(toolJsonSchema(Schema.toCodecJson(action.input), true))).map(({ name }) => name);
29
- if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")}`);
42
+ if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")} (wrap a union, an array or a scalar in a field, omit input for no arguments, or leave the action out of actions)`);
30
43
  const misplaced = lifted.filter(([action, lift]) => isMisplaced(action, lift)).map(([{ name }]) => name);
31
- if (misplaced.length > 0) throw new Error(`MCP media must be the success or an array of it, or a top-level field of a struct success, one, optional or a required array: ${misplaced.join(", ")}`);
44
+ if (misplaced.length > 0) throw new Error(`MCP media must be the success or an array of it, or a top-level field of a struct success, one, optional or a required array: ${misplaced.join(", ")} (make the image such a field, or give an optional array as a required one, empty when absent; an image in input or an error is not served over MCP, so leave that action out of actions)`);
32
45
  };
33
46
  const nativeInternalErrorText = "Tool execution failed due to an internal server error.";
34
47
  const textErrorResult = (text) => new McpSchema.CallToolResult({
@@ -118,13 +131,13 @@ const registerTools = Effect.fnUntraced(function* (apps, authentication) {
118
131
  }
119
132
  });
120
133
  const server = (apps, transport, features = Layer.empty, authentication) => {
121
- assertOnce("MCP tool", apps.flatMap((app) => app.actions));
134
+ assertOnce("MCP tool", apps.flatMap((app) => app.actions), heldOnce);
122
135
  const lifted = apps.flatMap((app) => app.actions.map((action) => [action, liftOf(action.success)]));
123
136
  assertToolShapes(lifted);
124
137
  const names = new Set(lifted.map(([{ name }]) => name));
125
138
  const register = Effect.gen(function* () {
126
139
  const namesClaimedByFeatures = (yield* McpServer.McpServer).tools.flatMap(({ tool }) => names.has(tool.name) ? [tool.name] : []);
127
- if (namesClaimedByFeatures.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${namesClaimedByFeatures.join(", ")}, claimed by an action and a native feature`));
140
+ if (namesClaimedByFeatures.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${namesClaimedByFeatures.join(", ")}, claimed by an action and a native feature (rename the native tool, or leave the action out of actions)`));
128
141
  yield* registerTools(apps, authentication);
129
142
  });
130
143
  return Layer.effectDiscard(register).pipe(Layer.provide(features), Layer.provide(transport), Layer.fresh, provideHandlers(apps));
@@ -151,6 +164,7 @@ const namesItsMethod = (request) => Effect.match(request.json, {
151
164
  onFailure: () => true,
152
165
  onSuccess: (body) => !Predicate.hasProperty(body, "method") || body.method === request.headers["mcp-method"]
153
166
  });
167
+ const readsItsBody = (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.method === "POST" ? Effect.andThen(request.text, route) : route);
154
168
  function layerHttp(apps, { actions, features, path = defaultPath, authentication, requestScoped, allowSubscriptions = features !== void 0, ...options }) {
155
169
  const selected = select(toList(apps), actions);
156
170
  const served = selected.flatMap((app) => app.actions);
@@ -166,17 +180,21 @@ function layerHttp(apps, { actions, features, path = defaultPath, authentication
166
180
  allowSessionTermination: true,
167
181
  allowSubscriptions
168
182
  }), features, authentication);
169
- if (authentication === void 0) return transport;
183
+ if (authentication === void 0) return transport.pipe(Layer.provide(HttpRouter.middleware(readsItsBody).layer));
170
184
  const required = requiresAuthentication(served);
171
- const middleware = HttpRouter.middleware(Effect.map(providerOf(authentication), (runtime) => (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.method !== "POST" || required(request.headers) ? runtime.http(route, false) : Effect.flatMap(namesItsMethod(request), (named) => runtime.http(route, named)))));
185
+ const middleware = HttpRouter.middleware(Effect.map(providerOf(authentication), (runtime) => (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.method !== "POST" || required(request.headers) ? runtime.http(readsItsBody(route), false) : Effect.flatMap(namesItsMethod(request), (named) => runtime.http(readsItsBody(route), named)))));
172
186
  return transport.pipe(Layer.provide(middleware.layer));
173
187
  }
188
+ const isJsonLine = (line) => Option.isSome(Schema.decodeOption(Schema.fromJsonString(Schema.Json))(line));
189
+ const jsonLinesOnly = Layer.effect(Stdio.Stdio, Effect.map(Stdio.Stdio, (stdio) => Stdio.make({
190
+ ...stdio,
191
+ stdin: stdio.stdin.pipe(Stream.decodeText, Stream.splitLines, Stream.filter(isJsonLine), Stream.map((line) => `${line}\n`), Stream.encodeText)
192
+ })));
174
193
  function runStdio(apps, { actions, features, ...options }) {
175
194
  const transport = server(select(toList(apps), actions), McpServer.layerStdio({
176
195
  ...options,
177
196
  protocols
178
- }), features);
197
+ }).pipe(Layer.provide(jsonLinesOnly)), features);
179
198
  return Layer.launch(transport).pipe(withStderrConsole, Effect.forkChild, Effect.flatMap(Fiber.await), Effect.flatMap((exit) => Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause) ? Effect.failCause(exit.cause) : Effect.void));
180
199
  }
181
- //#endregion
182
200
  export { McpCallError, client, layerHttp, request, runStdio };
@@ -8,7 +8,6 @@ import { Certain, ErrorsOf } from "./contract/declared.mjs";
8
8
  import { Context, Effect, Layer, Schema, Scope, Types } from "effect";
9
9
  import { HttpRouter } from "effect/http";
10
10
  import { Rpc, RpcClient, RpcClientError, RpcGroup, RpcMiddleware, RpcServer } from "effect/rpc";
11
- //#region src/rpc/ActionRpc.d.ts
12
11
  type BindingError = Errors[number];
13
12
  type ErrorOf<A extends Any$1, E extends Errors, D> = Schema.Union<ReadonlyArray<A["error"][number] | E[number] | VerifierError<A, D> | BuiltIns>>;
14
13
  type RpcOf<A extends Any$1, E extends Errors, D> = A extends Any$1 ? Rpc.Rpc<A["name"], A["input"], A["success"], ErrorOf<A, E, D>> : never;
@@ -18,6 +17,7 @@ type RpcOf<A extends Any$1, E extends Errors, D> = A extends Any$1 ? Rpc.Rpc<A["
18
17
  * this package, serves the same; `layer` serves it and `client` calls it.
19
18
  */
20
19
  export interface Binding<Actions extends ReadonlyArray<Any$1>, E extends Errors = [], D extends Any$2 | undefined = undefined> {
20
+ /** The descriptor authenticating its protected actions, or `undefined` when it names none. */
21
21
  readonly authentication: D;
22
22
  /** The exact actions bound to this binding. */
23
23
  readonly actions: Actions;
@@ -41,8 +41,15 @@ export interface Any {
41
41
  }
42
42
  /** Contract-level configuration shared by servers and clients. */
43
43
  export interface Options<E extends Errors = Errors> {
44
- /** Layer middleware's errors, which every rpc declares: one schema, or a list. */
44
+ /**
45
+ * What the layer's `middleware` fails with, which every rpc declares so clients decode it:
46
+ * one schema, or a list.
47
+ */
45
48
  readonly error?: E | E[number];
49
+ /**
50
+ * `Authentication.make(...)` of the protected actions' identity, which authenticates each of
51
+ * their messages: required when the binding holds a protected action, and of its identity.
52
+ */
46
53
  readonly authentication?: Any$2;
47
54
  }
48
55
  /**
@@ -77,48 +84,50 @@ type RpcLayer<B extends Any, Apps extends Served, M extends Middleware, A extend
77
84
  export interface LayerOptions<M extends Middleware = [], A extends Any$1 = never> {
78
85
  /**
79
86
  * The actions it serves, among the binding's actions the implementations hold: `[GetUser]`.
80
- * Defaults to every one of them. A layer whose middleware requires the identity lists
81
- * protected actions only.
87
+ * Defaults to every one of them.
82
88
  */
83
89
  readonly actions?: ReadonlyArray<A> | undefined;
84
90
  /**
85
- * Native rpc middleware every served rpc runs, the first innermost: inside the
86
- * authentication of a protected rpc, outside its decoding. Server-only: the binding's group,
87
- * which clients use, carries none. A middleware requiring the identity needs a layer
88
- * serving only protected actions.
91
+ * Native rpc middleware every served rpc runs on each message, the first innermost: inside
92
+ * the authentication of a protected rpc, so none provides what a verifier reads, and outside
93
+ * its decoding, so it also sees input that does not decode. Server-only: the binding's group,
94
+ * which clients use, carries none.
95
+ *
96
+ * **Gotchas**
97
+ *
98
+ * - A middleware requiring the identity leaves it owed on a layer serving a public action: bind the protected actions apart, or read the identity with `Effect.serviceOption`, which a public rpc's call lacks.
99
+ * - A middleware provides a service only to those listed before it and the handlers, and only in a list written in place as a tuple: listed earlier, or in an array-typed list, it leaves the service owed.
100
+ * - Router middleware around the layer runs once per WebSocket, at its upgrade, so its value holds for the whole connection: what differs per message needs a listed middleware.
89
101
  */
90
102
  readonly middleware?: M;
91
103
  }
92
104
  /**
93
105
  * Serve the binding's actions among `implementations` with Effect's own `RpcServer`, or those
94
- * its `actions` lists, as `ActionHttp.layer` selects them. The host provides the protocol,
95
- * such as `RpcServer.layerProtocolWebsocket` or `layerProtocolHttp` and its JSON serialization:
96
- * the layer refuses to build under a protocol that does not speak JSON. A protected rpc
97
- * authenticates each message from its headers with the binding's descriptor, outermost; then
98
- * the layer's `middleware` runs, then the input is decoded, a failure answering `InvalidInput`,
99
- * then the implementation's `authorize` and the handler, as on every surface.
106
+ * its `actions` lists, as `ActionHttp.layer` selects them, under the protocol and JSON
107
+ * serialization the host provides. Each message of a protected rpc authenticates from its own
108
+ * headers with the binding's descriptor; then the listed `middleware` runs, the input decodes,
109
+ * and the implementation's `authorize` and the handler run, as on every surface. The layer
110
+ * fails as the builders do and needs at startup what they need; per request it owes, as router
111
+ * request markers, what the authorizers and handlers read until its `middleware` provides it,
112
+ * or, over a router-mounted protocol, the route's request context.
113
+ * A protected action's identity comes from its authentication alone.
100
114
  *
101
- * The server serves its own copy of the binding's group, whose payloads, successes and errors
102
- * are the actions' JSON and which carries no native middleware: the protocol decodes a payload before
103
- * any middleware runs, and answers one that does not decode with a defect, so each handler
104
- * runs the authentication and the middleware itself, then decodes the input. A defect is
105
- * logged with its cause, reported to the call's `ErrorReporter`s, and answered as its
106
- * request's generic failure, never failing the connection's other calls; a declared failure is
107
- * the caller's answer, and not reported.
115
+ * **Gotchas**
108
116
  *
109
- * The layer fails as the builders do, needs at startup what they need, and per request what
110
- * each implementation's authorization and the handlers of the actions it serves need, as
111
- * router request markers, until its `middleware` provides them: over a router-mounted
112
- * protocol, the route's request context does too. The identity of a protected action is
113
- * provided by its authentication alone.
117
+ * - Provide the protocol to this layer, inside what `HttpRouter.serve` serves: `RpcServer.layerProtocolWebsocket` and `layerProtocolHttp` add their route to that router, and provided around `HttpRouter.serve` they leave `HttpRouter` owed.
118
+ * - An rpc the layer does not serve ends its call with `Unknown request tag`, as does every rpc of a second `ActionRpc.layer` given the same protocol, which never receives a message: serve a protocol's rpcs from one layer, or give the second a protocol on another path.
119
+ * - A declared error, or `InvalidInput` for input that does not decode, is the call's typed failure; a refusal is answered as `Action.Unauthenticated` and `Action.Forbidden` say; the socket stays open, so a token may expire under it and a client handles `Unauthenticated` itself.
120
+ * - A defect, an undeclared failure, a middleware that throws, or a success or an error that does not encode ends that call with an `Error` whose message is `Internal server error`, its cause logged and reported to the call's `ErrorReporter`s; the connection's other calls go on.
121
+ * - An `ErrorReporter` is given only what ends a call with `Internal server error`, never a declared failure, a refusal or `InvalidInput`, unlike `ActionHttp.layer`'s `Info`: an alert counting declared failures counts none from rpcs.
122
+ * - An rpc's credential is read from its headers alone: an API key `in: "query"` never reaches authentication.
123
+ * - Over a protocol with no router, such as `RpcServer.layerProtocolSocketServer` or `layerProtocolStdio`, there is no HTTP request or route: a provider publishing a `protectedResource` leaves `HttpRouter` owed, and a verifier yielding a request service leaves it owed.
114
124
  */
115
125
  export declare function layer<const B extends Any, const Apps extends Served>(binding: B, implementations: Apps): RpcLayer<B, Apps, []>;
116
126
  export declare function layer<const B extends Any, const Apps extends Served, const O extends LayerOptions<Middleware, B["actions"][number]>>(binding: B, implementations: Apps, options: O & ServerOnly<MiddlewareOf<O, Middleware>, B["error"]> & NoInfer<Known<O, LayerOptions<Middleware>>>): RpcLayer<B, Apps, MiddlewareOf<O, Middleware>, Selected<O, B["actions"][number]>>;
117
127
  /**
118
128
  * What one call of an action `A` fails with, beside the binding's errors `E`: its declared and
119
- * built-in errors, and Effect's own `RpcClientError`. Input that does not encode is
120
- * `InvalidInput`, and nothing is sent; an answer that does not decode is a defect, as the
121
- * native client makes it.
129
+ * built-in errors, and Effect's own `RpcClientError` when the server could not be reached. An
130
+ * answer that does not decode is a defect, as the native client makes it.
122
131
  */
123
132
  export type MethodError<A extends Any$1, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | RpcClientError.RpcClientError;
124
133
  type Method<A extends Any$1, E extends BindingError> = Call<A, Effect.Effect<A["success"]["Type"], MethodError<A, E>>>;
@@ -135,15 +144,17 @@ export type Client<B extends Any> = { readonly [A in B["actions"][number] as A["
135
144
  export type ClientOptions = Omit<NonNullable<Parameters<typeof RpcClient.make>[1]>, "flatten">;
136
145
  /**
137
146
  * Effect's native `RpcClient` for a binding, one method per action taking the action's input
138
- * directly: `client.greet({ name })`, as `ActionHttp.client`'s. The argument may be omitted
139
- * when `{}` is a valid input. Requires the native client `Protocol` and a `Scope`, as
140
- * `RpcClient.make` does; headers set with `RpcClient.withHeaders` around a call go with its
141
- * message. The native client itself stays available: `RpcClient.make(Rpc.group)`.
147
+ * directly: `client.greet({ name })`, as `ActionHttp.client`'s, the argument omitted when `{}`
148
+ * is a valid input, each call failing with its `MethodError`. Requires the native client
149
+ * `Protocol` and a `Scope`, as `RpcClient.make` does; headers set with `RpcClient.withHeaders`
150
+ * around a call go with its message. The native client stays available:
151
+ * `RpcClient.make(Rpc.group)`.
152
+ *
153
+ * **Gotchas**
142
154
  *
143
- * A call fails with a declared error value (the action's own, the binding's, a built-in
144
- * `InvalidInput`, `Unauthenticated` or `Forbidden`, or on a protected rpc its descriptor's),
145
- * or with Effect's own `RpcClientError` when the server could not be reached.
155
+ * - It drops undeclared input fields as it encodes, and fails with `InvalidInput`, sending nothing, for an input that does not encode.
156
+ * - A browser WebSocket cannot set an `Authorization` header on its upgrade, so under Bearer a protected call without its own token fails with `Unauthenticated`, `A bearer token is required.`: send the token on each call with `RpcClient.withHeaders`.
157
+ * - The socket belongs to the Layer providing the protocol, not to the `Scope` around the client: provided around one call, it opens a socket for that call alone.
146
158
  */
147
159
  export declare function client<const B extends Any>(binding: B, options?: ClientOptions): Effect.Effect<Client<B>, never, RpcClient.Protocol | Scope.Scope>;
148
- //#endregion
149
160
  export {};
@@ -7,7 +7,7 @@ import { Cause, Effect, ErrorReporter, Layer, Predicate, Result, Schema, SchemaT
7
7
  import { HttpClientRequest, HttpServerRequest } from "effect/http";
8
8
  import { HttpApiBuilder } from "effect/http-api";
9
9
  import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer } from "effect/rpc";
10
- //#region src/rpc/ActionRpc.ts
10
+ // src/rpc/ActionRpc.ts
11
11
  function make(actions, options = {}) {
12
12
  const bound = bindingOf("ActionRpc binding", actions, options);
13
13
  const group = RpcGroup.make(...actions.map((action) => Rpc.make(action.name, {
@@ -30,8 +30,8 @@ var InternalError = class extends Error {
30
30
  }
31
31
  };
32
32
  var DeclaredFailure = class {
33
- json;
34
33
  [ErrorReporter.ignore] = true;
34
+ json;
35
35
  constructor(json) {
36
36
  this.json = json;
37
37
  }
@@ -93,10 +93,9 @@ function client(binding, options) {
93
93
  const native = client;
94
94
  return Object.fromEntries(binding.actions.map((action) => {
95
95
  const method = native[action.name];
96
- if (method === void 0) throw new Error(`No client method for ${action.name}`);
96
+ if (method === void 0) throw new Error(`No client method for ${action.name} (use the binding \`ActionRpc.make\` returned, whose group holds this action's rpc)`);
97
97
  return [action.name, checked(action, (payload) => method(payload))];
98
98
  }));
99
99
  });
100
100
  }
101
- //#endregion
102
101
  export { client, layer, make };
@@ -3,7 +3,6 @@ import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Offered, O
3
3
  import { Any } from "./Action.mjs";
4
4
  import { Effect, Layer, Schema } from "effect";
5
5
  import { Prompt, Tool, Toolkit } from "effect/ai";
6
- //#region src/toolkit/ActionToolkit.d.ts
7
6
  type NativeTool<A extends Any, R> = Tool.Tool<A["name"], {
8
7
  readonly parameters: Schema.toCodecJson<A["input"]>;
9
8
  readonly success: Schema.toCodecJson<A["success"]>;
@@ -17,6 +16,12 @@ type ToolFor<App, Listed extends Any> = App extends unknown ? Serving<App, Liste
17
16
  * implementations: the `layer` of a `make` call serves the tools it selected in any `toolkit`
18
17
  * of the same implementations, while two implementations never run each other's handlers,
19
18
  * even with tools of one name.
19
+ *
20
+ * **Gotchas**
21
+ *
22
+ * - Two `make` calls of one implementation share their handlers: their `layer`s, given different startup services and provided together, serve both toolkits with one of them; use separate implementations for separate services.
23
+ * - Another implementation's `layer` with tools of the same names type-checks, and the first call dies with Effect's `Cannot read properties of undefined (reading 'context')`: provide the `layer` the same `make` returned.
24
+ * - `handle(name, arguments)` on `yield* toolkit` takes the JSON a model sends; its handler starts when `handle` runs, so the caller is provided around the whole `handle(...).pipe(Effect.flatMap(Stream.runCollect))`, never around the stream alone.
20
25
  */
21
26
  export interface Tools<T extends Record<string, Tool.Any>, E, R> {
22
27
  /**
@@ -28,7 +33,7 @@ export interface Tools<T extends Record<string, Tool.Any>, E, R> {
28
33
  }
29
34
  /**
30
35
  * One call of a tool, as `needsApproval` receives it: its action's name, the action, and
31
- * the call's decoded input. Checking `name` narrows the other two to that action's.
36
+ * the call's decoded input.
32
37
  */
33
38
  export type ToolCall<A extends Any = Any> = A extends Any ? {
34
39
  readonly name: A["name"];
@@ -39,28 +44,38 @@ export type ToolCall<A extends Any = Any> = A extends Any ? {
39
44
  export interface Options<A extends Any> {
40
45
  /**
41
46
  * The actions that are tools, among the implementations' actions: `[GetUser, RenameUser]`.
42
- * Each keeps its implementation's authorization and builder. Defaults to every action of them.
47
+ * Each keeps its implementation's authorization and builder. Defaults to every action of them,
48
+ * one added to an implementation later included.
43
49
  */
44
50
  readonly actions?: ReadonlyArray<A> | undefined;
45
51
  /**
46
52
  * Whether a model's call needs approval before it runs: a boolean, or an Effect of one, for
47
53
  * each call, with Effect's native approval context. `LanguageModel` asks for approval
48
- * instead of calling the tool when it is `true`. The Effect runs in the caller's context and
49
- * requires nothing, so it reads the caller with `Effect.serviceOption`. Defaults to none.
54
+ * instead of calling the tool when it is `true`, and when the Effect fails, so a policy that
55
+ * cannot be read asks; a defect, a throw included, ends the turn as Effect's own does. The
56
+ * Effect runs in the caller's context and requires nothing, so it reads the caller with
57
+ * `Effect.serviceOption`. Defaults to none.
58
+ *
59
+ * **Gotchas**
60
+ *
61
+ * - `call` ranges over every action of the implementations, whatever `actions` lists: check `call.name` to narrow `action` and `input`.
62
+ * - Approval authorizes nothing, which stays `authorize`'s, and only a model's turn asks for it: `handle` and `ActionMcp` never do.
50
63
  */
51
- readonly needsApproval?: (call: ToolCall<A>, context: Tool.NeedsApprovalContext) => boolean | Effect.Effect<boolean>;
64
+ readonly needsApproval?: (call: ToolCall<A>, context: Tool.NeedsApprovalContext) => boolean | Effect.Effect<boolean, unknown>;
52
65
  }
53
66
  /**
54
67
  * Project implementations into Effect's native AI toolkit: one tool per action, keyed by its
55
68
  * name, of the listed `actions` or, without them, of every action of the implementations.
56
- *
57
- * Unlike MCP, calls return the action's native success/failure values directly.
58
- * Build services are needed to construct `layer`; request services are needed
59
- * when the resulting toolkit handles a call, the identity an implementation's `authorize`
60
- * reads included: the caller provides it. `needsApproval` marks the calls a model must have
61
- * approved; it authorizes nothing, which stays `authorize`'s.
62
- * Guide: `docs/ActionToolkit.md` in this package; a loop served behind a route, with approval:
69
+ * Startup services build `layer`; request services, the identity `authorize` reads included,
70
+ * are provided around each turn or call the toolkit handles. Guide: `docs/ActionToolkit.md` in
71
+ * this package; a loop served behind a route, with approval:
63
72
  * `examples/kitchen-sink/src/assistant/`.
73
+ *
74
+ * **Gotchas**
75
+ *
76
+ * - A success is a result with `isFailure: false`, `result` the value and `encodedResult` its JSON; a declared error a result with `isFailure: true` and `result` the error, which the model reads, never a raised failure; a refusal as `Action.Unauthenticated` and `Action.Forbidden` say.
77
+ * - Arguments that do not decode are a result with `isFailure: true` whose `result` is an `AiError` with `reason` a `ToolParameterValidationError`, never `InvalidInput`, and neither `authorize` nor the handler runs; undeclared arguments are dropped and the call runs.
78
+ * - A defect, or a failure the action's errors reject, is the call's defect; a success or an error that does not encode fails the call with an `AiError`, `Failed to encode result for tool …`.
64
79
  */
65
80
  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
81
  /** A tool call a model asked to have approved, which no answer has resolved yet. */
@@ -70,7 +85,10 @@ export interface Approval {
70
85
  readonly toolCallId: string;
71
86
  /** The tool's name, the action's. */
72
87
  readonly name: string;
73
- /** The call's arguments as the model sent them: show them as JSON. */
88
+ /**
89
+ * The call's arguments as the model sent them: show them as JSON, and decode them with
90
+ * `Schema.toCodecJson(<action>.input)`, the tool's parameters, only where you read a field.
91
+ */
74
92
  readonly params: unknown;
75
93
  }
76
94
  /**
@@ -85,5 +103,4 @@ export interface Approval {
85
103
  * `examples/kitchen-sink/src/assistant/handlers.ts`.
86
104
  */
87
105
  export declare const openApprovals: ({ content: messages }: Prompt.Prompt) => ReadonlyArray<Approval>;
88
- //#endregion
89
106
  export {};
@@ -1,10 +1,10 @@
1
- import { assertOnce } from "./contract/names.mjs";
1
+ import { assertOnce, heldOnce } from "./contract/names.mjs";
2
2
  import { projectedErrors } from "./contract/declared.mjs";
3
3
  import { hintsOf } from "./contract/hints.mjs";
4
4
  import { Implementation, acquire, provideHandlers, select, toList } from "./contract/implementation.mjs";
5
5
  import { Effect, Schema } from "effect";
6
6
  import { Tool, Toolkit } from "effect/ai";
7
- //#region src/toolkit/ActionToolkit.ts
7
+ // src/toolkit/ActionToolkit.ts
8
8
  const annotateIfGiven = (tool, key, value) => value === void 0 ? tool : tool.annotate(key, value);
9
9
  const annotateFromContract = (tool, action) => {
10
10
  const { readOnlyHint, destructiveHint, idempotentHint, openWorldHint } = hintsOf(action);
@@ -22,12 +22,15 @@ const jsonToolOf = (action) => Tool.make(action.name, {
22
22
  function make(apps, options) {
23
23
  const served = select(toList(apps), options?.actions);
24
24
  const needsApproval = options?.needsApproval;
25
- assertOnce("tool", served.flatMap((app) => app.actions));
26
- const toolOf = (action, app) => Object.assign(needsApproval === void 0 ? jsonToolOf(action) : jsonToolOf(action).setNeedsApproval((input, context) => needsApproval({
27
- name: action.name,
28
- action,
29
- input
30
- }, context)), { id: `${Implementation.runKey(app)}/${action.name}` });
25
+ assertOnce("tool", served.flatMap((app) => app.actions), heldOnce);
26
+ const toolOf = (action, app) => Object.assign(needsApproval === void 0 ? jsonToolOf(action) : jsonToolOf(action).setNeedsApproval((input, context) => {
27
+ const decided = needsApproval({
28
+ name: action.name,
29
+ action,
30
+ input
31
+ }, context);
32
+ return Effect.isEffect(decided) ? Effect.catch(decided, () => Effect.succeed(true)) : decided;
33
+ }), { id: `${Implementation.runKey(app)}/${action.name}` });
31
34
  const toolkit = Toolkit.make(...served.flatMap((app) => app.actions.map((action) => annotateFromContract(toolOf(action, app), action))));
32
35
  return {
33
36
  toolkit,
@@ -61,5 +64,4 @@ const openApprovals = ({ content: messages }) => {
61
64
  }];
62
65
  });
63
66
  };
64
- //#endregion
65
67
  export { make, openApprovals };