@abloatai/ablo 0.5.1 → 0.7.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 (129) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +248 -124
  3. package/dist/BaseSyncedStore.d.ts +3 -3
  4. package/dist/BaseSyncedStore.js +3 -3
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +91 -93
  8. package/dist/client/Ablo.js +122 -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 +116 -90
  14. package/dist/client/createModelProxy.js +128 -128
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +5 -5
  18. package/dist/coordination/index.d.ts +6 -0
  19. package/dist/coordination/index.js +6 -0
  20. package/dist/coordination/schema.d.ts +329 -0
  21. package/dist/coordination/schema.js +209 -0
  22. package/dist/core/QueryView.d.ts +4 -1
  23. package/dist/core/QueryView.js +1 -1
  24. package/dist/core/index.d.ts +2 -0
  25. package/dist/core/index.js +7 -0
  26. package/dist/core/query-utils.d.ts +7 -10
  27. package/dist/core/query-utils.js +2 -3
  28. package/dist/errorCodes.d.ts +264 -0
  29. package/dist/errorCodes.js +251 -0
  30. package/dist/errors.d.ts +59 -14
  31. package/dist/errors.js +73 -12
  32. package/dist/index.d.ts +11 -9
  33. package/dist/index.js +8 -12
  34. package/dist/interfaces/index.d.ts +2 -10
  35. package/dist/mutators/Transaction.d.ts +2 -2
  36. package/dist/mutators/Transaction.js +2 -2
  37. package/dist/mutators/mutateActions.d.ts +44 -0
  38. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  39. package/dist/mutators/readerActions.d.ts +32 -0
  40. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  41. package/dist/policy/index.d.ts +1 -1
  42. package/dist/policy/index.js +1 -1
  43. package/dist/policy/types.d.ts +31 -0
  44. package/dist/policy/types.js +15 -0
  45. package/dist/query/types.d.ts +1 -1
  46. package/dist/react/AbloProvider.d.ts +13 -1
  47. package/dist/react/AbloProvider.js +14 -6
  48. package/dist/react/context.d.ts +4 -4
  49. package/dist/react/index.d.ts +4 -5
  50. package/dist/react/index.js +3 -7
  51. package/dist/react/useAblo.d.ts +14 -14
  52. package/dist/react/useAblo.js +26 -26
  53. package/dist/react/useIntent.d.ts +2 -2
  54. package/dist/react/useIntent.js +2 -2
  55. package/dist/react/useMutators.d.ts +1 -1
  56. package/dist/react/usePresence.d.ts +3 -3
  57. package/dist/react/usePresence.js +4 -4
  58. package/dist/react/useUndoScope.d.ts +1 -1
  59. package/dist/schema/ddl.d.ts +62 -0
  60. package/dist/schema/ddl.js +317 -0
  61. package/dist/schema/diff.d.ts +167 -0
  62. package/dist/schema/diff.js +280 -0
  63. package/dist/schema/field.d.ts +16 -19
  64. package/dist/schema/field.js +30 -17
  65. package/dist/schema/generate.d.ts +19 -0
  66. package/dist/schema/generate.js +87 -0
  67. package/dist/schema/index.d.ts +9 -3
  68. package/dist/schema/index.js +14 -2
  69. package/dist/schema/model.d.ts +87 -25
  70. package/dist/schema/model.js +33 -3
  71. package/dist/schema/relation.d.ts +17 -0
  72. package/dist/schema/roles.d.ts +148 -0
  73. package/dist/schema/roles.js +149 -0
  74. package/dist/schema/schema.d.ts +10 -69
  75. package/dist/schema/schema.js +58 -24
  76. package/dist/schema/select.d.ts +25 -0
  77. package/dist/schema/select.js +55 -0
  78. package/dist/schema/serialize.d.ts +96 -0
  79. package/dist/schema/serialize.js +231 -0
  80. package/dist/schema/sugar.d.ts +20 -3
  81. package/dist/schema/sugar.js +5 -1
  82. package/dist/schema/tenancy.d.ts +66 -0
  83. package/dist/schema/tenancy.js +58 -0
  84. package/dist/sync/HydrationCoordinator.d.ts +2 -0
  85. package/dist/sync/HydrationCoordinator.js +23 -17
  86. package/dist/sync/SyncWebSocket.d.ts +17 -0
  87. package/dist/sync/SyncWebSocket.js +46 -1
  88. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  89. package/dist/sync/awaitIntentGrant.js +60 -0
  90. package/dist/sync/createIntentStream.d.ts +2 -1
  91. package/dist/sync/createIntentStream.js +89 -5
  92. package/dist/sync/createPresenceStream.js +1 -1
  93. package/dist/sync/participants.d.ts +2 -2
  94. package/dist/sync/participants.js +9 -18
  95. package/dist/types/global.d.ts +43 -52
  96. package/dist/types/global.js +16 -18
  97. package/dist/types/streams.d.ts +90 -42
  98. package/docs/api-keys.md +44 -0
  99. package/docs/api.md +72 -173
  100. package/docs/audit.md +5 -5
  101. package/docs/cli.md +212 -0
  102. package/docs/client-behavior.md +42 -43
  103. package/docs/coordination.md +343 -0
  104. package/docs/data-sources.md +16 -16
  105. package/docs/examples/agent-human.md +30 -32
  106. package/docs/examples/ai-sdk-tool.md +32 -33
  107. package/docs/examples/existing-python-backend.md +38 -36
  108. package/docs/examples/nextjs.md +24 -25
  109. package/docs/examples/scoped-agent.md +78 -0
  110. package/docs/examples/server-agent.md +20 -61
  111. package/docs/guarantees.md +34 -56
  112. package/docs/identity.md +529 -0
  113. package/docs/index.md +18 -24
  114. package/docs/integration-guide.md +130 -144
  115. package/docs/interaction-model.md +32 -95
  116. package/docs/mcp/claude-code.md +3 -3
  117. package/docs/mcp/cursor.md +1 -1
  118. package/docs/mcp/windsurf.md +1 -1
  119. package/docs/mcp.md +11 -26
  120. package/docs/quickstart.md +43 -49
  121. package/docs/react.md +74 -24
  122. package/docs/roadmap.md +17 -7
  123. package/llms.txt +34 -39
  124. package/package.json +8 -1
  125. package/dist/react/useMutate.d.ts +0 -83
  126. package/dist/react/useQuery.d.ts +0 -123
  127. package/dist/react/useQuery.js +0 -145
  128. package/dist/react/useReader.d.ts +0 -69
  129. package/docs/capabilities.md +0 -163
