@gjermundgaraba/effect-actions 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -41
- package/dist/Action.d.mts +31 -117
- package/dist/Action.mjs +14 -53
- package/dist/ActionCli.d.mts +60 -28
- package/dist/ActionCli.mjs +54 -48
- package/dist/ActionHttp.d.mts +18 -67
- package/dist/ActionHttp.mjs +20 -70
- package/dist/ActionMcp.d.mts +23 -29
- package/dist/ActionMcp.mjs +56 -114
- package/dist/ActionRpc.d.mts +147 -0
- package/dist/ActionRpc.mjs +113 -0
- package/dist/ActionToolkit.d.mts +28 -9
- package/dist/ActionToolkit.mjs +31 -4
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +30 -23
- package/dist/Testing.mjs +27 -44
- package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
- package/dist/authentication/provider.mjs +12 -0
- package/dist/authentication/refusal.mjs +36 -0
- package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
- package/dist/cli/commands.mjs +302 -0
- package/dist/{internal → contract}/call.d.mts +2 -2
- package/dist/contract/call.mjs +10 -0
- package/dist/{internal → contract}/errors.d.mts +13 -10
- package/dist/{internal → contract}/errors.mjs +9 -14
- package/dist/{internal → contract}/implementation.d.mts +37 -21
- package/dist/{internal → contract}/implementation.mjs +18 -49
- package/dist/contract/media.d.mts +13 -0
- package/dist/contract/media.mjs +18 -0
- package/dist/contract/rules.d.mts +25 -0
- package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
- package/dist/{internal → http}/client.d.mts +7 -9
- package/dist/http/client.mjs +17 -0
- package/dist/http/declared.mjs +20 -0
- package/dist/http/schema-errors.mjs +9 -0
- package/dist/mcp/protocol.d.mts +18 -0
- package/dist/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
- package/dist/stdio/console.mjs +64 -0
- package/dist/{internal → testing}/memory.d.mts +1 -1
- package/dist/{internal → testing}/memory.mjs +5 -17
- package/dist/toolkit/tools.mjs +29 -0
- package/docs/Action-failures.md +47 -0
- package/docs/Action-in-process.md +93 -0
- package/docs/Action.md +211 -426
- package/docs/ActionCli-failures.md +104 -0
- package/docs/ActionCli.md +208 -293
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +205 -282
- package/docs/ActionMcp-failures.md +31 -0
- package/docs/ActionMcp.md +222 -271
- package/docs/ActionRpc-failures.md +21 -0
- package/docs/ActionRpc.md +179 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +103 -119
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +143 -392
- package/docs/CONTEXT.md +40 -24
- package/docs/README.md +75 -35
- package/docs/Testing-failures.md +21 -0
- package/docs/Testing.md +193 -151
- package/docs/guarantees.md +35 -96
- package/docs/setup-failures.md +37 -0
- package/docs/setup.md +90 -92
- package/examples/README.md +68 -0
- package/examples/assistant/README.md +92 -0
- package/examples/assistant/src/agent.ts +92 -0
- package/examples/assistant/src/cli.ts +34 -0
- package/examples/assistant/src/commands.ts +25 -0
- package/examples/assistant/src/identity/Identity.ts +33 -0
- package/examples/assistant/src/identity/authorize.ts +24 -0
- package/examples/assistant/src/identity/handlers.ts +10 -0
- package/examples/assistant/src/identity/members.ts +9 -0
- package/examples/assistant/src/identity/permissions.ts +23 -0
- package/examples/assistant/src/scripted.ts +23 -0
- package/examples/assistant/src/tasks/Tasks.ts +63 -0
- package/examples/assistant/src/tasks/handlers.ts +25 -0
- package/examples/assistant/src/tasks/store.ts +96 -0
- package/examples/assistant/src/toolkit.ts +30 -0
- package/examples/assistant/test/commands.test.ts +57 -0
- package/examples/assistant/test/identity/identity.test.ts +43 -0
- package/examples/assistant/test/tasks/store.test.ts +48 -0
- package/examples/assistant/test/tasks/tasks.test.ts +75 -0
- package/examples/assistant/test/toolkit.test.ts +121 -0
- package/examples/kitchen-sink/README.md +318 -0
- package/examples/kitchen-sink/src/Actions.ts +19 -0
- package/examples/kitchen-sink/src/Api.ts +24 -0
- package/examples/kitchen-sink/src/Rpc.ts +12 -0
- package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
- package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
- package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
- package/examples/kitchen-sink/src/admin/grants.ts +22 -0
- package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
- package/examples/kitchen-sink/src/admin/keys.ts +8 -0
- package/examples/kitchen-sink/src/agent.ts +47 -0
- package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
- package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
- package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
- package/examples/kitchen-sink/src/assistant/handlers.ts +126 -0
- package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
- package/examples/kitchen-sink/src/attachments/download.ts +49 -0
- package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
- package/examples/kitchen-sink/src/attachments/store.ts +58 -0
- package/examples/kitchen-sink/src/browser.ts +33 -0
- package/examples/kitchen-sink/src/cli.ts +85 -0
- package/examples/kitchen-sink/src/config.ts +18 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
- package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
- package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
- package/examples/kitchen-sink/src/identity/directory.ts +52 -0
- package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
- package/examples/kitchen-sink/src/identity/members.ts +13 -0
- package/examples/kitchen-sink/src/identity/quota.ts +62 -0
- package/examples/kitchen-sink/src/issues/Issues.ts +111 -0
- package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
- package/examples/kitchen-sink/src/issues/guide.ts +37 -0
- package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
- package/examples/kitchen-sink/src/issues/store.ts +96 -0
- package/examples/kitchen-sink/src/issues/write-limit.ts +42 -0
- package/examples/kitchen-sink/src/memory.ts +30 -0
- package/examples/kitchen-sink/src/ops.ts +42 -0
- package/examples/kitchen-sink/src/request-policy.ts +63 -0
- package/examples/kitchen-sink/src/routes.ts +120 -0
- package/examples/kitchen-sink/src/sdk.ts +46 -0
- package/examples/kitchen-sink/src/seed.ts +65 -0
- package/examples/kitchen-sink/src/server.ts +62 -0
- package/examples/kitchen-sink/src/stdio.ts +34 -0
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
- package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
- package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
- package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
- package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
- package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
- package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
- package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
- package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
- package/examples/kitchen-sink/test/routes.test.ts +123 -0
- package/examples/kitchen-sink/test/support/scripted.ts +40 -0
- package/examples/live-rpc/README.md +107 -0
- package/examples/live-rpc/src/Rpc.ts +8 -0
- package/examples/live-rpc/src/browser.ts +98 -0
- package/examples/live-rpc/src/chat/Chat.ts +77 -0
- package/examples/live-rpc/src/chat/authorize.ts +16 -0
- package/examples/live-rpc/src/chat/handlers.ts +50 -0
- package/examples/live-rpc/src/chat/room.ts +23 -0
- package/examples/live-rpc/src/chat/store.ts +54 -0
- package/examples/live-rpc/src/client.ts +37 -0
- package/examples/live-rpc/src/identity/Identity.ts +19 -0
- package/examples/live-rpc/src/identity/members.ts +8 -0
- package/examples/live-rpc/src/identity/verifier.ts +23 -0
- package/examples/live-rpc/src/routes.ts +22 -0
- package/examples/live-rpc/src/server.ts +43 -0
- package/examples/live-rpc/test/browser.test.ts +105 -0
- package/examples/live-rpc/test/chat/chat.test.ts +69 -0
- package/examples/live-rpc/test/routes.test.ts +99 -0
- package/examples/live-rpc/test/server.test.ts +75 -0
- package/examples/mcp-tools/README.md +141 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +121 -0
- package/examples/mcp-tools/src/printers/fake.ts +115 -0
- package/examples/mcp-tools/src/printers/farm.ts +136 -0
- package/examples/mcp-tools/src/printers/handlers.ts +29 -0
- package/examples/mcp-tools/src/routes.ts +52 -0
- package/examples/mcp-tools/src/server.ts +31 -0
- package/examples/mcp-tools/src/stdio.ts +26 -0
- package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
- package/examples/mcp-tools/test/routes.test.ts +202 -0
- package/examples/notes-service/README.md +111 -0
- package/examples/notes-service/src/Api.ts +13 -0
- package/examples/notes-service/src/browser.ts +41 -0
- package/examples/notes-service/src/cli.ts +22 -0
- package/examples/notes-service/src/commands.ts +137 -0
- package/examples/notes-service/src/identity/Identity.ts +31 -0
- package/examples/notes-service/src/identity/authorize.ts +18 -0
- package/examples/notes-service/src/identity/handlers.ts +10 -0
- package/examples/notes-service/src/identity/users.ts +9 -0
- package/examples/notes-service/src/identity/verifier.ts +36 -0
- package/examples/notes-service/src/notes/Notes.ts +119 -0
- package/examples/notes-service/src/notes/handlers.ts +39 -0
- package/examples/notes-service/src/notes/store.ts +84 -0
- package/examples/notes-service/src/routes.ts +45 -0
- package/examples/notes-service/src/server.ts +26 -0
- package/examples/notes-service/src/status/Status.ts +10 -0
- package/examples/notes-service/src/status/handlers.ts +14 -0
- package/examples/notes-service/src/stdio.ts +25 -0
- package/examples/notes-service/test/commands.test.ts +288 -0
- package/examples/notes-service/test/notes/notes.test.ts +103 -0
- package/examples/notes-service/test/routes.test.ts +84 -0
- package/examples/quickstart/README.md +31 -0
- package/examples/quickstart/src/Api.ts +5 -0
- package/examples/quickstart/src/client.ts +13 -0
- package/examples/quickstart/src/greeting/Greeting.ts +12 -0
- package/examples/quickstart/src/greeting/handlers.ts +6 -0
- package/examples/quickstart/src/routes.ts +11 -0
- package/examples/quickstart/src/server.ts +16 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
- package/package.json +27 -9
- package/dist/internal/actions.d.mts +0 -9
- package/dist/internal/authentication.mjs +0 -11
- package/dist/internal/call.mjs +0 -12
- package/dist/internal/cli.mjs +0 -328
- package/dist/internal/client.mjs +0 -25
- package/dist/internal/console.mjs +0 -77
- package/dist/internal/declared.d.mts +0 -13
- package/dist/internal/declared.mjs +0 -34
- package/dist/internal/mcp.d.mts +0 -14
- package/dist/internal/media.d.mts +0 -8
- package/dist/internal/media.mjs +0 -26
- package/dist/internal/refusal.mjs +0 -67
- package/dist/internal/schema-errors.mjs +0 -20
- package/dist/internal/tools.mjs +0 -44
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { Unauthenticated } from "./errors.mjs";
|
|
2
|
-
import { Anyone } from "./
|
|
2
|
+
import { Anyone, assertOnce } from "./rules.mjs";
|
|
3
3
|
import { Array, Context, Effect, Layer, Option } from "effect";
|
|
4
|
-
//#region src/
|
|
5
|
-
/**
|
|
6
|
-
* A random key segment, unique to its caller even across installed copies of this module,
|
|
7
|
-
* where a counter of each copy would repeat another's.
|
|
8
|
-
*/
|
|
4
|
+
//#region src/contract/implementation.ts
|
|
9
5
|
const uniqueKey = () => Math.random().toString(36).slice(2);
|
|
10
|
-
/** What `build` makes, as a layer of its own. */
|
|
11
6
|
const memoized = (build) => {
|
|
12
7
|
const key = Context.Service(`effect-actions/Implementation/${uniqueKey()}`);
|
|
13
8
|
return {
|
|
@@ -76,12 +71,7 @@ var Implementation = class Implementation {
|
|
|
76
71
|
return app;
|
|
77
72
|
}
|
|
78
73
|
};
|
|
79
|
-
/** A surface's implementations as a list. */
|
|
80
74
|
const toList = (served) => Array.ensure(served);
|
|
81
|
-
/**
|
|
82
|
-
* Refuse an action of `listed` that `held` lacks, by identity, as a stale list, marking one
|
|
83
|
-
* of a held name as another contract's.
|
|
84
|
-
*/
|
|
85
75
|
const assertHeld = (why, listed, held) => {
|
|
86
76
|
const missing = listed.filter((action) => !held.includes(action));
|
|
87
77
|
if (missing.length > 0) {
|
|
@@ -89,12 +79,6 @@ const assertHeld = (why, listed, held) => {
|
|
|
89
79
|
throw new Error(`Listed in actions, but ${why}: ${missing.map(({ name }) => names.has(name) ? `${name} (another contract)` : name).join(", ")}`);
|
|
90
80
|
}
|
|
91
81
|
};
|
|
92
|
-
/**
|
|
93
|
-
* Each of `apps` narrowed to the actions `listed` holds, matched by identity, behind its own
|
|
94
|
-
* authorization and sharing its builder; one holding none of them is
|
|
95
|
-
* left out, and not built. All of `apps` as they are when nothing is listed. A listed action
|
|
96
|
-
* none of them holds is refused, as a stale list or another contract of its name.
|
|
97
|
-
*/
|
|
98
82
|
const select = (apps, listed) => {
|
|
99
83
|
if (listed === void 0) return apps;
|
|
100
84
|
assertHeld("no implementation holds it", listed, apps.flatMap((app) => app.actions));
|
|
@@ -104,32 +88,24 @@ const select = (apps, listed) => {
|
|
|
104
88
|
return own.length === app.actions.length ? [app] : [Implementation.share(own, app)];
|
|
105
89
|
});
|
|
106
90
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
const servedBy = (what, bound, apps, listed) => {
|
|
92
|
+
if (listed !== void 0) assertHeld("the binding does not hold it", listed, bound);
|
|
93
|
+
const held = apps.flatMap((app) => app.actions);
|
|
94
|
+
const served = select(apps, listed ?? bound.filter((action) => held.includes(action)));
|
|
95
|
+
const actions = served.flatMap((app) => app.actions);
|
|
96
|
+
if (actions.length === 0 && listed === void 0 && apps.length > 0) {
|
|
97
|
+
const names = new Set(bound.map(({ name }) => name));
|
|
98
|
+
throw new Error(`No action of these implementations is in this ${what}: ${held.map(({ name }) => names.has(name) ? `${name} (another contract)` : name).join(", ") || "none"}`);
|
|
99
|
+
}
|
|
100
|
+
assertOnce("served action", actions);
|
|
101
|
+
return served;
|
|
102
|
+
};
|
|
111
103
|
const provideHandlers = (apps) => (layer) => {
|
|
112
104
|
const [first, ...rest] = apps.map((app) => Implementation.layerOf(app));
|
|
113
105
|
return first === void 0 ? layer : Layer.provide(layer, Layer.mergeAll(first, ...rest));
|
|
114
106
|
};
|
|
115
|
-
/**
|
|
116
|
-
* The builders of `apps` as one layer providing nothing: built above the surfaces, it runs
|
|
117
|
-
* each builder once for all of them, since every surface builds the same memoized layers.
|
|
118
|
-
*/
|
|
119
107
|
const builders = (apps) => Layer.empty.pipe(provideHandlers(apps));
|
|
120
|
-
/**
|
|
121
|
-
* Every action `apps` serve with its handler, behind its authorizer, as `provideHandlers` built
|
|
122
|
-
* them.
|
|
123
|
-
*/
|
|
124
108
|
const acquire = (apps) => Effect.map(Effect.forEach(apps, (app) => Implementation.boundOf(app)), (bound) => bound.flat());
|
|
125
|
-
/**
|
|
126
|
-
* Every action `apps` serve with its handler, behind its authorizer, their builders built as layers
|
|
127
|
-
* of the graph being built around the caller, into its memo map, as `HttpRouter` builds a
|
|
128
|
-
* middleware's dependencies: a builder acquiring them shares each builder's one run with the
|
|
129
|
-
* surfaces of its graph, whichever builds first. Outside any graph, a program under
|
|
130
|
-
* `Effect.provide` shares that layer's, and one under none builds into a map of its own. The
|
|
131
|
-
* caller's scope holds them; a surface serving them too keeps them until it is released.
|
|
132
|
-
*/
|
|
133
109
|
const built = (apps) => {
|
|
134
110
|
const [first, ...rest] = apps.map((app) => Implementation.layerOf(app));
|
|
135
111
|
return Effect.gen(function* () {
|
|
@@ -141,27 +117,20 @@ const built = (apps) => {
|
|
|
141
117
|
return yield* Effect.provideContext(acquire(apps), context);
|
|
142
118
|
});
|
|
143
119
|
};
|
|
144
|
-
/**
|
|
145
|
-
* One action's handler behind its admission: a protected action's identity, then its
|
|
146
|
-
* `authorize`. These run outside the action's span, so what they fail with is attributed to
|
|
147
|
-
* the surface rather than to a handler that never ran. Each call has a scope of its own, on
|
|
148
|
-
* every surface: what `authorize` and the handler acquire is released when the call ends, the
|
|
149
|
-
* handler's first, so no call needs a `Scope` of its caller.
|
|
150
|
-
*/
|
|
151
120
|
const dispatch = (action, handle, before) => {
|
|
152
|
-
const
|
|
121
|
+
const actionAttributes = {
|
|
153
122
|
"action.name": action.name,
|
|
154
123
|
"action.read_only": action.readOnly
|
|
155
124
|
};
|
|
156
125
|
const authentication = action.caller === Anyone ? Effect.void : Effect.flatMap(Effect.serviceOption(action.caller), (actor) => Option.isSome(actor) ? Effect.void : Effect.fail(new Unauthenticated()));
|
|
157
126
|
const authorization = action.caller === Anyone ? Effect.void : Effect.suspend(() => before(action));
|
|
158
127
|
return (input) => {
|
|
159
|
-
const handled = Effect.withSpan(Effect.annotateLogs(Effect.suspend(() => handle(input)),
|
|
128
|
+
const handled = Effect.withSpan(Effect.annotateLogs(Effect.suspend(() => handle(input)), actionAttributes), action.name, {
|
|
160
129
|
captureStackTrace: false,
|
|
161
|
-
attributes
|
|
130
|
+
attributes: actionAttributes
|
|
162
131
|
});
|
|
163
132
|
return Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(handled)));
|
|
164
133
|
};
|
|
165
134
|
};
|
|
166
135
|
//#endregion
|
|
167
|
-
export { Implementation, acquire, assertHeld, builders, built, memoized, provideHandlers, select,
|
|
136
|
+
export { Implementation, acquire, assertHeld, builders, built, memoized, provideHandlers, select, servedBy, toList };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Schema, SchemaAST } from "effect";
|
|
2
|
+
//#region src/contract/media.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* An image, a media field of a success: `{ data: Uint8Array, mimeType: string }`, `mimeType` such
|
|
5
|
+
* as `image/png`. A top-level field of a struct success, optional or an array, or the whole
|
|
6
|
+
* success, one or an array: an MCP tool lifts it into an image block of its own, and every other
|
|
7
|
+
* surface sends it as JSON, the bytes in base64. `ActionMcp` refuses it anywhere else.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Image: Schema.Struct<{
|
|
10
|
+
readonly data: Schema.Uint8Array;
|
|
11
|
+
readonly mimeType: Schema.String;
|
|
12
|
+
}>;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/contract/media.ts
|
|
3
|
+
const mediaKindAnnotation = "effect-actions/media";
|
|
4
|
+
const Media = Schema.Struct({
|
|
5
|
+
data: Schema.Uint8Array,
|
|
6
|
+
mimeType: Schema.String
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* An image, a media field of a success: `{ data: Uint8Array, mimeType: string }`, `mimeType` such
|
|
10
|
+
* as `image/png`. A top-level field of a struct success, optional or an array, or the whole
|
|
11
|
+
* success, one or an array: an MCP tool lifts it into an image block of its own, and every other
|
|
12
|
+
* surface sends it as JSON, the bytes in base64. `ActionMcp` refuses it anywhere else.
|
|
13
|
+
*/
|
|
14
|
+
const Image = Media.annotate({ [mediaKindAnnotation]: "image" });
|
|
15
|
+
const isMarked = (ast) => ast.annotations?.[mediaKindAnnotation] === "image";
|
|
16
|
+
const kindOf = (ast) => isMarked(ast) && ast.encoding === void 0 ? "image" : void 0;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { Image, Media, isMarked, kindOf };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Action_d_exports, Errors } from "../Action.mjs";
|
|
2
|
+
import { SchemaAST, Types } from "effect";
|
|
3
|
+
//#region src/contract/rules.d.ts
|
|
4
|
+
type ListOf<G> = G extends Errors ? G : readonly [G];
|
|
5
|
+
/**
|
|
6
|
+
* The errors options `O` declare, as at run time: those `error` always gives, or, where it may
|
|
7
|
+
* be absent, those it gives or none, so clients decode each that may arrive.
|
|
8
|
+
*/
|
|
9
|
+
export type ErrorsOf<O> = O extends unknown ? "error" extends keyof O ? O extends {
|
|
10
|
+
readonly error: infer G extends Errors | Errors[number];
|
|
11
|
+
} ? ListOf<G> : ListOf<Extract<O["error" & keyof O], Errors | Errors[number]>> | [] : [] : never;
|
|
12
|
+
type Single<T> = true extends Types.IsUnion<T> ? never : T;
|
|
13
|
+
/**
|
|
14
|
+
* The errors a binding of errors `E` surely declares: each slot of one error in a list of
|
|
15
|
+
* fixed length, never one of an array of unknown length, which may be empty, nor of a list
|
|
16
|
+
* `E` may be one of several.
|
|
17
|
+
*/
|
|
18
|
+
export type Certain<E extends Errors> = true extends Types.IsUnion<E> ? never : number extends E["length"] ? never : { readonly [K in keyof E]: Single<E[K]>; }[number];
|
|
19
|
+
/**
|
|
20
|
+
* The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
|
|
21
|
+
* authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
|
|
22
|
+
* agree on a name.
|
|
23
|
+
*/
|
|
24
|
+
export declare const Anyone: unique symbol;
|
|
25
|
+
//#endregion
|
|
@@ -1,37 +1,22 @@
|
|
|
1
1
|
import { builtIns, statuses } from "./errors.mjs";
|
|
2
2
|
import { Predicate, Schema, SchemaAST } from "effect";
|
|
3
|
-
//#region src/
|
|
4
|
-
/**
|
|
5
|
-
* The list an `error` option stands for: one schema, a list as given, or none, as
|
|
6
|
-
* `HttpApiEndpoint` takes it.
|
|
7
|
-
*/
|
|
3
|
+
//#region src/contract/rules.ts
|
|
8
4
|
const errorList = (error) => error === void 0 ? [] : Schema.isSchema(error) ? [error] : error;
|
|
9
|
-
/**
|
|
10
|
-
* What a projection of an action declares: the built-in errors, the action's own, and any
|
|
11
|
-
* its surface adds, such as a binding's. The built-ins come first, since a failure encodes
|
|
12
|
-
* and decodes with the first schema that accepts it, and they are classes, which accept
|
|
13
|
-
* only their own instances: a loose schema of the action's never captures one. A schema
|
|
14
|
-
* listed twice is not repeated.
|
|
15
|
-
*/
|
|
16
5
|
const projectedErrors = (action, surface = []) => [.../* @__PURE__ */ new Set([
|
|
17
6
|
...builtIns,
|
|
18
7
|
...action.error,
|
|
19
8
|
...surface
|
|
20
9
|
])];
|
|
21
10
|
/**
|
|
22
|
-
* The caller of a public action: anyone, signed in or not
|
|
23
|
-
* of the package agree on it, as they
|
|
11
|
+
* The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
|
|
12
|
+
* authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
|
|
13
|
+
* agree on a name.
|
|
24
14
|
*/
|
|
25
15
|
const Anyone = Symbol.for("@gjermundgaraba/effect-actions/Anyone");
|
|
26
16
|
const validName = /^[A-Za-z0-9_-]{1,128}$/;
|
|
27
|
-
/** Names become path segments, OpenAPI identifiers and client method keys. */
|
|
28
17
|
const assertName = (what, name) => {
|
|
29
18
|
if (!validName.test(name) || name === "then") throw new Error(`Invalid ${what}: ${name}`);
|
|
30
19
|
};
|
|
31
|
-
/**
|
|
32
|
-
* Names are checked by whoever owns them: an implementation, a binding, a CLI, or the MCP
|
|
33
|
-
* tools. `claimantOf` says who claims a name, so a clash between two claimants names both.
|
|
34
|
-
*/
|
|
35
20
|
const assertDistinct = (what, items, nameOf, claimantOf = nameOf) => {
|
|
36
21
|
const seen = /* @__PURE__ */ new Map();
|
|
37
22
|
for (const item of items) {
|
|
@@ -45,44 +30,23 @@ const assertDistinct = (what, items, nameOf, claimantOf = nameOf) => {
|
|
|
45
30
|
seen.set(name, claimant);
|
|
46
31
|
}
|
|
47
32
|
};
|
|
48
|
-
/** Refuse an action name `actions` hold twice. */
|
|
49
33
|
const assertOnce = (what, actions) => assertDistinct(what, actions, (action) => action.name);
|
|
50
|
-
/**
|
|
51
|
-
* Refuse a key of options keyed by action name, such as handlers, tools or commands, that no
|
|
52
|
-
* action of `names` has, so a stale option cannot outlive its action.
|
|
53
|
-
*/
|
|
54
34
|
const assertKnown = (what, keys, names) => {
|
|
55
35
|
const unknown = keys.filter((key) => !names.includes(key));
|
|
56
36
|
if (unknown.length > 0) throw new Error(`Unknown ${what}: ${unknown.join(", ")}`);
|
|
57
37
|
};
|
|
58
|
-
/** `ast` with any suspension at its top resolved, as a recursive schema's is. */
|
|
59
38
|
const unsuspended = (ast) => SchemaAST.isSuspend(ast) ? unsuspended(ast.thunk()) : ast;
|
|
60
|
-
/** The members of a union, nested and suspended ones included, or the one type otherwise. */
|
|
61
39
|
const members = (ast) => {
|
|
62
40
|
const resolved = unsuspended(ast);
|
|
63
41
|
return SchemaAST.isUnion(resolved) ? resolved.types.flatMap(members) : [resolved];
|
|
64
42
|
};
|
|
65
|
-
/**
|
|
66
|
-
* The values a literal or an enum accepts, or `undefined` for any other type, which has no
|
|
67
|
-
* fixed set.
|
|
68
|
-
*/
|
|
69
43
|
const literalValues = (ast) => SchemaAST.isLiteral(ast) ? [ast.literal] : SchemaAST.isEnum(ast) ? ast.enums.map(([, value]) => value) : [void 0];
|
|
70
|
-
|
|
71
|
-
* The `_tag`s a schema's encoding carries: each member's of a union, every value of a union of
|
|
72
|
-
* literals or of an enum, and a suspended schema's, as a recursive error is written.
|
|
73
|
-
*/
|
|
74
|
-
const tagsOf = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member) => {
|
|
44
|
+
const encodedTags = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member) => {
|
|
75
45
|
const tag = SchemaAST.isObjects(member) ? member.propertySignatures.find((property) => property.name === "_tag")?.type : void 0;
|
|
76
46
|
return tag === void 0 ? [] : members(tag).flatMap(literalValues).filter(Predicate.isString);
|
|
77
47
|
});
|
|
78
|
-
/**
|
|
79
|
-
* Refuse an error that encodes with a built-in error's `_tag`, the built-in itself included:
|
|
80
|
-
* every endpoint and tool declares the built-in errors already, a client decoding the answer
|
|
81
|
-
* could not tell a look-alike from them, and a look-alike of a refusal would step up. Checked
|
|
82
|
-
* where the action or the binding is made, so every surface and client of it may rely on it.
|
|
83
|
-
*/
|
|
84
48
|
const assertOwnTags = (what, errors) => {
|
|
85
|
-
const tag = errors.flatMap((error) =>
|
|
49
|
+
const tag = errors.flatMap((error) => encodedTags(error.ast)).find((tag) => Object.hasOwn(statuses, tag));
|
|
86
50
|
if (tag !== void 0) throw new Error(`${what}: error _tag "${tag}" is built in, and declared on every surface`);
|
|
87
51
|
};
|
|
88
52
|
//#endregion
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { BuiltIns } from "
|
|
2
|
-
import "
|
|
3
|
-
import { Call } from "./call.mjs";
|
|
1
|
+
import { BuiltIns } from "../contract/errors.mjs";
|
|
2
|
+
import { Call } from "../contract/call.mjs";
|
|
4
3
|
import { Action_d_exports, Any, Errors } from "../Action.mjs";
|
|
5
|
-
import { Any as Any$1, VerifierError } from "
|
|
4
|
+
import { Any as Any$1, VerifierError } from "../authentication/provider.mjs";
|
|
6
5
|
import { Effect, Schema } from "effect";
|
|
7
6
|
import { HttpClient, HttpClientError } from "effect/http";
|
|
8
7
|
import { HttpApi, HttpApiClient } from "effect/http-api";
|
|
9
|
-
//#region src/
|
|
8
|
+
//#region src/http/client.d.ts
|
|
10
9
|
/**
|
|
11
10
|
* The native `HttpApiClient.make` options except `transformResponse`: `baseUrl` and
|
|
12
11
|
* `transformClient`. `transformResponse` may change a call's success, failure or
|
|
13
12
|
* services, which the method types cannot follow.
|
|
14
13
|
*/
|
|
15
14
|
export type Options = Omit<NonNullable<Parameters<typeof HttpApiClient.make>[1]>, "transformResponse">;
|
|
16
|
-
/** An error schema the binding declares on every endpoint. */
|
|
17
15
|
type BindingError = Errors[number];
|
|
18
16
|
/**
|
|
19
17
|
* What one call of `A` fails with: a declared error value (the action's own, one `E` of the
|
|
@@ -22,11 +20,11 @@ type BindingError = Errors[number];
|
|
|
22
20
|
* the success does not decode. Input that does not encode is `InvalidInput`, and nothing is sent.
|
|
23
21
|
*/
|
|
24
22
|
export type MethodError<A extends Any, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | HttpClientError.HttpClientError | Schema.SchemaError;
|
|
25
|
-
/** One action as an Effect of its decoded success, failing with `MethodError`. */
|
|
26
23
|
type Method<A extends Any, E extends BindingError> = Call<A, Effect.Effect<A["success"]["Type"], MethodError<A, E>>>;
|
|
27
24
|
/**
|
|
28
|
-
* Any HTTP binding. The API is a native
|
|
29
|
-
* invariant in their endpoints; `actions` carries the
|
|
25
|
+
* Any HTTP binding, with its actions erased: what `layer` and `client` read. The API is a native
|
|
26
|
+
* constraint here because native groups are invariant in their endpoints; `actions` carries the
|
|
27
|
+
* types.
|
|
30
28
|
*/
|
|
31
29
|
export interface AnyHttp {
|
|
32
30
|
readonly authentication?: Any$1 | undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { checked } from "../contract/call.mjs";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { HttpApiClient } from "effect/http-api";
|
|
4
|
+
//#region src/http/client.ts
|
|
5
|
+
const assertInBinding = (actions, action) => {
|
|
6
|
+
if (!actions.includes(action)) throw new Error(`Action "${action.name}" is not in this HTTP binding`);
|
|
7
|
+
};
|
|
8
|
+
const methods = (http, options = {}) => Effect.map(HttpApiClient.make(http.api, options), (client) => {
|
|
9
|
+
const native = client;
|
|
10
|
+
return (action) => {
|
|
11
|
+
const method = native[action.name];
|
|
12
|
+
if (method === void 0) throw new Error(`No client method for ${action.name}`);
|
|
13
|
+
return checked(action, (payload) => method({ payload }));
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
//#endregion
|
|
17
|
+
export { assertInBinding, methods };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { unsuspended } from "../contract/rules.mjs";
|
|
2
|
+
import { Schema, SchemaAST } from "effect";
|
|
3
|
+
import { HttpServerResponse } from "effect/http";
|
|
4
|
+
import { HttpApiSchema } from "effect/http-api";
|
|
5
|
+
//#region src/http/declared.ts
|
|
6
|
+
const httpApiStatusOf = SchemaAST.resolveAt("httpApiStatus");
|
|
7
|
+
const statusThroughSuspensions = (ast) => httpApiStatusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThroughSuspensions(ast.thunk()) : void 0);
|
|
8
|
+
const declared = (error) => {
|
|
9
|
+
const status = statusThroughSuspensions(error.ast);
|
|
10
|
+
if (status !== void 0) return [httpApiStatusOf(error.ast) === void 0 ? HttpApiSchema.status(status)(error) : error];
|
|
11
|
+
const resolved = unsuspended(error.ast);
|
|
12
|
+
if (SchemaAST.isUnion(resolved) && resolved.checks === void 0 && resolved.encoding === void 0) return resolved.types.flatMap((member) => declared(Schema.make(member)));
|
|
13
|
+
return [HttpApiSchema.status(422)(error)];
|
|
14
|
+
};
|
|
15
|
+
const declaredResponse = (errors, error) => {
|
|
16
|
+
const schema = errors.flatMap(declared).find((each) => Schema.is(each)(error));
|
|
17
|
+
return schema === void 0 ? void 0 : HttpServerResponse.jsonUnsafe(Schema.encodeUnknownSync(Schema.toCodecJson(schema))(error), { status: httpApiStatusOf(schema.ast) ?? 422 });
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { declared, declaredResponse };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InvalidInput } from "../contract/errors.mjs";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { HttpApiMiddleware } from "effect/http-api";
|
|
4
|
+
//#region src/http/schema-errors.ts
|
|
5
|
+
const responseEncodingKinds = /* @__PURE__ */ new Set(["Body", "ResponseHeaders"]);
|
|
6
|
+
var SchemaErrors = class extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: [InvalidInput] }) {};
|
|
7
|
+
const schemaErrors = () => HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(responseEncodingKinds.has(failure.kind) ? failure : InvalidInput.fromSchemaError(failure.cause)));
|
|
8
|
+
//#endregion
|
|
9
|
+
export { SchemaErrors, schemaErrors };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "../contract/implementation.mjs";
|
|
2
|
+
import "../contract/media.mjs";
|
|
3
|
+
import "../Action.mjs";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
import { McpProtocol } from "effect/ai";
|
|
6
|
+
//#region src/mcp/protocol.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* An MCP request's parameters, as `Testing.mcpRequest` takes them: JSON, with any `_meta`, leaving
|
|
9
|
+
* out an `undefined` one, as a caller may. A helper passing on whole params types them as this,
|
|
10
|
+
* and a tool's `arguments` as `Schema.Json`, never `unknown`.
|
|
11
|
+
*/
|
|
12
|
+
export interface Params {
|
|
13
|
+
readonly _meta?: {
|
|
14
|
+
readonly [key: string]: Schema.Json;
|
|
15
|
+
};
|
|
16
|
+
readonly [key: string]: Schema.Json | undefined;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unsuspended } from "../contract/rules.mjs";
|
|
2
|
+
import { Media, isMarked, kindOf } from "../contract/media.mjs";
|
|
2
3
|
import { Predicate, Schema, SchemaAST } from "effect";
|
|
3
4
|
import { McpProtocol } from "effect/ai";
|
|
4
|
-
//#region src/
|
|
5
|
-
/**
|
|
6
|
-
* The one protocol revision served over HTTP. 2026-07-28 is stateless: every request
|
|
7
|
-
* stands alone. The stateful revisions keep a session per `initialize`, which the native
|
|
8
|
-
* HTTP runtime never expires and which no identity owns. A mixed endpoint's gate relies on it
|
|
9
|
-
* too: it decides from the routing headers, which the native runtime checks against the body
|
|
10
|
-
* of a stateless request alone, so serving a stateful revision over HTTP would open the gate.
|
|
11
|
-
*/
|
|
5
|
+
//#region src/mcp/protocol.ts
|
|
12
6
|
const httpProtocol = McpProtocol.v2026_07_28;
|
|
13
|
-
|
|
7
|
+
const protocols = [
|
|
8
|
+
McpProtocol.v2026_07_28,
|
|
9
|
+
McpProtocol.v2025_11_25,
|
|
10
|
+
McpProtocol.v2025_06_18
|
|
11
|
+
];
|
|
14
12
|
const defaultPath = "/mcp";
|
|
15
|
-
|
|
16
|
-
const routed = (method, params) => method === "resources/read" ? params.uri : params.name;
|
|
13
|
+
const mcpNameHeaderOf = (method, params) => method === "resources/read" ? params.uri : params.name;
|
|
17
14
|
/**
|
|
18
15
|
* One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it:
|
|
19
16
|
* the headers routing it, which repeat what its body says, and its JSON-RPC body. The client
|
|
@@ -22,7 +19,7 @@ const routed = (method, params) => method === "resources/read" ? params.uri : pa
|
|
|
22
19
|
*/
|
|
23
20
|
const statelessRequest = (method, params) => {
|
|
24
21
|
const { protocolVersion } = httpProtocol;
|
|
25
|
-
const name =
|
|
22
|
+
const name = mcpNameHeaderOf(method, params);
|
|
26
23
|
return {
|
|
27
24
|
headers: {
|
|
28
25
|
accept: "application/json, text/event-stream",
|
|
@@ -49,60 +46,41 @@ const statelessRequest = (method, params) => {
|
|
|
49
46
|
}
|
|
50
47
|
};
|
|
51
48
|
};
|
|
52
|
-
/** The `_meta` key of a lifted block naming its field. */
|
|
53
49
|
const fieldKey = "effect-actions/field";
|
|
54
50
|
const isMedia = Schema.is(Media);
|
|
55
51
|
const isMediaArray = Schema.is(Schema.Array(Media));
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const kindIn = (ast) => kindOf(resolved(ast));
|
|
60
|
-
/**
|
|
61
|
-
* The media `ast` is, one value or an array of them, as a whole success may be: an array without
|
|
62
|
-
* an encoding of its own whose every element is media of one kind, such as `Schema.Array` or
|
|
63
|
-
* `Schema.NonEmptyArray` of it.
|
|
64
|
-
*/
|
|
65
|
-
const wholeOf = (suspended) => {
|
|
66
|
-
const ast = resolved(suspended);
|
|
52
|
+
const kindOfUnsuspended = (ast) => kindOf(unsuspended(ast));
|
|
53
|
+
const wholeSuccessMediaOf = (suspended) => {
|
|
54
|
+
const ast = unsuspended(suspended);
|
|
67
55
|
const one = kindOf(ast);
|
|
68
56
|
if (one !== void 0) return {
|
|
69
57
|
kind: one,
|
|
70
58
|
many: false
|
|
71
59
|
};
|
|
72
60
|
if (!SchemaAST.isArrays(ast) || ast.encoding !== void 0) return void 0;
|
|
73
|
-
const [kind, ...others] = new Set([...ast.elements, ...ast.rest].map(
|
|
61
|
+
const [kind, ...others] = new Set([...ast.elements, ...ast.rest].map(kindOfUnsuspended));
|
|
74
62
|
return kind === void 0 || others.length > 0 ? void 0 : {
|
|
75
63
|
kind,
|
|
76
64
|
many: true
|
|
77
65
|
};
|
|
78
66
|
};
|
|
79
|
-
|
|
80
|
-
* The media a field's `ast` is: one value, which `Schema.optional` or `Schema.optionalKey` may
|
|
81
|
-
* make absent, or an array, which they may not, as no block would tell an absent one from an
|
|
82
|
-
* empty one.
|
|
83
|
-
*/
|
|
84
|
-
const fieldOf = (field) => {
|
|
67
|
+
const mediaFieldOf = (field) => {
|
|
85
68
|
const optional = field.context?.isOptional === true;
|
|
86
|
-
const ast =
|
|
69
|
+
const ast = unsuspended(field);
|
|
87
70
|
if (optional && SchemaAST.isUnion(ast) && ast.types.length === 2) {
|
|
88
|
-
const [
|
|
89
|
-
const kind =
|
|
71
|
+
const [definedMember, ...otherDefinedMembers] = ast.types.filter((member) => !SchemaAST.isUndefined(member));
|
|
72
|
+
const kind = definedMember === void 0 || otherDefinedMembers.length > 0 ? void 0 : kindOfUnsuspended(definedMember);
|
|
90
73
|
return kind === void 0 ? void 0 : {
|
|
91
74
|
kind,
|
|
92
75
|
many: false
|
|
93
76
|
};
|
|
94
77
|
}
|
|
95
|
-
const whole =
|
|
78
|
+
const whole = wholeSuccessMediaOf(ast);
|
|
96
79
|
return whole === void 0 || optional && whole.many ? void 0 : whole;
|
|
97
80
|
};
|
|
98
|
-
/**
|
|
99
|
-
* How an MCP tool sends `success`: it lifts the success itself, one value or an array, or the
|
|
100
|
-
* top-level media fields of a struct without an encoding of its own, keyed by strings, and
|
|
101
|
-
* sends the rest of the struct, if any is left.
|
|
102
|
-
*/
|
|
103
81
|
const liftOf = (success) => {
|
|
104
|
-
const ast =
|
|
105
|
-
const whole =
|
|
82
|
+
const ast = unsuspended(success.ast);
|
|
83
|
+
const whole = wholeSuccessMediaOf(ast);
|
|
106
84
|
if (whole !== void 0) return {
|
|
107
85
|
fields: [{
|
|
108
86
|
name: void 0,
|
|
@@ -115,7 +93,7 @@ const liftOf = (success) => {
|
|
|
115
93
|
rest: success
|
|
116
94
|
};
|
|
117
95
|
const fields = ast.propertySignatures.flatMap(({ name, type }) => {
|
|
118
|
-
const field =
|
|
96
|
+
const field = mediaFieldOf(type);
|
|
119
97
|
return field === void 0 ? [] : [{
|
|
120
98
|
name: String(name),
|
|
121
99
|
...field
|
|
@@ -127,14 +105,13 @@ const liftOf = (success) => {
|
|
|
127
105
|
};
|
|
128
106
|
const names = new Set(fields.map(({ name }) => name));
|
|
129
107
|
const rest = ast.propertySignatures.filter(({ name }) => !names.has(name));
|
|
130
|
-
const { identifier:
|
|
108
|
+
const { identifier: _identifierOfWholeSuccess, ...annotationsOfRest } = ast.annotations ?? {};
|
|
131
109
|
return {
|
|
132
110
|
fields,
|
|
133
|
-
rest: rest.length === 0 && ast.indexSignatures.length === 0 ? void 0 : Schema.make(new SchemaAST.Objects(rest, ast.indexSignatures,
|
|
111
|
+
rest: rest.length === 0 && ast.indexSignatures.length === 0 ? void 0 : Schema.make(new SchemaAST.Objects(rest, ast.indexSignatures, annotationsOfRest))
|
|
134
112
|
};
|
|
135
113
|
};
|
|
136
|
-
|
|
137
|
-
const partsOf = (ast) => [
|
|
114
|
+
const partsAndEncodedSideOf = (ast) => [
|
|
138
115
|
...SchemaAST.isDeclaration(ast) ? ast.typeParameters : [],
|
|
139
116
|
...SchemaAST.isArrays(ast) ? [...ast.elements, ...ast.rest] : [],
|
|
140
117
|
...SchemaAST.isObjects(ast) ? [...ast.propertySignatures, ...ast.indexSignatures].map(({ type }) => type) : [],
|
|
@@ -142,34 +119,24 @@ const partsOf = (ast) => [
|
|
|
142
119
|
...SchemaAST.isSuspend(ast) ? [ast.thunk()] : [],
|
|
143
120
|
...(ast.encoding ?? []).map(({ to }) => to)
|
|
144
121
|
];
|
|
145
|
-
|
|
146
|
-
const holdsMedia = (ast) => {
|
|
122
|
+
const holdsMediaAnywhere = (ast) => {
|
|
147
123
|
const seen = /* @__PURE__ */ new Set();
|
|
148
124
|
const pending = [ast];
|
|
149
125
|
for (let next = pending.pop(); next !== void 0; next = pending.pop()) {
|
|
150
126
|
if (seen.has(next)) continue;
|
|
151
127
|
seen.add(next);
|
|
152
128
|
if (isMarked(next)) return true;
|
|
153
|
-
pending.push(...
|
|
129
|
+
pending.push(...partsAndEncodedSideOf(next));
|
|
154
130
|
}
|
|
155
131
|
return false;
|
|
156
132
|
};
|
|
157
|
-
/**
|
|
158
|
-
* Whether `action`, whose success an MCP tool lifts as `lift` says, holds media where no tool
|
|
159
|
-
* lifts it: in its input, in a declared error, or in the rest of its success, such as nested in
|
|
160
|
-
* a field or in a union.
|
|
161
|
-
*/
|
|
162
133
|
const isMisplaced = (action, { rest }) => {
|
|
163
134
|
return [
|
|
164
135
|
action.input,
|
|
165
136
|
...action.error,
|
|
166
137
|
...rest === void 0 ? [] : [rest]
|
|
167
|
-
].some(({ ast }) =>
|
|
138
|
+
].some(({ ast }) => holdsMediaAnywhere(ast));
|
|
168
139
|
};
|
|
169
|
-
/**
|
|
170
|
-
* The media `lift` finds in `success`, a decoded value its schema has validated: the whole
|
|
171
|
-
* success's, or each field's, in field order, an array's in its order, none for an absent field.
|
|
172
|
-
*/
|
|
173
140
|
const liftedFrom = (lift, success) => lift.fields.flatMap(({ name, kind, many }) => {
|
|
174
141
|
const value = name === void 0 ? success : Predicate.hasProperty(success, name) ? success[name] : void 0;
|
|
175
142
|
return (many ? isMediaArray(value) ? value : [] : isMedia(value) ? [value] : []).map((media) => ({
|
|
@@ -179,4 +146,4 @@ const liftedFrom = (lift, success) => lift.fields.flatMap(({ name, kind, many })
|
|
|
179
146
|
}));
|
|
180
147
|
});
|
|
181
148
|
//#endregion
|
|
182
|
-
export { defaultPath, fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom, statelessRequest };
|
|
149
|
+
export { defaultPath, fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom, protocols, statelessRequest };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Clock, Console, Effect, Function, Predicate } from "effect";
|
|
2
|
+
//#region src/stdio/console.ts
|
|
3
|
+
const stderrConsole = (console, clock) => {
|
|
4
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5
|
+
const timers = /* @__PURE__ */ new Map();
|
|
6
|
+
const write = (...args) => console.error(...args);
|
|
7
|
+
const elapsed = (label, data) => {
|
|
8
|
+
const start = timers.get(label);
|
|
9
|
+
if (start !== void 0) {
|
|
10
|
+
const millis = Number(clock.monotonicTimeNanosUnsafe() - start) / 1e6;
|
|
11
|
+
write("%s: %sms", label, Number(millis.toFixed(3)), ...data);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const writeLabelKeepingFormat = (label, args) => {
|
|
15
|
+
const [formatOrFirst, ...rest] = args;
|
|
16
|
+
write(...Predicate.isString(formatOrFirst) ? [`${label}: ${formatOrFirst}`, ...rest] : [label, ...args]);
|
|
17
|
+
};
|
|
18
|
+
const group = (...labelOrUndefined) => {
|
|
19
|
+
if (labelOrUndefined.some((part) => part !== void 0)) write(...labelOrUndefined);
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
assert: (condition, ...args) => {
|
|
23
|
+
if (!condition) writeLabelKeepingFormat("Assertion failed", args);
|
|
24
|
+
},
|
|
25
|
+
clear: Function.constVoid,
|
|
26
|
+
count: (label = "default") => {
|
|
27
|
+
const count = (counts.get(label) ?? 0) + 1;
|
|
28
|
+
counts.set(label, count);
|
|
29
|
+
write("%s: %d", label, count);
|
|
30
|
+
},
|
|
31
|
+
countReset: (label = "default") => {
|
|
32
|
+
counts.delete(label);
|
|
33
|
+
},
|
|
34
|
+
debug: write,
|
|
35
|
+
dir: (item) => write("%O", item),
|
|
36
|
+
dirxml: write,
|
|
37
|
+
error: write,
|
|
38
|
+
group,
|
|
39
|
+
groupCollapsed: group,
|
|
40
|
+
groupEnd: Function.constVoid,
|
|
41
|
+
info: write,
|
|
42
|
+
log: write,
|
|
43
|
+
table: (data) => write(data),
|
|
44
|
+
time: (label = "default") => {
|
|
45
|
+
if (!timers.has(label)) timers.set(label, clock.monotonicTimeNanosUnsafe());
|
|
46
|
+
},
|
|
47
|
+
timeEnd: (label = "default") => {
|
|
48
|
+
elapsed(label, []);
|
|
49
|
+
timers.delete(label);
|
|
50
|
+
},
|
|
51
|
+
timeLog: (label = "default", ...data) => elapsed(label, data),
|
|
52
|
+
trace: (...args) => {
|
|
53
|
+
const { stack = "" } = /* @__PURE__ */ new Error();
|
|
54
|
+
const [_header, _traceFrame, ...callerFrames] = stack.split("\n");
|
|
55
|
+
const callerStack = callerFrames.join("\n");
|
|
56
|
+
writeLabelKeepingFormat("Trace", args);
|
|
57
|
+
if (callerStack !== "") write(callerStack);
|
|
58
|
+
},
|
|
59
|
+
warn: write
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const withStderrConsole = (effect) => Effect.provideServiceEffect(effect, Console.Console, Effect.zipWith(Effect.service(Console.Console), Effect.service(Clock.Clock), stderrConsole));
|
|
63
|
+
//#endregion
|
|
64
|
+
export { withStderrConsole };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
2
|
import { HttpClient, HttpRouter, HttpServer } from "effect/http";
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/testing/memory.d.ts
|
|
4
4
|
/** What in-memory routes may leave to the host: the router, the platform, nothing per request. */
|
|
5
5
|
export type Served = HttpRouter.HttpRouter | HttpRouter.Request<"Requires", never> | HttpRouter.Request<"GlobalRequires", never> | HttpRouter.Request<"Error", any> | HttpRouter.Request<"GlobalError", any> | Layer.Success<typeof HttpServer.layerServices>;
|
|
6
6
|
//#endregion
|