@abloatai/ablo 0.5.0 → 0.6.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 (94) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +242 -135
  3. package/dist/BaseSyncedStore.d.ts +2 -2
  4. package/dist/BaseSyncedStore.js +2 -2
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +90 -93
  8. package/dist/client/Ablo.js +121 -60
  9. package/dist/client/ApiClient.d.ts +14 -14
  10. package/dist/client/ApiClient.js +81 -55
  11. package/dist/client/createInternalComponents.d.ts +2 -3
  12. package/dist/client/createInternalComponents.js +2 -3
  13. package/dist/client/createModelProxy.d.ts +90 -87
  14. package/dist/client/createModelProxy.js +124 -127
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +3 -3
  18. package/dist/core/index.d.ts +2 -0
  19. package/dist/core/index.js +7 -0
  20. package/dist/errors.d.ts +8 -8
  21. package/dist/errors.js +18 -10
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +7 -11
  24. package/dist/interfaces/index.d.ts +2 -10
  25. package/dist/mutators/Transaction.d.ts +2 -2
  26. package/dist/mutators/Transaction.js +2 -2
  27. package/dist/mutators/mutateActions.d.ts +44 -0
  28. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  29. package/dist/mutators/readerActions.d.ts +32 -0
  30. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  31. package/dist/query/types.d.ts +1 -1
  32. package/dist/react/AbloProvider.d.ts +1 -1
  33. package/dist/react/AbloProvider.js +3 -3
  34. package/dist/react/context.d.ts +4 -4
  35. package/dist/react/index.d.ts +4 -5
  36. package/dist/react/index.js +3 -7
  37. package/dist/react/useAblo.d.ts +14 -14
  38. package/dist/react/useAblo.js +26 -26
  39. package/dist/react/useIntent.d.ts +2 -2
  40. package/dist/react/useIntent.js +2 -2
  41. package/dist/react/useMutators.d.ts +1 -1
  42. package/dist/react/usePresence.d.ts +3 -3
  43. package/dist/react/usePresence.js +4 -4
  44. package/dist/react/useUndoScope.d.ts +1 -1
  45. package/dist/schema/diff.d.ts +161 -0
  46. package/dist/schema/diff.js +262 -0
  47. package/dist/schema/generate.d.ts +19 -0
  48. package/dist/schema/generate.js +87 -0
  49. package/dist/schema/index.d.ts +4 -1
  50. package/dist/schema/index.js +7 -1
  51. package/dist/schema/schema.d.ts +83 -32
  52. package/dist/schema/schema.js +58 -12
  53. package/dist/schema/serialize.d.ts +92 -0
  54. package/dist/schema/serialize.js +227 -0
  55. package/dist/sync/SyncWebSocket.d.ts +17 -0
  56. package/dist/sync/SyncWebSocket.js +46 -1
  57. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  58. package/dist/sync/awaitIntentGrant.js +60 -0
  59. package/dist/sync/createIntentStream.js +43 -4
  60. package/dist/sync/createPresenceStream.js +1 -1
  61. package/dist/sync/participants.d.ts +2 -2
  62. package/dist/sync/participants.js +4 -4
  63. package/dist/types/global.d.ts +43 -52
  64. package/dist/types/global.js +16 -18
  65. package/dist/types/streams.d.ts +37 -9
  66. package/docs/api.md +68 -158
  67. package/docs/audit.md +5 -5
  68. package/docs/client-behavior.md +41 -42
  69. package/docs/coordination.md +294 -0
  70. package/docs/data-sources.md +14 -14
  71. package/docs/examples/agent-human.md +30 -32
  72. package/docs/examples/ai-sdk-tool.md +32 -33
  73. package/docs/examples/existing-python-backend.md +35 -33
  74. package/docs/examples/nextjs.md +24 -25
  75. package/docs/examples/server-agent.md +20 -61
  76. package/docs/guarantees.md +30 -55
  77. package/docs/identity.md +458 -0
  78. package/docs/index.md +12 -24
  79. package/docs/integration-guide.md +106 -116
  80. package/docs/interaction-model.md +29 -95
  81. package/docs/mcp/claude-code.md +3 -3
  82. package/docs/mcp/cursor.md +1 -1
  83. package/docs/mcp/windsurf.md +1 -1
  84. package/docs/mcp.md +11 -26
  85. package/docs/quickstart.md +43 -49
  86. package/docs/react.md +73 -23
  87. package/docs/roadmap.md +5 -7
  88. package/llms.txt +34 -39
  89. package/package.json +1 -1
  90. package/dist/react/useMutate.d.ts +0 -83
  91. package/dist/react/useQuery.d.ts +0 -123
  92. package/dist/react/useQuery.js +0 -145
  93. package/dist/react/useReader.d.ts +0 -69
  94. package/docs/capabilities.md +0 -163
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0f663e7: Coordination surface: fair queue, reactive wait-line, and lease renewal.
8
+ - **Claims acquire through a server FIFO queue.** On contention a claim waits its turn and re-reads before proceeding; reads are never blocked. Writes blocked by another participant's claim throw a typed `AbloBusyError`.
9
+ - **`ablo.<model>.queue(id)`** — reactive read of the wait-line behind a row: who's queued, their action, and FIFO position. Synced to peers like `activity(id)`.
10
+ - **Backpressure on `claim`** — `{ wait: false }` skips instead of waiting if the row is already held (claim-or-skip dedup); `{ maxQueueDepth: n }` bails with `AbloBusyError('queue_too_deep')` rather than joining a line already that deep.
11
+ - **Lease renewal** — a held claim renews automatically while the holder's connection is alive, so you never size a TTL; it lapses only after the holder goes silent. A queued claim that's abandoned is dequeued (no ghost waiters).
12
+ - **Reads are never gated by a claim**, including for agents.
13
+ - Intent vocabulary cleanup: a waiting claim is an `Intent` with `status: 'queued'` (`position` carries its place in line). Removed the unbuilt `whenFree`.
14
+
15
+ - **BREAKING — API renames** (apply when upgrading from 0.5.1):
16
+ - Change-listeners renamed to `.onChange(...)`: `ablo.<model>.subscribe(cb)`, `presence.subscribe()`, `intents.subscribe()` → `.onChange(...)`. (`subscribe` is reserved for an upcoming scope-grant verb.)
17
+ - Row-access API renamed Resource → Model: `Ablo.Resource.*` → `Ablo.Model.*`, `ablo.resource(name)` → `ablo.model(name)`, `ModelTarget.resource` → `ModelTarget.model`, error code `resource_not_found` → `model_not_found`.
18
+
19
+ ## 0.5.1
20
+
21
+ ### Patch Changes
22
+
23
+ - Docs: add a React quick-start (provider + `useAblo`), plain-language rewrite, and a "Set up with Claude Code" section.
24
+
3
25
  ## 0.5.0