@@ -52,18 +52,17 @@ that same model path.
52
52
  schema -> ablo.<model>.load(...) -> ablo.<model>.update(...)
53
53
  ```
54
54
 
55
- Capabilities, tasks, commits, and receipts exist under the hood. Most apps do
56
- not create them by hand.
55
+ Commits and receipts exist under the hood. Most apps do not create protocol
56
+ objects by hand.
57
57
 
58
58
  ## Pick The Backing Mode
59
59
 
60
60
  Every schema model has a backing store. The SDK call shape stays the same.
61
61
 
62
- | Mode | Rows live in | Use when |
63
- |---|---|---|
64
- | Ablo-managed | Ablo | New collaborative or agent-written state can live in Ablo. |
65
- | Data Source | Your app database | You already have tables, service logic, and API endpoints that remain canonical. |
66
- | Schema-less resource API | Custom runtime | A server worker, MCP route, or migration script intentionally cannot import the app schema. |
62
+ | Mode | Rows live in | Use when |
63
+ | ------------ | ----------------- | -------------------------------------------------------------------------------- |
64
+ | Ablo-managed | Ablo | New collaborative or agent-written state can live in Ablo. |
65
+ | Data Source | Your app database | You already have tables, service logic, and API endpoints that remain canonical. |
67
66
 
68
67
  Do not pass a database URL to `Ablo(...)`. Application and agent code use
69
68
  `ABLO_API_KEY`. If your database stays canonical, expose a signed Data Source
@@ -74,7 +73,7 @@ endpoint from your app and keep the database credentials inside your app.
74
73
  Use the public `/sandbox` page to understand the state flow. It is a visual,
75
74
  deterministic demo; it does not call your API key or mutate hosted Ablo data.
76
75
  It is also built for coding agents: copy the sandbox prompt into Claude Code or
77
- Codex and ask it to wire one real resource through the schema model API.
76
+ Codex and ask it to wire one real model through the schema model API.
78
77
 
79
78
  Use the authenticated org dashboard sandbox for real integration work. The
80
79
  default sandbox is the equivalent of Stripe test mode:
@@ -92,7 +91,7 @@ and write path are ready for production.
92
91
  When handing this to a coding agent, give it a concrete target:
93
92
 
94
93
  ```txt
