@gjermundgaraba/effect-actions 0.19.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 (326) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -48
  3. package/dist/Action.mjs +88 -44
  4. package/dist/ActionCli.d.mts +70 -35
  5. package/dist/ActionCli.mjs +53 -34
  6. package/dist/ActionHttp.d.mts +70 -38
  7. package/dist/ActionHttp.mjs +76 -25
  8. package/dist/ActionMcp.d.mts +56 -24
  9. package/dist/ActionMcp.mjs +104 -61
  10. package/dist/ActionRpc.d.mts +222 -57
  11. package/dist/ActionRpc.mjs +301 -49
  12. package/dist/ActionToolkit.d.mts +22 -7
  13. package/dist/ActionToolkit.mjs +37 -14
  14. package/dist/Authentication.d.mts +42 -12
  15. package/dist/Authentication.mjs +38 -32
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.d.mts +1 -3
  19. package/dist/Testing.mjs +2 -3
  20. package/dist/Webhook.d.mts +146 -0
  21. package/dist/Webhook.mjs +72 -0
  22. package/dist/authentication/provider.d.mts +35 -12
  23. package/dist/authentication/provider.mjs +2 -3
  24. package/dist/authentication/refusal.mjs +3 -4
  25. package/dist/cli/commands.d.mts +9 -7
  26. package/dist/cli/commands.mjs +85 -35
  27. package/dist/contract/ast.mjs +5 -3
  28. package/dist/contract/authorizer.d.mts +8 -0
  29. package/dist/contract/authorizer.mjs +20 -0
  30. package/dist/contract/call.d.mts +9 -8
  31. package/dist/contract/call.mjs +49 -8
  32. package/dist/contract/callback.d.mts +7 -0
  33. package/dist/contract/callback.mjs +27 -0
  34. package/dist/contract/caller.d.mts +12 -3
  35. package/dist/contract/caller.mjs +23 -5
  36. package/dist/contract/canonical.mjs +10 -0
  37. package/dist/contract/codec.d.mts +9 -0
  38. package/dist/contract/codec.mjs +9 -0
  39. package/dist/contract/codes.mjs +81 -0
  40. package/dist/contract/declared.d.mts +0 -2
  41. package/dist/contract/declared.mjs +26 -11
  42. package/dist/contract/digest.mjs +6 -0
  43. package/dist/contract/errors.d.mts +9 -5
  44. package/dist/contract/errors.mjs +2 -3
  45. package/dist/contract/feed.d.mts +127 -0
  46. package/dist/contract/feed.mjs +249 -0
  47. package/dist/contract/hints.mjs +1 -2
  48. package/dist/contract/hub.d.mts +94 -0
  49. package/dist/contract/hub.mjs +133 -0
  50. package/dist/contract/idempotency.d.mts +47 -0
  51. package/dist/contract/idempotency.mjs +99 -0
  52. package/dist/contract/implementation.d.mts +73 -51
  53. package/dist/contract/implementation.mjs +94 -60
  54. package/dist/contract/input-check.d.mts +15 -0
  55. package/dist/contract/input-check.mjs +16 -0
  56. package/dist/contract/lease.mjs +28 -0
  57. package/dist/contract/listed.d.mts +4 -0
  58. package/dist/contract/listed.mjs +22 -0
  59. package/dist/contract/log.d.mts +41 -0
  60. package/dist/contract/log.mjs +144 -0
  61. package/dist/contract/media.d.mts +1 -3
  62. package/dist/contract/media.mjs +1 -2
  63. package/dist/contract/names.mjs +15 -6
  64. package/dist/contract/occurrence.d.mts +29 -0
  65. package/dist/contract/occurrence.mjs +45 -0
  66. package/dist/contract/pages.d.mts +13 -0
  67. package/dist/contract/pages.mjs +39 -0
  68. package/dist/contract/progress.d.mts +26 -0
  69. package/dist/contract/progress.mjs +72 -0
  70. package/dist/contract/push.d.mts +20 -0
  71. package/dist/contract/push.mjs +96 -0
  72. package/dist/contract/receiving.d.mts +29 -0
  73. package/dist/contract/receiving.mjs +42 -0
  74. package/dist/contract/recent.mjs +12 -0
  75. package/dist/contract/reported.mjs +16 -0
  76. package/dist/contract/serving.mjs +63 -0
  77. package/dist/contract/status.mjs +15 -7
  78. package/dist/contract/stream.d.mts +34 -0
  79. package/dist/contract/stream.mjs +31 -0
  80. package/dist/contract/subscribe.d.mts +45 -0
  81. package/dist/contract/subscribe.mjs +50 -0
  82. package/dist/contract/transient.mjs +28 -0
  83. package/dist/contract/watch.d.mts +18 -0
  84. package/dist/contract/watch.mjs +105 -0
  85. package/dist/contract/webhooks.d.mts +42 -0
  86. package/dist/contract/webhooks.mjs +53 -0
  87. package/dist/http/client.d.mts +37 -14
  88. package/dist/http/client.mjs +52 -14
  89. package/dist/http/events.d.mts +56 -0
  90. package/dist/http/events.mjs +102 -0
  91. package/dist/http/placed.d.mts +36 -0
  92. package/dist/http/placed.mjs +71 -0
  93. package/dist/http/schema-errors.mjs +2 -3
  94. package/dist/http/streamed.d.mts +21 -0
  95. package/dist/http/streamed.mjs +94 -0
  96. package/dist/http/watch.mjs +50 -0
  97. package/dist/mcp/bridge.mjs +283 -0
  98. package/dist/mcp/client.d.mts +30 -99
  99. package/dist/mcp/client.mjs +60 -134
  100. package/dist/mcp/events.mjs +142 -0
  101. package/dist/mcp/progress.mjs +34 -0
  102. package/dist/mcp/protocol.mjs +40 -15
  103. package/dist/mcp/reply.d.mts +79 -0
  104. package/dist/mcp/reply.mjs +99 -0
  105. package/dist/mcp/request.d.mts +40 -0
  106. package/dist/mcp/request.mjs +51 -0
  107. package/dist/mcp/watch.mjs +115 -0
  108. package/dist/rpc/watch.mjs +39 -0
  109. package/dist/stdio/console.mjs +14 -7
  110. package/dist/testing/memory.d.mts +1 -3
  111. package/dist/testing/memory.mjs +4 -5
  112. package/dist/webhook/address.mjs +47 -0
  113. package/dist/webhook/attempt.mjs +65 -0
  114. package/dist/webhook/receiver.mjs +67 -0
  115. package/dist/webhook/signing.mjs +28 -0
  116. package/dist/webhook/stored.mjs +57 -0
  117. package/dist/webhook/subscriptions.mjs +342 -0
  118. package/docs/Action-failures.md +2 -2
  119. package/docs/Action-streams.md +115 -0
  120. package/docs/Action.md +58 -124
  121. package/docs/ActionCli-failures.md +1 -2
  122. package/docs/ActionCli.md +83 -88
  123. package/docs/ActionHttp-failures.md +3 -3
  124. package/docs/ActionHttp.md +90 -70
  125. package/docs/ActionMcp-failures.md +2 -1
  126. package/docs/ActionMcp.md +120 -92
  127. package/docs/ActionRpc-events.md +126 -0
  128. package/docs/ActionRpc-failures.md +1 -2
  129. package/docs/ActionRpc.md +108 -79
  130. package/docs/ActionToolkit-failures.md +1 -2
  131. package/docs/ActionToolkit.md +33 -90
  132. package/docs/Authentication-advanced.md +57 -61
  133. package/docs/Authentication-failures.md +5 -4
  134. package/docs/Authentication.md +23 -58
  135. package/docs/Event-failures.md +15 -0
  136. package/docs/Event.md +281 -0
  137. package/docs/README.md +51 -82
  138. package/docs/Testing-failures.md +1 -5
  139. package/docs/Testing.md +126 -86
  140. package/docs/Webhook-failures.md +8 -0
  141. package/docs/Webhook.md +319 -0
  142. package/docs/setup-failures.md +2 -3
  143. package/docs/setup.md +32 -53
  144. package/examples/README.md +12 -37
  145. package/examples/assistant/README.md +21 -26
  146. package/examples/assistant/src/agent.ts +66 -24
  147. package/examples/assistant/src/cli.ts +7 -4
  148. package/examples/assistant/src/commands.ts +9 -4
  149. package/examples/assistant/src/identity/Identity.ts +5 -4
  150. package/examples/assistant/src/identity/authorize.ts +13 -5
  151. package/examples/assistant/src/identity/handlers.ts +3 -2
  152. package/examples/assistant/src/identity/permissions.ts +9 -3
  153. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  154. package/examples/assistant/src/scripted.ts +7 -4
  155. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  156. package/examples/assistant/src/tasks/handlers.ts +9 -5
  157. package/examples/assistant/src/tasks/store.ts +38 -11
  158. package/examples/assistant/src/toolkit.ts +11 -7
  159. package/examples/assistant/test/commands.test.ts +53 -27
  160. package/examples/assistant/test/identity/identity.test.ts +27 -11
  161. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  162. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  163. package/examples/assistant/test/toolkit.test.ts +104 -75
  164. package/examples/build-events/README.md +83 -0
  165. package/examples/build-events/src/Api.ts +27 -0
  166. package/examples/build-events/src/Rpc.ts +26 -0
  167. package/examples/build-events/src/builds/Builds.ts +108 -0
  168. package/examples/build-events/src/builds/events.ts +54 -0
  169. package/examples/build-events/src/builds/handlers.ts +58 -0
  170. package/examples/build-events/src/builds/log.ts +30 -0
  171. package/examples/build-events/src/builds/status.ts +28 -0
  172. package/examples/build-events/src/builds/store.ts +130 -0
  173. package/examples/build-events/src/cli.ts +18 -0
  174. package/examples/build-events/src/commands.ts +32 -0
  175. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  176. package/examples/build-events/src/deploys/approvers.ts +55 -0
  177. package/examples/build-events/src/deploys/events.ts +32 -0
  178. package/examples/build-events/src/deploys/handlers.ts +38 -0
  179. package/examples/build-events/src/identity/Identity.ts +29 -0
  180. package/examples/build-events/src/identity/members.ts +7 -0
  181. package/examples/build-events/src/identity/verifier.ts +22 -0
  182. package/examples/build-events/src/notifier/Notices.ts +26 -0
  183. package/examples/build-events/src/notifier/routes.ts +71 -0
  184. package/examples/build-events/src/notifier.ts +48 -0
  185. package/examples/build-events/src/routes.ts +71 -0
  186. package/examples/build-events/src/server.ts +62 -0
  187. package/examples/build-events/test/builds/events.test.ts +68 -0
  188. package/examples/build-events/test/builds/log.test.ts +71 -0
  189. package/examples/build-events/test/builds/progress.test.ts +57 -0
  190. package/examples/build-events/test/builds/status.test.ts +42 -0
  191. package/examples/build-events/test/commands.test.ts +103 -0
  192. package/examples/build-events/test/deploys/events.test.ts +52 -0
  193. package/examples/build-events/test/http.test.ts +95 -0
  194. package/examples/build-events/test/notifier.test.ts +80 -0
  195. package/examples/build-events/test/routes.test.ts +96 -0
  196. package/examples/build-events/test/rpc.test.ts +84 -0
  197. package/examples/build-events/test/support/as.ts +8 -0
  198. package/examples/build-events/test/support/memory.ts +43 -0
  199. package/examples/build-events/test/webhooks.test.ts +143 -0
  200. package/examples/kitchen-sink/README.md +178 -166
  201. package/examples/kitchen-sink/src/Actions.ts +11 -3
  202. package/examples/kitchen-sink/src/Api.ts +12 -6
  203. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  204. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  205. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  206. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  207. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  208. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  209. package/examples/kitchen-sink/src/agent.ts +14 -6
  210. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  211. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  212. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  213. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  214. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  215. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  216. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  217. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  218. package/examples/kitchen-sink/src/browser.ts +10 -4
  219. package/examples/kitchen-sink/src/cli.ts +28 -12
  220. package/examples/kitchen-sink/src/config.ts +1 -1
  221. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  222. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  223. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  224. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  225. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  226. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  227. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  228. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  229. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  230. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  231. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  232. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  233. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  234. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  235. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  236. package/examples/kitchen-sink/src/memory.ts +11 -2
  237. package/examples/kitchen-sink/src/ops.ts +10 -5
  238. package/examples/kitchen-sink/src/request-policy.ts +25 -9
  239. package/examples/kitchen-sink/src/routes.ts +67 -27
  240. package/examples/kitchen-sink/src/sdk.ts +22 -12
  241. package/examples/kitchen-sink/src/seed.ts +15 -9
  242. package/examples/kitchen-sink/src/server.ts +24 -10
  243. package/examples/kitchen-sink/src/stdio.ts +9 -3
  244. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  245. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  246. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  247. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  248. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  249. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  250. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  251. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  252. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  253. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  254. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  255. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  256. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  257. package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
  258. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  259. package/examples/kitchen-sink/test/support/as.ts +9 -0
  260. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  261. package/examples/kitchen-sink/test/support/services.ts +17 -0
  262. package/examples/live-rpc/README.md +33 -35
  263. package/examples/live-rpc/src/Rpc.ts +9 -5
  264. package/examples/live-rpc/src/browser.ts +39 -11
  265. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  266. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  267. package/examples/live-rpc/src/chat/events.ts +74 -0
  268. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  269. package/examples/live-rpc/src/chat/room.ts +9 -5
  270. package/examples/live-rpc/src/chat/store.ts +51 -14
  271. package/examples/live-rpc/src/client.ts +22 -5
  272. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  273. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  274. package/examples/live-rpc/src/page.ts +58 -12
  275. package/examples/live-rpc/src/routes.ts +14 -10
  276. package/examples/live-rpc/src/server.ts +15 -5
  277. package/examples/live-rpc/test/browser.test.ts +127 -50
  278. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  279. package/examples/live-rpc/test/routes.test.ts +130 -46
  280. package/examples/live-rpc/test/server.test.ts +94 -33
  281. package/examples/mcp-tools/README.md +24 -38
  282. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  283. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  284. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  285. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  286. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  287. package/examples/mcp-tools/src/routes.ts +8 -6
  288. package/examples/mcp-tools/src/server.ts +12 -5
  289. package/examples/mcp-tools/src/stdio.ts +3 -2
  290. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  291. package/examples/mcp-tools/test/routes.test.ts +140 -81
  292. package/examples/notes-service/README.md +30 -45
  293. package/examples/notes-service/src/Api.ts +1 -0
  294. package/examples/notes-service/src/browser.ts +15 -14
  295. package/examples/notes-service/src/cli.ts +8 -3
  296. package/examples/notes-service/src/commands.ts +46 -20
  297. package/examples/notes-service/src/identity/Identity.ts +2 -2
  298. package/examples/notes-service/src/identity/authorize.ts +6 -2
  299. package/examples/notes-service/src/identity/handlers.ts +3 -2
  300. package/examples/notes-service/src/identity/users.ts +2 -1
  301. package/examples/notes-service/src/identity/verifier.ts +12 -6
  302. package/examples/notes-service/src/notes/Notes.ts +46 -22
  303. package/examples/notes-service/src/notes/handlers.ts +18 -9
  304. package/examples/notes-service/src/notes/store.ts +70 -43
  305. package/examples/notes-service/src/routes.ts +7 -5
  306. package/examples/notes-service/src/server.ts +12 -5
  307. package/examples/notes-service/src/status/Status.ts +1 -1
  308. package/examples/notes-service/src/status/handlers.ts +5 -3
  309. package/examples/notes-service/src/stdio.ts +7 -3
  310. package/examples/notes-service/src/worker.ts +6 -3
  311. package/examples/notes-service/test/commands.test.ts +164 -91
  312. package/examples/notes-service/test/notes/as.ts +3 -1
  313. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  314. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  315. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  316. package/examples/notes-service/test/routes.test.ts +106 -71
  317. package/examples/notes-service/test/worker.test.ts +21 -13
  318. package/examples/quickstart/README.md +2 -6
  319. package/examples/quickstart/src/Api.ts +1 -0
  320. package/examples/quickstart/src/client.ts +6 -3
  321. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  322. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  323. package/examples/quickstart/src/routes.ts +3 -2
  324. package/examples/quickstart/src/server.ts +6 -2
  325. package/examples/quickstart/test/routes.test.ts +3 -2
  326. package/package.json +19 -17
