@abloatai/ablo 0.3.0 → 0.4.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 (97) hide show
  1. package/CHANGELOG.md +43 -5
  2. package/NOTICE +2 -2
  3. package/README.md +30 -28
  4. package/dist/agent/Agent.d.ts +1 -1
  5. package/dist/agent/Agent.js +1 -1
  6. package/dist/agent/index.d.ts +4 -4
  7. package/dist/agent/index.js +6 -6
  8. package/dist/agent/types.d.ts +1 -1
  9. package/dist/ai-sdk/index.d.ts +3 -3
  10. package/dist/ai-sdk/index.js +3 -3
  11. package/dist/ai-sdk/intent-broadcast.d.ts +1 -1
  12. package/dist/ai-sdk/intent-broadcast.js +1 -1
  13. package/dist/auth/index.d.ts +1 -1
  14. package/dist/client/Ablo.d.ts +8 -14
  15. package/dist/client/Ablo.js +32 -1
  16. package/dist/client/auth.d.ts +3 -3
  17. package/dist/client/auth.js +5 -5
  18. package/dist/client/createModelProxy.d.ts +110 -32
  19. package/dist/client/createModelProxy.js +77 -38
  20. package/dist/client/index.d.ts +2 -2
  21. package/dist/client/index.js +2 -2
  22. package/dist/config/index.d.ts +1 -1
  23. package/dist/config/index.js +1 -1
  24. package/dist/core/index.d.ts +1 -1
  25. package/dist/core/index.js +2 -2
  26. package/dist/errors.d.ts +1 -1
  27. package/dist/errors.js +1 -1
  28. package/dist/index.d.ts +6 -6
  29. package/dist/index.js +9 -9
  30. package/dist/interfaces/headless.d.ts +1 -1
  31. package/dist/interfaces/headless.js +2 -2
  32. package/dist/policy/index.d.ts +2 -2
  33. package/dist/policy/index.js +2 -2
  34. package/dist/principal.d.ts +1 -1
  35. package/dist/principal.js +1 -1
  36. package/dist/react/ClientSideSuspense.d.ts +1 -1
  37. package/dist/react/SyncGroupProvider.js +1 -1
  38. package/dist/react/context.d.ts +1 -1
  39. package/dist/react/context.js +1 -1
  40. package/dist/react/index.d.ts +1 -1
  41. package/dist/react/index.js +1 -1
  42. package/dist/react/useCurrentUserId.js +1 -1
  43. package/dist/react/useErrorListener.js +1 -1
  44. package/dist/react/useMutate.d.ts +1 -1
  45. package/dist/react/useMutationFailureListener.js +1 -1
  46. package/dist/react/useReader.d.ts +1 -1
  47. package/dist/schema/field.d.ts +1 -1
  48. package/dist/schema/field.js +1 -1
  49. package/dist/schema/index.d.ts +2 -2
  50. package/dist/schema/index.js +2 -2
  51. package/dist/schema/model.d.ts +2 -2
  52. package/dist/schema/model.js +2 -2
  53. package/dist/schema/queries.d.ts +1 -1
  54. package/dist/schema/queries.js +1 -1
  55. package/dist/schema/relation.d.ts +1 -1
  56. package/dist/schema/relation.js +1 -1
  57. package/dist/schema/schema.d.ts +1 -1
  58. package/dist/schema/schema.js +1 -1
  59. package/dist/source/index.d.ts +22 -28
  60. package/dist/source/index.js +23 -20
  61. package/dist/source/pushQueue.d.ts +1 -1
  62. package/dist/source/pushQueue.js +2 -2
  63. package/dist/sync/SyncWebSocket.d.ts +14 -0
  64. package/dist/sync/createIntentStream.js +7 -0
  65. package/dist/testing/fixtures/models.d.ts +1 -1
  66. package/dist/testing/fixtures/models.js +1 -1
  67. package/dist/testing/helpers/react-wrapper.d.ts +2 -2
  68. package/dist/testing/helpers/react-wrapper.js +2 -2
  69. package/dist/testing/index.d.ts +1 -1
  70. package/dist/testing/index.js +1 -1
  71. package/dist/types/streams.d.ts +39 -0
  72. package/docs/api-keys.md +2 -2
  73. package/docs/api.md +81 -23
  74. package/docs/capabilities.md +1 -1
  75. package/docs/client-behavior.md +7 -7
  76. package/docs/data-sources.md +52 -18
  77. package/docs/examples/agent-human.md +3 -3
  78. package/docs/examples/ai-sdk-tool.md +16 -33
  79. package/docs/examples/existing-python-backend.md +10 -10
  80. package/docs/examples/nextjs.md +1 -1
  81. package/docs/examples/server-agent.md +2 -2
  82. package/docs/index.md +1 -1
  83. package/docs/integration-guide.md +15 -14
  84. package/docs/interaction-model.md +16 -4
  85. package/docs/mcp.md +1 -1
  86. package/docs/quickstart.md +23 -21
  87. package/docs/react.md +3 -3
  88. package/docs/roadmap.md +1 -1
  89. package/examples/README.md +3 -3
  90. package/examples/data-source/README.md +1 -1
  91. package/examples/data-source/ablo-driver.ts +5 -5
  92. package/examples/data-source/customer-server.ts +10 -10
  93. package/examples/data-source/run.ts +9 -11
  94. package/examples/data-source/schema.ts +1 -1
  95. package/examples/quickstart.ts +2 -2
  96. package/llms.txt +8 -8
  97. package/package.json +1 -1