95
- Add Ablo Sync to this app for one resource that humans and agents both edit.
94
+ Add Ablo to this app for one model that humans and agents both edit.
96
95
  Use the org sandbox sk_test_* key. Declare schema, add the Ablo client, replace
97
96
  one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
98
97
  wait: 'confirmed' }), and add a smoke test for two concurrent writers.
@@ -102,67 +101,65 @@ wait: 'confirmed' }), and add a smoke test for two concurrent writers.
102
101
 
103
102
  Start with fields and relations. Keep load strategies, indexing hints, and
104
103
  read-only/mutable shortcuts out of the first version unless you already need
105
- offline-heavy local cache behavior.
104
+ them.
106
105
 
107
106
  ```ts
108
- // src/ablo.schema.ts
107
+ // src/ablo/schema.ts
109
108
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
110
109
 
111
110
  export const schema = defineSchema(
112
111
  {
113
- tasks: model({
112
+ weatherReports: model({
114
113
  id: z.string(),
115
114
  projectId: z.string(),
116
- title: z.string(),
117
- status: z.enum(['todo', 'doing', 'done']),
115
+ location: z.string(),
116
+ status: z.enum(['pending', 'ready']),
118
117
  assigneeId: z.string().nullable(),
119
118
  updatedAt: z.string(),
120
119
  }),
121
120
  },
122
121
  {
123
- // Identity-anchored sync-group roles. The server walks these to
124
- // build each participant's allowed subscription set from the
125
- // resolved identity context. Templates and extractors are fully
126
- // consumer-controlled no hardcoded `org:` / `user:` convention
127
- // anywhere in the engine. Omit `identityRoles` entirely if your
128
- // schema doesn't need identity-derived scoping.
122
+ // Identity-anchored sync-group roles. The server walks these to build each
123
+ // participant's allowed subscription set from the resolved identity context.
124
+ // `kind` is the group prefix; `source` is the identity field to read both
125
+ // consumer-controlled, no hardcoded `org:` / `user:` convention anywhere in
126
+ // the engine. Pure data (no closures), so the schema stays JSON-serializable.
127
+ // Omit `identityRoles` entirely if you don't need identity-derived scoping.
129
128
  identityRoles: [
130
- {
131
- kind: 'tenant',
132
- template: 'org:{id}',
133
- extract: (i) => (i.organizationId ? [String(i.organizationId)] : []),
134
- },
135
- {
136
- kind: 'participant',
137
- template: 'user:{id}',
138
- extract: (i) => (i.userId ? [String(i.userId)] : []),
139
- },
129
+ identityRole({ kind: 'org', source: 'organizationId' }),
130
+ identityRole({ kind: 'user', source: 'userId' }),
140
131
  ],
141
- },
132
+ }
142
133
  );
143
134
  ```
144
135
 
145
136
  ### Declaring scope on a model
146
137
 
147
- Per-row tenancy and per-entity sync-group anchors live on the
148
- `defineModel` (or `model(...)`) options. The two halves compose: the
149
- identity roles above produce a participant's *allowed* set; the
150
- per-model options below define how rows are filtered server-side and
151
- which sync-group label each row fans out on.
138
+ > **Canonical reference: [Identity & Sync Groups](./identity.md).** This is the
139
+ > short version — `scope` (root), `parent` (containment), `grants` (membership),
140
+ > and the model-form `scope` prop are all covered in depth there. Read it once;
141
+ > this guide only shows the minimal shape inline.
142
+
143
+ Per-row tenancy and per-entity sync-group anchors live on the `model(...)`
144
+ options. The two halves compose: the identity roles above produce a
145
+ participant's _allowed_ set; the per-model options below define how rows are
146
+ filtered server-side and which sync-group each row fans out on.
152
147
 
