@abloatai/ablo 0.5.1 → 0.6.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +217 -122
  3. package/dist/BaseSyncedStore.d.ts +2 -2
  4. package/dist/BaseSyncedStore.js +2 -2
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +90 -93
  8. package/dist/client/Ablo.js +121 -60
  9. package/dist/client/ApiClient.d.ts +14 -14
  10. package/dist/client/ApiClient.js +81 -55
  11. package/dist/client/createInternalComponents.d.ts +2 -3
  12. package/dist/client/createInternalComponents.js +2 -3
  13. package/dist/client/createModelProxy.d.ts +90 -87
  14. package/dist/client/createModelProxy.js +124 -127
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +3 -3
  18. package/dist/core/index.d.ts +2 -0
  19. package/dist/core/index.js +7 -0
  20. package/dist/errors.d.ts +8 -8
  21. package/dist/errors.js +18 -10
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +7 -11
  24. package/dist/interfaces/index.d.ts +2 -10
  25. package/dist/mutators/Transaction.d.ts +2 -2
  26. package/dist/mutators/Transaction.js +2 -2
  27. package/dist/mutators/mutateActions.d.ts +44 -0
  28. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  29. package/dist/mutators/readerActions.d.ts +32 -0
  30. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  31. package/dist/query/types.d.ts +1 -1
  32. package/dist/react/AbloProvider.d.ts +1 -1
  33. package/dist/react/AbloProvider.js +3 -3
  34. package/dist/react/context.d.ts +4 -4
  35. package/dist/react/index.d.ts +4 -5
  36. package/dist/react/index.js +3 -7
  37. package/dist/react/useAblo.d.ts +14 -14
  38. package/dist/react/useAblo.js +26 -26
  39. package/dist/react/useIntent.d.ts +2 -2
  40. package/dist/react/useIntent.js +2 -2
  41. package/dist/react/useMutators.d.ts +1 -1
  42. package/dist/react/usePresence.d.ts +3 -3
  43. package/dist/react/usePresence.js +4 -4
  44. package/dist/react/useUndoScope.d.ts +1 -1
  45. package/dist/schema/diff.d.ts +161 -0
  46. package/dist/schema/diff.js +262 -0
  47. package/dist/schema/generate.d.ts +19 -0
  48. package/dist/schema/generate.js +87 -0
  49. package/dist/schema/index.d.ts +4 -1
  50. package/dist/schema/index.js +7 -1
  51. package/dist/schema/schema.d.ts +83 -32
  52. package/dist/schema/schema.js +58 -12
  53. package/dist/schema/serialize.d.ts +92 -0
  54. package/dist/schema/serialize.js +227 -0
  55. package/dist/sync/SyncWebSocket.d.ts +17 -0
  56. package/dist/sync/SyncWebSocket.js +46 -1
  57. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  58. package/dist/sync/awaitIntentGrant.js +60 -0
  59. package/dist/sync/createIntentStream.js +43 -4
  60. package/dist/sync/createPresenceStream.js +1 -1
  61. package/dist/sync/participants.d.ts +2 -2
  62. package/dist/sync/participants.js +4 -4
  63. package/dist/types/global.d.ts +43 -52
  64. package/dist/types/global.js +16 -18
  65. package/dist/types/streams.d.ts +37 -9
  66. package/docs/api.md +68 -158
  67. package/docs/audit.md +5 -5
  68. package/docs/client-behavior.md +41 -42
  69. package/docs/coordination.md +294 -0
  70. package/docs/data-sources.md +14 -14
  71. package/docs/examples/agent-human.md +30 -32
  72. package/docs/examples/ai-sdk-tool.md +32 -33
  73. package/docs/examples/existing-python-backend.md +35 -33
  74. package/docs/examples/nextjs.md +24 -25
  75. package/docs/examples/server-agent.md +20 -61
  76. package/docs/guarantees.md +30 -55
  77. package/docs/identity.md +458 -0
  78. package/docs/index.md +12 -24
  79. package/docs/integration-guide.md +106 -116
  80. package/docs/interaction-model.md +29 -95
  81. package/docs/mcp/claude-code.md +3 -3
  82. package/docs/mcp/cursor.md +1 -1
  83. package/docs/mcp/windsurf.md +1 -1
  84. package/docs/mcp.md +11 -26
  85. package/docs/quickstart.md +43 -49
  86. package/docs/react.md +73 -23
  87. package/docs/roadmap.md +5 -7
  88. package/llms.txt +34 -39
  89. package/package.json +1 -1
  90. package/dist/react/useMutate.d.ts +0 -83
  91. package/dist/react/useQuery.d.ts +0 -123
  92. package/dist/react/useQuery.js +0 -145
  93. package/dist/react/useReader.d.ts +0 -69
  94. package/docs/capabilities.md +0 -163
