@abloatai/ablo 0.3.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 (278) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +12 -0
  4. package/README.md +230 -0
  5. package/dist/BaseSyncedStore.d.ts +709 -0
  6. package/dist/BaseSyncedStore.js +1843 -0
  7. package/dist/Database.d.ts +344 -0
  8. package/dist/Database.js +1259 -0
  9. package/dist/LazyReferenceCollection.d.ts +181 -0
  10. package/dist/LazyReferenceCollection.js +460 -0
  11. package/dist/Model.d.ts +339 -0
  12. package/dist/Model.js +715 -0
  13. package/dist/ModelRegistry.d.ts +200 -0
  14. package/dist/ModelRegistry.js +535 -0
  15. package/dist/NetworkMonitor.d.ts +27 -0
  16. package/dist/NetworkMonitor.js +73 -0
  17. package/dist/ObjectPool.d.ts +202 -0
  18. package/dist/ObjectPool.js +1106 -0
  19. package/dist/SyncClient.d.ts +489 -0
  20. package/dist/SyncClient.js +1555 -0
  21. package/dist/SyncEngineContext.d.ts +46 -0
  22. package/dist/SyncEngineContext.js +74 -0
  23. package/dist/adapters/alwaysOnline.d.ts +16 -0
  24. package/dist/adapters/alwaysOnline.js +19 -0
  25. package/dist/adapters/inMemoryStorage.d.ts +30 -0
  26. package/dist/adapters/inMemoryStorage.js +94 -0
  27. package/dist/agent/Agent.d.ts +358 -0
  28. package/dist/agent/Agent.js +500 -0
  29. package/dist/agent/index.d.ts +115 -0
  30. package/dist/agent/index.js +128 -0
  31. package/dist/agent/session.d.ts +90 -0
  32. package/dist/agent/session.js +156 -0
  33. package/dist/agent/types.d.ts +73 -0
  34. package/dist/agent/types.js +10 -0
  35. package/dist/ai-sdk/coordination-context.d.ts +51 -0
  36. package/dist/ai-sdk/coordination-context.js +107 -0
  37. package/dist/ai-sdk/index.d.ts +68 -0
  38. package/dist/ai-sdk/index.js +68 -0
  39. package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
  40. package/dist/ai-sdk/intent-broadcast.js +72 -0
  41. package/dist/ai-sdk/wrap.d.ts +67 -0
  42. package/dist/ai-sdk/wrap.js +45 -0
  43. package/dist/api/index.d.ts +10 -0
  44. package/dist/api/index.js +9 -0
  45. package/dist/auth/index.d.ts +137 -0
  46. package/dist/auth/index.js +246 -0
  47. package/dist/client/Ablo.d.ts +835 -0
  48. package/dist/client/Ablo.js +1440 -0
  49. package/dist/client/ApiClient.d.ts +200 -0
  50. package/dist/client/ApiClient.js +659 -0
  51. package/dist/client/auth.d.ts +79 -0
  52. package/dist/client/auth.js +81 -0
  53. package/dist/client/createInternalComponents.d.ts +44 -0
  54. package/dist/client/createInternalComponents.js +88 -0
  55. package/dist/client/createModelProxy.d.ts +152 -0
  56. package/dist/client/createModelProxy.js +199 -0
  57. package/dist/client/identity.d.ts +63 -0
  58. package/dist/client/identity.js +156 -0
  59. package/dist/client/index.d.ts +36 -0
  60. package/dist/client/index.js +33 -0
  61. package/dist/client/persistence.d.ts +7 -0
  62. package/dist/client/persistence.js +11 -0
  63. package/dist/client/validateAbloOptions.d.ts +42 -0
  64. package/dist/client/validateAbloOptions.js +43 -0
  65. package/dist/config/index.d.ts +10 -0
  66. package/dist/config/index.js +12 -0
  67. package/dist/context.d.ts +27 -0
  68. package/dist/context.js +58 -0
  69. package/dist/core/DatabaseManager.d.ts +108 -0
  70. package/dist/core/DatabaseManager.js +361 -0
  71. package/dist/core/QueryProcessor.d.ts +77 -0
  72. package/dist/core/QueryProcessor.js +262 -0
  73. package/dist/core/QueryView.d.ts +64 -0
  74. package/dist/core/QueryView.js +219 -0
  75. package/dist/core/StoreManager.d.ts +131 -0
  76. package/dist/core/StoreManager.js +334 -0
  77. package/dist/core/ViewRegistry.d.ts +20 -0
  78. package/dist/core/ViewRegistry.js +55 -0
  79. package/dist/core/index.d.ts +34 -0
  80. package/dist/core/index.js +59 -0
  81. package/dist/core/openIDBWithTimeout.d.ts +27 -0
  82. package/dist/core/openIDBWithTimeout.js +63 -0
  83. package/dist/core/query-utils.d.ts +37 -0
  84. package/dist/core/query-utils.js +60 -0
  85. package/dist/errors.d.ts +235 -0
  86. package/dist/errors.js +243 -0
  87. package/dist/index.d.ts +41 -0
  88. package/dist/index.js +82 -0
  89. package/dist/interfaces/headless.d.ts +95 -0
  90. package/dist/interfaces/headless.js +41 -0
  91. package/dist/interfaces/index.d.ts +321 -0
  92. package/dist/interfaces/index.js +8 -0
  93. package/dist/mutators/RecordingTransaction.d.ts +36 -0
  94. package/dist/mutators/RecordingTransaction.js +216 -0
  95. package/dist/mutators/Transaction.d.ts +48 -0
  96. package/dist/mutators/Transaction.js +64 -0
  97. package/dist/mutators/UndoManager.d.ts +114 -0
  98. package/dist/mutators/UndoManager.js +143 -0
  99. package/dist/mutators/defineMutators.d.ts +55 -0
  100. package/dist/mutators/defineMutators.js +28 -0
  101. package/dist/policy/index.d.ts +19 -0
  102. package/dist/policy/index.js +18 -0
  103. package/dist/policy/types.d.ts +74 -0
  104. package/dist/policy/types.js +17 -0
  105. package/dist/principal.d.ts +44 -0
  106. package/dist/principal.js +49 -0
  107. package/dist/query/client.d.ts +43 -0
  108. package/dist/query/client.js +84 -0
  109. package/dist/query/index.d.ts +6 -0
  110. package/dist/query/index.js +5 -0
  111. package/dist/query/types.d.ts +143 -0
  112. package/dist/query/types.js +36 -0
  113. package/dist/react/AbloProvider.d.ts +205 -0
  114. package/dist/react/AbloProvider.js +398 -0
  115. package/dist/react/ClientSideSuspense.d.ts +36 -0
  116. package/dist/react/ClientSideSuspense.js +17 -0
  117. package/dist/react/DefaultFallback.d.ts +24 -0
  118. package/dist/react/DefaultFallback.js +43 -0
  119. package/dist/react/SyncGroupProvider.d.ts +19 -0
  120. package/dist/react/SyncGroupProvider.js +44 -0
  121. package/dist/react/context.d.ts +161 -0
  122. package/dist/react/context.js +35 -0
  123. package/dist/react/index.d.ts +64 -0
  124. package/dist/react/index.js +73 -0
  125. package/dist/react/internalContext.d.ts +35 -0
  126. package/dist/react/internalContext.js +3 -0
  127. package/dist/react/useAblo.d.ts +72 -0
  128. package/dist/react/useAblo.js +63 -0
  129. package/dist/react/useCurrentUserId.d.ts +21 -0
  130. package/dist/react/useCurrentUserId.js +33 -0
  131. package/dist/react/useErrorListener.d.ts +20 -0
  132. package/dist/react/useErrorListener.js +39 -0
  133. package/dist/react/useIntent.d.ts +29 -0
  134. package/dist/react/useIntent.js +42 -0
  135. package/dist/react/useMutate.d.ts +83 -0
  136. package/dist/react/useMutate.js +122 -0
  137. package/dist/react/useMutationFailureListener.d.ts +26 -0
  138. package/dist/react/useMutationFailureListener.js +38 -0
  139. package/dist/react/useMutators.d.ts +56 -0
  140. package/dist/react/useMutators.js +66 -0
  141. package/dist/react/usePresence.d.ts +32 -0
  142. package/dist/react/usePresence.js +41 -0
  143. package/dist/react/useQuery.d.ts +123 -0
  144. package/dist/react/useQuery.js +145 -0
  145. package/dist/react/useReactive.d.ts +35 -0
  146. package/dist/react/useReactive.js +111 -0
  147. package/dist/react/useReader.d.ts +69 -0
  148. package/dist/react/useReader.js +73 -0
  149. package/dist/react/useSyncStatus.d.ts +61 -0
  150. package/dist/react/useSyncStatus.js +76 -0
  151. package/dist/react/useUndoScope.d.ts +36 -0
  152. package/dist/react/useUndoScope.js +73 -0
  153. package/dist/realtime/index.d.ts +10 -0
  154. package/dist/realtime/index.js +9 -0
  155. package/dist/schema/field.d.ts +134 -0
  156. package/dist/schema/field.js +264 -0
  157. package/dist/schema/index.d.ts +29 -0
  158. package/dist/schema/index.js +38 -0
  159. package/dist/schema/model.d.ts +326 -0
  160. package/dist/schema/model.js +89 -0
  161. package/dist/schema/queries.d.ts +203 -0
  162. package/dist/schema/queries.js +145 -0
  163. package/dist/schema/relation.d.ts +172 -0
  164. package/dist/schema/relation.js +104 -0
  165. package/dist/schema/schema.d.ts +259 -0
  166. package/dist/schema/schema.js +188 -0
  167. package/dist/schema/sugar.d.ts +129 -0
  168. package/dist/schema/sugar.js +94 -0
  169. package/dist/source/index.d.ts +423 -0
  170. package/dist/source/index.js +320 -0
  171. package/dist/source/pushQueue.d.ts +112 -0
  172. package/dist/source/pushQueue.js +249 -0
  173. package/dist/stores/ObjectStore.d.ts +103 -0
  174. package/dist/stores/ObjectStore.js +371 -0
  175. package/dist/stores/ObjectStoreContract.d.ts +39 -0
  176. package/dist/stores/ObjectStoreContract.js +1 -0
  177. package/dist/stores/SyncActionStore.d.ts +101 -0
  178. package/dist/stores/SyncActionStore.js +481 -0
  179. package/dist/sync/BootstrapHelper.d.ts +127 -0
  180. package/dist/sync/BootstrapHelper.js +434 -0
  181. package/dist/sync/ConnectionManager.d.ts +136 -0
  182. package/dist/sync/ConnectionManager.js +465 -0
  183. package/dist/sync/HydrationCoordinator.d.ts +137 -0
  184. package/dist/sync/HydrationCoordinator.js +468 -0
  185. package/dist/sync/NetworkProbe.d.ts +43 -0
  186. package/dist/sync/NetworkProbe.js +113 -0
  187. package/dist/sync/OfflineFlush.d.ts +9 -0
  188. package/dist/sync/OfflineFlush.js +22 -0
  189. package/dist/sync/OfflineTransactionStore.d.ts +37 -0
  190. package/dist/sync/OfflineTransactionStore.js +263 -0
  191. package/dist/sync/SyncWebSocket.d.ts +663 -0
  192. package/dist/sync/SyncWebSocket.js +1336 -0
  193. package/dist/sync/createIntentStream.d.ts +33 -0
  194. package/dist/sync/createIntentStream.js +243 -0
  195. package/dist/sync/createPresenceStream.d.ts +46 -0
  196. package/dist/sync/createPresenceStream.js +192 -0
  197. package/dist/sync/createSnapshot.d.ts +33 -0
  198. package/dist/sync/createSnapshot.js +124 -0
  199. package/dist/sync/participants.d.ts +114 -0
  200. package/dist/sync/participants.js +336 -0
  201. package/dist/sync/schemas.d.ts +79 -0
  202. package/dist/sync/schemas.js +78 -0
  203. package/dist/testing/fixtures/bootstrap.d.ts +45 -0
  204. package/dist/testing/fixtures/bootstrap.js +53 -0
  205. package/dist/testing/fixtures/deltas.d.ts +86 -0
  206. package/dist/testing/fixtures/deltas.js +139 -0
  207. package/dist/testing/fixtures/models.d.ts +82 -0
  208. package/dist/testing/fixtures/models.js +270 -0
  209. package/dist/testing/helpers/react-wrapper.d.ts +66 -0
  210. package/dist/testing/helpers/react-wrapper.js +64 -0
  211. package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
  212. package/dist/testing/helpers/sync-engine-harness.js +70 -0
  213. package/dist/testing/helpers/wait.d.ts +25 -0
  214. package/dist/testing/helpers/wait.js +44 -0
  215. package/dist/testing/index.d.ts +21 -0
  216. package/dist/testing/index.js +32 -0
  217. package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
  218. package/dist/testing/mocks/MockMutationExecutor.js +139 -0
  219. package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
  220. package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
  221. package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
  222. package/dist/testing/mocks/MockSyncContext.js +100 -0
  223. package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
  224. package/dist/testing/mocks/MockSyncStore.js +171 -0
  225. package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
  226. package/dist/testing/mocks/MockWebSocket.js +117 -0
  227. package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
  228. package/dist/transactions/OptimisticEchoTracker.js +104 -0
  229. package/dist/transactions/TransactionQueue.d.ts +499 -0
  230. package/dist/transactions/TransactionQueue.js +1895 -0
  231. package/dist/transactions/index.d.ts +16 -0
  232. package/dist/transactions/index.js +7 -0
  233. package/dist/transactions/mutation-error-handler.d.ts +5 -0
  234. package/dist/transactions/mutation-error-handler.js +39 -0
  235. package/dist/types/global.d.ts +107 -0
  236. package/dist/types/global.js +38 -0
  237. package/dist/types/index.d.ts +241 -0
  238. package/dist/types/index.js +70 -0
  239. package/dist/types/streams.d.ts +495 -0
  240. package/dist/types/streams.js +11 -0
  241. package/dist/utils/asyncIterator.d.ts +41 -0
  242. package/dist/utils/asyncIterator.js +142 -0
  243. package/dist/utils/duration.d.ts +28 -0
  244. package/dist/utils/duration.js +47 -0
  245. package/dist/utils/mobx-setup.d.ts +42 -0
  246. package/dist/utils/mobx-setup.js +381 -0
  247. package/docs/api-keys.md +24 -0
  248. package/docs/api.md +230 -0
  249. package/docs/audit.md +81 -0
  250. package/docs/capabilities.md +163 -0
  251. package/docs/client-behavior.md +202 -0
  252. package/docs/data-sources.md +214 -0
  253. package/docs/examples/agent-human.md +84 -0
  254. package/docs/examples/ai-sdk-tool.md +92 -0
  255. package/docs/examples/existing-python-backend.md +249 -0
  256. package/docs/examples/nextjs.md +88 -0
  257. package/docs/examples/server-agent.md +86 -0
  258. package/docs/guarantees.md +148 -0
  259. package/docs/index.md +97 -0
  260. package/docs/integration-guide.md +493 -0
  261. package/docs/interaction-model.md +140 -0
  262. package/docs/mcp/claude-code.md +43 -0
  263. package/docs/mcp/cursor.md +53 -0
  264. package/docs/mcp/windsurf.md +46 -0
  265. package/docs/mcp.md +59 -0
  266. package/docs/quickstart.md +152 -0
  267. package/docs/react.md +115 -0
  268. package/docs/roadmap.md +45 -0
  269. package/examples/README.md +54 -0
  270. package/examples/data-source/README.md +102 -0
  271. package/examples/data-source/ablo-driver.ts +89 -0
  272. package/examples/data-source/customer-server.ts +208 -0
  273. package/examples/data-source/run.ts +101 -0
  274. package/examples/data-source/schema.ts +25 -0
  275. package/examples/quickstart.ts +54 -0
  276. package/examples/tsconfig.json +16 -0
  277. package/llms.txt +143 -0
  278. package/package.json +147 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @ablo/sync-engine-internal/agent — Agent SDK helpers
