@gjermundgaraba/effect-actions 0.20.0 → 0.21.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 +19 -41
- package/dist/Action.d.mts +140 -46
- package/dist/Action.mjs +87 -42
- package/dist/ActionCli.d.mts +69 -33
- package/dist/ActionCli.mjs +51 -32
- package/dist/ActionHttp.d.mts +69 -35
- package/dist/ActionHttp.mjs +75 -23
- package/dist/ActionMcp.d.mts +56 -22
- package/dist/ActionMcp.mjs +94 -69
- package/dist/ActionRpc.d.mts +222 -55
- package/dist/ActionRpc.mjs +299 -46
- package/dist/ActionToolkit.d.mts +22 -5
- package/dist/ActionToolkit.mjs +36 -12
- package/dist/Authentication.d.mts +42 -10
- package/dist/Authentication.mjs +37 -30
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.mjs +1 -1
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -10
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -2
- package/dist/cli/commands.d.mts +9 -5
- package/dist/cli/commands.mjs +84 -33
- package/dist/contract/ast.mjs +4 -1
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -6
- package/dist/contract/call.mjs +48 -6
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +13 -2
- package/dist/contract/caller.mjs +22 -3
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.mjs +25 -9
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -3
- package/dist/contract/errors.mjs +1 -1
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -47
- package/dist/contract/implementation.mjs +92 -58
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/names.mjs +14 -4
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +14 -5
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -12
- package/dist/http/client.mjs +51 -12
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +1 -1
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -97
- package/dist/mcp/client.mjs +59 -132
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +39 -13
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +13 -5
- package/dist/testing/memory.d.mts +1 -1
- package/dist/testing/memory.mjs +3 -3
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +52 -60
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -52
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +21 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +108 -47
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +10 -4
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { tmpdir } from "node:os";
|
|
2
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
3
4
|
import { Config, Effect, FileSystem, Layer, Schema } from "effect";
|
|
5
|
+
|
|
4
6
|
import { TaskStore } from "../tasks/store.js";
|
|
5
7
|
import { Task } from "../tasks/Tasks.js";
|
|
6
8
|
|
|
@@ -15,24 +17,26 @@ export const tasksFile = Layer.effect(
|
|
|
15
17
|
Effect.gen(function* () {
|
|
16
18
|
const files = yield* FileSystem.FileSystem;
|
|
17
19
|
|
|
18
|
-
const
|
|
19
|
-
Config.withDefault(join(tmpdir(), "assistant-tasks.json"))
|
|
20
|
+
const file = yield* Config.String("ASSISTANT_TASKS").pipe(
|
|
21
|
+
Config.withDefault(path.join(tmpdir(), "assistant-tasks.json"))
|
|
20
22
|
);
|
|
21
23
|
|
|
22
|
-
const load = files.readFileString(
|
|
23
|
-
Effect.catchReason("PlatformError", "NotFound", () =>
|
|
24
|
+
const load = files.readFileString(file).pipe(
|
|
25
|
+
Effect.catchReason("PlatformError", "NotFound", () =>
|
|
26
|
+
Effect.succeed("[]")
|
|
27
|
+
),
|
|
24
28
|
Effect.flatMap(Schema.decodeEffect(TasksFile)),
|
|
25
|
-
Effect.orDie
|
|
29
|
+
Effect.orDie
|
|
26
30
|
);
|
|
27
31
|
|
|
28
|
-
const save = (tasks:
|
|
32
|
+
const save = (tasks: readonly Task[]) =>
|
|
29
33
|
Schema.encodeEffect(TasksFile)(tasks).pipe(
|
|
30
|
-
Effect.flatMap((json) => files.writeFileString(
|
|
34
|
+
Effect.flatMap((json) => files.writeFileString(file, json)),
|
|
31
35
|
// A log, not a result: a command sends its handler's logs to stderr.
|
|
32
|
-
Effect.tap(() => Effect.log(`Saved ${tasks.length} tasks to ${
|
|
33
|
-
Effect.orDie
|
|
36
|
+
Effect.tap(() => Effect.log(`Saved ${tasks.length} tasks to ${file}.`)),
|
|
37
|
+
Effect.orDie
|
|
34
38
|
);
|
|
35
39
|
|
|
36
40
|
return TaskStore.make(load, save);
|
|
37
|
-
})
|
|
41
|
+
})
|
|
38
42
|
);
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Effect, Layer, Ref, Stream } from "effect";
|
|
2
|
-
import { LanguageModel
|
|
2
|
+
import { LanguageModel } from "effect/ai";
|
|
3
|
+
import type { Response } from "effect/ai";
|
|
3
4
|
|
|
4
5
|
// DEMO ONLY: a model that needs no API key. It answers its first request with the first reply
|
|
5
6
|
// it was given, its second with the second, whatever the prompt says, and with nothing once
|
|
6
7
|
// they run out. It stands exactly where a provider's model layer goes, such as
|
|
7
8
|
// `OpenAiLanguageModel.model(...)` of `@effect/ai-openai`.
|
|
8
|
-
export const scripted = (
|
|
9
|
+
export const scripted = (
|
|
10
|
+
...replies: ReadonlyArray<readonly Response.PartEncoded[]>
|
|
11
|
+
) =>
|
|
9
12
|
Layer.effect(
|
|
10
13
|
LanguageModel.LanguageModel,
|
|
11
14
|
Effect.gen(function* () {
|
|
@@ -15,9 +18,9 @@ export const scripted = (...replies: ReadonlyArray<ReadonlyArray<Response.PartEn
|
|
|
15
18
|
generateText: () =>
|
|
16
19
|
Effect.map(
|
|
17
20
|
Ref.getAndUpdate(answered, (count) => count + 1),
|
|
18
|
-
(count) => [...(replies[count] ?? [])]
|
|
21
|
+
(count) => [...(replies[count] ?? [])]
|
|
19
22
|
),
|
|
20
23
|
streamText: () => Stream.empty,
|
|
21
24
|
});
|
|
22
|
-
})
|
|
25
|
+
})
|
|
23
26
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
3
4
|
import { CurrentCaller } from "../identity/Identity.js";
|
|
4
5
|
|
|
5
6
|
export const Task = Schema.Struct({
|
|
@@ -14,14 +15,21 @@ export const Task = Schema.Struct({
|
|
|
14
15
|
export type Task = typeof Task.Type;
|
|
15
16
|
|
|
16
17
|
// A declared error is part of the contract.
|
|
17
|
-
export class TaskNotFound extends Schema.TaggedError<
|
|
18
|
+
export class TaskNotFound extends Schema.TaggedError<
|
|
19
|
+
TaskNotFound,
|
|
20
|
+
{ readonly brand: unique symbol }
|
|
21
|
+
>()("TaskNotFound", {
|
|
18
22
|
id: Schema.String,
|
|
19
23
|
}) {}
|
|
20
24
|
|
|
21
25
|
// A field's description is what a model reads beside the tool, and the flag's help text.
|
|
22
|
-
const Id = Schema.String.annotate({
|
|
26
|
+
const Id = Schema.String.annotate({
|
|
27
|
+
description: "The id listed beside the task.",
|
|
28
|
+
});
|
|
23
29
|
|
|
24
|
-
const Assignee = Schema.String.annotate({
|
|
30
|
+
const Assignee = Schema.String.annotate({
|
|
31
|
+
description: "A teammate's name, such as ada.",
|
|
32
|
+
});
|
|
25
33
|
|
|
26
34
|
const team = { caller: CurrentCaller } as const;
|
|
27
35
|
|
|
@@ -36,7 +44,10 @@ export const ListTasks = Action.make("listTasks", {
|
|
|
36
44
|
export const AddTask = Action.make("addTask", {
|
|
37
45
|
...team,
|
|
38
46
|
description: "Add a task and assign it to a teammate.",
|
|
39
|
-
input: {
|
|
47
|
+
input: {
|
|
48
|
+
title: Schema.String.check(Schema.isMinLength(1)),
|
|
49
|
+
assignee: Assignee,
|
|
50
|
+
},
|
|
40
51
|
success: Task,
|
|
41
52
|
readOnly: false,
|
|
42
53
|
});
|
|
@@ -53,7 +64,10 @@ export const CompleteTask = Action.make("completeTask", {
|
|
|
53
64
|
export const AnnotateTask = Action.make("annotateTask", {
|
|
54
65
|
...team,
|
|
55
66
|
description: "Replace a task's notes.",
|
|
56
|
-
input: {
|
|
67
|
+
input: {
|
|
68
|
+
id: Id,
|
|
69
|
+
notes: Schema.String.annotate({ description: "The notes, as plain text." }),
|
|
70
|
+
},
|
|
57
71
|
success: Task,
|
|
58
72
|
error: TaskNotFound,
|
|
59
73
|
readOnly: false,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
3
4
|
import { authorize } from "../identity/authorize.js";
|
|
4
5
|
import { CurrentCaller } from "../identity/Identity.js";
|
|
5
|
-
import { Tasks } from "./Tasks.js";
|
|
6
6
|
import { TaskStore } from "./store.js";
|
|
7
|
+
import { Tasks } from "./Tasks.js";
|
|
7
8
|
|
|
8
9
|
// The builder gets the store. `CurrentCaller` is read per call. No handler knows whether a model
|
|
9
10
|
// or a person at a terminal called it, and none checks a permission: `authorize` has run before
|
|
@@ -14,12 +15,15 @@ export const tasks = Action.implement(
|
|
|
14
15
|
const store = yield* TaskStore;
|
|
15
16
|
|
|
16
17
|
return {
|
|
17
|
-
listTasks: ({ assignee }) =>
|
|
18
|
+
listTasks: ({ assignee }) =>
|
|
19
|
+
Effect.map(store.list(assignee), (listed) => ({ tasks: listed })),
|
|
18
20
|
addTask: ({ title, assignee }) =>
|
|
19
|
-
Effect.flatMap(CurrentCaller, (caller) =>
|
|
21
|
+
Effect.flatMap(CurrentCaller, (caller) =>
|
|
22
|
+
store.add(caller.id, title, assignee)
|
|
23
|
+
),
|
|
20
24
|
completeTask: ({ id }) => store.complete(id),
|
|
21
25
|
annotateTask: ({ id, notes }) => store.annotate(id, notes),
|
|
22
26
|
};
|
|
23
27
|
}),
|
|
24
|
-
{ authorize }
|
|
28
|
+
{ authorize }
|
|
25
29
|
);
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Ref, Semaphore } from "effect";
|
|
2
|
-
import { Task, TaskNotFound } from "./Tasks.js";
|
|
3
2
|
|
|
4
|
-
type
|
|
3
|
+
import type { Task } from "./Tasks.js";
|
|
4
|
+
import { TaskNotFound } from "./Tasks.js";
|
|
5
|
+
|
|
6
|
+
type TaskList = readonly Task[];
|
|
5
7
|
|
|
6
8
|
// Every method reads the whole list, and a change writes it back: a layer says only where the
|
|
7
9
|
// list is kept, in memory below or in a file (src/platform/tasks-file.ts).
|
|
8
|
-
const make = (
|
|
10
|
+
const make = (
|
|
11
|
+
load: Effect.Effect<TaskList>,
|
|
12
|
+
save: (tasks: TaskList) => Effect.Effect<void>
|
|
13
|
+
) => {
|
|
9
14
|
// A model's tool calls in one turn run concurrently. Two changes reading the list before
|
|
10
15
|
// either writes it would lose one, so a change holds this lock from its read to its write.
|
|
11
16
|
const lock = Semaphore.makeUnsafe(1);
|
|
12
17
|
|
|
13
|
-
const change = Effect.fn("TaskStore.change")(function* (
|
|
18
|
+
const change = Effect.fn("TaskStore.change")(function* (
|
|
19
|
+
id: string,
|
|
20
|
+
to: (task: Task) => Task
|
|
21
|
+
) {
|
|
14
22
|
const tasks = yield* load;
|
|
15
23
|
const task = tasks.find((each) => each.id === id);
|
|
16
24
|
|
|
17
|
-
if (task === undefined)
|
|
25
|
+
if (task === undefined) {
|
|
26
|
+
return yield* new TaskNotFound({ id });
|
|
27
|
+
}
|
|
18
28
|
|
|
19
29
|
const changed = to(task);
|
|
20
30
|
|
|
@@ -26,11 +36,21 @@ const make = (load: Effect.Effect<TaskList>, save: (tasks: TaskList) => Effect.E
|
|
|
26
36
|
return TaskStore.of({
|
|
27
37
|
list: (assignee) =>
|
|
28
38
|
Effect.map(load, (tasks) =>
|
|
29
|
-
assignee === undefined
|
|
39
|
+
assignee === undefined
|
|
40
|
+
? tasks
|
|
41
|
+
: tasks.filter((task) => task.assignee === assignee)
|
|
30
42
|
),
|
|
31
43
|
add: Effect.fn("TaskStore.add")(function* (addedBy, title, assignee) {
|
|
32
44
|
const tasks = yield* load;
|
|
33
|
-
|
|
45
|
+
|
|
46
|
+
const task = {
|
|
47
|
+
id: `${tasks.length + 1}`,
|
|
48
|
+
title,
|
|
49
|
+
assignee,
|
|
50
|
+
addedBy,
|
|
51
|
+
notes: "",
|
|
52
|
+
done: false,
|
|
53
|
+
};
|
|
34
54
|
|
|
35
55
|
yield* save([...tasks, task]);
|
|
36
56
|
|
|
@@ -46,9 +66,16 @@ export class TaskStore extends Context.Service<
|
|
|
46
66
|
TaskStore,
|
|
47
67
|
{
|
|
48
68
|
readonly list: (assignee: string | undefined) => Effect.Effect<TaskList>;
|
|
49
|
-
readonly add: (
|
|
69
|
+
readonly add: (
|
|
70
|
+
addedBy: string,
|
|
71
|
+
title: string,
|
|
72
|
+
assignee: string
|
|
73
|
+
) => Effect.Effect<Task>;
|
|
50
74
|
readonly complete: (id: string) => Effect.Effect<Task, TaskNotFound>;
|
|
51
|
-
readonly annotate: (
|
|
75
|
+
readonly annotate: (
|
|
76
|
+
id: string,
|
|
77
|
+
notes: string
|
|
78
|
+
) => Effect.Effect<Task, TaskNotFound>;
|
|
52
79
|
}
|
|
53
80
|
>()("assistant/TaskStore") {
|
|
54
81
|
/** The store over a list kept by `load` and `save`, each change serialized. */
|
|
@@ -58,7 +85,7 @@ export class TaskStore extends Context.Service<
|
|
|
58
85
|
static readonly layerMemory = Layer.effect(
|
|
59
86
|
TaskStore,
|
|
60
87
|
Effect.map(Ref.make<TaskList>([]), (tasks) =>
|
|
61
|
-
make(Ref.get(tasks), (next) => Ref.set(tasks, next))
|
|
62
|
-
)
|
|
88
|
+
make(Ref.get(tasks), (next) => Ref.set(tasks, next))
|
|
89
|
+
)
|
|
63
90
|
);
|
|
64
91
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
|
|
1
2
|
import { Effect, Option } from "effect";
|
|
2
3
|
import { Tool, Toolkit } from "effect/ai";
|
|
3
|
-
|
|
4
|
+
|
|
4
5
|
import { identity } from "./identity/handlers.js";
|
|
5
|
-
import { CurrentCaller
|
|
6
|
+
import { CurrentCaller } from "./identity/Identity.js";
|
|
7
|
+
import type { Member } from "./identity/Identity.js";
|
|
6
8
|
import { tasks } from "./tasks/handlers.js";
|
|
7
9
|
|
|
8
10
|
// `actions.layer` holds the handlers, and requires the startup services their builders read;
|
|
@@ -17,8 +19,9 @@ const actions = ActionToolkit.make([tasks, identity], {
|
|
|
17
19
|
Effect.serviceOption(CurrentCaller),
|
|
18
20
|
Option.match({
|
|
19
21
|
onNone: () => true,
|
|
20
|
-
onSome: (caller) =>
|
|
21
|
-
|
|
22
|
+
onSome: (caller) =>
|
|
23
|
+
call.name !== "addTask" || call.input.assignee !== caller.id,
|
|
24
|
+
})
|
|
22
25
|
),
|
|
23
26
|
});
|
|
24
27
|
|
|
@@ -26,13 +29,14 @@ const actions = ActionToolkit.make([tasks, identity], {
|
|
|
26
29
|
// The later definition wins; annotating the generated tool keeps its handler and approval rule.
|
|
27
30
|
export const toolkit = Toolkit.merge(
|
|
28
31
|
actions.toolkit,
|
|
29
|
-
Toolkit.make(actions.toolkit.tools.addTask.annotate(Tool.Title, "Add a task"))
|
|
32
|
+
Toolkit.make(actions.toolkit.tools.addTask.annotate(Tool.Title, "Add a task"))
|
|
30
33
|
);
|
|
31
34
|
|
|
32
|
-
export const layer = actions
|
|
35
|
+
export const { layer } = actions;
|
|
33
36
|
|
|
34
37
|
// One toolkit serves every member, so who a turn runs as is provided around that turn. This is
|
|
35
38
|
// how this example names a model's caller, and it takes a `Member`. The tools themselves accept
|
|
36
39
|
// any `CurrentCaller`, so keeping the operator away from a model is this function's doing, and
|
|
37
40
|
// that of every host using it.
|
|
38
|
-
export const actingFor = (member: Member) =>
|
|
41
|
+
export const actingFor = (member: Member) =>
|
|
42
|
+
Effect.provideService(CurrentCaller, member);
|
|
@@ -3,8 +3,10 @@ import { describe, expect, it } from "@effect/vitest";
|
|
|
3
3
|
import { Effect, Layer, Stdio, Stream } from "effect";
|
|
4
4
|
import { Command } from "effect/cli";
|
|
5
5
|
import { TestConsole } from "effect/testing";
|
|
6
|
+
|
|
6
7
|
import { commands } from "../src/commands.js";
|
|
7
|
-
import { CurrentCaller
|
|
8
|
+
import { CurrentCaller } from "../src/identity/Identity.js";
|
|
9
|
+
import type { Operator } from "../src/identity/Identity.js";
|
|
8
10
|
import { Permissions } from "../src/identity/permissions.js";
|
|
9
11
|
import { TaskStore } from "../src/tasks/store.js";
|
|
10
12
|
|
|
@@ -17,46 +19,70 @@ const operator: Operator = { kind: "operator", id: "root" };
|
|
|
17
19
|
|
|
18
20
|
// Effect's own runner, given arguments instead of the process's, and stdin the text given here.
|
|
19
21
|
// The console of `it.effect` keeps every line printed, so a run returns the lines it added.
|
|
20
|
-
const run = (args:
|
|
22
|
+
const run = (args: readonly string[], stdin = "") =>
|
|
21
23
|
Effect.gen(function* () {
|
|
22
24
|
const before = (yield* TestConsole.logLines).length;
|
|
23
25
|
|
|
24
|
-
yield* Command.runWith(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
yield* Command.runWith(
|
|
27
|
+
commands.pipe(Command.provideSync(CurrentCaller, operator)),
|
|
28
|
+
{
|
|
29
|
+
version: "0",
|
|
30
|
+
}
|
|
31
|
+
)(args).pipe(
|
|
32
|
+
Effect.provide(
|
|
33
|
+
Stdio.layerTest({
|
|
34
|
+
stdin: Stream.make(new TextEncoder().encode(stdin)),
|
|
35
|
+
}).pipe(Layer.provideMerge(NodeServices.layer))
|
|
36
|
+
)
|
|
29
37
|
);
|
|
30
38
|
|
|
31
39
|
return (yield* TestConsole.logLines).slice(before);
|
|
32
40
|
});
|
|
33
41
|
|
|
34
42
|
describe("the commands", () => {
|
|
35
|
-
it.effect(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
it.effect(
|
|
44
|
+
"run as the operator the host names, taking an argument and stdin",
|
|
45
|
+
() =>
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
yield* run([
|
|
48
|
+
"add-task",
|
|
49
|
+
"--title",
|
|
50
|
+
"Rotate the keys",
|
|
51
|
+
"--assignee",
|
|
52
|
+
"ada",
|
|
53
|
+
]);
|
|
54
|
+
yield* run(["annotate-task", "--id", "1"], "Before Friday.\n");
|
|
55
|
+
yield* run(["complete-task", "1"]);
|
|
56
|
+
|
|
57
|
+
const store = yield* TaskStore;
|
|
58
|
+
|
|
59
|
+
expect(yield* store.list("ada")).toMatchObject([
|
|
60
|
+
{ addedBy: "root", notes: "Before Friday.", done: true },
|
|
61
|
+
]);
|
|
62
|
+
}).pipe(Effect.provide(services))
|
|
47
63
|
);
|
|
48
64
|
|
|
49
|
-
it.effect(
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
it.effect(
|
|
66
|
+
"print what `render` returns, and JSON where a command states none",
|
|
67
|
+
() =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
yield* run([
|
|
70
|
+
"add-task",
|
|
71
|
+
"--title",
|
|
72
|
+
"Rotate the keys",
|
|
73
|
+
"--assignee",
|
|
74
|
+
"ada",
|
|
75
|
+
]);
|
|
52
76
|
|
|
53
|
-
|
|
77
|
+
const printed = yield* run(["list-tasks", "-a", "ada"]);
|
|
54
78
|
|
|
55
|
-
|
|
79
|
+
expect(printed).toStrictEqual(["[ ] 1 Rotate the keys (ada)"]);
|
|
56
80
|
|
|
57
|
-
|
|
81
|
+
const json = yield* run(["who-am-i"]);
|
|
58
82
|
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
expect(json).toStrictEqual([
|
|
84
|
+
JSON.stringify({ id: "root", kind: "operator" }, undefined, 2),
|
|
85
|
+
]);
|
|
86
|
+
}).pipe(Effect.provide(services))
|
|
61
87
|
);
|
|
62
88
|
});
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest";
|
|
2
|
-
import { Effect, Ref } from "effect";
|
|
3
2
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
3
|
+
import { Effect, Ref } from "effect";
|
|
4
|
+
|
|
4
5
|
import { identity } from "../../src/identity/handlers.js";
|
|
5
|
-
import { CurrentCaller
|
|
6
|
+
import { CurrentCaller } from "../../src/identity/Identity.js";
|
|
7
|
+
import type { Member, Operator } from "../../src/identity/Identity.js";
|
|
6
8
|
import { members } from "../../src/identity/members.js";
|
|
7
|
-
import {
|
|
9
|
+
import { Permissions } from "../../src/identity/permissions.js";
|
|
10
|
+
import type { Permission } from "../../src/identity/permissions.js";
|
|
8
11
|
|
|
9
|
-
const as = (caller: Member | Operator) =>
|
|
12
|
+
const as = (caller: Member | Operator) =>
|
|
13
|
+
Effect.provideService(CurrentCaller, caller);
|
|
10
14
|
|
|
11
15
|
const operator: Operator = { kind: "operator", id: "root" };
|
|
12
16
|
|
|
@@ -15,19 +19,28 @@ describe("identity, in process", () => {
|
|
|
15
19
|
Effect.gen(function* () {
|
|
16
20
|
const client = yield* Action.client(identity);
|
|
17
21
|
|
|
18
|
-
expect(yield* client.whoAmI().pipe(as(members.ada))).
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
expect(yield* client.whoAmI().pipe(as(members.ada))).toStrictEqual({
|
|
23
|
+
id: "ada",
|
|
24
|
+
kind: "member",
|
|
25
|
+
});
|
|
26
|
+
expect(yield* client.whoAmI().pipe(as(operator))).toStrictEqual({
|
|
27
|
+
id: "root",
|
|
28
|
+
kind: "operator",
|
|
29
|
+
});
|
|
30
|
+
}).pipe(Effect.provide(Permissions.layerMemory))
|
|
21
31
|
);
|
|
22
32
|
|
|
23
33
|
it.effect("gets the permission store once and asks it on every call", () =>
|
|
24
34
|
Effect.gen(function* () {
|
|
25
|
-
const granted = yield* Ref.make<
|
|
35
|
+
const granted = yield* Ref.make<readonly Permission[]>([]);
|
|
26
36
|
|
|
27
37
|
// The store is a startup service, so it is provided where the client is acquired: the
|
|
28
38
|
// authorizer is built there. No call is given it again.
|
|
29
39
|
const client = yield* Action.client(identity).pipe(
|
|
30
|
-
Effect.provideService(
|
|
40
|
+
Effect.provideService(
|
|
41
|
+
Permissions,
|
|
42
|
+
Permissions.of({ granted: () => Ref.get(granted) })
|
|
43
|
+
)
|
|
31
44
|
);
|
|
32
45
|
|
|
33
46
|
const refusal = yield* Effect.flip(client.whoAmI().pipe(as(members.ada)));
|
|
@@ -37,7 +50,10 @@ describe("identity, in process", () => {
|
|
|
37
50
|
|
|
38
51
|
yield* Ref.set(granted, ["tasks:read"]);
|
|
39
52
|
|
|
40
|
-
expect(yield* client.whoAmI().pipe(as(members.ada))).
|
|
41
|
-
|
|
53
|
+
expect(yield* client.whoAmI().pipe(as(members.ada))).toStrictEqual({
|
|
54
|
+
id: "ada",
|
|
55
|
+
kind: "member",
|
|
56
|
+
});
|
|
57
|
+
})
|
|
42
58
|
);
|
|
43
59
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NodeServices } from "@effect/platform-node";
|
|
2
2
|
import { describe, expect, it } from "@effect/vitest";
|
|
3
3
|
import { ConfigProvider, Effect, FileSystem, Layer, Path } from "effect";
|
|
4
|
+
|
|
4
5
|
import { tasksFile } from "../../src/platform/tasks-file.js";
|
|
5
6
|
import { TaskStore } from "../../src/tasks/store.js";
|
|
6
7
|
|
|
@@ -18,32 +19,47 @@ const fileStore = Effect.gen(function* () {
|
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
describe("the file store", () => {
|
|
21
|
-
it.effect(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
it.effect(
|
|
23
|
+
"gives each of several concurrent adds, as one model turn makes them, its own id",
|
|
24
|
+
() =>
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const store = yield* TaskStore;
|
|
27
|
+
|
|
28
|
+
const added = yield* Effect.forEach(
|
|
29
|
+
["Book the venue", "Send invitations", "Order lunch"],
|
|
30
|
+
(title) => store.add("ada", title, "ada"),
|
|
31
|
+
{ concurrency: "unbounded" }
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(added.map(({ id }) => id)).toStrictEqual(["1", "2", "3"]);
|
|
35
|
+
expect(yield* store.list("ada")).toStrictEqual(added);
|
|
36
|
+
}).pipe(
|
|
37
|
+
Effect.provide(
|
|
38
|
+
Layer.unwrap(fileStore).pipe(Layer.provideMerge(NodeServices.layer))
|
|
39
|
+
)
|
|
40
|
+
)
|
|
35
41
|
);
|
|
36
42
|
|
|
37
|
-
it.effect(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
it.effect(
|
|
44
|
+
"keeps tasks for the next process: a layer built again reads them",
|
|
45
|
+
() =>
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const store = yield* fileStore;
|
|
48
|
+
|
|
49
|
+
const add = Effect.flatMap(TaskStore, (tasks) =>
|
|
50
|
+
tasks.add("root", "Rotate keys", "ada")
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const list = Effect.flatMap(TaskStore, (tasks) =>
|
|
54
|
+
tasks.list(undefined)
|
|
55
|
+
);
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
// `local` builds the layer anew for each, as each run of the agent or of a command does.
|
|
58
|
+
const task = yield* add.pipe(Effect.provide(store, { local: true }));
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
expect(
|
|
61
|
+
yield* list.pipe(Effect.provide(store, { local: true }))
|
|
62
|
+
).toStrictEqual([task]);
|
|
63
|
+
}).pipe(Effect.provide(NodeServices.layer))
|
|
48
64
|
);
|
|
49
65
|
});
|