@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.
- package/CHANGELOG.md +43 -5
- package/NOTICE +2 -2
- package/README.md +30 -28
- package/dist/agent/Agent.d.ts +1 -1
- package/dist/agent/Agent.js +1 -1
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +6 -6
- package/dist/agent/types.d.ts +1 -1
- package/dist/ai-sdk/index.d.ts +3 -3
- package/dist/ai-sdk/index.js +3 -3
- package/dist/ai-sdk/intent-broadcast.d.ts +1 -1
- package/dist/ai-sdk/intent-broadcast.js +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/client/Ablo.d.ts +8 -14
- package/dist/client/Ablo.js +32 -1
- package/dist/client/auth.d.ts +3 -3
- package/dist/client/auth.js +5 -5
- package/dist/client/createModelProxy.d.ts +110 -32
- package/dist/client/createModelProxy.js +77 -38
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +2 -2
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +9 -9
- package/dist/interfaces/headless.d.ts +1 -1
- package/dist/interfaces/headless.js +2 -2
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +2 -2
- package/dist/principal.d.ts +1 -1
- package/dist/principal.js +1 -1
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/SyncGroupProvider.js +1 -1
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutate.d.ts +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useReader.d.ts +1 -1
- package/dist/schema/field.d.ts +1 -1
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/model.d.ts +2 -2
- package/dist/schema/model.js +2 -2
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -1
- package/dist/schema/relation.d.ts +1 -1
- package/dist/schema/relation.js +1 -1
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/source/index.d.ts +22 -28
- package/dist/source/index.js +23 -20
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +2 -2
- package/dist/sync/SyncWebSocket.d.ts +14 -0
- package/dist/sync/createIntentStream.js +7 -0
- package/dist/testing/fixtures/models.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -1
- package/dist/testing/helpers/react-wrapper.d.ts +2 -2
- package/dist/testing/helpers/react-wrapper.js +2 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/types/streams.d.ts +39 -0
- package/docs/api-keys.md +2 -2
- package/docs/api.md +81 -23
- package/docs/capabilities.md +1 -1
- package/docs/client-behavior.md +7 -7
- package/docs/data-sources.md +52 -18
- package/docs/examples/agent-human.md +3 -3
- package/docs/examples/ai-sdk-tool.md +16 -33
- package/docs/examples/existing-python-backend.md +10 -10
- package/docs/examples/nextjs.md +1 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/index.md +1 -1
- package/docs/integration-guide.md +15 -14
- package/docs/interaction-model.md +16 -4
- package/docs/mcp.md +1 -1
- package/docs/quickstart.md +23 -21
- package/docs/react.md +3 -3
- package/docs/roadmap.md +1 -1
- package/examples/README.md +3 -3
- package/examples/data-source/README.md +1 -1
- package/examples/data-source/ablo-driver.ts +5 -5
- package/examples/data-source/customer-server.ts +10 -10
- package/examples/data-source/run.ts +9 -11
- package/examples/data-source/schema.ts +1 -1
- package/examples/quickstart.ts +2 -2
- package/llms.txt +8 -8
- 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
|
|
11
|
-
import { defineSchema, model, z } from '@ablo/
|
|
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
|
|
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
|
|
115
|
-
target: { resource: 'tasks', id: 'task_123', field: 'status' },
|
|
116
|
-
action: 'update',
|
|
117
|
-
});
|
|
165
|
+
const task = ablo.tasks.intent('task_123');
|
|
118
166
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
`
|
|
134
|
-
`
|
|
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
|
|
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
|
-
|
|
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: [] };
|
package/docs/capabilities.md
CHANGED
package/docs/client-behavior.md
CHANGED
|
@@ -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
|
|
9
|
-
import { defineSchema, model, z } from '@ablo/
|
|
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
|
|
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
|
|
196
|
-
- `@ablo/
|
|
197
|
-
- `@ablo/
|
|
198
|
-
- `@ablo/
|
|
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
|
package/docs/data-sources.md
CHANGED
|
@@ -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
|
|
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
|
|
66
|
-
|
|
|
67
|
-
|
|
|
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.
|
|
73
|
-
2. Store
|
|
74
|
-
3.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
213
|
-
|
|
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
|
|
22
|
-
import { defineSchema, model, z } from '@ablo/
|
|
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/
|
|
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
|
|
7
|
-
import { defineSchema, model, z as schemaZ } from '@ablo/
|
|
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
|
|
38
|
-
if (
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
}
|
|
68
|
-
await
|
|
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
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
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/
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
|
104
|
+
Store the Ablo API key in the Python server:
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
|
|
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
|
-
|
|
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()
|
|
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(
|
|
143
|
+
hmac.new(api_key, payload, hashlib.sha256).digest()
|
|
144
144
|
).decode()
|
|
145
145
|
|
|
146
146
|
presented = [
|
package/docs/examples/nextjs.md
CHANGED
|
@@ -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/
|
|
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
|
|
8
|
-
import { defineSchema, model, z } from '@ablo/
|
|
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
|
|
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
|
|
44
|
-
import { defineSchema, model, z } from '@ablo/
|
|
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,
|
|
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/
|
|
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
|
|
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/
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
408
|
-
Your app
|
|
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
|
-
|
|
412
|
+
ABLO_API_KEY=sk_live_...
|
|
412
413
|
```
|
|
413
414
|
|
|
414
|
-
The
|
|
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` |
|
|
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.
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
| `
|
|
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.
|