@@ -1,25 +1,37 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
- import { Deferred, Effect, Fiber, flow, Layer } from "effect";
3
- import type { LanguageModel } from "effect/ai";
4
- import { HttpClient, HttpClientRequest } from "effect/http";
5
2
  import * as Action from "@gjermundgaraba/effect-actions/Action";
6
3
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
7
4
  import * as Testing from "@gjermundgaraba/effect-actions/Testing";
5
+ import { Deferred, Effect, Fiber, flow, Layer } from "effect";
6
+ import type { LanguageModel } from "effect/ai";
7
+ import { HttpClient, HttpClientRequest } from "effect/http";
8
+
8
9
  import { Http } from "../../src/Api.js";
9
- import { AssistantFailed, ConversationNotFound } from "../../src/assistant/Assistant.js";
10
- import { memory } from "../../src/memory.js";
10
+ import {
11
+ AssistantFailed,
12
+ ConversationNotFound,
13
+ } from "../../src/assistant/Assistant.js";
11
14
  import { publicRoutes } from "../../src/routes.js";
12
15
  import { down, scripted } from "../support/scripted.js";
16
+ import { services } from "../support/services.js";
13
17
 
14
18
  // The assistant as its callers reach it: over HTTP, a request per turn, on the real routes in
15
19
  // memory. Each test gives the routes the model it is about, in place of the demo one: the