package/docs/mcp.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Model Context Protocol
2
2
 
3
- Ablo Sync ships an MCP server at `/api/mcp`. Connect any MCP-compatible AI
4
- assistant — Claude Code, Cursor, Windsurf — and your sync resources become
3
+ Ablo ships an MCP server at `/api/mcp`. Connect any MCP-compatible AI
4
+ assistant — Claude Code, Cursor, Windsurf — and your sync models become
5
5
  typed, callable tools.
6
6
 
7
7
  ## Install
@@ -14,38 +14,23 @@ Pick your client:
14
14
 
15
15
  ## How it works
16
16
 
17
- Each resource you declare becomes one or more MCP tools:
17
+ Each model you declare becomes one or more MCP tools:
18
18
 
19
- | Resource method | MCP tool name | What it does |
19
+ | Model method | MCP tool name | What it does |
20
20
  |---|---|---|
21
- | `retrieve` | `<resource>.retrieve` | Returns the row + a stamp. |
22
- | `list` | `<resource>.list` | Cursor-paginated discovery. |
23
- | `update` | `<resource>.update` | Write, requires the prior stamp. |
24
- | `<model>.intent` | `intent.create` | Claim a row before writing, then auto-release on update. |
21
+ | `retrieve` | `<model>.retrieve` | Returns the row + a stamp. |
22
+ | `list` | `<model>.list` | Cursor-paginated discovery. |
23
+ | `update` | `<model>.update` | Write, requires the prior stamp. |
24
+ | `<model>.claim` | `claim.create` | Claim a row before writing, then release when held work finishes. |
25
25
 
26
26
  The assistant gets typed JSON schemas, real argument types, and typed
27
27
  rejections when it writes stale state. No invention, no hallucinated IDs.
28
28
 
29
29
  ## Auth
30
30
 
31
- The MCP transport requires a capability token. Create one scoped to the
32
- assistant's session:
33
-
34
- ```ts
35
- const capability = await ablo.capabilities.create({
36
- participantKind: 'agent',
37
- participantId: 'agent:claude-code',
38
- // Strings derive from the schema's `identityRoles` templates
39
- // (see integration-guide.md §1).
40
- syncGroups: ['org:acme'],
41
- operations: ['tasks.retrieve', 'tasks.update'],
42
- label: 'claude-code dev session',
43
- lease: '8h',
44
- });
45
- ```
46
-
47
- Pass `capability.token` into the MCP client's auth header configuration.
48
- See your client's setup guide for the exact mechanism.
31
+ The MCP transport uses a scoped bearer token issued by your server. Pass that
32
+ token into the MCP client's auth header configuration. See your client's setup
33
+ guide for the exact mechanism.
49
34
 
50
35
  ## Limits
51
36
 
@@ -21,7 +21,7 @@ export ABLO_API_KEY=sk_test_...
21
21
  ```
22
22
 
23
23
  `ABLO_API_KEY` is for trusted server runtimes. Browser apps should use the React
24
- provider with a scoped capability/session route, not a bundled API key.
24
+ provider with a scoped session route, not a bundled API key.
25
25
 
26
26
  ## 3. Declare a Schema
27
27
 
@@ -44,7 +44,7 @@ export const ablo = Ablo({
44
44
  ```
45
45
 
46
46
  Customer apps should always pass `schema`. Treat it like Prisma's schema file:
47
- it is the source of truth for typed model resources, realtime subscriptions,
47
+ it is the source of truth for typed model clients, realtime subscriptions,
48
48
  agent writes, and Data Source requests.
49
49
 
50
50
  ## 4. Create and Update
@@ -81,71 +81,65 @@ ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
81
81
  ## 6. AI Activity on Existing State
82
82
 
83
83
  When AI or background work will touch an existing row for more than a quick
