@gjermundgaraba/effect-actions 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +57 -50
  2. package/dist/Action.d.mts +285 -79
  3. package/dist/Action.mjs +124 -23
  4. package/dist/ActionCli.d.mts +102 -31
  5. package/dist/ActionCli.mjs +114 -16
  6. package/dist/ActionHttp.d.mts +170 -51
  7. package/dist/ActionHttp.mjs +130 -79
  8. package/dist/ActionMcp.d.mts +71 -21
  9. package/dist/ActionMcp.mjs +108 -81
  10. package/dist/ActionToolkit.d.mts +56 -27
  11. package/dist/ActionToolkit.mjs +16 -5
  12. package/dist/Authentication.d.mts +129 -33
  13. package/dist/Authentication.mjs +231 -38
  14. package/dist/Testing.d.mts +82 -57
  15. package/dist/Testing.mjs +89 -79
  16. package/dist/internal/actions.d.mts +5 -25
  17. package/dist/internal/actions.mjs +75 -27
  18. package/dist/internal/authentication.d.mts +102 -0
  19. package/dist/internal/authentication.mjs +11 -0
  20. package/dist/internal/call.d.mts +12 -0
  21. package/dist/internal/call.mjs +12 -0
  22. package/dist/internal/cli.d.mts +36 -22
  23. package/dist/internal/cli.mjs +311 -33
  24. package/dist/internal/client.d.mts +43 -0
  25. package/dist/internal/client.mjs +25 -0
  26. package/dist/internal/console.mjs +77 -0
  27. package/dist/internal/declared.d.mts +13 -0
  28. package/dist/internal/declared.mjs +34 -0
  29. package/dist/internal/errors.d.mts +77 -0
  30. package/dist/internal/errors.mjs +79 -0
  31. package/dist/internal/implementation.d.mts +171 -41
  32. package/dist/internal/implementation.mjs +147 -29
  33. package/dist/internal/mcp.d.mts +11 -0
  34. package/dist/internal/mcp.mjs +52 -0
  35. package/dist/internal/memory.d.mts +6 -0
  36. package/dist/internal/memory.mjs +38 -0
  37. package/dist/internal/refusal.mjs +67 -0
  38. package/dist/internal/schema-errors.mjs +20 -0
  39. package/dist/internal/tools.mjs +27 -57
  40. package/docs/Action.md +401 -73
  41. package/docs/ActionCli.md +280 -51
  42. package/docs/ActionHttp.md +266 -82
  43. package/docs/ActionMcp.md +149 -104
  44. package/docs/ActionToolkit.md +97 -35
  45. package/docs/Authentication.md +369 -62
  46. package/docs/CONTEXT.md +23 -36
  47. package/docs/README.md +29 -73
  48. package/docs/Testing.md +154 -77
  49. package/docs/guarantees.md +81 -43
  50. package/docs/setup.md +90 -27
  51. package/package.json +9 -17
  52. package/dist/ActionCatalog.d.mts +0 -30
  53. package/dist/ActionCatalog.mjs +0 -39
  54. package/dist/ActionCliClient.d.mts +0 -48
  55. package/dist/ActionCliClient.mjs +0 -43
  56. package/dist/ActionGroup.d.mts +0 -44
  57. package/dist/ActionGroup.mjs +0 -35
  58. package/dist/ActionHttpClient.d.mts +0 -33
  59. package/dist/ActionHttpClient.mjs +0 -16
  60. package/dist/TestingClient.d.mts +0 -17
  61. package/dist/TestingClient.mjs +0 -22
  62. package/dist/internal/fetchClient.d.mts +0 -6
  63. package/dist/internal/fetchClient.mjs +0 -8
  64. package/dist/internal/tools.d.mts +0 -22
  65. package/docs/ActionCatalog.md +0 -54
  66. package/docs/ActionCliClient.md +0 -63
  67. package/docs/ActionGroup.md +0 -128
  68. package/docs/ActionHttpClient.md +0 -80
