@abloatai/ablo 0.50.0 → 0.52.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 +1 -1
- package/CHANGELOG.md +103 -14
- package/NOTICE +1 -1
- package/docs/agents.md +28 -28
- package/docs/api-keys.md +30 -3
- package/docs/api.md +1 -1
- package/docs/cli.md +3 -3
- package/docs/concurrency-convention.md +7 -7
- package/docs/context.md +11 -11
- package/docs/coordination.md +19 -19
- package/docs/customer-organizations.md +215 -0
- package/docs/data-sources.md +3 -3
- package/docs/debugging.md +8 -8
- package/docs/examples/agent-human.md +17 -17
- package/docs/examples/ai-sdk-tool.md +6 -6
- package/docs/examples/existing-python-backend.md +1 -1
- package/docs/examples/nextjs.md +15 -15
- package/docs/examples/scoped-agent.md +10 -10
- package/docs/examples/server-agent.md +14 -14
- package/docs/groups.md +12 -12
- package/docs/how-it-works.md +7 -7
- package/docs/idempotency.md +4 -4
- package/docs/identity.md +45 -38
- package/docs/index.md +2 -1
- package/docs/integration-guide.md +1 -1
- package/docs/integrations/inngest.md +2 -2
- package/docs/integrations/temporal.md +3 -3
- package/docs/integrations.md +3 -3
- package/docs/react.md +4 -4
- package/docs/sessions.md +45 -28
- package/docs/webhooks.md +2 -2
- package/examples/README.md +6 -6
- package/examples/agent-turn.ts +11 -11
- package/examples/data-source/README.md +3 -3
- package/examples/data-source/customer-server.ts +23 -23
- package/examples/data-source/run.ts +8 -8
- package/examples/data-source/schema.ts +2 -2
- package/package.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -90,7 +90,7 @@ Claims live on a callable namespace beside `create` / `update` / `get`. Every me
|
|
|
90
90
|
Keep admission behavior together for anything beyond the default wait:
|
|
91
91
|
|
|
92
92
|
```ts
|
|
93
|
-
const claim = await ablo.
|
|
93
|
+
const claim = await ablo.records.claim({
|
|
94
94
|
id,
|
|
95
95
|
contention: {
|
|
96
96
|
mode: 'skip', // use 'wait' with maxDepth / timeoutMs when waiting is useful
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.52.0
|
|
4
|
+
|
|
5
|
+
### Models carry only `id`
|
|
6
|
+
|
|
7
|
+
`createdAt`, `updatedAt`, `organizationId`, and `createdBy` are no longer added
|
|
8
|
+
to every model. Declare them as ordinary fields wherever you want them, and
|
|
9
|
+
declare them to keep reading and writing them if you relied on Ablo supplying
|
|
10
|
+
them. Ablo still records who made each change in its own transaction log, and
|
|
11
|
+
still owns the tenancy value on every write.
|
|
12
|
+
|
|
13
|
+
A model can point at a table Ablo did not create, naming the columns that
|
|
14
|
+
differ:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { defineSchema, field, model } from '@abloatai/ablo/schema';
|
|
18
|
+
|
|
19
|
+
export const schema = defineSchema({
|
|
20
|
+
itemEvents: model(
|
|
21
|
+
{
|
|
22
|
+
itemId: field.string().from('item_id'),
|
|
23
|
+
createdAt: field.number().from('created_at'),
|
|
24
|
+
},
|
|
25
|
+
{ tableName: 'item_events' }
|
|
26
|
+
),
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Database adapters accept identifiers the database generates and return them as
|
|
31
|
+
canonical string ids, taking the id type from the connection rather than from
|
|
32
|
+
the model.
|
|
33
|
+
|
|
34
|
+
### Updates can carry a precondition
|
|
35
|
+
|
|
36
|
+
An update operation accepts `where`. The database changes the row only while its
|
|
37
|
+
current values still match. On a mismatch the commit fails with
|
|
38
|
+
`precondition_failed` and the whole batch declines, leaving every operation in it
|
|
39
|
+
unapplied. The Kysely source adapter supports preconditions; the Drizzle,
|
|
40
|
+
Prisma, and memory adapters report `source_adapter_misconfigured`.
|
|
41
|
+
|
|
42
|
+
### Commit receipts return the rows the database wrote
|
|
43
|
+
|
|
44
|
+
Receipts carry `operationResults`, pairing each operation's `transactionId` with
|
|
45
|
+
its outcome and the authoritative row the database transaction returned,
|
|
46
|
+
including identifiers and timestamps the database generated.
|
|
47
|
+
|
|
48
|
+
### Two error codes renamed
|
|
49
|
+
|
|
50
|
+
`task_id_missing` is now `item_id_missing`, and `task_id_required` is now
|
|
51
|
+
`item_id_required`. Neither old code was ever returned by a request, so a caller
|
|
52
|
+
matching on error codes has nothing to change unless it names one directly.
|
|
53
|
+
|
|
54
|
+
### CLI
|
|
55
|
+
|
|
56
|
+
`ablo setup` reads the repository and the current Ablo target, then prints the
|
|
57
|
+
decisions, actions, blockers, and postconditions a verified setup requires. It
|
|
58
|
+
reports and leaves the project untouched.
|
|
59
|
+
|
|
60
|
+
`ablo init --plan` shows every file action before any of it happens.
|
|
61
|
+
|
|
62
|
+
`ablo telemetry` controls limited CLI usage analytics. Collection is on by
|
|
63
|
+
default and stays off in continuous integration and whenever `DO_NOT_TRACK=1` or
|
|
64
|
+
`ABLO_TELEMETRY_DISABLED=1` is set. Run `ablo telemetry status` to see the
|
|
65
|
+
current state, `ablo telemetry disable` to turn collection off, and
|
|
66
|
+
`ablo telemetry reset` to rotate the local installation identity.
|
|
67
|
+
|
|
68
|
+
## 0.51.0
|
|
69
|
+
|
|
70
|
+
### One platform schema can serve every customer organization
|
|
71
|
+
|
|
72
|
+
Platforms no longer need to copy the same schema into every customer
|
|
73
|
+
organization. When a platform key creates a session for another organization,
|
|
74
|
+
Ablo now reads the schema from the platform's project while keeping every row
|
|
75
|
+
inside the customer's organization.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const { token } = await ablo.sessions.create({
|
|
79
|
+
user: { id: userId },
|
|
80
|
+
organizationId: customerOrganizationId,
|
|
81
|
+
can: { records: ['read', 'update'] },
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Most platforms need no schema option at all. Migrations and advanced routing can
|
|
86
|
+
still select one explicitly with `schemaProject`.
|
|
87
|
+
|
|
88
|
+
The sessions guide now draws a firm line between policy-scoped customers and
|
|
89
|
+
separate customer organizations. Sync groups decide which changes travel; they
|
|
90
|
+
do not authorize reads.
|
|
91
|
+
|
|
3
92
|
## 0.50.0
|
|
4
93
|
|
|
5
94
|
### Context can travel from reads to a model and back to a write
|
|
@@ -12,14 +101,14 @@ and carries exact Ablo rows into `ctx.reads` for the write that follows.
|
|
|
12
101
|
const ctx = await context({
|
|
13
102
|
ablo,
|
|
14
103
|
data: {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
memory: loadMemories(
|
|
104
|
+
record: ablo.records.get({ id: recordId }),
|
|
105
|
+
records: ablo.records.list({ where: { recordId } }),
|
|
106
|
+
memory: loadMemories(recordId),
|
|
18
107
|
},
|
|
19
108
|
});
|
|
20
109
|
|
|
21
|
-
await ablo.
|
|
22
|
-
id:
|
|
110
|
+
await ablo.records.update({
|
|
111
|
+
id: recordId,
|
|
23
112
|
data: result,
|
|
24
113
|
reads: ctx.reads,
|
|
25
114
|
});
|
|
@@ -45,11 +134,11 @@ write lands anyway, on top of a decision that is no longer true. Pass the rows
|
|
|
45
134
|
the decision was based on:
|
|
46
135
|
|
|
47
136
|
```ts
|
|
48
|
-
const
|
|
49
|
-
await ablo.
|
|
50
|
-
id:
|
|
51
|
-
data: { status: 'done', result: `Completed: ${
|
|
52
|
-
reads: [
|
|
137
|
+
const record = await ablo.records.get({ id: recordId });
|
|
138
|
+
await ablo.records.update({
|
|
139
|
+
id: record.id,
|
|
140
|
+
data: { status: 'done', result: `Completed: ${record.title}` },
|
|
141
|
+
reads: [record],
|
|
53
142
|
});
|
|
54
143
|
```
|
|
55
144
|
|
|
@@ -70,7 +159,7 @@ turn or skips, expires on its own, and keeps itself alive with a heartbeat while
|
|
|
70
159
|
the agent works.
|
|
71
160
|
|
|
72
161
|
If an agent loses its claim during a model call, its final write is refused. Two
|
|
73
|
-
agents cannot both believe they own the same
|
|
162
|
+
agents cannot both believe they own the same record and both write, and a slow
|
|
74
163
|
agent cannot land its answer on top of whoever picked the work up after it. Ablo
|
|
75
164
|
decides who holds the claim, so an agent cannot assert one it does not have.
|
|
76
165
|
|
|
@@ -95,7 +184,7 @@ database you connected.
|
|
|
95
184
|
|
|
96
185
|
An agent holding a key can now ask what that key permits and get the answer from
|
|
97
186
|
Ablo, whether it keeps a connection open or calls over HTTP for a single turn.
|
|
98
|
-
An agent that mints a narrower key for a sub-
|
|
187
|
+
An agent that mints a narrower key for a sub-record can confirm what it handed
|
|
99
188
|
over.
|
|
100
189
|
|
|
101
190
|
### A refused action says which permission was missing
|
|
@@ -288,7 +377,7 @@ New clients can pin their intended project and branch with `projectId` /
|
|
|
288
377
|
`ABLO_PROJECT_ID` and `branchId` / `ABLO_BRANCH_ID`. `ablo dev` writes both
|
|
289
378
|
immutable coordinates beside its branch-bound key, and `ready()` compares them
|
|
290
379
|
with the key's server-resolved target before opening the sync connection. A mail
|
|
291
|
-
deployment carrying
|
|
380
|
+
deployment carrying an unrelated key now fails with `project_scope_denied`; a
|
|
292
381
|
same-project key for the wrong environment fails with `branch_scope_denied`.
|
|
293
382
|
|
|
294
383
|
### Pre-existing rows arrive on their own
|
|
@@ -331,7 +420,7 @@ end-to-end regression pins the behavior.
|
|
|
331
420
|
The new `contention` option names what happens when the row is already held:
|
|
332
421
|
|
|
333
422
|
```ts
|
|
334
|
-
const claim = await ablo.
|
|
423
|
+
const claim = await ablo.records.claim({
|
|
335
424
|
id,
|
|
336
425
|
contention: {
|
|
337
426
|
mode: 'skip',
|
package/NOTICE
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Copyright 2025-2026 Lukas Andersson
|
|
3
3
|
|
|
4
4
|
This product includes software developed by Lukas Andersson
|
|
5
|
-
(https://
|
|
5
|
+
(https://abloatai.com).
|
|
6
6
|
|
|
7
7
|
"Ablo" is a trademark of Lukas Andersson. This license does not grant
|
|
8
8
|
permission to use the Ablo name, logo, or trademarks. Third parties
|
package/docs/agents.md
CHANGED
|
@@ -14,8 +14,8 @@ plugin — get the live plane (WebSocket: presence, optimistic, sub-100ms).
|
|
|
14
14
|
other*.**
|
|
15
15
|
|
|
16
16
|
<Note>
|
|
17
|
-
Agents transact against your **pushed schema**, same as everyone — `ablo.
|
|
18
|
-
exists because you defined a `
|
|
17
|
+
Agents transact against your **pushed schema**, same as everyone — `ablo.records`
|
|
18
|
+
exists because you defined a `record` model and ran `ablo push`. The key
|
|
19
19
|
authenticates; the [schema](/quickstart) defines what you can call.
|
|
20
20
|
</Note>
|
|
21
21
|
|
|
@@ -32,13 +32,13 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: "http"
|
|
|
32
32
|
|
|
33
33
|
// Reads + writes, fully typed off your schema.
|
|
34
34
|
// `get` resolves to the row, or `undefined` when none matches.
|
|
35
|
-
const open = await ablo.
|
|
35
|
+
const open = await ablo.records.list({ where: { status: "todo" } });
|
|
36
36
|
|
|
37
|
-
const
|
|
38
|
-
if (!
|
|
37
|
+
const record = await ablo.records.get({ id: open[0].id });
|
|
38
|
+
if (!record) throw new Error("record not found");
|
|
39
39
|
|
|
40
|
-
console.log(
|
|
41
|
-
await ablo.
|
|
40
|
+
console.log(record.title);
|
|
41
|
+
await ablo.records.update({ id: record.id, data: { status: "done" } });
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
It exposes `get` / `list` / `create` / `update` / `delete`, plus `commits`
|
|
@@ -61,27 +61,27 @@ import {
|
|
|
61
61
|
} from '@abloatai/ablo/ai-sdk';
|
|
62
62
|
|
|
63
63
|
const tools = {
|
|
64
|
-
getTask: readTool(ablo.
|
|
65
|
-
description: 'Read the current
|
|
66
|
-
inputSchema: z.object({
|
|
67
|
-
id: ({
|
|
64
|
+
getTask: readTool(ablo.records, {
|
|
65
|
+
description: 'Read the current record.',
|
|
66
|
+
inputSchema: z.object({ recordId: z.string() }),
|
|
67
|
+
id: ({ recordId }) => recordId,
|
|
68
68
|
}),
|
|
69
|
-
createTask: createTool(ablo.
|
|
70
|
-
description: 'Create a
|
|
69
|
+
createTask: createTool(ablo.records, {
|
|
70
|
+
description: 'Create a record.',
|
|
71
71
|
inputSchema: z.object({ requestId: z.string(), title: z.string() }),
|
|
72
72
|
id: ({ requestId }) => requestId,
|
|
73
73
|
data: ({ title }) => ({ title, status: 'todo' }),
|
|
74
74
|
}),
|
|
75
|
-
updateTask: updateTool(ablo.
|
|
76
|
-
description: 'Update a
|
|
77
|
-
inputSchema: z.object({
|
|
78
|
-
id: ({
|
|
75
|
+
updateTask: updateTool(ablo.records, {
|
|
76
|
+
description: 'Update a record without overwriting concurrent work.',
|
|
77
|
+
inputSchema: z.object({ recordId: z.string(), status: z.string() }),
|
|
78
|
+
id: ({ recordId }) => recordId,
|
|
79
79
|
apply: (_current, { status }) => ({ status }),
|
|
80
80
|
}),
|
|
81
|
-
deleteTask: deleteTool(ablo.
|
|
82
|
-
description: 'Delete a
|
|
83
|
-
inputSchema: z.object({
|
|
84
|
-
id: ({
|
|
81
|
+
deleteTask: deleteTool(ablo.records, {
|
|
82
|
+
description: 'Delete a record after taking its claim.',
|
|
83
|
+
inputSchema: z.object({ recordId: z.string() }),
|
|
84
|
+
id: ({ recordId }) => recordId,
|
|
85
85
|
// Destructive tools require AI SDK approval by default.
|
|
86
86
|
}),
|
|
87
87
|
};
|
|
@@ -102,18 +102,18 @@ clobber the same record.
|
|
|
102
102
|
|
|
103
103
|
```ts
|
|
104
104
|
// Acquire a lease, do work with the held row, release on scope exit:
|
|
105
|
-
await using claim = await ablo.
|
|
106
|
-
const
|
|
105
|
+
await using claim = await ablo.records.claim({ id: recordId });
|
|
106
|
+
const record = claim.data;
|
|
107
107
|
// …no one else can hold this row while you work…
|
|
108
|
-
await ablo.
|
|
109
|
-
id:
|
|
108
|
+
await ablo.records.update({
|
|
109
|
+
id: record.id,
|
|
110
110
|
data: { status: "in_review" },
|
|
111
111
|
claim,
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
await ablo.
|
|
115
|
-
await ablo.
|
|
116
|
-
await ablo.
|
|
114
|
+
await ablo.records.claim.state({ id: recordId }); // who holds it now (or null)
|
|
115
|
+
await ablo.records.claim.queue({ id: recordId }); // the FIFO wait-line behind the holder
|
|
116
|
+
await ablo.records.claim.reorder({ id: recordId, order: line }); // re-rank the line (privileged)
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
Think of it as a queue per row — a durable, inspectable, reorderable lease
|
package/docs/api-keys.md
CHANGED
|
@@ -119,9 +119,9 @@ knobs, and you set exactly one.
|
|
|
119
119
|
|
|
120
120
|
| Mint | Call | Result |
|
|
121
121
|
|---|---|---|
|
|
122
|
-
| Human end-user session | `await server.sessions.create({ user: { id }, can: {
|
|
123
|
-
| Ready agent client | `await server.agents.create({ can: {
|
|
124
|
-
| Raw delegated agent token | `await server.sessions.create({ agent: { id }, can: {
|
|
122
|
+
| Human end-user session | `await server.sessions.create({ user: { id }, can: { records: ['read'] } })` | `ek_` (scoped to `can`) |
|
|
123
|
+
| Ready agent client | `await server.agents.create({ can: { records: ['update'] } })` | Auto-refreshing client scoped to `can` |
|
|
124
|
+
| Raw delegated agent token | `await server.sessions.create({ agent: { id }, can: { records: ['update'] } })` | `rk_` for another runtime |
|
|
125
125
|
|
|
126
126
|
The principal kind comes from *which* shape you pass — `{ user, can }` → `user`, `{ agent, can }` → `agent`.
|
|
127
127
|
|
|
@@ -217,6 +217,12 @@ restricted to exactly those grants:
|
|
|
217
217
|
- `project:manage` — list, create, and rename projects.
|
|
218
218
|
- `branch:manage` — list, create, and delete child branches and mint their
|
|
219
219
|
temporary credentials.
|
|
220
|
+
- `ephemeral:mint-any-org` — cross-organization authority to mint a short-lived
|
|
221
|
+
user session into a customer organization. It follows the Stripe Connect shape:
|
|
222
|
+
the request names the customer organization, but the resulting session is
|
|
223
|
+
still bounded by its `can` grant and expiry. A key restricted to this scope
|
|
224
|
+
cannot directly read or write customer organizations' rows, push schema, or
|
|
225
|
+
manage projects.
|
|
220
226
|
|
|
221
227
|
Both management scopes are explicit grants on `mk_` credentials. Runtime
|
|
222
228
|
`sk_`, `rk_`, `pk_`, and `ek_` credentials cannot become management
|
|
@@ -226,6 +232,27 @@ Branch binding remains an authority boundary even when a key has no granular
|
|
|
226
232
|
scope strings: a temporary child key can act only inside that child. It cannot
|
|
227
233
|
manage siblings or gain root authority.
|
|
228
234
|
|
|
235
|
+
### Cross-organization mint keys
|
|
236
|
+
|
|
237
|
+
Most applications do not need `ephemeral:mint-any-org`: their backend key mints
|
|
238
|
+
users into its own organization. A multi-organization backend needs it only
|
|
239
|
+
when each customer is a separate Ablo organization and one trusted service
|
|
240
|
+
mints sessions for all of them.
|
|
241
|
+
|
|
242
|
+
Treat that key as a dedicated minting credential:
|
|
243
|
+
|
|
244
|
+
- keep it in a server-side secret manager, never a browser or repository;
|
|
245
|
+
- grant only `ephemeral:mint-any-org`, with no data or schema scopes;
|
|
246
|
+
- mint short-lived sessions with the smallest typed `can` grant;
|
|
247
|
+
- rotate it on a schedule and revoke it immediately after suspected exposure;
|
|
248
|
+
- log the target `organizationId`, minted session id, and request id for audit.
|
|
249
|
+
|
|
250
|
+
The scope's broad name describes the cross-organization check it passes, not
|
|
251
|
+
the authority of each resulting session. The session can act only inside the
|
|
252
|
+
named customer organization and only for the models/verbs in `can`. See
|
|
253
|
+
[Customer Organizations](./customer-organizations.md) for the complete
|
|
254
|
+
integration.
|
|
255
|
+
|
|
229
256
|
## Current and legacy key spellings
|
|
230
257
|
|
|
231
258
|
New credentials use one spelling per capability class:
|
package/docs/api.md
CHANGED
|
@@ -173,7 +173,7 @@ The SDK is a convenience wrapper over a model-scoped HTTP surface — the same
|
|
|
173
173
|
noun (`model`) and verbs as `ablo.<model>.…`. Non-JS callers (or curl) use it
|
|
174
174
|
directly. The table below shows the shape with `{model}` as a placeholder; the
|
|
175
175
|
[OpenAPI spec](./openapi.json) expands it into one **typed** path per model
|
|
176
|
-
(`/api/v1/models/
|
|
176
|
+
(`/api/v1/models/record`, `/api/v1/models/workspace`, …, generated from your schema) so each
|
|
177
177
|
endpoint documents that model's real field contract instead of a generic blob.
|
|
178
178
|
|
|
179
179
|
| SDK call | HTTP |
|
package/docs/cli.md
CHANGED
|
@@ -189,7 +189,7 @@ streams production. You never pass a project or branch. Follows by default;
|
|
|
189
189
|
|
|
190
190
|
```bash
|
|
191
191
|
npx ablo logs # last 50, then stream
|
|
192
|
-
npx ablo logs -n 100 --model
|
|
192
|
+
npx ablo logs -n 100 --model record # backfill 100, one model
|
|
193
193
|
npx ablo logs --since 15m --json # last 15m as NDJSON, then stream
|
|
194
194
|
```
|
|
195
195
|
|
|
@@ -237,7 +237,7 @@ DATABASE_URL=postgres://… npx ablo check
|
|
|
237
237
|
```
|
|
238
238
|
|
|
239
239
|
```text
|
|
240
|
-
✓
|
|
240
|
+
✓ records → records (id, organization_id ok)
|
|
241
241
|
✗ projects → projects
|
|
242
242
|
• missing "organization_id" — add it, or move this model behind a Data Source
|
|
243
243
|
2 models · 1 ok · 1 error
|
|
@@ -308,7 +308,7 @@ that broke and the Postgres SQLSTATE, not just "migration failed".
|
|
|
308
308
|
```txt
|
|
309
309
|
[migrate] migration plan failed {
|
|
310
310
|
code: 'migration_failed',
|
|
311
|
-
failedStatement: 'ALTER TABLE "public"."
|
|
311
|
+
failedStatement: 'ALTER TABLE "public"."records" RENAME COLUMN a TO b;',
|
|
312
312
|
failedStatementIndex: 4,
|
|
313
313
|
pgCode: '42P01',
|
|
314
314
|
durationMs: 133
|
|
@@ -13,7 +13,7 @@ you declared and nothing else.
|
|
|
13
13
|
A plain write has no stale premise:
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
await ablo.
|
|
16
|
+
await ablo.records.update({ id, data: { status: 'done' } });
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
If no active claim conflicts with it, the write is last-write-wins. That is a
|
|
@@ -26,14 +26,14 @@ once in its `conflict` setting instead of at every call site.
|
|
|
26
26
|
Pass the exact returned rows when a write is based on values previously read:
|
|
27
27
|
|
|
28
28
|
```ts
|
|
29
|
-
const
|
|
29
|
+
const record = await ablo.records.get({ id });
|
|
30
30
|
const policy = await ablo.policies.get({ id: policyId });
|
|
31
|
-
if (!
|
|
31
|
+
if (!record || !policy) throw new Error('required input is missing');
|
|
32
32
|
|
|
33
|
-
await ablo.
|
|
34
|
-
id:
|
|
33
|
+
await ablo.records.update({
|
|
34
|
+
id: record.id,
|
|
35
35
|
data: { status: 'done' },
|
|
36
|
-
reads: [
|
|
36
|
+
reads: [record, policy],
|
|
37
37
|
});
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -83,7 +83,7 @@ See [Coordination](./coordination.md#claims) for the API.
|
|
|
83
83
|
## Cross-row and batch premises
|
|
84
84
|
|
|
85
85
|
Model writes and lower-level commits can declare rows they read even when the
|
|
86
|
-
write targets somewhere else. This protects decisions such as “update the
|
|
86
|
+
write targets somewhere else. This protects decisions such as “update the record
|
|
87
87
|
only if the deal I inspected has not changed.” A stale batch premise applies to
|
|
88
88
|
the whole batch so atomicity is preserved.
|
|
89
89
|
|
package/docs/context.md
CHANGED
|
@@ -21,13 +21,13 @@ import { generateText } from 'ai';
|
|
|
21
21
|
const ctx = await context({
|
|
22
22
|
ablo,
|
|
23
23
|
data: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
memory: loadMemories(
|
|
24
|
+
record: ablo.records.get({ id: recordId }),
|
|
25
|
+
records: ablo.records.list({ where: { recordId } }),
|
|
26
|
+
memory: loadMemories(recordId),
|
|
27
27
|
},
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
if (!ctx.data.
|
|
30
|
+
if (!ctx.data.record) throw new Error('Record not found');
|
|
31
31
|
|
|
32
32
|
const result = await generateText({
|
|
33
33
|
model,
|
|
@@ -35,8 +35,8 @@ const result = await generateText({
|
|
|
35
35
|
tools,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
await ablo.
|
|
39
|
-
id: ctx.data.
|
|
38
|
+
await ablo.records.update({
|
|
39
|
+
id: ctx.data.record.id,
|
|
40
40
|
data: parseTaskUpdate(result.text),
|
|
41
41
|
reads: ctx.reads,
|
|
42
42
|
});
|
|
@@ -80,7 +80,7 @@ This distinction is visible in `sources`:
|
|
|
80
80
|
```ts
|
|
81
81
|
ctx.sources;
|
|
82
82
|
// [
|
|
83
|
-
// { key: '
|
|
83
|
+
// { key: 'record', kind: 'ablo', guarantee: 'guardable', cursor: 42 },
|
|
84
84
|
// { key: 'memory', kind: 'value', guarantee: 'informational', cursor: null },
|
|
85
85
|
// ]
|
|
86
86
|
```
|
|
@@ -89,7 +89,7 @@ A top-level value may contain both kinds. It is then marked `mixed` and only
|
|
|
89
89
|
its exact Ablo rows appear in `ctx.reads`:
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
|
-
// data: { briefing: {
|
|
92
|
+
// data: { briefing: { record, memory } }
|
|
93
93
|
// sources: [
|
|
94
94
|
// { key: 'briefing', kind: 'mixed', guarantee: 'partial', cursor: 42 },
|
|
95
95
|
// ]
|
|
@@ -108,10 +108,10 @@ Reducto, or another system behind their own interfaces.
|
|
|
108
108
|
const ctx = await context({
|
|
109
109
|
ablo,
|
|
110
110
|
data: {
|
|
111
|
-
|
|
111
|
+
record: ablo.records.get({ id: recordId }),
|
|
112
112
|
memory: loadMemories({ query, userId }),
|
|
113
113
|
related: findRelatedChunks({ projectId, query }),
|
|
114
|
-
evidence: extractEvidence({
|
|
114
|
+
evidence: extractEvidence({ recordId }),
|
|
115
115
|
},
|
|
116
116
|
});
|
|
117
117
|
```
|
|
@@ -141,7 +141,7 @@ await generateText({
|
|
|
141
141
|
model,
|
|
142
142
|
messages: [
|
|
143
143
|
...history,
|
|
144
|
-
contextMessage(ctx, { include: ['
|
|
144
|
+
contextMessage(ctx, { include: ['record', 'documents', 'memory'] }),
|
|
145
145
|
],
|
|
146
146
|
tools,
|
|
147
147
|
});
|
package/docs/coordination.md
CHANGED
|
@@ -9,7 +9,7 @@ meaning. Choose the narrowest one that matches the operation.
|
|
|
9
9
|
|---|---|---|
|
|
10
10
|
| Set an independent value | `update({ id, data })` | Last-write-wins when no claim applies. |
|
|
11
11
|
| Compute a value from the current row | `update(id, current => next)` | Re-reads and retries if the row changes concurrently. |
|
|
12
|
-
| Write only if earlier rows are still current | `reads: [
|
|
12
|
+
| Write only if earlier rows are still current | `reads: [record, policy]` | Rejects when an explicitly named dependency changed. |
|
|
13
13
|
| Read, call a model, then write | `claim({ id })` | Other participants cannot write the claimed target by default until your claim ends. |
|
|
14
14
|
|
|
15
15
|
**If a model call sits between the read and the write, take a claim.** A stale
|
|
@@ -25,16 +25,16 @@ does not carry a stale premise. It is intentionally last-write-wins.
|
|
|
25
25
|
Pass the exact rows that produced a decision on the write:
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
|
-
const
|
|
28
|
+
const record = await ablo.records.get({ id: recordId });
|
|
29
29
|
const policy = await ablo.policies.get({ id: policyId });
|
|
30
|
-
if (!
|
|
30
|
+
if (!record || !policy) throw new Error('required input is missing');
|
|
31
31
|
|
|
32
|
-
const result = await model({
|
|
32
|
+
const result = await model({ record, policy });
|
|
33
33
|
|
|
34
|
-
await ablo.
|
|
35
|
-
id:
|
|
34
|
+
await ablo.records.update({
|
|
35
|
+
id: record.id,
|
|
36
36
|
data: result,
|
|
37
|
-
reads: [
|
|
37
|
+
reads: [record, policy],
|
|
38
38
|
});
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -57,7 +57,7 @@ When the next value is a function of the current one, pass an updater rather
|
|
|
57
57
|
than fixed data:
|
|
58
58
|
|
|
59
59
|
```ts
|
|
60
|
-
const document = await ablo.
|
|
60
|
+
const document = await ablo.records.update(recordId, (current) => ({
|
|
61
61
|
revision: current.revision + 1,
|
|
62
62
|
content: revise(current.content),
|
|
63
63
|
}));
|
|
@@ -75,8 +75,8 @@ effect inside it.
|
|
|
75
75
|
You can bound or cancel reconciliation:
|
|
76
76
|
|
|
77
77
|
```ts
|
|
78
|
-
await ablo.
|
|
79
|
-
|
|
78
|
+
await ablo.records.update(
|
|
79
|
+
recordId,
|
|
80
80
|
(current) => ({ revision: current.revision + 1 }),
|
|
81
81
|
{ retries: 8, signal: request.signal },
|
|
82
82
|
);
|
|
@@ -184,8 +184,8 @@ protect the row version rather than a participant-held claim.
|
|
|
184
184
|
For deduplicated jobs, skip work when another participant already owns it:
|
|
185
185
|
|
|
186
186
|
```ts
|
|
187
|
-
const claim = await ablo.
|
|
188
|
-
id:
|
|
187
|
+
const claim = await ablo.records.claim({
|
|
188
|
+
id: recordId,
|
|
189
189
|
contention: { mode: 'skip' },
|
|
190
190
|
});
|
|
191
191
|
|
|
@@ -201,8 +201,8 @@ try {
|
|
|
201
201
|
To wait with limits, keep the policy together:
|
|
202
202
|
|
|
203
203
|
```ts
|
|
204
|
-
const claim = await ablo.
|
|
205
|
-
id:
|
|
204
|
+
const claim = await ablo.records.claim({
|
|
205
|
+
id: recordId,
|
|
206
206
|
contention: {
|
|
207
207
|
mode: 'wait',
|
|
208
208
|
maxDepth: 3,
|
|
@@ -217,9 +217,9 @@ const claim = await ablo.tasks.claim({
|
|
|
217
217
|
Narrow a claim when independent fields may be edited concurrently:
|
|
218
218
|
|
|
219
219
|
```ts
|
|
220
|
-
await using claim = await ablo.
|
|
221
|
-
id:
|
|
222
|
-
fields: (
|
|
220
|
+
await using claim = await ablo.records.claim({
|
|
221
|
+
id: recordId,
|
|
222
|
+
fields: (record) => record.status,
|
|
223
223
|
});
|
|
224
224
|
```
|
|
225
225
|
|
|
@@ -239,8 +239,8 @@ The target options are:
|
|
|
239
239
|
Read current claim state without blocking:
|
|
240
240
|
|
|
241
241
|
```ts
|
|
242
|
-
const holder = ablo.
|
|
243
|
-
const queue = ablo.
|
|
242
|
+
const holder = ablo.records.claim.state({ id: recordId });
|
|
243
|
+
const queue = ablo.records.claim.queue({ id: recordId });
|
|
244
244
|
```
|
|
245
245
|
|
|
246
246
|
Use this state for presence and progress UI. Do not use an observed `null` as a
|