@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/dist/ActionMcp.mjs
CHANGED
|
@@ -1,98 +1,249 @@
|
|
|
1
|
-
import "./internal/
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { Anyone } from "./internal/actions.mjs";
|
|
2
|
+
import { provideHandlers, select, toList } from "./internal/implementation.mjs";
|
|
3
|
+
import { assertAuthentication, promote } from "./internal/authentication.mjs";
|
|
4
|
+
import { fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom } from "./internal/mcp.mjs";
|
|
5
|
+
import { recordStepUp } from "./internal/refusal.mjs";
|
|
6
|
+
import { logToStderr } from "./internal/console.mjs";
|
|
7
|
+
import { bindTools } from "./internal/tools.mjs";
|
|
8
|
+
import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Option, Predicate, References, Result, Schema, Stream } from "effect";
|
|
9
|
+
import { HttpRouter, HttpServerRequest } from "effect/http";
|
|
10
|
+
import { AiError, McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
|
|
5
11
|
//#region src/ActionMcp.ts
|
|
6
12
|
/**
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
13
|
+
* The revisions served over stdio: 2026-07-28 and the stateful revisions hosts open stdio
|
|
14
|
+
* with, newest first; a host asking for another is offered 2025-11-25. A success is sent as it
|
|
15
|
+
* is on each. 2026-07-28 carries any success as `structuredContent`; the adapters of 2025-11-25
|
|
16
|
+
* and 2025-06-18 carry only an object there, and list only an object-rooted `outputSchema`. A
|
|
17
|
+
* success they do not structure is text alone, a string as itself.
|
|
9
18
|
*/
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const stdioProtocols = [
|
|
20
|
+
McpProtocol.v2026_07_28,
|
|
21
|
+
McpProtocol.v2025_11_25,
|
|
22
|
+
McpProtocol.v2025_06_18
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* MCP has a JSON-only wire contract. Success is the encoded success as structured content,
|
|
26
|
+
* and its JSON as text, but for its media, each lifted into a block of its own; declared
|
|
27
|
+
* failures are returned as JSON text. The native server refuses undeclared arguments and
|
|
28
|
+
* publishes closed input schemas.
|
|
29
|
+
*/
|
|
30
|
+
const projection = {
|
|
31
|
+
label: "MCP tool",
|
|
32
|
+
tool: (tool) => tool.annotate(Tool.Strict, true)
|
|
33
|
+
};
|
|
34
|
+
/** Whether a JSON Schema is one the native server takes for a tool's arguments. */
|
|
35
|
+
const isToolJson = Schema.is(McpSchema.ToolJson);
|
|
36
|
+
/**
|
|
37
|
+
* The JSON Schema of `schema`, a JSON codec, as the native server lists it for a tool: a
|
|
38
|
+
* `$ref` at its root resolved, as MCP requires an object root, and its definitions as `$defs`.
|
|
39
|
+
* A strict one refuses undeclared keys.
|
|
40
|
+
*/
|
|
41
|
+
const toolJsonSchema = (schema, strict) => {
|
|
42
|
+
const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
|
|
43
|
+
const [scope, key] = Predicate.isString(document.schema.$ref) ? JsonPointer.parseUriFragment(document.schema.$ref) ?? [] : [];
|
|
44
|
+
const root = scope === "$defs" && key !== void 0 ? document.definitions[key] : document.schema;
|
|
45
|
+
return root === void 0 || Object.keys(document.definitions).length === 0 ? root : {
|
|
46
|
+
...root,
|
|
47
|
+
$defs: document.definitions
|
|
24
48
|
};
|
|
25
49
|
};
|
|
26
50
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
51
|
+
* Refuse, when the server is made, the actions whose input is not one object, as the root of a
|
|
52
|
+
* tool's arguments must be, naming every one, reading the JSON Schema the native
|
|
53
|
+
* server reads: not a union, an array, a scalar, nor `Schema.Struct({})`, which takes any value
|
|
54
|
+
* but `null`, where the native server dies when the layer builds, suggesting
|
|
55
|
+
* `Tool.EmptyParams`. Then refuse those holding media where no tool lifts it, as each action's
|
|
56
|
+
* `Lift` says.
|
|
30
57
|
*/
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return Effect.succeed({
|
|
37
|
-
...output,
|
|
38
|
-
properties: detach(listed.properties, field).rest,
|
|
39
|
-
...required.length === 0 ? {} : { required }
|
|
40
|
-
});
|
|
58
|
+
const assertShapes = (lifted) => {
|
|
59
|
+
const inputs = lifted.map(([action]) => action).filter((action) => !isToolJson(toolJsonSchema(Schema.toCodecJson(action.input), true))).map(({ name }) => name);
|
|
60
|
+
if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")}`);
|
|
61
|
+
const misplaced = lifted.filter(([action, lift]) => isMisplaced(action, lift)).map(([{ name }]) => name);
|
|
62
|
+
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(", ")}`);
|
|
41
63
|
};
|
|
42
64
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* the field, is the native one.
|
|
65
|
+
* What the native server answers a call that fails other than as its contract declares: a
|
|
66
|
+
* defect, an invalid success or error, or an unknown tool.
|
|
46
67
|
*/
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
const internalError = "Tool execution failed due to an internal server error.";
|
|
69
|
+
/** A tool's error result, `text` its one text block. */
|
|
70
|
+
const errorResult = (text) => new McpSchema.CallToolResult({
|
|
71
|
+
isError: true,
|
|
72
|
+
content: [{
|
|
73
|
+
type: "text",
|
|
74
|
+
text
|
|
75
|
+
}]
|
|
76
|
+
});
|
|
77
|
+
/** The one text block holding `encoded`'s JSON, none for `undefined`, as the native server sends it. */
|
|
78
|
+
const textOf = (encoded) => encoded === void 0 ? [] : [{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: JSON.stringify(encoded)
|
|
81
|
+
}];
|
|
82
|
+
/** The content block a lifted media value is sent as, naming its field where it has one. */
|
|
83
|
+
const blockOf = ({ kind, field, value }) => ({
|
|
84
|
+
type: kind,
|
|
85
|
+
data: value.data,
|
|
86
|
+
mimeType: value.mimeType,
|
|
87
|
+
...field === void 0 ? {} : { _meta: { [fieldKey]: field } }
|
|
88
|
+
});
|
|
89
|
+
/** Why a tool call's arguments failed to decode, as the native server tells it, if they did. */
|
|
90
|
+
const invalidParameters = (error) => AiError.isAiError(error) && Predicate.isTagged(error.reason, "ToolParameterValidationError") ? error.reason : void 0;
|
|
91
|
+
const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, References.CurrentLogLevel);
|
|
92
|
+
/**
|
|
93
|
+
* Register the tools of `toolkit` on the endpoint's registry, as `McpServer.registerToolkit`
|
|
94
|
+
* does, lifting the media `lifts` finds in a tool's success into blocks of their own. The native
|
|
95
|
+
* registration sends every success as structured content and one JSON text block, and takes
|
|
96
|
+
* no option, so this one follows it: its decode options, failure classification, defect
|
|
97
|
+
* scrubbing and listing, and the request services it leaves out of what it retains. A tool
|
|
98
|
+
* whose action holds no media is registered as the native one would be: a test compares their
|
|
99
|
+
* listings and results, not the services each call runs with.
|
|
100
|
+
*/
|
|
101
|
+
const registerTools = Effect.fnUntraced(function* (toolkit, lifts) {
|
|
102
|
+
const registry = yield* McpServer.McpServer;
|
|
103
|
+
const built = yield* toolkit.pipe(Effect.updateContext((context) => Context.merge(context, Context.mergeAll(...Object.values(toolkit.tools).flatMap((tool) => {
|
|
104
|
+
const key = Context.Service(tool.id);
|
|
105
|
+
return Option.toArray(Context.getOption(context, key)).map((handler) => Context.make(key, {
|
|
106
|
+
...handler,
|
|
107
|
+
context: omitRequestServices(handler.context)
|
|
108
|
+
}));
|
|
109
|
+
})))));
|
|
110
|
+
const services = omitRequestServices(yield* Effect.context());
|
|
111
|
+
const internalToolError = (cause) => {
|
|
112
|
+
const failure = Cause.findFail(cause);
|
|
113
|
+
return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(cause).pipe(Effect.andThen(Effect.provideContext(ErrorReporter.report(cause), services)), Effect.as(errorResult(internalError)));
|
|
114
|
+
};
|
|
115
|
+
const handleCause = (cause) => {
|
|
116
|
+
const failure = Cause.findFail(cause);
|
|
117
|
+
if (Result.isSuccess(failure)) {
|
|
118
|
+
const { error } = failure.success;
|
|
119
|
+
const invalid = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin) === "parameters" ? invalidParameters(error) : void 0;
|
|
120
|
+
if (invalid !== void 0) return Effect.fail(new McpSchema.InvalidParams({ message: invalid.message }));
|
|
121
|
+
}
|
|
122
|
+
return internalToolError(cause);
|
|
123
|
+
};
|
|
124
|
+
const registrations = yield* Effect.forEach(Object.entries(built.tools), ([name, tool]) => Effect.gen(function* () {
|
|
125
|
+
const strict = Tool.getStrictMode(tool) === true;
|
|
126
|
+
const decodeOptions = {
|
|
127
|
+
onExcessProperty: strict ? "error" : "ignore",
|
|
128
|
+
errors: "all"
|
|
129
|
+
};
|
|
130
|
+
const lift = lifts.get(name);
|
|
131
|
+
const media = lift === void 0 || lift.fields.length === 0 ? void 0 : lift;
|
|
132
|
+
const structured = media === void 0 ? tool.successSchema : media.rest === void 0 ? void 0 : Schema.toCodecJson(media.rest);
|
|
133
|
+
const outputSchema = structured === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(toolJsonSchema(structured, false)).pipe(Effect.orDie);
|
|
134
|
+
const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(toolJsonSchema(tool.parametersSchema, strict)).pipe(Effect.orDie);
|
|
135
|
+
const lifted = new Set(media?.fields.map(({ name }) => name));
|
|
136
|
+
const succeeded = ({ encodedResult, result }) => {
|
|
137
|
+
if (media === void 0) return new McpSchema.CallToolResult({
|
|
138
|
+
isError: false,
|
|
139
|
+
structuredContent: encodedResult,
|
|
140
|
+
content: textOf(encodedResult)
|
|
141
|
+
});
|
|
142
|
+
const blocks = liftedFrom(media, result).map(blockOf);
|
|
143
|
+
if (structured === void 0) return new McpSchema.CallToolResult({
|
|
144
|
+
isError: false,
|
|
145
|
+
content: blocks
|
|
146
|
+
});
|
|
147
|
+
const rest = Predicate.isObject(encodedResult) ? Object.fromEntries(Object.entries(encodedResult).filter(([key]) => !lifted.has(key))) : encodedResult;
|
|
148
|
+
return new McpSchema.CallToolResult({
|
|
149
|
+
isError: false,
|
|
150
|
+
structuredContent: rest,
|
|
151
|
+
content: [...textOf(rest), ...blocks]
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
tool: new McpSchema.Tool({
|
|
156
|
+
name,
|
|
157
|
+
description: Tool.getDescription(tool),
|
|
158
|
+
inputSchema,
|
|
159
|
+
outputSchema,
|
|
160
|
+
annotations: {
|
|
161
|
+
...Context.getOption(tool.annotations, Tool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
|
|
162
|
+
readOnlyHint: Context.get(tool.annotations, Tool.Readonly),
|
|
163
|
+
destructiveHint: Context.get(tool.annotations, Tool.Destructive),
|
|
164
|
+
idempotentHint: Context.get(tool.annotations, Tool.Idempotent),
|
|
165
|
+
openWorldHint: Context.get(tool.annotations, Tool.OpenWorld)
|
|
166
|
+
},
|
|
167
|
+
_meta: Context.getOrUndefined(tool.annotations, Tool.Meta)
|
|
168
|
+
}),
|
|
169
|
+
annotations: tool.annotations,
|
|
170
|
+
handle: (payload) => built.handle(name, payload ?? {}, void 0, decodeOptions).pipe(Stream.unwrap, Stream.runLast, Effect.flatMap(Effect.fromOption), Effect.flatMap((result) => !result.isFailure ? Effect.succeed(succeeded(result)) : result.failureOrigin === "handler" ? Effect.succeed(new McpSchema.CallToolResult({
|
|
171
|
+
isError: true,
|
|
172
|
+
content: textOf(result.encodedResult)
|
|
173
|
+
})) : Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result")))), Effect.catchCause(handleCause), Effect.provideContext(services))
|
|
174
|
+
};
|
|
175
|
+
}));
|
|
176
|
+
for (const registration of registrations) yield* registry.addTool(registration);
|
|
177
|
+
});
|
|
178
|
+
const server = (apps, transport, features = Layer.empty, authentication) => {
|
|
179
|
+
const binding = bindTools(apps, authentication === void 0 ? projection : {
|
|
180
|
+
...projection,
|
|
181
|
+
handler: (run, action) => (input) => action.caller === Anyone ? run(input) : recordStepUp(promote(authentication, run(input)))
|
|
182
|
+
});
|
|
183
|
+
const lifted = apps.flatMap((app) => app.actions.map((action) => [action, liftOf(action.success)]));
|
|
184
|
+
assertShapes(lifted);
|
|
185
|
+
const lifts = new Map(lifted.map(([{ name }, lift]) => [name, lift]));
|
|
186
|
+
const register = Effect.gen(function* () {
|
|
187
|
+
const registry = yield* McpServer.McpServer;
|
|
188
|
+
const claimed = registry.tools.flatMap(({ tool }) => lifts.has(tool.name) ? [tool.name] : []);
|
|
189
|
+
if (claimed.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${claimed.join(", ")}, claimed by an action and a native feature`));
|
|
190
|
+
const handlers = yield* Layer.build(binding.layer);
|
|
191
|
+
yield* registerTools(binding.toolkit, lifts).pipe(Effect.setContext(Context.add(handlers, McpServer.McpServer, registry)));
|
|
62
192
|
});
|
|
193
|
+
return Layer.effectDiscard(register).pipe(Layer.provide(features), Layer.provide(transport), Layer.fresh, provideHandlers(apps));
|
|
63
194
|
};
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
195
|
+
/**
|
|
196
|
+
* What an anonymous caller of a mixed endpoint may send besides a public tool's call:
|
|
197
|
+
* discovering the server and its tools, which the contracts describe, and cancelling.
|
|
198
|
+
* Everything else, a native feature's listing, completion or subscription among them, and
|
|
199
|
+
* any method a later revision adds, authenticates.
|
|
200
|
+
*/
|
|
201
|
+
const anonymous = /* @__PURE__ */ new Set([
|
|
202
|
+
"server/discover",
|
|
203
|
+
"tools/list",
|
|
204
|
+
"notifications/cancelled"
|
|
205
|
+
]);
|
|
206
|
+
/**
|
|
207
|
+
* Whether a request to an endpoint serving `actions` must authenticate before the native
|
|
208
|
+
* server reads its body. An endpoint whose every tool is protected authenticates every
|
|
209
|
+
* request, so a host signs in when it connects. Otherwise the request's routing headers
|
|
210
|
+
* decide, which the native server refuses when they disagree with the body: discovery and a
|
|
211
|
+
* call of a public tool authenticate only a caller presenting credentials, and everything
|
|
212
|
+
* else authenticates, so native features are protected too. A request whose headers name
|
|
213
|
+
* no public tool as it is authenticates: it fails closed.
|
|
214
|
+
*/
|
|
215
|
+
const requiresAuthentication = (actions) => {
|
|
216
|
+
const open = new Set(actions.filter(({ caller }) => caller === Anyone).map(({ name }) => name));
|
|
217
|
+
return (headers) => {
|
|
218
|
+
if (open.size === 0) return true;
|
|
219
|
+
const method = headers["mcp-method"];
|
|
220
|
+
if (method === void 0) return true;
|
|
221
|
+
if (anonymous.has(method)) return false;
|
|
222
|
+
if (method !== "tools/call") return true;
|
|
223
|
+
const name = headers["mcp-name"];
|
|
224
|
+
return name === void 0 || !open.has(name);
|
|
225
|
+
};
|
|
83
226
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
227
|
+
function layerHttp(apps, { actions, features, path, authentication, ...options }) {
|
|
228
|
+
const selected = select(toList(apps), actions);
|
|
229
|
+
const served = selected.flatMap((app) => app.actions);
|
|
230
|
+
assertAuthentication(served, authentication);
|
|
231
|
+
const transport = server(selected, McpServer.layerHttp({
|
|
87
232
|
...options,
|
|
88
|
-
|
|
89
|
-
|
|
233
|
+
path: path ?? "/mcp",
|
|
234
|
+
protocols: [httpProtocol]
|
|
235
|
+
}), features, authentication);
|
|
236
|
+
if (authentication === void 0) return transport;
|
|
237
|
+
const required = requiresAuthentication(served);
|
|
238
|
+
const middleware = HttpRouter.middleware(Effect.map(authentication["~provider"], (runtime) => (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => runtime.http(route, !required(request.headers)))));
|
|
239
|
+
return transport.pipe(Layer.provide(middleware.layer));
|
|
90
240
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
241
|
+
function runStdio(apps, { actions, features, ...options }) {
|
|
242
|
+
const transport = server(select(toList(apps), actions), McpServer.layerStdio({
|
|
93
243
|
...options,
|
|
94
|
-
protocols
|
|
95
|
-
}));
|
|
244
|
+
protocols: stdioProtocols
|
|
245
|
+
}), features);
|
|
246
|
+
return Layer.launch(transport).pipe(logToStderr, Effect.forkChild, Effect.flatMap(Fiber.await), Effect.flatMap((exit) => Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause) ? Effect.failCause(exit.cause) : Effect.void));
|
|
96
247
|
}
|
|
97
248
|
//#endregion
|
|
98
|
-
export { layerHttp,
|
|
249
|
+
export { layerHttp, runStdio };
|
package/dist/ActionToolkit.d.mts
CHANGED
|
@@ -1,40 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Layer, Schema } from "effect";
|
|
1
|
+
import { BuiltIns } from "./internal/errors.mjs";
|
|
2
|
+
import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Offered, OptionalUnless, RequestOf, SelectedOf, Served, Serving } from "./internal/implementation.mjs";
|
|
3
|
+
import { Action_d_exports, Any } from "./Action.mjs";
|
|
4
|
+
import { Effect, Layer, Schema } from "effect";
|
|
5
5
|
import { Tool, Toolkit } from "effect/ai";
|
|
6
6
|
//#region src/ActionToolkit.d.ts
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
readonly
|
|
12
|
-
} ? Tool.Tool<Name, {
|
|
13
|
-
readonly parameters: A["input"];
|
|
14
|
-
readonly success: A["success"];
|
|
15
|
-
readonly failure: Schema.Union<ReadonlyArray<A["errors"][number] | E>>;
|
|
7
|
+
/** A native tool named after its action, taking and giving JSON. */
|
|
8
|
+
type NativeTool<A extends Any, R> = Tool.Tool<A["name"], {
|
|
9
|
+
readonly parameters: Schema.toCodecJson<A["input"]>;
|
|
10
|
+
readonly success: Schema.toCodecJson<A["success"]>;
|
|
11
|
+
readonly failure: Schema.toCodecJson<Schema.Union<ReadonlyArray<A["error"][number] | BuiltIns>>>;
|
|
16
12
|
readonly failureMode: "return";
|
|
17
|
-
}, R
|
|
18
|
-
type HandlersOf<App extends AnyImplementation> = App extends Implementation<any, infer H, any, any> ? H : never;
|
|
13
|
+
}, R>;
|
|
19
14
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* The tool of each action of `App` among `Listed`. A tool needs what its handler and its
|
|
16
|
+
* implementation's authorization need.
|
|
22
17
|
*/
|
|
23
|
-
type
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
type ToolFor<App, Listed extends Any> = App extends unknown ? Serving<App, Listed> extends (infer A extends Any) ? A extends Any ? NativeTool<A, RequestOf<App, A>> : never : never : never;
|
|
19
|
+
/**
|
|
20
|
+
* Native tools bound to their action implementations: a native `Toolkit` and the layer of
|
|
21
|
+
* its handlers, for `LanguageModel`, `Toolkit.merge` or `handle`. Tools belong to their
|
|
22
|
+
* implementations: the `layer` of a `make` call serves the tools it selected in any `toolkit`
|
|
23
|
+
* of the same implementations, while two implementations never run each other's handlers,
|
|
24
|
+
* even with tools of one name.
|
|
25
|
+
*/
|
|
26
|
+
export interface Tools<T extends Record<string, Tool.Any>, E, R> {
|
|
27
|
+
/**
|
|
28
|
+
* The native toolkit: its `tools` are the definitions, by name, with schemas, hints and approval.
|
|
29
|
+
*/
|
|
30
|
+
readonly toolkit: Toolkit.Toolkit<T>;
|
|
29
31
|
/** Acquires handlers once in the layer scope; handler requirements remain at invocation. */
|
|
30
|
-
readonly layer: Layer.Layer<Tool.HandlersFor<
|
|
32
|
+
readonly layer: Layer.Layer<Tool.HandlersFor<T>, E, R>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One call of a tool, as `needsApproval` receives it: its action's name, the action, and
|
|
36
|
+
* the call's decoded input. Checking `name` narrows the other two to that action's.
|
|
37
|
+
*/
|
|
38
|
+
export type ToolCall<A extends Any = Any> = A extends Any ? {
|
|
39
|
+
readonly name: A["name"];
|
|
40
|
+
readonly action: A;
|
|
41
|
+
readonly input: A["input"]["Type"];
|
|
42
|
+
} : never;
|
|
43
|
+
/** How `make` projects its tools, the actions `A`. */
|
|
44
|
+
export interface Options<A extends Any> {
|
|
45
|
+
/**
|
|
46
|
+
* The actions that are tools, among the implementations' actions: `[GetUser, RenameUser]`.
|
|
47
|
+
* Each keeps its implementation's authorization and builder. Defaults to every action of them.
|
|
48
|
+
*/
|
|
49
|
+
readonly actions?: ReadonlyArray<A> | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a model's call needs approval before it runs: a boolean, or an Effect of one, for
|
|
52
|
+
* each call, with Effect's native approval context. `LanguageModel` asks for approval
|
|
53
|
+
* instead of calling the tool when it is `true`. The Effect runs in the caller's context and
|
|
54
|
+
* requires nothing, so it reads the caller with `Effect.serviceOption`. Defaults to none.
|
|
55
|
+
*/
|
|
56
|
+
readonly needsApproval?: (call: ToolCall<A>, context: Tool.NeedsApprovalContext) => boolean | Effect.Effect<boolean>;
|
|
31
57
|
}
|
|
32
58
|
/**
|
|
33
|
-
* Project implementations into Effect's native AI toolkit
|
|
59
|
+
* Project implementations into Effect's native AI toolkit: one tool per action, keyed by its
|
|
60
|
+
* name, of the listed `actions` or, without them, of every action of the implementations.
|
|
34
61
|
*
|
|
35
62
|
* Unlike MCP, calls return the action's native success/failure values directly.
|
|
36
63
|
* Build services are needed to construct `layer`; request services are needed
|
|
37
|
-
* when the resulting toolkit handles a call
|
|
64
|
+
* when the resulting toolkit handles a call, the identity an implementation's `authorize`
|
|
65
|
+
* reads included: the caller provides it. `needsApproval` marks the calls a model must have
|
|
66
|
+
* approved; it authorizes nothing, which stays `authorize`'s.
|
|
38
67
|
*/
|
|
39
|
-
export declare function make<const Apps extends
|
|
68
|
+
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>>>;
|
|
40
69
|
//#endregion
|
package/dist/ActionToolkit.mjs
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import "./internal/implementation.mjs";
|
|
1
|
+
import { Implementation, provideHandlers, select, toList } from "./internal/implementation.mjs";
|
|
2
2
|
import { bindTools } from "./internal/tools.mjs";
|
|
3
|
-
import "effect";
|
|
4
|
-
import "effect/ai";
|
|
5
3
|
//#region src/ActionToolkit.ts
|
|
6
|
-
function make(apps, options
|
|
7
|
-
|
|
4
|
+
function make(apps, options) {
|
|
5
|
+
const served = select(toList(apps), options?.actions);
|
|
6
|
+
const needsApproval = options?.needsApproval;
|
|
7
|
+
const { toolkit, layer } = bindTools(served, {
|
|
8
|
+
label: "tool",
|
|
9
|
+
tool: (tool, action, app) => Object.assign(needsApproval === void 0 ? tool : tool.setNeedsApproval((input, context) => needsApproval({
|
|
10
|
+
name: action.name,
|
|
11
|
+
action,
|
|
12
|
+
input
|
|
13
|
+
}, context)), { id: `${Implementation.runKey(app)}/${action.name}` })
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
toolkit,
|
|
17
|
+
layer: layer.pipe(provideHandlers(served))
|
|
18
|
+
};
|
|
8
19
|
}
|
|
9
20
|
//#endregion
|
|
10
21
|
export { make };
|