@@ -0,0 +1,67 @@
1
+ import { Refusal, statuses } from "./errors.mjs";
2
+ import { Cause, Context, Effect, Exit, Option, Predicate, Ref, Schema } from "effect";
3
+ import { HttpServerResponse } from "effect/http";
4
+ //#region src/internal/refusal.ts
5
+ /** An RFC 6750 error description: printable ASCII but `"` and `\`. */
6
+ const description = /^[\x20\x21\x23-\x5B\x5D-\x7E]+$/;
7
+ /**
8
+ * An RFC 9110 challenge of `scheme` and the parameters given, each a quoted string whose `"`
9
+ * and `\` are escaped: a metadata URL's query may hold a `\`.
10
+ */
11
+ const challenge = (scheme, parameters) => {
12
+ const given = parameters.flatMap(([name, value]) => value === void 0 ? [] : [`${name}="${value.replace(/["\\]/g, "\\$&")}"`]);
13
+ return given.length === 0 ? scheme : `${scheme} ${given.join(", ")}`;
14
+ };
15
+ /** An RFC 6750 `Bearer` challenge of the parameters given. */
16
+ const bearer = (parameters) => challenge("Bearer", parameters);
17
+ const encode = Schema.encodeSync(Refusal);
18
+ /** Whether `error` is a built-in refusal, `Unauthenticated` or `Forbidden`. */
19
+ const isRefusal = Schema.is(Refusal);
20
+ /**
21
+ * The RFC 6750 `insufficient_scope` challenge an OAuth client steps up on, for a `Forbidden`
22
+ * naming scopes, naming the resource's metadata URL when there is one.
23
+ */
24
+ const insufficientScope = (error, metadataUrl) => Predicate.isTagged(error, "Forbidden") && error.scopes !== void 0 ? bearer([
25
+ ["error", "insufficient_scope"],
26
+ ["scope", error.scopes.join(" ")],
27
+ ["resource_metadata", metadataUrl],
28
+ ["error_description", description.test(error.message) ? error.message : void 0]
29
+ ]) : void 0;
30
+ /** A refusal as its JSON with its status alone, as a scheme no OAuth client acts on answers it. */
31
+ const plain = (error) => HttpServerResponse.jsonUnsafe(encode(error), { status: statuses[error._tag] });
32
+ /**
33
+ * A refusal as its JSON with its status, as every endpoint declares it, and its
34
+ * `insufficient_scope` challenge if it has one.
35
+ */
36
+ const answer = (error, metadataUrl) => {
37
+ const response = plain(error);
38
+ const scoped = insufficientScope(error, metadataUrl);
39
+ return scoped === void 0 ? response : HttpServerResponse.setHeader(response, "www-authenticate", scoped);
40
+ };
41
+ /** The step-up refusal a tool's call under `answerStepUp` failed with, which answers its request. */
42
+ var SteppedUp = class extends Context.Service()("effect-actions/SteppedUp") {};
43
+ /**
44
+ * Whether `error` is a refusal an OAuth client acts on: `Unauthenticated`, or `Forbidden`
45
+ * naming scopes.
46
+ */
47
+ const isStepUp = (error) => isRefusal(error) && (!Predicate.isTagged(error, "Forbidden") || error.scopes !== void 0);
48
+ /**
49
+ * A protected MCP tool's `call`, whose step-up refusal answers the request under a Bearer
50
+ * descriptor's `answerStepUp`. Elsewhere it is `call` as it is.
51
+ */
52
+ const recordStepUp = (call) => Effect.tapError(call, (error) => isStepUp(error) ? Effect.flatMap(Effect.serviceOption(SteppedUp), (slot) => Option.isSome(slot) ? Ref.set(slot.value, Option.some(error)) : Effect.void) : Effect.void);
53
+ /**
54
+ * An MCP request, answered with the step-up refusal a tool's call in it failed with, its
55
+ * status, JSON and challenge naming `metadataUrl`, however the request ended short of a defect
56
+ * or an interruption: a tool's failure is a successful result, and MCP authorization requires
57
+ * 401 or 403.
58
+ */
59
+ const answerStepUp = (route, metadataUrl) => Effect.gen(function* () {
60
+ const slot = yield* Ref.make(Option.none());
61
+ const exit = yield* Effect.exit(Effect.provideService(route, SteppedUp, slot));
62
+ const refused = yield* Ref.get(slot);
63
+ const broken = Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause));
64
+ return Option.isSome(refused) && !broken ? answer(refused.value, metadataUrl) : yield* exit;
65
+ });
66
+ //#endregion
67
+ export { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain, recordStepUp };
@@ -0,0 +1,20 @@
1
+ import { InvalidInput } from "./errors.mjs";
2
+ import { Effect } from "effect";
3
+ import { HttpApiMiddleware } from "effect/http-api";
4
+ //#region src/internal/schema-errors.ts
5
+ /**
6
+ * `HttpApiBuilder` reports these kinds while encoding the handler's answer, after the
7
+ * handler ran; every other kind (`Payload`, `Params`, `Headers`, `Query`) comes from
8
+ * decoding the request before it.
9
+ */
10
+ const responseKinds = /* @__PURE__ */ new Set(["Body", "ResponseHeaders"]);
11
+ /**
12
+ * The native middleware answering schema failures: a request that does not decode with
13
+ * `InvalidInput`, the schema's own message and issues, a result that does not encode as a defect,
14
+ * the empty 500 of any other server bug.
15
+ */
16
+ var SchemaErrors = class extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: [InvalidInput] }) {};
17
+ /** `SchemaErrors`' server side, built by `ActionHttp.layer` alone, so a client's bundle leaves it out. */
18
+ const schemaErrors = () => HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => responseKinds.has(failure.kind) ? Effect.die(failure.cause) : Effect.fail(InvalidInput.fromSchemaError(failure.cause)));
19
+ //#endregion
20
+ export { SchemaErrors, schemaErrors };
@@ -1,74 +1,44 @@
1
- import { assertDistinct, projectedErrors } from "./actions.mjs";
2
- import { dispatch } from "./implementation.mjs";
3
- import { Context, Effect, Schema } from "effect";
1
+ import { assertOnce, projectedErrors } from "./actions.mjs";
2
+ import { acquire } from "./implementation.mjs";
3
+ import { Effect, Schema } from "effect";
4
4
  import { Tool, Toolkit } from "effect/ai";
