@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
@@ -9,7 +9,7 @@ that need multiplayer now and agent-safe writes later.
9
9
 
10
10
  This also applies to any API-backed app, not only Python. A product like a YC
11
11
  company's existing dashboard can keep its current endpoint/service/database
12
- shape and migrate one coordinated resource at a time.
12
+ shape and migrate one coordinated model at a time.
13
13
 
14
14
  ```txt
15
15
  Browser UI
@@ -30,10 +30,10 @@ Create a schema for the records that need realtime coordination.
30
30
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
31
31
 
32
32
  export const schema = defineSchema({
33
- tasks: model({
33
+ weatherReports: model({
34
34
  id: z.string(),
35
- title: z.string(),
36
- status: z.enum(['todo', 'doing', 'done']),
35
+ location: z.string(),
36
+ status: z.enum(['pending', 'ready']),
37
37
  updatedAt: z.string(),
38
38
  }),
39
39
  });
@@ -51,7 +51,7 @@ export const ablo = Ablo({
51
51
  ```
52
52
 
53
53
  Mount the React provider near the app root so client components can subscribe to
54
- model resources without importing server credentials.
54
+ model clients without importing server credentials.
55
55
 
56
56
  ```tsx
57
57
  // web/app/providers.tsx
@@ -68,30 +68,32 @@ export function Providers({ children }: { children: React.ReactNode }) {
68
68
  ## 2. Add Live Reads In The UI
69
69
 
70
70
  Keep the first render backed by the existing Python endpoint. After that,
71
- subscribe to the same model resource Ablo writes through.
71
+ subscribe to the same model client Ablo writes through.
72
72
 
73
73
  ```tsx
74
74
  'use client';
75
75
 
76
76
  import { useAblo } from '@abloatai/ablo/react';
77
77
 
