@abloatai/ablo 0.57.0 → 0.58.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 +10 -4
- package/CHANGELOG.md +199 -13
- package/README.md +2 -1
- 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/agent-integration-decision-guide.md +123 -0
- package/docs/agents.md +18 -14
- package/docs/api-keys.md +6 -6
- package/docs/api.md +52 -29
- package/docs/branch-development.md +23 -4
- package/docs/cli.md +16 -9
- package/docs/client-behavior.md +21 -15
- package/docs/concurrency-convention.md +67 -77
- package/docs/context.md +56 -31
- package/docs/coordination.md +115 -36
- package/docs/data-sources.md +12 -6
- package/docs/debugging.md +1 -1
- package/docs/examples/agent-human.md +6 -18
- package/docs/examples/coordination-conformance.md +69 -0
- package/docs/examples/existing-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/groups.md +19 -139
- package/docs/guarantees.md +5 -6
- package/docs/identity.md +2 -1
- package/docs/index.md +5 -0
- package/docs/integration-guide.md +20 -19
- package/docs/integrations/sandbox-runtime.md +148 -0
- package/docs/integrations.md +9 -0
- package/docs/operating-on-your-database.md +7 -0
- package/docs/quickstart.md +19 -13
- package/docs/react.md +9 -9
- package/docs/schema-contract.md +14 -13
- package/docs/sessions.md +1 -1
- package/examples/README.md +2 -2
- package/examples/agent-turn.ts +1 -1
- package/examples/expensive-agent-turn.ts +1 -1
- package/llms.txt +22 -11
- package/package.json +6 -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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Agent Integration Decision Guide
|
|
2
|
+
|
|
3
|
+
> Choose one integration route before reading an example. Most existing products should coordinate one existing operation first; they should not copy the document pipeline.
|
|
4
|
+
|
|
5
|
+
## Start with the operation you already own
|
|
6
|
+
|
|
7
|
+
Name one existing application operation, such as `completeTask`, `approveInvoice`,
|
|
8
|
+
or `publishReport`. Keep its authorization, database transaction, constraints,
|
|
9
|
+
and public API in place. Add Ablo at that operation boundary.
|
|
10
|
+
|
|
11
|
+
Use this routing table for the decisions that are easy to conflate:
|
|
12
|
+
|
|
13
|
+
| Question | Choose | When |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| What is being coordinated? | Identifier-only claim | Work has a stable business identity, but the authoritative row and final write remain in the existing service or Postgres. |
|
|
16
|
+
| | Row-backed claim | The coordinated row is an Ablo schema model and the final write goes through that model resource. |
|
|
17
|
+
| Did the decision depend on previously read rows? | Captured reads | Read the premises with `read(...)`, then pass those returned rows through `reads`. Use this even when the written row is different from a premise row. |
|
|
18
|
+
| Must several Ablo mutations either all land or none land? | Atomic commit | Put the mutations, captured reads, and any typed claim handles in one `commits.create(...)`. Separate mutation calls are independently successful or failed. |
|
|
19
|
+
| Where should the final write happen? | Existing database write | Keep it when the current service owns the transaction, constraints, or rollout switch. Ablo's lease does not join a transaction running in another process. Re-read and validate inside the database transaction. |
|
|
20
|
+
| | Ablo-routed write | Use it for declared schema models after the database connection and server schema are configured. Guard decision-dependent writes with a held claim or captured reads. |
|
|
21
|
+
| Who is participating? | Stateless HTTP client | Agents, jobs, and request/response server handlers. Give each concurrent participant its own scoped credential. |
|
|
22
|
+
| | Reactive WebSocket client | Human-facing applications that need live state, presence, or local reactive reads. This transport is not required for worker coordination. |
|
|
23
|
+
|
|
24
|
+
These choices compose. For example, a stateless worker can take an
|
|
25
|
+
identifier-only claim, perform slow work, and then call an existing Postgres
|
|
26
|
+
operation. Another worker can take a row-backed claim and submit an atomic Ablo
|
|
27
|
+
commit guarded by captured premise rows.
|
|
28
|
+
|
|
29
|
+
## Choose the smallest example
|
|
30
|
+
|
|
31
|
+
### Coordinate existing work
|
|
32
|
+
|
|
33
|
+
Start with
|
|
34
|
+
[`examples/graphql-existing-backend`](../../../examples/graphql-existing-backend/README.md)
|
|
35
|
+
when an application already owns its API, operation, and Postgres write.
|
|
36
|
+
|
|
37
|
+
It demonstrates:
|
|
38
|
+
|
|
39
|
+
- GraphQL delegating to a named application operation;
|
|
40
|
+
- an identifier lease around expensive work;
|
|
41
|
+
- the existing service retaining its authoritative transaction and re-read;
|
|
42
|
+
- an operation-level switch between existing and coordinated paths; and
|
|
43
|
+
- recovery and contract parity without replacing the application's API.
|
|
44
|
+
|
|
45
|
+
Use
|
|
46
|
+
[`examples/coordination-conformance`](../../../examples/coordination-conformance/README.md)
|
|
47
|
+
alongside it to verify real hosted lease behavior independently of the domain.
|
|
48
|
+
|
|
49
|
+
### Build evidence-backed document state
|
|
50
|
+
|
|
51
|
+
Read
|
|
52
|
+
[`examples/existing-document-pipeline`](../../../examples/existing-document-pipeline/README.md)
|
|
53
|
+
only when the feature genuinely needs versioned source evidence, citations,
|
|
54
|
+
guarded review decisions, atomic multi-row review writes, and retained search
|
|
55
|
+
projections.
|
|
56
|
+
|
|
57
|
+
That example is an advanced reference application. Its document ingestion,
|
|
58
|
+
search, review, projection-retention, and append-only event policies are not
|
|
59
|
+
prerequisites for adopting Ablo.
|
|
60
|
+
|
|
61
|
+
## Know which owner makes each promise
|
|
62
|
+
|
|
63
|
+
| Ablo responsibility | Application responsibility |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Participant-scoped claims, lease expiry, wait/skip behavior, heartbeat, release, and commit-time fencing | Choosing the business claim identity and issuing distinct participant credentials |
|
|
66
|
+
| Capturing model-row versions returned by `read(...)` and rejecting a guarded write when those premises are stale | Choosing every row that is a premise of the decision |
|
|
67
|
+
| Atomicity among mutations submitted in one Ablo commit | Database transactions and constraints outside that commit; never presenting separate writes as an atomic batch |
|
|
68
|
+
| Request idempotency within the documented identity, retention, and identical-request rules | Durable workflow idempotency and deduplication of external effects |
|
|
69
|
+
| Synchronizing declared model rows and serving reactive state | Uploads, search semantics, projections, workflow execution, review policy, and external APIs |
|
|
70
|
+
| Credentials, participant attribution, and schema-declared scope enforcement | Existing application authentication and authorization at the operation boundary |
|
|
71
|
+
|
|
72
|
+
Claims coordinate cooperative participants; they are leases, not absolute locks.
|
|
73
|
+
A writer outside the coordinated path can still change Postgres. Database
|
|
74
|
+
constraints and a commit-time guarded re-read remain the final backstop.
|
|
75
|
+
|
|
76
|
+
## Minimum integration contract
|
|
77
|
+
|
|
78
|
+
Write down these answers next to the operation before implementing it:
|
|
79
|
+
|
|
80
|
+
1. **Existing operation:** Which named operation and public API remain stable?
|
|
81
|
+
2. **Claim identity:** Which stable model row or business identifier represents the contested work?
|
|
82
|
+
3. **Participant identity:** Which distinct scoped credential does each concurrent human, agent, or worker use?
|
|
83
|
+
4. **Premises:** Which exact rows does the decision depend on, and which of them must use `read(...)`?
|
|
84
|
+
5. **Atomic boundary:** Which writes must all succeed together? Are they one Ablo commit, one existing database transaction, or deliberately independent?
|
|
85
|
+
6. **Persistence owner:** What remains in Postgres and which writes, if any, are routed through Ablo?
|
|
86
|
+
7. **Failure behavior:** What happens on contention, lease expiry, stale evidence, request retry, partial completion, and an external side-effect failure?
|
|
87
|
+
8. **Proof:** Which local contract test and which hosted or staging test proves each claimed guarantee?
|
|
88
|
+
|
|
89
|
+
If an answer is unknown, keep the existing write path available. Do not broaden
|
|
90
|
+
the schema or copy an advanced example to hide the missing decision.
|
|
91
|
+
|
|
92
|
+
## Guarantee-to-test matrix
|
|
93
|
+
|
|
94
|
+
The examples prove different layers. A local fixture proves application behavior;
|
|
95
|
+
it does not prove hosted infrastructure. Conversely, hosted claim conformance
|
|
96
|
+
does not prove a domain transition or a real Postgres transaction.
|
|
97
|
+
|
|
98
|
+
| Statement | Proof level | Executable evidence |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| GraphQL keeps the same result while the named operation changes implementation | Local application contract | `examples/graphql-existing-backend/tests/graphql.test.ts` and `tests/pilot.test.ts` — `the GraphQL resolver delegates one typed input to the named operation`; `the operation switch preserves the uncontended GraphQL contract` |
|
|
101
|
+
| Coordination moves expensive work outside the retained database critical section | Local application contract; real DB timing requires staging | `examples/graphql-existing-backend/tests/pilot.test.ts` — `coordination moves expensive work outside the retained critical section`; optional `npm run test:live:postgres` |
|
|
102
|
+
| Contenders do not duplicate expensive work, and failure releases the path | Local application contract | `examples/graphql-existing-backend/tests/pilot.test.ts` — `two coordinated workers pay for expensive work once`; `a failed owner releases coordination so the existing path remains available` |
|
|
103
|
+
| Distinct hosted participants exclude one another, heartbeat, release, and recover after expiry | Hosted infrastructure | `examples/coordination-conformance`: `npm run test:live`; its live runner also executes the exit-without-release expiry probe |
|
|
104
|
+
| A decision based on changed source evidence is rejected | Local application contract using Ablo-shaped guards | `examples/existing-document-pipeline/tests/processDocument.test.ts` — `a source change rejects stale extracted output`; `tests/review.test.ts` — `guarded mutations reject stale evidence and release claims for retry` |
|
|
105
|
+
| Several review records submitted as one commit are atomic; separate calls can partially complete | Local application contract | `examples/existing-document-pipeline/tests/review.test.ts` — `requesting review atomically creates durable issue state and an event`; `independent review writes expose partial completion and retry only the stale target` |
|
|
106
|
+
| Rebuilding search does not remove a complete snapshot referenced by durable review evidence | Local application policy | `examples/existing-document-pipeline/tests/search.test.ts` — `publishing a rebuild retains the complete projection snapshot referenced by review`; `an unreferenced superseded projection becomes removable as one snapshot` |
|
|
107
|
+
| The documented ownership tree, public exports, dependency direction, and lack of cycles match disk | Local structure contract | Each focused example's `tests/structure.test.ts`; the document fixture additionally validates exact inventory and dependency direction from `structure.json` |
|
|
108
|
+
| Authorization, latency, database locking, and external-effect behavior match the production application | Partner staging | Run the operation against the real auth, Postgres schema, workload, and provider sandbox. No repository fixture can establish this claim. |
|
|
109
|
+
|
|
110
|
+
## Safe first adoption
|
|
111
|
+
|
|
112
|
+
For an existing product, the default sequence is:
|
|
113
|
+
|
|
114
|
+
1. Wrap one named operation without changing its public API.
|
|
115
|
+
2. Use an identifier-only claim if the existing database remains authoritative.
|
|
116
|
+
3. Keep the Postgres transaction, lock, validation, and constraints in place.
|
|
117
|
+
4. Verify participant-scoped lease behavior with coordination conformance.
|
|
118
|
+
5. Add captured reads or an atomic Ablo commit only when the operation actually needs them.
|
|
119
|
+
6. Move more persistence through Ablo only after the guarded hosted write path and staging behavior are proven for that operation.
|
|
120
|
+
|
|
121
|
+
Continue with the [Integration Guide](./integration-guide.md) for setup and API
|
|
122
|
+
details, or [Concurrency Convention](./concurrency-convention.md) for the exact
|
|
123
|
+
guarding rules.
|
package/docs/agents.md
CHANGED
|
@@ -34,7 +34,7 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: "http"
|
|
|
34
34
|
// `get` resolves to the row, or `undefined` when none matches.
|
|
35
35
|
const open = await ablo.records.list({ where: { status: "todo" } });
|
|
36
36
|
|
|
37
|
-
const record = await ablo.records.
|
|
37
|
+
const record = await ablo.records.read({ id: open[0].id });
|
|
38
38
|
if (!record) throw new Error("record not found");
|
|
39
39
|
|
|
40
40
|
console.log(record.title);
|
|
@@ -42,9 +42,11 @@ 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`
|
|
45
|
-
and `claim`. It does **not** expose stateful-only `local` reads or
|
|
46
|
-
subscriptions. Those need a
|
|
47
|
-
are compile errors
|
|
45
|
+
and `claim`. It does **not** expose stateful-only `local` reads or model
|
|
46
|
+
`onChange` subscriptions. Those need a WebSocket, so with `transport: 'http'`
|
|
47
|
+
they are compile errors. `context().onChange` is separate: while its listener
|
|
48
|
+
is active, it holds one HTTP response open until the context changes or the
|
|
49
|
+
listener stops.
|
|
48
50
|
|
|
49
51
|
## Managed scoped agents
|
|
50
52
|
|
|
@@ -58,7 +60,7 @@ model output or an HTTP request body. A serverless handler normally creates and
|
|
|
58
60
|
disposes one child per invocation:
|
|
59
61
|
|
|
60
62
|
```ts
|
|
61
|
-
const run = await control.runs.
|
|
63
|
+
const run = await control.runs.read({ id: verifiedRunId });
|
|
62
64
|
if (!run) throw new Error('run not found');
|
|
63
65
|
|
|
64
66
|
const agent = await control.agents.create({
|
|
@@ -204,19 +206,21 @@ default caller here, not a bolt-on.
|
|
|
204
206
|
|
|
205
207
|
```text
|
|
206
208
|
something happens ──▶ your agent (HTTP, no socket)
|
|
207
|
-
(a job, a webhook, read context (list/get)
|
|
209
|
+
(a job, a webhook, read context (list/get/read)
|
|
208
210
|
a queue message) claim → work → commit
|
|
209
211
|
done — no held connection
|
|
210
212
|
```
|
|
211
213
|
|
|
212
|
-
|
|
213
|
-
restarts are free, and you scale by adding
|
|
214
|
-
|
|
214
|
+
Without `context().onChange`, an agent holds nothing open and remains a
|
|
215
|
+
**stateless worker**: deploys and restarts are free, and you scale by adding
|
|
216
|
+
workers. Each active context listener is explicit connection capacity and must
|
|
217
|
+
be stopped when its work ends.
|
|
215
218
|
|
|
216
219
|
## What stays on the live (human) plane
|
|
217
220
|
|
|
218
|
-
`onChange`
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
Model `onChange` and `local` reads require a WebSocket and a local store —
|
|
222
|
+
they're for interactive UIs. An HTTP agent normally reacts to an external
|
|
223
|
+
trigger, then reads with `list`/`get`. For costly work, `context().onChange` can
|
|
224
|
+
stop that one run early while the final write still uses `context().reads`.
|
|
225
|
+
See [client behavior](/client-behavior) for the full surface and
|
|
226
|
+
[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,8 +85,14 @@ 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
|
|
|
@@ -157,30 +172,35 @@ whenever a value is going away, and the type will hold you to it — only a
|
|
|
157
172
|
field your schema declares optional accepts one, since a required field has no
|
|
158
173
|
empty value to move to.
|
|
159
174
|
|
|
160
|
-
##
|
|
175
|
+
## Guarded Writes
|
|
161
176
|
|
|
162
|
-
Use `
|
|
177
|
+
Use `read` when a write depends on the row's current state, then pass that exact
|
|
178
|
+
row in `reads`:
|
|
163
179
|
|
|
164
180
|
```ts
|
|
165
|
-
const
|
|
181
|
+
const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
|
|
182
|
+
if (!report) throw new Error('report not found');
|
|
166
183
|
|
|
167
184
|
await ablo.weatherReports.update({
|
|
168
|
-
id:
|
|
185
|
+
id: report.id,
|
|
169
186
|
data: { status: 'ready' },
|
|
170
|
-
|
|
171
|
-
onStale: 'reject',
|
|
187
|
+
reads: [report],
|
|
172
188
|
});
|
|
173
189
|
```
|
|
174
190
|
|
|
175
191
|
Reactive local state changes optimistically at call time; awaiting the model
|
|
176
192
|
write waits for authoritative confirmation.
|
|
177
193
|
|
|
178
|
-
|
|
194
|
+
If the row changed after `read`, the write rejects with
|
|
195
|
+
`AbloStaleContextError`. Ablo retains only model, id, and the read watermark as
|
|
196
|
+
evidence; it does not record the row contents. A write without `reads` is an
|
|
197
|
+
intentional unconditional assignment.
|
|
198
|
+
|
|
199
|
+
Write options:
|
|
179
200
|
|
|
180
201
|
| Option | Purpose |
|
|
181
202
|
|---|---|
|
|
182
|
-
| `
|
|
183
|
-
| `onStale` | Stale-state policy. Prefer `reject` for agent writes. |
|
|
203
|
+
| `reads` | Exact rows returned by `read` that the mutation depends on. |
|
|
184
204
|
| `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
|
|
185
205
|
| `timeout` | Maximum time to wait for the write call. |
|
|
186
206
|
|
|
@@ -194,10 +214,12 @@ on its own if the holder stops, and is never saved as a row.
|
|
|
194
214
|
|
|
195
215
|
You coordinate a row with calls on its model, beside `create`/`update`/`get`:
|
|
196
216
|
`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.
|
|
217
|
+
`ablo.<model>.claim.state({ id })` reads who currently holds it, and
|
|
218
|
+
`ablo.<model>.claim.release({ id })` releases it early. These reads are synchronous
|
|
219
|
+
on the stateful client and awaited server calls on the HTTP client. The full
|
|
220
|
+
coordination surface is `claim.state({ id })` / `claim.list({ id })` /
|
|
221
|
+
`claim.queue({ id })` / `claim.release({ id })` /
|
|
222
|
+
`claim.reorder({ id, order })` hanging off `claim`.
|
|
201
223
|
|
|
202
224
|
The fields on a claim, its lifecycle diagram, and the full method surface are in
|
|
203
225
|
[Coordination](./coordination.md#the-claim-state-object), which is where that
|
|
@@ -207,8 +229,9 @@ line.
|
|
|
207
229
|
|
|
208
230
|
### Reading and claiming
|
|
209
231
|
|
|
210
|
-
`claim.state({ id })` is the read side for observers
|
|
211
|
-
|
|
232
|
+
`claim.state({ id })` is the read side for observers and returns the current claim
|
|
233
|
+
state object (or `null`). It reads the stateful client's local cache synchronously;
|
|
234
|
+
the HTTP client returns a promise because it asks the server. `claim({ id })` is the write
|
|
212
235
|
side: it takes the claim and returns a `ClaimHandle`. Claims don't lock — if someone else
|
|
213
236
|
already holds the row, `claim` waits for them to finish, re-reads the fresh row,
|
|
214
237
|
then hands it to you, so you always proceed from current state. Default reads
|
|
@@ -218,7 +241,7 @@ Reads never block on a claim — to wait for a row to free up, `claim({ id })` i
|
|
|
218
241
|
(the claim queues fairly behind the holder).
|
|
219
242
|
|
|
220
243
|
```ts
|
|
221
|
-
const claim = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
244
|
+
const claim = await ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
222
245
|
if (claim) {
|
|
223
246
|
claim.heldBy;
|
|
224
247
|
claim.description;
|
|
@@ -258,7 +281,7 @@ endpoint documents that model's real field contract instead of a generic blob.
|
|
|
258
281
|
|---|---|
|
|
259
282
|
| `ablo.<model>.create({ data })` | `POST /api/v1/models/{model}` |
|
|
260
283
|
| `ablo.<model>.list({ where })` | `GET /api/v1/models/{model}` |
|
|
261
|
-
| `ablo.<model>.
|
|
284
|
+
| `ablo.<model>.read({ id })` | `GET /api/v1/models/{model}/{id}` |
|
|
262
285
|
| `ablo.<model>.update({ id, data })` | `PATCH /api/v1/models/{model}/{id}` |
|
|
263
286
|
| `ablo.<model>.delete({ id })` | `DELETE /api/v1/models/{model}/{id}` |
|
|
264
287
|
| `ablo.<model>.claim({ id })` | `POST /api/v1/models/{model}/{id}/claim` |
|
|
@@ -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
|
|
package/docs/client-behavior.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Client Behavior
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Guarded writes, claim behavior, and which errors are safe to retry.
|
|
4
4
|
|
|
5
|
-
When several writers touch the same data at once — an agent worker, a Server
|
|
5
|
+
When several writers touch the same data at once — an agent worker, a Server
|
|
6
|
+
Action, a person in the browser — the SDK protects explicit read dependencies
|
|
7
|
+
and claims records across slow work. This page describes those guarantees and
|
|
8
|
+
which errors are safe to retry.
|
|
6
9
|
|
|
7
10
|
Claims don't lock. If another writer holds the row, `claim` waits for them, re-reads the fresh row, then hands it to you — so two writers serialize instead of clobbering.
|
|
8
11
|
|
|
@@ -34,7 +37,7 @@ Common options:
|
|
|
34
37
|
| `baseURL` | Override the hosted sync endpoint for staging or private deployments. An HTTPS origin, optionally with a path prefix; plain HTTP is accepted for localhost. Your key travels here, so a URL carrying its own credentials, a query, or a fragment is refused at construction. |
|
|
35
38
|
| `persistence` | `memory` by default. Use `indexeddb` for a durable browser cache that survives reloads. |
|
|
36
39
|
| `durableWrites` | Optional crash recovery for unacknowledged agent/worker writes. Independent of the default memory cache; accepts `{ store, namespace? }`. |
|
|
37
|
-
| `transport` | `'websocket'` (default) is the live, stateful client: a persistent socket, a local synced pool, and `onChange` subscriptions. `'http'` returns the **stateless** client for server-side actors (agents, workers, serverless): the same `ablo.<model>` read/write/claim surface, but
|
|
40
|
+
| `transport` | `'websocket'` (default) is the live, stateful client: a persistent socket, a local synced pool, and model `onChange` subscriptions. `'http'` returns the **stateless** client for server-side actors (agents, workers, serverless): the same `ablo.<model>` read/write/claim surface, but ordinary calls are HTTP round trips with no socket. Stateful-only model methods (`local`, model `onChange`, and `join`) are compile errors. A listener added through `context().onChange` holds one HTTP response open only until its context changes or its last listener stops. |
|
|
38
41
|
| `fetch` | Custom fetch implementation for tests or non-standard runtimes. |
|
|
39
42
|
| `defaultHeaders` | Extra headers attached to every HTTP request. |
|
|
40
43
|
| `defaultQuery` | Extra query parameters attached to every HTTP request. |
|
|
@@ -53,7 +56,7 @@ Each schema model becomes a typed model:
|
|
|
53
56
|
```ts
|
|
54
57
|
await ablo.ready();
|
|
55
58
|
|
|
56
|
-
const report = await ablo.weatherReports.
|
|
59
|
+
const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
|
|
57
60
|
const local = ablo.weatherReports.local.get('report_stockholm');
|
|
58
61
|
|
|
59
62
|
await ablo.weatherReports.create({ data: { location: 'Stockholm', status: 'pending' } });
|
|
@@ -65,7 +68,7 @@ On the reactive client, each model write changes local state optimistically
|
|
|
65
68
|
before the call returns. Its promise always waits for authoritative
|
|
66
69
|
confirmation, so `await update(...)` is the confirmation barrier.
|
|
67
70
|
|
|
68
|
-
Call `get`/`list`
|
|
71
|
+
Call `get`/`list` to observe, or `read` when a later mutation depends on the row.
|
|
69
72
|
After that, `local.get`/`local.list`/`local.count` read the already-synced data instantly with
|
|
70
73
|
no `await`, and stay reactive in render. Use the async pair to load, the sync trio
|
|
71
74
|
to read.
|
|
@@ -83,14 +86,13 @@ through the same model client path. A human Server Action, a browser view, and a
|
|
|
83
86
|
agent worker can all use `ablo.weatherReports`:
|
|
84
87
|
|
|
85
88
|
```ts
|
|
86
|
-
const report = await ablo.weatherReports.
|
|
87
|
-
|
|
89
|
+
const report = await ablo.weatherReports.read({ id });
|
|
90
|
+
if (!report) throw new Error('Row not found');
|
|
88
91
|
|
|
89
92
|
await ablo.weatherReports.update({
|
|
90
93
|
id,
|
|
91
94
|
data: patch,
|
|
92
|
-
|
|
93
|
-
onStale: 'reject',
|
|
95
|
+
reads: [report],
|
|
94
96
|
});
|
|
95
97
|
```
|
|
96
98
|
|
|
@@ -106,24 +108,28 @@ actor routing through Ablo is coordinated. The one write it can't coordinate is
|
|
|
106
108
|
one made directly against your database, around Ablo — the WAL echo still catches
|
|
107
109
|
it for reads, but it bypasses claims and ordering.
|
|
108
110
|
|
|
109
|
-
##
|
|
111
|
+
## Guarded Writes
|
|
110
112
|
|
|
111
113
|
```ts
|
|
114
|
+
const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
|
|
115
|
+
if (!report) throw new Error('report not found');
|
|
116
|
+
|
|
112
117
|
await ablo.weatherReports.update({
|
|
113
|
-
id:
|
|
118
|
+
id: report.id,
|
|
114
119
|
data: { status: 'ready' },
|
|
115
|
-
|
|
116
|
-
onStale: 'reject',
|
|
120
|
+
reads: [report],
|
|
117
121
|
idempotencyKey: 'report_stockholm:mark-ready:v1',
|
|
118
122
|
});
|
|
119
123
|
```
|
|
120
124
|
|
|
121
125
|
| Option | Purpose |
|
|
122
126
|
|---|---|
|
|
123
|
-
| `
|
|
124
|
-
| `onStale` | Policy when the target changed after `readAt`. Prefer `reject`. |
|
|
127
|
+
| `reads` | Exact rows returned by `read` that this mutation depends on. |
|
|
125
128
|
| `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
|
|
126
129
|
|
|
130
|
+
A stale premise always rejects with `AbloStaleContextError`. Omit `reads` only
|
|
131
|
+
when the assignment is intentionally unconditional.
|
|
132
|
+
|
|
127
133
|
## Claimed Behavior
|
|
128
134
|
|
|
129
135
|
If your update involves a slow step — an API call, an LLM round-trip — and someone
|