5
5
  //#region src/internal/tools.ts
6
+ /** `tool` annotated with `value` under `key`, or as it is where `value` is not given. */
7
+ const annotateIf = (tool, key, value) => value === void 0 ? tool : tool.annotate(key, value);
6
8
  /**
7
- * The success field an MCP tool sends as a raw text block, from `mcp.text`. The native
8
- * server reads a tool's annotations when it registers the tool, so `ActionMcp` finds
9
- * it there.
9
+ * A tool's annotations: read-only exactly when its action is, and the contract's `mcp` ones
10
+ * where given, so the native defaults, MCP's, stand for the others. A read-only tool is not
11
+ * destructive unless it says so, where the native default calls every tool destructive.
10
12
  */
11
- const TextField = Context.Reference("@gjermundgaraba/effect-actions/TextField", { defaultValue: () => void 0 });
12
- const isToolAction = (action) => action.mcp !== false;
13
- /** Only tool-enabled groups are acquired; each implementation appears once. */
14
- const toolApps = (apps) => apps.flatMap((app) => {
15
- const actions = app.group.actions.filter(isToolAction);
16
- return actions.length === 0 ? [] : [{
17
- app,
18
- actions
19
- }];
20
- });
21
- /** Flatten the projected contracts only after deciding which groups are served. */
22
- const entries = (apps) => apps.flatMap(({ app, actions }) => actions.map((action) => ({
23
- app,
24
- action
25
- })));
26
- const annotate = (tool, mcp) => tool.annotate(Tool.Readonly, mcp.readOnly).annotate(Tool.Destructive, mcp.destructive).annotate(Tool.Idempotent, mcp.idempotent).annotate(Tool.OpenWorld, mcp.openWorld);
27
- /**
28
- * A native Effect AI tool. Its schemas retain action transforms and its result
29
- * is the action result itself, rather than an MCP response envelope.
30
- */
31
- const nativeTool = (action, errors) => annotate(Tool.make(action.mcp.name, {
32
- description: action.description,
33
- parameters: action.input,
34
- success: action.success,
35
- failure: Schema.Union(errors),
36
- failureMode: "return"
37
- }), action.mcp);
13
+ const annotate = (tool, { readOnly, mcp }) => {
14
+ const destructive = mcp.destructiveHint ?? (readOnly ? false : void 0);
15
+ const meta = mcp._meta === void 0 ? void 0 : { ...mcp._meta };
16
+ const hinted = annotateIf(annotateIf(annotateIf(tool.annotate(Tool.Readonly, readOnly), Tool.Destructive, destructive), Tool.Idempotent, mcp.idempotentHint), Tool.OpenWorld, mcp.openWorldHint);
17
+ return annotateIf(annotateIf(hinted, Tool.Title, mcp.title), Tool.Meta, meta);
18
+ };
38
19
  /**
39
- * MCP has a JSON-only wire contract. Success is the encoded success as structured
40
- * content; declared failures are returned as JSON text.
20
+ * The native tool of `action`. A model and an MCP client speak JSON, so it takes and gives
21
+ * the JSON encoding its schemas advertise, the whole success; handlers and callers see decoded
22
+ * values. A refusal by authentication or `authorize`, or a handler's built-in failure, is the implementation's failure, so
23
+ * it declares the built-in errors alongside the action's own, and returns them as its result.
41
24
  */
