@abloatai/ablo 0.7.0 → 0.8.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 +32 -0
- package/README.md +54 -45
- package/dist/BaseSyncedStore.js +7 -3
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +3 -2
- package/dist/auth/index.js +39 -11
- package/dist/client/Ablo.d.ts +111 -3
- package/dist/client/Ablo.js +143 -10
- package/dist/client/ApiClient.d.ts +32 -0
- package/dist/client/ApiClient.js +76 -44
- package/dist/client/auth.d.ts +11 -1
- package/dist/client/auth.js +21 -2
- package/dist/client/createModelProxy.d.ts +107 -63
- package/dist/client/createModelProxy.js +65 -33
- package/dist/client/identity.js +14 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +57 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- package/dist/errorCodes.d.ts +23 -1
- package/dist/errorCodes.js +34 -1
- package/dist/errors.d.ts +52 -1
- package/dist/errors.js +140 -42
- package/dist/index.d.ts +9 -5
- package/dist/index.js +9 -5
- package/dist/keys/index.d.ts +61 -0
- package/dist/keys/index.js +151 -0
- package/dist/query/client.js +19 -8
- package/dist/react/AbloProvider.d.ts +25 -0
- package/dist/react/AbloProvider.js +97 -2
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/serialize.d.ts +3 -3
- package/dist/schema/serialize.js +2 -2
- package/dist/sync/BootstrapHelper.js +46 -27
- package/dist/sync/ConnectionManager.d.ts +3 -1
- package/dist/sync/ConnectionManager.js +37 -1
- package/dist/sync/HydrationCoordinator.js +3 -2
- package/dist/sync/NetworkProbe.d.ts +8 -0
- package/dist/sync/NetworkProbe.js +24 -2
- package/dist/sync/SyncWebSocket.d.ts +1 -1
- package/dist/sync/SyncWebSocket.js +43 -53
- package/dist/sync/participants.js +5 -2
- package/dist/transactions/TransactionQueue.js +13 -1
- package/docs/api-keys.md +5 -5
- package/docs/api.md +101 -44
- package/docs/audit.md +16 -9
- package/docs/cli.md +27 -17
- package/docs/client-behavior.md +34 -20
- package/docs/coordination.md +40 -51
- package/docs/data-sources.md +21 -19
- package/docs/examples/agent-human.md +72 -28
- package/docs/examples/ai-sdk-tool.md +14 -11
- package/docs/examples/existing-python-backend.md +27 -16
- package/docs/examples/nextjs.md +21 -8
- package/docs/examples/scoped-agent.md +42 -27
- package/docs/examples/server-agent.md +27 -5
- package/docs/guarantees.md +26 -17
- package/docs/identity.md +65 -59
- package/docs/index.md +30 -19
- package/docs/integration-guide.md +52 -52
- package/docs/interaction-model.md +38 -26
- package/docs/mcp/claude-code.md +9 -17
- package/docs/mcp/cursor.md +6 -24
- package/docs/mcp/windsurf.md +6 -19
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +31 -39
- package/docs/react.md +15 -11
- package/docs/roadmap.md +13 -13
- package/docs/schema-contract.md +109 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +6 -2
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +27 -16
- package/package.json +6 -1
package/docs/identity.md
CHANGED
|
@@ -30,65 +30,16 @@ A **sync group** is a named channel of shared state — a string like
|
|
|
30
30
|
There is no built-in `org` / `team` / `user` concept in the engine. Those are
|
|
31
31
|
*your* domain words. Ablo only knows sync-group strings. The mapping from "this
|
|
32
32
|
is user U in org O" to "they may subscribe to `org:acme` and `user:U`" is
|
|
33
|
-
something **you declare in your schema
|
|
34
|
-
|
|
35
|
-
### Two kinds of group — the whole mental model
|
|
36
|
-
|
|
37
|
-
Every sync group is named after one of two things, and that's the cleanest way
|
|
38
|
-
to hold the model in your head:
|
|
39
|
-
|
|
40
|
-
- **Membership groups** — named after *who you are*: `org:{id}`, `team:{id}`,
|
|
41
|
-
`user:{id}`. Produced from **identity** (`identityRoles`, Half 1). They're
|
|
42
|
-
standing and durable — they don't change as you work.
|
|
43
|
-
- **Entity groups** — named after *a thing*: `dataroom:{id}`, `deck:{id}`,
|
|
44
|
-
`slide:{id}`. Produced from a **row's id** (a model's entity scope, Half 2).
|
|
45
|
-
They're granular — one per record — and any participant can be pointed at a
|
|
46
|
-
specific set of them.
|
|
47
|
-
|
|
48
|
-
Humans and agents fill that same space differently — and, crucially, the two
|
|
49
|
-
are **declared in different places**, because membership is static (a property of
|
|
50
|
-
identity) while entity scope is dynamic (a property of the task):
|
|
51
|
-
|
|
52
|
-
| | Subscribed by | Declared where | Gets |
|
|
53
|
-
| --- | --- | --- | --- |
|
|
54
|
-
| **Human** | *who they are* — membership | **the schema** (`identityRoles`) — a rule, written once | every `org` / `team` / `user` group their identity implies — their whole standing world |
|
|
55
|
-
| **Agent** | *what it's been given* — entities | **code, at the spawn site** — chosen per run | a handful of entity groups: the dataroom it's in, the slides it has read — never beyond what its user's membership could reach |
|
|
56
|
-
|
|
57
|
-
> **One line:** humans subscribe by who they are; agents subscribe by what
|
|
58
|
-
> they've been given.
|
|
59
|
-
|
|
60
|
-
The asymmetry is the point. A user's org/team/user don't change per request, so
|
|
61
|
-
their scope is a **rule the schema derives automatically** — you never write
|
|
62
|
-
per-user scope code. An agent's reach depends on *what it's working on*, which is
|
|
63
|
-
only knowable at dispatch — so you pass its `syncGroups` **at the call site, in
|
|
64
|
-
code**. The schema's only job for entities is to declare *that* a model is
|
|
65
|
-
entity-scopable and *what its group is named* (`scope: 'deck'` → `deck:{id}`);
|
|
66
|
-
it never declares *which* entities a given agent gets. (A human can opt into the
|
|
67
|
-
same runtime narrowing — a page scoped to one deck — but by default a human's
|
|
68
|
-
scope is fully schema-derived.)
|
|
69
|
-
|
|
70
|
-
So an agent doesn't need a `user:{id}` standing grant. It's a participant pointed
|
|
71
|
-
at a few entity groups, bounded above by its triggering user's membership. That
|
|
72
|
-
boundary is the whole safety story, and it's covered in
|
|
73
|
-
[Agents are participants too](#agents-are-participants-too).
|
|
74
|
-
|
|
75
|
-
```txt
|
|
76
|
-
your auth → identity { kind, userId|agentId, organizationId, teamIds }
|
|
77
|
-
→ identityRoles (schema) → allowed sync groups
|
|
78
|
-
→ participant receives deltas for rows in those groups
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
The identity is a **participant** — and a participant is either a human
|
|
82
|
-
(`kind: 'user'`) or an agent (`kind: 'agent'`). Same shape, same path; see
|
|
83
|
-
[Agents are participants too](#agents-are-participants-too) below. Everything in
|
|
84
|
-
the next two sections applies to both.
|
|
33
|
+
something **you declare in your schema**. Here is that whole declaration in one
|
|
34
|
+
runnable place, so the concepts below have code to attach to.
|
|
85
35
|
|
|
86
36
|
## Declare it, end to end
|
|
87
37
|
|
|
88
38
|
The entire declaration surface is: `identityRoles` (who may see what), and on
|
|
89
39
|
each model `scope` / `parent` / `grants` (which group a row fans out on), plus an
|
|
90
|
-
optional `syncGroups` prop (narrowing).
|
|
91
|
-
|
|
40
|
+
optional `syncGroups` prop (narrowing). Read the three blocks first — a human
|
|
41
|
+
gets their `org` / `team` scope, an agent gets one `deck` — then the sections
|
|
42
|
+
after explain each.
|
|
92
43
|
|
|
93
44
|
```ts
|
|
94
45
|
// 1. src/ablo/schema.ts — map identity → groups, and anchor each model to a group
|
|
@@ -144,6 +95,60 @@ export const schema = defineSchema(
|
|
|
144
95
|
|
|
145
96
|
That's the whole surface. The rest of this doc is the *why* behind each line.
|
|
146
97
|
|
|
98
|
+
## Two kinds of group — the whole mental model
|
|
99
|
+
|
|
100
|
+
You just saw a human get `org` / `team` groups and an agent get one `deck`
|
|
101
|
+
group. That split is the model. Every sync group is named after one of two
|
|
102
|
+
things:
|
|
103
|
+
|
|
104
|
+
- **Membership groups** — named after *who you are*: `org:{id}`, `team:{id}`,
|
|
105
|
+
`user:{id}`. Produced from **identity** (`identityRoles`, Half 1). They're
|
|
106
|
+
standing and durable — they don't change as you work.
|
|
107
|
+
- **Entity groups** — named after *a thing*: `dataroom:{id}`, `deck:{id}`,
|
|
108
|
+
`slide:{id}`. Produced from a **row's id** (a model's entity scope, Half 2).
|
|
109
|
+
They're granular — one per record — and any participant can be pointed at a
|
|
110
|
+
specific set of them.
|
|
111
|
+
|
|
112
|
+
Humans and agents fill that same space differently, and you declare the two in
|
|
113
|
+
different places. A human's groups come from who they are, so you declare them
|
|
114
|
+
once in the schema. An agent's groups come from what it's working on right now,
|
|
115
|
+
so you pass them in code when you start the run.
|
|
116
|
+
|
|
117
|
+
| | Subscribed by | Declared where | Gets |
|
|
118
|
+
| --- | --- | --- | --- |
|
|
119
|
+
| **Human** | *who they are* — membership | **the schema** (`identityRoles`) — a rule, written once | every `org` / `team` / `user` group their identity implies — their whole standing world |
|
|
120
|
+
| **Agent** | *what it's been given* — entities | **code, at the spawn site** — chosen per run | a handful of entity groups: the dataroom it's in, the slides it has read — never beyond what its user's membership could reach |
|
|
121
|
+
|
|
122
|
+
> **One line:** humans subscribe by who they are; agents subscribe by what
|
|
123
|
+
> they've been given.
|
|
124
|
+
|
|
125
|
+
That's why you never write per-user scope code, but you always pass an agent's
|
|
126
|
+
scope at the call site. A user's org/team/user don't change per request, so
|
|
127
|
+
their scope is a **rule the schema derives automatically**. An agent's reach
|
|
128
|
+
depends on *what it's working on*, which is only knowable at dispatch — so you
|
|
129
|
+
pass its `syncGroups` **at the call site, in code**. The schema's only job for
|
|
130
|
+
entities is to declare *that* a model is
|
|
131
|
+
entity-scopable and *what its group is named* (`scope: 'deck'` → `deck:{id}`);
|
|
132
|
+
it never declares *which* entities a given agent gets. (A human can opt into the
|
|
133
|
+
same runtime narrowing — a page scoped to one deck — but by default a human's
|
|
134
|
+
scope is fully schema-derived.)
|
|
135
|
+
|
|
136
|
+
So an agent doesn't need a `user:{id}` standing grant. It's a participant pointed
|
|
137
|
+
at a few entity groups, bounded above by its triggering user's membership. That
|
|
138
|
+
boundary is the whole safety story, and it's covered in
|
|
139
|
+
[Agents are participants too](#agents-are-participants-too).
|
|
140
|
+
|
|
141
|
+
```txt
|
|
142
|
+
your auth → identity { kind, userId|agentId, organizationId, teamIds }
|
|
143
|
+
→ identityRoles (schema) → allowed sync groups
|
|
144
|
+
→ participant receives deltas for rows in those groups
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The identity is a **participant** — and a participant is either a human
|
|
148
|
+
(`kind: 'user'`) or an agent (`kind: 'agent'`). Same shape, same path; see
|
|
149
|
+
[Agents are participants too](#agents-are-participants-too) below. Everything in
|
|
150
|
+
the next two sections applies to both.
|
|
151
|
+
|
|
147
152
|
## The two halves of scoping
|
|
148
153
|
|
|
149
154
|
Scoping is two declarations that meet in the middle. One describes the
|
|
@@ -355,9 +360,10 @@ agent authority = (triggering user's allowed set) ← ceiling, inherited (on-
|
|
|
355
360
|
∩ (the model instances it touches) ← floor, least privilege per run
|
|
356
361
|
```
|
|
357
362
|
|
|
358
|
-
|
|
359
|
-
[Half 2](#half-2--per-model-scope-row--group)
|
|
360
|
-
|
|
363
|
+
Concretely: each model an agent edits declares a `scope`
|
|
364
|
+
([Half 2](#half-2--per-model-scope-row--group)), so each row forms its own
|
|
365
|
+
group. The agent subscribes only to the groups for the rows it touches. Declare
|
|
366
|
+
an entity anchor on the models an agent operates on:
|
|
361
367
|
|
|
362
368
|
```ts
|
|
363
369
|
// each scope-root model an agent edits forms a per-entity group
|
|
@@ -393,8 +399,8 @@ user it ran on behalf of, so audit answers "who did this, and on whose behalf."
|
|
|
393
399
|
It never appears in an `identityRole`, because it changes *who's accountable*,
|
|
394
400
|
not *what's reachable*.
|
|
395
401
|
|
|
396
|
-
|
|
397
|
-
|
|
402
|
+
Three rules make agent access safe, and they fall out of the model above rather
|
|
403
|
+
than needing a separate agent permission system:
|
|
398
404
|
|
|
399
405
|
- **Inherit the user, and no more** — the OAuth
|
|
400
406
|
[on-behalf-of](https://workos.com/blog/oauth-on-behalf-of-ai-agents) model: the
|
package/docs/index.md
CHANGED
|
@@ -1,37 +1,47 @@
|
|
|
1
1
|
# Ablo Docs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
You have a database, and you want an AI agent to edit the same rows your
|
|
4
|
+
users are editing — without the two clobbering each other's work. Ablo gives
|
|
5
|
+
the agent a narrow, audited write path: you declare your models, then everyone
|
|
6
|
+
(React components, server actions, and agents) calls the same
|
|
7
|
+
`ablo.deck.update(...)`. Ablo streams confirmed changes to everyone live and
|
|
8
|
+
rejects any write based on stale data.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
The flow is four steps: declare your models, read the current rows, claim the
|
|
11
|
+
row you're about to change, then write — and the write is rejected if someone
|
|
12
|
+
changed the row first.
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
```ts
|
|
15
|
+
// The same call, whether a person, a server action, or an agent makes it.
|
|
16
|
+
await ablo.deck.update(deckId, { title: "Q3 Strategy" });
|
|
17
|
+
```
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
20
|
+
re-reads the fresh row, then hands it to you — so two writers serialize
|
|
21
|
+
instead of clobbering.
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
query-shaped sync).
|
|
23
|
+
## What you get
|
|
24
|
+
|
|
25
|
+
Three things stay true no matter how you use Ablo:
|
|
20
26
|
|
|
21
27
|
- **One model API for every actor.** `ablo.<model>.update(...)` is the
|
|
22
28
|
call from React components, server actions, background workers, and
|
|
23
29
|
AI agents alike. No separate "agent SDK," no parallel mutation path.
|
|
24
30
|
Attribution comes from the credential, not the call site.
|
|
25
|
-
- **
|
|
31
|
+
- **You declare tenancy scopes once.** Tenancy / per-entity scope
|
|
26
32
|
prefixes (`org:`, `deck:`, or your own `region:` / `customer:`) are
|
|
27
|
-
declared once on the schema's `identityRoles
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
declared once on the schema's `identityRoles`, so application code
|
|
34
|
+
never builds an `org:123` string by hand — which keeps tenant
|
|
35
|
+
boundaries from leaking.
|
|
36
|
+
- **Stale writes are rejected.** If the row changed after you read it,
|
|
37
|
+
your write is turned away instead of silently overwriting the change
|
|
38
|
+
you didn't see.
|
|
30
39
|
|
|
31
40
|
## Start here
|
|
32
41
|
|
|
33
42
|
- [Quickstart](./quickstart.md) — Make your first schema-backed write.
|
|
34
|
-
- [
|
|
43
|
+
- [Schema Contract](./schema-contract.md) — One schema becomes typed model clients, React reads, agent writes, Data Source shape, and schema push.
|
|
44
|
+
- [CLI & Migrations](./cli.md) — `init` / `migrate` / `push` / `generate`, the shared Zod→Postgres type map, and structured migration errors.
|
|
35
45
|
- [Identity & Sync Groups](./identity.md) — Bring your own auth; tell Ablo who's connecting and how org / team / user map to sync-group scope.
|
|
36
46
|
- [Integration Guide](./integration-guide.md) — Choose Ablo-managed state, Data Source, React, multiplayer, and agent patterns.
|
|
37
47
|
- [Guarantees](./guarantees.md) — What confirmed writes, stale checks, and claims guarantee.
|
|
@@ -60,10 +70,11 @@ query-shaped sync).
|
|
|
60
70
|
|
|
61
71
|
## Concepts
|
|
62
72
|
|
|
73
|
+
- [Schema Contract](./schema-contract.md) — What the schema drives across SDK, React, agents, Data Source, and migrations.
|
|
63
74
|
- [Model Methods](./api.md#model-methods) — Load and write typed state.
|
|
64
75
|
- [Integration Guide](./integration-guide.md) — The normal app path and optional pieces.
|
|
65
76
|
- [Guarantees](./guarantees.md) — Confirmed writes, optimistic state, stale-write protection, and agent lifecycle.
|
|
66
|
-
- [Coordination](./coordination.md) — `claim`, `
|
|
77
|
+
- [Coordination](./coordination.md) — `claim`, `claim.state`, and `claim.queue` for active work.
|
|
67
78
|
- [Connect Your Database](./data-sources.md) — Where data lands when your app database is canonical.
|
|
68
79
|
- [Receipt](./api.md#receipt) — Confirm what landed.
|
|
69
80
|
- [Usage](./api.md#usage) — Metering and audit dimensions.
|
|
@@ -1,39 +1,26 @@
|
|
|
1
1
|
# Integration Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
If humans and AI agents both edit the same records in your app, they overwrite
|
|
4
|
+
each other and there's no good place to coordinate. Ablo gives them one shared,
|
|
5
|
+
typed write path — the same `ablo.<model>.update(...)` call for a React
|
|
6
|
+
component, a server action, a background worker, or an agent — and reconciles the
|
|
7
|
+
edits. This guide adds it to a product that already has a backend and database,
|
|
8
|
+
one model at a time.
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Ablo is a sync engine designed from the ground up for **humans and AI
|
|
8
|
-
agents editing the same state at the same time**. That premise drives
|
|
9
|
-
every design choice in this guide; if you only need server-to-server
|
|
10
|
-
data syncing without agents in the loop, the trade-offs land elsewhere
|
|
11
|
-
(Replicache, ElectricSQL, PowerSync are good answers for human-only
|
|
12
|
-
real-time apps; Zero is a good answer for query-shaped sync).
|
|
13
|
-
|
|
14
|
-
The shape of the SDK reflects three commitments:
|
|
10
|
+
Three things hold no matter which actor is writing:
|
|
15
11
|
|
|
16
12
|
- **One model API for every actor.** `ablo.<model>.update(...)` is what
|
|
17
13
|
React components, server actions, background workers, and AI agents
|
|
18
14
|
all call. No separate "agent SDK," no parallel mutation path. The
|
|
19
15
|
attribution comes from the credential, not the call site.
|
|
20
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
API keys protect server-to-server humans. Agents get per-run,
|
|
29
|
-
per-scope, leased credentials with per-request signature verification
|
|
30
|
-
and instant revocation. The 2025-2026 AI-agent auth consensus
|
|
31
|
-
(OAuth 2.1 / MCP, AWS STS, Vault leases, Auth0 Token Vault) converged
|
|
32
|
-
on this shape. Capabilities are Ablo's instance.
|
|
33
|
-
|
|
34
|
-
If you have already built a sync layer or an agent runtime, you know
|
|
35
|
-
what each of those costs. This guide assumes you want them solved once,
|
|
36
|
-
together, behind one client.
|
|
16
|
+
- **You never type `org:123` in client code.** The server derives what each
|
|
17
|
+
caller can see from their authenticated identity, using the `identityRoles`
|
|
18
|
+
you declare once in the schema. The client just names which model and id it
|
|
19
|
+
wants. The `org:` / `user:` / `team:` (or your own `region:` / `customer:`)
|
|
20
|
+
prefixes live in the schema, never in consumer code.
|
|
21
|
+
- **Agents don't use your account API key.** Each agent run gets a short-lived
|
|
22
|
+
credential scoped to just what that run can touch, verified per request and
|
|
23
|
+
revocable instantly. (See the Agents section below for the actual calls.)
|
|
37
24
|
|
|
38
25
|
## The integration in one diagram
|
|
39
26
|
|
|
@@ -49,7 +36,7 @@ Declare the models Ablo coordinates, then read and write through
|
|
|
49
36
|
that same model path.
|
|
50
37
|
|
|
51
38
|
```txt
|
|
52
|
-
schema -> ablo.<model>.
|
|
39
|
+
schema -> ablo.<model>.list(...) -> ablo.<model>.update(...)
|
|
53
40
|
```
|
|
54
41
|
|
|
55
42
|
Commits and receipts exist under the hood. Most apps do not create protocol
|
|
@@ -226,21 +213,28 @@ refreshes before expiry.
|
|
|
226
213
|
|
|
227
214
|
## 3. Read State
|
|
228
215
|
|
|
229
|
-
|
|
216
|
+
Reads come in two flavors, and you pick based on whether you can wait.
|
|
217
|
+
`retrieve(id)` and `list({ where })` hit the server (and hydrate the local
|
|
218
|
+
store) — they're async, so you `await` them. `get(id)`, `getAll({ where })`,
|
|
219
|
+
and `getCount({ where })` read the already-synced local graph synchronously, so
|
|
220
|
+
they're the ones you call in render.
|
|
221
|
+
|
|
222
|
+
Use `retrieve` when the row may not be local yet — it fetches from the server
|
|
223
|
+
and waits.
|
|
230
224
|
|
|
231
225
|
```ts
|
|
232
226
|
await ablo.ready();
|
|
233
227
|
|
|
234
|
-
const
|
|
228
|
+
const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
235
229
|
if (!report) throw new Error('report not found');
|
|
236
230
|
```
|
|
237
231
|
|
|
238
|
-
Use `
|
|
239
|
-
|
|
232
|
+
Use `get`, `getAll`, and `getCount` for synchronous local-graph reads after
|
|
233
|
+
data has synced.
|
|
240
234
|
|
|
241
235
|
```ts
|
|
242
|
-
const report = ablo.weatherReports.
|
|
243
|
-
const activeReports = ablo.weatherReports.
|
|
236
|
+
const report = ablo.weatherReports.get('report_stockholm');
|
|
237
|
+
const activeReports = ablo.weatherReports.getAll({
|
|
244
238
|
where: { projectId: 'proj_123' },
|
|
245
239
|
filter: (report) => report.status !== 'ready',
|
|
246
240
|
orderBy: { updatedAt: 'desc' },
|
|
@@ -260,8 +254,8 @@ export function ReportRow({
|
|
|
260
254
|
}: {
|
|
261
255
|
report: { id: string; location: string; status: string };
|
|
262
256
|
}) {
|
|
263
|
-
const report = useAblo((ablo) => ablo.weatherReports.
|
|
264
|
-
const active = useAblo((ablo) => ablo.weatherReports.
|
|
257
|
+
const report = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
|
|
258
|
+
const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
|
|
265
259
|
|
|
266
260
|
return <button disabled={Boolean(active) || report.status === 'ready'}>{report.location}</button>;
|
|
267
261
|
}
|
|
@@ -342,11 +336,11 @@ The migration can be gradual:
|
|
|
342
336
|
|
|
343
337
|
1. Declare schema for one model, such as `reports`.
|
|
344
338
|
2. Keep existing server loads for first paint.
|
|
345
|
-
3. Add `useAblo((ablo) => ablo.weatherReports.
|
|
339
|
+
3. Add `useAblo((ablo) => ablo.weatherReports.get(id)) ?? serverReport` for live rows.
|
|
346
340
|
4. Add one Data Source endpoint that calls the existing service layer.
|
|
347
341
|
5. Move one mutation button from `fetch('/api/reports/...')` to `ablo.weatherReports.update(...)`.
|
|
348
342
|
6. Add an outbox/events path for writes that still happen outside Ablo.
|
|
349
|
-
7. Let agents use the same `ablo.weatherReports.
|
|
343
|
+
7. Let agents use the same `ablo.weatherReports.list(...)` and `ablo.weatherReports.update(...)`.
|
|
350
344
|
|
|
351
345
|
For the full Python shape, see
|
|
352
346
|
[Existing Python Backend](./examples/existing-python-backend.md).
|
|
@@ -407,8 +401,13 @@ The API key verifies Ablo's request. It is not a database credential.
|
|
|
407
401
|
Agents should use the same model methods as the app when they can import the
|
|
408
402
|
schema.
|
|
409
403
|
|
|
404
|
+
An agent often reads a row, calls an LLM, then writes back — a slow gap during
|
|
405
|
+
which a human might touch the same row. Wrap that work in a claim. Claims don't
|
|
406
|
+
lock. If another writer holds the row, `claim` waits for them, re-reads the
|
|
407
|
+
fresh row, then hands it to you — so two writers serialize instead of clobbering.
|
|
408
|
+
|
|
410
409
|
```ts
|
|
411
|
-
const
|
|
410
|
+
const report = await ablo.weatherReports.retrieve(reportId);
|
|
412
411
|
if (!report) return;
|
|
413
412
|
|
|
414
413
|
await ablo.weatherReports.claim(
|
|
@@ -455,7 +454,7 @@ Keep agent writes on the same schema client surface as the app.
|
|
|
455
454
|
| `/testing` | Test harnesses and deterministic mocks. |
|
|
456
455
|
| `Data Source` | Keep your app database canonical. |
|
|
457
456
|
| `persistence: 'indexeddb'` | Durable browser cache that survives reloads, for apps that need it. |
|
|
458
|
-
| `claim` / `
|
|
457
|
+
| `claim` / `claim.state` / `claim.queue` | Show active work and coordinate before a write. |
|
|
459
458
|
| `snapshot` + `readAt` | Reject writes based on stale state. |
|
|
460
459
|
| `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and read tuning. |
|
|
461
460
|
|
|
@@ -465,16 +464,17 @@ them.
|
|
|
465
464
|
|
|
466
465
|
## Method Cheatsheet
|
|
467
466
|
|
|
468
|
-
| Method | Use it for
|
|
469
|
-
| ---------------------------- |
|
|
470
|
-
| `
|
|
471
|
-
| `
|
|
472
|
-
| `
|
|
473
|
-
| `
|
|
474
|
-
| `
|
|
475
|
-
| `
|
|
476
|
-
| `
|
|
477
|
-
| `
|
|
478
|
-
| `claim(id
|
|
467
|
+
| Method | Use it for |
|
|
468
|
+
| ---------------------------- | --------------------------------------------------------------------------- |
|
|
469
|
+
| `retrieve(id)` | Async read of one row from the server (await it). |
|
|
470
|
+
| `list({ where })` | Async read of many rows from the server (await it). |
|
|
471
|
+
| `get(id)` | Synchronous local read of one synced row (use in render). |
|
|
472
|
+
| `getAll({ where })` | Synchronous local read of many synced rows. |
|
|
473
|
+
| `getCount({ where })` | Synchronous local count of synced 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
|
+
| `claim.state(id)` | See who is currently working on a row (synchronous). |
|
|
478
|
+
| `claim(id, work, options?)` | Wait for your turn, re-read, and hold the row while `work` runs. |
|
|
479
479
|
|
|
480
480
|
Keep first integrations on the model methods above.
|
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
# Interaction Model
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
When a person, a server action, and an AI agent can all write to the same row,
|
|
4
|
+
you need one write path that stops them from clobbering each other. Ablo gives
|
|
5
|
+
you exactly one: load the row, claim it while you work, update it, and wait for
|
|
6
|
+
confirmation. This page walks through that path and the few primitives behind it.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
Here's the whole path in one block — claim a row, update it inside the claim, and
|
|
9
|
+
let the claim release when your callback returns:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
13
|
+
|
|
14
|
+
await ablo.weatherReports.claim('report_stockholm', async (report) => {
|
|
15
|
+
await ablo.weatherReports.update(report.id, { status: 'ready' }, { wait: 'confirmed' });
|
|
16
|
+
});
|
|
8
17
|
```
|
|
9
18
|
|
|
19
|
+
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
20
|
+
re-reads the fresh row, then hands it to you — so two writers serialize instead
|
|
21
|
+
of clobbering.
|
|
22
|
+
|
|
10
23
|
## Primitives
|
|
11
24
|
|
|
12
25
|
| Primitive | Plane | Purpose |
|
|
13
26
|
|---|---|---|
|
|
14
27
|
| `Schema` | State | Declares typed models the app and agents can read and write. |
|
|
15
|
-
| `Model` | State | The generated `ablo.<model>` model. Use `
|
|
16
|
-
| `Claim` | Coordination | Who is working on a target.
|
|
28
|
+
| `Model` | State | The generated `ablo.<model>` model. Use `retrieve`/`list` (async server reads), `get`/`getAll`/`getCount` (synchronous local reads), `create`, `update`, and `delete`. |
|
|
29
|
+
| `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim(id, work)` and read via `ablo.<model>.claim.state(id)`. Ephemeral — never persisted. |
|
|
17
30
|
| `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
|
|
18
31
|
| `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
|
|
19
32
|
|
|
20
33
|
### Why each primitive is separate
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
you over that minimum:
|
|
35
|
+
Why are `Claim`, `Commit`, and `Receipt` separate things instead of one? Each
|
|
36
|
+
does a job the others can't. If you're coming from Replicache or Yjs you'd
|
|
37
|
+
expect just `Commit`; here's what the other two buy you over that minimum:
|
|
26
38
|
|
|
27
39
|
- **`Claim` is not a read lock.** Reads stay open. Claims serialize
|
|
28
40
|
acting-on-the-row, so slow work can wait in FIFO order, re-read, and write
|
|
@@ -33,45 +45,45 @@ you over that minimum:
|
|
|
33
45
|
client. A status code can't be re-read by a sub-agent that wasn't on
|
|
34
46
|
the original call.
|
|
35
47
|
|
|
36
|
-
The shape is borrowed from systems that learned the cost of collapse:
|
|
37
|
-
coordination from operational-transform CRDTs and Linear's optimistic
|
|
38
|
-
multiplayer model, and receipts from durable write protocols.
|
|
39
|
-
|
|
40
48
|
## Run Loop
|
|
41
49
|
|
|
42
50
|
A normal schema-backed run is:
|
|
43
51
|
|
|
44
|
-
```
|
|
45
|
-
const
|
|
46
|
-
const active = ablo.weatherReports.
|
|
52
|
+
```ts
|
|
53
|
+
const report = await ablo.weatherReports.retrieve(id);
|
|
54
|
+
const active = ablo.weatherReports.claim.state(id);
|
|
47
55
|
await ablo.weatherReports.claim(id, async (report) => {
|
|
48
56
|
await ablo.weatherReports.update(report.id, patch, { wait: 'confirmed' });
|
|
49
57
|
});
|
|
50
58
|
```
|
|
51
59
|
|
|
60
|
+
`retrieve(id)` is an async server read (await it). `claim.state(id)` is a
|
|
61
|
+
synchronous local read of who currently holds the row — it never blocks.
|
|
62
|
+
|
|
52
63
|
## Coordination
|
|
53
64
|
|
|
54
65
|
> Loop view only. Full claim reference — methods, the claim-state object, the
|
|
55
|
-
> `queue`, errors — is [Coordination](./coordination.md).
|
|
66
|
+
> `claim.queue`, errors — is [Coordination](./coordination.md).
|
|
56
67
|
|
|
57
|
-
Claims broadcast across the org.
|
|
58
|
-
|
|
68
|
+
Claims broadcast across the org. Call `claim(id, callback)`, do your writes with
|
|
69
|
+
the normal `update` inside the callback, and the claim releases automatically
|
|
70
|
+
when the callback returns:
|
|
59
71
|
|
|
60
72
|
```ts
|
|
61
73
|
await ablo.weatherReports.claim(
|
|
62
74
|
'report_stockholm',
|
|
63
75
|
async (report) => {
|
|
64
|
-
await ablo.weatherReports.update(report.id, { status: 'ready' }); //
|
|
76
|
+
await ablo.weatherReports.update(report.id, { status: 'ready' }); // rejected if the row changed under the claim
|
|
65
77
|
},
|
|
66
78
|
{ action: 'editing' },
|
|
67
79
|
);
|
|
68
80
|
```
|
|
69
81
|
|
|
70
|
-
`ablo.weatherReports.
|
|
71
|
-
blocking.
|
|
72
|
-
`claim` waits for them to finish
|
|
73
|
-
same signal is visible to every schema client through `
|
|
74
|
-
claim stream.
|
|
82
|
+
`ablo.weatherReports.claim.state('report_stockholm')` reads the live claim (or
|
|
83
|
+
`null`) without blocking. Claims don't lock: if another participant holds the
|
|
84
|
+
row, `claim` waits for them to finish, re-reads, and then hands you the fresh
|
|
85
|
+
row. The same signal is visible to every schema client through `claim.state(id)`
|
|
86
|
+
and the live claim stream.
|
|
75
87
|
|
|
76
88
|
## Conflict resolution
|
|
77
89
|
|
package/docs/mcp/claude-code.md
CHANGED
|
@@ -6,19 +6,9 @@
|
|
|
6
6
|
claude mcp add --transport http ablo-sync https://<your-app>/api/mcp
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
That's it
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
If your deployment requires a scoped bearer token (production setups should):
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
claude mcp add --transport http ablo-sync https://<your-app>/api/mcp \
|
|
17
|
-
--header "Authorization=Bearer $ABLO_MCP_TOKEN"
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Create a session-scoped bearer token from your server or dashboard — see
|
|
21
|
-
[MCP overview](/docs/mcp#auth).
|
|
9
|
+
That's it — no token or header needed. The endpoint is public and serves
|
|
10
|
+
only docs, schema lint, and scaffolds. The next `/help` in Claude Code will
|
|
11
|
+
list the Ablo Sync tools.
|
|
22
12
|
|
|
23
13
|
## Verify
|
|
24
14
|
|
|
@@ -28,7 +18,9 @@ In Claude Code, run:
|
|
|
28
18
|
/mcp list
|
|
29
19
|
```
|
|
30
20
|
|
|
31
|
-
You should see `ablo-sync` with the
|
|
21
|
+
You should see `ablo-sync` with the integration tools enumerated:
|
|
22
|
+
`search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
|
|
23
|
+
`scaffold_app`.
|
|
32
24
|
|
|
33
25
|
## Removing
|
|
34
26
|
|
|
@@ -38,6 +30,6 @@ claude mcp remove ablo-sync
|
|
|
38
30
|
|
|
39
31
|
## More
|
|
40
32
|
|
|
41
|
-
- [MCP overview](/docs/mcp) — how the transport works.
|
|
42
|
-
- [Cursor setup](/docs/mcp/cursor) — same
|
|
43
|
-
- [Windsurf setup](/docs/mcp/windsurf) — same
|
|
33
|
+
- [MCP overview](/docs/mcp) — what the server exposes and how the transport works.
|
|
34
|
+
- [Cursor setup](/docs/mcp/cursor) — same URL, different UI.
|
|
35
|
+
- [Windsurf setup](/docs/mcp/windsurf) — same URL, different UI.
|
package/docs/mcp/cursor.md
CHANGED
|
@@ -15,39 +15,21 @@ Add the Ablo Sync MCP server to Cursor's `mcp.json`:
|
|
|
15
15
|
}
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
The file lives at `~/.cursor/mcp.json` on macOS / Linux.
|
|
18
|
+
The file lives at `~/.cursor/mcp.json` on macOS / Linux. No auth header is
|
|
19
|
+
needed — the endpoint is public and serves only docs, schema lint, and
|
|
20
|
+
scaffolds.
|
|
19
21
|
|
|
20
22
|
Restart Cursor. The Ablo Sync tools appear under the MCP icon in the agent
|
|
21
23
|
panel.
|
|
22
24
|
|
|
23
|
-
## With auth
|
|
24
|
-
|
|
25
|
-
Add a `headers` block:
|
|
26
|
-
|
|
27
|
-
```json
|
|
28
|
-
{
|
|
29
|
-
"mcpServers": {
|
|
30
|
-
"ablo-sync": {
|
|
31
|
-
"transport": "http",
|
|
32
|
-
"url": "https://<your-app>/api/mcp",
|
|
33
|
-
"headers": {
|
|
34
|
-
"Authorization": "Bearer $ABLO_MCP_TOKEN"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Cursor expands shell-style env vars in this block. Set `ABLO_MCP_TOKEN`
|
|
42
|
-
in your shell config.
|
|
43
|
-
|
|
44
25
|
## Verify
|
|
45
26
|
|
|
46
27
|
In Cursor's agent panel, open the MCP tools list. You should see the
|
|
47
|
-
Ablo Sync
|
|
28
|
+
Ablo Sync integration tools and their JSON schemas: `search_ablo_docs`,
|
|
29
|
+
`get_recipe`, `get_api_surface`, `validate_schema`, `scaffold_app`.
|
|
48
30
|
|
|
49
31
|
## More
|
|
50
32
|
|
|
51
|
-
- [MCP overview](/docs/mcp) — how the transport works.
|
|
33
|
+
- [MCP overview](/docs/mcp) — what the server exposes and how the transport works.
|
|
52
34
|
- [Claude Code setup](/docs/mcp/claude-code) — CLI install.
|
|
53
35
|
- [Windsurf setup](/docs/mcp/windsurf) — same JSON shape.
|