84
- write, coordinate through `ablo.<model>.intent(id)`. It returns a handle
85
- synchronously read `.current` to see who's working on the row, `claim()` to
86
- claim it, `update()` to write under the claim (which auto-releases).
84
+ write, coordinate through the flat model verbs: `ablo.<model>.claim(id, ...)` to
85
+ claim the row (returns the row), `ablo.<model>.claimState(id)` to read who's working
86
+ on it (synchronous; never blocks), and the normal `ablo.<model>.update(id, ...)`
87
+ to write. Normal reads still work while the claim is held; server reads can opt
88
+ into `ifClaimed: 'wait'` or `ifClaimed: 'fail'` when they should not read through
89
+ active work. The callback form releases the claim when the callback returns or
90
+ throws.
87
91
 
88
92
  ```ts
89
- const report = ablo.weatherReports.intent('weather_stockholm');
90
-
91
- // If another participant holds it, wait for them to finish.
92
- if (report.current) await report.whenFree();
93
-
94
- // Claim it so other participants yield while we work.
95
- await report.claim({ action: 'checking_weather', field: 'forecast', ttl: '2m' });
96
-
97
- // Your existing weather tool or agent call. While this runs, other clients see
98
- // that weather_stockholm is being checked.
99
- const row = ablo.weatherReports.retrieve('weather_stockholm');
100
- const weather = await weatherAgent.getWeather(row.location);
101
-
102
- await report.update({
103
- status: 'ready',
104
- forecast: weather.summary,
105
- });
93
+ // Claim the row so other participants serialize behind us while we work.
94
+ await ablo.weatherReports.claim(
95
+ 'weather_stockholm',
96
+ async (report) => {
97
+ // Your existing weather tool or agent call. While this runs, other clients
98
+ // see that weather_stockholm is being checked.
99
+ const weather = await weatherAgent.getWeather(report.location);
100
+
101
+ await ablo.weatherReports.update(report.id, {
102
+ status: 'ready',
103
+ forecast: weather.summary,
104
+ });
105
+ },
106
+ { action: 'checking_weather', ttl: '2m' },
107
+ );
106
108
  ```
107
109
 
108
- Ablo does not fetch the weather. It keeps the activity visible while the work
109
- runs, rejects `report.update(...)` with `AbloStaleContextError` if the row
110
- changed under you, and releases the intent automatically once the write lands.
110
+ Ablo does not fetch the weather. The claim is **advisory**: if another
111
+ participant already holds the row, `claim` waits for them to finish and re-reads
112
+ before handing back the row. While you hold the claim, `update(id, ...)` is
113
+ stale-guarded and rejects with `AbloStaleContextError` if the row changed under
114
+ you. The claim releases automatically once the callback returns or throws.
111
115
 
112
- ## 7. Multiplayer and Busy Work
116
+ ## 7. Multiplayer and Claimed Work
113
117
 
114
118
  There is no separate multiplayer mode. Use the same schema client for human UI,
115
119
  server actions, and agents; Ablo fans out confirmed writes and keeps active
116
- intents visible on the same resource.
120
+ claims visible on the same model row.
117
121
 
118
- Intents tell you when another human or agent is active on the same target. For
119
- schema clients, wait on the intent stream and then write through the model.
122
+ `claimState(id)` tells you when another human or agent is active on the same row.
123
+ For schema clients, `claim(id, work)` waits fairly, re-reads, and then lets you
124
+ write through the model.
120
125
 
121
126
  ```ts
122
- const busy = ablo.intents.list({
123
- resource: 'weatherReports',
124
- id: 'weather_stockholm',
125
- });
126
-
127
- if (busy.length > 0) {
128
- await ablo.intents.waitFor(
129
- { resource: 'weatherReports', id: 'weather_stockholm' },
130
- { timeout: 30_000 },
131
- );
127
+ const active = ablo.weatherReports.claimState('weather_stockholm');
128
+ if (active) {
129
+ console.log(`${active.heldBy} is ${active.action}`);
132
130
  }
133
131
 
134
- await ablo.weatherReports.update('weather_stockholm', { status: 'ready' });
132
+ await ablo.weatherReports.claim('weather_stockholm', async (report) => {
133
+ await ablo.weatherReports.update(report.id, { status: 'ready' });
134
+ });
135
135
  ```
136
136
 
137
- `ifBusy` controls what happens when another human or agent is already working
138
- on the same target:
139
-
140
- - `return` returns immediately with active intents.
141
- - `wait` waits for the intent stream to clear.
142
- - `fail` throws `AbloBusyError` with the active intents attached.
137
+ Use `{ wait: false }` on `claim` when work should be skipped instead of queued
138
+ behind an active holder.
143
139
 
144
140
  ## 8. Next Steps
145
141
 