42
- const mcpTool = (action, errors) => annotate(Tool.make(action.mcp.name, {
25
+ const toolOf = (action) => Tool.make(action.name, {
43
26
  description: action.description,
44
27
  parameters: Schema.toCodecJson(action.input),
45
28
  success: Schema.toCodecJson(action.success),
46
- failure: Schema.toCodecJson(Schema.Union(errors)),
29
+ failure: Schema.toCodecJson(Schema.Union(projectedErrors(action))),
47
30
  failureMode: "return"
48
- }), action.mcp).annotate(TextField, action.mcp.text);
49
- const project = (projection, entry, options) => {
50
- const errors = projectedErrors(entry.action, options.errors);
51
- const tool = projection === "native" ? nativeTool(entry.action, errors) : mcpTool(entry.action, errors);
52
- if (projection === "native") return tool;
53
- return tool.annotate(Tool.Strict, true);
54
- };
31
+ });
55
32
  /**
56
- * Project MCP-enabled actions and acquire their handlers exactly once per adapter layer.
57
- * Native and MCP differ only in tool codecs; selection, scoped acquisition, dispatch
58
- * and native Toolkit binding stay identical.
33
+ * Project actions as tools named after them, sharing each implementation's built handlers.
59
34
  */
60
- const bindTools = (apps, projection, options) => {
61
- const served = toolApps(apps);
62
- const selected = entries(served);
63
- assertDistinct("MCP tool", selected.map(({ action }) => action.mcp.name));
64
- const toolkit = Toolkit.make(...selected.map((entry) => project(projection, entry, options)));
35
+ const bindTools = (apps, projection) => {
36
+ assertOnce(projection.label, apps.flatMap((app) => app.actions));
37
+ const toolkit = Toolkit.make(...apps.flatMap((app) => app.actions.map((action) => annotate(projection.tool(toolOf(action), action, app), action))));
65
38
  return {
66
39
  toolkit,
67
- layer: toolkit.toLayer(Effect.map(Effect.forEach(served, ({ app, actions }) => Effect.map(app.build, (handlers) => {
68
- const record = handlers;
69
- return actions.map((action) => [action.mcp.name, dispatch(app.group, action, record, options.before)]);
70
- })), (built) => Object.fromEntries(built.flat())))
40
+ layer: toolkit.toLayer(Effect.map(acquire(apps), (bound) => Object.fromEntries(bound.map(([action, run]) => [action.name, projection.handler?.(run, action) ?? run]))))
71
41
  };
72
42
  };
73
43
  //#endregion
74
- export { TextField, bindTools };
44
+ export { bindTools };