16
20
  // first `Layer.provide` is what the routes get.
17
21
  const served = (model: Layer.Layer<LanguageModel.LanguageModel>) =>
18
- Effect.provide(Testing.layer(publicRoutes).pipe(Layer.provide(model), Layer.provide(memory)));
22
+ Effect.provide(
23
+ Testing.layer(publicRoutes).pipe(
24
+ Layer.provide(model),
25
+ Layer.provide(services())
26
+ )
27
+ );
19
28
 
20
29
  const signedIn = (token: string) => ({
21
30
  transformClient: HttpClient.mapRequest(
22
- flow(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("x-workspace", "acme")),
31
+ flow(
32
+ HttpClientRequest.bearerToken(token),
33
+ HttpClientRequest.setHeader("x-workspace", "acme")
34
+ )
23
35
  ),
24
36
  });
25
37
 
@@ -28,8 +40,11 @@ const asAda = ActionHttp.client(Http, signedIn("ada"));
28
40
 
29
41
  const asLinus = ActionHttp.client(Http, signedIn("linus"));
30
42
 
31
- const call = (id: string, name: string, params: Readonly<Record<string, string>>) =>
32
- ({ type: "tool-call", id, name, params }) as const;
43
+ const call = (
44
+ id: string,
45
+ name: string,
46
+ params: Readonly<Record<string, string>>
47
+ ) => ({ type: "tool-call", id, name, params }) as const;
33
48
 