153
148
  ```ts
154
149
  model(
155
- { /* fields */ },
150
+ {
151
+ /* fields */
152
+ },
156
153
  /* relations */ {},
157
154
  {
158
155
  // Rows carry organization_id and bootstrap filters on it.
159
156
  orgScoped: true,
160
157
 
161
- // Per-entity sync-group anchor. Lets a capability narrow into
162
- // one row's scope via `syncGroupFormat.replace('{id}', rowId)`.
163
- syncGroupFormat: 'matter:{id}',
164
- },
165
- )
158
+ // Scope root: rows form the group `matter:<id>`. Children point at it with
159
+ // `relation.belongsTo('matters', 'matterId', { parent: true })` to inherit.
160
+ scope: 'matter',
161
+ }
162
+ );
166
163
  ```
167
164
 
168
165
  For rows that don't carry `organization_id` themselves but inherit
@@ -177,7 +174,7 @@ Trusted runtimes can use `ABLO_API_KEY`.
177
174
  ```ts
178
175
  // src/ablo.ts
179
176
  import Ablo from '@abloatai/ablo';
180
- import { schema } from './ablo.schema';
177
+ import { schema } from './ablo/schema';
181
178
 
182
179
  export const ablo = Ablo({
183
180
  schema,
@@ -185,7 +182,7 @@ export const ablo = Ablo({
185
182
  });
186
183
  ```
187
184
 
188
- Browser apps should use the React provider or a scoped session/capability, not a
185
+ Browser apps should use the React provider or a scoped session token, not a
189
186
  server API key in the bundle.
190
187
 
191
188
  ```tsx
@@ -193,7 +190,7 @@ server API key in the bundle.
193
190
  'use client';
194
191
 
195
192
  import { AbloProvider } from '@abloatai/ablo/react';
196
- import { schema } from '@/ablo.schema';
193
+ import { schema } from '@/ablo/schema';
197
194
 
198
195
  export function Providers({ children }: { children: React.ReactNode }) {
199
196
  return <AbloProvider schema={schema}>{children}</AbloProvider>;
@@ -208,15 +205,14 @@ bundle, and signs server-to-server requests. It is the right credential
208
205
  for trusted runtimes (Next.js server actions, background workers,
209
206
  migration scripts) where the code reading it is yours.
210
207
 
211
- A browser or an agent runtime is not that environment. The React
212
- provider and the `agent.run(...)` wrapper both exchange your api key for
213
- a **capability** a short-lived, narrowly-scoped bearer token. The
214
- browser holds the cap; the api key never leaves the server. The
215
- exchange is the bridge between two credential shapes:
208
+ A browser is not that environment. The React provider exchanges your API key for
209
+ a short-lived, narrowly scoped bearer token. The browser holds that scoped token;
210
+ the API key never leaves the server. The exchange is the bridge between two
211
+ credential shapes:
216
212
 
217
213
  ```
218
214
  trusted runtime browser / agent
219
- ABLO_API_KEY ─exchange─► Capability token ────► narrow scope, leased
215
+ ABLO_API_KEY ─exchange─► scoped token ────────► narrow scope, leased
220
216
  (long-lived, (short-lived,
221
217
  broad scope, per-actor scope,
222
218
  server only) revocable)
@@ -225,10 +221,8 @@ ABLO_API_KEY ─exchange─► Capability token ────► narrow scope, l
225
221
  This is the same shape as Stripe's
226
222
  ephemeral keys (Issuing Elements expires in 15 minutes) and AWS STS
227
223
  AssumeRole (returns time-bounded creds with the minimal needed scope).
228
- You never *type* a capability into your app; the SDK mints one when it
229
- needs one and refreshes before expiry. See
230
- [Capabilities](./capabilities.md) for the design rationale and the
231
- manual create/revoke surface that custom runtimes use.
224
+ You never type that token into your app; the SDK mints one when it needs one and
225
+ refreshes before expiry.
232
226
 
233
227
  ## 3. Read State
