@gjermundgaraba/effect-actions 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -50
- package/dist/Action.d.mts +286 -79
- package/dist/Action.mjs +125 -23
- package/dist/ActionCli.d.mts +102 -31
- package/dist/ActionCli.mjs +114 -16
- package/dist/ActionHttp.d.mts +170 -51
- package/dist/ActionHttp.mjs +130 -79
- package/dist/ActionMcp.d.mts +71 -21
- package/dist/ActionMcp.mjs +231 -80
- package/dist/ActionToolkit.d.mts +56 -27
- package/dist/ActionToolkit.mjs +16 -5
- package/dist/Authentication.d.mts +129 -33
- package/dist/Authentication.mjs +231 -38
- package/dist/Testing.d.mts +84 -57
- package/dist/Testing.mjs +121 -79
- package/dist/internal/actions.d.mts +5 -25
- package/dist/internal/actions.mjs +75 -27
- package/dist/internal/authentication.d.mts +102 -0
- package/dist/internal/authentication.mjs +11 -0
- package/dist/internal/call.d.mts +12 -0
- package/dist/internal/call.mjs +12 -0
- package/dist/internal/cli.d.mts +36 -22
- package/dist/internal/cli.mjs +311 -33
- package/dist/internal/client.d.mts +43 -0
- package/dist/internal/client.mjs +25 -0
- package/dist/internal/console.mjs +77 -0
- package/dist/internal/declared.d.mts +13 -0
- package/dist/internal/declared.mjs +34 -0
- package/dist/internal/errors.d.mts +77 -0
- package/dist/internal/errors.mjs +79 -0
- package/dist/internal/implementation.d.mts +171 -41
- package/dist/internal/implementation.mjs +147 -29
- package/dist/internal/mcp.d.mts +14 -0
- package/dist/internal/mcp.mjs +182 -0
- package/dist/internal/media.d.mts +8 -0
- package/dist/internal/media.mjs +26 -0
- package/dist/internal/memory.d.mts +6 -0
- package/dist/internal/memory.mjs +38 -0
- package/dist/internal/refusal.mjs +67 -0
- package/dist/internal/schema-errors.mjs +20 -0
- package/dist/internal/tools.mjs +27 -57
- package/docs/Action.md +403 -73
- package/docs/ActionCli.md +281 -51
- package/docs/ActionHttp.md +267 -82
- package/docs/ActionMcp.md +232 -102
- package/docs/ActionToolkit.md +99 -35
- package/docs/Authentication.md +369 -62
- package/docs/CONTEXT.md +24 -36
- package/docs/README.md +29 -73
- package/docs/Testing.md +154 -77
- package/docs/guarantees.md +82 -43
- package/docs/setup.md +95 -27
- package/package.json +12 -20
- package/dist/ActionCatalog.d.mts +0 -30
- package/dist/ActionCatalog.mjs +0 -39
- package/dist/ActionCliClient.d.mts +0 -48
- package/dist/ActionCliClient.mjs +0 -43
- package/dist/ActionGroup.d.mts +0 -44
- package/dist/ActionGroup.mjs +0 -35
- package/dist/ActionHttpClient.d.mts +0 -33
- package/dist/ActionHttpClient.mjs +0 -16
- package/dist/TestingClient.d.mts +0 -17
- package/dist/TestingClient.mjs +0 -22
- package/dist/internal/fetchClient.d.mts +0 -6
- package/dist/internal/fetchClient.mjs +0 -8
- package/dist/internal/tools.d.mts +0 -22
- package/docs/ActionCatalog.md +0 -54
- package/docs/ActionCliClient.md +0 -63
- package/docs/ActionGroup.md +0 -128
- package/docs/ActionHttpClient.md +0 -80
package/docs/ActionToolkit.md
CHANGED
|
@@ -1,71 +1,135 @@
|
|
|
1
1
|
# ActionToolkit
|
|
2
2
|
|
|
3
3
|
Implementations as Effect's native AI `Toolkit`, for programs that call tools in-process
|
|
4
|
-
with `LanguageModel` or by hand. No server,
|
|
4
|
+
with `LanguageModel` or by hand. No server. A tool takes and gives JSON, as a model does; code
|
|
5
|
+
of your own calls the implementations typed, with `Action.client` ([Action.md](Action.md#clients)).
|
|
5
6
|
|
|
6
7
|
## API
|
|
7
8
|
|
|
8
9
|
Import `@gjermundgaraba/effect-actions/ActionToolkit`.
|
|
9
10
|
|
|
10
|
-
| API
|
|
11
|
-
|
|
|
12
|
-
| `make(
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `Options`, `Binding` | Public configuration and binding types. |
|
|
11
|
+
| API | Purpose |
|
|
12
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `make(implementations, options?)` | Project an implementation or a list; returns `{ toolkit, layer }`. |
|
|
14
|
+
| `toolkit` | A native `Toolkit`: `toolkit.tools` holds the tool definitions by name, with typed schemas, MCP hints and approval. |
|
|
15
|
+
| `layer` | The handler layer: builds handlers and built authorizers in its scope; requires build-time services. |
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
| Option | Meaning |
|
|
18
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
19
|
+
| `actions` | The actions that are tools, among the implementations': `[GetUser, RenameUser]`. Defaults to every action of them. |
|
|
20
|
+
| `needsApproval` | `(call) =>` a boolean, or an `Effect` of one: the calls `LanguageModel` asks approval for instead of running them ([Approval](#approval)). |
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
Exported types: `Tools`, what `make` returns; `Options`, what it takes; `ToolCall<A>`, the call `needsApproval` receives, `A` the union of its actions, for a rule written apart from `make`.
|
|
23
|
+
|
|
24
|
+
A local surface: each implementation's `authorize` runs before the handlers of its protected
|
|
25
|
+
actions, and the caller provides the identity.
|
|
26
|
+
|
|
27
|
+
Each tool takes and gives the JSON encoding of the action's schemas, as a model speaks and
|
|
28
|
+
as an MCP tool does, combines the action's errors with the built-in `InvalidInput`,
|
|
29
|
+
`Unauthenticated` and `Forbidden` in a native union, and uses `failureMode: "return"`. Its request requirements are its handler's and its authorizer's, and a protected
|
|
30
|
+
action's identity, whether or not anything reads it; build failures and services belong to the
|
|
31
|
+
handler layer.
|
|
23
32
|
|
|
24
33
|
## Canonical
|
|
25
34
|
|
|
26
|
-
```ts
|
|
35
|
+
```ts example=toolkit-authorized.ts
|
|
27
36
|
import { NodeRuntime } from "@effect/platform-node";
|
|
28
37
|
import { Console, Effect, Layer, Stream } from "effect";
|
|
29
38
|
import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
|
|
30
|
-
import { actors,
|
|
31
|
-
import {
|
|
39
|
+
import { actors, CurrentActor } from "./authorization.js";
|
|
40
|
+
import { double, userActions } from "./handlers.js";
|
|
32
41
|
import { Users } from "./users.js";
|
|
33
42
|
|
|
34
|
-
// The
|
|
35
|
-
const
|
|
43
|
+
// The implementations' `authorize` runs for the in-process caller as for the servers.
|
|
44
|
+
const { toolkit, layer } = ActionToolkit.make([userActions, double]);
|
|
36
45
|
|
|
37
46
|
const program = Effect.gen(function* () {
|
|
38
|
-
const tools = yield*
|
|
39
|
-
const calls = yield* tools.handle("
|
|
47
|
+
const tools = yield* toolkit;
|
|
48
|
+
const calls = yield* tools.handle("getUser", { id: "1" }); // encoded arguments
|
|
40
49
|
const results = yield* Stream.runCollect(calls);
|
|
41
50
|
yield* Console.log(results);
|
|
42
51
|
}).pipe(
|
|
43
52
|
Effect.provideService(CurrentActor, actors.alice), // identity around the whole invocation
|
|
44
|
-
Effect.provide(
|
|
53
|
+
Effect.provide(layer.pipe(Layer.provide(Users.layerMemory))), // startup services only
|
|
45
54
|
);
|
|
46
55
|
|
|
47
56
|
program.pipe(NodeRuntime.runMain);
|
|
48
57
|
```
|
|
49
58
|
|
|
50
|
-
With a model: pass `
|
|
59
|
+
With a model: pass `toolkit` as `toolkit` to `LanguageModel.generateText` and provide `layer`.
|
|
60
|
+
Beside tools of your own, or provider-defined ones such as a web search, merge the toolkits
|
|
61
|
+
natively and provide every handler layer:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
LanguageModel.generateText({ prompt, toolkit: Toolkit.merge(toolkit, WebKit) }).pipe(
|
|
65
|
+
Effect.provide(Layer.mergeAll(layer, WebKitHandlers)),
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Approval
|
|
70
|
+
|
|
71
|
+
```ts example=toolkit-approval.ts
|
|
72
|
+
import { Effect, Option } from "effect";
|
|
73
|
+
import { LanguageModel } from "effect/ai";
|
|
74
|
+
import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
|
|
75
|
+
import { type Actor, CurrentActor } from "./authorization.js";
|
|
76
|
+
import { double, userActions } from "./handlers.js";
|
|
77
|
+
|
|
78
|
+
// A model's writes wait for their caller's approval, except a rename of the caller itself:
|
|
79
|
+
// `call.name` narrows `call.input` across both implementations. Without a caller, it asks.
|
|
80
|
+
export const { toolkit, layer } = ActionToolkit.make([userActions, double], {
|
|
81
|
+
needsApproval: (call) =>
|
|
82
|
+
!call.action.readOnly &&
|
|
83
|
+
Effect.map(
|
|
84
|
+
Effect.serviceOption(CurrentActor),
|
|
85
|
+
Option.match({
|
|
86
|
+
onNone: () => true,
|
|
87
|
+
onSome: ({ id }) => call.name !== "renameUser" || call.input.id !== id,
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// One turn: the caller is provided around it, for the approval, `authorize` and the handlers.
|
|
93
|
+
export const chat = (actor: Actor, prompt: string) =>
|
|
94
|
+
LanguageModel.generateText({ prompt, toolkit }).pipe(Effect.provideService(CurrentActor, actor));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A call that needs approval ends the turn with a `tool-approval-request` part instead of a
|
|
98
|
+
result; the host answers it with a native `tool-approval-response` prompt part in the next turn.
|
|
99
|
+
Every write, whoever calls: `needsApproval: (call) => !call.action.readOnly`. A
|
|
100
|
+
toolkit made for another agent or policy from the same implementations runs with this `layer`:
|
|
101
|
+
provide it once. So does one of fewer tools, such as an agent's
|
|
102
|
+
`ActionToolkit.make(userActions, { actions: [GetUser] }).toolkit`.
|
|
51
103
|
|
|
52
104
|
## Rules
|
|
53
105
|
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
-
|
|
59
|
-
-
|
|
106
|
+
- Every action of the implementations passed becomes a tool, named after the action, with its `mcp` hints, unless `actions` lists the tools: then only those, each behind its implementation's authorizer and from its builder's one run, and an implementation holding none of them is not built. `make(implementations)` accepts one implementation or a list, such as `[userActions, double]`. The types take only actions of the implementations, and type `toolkit` and `layer`'s build services by the listed actions alone; `needsApproval`'s `call` is typed by every action of the implementations. Options whose `actions` may be absent, such as a value typed `ActionToolkit.Options<A>`, serve every action when it is absent and the listed ones when given, so their types owe what every action owes, and hold only the tools present either way ([guarantees.md](guarantees.md#names)).
|
|
107
|
+
- A result's `result` is the action's decoded success or declared failure, and its `encodedResult` the JSON a model reads, such as `"42"` for a `BigInt`. Declared failures are returned as native tool results (`failureMode: "return"`), not raised.
|
|
108
|
+
- A media field, `Action.Image`, reaches a model as base64 text: the tool's `encodedResult` is the JSON success, `{ "data": "<base64>", "mimeType": "image/png" }` for an image, as HTTP sends it, and Effect's providers send a tool's result to the model as text. The model reads no image from it, and the base64 fills its context. Leave such an action out of `actions` unless the model needs those bytes; over MCP the same action sends an image block ([ActionMcp.md](ActionMcp.md#media)).
|
|
109
|
+
- `tools.handle(name, encodedInput)` takes JSON arguments, as a model sends them: an ISO string for a `Schema.Date`, `null` for an absent `Schema.optional` field. It returns an Effect producing a result stream. The handler starts while that stream is constructed, so request services must be provided around the entire `handle(...).pipe(Effect.flatMap(Stream.runCollect))`, not only around the stream.
|
|
110
|
+
- Builders, calls and identity follow the [dependency lifetimes](guarantees.md#dependency-lifetimes), and authorization the [authorization rules](guarantees.md#authorization): `layer` builds, a call releases what it acquired when it ends, whether a model's turn or `tools.handle` makes it, and identity is supplied at invocation, never when building the layer. A protected tool's call owes its identity in its type; one call without it anyway, as from plain JavaScript, fails with `Unauthenticated` before `authorize` runs. The innermost caller provided wins: a call made under a narrower identity runs as that one.
|
|
111
|
+
- A model loop in a route yields `toolkit` in its implementation's builder, `Action.implement(Chat, Effect.map(toolkit, (tools) => ({ prompt }) => ...), { authorize })`, and passes `tools` to `LanguageModel.generateText`; the route's layer takes the toolkit's `layer`, `ActionHttp.layer(ChatHttp, chat).pipe(Layer.provide(layer))`. That is one build, in the routes' layer graph, shared with every surface serving the same implementations. `Effect.provide(layer)` in the handler builds it for every call unless a surface of the graph serves the same implementations, and `HttpRouter.provideRequest(layer)` once, but in a layer graph of its own: a second build when another surface serves them and has not built them first.
|
|
112
|
+
- Each tool carries only its own handler's request requirements, plus its authorizer's, not those of sibling actions.
|
|
113
|
+
- `needsApproval` is one function over every call a model makes, before the call runs: `(call, context) =>`, where `call` is `{ name, action, input }`, the input decoded, and `context` is Effect's native approval context. `call` is typed by every action of the implementations, whatever `actions` lists, and checking `call.name` narrows `call.action` and `call.input` to that action's, across implementations too: `(call) => call.name === "erase" && call.input.id !== "draft"`. It gives a boolean, or an `Effect` of one that requires nothing: it runs in the caller's context, so it reads the caller with `Effect.serviceOption`. Default: no tool needs approval.
|
|
114
|
+
- A `needsApproval` Effect that fails means no approval is needed, as `LanguageModel` decides natively. Its `Effect` cannot fail in its type: decide in `needsApproval` what a failure means, `Effect.orElseSucceed(() => true)` to ask when a policy cannot be read.
|
|
115
|
+
- Tools belong to what runs them, their implementation's handlers behind its authorizer: the `layer` of a `make` call serves the tools it selected in any `toolkit` of the same implementations, a toolkit of fewer `actions` included, so toolkits made per agent or per approval policy run with one handler layer, built once. Two implementations never run each other's handlers, even with tools of one name, such as two implementations of one action behind different authorizers, whatever order their layers are provided in.
|
|
116
|
+
- `toolkit` is a native `Toolkit`: `Toolkit.merge` combines it with other tools, and `yield* toolkit` gives the handled toolkit once the handler layers of all its tools are provided.
|
|
117
|
+
- `LanguageModel` enforces approval; `tools.handle` ignores it, like any caller that is not a model's turn. It is not authorization, which stays the implementation's `authorize`. MCP has no such field, so `ActionMcp` takes no such option.
|
|
60
118
|
- This is not an MCP server. Use `ActionMcp` to expose the same actions to external clients.
|
|
61
|
-
-
|
|
62
|
-
- `before` runs after successful argument decoding and before the selected handler, with its action contract. Invalid arguments skip both. Its services join each tool's requirements, supplied by the caller.
|
|
119
|
+
- Every tool declares the built-in errors ([guarantees.md](guarantees.md#wire-behavior)), so an authorizer's refusal, or a handler's built-in failure, is an ordinary returned tool failure.
|
|
63
120
|
|
|
64
121
|
## Failure modes
|
|
65
122
|
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
- `
|
|
69
|
-
-
|
|
70
|
-
- A
|
|
71
|
-
-
|
|
123
|
+
- A model reads base64 text where an image was meant: a tool's success holds an `Action.Image`, which a Toolkit gives the model as JSON text. Leave the action out of `actions`, or serve it over MCP, which sends an image block.
|
|
124
|
+
- Tool missing from the toolkit: its implementation was not passed to `make`, or `actions` leaves it out.
|
|
125
|
+
- `Listed in actions, but no implementation holds it: <names>` thrown at `make`: those actions are not among the implementations' actions, by identity. `(another contract)` marks one whose name an implementation holds, as a second copy of the contracts module makes. Pass the implementation holding it, or list the contract it implements.
|
|
126
|
+
- A defect when a tool handles a call: no `layer` of its implementation is provided, only one of another implementation with tools of the same names, such as another implementation of the same actions, which the types accept. Provide a `layer` made from the implementations the `toolkit` was made from.
|
|
127
|
+
- A job or an agent beside `HttpRouter.serve`, or a model loop in a route given `layer` with `HttpRouter.provideRequest`, reads other state than the routes, or its builder runs twice: its `layer` builds in another layer graph than the routes. Put it inside the layer the server serves, a route's toolkit in its builder ([Rules](#rules)), or provide `Action.layer` and the services they share above both ([dependency lifetimes](guarantees.md#dependency-lifetimes)).
|
|
128
|
+
- Every toolkit of an implementation answering with one set of startup services: two layers of it, built apart with different services, were provided together, and one of them serves its tools. Use separate implementations for separate services.
|
|
129
|
+
- Type error on `needsApproval`'s `Effect`, which requires a service such as the caller: `needsApproval` requires nothing. Read the caller with `Effect.serviceOption(CurrentActor)`, which gives `Option.none()` without one.
|
|
130
|
+
- `Property '<field>' does not exist` on `call.input`: `call` is a union over every action of the toolkit, and another action's input lacks the field. Check `call.name` first.
|
|
131
|
+
- `Duplicate tool: <name>` thrown at `make`: two implementations serve actions of the same name. Rename one action. `actions` tells apart only separate contracts sharing a name, listing one; two implementations of the same contract stay both listed: pass one implementation.
|
|
132
|
+
- `Service not found` for a request tag at call time: it was provided only to the stream. Provide it around the whole call effect. Providing a service to `layer` does not fail: that startup value fills in for every call that lacks its own ([guarantees.md](guarantees.md#dependency-lifetimes)), so never provide an identity there; a protected tool's call still owes it in its type.
|
|
133
|
+
- `Type 'CurrentActor' is not assignable to type 'never'` where a program calling a tool runs: the tool's action is protected, and its call owes the caller, whatever the handler reads. Provide it around the call, or around the turn.
|
|
134
|
+
- A returned failure whose `result` is an `AiError` with reason `ToolParameterValidationError` (`failureOrigin: "parameters"`), not a declared error: the arguments did not decode. Authorization and the handler did not run.
|
|
135
|
+
- Type error on `layer` requirements: a build-time service is missing. Provide its Layer before `layer`.
|