package/docs/api.md CHANGED
@@ -7,8 +7,8 @@ agents, read [Integration Guide](./integration-guide.md).
7
7
 
8
8
 
9
9
  ```ts
10
- import Ablo from '@ablo/sync-engine';
11
- import { defineSchema, model, z } from '@ablo/sync-engine/schema';
10
+ import Ablo from '@abloatai/ablo';
11
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
12
12
 
13
13
  const schema = defineSchema({
14
14
  tasks: model({
@@ -107,31 +107,89 @@ that work to clear, or `ifBusy: 'fail'` to throw `AbloBusyError`.
107
107
 
108
108
  ## Intent
109
109
 
110
- Intent is the coordination signal. It tells humans and agents who is working on
111
- a target before the write lands.
110
+ Intent is the coordination object it tells humans and agents who is working on
111
+ a target before the write lands. Like Stripe's `PaymentIntent`, one
112
+ self-describing object carries the whole lifecycle in a single `status` field.
113
+ It lives on the **coordination plane**: ephemeral, TTL'd, broadcast to peers in
114
+ real time, and never persisted as a row.
115
+
116
+ Read or open one through the model accessor — `ablo.<model>.intent(id)` — which
117
+ sits beside `create`/`update`/`retrieve` and returns a handle **synchronously**,
118
+ so you can inspect who holds a target without awaiting.
119
+
120
+ ### The Intent object
121
+
122
+ | Field | Type | Description |
123
+ |---|---|---|
124
+ | `object` | `'intent'` | String representing the object's type. Always `'intent'`. |
125
+ | `id` | string | Unique identifier for the intent. |
126
+ | `status` | `'active' \| 'committed' \| 'expired' \| 'canceled'` | The whole lifecycle, in one field. |
127
+ | `target` | `{ type, id, field? }` | What is being coordinated. |
128
+ | `action` | string | Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. |
129
+ | `heldBy` | string | Participant id holding the intent. |
130
+ | `participantKind` | `'human' \| 'agent'` | Whether a human session or an agent holds it. |
131
+ | `expiresAt` | string | Ms-epoch at which the server auto-expires it if the holder doesn't finish. |
132
+
133
+ ```json
134
+ {
135
+ "object": "intent",
136
+ "id": "int_3MtwBwLkdIwHu7ix",
137
+ "status": "active",
138
+ "target": { "type": "tasks", "id": "task_123", "field": "status" },
139
+ "action": "editing",
140
+ "heldBy": "agent:task-writer",
141
+ "participantKind": "agent",
142
+ "expiresAt": "1716580000000"
143
+ }
144
+ ```
145
+
146
+ ### Lifecycle
147
+
148
+ ```
149
+ acquire() update() lands
150
+ (free) ───────────▶ active ───────────────────────▶ committed
151
+
152
+ ┌───────────┴───────────┐
153
+ ▼ ▼
154
+ canceled expired
155
+ (release w/o write) (TTL; holder died)
156
+ ```
157
+
158
+ A target is free when `ablo.<model>.intent(id).current` is `null`. Terminal
159
+ states drop out of the live stream — a present intent is, by definition,
160
+ `active`.
161
+
162
+ ### Reading and acquiring
112
163
 
113
164
  ```ts
