@abloatai/ablo 0.59.2 → 0.60.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/CHANGELOG.md +60 -0
- package/README.md +1 -1
- package/dist/ai-sdk.js.map +1 -1
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +2 -0
- package/dist/sessions.js.map +1 -0
- package/docs/agent-messaging.md +3 -2
- package/docs/agents.md +49 -32
- package/docs/api-keys.md +18 -12
- package/docs/client-behavior.md +6 -4
- package/docs/coordination.md +6 -3
- package/docs/customer-organizations.md +8 -19
- package/docs/deployment.md +7 -6
- package/docs/examples/existing-python-backend.md +15 -18
- package/docs/examples/nextjs.md +33 -66
- package/docs/examples/scoped-agent.md +4 -3
- package/docs/examples/server-agent.md +4 -3
- package/docs/groups.md +11 -12
- package/docs/identity.md +33 -29
- package/docs/integration-guide.md +12 -18
- package/docs/options.md +80 -29
- package/docs/react.md +8 -52
- package/docs/security.md +1 -1
- package/docs/sessions.md +97 -69
- package/docs/transports.md +124 -0
- package/examples/README.md +7 -0
- package/examples/terminal-showcase/index.ts +219 -0
- package/examples/terminal-showcase/schema.ts +12 -0
- package/llms.txt +9 -9
- package/package.json +10 -5
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deterministic terminal proof of Ablo's product contract.
|
|
3
|
+
*
|
|
4
|
+
* It uses a real Ablo project, mints distinct human and agent participants,
|
|
5
|
+
* proves stale work is rejected, then proves claim contention serializes the
|
|
6
|
+
* same two participants onto fresh state. The final write is looked up through
|
|
7
|
+
* the durable commit API so the terminal shows evidence, not just application
|
|
8
|
+
* output.
|
|
9
|
+
*
|
|
10
|
+
* Run from packages/ablo:
|
|
11
|
+
*
|
|
12
|
+
* npx ablo push --schema examples/terminal-showcase/schema.ts
|
|
13
|
+
* ABLO_API_KEY=sk_... npx tsx examples/terminal-showcase/index.ts
|
|
14
|
+
*/
|
|
15
|
+
import Ablo, { AbloStaleContextError } from '@abloatai/ablo';
|
|
16
|
+
import Sessions from '@abloatai/ablo/sessions';
|
|
17
|
+
import { schema } from './schema';
|
|
18
|
+
|
|
19
|
+
const apiKey = process.env.ABLO_API_KEY;
|
|
20
|
+
if (!apiKey?.startsWith('sk_')) {
|
|
21
|
+
throw new Error('ABLO_API_KEY must be a secret sk_ project key so the demo can mint scoped participants.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const delayMs = Number.parseInt(process.env.ABLO_SHOWCASE_DELAY_MS ?? '650', 10);
|
|
25
|
+
const keepRow = process.env.ABLO_SHOWCASE_KEEP === '1';
|
|
26
|
+
const color = process.stdout.isTTY && process.env.NO_COLOR === undefined;
|
|
27
|
+
const ink = {
|
|
28
|
+
blue: (value: string) => color ? `\u001B[34m${value}\u001B[39m` : value,
|
|
29
|
+
dim: (value: string) => color ? `\u001B[2m${value}\u001B[22m` : value,
|
|
30
|
+
green: (value: string) => color ? `\u001B[32m${value}\u001B[39m` : value,
|
|
31
|
+
red: (value: string) => color ? `\u001B[31m${value}\u001B[39m` : value,
|
|
32
|
+
yellow: (value: string) => color ? `\u001B[33m${value}\u001B[39m` : value,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function pause(ms = delayMs): Promise<void> {
|
|
36
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function heading(index: number, title: string): void {
|
|
40
|
+
console.log(`\n${ink.blue(String(index).padStart(2, '0'))} ${title}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fact(label: string, value: unknown): void {
|
|
44
|
+
console.log(` ${ink.dim(label.padEnd(16))}${String(value)}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ok(message: string): void {
|
|
48
|
+
console.log(` ${ink.green('✓')} ${message}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main(): Promise<void> {
|
|
52
|
+
const control = Ablo({ schema, apiKey });
|
|
53
|
+
const sessions = Sessions({ schema, apiKey });
|
|
54
|
+
const participants: Array<{ dispose(): Promise<void> }> = [];
|
|
55
|
+
let dealId: string | undefined;
|
|
56
|
+
let humanClaim: { release(): Promise<void> } | undefined;
|
|
57
|
+
let agentClaim: { release(): Promise<void> } | undefined;
|
|
58
|
+
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(' ABLO / ONE ROW, TWO ACTORS, ONE CONFIRMED REALITY');
|
|
61
|
+
console.log(ink.dim(' Real credentials · real coordination · no model call · no simulated server'));
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await control.ready();
|
|
65
|
+
|
|
66
|
+
heading(1, 'Create one shared row');
|
|
67
|
+
const deal = await control.deals.create({
|
|
68
|
+
data: {
|
|
69
|
+
name: `Northstar renewal ${Date.now().toString(36)}`,
|
|
70
|
+
stage: 'open',
|
|
71
|
+
value: 100_000,
|
|
72
|
+
revision: 1,
|
|
73
|
+
note: 'Initial account plan',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
dealId = deal.id;
|
|
77
|
+
fact('row', `${deal.id} · revision ${deal.revision}`);
|
|
78
|
+
ok('create resolved after authoritative confirmation');
|
|
79
|
+
|
|
80
|
+
heading(2, 'Give each actor its own bounded authority');
|
|
81
|
+
const humanSession = await sessions.create({
|
|
82
|
+
user: { id: `showcase-human-${deal.id}` },
|
|
83
|
+
can: { deals: ['read', 'update'] },
|
|
84
|
+
ttlSeconds: 300,
|
|
85
|
+
});
|
|
86
|
+
const agentSession = await sessions.create({
|
|
87
|
+
agent: { id: `showcase-agent-${deal.id}` },
|
|
88
|
+
onBehalfOf: { user: { id: `showcase-human-${deal.id}` } },
|
|
89
|
+
can: { deals: ['read', 'update'] },
|
|
90
|
+
ttlSeconds: 300,
|
|
91
|
+
});
|
|
92
|
+
const human = Ablo({ schema, session: humanSession, transport: 'http' });
|
|
93
|
+
const pricingAgent = Ablo({ schema, session: agentSession, transport: 'http' });
|
|
94
|
+
participants.push(human, pricingAgent);
|
|
95
|
+
await Promise.all([human.ready(), pricingAgent.ready()]);
|
|
96
|
+
fact('human', `${human.identity?.participantKind}:${human.identity?.participantId}`);
|
|
97
|
+
fact('agent', `${pricingAgent.identity?.participantKind}:${pricingAgent.identity?.participantId}`);
|
|
98
|
+
fact('agent can', pricingAgent.identity?.operations.join(', '));
|
|
99
|
+
|
|
100
|
+
heading(3, 'Reject reasoning built on stale state');
|
|
101
|
+
const agentRead = await pricingAgent.deals.read({ id: deal.id });
|
|
102
|
+
if (!agentRead) throw new Error('The agent could not read the showcase row.');
|
|
103
|
+
fact('agent reads', `revision ${agentRead.revision} · value ${agentRead.value}`);
|
|
104
|
+
console.log(` ${ink.yellow('…')} agent starts a slow pricing calculation`);
|
|
105
|
+
await pause();
|
|
106
|
+
|
|
107
|
+
const humanEdit = await human.deals.update({
|
|
108
|
+
id: deal.id,
|
|
109
|
+
data: {
|
|
110
|
+
value: 120_000,
|
|
111
|
+
revision: 2,
|
|
112
|
+
note: 'Human added the expansion seats',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
fact('human writes', `revision ${humanEdit.revision} · value ${humanEdit.value}`);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await pricingAgent.deals.update({
|
|
119
|
+
id: deal.id,
|
|
120
|
+
data: {
|
|
121
|
+
value: 105_000,
|
|
122
|
+
revision: 2,
|
|
123
|
+
note: 'Price calculated from revision 1',
|
|
124
|
+
},
|
|
125
|
+
reads: [agentRead],
|
|
126
|
+
idempotencyKey: `showcase:${deal.id}:stale-price`,
|
|
127
|
+
});
|
|
128
|
+
throw new Error('The stale write unexpectedly landed.');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (!(error instanceof AbloStaleContextError)) throw error;
|
|
131
|
+
console.log(` ${ink.red('REJECTED')} ${error.code} — the human edit was not overwritten`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const afterRejection = await control.deals.get({ id: deal.id });
|
|
135
|
+
if (!afterRejection) throw new Error('The showcase row disappeared after stale rejection.');
|
|
136
|
+
fact('still true', `revision ${afterRejection.revision} · value ${afterRejection.value}`);
|
|
137
|
+
|
|
138
|
+
heading(4, 'Serialize slow work with a visible claim');
|
|
139
|
+
const claimedByHuman = await human.deals.claim({
|
|
140
|
+
id: deal.id,
|
|
141
|
+
description: 'reviewing commercial terms',
|
|
142
|
+
ttl: '30s',
|
|
143
|
+
});
|
|
144
|
+
humanClaim = claimedByHuman;
|
|
145
|
+
fact('human holds', `fence ${claimedByHuman.fenceToken}`);
|
|
146
|
+
|
|
147
|
+
const queuedClaim = pricingAgent.deals.claim({
|
|
148
|
+
id: deal.id,
|
|
149
|
+
description: 'recalculating final price',
|
|
150
|
+
ttl: '30s',
|
|
151
|
+
queue: true,
|
|
152
|
+
});
|
|
153
|
+
await pause();
|
|
154
|
+
const queue = await control.deals.claim.queue({ id: deal.id });
|
|
155
|
+
fact('agent waits', `${queue.size} participant in FIFO queue`);
|
|
156
|
+
|
|
157
|
+
await human.deals.update({
|
|
158
|
+
id: deal.id,
|
|
159
|
+
data: {
|
|
160
|
+
stage: 'reviewing',
|
|
161
|
+
revision: 3,
|
|
162
|
+
note: 'Commercial review complete',
|
|
163
|
+
},
|
|
164
|
+
claim: claimedByHuman,
|
|
165
|
+
idempotencyKey: `showcase:${deal.id}:human-review`,
|
|
166
|
+
});
|
|
167
|
+
await claimedByHuman.release();
|
|
168
|
+
humanClaim = undefined;
|
|
169
|
+
|
|
170
|
+
const claimedByAgent = await queuedClaim;
|
|
171
|
+
agentClaim = claimedByAgent;
|
|
172
|
+
fact('agent receives', `fresh revision ${claimedByAgent.data.revision} · fence ${claimedByAgent.fenceToken}`);
|
|
173
|
+
ok('the queued agent did not continue from its earlier revision 1 read');
|
|
174
|
+
|
|
175
|
+
heading(5, 'Commit and inspect the durable evidence');
|
|
176
|
+
const commitId = `showcase:${deal.id}:agent-approval`;
|
|
177
|
+
const confirmed = await pricingAgent.deals.update({
|
|
178
|
+
id: deal.id,
|
|
179
|
+
data: {
|
|
180
|
+
stage: 'approved',
|
|
181
|
+
value: 118_000,
|
|
182
|
+
revision: 4,
|
|
183
|
+
note: 'Repriced from the fresh claimed row',
|
|
184
|
+
},
|
|
185
|
+
claim: claimedByAgent,
|
|
186
|
+
idempotencyKey: commitId,
|
|
187
|
+
});
|
|
188
|
+
await claimedByAgent.release();
|
|
189
|
+
agentClaim = undefined;
|
|
190
|
+
|
|
191
|
+
const evidence = await control.commits.get({ id: commitId });
|
|
192
|
+
if (!evidence) throw new Error(`Durable commit ${commitId} was not found.`);
|
|
193
|
+
fact('row', `${confirmed.id} · revision ${confirmed.revision} · ${confirmed.stage}`);
|
|
194
|
+
fact('commit', commitId);
|
|
195
|
+
fact('status', evidence.status);
|
|
196
|
+
fact('actor', `${evidence.actor.kind}:${evidence.actor.id}`);
|
|
197
|
+
fact('claim refs', evidence.claims.length);
|
|
198
|
+
fact('attempts', evidence.attempts.length);
|
|
199
|
+
fact('confirmation', `${Date.parse(evidence.statusAt) - Date.parse(evidence.createdAt)} ms`);
|
|
200
|
+
ok('the authoritative source confirmed the write before the SDK resolved');
|
|
201
|
+
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(` ${ink.green('PROVEN')} stale work did not land · contenders serialized · fresh state won · evidence persisted`);
|
|
204
|
+
if (keepRow) fact('kept row', deal.id);
|
|
205
|
+
} finally {
|
|
206
|
+
await agentClaim?.release().catch(() => undefined);
|
|
207
|
+
await humanClaim?.release().catch(() => undefined);
|
|
208
|
+
await Promise.all(participants.map((participant) => participant.dispose()));
|
|
209
|
+
if (dealId && !keepRow) {
|
|
210
|
+
await control.deals.delete({ id: dealId }).catch(() => undefined);
|
|
211
|
+
}
|
|
212
|
+
await control.dispose();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
main().catch((error: unknown) => {
|
|
217
|
+
console.error(`\n ${ink.red('SHOWCASE FAILED')}`, error);
|
|
218
|
+
process.exitCode = 1;
|
|
219
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The isolated model used by the terminal product proof. */
|
|
2
|
+
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
3
|
+
|
|
4
|
+
export const schema = defineSchema({
|
|
5
|
+
deals: model({
|
|
6
|
+
name: z.string(),
|
|
7
|
+
stage: z.enum(['open', 'reviewing', 'approved']),
|
|
8
|
+
value: z.number(),
|
|
9
|
+
revision: z.number(),
|
|
10
|
+
note: z.string(),
|
|
11
|
+
}),
|
|
12
|
+
});
|
package/llms.txt
CHANGED
|
@@ -66,7 +66,7 @@ Responses also carry `RateLimit-Policy` (the standing allowance, e.g. `"secret";
|
|
|
66
66
|
|
|
67
67
|
Every surface reaches the same coordinated state. They are not interchangeable.
|
|
68
68
|
|
|
69
|
-
- SDK, `@abloatai/ablo`
|
|
69
|
+
- SDK, `@abloatai/ablo` — API-key service clients use HTTP; scoped session clients use one reconnecting WebSocket by default. The credential is the identity in both cases.
|
|
70
70
|
- Coordination MCP, `@abloatai/mcp` — an agent living inside an MCP host (Claude, Cursor). Two jobs: MANAGING your Ablo the way the CLI does (`get_schema`, `list_projects`, `create_project`, `tail_logs`, `get_usage`), and claim/commit as tools over your rows. Call `get_schema` first — it is the only way to learn the model names every data tool needs. `init`, `push`, `pull`, `generate` have no tools (they touch your repo); run those in a shell.
|
|
71
71
|
- `humans()` with `@abloatai/humans/react` — the interfaces a person watches agent work arrive in. The bare client is the coordination layer (commit, read, observe, claim); `humans()` is the plugin that adds the local watchable copy, live queries, and presence. It `requires: { duplex: true }`, so a `transport: 'http'` agent is rejected at construction rather than left holding a subscription that never delivers. It remains the compatibility default for omitted `plugins`; new code should install it explicitly from `@abloatai/humans`. There is no `agents()` plugin — agents are the default caller, not a special one. A browser NEVER receives the secret key; mint a session token.
|
|
72
72
|
- CLI, `ablo` — scaffolding, schema push, connecting a database. Terminals and CI. Agents must run it non-interactively (see the CLI section below).
|
|
@@ -153,8 +153,8 @@ beside the rows. Pass `nextCursor` back as `cursor`, keeping `where` and
|
|
|
153
153
|
`orderBy` the same, to walk the rest. Check `hasMore` before treating a result
|
|
154
154
|
as complete.
|
|
155
155
|
|
|
156
|
-
Workers import the same app schema
|
|
157
|
-
|
|
156
|
+
Workers import the same app schema. API-key workers use HTTP; session workers
|
|
157
|
+
use WebSocket by default. The typed `ablo.<model>` contract does not change. There is no public
|
|
158
158
|
schema-less or string-keyed model client.
|
|
159
159
|
|
|
160
160
|
React reads should use selector `useAblo`: `useAblo((ablo) => ablo.weatherReports.local.get(id))` (synchronous local read, reactive in render).
|
|
@@ -269,13 +269,13 @@ one direct mutation with a typed `ablo.<model>.update(...)`, use selector
|
|
|
269
269
|
|
|
270
270
|
A PLANE is what a credential acts on: `production` is the root branch and development/preview branches are children. Rows, the registered database, and the active schema artifact are all PER PLANE. Every `sk_` has an immutable persisted branch binding; its spelling does not distinguish root from child, so app code never passes an environment. A child copies its parent's active schema at creation and owns its artifact after that. A CHILD push does NOT reach production. Production gets models only when the reviewed deployment pushes to the root.
|
|
271
271
|
|
|
272
|
-
Going live is three things, each done with a root-bound `sk_` key: register the production database (`ablo connect apply` — the DIRECT host, never a pooler; a pooler refuses in the words of a wrong password), push the schema AHEAD of the code that needs it, and hold the right credential per runtime (server/serverless `sk_`; browser `pk_` read-only or
|
|
272
|
+
Going live is three things, each done with a root-bound `sk_` key: register the production database (`ablo connect apply` — the DIRECT host, never a pooler; a pooler refuses in the words of a wrong password), push the schema AHEAD of the code that needs it, and hold the right credential per runtime (server/serverless `sk_`; browser `pk_` read-only or `session: { endpoint }` minting `ek_`). `ablo login` stores an `mk_` management credential and CANNOT read/write application data or push the production schema — a production push needs a root-bound `sk_` in `ABLO_API_KEY`. Gate a deploy on `npx ablo status --json` having an EMPTY `blockers` array; each blocker carries a `problem` and the one `fix`. Your agents do NOT each hold a database connection — they talk to Ablo, and Ablo holds at most 4 connections per plane (`application_name = 'ablo-direct-writer'`) however many callers write behind them, so size the database for that number and not for your agent count. Read `deployment` for the full path.
|
|
273
273
|
|
|
274
274
|
## Public Surface
|
|
275
275
|
|
|
276
276
|
Import from these public paths only:
|
|
277
277
|
|
|
278
|
-
- `@abloatai/ablo` — headless
|
|
278
|
+
- `@abloatai/ablo` — headless `Ablo` (API-key HTTP or session WebSocket), errors, typed model clients, claims, durable observation, and `dataSource`.
|
|
279
279
|
- `@abloatai/ablo/client` — reactive WebSocket client, presence, and the human-facing materializer.
|
|
280
280
|
- `@abloatai/ablo/schema` — schema DSL.
|
|
281
281
|
- `@abloatai/ablo/react` — React provider and hooks.
|
|
@@ -288,10 +288,10 @@ Import from these public paths only:
|
|
|
288
288
|
|
|
289
289
|
Do not teach `/api`, `/agent`, `/core`, `/realtime`, or internal subpaths. (`/source` and `/ai-sdk` are public.)
|
|
290
290
|
|
|
291
|
-
`onChange`
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
291
|
+
`context().onChange` uses a one-shot SSE response for an HTTP client and reuses
|
|
292
|
+
the session socket for a session client. The
|
|
293
|
+
human-facing client also owns a socket and local graph. A session identifies the
|
|
294
|
+
caller; it is not itself a transport.
|
|
295
295
|
|
|
296
296
|
## CLI: agents run it NON-INTERACTIVELY
|
|
297
297
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"description": "The public Ablo SDK for coordinated reads, commits, claims, observation, and reactive applications.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
"import": "./dist/auth.js",
|
|
66
66
|
"default": "./dist/auth.js"
|
|
67
67
|
},
|
|
68
|
+
"./sessions": {
|
|
69
|
+
"types": "./dist/sessions.d.ts",
|
|
70
|
+
"import": "./dist/sessions.js",
|
|
71
|
+
"default": "./dist/sessions.js"
|
|
72
|
+
},
|
|
68
73
|
"./coordination": {
|
|
69
74
|
"types": "./dist/coordination.d.ts",
|
|
70
75
|
"import": "./dist/coordination.js",
|
|
@@ -115,8 +120,8 @@
|
|
|
115
120
|
"verify:context-package": "node scripts/verify-context-package.mjs",
|
|
116
121
|
"typecheck": "tsc --noEmit && tsc -p typetests/tsconfig.json && tsc -p examples/tsconfig.json",
|
|
117
122
|
"test": "vitest run",
|
|
118
|
-
"generate:errors": "tsx scripts/generate-error-docs.mts",
|
|
119
|
-
"lint:errors": "tsx scripts/check-error-docs.mts",
|
|
123
|
+
"generate:errors": "tsx --conditions=@ablo/source scripts/generate-error-docs.mts",
|
|
124
|
+
"lint:errors": "tsx --conditions=@ablo/source scripts/check-error-docs.mts",
|
|
120
125
|
"generate:pricing": "tsx scripts/generate-pricing-docs.mts",
|
|
121
126
|
"lint:pricing": "tsx scripts/check-pricing-docs.mts",
|
|
122
127
|
"generate:openapi": "tsx --conditions=@ablo/source scripts/generate-openapi.mts",
|
|
@@ -140,8 +145,8 @@
|
|
|
140
145
|
"directory": "packages/ablo"
|
|
141
146
|
},
|
|
142
147
|
"dependencies": {
|
|
143
|
-
"@abloatai/humans": "^0.
|
|
144
|
-
"@abloatai/transaction": "^0.
|
|
148
|
+
"@abloatai/humans": "^0.60.0",
|
|
149
|
+
"@abloatai/transaction": "^0.60.0",
|
|
145
150
|
"zod": "^4.4.3"
|
|
146
151
|
},
|
|
147
152
|
"peerDependencies": {
|