@abloatai/ablo 0.57.0 → 0.59.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 +12 -5
- package/CHANGELOG.md +269 -13
- package/README.md +6 -5
- package/dist/ai-sdk.d.ts +1 -1
- package/dist/ai-sdk.d.ts.map +1 -1
- package/dist/context/evidence.d.ts +6 -8
- package/dist/context/evidence.d.ts.map +1 -1
- package/dist/context/evidence.js +6 -20
- package/dist/context/evidence.js.map +1 -1
- package/dist/context/index.d.ts +23 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +26 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/onChange.d.ts +9 -0
- package/dist/context/onChange.d.ts.map +1 -0
- package/dist/context/onChange.js +37 -0
- package/dist/context/onChange.js.map +1 -0
- package/dist/source-conformance.d.ts +1 -1
- package/dist/source-conformance.d.ts.map +1 -1
- package/dist/source-conformance.js +1 -1
- package/dist/source-conformance.js.map +1 -1
- package/dist/source-drizzle.d.ts +1 -1
- package/dist/source-drizzle.d.ts.map +1 -1
- package/dist/source-drizzle.js +1 -1
- package/dist/source-drizzle.js.map +1 -1
- package/dist/source-kysely.d.ts +1 -1
- package/dist/source-kysely.d.ts.map +1 -1
- package/dist/source-kysely.js +1 -1
- package/dist/source-kysely.js.map +1 -1
- package/dist/source-next.d.ts +1 -1
- package/dist/source-next.d.ts.map +1 -1
- package/dist/source-next.js +1 -1
- package/dist/source-next.js.map +1 -1
- package/docs/agents.md +38 -15
- package/docs/api-keys.md +6 -6
- package/docs/api.md +107 -34
- package/docs/basic-usage.md +84 -0
- package/docs/branch-development.md +23 -4
- package/docs/cli.md +16 -9
- package/docs/client-behavior.md +27 -29
- package/docs/comparison.md +63 -0
- package/docs/concurrency-convention.md +92 -75
- package/docs/context.md +76 -31
- package/docs/coordinate-existing-work.md +104 -0
- package/docs/coordination.md +93 -38
- package/docs/data-sources.md +12 -6
- package/docs/debugging.md +1 -1
- package/docs/deployment.md +19 -1
- package/docs/examples/agent-human.md +6 -18
- package/docs/examples/coordination-conformance.md +69 -0
- package/docs/examples/evidence-backed-document-pipeline.md +488 -0
- package/docs/examples/existing-python-backend.md +10 -13
- package/docs/examples/nextjs.md +2 -2
- package/docs/examples/scoped-agent.md +18 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/faq.md +75 -0
- package/docs/groups.md +19 -139
- package/docs/guarantees.md +8 -8
- package/docs/idempotency.md +3 -0
- package/docs/identity.md +2 -1
- package/docs/implement.md +61 -0
- package/docs/implementation-index.md +20 -0
- package/docs/index.md +59 -173
- package/docs/installation.md +77 -0
- package/docs/instrumentation.md +52 -0
- package/docs/integration-guide.md +20 -19
- package/docs/integrations/sandbox-runtime.md +157 -0
- package/docs/integrations.md +9 -0
- package/docs/migration.md +12 -7
- package/docs/operating-on-your-database.md +7 -0
- package/docs/options.md +172 -0
- package/docs/quickstart.md +25 -14
- package/docs/react.md +9 -9
- package/docs/schema-contract.md +14 -13
- package/docs/security.md +64 -0
- package/docs/sessions.md +1 -1
- package/examples/README.md +8 -2
- package/examples/agent-turn.ts +1 -1
- package/examples/expensive-agent-turn.ts +1 -1
- package/examples/stale-context-agent-turn.ts +106 -0
- package/llms.txt +23 -12
- package/package.json +7 -6
- package/dist/context/sources.d.ts +0 -21
- package/dist/context/sources.d.ts.map +0 -1
- package/dist/context/sources.js +0 -36
- package/dist/context/sources.js.map +0 -1
- package/dist/context.d.ts +0 -22
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -33
- package/dist/context.js.map +0 -1
package/docs/faq.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# FAQ
|
|
2
|
+
|
|
3
|
+
> Short answers to the implementation choices developers encounter most often.
|
|
4
|
+
|
|
5
|
+
## Does Ablo replace PostgreSQL?
|
|
6
|
+
|
|
7
|
+
No. PostgreSQL remains the durable source of truth. Keep its schema, constraints,
|
|
8
|
+
transactions, row-level security, and short write locks. Ablo coordinates work
|
|
9
|
+
around that state and can route guarded writes into it.
|
|
10
|
+
|
|
11
|
+
## Does Ablo replace Redis locks?
|
|
12
|
+
|
|
13
|
+
It can replace an application-owned reservation layer, but it does not require a
|
|
14
|
+
rewrite. Ablo standardizes resource identity, participant identity, leases,
|
|
15
|
+
waiting, recovery, stale-work rejection, and visibility. Teams can begin by
|
|
16
|
+
coordinating an existing operation while its final transaction remains in the
|
|
17
|
+
application.
|
|
18
|
+
|
|
19
|
+
## Is Ablo only for agents?
|
|
20
|
+
|
|
21
|
+
No. Agents, workers, application services, and people can coordinate over the
|
|
22
|
+
same resources. The package-root client is suited to stateless server work; the
|
|
23
|
+
[React client](./react.md) adds live state and presence for human interfaces.
|
|
24
|
+
|
|
25
|
+
## Which client should I import?
|
|
26
|
+
|
|
27
|
+
Use the default export from `@abloatai/ablo` for agents, workers, route handlers,
|
|
28
|
+
and server operations. It uses ordinary request/response HTTP. Use
|
|
29
|
+
`@abloatai/ablo/react` when a live interface needs local synchronized state,
|
|
30
|
+
subscriptions, or presence.
|
|
31
|
+
|
|
32
|
+
## What is the difference between get and read?
|
|
33
|
+
|
|
34
|
+
`get({ id })` observes the current row. `read({ id })` captures the exact row
|
|
35
|
+
version as evidence for a later guarded write. Pass that returned row in the
|
|
36
|
+
mutation's `reads` array when the decision must be rejected if its premise
|
|
37
|
+
changed.
|
|
38
|
+
|
|
39
|
+
## When should I claim a resource?
|
|
40
|
+
|
|
41
|
+
Claim before slow or expensive work when another participant should not perform
|
|
42
|
+
conflicting work on the same business resource. Do not add a claim to every
|
|
43
|
+
write: a short, independent update can use its normal database and mutation
|
|
44
|
+
semantics.
|
|
45
|
+
|
|
46
|
+
## Is a claim a database lock?
|
|
47
|
+
|
|
48
|
+
No. A claim is a participant-scoped lease held across work that may outlive one
|
|
49
|
+
database transaction. It expires after heartbeat loss. PostgreSQL locks still
|
|
50
|
+
protect the short authoritative transaction.
|
|
51
|
+
|
|
52
|
+
## What if code writes directly to PostgreSQL?
|
|
53
|
+
|
|
54
|
+
Ablo observes the resulting change through the configured data source, but the
|
|
55
|
+
writer bypasses Ablo claims and request ordering. Keep database constraints for
|
|
56
|
+
rules that must apply to every writer.
|
|
57
|
+
|
|
58
|
+
## Are retries exactly once?
|
|
59
|
+
|
|
60
|
+
Ablo idempotency deduplicates the same Ablo request within its retention window.
|
|
61
|
+
It does not make external side effects exactly once. Use the external provider's
|
|
62
|
+
idempotency mechanism or an application-owned effect record.
|
|
63
|
+
|
|
64
|
+
## Does Ablo run long workflows?
|
|
65
|
+
|
|
66
|
+
No. Temporal, Inngest, queues, and application workers still own scheduling,
|
|
67
|
+
retries, and durable workflow progress. Ablo coordinates the shared state those
|
|
68
|
+
executions read and change.
|
|
69
|
+
|
|
70
|
+
## Do I need to understand fencing first?
|
|
71
|
+
|
|
72
|
+
No. Start from the public behavior: a claim expires, another participant can
|
|
73
|
+
take over, and an obsolete owner cannot use an old claim to commit through Ablo.
|
|
74
|
+
The implementation mechanism is documented for operators and advanced
|
|
75
|
+
integrations, not required for basic SDK use.
|
package/docs/groups.md
CHANGED
|
@@ -17,30 +17,28 @@ block `C`. Between those steps someone else edits `A`. The agent is now building
|
|
|
17
17
|
`C` on a premise that has moved — and nothing about writing `C` looks wrong in
|
|
18
18
|
isolation. That is stale context, and it is the thing sync groups let you catch.
|
|
19
19
|
|
|
20
|
-
The recipe is one field on the commit: declare the group you read as a premise
|
|
21
|
-
and say what should happen if it moved.
|
|
20
|
+
The recipe is one field on the commit: declare the group you read as a premise.
|
|
22
21
|
|
|
23
22
|
```ts
|
|
24
23
|
// The agent read everything under workspace:abc to compose this write.
|
|
25
24
|
await ablo.blocks.update({
|
|
26
25
|
id: 'block-C',
|
|
27
26
|
data: { text: revised },
|
|
28
|
-
reads: [{ group: 'workspace:abc', readAt: watermark
|
|
27
|
+
reads: [{ group: 'workspace:abc', readAt: watermark }],
|
|
29
28
|
});
|
|
30
29
|
```
|
|
31
30
|
|
|
32
31
|
At commit, inside the write transaction, the engine asks a single question: *did
|
|
33
32
|
any delta routed to `workspace:abc` land after `watermark`?* If nothing moved, the
|
|
34
|
-
write applies. If something moved,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
never persists work built on a premise it can no longer see.
|
|
33
|
+
write applies. If something moved, Ablo rejects the batch with a `409`, so the
|
|
34
|
+
agent can re-read `workspace:abc`, regenerate, and submit a fresh guarded
|
|
35
|
+
write. The agent never persists work built on a premise it can no longer see.
|
|
38
36
|
|
|
39
37
|
---
|
|
40
38
|
|
|
41
39
|
## How you hear about it
|
|
42
40
|
|
|
43
|
-
|
|
41
|
+
Three channels carry "something changed", and they answer three different
|
|
44
42
|
questions. Pick by the question you have.
|
|
45
43
|
|
|
46
44
|
```ts
|
|
@@ -52,10 +50,9 @@ await using room = await ablo.records.join(documentIds, { ttl: '5m' });
|
|
|
52
50
|
room.peers;
|
|
53
51
|
|
|
54
52
|
// Stop this write if the thing I read moved while I composed it.
|
|
55
|
-
await ablo.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
await ablo.records.track({ id: 's-1' });
|
|
53
|
+
const record = await ablo.records.read({ id: 's-1' });
|
|
54
|
+
if (!record) throw new Error('Record not found');
|
|
55
|
+
await ablo.blocks.update({ id, data, reads: [record] });
|
|
59
56
|
```
|
|
60
57
|
|
|
61
58
|
| Question | Channel | Arrives |
|
|
@@ -63,29 +60,12 @@ await ablo.records.track({ id: 's-1' });
|
|
|
63
60
|
| What do the rows say right now? | `onChange` | As deltas land, on the socket |
|
|
64
61
|
| Who else is working here? | `join`, then `room.peers` and `room.claims` | As participants come and go, on the socket |
|
|
65
62
|
| Did the premise for **this** write move? | `reads` on the write | On that write's receipt, before it applies |
|
|
66
|
-
| Has anything I read moved since? | `track` | On your next commit's receipt |
|
|
67
|
-
| Stop me writing if it has | `track` with `onStale: 'reject'` | Refuses that commit, at the chokepoint |
|
|
68
|
-
|
|
69
|
-
Two distinctions do most of the work here.
|
|
70
|
-
|
|
71
|
-
**`join` is about people; `track` is about data.** Both open a subscription and
|
|
72
|
-
both are scoped by sync group, which is why they look alike. `join` reports
|
|
73
|
-
participants: who is present, what they are doing, which rows they hold. `track`
|
|
74
|
-
reports the rows themselves: something you said you cared about moved, here is
|
|
75
|
-
the watermark to re-read it at. A tool that wants to avoid duplicating a peer's
|
|
76
|
-
work needs `join`. A tool whose output goes stale when its inputs change needs
|
|
77
|
-
`track`.
|
|
78
|
-
|
|
79
|
-
**`reads` guards one write; `track` outlives it.** They speak the same
|
|
80
|
-
vocabulary and produce the same `StaleNotification`. A `reads` entry is checked
|
|
81
|
-
once, at the commit that carried it, and discarded. A `track` is persisted and
|
|
82
|
-
re-checked against every delta after it, so a long-running actor hears about a
|
|
83
|
-
change that landed while it was thinking, on the next commit it makes.
|
|
84
63
|
|
|
85
64
|
`onChange` and `join` need a live socket, so they are available on the default
|
|
86
|
-
WebSocket client. `reads`
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
WebSocket client. `reads` rides the commit, so it reaches a socketless actor over
|
|
66
|
+
HTTP too. The row returned by `read` privately carries its model, id, and
|
|
67
|
+
watermark; passing that row in `reads` is enough to protect a later write. Ablo
|
|
68
|
+
does not retain the row contents as read evidence.
|
|
89
69
|
|
|
90
70
|
---
|
|
91
71
|
|
|
@@ -158,13 +138,12 @@ Two consequences worth designing around:
|
|
|
158
138
|
## Declaring the batch premise
|
|
159
139
|
|
|
160
140
|
`reads[]` declares what the commit was based on. Each entry is a premise, and
|
|
161
|
-
each governs the *whole* commit: if one goes stale,
|
|
162
|
-
|
|
163
|
-
entry.
|
|
141
|
+
each governs the *whole* commit: if one goes stale, every write in the batch
|
|
142
|
+
rejects. You choose the granularity per entry.
|
|
164
143
|
|
|
165
144
|
```ts
|
|
166
145
|
reads: [
|
|
167
|
-
{ group: 'workspace:abc', readAt: N
|
|
146
|
+
{ group: 'workspace:abc', readAt: N }, // did anything in the workspace move?
|
|
168
147
|
{ model: 'Document', id: 's-1', readAt: N, fields: ['title'] }, // did this row (this field) move?
|
|
169
148
|
]
|
|
170
149
|
```
|
|
@@ -175,108 +154,9 @@ literal: this object, optionally these fields. A row premise with `fields`
|
|
|
175
154
|
conflicts only on real field overlap, so two actors editing disjoint fields of
|
|
176
155
|
the same row don't collide.
|
|
177
156
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
For a group premise it carries the group name and the new watermark
|
|
182
|
-
(`observedSyncId`); re-read the group at that point and regenerate. This is the
|
|
183
|
-
setting a chain wants — the actor gets the truth and resolves it.
|
|
184
|
-
- **`reject`** aborts the batch with a `stale_context` error (`409`). The right
|
|
185
|
-
default when there is nothing to reconcile and the write should simply not
|
|
186
|
-
land.
|
|
187
|
-
- **`overwrite`** skips the check and lets the write land — last-write-wins, the
|
|
188
|
-
explicit escape hatch.
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
## Staying subscribed across commits: `track`
|
|
193
|
-
|
|
194
|
-
A batch premise guards a single commit: you state what you read, the engine
|
|
195
|
-
checks it, the premise is gone. That fits an actor that reads and writes in one
|
|
196
|
-
breath. It does not fit a long-running one — an agent that reads a row now,
|
|
197
|
-
works for a few minutes, and writes much later. By the time it commits, the
|
|
198
|
-
premise it would have declared is stale, and there was no commit in between on
|
|
199
|
-
which to hear that the ground had shifted.
|
|
200
|
-
|
|
201
|
-
`track` is the durable half of the same idea. Register what you are watching and
|
|
202
|
-
it persists on the server; the next time you commit anything, a change that
|
|
203
|
-
landed on the tracked target since you registered rides back on your receipt —
|
|
204
|
-
the same `StaleNotification` an `onStale: 'notify'` premise would have handed
|
|
205
|
-
you, arriving on the write you were going to make anyway.
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
// Register interest and walk away — no write required.
|
|
209
|
-
await ablo.records.track({ id: 's-1' });
|
|
210
|
-
|
|
211
|
-
// …minutes of other work later, on your next commit…
|
|
212
|
-
const res = await ablo.blocks.update({ id: 'block-C', data: { text: revised } });
|
|
213
|
-
res.notifications; // populated if s-1 moved under you in the meantime
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
The target is a row (`{ id }` on the model verb) or a sync group (as a write
|
|
217
|
-
option, below). A track is an idempotent registration: calling it again refreshes
|
|
218
|
-
the same subscription rather than stacking duplicates, and once a change fires the
|
|
219
|
-
track re-baselines, so the same change notifies once. Your own writes to a target
|
|
220
|
-
you track never notify you — the signal is about what *others* did.
|
|
221
|
-
|
|
222
|
-
You can also register a track as part of a write you are already making, the
|
|
223
|
-
persisted companion to `reads`:
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
await ablo.records.update({
|
|
227
|
-
id: 's-1',
|
|
228
|
-
data: { title: revised },
|
|
229
|
-
reads: [{ group: 'workspace:abc', readAt: N, onStale: 'notify' }], // guards THIS commit
|
|
230
|
-
track: [{ group: 'workspace:abc' }], // and keeps watching after it
|
|
231
|
-
});
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
So `reads` is the premise for the commit in hand; `track` is a standing
|
|
235
|
-
subscription that outlives it. Both speak the same notification vocabulary.
|
|
236
|
-
Delivery is on your next commit's receipt; a track does not yet push out of band
|
|
237
|
-
between commits.
|
|
238
|
-
|
|
239
|
-
### Reporting, or gating: `onStale`
|
|
240
|
-
|
|
241
|
-
A track says what a moved belief should do to your **next write**. Same enum the
|
|
242
|
-
`reads` premise carries, minus the one mode that cannot apply:
|
|
243
|
-
|
|
244
|
-
```ts
|
|
245
|
-
await ablo.records.track({ id: 's-1' }); // notify (default)
|
|
246
|
-
await ablo.records.track({ id: 's-1', onStale: 'reject' }); // gate
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
- **`notify`** — the change rides your next receipt and the commit proceeds. You
|
|
250
|
-
decide what it means.
|
|
251
|
-
- **`reject`** — your next commit is **refused** while that belief is stale, even
|
|
252
|
-
if it writes an unrelated row. The gate is on what you *know*, not on what you
|
|
253
|
-
are touching.
|
|
254
|
-
|
|
255
|
-
`reject` is for an actor that must not act on a stale picture — an agent that
|
|
256
|
-
read a row, reasoned for minutes, and is about to write based on what it
|
|
257
|
-
believed. Ablo enforces it at the commit chokepoint, so it is a guarantee rather
|
|
258
|
-
than a convention: the write cannot land.
|
|
259
|
-
|
|
260
|
-
The gate takes a key. It does **not** reopen on its own, because an agent that
|
|
261
|
-
retried blindly would land exactly the write `reject` was asked to prevent. You
|
|
262
|
-
re-read, then re-register the track to say so:
|
|
263
|
-
|
|
264
|
-
```ts
|
|
265
|
-
try {
|
|
266
|
-
await ablo.records.update({ id, data });
|
|
267
|
-
} catch (err) {
|
|
268
|
-
if (err.code === 'stale_context') {
|
|
269
|
-
const fresh = await ablo.records.get({ id: 's-1' }); // read
|
|
270
|
-
await ablo.records.track({ id: 's-1', onStale: 'reject' }); // acknowledge
|
|
271
|
-
await ablo.records.update({ id, data: reconsider(fresh) }); // now it lands
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
A track-only call is never gated, so acknowledging is always reachable.
|
|
277
|
-
|
|
278
|
-
`overwrite` is not offered: it means "apply my write anyway", and a track guards
|
|
279
|
-
no write of its own to apply.
|
|
157
|
+
Any stale premise aborts the batch with `stale_context` (`409`). Re-read and
|
|
158
|
+
regenerate if the work is still relevant. Omit `reads` only when the write is
|
|
159
|
+
intentionally unconditional.
|
|
280
160
|
|
|
281
161
|
---
|
|
282
162
|
|
package/docs/guarantees.md
CHANGED
|
@@ -44,11 +44,11 @@ The server remains the source of truth.
|
|
|
44
44
|
|
|
45
45
|
## Stale-Write Protection
|
|
46
46
|
|
|
47
|
-
Use `
|
|
48
|
-
read:
|
|
47
|
+
Use `read(...)` and carry the returned row in `reads` when a write depends on
|
|
48
|
+
state the agent already read:
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
|
-
const report = await ablo.weatherReports.
|
|
51
|
+
const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
|
|
52
52
|
if (!report) throw new Error('report missing');
|
|
53
53
|
|
|
54
54
|
await ablo.weatherReports.update({
|
|
@@ -67,7 +67,7 @@ not acquire this guarantee.
|
|
|
67
67
|
|
|
68
68
|
Two other dispositions exist. `overwrite` applies the write with no stale check
|
|
69
69
|
at all. `notify` **holds** the write, so the row is left as it stands, and hands
|
|
70
|
-
back
|
|
70
|
+
back an `AbloStaleContextError` so the actor can re-read and reconcile
|
|
71
71
|
and re-issue; the rest of the batch still commits.
|
|
72
72
|
|
|
73
73
|
See [Concurrency Convention](./concurrency-convention.md) for the full taxonomy,
|
|
@@ -96,10 +96,10 @@ claim queues fairly behind the holder).
|
|
|
96
96
|
|
|
97
97
|
By default, a held claim rejects writes from other participants to the claimed
|
|
98
98
|
target. Contenders that call `claim` wait their turn; ordinary reads remain
|
|
99
|
-
open.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
open. While you hold a claim, the matching
|
|
100
|
+
`ablo.<model>.update({ id, ..., claim })` proves ownership at write time and is
|
|
101
|
+
rejected with `AbloStaleContextError` if the row changed underneath you after
|
|
102
|
+
your claim point. Do not omit `claim` from a row-backed claimed write.
|
|
103
103
|
|
|
104
104
|
## Agent Runs
|
|
105
105
|
|
package/docs/idempotency.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> Make a retried write safe: the same key never applies the same change twice.
|
|
4
4
|
|
|
5
|
+
This page owns idempotency for updates and retries, including changed intent
|
|
6
|
+
and the `idempotency_conflict` failure code.
|
|
7
|
+
|
|
5
8
|
An agent retries. A socket drops mid-commit, a worker restarts, a queue redelivers — and the write
|
|
6
9
|
you already sent arrives again. An idempotency key is how Ablo tells a retry from a new intention.
|
|
7
10
|
|
package/docs/identity.md
CHANGED
|
@@ -410,7 +410,7 @@ it to the provider:
|
|
|
410
410
|
|
|
411
411
|
```ts
|
|
412
412
|
// lib/ablo.ts
|
|
413
|
-
import Ablo from '@abloatai/ablo';
|
|
413
|
+
import { Ablo } from '@abloatai/ablo/react';
|
|
414
414
|
import { schema } from '@/ablo/schema';
|
|
415
415
|
|
|
416
416
|
// Build the client from the identity your server already resolved.
|
|
@@ -505,6 +505,7 @@ subset of what its user could see:
|
|
|
505
505
|
// (the floor). Build each group from the model's scope with `syncGroup(kind, id)`.
|
|
506
506
|
const session = await server.sessions.create({
|
|
507
507
|
agent: { id: agentId },
|
|
508
|
+
onBehalfOf: { user: { id: triggeringUser.id } },
|
|
508
509
|
can: { Document: ['read', 'update'], Workspace: ['read', 'update'] },
|
|
509
510
|
syncGroups: [syncGroup('document', recordId), syncGroup('workspace', workspaceId)],
|
|
510
511
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Choose the Ablo operation
|
|
2
|
+
|
|
3
|
+
> Find one implementation route from the work you are doing, copy its smallest recipe, and open guarantee details only when you need recovery behavior.
|
|
4
|
+
|
|
5
|
+
Do not read the documentation front to back. Start with the operation your
|
|
6
|
+
application already has, then use one row below.
|
|
7
|
+
|
|
8
|
+
| What you are implementing | Start here | Add only when |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| Get one row by id or list rows with the installed client | [Basic usage](./basic-usage.md) | Use `read`, not `get`, only when a later Ablo write depends on that exact row. |
|
|
11
|
+
| Configure a stateless worker's identity, permissions, or lifecycle | [Agents](./agents.md) | Keep the worker on HTTP; live human interfaces use React. |
|
|
12
|
+
| Wrap an existing API, service, Postgres transaction, filesystem write, or Git merge | [Coordinate existing work](./coordinate-existing-work.md) | Keep the final write in its existing owner. |
|
|
13
|
+
| Expose an existing named operation through GraphQL.js | [GraphQL.js](./approaches/graphql/graphql-js.md) | Keep the resolver dependent on that operation, not directly on Ablo. |
|
|
14
|
+
| Hold an Ablo model row while slow work runs, then write it through Ablo | [Coordination](./coordination.md) | Pass the returned claim handle to the write. |
|
|
15
|
+
| Reject a write when an earlier decision input changed | [Concurrency convention](./concurrency-convention.md) | Pass the exact object returned by `read` through `reads`. |
|
|
16
|
+
| Apply several Ablo writes all-or-none | [Atomic commits](./api.md#atomic-commits) | Put every operation and every captured premise in one `commits.create`. |
|
|
17
|
+
| Make a retried Ablo write safe | [Idempotency](./idempotency.md) | Derive one key from the business event and reuse it only for the identical request. |
|
|
18
|
+
| Send email, charge money, call a provider, or write a file | Keep that effect in the application | Use the provider's key or an application outbox; an Ablo key covers only the Ablo mutation. |
|
|
19
|
+
| Add a live human interface | [React](./react.md) | Humans use the WebSocket/live plane; stateless workers stay on HTTP. |
|
|
20
|
+
|
|
21
|
+
## The four choices agents most often confuse
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// Observe one current row. No later stale check.
|
|
25
|
+
const task = await ablo.tasks.get({ id });
|
|
26
|
+
|
|
27
|
+
// Declare a premise for one later Ablo write.
|
|
28
|
+
const premise = await ablo.tasks.read({ id });
|
|
29
|
+
if (!premise) throw new Error('task not found');
|
|
30
|
+
await ablo.tasks.update({ id, data, reads: [premise] });
|
|
31
|
+
|
|
32
|
+
// Hold an Ablo row across slow work. The final write goes through Ablo.
|
|
33
|
+
await using claim = await ablo.tasks.claim({ id });
|
|
34
|
+
await ablo.tasks.update({ id, data, claim });
|
|
35
|
+
|
|
36
|
+
// Coordinate row-free work whose final write stays in the application.
|
|
37
|
+
await using lease = await ablo.taskRuns.claim(id, {
|
|
38
|
+
contention: { mode: 'skip' },
|
|
39
|
+
});
|
|
40
|
+
if (lease) await existingTaskService.complete(id);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Use only one of those shapes unless the operation genuinely has both a claimed
|
|
44
|
+
target and separate captured premises. Claims answer who may work; `reads`
|
|
45
|
+
answer whether evidence is still current; `commits.create` answers whether
|
|
46
|
+
several Ablo writes land together; the existing database transaction still
|
|
47
|
+
owns atomicity for application-owned writes.
|
|
48
|
+
|
|
49
|
+
## Before writing code
|
|
50
|
+
|
|
51
|
+
Answer these five questions:
|
|
52
|
+
|
|
53
|
+
1. Which existing operation am I preserving?
|
|
54
|
+
2. Does the final write belong to Ablo or to the application?
|
|
55
|
+
3. Is the coordination identity a model row or only a stable business id?
|
|
56
|
+
4. Which exact rows influenced the decision?
|
|
57
|
+
5. Which writes must land together?
|
|
58
|
+
|
|
59
|
+
If an answer is unknown, preserve the existing operation and database boundary.
|
|
60
|
+
Do not introduce claims, captured reads, or atomic commits merely because they
|
|
61
|
+
exist.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Ablo implementation index
|
|
2
|
+
|
|
3
|
+
> Route an implementation task to the smallest relevant Ablo guide before reading details.
|
|
4
|
+
|
|
5
|
+
Start with [Introduction](./index.md), then [Choose the Ablo operation](./implement.md). Its one routing table
|
|
6
|
+
separates ordinary reads, existing application writes, claims, captured reads,
|
|
7
|
+
atomic commits, retries, GraphQL, and external effects. Do not scan every page.
|
|
8
|
+
|
|
9
|
+
| Your task | Read first |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Choose among nearby SDK and ownership patterns | [Choose the Ablo operation](./implement.md) |
|
|
12
|
+
| Add Ablo to existing work without replacing its API, transaction, filesystem write, or Git merge | [Coordinate existing work](./coordinate-existing-work.md) |
|
|
13
|
+
| Install Ablo and create a typed client | [Installation](./installation.md) |
|
|
14
|
+
| Read, write, and coordinate shared state | [Basic usage](./basic-usage.md) |
|
|
15
|
+
| Choose between a plain read, guarded read, claim, or atomic commit | [Concurrency convention](./concurrency-convention.md) |
|
|
16
|
+
| Look up an exact method, option, or error type | [API reference](./api.md) |
|
|
17
|
+
| Connect Ablo to an existing Postgres database | [Integration guide](./integration-guide.md) |
|
|
18
|
+
|
|
19
|
+
Follow links from that page only when its routing rule applies. Examples prove a
|
|
20
|
+
specific integration; they are not required reading for a first implementation.
|