146
- Keep using the schema client for app and agent writes. Reach for the advanced
147
- schema-less agent wrapper only when a worker intentionally cannot import the
148
- app schema.
142
+ Keep using the schema client for app and agent writes.
149
143
 
150
144
  - [Integration Guide](./integration-guide.md) explains the full app, React, Data Source, multiplayer, and agent path.
151
145
  - [Guarantees](./guarantees.md) explains what confirmed writes and stale checks mean.
package/docs/react.md CHANGED
@@ -14,21 +14,71 @@ The React bindings ship with the main package — no extra install.
14
14
  import { useAblo } from '@abloatai/ablo/react';
15
15
  ```
16
16
 
17
- ## useAblo — model resource
17
+ ## AbloProvider
18
+
19
+ Mount it once near the root of your tree. It owns the connection, the local
20
+ pool, and the engine lifecycle; everything below it reads with `useAblo`.
21
+
22
+ ```tsx
23
+ 'use client';
24
+
25
+ import { AbloProvider } from '@abloatai/ablo/react';
26
+ import { schema } from '@/ablo.schema';
27
+
28
+ export function Providers({
29
+ children,
30
+ user, // resolved server-side from YOUR auth
31
+ }: {
32
+ children: React.ReactNode;
33
+ user: { id: string; teamIds: string[] };
34
+ }) {
35
+ return (
36
+ <AbloProvider
37
+ schema={schema}
38
+ userId={user.id}
39
+ teamIds={user.teamIds}
40
+ fallback={<AppSkeleton />}
41
+ >
42
+ {children}
43
+ </AbloProvider>
44
+ );
45
+ }
46
+ ```
47
+
48
+ `schema` is the only required prop. The rest are situational:
49
+
50
+ | Prop | Default | Purpose |
51
+ | ------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
52
+ | `schema` | — | **Required.** From `defineSchema()`. Determines the typed hook surface. |
53
+ | `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles.extract`. Not the security boundary. |
54
+ | `teamIds` | resolved from auth | Team ids expanded into team sync groups via `identityRoles`. |
55
+ | `syncGroups` | full allowed set | **Narrows** the subscription to a subset of what auth allows (e.g. `['deck:abc123']`). Never widens it. |
56
+ | `url` | hosted endpoint | WebSocket URL of the sync server (`wss://…`). Hosted apps omit it. |
57
+ | `apiKey` | session/cookie | Bootstrap auth. Browser apps **omit this** — the key stays server-side. See Identity below. |
58
+ | `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
59
+ | `bootstrapMode` | `'full'` | `'full'` pulls the org's baseline before ready; `'none'` skips the baseline and processes live deltas only.|
60
+ | `persistence` | `'volatile'` | `'indexeddb'` opts into an offline queue + reload-surviving cache. |
61
+ | `onSessionExpired` | — | Fired after the engine has already purged on a rejected session — use for redirect-to-sign-in. |
62
+ | `onError` | — | Engine / WebSocket / `postBootstrap` errors. Wire to Sentry / Datadog. |
63
+
64
+ Where `userId` / `teamIds` / `syncGroups` come from, and why the API key never
65
+ reaches the browser, is the whole of
66
+ [Identity & Sync Groups](./identity.md) — read that if it isn't obvious how org
67
+ / team / user map to what a participant can see.
68
+
69
+ ## useAblo — model client
18
70
 
19
71
  ```tsx
20
72
  'use client';
21
73
 
22
74
  import { useAblo } from '@abloatai/ablo/react';
23
75
 