234
228
 
@@ -237,18 +231,18 @@ Use `load` when the row may not already be local.
237
231
  ```ts
238
232
  await ablo.ready();
239
233
 
240
- const [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
241
- if (!task) throw new Error('task not found');
234
+ const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
235
+ if (!report) throw new Error('report not found');
242
236
  ```
243
237
 
244
- Use `retrieve`, `list`, and `count` for synchronous local reads after data has
238
+ Use `retrieve`, `list`, and `count` for synchronous reads after data has
245
239
  loaded.
246
240
 
247
241
  ```ts
248
- const task = ablo.tasks.retrieve('task_123');
249
- const activeTasks = ablo.tasks.list({
242
+ const report = ablo.weatherReports.retrieve('report_stockholm');
243
+ const activeReports = ablo.weatherReports.list({
250
244
  where: { projectId: 'proj_123' },
251
- filter: (task) => task.status !== 'done',
245
+ filter: (report) => report.status !== 'ready',
252
246
  orderBy: { updatedAt: 'desc' },
253
247
  limit: 50,
254
248
  });
@@ -261,17 +255,15 @@ In React, selector `useAblo` is the public read API:
261
255
 
262
256
  import { useAblo } from '@abloatai/ablo/react';
263
257
 
264
- export function TaskRow({ task: serverTask }: { task: Task }) {
265
- const task = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
266
- const intents = useAblo((ablo) =>
267
- ablo.intents.list({ resource: 'tasks', id: serverTask.id }),
268
- ) ?? [];
269
-
270
- return (
271
- <button disabled={intents.length > 0 || task.status === 'done'}>
272
- {task.title}
273
- </button>
274
- );
258
+ export function ReportRow({
259
+ report: serverReport,
260
+ }: {
261
+ report: { id: string; location: string; status: string };
262
+ }) {
263
+ const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
264
+ const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
265
+
266
+ return <button disabled={Boolean(active) || report.status === 'ready'}>{report.location}</button>;
275
267
  }
276
268
  ```
277
269
 
@@ -286,27 +278,23 @@ const ablo = useAblo();
286
278
  For simple writes:
287
279
 
288
280
  ```ts
289
- await ablo.tasks.update(
290
- 'task_123',
291
- { status: 'done' },
292
- { wait: 'confirmed' },
293
- );
281
+ await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
294
282
  ```
295
283
 
296
284
  For writes based on state the user or agent already read, snapshot first and
297
285
  reject stale updates:
298
286
 
299
287
  ```ts
300
- const snap = ablo.snapshot({ tasks: 'task_123' });
288
+ const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
301
289
 
302
- await ablo.tasks.update(
303
- 'task_123',
304
- { status: 'done' },
290
+ await ablo.weatherReports.update(
291
+ 'report_stockholm',
292
+ { status: 'ready' },
305
293
  {
306
294
  readAt: snap.stamp,
307
295
  onStale: 'reject',
308
296
  wait: 'confirmed',
309
- },
297
+ }
310
298
  );
311
299
  ```
312
300
 
@@ -317,16 +305,16 @@ back optimistic local state and throw a typed `AbloError`.
317
305
 
318
306
  There is no separate multiplayer setup.
319
307
 
320
- If humans, server actions, and agents use the same schema model resource, they
308
+ If humans, server actions, and agents use the same schema client, they
321
309
  share the same stream:
322
310
 
323
311
  ```txt
324
- human UI -> ablo.tasks.update(...)
325
- agent -> ablo.tasks.update(...)
326
- server -> ablo.tasks.update(...)
312
+ human UI -> ablo.weatherReports.update(...)
313
+ agent -> ablo.weatherReports.update(...)
314
+ server -> ablo.weatherReports.update(...)
327
315
  ```
328
316
 
329
- Ablo coordinates those writes, fans out confirmed deltas, exposes active intents,
317
+ Ablo coordinates those writes, fans out confirmed deltas, exposes active claims,
330
318
  and lets callers reject stale writes with `readAt`.
331
319
 
332
320
  Direct writes to your own database bypass that stream until your app reports the
@@ -342,7 +330,7 @@ the records that need multiplayer now and agent-safe writes later.
342
330
 
