@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.
Files changed (321) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -46
  3. package/dist/Action.mjs +87 -42
  4. package/dist/ActionCli.d.mts +69 -33
  5. package/dist/ActionCli.mjs +51 -32
  6. package/dist/ActionHttp.d.mts +69 -35
  7. package/dist/ActionHttp.mjs +75 -23
  8. package/dist/ActionMcp.d.mts +56 -22
  9. package/dist/ActionMcp.mjs +94 -69
  10. package/dist/ActionRpc.d.mts +222 -55
  11. package/dist/ActionRpc.mjs +299 -46
  12. package/dist/ActionToolkit.d.mts +22 -5
  13. package/dist/ActionToolkit.mjs +36 -12
  14. package/dist/Authentication.d.mts +42 -10
  15. package/dist/Authentication.mjs +37 -30
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.mjs +1 -1
  19. package/dist/Webhook.d.mts +146 -0
  20. package/dist/Webhook.mjs +72 -0
  21. package/dist/authentication/provider.d.mts +35 -10
  22. package/dist/authentication/provider.mjs +1 -1
  23. package/dist/authentication/refusal.mjs +2 -2
  24. package/dist/cli/commands.d.mts +9 -5
  25. package/dist/cli/commands.mjs +84 -33
  26. package/dist/contract/ast.mjs +4 -1
  27. package/dist/contract/authorizer.d.mts +8 -0
  28. package/dist/contract/authorizer.mjs +20 -0
  29. package/dist/contract/call.d.mts +9 -6
  30. package/dist/contract/call.mjs +48 -6
  31. package/dist/contract/callback.d.mts +7 -0
  32. package/dist/contract/callback.mjs +27 -0
  33. package/dist/contract/caller.d.mts +13 -2
  34. package/dist/contract/caller.mjs +22 -3
  35. package/dist/contract/canonical.mjs +10 -0
  36. package/dist/contract/codec.d.mts +9 -0
  37. package/dist/contract/codec.mjs +9 -0
  38. package/dist/contract/codes.mjs +81 -0
  39. package/dist/contract/declared.mjs +25 -9
  40. package/dist/contract/digest.mjs +6 -0
  41. package/dist/contract/errors.d.mts +9 -3
  42. package/dist/contract/errors.mjs +1 -1
  43. package/dist/contract/feed.d.mts +127 -0
  44. package/dist/contract/feed.mjs +249 -0
  45. package/dist/contract/hub.d.mts +94 -0
  46. package/dist/contract/hub.mjs +133 -0
  47. package/dist/contract/idempotency.d.mts +47 -0
  48. package/dist/contract/idempotency.mjs +99 -0
  49. package/dist/contract/implementation.d.mts +73 -47
  50. package/dist/contract/implementation.mjs +92 -58
  51. package/dist/contract/input-check.d.mts +15 -0
  52. package/dist/contract/input-check.mjs +16 -0
  53. package/dist/contract/lease.mjs +28 -0
  54. package/dist/contract/listed.d.mts +4 -0
  55. package/dist/contract/listed.mjs +22 -0
  56. package/dist/contract/log.d.mts +41 -0
  57. package/dist/contract/log.mjs +144 -0
  58. package/dist/contract/names.mjs +14 -4
  59. package/dist/contract/occurrence.d.mts +29 -0
  60. package/dist/contract/occurrence.mjs +45 -0
  61. package/dist/contract/pages.d.mts +13 -0
  62. package/dist/contract/pages.mjs +39 -0
  63. package/dist/contract/progress.d.mts +26 -0
  64. package/dist/contract/progress.mjs +72 -0
  65. package/dist/contract/push.d.mts +20 -0
  66. package/dist/contract/push.mjs +96 -0
  67. package/dist/contract/receiving.d.mts +29 -0
  68. package/dist/contract/receiving.mjs +42 -0
  69. package/dist/contract/recent.mjs +12 -0
  70. package/dist/contract/reported.mjs +16 -0
  71. package/dist/contract/serving.mjs +63 -0
  72. package/dist/contract/status.mjs +14 -5
  73. package/dist/contract/stream.d.mts +34 -0
  74. package/dist/contract/stream.mjs +31 -0
  75. package/dist/contract/subscribe.d.mts +45 -0
  76. package/dist/contract/subscribe.mjs +50 -0
  77. package/dist/contract/transient.mjs +28 -0
  78. package/dist/contract/watch.d.mts +18 -0
  79. package/dist/contract/watch.mjs +105 -0
  80. package/dist/contract/webhooks.d.mts +42 -0
  81. package/dist/contract/webhooks.mjs +53 -0
  82. package/dist/http/client.d.mts +37 -12
  83. package/dist/http/client.mjs +51 -12
  84. package/dist/http/events.d.mts +56 -0
  85. package/dist/http/events.mjs +102 -0
  86. package/dist/http/placed.d.mts +36 -0
  87. package/dist/http/placed.mjs +71 -0
  88. package/dist/http/schema-errors.mjs +1 -1
  89. package/dist/http/streamed.d.mts +21 -0
  90. package/dist/http/streamed.mjs +94 -0
  91. package/dist/http/watch.mjs +50 -0
  92. package/dist/mcp/bridge.mjs +283 -0
  93. package/dist/mcp/client.d.mts +30 -97
  94. package/dist/mcp/client.mjs +59 -132
  95. package/dist/mcp/events.mjs +142 -0
  96. package/dist/mcp/progress.mjs +34 -0
  97. package/dist/mcp/protocol.mjs +39 -13
  98. package/dist/mcp/reply.d.mts +79 -0
  99. package/dist/mcp/reply.mjs +99 -0
  100. package/dist/mcp/request.d.mts +40 -0
  101. package/dist/mcp/request.mjs +51 -0
  102. package/dist/mcp/watch.mjs +115 -0
  103. package/dist/rpc/watch.mjs +39 -0
  104. package/dist/stdio/console.mjs +13 -5
  105. package/dist/testing/memory.d.mts +1 -1
  106. package/dist/testing/memory.mjs +3 -3
  107. package/dist/webhook/address.mjs +47 -0
  108. package/dist/webhook/attempt.mjs +65 -0
  109. package/dist/webhook/receiver.mjs +67 -0
  110. package/dist/webhook/signing.mjs +28 -0
  111. package/dist/webhook/stored.mjs +57 -0
  112. package/dist/webhook/subscriptions.mjs +342 -0
  113. package/docs/Action-failures.md +2 -2
  114. package/docs/Action-streams.md +115 -0
  115. package/docs/Action.md +58 -124
  116. package/docs/ActionCli-failures.md +1 -2
  117. package/docs/ActionCli.md +83 -88
  118. package/docs/ActionHttp-failures.md +3 -3
  119. package/docs/ActionHttp.md +90 -70
  120. package/docs/ActionMcp-failures.md +2 -1
  121. package/docs/ActionMcp.md +120 -92
  122. package/docs/ActionRpc-events.md +126 -0
  123. package/docs/ActionRpc-failures.md +1 -2
  124. package/docs/ActionRpc.md +108 -79
  125. package/docs/ActionToolkit-failures.md +1 -2
  126. package/docs/ActionToolkit.md +33 -90
  127. package/docs/Authentication-advanced.md +52 -60
  128. package/docs/Authentication-failures.md +5 -4
  129. package/docs/Authentication.md +23 -58
  130. package/docs/Event-failures.md +15 -0
  131. package/docs/Event.md +281 -0
  132. package/docs/README.md +51 -82
  133. package/docs/Testing-failures.md +1 -5
  134. package/docs/Testing.md +126 -86
  135. package/docs/Webhook-failures.md +8 -0
  136. package/docs/Webhook.md +319 -0
  137. package/docs/setup-failures.md +2 -3
  138. package/docs/setup.md +32 -52
  139. package/examples/README.md +12 -37
  140. package/examples/assistant/README.md +21 -26
  141. package/examples/assistant/src/agent.ts +66 -24
  142. package/examples/assistant/src/cli.ts +7 -4
  143. package/examples/assistant/src/commands.ts +9 -4
  144. package/examples/assistant/src/identity/Identity.ts +5 -4
  145. package/examples/assistant/src/identity/authorize.ts +13 -5
  146. package/examples/assistant/src/identity/handlers.ts +3 -2
  147. package/examples/assistant/src/identity/permissions.ts +9 -3
  148. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  149. package/examples/assistant/src/scripted.ts +7 -4
  150. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  151. package/examples/assistant/src/tasks/handlers.ts +9 -5
  152. package/examples/assistant/src/tasks/store.ts +38 -11
  153. package/examples/assistant/src/toolkit.ts +11 -7
  154. package/examples/assistant/test/commands.test.ts +53 -27
  155. package/examples/assistant/test/identity/identity.test.ts +27 -11
  156. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  157. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  158. package/examples/assistant/test/toolkit.test.ts +104 -75
  159. package/examples/build-events/README.md +83 -0
  160. package/examples/build-events/src/Api.ts +27 -0
  161. package/examples/build-events/src/Rpc.ts +26 -0
  162. package/examples/build-events/src/builds/Builds.ts +108 -0
  163. package/examples/build-events/src/builds/events.ts +54 -0
  164. package/examples/build-events/src/builds/handlers.ts +58 -0
  165. package/examples/build-events/src/builds/log.ts +30 -0
  166. package/examples/build-events/src/builds/status.ts +28 -0
  167. package/examples/build-events/src/builds/store.ts +130 -0
  168. package/examples/build-events/src/cli.ts +18 -0
  169. package/examples/build-events/src/commands.ts +32 -0
  170. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  171. package/examples/build-events/src/deploys/approvers.ts +55 -0
  172. package/examples/build-events/src/deploys/events.ts +32 -0
  173. package/examples/build-events/src/deploys/handlers.ts +38 -0
  174. package/examples/build-events/src/identity/Identity.ts +29 -0
  175. package/examples/build-events/src/identity/members.ts +7 -0
  176. package/examples/build-events/src/identity/verifier.ts +22 -0
  177. package/examples/build-events/src/notifier/Notices.ts +26 -0
  178. package/examples/build-events/src/notifier/routes.ts +71 -0
  179. package/examples/build-events/src/notifier.ts +48 -0
  180. package/examples/build-events/src/routes.ts +71 -0
  181. package/examples/build-events/src/server.ts +62 -0
  182. package/examples/build-events/test/builds/events.test.ts +68 -0
  183. package/examples/build-events/test/builds/log.test.ts +71 -0
  184. package/examples/build-events/test/builds/progress.test.ts +57 -0
  185. package/examples/build-events/test/builds/status.test.ts +42 -0
  186. package/examples/build-events/test/commands.test.ts +103 -0
  187. package/examples/build-events/test/deploys/events.test.ts +52 -0
  188. package/examples/build-events/test/http.test.ts +95 -0
  189. package/examples/build-events/test/notifier.test.ts +80 -0
  190. package/examples/build-events/test/routes.test.ts +96 -0
  191. package/examples/build-events/test/rpc.test.ts +84 -0
  192. package/examples/build-events/test/support/as.ts +8 -0
  193. package/examples/build-events/test/support/memory.ts +43 -0
  194. package/examples/build-events/test/webhooks.test.ts +143 -0
  195. package/examples/kitchen-sink/README.md +178 -166
  196. package/examples/kitchen-sink/src/Actions.ts +11 -3
  197. package/examples/kitchen-sink/src/Api.ts +12 -6
  198. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  199. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  200. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  201. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  202. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  203. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  204. package/examples/kitchen-sink/src/agent.ts +14 -6
  205. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  206. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  207. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  208. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  209. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  210. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  211. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  212. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  213. package/examples/kitchen-sink/src/browser.ts +10 -4
  214. package/examples/kitchen-sink/src/cli.ts +28 -12
  215. package/examples/kitchen-sink/src/config.ts +1 -1
  216. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  217. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  218. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  219. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  220. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  221. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  222. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  223. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  224. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  225. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  226. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  227. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  228. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  229. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  230. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  231. package/examples/kitchen-sink/src/memory.ts +11 -2
  232. package/examples/kitchen-sink/src/ops.ts +10 -5
  233. package/examples/kitchen-sink/src/request-policy.ts +21 -9
  234. package/examples/kitchen-sink/src/routes.ts +67 -27
  235. package/examples/kitchen-sink/src/sdk.ts +22 -12
  236. package/examples/kitchen-sink/src/seed.ts +15 -9
  237. package/examples/kitchen-sink/src/server.ts +24 -10
  238. package/examples/kitchen-sink/src/stdio.ts +9 -3
  239. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  240. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  241. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  242. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  243. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  244. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  245. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  246. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  247. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  248. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  249. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  250. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  251. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  252. package/examples/kitchen-sink/test/request-policy.test.ts +108 -47
  253. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  254. package/examples/kitchen-sink/test/support/as.ts +9 -0
  255. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  256. package/examples/kitchen-sink/test/support/services.ts +17 -0
  257. package/examples/live-rpc/README.md +33 -35
  258. package/examples/live-rpc/src/Rpc.ts +9 -5
  259. package/examples/live-rpc/src/browser.ts +39 -11
  260. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  261. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  262. package/examples/live-rpc/src/chat/events.ts +74 -0
  263. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  264. package/examples/live-rpc/src/chat/room.ts +9 -5
  265. package/examples/live-rpc/src/chat/store.ts +51 -14
  266. package/examples/live-rpc/src/client.ts +22 -5
  267. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  268. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  269. package/examples/live-rpc/src/page.ts +58 -12
  270. package/examples/live-rpc/src/routes.ts +14 -10
  271. package/examples/live-rpc/src/server.ts +15 -5
  272. package/examples/live-rpc/test/browser.test.ts +127 -50
  273. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  274. package/examples/live-rpc/test/routes.test.ts +130 -46
  275. package/examples/live-rpc/test/server.test.ts +94 -33
  276. package/examples/mcp-tools/README.md +24 -38
  277. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  278. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  279. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  280. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  281. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  282. package/examples/mcp-tools/src/routes.ts +8 -6
  283. package/examples/mcp-tools/src/server.ts +12 -5
  284. package/examples/mcp-tools/src/stdio.ts +3 -2
  285. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  286. package/examples/mcp-tools/test/routes.test.ts +140 -81
  287. package/examples/notes-service/README.md +30 -45
  288. package/examples/notes-service/src/Api.ts +1 -0
  289. package/examples/notes-service/src/browser.ts +15 -14
  290. package/examples/notes-service/src/cli.ts +8 -3
  291. package/examples/notes-service/src/commands.ts +46 -20
  292. package/examples/notes-service/src/identity/Identity.ts +2 -2
  293. package/examples/notes-service/src/identity/authorize.ts +6 -2
  294. package/examples/notes-service/src/identity/handlers.ts +3 -2
  295. package/examples/notes-service/src/identity/users.ts +2 -1
  296. package/examples/notes-service/src/identity/verifier.ts +12 -6
  297. package/examples/notes-service/src/notes/Notes.ts +46 -22
  298. package/examples/notes-service/src/notes/handlers.ts +18 -9
  299. package/examples/notes-service/src/notes/store.ts +70 -43
  300. package/examples/notes-service/src/routes.ts +7 -5
  301. package/examples/notes-service/src/server.ts +12 -5
  302. package/examples/notes-service/src/status/Status.ts +1 -1
  303. package/examples/notes-service/src/status/handlers.ts +5 -3
  304. package/examples/notes-service/src/stdio.ts +7 -3
  305. package/examples/notes-service/src/worker.ts +6 -3
  306. package/examples/notes-service/test/commands.test.ts +164 -91
  307. package/examples/notes-service/test/notes/as.ts +3 -1
  308. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  309. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  310. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  311. package/examples/notes-service/test/routes.test.ts +106 -71
  312. package/examples/notes-service/test/worker.test.ts +21 -13
  313. package/examples/quickstart/README.md +2 -6
  314. package/examples/quickstart/src/Api.ts +1 -0
  315. package/examples/quickstart/src/client.ts +6 -3
  316. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  317. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  318. package/examples/quickstart/src/routes.ts +3 -2
  319. package/examples/quickstart/src/server.ts +6 -2
  320. package/examples/quickstart/test/routes.test.ts +3 -2
  321. package/package.json +10 -4