24
- export function TaskView({ task: serverTask }: { task: { id: string; title: string } }) {
25
- const task = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
26
- const intents = useAblo((ablo) =>
27
- ablo.intents.list({ resource: 'tasks', id: serverTask.id }),
28
- ) ?? [];
29
- const busy = intents.length > 0;
76
+ export function ReportView({ report: serverReport }: { report: { id: string; location: string } }) {
77
+ const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
78
+ const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
79
+ const claimed = Boolean(active);
30
80
 
31
- return <article>{task.title}</article>;
81
+ return <article>{report.location}</article>;
32
82
  }
33
83
  ```
34
84
 
@@ -37,9 +87,9 @@ The hook:
37
87
  1. Reads through the same `ablo.<model>` methods as the rest of the SDK.
38
88
  2. Tracks the model fields read by the selector and re-renders when confirmed
39
89
  deltas arrive.
40
- 3. Lets Server Component data stay outside the hook: use `?? serverTask` when a
90
+ 3. Lets Server Component data stay outside the hook: use `?? serverReport` when a
41
91
  parent already loaded the row.
42
- 4. Works for coordination state too, such as `ablo.intents.list(...)`.
92
+ 4. Works for coordination state too, such as `ablo.weatherReports.claimState(id)`.
43
93
 
44
94
  Use the zero-argument form only when you need the full client for callbacks,
45
95
  effects, or writes:
@@ -50,15 +100,15 @@ const abloClient = useAblo();
50
100
 
51
101
  Prefer selector reads like `useAblo((ablo) => ablo.<model>.retrieve(id))`.
52
102
  String model names are kept on older hooks for compatibility, but first examples
53
- should use the same model-resource shape as the rest of the SDK.
103
+ should use the same model-client shape as the rest of the SDK.
54
104
 
55
- For collections, keep the selector on the model resource too:
105
+ For collections, keep the selector on the model client too:
56
106
 
57
107
  ```tsx
58
- const tasks = useAblo((ablo) =>
59
- ablo.tasks.list({
108
+ const reports = useAblo((ablo) =>
109
+ ablo.weatherReports.list({
60
110
  where: { projectId },
61
- filter: (task) => task.status !== 'done',
111
+ filter: (report) => report.status !== 'ready',
62
112
  scope: 'live',
63
113
  }),
64
114
  );
@@ -67,7 +117,7 @@ const tasks = useAblo((ablo) =>
67
117
  ## Server Load
68
118
 
69
119
  ```tsx
70
- const [task] = await ablo.tasks.load({ where: { id } });
120
+ const [report] = await ablo.weatherReports.load({ where: { id } });
71
121
  ```
72
122
 
73
123
  Use `load` in Server Components when the row may not be in the local pool yet.
@@ -79,8 +129,8 @@ For Server Actions and route handlers, call the SDK directly:
79
129
  ```ts
80
130
  import { ablo } from '@/lib/ablo';
81
131
 
82
- const snap = ablo.snapshot({ tasks: id });
83
- await ablo.tasks.update(id, patch, {
132
+ const snap = ablo.snapshot({ weatherReports: id });
133
+ await ablo.weatherReports.update(id, patch, {
84
134
  readAt: snap.stamp,
85
135
  onStale: 'reject',
86
136
  wait: 'confirmed',
@@ -88,17 +138,17 @@ await ablo.tasks.update(id, patch, {
88
138
  ```
89
139
 
90
140
  For client event handlers, get the provider-owned client and call the same
91
- model resource:
141
+ model client:
92
142
 
93
143
  ```tsx
94
144
  const ablo = useAblo();
95
145
 
96
- async function markDone() {
146
+ async function markReady() {
97
147
  if (!ablo) return;
98
- const snap = ablo.snapshot({ tasks: id });
99
- await ablo.tasks.update(
148
+ const snap = ablo.snapshot({ weatherReports: id });
149
+ await ablo.weatherReports.update(
100
150
  id,
101
- { status: 'done' },
151
+ { status: 'ready' },
102
152
  { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
103
153
  );
104
154
  }
package/docs/roadmap.md CHANGED
@@ -4,8 +4,7 @@ What is shipped, what is next, and what we will not build.
4
4
 
5
5
  ## Shipped
6
6
 
7
- - **Resources, intents, commits** — the core API.
8
- - **Capability tokens** — Biscuit-signed, scoped, attenuable, hot-revocable.
7
+ - **Models, claims, commits** — the core API.
9
8
  - **Audit log** — hash-chained per principal, with `delegationChainRoot`.
10
9
  - **MCP transport** — HTTP server at `/api/mcp`.
11
10
  - **TypeScript SDK** — `@abloatai/ablo`, with React bindings.
@@ -13,13 +12,12 @@ What is shipped, what is next, and what we will not build.
13
12
 
14
13
  ## In flight
15
14
 
16
- - **Real-time presence** — see who else is viewing/editing a resource.
15
+ - **Real-time presence** — see who else is viewing/editing a model.
17
16
  - **Cross-instance fan-out via Redis** — pub/sub deltas at scale.
18
- - **Hot capability revocation UI** — in the dashboard, today via API only.
19
17
 
20
18
  ## On deck
21
19
 
22
- - **Schema migrations** — declarative resource schema changes.
20
+ - **Schema migrations** — declarative model schema changes.
23
21
  - **Field-level subscriptions** — subscribe to one path, not the whole row.
24
22
  - **Bulk import/export** — CSV/JSON round-trip with chain verification.
25
23
 
@@ -31,11 +29,11 @@ What is shipped, what is next, and what we will not build.
31
29
 
32
30
  ## We will not build
33
31
 
34
- - **A general-purpose Postgres wrapper** — Ablo Sync is for state with
32
+ - **A general-purpose Postgres wrapper** — Ablo is for state with
35
33
  concurrency semantics, not for storing every table.
36
34
  - **Server-side compute** — no triggers, no stored procedures. Compute
37
35
  belongs in your application code.
38
- - **A document database UI** — your data lives in Ablo Sync; the UI is your
36
+ - **A document database UI** — your data lives in Ablo; the UI is your
39
37
  product, not ours.
40
38
 
41
39
  ## How priorities shift
package/llms.txt CHANGED
@@ -11,34 +11,26 @@ import Ablo from '@abloatai/ablo';
11
11
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
12
12
 
13
13
  const schema = defineSchema({
14
- tasks: model({
14
+ weatherReports: model({
15
15
  id: z.string(),
16
- title: z.string(),
17
- status: z.enum(['todo', 'doing', 'done']),
18
- summary: z.string().optional(),
16
+ location: z.string(),
17
+ status: z.enum(['pending', 'ready']),
18
+ forecast: z.string().optional(),
19
19
  }),
20
20
  });
21
21
 
22
22
  const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
23
23
 
24
- const [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
25
- if (!task) throw new Error('Task not found');
26
-
27
- const intents = ablo.intents.list({ resource: 'tasks', id: 'task_123' });
28
- if (intents.length > 0) {
29
- await ablo.intents.waitFor({ resource: 'tasks', id: 'task_123' }, { timeout: 30_000 });
30
- }
31
-
32
- const snap = ablo.snapshot({ tasks: 'task_123' });
33
- const updated = await ablo.tasks.update(
34
- 'task_123',
35
- { status: 'done', summary: await summarize(task) },
36
- {
37
- readAt: snap.stamp,
38
- onStale: 'reject',
39
- wait: 'confirmed',
40
- },
41
- );
24
+ const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
25
+ if (!report) throw new Error('Row not found');
26
+
27
+ const updated = await ablo.weatherReports.claim('report_stockholm', async (report) => {
28
+ return ablo.weatherReports.update(
29
+ report.id,
30
+ { status: 'ready', forecast: await getForecast(report) },
31
+ { wait: 'confirmed' },
32
+ );
33
+ });
42
34
  ```
43
35
 
44
36
  That is the normal app path: declare models in a schema, then use `ablo.<model>.load(...)`, `ablo.<model>.retrieve(...)`, `ablo.<model>.create(...)`, `ablo.<model>.update(...)`, and `ablo.<model>.delete(...)`.
@@ -54,7 +46,7 @@ defaults to `'live'`, with `'archived'` and `'all'` for lifecycle-aware reads.
54
46
  Advanced schema-less agents exist for workers that cannot import the app schema,
55
47
  but do not teach that path first.
56
48
 
57
- React reads should use selector `useAblo`: `useAblo((ablo) => ablo.tasks.retrieve(id))`.
49
+ React reads should use selector `useAblo`: `useAblo((ablo) => ablo.weatherReports.retrieve(id))`.
58
50
  Use zero-argument `useAblo()` only when a component needs the client for an
59
51
  event handler or effect. Treat `useQuery`, `useOne`, `useReader`, and
60
52
  `useMutate` as compatibility hooks for older string-keyed integrations, not the
@@ -64,36 +56,39 @@ first integration path.
64
56
 
65
57
  Multiplayer is not a separate mode. When human UI, server actions, and agents use
66
58
  the same schema client and write through `ablo.<model>`, Ablo coordinates the
67
- shared resource stream: confirmed deltas fan out to subscribers, active intents
68
- are visible, and stale writes can be rejected with `readAt`.
59
+ shared model stream: confirmed deltas fan out to subscribers, active claims are
60
+ visible through `claimState(id)`, and stale writes can be rejected with `readAt`.
69
61
 
70
62
  If an app writes directly to its own database outside Ablo, that write bypasses
71
63
  coordination until the app reports it through Data Source events.
72
64
 
73
65
  ## Nouns
74
66
 
75
- - `Model resource` is the typed `ablo.<model>` object generated from schema.
76
- - `Intent` declares active work on a resource so humans and agents can coordinate.
67
+ - `Model client` is the typed `ablo.<model>` object generated from schema.
68
+ - `Claim` holds a model row while slow work runs; `claimState(id)` observes it.
77
69
  - `Commit` is the durable protocol write behind `ablo.<model>.update(...)`.
78
70
  - `Receipt` confirms the commit.
79
- - `Capability` scopes what an agent may do. `agent.run(...)` handles the common case.
80
- - `Task` is one agent run, with audit and cost attribution.
81
71
 
82
- ## Busy Behavior
72
+ ## Claimed Behavior
83
73
 
84
- Reads never silently block. Pass `ifBusy: 'return'` to receive active intents, `ifBusy: 'fail'` to throw `AbloBusyError`, or `ifBusy: 'wait'` to wait until the active intent clears.
74
+ Reads never silently block. Schema reads stay open while a row is claimed.
75
+ Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'` to
76
+ receive active claims, `ifClaimed: 'fail'` to throw `AbloClaimedError`, or
77
+ `ifClaimed: 'wait'` to wait until the active claim clears.
85
78
 
86
- Schema clients wait from the realtime intent stream. Schema-less HTTP callers must provide an explicit `busyPollInterval` when using `ifBusy: 'wait'`; Ablo does not hide a hard-coded polling loop.
79
+ Schema clients wait from the realtime claim stream. Schema-less HTTP callers must provide an explicit `claimedPollInterval` when using `ifClaimed: 'wait'`; Ablo does not hide a hard-coded polling loop.
87
80
 
88
- Use `busyTimeout` only as a maximum wait, not as the coordination mechanism.
81
+ Use `claimedTimeout` only as a maximum wait, not as the coordination mechanism.
89
82
 
90
83
  ## Guarantees
91
84
 
92
85
  `wait: 'confirmed'` means the server accepted the write. Schema model writes are optimistic by default; server rejection rolls back local state. Use `snapshot(...)` plus `readAt` and `onStale: 'reject'` to prevent lost updates.
93
86
 
94
- Intents are coordination signals, not database locks. Capabilities and tasks are real protocol primitives, but most users let the SDK manage them through schema-backed writes or advanced `agent.run(...)`.
87
+ Claims coordinate writers; they do not block readers. Most users should stay on
88
+ schema-backed reads/writes and `claim(...)`; do not teach manual protocol
89
+ bookkeeping in the happy path.
95
90
 
96
- All SDK errors extend `AbloError`. Important classes: `AbloBusyError`, `AbloStaleContextError`, `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`, `AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`, and `AbloServerError`.
91
+ All SDK errors extend `AbloError`. Important classes: `AbloClaimedError`, `AbloStaleContextError`, `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`, `AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`, and `AbloServerError`.
97
92
 
98
93
  ## Schema Scope
99
94
 
@@ -114,7 +109,7 @@ import { dataSource } from '@abloatai/ablo';
114
109
  ## Sandboxes
115
110
 
116
111
  Public `/sandbox` is a deterministic visual demo. It should teach shared state,
117
- intents, stale-write rejection, receipts, and deltas, but it does not use a real
112
+ claims, stale-write rejection, receipts, and deltas, but it does not use a real
118
113
  API key. It also exposes a Claude Code / Codex handoff prompt. Prefer that shape
119
114
  when an agent is asked to "make Ablo work" in an existing app.
120
115
 
@@ -124,16 +119,16 @@ sandbox like Stripe test mode: it has an isolated sync group prefix and mints
124
119
  Resetting a sandbox creates a clean future stream without touching live data.
125
120
  Use `sk_live_*` only for production.
126
121
 
127
- For coding agents, the sandbox success path is: pick one shared resource,
122
+ For coding agents, the sandbox success path is: pick one shared model,
128
123
  declare schema, create the Ablo client, replace one direct mutation with a typed
129
124
  `ablo.<model>.update(...)`, use selector `useAblo` for live reads, and add a
130
- two-writer stale/intent smoke test.
125
+ two-writer stale/claim smoke test.
131
126
 
132
127
  ## Public Surface
133
128
 
134
129
  Import from these public paths only:
135
130
 
136
- - `@abloatai/ablo` — `Ablo`, errors, typed model resources, intents, `dataSource`, and advanced protocol resources.
131
+ - `@abloatai/ablo` — `Ablo`, errors, typed model clients, claims, `dataSource`, and advanced protocol models.
137
132
  - `@abloatai/ablo/schema` — schema DSL.
138
133
  - `@abloatai/ablo/react` — React provider and hooks.
139
134
  - `@abloatai/ablo/testing` — test harnesses and mocks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "State control API for AI agents and collaborative apps.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,83 +0,0 @@
1
- import type { Schema, InferModel, InferCreate } from '../schema/schema.js';
2
- import type { ResolveSchema } from '../types/global.js';
3
- import type { SyncStoreContract } from './context.js';
4
- type GlobalMutateKey = ResolveSchema extends {
5
- models: infer M;
6
- } ? keyof M & string : string;
7
- type GlobalMutateActions<K extends string> = ResolveSchema extends Schema ? K extends keyof ResolveSchema['models'] & string ? MutateActions<ResolveSchema, K> : MutateActions<Schema, string> : MutateActions<Schema, string>;
8
- /**
9
- * Compatibility mutation hook. Returns CRUD methods for a single model type.
10
- *
11
- * Prefer `useAblo()` and call `ablo.<model>.create/update/delete` inside
12
- * callbacks for new integrations. This hook remains for older string-keyed code.
13
- *
14
- * @example
15
- * import { schema } from '@ablo/schema';
16
- * import { useMutate } from '@abloatai/ablo/react';
17
- *
18
- * const tasks = useMutate(schema, 'tasks');
19
- *
20
- * // Create — fields are type-checked against the schema's Zod shape
21
- * await tasks.create({ title: 'Fix bug', status: 'todo', projectId });
22
- *
23
- * // Update — id + partial changes, no need to hold a model instance
24
- * await tasks.update({ id: task.id, status: 'done', completedAt: new Date() });
25
- *
26
- * // Delete / archive / unarchive — by id
27
- * await tasks.delete(task.id);
28
- * await tasks.archive(task.id);
29
- *
30
- * Mirrors the Zero pattern: `zero.mutate.task.update({ id, status: 'done' })`.
31
- */
32
- /**
33
- * `create` / `update` / `delete` are overloaded: pass one row or an
34
- * array. Drizzle and Prisma use the same shape (`db.insert(table).values(rowOrRows)`).
35
- * Avoids the `*Many` suffix while keeping the semantics: every entry in
36
- * an array call lands in the same synchronous tick (Promise.all under
37
- * the hood), so the microtask coalescer in `TransactionQueue` collapses
38
- * N pushes into one wire commit with one `batchIndex` — structurally
39
- * identical to Zero's mutator-boundary commit.
40
- */
41
- type UpdatePatch<S extends Schema, K extends keyof S['models'] & string> = {
42
- id: string;
43
- } & Partial<InferModel<S, K>>;
44
- export interface MutateActions<S extends Schema, K extends keyof S['models'] & string> {
45
- /**
46
- * Create one entity, or an array of entities in a single tick. ID,
47
- * createdAt, updatedAt, organizationId default automatically per row.
48
- */
49
- create(data: InferCreate<S, K>): Promise<InferModel<S, K>>;
50
- create(data: InferCreate<S, K>[]): Promise<InferModel<S, K>[]>;
51
- /**
52
- * Update one row, or an array of rows in a single tick. Each patch is
53
- * `{ id, ...changes }` — missing ids throw. Schema-generated models
54
- * are MobX-observable, so direct assignment fires reactivity.
55
- */
56
- update(patch: UpdatePatch<S, K>): Promise<InferModel<S, K>>;
57
- update(patches: UpdatePatch<S, K>[]): Promise<InferModel<S, K>[]>;
58
- /**
59
- * Delete one row by id, or an array of ids in a single tick. Missing
60
- * ids are silently ignored.
61
- */
62
- delete(id: string): Promise<void>;
63
- delete(ids: string[]): Promise<void>;
64
- /** Soft-archive by ID. */
65
- archive: (id: string) => Promise<void>;
66
- /** Restore an archived entity by ID. */
67
- unarchive: (id: string) => Promise<void>;
68
- }
69
- /**
70
- * Pure factory — testable without React. The hook just wraps this in
71
- * useMemo with the React context.
72
- */
73
- export declare function createMutateActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract, organizationId: string): MutateActions<S, K>;
74
- /** @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`. */
75
- export declare function useMutate<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K): MutateActions<S, K>;
76
- /** Typed CRUD via the `AbloSync` global augmentation. The schema is
77
- * resolved from the `SyncProvider`'s context — consumer doesn't pass it
78
- * at the call site.
79
- *
80
- * @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`.
81
- */
82
- export declare function useMutate<K extends GlobalMutateKey>(modelKey: K): GlobalMutateActions<K>;
83
- export {};