343
331
  ```txt
344
332
  Button
345
- -> ablo.tasks.update(...)
333
+ -> ablo.weatherReports.update(...)
346
334
  -> Ablo
347
335
  -> signed Data Source request
348
336
  -> existing backend service
@@ -352,13 +340,13 @@ Button
352
340
 
353
341
  The migration can be gradual:
354
342
 
355
- 1. Declare schema for one resource, such as `tasks`.
343
+ 1. Declare schema for one model, such as `reports`.
356
344
  2. Keep existing server loads for first paint.
357
- 3. Add `useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask` for live rows.
345
+ 3. Add `useAblo((ablo) => ablo.weatherReports.retrieve(id)) ?? serverReport` for live rows.
358
346
  4. Add one Data Source endpoint that calls the existing service layer.
359
- 5. Move one mutation button from `fetch('/api/tasks/...')` to `ablo.tasks.update(...)`.
347
+ 5. Move one mutation button from `fetch('/api/reports/...')` to `ablo.weatherReports.update(...)`.
360
348
  6. Add an outbox/events path for writes that still happen outside Ablo.
361
- 7. Let agents use the same `ablo.tasks.load(...)` and `ablo.tasks.update(...)`.
349
+ 7. Let agents use the same `ablo.weatherReports.load(...)` and `ablo.weatherReports.update(...)`.
362
350
 
363
351
  For the full Python shape, see
364
352
  [Existing Python Backend](./examples/existing-python-backend.md).
@@ -370,7 +358,7 @@ Use a Data Source when your app database remains the source of truth.
370
358
  ```ts
371
359
  // app/api/ablo/source/route.ts
372
360
  import { dataSource } from '@abloatai/ablo';
373
- import { schema } from '@/ablo.schema';
361
+ import { schema } from '@/ablo/schema';
374
362
  import { db } from '@/db';
375
363
 
