experimental-a2 0.20.0 → 0.22.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 (309) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
  4. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-client.js +2 -2
  7. package/dist/actor-react.d.ts +3 -3
  8. package/dist/actor-react.js +5 -5
  9. package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
  10. package/dist/actor-shared-DFET5q16.js.map +1 -0
  11. package/dist/actor.d.ts +1 -1
  12. package/dist/actor.js +54 -43
  13. package/dist/actor.js.map +1 -1
  14. package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
  17. package/dist/ai-EEfKyc-z.js.map +1 -0
  18. package/dist/ai-client.d.ts +2 -2
  19. package/dist/ai-client.js +2 -2
  20. package/dist/ai-fx.d.ts +4 -2
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +214 -67
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  25. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  26. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  27. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  28. package/dist/ai-pi.d.ts +1 -1
  29. package/dist/ai-pi.js +3 -3
  30. package/dist/ai-pi.js.map +1 -1
  31. package/dist/ai-react.d.ts +5 -5
  32. package/dist/ai-react.js +2 -2
  33. package/dist/ai-server.d.ts +14 -17
  34. package/dist/ai-server.d.ts.map +1 -1
  35. package/dist/ai-server.js +86 -73
  36. package/dist/ai-server.js.map +1 -1
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +2 -2
  39. package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
  40. package/dist/client-BQS4i_V9.js.map +1 -0
  41. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  42. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  43. package/dist/client.d.ts +3 -3
  44. package/dist/client.js +1 -1
  45. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  46. package/dist/contract-CWukt01d.js.map +1 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +3 -3
  49. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  50. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  51. package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
  52. package/dist/internal-CvsrprnU.js.map +1 -0
  53. package/dist/otel.d.ts +1 -1
  54. package/dist/otel.js +1 -1
  55. package/dist/otel.js.map +1 -1
  56. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  57. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  61. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  62. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  63. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  64. package/dist/runtime-consumer-CIeL33wj.js +12 -0
  65. package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.js +3 -3
  68. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  69. package/dist/scheduler-vercel.d.ts +2 -2
  70. package/dist/scheduler-vercel.js +3 -3
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
  73. package/dist/server-DtFrWxi3.js.map +1 -0
  74. package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
  75. package/dist/server-qgez0Xny.d.ts.map +1 -0
  76. package/dist/server.browser.js +1 -1
  77. package/dist/server.browser.js.map +1 -1
  78. package/dist/server.d.ts +4 -4
  79. package/dist/server.js +2 -2
  80. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  81. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  82. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  83. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  84. package/dist/store-D0MhBvg6.d.ts +532 -0
  85. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js.map +1 -1
  87. package/dist/store-codec-DwrWlgQx.js +65 -0
  88. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +406 -225
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
  94. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +1598 -587
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  100. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.js +4 -4
  103. package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
  104. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  105. package/dist/store-redis.d.ts +1 -1
  106. package/dist/store-redis.d.ts.map +1 -1
  107. package/dist/store-redis.js +4 -5
  108. package/dist/store-redis.js.map +1 -1
  109. package/dist/store-sqlite.d.ts +1 -1
  110. package/dist/store-sqlite.d.ts.map +1 -1
  111. package/dist/store-sqlite.js +527 -299
  112. package/dist/store-sqlite.js.map +1 -1
  113. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  114. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  115. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  116. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  117. package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
  118. package/dist/wire-CV-Z7wz-.js.map +1 -0
  119. package/docs/01-quickstart.mdx +21 -15
  120. package/docs/02-examples.mdx +1 -1
  121. package/docs/actors/01-introduction.mdx +13 -2
  122. package/docs/actors/02-concurrency.mdx +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +12 -11
  125. package/docs/agents/03-conversations.mdx +36 -25
  126. package/docs/agents/04-harnesses.mdx +14 -7
  127. package/docs/client/03-presence.mdx +18 -14
  128. package/docs/contract/01-events.mdx +2 -2
  129. package/docs/contract/02-state.mdx +18 -14
  130. package/docs/contract/03-sessions.mdx +32 -25
  131. package/docs/index.mdx +29 -24
  132. package/docs/reference/01-core.mdx +7 -6
  133. package/docs/reference/02-server.mdx +250 -191
  134. package/docs/reference/04-ai.mdx +33 -23
  135. package/docs/reference/05-telemetry.mdx +27 -13
  136. package/docs/reference/06-errors.mdx +25 -19
  137. package/docs/reference/meta.ts +1 -1
  138. package/docs/schedulers/01-overview.mdx +8 -8
  139. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  140. package/docs/schedulers/03-qstash.mdx +4 -4
  141. package/docs/server/01-consumers.mdx +418 -0
  142. package/docs/server/02-timers.mdx +31 -21
  143. package/docs/server/03-cancellation.mdx +52 -44
  144. package/docs/server/04-routes.mdx +4 -1
  145. package/docs/server/05-sql-projections.mdx +391 -0
  146. package/docs/server/06-guarantees.mdx +61 -51
  147. package/docs/stores/01-overview.mdx +3 -3
  148. package/docs/stores/02-redis.mdx +24 -10
  149. package/docs/stores/03-redis-http.mdx +8 -6
  150. package/docs/stores/04-postgres.mdx +65 -14
  151. package/docs/stores/05-sqlite.mdx +5 -3
  152. package/docs/upgrades/01-overview.mdx +31 -0
  153. package/docs/upgrades/78-0-22.mdx +115 -0
  154. package/docs/upgrades/79-0-21.mdx +165 -0
  155. package/docs/upgrades/80-0-20.mdx +43 -0
  156. package/docs/upgrades/81-0-19.mdx +33 -0
  157. package/docs/upgrades/82-0-18.mdx +33 -0
  158. package/docs/upgrades/83-0-17.mdx +69 -0
  159. package/docs/upgrades/meta.ts +3 -0
  160. package/examples/playground/DEPLOY.md +39 -7
  161. package/examples/playground/app/a0/chat.test.tsx +2 -2
  162. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  163. package/examples/playground/app/a0/sandbox.ts +58 -13
  164. package/examples/playground/app/a0/server.ts +4 -4
  165. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  166. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  167. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  168. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  169. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  170. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  171. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  172. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  173. package/examples/playground/app/a2slack/db/a2.ts +209 -0
  174. package/examples/playground/app/a2slack/db/index.ts +73 -0
  175. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  176. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  177. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  178. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  179. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  180. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  181. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  182. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  183. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  184. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
  186. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  187. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  188. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  189. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  190. package/examples/playground/app/a2slack/events/route.ts +36 -0
  191. package/examples/playground/app/a2slack/identity.ts +44 -0
  192. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  193. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  194. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  195. package/examples/playground/app/a2slack/lag.ts +73 -0
  196. package/examples/playground/app/a2slack/layout.tsx +43 -0
  197. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  198. package/examples/playground/app/a2slack/model.test.ts +208 -0
  199. package/examples/playground/app/a2slack/model.ts +369 -0
  200. package/examples/playground/app/a2slack/models.test.ts +72 -0
  201. package/examples/playground/app/a2slack/page.tsx +10 -0
  202. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  203. package/examples/playground/app/a2slack/search/route.ts +40 -0
  204. package/examples/playground/app/a2slack/search-index.ts +108 -0
  205. package/examples/playground/app/a2slack/server.test.ts +315 -0
  206. package/examples/playground/app/a2slack/server.ts +55 -0
  207. package/examples/playground/app/a2slack/sessions.ts +50 -0
  208. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  209. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  210. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  211. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  212. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  213. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  214. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  215. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  216. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  217. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  218. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  219. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  220. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  221. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  222. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  223. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  224. package/examples/playground/app/agent/server.ts +3 -3
  225. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  226. package/examples/playground/app/chat/server.ts +45 -35
  227. package/examples/playground/app/components/demo-icons.tsx +7 -0
  228. package/examples/playground/app/components/demos.tsx +7 -0
  229. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  230. package/examples/playground/app/orders/page.tsx +1 -1
  231. package/examples/playground/app/orders/server.ts +49 -30
  232. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  233. package/examples/playground/app/recovery/page.tsx +4 -4
  234. package/examples/playground/app/recovery/server.ts +23 -32
  235. package/examples/playground/drizzle.config.ts +14 -0
  236. package/examples/playground/lib/store.ts +1 -1
  237. package/examples/playground/next.config.ts +6 -1
  238. package/examples/playground/package.json +12 -1
  239. package/examples/playground/scripts/migrate.mjs +31 -0
  240. package/examples/playground/vercel.json +9 -0
  241. package/examples/playground/vitest.drills.config.ts +32 -0
  242. package/package.json +5 -2
  243. package/sql/postgres/drizzle-schema.ts +209 -0
  244. package/sql/postgres/schema.sql +227 -67
  245. package/src/actor-shared.ts +10 -2
  246. package/src/actor.ts +101 -71
  247. package/src/ai-agent.ts +162 -111
  248. package/src/ai-control-server.ts +8 -8
  249. package/src/ai-fx.ts +324 -82
  250. package/src/ai-harness.ts +17 -2
  251. package/src/ai-pi.ts +2 -2
  252. package/src/ai-server.ts +2 -2
  253. package/src/ai.ts +7 -7
  254. package/src/client.ts +19 -2
  255. package/src/contract.ts +1 -1
  256. package/src/deterministic-id.ts +27 -4
  257. package/src/errors.ts +8 -5
  258. package/src/idempotent-replay.ts +0 -1
  259. package/src/index.ts +1 -1
  260. package/src/internal.ts +57 -16
  261. package/src/otel.ts +1 -1
  262. package/src/postgres-schema.ts +98 -57
  263. package/src/presence-recovery.ts +18 -11
  264. package/src/presence-tombstones.ts +52 -0
  265. package/src/push-envelope.ts +20 -0
  266. package/src/runtime-consumer.ts +10 -0
  267. package/src/scheduler-task.ts +2 -2
  268. package/src/scheduler-vercel.ts +1 -1
  269. package/src/server.browser.ts +1 -1
  270. package/src/server.ts +819 -218
  271. package/src/session-socket.ts +1 -1
  272. package/src/store-codec.ts +66 -0
  273. package/src/store-memory.ts +604 -286
  274. package/src/store-postgres.ts +2006 -857
  275. package/src/store-presence-polling.ts +59 -34
  276. package/src/store-redis-core.ts +1961 -1333
  277. package/src/store-redis-notify.ts +76 -26
  278. package/src/store-redis.ts +2 -3
  279. package/src/store-sqlite.ts +915 -516
  280. package/src/store.ts +241 -102
  281. package/src/telemetry.ts +2 -1
  282. package/src/wire.ts +1 -0
  283. package/dist/actor-C7b-1arn.d.ts.map +0 -1
  284. package/dist/actor-shared-t7w8xIKP.js.map +0 -1
  285. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  286. package/dist/ai-mMiWul1T.js.map +0 -1
  287. package/dist/client-BhHLoNKF.js.map +0 -1
  288. package/dist/contract-BYKASlCV.js.map +0 -1
  289. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  290. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  291. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  292. package/dist/internal-CscSY7K_.js.map +0 -1
  293. package/dist/server-DYAATFuT.d.ts.map +0 -1
  294. package/dist/server-qyb5aJJG.js.map +0 -1
  295. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  296. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  297. package/dist/store-CyLUy86v.d.ts +0 -404
  298. package/dist/store-CyLUy86v.d.ts.map +0 -1
  299. package/dist/store-codec-DTG0Ftek.js +0 -10
  300. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  301. package/dist/store-presence-polling-B1ixeqIS.js +0 -94
  302. package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
  303. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  304. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  305. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  306. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
  307. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  308. package/dist/wire-BKxH1vbP.js.map +0 -1
  309. package/docs/server/01-handlers.mdx +0 -227