114
- const intent = await ablo.intents.create({
115
- target: { resource: 'tasks', id: 'task_123', field: 'status' },
116
- action: 'update',
117
- });
165
+ const task = ablo.tasks.intent('task_123');
118
166
 
119
- try {
120
- const snap = ablo.snapshot({ tasks: 'task_123' });
121
- const task = await ablo.tasks.update(
122
- 'task_123',
123
- { status: 'done' },
124
- { intent, readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
125
- );
126
-
127
- task.status; // done
128
- } finally {
129
- await intent.release();
167
+ // Read side — who is working on this target right now?
168
+ if (task.current) {
169
+ task.current.heldBy; // 'agent:task-writer'
170
+ task.current.action; // 'editing'
171
+ await task.settled(); // wait until they finish, then continue
130
172
  }
173
+
174
+ // Write side — claim, write, auto-release in one flow.
175
+ await task.acquire({ action: 'editing', field: 'status', ttl: '2m' });
176
+ const updated = await task.update({ status: 'done' });
177
+ updated.status; // 'done'
131
178
  ```
132
179
 
133
- `intents.waitFor(target)` waits until the live intent stream clears. Pass
134
- `timeout` only when your product needs an upper bound.
180
+ `task.update(...)` carries the same stale-check as a plain update: it rejects
181
+ with `AbloStaleContextError` if the row advanced past your acquire point, so you
182
+ re-read before retrying. The intent releases automatically when `update`
183
+ resolves; call `task.release()` if the work ends without a write.
184
+
185
+ `task.settled({ timeout })` waits until the target is free. Pass `timeout` only
186
+ when your product needs an upper bound.
187
+
188
+ ### Cross-resource coordination
189
+
190
+ For lower-level coordination that isn't scoped to a single model row, the
191
+ top-level `ablo.intents` resource (`create`, `list`, `waitFor`) remains
192
+ available. Most callers should prefer `ablo.<model>.intent(id)`.
135
193
 
136
194
  ## Advanced Commit API
137
195
 
@@ -171,12 +229,12 @@ Use `dataSource(...)` only when the customer's app database remains canonical
171
229
  and Ablo should call a signed endpoint instead of storing customer rows itself.
172
230
 
173
231
  ```ts
174
- import { dataSource } from '@ablo/sync-engine';
232
+ import { dataSource } from '@abloatai/ablo';
175
233
  import { schema } from './ablo.schema';
176
234
 
177
235
  export const POST = dataSource({
178
236
  schema,
179
- signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
237
+ apiKey: process.env.ABLO_API_KEY,
180
238
  async commit({ operations, clientTxId, context }) {
181
239
  // Write operations to the customer's database transaction.
182
240
  return { rows: [] };
@@ -104,7 +104,7 @@ behalf-of).
104
104
  ## Create
105
105
 
106
106
  ```ts
107
- import Ablo from '@ablo/sync-engine';
107
+ import Ablo from '@abloatai/ablo';
108
108
 
109
109
  const admin = Ablo({ apiKey: process.env.ABLO_API_KEY });
110
110
 
@@ -5,8 +5,8 @@ This page covers the SDK behavior around options, errors, retries, and runtimes.
5
5
  ## Constructor
6
6
 
7
7
  ```ts
8
- import Ablo from '@ablo/sync-engine';
9
- import { defineSchema, model, z } from '@ablo/sync-engine/schema';
8
+ import Ablo from '@abloatai/ablo';
9
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
10
10
 
11
11
  const schema = defineSchema({
12
12
  tasks: model({
@@ -152,7 +152,7 @@ All SDK errors extend `AbloError` and carry a stable `type`.
152
152
  | `AbloBusyError` | Active intent conflicted with `ifBusy: 'fail'` or a busy wait timed out. |
153
153
 
154
154
  ```ts
155
- import { AbloBusyError } from '@ablo/sync-engine';
155
+ import { AbloBusyError } from '@abloatai/ablo';
156
156
 
157
157
  try {
158
158
  await ablo.tasks.update('task_123', { status: 'done' }, { wait: 'confirmed' });
@@ -192,10 +192,10 @@ request bodies that may contain customer data.
192
192
 
193
193
  Only these imports are public SemVer surface:
194
194
 
195
- - `@ablo/sync-engine`
196
- - `@ablo/sync-engine/schema`
197
- - `@ablo/sync-engine/react`
198
- - `@ablo/sync-engine/testing`
195
+ - `@abloatai/ablo`
196
+ - `@abloatai/ablo/schema`
197
+ - `@abloatai/ablo/react`
198
+ - `@abloatai/ablo/testing`
199
199
 
200
200
  `dataSource(...)` is exported from the root package for customer-owned storage
201
201
  adapters. Everything outside the four import paths is internal to Ablo-owned
@@ -2,6 +2,11 @@
2
2
 
3
3
  Every schema model has a backing store.
4
4
 
5
+ Customer apps must define an Ablo schema. The schema is the contract between
6
+ the SDK, agents, realtime subscriptions, and the Data Source endpoint. Use
7
+ `defineSchema`, `model`, and Zod the same way a Prisma project starts with a
8
+ `schema.prisma`.
9
+
5
10
  By default, Ablo stores the rows for the models you declare. That makes Ablo the
6
11
  managed state store for those resources, the same way Stripe stores `Customer`
7
12
  and `PaymentIntent` objects that you create through Stripe's API.
@@ -10,10 +15,15 @@ If you already have application tables and want those tables to remain
10
15
  canonical, attach a Data Source. Then Ablo coordinates the write and calls your
11
16
  app to commit it.
12
17
 
18
+ Your app can keep using its own `DATABASE_URL`. Store that value in your app or
19
+ backend environment, not in Ablo. The integration boundary is the HTTPS
20
+ endpoint your app exposes. The happy path uses the same server-side
21
+ `ABLO_API_KEY` to verify Ablo calls.
22
+
13
23
  Use the SDK with an API key:
14
24
 
15
25
  ```ts
16
- import Ablo from '@ablo/sync-engine';
26
+ import Ablo from '@abloatai/ablo';
17
27
  import { schema } from './ablo.schema';
18
28
 
19
29
  export const ablo = Ablo({
@@ -24,6 +34,16 @@ export const ablo = Ablo({
24
34
 
25
35
  Do not pass a database URL to `Ablo(...)`.
26
36
 
37
+ For the first production integration, prefer this shape:
38
+
39
+ ```bash
40
+ # Stored only in your app/backend
41
+ DATABASE_URL=postgres://...
42
+
43
+ # The only Ablo credential in the customer app
44
+ ABLO_API_KEY=sk_live_...
45
+ ```
46
+
27
47
  ## Backing Modes
28
48
 
29
49
  | Mode | Where rows live | What `create/update/delete` does | Use when |
@@ -62,33 +82,30 @@ When you add a Data Source in Ablo, you get:
62
82
 
63
83
  | Field | Purpose |
64
84
  |---|---|
65
- | Data Source URL | The public HTTPS route in your app that Ablo will call. |
66
- | Signing secret | Stored in your app as `ABLO_DATA_SOURCE_SIGNING_SECRET`; used to verify Ablo calls. |
67
- | Push events URL | Ablo endpoint your app can call when rows change outside Ablo. |
85
+ | Data Source endpoint | The public HTTPS endpoint in your app that Ablo calls. |
86
+ | API key | Stored in your app as `ABLO_API_KEY`; used by the SDK and the Data Source endpoint. |
87
+ | External-write feed | Optional `events` handler on the same Data Source endpoint. |
68
88
  | Status | Last successful request, last error, and delivery attempts. |
69
89
 
70
90
  The shape is the same as a production webhook integration:
71
91
 
72
- 1. Add a Data Source URL in Ablo.
73
- 2. Store the signing secret in your app.
74
- 3. Expose one signed HTTP route from your app.
92
+ 1. Expose one Data Source endpoint in your app.
93
+ 2. Store `ABLO_API_KEY` in your app.
94
+ 3. Verify signed HTTP calls before opening a database transaction.
75
95
  4. Keep your database credentials in your app.
76
-
77
- ```bash
78
- ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
79
- ```
96
+ 5. Write an outbox row when data changes outside Ablo.
80
97
 
81
98
  ## Route
82
99
 
83
100
  ```ts
84
101
  // app/api/ablo/source/route.ts
85
- import { dataSource } from '@ablo/sync-engine';
102
+ import { dataSource } from '@abloatai/ablo';
86
103
  import { schema } from '@/ablo.schema';
87
104
  import { db } from '@/db';
88
105
 
89
106
  export const POST = dataSource({
90
107
  schema,
91
- signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
108
+ apiKey: process.env.ABLO_API_KEY,
92
109
 
93
110
  authorize() {
94
111
  return { db };
@@ -176,7 +193,7 @@ handler so connected humans and agents stay current:
176
193
  ```ts
177
194
  export const POST = dataSource({
178
195
  schema,
179
- signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
196
+ apiKey: process.env.ABLO_API_KEY,
180
197
 
181
198
  async events({ cursor, limit, context }) {
182
199
  const page = await context.auth.db.outbox.after(cursor, { limit });
@@ -201,14 +218,31 @@ export const POST = dataSource({
201
218
  `clientTxId` lets Ablo drop SDK echoes that already produced a realtime update.
202
219
  Events without `clientTxId` are treated as external writes.
203
220
 
221
+ ## Production Checklist
222
+
223
+ Before using a customer-owned database in production:
224
+
225
+ - Keep `DATABASE_URL` in the customer app or backend environment.
226
+ - Use only the Data Source endpoint and `ABLO_API_KEY` as the customer-facing integration boundary.
227
+ - Verify signatures before opening a database transaction.
228
+ - Store `clientTxId` in an idempotency table before applying writes.
229
+ - Return canonical rows after each commit.
230
+ - Write outbox events in the same transaction as non-Ablo writes.
231
+ - Dedupe outbox events by event `id`.
232
+ - Monitor last success, last error, retry count, event lag, and cursor.
233
+
234
+ Do not send the customer's database URL to Ablo for this path. Direct database
235
+ URL custody would be a separate connector product with encrypted secret storage,
236
+ rotation, least-privilege roles, connection limits, table allowlists, and clear
237
+ data-processing terms.
238
+
204
239
  ## Security
205
240
 
206
- - Verify requests with `ABLO_DATA_SOURCE_SIGNING_SECRET`.
241
+ - Verify requests with `ABLO_API_KEY`.
207
242
  - Keep database credentials in your app.
208
243
  - Dedupe commits by `clientTxId`.
209
244
  - Dedupe external events by event `id`.
210
245
  - Use HTTPS in production.
211
246
 
212
- The signing secret is not a database credential and does not give Ablo access to
213
- your database. It only lets your route verify that the request came from Ablo
214
- and was not modified in transit.
247
+ The API key is not a database credential. It only lets your route verify that
248
+ the request came from Ablo and was not modified in transit.
@@ -18,8 +18,8 @@ Use the same schema client the app uses. The worker loads the task, checks activ
18
18
  intents, and writes through `ablo.tasks.update(...)`.
19
19
 
20
20
  ```ts
21
- import Ablo from '@ablo/sync-engine';
22
- import { defineSchema, model, z } from '@ablo/sync-engine/schema';
21
+ import Ablo from '@abloatai/ablo';
22
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
23
23
 
24
24
  const schema = defineSchema({
25
25
  tasks: model({
@@ -58,7 +58,7 @@ not the first integration path.
58
58
  ```tsx
59
59
  'use client';
60
60
 
61
- import { useAblo } from '@ablo/sync-engine/react';
61
+ import { useAblo } from '@abloatai/ablo/react';
62
62
 
63
63
  export function TaskRow({ task: serverTask }: Props) {
64
64
  const data = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
@@ -3,8 +3,8 @@
3
3
  Use AI SDK for the loop and Ablo for the state boundary inside the tool.
4
4
 
5
5
  ```ts
6
- import Ablo from '@ablo/sync-engine';
7
- import { defineSchema, model, z as schemaZ } from '@ablo/sync-engine/schema';
6
+ import Ablo from '@abloatai/ablo';
7
+ import { defineSchema, model, z as schemaZ } from '@abloatai/ablo/schema';
8
8
  import { streamText, tool } from 'ai';
9
9
  import { z } from 'zod';
10
10
 
@@ -34,38 +34,21 @@ const updateTask = tool({
34
34
  const [task] = await ablo.tasks.load({ where: { id: taskId } });
35
35
  if (!task) return { ok: false, reason: 'not_found' };
36
36
 
37
- const busy = ablo.intents.list({ resource: 'tasks', id: taskId });
38
- if (busy.length > 0) {
39
- await ablo.intents.waitFor(
40
- { resource: 'tasks', id: taskId },
41
- { timeout: 30_000 },
42
- );
43
- }
44
-
45
- const snap = ablo.snapshot({ tasks: taskId });
46
- const intent = await ablo.intents.create({
47
- target: { resource: 'tasks', id: taskId, field: 'status' },
48
- action: 'update',
49
- });
37
+ const claim = ablo.tasks.intent(taskId);
38
+ if (claim.current) await claim.settled({ timeout: 30_000 });
50
39
 
40
+ await claim.acquire({ action: 'editing', field: 'status', ttl: '2m' });
51
41
  try {
52
- const updated = await ablo.tasks.update(
53
- taskId,
54
- {
55
- status: status ?? task.status,
56
- summary: summary ?? task.summary,
57
- },
58
- {
59
- intent,
60
- readAt: snap.stamp,
61
- onStale: 'reject',
62
- wait: 'confirmed',
63
- },
64
- );
42
+ // update commits with the held claim and auto-releases on success
43
+ const updated = await claim.update({
44
+ status: status ?? task.status,
45
+ summary: summary ?? task.summary,
46
+ });
65
47
 
66
48
  return { ok: true, task: updated };
67
- } finally {
68
- await intent.release();
49
+ } catch (err) {
50
+ await claim.release();
51
+ throw err;
69
52
  }
70
53
  },
71
54
  });
@@ -85,8 +68,8 @@ The important part is not the model provider. The important part is that the
85
68
  tool:
86
69
 
87
70
  - loads the latest task,
88
- - checks active intent,
89
- - writes with `readAt`,
90
- - rejects stale state,
71
+ - waits if another participant already holds the row,
72
+ - acquires a claim before writing,
73
+ - writes and auto-releases through the same handle,
91
74
  - waits for server confirmation.
92
75
 
@@ -27,7 +27,7 @@ Create a schema for the records that need realtime coordination.
27
27
 
28
28
  ```ts
29
29
  // web/ablo.schema.ts
30
- import { defineSchema, model, z } from '@ablo/sync-engine/schema';
30
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
31
31
 
32
32
  export const schema = defineSchema({
33
33
  tasks: model({
@@ -41,7 +41,7 @@ export const schema = defineSchema({
41
41
 
42
42
  ```ts
43
43
  // web/ablo.ts
44
- import Ablo from '@ablo/sync-engine';
44
+ import Ablo from '@abloatai/ablo';
45
45
  import { schema } from './ablo.schema';
46
46
 
47
47
  export const ablo = Ablo({
@@ -57,7 +57,7 @@ model resources without importing server credentials.
57
57
  // web/app/providers.tsx
58
58
  'use client';
59
59
 
60
- import { AbloProvider } from '@ablo/sync-engine/react';
60
+ import { AbloProvider } from '@abloatai/ablo/react';
61
61
  import { schema } from '@/ablo.schema';
62
62
 
63
63
  export function Providers({ children }: { children: React.ReactNode }) {
@@ -73,7 +73,7 @@ subscribe to the same model resource Ablo writes through.
73
73
  ```tsx
74
74
  'use client';
75
75
 
76
- import { useAblo } from '@ablo/sync-engine/react';
76
+ import { useAblo } from '@abloatai/ablo/react';
77
77
 
78
78
  export function TaskRow({ task: serverTask }: { task: Task }) {
79
79
  const task = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
@@ -95,16 +95,16 @@ No string model key is needed in the first example. The selector reads from
95
95
 
96
96
  ## 3. Add One Python Data Source Endpoint
97
97
 
98
- In Ablo, configure the Data Source URL:
98
+ Expose one customer-owned Data Source endpoint:
99
99
 
100
100
  ```txt
101
101
  https://api.example.com/api/ablo/source
102
102
  ```
103
103
 
104
- Store the signing secret in the Python server:
104
+ Store the Ablo API key in the Python server:
105
105
 
106
106
  ```bash
107
- ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
107
+ ABLO_API_KEY=sk_live_...
108
108
  ```
109
109
 
110
110
  Then expose one route that verifies the signed request and calls the existing
@@ -126,7 +126,7 @@ router = APIRouter()
126
126
 
127
127
 
128
128
  def verify_ablo_signature(request: Request, raw_body: bytes) -> None:
129
- secret = os.environ["ABLO_DATA_SOURCE_SIGNING_SECRET"].encode()
129
+ api_key = os.environ["ABLO_API_KEY"].encode()
130
130
  message_id = request.headers.get("webhook-id")
131
131
  timestamp = request.headers.get("webhook-timestamp")
132
132
  signature_header = request.headers.get("webhook-signature", "")
@@ -135,12 +135,12 @@ def verify_ablo_signature(request: Request, raw_body: bytes) -> None:
135
135
  raise HTTPException(status_code=401, detail="missing signature")
136
136
 
137
137
  signed_at = int(timestamp)
138
- if abs(int(time.time() * 1000) - signed_at) > 5 * 60 * 1000:
138
+ if abs(int(time.time()) - signed_at) > 5 * 60:
139
139
  raise HTTPException(status_code=401, detail="expired signature")
140
140
 
141
141
  payload = message_id.encode() + b"." + timestamp.encode() + b"." + raw_body
142
142
  expected = base64.b64encode(
143
- hmac.new(secret, payload, hashlib.sha256).digest()
143
+ hmac.new(api_key, payload, hashlib.sha256).digest()
144
144
  ).decode()
145
145
 
146
146
  presented = [
@@ -64,7 +64,7 @@ rejects. The action can re-fetch and ask the user to retry.
64
64
  ```tsx
65
65
  'use client';
66
66
 
67
- import { useAblo } from '@ablo/sync-engine/react';
67
+ import { useAblo } from '@abloatai/ablo/react';
68
68
 
69
69
  export function TaskEditor({ task: serverTask }: Props) {
70
70
  const data = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
@@ -4,8 +4,8 @@ Most server agents should import the app schema and use the same model methods
4
4
  as the product UI.
5
5
 
6
6
  ```ts
7
- import Ablo from '@ablo/sync-engine';
8
- import { defineSchema, model, z } from '@ablo/sync-engine/schema';
7
+ import Ablo from '@abloatai/ablo';
8
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
9
9
 
10
10
  const schema = defineSchema({
11
11
  tasks: model({
package/docs/index.md CHANGED
@@ -87,7 +87,7 @@ query-shaped sync).
87
87
 
88
88
  ## Runtime builds
89
89
 
90
- - `@ablo/sync-engine` — schema-powered sync client for typed model operations, realtime, intents, and receipts.
90
+ - `@abloatai/ablo` — schema-powered sync client for typed model operations, realtime, intents, and receipts.
91
91
  - `Ablo({ apiKey })` — advanced resource client for runtimes that intentionally cannot import a schema.
92
92
 
93
93
  ## More
@@ -40,8 +40,8 @@ together, behind one client.
40
40
  The normal integration is one client:
41
41
 
42
42
  ```ts
43
- import Ablo from '@ablo/sync-engine';
44
- import { defineSchema, model, z } from '@ablo/sync-engine/schema';
43
+ import Ablo from '@abloatai/ablo';
44
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
45
45
  ```
46
46
 
47
47
  Declare the models Ablo coordinates, then read and write through
@@ -66,8 +66,8 @@ Every schema model has a backing store. The SDK call shape stays the same.
66
66
  | Schema-less resource API | Custom runtime | A server worker, MCP route, or migration script intentionally cannot import the app schema. |
67
67
 
68
68
  Do not pass a database URL to `Ablo(...)`. Application and agent code use
69
- `ABLO_API_KEY`. If your database stays canonical, add a Data Source URL in Ablo
70
- and keep the database credentials inside your app.
69
+ `ABLO_API_KEY`. If your database stays canonical, expose a signed Data Source
70
+ endpoint from your app and keep the database credentials inside your app.
71
71
 
72
72
  ## Test With Sandboxes
73
73
 
@@ -106,7 +106,7 @@ offline-heavy local cache behavior.
106
106
 
107
107
  ```ts
108
108
  // src/ablo.schema.ts
109
- import { defineSchema, model, z } from '@ablo/sync-engine/schema';
109
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
110
110
 
111
111
  export const schema = defineSchema(
112
112
  {
@@ -176,7 +176,7 @@ Trusted runtimes can use `ABLO_API_KEY`.
176
176
 
177
177
  ```ts
178
178
  // src/ablo.ts
179
- import Ablo from '@ablo/sync-engine';
179
+ import Ablo from '@abloatai/ablo';
180
180
  import { schema } from './ablo.schema';
181
181
 
182
182
  export const ablo = Ablo({
@@ -192,7 +192,7 @@ server API key in the bundle.
192
192
  // app/providers.tsx
193
193
  'use client';
194
194
 
195
- import { AbloProvider } from '@ablo/sync-engine/react';
195
+ import { AbloProvider } from '@abloatai/ablo/react';
196
196
  import { schema } from '@/ablo.schema';
197
197
 
198
198
  export function Providers({ children }: { children: React.ReactNode }) {
@@ -259,7 +259,7 @@ In React, selector `useAblo` is the public read API:
259
259
  ```tsx
260
260
  'use client';
261
261
 
262
- import { useAblo } from '@ablo/sync-engine/react';
262
+ import { useAblo } from '@abloatai/ablo/react';
263
263
 
264
264
  export function TaskRow({ task: serverTask }: { task: Task }) {
265
265
  const task = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
@@ -369,13 +369,13 @@ Use a Data Source when your app database remains the source of truth.
369
369
 
370
370
  ```ts
371
371
  // app/api/ablo/source/route.ts
372
- import { dataSource } from '@ablo/sync-engine';
372
+ import { dataSource } from '@abloatai/ablo';
373
373
  import { schema } from '@/ablo.schema';
374
374
  import { db } from '@/db';
375
375
 
376
376
  export const POST = dataSource({
377
377
  schema,
378
- signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
378
+ apiKey: process.env.ABLO_API_KEY,
379
379
 
380
380
  authorize() {
381
381
  return { db };
@@ -404,14 +404,15 @@ export const POST = dataSource({
404
404
  });
405
405
  ```
406
406
 
407
- Ablo gives you a Data Source URL, a signing secret, push/events URL, and status.
408
- Your app stores:
407
+ Ablo needs your Data Source endpoint and API key. External writes can be
408
+ reported through an optional `events` handler on the same route. Your app
409
+ stores one Ablo credential:
409
410
 
410
411
  ```bash
411
- ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
412
+ ABLO_API_KEY=sk_live_...
412
413
  ```
413
414
 
414
- The signing secret verifies Ablo's request. It is not a database credential.
415
+ The API key verifies Ablo's request. It is not a database credential.
415
416
 
416
417
  ## 8. Agents
417
418
 
@@ -20,7 +20,7 @@ Capability -> Task -> Usage
20
20
  |---|---|---|
21
21
  | `Schema` | State | Declares typed models the app and agents can read and write. |
22
22
  | `Model` | State | The generated `ablo.<model>` resource. Use `load`, `retrieve`, `create`, `update`, and `delete`. |
23
- | `Intent` | State | Pre-write coordination. It says what this actor is preparing to change. |
23
+ | `Intent` | Coordination | Who is working on a target, as one Stripe-shaped object with a single `status`. Opened and read through `ablo.<model>.intent(id)`. Ephemeral — never persisted. |
24
24
  | `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
25
25
  | `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
26
26
  | `Capability` | Control | Signed credentials. It says who can do what, where, for how long, and on whose behalf. |
@@ -110,9 +110,21 @@ Removing any one of the three leaves a class of attack uncovered. The pattern ma
110
110
 
111
111
  ## Coordination
112
112
 
113
- Intents broadcast across the org. When `agent:task-writer` declares an intent to
114
- update a task, schema clients can see it through `ablo.intents.list(...)` or the
115
- live intent stream. Callers decide whether to yield, wait, or fail fast.
113
+ Intents broadcast across the org. Open and read one on any row through the
114
+ model accessor:
115
+
116
+ ```ts
117
+ const task = ablo.tasks.intent('task_123');
118
+ if (task.current) await task.settled(); // someone's working — wait
119
+ await task.acquire({ action: 'editing' }); // claim so others yield
120
+ await task.update({ status: 'done' }); // commits + releases
121
+ ```
122
+
123
+ `task.current` is the live `Intent` (or `null`); `task.settled()` resolves when
124
+ the holder finishes. The same signal is visible to every schema client through
125
+ `ablo.intents.list(...)` and the live intent stream, so callers decide whether
126
+ to yield, wait, or fail fast. See [API → Intent](./api.md#intent) for the object
127
+ reference and lifecycle.
116
128
 
117
129
  ## Conflict resolution
118
130
 
package/docs/mcp.md CHANGED
@@ -21,7 +21,7 @@ Each resource you declare becomes one or more MCP tools:
21
21
  | `retrieve` | `<resource>.retrieve` | Returns the row + a stamp. |
22
22
  | `list` | `<resource>.list` | Cursor-paginated discovery. |
23
23
  | `update` | `<resource>.update` | Write, requires the prior stamp. |
24
- | `intents.create` | `intent.create` | Declare a claim before writing. |
24
+ | `<model>.intent` | `intent.create` | Claim a row before writing, then auto-release on update. |
25
25
 
26
26
  The assistant gets typed JSON schemas, real argument types, and typed
27
27
  rejections when it writes stale state. No invention, no hallucinated IDs.