@abloatai/ablo 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +96 -26
- package/NOTICE +1 -1
- package/docs/agents.md +28 -28
- package/docs/api-keys.md +30 -3
- package/docs/api.md +1 -1
- package/docs/cli.md +3 -3
- package/docs/concurrency-convention.md +7 -7
- package/docs/context.md +11 -11
- package/docs/coordination.md +19 -19
- package/docs/customer-organizations.md +215 -0
- package/docs/data-sources.md +3 -3
- package/docs/debugging.md +8 -8
- package/docs/examples/agent-human.md +17 -17
- package/docs/examples/ai-sdk-tool.md +6 -6
- package/docs/examples/existing-python-backend.md +1 -1
- package/docs/examples/nextjs.md +15 -15
- package/docs/examples/scoped-agent.md +10 -10
- package/docs/examples/server-agent.md +14 -14
- package/docs/groups.md +12 -12
- package/docs/how-it-works.md +7 -7
- package/docs/idempotency.md +4 -4
- package/docs/identity.md +45 -38
- package/docs/index.md +2 -1
- package/docs/integration-guide.md +1 -1
- package/docs/integrations/inngest.md +2 -2
- package/docs/integrations/temporal.md +3 -3
- package/docs/integrations.md +3 -3
- package/docs/react.md +4 -4
- package/docs/sessions.md +21 -16
- package/docs/webhooks.md +2 -2
- package/examples/README.md +6 -6
- package/examples/agent-turn.ts +11 -11
- package/examples/data-source/README.md +3 -3
- package/examples/data-source/customer-server.ts +23 -23
- package/examples/data-source/run.ts +8 -8
- package/examples/data-source/schema.ts +2 -2
- package/package.json +3 -3
package/examples/agent-turn.ts
CHANGED
|
@@ -2,32 +2,32 @@
|
|
|
2
2
|
* Canonical cheap turn: explicitly attach the exact rows used to decide the
|
|
3
3
|
* write. Ablo keeps their watermarks opaque and checks them at commit time.
|
|
4
4
|
*
|
|
5
|
-
* Run: ABLO_API_KEY=sk_...
|
|
5
|
+
* Run: ABLO_API_KEY=sk_... RECORD_ID=record_... npx tsx examples/agent-turn.ts
|
|
6
6
|
*/
|
|
7
7
|
import { Ablo } from '@abloatai/ablo';
|
|
8
8
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
9
9
|
|
|
10
10
|
const schema = defineSchema({
|
|
11
|
-
|
|
11
|
+
records: model({
|
|
12
12
|
title: z.string(),
|
|
13
13
|
status: z.enum(['pending', 'done']),
|
|
14
14
|
result: z.string().optional(),
|
|
15
15
|
}),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
if (!
|
|
18
|
+
const recordId = process.env.RECORD_ID;
|
|
19
|
+
if (!recordId) throw new Error('RECORD_ID is required');
|
|
20
20
|
|
|
21
21
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
22
22
|
try {
|
|
23
23
|
await ablo.ready();
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
26
|
-
const commitId = `
|
|
27
|
-
await ablo.
|
|
28
|
-
id:
|
|
29
|
-
data: { status: 'done', result: `Completed: ${
|
|
30
|
-
reads: [
|
|
24
|
+
const record = await ablo.records.get({ id: recordId });
|
|
25
|
+
if (!record) throw new Error(`Record ${recordId} was not found`);
|
|
26
|
+
const commitId = `record:${recordId}:cheap`;
|
|
27
|
+
await ablo.records.update({
|
|
28
|
+
id: record.id,
|
|
29
|
+
data: { status: 'done', result: `Completed: ${record.title}` },
|
|
30
|
+
reads: [record],
|
|
31
31
|
idempotencyKey: commitId,
|
|
32
32
|
});
|
|
33
33
|
const record = await ablo.commits.get({ id: commitId });
|
|
@@ -93,9 +93,9 @@ createServer(async (req, res) => {
|
|
|
93
93
|
Replace the `Map`-based store in `customer-server.ts` with your real
|
|
94
94
|
data layer. The handler shape stays the same:
|
|
95
95
|
|
|
96
|
-
- `
|
|
97
|
-
- `
|
|
98
|
-
- `
|
|
96
|
+
- `records.load({ id })` -> `db.record.findUnique({ where: { id } })`
|
|
97
|
+
- `records.list({ query })` -> `db.record.findMany({ take, cursor })`
|
|
98
|
+
- `records.commit({ operations, clientTxId })` -> `db.$transaction` that
|
|
99
99
|
applies each `op` and writes an outbox marker with `clientTxId` before commit
|
|
100
100
|
- `events({ cursor, limit })` -> read from your outbox table, return
|
|
101
101
|
rows with their `clientTxId` (Ablo dedupes its own commits) and the
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from '@abloatai/ablo/source';
|
|
24
24
|
import { schema } from './schema';
|
|
25
25
|
|
|
26
|
-
type
|
|
26
|
+
type RecordRow = {
|
|
27
27
|
id: string;
|
|
28
28
|
title: string;
|
|
29
29
|
status: 'todo' | 'doing' | 'done';
|
|
@@ -31,9 +31,9 @@ type TaskRow = {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
// Stand-in for the customer's real database. Map keyed by row id.
|
|
34
|
-
const
|
|
34
|
+
const recordStore = new Map<string, RecordRow>();
|
|
35
35
|
|
|
36
|
-
// Outbox table. In production this is a `
|
|
36
|
+
// Outbox table. In production this is a `records_outbox` Postgres table
|
|
37
37
|
// populated in the same transaction as the app-row write. Ablo polls `events`
|
|
38
38
|
// to fan out changes that bypassed Ablo, and to repair SDK-origin writes if
|
|
39
39
|
// Ablo's immediate post-commit append failed.
|
|
@@ -41,8 +41,8 @@ const outbox: SourceEvent[] = [];
|
|
|
41
41
|
let outboxSequence = 0;
|
|
42
42
|
|
|
43
43
|
// Seed one row so the example's first `load` returns something.
|
|
44
|
-
|
|
45
|
-
id: '
|
|
44
|
+
recordStore.set('record_seed', {
|
|
45
|
+
id: 'record_seed',
|
|
46
46
|
title: 'Seeded by customer database',
|
|
47
47
|
status: 'todo',
|
|
48
48
|
});
|
|
@@ -91,13 +91,13 @@ export const handleAbloSource = dataSource({
|
|
|
91
91
|
return {};
|
|
92
92
|
},
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
records: {
|
|
95
95
|
load({ id }) {
|
|
96
|
-
return
|
|
96
|
+
return recordStore.get(id) ?? null;
|
|
97
97
|
},
|
|
98
98
|
|
|
99
99
|
list({ query }) {
|
|
100
|
-
const all = Array.from(
|
|
100
|
+
const all = Array.from(recordStore.values());
|
|
101
101
|
const start = query.cursor ? Number(query.cursor) : 0;
|
|
102
102
|
const limit = query.limit ?? 50;
|
|
103
103
|
const page = all.slice(start, start + limit);
|
|
@@ -115,7 +115,7 @@ export const handleAbloSource = dataSource({
|
|
|
115
115
|
// update; the surrounding `apply` helper shows where you would
|
|
116
116
|
// open `db.transaction(async (tx) => { ... })`.
|
|
117
117
|
commit({ operations, clientTxId }) {
|
|
118
|
-
const rows:
|
|
118
|
+
const rows: RecordRow[] = [];
|
|
119
119
|
for (const op of operations) {
|
|
120
120
|
const row = applyOperation(op, clientTxId);
|
|
121
121
|
if (row) rows.push(row);
|
|
@@ -145,38 +145,38 @@ export const handleAbloSource = dataSource({
|
|
|
145
145
|
function applyOperation(
|
|
146
146
|
op: SourceOperation,
|
|
147
147
|
clientTxId: string | undefined,
|
|
148
|
-
):
|
|
149
|
-
if (op.model !== '
|
|
150
|
-
const id = op.id ?? `
|
|
148
|
+
): RecordRow | null {
|
|
149
|
+
if (op.model !== 'records') return null;
|
|
150
|
+
const id = op.id ?? `record_${Math.random().toString(36).slice(2, 10)}`;
|
|
151
151
|
|
|
152
152
|
if (op.type === 'CREATE') {
|
|
153
|
-
const row:
|
|
153
|
+
const row: RecordRow = {
|
|
154
154
|
id,
|
|
155
155
|
title: String(op.input?.title ?? ''),
|
|
156
156
|
status:
|
|
157
|
-
(op.input?.status as
|
|
157
|
+
(op.input?.status as RecordRow['status'] | undefined) ?? 'todo',
|
|
158
158
|
...(op.input?.assignee
|
|
159
159
|
? { assignee: String(op.input.assignee) }
|
|
160
160
|
: {}),
|
|
161
161
|
};
|
|
162
|
-
|
|
162
|
+
recordStore.set(id, row);
|
|
163
163
|
appendOutbox({ operation: op, entityId: id, data: row, clientTxId });
|
|
164
164
|
return row;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
if (op.type === 'UPDATE') {
|
|
168
|
-
const existing =
|
|
168
|
+
const existing = recordStore.get(id);
|
|
169
169
|
if (!existing) return null;
|
|
170
|
-
const next:
|
|
171
|
-
|
|
170
|
+
const next: RecordRow = { ...existing, ...(op.input as Partial<RecordRow>) };
|
|
171
|
+
recordStore.set(id, next);
|
|
172
172
|
appendOutbox({ operation: op, entityId: id, data: next, clientTxId });
|
|
173
173
|
return next;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
if (op.type === 'DELETE') {
|
|
177
|
-
const existing =
|
|
177
|
+
const existing = recordStore.get(id);
|
|
178
178
|
if (!existing) return null;
|
|
179
|
-
|
|
179
|
+
recordStore.delete(id);
|
|
180
180
|
appendOutbox({ operation: op, entityId: id, data: null, clientTxId });
|
|
181
181
|
return existing;
|
|
182
182
|
}
|
|
@@ -187,7 +187,7 @@ function applyOperation(
|
|
|
187
187
|
function appendOutbox(input: {
|
|
188
188
|
operation: SourceOperation;
|
|
189
189
|
entityId: string;
|
|
190
|
-
data:
|
|
190
|
+
data: RecordRow | null;
|
|
191
191
|
clientTxId: string | undefined;
|
|
192
192
|
}): void {
|
|
193
193
|
outboxSequence += 1;
|
|
@@ -205,11 +205,11 @@ function appendOutbox(input: {
|
|
|
205
205
|
// Exposed for the orchestrator's `run.ts`. A real customer doesn't
|
|
206
206
|
// need this — it's a back door for the demo to verify state.
|
|
207
207
|
export function _inspectStore(): {
|
|
208
|
-
rows:
|
|
208
|
+
rows: RecordRow[];
|
|
209
209
|
outboxSize: number;
|
|
210
210
|
} {
|
|
211
211
|
return {
|
|
212
|
-
rows: Array.from(
|
|
212
|
+
rows: Array.from(recordStore.values()),
|
|
213
213
|
outboxSize: outbox.length,
|
|
214
214
|
};
|
|
215
215
|
}
|
|
@@ -36,7 +36,7 @@ async function main() {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
log('--- 1. load (existing seeded row) ---');
|
|
39
|
-
const seeded = await driver.load('
|
|
39
|
+
const seeded = await driver.load('records', 'record_seed');
|
|
40
40
|
log('loaded:', seeded);
|
|
41
41
|
|
|
42
42
|
log('\n--- 2. commit (CREATE + UPDATE in one batch) ---');
|
|
@@ -44,14 +44,14 @@ async function main() {
|
|
|
44
44
|
[
|
|
45
45
|
{
|
|
46
46
|
type: 'CREATE',
|
|
47
|
-
model: '
|
|
48
|
-
id: '
|
|
47
|
+
model: 'records',
|
|
48
|
+
id: 'record_new',
|
|
49
49
|
input: { title: 'Wire the data source', status: 'todo' },
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
type: 'UPDATE',
|
|
53
|
-
model: '
|
|
54
|
-
id: '
|
|
53
|
+
model: 'records',
|
|
54
|
+
id: 'record_seed',
|
|
55
55
|
input: { status: 'doing', assignee: 'alice' },
|
|
56
56
|
},
|
|
57
57
|
],
|
|
@@ -59,8 +59,8 @@ async function main() {
|
|
|
59
59
|
);
|
|
60
60
|
log('committed rows:', committed);
|
|
61
61
|
|
|
62
|
-
log('\n--- 3. list (all
|
|
63
|
-
const listed = await driver.list('
|
|
62
|
+
log('\n--- 3. list (all records after commit) ---');
|
|
63
|
+
const listed = await driver.list('records');
|
|
64
64
|
log('listed:', listed);
|
|
65
65
|
|
|
66
66
|
log('\n--- 4. events (outbox feed for cross-channel writes) ---');
|
|
@@ -73,7 +73,7 @@ async function main() {
|
|
|
73
73
|
apiKey: 'sk_wrong_example_key',
|
|
74
74
|
});
|
|
75
75
|
try {
|
|
76
|
-
await badDriver.load('
|
|
76
|
+
await badDriver.load('records', 'record_seed');
|
|
77
77
|
throw new Error('expected signature failure');
|
|
78
78
|
} catch (err) {
|
|
79
79
|
log('rejected as expected:', (err as Error).message);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The schema is the contract between three sides:
|
|
5
5
|
*
|
|
6
|
-
* 1. The application UI — `ablo.
|
|
6
|
+
* 1. The application UI — `ablo.records.update(...)`.
|
|
7
7
|
* 2. The Ablo Cloud — translates writes into signed POSTs.
|
|
8
8
|
* 3. The customer's Data Source endpoint — applies them to its own
|
|
9
9
|
* database.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
16
16
|
|
|
17
17
|
export const schema = defineSchema({
|
|
18
|
-
|
|
18
|
+
records: model({
|
|
19
19
|
title: z.string(),
|
|
20
20
|
status: z.enum(['todo', 'doing', 'done']),
|
|
21
21
|
assignee: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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",
|
|
@@ -137,8 +137,8 @@
|
|
|
137
137
|
"directory": "packages/ablo"
|
|
138
138
|
},
|
|
139
139
|
"dependencies": {
|
|
140
|
-
"@abloatai/humans": "^0.
|
|
141
|
-
"@abloatai/transaction": "^0.
|
|
140
|
+
"@abloatai/humans": "^0.52.0",
|
|
141
|
+
"@abloatai/transaction": "^0.52.0",
|
|
142
142
|
"zod": "^4.4.3"
|
|
143
143
|
},
|
|
144
144
|
"peerDependencies": {
|