78
- export function TaskRow({ task: serverTask }: { task: Task }) {
79
- const task = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
80
- const intents = useAblo((ablo) =>
81
- ablo.intents.list({ resource: 'tasks', id: serverTask.id }),
82
- ) ?? [];
83
- const busy = intents.length > 0;
78
+ export function ReportRow({
79
+ report: serverReport,
80
+ }: {
81
+ report: { id: string; location: string; status: string };
82
+ }) {
83
+ const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
84
+ const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
85
+ const claimed = Boolean(active);
84
86
 
85
87
  return (
86
- <button disabled={busy || task.status === 'done'}>
87
- {busy ? 'Someone is editing' : task.title}
88
+ <button disabled={claimed || report.status === 'ready'}>
89
+ {claimed ? 'Someone is editing' : report.location}
88
90
  </button>
89
91
  );
90
92
  }
91
93
  ```
92
94
 
93
95
  No string model key is needed in the first example. The selector reads from
94
- `ablo.tasks`, so React uses the same model resource as writes and agents.
96
+ `ablo.weatherReports`, so React uses the same model client as writes and agents.
95
97
 
96
98
  ## 3. Add One Python Data Source Endpoint
97
99
 
@@ -120,7 +122,7 @@ import os
120
122
  import time
121
123
  from fastapi import APIRouter, HTTPException, Request
122
124
 
123
- from app.services.tasks import get_task, list_tasks, apply_task_operations
125
+ from app.services.reports import get_report, list_reports, apply_report_operations
124
126
 
125
127
  router = APIRouter()
126
128
 
@@ -160,15 +162,15 @@ async def ablo_source(request: Request):
160
162
  body = json.loads(raw_body)
161
163
 
162
164
  if body["type"] == "load":
163
- if body["model"] == "tasks":
164
- return {"row": await get_task(body["id"])}
165
+ if body["model"] == "weatherReports":
166
+ return {"row": await get_report(body["id"])}
165
167
 
166
168
  if body["type"] == "list":
167
- if body["model"] == "tasks":
168
- return {"rows": await list_tasks(body.get("query", {}))}
169
+ if body["model"] == "weatherReports":
170
+ return {"rows": await list_reports(body.get("query", {}))}
169
171
 
170
172
  if body["type"] == "commit":
171
- rows = await apply_task_operations(
173
+ rows = await apply_report_operations(
172
174
  operations=body["operations"],
173
175
  client_tx_id=body.get("clientTxId"),
174
176
  scope=body.get("scope", {}),
@@ -178,7 +180,7 @@ async def ablo_source(request: Request):
178
180
  raise HTTPException(status_code=400, detail="unsupported request")
179
181
  ```
180
182
 
181
- `apply_task_operations` should reuse the same transaction and validation logic
183
+ `apply_report_operations` should reuse the same transaction and validation logic
182
184
  the existing Python endpoints already use. Dedupe by `clientTxId` so retries are
183
185
  safe.
184
186
 
@@ -193,20 +195,20 @@ Button -> Python endpoint -> service -> database
193
195
  Target button path:
194
196
 
195
197
  ```txt
196
- Button -> ablo.tasks.update(...)
198
+ Button -> ablo.weatherReports.update(...)
197
199
  Ablo -> Python Data Source endpoint
198
200
  Python service -> database
199
201
  Ablo -> realtime fanout and receipt
200
202
  ```
201
203
 
202
- The app does not need a flag-day rewrite. Move one resource at a time.
204
+ The app does not need a flag-day rewrite. Move one model at a time.
203
205
 
204
206
  ```ts
205
- const snap = ablo.snapshot({ tasks: taskId });
207
+ const snap = ablo.snapshot({ weatherReports: reportId });
206
208
 
207
- await ablo.tasks.update(
208
- taskId,
209
- { status: 'done' },
209
+ await ablo.weatherReports.update(
210
+ reportId,
211
+ { status: 'ready' },
210
212
  { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
211
213
  );
212
214
  ```
@@ -235,12 +237,12 @@ and timestamp. If the change originated from an Ablo commit, include the same
235
237
  Agents use the same model API as the UI:
236
238
 
237
239
  ```ts
238
- const [task] = await ablo.tasks.load({ where: { id: taskId } });
239
- const snap = ablo.snapshot({ tasks: taskId });
240
+ const [report] = await ablo.weatherReports.load({ where: { id: reportId } });
241
+ const snap = ablo.snapshot({ weatherReports: reportId });
240
242
 
241
- await ablo.tasks.update(
242
- taskId,
243
- { status: 'done' },
243
+ await ablo.weatherReports.update(
244
+ reportId,
245
+ { status: 'ready' },
244
246
  { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
245
247
  );
246
248
  ```
@@ -7,11 +7,11 @@ Server Components, and live client subscriptions.
7
7
 
8
8
  ```txt
9
9
  app/
10
- tasks/
10
+ reports/
11
11
  [id]/
12
12
  page.tsx # RSC: retrieve + render
13
13
  actions.ts # Server Action: schema update with stale-state check
14
- TaskEditor.tsx # Client: live updates
14
+ ReportEditor.tsx # Client: live updates
15
15
  lib/
16
16
  ablo.ts # Schema-backed Ablo client for server actions
17
17
  ```
@@ -19,40 +19,41 @@ app/
19
19
  ## RSC Initial Render
20
20
 
21
21
  ```tsx
22
- // app/tasks/[id]/page.tsx
22
+ // app/reports/[id]/page.tsx
23
23
  import { ablo } from '@/lib/ablo';
24
24
 
25
- export default async function TaskPage({
25
+ export default async function ReportPage({
26
26
  params,
27
27
  }: { params: { id: string } }) {
28
28
  await ablo.ready();
29
- const [task] = await ablo.tasks.load({ where: { id: params.id } });
30
- if (!task) return null;
29
+ const [report] = await ablo.weatherReports.load({ where: { id: params.id } });
30
+ if (!report) return null;
31
31
 
32
- return <TaskEditor task={task} />;
32
+ return <ReportEditor report={report} />;
33
33
  }
34
34
  ```
35
35
 
36
36
  ## Server Action Commit
37
37
 
38
38
  ```ts
39
- // app/tasks/[id]/actions.ts
39
+ // app/reports/[id]/actions.ts
40
40
  'use server';
41
41
 
42
42
  import { ablo } from '@/lib/ablo';
43
43
 
44
- export async function markDone(id: string) {
45
- const busy = ablo.intents.list({ resource: 'tasks', id });
46
- if (busy.length > 0) return { status: 'busy', intents: busy };
47
-
48
- const snap = ablo.snapshot({ tasks: id });
49
- const task = await ablo.tasks.update(
44
+ export async function markReady(id: string) {
45
+ const report = await ablo.weatherReports.claim(
50
46
  id,
51
- { status: 'done' },
52
- { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
47
+ async (claimed) =>
48
+ ablo.weatherReports.update(
49
+ claimed.id,
50
+ { status: 'ready' },
51
+ { wait: 'confirmed' },
52
+ ),
53
+ { wait: false, action: 'marking_ready' },
53
54
  );
54
55
 
55
- return { status: 'done', task };
56
+ return { status: 'ready', report };
56
57
  }
57
58
  ```
58
59
 
@@ -66,16 +67,14 @@ rejects. The action can re-fetch and ask the user to retry.
66
67
 
67
68
  import { useAblo } from '@abloatai/ablo/react';
68
69
 
69
- export function TaskEditor({ task: serverTask }: Props) {
70
- const data = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
71
- const intents = useAblo((ablo) =>
72
- ablo.intents.list({ resource: 'tasks', id: serverTask.id }),
73
- ) ?? [];
74
- const busy = intents.length > 0;
70
+ export function ReportEditor({ report: serverReport }: Props) {
71
+ const data = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
72
+ const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
73
+ const claimed = Boolean(active);
75
74
 
76
75
  return (
77
- <button disabled={busy || data.status === 'done'}>
78
- {busy ? 'Someone is editing' : 'Mark done'}
76
+ <button disabled={claimed || data.status === 'ready'}>
77
+ {claimed ? 'Someone is editing' : 'Mark ready'}
79
78
  </button>
80
79
  );
81
80
  }
@@ -8,10 +8,10 @@ import Ablo from '@abloatai/ablo';
8
8
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
9
9
 
10
10
  const schema = defineSchema({
11
- tasks: model({
12
- title: z.string(),
13
- status: z.enum(['todo', 'doing', 'done']),
14
- summary: z.string().optional(),
11
+ weatherReports: model({
12
+ location: z.string(),
13
+ status: z.enum(['pending', 'ready']),
14
+ forecast: z.string().optional(),
15
15
  }),
16
16
  });
17
17
 
@@ -20,67 +20,26 @@ const ablo = Ablo({
20
20
  apiKey: process.env.ABLO_API_KEY,
21
21
  });
22
22
 
23
- export async function completeTask(taskId: string) {
23
+ export async function completeReport(reportId: string) {
24
24
  await ablo.ready();
25
25
 
26
- const [task] = await ablo.tasks.load({ where: { id: taskId } });
27
- if (!task) return { status: 'not_found' };
28
-
29
- const busy = ablo.intents.list({ resource: 'tasks', id: taskId });
30
- if (busy.length > 0) {
31
- return { status: 'busy', intents: busy };
32
- }
33
-
34
- const snap = ablo.snapshot({ tasks: taskId });
35
- const updated = await ablo.tasks.update(
36
- taskId,
37
- { status: 'done' },
38
- { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
26
+ const [report] = await ablo.weatherReports.load({ where: { id: reportId } });
27
+ if (!report) return { status: 'not_found' };
28
+
29
+ const updated = await ablo.weatherReports.claim(
30
+ reportId,
31
+ async (claimed) =>
32
+ ablo.weatherReports.update(
33
+ claimed.id,
34
+ { status: 'ready' },
35
+ { wait: 'confirmed' },
36
+ ),
37
+ { wait: false, action: 'completing' },
39
38
  );
40
39
 
41
- return { status: 'done', task: updated };
40
+ return { status: 'ready', report: updated };
42
41
  }
43
42
  ```
44
43
 
45
- ## Advanced Schema-Less Run
46
-
47
- Use `agent.run(...)` when the worker intentionally cannot import the app schema.
48
- It creates the run envelope and returns `done`, `failed`, or `cancelled`.
49
-
50
- ```ts
51
- const api = Ablo({ apiKey: process.env.ABLO_API_KEY });
52
-
53
- const result = await api.agent('task-writer', {
54
- can: ['tasks.retrieve', 'tasks.update'],
55
- syncGroups: ['workspace:acme'],
56
- }).run(
57
- {
58
- prompt: 'Mark task_123 done.',
59
- surface: 'agent_worker',
60
- },
61
- async ({ resource }) => {
62
- const tasks = resource<{ title: string; status: string }>('tasks');
63
-
64
- const { data, stamp, intents } = await tasks.retrieve('task_123', {
65
- ifBusy: 'return',
66
- });
67
-
68
- if (intents.length > 0) {
69
- return { skipped: true, reason: 'busy' };
70
- }
71
-
72
- return tasks.update(
73
- 'task_123',
74
- { status: 'done' },
75
- { readAt: stamp, onStale: 'reject', wait: 'confirmed' },
76
- );
77
- },
78
- );
79
-
80
- if (result.status === 'failed') throw result.error;
81
- if (result.status === 'cancelled') return;
82
- ```
83
-
84
- Use the schema-backed version first. The schema-less version is for generic
85
- agent infrastructure, MCP routes, and platform code.
86
-
44
+ Use the schema-backed version for server agents so the worker, app, and React UI
45
+ share the same model methods.
@@ -1,6 +1,6 @@
1
1
  # Guarantees
2
2
 
3
- This page is the short contract for what Ablo Sync guarantees at the state
3
+ This page is the short contract for what Ablo guarantees at the state
4
4
  boundary.
5
5
 
6
6
  ## Confirmed Writes
@@ -9,19 +9,18 @@ boundary.
9
9
  the authoritative sync cursor.
10
10
 
11
11
  ```ts
12
- const updated = await ablo.tasks.update(
13
- 'task_123',
14
- { status: 'done' },
12
+ const updated = await ablo.weatherReports.update(
13
+ 'report_stockholm',
14
+ { status: 'ready' },
15
15
  { wait: 'confirmed' },
16
16
  );
17
17
  ```
18
18
 
19
19
  If the call resolves, the write was accepted by the server. If it rejects, the
20
20
  error explains whether the write was rejected for auth, validation, stale state,
21
- active intent conflict, idempotency, rate limit, or transport failure.
21
+ active claim conflict, idempotency, rate limit, or transport failure.
22
22
 
23
- Schema model writes return the updated model row. Advanced resource writes and
24
- `commits.create(...)` return a receipt with the commit status and sync cursor.
23
+ Schema model writes return the updated model row.
25
24
 
26
25
  ## Optimistic Local State
27
26
 
@@ -42,11 +41,11 @@ Use `snapshot(...)` and `readAt` when a write depends on state the agent already
42
41
  read:
43
42
 
44
43
  ```ts
45
- const snap = ablo.snapshot({ tasks: 'task_123' });
44
+ const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
46
45
 
47
- await ablo.tasks.update(
48
- 'task_123',
49
- { status: 'done' },
46
+ await ablo.weatherReports.update(
47
+ 'report_stockholm',
48
+ { status: 'ready' },
50
49
  { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
51
50
  );
52
51
  ```
@@ -61,45 +60,27 @@ Advanced policies exist for controlled product flows:
61
60
  - `flag` accepts the write and marks it for product review.
62
61
  - `merge` is reserved for server-defined merge behavior.
63
62
 
64
- ## Intent Coordination
63
+ ## Claim Coordination
65
64
 
66
- Intents are live coordination signals. They are not database locks.
65
+ Claims are live coordination signals. They are not database locks.
67
66
 
68
- When another human or agent is active on the same target, the caller chooses the
69
- behavior:
67
+ Claims are **advisory** and **cooperative**. `ablo.<model>.claim(id, ...)`
68
+ serializes on contention: if another human or agent already holds the row, the
69
+ claim waits for them to finish, then re-reads the row before handing it back, so
70
+ you proceed from fresh state. Reads are open by default —
71
+ `ablo.<model>.claimState(id)` returns the current claim state (or `null`) without
72
+ ever blocking. Server/model reads can opt into `ifClaimed: 'wait'` or
73
+ `ifClaimed: 'fail'` when they should not read through active work.
70
74
 
71
- - `ifBusy: 'return'` returns active intents immediately.
72
- - `ifBusy: 'wait'` waits until the matching intent clears.
73
- - `ifBusy: 'fail'` throws `AbloBusyError` with the active intents attached.
74
-
75
- Schema clients wait from the realtime intent stream. Schema-less HTTP callers
76
- must pass an explicit `busyPollInterval` if they choose `ifBusy: 'wait'`; Ablo
77
- does not hide a hard-coded polling loop. `busyTimeout` is only a maximum wait.
75
+ A claim does not reject or block other writers; it announces work so peers
76
+ serialize behind it rather than racing. While you hold a claim, the matching
77
+ `ablo.<model>.update(id, ...)` is stale-guarded and rejects with
78
+ `AbloStaleContextError` if the row advanced past your claim point.
78
79
 
79
80
  ## Agent Runs
80
81
 
81
- `agent.run(...)` is the advanced schema-less run envelope for workers that cannot
82
- import the app schema. It returns one of three statuses:
83
-
84
- - `done` — the handler returned successfully.
85
- - `failed` — the handler threw or the commit failed.
86
- - `cancelled` — the run signal aborted.
87
-
88
- Normal schema-backed agents should import the same schema as the app and write
89
- through `ablo.<model>.update(...)`. The lower-level run envelope exists for
90
- platform runtimes that need capability and task management without app code.
91
-
92
- ## Capabilities and Tasks
93
-
94
- Capabilities scope what an agent is allowed to do. Tasks group a run for audit
95
- and cost attribution.
96
-
97
- Most users do not create either one manually. The SDK and hosted API manage the
98
- common case. Manual capability and task APIs are for platform builders, custom
99
- agent runtimes, and internal infrastructure.
100
-
101
- Use `lease` as a crash cleanup window. A successful agent run still closes when
102
- the handler returns, fails, or is cancelled.
82
+ Agents should import the same schema as the app and write through
83
+ `ablo.<model>.claim(...)` plus `ablo.<model>.update(...)`.
103
84
 
104
85
  ## Audit Trail
105
86
 
@@ -107,9 +88,7 @@ Accepted writes can be attributed to:
107
88
 
108
89
  - the actor that wrote,
109
90
  - the human or system the actor worked on behalf of,
110
- - the capability that scoped the write,
111
- - the task or run that caused it,
112
- - the resource, operation, and state cursor.
91
+ - the model, operation, and state cursor.
113
92
 
114
93
  For agent work, this is what lets an audit surface answer: "what changed, who
115
94
  authorized it, which run did it, and what state was it based on?"
@@ -137,12 +116,8 @@ Ablo does not need a customer database URL. When your own database is canonical,
137
116
  Ablo calls a signed Data Source endpoint and records the coordination result for
138
117
  receipts, realtime fanout, and audit. See [Connect Your Database](./data-sources.md).
139
118
 
140
- ## Batches
141
-
142
- Most apps should use `ablo.<model>.create/update/delete`. Use
143
- `commits.create(...)` only when you need a low-level batch or a schema-less
144
- runtime.
119
+ ## Writes
145
120
 
146
- Each operation in the commit carries its own target, data, stale policy, and
147
- idempotency context. The server validates authorization, stale state, active
148
- intent conflicts, and idempotency before accepting the commit.
121
+ Use `ablo.<model>.create/update/delete` for state changes. The server validates
122
+ authorization, stale state, active claim conflicts, and idempotency before
123
+ accepting the write.