@@ -0,0 +1,199 @@
1
+ 'use client'
2
+ import { useEffect, useRef, useState, type ReactNode } from 'react'
3
+ import Link from 'next/link'
4
+ import { usePathname, useRouter } from 'next/navigation'
5
+ import { SiteNav } from '@/app/components/site-header'
6
+ import { channelIdSchema } from '@/app/a2slack/model'
7
+ import { useIdentity } from '../identity'
8
+ import { useWorkspace } from '../sessions'
9
+ import { useInboxPanel } from './InboxContext'
10
+ import { useSearch } from './SearchContext'
11
+
12
+ const MIN_WIDTH = 180
13
+ const MAX_WIDTH = 400
14
+ const WIDTH_KEY = 'a2slack-sidebar-width'
15
+
16
+ function slugify(raw: string): string {
17
+ return raw
18
+ .toLowerCase()
19
+ .trim()
20
+ .replace(/\s+/g, '-')
21
+ .replace(/[^a-z0-9-]/g, '')
22
+ .replace(/^-+/, '')
23
+ .slice(0, 32)
24
+ }
25
+
26
+ type SidebarSession = ReturnType<typeof useWorkspace> & {
27
+ state: NonNullable<ReturnType<typeof useWorkspace>['state']>
28
+ }
29
+
30
+ export function Sidebar(): ReactNode {
31
+ const session = useWorkspace()
32
+ if (session.state === undefined) {
33
+ const error =
34
+ 'error' in session.connection ? session.connection.error : null
35
+ return (
36
+ <p className="a2slack-loading" role="status">
37
+ {error?.message ?? 'Loading…'}
38
+ </p>
39
+ )
40
+ }
41
+ return <SidebarView session={{ ...session, state: session.state }} />
42
+ }
43
+
44
+ function SidebarView({
45
+ session,
46
+ }: Record<never, never> & { session: SidebarSession }): ReactNode {
47
+ const identity = useIdentity()
48
+ const search = useSearch()
49
+ const inbox = useInboxPanel()
50
+ const { state, push } = session
51
+ const pathname = usePathname()
52
+ const router = useRouter()
53
+ const [draft, setDraft] = useState('')
54
+ const [width, setWidth] = useState(224)
55
+ const [resizing, setResizing] = useState(false)
56
+ const startRef = useRef({ x: 0, width: 224 })
57
+
58
+ useEffect(() => {
59
+ const stored = Number(localStorage.getItem(WIDTH_KEY))
60
+ if (stored >= MIN_WIDTH && stored <= MAX_WIDTH) setWidth(stored)
61
+ }, [])
62
+
63
+ const create = (): void => {
64
+ const id = slugify(draft)
65
+ if (!identity || !channelIdSchema.safeParse(id).success) return
66
+ if (!state.channels.includes(id)) {
67
+ push({ type: 'channel.created', payload: { id, by: identity } }).catch(
68
+ () => {},
69
+ )
70
+ }
71
+ setDraft('')
72
+ search.clear()
73
+ inbox.setOpen(false)
74
+ router.push(`/a2slack/c/${id}`)
75
+ }
76
+
77
+ return (
78
+ <nav className="a2slack-sidebar" style={{ width }}>
79
+ <div className="site-header a2slack-head">
80
+ <SiteNav />
81
+ </div>
82
+ <form
83
+ className="a2slack-form a2slack-search"
84
+ role="search"
85
+ onSubmit={(e) => {
86
+ e.preventDefault()
87
+ search.submit()
88
+ }}
89
+ >
90
+ <input
91
+ ref={search.input}
92
+ className="a2slack-input"
93
+ aria-label="search messages"
94
+ enterKeyHint="search"
95
+ placeholder="search…"
96
+ value={search.query}
97
+ onChange={(e) => {
98
+ search.setQuery(e.target.value)
99
+ inbox.setOpen(false)
100
+ }}
101
+ onKeyDown={(e) => {
102
+ if (e.key !== 'Escape') return
103
+ e.preventDefault()
104
+ search.clear()
105
+ }}
106
+ />
107
+ </form>
108
+ <div className="a2slack-list">
109
+ <button
110
+ type="button"
111
+ className="a2slack-channel a2slack-inbox-row"
112
+ aria-current={inbox.open ? 'page' : undefined}
113
+ aria-label={`inbox, ${inbox.unread} unread`}
114
+ onClick={() => {
115
+ search.clear()
116
+ inbox.setOpen(!inbox.open)
117
+ }}
118
+ >
119
+ <span>inbox</span>
120
+ {inbox.unread > 0 && (
121
+ <span className="a2slack-unread">{inbox.unread}</span>
122
+ )}
123
+ </button>
124
+ <span className="a2slack-label a2slack-label-spaced">channels</span>
125
+ {state.channels.map((id) => (
126
+ <Link
127
+ key={id}
128
+ href={`/a2slack/c/${id}`}
129
+ className="a2slack-channel"
130
+ aria-current={
131
+ !inbox.open && pathname === `/a2slack/c/${id}`
132
+ ? 'page'
133
+ : undefined
134
+ }
135
+ onClick={() => {
136
+ search.clear()
137
+ inbox.setOpen(false)
138
+ }}
139
+ >
140
+ #{id}
141
+ </Link>
142
+ ))}
143
+ </div>
144
+ <form
145
+ className="a2slack-form"
146
+ onSubmit={(e) => {
147
+ e.preventDefault()
148
+ create()
149
+ }}
150
+ >
151
+ <input
152
+ className="a2slack-input"
153
+ placeholder="+ new channel"
154
+ value={draft}
155
+ onChange={(e) => setDraft(e.target.value)}
156
+ />
157
+ </form>
158
+ {identity && (
159
+ <div className="a2slack-you">
160
+ <span>you: {identity.name}</span>
161
+ <Link className="a2slack-lag-link" href="/a2slack/lag">
162
+ lag
163
+ </Link>
164
+ </div>
165
+ )}
166
+ <div
167
+ className="a2slack-handle"
168
+ onPointerDown={(e) => {
169
+ e.preventDefault()
170
+ startRef.current = { x: e.clientX, width }
171
+ setResizing(true)
172
+ e.currentTarget.setPointerCapture(e.pointerId)
173
+ }}
174
+ onPointerMove={(e) => {
175
+ if (!resizing) return
176
+ const next = Math.min(
177
+ MAX_WIDTH,
178
+ Math.max(
179
+ MIN_WIDTH,
180
+ startRef.current.width + e.clientX - startRef.current.x,
181
+ ),
182
+ )
183
+ setWidth(next)
184
+ }}
185
+ onPointerUp={(e) => {
186
+ if (!resizing) return
187
+ setResizing(false)
188
+ e.currentTarget.releasePointerCapture(e.pointerId)
189
+ localStorage.setItem(WIDTH_KEY, String(width))
190
+ }}
191
+ >
192
+ <span
193
+ className="a2slack-handle-line"
194
+ data-active={resizing || undefined}
195
+ />
196
+ </div>
197
+ </nav>
198
+ )
199
+ }
@@ -0,0 +1,134 @@
1
+ 'use client'
2
+ import { useEffect, useMemo, useState, type ReactNode } from 'react'
3
+ import { X } from 'lucide-react'
4
+ import {
5
+ liveThread,
6
+ type ChatMessage,
7
+ type ThreadPage,
8
+ } from '@/app/a2slack/model'
9
+ import { useWorkspace } from '../sessions'
10
+ import { Composer } from './Composer'
11
+ import { useInboxPanel } from './InboxContext'
12
+ import { MessageRow } from './MessageRow'
13
+
14
+ /**
15
+ * One thread, read from `/a2slack/thread` when it opens and kept live from
16
+ * the workspace's events. Until the read answers it shows the root as the
17
+ * feed has it, if the feed has it.
18
+ */
19
+ export function ThreadPanel({
20
+ channel,
21
+ rootId,
22
+ known,
23
+ onClose,
24
+ }: {
25
+ channel: string
26
+ rootId: string
27
+ /** The root, live, when it's on the channel's newest page. */
28
+ known: ChatMessage | null
29
+ onClose: () => void
30
+ }): ReactNode {
31
+ const { events } = useWorkspace()
32
+ const inbox = useInboxPanel()
33
+ const [page, setPage] = useState<ThreadPage | null>(null)
34
+ const [failed, setFailed] = useState(false)
35
+
36
+ useEffect(() => {
37
+ const controller = new AbortController()
38
+ const read = async (): Promise<void> => {
39
+ try {
40
+ const response = await fetch(
41
+ `/a2slack/thread?root=${encodeURIComponent(rootId)}`,
42
+ { signal: controller.signal },
43
+ )
44
+ if (!response.ok) throw new Error(`thread: ${response.status}`)
45
+ setPage((await response.json()) as ThreadPage)
46
+ } catch {
47
+ if (!controller.signal.aborted) setFailed(true)
48
+ }
49
+ }
50
+ void read()
51
+ return () => controller.abort()
52
+ }, [rootId])
53
+
54
+ const thread = useMemo(
55
+ () => (page ? liveThread(page, events, rootId) : null),
56
+ [page, events, rootId],
57
+ )
58
+ const root = thread?.root ?? known
59
+ const replies = thread?.replies ?? []
60
+
61
+ // Reading the thread reads its notifications, including any that arrive
62
+ // while it's open.
63
+ const unread =
64
+ inbox.items.find((item) => item.threadId === rootId)?.unread ?? 0
65
+ const { markRead } = inbox
66
+ useEffect(() => {
67
+ if (unread > 0) markRead(rootId)
68
+ }, [unread, rootId, markRead])
69
+
70
+ return (
71
+ <aside className="a2slack-thread">
72
+ <header className="a2slack-thread-header">
73
+ <span className="a2slack-thread-title">thread</span>
74
+ <button className="a2slack-close" onClick={onClose} title="close">
75
+ <X size={15} strokeWidth={1.75} />
76
+ </button>
77
+ </header>
78
+ <div className="a2slack-thread-scroll">
79
+ {root === null ? (
80
+ <div className="a2slack-divider">
81
+ <span>
82
+ {failed
83
+ ? "this thread didn't load"
84
+ : page
85
+ ? 'this message is gone'
86
+ : 'loading…'}
87
+ </span>
88
+ <span className="a2slack-divider-line" />
89
+ </div>
90
+ ) : (
91
+ <>
92
+ <MessageRow message={root} grouped={false} inThread />
93
+ <div className="a2slack-divider">
94
+ <span>
95
+ {root.replyCount === 0
96
+ ? 'no replies yet'
97
+ : `${root.replyCount} ${root.replyCount === 1 ? 'reply' : 'replies'}`}
98
+ {thread === null &&
99
+ root.replyCount > 0 &&
100
+ (failed ? " · these didn't load" : ' · loading…')}
101
+ </span>
102
+ <span className="a2slack-divider-line" />
103
+ </div>
104
+ {replies.map((reply, i) => {
105
+ const prev = replies[i - 1]
106
+ const grouped =
107
+ prev !== undefined &&
108
+ !prev.deleted &&
109
+ prev.author.id === reply.author.id &&
110
+ Date.parse(reply.at) - Date.parse(prev.at) < 5 * 60 * 1000
111
+ return (
112
+ <MessageRow
113
+ key={reply.id}
114
+ message={reply}
115
+ grouped={grouped}
116
+ inThread
117
+ />
118
+ )
119
+ })}
120
+ </>
121
+ )}
122
+ </div>
123
+ {root !== null && (
124
+ <Composer
125
+ channel={channel}
126
+ threadId={root.id}
127
+ placeholder="reply… or ask @bot"
128
+ autoFocus
129
+ compact
130
+ />
131
+ )}
132
+ </aside>
133
+ )
134
+ }
@@ -0,0 +1,71 @@
1
+ 'use client'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+ import { liveInbox, type InboxItem, type InboxPage } from '@/app/a2slack/model'
4
+ import { useWorkspace } from '../sessions'
5
+
6
+ export type Inbox = {
7
+ /** One per thread with news for `me`, newest first. */
8
+ items: InboxItem[]
9
+ /** Unread notifications across every thread. */
10
+ unread: number
11
+ /** The page hasn't answered yet. */
12
+ loading: boolean
13
+ error: string | null
14
+ }
15
+
16
+ /**
17
+ * `me`'s inbox: the page `/a2slack/inbox` read, kept live from the
18
+ * workspace's events after it. A new `me` reads a new page.
19
+ */
20
+ export function useInbox(me: string | null): Inbox {
21
+ const { events } = useWorkspace()
22
+ const [read, setRead] = useState<{
23
+ me: string
24
+ page: InboxPage | null
25
+ error: string | null
26
+ } | null>(null)
27
+
28
+ useEffect(() => {
29
+ if (me === null) return
30
+ const controller = new AbortController()
31
+ const load = async (): Promise<void> => {
32
+ try {
33
+ const response = await fetch(
34
+ `/a2slack/inbox?user=${encodeURIComponent(me)}`,
35
+ { signal: controller.signal },
36
+ )
37
+ const body = (await response.json().catch(() => ({}))) as
38
+ InboxPage | { error?: string }
39
+ if (response.ok && 'items' in body) {
40
+ setRead({ me, page: body, error: null })
41
+ return
42
+ }
43
+ const error = 'error' in body ? body.error : undefined
44
+ setRead({
45
+ me,
46
+ page: null,
47
+ error: error ?? `the inbox failed (${response.status})`,
48
+ })
49
+ } catch {
50
+ if (controller.signal.aborted) return
51
+ setRead({ me, page: null, error: "the inbox didn't answer" })
52
+ }
53
+ }
54
+ void load()
55
+ return () => controller.abort()
56
+ }, [me])
57
+
58
+ const current = me !== null && read?.me === me ? read : null
59
+ const items = useMemo(
60
+ () =>
61
+ current?.page && me !== null ? liveInbox(current.page, events, me) : [],
62
+ [current, events, me],
63
+ )
64
+
65
+ return {
66
+ items,
67
+ unread: items.reduce((sum, item) => sum + item.unread, 0),
68
+ loading: me !== null && current === null,
69
+ error: current?.error ?? null,
70
+ }
71
+ }
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer'
3
3
  import { createHash } from 'node:crypto'