4
26
 
5
27
  ### Minor Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ablo
2
2
 
3
- Ablo Sync is a typed sync engine for shared app state — the kind that humans,
3
+ Ablo is a typed sync engine for shared app state — the kind that humans,
4
4
  server code, and AI agents all edit at once.
5
5
 
6
6
  Reach for it when those edits need to show up everywhere in real time, not
@@ -8,29 +8,32 @@ silently overwrite each other, expose who's working on what, and leave a record
8
8
  of who changed what.
9
9
 
10
10
  ```txt
11
- schema -> ablo.<model>.create/load/update(...)
11
+ schema -> ablo.<model>.create/retrieve/update/claim(...)
12
12
  ```
13
13
 
14
- ## Install
14
+ ## Set up
15
15
 
16
16
  ```bash
17
17
  npm install @abloatai/ablo
18
18
  ```
19
19
 
20
- Requires Node 22+ and TypeScript 5+.
20
+ The package ships an `llms.txt` — a precise map of the API — so a coding agent
21
+ integrates from the real surface instead of guessing it. Point Claude Code or
22
+ Cursor at it:
21
23
 
22
- ## Get a Test Key
24
+ > Read `node_modules/@abloatai/ablo/llms.txt`, then add an Ablo schema and
25
+ > `<AbloProvider>`, wire my first create / retrieve / update, and use `claim`
26
+ > for anything an agent edits across a slow step (read → LLM → write).
23
27
 