@@ -1,6 +1,8 @@
1
1
  import { tmpdir } from "node:os";
2
- import { join } from "node:path";
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 path = yield* Config.String("ASSISTANT_TASKS").pipe(
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(path).pipe(
23
- Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed("[]")),
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: ReadonlyArray<Task>) =>
32
+ const save = (tasks: readonly Task[]) =>
29
33
  Schema.encodeEffect(TasksFile)(tasks).pipe(
30
- Effect.flatMap((json) => files.writeFileString(path, json)),
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 ${path}.`)),
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, type Response } from "effect/ai";
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 = (...replies: ReadonlyArray<ReadonlyArray<Response.PartEncoded>>) =>
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<TaskNotFound>()("TaskNotFound", {
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({ description: "The id listed beside the task." });
26
+ const Id = Schema.String.annotate({
27
+ description: "The id listed beside the task.",
28
+ });
23
29
 
24
- const Assignee = Schema.String.annotate({ description: "A teammate's name, such as ada." });
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: { title: Schema.String.check(Schema.isMinLength(1)), assignee: Assignee },
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: { id: Id, notes: Schema.String.annotate({ description: "The notes, as plain text." }) },
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 }) => Effect.map(store.list(assignee), (tasks) => ({ tasks })),
18
+ listTasks: ({ assignee }) =>
19
+ Effect.map(store.list(assignee), (listed) => ({ tasks: listed })),
18
20
  addTask: ({ title, assignee }) =>
19
- Effect.flatMap(CurrentCaller, (caller) => store.add(caller.id, title, assignee)),
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 TaskList = ReadonlyArray<Task>;
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 = (load: Effect.Effect<TaskList>, save: (tasks: TaskList) => Effect.Effect<void>) => {
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* (id: string, to: (task: Task) => Task) {
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) return yield* new TaskNotFound({ id });
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 ? tasks : tasks.filter((task) => task.assignee === assignee),
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
- const task = { id: `${tasks.length + 1}`, title, assignee, addedBy, notes: "", done: false };
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: (addedBy: string, title: string, assignee: string) => Effect.Effect<Task>;
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: (id: string, notes: string) => Effect.Effect<Task, TaskNotFound>;
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
- import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
4
+
4
5
  import { identity } from "./identity/handlers.js";
5
- import { CurrentCaller, type Member } from "./identity/Identity.js";
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) => call.name !== "addTask" || call.input.assignee !== caller.id,
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.layer;
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) => Effect.provideService(CurrentCaller, 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, type Operator } from "../src/identity/Identity.js";
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: ReadonlyArray<string>, stdin = "") =>
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(commands.pipe(Command.provideSync(CurrentCaller, operator)), {
25
- version: "0",
26
- })(args).pipe(
27
- Effect.provide(Stdio.layerTest({ stdin: Stream.make(new TextEncoder().encode(stdin)) })),
28
- Effect.provide(NodeServices.layer),
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("run as the operator the host names, taking an argument and stdin", () =>
36
- Effect.gen(function* () {
37
- yield* run(["add-task", "--title", "Rotate the keys", "--assignee", "ada"]);
38
- yield* run(["annotate-task", "--id", "1"], "Before Friday.\n");
39
- yield* run(["complete-task", "1"]);
40
-
41
- const store = yield* TaskStore;
42
-
43
- expect(yield* store.list("ada")).toMatchObject([
44
- { addedBy: "root", notes: "Before Friday.", done: true },
45
- ]);
46
- }).pipe(Effect.provide(services)),
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("print what `render` returns, and JSON where a command states none", () =>
50
- Effect.gen(function* () {
51
- yield* run(["add-task", "--title", "Rotate the keys", "--assignee", "ada"]);
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
- const printed = yield* run(["list-tasks", "-a", "ada"]);
77
+ const printed = yield* run(["list-tasks", "-a", "ada"]);
54
78
 
55
- expect(printed).toEqual(["[ ] 1 Rotate the keys (ada)"]);
79
+ expect(printed).toStrictEqual(["[ ] 1 Rotate the keys (ada)"]);
56
80
 
57
- const json = yield* run(["who-am-i"]);
81
+ const json = yield* run(["who-am-i"]);
58
82
 
59
- expect(json).toEqual([JSON.stringify({ id: "root", kind: "operator" }, undefined, 2)]);
60
- }).pipe(Effect.provide(services)),
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, type Member, type Operator } from "../../src/identity/Identity.js";
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 { type Permission, Permissions } from "../../src/identity/permissions.js";
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) => Effect.provideService(CurrentCaller, caller);
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))).toEqual({ id: "ada", kind: "member" });
19
- expect(yield* client.whoAmI().pipe(as(operator))).toEqual({ id: "root", kind: "operator" });
20
- }).pipe(Effect.provide(Permissions.layerMemory)),
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<ReadonlyArray<Permission>>([]);
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(Permissions, Permissions.of({ granted: () => Ref.get(granted) })),
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))).toEqual({ id: "ada", kind: "member" });
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("gives each of several concurrent adds, as one model turn makes them, its own id", () =>
22
- Effect.gen(function* () {
23
- const store = yield* TaskStore;
24
-
25
- const added = yield* Effect.all(
26
- ["Book the venue", "Send invitations", "Order lunch"].map((title) =>
27
- store.add("ada", title, "ada"),
28
- ),
29
- { concurrency: "unbounded" },
30
- );
31
-
32
- expect(added.map(({ id }) => id)).toEqual(["1", "2", "3"]);
33
- expect(yield* store.list("ada")).toEqual(added);
34
- }).pipe(Effect.provide(Layer.unwrap(fileStore)), Effect.provide(NodeServices.layer)),
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("keeps tasks for the next process: a layer built again reads them", () =>
38
- Effect.gen(function* () {
39
- const store = yield* fileStore;
40
- const add = Effect.flatMap(TaskStore, (tasks) => tasks.add("root", "Rotate keys", "ada"));
41
- const list = Effect.flatMap(TaskStore, (tasks) => tasks.list(undefined));
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
- // `local` builds the layer anew for each, as each run of the agent or of a command does.
44
- const task = yield* add.pipe(Effect.provide(store, { local: true }));
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
- expect(yield* list.pipe(Effect.provide(store, { local: true }))).toEqual([task]);
47
- }).pipe(Effect.provide(NodeServices.layer)),
60
+ expect(
61
+ yield* list.pipe(Effect.provide(store, { local: true }))
62
+ ).toStrictEqual([task]);
63
+ }).pipe(Effect.provide(NodeServices.layer))
48
64
  );
49
65
  });