@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/dist/source-drizzle.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/drizzle';
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters/drizzle';
|
|
2
2
|
//# sourceMappingURL=source-drizzle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-drizzle.d.ts","sourceRoot":"","sources":["../src/source-drizzle.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-drizzle.d.ts","sourceRoot":"","sources":["../src/source-drizzle.ts"],"names":[],"mappings":"AAAA,cAAc,+CAA+C,CAAC"}
|
package/dist/source-drizzle.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/drizzle';
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters/drizzle';
|
|
2
2
|
//# sourceMappingURL=source-drizzle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-drizzle.js","sourceRoot":"","sources":["../src/source-drizzle.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-drizzle.js","sourceRoot":"","sources":["../src/source-drizzle.ts"],"names":[],"mappings":"AAAA,cAAc,+CAA+C,CAAC"}
|
package/dist/source-kysely.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/kysely';
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters/kysely';
|
|
2
2
|
//# sourceMappingURL=source-kysely.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-kysely.d.ts","sourceRoot":"","sources":["../src/source-kysely.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-kysely.d.ts","sourceRoot":"","sources":["../src/source-kysely.ts"],"names":[],"mappings":"AAAA,cAAc,8CAA8C,CAAC"}
|
package/dist/source-kysely.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/kysely';
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters/kysely';
|
|
2
2
|
//# sourceMappingURL=source-kysely.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-kysely.js","sourceRoot":"","sources":["../src/source-kysely.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-kysely.js","sourceRoot":"","sources":["../src/source-kysely.ts"],"names":[],"mappings":"AAAA,cAAc,8CAA8C,CAAC"}
|
package/dist/source-next.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/
|
|
1
|
+
export * from '@abloatai/transaction/source/endpoint';
|
|
2
2
|
//# sourceMappingURL=source-next.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-next.d.ts","sourceRoot":"","sources":["../src/source-next.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-next.d.ts","sourceRoot":"","sources":["../src/source-next.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAC"}
|
package/dist/source-next.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/
|
|
1
|
+
export * from '@abloatai/transaction/source/endpoint';
|
|
2
2
|
//# sourceMappingURL=source-next.js.map
|
package/dist/source-next.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-next.js","sourceRoot":"","sources":["../src/source-next.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-next.js","sourceRoot":"","sources":["../src/source-next.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAC"}
|
package/docs/agents.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
> The stateless participant: wake on a trigger, read, claim, commit, go idle.
|
|
4
4
|
|
|
5
|
+
## Stateless HTTP reads
|
|
6
|
+
|
|
7
|
+
Use `get` to read one task row by id and `list` to find matching rows. Agents
|
|
8
|
+
and other stateless workers use the HTTP client directly, without a
|
|
9
|
+
synchronization step or a `.data` wrapper.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const task = await ablo.tasks.get({ id: taskId });
|
|
13
|
+
if (!task) throw new Error('task not found');
|
|
14
|
+
console.log(task.title);
|
|
15
|
+
|
|
16
|
+
const matching = await ablo.tasks.list({ where: { title } });
|
|
17
|
+
if (!matching[0]) throw new Error('task not found');
|
|
18
|
+
console.log(matching[0].title);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
These are observational reads. Use `read({ id })` only when a later Ablo write
|
|
22
|
+
depends on that exact version and will pass it through `reads`.
|
|
23
|
+
|
|
5
24
|
An agent is a **reactive** participant: it wakes on something happening, reads
|
|
6
25
|
what it needs, writes a result, and goes idle. That's a request/response
|
|
7
26
|
workload — so agents talk to Ablo over **plain HTTP**, holding no WebSocket. The
|
|
@@ -16,7 +35,7 @@ other*.**
|
|
|
16
35
|
<Note>
|
|
17
36
|
Agents transact against your **pushed schema**, same as everyone — `ablo.records`
|
|
18
37
|
exists because you defined a `record` model and ran `ablo push`. The key
|
|
19
|
-
authenticates; the [schema](/
|
|
38
|
+
authenticates; the [schema](/installation) defines what you can call.
|
|
20
39
|
</Note>
|
|
21
40
|
|
|
22
41
|
## The agent client
|
|
@@ -34,7 +53,7 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: "http"
|
|
|
34
53
|
// `get` resolves to the row, or `undefined` when none matches.
|
|
35
54
|
const open = await ablo.records.list({ where: { status: "todo" } });
|
|
36
55
|
|
|
37
|
-
const record = await ablo.records.
|
|
56
|
+
const record = await ablo.records.read({ id: open[0].id });
|
|
38
57
|
if (!record) throw new Error("record not found");
|
|
39
58
|
|
|
40
59
|
console.log(record.title);
|
|
@@ -42,9 +61,11 @@ await ablo.records.update({ id: record.id, data: { status: "done" } });
|
|
|
42
61
|
```
|
|
43
62
|
|
|
44
63
|
It exposes `get` / `list` / `create` / `update` / `delete`, plus `commits`
|
|
45
|
-
and `claim`. It does **not** expose stateful-only `local` reads or
|
|
46
|
-
subscriptions. Those need a
|
|
47
|
-
are compile errors
|
|
64
|
+
and `claim`. It does **not** expose stateful-only `local` reads or model
|
|
65
|
+
`onChange` subscriptions. Those need a WebSocket, so with `transport: 'http'`
|
|
66
|
+
they are compile errors. `context().onChange` is separate: while its listener
|
|
67
|
+
is active, it holds one HTTP response open until the context changes or the
|
|
68
|
+
listener stops.
|
|
48
69
|
|
|
49
70
|
## Managed scoped agents
|
|
50
71
|
|
|
@@ -58,7 +79,7 @@ model output or an HTTP request body. A serverless handler normally creates and
|
|
|
58
79
|
disposes one child per invocation:
|
|
59
80
|
|
|
60
81
|
```ts
|
|
61
|
-
const run = await control.runs.
|
|
82
|
+
const run = await control.runs.read({ id: verifiedRunId });
|
|
62
83
|
if (!run) throw new Error('run not found');
|
|
63
84
|
|
|
64
85
|
const agent = await control.agents.create({
|
|
@@ -204,19 +225,21 @@ default caller here, not a bolt-on.
|
|
|
204
225
|
|
|
205
226
|
```text
|
|
206
227
|
something happens ──▶ your agent (HTTP, no socket)
|
|
207
|
-
(a job, a webhook, read context (list/get)
|
|
228
|
+
(a job, a webhook, read context (list/get/read)
|
|
208
229
|
a queue message) claim → work → commit
|
|
209
230
|
done — no held connection
|
|
210
231
|
```
|
|
211
232
|
|
|
212
|
-
|
|
213
|
-
restarts are free, and you scale by adding
|
|
214
|
-
|
|
233
|
+
Without `context().onChange`, an agent holds nothing open and remains a
|
|
234
|
+
**stateless worker**: deploys and restarts are free, and you scale by adding
|
|
235
|
+
workers. Each active context listener is explicit connection capacity and must
|
|
236
|
+
be stopped when its work ends.
|
|
215
237
|
|
|
216
238
|
## What stays on the live (human) plane
|
|
217
239
|
|
|
218
|
-
`onChange`
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
240
|
+
Model `onChange` and `local` reads require a WebSocket and a local store —
|
|
241
|
+
they're for interactive UIs. An HTTP agent normally reacts to an external
|
|
242
|
+
trigger, then reads with `list`/`get`. For costly work, `context().onChange` can
|
|
243
|
+
stop that one run early while the final write still uses `context().reads`.
|
|
244
|
+
See [client behavior](/client-behavior) for the full surface and
|
|
245
|
+
[guarantees](/guarantees) for the coordination semantics.
|
package/docs/api-keys.md
CHANGED
|
@@ -23,9 +23,9 @@ and remap them before each command.
|
|
|
23
23
|
|
|
24
24
|
| Job | Credential | How you get it |
|
|
25
25
|
|---|---|---|
|
|
26
|
-
| Manage a project or its branches | `mk_` | `npx ablo login
|
|
26
|
+
| Manage a project or its branches | `mk_` | `npx ablo login` stores it for the CLI; pick the project in the terminal, or name it with `--project <slug>`. |
|
|
27
27
|
| Develop locally | expiring `sk_` bound to the current branch | `npx ablo dev` writes it as `ABLO_API_KEY` in gitignored `.env.local`. |
|
|
28
|
-
| Prepare a branch once, including CI | expiring `sk_` bound to that branch | `npx ablo dev --no-watch --branch <ref>`; CI supplies `
|
|
28
|
+
| Prepare a branch once, including CI | expiring `sk_` bound to that branch | `npx ablo dev --no-watch --branch <ref>`; headless CI supplies an `mk_` credential through `ABLO_API_KEY`. |
|
|
29
29
|
| Run the production backend | `sk_` bound to the production root | Store it as the deployment's `ABLO_API_KEY`. |
|
|
30
30
|
| Read in a browser | `pk_` | Publishable, read-only key. |
|
|
31
31
|
| Write in a browser as a user | short-lived `ek_` | Your backend exposes `authEndpoint` and mints it. |
|
|
@@ -33,9 +33,9 @@ and remap them before each command.
|
|
|
33
33
|
The everyday loop is therefore:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
npx ablo login
|
|
37
|
-
npx ablo dev
|
|
38
|
-
npx ablo status
|
|
36
|
+
npx ablo login # once per project: approve in the browser, pick the project
|
|
37
|
+
npx ablo dev # follows Git, mints and wires this branch
|
|
38
|
+
npx ablo status # broad readiness report
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Application code and agents still read one variable:
|
|
@@ -91,7 +91,7 @@ The credential class lives in the prefix:
|
|
|
91
91
|
|
|
92
92
|
| Prefix | Purpose | Stored where |
|
|
93
93
|
|---|---|---|
|
|
94
|
-
| `mk_` | project and branch management | CLI credential store
|
|
94
|
+
| `mk_` | project and branch management | CLI credential store; `ABLO_API_KEY` only in headless automation |
|
|
95
95
|
| `sk_` | trusted runtime, full branch authority | server-side `ABLO_API_KEY` |
|
|
96
96
|
| `rk_` | restricted runtime or agent | trusted runtime that needs the delegated scope |
|
|
97
97
|
| `pk_` | publishable, browser-safe read access | browser bundle |
|
package/docs/api.md
CHANGED
|
@@ -12,9 +12,11 @@ confirmed write streams to everyone watching. When two writers touch the same
|
|
|
12
12
|
row, you can optionally `claim` it so they serialize instead of clobbering
|
|
13
13
|
each other.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Three things to know before the method list. **`get` observes; `read` declares.**
|
|
16
|
+
Both fetch one current row, but only the exact object returned by `read({ id })`
|
|
17
|
+
can be carried in a mutation's `reads` array. If it changed, that mutation does
|
|
18
|
+
not land. `get({ id })` and `list({ where })` are ordinary queries with no stale
|
|
19
|
+
guard. **Local reads do not fetch.** Put `local.` in front of a query and
|
|
18
20
|
you get the same read restricted to what is already here, which is why it can
|
|
19
21
|
return a value rather than a promise: `local.get(id)`, `local.list({ where })`,
|
|
20
22
|
`local.count({ where })`. Use those in render, after data has synced.
|
|
@@ -38,10 +40,14 @@ const schema = defineSchema({
|
|
|
38
40
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
39
41
|
|
|
40
42
|
await ablo.ready();
|
|
41
|
-
const report = await ablo.weatherReports.
|
|
43
|
+
const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
|
|
42
44
|
if (!report) throw new Error('Row not found');
|
|
43
45
|
|
|
44
|
-
await ablo.weatherReports.update({
|
|
46
|
+
await ablo.weatherReports.update({
|
|
47
|
+
id: 'report_stockholm',
|
|
48
|
+
data: { status: 'ready' },
|
|
49
|
+
reads: [report],
|
|
50
|
+
});
|
|
45
51
|
```
|
|
46
52
|
|
|
47
53
|
For end-to-end app setup across React, existing backends, Data Source, and
|
|
@@ -51,15 +57,17 @@ agents, read the [Integration Guide](./integration-guide.md).
|
|
|
51
57
|
|
|
52
58
|
Each schema model becomes a typed model on the client:
|
|
53
59
|
|
|
54
|
-
- `ablo.weatherReports.get({ id })`
|
|
55
|
-
- `ablo.weatherReports.
|
|
60
|
+
- `ablo.weatherReports.get({ id })` fetches one row without declaring a decision dependency.
|
|
61
|
+
- `ablo.weatherReports.read({ id })` fetches one guardable decision input.
|
|
62
|
+
- `ablo.weatherReports.list({ where })` fetches an observational collection.
|
|
56
63
|
- `ablo.weatherReports.listAll({ where })` explicitly reads every matching page.
|
|
57
64
|
- `ablo.weatherReports.local.get(id)` reads one row synchronously from the local graph.
|
|
58
65
|
- `ablo.weatherReports.create({ data })` creates a row.
|
|
59
66
|
- `ablo.weatherReports.update({ id, data, ...options })` updates a row.
|
|
60
67
|
- `ablo.weatherReports.delete({ id, ...options })` deletes a row.
|
|
68
|
+
- `ablo.weatherReports.claim({ id, description })` acquires a durable write lease; the HTTP form is awaited.
|
|
61
69
|
|
|
62
|
-
`local.` narrows a
|
|
70
|
+
`local.` narrows a query to what has already synced. `get({ id })`, `read({ id })`, and
|
|
63
71
|
`list({ where })` answer from the local graph and fall back to IndexedDB and
|
|
64
72
|
then the network, so reach for them when the row may not be here yet.
|
|
65
73
|
`local.get(id)` and `local.list({ where })` are the same reads with the
|
|
@@ -67,8 +75,9 @@ fallback removed — nothing to await, so they return a value.
|
|
|
67
75
|
|
|
68
76
|
| Method | Returns | Use when |
|
|
69
77
|
|---|---|---|
|
|
70
|
-
| `get({ id })` | `Promise<T \| undefined>` | You need
|
|
71
|
-
| `
|
|
78
|
+
| `get({ id })` | `Promise<T \| undefined>` | You need to observe one current row. |
|
|
79
|
+
| `read({ id })` | `Promise<CapturedRow<T> \| undefined>` | A later mutation is based on this row. |
|
|
80
|
+
| `list({ where })` | `Promise<ModelList<T>>` | You need to observe a collection. |
|
|
72
81
|
| `listAll({ where, maxPages?, signal? })` | `Promise<T[]>` | You deliberately need every matching row. |
|
|
73
82
|
| `local.get(id)` | `T \| undefined` | You want a synchronous snapshot of one local row. |
|
|
74
83
|
| `local.list(options?)` | `T[]` | You want a synchronous snapshot of a local collection. |
|
|
@@ -76,11 +85,60 @@ fallback removed — nothing to await, so they return a value.
|
|
|
76
85
|
| `create({ data, ...options })` | `Promise<T>` | You want to create through the schema model. |
|
|
77
86
|
| `update({ id, data, ...options })` | `Promise<T>` | You want to update through the schema model. |
|
|
78
87
|
| `delete({ id, ...options })` | `Promise<void>` | You want to delete through the schema model. |
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
| `claim({ id, description })` | `Promise<HeldClaim<T>>` | Slow or expensive work must exclude another writer. |
|
|
89
|
+
| `claim.state({ id })` | `Promise<Claim \| null>` on HTTP | You need the current holder without acquiring the row. |
|
|
90
|
+
| `claim.list({ id })` | `Promise<{ object: 'list'; data: Claim[] }>` on HTTP | You need every disjoint holder on the row. |
|
|
91
|
+
| `claim.queue({ id })` | `Promise<ClaimQueueView>` on HTTP | You need the durable wait line. |
|
|
92
|
+
| `claim.release({ id })` | `Promise<void>` on HTTP | You need to release a claim early. |
|
|
93
|
+
| `claim.reorder({ id, order })` | `Promise<void>` on HTTP | A privileged coordinator needs to reorder the wait line. |
|
|
94
|
+
|
|
95
|
+
`get`, `read`, `list`, `create`, `update`, `delete`, and `claim` go
|
|
81
96
|
through the server. The `local` reads work off the rows a session has already
|
|
82
97
|
synced, so a cheap re-read needs no round-trip.
|
|
83
98
|
|
|
99
|
+
## Atomic commits
|
|
100
|
+
|
|
101
|
+
Use one `ablo.commits.create` when several Ablo model writes must all land or
|
|
102
|
+
none may land. Put every operation in `operations` and every exact row returned
|
|
103
|
+
by `read` that influenced the batch in the top-level `reads` array.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { AbloStaleContextError } from '@abloatai/ablo';
|
|
107
|
+
|
|
108
|
+
const task = await ablo.tasks.read({ id: taskId });
|
|
109
|
+
if (!task) throw new Error('task not found');
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await ablo.commits.create({
|
|
113
|
+
operations: [
|
|
114
|
+
{
|
|
115
|
+
action: 'update', model: 'tasks',
|
|
116
|
+
id: task.id,
|
|
117
|
+
data: { status: 'done' },
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
action: 'create', model: 'tasks',
|
|
121
|
+
id: markerId,
|
|
122
|
+
data: { title: 'atomic marker', status: 'done' },
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
reads: [task],
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof AbloStaleContextError && error.code === 'stale_context') {
|
|
129
|
+
console.log(error.code);
|
|
130
|
+
} else {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The server checks the premises and applies the operations in one transaction.
|
|
137
|
+
If any premise is stale or any operation fails, no operation lands. Independent
|
|
138
|
+
model calls are not an atomic batch. External effects and application-owned
|
|
139
|
+
Postgres writes cannot join this commit; keep those in their existing
|
|
140
|
+
transaction or outbox.
|
|
141
|
+
|
|
84
142
|
### Reading a whole collection
|
|
85
143
|
|
|
86
144
|
Prefer a filtered `listAll` when the application truly needs one complete
|
|
@@ -157,30 +215,35 @@ whenever a value is going away, and the type will hold you to it — only a
|
|
|
157
215
|
field your schema declares optional accepts one, since a required field has no
|
|
158
216
|
empty value to move to.
|
|
159
217
|
|
|
160
|
-
##
|
|
218
|
+
## Guarded Writes
|
|
161
219
|
|
|
162
|
-
Use `
|
|
220
|
+
Use `read` when a write depends on the row's current state, then pass that exact
|
|
221
|
+
row in `reads`:
|
|
163
222
|
|
|
164
223
|
```ts
|
|
165
|
-
const
|
|
224
|
+
const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
|
|
225
|
+
if (!report) throw new Error('report not found');
|
|
166
226
|
|
|
167
227
|
await ablo.weatherReports.update({
|
|
168
|
-
id:
|
|
228
|
+
id: report.id,
|
|
169
229
|
data: { status: 'ready' },
|
|
170
|
-
|
|
171
|
-
onStale: 'reject',
|
|
230
|
+
reads: [report],
|
|
172
231
|
});
|
|
173
232
|
```
|
|
174
233
|
|
|
175
234
|
Reactive local state changes optimistically at call time; awaiting the model
|
|
176
235
|
write waits for authoritative confirmation.
|
|
177
236
|
|
|
178
|
-
|
|
237
|
+
If the row changed after `read`, the write rejects with
|
|
238
|
+
`AbloStaleContextError`. Ablo retains only model, id, and the read watermark as
|
|
239
|
+
evidence; it does not record the row contents. A write without `reads` is an
|
|
240
|
+
intentional unconditional assignment.
|
|
241
|
+
|
|
242
|
+
Write options:
|
|
179
243
|
|
|
180
244
|
| Option | Purpose |
|
|
181
245
|
|---|---|
|
|
182
|
-
| `
|
|
183
|
-
| `onStale` | Stale-state policy. Prefer `reject` for agent writes. |
|
|
246
|
+
| `reads` | Exact rows returned by `read` that the mutation depends on. |
|
|
184
247
|
| `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
|
|
185
248
|
| `timeout` | Maximum time to wait for the write call. |
|
|
186
249
|
|
|
@@ -194,10 +257,12 @@ on its own if the holder stops, and is never saved as a row.
|
|
|
194
257
|
|
|
195
258
|
You coordinate a row with calls on its model, beside `create`/`update`/`get`:
|
|
196
259
|
`ablo.<model>.claim({ id })` takes the claim and returns a handle,
|
|
197
|
-
`ablo.<model>.claim.state({ id })` reads who currently holds it
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
`claim.
|
|
260
|
+
`ablo.<model>.claim.state({ id })` reads who currently holds it, and
|
|
261
|
+
`ablo.<model>.claim.release({ id })` releases it early. These reads are synchronous
|
|
262
|
+
on the stateful client and awaited server calls on the HTTP client. The full
|
|
263
|
+
coordination surface is `claim.state({ id })` / `claim.list({ id })` /
|
|
264
|
+
`claim.queue({ id })` / `claim.release({ id })` /
|
|
265
|
+
`claim.reorder({ id, order })` hanging off `claim`.
|
|
201
266
|
|
|
202
267
|
The fields on a claim, its lifecycle diagram, and the full method surface are in
|
|
203
268
|
[Coordination](./coordination.md#the-claim-state-object), which is where that
|
|
@@ -207,8 +272,9 @@ line.
|
|
|
207
272
|
|
|
208
273
|
### Reading and claiming
|
|
209
274
|
|
|
210
|
-
`claim.state({ id })` is the read side for observers
|
|
211
|
-
|
|
275
|
+
`claim.state({ id })` is the read side for observers and returns the current claim
|
|
276
|
+
state object (or `null`). It reads the stateful client's local cache synchronously;
|
|
277
|
+
the HTTP client returns a promise because it asks the server. `claim({ id })` is the write
|
|
212
278
|
side: it takes the claim and returns a `ClaimHandle`. Claims don't lock — if someone else
|
|
213
279
|
already holds the row, `claim` waits for them to finish, re-reads the fresh row,
|
|
214
280
|
then hands it to you, so you always proceed from current state. Default reads
|
|
@@ -218,7 +284,7 @@ Reads never block on a claim — to wait for a row to free up, `claim({ id })` i
|
|
|
218
284
|
(the claim queues fairly behind the holder).
|
|
219
285
|
|
|
220
286
|
```ts
|
|
221
|
-
const claim = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
287
|
+
const claim = await ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
222
288
|
if (claim) {
|
|
223
289
|
claim.heldBy;
|
|
224
290
|
claim.description;
|
|
@@ -229,13 +295,18 @@ const handle = await ablo.weatherReports.claim({
|
|
|
229
295
|
description: 'editing',
|
|
230
296
|
ttl: '2m',
|
|
231
297
|
});
|
|
232
|
-
await ablo.weatherReports.update({
|
|
298
|
+
await ablo.weatherReports.update({
|
|
299
|
+
id: handle.data.id,
|
|
300
|
+
data: { status: 'ready' },
|
|
301
|
+
claim: handle,
|
|
302
|
+
});
|
|
233
303
|
await handle.release();
|
|
234
304
|
```
|
|
235
305
|
|
|
236
|
-
Writes go through the normal
|
|
237
|
-
|
|
238
|
-
|
|
306
|
+
Writes go through the normal model mutation and pass the held handle as `claim`.
|
|
307
|
+
That explicit handle carries commit-time fencing. If the row changed underneath
|
|
308
|
+
you since you took the claim, the update rejects with `AbloStaleContextError`,
|
|
309
|
+
so you re-read before retrying.
|
|
239
310
|
Call `handle.release()` (or `ablo.weatherReports.claim.release({ id })`) to release
|
|
240
311
|
the claim when your work is done.
|
|
241
312
|
|
|
@@ -258,7 +329,7 @@ endpoint documents that model's real field contract instead of a generic blob.
|
|
|
258
329
|
|---|---|
|
|
259
330
|
| `ablo.<model>.create({ data })` | `POST /api/v1/models/{model}` |
|
|
260
331
|
| `ablo.<model>.list({ where })` | `GET /api/v1/models/{model}` |
|
|
261
|
-
| `ablo.<model>.
|
|
332
|
+
| `ablo.<model>.read({ id })` | `GET /api/v1/models/{model}/{id}` |
|
|
262
333
|
| `ablo.<model>.update({ id, data })` | `PATCH /api/v1/models/{model}/{id}` |
|
|
263
334
|
| `ablo.<model>.delete({ id })` | `DELETE /api/v1/models/{model}/{id}` |
|
|
264
335
|
| `ablo.<model>.claim({ id })` | `POST /api/v1/models/{model}/{id}/claim` |
|
|
@@ -284,7 +355,9 @@ rendered as agent tools.
|
|
|
284
355
|
|
|
285
356
|
## Errors
|
|
286
357
|
|
|
287
|
-
All SDK errors extend `AbloError`
|
|
358
|
+
All SDK errors extend `AbloError`. `type` is the class-name discriminator, such
|
|
359
|
+
as `AbloStaleContextError`; `code` is the wire condition, such as
|
|
360
|
+
`stale_context`. Use `instanceof` in-process and `type` after serialization.
|
|
288
361
|
|
|
289
362
|
| Error | Meaning |
|
|
290
363
|
|---|---|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Basic Usage
|
|
2
|
+
|
|
3
|
+
> Read one task row by id, list matching rows, create and update state, and coordinate work when needed.
|
|
4
|
+
|
|
5
|
+
Assume the configured client from [Installation](./installation.md) is exported
|
|
6
|
+
from `./ablo/client`.
|
|
7
|
+
|
|
8
|
+
## Read rows
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { ablo } from './ablo/client';
|
|
12
|
+
|
|
13
|
+
const task = await ablo.tasks.get({ id: taskId });
|
|
14
|
+
if (!task) throw new Error('task not found');
|
|
15
|
+
|
|
16
|
+
const open = await ablo.tasks.list({ where: { status: 'open' } });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`get` observes one current row. Use `read` instead only when a later Ablo write
|
|
20
|
+
must be rejected if that exact premise changes.
|
|
21
|
+
|
|
22
|
+
## Write rows
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const created = await ablo.tasks.create({
|
|
26
|
+
data: { title: 'Review return', status: 'open' },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await ablo.tasks.update({
|
|
30
|
+
id: created.id,
|
|
31
|
+
data: { status: 'done' },
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Writes return after Ablo confirms the authoritative result. Your PostgreSQL
|
|
36
|
+
constraints and schema remain in force.
|
|
37
|
+
|
|
38
|
+
## Coordinate slow work
|
|
39
|
+
|
|
40
|
+
When the target is an Ablo model row, claim it before the expensive step and
|
|
41
|
+
pass the claim to the final write.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
await using claim = await ablo.tasks.claim({ id: taskId });
|
|
45
|
+
|
|
46
|
+
const result = await performExpensiveWork(claim.data);
|
|
47
|
+
|
|
48
|
+
await ablo.tasks.update({
|
|
49
|
+
id: claim.data.id,
|
|
50
|
+
data: { title: result.title, status: 'done' },
|
|
51
|
+
claim,
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A contender follows the chosen wait, skip, or fail policy. Disposal releases
|
|
56
|
+
the claim, and expiry lets another participant recover when the owner
|
|
57
|
+
disappears.
|
|
58
|
+
|
|
59
|
+
## Preserve an existing write
|
|
60
|
+
|
|
61
|
+
The claimed target does not have to be an Ablo row. Claim a stable business id
|
|
62
|
+
and keep the final transaction in the application that already owns it.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
await using lease = await ablo.taskRuns.claim(taskId, {
|
|
66
|
+
contention: { mode: 'skip' },
|
|
67
|
+
});
|
|
68
|
+
if (!lease) return;
|
|
69
|
+
|
|
70
|
+
const prepared = await performExpensiveWork(taskId);
|
|
71
|
+
return existingTaskService.commitPrepared(taskId, prepared);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That transaction must still re-read, validate, and commit authoritatively. An
|
|
75
|
+
Ablo claim does not join a transaction in another process.
|
|
76
|
+
|
|
77
|
+
## Add stronger guarantees when required
|
|
78
|
+
|
|
79
|
+
- [Concurrency Convention](./concurrency-convention.md): reject a write when an
|
|
80
|
+
earlier premise changed.
|
|
81
|
+
- [Atomic commits](./api.md#atomic-commits): apply several Ablo writes together.
|
|
82
|
+
- [Idempotency](./idempotency.md): make retrying the same Ablo mutation safe.
|
|
83
|
+
- [Agents](./agents.md): configure a stateless HTTP worker.
|
|
84
|
+
- [React](./react.md): add live state and presence for a human interface.
|
|
@@ -66,7 +66,9 @@ Login stores one project-scoped `mk_` management credential. It has no
|
|
|
66
66
|
production/test mode and no application-data authority. It can manage projects
|
|
67
67
|
and branches and exchange for an expiring credential bound to one branch.
|
|
68
68
|
|
|
69
|
-
If you switch projects, log in for the selected project before running `dev
|
|
69
|
+
If you switch projects, log in for the selected project before running `dev`.
|
|
70
|
+
A plain `npx ablo login` offers the organization's projects in the terminal;
|
|
71
|
+
`--project` names one outright:
|
|
70
72
|
|
|
71
73
|
```bash
|
|
72
74
|
npx ablo projects use orders
|
|
@@ -253,8 +255,8 @@ database's safe coordinates, and an exact readiness fix.
|
|
|
253
255
|
For a one-shot CI schema check:
|
|
254
256
|
|
|
255
257
|
```bash
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
+
# The one credential input temporarily carries CI's masked management grant.
|
|
259
|
+
ABLO_API_KEY="mk_..." \
|
|
258
260
|
ABLO_BRANCH="preview-pr-${PR_NUMBER}" \
|
|
259
261
|
npx ablo dev --no-watch
|
|
260
262
|
```
|
|
@@ -273,6 +275,22 @@ npx ablo branch ensure "preview-pr-${PR_NUMBER}" \
|
|
|
273
275
|
result as a secret, mask it in logs, and pass it through the deployment
|
|
274
276
|
provider's secret-variable mechanism.
|
|
275
277
|
|
|
278
|
+
For live integration tests that should not require a customer database, request
|
|
279
|
+
an explicitly hosted, expiring test branch:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
npx ablo branch ensure "sandbox-live-${RUN_ID}" \
|
|
283
|
+
--kind test \
|
|
284
|
+
--hosted \
|
|
285
|
+
--expires-at "${EXPIRES_AT}" \
|
|
286
|
+
--credential \
|
|
287
|
+
--json
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Hosted storage is never inferred. It is accepted only for `kind: test` branches
|
|
291
|
+
that expire within 24 hours; ordinary branches stay unbound until connected to
|
|
292
|
+
the customer's database.
|
|
293
|
+
|
|
276
294
|
Closing a preview should call `branch delete`. Deletion immediately makes
|
|
277
295
|
branch-bound credentials fail authentication even if their expiry is later.
|
|
278
296
|
|
|
@@ -338,7 +356,8 @@ Run:
|
|
|
338
356
|
npx ablo login
|
|
339
357
|
```
|
|
340
358
|
|
|
341
|
-
|
|
359
|
+
The terminal then offers the organization's projects, cursor on the active
|
|
360
|
+
one; a plain Enter keeps it. To name the project and skip the picker:
|
|
342
361
|
|
|
343
362
|
```bash
|
|
344
363
|
npx ablo login --project <project>
|
package/docs/cli.md
CHANGED
|
@@ -32,8 +32,8 @@ resume a branch and exchanges it for a temporary branch-bound runtime key.
|
|
|
32
32
|
|
|
33
33
|
| Command | What it does |
|
|
34
34
|
| ------------------------ | -------------------------------------------------------------------------- |
|
|
35
|
-
| `ablo login` | Authorize in the browser; store
|
|
36
|
-
| `ablo login --project <slug>` | Same
|
|
35
|
+
| `ablo login` | Authorize in the browser, pick a project; store its management credential. |
|
|
36
|
+
| `ablo login --project <slug>` | Same without the picker: scoped to the named project, which becomes active. |
|
|
37
37
|
| `ablo logout` | Remove the stored credentials. |
|
|
38
38
|
| `ablo whoami` | Strictly confirm which project and branch a credential acts on. |
|
|
39
39
|
| `ablo status` | Show the active org/project, resolved runtime credential, branch target, and server health. |
|
|
@@ -55,9 +55,10 @@ reports the server-confirmed branch before it writes. For one-time recovery,
|
|
|
55
55
|
putting the secret in argv.
|
|
56
56
|
|
|
57
57
|
Keys live in `~/.config/ablo/credentials.json` (mode `0600`), keyed by project.
|
|
58
|
-
The non-secret `config.json` holds the active project.
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
The non-secret `config.json` holds the active project. There is one explicit
|
|
59
|
+
credential input: `ABLO_API_KEY`. In headless CI it may temporarily contain an
|
|
60
|
+
`mk_` credential during branch bootstrap; the runtime receives the resulting
|
|
61
|
+
branch-bound `sk_` or restricted `rk_` value through the same variable.
|
|
61
62
|
|
|
62
63
|
## Development branches and the production root
|
|
63
64
|
|
|
@@ -94,7 +95,8 @@ with `projects use`) selects which profile every command authenticates with.
|
|
|
94
95
|
| `ablo projects list` | List the org's projects (marks the active one and the org-default). |
|
|
95
96
|
| `ablo projects create <slug>` | Create a project (`--name "Display Name"`). Its keys/schema/data are isolated. |
|
|
96
97
|
| `ablo projects use <slug>` | Switch the active project. `ablo projects use default` returns to the org-default. |
|
|
97
|
-
| `ablo login
|
|
98
|
+
| `ablo login` | Pick a project in the terminal; store its management access and make it active. |
|
|
99
|
+
| `ablo login --project <slug>` | The same for a named project, with no picker. |
|
|
98
100
|
|
|
99
101
|
Because keys are fixed to a project, `projects use` only changes which profile
|
|
100
102
|
is active — it never re-scopes an existing key. Switch to a project you haven't
|
|
@@ -108,12 +110,17 @@ npx ablo projects use war-room
|
|
|
108
110
|
npx ablo login --project war-room # stores its management credential, keeps it active
|
|
109
111
|
```
|
|
110
112
|
|
|
113
|
+
A plain `npx ablo login` reaches the same place through a picker: once the
|
|
114
|
+
browser has approved, the terminal lists the organization's projects, with the
|
|
115
|
+
cursor on the active one, and the choice becomes the credential's project. An
|
|
116
|
+
organization holding only its default project is not asked.
|
|
117
|
+
|
|
111
118
|
If you run a project-scoped command (`push`, `dev`) while the active project has
|
|
112
119
|
no key — but other projects do — the CLI **refuses** rather than silently
|
|
113
120
|
deploying with the wrong project's credential, and names the fix
|
|
114
|
-
(`ablo login --project <slug>`). In CI, an explicit `
|
|
115
|
-
bypasses profiles for project/branch administration
|
|
116
|
-
|
|
121
|
+
(`ablo login --project <slug>`). In CI, an explicit `mk_` in `ABLO_API_KEY`
|
|
122
|
+
bypasses profiles for project/branch administration. Replace it with the
|
|
123
|
+
branch-bound runtime credential before starting application code.
|
|
117
124
|
|
|
118
125
|
## Commands
|
|
119
126
|
|