4
4
  import { Sandbox } from '@vercel/sandbox'
5
5
  import { tool } from 'ai'
6
- import { handlerContext, inputs } from 'experimental-a2/ai'
6
+ import { consumerContext, inputs } from 'experimental-a2/ai'
7
7
  import { agent } from 'experimental-a2/ai/server'
8
8
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
9
9
  import { otel } from 'experimental-a2/otel'
@@ -45,7 +45,7 @@ const tools = {
45
45
  command: z.string().min(1).describe('The Bash command to execute.'),
46
46
  }),
47
47
  execute: async ({ command }) => {
48
- const context = handlerContext(demoAgent)
48
+ const context = consumerContext(demoAgent)
49
49
  const sandbox = await Sandbox.getOrCreate({
50
50
  name: sandboxName(context.session.id),
51
51
  persistent: true,
@@ -109,7 +109,7 @@ const tools = {
109
109
  reminder: z.string().min(1).max(500),
110
110
  }),
111
111
  execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
112
- const context = handlerContext(demoAgent)
112
+ const context = consumerContext(demoAgent)
113
113
  await context.session.schedule(
114
114
  `reminder:${toolCallId}`,
115
115
  { delay: `${delaySeconds}s` },
@@ -488,7 +488,7 @@ export function ChatClient({
488
488
  inputRef.current?.focus({ preventScroll: true }),
489
489
  )
490
490
  try {
491
- // The browser authors only the room fact. A serialized server handler
491
+ // The browser authors only the room fact. A serialized server consumer
492
492
  // derives AI metadata and generation policy from its durable log index.
493
493
  await push({
494
494
  type: 'message.sent',
@@ -1,10 +1,14 @@
1
1
  import 'server-only'
2
2
  import { tool } from 'ai'
3
3
  import { idempotentId } from 'experimental-a2'
4
- import { handlerContext, inputs } from 'experimental-a2/ai'
4
+ import { consumerContext, inputs } from 'experimental-a2/ai'
5
5
  import { agent } from 'experimental-a2/ai/server'
6
6
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
7
- import { createServer, type A2Operation } from 'experimental-a2/server'
7
+ import {
8
+ consumer,
9
+ createServer,
10
+ type A2Operation,
11
+ } from 'experimental-a2/server'
8
12
  import { store } from '@/lib/store'
9
13
  import {
10
14
  chat,
@@ -22,7 +26,7 @@ const tools = {
22
26
  'Post a visible message to the multiplayer chat. Call it once for each chat bubble you want to send; this tool is the only way to speak in the room.',
23
27
  inputSchema: chatPostInputSchema,
24
28
  execute: async ({ message }, { toolCallId }) => {
25
- const context = handlerContext(chatAgent)
29
+ const context = consumerContext(chatAgent)
26
30
  context.signal.throwIfAborted()
27
31
  const { generationId, messageId } = context.event.payload.call
28
32
  // The agent's session shares the room's id. The explicit id makes a
@@ -114,42 +118,48 @@ Use as many post calls as the moment naturally calls for. Several short messages
114
118
  },
115
119
  })
116
120
 
117
- /** The room. Each message goes on to the agent's session, marked for whether it should answer. */
121
+ /**
122
+ * Each room message goes on to the agent's session, marked for whether it
123
+ * should answer. One lane keeps the agent's messages in the room's order.
124
+ */
125
+ const forwardToAgent = consumer({
126
+ contract: chat,
127
+ name: 'forward-to-agent',
128
+ types: ['message.sent'],
129
+ lane: 'a2.chat.ingress',
130
+ }).run(async (context) => {
131
+ const ambient = (await context.session.state()).state.ambient.enabled
132
+ const { author, text } = context.event.payload
133
+ const mode: ChatParticipantMode = ambient
134
+ ? 'ambient'
135
+ : mentionsAgent(text)
136
+ ? 'mention'
137
+ : 'passive'
138
+ const message: ChatAgentMessage = {
139
+ // Derived from the room event's id, so a retried forward converges on
140
+ // the same message in the agent's session.
141
+ id: await idempotentId('chat.message', context.event.id),
142
+ role: 'user',
143
+ metadata: {
144
+ kind: 'participant',
145
+ author,
146
+ text,
147
+ sentAt: context.event.createdAt.toISOString(),
148
+ mode,
149
+ },
150
+ parts: [{ type: 'text', text: `${author}: ${text}` }],
151
+ }
152
+ await chatAgentServer
153
+ .session(context.session.id)
154
+ .append(...inputs.message(message, { generate: mode !== 'passive' }))
155
+ })
156
+
157
+ /** The room. Its messages reach the agent through `forwardToAgent`. */
118
158
  export const chatServer = createServer({
119
159
  ...(store ? { store } : {}),
120
160
  ...(chatScheduler ? { scheduler: chatScheduler } : {}),
121
161
  contract: chat,
122
- handlers: {
123
- 'message.sent': {
124
- lane: 'a2.chat.ingress',
125
- handler: async (context) => {
126
- const ambient = (await context.session.state()).state.ambient.enabled
127
- const { author, text } = context.event.payload
128
- const mode: ChatParticipantMode = ambient
129
- ? 'ambient'
130
- : mentionsAgent(text)
131
- ? 'mention'
132
- : 'passive'
133
- const message: ChatAgentMessage = {
134
- // Event ids are unique across a store's sessions, so the forward
135
- // derives its own; a retried forward converges on the same id.
136
- id: await idempotentId('chat.message', context.event.id),
137
- role: 'user',
138
- metadata: {
139
- kind: 'participant',
140
- author,
141
- text,
142
- sentAt: context.event.createdAt.toISOString(),
143
- mode,
144
- },
145
- parts: [{ type: 'text', text: `${author}: ${text}` }],
146
- }
147
- await chatAgentServer
148
- .session(context.session.id)
149
- .append(...inputs.message(message, { generate: mode !== 'passive' }))
150
- },
151
- },
152
- },
162
+ consumers: [forwardToAgent],
153
163
  })
154
164
 
155
165
  /** Room pushes: people's messages, typing, and ambient mode. Posts are the agent's. */
@@ -40,6 +40,13 @@ export const ChatIcon = (): ReactNode => (
40
40
  </Glyph>
41
41
  )
42
42
 
43
+ export const A2slackIcon = (): ReactNode => (
44
+ <Glyph>
45
+ <path d="M3 4h18v13H10l-4 3.5V17H3z" />
46
+ <path className="accent" d="M10.5 7l-1 7M14.5 7l-1 7M8 9h8M7.5 12h8" />
47
+ </Glyph>
48
+ )
49
+
43
50
  export const AgentIcon = (): ReactNode => (
44
51
  <Glyph>
45
52
  <rect x="3" y="3" width="18" height="18" />
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import {
3
3
  A0Icon,
4
+ A2slackIcon,
4
5
  AgentIcon,
5
6
  CanvasIcon,
6
7
  ChatIcon,
@@ -24,6 +25,12 @@ export const demos: Demo[] = [
24
25
  { href: '/a0', name: 'A0', icon: <A0Icon />, fullWindow: true },
25
26
  { href: '/orders', name: 'Orders', icon: <OrdersIcon /> },
26
27
  { href: '/chat', name: 'Chat', icon: <ChatIcon /> },
28
+ {
29
+ href: '/a2slack',
30
+ name: 'a2slack',
31
+ icon: <A2slackIcon />,
32
+ fullWindow: true,
33
+ },
27
34
  { href: '/agent', name: 'Agent', icon: <AgentIcon /> },
28
35
  { href: '/canvas', name: 'Canvas', icon: <CanvasIcon /> },
29
36
  { href: '/documents', name: 'Documents', icon: <DocumentsIcon /> },
@@ -118,7 +118,7 @@ export function OrderClient({ sessionId }: { sessionId: string }): ReactNode {
118
118
  <p className="hint">
119
119
  The button pushes <code>shop.started</code> optimistically — the
120
120
  badge flips before the server acks. Everything after that is
121
- handlers appending the next fact: courier assigned, picked up,
121
+ consumers appending the next fact: courier assigned, picked up,
122
122
  delivered — watch it arrive over the stream, hands off.
123
123
  </p>
124
124
  </>
@@ -129,7 +129,7 @@ export function OrderClient({ sessionId }: { sessionId: string }): ReactNode {
129
129
  </p>
130
130
  ) : (
131
131
  <p className="hint">
132
- Hands off from here — each status lands on its own as handlers append
132
+ Hands off from here — each status lands on its own as consumers append
133
133
  the next fact.
134
134
  </p>
135
135
  )}
@@ -8,7 +8,7 @@ export default function OrdersPage(): ReactNode {
8
8
  <p className="lede">
9
9
  Creating an order appends a single <code>created</code> event.
10
10
  Everything after that — the shop notification, the courier, the delivery
11
- itself — is handlers appending the next fact to the same log, all the
11
+ itself — is consumers appending the next fact to the same log, all the
12
12
  way to <code>delivered</code>.
13
13
  </p>
14
14
  <NewOrderForm />