@abloatai/ablo 0.48.0 → 0.50.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/AGENTS.md +2 -2
- package/CHANGELOG.md +144 -2
- package/README.md +12 -12
- package/dist/ai-sdk.d.ts +11 -0
- package/dist/ai-sdk.d.ts.map +1 -1
- package/dist/ai-sdk.js +15 -0
- package/dist/ai-sdk.js.map +1 -1
- package/dist/batching.d.ts +7 -0
- package/dist/batching.d.ts.map +1 -0
- package/dist/batching.js +7 -0
- package/dist/batching.js.map +1 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +9 -0
- package/dist/client.js.map +1 -1
- package/dist/context/await.d.ts +10 -0
- package/dist/context/await.d.ts.map +1 -0
- package/dist/context/await.js +33 -0
- package/dist/context/await.js.map +1 -0
- package/dist/context/evidence.d.ts +11 -0
- package/dist/context/evidence.d.ts.map +1 -0
- package/dist/context/evidence.js +53 -0
- package/dist/context/evidence.js.map +1 -0
- package/dist/context/sources.d.ts +21 -0
- package/dist/context/sources.d.ts.map +1 -0
- package/dist/context/sources.js +36 -0
- package/dist/context/sources.js.map +1 -0
- package/dist/context.d.ts +22 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +33 -0
- package/dist/context.js.map +1 -0
- package/dist/coordination.d.ts +8 -0
- package/dist/coordination.d.ts.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/docs/agent-messaging.md +2 -2
- package/docs/api.md +2 -2
- package/docs/concurrency-convention.md +82 -268
- package/docs/context.md +170 -0
- package/docs/coordination.md +178 -905
- package/docs/data-sources.md +32 -1
- package/docs/examples/ai-sdk-tool.md +4 -0
- package/docs/guarantees.md +29 -21
- package/docs/how-it-works.md +10 -40
- package/docs/identity.md +48 -0
- package/docs/index.md +2 -2
- package/docs/integrations.md +4 -0
- package/docs/migration.md +46 -443
- package/docs/operating-on-your-database.md +29 -36
- package/examples/README.md +24 -0
- package/examples/agent-turn.ts +37 -0
- package/examples/data-source/ablo-driver.ts +4 -4
- package/examples/data-source/customer-server.ts +9 -4
- package/examples/data-source/schema.ts +1 -1
- package/examples/expensive-agent-turn.ts +76 -0
- package/examples/quickstart.ts +9 -7
- package/package.json +19 -5
- package/docs/internal/README.md +0 -18
- package/docs/internal/agent-fleet-coordination-design.md +0 -171
- package/docs/internal/commit-identifiers.md +0 -91
- package/docs/internal/concurrency-open-decisions.md +0 -37
- package/docs/internal/data-source-reverse-channel.md +0 -147
- package/docs/internal/per-field-conflict-detection.md +0 -165
- package/docs/internal/postgres-replication.md +0 -64
- package/docs/internal/serializable-schema.md +0 -119
- package/docs/internal/structure.md +0 -36
package/docs/data-sources.md
CHANGED
|
@@ -29,7 +29,7 @@ branch's database unavailable instead of silently writing somewhere else.
|
|
|
29
29
|
|
|
30
30
|
Yes for the Ablo application path: model reads and lists, coordinated writes,
|
|
31
31
|
claims, subscriptions, idempotency, confirmations, and transactional outbox
|
|
32
|
-
|
|
32
|
+
confirmation all work against localhost Postgres. The browser, server code, and
|
|
33
33
|
agents still connect to Ablo Cloud; only database operations cross the narrow
|
|
34
34
|
signed connector to your machine.
|
|
35
35
|
|
|
@@ -47,12 +47,43 @@ path with a network-reachable Postgres endpoint, PrivateLink/peering/VPN, or a
|
|
|
47
47
|
database-capable secure tunnel. Do not expose Postgres without TLS,
|
|
48
48
|
authentication, and network restrictions.
|
|
49
49
|
|
|
50
|
+
### For localhost-first open-source projects
|
|
51
|
+
|
|
52
|
+
Do not require contributors to buy hosted Postgres or expose port 5432 merely to
|
|
53
|
+
run the project. Treat the connector as the default contributor topology:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"scripts": {
|
|
58
|
+
"ablo:setup": "ablo migrate",
|
|
59
|
+
"ablo:dev": "ablo dev --local"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Keep `DATABASE_URL=postgres://…@localhost:5432/…` in `.env.example`, commit the
|
|
65
|
+
generated `ablo/data-source.ts` handler, and document two long-running processes:
|
|
66
|
+
the application and `npm run ablo:dev`. Contributors provide their own Ablo
|
|
67
|
+
branch credential through `ablo login`; the repository never contains it.
|
|
68
|
+
|
|
69
|
+
For collaborative models, route mutations through Ablo or the signed Data Source
|
|
70
|
+
adapter. If the existing project intentionally writes those same tables through
|
|
71
|
+
raw SQL or an unrelated ORM path, choose one explicitly:
|
|
72
|
+
|
|
73
|
+
- add the supported transactional outbox/source-push integration for those writes;
|
|
74
|
+
- state that only Ablo-mediated changes participate in live coordination locally; or
|
|
75
|
+
- make WAL integration tests opt-in through a secure direct tunnel or hosted test database.
|
|
76
|
+
|
|
77
|
+
That keeps the zero-cost localhost quickstart honest without weakening Ablo's
|
|
78
|
+
coordination boundary or pretending endpoint mode can see WAL.
|
|
79
|
+
|
|
50
80
|
### Local connector errors
|
|
51
81
|
|
|
52
82
|
Every stable code links to the generated [error reference](https://docs.abloatai.com/errors):
|
|
53
83
|
|
|
54
84
|
| Code | Meaning and fix |
|
|
55
85
|
|---|---|
|
|
86
|
+
| `database_loopback_requires_connector` | A direct connection was configured with localhost. For the normal OSS/dev path, run `ablo migrate` and `ablo dev --local`; use a direct network route only when arbitrary SQL writes need WAL observation. |
|
|
56
87
|
| `source_connector_not_attached` | The branch is connector-only but no process is attached. Start or restart `ablo dev --local`. |
|
|
57
88
|
| `source_connector_unauthenticated` | The temporary branch key is missing, expired, or rejected. Rerun `ablo dev --local` to mint a fresh key. |
|
|
58
89
|
| `source_connector_requires_secret_key` | The connector received the wrong key kind. Let `ablo dev` supply its branch-bound `sk_` key. |
|
|
@@ -83,3 +83,7 @@ model should skip work already owned by someone else, or `strategy: 'queue'`
|
|
|
83
83
|
when it should wait in Ablo's server-owned FIFO claim queue. The same entrypoint
|
|
84
84
|
also exports `readTool`, `createTool`, and `deleteTool`; deletes require AI SDK
|
|
85
85
|
approval unless the application explicitly disables it.
|
|
86
|
+
|
|
87
|
+
When the model call needs several current reads rather than one model tool,
|
|
88
|
+
[Context](../context.md) assembles them and formats an optional user message
|
|
89
|
+
without taking ownership of the AI SDK loop.
|
package/docs/guarantees.md
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Exactly what a confirmed write, a rejected stale write, and a held claim each promise.
|
|
4
4
|
|
|
5
|
-
When an Ablo write succeeds, the server has
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
every change.
|
|
5
|
+
When an Ablo write succeeds, the server has confirmed it. Concurrency behavior
|
|
6
|
+
depends on the write form you choose: plain writes are last-write-wins;
|
|
7
|
+
functional updates, stale guards, and claims add protection when a write depends
|
|
8
|
+
on earlier state. This page is the precise list of what each form promises.
|
|
10
9
|
|
|
11
10
|
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
12
11
|
re-reads the fresh row, then hands it to you — so two writers serialize instead
|
|
@@ -49,18 +48,22 @@ Use `snapshot(...)` and `readAt` when a write depends on state the agent already
|
|
|
49
48
|
read:
|
|
50
49
|
|
|
51
50
|
```ts
|
|
52
|
-
const
|
|
51
|
+
const report = await ablo.weatherReports.get({ id: 'report_stockholm' });
|
|
52
|
+
if (!report) throw new Error('report missing');
|
|
53
53
|
|
|
54
54
|
await ablo.weatherReports.update({
|
|
55
|
-
id:
|
|
55
|
+
id: report.id,
|
|
56
56
|
data: { status: 'ready' },
|
|
57
|
-
|
|
58
|
-
onStale: 'reject',
|
|
57
|
+
reads: [report],
|
|
59
58
|
});
|
|
60
59
|
```
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
The returned row carries opaque evidence. If it changed after the read, the
|
|
62
|
+
server rejects the write instead of applying stale reasoning.
|
|
63
|
+
|
|
64
|
+
[Context](./context.md) collects the same exact-row evidence when an action
|
|
65
|
+
depends on several awaited values. External values remain informational and do
|
|
66
|
+
not acquire this guarantee.
|
|
64
67
|
|
|
65
68
|
Two other dispositions exist. `overwrite` applies the write with no stale check
|
|
66
69
|
at all. `notify` **holds** the write, so the row is left as it stands, and hands
|
|
@@ -70,6 +73,11 @@ and re-issue; the rest of the batch still commits.
|
|
|
70
73
|
See [Concurrency Convention](./concurrency-convention.md) for the full taxonomy,
|
|
71
74
|
what each disposition is checked against, and where the convention stops.
|
|
72
75
|
|
|
76
|
+
A plain `update({ id, data })` carries no stale premise. If no one holds a claim
|
|
77
|
+
on the target, it is last-write-wins. That is appropriate for independent values
|
|
78
|
+
such as status flags, but not for a read-modify-write calculation. For the latter,
|
|
79
|
+
use the functional update form or pass exact returned rows through `reads`.
|
|
80
|
+
|
|
73
81
|
## Claim Coordination
|
|
74
82
|
|
|
75
83
|
> The guarantee, not the how-to. Methods, the claim-state object, and the `claim.queue`
|
|
@@ -86,10 +94,12 @@ error out, when it should not return a row while someone else is mid-edit. Reads
|
|
|
86
94
|
never block on a claim — to wait for a row to free up, `claim({ id })` it (the
|
|
87
95
|
claim queues fairly behind the holder).
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
By default, a held claim rejects writes from other participants to the claimed
|
|
98
|
+
target. Contenders that call `claim` wait their turn; ordinary reads remain
|
|
99
|
+
open. An explicit model conflict policy can choose another disposition for a
|
|
100
|
+
participant kind. While you hold a claim, the matching
|
|
101
|
+
`ablo.<model>.update({ id, ... })` is rejected with `AbloStaleContextError` if
|
|
102
|
+
the row changed underneath you after your claim point.
|
|
93
103
|
|
|
94
104
|
## Agent Runs
|
|
95
105
|
|
|
@@ -172,12 +182,10 @@ separates workflow or deployment lanes sharing the same store.
|
|
|
172
182
|
|
|
173
183
|
## Storage Boundary
|
|
174
184
|
|
|
175
|
-
Your rows live in your database; Ablo holds
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
forwards the write to a signed Data Source endpoint instead — the marked
|
|
180
|
-
fallback. Either way Ablo never holds a database connection string. See
|
|
185
|
+
Your rows live in your database; Ablo holds change history and coordination
|
|
186
|
+
state. Writes land in your Postgres through a scoped writer role and are
|
|
187
|
+
confirmed from its authoritative change feed. For a database that cannot grant
|
|
188
|
+
replication, Ablo uses a signed Data Source endpoint instead. See
|
|
181
189
|
[Connect Your Database](./data-sources.md).
|
|
182
190
|
|
|
183
191
|
## Writes
|
package/docs/how-it-works.md
CHANGED
|
@@ -19,20 +19,20 @@ Ablo is a **coordination layer in front of your Postgres**. Agents, background
|
|
|
19
19
|
jobs, and the people alongside them all change the same application data through
|
|
20
20
|
one API, and Ablo makes sure their writes don't clobber each other.
|
|
21
21
|
|
|
22
|
-
- **Writes go through Ablo:** `ablo.<model>.create / update / delete`
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
- **Writes go through Ablo:** `ablo.<model>.create / update / delete` are
|
|
23
|
+
authorized, made idempotent, and applied to your Postgres through a scoped
|
|
24
|
+
writer role. A plain write is last-write-wins. Use a functional update, a
|
|
25
|
+
explicit `reads: [returnedRow]`, or a claim when the new value depends on an
|
|
26
|
+
earlier read.
|
|
26
27
|
- **Your database confirms the write.** Ablo tails your write-ahead log (WAL). When
|
|
27
28
|
the row it wrote shows up there, the receipt is promoted to `confirmed`. So your
|
|
28
29
|
database, not Ablo, is the source of truth for row state — the WAL echo is how
|
|
29
30
|
Ablo *confirms*, not how it writes.
|
|
30
31
|
- **Reads are live.** Ablo serves current state and keeps every connected client up
|
|
31
32
|
to date off that same stream.
|
|
32
|
-
- **Ablo stores only the
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
your migration tool stays in charge of the shape of your database.
|
|
33
|
+
- **Ablo stores only the change history and coordination state.** Your rows live
|
|
34
|
+
in your database. Ablo does not run application migrations; your migration
|
|
35
|
+
tool stays in charge of tables and columns.
|
|
36
36
|
|
|
37
37
|
That's the shape: **you write through Ablo → it lands in your Postgres → the WAL
|
|
38
38
|
echo confirms it → everyone connected sees it live.**
|
|
@@ -45,21 +45,7 @@ echo confirms it → everyone connected sees it live.**
|
|
|
45
45
|
| `Model` | State | The generated `ablo.<model>` model. Use `get`/`list` (async reads), `local.get`/`local.list`/`local.count` (the same verbs, synchronous and local-only), `create`, `update`, and `delete`. |
|
|
46
46
|
| `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim({ id })` and read via `ablo.<model>.claim.state({ id })`. Ephemeral, never persisted. |
|
|
47
47
|
| `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
|
|
48
|
-
| `Receipt` | Protocol | The lower-level
|
|
49
|
-
|
|
50
|
-
### Why each primitive is separate
|
|
51
|
-
|
|
52
|
-
Why are `Claim`, `Commit`, and `Receipt` separate things instead of one? Each
|
|
53
|
-
does a job the others cannot. If you are coming from Replicache or Yjs you would
|
|
54
|
-
expect just `Commit`. Here is what the other two buy you over that minimum:
|
|
55
|
-
|
|
56
|
-
- **`Claim` is not a read lock.** Reads stay open. Claims serialize
|
|
57
|
-
acting-on-the-row, so slow work can wait in FIFO order, re-read, and write
|
|
58
|
-
from fresh state.
|
|
59
|
-
- **`Receipt` is not a `200 OK`.** It is the durable artifact a commit produced:
|
|
60
|
-
accepted commit id, server-assigned timestamps, stale-check outcome. It is
|
|
61
|
-
addressable after the fact and replayable into a different client. A status
|
|
62
|
-
code cannot be re-read by a sub-agent that was not on the original call.
|
|
48
|
+
| `Receipt` | Protocol | The result of a lower-level commit. Awaiting a schema write waits for confirmation. |
|
|
63
49
|
|
|
64
50
|
## Where your data lives
|
|
65
51
|
|
|
@@ -117,22 +103,6 @@ const latest = ablo.tasks.local.get('task_42'); // read after claiming, not from
|
|
|
117
103
|
await ablo.tasks.update({ id: 'task_42', data: { status: 'done' } });
|
|
118
104
|
```
|
|
119
105
|
|
|
120
|
-
## Reading the architecture correctly
|
|
121
|
-
|
|
122
|
-
The model changed once (ADR 0010), and older material described the earlier one.
|
|
123
|
-
If you've read that a customer's app keeps writing while Ablo only tails the WAL,
|
|
124
|
-
here is the current, correct reading:
|
|
125
|
-
|
|
126
|
-
- **"Ablo tails the WAL."** True — to *confirm* your writes and serve live reads.
|
|
127
|
-
It is not the write path. You write **through** Ablo.
|
|
128
|
-
- **"Ablo is out of the write path."** Not anymore. Ablo is the write chokepoint;
|
|
129
|
-
every write flows through its coordination and then into your database.
|
|
130
|
-
- **"Ablo holds my rows."** Only on a development branch where you have not
|
|
131
|
-
connected a database yet. Once you connect one, your rows live in *your*
|
|
132
|
-
Postgres — Ablo keeps just the log.
|
|
133
|
-
- **"Ablo migrates my schema."** No. Ablo writes rows through a scoped role and runs
|
|
134
|
-
no DDL; your migrations own the shape of your database.
|
|
135
|
-
|
|
136
106
|
For the setup details, see [Connect Your Database](./data-sources.md). For the
|
|
137
|
-
coordination loop, see [
|
|
107
|
+
coordination loop, see [Coordination](./coordination.md). For
|
|
138
108
|
what `queued` and `confirmed` guarantee, see [Guarantees](./guarantees.md).
|
package/docs/identity.md
CHANGED
|
@@ -14,6 +14,54 @@ NextAuth, WorkOS, your own session table. Ablo's job begins **after** you've
|
|
|
14
14
|
authenticated the user: you hand Ablo the already-authenticated identity, and
|
|
15
15
|
Ablo decides which **sync groups** that identity may read and write.
|
|
16
16
|
|
|
17
|
+
## Inspect the credential the application is actually using
|
|
18
|
+
|
|
19
|
+
`ablo whoami` describes the developer running the CLI. Runtime code should
|
|
20
|
+
inspect `ablo.identity`, which is the server-confirmed `EffectiveAuthority` of
|
|
21
|
+
the credential attached to that client. It is never decoded or reconstructed
|
|
22
|
+
locally.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Ablo } from '@abloatai/ablo';
|
|
26
|
+
import { CapabilityError } from '@abloatai/ablo';
|
|
27
|
+
|
|
28
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
29
|
+
await ablo.ready();
|
|
30
|
+
|
|
31
|
+
console.log(ablo.identity?.operations);
|
|
32
|
+
console.log(ablo.identity?.syncGroups);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The HTTP and stateful clients expose the same value after `ready()`. A denied
|
|
36
|
+
scoped operation throws `CapabilityError`; compare its
|
|
37
|
+
`requiredCapability.scope` directly with `ablo.identity.operations`.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
try {
|
|
41
|
+
await ablo.tasks.update({ id, data: { status: 'done' } });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof CapabilityError) {
|
|
44
|
+
console.error('missing grant', error.requiredCapability);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Do not broaden the credential in the client. A backend holding the project
|
|
50
|
+
secret mints a replacement, least-privilege agent credential with the schema-
|
|
51
|
+
typed grant:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const session = await control.sessions.create({
|
|
55
|
+
agent: { id: agentId },
|
|
56
|
+
can: { tasks: ['read', 'update'] },
|
|
57
|
+
syncGroups: [syncGroup('workspace', workspaceId)],
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Install `session.token` in the agent process and call `ready()` again. The next
|
|
62
|
+
`ablo.identity` is the authority the server will enforce; no automatic grant
|
|
63
|
+
escalation occurs.
|
|
64
|
+
|
|
17
65
|
So the integration question is never "how do I log into Ablo?" It's: *"My app
|
|
18
66
|
already knows this request is user `U` in org `O`. How do I tell Ablo, so it
|
|
19
67
|
scopes their realtime data correctly?"* The rest of this doc answers exactly
|
package/docs/index.md
CHANGED
|
@@ -140,7 +140,7 @@ default caller, not a special one.
|
|
|
140
140
|
|
|
141
141
|
- [How Ablo Works](./how-it-works.md) — the mental model in one page: you write through Ablo, it lands in your Postgres, the write-ahead log confirms it. **Read this first.**
|
|
142
142
|
- [Coordination](./coordination.md) — `claim`, `claim.state`, and `claim.queue`: who holds a row, and who is waiting.
|
|
143
|
-
- [Concurrency Convention](./concurrency-convention.md) — the
|
|
143
|
+
- [Concurrency Convention](./concurrency-convention.md) — the precise rule for guarded and unguarded writes.
|
|
144
144
|
- [Guarantees](./guarantees.md) — what a confirmed write, a stale-write rejection, and a claim each promise.
|
|
145
145
|
- [Idempotency](./idempotency.md) — make a retried write safe; what replays, what re-runs, and for how long.
|
|
146
146
|
- [Schema Contract](./schema-contract.md) — one schema becomes typed clients, agent writes, React reads, and the push.
|
|
@@ -175,7 +175,7 @@ default caller, not a special one.
|
|
|
175
175
|
|
|
176
176
|
- [API Reference](./api.md) — model-by-model method shape.
|
|
177
177
|
- [Errors](./errors.md) — the code registry, its categories, and what to do about each.
|
|
178
|
-
- [
|
|
178
|
+
- [Upgrade Guide](./migration.md) — upgrade a pinned pre-1.0 SDK safely.
|
|
179
179
|
- [Changelog](../CHANGELOG.md) — what shipped recently.
|
|
180
180
|
|
|
181
181
|
## Examples
|
package/docs/integrations.md
CHANGED
|
@@ -7,6 +7,10 @@ Integrations live at the application edge. Ablo continues to own typed reads
|
|
|
7
7
|
and writes, idempotency, claims, and authoritative confirmation; the external
|
|
8
8
|
runtime keeps owning the job it was designed for.
|
|
9
9
|
|
|
10
|
+
[Context](./context.md) is the small composition point for values returned by
|
|
11
|
+
those systems. It carries Ablo read evidence without turning an external result
|
|
12
|
+
into authoritative application state.
|
|
13
|
+
|
|
10
14
|
| Category | Integration | Status | Use it for |
|
|
11
15
|
|---|---|---|---|
|
|
12
16
|
| Long-running tasks | [Temporal](./integrations/temporal.md) | Available | Durable Workflows, Activity retries, timers, cancellation, and durable AI SDK calls |
|