@abloatai/ablo 0.9.1 → 0.9.2
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 +84 -0
- package/CHANGELOG.md +34 -0
- package/README.md +14 -6
- package/dist/BaseSyncedStore.js +6 -2
- package/dist/SyncClient.d.ts +12 -0
- package/dist/SyncClient.js +15 -0
- package/dist/agent/index.js +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/client/Ablo.d.ts +7 -49
- package/dist/client/Ablo.js +2 -104
- package/dist/client/ApiClient.d.ts +1 -113
- package/dist/client/ApiClient.js +0 -232
- package/dist/client/auth.js +32 -2
- package/dist/client/httpClient.d.ts +5 -6
- package/dist/client/httpClient.js +2 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/errorCodes.js +3 -3
- package/dist/index.js +1 -1
- package/dist/interfaces/index.d.ts +4 -4
- package/dist/mutators/UndoManager.d.ts +17 -0
- package/dist/mutators/UndoManager.js +53 -0
- package/dist/react/AbloProvider.d.ts +18 -8
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useUndoScope.js +7 -0
- package/dist/server/commit.d.ts +4 -5
- package/dist/types/streams.d.ts +2 -1
- package/dist/wire/frames.d.ts +6 -8
- package/docs/api.md +1 -1
- package/docs/cli.md +17 -4
- package/docs/data-sources.md +68 -83
- package/docs/examples/ai-sdk-tool.md +11 -5
- package/docs/examples/existing-python-backend.md +26 -4
- package/docs/examples/nextjs.md +3 -2
- package/docs/examples/scoped-agent.md +38 -11
- package/docs/identity.md +86 -59
- package/docs/integration-guide.md +85 -54
- package/docs/react.md +39 -28
- package/llms.txt +18 -11
- package/package.json +2 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Ablo lets AI agents and humans safely edit the same typed data without clobbering each other. When two of them touch the same row, a "claim" makes one wait for the other instead of overwriting it. This file shows a coding assistant the one safe pattern: read a row, claim it, then write.
|
|
4
|
+
|
|
5
|
+
Claims don't lock. If another writer holds the row, `claim` waits for them and re-reads the fresh row before handing it to you — so two writers serialize instead of clobbering.
|
|
6
|
+
|
|
7
|
+
## Start here — scaffold with `ablo init`
|
|
8
|
+
|
|
9
|
+
Don't hand-write the integration. Run the CLI; it generates the current-API schema, client, Data Source endpoint, and (for Next.js) the browser provider + session route:
|
|
10
|
+
|
|
11
|
+
- **Scaffold:** `npx ablo init --yes` — flag-driven, never prompts. Override defaults with `--framework <nextjs|vite|remix|vanilla>`, `--auth <apikey|…>`, `--storage <datasource|managed>`, `--no-agent`, `--no-pull`, `--no-install`, `--no-login`. (Plain `ablo init` needs a TTY and will **HANG** in an agent/CI run — always pass `--yes`.)
|
|
12
|
+
- **Auth:** set `ABLO_API_KEY` in the environment. Do **NOT** run `ablo login` — it opens a browser device flow and blocks an agent.
|
|
13
|
+
- **Provision your DB (Data Source mode):** `npx ablo migrate` creates the tables for your Ablo models plus the adapter's bookkeeping tables (`ablo_outbox`, `ablo_idempotency`). It does **not** touch your other tables — keep your own migrations (drizzle-kit / prisma migrate) for auth and anything not in the Ablo schema.
|
|
14
|
+
- **Adopt an existing DB schema:** `npx ablo pull prisma [path]` / `pull drizzle <module>` (lossless) or `pull` (live DB, lossy). Writes `ablo/schema.ts`.
|
|
15
|
+
- **Push your schema — REQUIRED before any write works.** The server keeps its OWN copy of the schema. After you create or edit `ablo/schema.ts`, run `npx ablo push` (one-shot) — or `npx ablo dev --no-watch`. **Skip this and every write to a new or changed model fails with `server_execute_unknown_model`.** (Plain `ablo dev` watches forever — never run it bare in an agent.)
|
|
16
|
+
- **Other long-running:** `npx ablo logs --no-follow` (default tails forever). `npx ablo mode test|live` ALWAYS pass the argument. `status`, `push`, `pull`, `check`, `generate` are one-shot — safe as-is.
|
|
17
|
+
|
|
18
|
+
The generated `ablo/data-source.ts` is the whole Data Source endpoint and needs no hand-editing: `dataSourceNext({ schema, apiKey, adapter: prismaDataSource(prisma, schema) })` (or `drizzleDataSource(db, schema)`). The adapter owns commit / idempotency / outbox.
|
|
19
|
+
|
|
20
|
+
## Rule
|
|
21
|
+
|
|
22
|
+
Edit the generated files; teach this API only:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The schema is the integration contract — it drives typed model clients, React selectors, server and agent writes, the Data Source shape, and schema push. Ablo owns only the models you declare; your auth and other non-synced tables stay in your own ORM schema, side by side in the same database. Don't create a parallel string-keyed write path for rows that belong to a schema model.
|
|
29
|
+
|
|
30
|
+
Every model verb takes ONE options object. The common loop:
|
|
31
|
+
|
|
32
|
+
1. **Read** the row — `await ablo.<model>.retrieve({ id })` (async; from the server) or `await ablo.<model>.list({ where })` for many. In React render, read synchronously with `useAblo((a) => a.<model>.get(id))`.
|
|
33
|
+
2. **See who's active** (optional) — `ablo.<model>.claim.state({ id })` (synchronous; never blocks).
|
|
34
|
+
3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id, action?, ttl? })`. If someone else holds it, this waits for them, then gives you the fresh row on `claim.data`. The claim auto-releases when it goes out of scope (`await using`).
|
|
35
|
+
4. **Write** — `await ablo.<model>.update({ id: claim.data.id, data })`. Because you hold the claim, the write is rejected if the row changed underneath you.
|
|
36
|
+
|
|
37
|
+
Keep coding assistants on this schema-backed path.
|
|
38
|
+
|
|
39
|
+
## Minimal example
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import Ablo from '@abloatai/ablo';
|
|
43
|
+
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
44
|
+
|
|
45
|
+
const schema = defineSchema({
|
|
46
|
+
weatherReports: model({
|
|
47
|
+
location: z.string(),
|
|
48
|
+
status: z.enum(['pending', 'ready']),
|
|
49
|
+
forecast: z.string().optional(),
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
54
|
+
|
|
55
|
+
const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
56
|
+
if (!report) throw new Error('Report not found');
|
|
57
|
+
|
|
58
|
+
// If someone else holds the row, claim waits for them and re-reads the fresh
|
|
59
|
+
// row before resolving. Auto-released at the end of this scope (`await using`).
|
|
60
|
+
await using claim = await ablo.weatherReports.claim({
|
|
61
|
+
id: 'report_stockholm',
|
|
62
|
+
action: 'forecasting',
|
|
63
|
+
ttl: '2m',
|
|
64
|
+
});
|
|
65
|
+
const claimed = claim.data;
|
|
66
|
+
|
|
67
|
+
// Because we hold the claim, update is rejected if the row changed underneath us.
|
|
68
|
+
await ablo.weatherReports.update({
|
|
69
|
+
id: claimed.id,
|
|
70
|
+
data: { status: 'ready', forecast: await getForecast(claimed.location) },
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Coordination surface
|
|
75
|
+
|
|
76
|
+
Claims live on a callable namespace beside `create` / `update` / `retrieve`. Every member takes an options object:
|
|
77
|
+
|
|
78
|
+
- `await using claim = await ablo.<model>.claim({ id })` — acquire the row (waits if held); read it via `claim.data`; auto-releases on scope exit (or call `claim.release()`).
|
|
79
|
+
- `ablo.<model>.claim.state({ id })` — who is currently working on the row (synchronous; never blocks).
|
|
80
|
+
- `ablo.<model>.claim.queue({ id })` — who is waiting behind the current holder.
|
|
81
|
+
- `ablo.<model>.claim.release({ id })` — release a claim early.
|
|
82
|
+
- `ablo.<model>.claim.reorder({ id, order })` — reorder the waiting queue.
|
|
83
|
+
|
|
84
|
+
Most users declare a schema and write through `ablo.<model>.update({ id, data })`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Developer-onboarding overhaul so an LLM or a person gets a working integration on the first try.
|
|
8
|
+
- **`ablo init` scaffolds a project that builds and is current-API.** The Next.js scaffold now ships `app/providers.tsx` + an `app/api/ablo-session` route, uses `useAblo` (the removed `withSync` is gone), object-param verbs, and never bundles your `sk_` key into the browser. The webhook receiver moved off the `[...all]` catch-all.
|
|
9
|
+
- **Agent docs are accurate and ship.** `AGENTS.md`, `llms.txt`, and `llms-full.txt` are on the 0.9.x API (object-param `create`/`update`/`delete`/`retrieve`, disposable `await using claim`, `AbloProvider client` prop), lead with `ablo init`, and `AGENTS.md` now ships in the package.
|
|
10
|
+
- **`ablo push` is self-documenting.** Writing to a model the server hasn't seen now fails with an error that tells you to run `ablo push` (the `server_execute_unknown_model` / `unknown_model` messages), instead of a cryptic "unknown model."
|
|
11
|
+
- **`intents` is deprecated in favor of `claim`** everywhere the docs and the MCP scaffold/prompts teach or generate coordination; the public `ablo.intents` accessor is marked `@internal`.
|
|
12
|
+
- Docs say Node 24+, and the `drizzle-orm` peer floor is `>=0.44`.
|
|
13
|
+
|
|
14
|
+
- a88747a: Remove the `turn` primitive and the agent-work `tasks` resource from the client surface — the SDK is now purely `ablo.<model>` + `claim`.
|
|
15
|
+
|
|
16
|
+
**Breaking**
|
|
17
|
+
- `engine.beginTurn()`, the `Turn` handle interface, and the `Ablo.Turn` type are removed. `AbloApi.beginTurn` and the HTTP client's `beginTurn` are gone too.
|
|
18
|
+
- `CommitCreateOptions.causedByTaskId` is removed. (Lineage is no longer stamped from the client.)
|
|
19
|
+
- The engine no longer exposes a `protocol` accessor or a public `tasks` work-unit resource. `ablo.tasks` is, and always was, the schema `tasks` model proxy.
|
|
20
|
+
- The **`agent().run()` helper and the low-level agent/task type family are removed**: `AbloApi.agent(id, options)` and `AbloApi.tasks` (the `TaskResource`), plus the exported types `Agent`, `AgentOptions`, `AgentRunOptions`, `AgentRunResult`/`Done`/`Failed`/`Cancelled`, `AgentRunStatus`, `AgentRunContext`, `AgentModelClient`, `AgentModelReadOptions`, `AgentModelMutationOptions`, `AgentIntentOptions`, `AgentIntentInput`, `Task`, `TaskResource`, `TaskCreateOptions`, `TaskCloseOptions`, `TaskCloseResult` (and the `Ablo.*` namespace aliases for all of them). The `Ablo.Auth.Agent` principal constructor and the schema-backed `tasks` model are unaffected.
|
|
21
|
+
|
|
22
|
+
**Why**
|
|
23
|
+
|
|
24
|
+
`turn`/`agent_tasks` was a second coordination-and-attribution mechanism living alongside `claim`. It is redundant on the client:
|
|
25
|
+
- `claim` already serializes writers **and** carries the causal link — its `intent` id rides on every guarded write.
|
|
26
|
+
- The server stamps `actor` / `onBehalfOf` / `capabilityId` onto each delta from the auth context.
|
|
27
|
+
- Per-run token/cost is recorded in Langfuse, not the `agent_tasks` table.
|
|
28
|
+
|
|
29
|
+
So the only thing the client lost is the audit pane's "show everything this exact prompt produced" filter, which keyed off `caused_by_task_id`; new writes leave that column null.
|
|
30
|
+
|
|
31
|
+
**Migration**
|
|
32
|
+
|
|
33
|
+
Agents stop opening/closing tasks — just issue `ablo.<model>` writes (schema-backed) or `ablo.commits.create(...)` (schema-less) under a `claim`. Replace `Ablo({ apiKey }).agent(id, opts).run(prompt, handler)` with: mint a scoped credential via `sessions.create({ agent })`, then `claim` the row and `update` / `commits.create`.
|
|
34
|
+
|
|
35
|
+
The **server** `agent_tasks` table, the `caused_by_task_id` delta column, the `/api/sync/commit` wire field, and the `agent_actions_log` compliance hash-chain remain in place but **dormant** (client writes leave the field null) — they are load-bearing for the tamper-evident audit chain and historical-row audit JOINs, so they are intentionally NOT dropped. The dead `/v1/tasks` + `/api/agent/turn` route handlers ARE removed (zero live callers).
|
|
36
|
+
|
|
3
37
|
## 0.9.1
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@abloatai/ablo)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
[](#)
|
|
6
|
-
[](#keys--runtime)
|
|
7
7
|
|
|
8
8
|
**Let people and AI agents work on the same data without overwriting each other.**
|
|
9
9
|
|
|
@@ -46,7 +46,7 @@ anywhere people and agents change shared state and everyone has to see it live.
|
|
|
46
46
|
npm install @abloatai/ablo
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
**Keys & runtime.** Ablo needs Node
|
|
49
|
+
**Keys & runtime.** Ablo needs Node 24+ and TypeScript 5+. Grab an `sk_test_*`
|
|
50
50
|
key for a sandbox
|
|
51
51
|
(`export ABLO_API_KEY=sk_test_...`); keep keys in trusted server runtimes only.
|
|
52
52
|
In the browser, `<AbloProvider>` authenticates with the signed-in user's
|
|
@@ -218,12 +218,16 @@ provider and read with hooks, from `@abloatai/ablo/react`. Wrap your tree once;
|
|
|
218
218
|
everything inside is live.
|
|
219
219
|
|
|
220
220
|
```tsx
|
|
221
|
+
import Ablo from '@abloatai/ablo';
|
|
221
222
|
import { AbloProvider, useAblo } from '@abloatai/ablo/react';
|
|
222
223
|
import { schema } from './ablo/schema';
|
|
223
224
|
|
|
225
|
+
// Build the client once — it authenticates via your session route, no key in the browser.
|
|
226
|
+
const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
|
|
227
|
+
|
|
224
228
|
function App() {
|
|
225
229
|
return (
|
|
226
|
-
<AbloProvider
|
|
230
|
+
<AbloProvider client={ablo}>
|
|
227
231
|
<Report id="report_stockholm" />
|
|
228
232
|
</AbloProvider>
|
|
229
233
|
);
|
|
@@ -250,8 +254,9 @@ method as the server example above.
|
|
|
250
254
|
`<AbloProvider>` owns the connection — no API key in the browser. That's the
|
|
251
255
|
whole loop: read with `useAblo(selector)`, write with `ablo.<model>`, and every
|
|
252
256
|
other client (human or agent) on that row sees it in real time. See
|
|
253
|
-
[React](./docs/react.md) for the
|
|
254
|
-
`
|
|
257
|
+
[React](./docs/react.md) for the `<AbloProvider>` prop surface (`client`,
|
|
258
|
+
`userId`, `fallback`, `onError`) — schema, scope, and team membership live on the
|
|
259
|
+
`Ablo({ … })` client you pass it — plus status hooks.
|
|
255
260
|
|
|
256
261
|
## Identity & Sync Groups
|
|
257
262
|
|
|
@@ -270,7 +275,10 @@ to sync-group strings.
|
|
|
270
275
|
`userId` / `teamIds` come from your auth, resolved server-side:
|
|
271
276
|
|
|
272
277
|
```tsx
|
|
273
|
-
|
|
278
|
+
// team membership is asserted server-side when the session route mints the token.
|
|
279
|
+
const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
|
|
280
|
+
|
|
281
|
+
<AbloProvider client={ablo} userId={user.id}>
|
|
274
282
|
<App />
|
|
275
283
|
</AbloProvider>
|
|
276
284
|
```
|
package/dist/BaseSyncedStore.js
CHANGED
|
@@ -421,8 +421,12 @@ export class BaseSyncedStore {
|
|
|
421
421
|
* emit here, so undo is naturally local-only (you can't undo a teammate).
|
|
422
422
|
*/
|
|
423
423
|
subscribeLocalMutations(handler) {
|
|
424
|
-
|
|
425
|
-
|
|
424
|
+
// Tap the TransactionQueue directly via `onLocalTransaction`. The previous
|
|
425
|
+
// `syncClient.subscribe('transaction:created', …)` route registered the
|
|
426
|
+
// handler on SyncClient's OWN emitter, which never fires that event (only
|
|
427
|
+
// the queue's emitter does) — so undo recorded nothing. See
|
|
428
|
+
// `SyncClient.onLocalTransaction` for the full rationale.
|
|
429
|
+
return this.syncClient.onLocalTransaction((tx) => {
|
|
426
430
|
if (!tx || !tx.type || !tx.modelName || !tx.modelId)
|
|
427
431
|
return;
|
|
428
432
|
handler({
|
package/dist/SyncClient.d.ts
CHANGED
|
@@ -383,6 +383,18 @@ export declare class SyncClient extends EventEmitter {
|
|
|
383
383
|
error: Error;
|
|
384
384
|
permanent?: boolean;
|
|
385
385
|
}) => void): () => void;
|
|
386
|
+
/**
|
|
387
|
+
* Subscribe to LOCAL transaction creation with the full {@link Transaction}
|
|
388
|
+
* payload (`type`, `modelName`, `modelId`, `data`, `previousData`). This is
|
|
389
|
+
* the feed `BaseSyncedStore.subscribeLocalMutations` taps for undo recording.
|
|
390
|
+
*
|
|
391
|
+
* MUST subscribe to the TransactionQueue's emitter directly — that is the
|
|
392
|
+
* ONLY emitter that fires `transaction:created`. SyncClient's own emitter
|
|
393
|
+
* (reached via `subscribe()`) never re-broadcasts it, so routing undo through
|
|
394
|
+
* `subscribe('transaction:created')` silently records nothing. Mirrors
|
|
395
|
+
* `onMutationFailure`, which taps the queue for the same reason.
|
|
396
|
+
*/
|
|
397
|
+
onLocalTransaction(listener: (tx: import('./transactions/TransactionQueue.js').Transaction) => void): () => void;
|
|
386
398
|
/**
|
|
387
399
|
* Wait for the latest in-flight transaction for (modelName, modelId)
|
|
388
400
|
* to be confirmed by the server, or reject if it's rolled back.
|
package/dist/SyncClient.js
CHANGED
|
@@ -1298,6 +1298,21 @@ export class SyncClient extends EventEmitter {
|
|
|
1298
1298
|
this.transactionQueue.on('transaction:failed', listener);
|
|
1299
1299
|
return () => this.transactionQueue.off('transaction:failed', listener);
|
|
1300
1300
|
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Subscribe to LOCAL transaction creation with the full {@link Transaction}
|
|
1303
|
+
* payload (`type`, `modelName`, `modelId`, `data`, `previousData`). This is
|
|
1304
|
+
* the feed `BaseSyncedStore.subscribeLocalMutations` taps for undo recording.
|
|
1305
|
+
*
|
|
1306
|
+
* MUST subscribe to the TransactionQueue's emitter directly — that is the
|
|
1307
|
+
* ONLY emitter that fires `transaction:created`. SyncClient's own emitter
|
|
1308
|
+
* (reached via `subscribe()`) never re-broadcasts it, so routing undo through
|
|
1309
|
+
* `subscribe('transaction:created')` silently records nothing. Mirrors
|
|
1310
|
+
* `onMutationFailure`, which taps the queue for the same reason.
|
|
1311
|
+
*/
|
|
1312
|
+
onLocalTransaction(listener) {
|
|
1313
|
+
this.transactionQueue.on('transaction:created', listener);
|
|
1314
|
+
return () => this.transactionQueue.off('transaction:created', listener);
|
|
1315
|
+
}
|
|
1301
1316
|
/**
|
|
1302
1317
|
* Wait for the latest in-flight transaction for (modelName, modelId)
|
|
1303
1318
|
* to be confirmed by the server, or reject if it's rolled back.
|
package/dist/agent/index.js
CHANGED
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
// const ctx: Agent.Context = { perception };
|
|
123
123
|
// const s: Agent.SessionOptions = { ... };
|
|
124
124
|
//
|
|
125
|
-
// Everything else (Activity, Claim,
|
|
125
|
+
// Everything else (Activity, Claim, Peer, ActiveIntent, ...)
|
|
126
126
|
// lives on the `Ablo.*` namespace via
|
|
127
127
|
// `import type { Ablo } from '@abloatai/ablo'`.
|
|
128
128
|
export { Agent } from './Agent.js';
|
package/dist/api/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Use this build for serverless functions, scripts, and backends that want
|
|
5
5
|
* model reads/writes and commits over HTTP without the realtime sync runtime.
|
|
6
6
|
*/
|
|
7
|
-
export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type
|
|
7
|
+
export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, } from '../client/ApiClient.js';
|
|
8
8
|
export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, IntentCreateOptions, IntentHandle, IntentWaitOptions, ClaimedOptions, IfClaimedPolicy, ModelClient, ModelClaim, ModelMutationOptions, ModelReadOptions, ModelRead, ModelTarget, } from '../client/Ablo.js';
|
|
9
9
|
import { createProtocolClient } from '../client/ApiClient.js';
|
|
10
10
|
export default createProtocolClient;
|
package/dist/client/Ablo.d.ts
CHANGED
|
@@ -28,22 +28,6 @@ import type { IntentStream, IntentWaitOptions, PresenceStream, Snapshot } from '
|
|
|
28
28
|
import type { ParticipantManager } from '../sync/participants.js';
|
|
29
29
|
import type { ActiveIntent, Duration, Intent, TargetRange } from '../types/streams.js';
|
|
30
30
|
import { type AbloApi, type AbloApiClientOptions, type AbloApiIntents } from './ApiClient.js';
|
|
31
|
-
/**
|
|
32
|
-
* Handle returned by `engine.beginTurn()`. While alive, every commit
|
|
33
|
-
* automatically carries this turn's id on the wire. Call `close(stats?)`
|
|
34
|
-
* when the turn finishes, or `dispose()` to abandon without recording
|
|
35
|
-
* usage. Idempotent.
|
|
36
|
-
*/
|
|
37
|
-
export interface Turn {
|
|
38
|
-
readonly turnId: string;
|
|
39
|
-
close(stats?: {
|
|
40
|
-
readonly costInputTokens?: number;
|
|
41
|
-
readonly costOutputTokens?: number;
|
|
42
|
-
readonly costComputeMs?: number;
|
|
43
|
-
}): Promise<void>;
|
|
44
|
-
dispose(): void;
|
|
45
|
-
[Symbol.asyncDispose](): Promise<void>;
|
|
46
|
-
}
|
|
47
31
|
/**
|
|
48
32
|
* Async function that resolves an apiKey at request time. Use for
|
|
49
33
|
* credential rotation — rotate from a vault, refresh from session
|
|
@@ -809,10 +793,13 @@ export type Ablo<S extends SchemaRecord> = {
|
|
|
809
793
|
*/
|
|
810
794
|
readonly presence: PresenceStream;
|
|
811
795
|
/**
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
796
|
+
* @internal — the public coordination API is `ablo.<model>.claim`. This
|
|
797
|
+
* accessor is the internal stream `claim` is built on; it is NOT part of the
|
|
798
|
+
* supported public surface and will be moved off the public type (it currently
|
|
799
|
+
* stays only because internal SDK modules are still typed against it).
|
|
800
|
+
*
|
|
801
|
+
* Cooperative-mutex layer over presence — announce "I'm about to do X on Y" so
|
|
802
|
+
* peers can yield before colliding. Same socket as entity sync.
|
|
816
803
|
*/
|
|
817
804
|
readonly intents: IntentResource;
|
|
818
805
|
/**
|
|
@@ -861,18 +848,6 @@ export type Ablo<S extends SchemaRecord> = {
|
|
|
861
848
|
snapshot<ModelName extends keyof S & string>(entities: {
|
|
862
849
|
readonly [M in ModelName]: string | readonly string[];
|
|
863
850
|
}): Snapshot<Schema<S>, ModelName>;
|
|
864
|
-
/**
|
|
865
|
-
* Open a turn — every commit issued while the returned handle is
|
|
866
|
-
* alive carries `caused_by_task_id` on the wire so the server
|
|
867
|
-
* stamps it onto each delta. Powers `agent_tasks` audit trails.
|
|
868
|
-
* Server: `POST /api/agent/turn` with the capability bearer.
|
|
869
|
-
*/
|
|
870
|
-
beginTurn(options: {
|
|
871
|
-
readonly prompt: string;
|
|
872
|
-
readonly parentTaskId?: string;
|
|
873
|
-
readonly surface?: string;
|
|
874
|
-
readonly metadata?: Record<string, unknown>;
|
|
875
|
-
}): Promise<Turn>;
|
|
876
851
|
/**
|
|
877
852
|
* The internal BaseSyncedStore. Implements SyncStoreContract — pass to
|
|
878
853
|
* SyncContext.Provider so the SDK's useModel/useModels/useMutations hooks
|
|
@@ -973,17 +948,6 @@ export declare namespace Ablo {
|
|
|
973
948
|
type Options<S extends SchemaRecord = SchemaRecord> = AbloOptions<S>;
|
|
974
949
|
type Api = AbloApi;
|
|
975
950
|
type ApiIntents = AbloApiIntents;
|
|
976
|
-
type Agent = import('./ApiClient.js').Agent;
|
|
977
|
-
type AgentOptions = import('./ApiClient.js').AgentOptions;
|
|
978
|
-
type AgentRunOptions = import('./ApiClient.js').AgentRunOptions;
|
|
979
|
-
type AgentRunStatus = import('./ApiClient.js').AgentRunStatus;
|
|
980
|
-
type AgentRunResult<T> = import('./ApiClient.js').AgentRunResult<T>;
|
|
981
|
-
type AgentRunContext = import('./ApiClient.js').AgentRunContext;
|
|
982
|
-
type AgentModelClient<T = Record<string, unknown>> = import('./ApiClient.js').AgentModelClient<T>;
|
|
983
|
-
type AgentModelReadOptions = import('./ApiClient.js').AgentModelReadOptions;
|
|
984
|
-
type AgentModelMutationOptions = import('./ApiClient.js').AgentModelMutationOptions;
|
|
985
|
-
type AgentIntentOptions = import('./ApiClient.js').AgentIntentOptions;
|
|
986
|
-
type AgentIntentInput = import('./ApiClient.js').AgentIntentInput;
|
|
987
951
|
type Capability = import('./ApiClient.js').Capability;
|
|
988
952
|
type CapabilityCreateOptions = import('./ApiClient.js').CapabilityCreateOptions;
|
|
989
953
|
type CapabilityRecord = import('./ApiClient.js').CapabilityRecord;
|
|
@@ -991,11 +955,6 @@ export declare namespace Ablo {
|
|
|
991
955
|
type CapabilityRevocation = import('./ApiClient.js').CapabilityRevocation;
|
|
992
956
|
type CapabilityRotateOptions = import('./ApiClient.js').CapabilityRotateOptions;
|
|
993
957
|
type RotatedCapability = import('./ApiClient.js').RotatedCapability;
|
|
994
|
-
type Task = import('./ApiClient.js').Task;
|
|
995
|
-
type TaskCreateOptions = import('./ApiClient.js').TaskCreateOptions;
|
|
996
|
-
type TaskCloseOptions = import('./ApiClient.js').TaskCloseOptions;
|
|
997
|
-
type TaskCloseResult = import('./ApiClient.js').TaskCloseResult;
|
|
998
|
-
type TaskResource = import('./ApiClient.js').TaskResource;
|
|
999
958
|
type IfClaimedPolicy = import('./Ablo.js').IfClaimedPolicy;
|
|
1000
959
|
type ClaimedOptions = import('./Ablo.js').ClaimedOptions;
|
|
1001
960
|
type EntityRef = _Streams.EntityRef;
|
|
@@ -1011,7 +970,6 @@ export declare namespace Ablo {
|
|
|
1011
970
|
type IntentRejection = _Streams.IntentRejection;
|
|
1012
971
|
type IntentLost = _Streams.IntentLost;
|
|
1013
972
|
type Snapshot<TSchema extends _SchemaTypes.Schema = _SchemaTypes.Schema, K extends keyof TSchema['models'] = keyof TSchema['models']> = _Streams.Snapshot<TSchema, K>;
|
|
1014
|
-
type Turn = import('./Ablo.js').Turn;
|
|
1015
973
|
namespace Auth {
|
|
1016
974
|
type Principal = _Streams.Principal;
|
|
1017
975
|
type Session = _Streams.SessionRef;
|
package/dist/client/Ablo.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* await sync.reports.delete({ id: reportId });
|
|
20
20
|
*/
|
|
21
21
|
import { z } from 'zod';
|
|
22
|
-
import { AbloClaimedError, AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError,
|
|
22
|
+
import { AbloClaimedError, AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, toAbloError } from '../errors.js';
|
|
23
23
|
import { LoadStrategy, PropertyType } from '../types/index.js';
|
|
24
24
|
import { initSyncEngine } from '../context.js';
|
|
25
25
|
import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
|
|
@@ -812,13 +812,6 @@ export function Ablo(options) {
|
|
|
812
812
|
// becomes null, so the first Ablo's commits start throwing
|
|
813
813
|
// `ws_not_ready` forever (terminal AgentJob writes hang on retry).
|
|
814
814
|
syncClient.getTransactionQueue().setMutationExecutor(executor);
|
|
815
|
-
// Active turn id, set by `beginTurn(...)`, cleared on close. While
|
|
816
|
-
// set, every batch commit attaches `causedByTaskId` so server
|
|
817
|
-
// delta rows get stamped with it. Single-turn-at-a-time per Ablo
|
|
818
|
-
// — opening a second turn overwrites the active id without closing
|
|
819
|
-
// the prior. Callers who need parallel turns construct multiple
|
|
820
|
-
// Ablo instances, matching the SyncAgent semantics.
|
|
821
|
-
let activeTurnId = null;
|
|
822
815
|
// Presence + intent streams — built eagerly so `engine.presence`
|
|
823
816
|
// and `engine.intents` return the same reference for the engine's
|
|
824
817
|
// lifetime. The transport doesn't exist yet (BaseSyncedStore.initialize
|
|
@@ -1363,9 +1356,7 @@ export function Ablo(options) {
|
|
|
1363
1356
|
// SyncClient we already hold from createInternalComponents —
|
|
1364
1357
|
// no need to leak an accessor through BaseSyncedStore.
|
|
1365
1358
|
const queue = syncClient.getTransactionQueue();
|
|
1366
|
-
queue.enqueueCommit(clientTxId, operations
|
|
1367
|
-
causedByTaskId: activeTurnId,
|
|
1368
|
-
});
|
|
1359
|
+
queue.enqueueCommit(clientTxId, operations);
|
|
1369
1360
|
if (wait === 'queued') {
|
|
1370
1361
|
return { id: clientTxId, status: 'queued' };
|
|
1371
1362
|
}
|
|
@@ -1638,99 +1629,6 @@ export function Ablo(options) {
|
|
|
1638
1629
|
entities,
|
|
1639
1630
|
});
|
|
1640
1631
|
},
|
|
1641
|
-
// ── Turn handles ────────────────────────────────────────────────
|
|
1642
|
-
//
|
|
1643
|
-
// Open a turn — every commit issued while the returned handle is
|
|
1644
|
-
// alive carries `caused_by_task_id` on the wire so the server
|
|
1645
|
-
// stamps it onto each delta. The product surface this powers:
|
|
1646
|
-
// `agent_tasks` audit trails ("which AI prompt produced this
|
|
1647
|
-
// mutation"), parent/child turn chains, cost accounting per turn.
|
|
1648
|
-
//
|
|
1649
|
-
// POST /api/agent/turn (capability bearer) → returns turnId.
|
|
1650
|
-
// POST /api/agent/turn/:id/close (capability bearer) → records
|
|
1651
|
-
// final cost stats. Idempotent.
|
|
1652
|
-
async beginTurn(beginOptions) {
|
|
1653
|
-
const baseUrl = url.replace(/\/+$/, '');
|
|
1654
|
-
const turnUrl = `${baseUrl.replace(/^ws/, 'http')}/api/agent/turn`;
|
|
1655
|
-
const headers = authCredentials.withAuthHeaders({ 'Content-Type': 'application/json' });
|
|
1656
|
-
const res = await fetch(turnUrl, {
|
|
1657
|
-
method: 'POST',
|
|
1658
|
-
headers,
|
|
1659
|
-
body: JSON.stringify({
|
|
1660
|
-
prompt: beginOptions.prompt,
|
|
1661
|
-
parentTaskId: beginOptions.parentTaskId,
|
|
1662
|
-
surface: beginOptions.surface,
|
|
1663
|
-
metadata: beginOptions.metadata,
|
|
1664
|
-
}),
|
|
1665
|
-
});
|
|
1666
|
-
if (!res.ok) {
|
|
1667
|
-
const text = await res.text().catch(() => '');
|
|
1668
|
-
let parsed = text;
|
|
1669
|
-
if (text) {
|
|
1670
|
-
try {
|
|
1671
|
-
parsed = JSON.parse(text);
|
|
1672
|
-
}
|
|
1673
|
-
catch {
|
|
1674
|
-
/* keep raw text */
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
// Preserve the server's structured envelope (code/message/doc_url) when
|
|
1678
|
-
// present; fall back to turn_open_failed for a bare/non-Ablo body.
|
|
1679
|
-
throw hasWireCode(parsed)
|
|
1680
|
-
? translateHttpError(res.status, parsed, res.headers.get('x-request-id') ?? undefined)
|
|
1681
|
-
: new AbloError(`beginTurn failed: ${res.status} ${text}`, {
|
|
1682
|
-
code: 'turn_open_failed',
|
|
1683
|
-
httpStatus: res.status,
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
const json = (await res.json());
|
|
1687
|
-
const turnId = json.turnId;
|
|
1688
|
-
activeTurnId = turnId;
|
|
1689
|
-
let closed = false;
|
|
1690
|
-
const close = async (stats) => {
|
|
1691
|
-
if (closed)
|
|
1692
|
-
return;
|
|
1693
|
-
closed = true;
|
|
1694
|
-
if (activeTurnId === turnId)
|
|
1695
|
-
activeTurnId = null;
|
|
1696
|
-
const closeUrl = `${turnUrl}/${encodeURIComponent(turnId)}/close`;
|
|
1697
|
-
const closeRes = await fetch(closeUrl, {
|
|
1698
|
-
method: 'POST',
|
|
1699
|
-
headers,
|
|
1700
|
-
body: JSON.stringify({
|
|
1701
|
-
costInputTokens: stats?.costInputTokens ?? 0,
|
|
1702
|
-
costOutputTokens: stats?.costOutputTokens ?? 0,
|
|
1703
|
-
costComputeMs: stats?.costComputeMs ?? 0,
|
|
1704
|
-
}),
|
|
1705
|
-
});
|
|
1706
|
-
if (!closeRes.ok) {
|
|
1707
|
-
const text = await closeRes.text().catch(() => '');
|
|
1708
|
-
let parsed = text;
|
|
1709
|
-
if (text) {
|
|
1710
|
-
try {
|
|
1711
|
-
parsed = JSON.parse(text);
|
|
1712
|
-
}
|
|
1713
|
-
catch {
|
|
1714
|
-
/* keep raw text */
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
throw hasWireCode(parsed)
|
|
1718
|
-
? translateHttpError(closeRes.status, parsed, closeRes.headers.get('x-request-id') ?? undefined)
|
|
1719
|
-
: new AbloError(`closeTurn failed: ${closeRes.status} ${text}`, {
|
|
1720
|
-
code: 'turn_close_failed',
|
|
1721
|
-
httpStatus: closeRes.status,
|
|
1722
|
-
});
|
|
1723
|
-
}
|
|
1724
|
-
};
|
|
1725
|
-
const dispose = () => {
|
|
1726
|
-
if (closed)
|
|
1727
|
-
return;
|
|
1728
|
-
closed = true;
|
|
1729
|
-
if (activeTurnId === turnId)
|
|
1730
|
-
activeTurnId = null;
|
|
1731
|
-
};
|
|
1732
|
-
return { turnId, close, dispose, [Symbol.asyncDispose]: () => close() };
|
|
1733
|
-
},
|
|
1734
1632
|
};
|
|
1735
1633
|
return engine;
|
|
1736
1634
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
|
|
6
6
|
* nouns directly to HTTP routes on sync-server.
|
|
7
7
|
*/
|
|
8
|
-
import type { AbloOptions,
|
|
8
|
+
import type { AbloOptions, CommitResource, IntentCreateOptions, IntentHandle, IntentWaitOptions, ModelClient, ModelClaim, ModelTarget } from './Ablo.js';
|
|
9
9
|
import type { Duration } from '../utils/duration.js';
|
|
10
10
|
export type AbloApiClientOptions = Omit<AbloOptions, 'schema'> & {
|
|
11
11
|
readonly schema?: null | undefined;
|
|
@@ -115,127 +115,15 @@ export interface CapabilityResource {
|
|
|
115
115
|
*/
|
|
116
116
|
mint(options: CapabilityCreateOptions): Promise<Capability>;
|
|
117
117
|
}
|
|
118
|
-
export interface TaskCreateOptions {
|
|
119
|
-
readonly prompt: string;
|
|
120
|
-
readonly parentTaskId?: string;
|
|
121
|
-
readonly surface?: string;
|
|
122
|
-
readonly metadata?: Record<string, unknown>;
|
|
123
|
-
}
|
|
124
|
-
export interface TaskCloseOptions {
|
|
125
|
-
readonly costInputTokens?: number;
|
|
126
|
-
readonly costOutputTokens?: number;
|
|
127
|
-
readonly costComputeMs?: number;
|
|
128
|
-
}
|
|
129
|
-
export interface Task {
|
|
130
|
-
readonly id: string;
|
|
131
|
-
readonly turnId: string;
|
|
132
|
-
readonly promptHash?: string;
|
|
133
|
-
readonly openedAt?: string;
|
|
134
|
-
close(stats?: TaskCloseOptions): Promise<TaskCloseResult>;
|
|
135
|
-
}
|
|
136
|
-
export interface TaskCloseResult {
|
|
137
|
-
readonly id: string;
|
|
138
|
-
readonly turnId: string;
|
|
139
|
-
readonly closed: boolean;
|
|
140
|
-
readonly alreadyClosed?: boolean;
|
|
141
|
-
readonly endedAt?: string;
|
|
142
|
-
}
|
|
143
|
-
export interface TaskResource {
|
|
144
|
-
create(options: TaskCreateOptions): Promise<Task>;
|
|
145
|
-
close(id: string, stats?: TaskCloseOptions): Promise<TaskCloseResult>;
|
|
146
|
-
/**
|
|
147
|
-
* Alias for `create`. Kept for the agent-run vocabulary; `create` is
|
|
148
|
-
* the canonical SDK verb.
|
|
149
|
-
*/
|
|
150
|
-
open(options: TaskCreateOptions): Promise<Task>;
|
|
151
|
-
}
|
|
152
|
-
export interface AgentOptions {
|
|
153
|
-
readonly can: readonly string[];
|
|
154
|
-
readonly syncGroups?: readonly string[];
|
|
155
|
-
readonly label?: string;
|
|
156
|
-
readonly userMeta?: Record<string, unknown>;
|
|
157
|
-
/**
|
|
158
|
-
* Internal lease for the run capability. Most callers should omit it.
|
|
159
|
-
* The SDK revokes the capability when `run` finishes; the lease exists
|
|
160
|
-
* to clean up crashed or abandoned runs.
|
|
161
|
-
*/
|
|
162
|
-
readonly lease?: Duration;
|
|
163
|
-
readonly leaseSeconds?: number;
|
|
164
|
-
}
|
|
165
|
-
export interface AgentRunOptions extends TaskCreateOptions {
|
|
166
|
-
readonly signal?: AbortSignal;
|
|
167
|
-
readonly costInputTokens?: number;
|
|
168
|
-
readonly costOutputTokens?: number;
|
|
169
|
-
readonly costComputeMs?: number;
|
|
170
|
-
}
|
|
171
|
-
export type AgentRunStatus = 'done' | 'failed' | 'cancelled';
|
|
172
|
-
export interface AgentRunDone<T> {
|
|
173
|
-
readonly status: 'done';
|
|
174
|
-
readonly task: Task;
|
|
175
|
-
readonly value: T;
|
|
176
|
-
}
|
|
177
|
-
export interface AgentRunFailed {
|
|
178
|
-
readonly status: 'failed';
|
|
179
|
-
readonly task?: Task;
|
|
180
|
-
readonly error: unknown;
|
|
181
|
-
}
|
|
182
|
-
export interface AgentRunCancelled {
|
|
183
|
-
readonly status: 'cancelled';
|
|
184
|
-
readonly task?: Task;
|
|
185
|
-
readonly error?: unknown;
|
|
186
|
-
}
|
|
187
|
-
export type AgentRunResult<T> = AgentRunDone<T> | AgentRunFailed | AgentRunCancelled;
|
|
188
|
-
export interface AgentIntentOptions {
|
|
189
|
-
readonly action: string;
|
|
190
|
-
readonly field?: string;
|
|
191
|
-
readonly ttl?: Duration;
|
|
192
|
-
readonly target?: Partial<ModelTarget>;
|
|
193
|
-
}
|
|
194
|
-
export type AgentIntentInput = string | AgentIntentOptions;
|
|
195
|
-
export interface AgentModelReadOptions extends ModelReadOptions {
|
|
196
|
-
}
|
|
197
|
-
export interface AgentModelMutationOptions extends Omit<ModelMutationOptions, 'intent'> {
|
|
198
|
-
readonly intent?: AgentIntentInput | {
|
|
199
|
-
readonly id: string;
|
|
200
|
-
} | null;
|
|
201
|
-
}
|
|
202
|
-
export interface AgentModelClient<T = Record<string, unknown>> {
|
|
203
|
-
retrieve(params: AgentModelReadOptions & {
|
|
204
|
-
readonly id: string;
|
|
205
|
-
}): Promise<ModelRead<T>>;
|
|
206
|
-
create(params: AgentModelMutationOptions & {
|
|
207
|
-
readonly data: Record<string, unknown>;
|
|
208
|
-
readonly id?: string | null;
|
|
209
|
-
}): Promise<CommitReceipt>;
|
|
210
|
-
update(params: AgentModelMutationOptions & {
|
|
211
|
-
readonly id: string;
|
|
212
|
-
readonly data: Record<string, unknown>;
|
|
213
|
-
}): Promise<CommitReceipt>;
|
|
214
|
-
delete(params: AgentModelMutationOptions & {
|
|
215
|
-
readonly id: string;
|
|
216
|
-
}): Promise<CommitReceipt>;
|
|
217
|
-
}
|
|
218
|
-
export interface AgentRunContext {
|
|
219
|
-
readonly task: Task;
|
|
220
|
-
readonly ablo: AbloApi;
|
|
221
|
-
model<T = Record<string, unknown>>(name: string): AgentModelClient<T>;
|
|
222
|
-
}
|
|
223
|
-
export interface Agent {
|
|
224
|
-
readonly id: string;
|
|
225
|
-
run<T>(options: AgentRunOptions, handler: (context: AgentRunContext) => Promise<T> | T): Promise<AgentRunResult<T>>;
|
|
226
|
-
}
|
|
227
118
|
export interface AbloApi {
|
|
228
119
|
ready(): Promise<void>;
|
|
229
120
|
waitForFlush(): Promise<void>;
|
|
230
121
|
dispose(): Promise<void>;
|
|
231
122
|
purge(): Promise<void>;
|
|
232
123
|
readonly capabilities: CapabilityResource;
|
|
233
|
-
readonly tasks: TaskResource;
|
|
234
124
|
readonly intents: AbloApiIntents;
|
|
235
125
|
readonly commits: CommitResource;
|
|
236
|
-
agent(id: string, options: AgentOptions): Agent;
|
|
237
126
|
model<T = Record<string, unknown>>(name: string): ModelClient<T>;
|
|
238
|
-
beginTurn(options: TaskCreateOptions): Promise<Turn>;
|
|
239
127
|
/**
|
|
240
128
|
* Resolve the active bearer credential this client authenticates with — the
|
|
241
129
|
* same token its own requests carry in `Authorization`. Returns `null` when
|