34
49
  const says = (text: string) => [{ type: "text", text }] as const;
35
50
 
@@ -37,374 +52,531 @@ const approving = (waiting: ReadonlyArray<{ readonly approvalId: string }>) =>
37
52
  waiting.map(({ approvalId }) => ({ approvalId, approved: true }));
38
53
 
39
54
  describe("the assistant, behind a route", () => {
40
- it.effect("reads at once, and writes in the request that carries its member's approval", () =>
41
- Effect.gen(function* () {
42
- const ada = yield* asAda;
43
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
55
+ it.effect(
56
+ "reads at once, and writes in the request that carries its member's approval",
57
+ () =>
58
+ Effect.gen(function* () {
59
+ const ada = yield* asAda;
44
60
 
45
- // One request, two requests to the model: the second lets it read its tool's result.
46
- const read = yield* ada.askAssistant({ prompt: "What is open?" });
61
+ const { id } = yield* ada.createIssue({
62
+ title: "Crash",
63
+ body: "On save.",
64
+ });
47
65
 
48
- expect(read).toMatchObject({
49
- text: "One issue is open.",
50
- ran: [{ tool: "listIssues", failed: false }],
51
- waiting: [],
52
- });
66
+ // One request, two requests to the model: the second lets it read its tool's result.
67
+ const read = yield* ada.askAssistant({ prompt: "What is open?" });
53
68
 
54
- // The same conversation, a request later. The write has not run: the turn ended on it.
55
- const asked = yield* ada.askAssistant({
56
- conversation: read.conversation,
57
- prompt: "Close it.",
58
- });
69
+ expect(read).toMatchObject({
70
+ text: "One issue is open.",
71
+ ran: [{ tool: "listIssues", failed: false }],
72
+ waiting: [],
73
+ });
59
74
 
60
- expect(asked).toMatchObject({
61
- conversation: read.conversation,
62
- ran: [],
63
- waiting: [{ tool: "closeIssue", arguments: JSON.stringify({ id }) }],
64
- });
65
- expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
75
+ // The same conversation, a request later. The write has not run: the turn ended on it.
76
+ const asked = yield* ada.askAssistant({
77
+ conversation: read.conversation,
78
+ prompt: "Close it.",
79
+ });
66
80
 
67
- const answered = yield* ada.answerAssistant({
68
- conversation: asked.conversation,
69
- answers: approving(asked.waiting),
70
- });
81
+ expect(asked).toMatchObject({
82
+ conversation: read.conversation,
83
+ ran: [],
84
+ waiting: [{ tool: "closeIssue", arguments: JSON.stringify({ id }) }],
85
+ });
86
+ expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
71
87
 
72
- expect(answered).toEqual({
73
- conversation: asked.conversation,
74
- text: "Closed.",
75
- ran: [{ tool: "closeIssue", failed: false }],
76
- waiting: [],
77
- });
78
- expect(yield* ada.getIssue({ id })).toMatchObject({ status: "closed" });
79
- }).pipe(
80
- served(
81
- scripted(
82
- [call("1", "listIssues", { status: "open" })],
83
- says("One issue is open."),
84
- [call("2", "closeIssue", { id: "ACME-1" })],
85
- says("Closed."),
86
- ),
87
- ),
88
- ),
88
+ const answered = yield* ada.answerAssistant({
89
+ conversation: asked.conversation,
90
+ answers: approving(asked.waiting),
91
+ });
92
+
93
+ expect(answered).toStrictEqual({
94
+ conversation: asked.conversation,
95
+ text: "Closed.",
96
+ ran: [{ tool: "closeIssue", failed: false }],
97
+ waiting: [],
98
+ });
99
+ expect(yield* ada.getIssue({ id })).toMatchObject({ status: "closed" });
100
+ }).pipe(
101
+ served(
102
+ scripted(
103
+ [call("1", "listIssues", { status: "open" })],
104
+ says("One issue is open."),
105
+ [call("2", "closeIssue", { id: "ACME-1" })],
106
+ says("Closed.")
107
+ )
108
+ )
109
+ )
89
110
  );
90
111
 
91
- it.effect("lets only the member who was asked answer, or continue the conversation", () =>
92
- Effect.gen(function* () {
93
- const ada = yield* asAda;
94
- const linus = yield* asLinus;
95
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
96
- const asked = yield* ada.askAssistant({ prompt: "Close it." });
97
- const { conversation } = asked;
98
-
99
- expect(
100
- yield* Effect.flip(
101
- linus.answerAssistant({ conversation, answers: approving(asked.waiting) }),
102
- ),
103
- ).toEqual(new ConversationNotFound({ conversation }));
104
- expect(yield* Effect.flip(linus.askAssistant({ conversation, prompt: "Go on." }))).toEqual(
105
- new ConversationNotFound({ conversation }),
106
- );
107
- expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
108
- }).pipe(served(scripted([call("1", "closeIssue", { id: "ACME-1" })]))),
112
+ it.effect(
113
+ "lets only the member who was asked answer, or continue the conversation",
114
+ () =>
115
+ Effect.gen(function* () {
116
+ const ada = yield* asAda;
117
+ const linus = yield* asLinus;
118
+
119
+ const { id } = yield* ada.createIssue({
120
+ title: "Crash",
121
+ body: "On save.",
122
+ });
123
+
124
+ const asked = yield* ada.askAssistant({ prompt: "Close it." });
125
+ const { conversation } = asked;
126
+
127
+ expect(
128
+ yield* Effect.flip(
129
+ linus.answerAssistant({
130
+ conversation,
131
+ answers: approving(asked.waiting),
132
+ })
133
+ )
134
+ ).toStrictEqual(new ConversationNotFound({ conversation }));
135
+ expect(
136
+ yield* Effect.flip(
137
+ linus.askAssistant({ conversation, prompt: "Go on." })
138
+ )
139
+ ).toStrictEqual(new ConversationNotFound({ conversation }));
140
+ expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
141
+ }).pipe(served(scripted([call("1", "closeIssue", { id: "ACME-1" })])))
109
142
  );
110
143
 
111
- it.effect("runs an approved call as its caller: approval lets no one do what they may not", () =>
112
- Effect.gen(function* () {
113
- const ada = yield* asAda;
114
- const linus = yield* asLinus;
115
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
144
+ it.effect(
145
+ "runs an approved call as its caller: approval lets no one do what they may not",
146
+ () =>
147
+ Effect.gen(function* () {
148
+ const ada = yield* asAda;
149
+ const linus = yield* asLinus;
116
150
 
117
- // Anyone signed in may ask. The tool's own `authorize` then refuses the reader's write,
118
- // as a result the model reads.
119
- const asked = yield* linus.askAssistant({ prompt: "Close it." });
151
+ const { id } = yield* ada.createIssue({
152
+ title: "Crash",
153
+ body: "On save.",
154
+ });
120
155
 
121
- const answered = yield* linus.answerAssistant({
122
- conversation: asked.conversation,
123
- answers: approving(asked.waiting),
124
- });
156
+ // Anyone signed in may ask. The tool's own `authorize` then refuses the reader's write,
157
+ // as a result the model reads.
158
+ const asked = yield* linus.askAssistant({ prompt: "Close it." });
125
159
 
126
- expect(answered).toMatchObject({
127
- text: "You may not close issues.",
128
- ran: [{ tool: "closeIssue", failed: true }],
129
- });
130
- expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
131
- }).pipe(
132
- served(
133
- scripted([call("1", "closeIssue", { id: "ACME-1" })], says("You may not close issues.")),
134
- ),
135
- ),
160
+ const answered = yield* linus.answerAssistant({
161
+ conversation: asked.conversation,
162
+ answers: approving(asked.waiting),
163
+ });
164
+
165
+ expect(answered).toMatchObject({
166
+ text: "You may not close issues.",
167
+ ran: [{ tool: "closeIssue", failed: true }],
168
+ });
169
+ expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
170
+ }).pipe(
171
+ served(
172
+ scripted(
173
+ [call("1", "closeIssue", { id: "ACME-1" })],
174
+ says("You may not close issues.")
175
+ )
176
+ )
177
+ )
136
178
  );
137
179
 
138
- it.effect("ends the conversation when the model fails after an approved call ran", () =>
139
- Effect.gen(function* () {
140
- const ada = yield* asAda;
141
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
142
- const asked = yield* ada.askAssistant({ prompt: "Say it is fixed." });
180
+ it.effect(
181
+ "ends the conversation when the model fails after an approved call ran",
182
+ () =>
183
+ Effect.gen(function* () {
184
+ const ada = yield* asAda;
143
185
 
144
- const answer = ada.answerAssistant({
145
- conversation: asked.conversation,
146
- answers: approving(asked.waiting),
147
- });
186
+ const { id } = yield* ada.createIssue({
187
+ title: "Crash",
188
+ body: "On save.",
189
+ });
148
190
 
149
- // The approved call ran, then the model failed: the error says so.
150
- expect(yield* Effect.flip(answer)).toBeInstanceOf(AssistantFailed);
151
- expect((yield* ada.getIssue({ id })).comments).toEqual([{ author: "ada", text: "Fixed." }]);
191
+ const asked = yield* ada.askAssistant({ prompt: "Say it is fixed." });
152
192
 
153
- // The same answer, sent again, runs nothing: its conversation is gone.
154
- expect(yield* Effect.flip(answer)).toEqual(
155
- new ConversationNotFound({ conversation: asked.conversation }),
156
- );
157
- expect((yield* ada.getIssue({ id })).comments).toHaveLength(1);
158
- }).pipe(
159
- served(scripted([call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." })], down)),
160
- ),
193
+ const answer = ada.answerAssistant({
194
+ conversation: asked.conversation,
195
+ answers: approving(asked.waiting),
196
+ });
197
+
198
+ // The approved call ran, then the model failed: the error says so.
199
+ expect(yield* Effect.flip(answer)).toBeInstanceOf(AssistantFailed);
200
+ expect((yield* ada.getIssue({ id })).comments).toStrictEqual([
201
+ { author: "ada", text: "Fixed." },
202
+ ]);
203
+
204
+ // The same answer, sent again, runs nothing: its conversation is gone.
205
+ expect(yield* Effect.flip(answer)).toStrictEqual(
206
+ new ConversationNotFound({ conversation: asked.conversation })
207
+ );
208
+ expect((yield* ada.getIssue({ id })).comments).toHaveLength(1);
209
+ }).pipe(
210
+ served(
211
+ scripted(
212
+ [call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." })],
213
+ down
214
+ )
215
+ )
216
+ )
161
217
  );
162
218
 
163
219
  it.effect("runs nothing for a denied call, and lists none as run", () =>
164
220
  Effect.gen(function* () {
165
221
  const ada = yield* asAda;
166
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
222
+
223
+ const { id } = yield* ada.createIssue({
224
+ title: "Crash",
225
+ body: "On save.",
226
+ });
227
+
167
228
  const asked = yield* ada.askAssistant({ prompt: "Close it." });
168
229
 
169
230
  const denied = yield* ada.answerAssistant({
170
231
  conversation: asked.conversation,
171
- answers: asked.waiting.map(({ approvalId }) => ({ approvalId, approved: false })),
232
+ answers: asked.waiting.map(({ approvalId }) => ({
233
+ approvalId,
234
+ approved: false,
235
+ })),
172
236
  });
173
237
 
174
- expect(denied).toMatchObject({ text: "Left open.", ran: [], waiting: [] });
175
- expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
176
- }).pipe(served(scripted([call("1", "closeIssue", { id: "ACME-1" })], says("Left open.")))),
177
- );
178
-
179
- it.effect("lists no call of a reply the provider cut short: none of them ran", () =>
180
- Effect.gen(function* () {
181
- const ada = yield* asAda;
182
-
183
- yield* ada.createIssue({ title: "Crash", body: "On save." });
184
-
185
- expect(yield* ada.askAssistant({ prompt: "What is open?" })).toMatchObject({
186
- text: "I was cut off.",
238
+ expect(denied).toMatchObject({
239
+ text: "Left open.",
187
240
  ran: [],
188
241
  waiting: [],
189
242
  });
243
+ expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
190
244
  }).pipe(
191
245
  served(
192
246
  scripted(
193
- [
194
- call("1", "listIssues", { status: "open" }),
195
- { type: "finish", reason: "length", usage: { inputTokens: {}, outputTokens: {} } },
196
- ],
197
- says("I was cut off."),
198
- ),
199
- ),
200
- ),
247
+ [call("1", "closeIssue", { id: "ACME-1" })],
248
+ says("Left open.")
249
+ )
250
+ )
251
+ )
201
252
  );
202
253
 
203
- it.effect("runs the one call of two that is approved, and leaves the other open", () =>
204
- Effect.gen(function* () {
205
- const ada = yield* asAda;
206
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
207
- const asked = yield* ada.askAssistant({ prompt: "Say it is fixed, and close it." });
208
- const [first, second] = approving(asked.waiting);
254
+ it.effect(
255
+ "lists no call of a reply the provider cut short: none of them ran",
256
+ () =>
257
+ Effect.gen(function* () {
258
+ const ada = yield* asAda;
209
259
 
210
- expect(asked.waiting.map(({ tool }) => tool)).toEqual(["commentOnIssue", "closeIssue"]);
260
+ yield* ada.createIssue({ title: "Crash", body: "On save." });
211
261
 
212
- const answered = yield* ada.answerAssistant({
213
- conversation: asked.conversation,
214
- answers: first === undefined ? [] : [first],
215
- });
262
+ expect(
263
+ yield* ada.askAssistant({ prompt: "What is open?" })
264
+ ).toMatchObject({
265
+ text: "I was cut off.",
266
+ ran: [],
267
+ waiting: [],
268
+ });
269
+ }).pipe(
270
+ served(
271
+ scripted(
272
+ [
273
+ call("1", "listIssues", { status: "open" }),
274
+ {
275
+ type: "finish",
276
+ reason: "length",
277
+ usage: { inputTokens: {}, outputTokens: {} },
278
+ },
279
+ ],
280
+ says("I was cut off.")
281
+ )
282
+ )
283
+ )
284
+ );
216
285
 
217
- // The call left open is listed again, in every turn until it is answered.
218
- expect(answered).toMatchObject({ ran: [{ tool: "commentOnIssue" }] });
219
- expect(answered.waiting).toEqual([
220
- { approvalId: second?.approvalId, tool: "closeIssue", arguments: '{"id":"ACME-1"}' },
221
- ]);
222
- expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
286
+ it.effect(
287
+ "runs the one call of two that is approved, and leaves the other open",
288
+ () =>
289
+ Effect.gen(function* () {
290
+ const ada = yield* asAda;
223
291
 
224
- const closed = yield* ada.answerAssistant({
225
- conversation: asked.conversation,
226
- answers: second === undefined ? [] : [second],
227
- });
292
+ const { id } = yield* ada.createIssue({
293
+ title: "Crash",
294
+ body: "On save.",
295
+ });
228
296
 
229
- expect(closed.waiting).toEqual([]);
230
- expect(yield* ada.getIssue({ id })).toMatchObject({ status: "closed" });
231
- expect((yield* ada.getIssue({ id })).comments).toHaveLength(1);
232
- }).pipe(
233
- served(
234
- scripted(
235
- [
236
- call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." }),
237
- call("2", "closeIssue", { id: "ACME-1" }),
238
- ],
239
- says("Said."),
240
- says("Closed."),
241
- ),
242
- ),
243
- ),
297
+ const asked = yield* ada.askAssistant({
298
+ prompt: "Say it is fixed, and close it.",
299
+ });
300
+
301
+ const [first, second] = approving(asked.waiting);
302
+
303
+ expect(asked.waiting.map(({ tool }) => tool)).toStrictEqual([
304
+ "commentOnIssue",
305
+ "closeIssue",
306
+ ]);
307
+
308
+ const answered = yield* ada.answerAssistant({
309
+ conversation: asked.conversation,
310
+ answers: first === undefined ? [] : [first],
311
+ });
312
+
313
+ // The call left open is listed again, in every turn until it is answered.
314
+ expect(answered).toMatchObject({ ran: [{ tool: "commentOnIssue" }] });
315
+ expect(answered.waiting).toStrictEqual([
316
+ {
317
+ approvalId: second?.approvalId,
318
+ tool: "closeIssue",
319
+ arguments: '{"id":"ACME-1"}',
320
+ },
321
+ ]);
322
+ expect(yield* ada.getIssue({ id })).toMatchObject({ status: "open" });
323
+
324
+ const closed = yield* ada.answerAssistant({
325
+ conversation: asked.conversation,
326
+ answers: second === undefined ? [] : [second],
327
+ });
328
+
329
+ expect(closed.waiting).toStrictEqual([]);
330
+ expect(yield* ada.getIssue({ id })).toMatchObject({ status: "closed" });
331
+ expect((yield* ada.getIssue({ id })).comments).toHaveLength(1);
332
+ }).pipe(
333
+ served(
334
+ scripted(
335
+ [
336
+ call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." }),
337
+ call("2", "closeIssue", { id: "ACME-1" }),
338
+ ],
339
+ says("Said."),
340
+ says("Closed.")
341
+ )
342
+ )
343
+ )
244
344
  );
245
345
 
246
- it.effect("keeps listing a request left open while its member asks for something else", () =>
247
- Effect.gen(function* () {
248
- const ada = yield* asAda;
249
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
250
- const first = yield* ada.askAssistant({ prompt: "Say it is fixed." });
346
+ it.effect(
347
+ "keeps listing a request left open while its member asks for something else",
348
+ () =>
349
+ Effect.gen(function* () {
350
+ const ada = yield* asAda;
251
351
 
252
- const second = yield* ada.askAssistant({
253
- conversation: first.conversation,
254
- prompt: "Close it too.",
255
- });
352
+ const { id } = yield* ada.createIssue({
353
+ title: "Crash",
354
+ body: "On save.",
355
+ });
256
356
 
257
- expect(second.waiting.map(({ tool }) => tool)).toEqual(["commentOnIssue", "closeIssue"]);
357
+ const first = yield* ada.askAssistant({ prompt: "Say it is fixed." });
258
358
 
259
- const [, close] = approving(second.waiting);
359
+ const second = yield* ada.askAssistant({
360
+ conversation: first.conversation,
361
+ prompt: "Close it too.",
362
+ });
260
363
 
261
- const closed = yield* ada.answerAssistant({
262
- conversation: first.conversation,
263
- answers: close === undefined ? [] : [close],
264
- });
364
+ expect(second.waiting.map(({ tool }) => tool)).toStrictEqual([
365
+ "commentOnIssue",
366
+ "closeIssue",
367
+ ]);
265
368
 
266
- // Each answer runs its own call: the close ran, the comment still waits.
267
- expect(closed.ran).toEqual([{ tool: "closeIssue", failed: false }]);
268
- expect(closed.waiting).toEqual(first.waiting);
269
- expect(yield* ada.getIssue({ id })).toMatchObject({ status: "closed", comments: [] });
270
- }).pipe(
271
- served(
272
- scripted(
273
- [call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." })],
274
- [call("2", "closeIssue", { id: "ACME-1" })],
275
- says("Closed."),
276
- ),
277
- ),
278
- ),
369
+ const [, close] = approving(second.waiting);
370
+
371
+ const closed = yield* ada.answerAssistant({
372
+ conversation: first.conversation,
373
+ answers: close === undefined ? [] : [close],
374
+ });
375
+
376
+ // Each answer runs its own call: the close ran, the comment still waits.
377
+ expect(closed.ran).toStrictEqual([
378
+ { tool: "closeIssue", failed: false },
379
+ ]);
380
+ expect(closed.waiting).toStrictEqual(first.waiting);
381
+ expect(yield* ada.getIssue({ id })).toMatchObject({
382
+ status: "closed",
383
+ comments: [],
384
+ });
385
+ }).pipe(
386
+ served(
387
+ scripted(
388
+ [call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." })],
389
+ [call("2", "closeIssue", { id: "ACME-1" })],
390
+ says("Closed.")
391
+ )
392
+ )
393
+ )
279
394
  );
280
395
 
281
396
  // A provider gives each call an id of its own. Where a model does not, Effect runs the last
282
397
  // call with the id an approval names: the list shows that call, not the one first asked.
283
- it.effect("lists, for a call id a model used twice, the call an approval would run", () =>
284
- Effect.gen(function* () {
285
- const ada = yield* asAda;
286
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
287
- const first = yield* ada.askAssistant({ prompt: "Say it is fixed." });
398
+ it.effect(
399
+ "lists, for a call id a model used twice, the call an approval would run",
400
+ () =>
401
+ Effect.gen(function* () {
402
+ const ada = yield* asAda;
288
403
 
289
- const second = yield* ada.askAssistant({
290
- conversation: first.conversation,
291
- prompt: "Close it too.",
292
- });
404
+ const { id } = yield* ada.createIssue({
405
+ title: "Crash",
406
+ body: "On save.",
407
+ });
293
408
 
294
- expect(second.waiting.map(({ tool }) => tool)).toEqual(["closeIssue", "closeIssue"]);
409
+ const first = yield* ada.askAssistant({ prompt: "Say it is fixed." });
295
410
 
296
- const answered = yield* ada.answerAssistant({
297
- conversation: first.conversation,
298
- answers: approving(first.waiting),
299
- });
411
+ const second = yield* ada.askAssistant({
412
+ conversation: first.conversation,
413
+ prompt: "Close it too.",
414
+ });
300
415
 
301
- expect(answered).toMatchObject({ ran: [{ tool: "closeIssue" }], waiting: [] });
302
- expect(yield* ada.getIssue({ id })).toMatchObject({ status: "closed", comments: [] });
303
- }).pipe(
304
- served(
305
- scripted(
306
- [call("same", "commentOnIssue", { id: "ACME-1", text: "Fixed." })],
307
- [call("same", "closeIssue", { id: "ACME-1" })],
308
- says("Closed."),
309
- ),
310
- ),
311
- ),
416
+ expect(second.waiting.map(({ tool }) => tool)).toStrictEqual([
417
+ "closeIssue",
418
+ "closeIssue",
419
+ ]);
420
+
421
+ const answered = yield* ada.answerAssistant({
422
+ conversation: first.conversation,
423
+ answers: approving(first.waiting),
424
+ });
425
+
426
+ expect(answered).toMatchObject({
427
+ ran: [{ tool: "closeIssue" }],
428
+ waiting: [],
429
+ });
430
+ expect(yield* ada.getIssue({ id })).toMatchObject({
431
+ status: "closed",
432
+ comments: [],
433
+ });
434
+ }).pipe(
435
+ served(
436
+ scripted(
437
+ [call("same", "commentOnIssue", { id: "ACME-1", text: "Fixed." })],
438
+ [call("same", "closeIssue", { id: "ACME-1" })],
439
+ says("Closed.")
440
+ )
441
+ )
442
+ )
312
443
  );
313
444
 
314
- it.effect("runs what its member approves in a later turn of the demo model", () =>
315
- Effect.gen(function* () {
316
- const ada = yield* asAda;
445
+ it.effect(
446
+ "runs what its member approves in a later turn of the demo model",
447
+ () =>
448
+ Effect.gen(function* () {
449
+ const ada = yield* asAda;
317
450
 
318
- yield* ada.createIssue({ title: "Crash", body: "On save." });
451
+ yield* ada.createIssue({ title: "Crash", body: "On save." });
319
452
 
320
- const first = yield* ada.askAssistant({ prompt: "Tidy up." });
453
+ const first = yield* ada.askAssistant({ prompt: "Tidy up." });
321
454
 
322
- yield* ada.answerAssistant({
323
- conversation: first.conversation,
324
- answers: approving(first.waiting),
325
- });
455
+ yield* ada.answerAssistant({
456
+ conversation: first.conversation,
457
+ answers: approving(first.waiting),
458
+ });
326
459
 
327
- const again = yield* ada.askAssistant({
328
- conversation: first.conversation,
329
- prompt: "Once more.",
330
- });
460
+ const again = yield* ada.askAssistant({
461
+ conversation: first.conversation,
462
+ prompt: "Once more.",
463
+ });
331
464
 
332
- const answered = yield* ada.answerAssistant({
333
- conversation: first.conversation,
334
- answers: approving(again.waiting),
335
- });
465
+ const answered = yield* ada.answerAssistant({
466
+ conversation: first.conversation,
467
+ answers: approving(again.waiting),
468
+ });
336
469
 
337
- expect(again.waiting.map(({ tool }) => tool)).toEqual(["closeIssue"]);
338
- expect(answered).toMatchObject({ ran: [{ tool: "closeIssue" }], waiting: [] });
339
- }).pipe(Effect.provide(Testing.layer(publicRoutes).pipe(Layer.provide(memory)))),
470
+ expect(again.waiting.map(({ tool }) => tool)).toStrictEqual([
471
+ "closeIssue",
472
+ ]);
473
+ expect(answered).toMatchObject({
474
+ ran: [{ tool: "closeIssue" }],
475
+ waiting: [],
476
+ });
477
+ }).pipe(
478
+ Effect.provide(
479
+ Testing.layer(publicRoutes).pipe(Layer.provide(services()))
480
+ )
481
+ )
340
482
  );
341
483
 
342
- it.effect("refuses a request answering one approval twice, and runs nothing of it", () =>
343
- Effect.gen(function* () {
344
- const ada = yield* asAda;
345
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
346
- const asked = yield* ada.askAssistant({ prompt: "Say it is fixed." });
347
- const [answer] = approving(asked.waiting);
484
+ it.effect(
485
+ "refuses a request answering one approval twice, and runs nothing of it",
486
+ () =>
487
+ Effect.gen(function* () {
488
+ const ada = yield* asAda;
348
489
 
349
- expect(
350
- yield* Effect.flip(
351
- ada.answerAssistant({
352
- conversation: asked.conversation,
353
- answers: answer === undefined ? [] : [answer, answer],
354
- }),
355
- ),
356
- ).toBeInstanceOf(Action.InvalidInput);
357
- expect((yield* ada.getIssue({ id })).comments).toHaveLength(0);
490
+ const { id } = yield* ada.createIssue({
491
+ title: "Crash",
492
+ body: "On save.",
493
+ });
358
494
 
359
- yield* ada.answerAssistant({
360
- conversation: asked.conversation,
361
- answers: approving(asked.waiting),
362
- });
495
+ const asked = yield* ada.askAssistant({ prompt: "Say it is fixed." });
496
+ const [answer] = approving(asked.waiting);
497
+
498
+ expect(
499
+ yield* Effect.flip(
500
+ ada.answerAssistant({
501
+ conversation: asked.conversation,
502
+ answers: answer === undefined ? [] : [answer, answer],
503
+ })
504
+ )
505
+ ).toBeInstanceOf(Action.InvalidInput);
506
+ expect((yield* ada.getIssue({ id })).comments).toHaveLength(0);
507
+
508
+ yield* ada.answerAssistant({
509
+ conversation: asked.conversation,
510
+ answers: approving(asked.waiting),
511
+ });
363
512
 
364
- expect((yield* ada.getIssue({ id })).comments).toHaveLength(1);
365
- }).pipe(
366
- served(
367
- scripted([call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." })], says("Said.")),
368
- ),
369
- ),
513
+ expect((yield* ada.getIssue({ id })).comments).toHaveLength(1);
514
+ }).pipe(
515
+ served(
516
+ scripted(
517
+ [call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." })],
518
+ says("Said.")
519
+ )
520
+ )
521
+ )
370
522
  );
371
523
 
372
- it.effect("runs an approved call once when its answer arrives twice at once", () =>
373
- Effect.gen(function* () {
374
- const ada = yield* asAda;
375
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
376
- const asked = yield* ada.askAssistant({ prompt: "Say it is fixed." });
524
+ it.effect(
525
+ "runs an approved call once when its answer arrives twice at once",
526
+ () =>
527
+ Effect.gen(function* () {
528
+ const ada = yield* asAda;
377
529
 
378
- const answer = Effect.flip(
379
- ada.answerAssistant({
380
- conversation: asked.conversation,
381
- answers: approving(asked.waiting),
382
- }),
383
- );
530
+ const { id } = yield* ada.createIssue({
531
+ title: "Crash",
532
+ body: "On save.",
533
+ });
384
534
 
385
- // A double click, or a client's retry. One turn runs at a time, and the one that waited
386
- // finds its conversation ended by the one that failed.
387
- const refusals = yield* Effect.all([answer, answer], { concurrency: 2 });
535
+ const asked = yield* ada.askAssistant({ prompt: "Say it is fixed." });
388
536
 
389
- expect(refusals.map(({ _tag }) => _tag).toSorted()).toEqual([
390
- "AssistantFailed",
391
- "ConversationNotFound",
392
- ]);
393
- expect((yield* ada.getIssue({ id })).comments).toHaveLength(1);
394
- }).pipe(
395
- served(scripted([call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." })], down, down)),
396
- ),
537
+ const answer = Effect.flip(
538
+ ada.answerAssistant({
539
+ conversation: asked.conversation,
540
+ answers: approving(asked.waiting),
541
+ })
542
+ );
543
+
544
+ // A double click, or a client's retry. One turn runs at a time, and the one that waited
545
+ // finds its conversation ended by the one that failed.
546
+ const refusals = yield* Effect.all([answer, answer], {
547
+ concurrency: 2,
548
+ });
549
+
550
+ expect(refusals.map(({ _tag }) => _tag).toSorted()).toStrictEqual([
551
+ "AssistantFailed",
552
+ "ConversationNotFound",
553
+ ]);
554
+ expect((yield* ada.getIssue({ id })).comments).toHaveLength(1);
555
+ }).pipe(
556
+ served(
557
+ scripted(
558
+ [call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." })],
559
+ down,
560
+ down
561
+ )
562
+ )
563
+ )
397
564
  );
398
565
 
399
566
  it.effect(
400
567
  "ends the conversation of a turn that is interrupted after its approved call ran",
401
568
  () => {
402
569
  // Completed when the model is asked what to say about the approved call's result.
403
- const thinking = Deferred.makeUnsafe<void>();
570
+ const thinking = Deferred.makeUnsafe<undefined>();
404
571
 
405
572
  return Effect.gen(function* () {
406
573
  const ada = yield* asAda;
407
- const { id } = yield* ada.createIssue({ title: "Crash", body: "On save." });
574
+
575
+ const { id } = yield* ada.createIssue({
576
+ title: "Crash",
577
+ body: "On save.",
578
+ });
579
+
408
580
  const asked = yield* ada.askAssistant({ prompt: "Say it is fixed." });
409
581
 
410
582
  const answer = ada.answerAssistant({
@@ -418,18 +590,18 @@ describe("the assistant, behind a route", () => {
418
590
  yield* Deferred.await(thinking);
419
591
  yield* Fiber.interrupt(leaving);
420
592
 
421
- expect(yield* Effect.flip(answer)).toEqual(
422
- new ConversationNotFound({ conversation: asked.conversation }),
593
+ expect(yield* Effect.flip(answer)).toStrictEqual(
594
+ new ConversationNotFound({ conversation: asked.conversation })
423
595
  );
424
596
  expect((yield* ada.getIssue({ id })).comments).toHaveLength(1);
425
597
  }).pipe(
426
598
  served(
427
599
  scripted(
428
600
  [call("1", "commentOnIssue", { id: "ACME-1", text: "Fixed." })],
429
- Effect.andThen(Deferred.succeed(thinking, undefined), Effect.never),
430
- ),
431
- ),
601
+ Effect.andThen(Deferred.succeed(thinking, undefined), Effect.never)
602
+ )
603
+ )
432
604
  );
433
- },
605
+ }
434
606
  );
435
607
  });