24
- Create an Ablo sandbox and copy an `sk_test_*` API key. Keep API keys in trusted
25
- server runtimes only.
28
+ Or wire it by hand the [Quick Start](#quick-start) below is the shape it
29
+ produces. For production (React, an existing backend, Data Source, agents), the
30
+ [Integration Guide](./docs/integration-guide.md) is the deeper map.
26
31
 
27
- ```bash
28
- export ABLO_API_KEY=sk_test_...
29
- ```
30
-
31
- In the browser, connect through the React provider (`<AbloProvider>`), which
32
- authenticates with the signed-in user's session — never the raw API key. Do not
33
- ship `ABLO_API_KEY` in a browser bundle.
32
+ **Keys & runtime.** Ablo is ESM-only (`import`, not `require`) and needs Node
33
+ 22+ and TypeScript 5+. Grab an `sk_test_*` key
34
+ for a sandbox (`export ABLO_API_KEY=sk_test_...`); keep keys in trusted server
35
+ runtimes only. In the browser, `<AbloProvider>` authenticates with the signed-in
36
+ user's session never the raw key.
34
37
 
35
38
  ## Quick Start
36
39
 
@@ -74,139 +77,202 @@ Expected output:
74
77
  { id: '...', status: 'ready' }
75
78
  ```
76
79
 
77
- Pass `schema` to get typed models like `ablo.weatherReports.update(...)`. Omit it
78
- only for the lower-level client used by custom agents and MCP routes that can't
79
- import your app's schema.
80
+ Pass `schema` to get typed models like `ablo.weatherReports.update(...)`.
80
81
 
81
- Run the package example from this directory:
82
+ ## Reading
82
83
 
83
- ```bash
84
- cd examples
85
- ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
84
+ `retrieve(id)` returns one row from the local cache — synchronous, no round-trip.
85
+ `list(...)` filters and sorts what's already synced; it's also synchronous, and
86
+ reactive under `useAblo`/`subscribe`. `load(...)` fetches from the server when a
87
+ row may not be local yet.
88
+
89
+ ```ts
90
+ ablo.weatherReports.retrieve('report_stockholm'); // → row | undefined
91
+
92
+ // Synchronous, from the local cache → row[]
93
+ const pending = ablo.weatherReports.list({
94
+ where: { status: 'pending' }, // equality filter (an array value means IN)
95
+ orderBy: { location: 'asc' },
96
+ limit: 20,
97
+ });
98
+
99
+ // Server fetch → Promise<row[]>. 'complete' waits for the server; 'unknown'
100
+ // returns what's local now and refreshes in the background.
101
+ const ready = await ablo.weatherReports.load({
102
+ where: { status: 'ready' },
103
+ type: 'complete',
104
+ });
86
105
  ```
87
106
 
88
- For a production integration with React, an existing backend, Data Source, and
89
- future agents, read [Integration Guide](./docs/integration-guide.md).
107
+ ## Writing
90
108
 
91
- ## AI Activity on Existing State
109
+ `create` / `update` apply optimistically and resolve to the row. Two options
110
+ matter day to day:
92
111
 
93
- When AI or background work will spend real time on an existing row — not just a
94
- one-shot write coordinate through `ablo.<model>.intent(id)`, the coordination
95
- accessor that sits beside `create`/`update`/`retrieve`. It takes the row's `id` (the same
96
- id you pass to `retrieve(id)` / `update(id, …)`) and returns a handle
97
- synchronously, so you can see who's already working on a row before you start.
112
+ | Option | Values | What it does |
113
+ | --- | --- | --- |
114
+ | `wait` | `'queued'` \| `'confirmed'` | `'confirmed'` resolves only after the server acks the write; `'queued'` resolves as soon as it's locally queued (fire-and-forget). |
115
+ | `idempotencyKey` | `string` | Auto-generated per call. Override only when you own the retry boundary (e.g. a job id) so a re-run dedupes server-side. |
98
116
 
99
117
  ```ts
100
- // `report_stockholm` is this weather report's id set at create time, or the
101
- // `created.id` returned above.
102
- const report = ablo.weatherReports.intent('report_stockholm');
103
-
104
- // Read side: is someone already on it? Wait for them to finish.
105
- if (report.current) {
106
- report.current.heldBy; // 'agent:forecaster'
107
- await report.whenFree();
108
- }
118
+ await ablo.weatherReports.update(id, { status: 'ready' }, { wait: 'confirmed' });
119
+ ```
109
120
 