3
+ *
4
+ * Two entry points depending on agent lifetime:
5
+ *
6
+ * ─────────────────────────────────────────────────────────────────────────
7
+ * LONG-LIVED AGENT (browser, daemon, persistent Node process)
8
+ * ─────────────────────────────────────────────────────────────────────────
9
+ * Use the unified `Ablo({...})` factory directly with `kind: 'agent'`.
10
+ * The factory holds the WebSocket, reactive subscriptions, mutations, and
11
+ * presence/intents — same surface as a browser user, just with a
12
+ * server-issued capability token instead of session cookies.
13
+ *
14
+ * ```ts
15
+ * import Ablo from '@ablo/sync-engine';
16
+ *
17
+ * const ablo = Ablo({
18
+ * schema,
19
+ * url: 'wss://api.example.com',
20
+ * organizationId,
21
+ * kind: 'agent',
22
+ * agentId: 'reviewer-bot',
23
+ * capabilityToken: mintedToken,
24
+ * syncGroups: [`org:${organizationId}`],
25
+ * inMemory: true, // Node has no IndexedDB
26
+ * });
27
+ *
28
+ * await ablo.ready();
29
+ * for (const task of ablo.tasks.list({ where: { status: 'pending_review' } })) {
30
+ * await ablo.tasks.update(task.id, { status: 'reviewed' });
31
+ * }
32
+ * ```
33
+ *
34
+ * For server-side caching across requests, use {@link Agent.session}
35
+ * — it caches one engine per identity and refreshes capability tokens
36
+ * before expiry.
37
+ *
38
+ * ─────────────────────────────────────────────────────────────────────────
39
+ * SHORT-LIVED AGENT (SQS consumer, serverless, API route)
40
+ * ─────────────────────────────────────────────────────────────────────────
41
+ * Use {@link Agent}. Stateless REST hooks that slot directly into
42
+ * the Vercel AI SDK's `generateText` / `streamText`. No WebSocket. Ideal
43
+ * for agent-worker jobs that process one task and exit.
44
+ *
45
+ * ```ts
46
+ * import { generateText, tool, stepCountIs } from 'ai';
47
+ * import { Agent } from '@ablo/sync-engine-internal/agent';
48
+ *
49
+ * const perception = new Agent({
50
+ * syncServerUrl: 'http://localhost:8080',
51
+ * agentId: job.id,
52
+ * organizationId: job.organizationId,
53
+ * syncGroups: [`org:${job.organizationId}`],
54
+ * });
55
+ *
56
+ * await generateText({
57
+ * model: 'anthropic/claude-sonnet-4.5',
58
+ * messages,
59
+ * stopWhen: stepCountIs(10),
60
+ * tools: {
61
+ * updateSlide: perception.wrapTool(tool({ ... }), {
62
+ * entityType: 'Slide',
63
+ * getEntityId: (args) => args.id,
64
+ * }),
65
+ * },
66
+ * prepareStep: perception.prepareStep(),
67
+ * onStepFinish: perception.onStepFinish(),
68
+ * });
69
+ * ```
70
+ *
71
+ * ─────────────────────────────────────────────────────────────────────────
72
+ * IDIOMATIC TOOL PATTERN (ported from vercel-labs/open-agents)
73
+ * ─────────────────────────────────────────────────────────────────────────
74
+ * Tools are factory functions that pull ambient state from
75
+ * `experimental_context`. The caller builds an {@link AgentContext} once
76
+ * and passes it to `generateText`; every tool reaches in.
77
+ *
78
+ * ```ts
79
+ * import { tool } from 'ai';
80
+ * import { z } from 'zod';
81
+ * import { Agent, type AgentContext } from '@ablo/sync-engine-internal/agent';
82
+ *
83
+ * export const updateSlideTool = () => tool({
84
+ * description: 'Update a slide title',
85
+ * inputSchema: z.object({ id: z.string(), title: z.string() }),
86
+ * execute: async (args, { experimental_context }) => {
87
+ * const perception = Agent.fromContext(experimental_context, 'updateSlide');
88
+ * const check = await perception.checkFreshness('Slide', args.id, Date.now() - 5000);
89
+ * if (check.stale) return check.summary;
90
+ * return { ok: true };
91
+ * },
92
+ * });
93
+ * ```
94
+ *
95
+ * ─────────────────────────────────────────────────────────────────────────
96
+ * COMPOSED (long-lived `Ablo({kind:'agent'})` + Agent together)
97
+ * ─────────────────────────────────────────────────────────────────────────
98
+ * If you have a long-lived `Ablo` instance AND want AI SDK hooks, pass it
99
+ * as the `announcer` to Agent — presence announcements route
100
+ * through the existing WebSocket instead of opening new HTTP calls.
101
+ *
102
+ * ```ts
103
+ * const ablo = Ablo({ kind: 'agent', schema, capabilityToken, ... });
104
+ * await ablo.ready();
105
+ *
106
+ * const perception = new Agent({
107
+ * ...sharedConfig,
108
+ * announcer: ablo, // reuse the WebSocket
109
+ * });
110
+ * ```
111
+ *
112
+ * Both `Ablo` and `Agent` implement the
113
+ * {@link PresenceAnnouncer} interface.
114
+ */
115
+ // ── The entire `/agent` surface — one symbol ────────────────────────────
116
+ //
117
+ // `Agent` is the class AND the namespace for its types. Reach for
118
+ // options, context, and session options via dot access:
119
+ //
120
+ // import { Agent } from '@ablo/sync-engine-internal/agent';
121
+ // const opts: Agent.Options = { ... };
122
+ // const ctx: Agent.Context = { perception };
123
+ // const s: Agent.SessionOptions = { ... };
124
+ //
125
+ // Everything else (Activity, Claim, Turn, Peer, ActiveIntent, ...)
126
+ // lives on the `Ablo.*` namespace via
127
+ // `import type { Ablo } from '@ablo/sync-engine'`.
128
+ export { Agent } from './Agent.js';
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Agent session — cache + lifecycle for server-side `SyncAgent`s.
3
+ *
4
+ * Captures the pattern every server-side consumer needs:
5
+ * 1. Cache `SyncAgent` instances per (org, user, surface, target).
6
+ * 2. Re-mint capabilities before TTL elapses.
7
+ * 3. Align the SyncAgent ctor's `syncGroups` with the cap allowlist
8
+ * so the upgrade-time intersection is non-empty (avoid the
9
+ * silent black-hole-broadcast bug).
10
+ * 4. Connect / disconnect / dispose lifecycle.
11
+ *
12
+ * What's generic, what isn't:
13
+ * - Cache, TTL, sync_groups alignment, lifecycle: SAME for every
14
+ * consumer. Lives here.
15
+ * - Cap mint: AUTH-FLOW-SPECIFIC. Every consumer has a different
16
+ * way to obtain a token (Better Auth cookie forwarding, API key
17
+ * exchange, OAuth, etc.). Consumer provides via the
18
+ * `issueToken` callback.
19
+ *
20
+ * The helper itself imports nothing app-specific. Open-source-clean.
21
+ */
22
+ import { Ablo } from '../client/Ablo.js';
23
+ import type { Schema, SchemaRecord } from '../schema/schema.js';
24
+ interface IssuedToken {
25
+ readonly token: string;
26
+ readonly expiresAtMs: number;
27
+ /**
28
+ * Sync groups allowed by this capability. Must include every group
29
+ * the agent will subscribe to — the upgrade-time intersection of
30
+ * (allowed) ∩ (requested) determines effective subscription. Returning
31
+ * a list that doesn't include the needed groups produces an empty
32
+ * intersection and silent broadcast failure.
33
+ */
34
+ readonly syncGroups: readonly string[];
35
+ }
36
+ interface AgentIdentity {
37
+ readonly userId: string;
38
+ readonly organizationId: string;
39
+ /**
40
+ * Surface class — `'chat'`, `'mcp'`, `'agent_worker'`, etc. Session
41
+ * caches per surface so two surfaces don't share token or WS.
42
+ */
43
+ readonly surfaceClass: string;
44
+ readonly target?: {
45
+ readonly entityType: string;
46
+ readonly entityId: string;
47
+ } | null;
48
+ }
49
+ export interface AgentSessionOptions<R extends SchemaRecord = SchemaRecord> {
50
+ /** Sync-server WebSocket URL — `wss://sync.example.com` or `ws://localhost:3001`. */
51
+ readonly syncServerUrl: string;
52
+ /** Schema for the typed model proxy on the returned Ablo. After
53
+ * the dual-engine collapse, `Ablo({kind:'agent'})` is the unified
54
+ * factory and requires the schema to expose
55
+ * `agent.<model>.create/update/delete`. */
56
+ readonly schema: Schema<R>;
57
+ /**
58
+ * Token-issuing callback. Called on cache miss / expiry. Owns the
59
+ * consumer's auth flow (Better Auth cookies, API key exchange, OAuth,
60
+ * etc.) so the engine stays auth-flow-agnostic.
61
+ */
62
+ readonly issueToken: (identity: AgentIdentity) => Promise<IssuedToken>;
63
+ /**
64
+ * Soft window before actual expiry to re-mint. Defaults to 30s.
65
+ * Avoids races between mint-time and clock-skew at use-time.
66
+ */
67
+ readonly reissueBufferMs?: number;
68
+ /**
69
+ * Optional agent-id strategy. Default: `${surfaceClass}:${userId}`.
70
+ * Override when the consumer wants different attribution shape.
71
+ */
72
+ readonly agentIdFor?: (identity: AgentIdentity) => string;
73
+ }
74
+ /**
75
+ * Returns a session whose `getAgent` method handles cache, mint,
76
+ * sync_groups alignment, and lifecycle. Call `disposeAll()` from
77
+ * the consumer's process shutdown hook.
78
+ *
79
+ * Threading: the session is intended to be a long-lived singleton
80
+ * shared across requests. The cache is keyed precisely so two
81
+ * concurrent requests for the same (user, org, surface, target)
82
+ * share one agent + one WS, while different requests get
83
+ * independent agents.
84
+ */
85
+ export declare function createAgentSession<R extends SchemaRecord = SchemaRecord>(options: AgentSessionOptions<R>): {
86
+ getAgent: (identity: AgentIdentity) => Promise<Ablo<R>>;
87
+ evict: (identity: AgentIdentity) => void;
88
+ disposeAll: () => void;
89
+ };
90
+ export {};
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Agent session — cache + lifecycle for server-side `SyncAgent`s.
3
+ *
4
+ * Captures the pattern every server-side consumer needs:
5
+ * 1. Cache `SyncAgent` instances per (org, user, surface, target).
6
+ * 2. Re-mint capabilities before TTL elapses.
7
+ * 3. Align the SyncAgent ctor's `syncGroups` with the cap allowlist
8
+ * so the upgrade-time intersection is non-empty (avoid the
9
+ * silent black-hole-broadcast bug).
10
+ * 4. Connect / disconnect / dispose lifecycle.
11
+ *
12
+ * What's generic, what isn't:
13
+ * - Cache, TTL, sync_groups alignment, lifecycle: SAME for every
14
+ * consumer. Lives here.
15
+ * - Cap mint: AUTH-FLOW-SPECIFIC. Every consumer has a different
16
+ * way to obtain a token (Better Auth cookie forwarding, API key
17
+ * exchange, OAuth, etc.). Consumer provides via the
18
+ * `issueToken` callback.
19
+ *
20
+ * The helper itself imports nothing app-specific. Open-source-clean.
21
+ */
22
+ import { Ablo } from '../client/Ablo.js';
23
+ /**
24
+ * Returns a session whose `getAgent` method handles cache, mint,
25
+ * sync_groups alignment, and lifecycle. Call `disposeAll()` from
26
+ * the consumer's process shutdown hook.
27
+ *
28
+ * Threading: the session is intended to be a long-lived singleton
29
+ * shared across requests. The cache is keyed precisely so two
30
+ * concurrent requests for the same (user, org, surface, target)
31
+ * share one agent + one WS, while different requests get
32
+ * independent agents.
33
+ */
34
+ export function createAgentSession(options) {
35
+ const reissueBufferMs = options.reissueBufferMs ?? 30_000;
36
+ const agentIdFor = options.agentIdFor ??
37
+ ((id) => `${id.surfaceClass}:${id.userId}`);
38
+ const cacheByKey = new Map();
39
+ function cacheKey(id) {
40
+ const targetSeg = id.target
41
+ ? `:${id.target.entityType}:${id.target.entityId}`
42
+ : '';
43
+ return `${id.organizationId}:${id.userId}:${id.surfaceClass}${targetSeg}`;
44
+ }
45
+ async function getAgent(identity) {
46
+ const key = cacheKey(identity);
47
+ const cached = cacheByKey.get(key);
48
+ if (cached && cached.expiresAtMs - Date.now() > reissueBufferMs) {
49
+ return cached.agent;
50
+ }
51
+ // Best-effort cleanup of stale agent — don't let a stuck cached
52
+ // entry block fresh issuance.
53
+ if (cached) {
54
+ try {
55
+ await cached.agent.dispose();
56
+ }
57
+ catch {
58
+ /* ignore */
59
+ }
60
+ }
61
+ const minted = await options.issueToken(identity);
62
+ // Sync_groups alignment is the load-bearing detail. The SDK
63
+ // ctor's `syncGroups` and the cap mint's `syncGroups`
64
+ // MUST overlap or the upgrade intersection is empty and every
65
+ // broadcast filter returns false. Use the cap's allowed list
66
+ // verbatim — the caller controlled what went in there, so it's
67
+ // exactly what the SDK should request.
68
+ // `AbloOptions` exposes the URL as `baseURL` (resolved by
69
+ // `resolveBaseURL`). Earlier code passed `url:` here — `Ablo()`
70
+ // silently dropped the unknown field (the cast below masked the
71
+ // type error) and `resolveBaseURL` fell through to the hardcoded
72
+ // default `wss://api.ablo.cloud` (now `wss://mesh.ablo.finance`).
73
+ // Staging surfaced the bug 2026-05-07 — DNS lookup hit the wrong
74
+ // host even though the caller threaded `syncServerUrl` through
75
+ // correctly. Forward as `baseURL` so the caller's URL is the only
76
+ // source of truth and the package default never silently applies.
77
+ const wsUrl = toWsUrl(options.syncServerUrl);
78
+ const agentOptions = {
79
+ baseURL: wsUrl,
80
+ schema: options.schema,
81
+ kind: 'agent',
82
+ capabilityToken: minted.token,
83
+ agentId: agentIdFor(identity),
84
+ organizationId: identity.organizationId,
85
+ syncGroups: [...minted.syncGroups],
86
+ // Agents run in Node — no IDB available, no need for it.
87
+ inMemory: true,
88
+ };
89
+ const agent = Ablo(agentOptions);
90
+ try {
91
+ await agent.ready();
92
+ }
93
+ catch (err) {
94
+ const e = err;
95
+ const code = e.cause?.code;
96
+ const causeMsg = e.cause?.message;
97
+ // Best-effort dispose so the failed agent doesn't leak ws state.
98
+ try {
99
+ await agent.dispose();
100
+ }
101
+ catch { /* ignore */ }
102
+ // Use console.error directly (rather than the engine logger)
103
+ // because this path may run before the per-agent logger is
104
+ // attached. The structured fields match the cap-mint logger in
105
+ // `connectAgent.ts` so a single search picks both up.
106
+ // eslint-disable-next-line no-console
107
+ console.error('[Agent.session] ws bootstrap failed', {
108
+ url: wsUrl,
109
+ surfaceClass: identity.surfaceClass,
110
+ orgId: identity.organizationId,
111
+ userId: identity.userId,
112
+ code,
113
+ causeMsg,
114
+ err,
115
+ });
116
+ throw new Error(`ws bootstrap ${wsUrl} failed: ${e.message ?? 'bootstrap failed'}` +
117
+ (code ? ` (${code})` : ''));
118
+ }
119
+ cacheByKey.set(key, { agent, expiresAtMs: minted.expiresAtMs });
120
+ return agent;
121
+ }
122
+ function disposeAll() {
123
+ for (const { agent } of cacheByKey.values()) {
124
+ try {
125
+ void agent.dispose();
126
+ }
127
+ catch {
128
+ /* ignore */
129
+ }
130
+ }
131
+ cacheByKey.clear();
132
+ }
133
+ /**
134
+ * Eject a specific cached agent — useful when the consumer knows
135
+ * the underlying token is invalidated (revocation, role change)
136
+ * and wants the next `getAgent` call to mint fresh.
137
+ */
138
+ function evict(identity) {
139
+ const key = cacheKey(identity);
140
+ const cached = cacheByKey.get(key);
141
+ if (cached) {
142
+ try {
143
+ void cached.agent.dispose();
144
+ }
145
+ catch {
146
+ /* ignore */
147
+ }
148
+ cacheByKey.delete(key);
149
+ }
150
+ }
151
+ return { getAgent, evict, disposeAll };
152
+ }
153
+ /** `https://host` → `wss://host`; `http://host` → `ws://host`. */
154
+ function toWsUrl(url) {
155
+ return url.replace(/^http/, 'ws');
156
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Agent-SDK abstractions. The engine's data vocabulary
3
+ * (`Peer`, `Activity`, `IntentClaim`, `ActiveIntent`,
4
+ * `PresenceUpdatePayload`, `PresenceKind`) lives in
5
+ * `../types/streams.ts`. This file holds only the bits that are
6
+ * specific to the agent module: the `PresenceAnnouncer` abstraction
7
+ * (transport-agnostic announce contract) and `AgentContext` (the AI
8
+ * SDK `experimental_context` bag).
9
+ */
10
+ import type { Activity } from '../types/streams.js';
11
+ /**
12
+ * A minimal interface for announcing presence — abstract over WebSocket
13
+ * (`Ablo({kind: 'agent'})`) and REST (`Agent`). Both
14
+ * implementations satisfy this, so higher-level code can depend on it
15
+ * without caring about transport.
16
+ */
17
+ export interface PresenceAnnouncer {
18
+ announce(status: 'online' | 'away' | 'offline', activity?: Activity): Promise<void>;
19
+ }
20
+ /**
21
+ * Ambient context threaded into AI SDK tools via `experimental_context`.
22
+ *
23
+ * The pattern: the caller constructs an AgentContext once per agent
24
+ * invocation and passes it as `experimental_context`. Each tool's
25
+ * `execute` function extracts what it needs from
26
+ * `options.experimental_context` instead of closing over module-level
27
+ * state.
28
+ *
29
+ * Benefits over closure-based tool wiring:
30
+ * - Tools are framework-agnostic module exports (portable across agents)
31
+ * - The context is typed in one place, not scattered across closures
32
+ * - New tools can access any field without changing tool signatures
33
+ *
34
+ * Ported from the vercel-labs/open-agents pattern.
35
+ *
36
+ * ```ts
37
+ * import { generateText, tool } from 'ai';
38
+ * import { Agent, type AgentContext } from '@ablo/sync-engine-internal/agent';
39
+ *
40
+ * const updateSlideTool = () => tool({
41
+ * inputSchema: z.object({ id: z.string(), title: z.string() }),
42
+ * execute: async (args, { experimental_context }) => {
43
+ * const perception = Agent.fromContext(experimental_context);
44
+ * const check = await perception.checkFreshness('Slide', args.id, Date.now() - 5000);
45
+ * if (check.stale) return check.summary;
46
+ * // ... actual mutation
47
+ * },
48
+ * });
49
+ *
50
+ * await generateText({
51
+ * model: 'anthropic/claude-sonnet-4.5',
52
+ * tools: { updateSlide: updateSlideTool() },
53
+ * experimental_context: { perception, organizationId, userId } satisfies AgentContext,
54
+ * });
55
+ * ```
56
+ *
57
+ * Consumers can extend AgentContext via module augmentation or by
58
+ * intersecting with their own context type.
59
+ */
60
+ export interface AgentContext {
61
+ /** Presence / freshness / AI SDK hook primitives. Required. */
62
+ perception: PresenceAnnouncer & {
63
+ checkFreshness?: (entityType: string, entityId: string, lastSeenAt: number) => Promise<unknown>;
64
+ };
65
+ /** Organization scope for all operations. */
66
+ organizationId?: string;
67
+ /** User or agent identifier — format: "agent:<id>" for agents. */
68
+ userId?: string;
69
+ /** Sync groups the agent belongs to. */
70
+ syncGroups?: string[];
71
+ /** Allow extension with product-specific fields. */
72
+ [key: string]: unknown;
73
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Agent-SDK abstractions. The engine's data vocabulary
3
+ * (`Peer`, `Activity`, `IntentClaim`, `ActiveIntent`,
4
+ * `PresenceUpdatePayload`, `PresenceKind`) lives in
5
+ * `../types/streams.ts`. This file holds only the bits that are
6
+ * specific to the agent module: the `PresenceAnnouncer` abstraction
7
+ * (transport-agnostic announce contract) and `AgentContext` (the AI
8
+ * SDK `experimental_context` bag).
9
+ */
10
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Coordination context middleware — reads peer intents on the same
3
+ * entity from the sync engine's presence stream and injects a brief
4
+ * coordination note into the prompt before the LLM call.
5
+ *
6
+ * The complement of `intent-broadcast.ts`: that one declares what
7
+ * THIS agent is about to do; this one reads what OTHERS are doing
8
+ * and tells the LLM about it. Together they make multiplayer-with-
9
+ * AI structurally real — the AI knows when a human or another
10
+ * agent is mid-edit and can defer / phrase its work as
11
+ * "while you finish that, I'll …" / suggest waiting / coordinate
12
+ * explicitly.
13
+ *
14
+ * Open-source-clean: depends only on `@ai-sdk/provider` types and
15
+ * the package's own `SyncAgent`. Consumers compose via the AI
16
+ * SDK's `wrapLanguageModel`.
17
+ *
18
+ * Cost: zero extra LLM calls (read happens locally from the agent's
19
+ * cached presence stream — already in memory from the WS subscription).
20
+ * Adds a few sentences to the system prompt (typically <100 tokens)
21
+ * only when peers are actively editing.
22
+ */
23
+ import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
24
+ import type { Ablo } from '../client/Ablo.js';
25
+ import type { SchemaRecord } from '../schema/schema.js';
26
+ import type { IntentTarget } from './intent-broadcast.js';
27
+ export interface CoordinationContextMiddlewareOptions<R extends SchemaRecord = SchemaRecord> {
28
+ readonly agent: Ablo<R> | null;
29
+ readonly target: IntentTarget | null;
30
+ /**
31
+ * Optional intentId(s) to exclude from the read — typically this
32
+ * agent's own active claim so the coordination note doesn't tell
33
+ * the AI "you yourself are editing this." When middleware is
34
+ * composed with `intentBroadcastMiddleware` in the standard order,
35
+ * `transformParams` runs BEFORE the broadcast's `wrapStream`
36
+ * declares its claim, so the agent's own claim isn't yet in the
37
+ * cached presence and self-filtering isn't needed. The hook is
38
+ * here for callers that compose differently or for fleet
39
+ * coordination (filter sibling worker intents).
40
+ */
41
+ readonly excludeIntentIds?: readonly string[];
42
+ }
43
+ /**
44
+ * Build the middleware. When `agent` or `target` is null, returns a
45
+ * pass-through.
46
+ *
47
+ * Generic over the schema record — see `intentBroadcastMiddleware`
48
+ * for why `Ablo<S>` and `Ablo<SchemaRecord>` aren't structurally
49
+ * assignable.
50
+ */
51
+ export declare function coordinationContextMiddleware<R extends SchemaRecord = SchemaRecord>(options: CoordinationContextMiddlewareOptions<R>): LanguageModelV3Middleware;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Coordination context middleware — reads peer intents on the same
3
+ * entity from the sync engine's presence stream and injects a brief
4
+ * coordination note into the prompt before the LLM call.
5
+ *
6
+ * The complement of `intent-broadcast.ts`: that one declares what
7
+ * THIS agent is about to do; this one reads what OTHERS are doing
8
+ * and tells the LLM about it. Together they make multiplayer-with-
9
+ * AI structurally real — the AI knows when a human or another
10
+ * agent is mid-edit and can defer / phrase its work as
11
+ * "while you finish that, I'll …" / suggest waiting / coordinate
12
+ * explicitly.
13
+ *
14
+ * Open-source-clean: depends only on `@ai-sdk/provider` types and
15
+ * the package's own `SyncAgent`. Consumers compose via the AI
16
+ * SDK's `wrapLanguageModel`.
17
+ *
18
+ * Cost: zero extra LLM calls (read happens locally from the agent's
19
+ * cached presence stream — already in memory from the WS subscription).
20
+ * Adds a few sentences to the system prompt (typically <100 tokens)
21
+ * only when peers are actively editing.
22
+ */
23
+ /**
24
+ * Build the middleware. When `agent` or `target` is null, returns a
25
+ * pass-through.
26
+ *
27
+ * Generic over the schema record — see `intentBroadcastMiddleware`
28
+ * for why `Ablo<S>` and `Ablo<SchemaRecord>` aren't structurally
29
+ * assignable.
30
+ */
31
+ export function coordinationContextMiddleware(options) {
32
+ const { agent, target } = options;
33
+ const excludeIntentIds = new Set(options.excludeIntentIds ?? []);
34
+ return {
35
+ specificationVersion: 'v3',
36
+ transformParams: async ({ params }) => {
37
+ if (!agent || !target)
38
+ return params;
39
+ // Read peer intents on the same target. Synchronous lookup
40
+ // against the engine's reactive intents.others array — no I/O.
41
+ const peerClaims = agent.intents.others.filter((claim) => claim.target.type === target.entityType &&
42
+ claim.target.id === target.entityId &&
43
+ targetsOverlap(claim.target, target) &&
44
+ !excludeIntentIds.has(claim.id));
45
+ if (peerClaims.length === 0)
46
+ return params;
47
+ const note = formatCoordinationNote(peerClaims, target);
48
+ return injectSystemNote(params, note);
49
+ },
50
+ };
51
+ }
52
+ function hasSubtarget(target) {
53
+ return Boolean(target.path || target.field || target.range);
54
+ }
55
+ function rangesOverlap(a, b) {
56
+ return a.startLine <= b.endLine && b.startLine <= a.endLine;
57
+ }
58
+ function targetsOverlap(claimTarget, target) {
59
+ if (!hasSubtarget(claimTarget) || !hasSubtarget(target))
60
+ return true;
61
+ if (claimTarget.path &&
62
+ target.path &&
63
+ claimTarget.path.toLowerCase() !== target.path.toLowerCase()) {
64
+ return false;
65
+ }
66
+ const fieldOverlaps = !claimTarget.field ||
67
+ !target.field ||
68
+ claimTarget.field.toLowerCase() === target.field.toLowerCase();
69
+ const rangeOverlaps = !claimTarget.range ||
70
+ !target.range ||
71
+ rangesOverlap(claimTarget.range, target.range);
72
+ return fieldOverlaps && rangeOverlaps;
73
+ }
74
+ /**
75
+ * Format a one-paragraph coordination note for the LLM. Includes
76
+ * who's editing and what (when known). Kept short — the goal is
77
+ * "AI knows," not "AI gets a wall of text."
78
+ */
79
+ function formatCoordinationNote(claims, target) {
80
+ const entityLabel = target.entityType.toLowerCase();
81
+ if (claims.length === 1) {
82
+ const c = claims[0];
83
+ return (`<multiplayer_context>\n` +
84
+ `Another participant is currently editing this ${entityLabel}. ` +
85
+ `Action declared: ${c.reason}. ` +
86
+ `Defer to their concurrent changes when reasonable, or note your work as complementary to theirs. ` +
87
+ `Avoid stomping their in-flight edits.\n` +
88
+ `</multiplayer_context>`);
89
+ }
90
+ const actions = Array.from(new Set(claims.map((c) => c.reason))).join(', ');
91
+ return (`<multiplayer_context>\n` +
92
+ `${claims.length} other participants are currently editing this ${entityLabel}. ` +
93
+ `Active actions: ${actions}. ` +
94
+ `Coordinate with their in-flight work — defer where reasonable, ` +
95
+ `or describe your work as complementary.\n` +
96
+ `</multiplayer_context>`);
97
+ }
98
+ /**
99
+ * Append a system-role message to the prompt array. The AI SDK's
100
+ * `LanguageModelV3Prompt` is an ordered list of messages.
101
+ */
102
+ function injectSystemNote(params, note) {
103
+ return {
104
+ ...params,
105
+ prompt: [...params.prompt, { role: 'system', content: note }],
106
+ };
107
+ }