376
364
  export const POST = dataSource({
@@ -390,13 +378,13 @@ export const POST = dataSource({
390
378
  return { rows };
391
379
  },
392
380
 
393
- tasks: {
381
+ reports: {
394
382
  async load({ id, context }) {
395
- return context.auth.db.task.findUnique({ where: { id } });
383
+ return context.auth.db.report.findUnique({ where: { id } });
396
384
  },
397
385
 
398
386
  async list({ query, context }) {
399
- return context.auth.db.task.findMany({
387
+ return context.auth.db.report.findMany({
400
388
  take: query.limit ?? 100,
401
389
  });
402
390
  },
@@ -420,18 +408,19 @@ Agents should use the same model methods as the app when they can import the
420
408
  schema.
421
409
 
422
410
  ```ts
423
- const [task] = await ablo.tasks.load({ where: { id: taskId } });
424
- if (!task) return;
425
-
426
- const intents = ablo.intents.list({ resource: 'tasks', id: taskId });
427
- if (intents.length > 0) return { skipped: true, reason: 'busy' };
428
-
429
- const snap = ablo.snapshot({ tasks: taskId });
430
-
431
- await ablo.tasks.update(
432
- taskId,
433
- { status: 'done', summary: await summarize(task) },
434
- { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
411
+ const [report] = await ablo.weatherReports.load({ where: { id: reportId } });
412
+ if (!report) return;
413
+
414
+ await ablo.weatherReports.claim(
415
+ reportId,
416
+ async (claimed) => {
417
+ await ablo.weatherReports.update(
418
+ claimed.id,
419
+ { status: 'ready', forecast: await getForecast(claimed) },
420
+ { wait: 'confirmed' }
421
+ );
422
+ },
423
+ { wait: false, action: 'forecasting' }
435
424
  );
436
425
  ```
437
426
 
@@ -439,38 +428,36 @@ Use AI SDK for the model loop. Put Ablo inside the tool that persists the final
439
428
  change.
440
429
 
441
430
  ```ts
442
- const completeTask = tool({
443
- description: 'Mark a task done with a summary',
431
+ const completeReport = tool({
432
+ description: 'Mark a weather report ready with a forecast',
444
433
  inputSchema: z.object({
445
- taskId: z.string(),
446
- summary: z.string(),
434
+ reportId: z.string(),
435
+ forecast: z.string(),
447
436
  }),
448
- execute: async ({ taskId, summary }) => {
449
- const snap = ablo.snapshot({ tasks: taskId });
450
- return ablo.tasks.update(
451
- taskId,
452
- { status: 'done', summary },
453
- { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
437
+ execute: async ({ reportId, forecast }) => {
438
+ const snap = ablo.snapshot({ weatherReports: reportId });
439
+ return ablo.weatherReports.update(
440
+ reportId,
441
+ { status: 'ready', forecast },
442
+ { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' }
454
443
  );
455
444
  },
456
445
  });
457
446
  ```
458
447
 
459
- Use schema-less `agent.run(...)`, `resource(...)`, and `commits.create(...)` only
460
- for custom server runtimes that intentionally cannot import the schema.
448
+ Keep agent writes on the same schema client surface as the app.
461
449
 
462
450
  ## Optional Surface
463
451
 
464
- | Optional piece | Why it exists |
465
- |---|---|
466
- | `/react` | Live React selectors, provider lifecycle, presence, sync status. |
467
- | `/testing` | Test harnesses and deterministic mocks. |
468
- | `Data Source` | Keep your app database canonical. |
469
- | `persistence: 'indexeddb'` | Durable browser cache and offline queueing for apps that need it. |
470
- | `intents` | Show active work and coordinate before a write. |
471
- | `snapshot` + `readAt` | Reject writes based on stale state. |
472
- | `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and local-cache tuning. |
473
- | `resource(...)` and `commits.create(...)` | Low-level protocol access for custom runtimes. |
452
+ | Optional piece | Why it exists |
453
+ | ----------------------------------------- | ----------------------------------------------------------------- |
454
+ | `/react` | Live React selectors, provider lifecycle, presence, sync status. |
455
+ | `/testing` | Test harnesses and deterministic mocks. |
456
+ | `Data Source` | Keep your app database canonical. |
457
+ | `persistence: 'indexeddb'` | Durable browser cache that survives reloads, for apps that need it. |
458
+ | `claim` / `claimState` / `queue` | Show active work and coordinate before a write. |
459
+ | `snapshot` + `readAt` | Reject writes based on stale state. |
460
+ | `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and read tuning. |
474
461
 
475
462
  The first integration should not need most of these. Start with schema and
476
463
  model methods, then add the optional pieces where the product actually needs
@@ -478,17 +465,16 @@ them.
478
465
 
479
466
  ## Method Cheatsheet
480
467
 
481
- | Method | Use it for |
482
- |---|---|
483
- | `load({ where })` | Async hydration from backing store/server. |
484
- | `retrieve(id)` | Synchronous local read of one loaded row. |
485
- | `list(options?)` | Synchronous local collection read. |
486
- | `count(options?)` | Synchronous local count. |
487
- | `create(data, options?)` | Create through the model resource. |
488
- | `update(id, data, options?)` | Update through the model resource. |
489
- | `delete(id, options?)` | Delete through the model resource. |
490
- | `intents.list(target)` | See active work on a resource. |
491
- | `intents.waitFor(target)` | Wait on the live intent stream. |
492
-
493
- Do not use `ablo.commit` as the first write API. Most callers should never need
494
- the low-level commit plane directly.
468
+ | Method | Use it for |
469
+ | ---------------------------- | ---------------------------------------------------------------- |
470
+ | `load({ where })` | Async hydration from backing store/server. |
471
+ | `retrieve(id)` | Synchronous read of one already-loaded row. |
472
+ | `list(options?)` | Synchronous collection read of loaded rows. |
473
+ | `count(options?)` | Synchronous count of loaded rows. |
474
+ | `create(data, options?)` | Create through the model client. |
475
+ | `update(id, data, options?)` | Update through the model client. |
476
+ | `delete(id, options?)` | Delete through the model client. |
477
+ | `claimState(id)` | See active work on a model row. |
478
+ | `claim(id, work, options?)` | Wait for your turn, re-read, and hold the row while `work` runs. |
479
+
480
+ Keep first integrations on the model methods above.