110
- // Write side: claim so other participants yield while we work.
111
- await report.claim({ action: 'checking_weather', field: 'forecast', ttl: '2m' });
121
+ To guard a write against a row that changed under you, pass `readAt` + `onStale`
122
+ see [Coordinating long agent work](#coordinating-long-agent-work).
112
123
 
113
- // Your existing weather tool or agent call. While this runs, other clients see
114
- // that report_stockholm is being checked.
115
- const row = ablo.weatherReports.retrieve('report_stockholm');
116
- const weather = await weatherAgent.getWeather(row.location);
124
+ ## Coordinating long agent work
117
125
 
118
- await report.update({
119
- status: 'ready',
120
- forecast: weather.summary,
121
- });
126
+ An agent reads a row, thinks for 30s, writes back — and clobbers whatever changed
127
+ meanwhile, or worse, acts on stale state. `claim` holds the row across that gap:
128
+
129
+ ```ts
130
+ await ablo.weatherReports.claim('report_stockholm', async (report) => {
131
+ // If someone else holds it, claim() WAITS in a fair queue, then re-reads —
132
+ // so `report` is the current row, never a stale snapshot. Reads stay open by
133
+ // default; only acting-on-the-row serializes.
134
+
135
+ const forecast = await weatherAgent.getWeather(report.location); // slow LLM gap
136
+ await ablo.weatherReports.update(report.id, { forecast, status: 'ready' });
137
+ }); // claim released here, whether the callback returns or throws
122
138
  ```
123
139
 
124
- Ablo does not fetch the weather. It keeps the activity visible while the work
125
- runs, rejects `report.update(...)` with `AbloStaleContextError` if the row
126
- changed under you, and finishes the claim automatically once the write lands.
140
+ See who's mid-edit before you act decide to wait, or skip:
127
141
 
128
- ## Multiplayer
142
+ ```ts
143
+ ablo.weatherReports.claimState('report_stockholm'); // → the holder, or null
144
+ ablo.weatherReports.queue('report_stockholm'); // → { data: [{ heldBy, action, position }, …] }
129
145
 
130
- There is no separate multiplayer mode. When human UI, server actions, and agent
131
- workers share the same schema and write through `ablo.<model>`, they all see
132
- each other's changes in real time that's the default, not a feature you turn on.
146
+ await ablo.weatherReports.claim(id, async (report) => {
147
+ /* do the held work */
148
+ }, { wait: false }); // held? skip (dedup) throws instead of waiting
133
149
 
134
- - `ablo.<model>.create/update/delete` fan out confirmed deltas to subscribers.
135
- - `useAblo(...)` gives React clients the live row plus active intents.
136
- - `ablo.<model>.intent(id)` lets humans and agents see and coordinate active work before a write lands.
137
-
138
- Always write through Ablo — the SDK (`ablo.<model>.create/update/delete`) or the
139
- HTTP API (`POST /v1/commits`). If you write straight to your own database
140
- instead, those changes won't reach connected clients. Use Ablo's endpoints and
141
- the fan-out is automatic.
142
-
143
- Under the hood, capabilities, tasks, leases, intents, commits, and receipts are
144
- real protocol primitives. They exist so agent work is scoped, coordinated,
145
- attributable, and cleaned up if a runtime disappears — but you don't touch them
146
- in a first integration. The default path above is enough.
147
-
148
- ## HTTP API
149
-
150
- The SDK is a typed wrapper over a signed HTTP API, the same way `stripe-node`
151
- wraps Stripe's REST API. Everything you do through `ablo.<model>` is reachable
152
- over HTTP, so backends in other languages and server-to-server callers work
153
- without the SDK.
154
-
155
- Writes — create, update, and delete — all go through one endpoint as a batch of
156
- operations:
157
-
158
- ```http
159
- POST /v1/commits
160
- Authorization: Bearer sk_live_...
161
- Idempotency-Key: <your-unique-id>
162
-
163
- {
164
- "operations": [
165
- {
166
- "type": "UPDATE",
167
- "model": "weatherReports",
168
- "id": "report_stockholm",
169
- "input": { "status": "ready" },
170
- "readAt": 1042,
171
- "onStale": "reject"
172
- }
173
- ]
150
+ await ablo.weatherReports.claim(id, async (report) => {
151
+ /* do the held work */
152
+ }, { maxQueueDepth: 2 }); // 2+ already ahead? bail
153
+ ```
154
+
155
+ Default reads keep working while a row is claimed. Server reads that need claimed
156
+ semantics can opt in with `ifClaimed: 'return' | 'wait' | 'fail'`.
157
+
158
+ Even an unclaimed write can't land on stale reasoning — the commit is guarded:
159
+
160
+ ```ts
161
+ try {
162
+ await ablo.weatherReports.update(id, { status: 'ready' }, { readAt, onStale: 'reject' });
163
+ } catch (e) {
164
+ if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
174
165
  }
175
166
  ```
176
167
 
177
- Each operation is one `CREATE` / `UPDATE` / `DELETE` (plus `ARCHIVE` /
178
- `UNARCHIVE`). Reads fetch a single row with `GET /v1/resources/{model}/{id}`.
179
- The same API key, scope, idempotency, and stale-write rules apply as in the SDK
180
- — the SDK just removes the boilerplate.
168
+ > Prefer the callback form for ordinary held work. Manual scoped claims are
169
+ > available for wider lifetimes, but callback claims are the docs default.
181
170
 
182
- ## Load vs Retrieve
171
+ See [Coordination](./docs/coordination.md) for the full `claim` / `claimState` /
172
+ `queue` / `release` reference.
183
173
 
184
- Most reads are `retrieve` — it's the everyday path, especially inside `useAblo(...)`:
174
+ ## React
185
175
 
186
- - `ablo.weatherReports.retrieve(id)` is sync. It returns the already-loaded row from
187
- the local pool (or `undefined` if it isn't loaded yet). In React,
188
- `useAblo((ablo) => ablo.weatherReports.retrieve(id))` keeps that read live.
189
- - `ablo.weatherReports.load({ where })` is async. Reach for it when you need to
190
- guarantee a row is hydrated before you read it — initial fetch, SSR, scripts, or
191
- any non-reactive runtime.
176
+ In a React app it's the **same `ablo.<model>` API** just mounted through a
177
+ provider and read with hooks, from `@abloatai/ablo/react`. Wrap your tree once;
178
+ everything inside is live.
192
179
 
193
- ## Persistence
180
+ ```tsx
181
+ import { AbloProvider, useAblo } from '@abloatai/ablo/react';
182
+ import { schema } from './ablo.schema';
194
183
 
195
- Ablo keeps local state in memory by default. That keeps the SDK focused on
196
- coordinating shared state, rather than silently turning every browser app into
197
- an offline database it didn't ask for.
184
+ function App() {
185
+ return (
186
+ <AbloProvider schema={schema}>
187
+ <Report id="report_stockholm" />
188
+ </AbloProvider>
189
+ );
190
+ }
198
191
 
199
- Opt into a durable browser cache and offline write queue when you want it:
192
+ function Report({ id }: { id: string }) {
193
+ // Reactive read: this re-renders whenever the row changes — whether you,
194
+ // a teammate, or an agent changed it.
195
+ const report = useAblo((ablo) => ablo.weatherReports.retrieve(id));
196
+ const ablo = useAblo();
200
197
 
201
- ```ts
202
- const ablo = Ablo({
203
- schema,
204
- apiKey: process.env.ABLO_API_KEY,
205
- persistence: 'indexeddb',
206
- });
198
+ if (!report) return null;
199
+
200
+ // Write: same method as the server example above. Optimistic; fans out.
201
+ return (
202
+ <button onClick={() => ablo?.weatherReports.update(id, { status: 'ready' })}>
203
+ {report.status}
204
+ </button>
205
+ );
206
+ }
207
207
  ```
208
208
 
209
- Node, SSR, tests, and agents use volatile in-memory persistence automatically.
209
+ `<AbloProvider>` owns the connection no API key in the browser. That's the
210
+ whole loop: read with `useAblo(selector)`, write with `ablo.<model>`, and every
211
+ other client (human or agent) on that row sees it in real time. See
212
+ [React](./docs/react.md) for the full `<AbloProvider>` prop surface (`userId`,
213
+ `teamIds`, `syncGroups`, `fallback`, `bootstrapMode`) and status hooks.
214
+
215
+ ## Identity & Sync Groups
216
+
217
+ Ablo is **not** an auth provider — you keep your own (Clerk, Auth0, NextAuth,
218
+ whatever). Ablo's job starts after you've authenticated a request: you tell it
219
+ *who* is connecting, and it scopes their realtime data to the right **sync
220
+ groups** (named channels like `org:acme` or `deck:abc123` that are both the unit
221
+ of fan-out and the unit of access).
222
+
223
+ The model is a proxy: your `ABLO_API_KEY` stays on your trusted server, your
224
+ server resolves the signed-in user (org / team / user) from your own auth, and
225
+ the browser connects as an already-scoped participant — it never holds the key
226
+ and can't widen its own scope. Your schema's `identityRoles` map that identity
227
+ to sync-group strings.
228
+
229
+ ```tsx
230
+ // userId / teamIds come from YOUR auth, resolved server-side
231
+ <AbloProvider schema={schema} userId={user.id} teamIds={user.teamIds}>
232
+ <App />
233
+ </AbloProvider>
234
+ ```
235
+
236
+ If it isn't obvious where org / team / user come from in the Quick Start above,
237
+ that's because they come from *your* app — see
238
+ [Identity & Sync Groups](./docs/identity.md) for the full picture: what a sync
239
+ group is, the two halves of scoping (`identityRoles` + per-model `orgScoped` /
240
+ `syncGroupFormat`), and how identity reaches Ablo without an API key in the
241
+ browser.
242
+
243
+ ## Multiplayer
244
+
245
+ There is no separate multiplayer mode. When human UI, server actions, and agent
246
+ workers share the same schema and write through `ablo.<model>`, they all see
247
+ each other's changes in real time — that's the default, not a feature you turn on.
248
+
249
+ - `ablo.<model>.create/update/delete` fan out confirmed deltas to subscribers.
250
+ - `useAblo(...)` gives React clients the live row, kept current automatically.
251
+ - `ablo.<model>.claim(id)` / `claimState(id)` / `queue(id)` let humans and agents coordinate (and observe) active work on a row — and the line waiting behind it — before a write lands.
252
+
253
+ Always write through Ablo — either the SDK model methods
254
+ (`ablo.<model>.create/update/delete`) or the HTTP write endpoint below. If you
255
+ write straight to your own database instead, those changes won't reach connected
256
+ clients.
257
+
258
+ ## HTTP Writes
259
+
260
+ Use the SDK when you are in JavaScript and want typed models or realtime. Use the
261
+ HTTP endpoint when a server-to-server caller needs to write without opening a
262
+ WebSocket:
263
+
264
+ ```bash
265
+ curl https://api.ablo.dev/v1/commits \
266
+ -H "Authorization: Bearer sk_test_..." \
267
+ -H "Content-Type: application/json" \
268
+ -d '{ "operations": [
269
+ { "action": "update", "model": "weatherReports", "id": "report_stockholm", "data": { "status": "ready" } }
270
+ ] }'
271
+ ```
272
+
273
+ ```json
274
+ { "object": "commit_receipt", "status": "confirmed", "serverTxId": "tx_…", "lastSyncId": 1042, "ops": 1 }
275
+ ```
210
276
 
211
277
  ## Connect Your Database
212
278
 
@@ -216,35 +282,76 @@ write to Ablo-managed state.
216
282
 
217
283
  If your existing database stays the source of truth, connect it as a Data
218
284
  Source: Ablo sends signed commit requests to an endpoint you host, and your app
219
- writes its own database. Ablo never sees your database credentials — only the
220
- API key:
285
+ writes its own database. Your `DATABASE_URL` stays in your appAblo only ever
286
+ sees the API key.
221
287
 
222
- ```bash
223
- # stays in your app — Ablo never receives this
224
- DATABASE_URL=postgres://...
288
+ See [Connect Your Database](./docs/data-sources.md) for the integration shape.
225
289
 
226
- # the only Ablo credential your app needs
227
- ABLO_API_KEY=sk_live_...
228
- ```
290
+ ## Configuration
229
291
 
230
- See [Connect Your Database](./docs/data-sources.md) for the route and commit shape.
292
+ `Ablo({ ... })` takes one required option and a couple of transport overrides:
231
293
 
232
- ## Agent Runs
294
+ | Option | Type | Default | Purpose |
295
+ | --- | --- | --- | --- |
296
+ | `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
297
+ | `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
298
+ | `baseURL` | `string` | `wss://mesh.ablo.finance` | Point at a self-hosted or staging mesh |
299
+
300
+ Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
301
+ authenticates with the signed-in user's session; the raw-key path is gated
302
+ behind `dangerouslyAllowBrowser` for server-proxy setups only. Self-hosted
303
+ deployments can pass `authToken` instead of `apiKey`. Advanced hooks (custom
304
+ `fetch`, logging, observability) live in [Client Behavior](./docs/client-behavior.md).
305
+
306
+ ## Errors
307
+
308
+ Every SDK error extends `AbloError` and carries a `requestId` for support.
309
+ Discriminate with `instanceof` or the `type` string — the string form also
310
+ survives worker / `postMessage` boundaries, where `instanceof` does not:
311
+
312
+ ```ts
313
+ try {
314
+ await ablo.weatherReports.update(id, { status: 'ready' }, { readAt, onStale: 'reject' });
315
+ } catch (e) {
316
+ if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
317
+ if ((e as AbloError).type === 'AbloClaimedError') { /* another participant holds it */ }
318
+ }
319
+ ```
233
320
 
234
- Most agent workers should import the same schema and use
235
- `ablo.<model>.load(...)` plus `ablo.<model>.update(...)`. The schema-less
236
- `agent.run(...)` wrapper exists for advanced workers that intentionally cannot
237
- import the app schema.
321
+ | Error | When |
322
+ | --- | --- |
323
+ | `AbloAuthenticationError` | Invalid / missing / expired credentials |
324
+ | `AbloPermissionError` / `CapabilityError` | Action forbidden by scope |
325
+ | `AbloRateLimitError` | Rate limited (carries `retryAfterSeconds`) |
326
+ | `AbloIdempotencyError` | Same `idempotencyKey` reused with a different body |
327
+ | `AbloValidationError` | Invalid request payload |
328
+ | `AbloStaleContextError` | Write carried `readAt`, but the row has newer changes (`conflicts`) |
329
+ | `AbloClaimedError` | Target is claimed by another participant (`claims`) |
330
+ | `AbloConnectionError` / `AbloServerError` | Transport failure / server 5xx |
331
+ | `SyncSessionError` | Session expired (prompts re-auth) |
332
+
333
+ ## Reconnect & retries
334
+
335
+ The client owns reconnection so your code doesn't have to. A dropped WebSocket
336
+ reconnects automatically with exponential backoff (1s → 30s, ±15% jitter, up to
337
+ ~7.5 minutes); session errors (401/403) suppress it so you re-authenticate
338
+ instead of looping. Commits are idempotent by client transaction id, and a
339
+ commit that times out is never silently rolled back — the client reconciles
340
+ against authoritative server state on reconnect. These defaults are the
341
+ contract; there are no retry or timeout knobs to tune.
238
342
 
239
343
  ## Production Reference
240
344
 
241
- - [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, intent coordination, and agent lifecycle.
345
+ - [Identity & Sync Groups](./docs/identity.md) — bring your own auth; tell Ablo who's connecting and how org / team / user map to sync-group scope.
346
+ - [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, claim coordination, and agent lifecycle.
242
347
  - [Integration Guide](./docs/integration-guide.md) — pick the backing mode and integrate React, Data Source, multiplayer, and agents.
348
+ - [React](./docs/react.md) — `<AbloProvider>`, `useAblo`, presence, status, and bootstrap gating.
349
+ - [Coordination](./docs/coordination.md) — `claim` / `claimState` / `queue` / `release` reference: hold a row across slow agent work, and observe the line waiting behind it.
243
350
  - [Client Behavior](./docs/client-behavior.md) — options, errors, retries, timeouts, and public imports.
244
351
  - [Connect Your Database](./docs/data-sources.md) — keep canonical rows in your app database without giving Ablo database credentials.
245
352
  - [Existing Python Backend](./docs/examples/existing-python-backend.md) — migrate existing Python endpoints to multiplayer and agent-safe writes gradually.
246
353
  - [AI SDK Tool](./docs/examples/ai-sdk-tool.md) — use Ablo inside an AI SDK tool call.
247
- - [Server Agent](./docs/examples/server-agent.md) — schema-backed worker plus advanced schema-less run.
354
+ - [Server Agent](./docs/examples/server-agent.md) — schema-backed worker.
248
355
 
249
356
  ## License
250
357
 
@@ -21,7 +21,7 @@ import { QueryProcessor } from './core/QueryProcessor.js';
21
21
  import { Model } from './Model.js';
22
22
  import { ModelScope } from './ObjectPool.js';
23
23
  import type { Schema } from './schema/schema.js';
24
- import { type ReaderActions } from './react/useReader.js';
24
+ import { type ReaderActions } from './mutators/readerActions.js';
25
25
  /** Constructor type for Model subclasses (accepts abstract classes) */
26
26
  export type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
27
27
  /** Concrete constructor type for instantiation */
@@ -139,7 +139,7 @@ export interface UserContext {
139
139
  * - `'full'` (default): pull every delta in scope before `ready()`
140
140
  * resolves. The standard browser/user replica behavior.
141
141
  * - `'none'`: open the WebSocket and process live deltas only.
142
- * Reads go through `resource.retrieve()` / filtered subscriptions
142
+ * Reads go through `model.retrieve()` / filtered subscriptions
143
143
  * backfilled by `Covering` deltas. Suitable for transactional
144
144
  * participants — agent-worker, video-pipeline, routine runners —
145
145
  * that don't need a local replica of the org's tenant plane.
@@ -22,7 +22,7 @@ import { getContext } from './context.js';
22
22
  import { SyncSessionError } from './errors.js';
23
23
  import { ModelScope } from './ObjectPool.js';
24
24
  import { LazyReferenceCollection } from './LazyReferenceCollection.js';
25
- import { createReaderActions } from './react/useReader.js';
25
+ import { createReaderActions } from './mutators/readerActions.js';
26
26
  /** Bootstrap timeout configuration */
27
27
  export const BOOTSTRAP_CONFIG = {
28
28
  OVERALL_TIMEOUT_MS: 15_000,
@@ -790,7 +790,7 @@ export class BaseSyncedStore {
790
790
  //
791
791
  // `bootstrapMode: 'none'` participants (agent-worker, headless
792
792
  // task runners) skip baseline replication — they read via
793
- // `resource.retrieve()` round-trips and rely on covering deltas
793
+ // `model.retrieve()` round-trips and rely on covering deltas
794
794
  // from filtered subscriptions to populate the pool lazily. The
795
795
  // WS is already open by `setupWebSocketSync` above, so live
796
796
  // delta flow works regardless of this branch.
@@ -2,9 +2,9 @@
2
2
  * Internal compatibility entrypoint for the stateless hosted protocol client.
3
3
  *
4
4
  * Use this build for serverless functions, scripts, and backends that want
5
- * Resource / Intent / Commit over HTTP without the realtime sync runtime.
5
+ * model reads/writes and commits over HTTP without the realtime sync runtime.
6
6
  */
7
- export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type Agent, type AgentIntentInput, type AgentIntentOptions, type AgentOptions, type AgentResourceClient, type AgentResourceReadOptions, type AgentResourceMutationOptions, type AgentRunContext, type AgentRunDone, type AgentRunFailed, type AgentRunCancelled, type AgentRunOptions, type AgentRunResult, type AgentRunStatus, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, type Task, type TaskCloseOptions, type TaskCloseResult, type TaskCreateOptions, type TaskResource, } from '../client/ApiClient.js';
8
- export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, IntentCreateOptions, IntentHandle, IntentWaitOptions, BusyOptions, BusyPolicy, ResourceClient, ResourceIntent, ResourceMutationOptions, ResourceReadOptions, ResourceRead, ResourceTarget, } from '../client/Ablo.js';
7
+ export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type Agent, type AgentIntentInput, type AgentIntentOptions, type AgentOptions, type AgentModelClient, type AgentModelReadOptions, type AgentModelMutationOptions, type AgentRunContext, type AgentRunDone, type AgentRunFailed, type AgentRunCancelled, type AgentRunOptions, type AgentRunResult, type AgentRunStatus, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, type Task, type TaskCloseOptions, type TaskCloseResult, type TaskCreateOptions, type TaskResource, } from '../client/ApiClient.js';
8
+ export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, IntentCreateOptions, IntentHandle, IntentWaitOptions, ClaimedOptions, IfClaimedPolicy, ModelClient, ModelClaim, ModelMutationOptions, ModelReadOptions, ModelRead, ModelTarget, } from '../client/Ablo.js';
9
9
  import { createProtocolClient } from '../client/ApiClient.js';
10
10
  export default createProtocolClient;
package/dist/api/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Internal compatibility entrypoint for the stateless hosted protocol client.
3
3
  *
4
4
  * Use this build for serverless functions, scripts, and backends that want
5
- * Resource / Intent / Commit over HTTP without the realtime sync runtime.
5
+ * model reads/writes and commits over HTTP without the realtime sync runtime.
6
6
  */
7
7
  export { createProtocolClient, createProtocolClient as Ablo, } from '../client/ApiClient.js';
8
8
  import { createProtocolClient } from '../client/ApiClient.js';