@abloatai/ablo 0.10.1 → 0.11.1
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 +34 -0
- package/README.md +63 -23
- package/dist/BaseSyncedStore.d.ts +75 -0
- package/dist/BaseSyncedStore.js +193 -8
- package/dist/Database.d.ts +10 -2
- package/dist/Database.js +15 -1
- package/dist/SyncClient.d.ts +12 -1
- package/dist/SyncClient.js +110 -26
- package/dist/agent/Agent.d.ts +9 -9
- package/dist/agent/Agent.js +16 -16
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
- package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
- package/dist/ai-sdk/coordination-context.d.ts +9 -9
- package/dist/ai-sdk/coordination-context.js +8 -8
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.js +1 -1
- package/dist/ai-sdk/wrap.d.ts +4 -4
- package/dist/ai-sdk/wrap.js +4 -4
- package/dist/api/index.d.ts +2 -2
- package/dist/cli.cjs +369 -67
- package/dist/client/Ablo.d.ts +30 -63
- package/dist/client/Ablo.js +124 -103
- package/dist/client/ApiClient.d.ts +6 -5
- package/dist/client/ApiClient.js +86 -62
- package/dist/client/auth.d.ts +9 -4
- package/dist/client/auth.js +40 -5
- package/dist/client/createModelProxy.d.ts +41 -54
- package/dist/client/createModelProxy.js +123 -20
- package/dist/client/httpClient.d.ts +2 -0
- package/dist/client/httpClient.js +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/writeOptionsSchema.d.ts +4 -4
- package/dist/client/writeOptionsSchema.js +4 -4
- package/dist/coordination/schema.d.ts +249 -38
- package/dist/coordination/schema.js +172 -39
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +4 -4
- package/dist/errorCodes.d.ts +9 -9
- package/dist/errorCodes.js +16 -16
- package/dist/errors.d.ts +51 -2
- package/dist/errors.js +94 -5
- package/dist/interfaces/index.d.ts +8 -4
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/types.d.ts +13 -13
- package/dist/policy/types.js +8 -8
- package/dist/react/AbloProvider.d.ts +51 -4
- package/dist/react/AbloProvider.js +95 -11
- package/dist/react/context.d.ts +26 -9
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +4 -4
- package/dist/react/useAblo.js +5 -5
- package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
- package/dist/react/useClaim.js +42 -0
- package/dist/schema/index.js +1 -1
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/sugar.d.ts +3 -3
- package/dist/schema/sugar.js +3 -3
- package/dist/schema/sync-delta-wire.d.ts +8 -8
- package/dist/server/commit.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.d.ts +162 -0
- package/dist/sync/AreaOfInterestManager.js +233 -0
- package/dist/sync/BootstrapHelper.d.ts +9 -1
- package/dist/sync/BootstrapHelper.js +15 -5
- package/dist/sync/NetworkProbe.d.ts +1 -1
- package/dist/sync/NetworkProbe.js +1 -1
- package/dist/sync/SyncWebSocket.d.ts +59 -25
- package/dist/sync/SyncWebSocket.js +123 -26
- package/dist/sync/awaitClaimGrant.d.ts +40 -0
- package/dist/sync/awaitClaimGrant.js +86 -0
- package/dist/sync/createClaimStream.d.ts +34 -0
- package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
- package/dist/sync/createPresenceStream.js +3 -2
- package/dist/sync/participants.d.ts +10 -10
- package/dist/sync/participants.js +17 -10
- package/dist/sync/schemas.d.ts +8 -8
- package/dist/transactions/TransactionQueue.d.ts +23 -0
- package/dist/transactions/TransactionQueue.js +186 -12
- package/dist/types/global.d.ts +18 -13
- package/dist/types/global.js +11 -6
- package/dist/types/index.d.ts +9 -7
- package/dist/types/index.js +2 -2
- package/dist/types/streams.d.ts +114 -98
- package/dist/types/streams.js +1 -1
- package/dist/utils/asyncIterator.d.ts +1 -1
- package/dist/utils/asyncIterator.js +1 -1
- package/dist/wire/frames.d.ts +2 -2
- package/docs/api.md +3 -3
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +13 -3
- package/docs/data-sources.md +29 -9
- package/docs/migration.md +40 -0
- package/docs/quickstart.md +61 -33
- package/docs/react.md +46 -0
- package/llms-full.txt +25 -8
- package/llms.txt +11 -9
- package/package.json +3 -2
- package/dist/react/useIntent.js +0 -42
- package/dist/sync/awaitIntentGrant.d.ts +0 -40
- package/dist/sync/awaitIntentGrant.js +0 -62
- package/dist/sync/createIntentStream.d.ts +0 -34
package/dist/types/streams.d.ts
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ablo treats humans and agents as participants on live application
|
|
5
5
|
* entities. Participants announce what they are reading or editing,
|
|
6
|
-
* claim
|
|
6
|
+
* claim before writing, and capture context watermarks before
|
|
7
7
|
* long-running AI work. The customer keeps their own schema, agent
|
|
8
8
|
* stack, tools, prompts, and product policy; the sync engine provides
|
|
9
9
|
* the shared coordination substrate.
|
|
10
10
|
*/
|
|
11
11
|
import type { InferModel, Schema } from '../schema/schema.js';
|
|
12
|
-
import type { TargetRange, OnStaleMode,
|
|
13
|
-
export type { TargetRange, OnStaleMode,
|
|
12
|
+
import type { TargetRange, OnStaleMode, WireClaim, ClaimRejection, PresenceKind, ParticipantKind } from '../coordination/schema.js';
|
|
13
|
+
export type { TargetRange, OnStaleMode, WireClaim, ClaimRejection, PresenceKind, ParticipantKind };
|
|
14
14
|
/**
|
|
15
15
|
* Any JSON-serializable value. Used where the SDK accepts free-form
|
|
16
16
|
* metadata that will be persisted / transported as JSON — avoids
|
|
@@ -290,15 +290,15 @@ export interface Activity {
|
|
|
290
290
|
* writes this shape only.
|
|
291
291
|
*/
|
|
292
292
|
export interface Peer {
|
|
293
|
-
readonly participantKind:
|
|
293
|
+
readonly participantKind: ParticipantKind;
|
|
294
294
|
readonly participantId: string;
|
|
295
295
|
readonly label?: string;
|
|
296
296
|
readonly syncGroups: readonly string[];
|
|
297
297
|
readonly activity: Activity;
|
|
298
298
|
/** Server timestamp of the most recent frame from this participant. */
|
|
299
299
|
readonly lastActive: string;
|
|
300
|
-
/** Pending-mutation
|
|
301
|
-
readonly
|
|
300
|
+
/** Pending-mutation claims this participant has declared. */
|
|
301
|
+
readonly activeClaims?: ReadonlyArray<Claim>;
|
|
302
302
|
}
|
|
303
303
|
/** Outbound `presence_update` payload. */
|
|
304
304
|
export interface PresenceUpdatePayload {
|
|
@@ -307,24 +307,24 @@ export interface PresenceUpdatePayload {
|
|
|
307
307
|
readonly isAgent?: boolean;
|
|
308
308
|
}
|
|
309
309
|
/**
|
|
310
|
-
*
|
|
310
|
+
* Claim broadcasts — "I'm about to do X on Y." Broadcasts flow on
|
|
311
311
|
* the same WS as presence, so every participant sees them in real
|
|
312
|
-
* time. Cooperative mutex: the
|
|
312
|
+
* time. Cooperative mutex: the claim doesn't enforce exclusion; it
|
|
313
313
|
* announces. Other agents observe and yield. This is cheaper and
|
|
314
314
|
* more flexible than a central lock table and composes with presence.
|
|
315
315
|
*/
|
|
316
316
|
/**
|
|
317
|
-
* Options common to every verb-style
|
|
318
|
-
* (`
|
|
317
|
+
* Options common to every verb-style claim announcement
|
|
318
|
+
* (`claims.analyzing`, `.drafting`, etc.).
|
|
319
319
|
*
|
|
320
320
|
* The one required field is the *target* — everything else is a
|
|
321
|
-
* sensible default. Prefer the verb methods in `
|
|
321
|
+
* sensible default. Prefer the verb methods in `ClaimStream` below
|
|
322
322
|
* (`analyzing(entity, { ttl: '3m' })`) over the raw `announce(...)`
|
|
323
323
|
* escape hatch.
|
|
324
324
|
*/
|
|
325
|
-
export interface
|
|
325
|
+
export interface ClaimLeaseOptions {
|
|
326
326
|
/**
|
|
327
|
-
* How long before the server auto-expires this
|
|
327
|
+
* How long before the server auto-expires this claim if the
|
|
328
328
|
* participant doesn't finish the work. Accepts either a number (in
|
|
329
329
|
* seconds — back-compat with `ttlSeconds`) or a duration string:
|
|
330
330
|
* `'500ms'`, `'30s'`, `'3m'`, `'24h'`.
|
|
@@ -333,7 +333,7 @@ export interface IntentOptions {
|
|
|
333
333
|
}
|
|
334
334
|
/** Re-export of the duration helper shape. See `./duration.ts`. */
|
|
335
335
|
export type Duration = import('../utils/duration.js').Duration;
|
|
336
|
-
export interface ClaimOptions extends
|
|
336
|
+
export interface ClaimOptions extends ClaimLeaseOptions {
|
|
337
337
|
/**
|
|
338
338
|
* Free-form reason describing why you're claiming. Surfaces in conflict
|
|
339
339
|
* messages and the activity overlay. Defaults to `'editing'`. Common
|
|
@@ -349,21 +349,21 @@ export interface ClaimOptions extends IntentOptions {
|
|
|
349
349
|
readonly description?: string;
|
|
350
350
|
/**
|
|
351
351
|
* Join the server's fair FIFO queue on contention instead of being
|
|
352
|
-
* rejected. The grant arrives asynchronously (`
|
|
353
|
-
* target was free, `
|
|
352
|
+
* rejected. The grant arrives asynchronously (`claim_acquired` if the
|
|
353
|
+
* target was free, `claim_granted` once promoted to the head of the line).
|
|
354
354
|
* The low-level `claim` returns its handle immediately regardless; callers
|
|
355
355
|
* that need to *wait* for the grant use the awaiting wrappers
|
|
356
|
-
* (`ablo.<model>.claim`), which pair this flag with `
|
|
356
|
+
* (`ablo.<model>.claim`), which pair this flag with `awaitClaimGrant`.
|
|
357
357
|
*/
|
|
358
358
|
readonly queue?: boolean;
|
|
359
359
|
}
|
|
360
|
-
export interface
|
|
360
|
+
export interface ClaimStream {
|
|
361
361
|
/**
|
|
362
|
-
* Claim an exclusive
|
|
362
|
+
* Claim an exclusive claim on a target. Returns a handle — call
|
|
363
363
|
* `.revoke()` to cancel, let it expire via TTL, or use `await using`
|
|
364
364
|
* (TC39 explicit resource management) to auto-revoke on scope exit.
|
|
365
365
|
*
|
|
366
|
-
* Server rejects via `
|
|
366
|
+
* Server rejects via `claim_rejected` when another participant
|
|
367
367
|
* already holds a claim on the same target. Default `reason` is
|
|
368
368
|
* `'editing'`; pass `{reason: 'writing'}` (or any string) to override.
|
|
369
369
|
*
|
|
@@ -372,30 +372,30 @@ export interface IntentStream {
|
|
|
372
372
|
* scoped `claim(reason, opts)` overload were collapsed into this
|
|
373
373
|
* single primitive.
|
|
374
374
|
*/
|
|
375
|
-
claim(target: PresenceTarget, opts?: ClaimOptions):
|
|
375
|
+
claim(target: PresenceTarget, opts?: ClaimOptions): ClaimHandle;
|
|
376
376
|
/**
|
|
377
|
-
* Reactive view of every other participant's active
|
|
377
|
+
* Reactive view of every other participant's active claims.
|
|
378
378
|
* Reads return the current snapshot; pair with `subscribe(...)`
|
|
379
379
|
* below to get notified on change.
|
|
380
380
|
*/
|
|
381
|
-
readonly others: ReadonlyArray<
|
|
381
|
+
readonly others: ReadonlyArray<ActiveClaim>;
|
|
382
382
|
/**
|
|
383
383
|
* Reactive view of the wait queue on one target — the FIFO line of
|
|
384
|
-
* `status: 'queued'`
|
|
384
|
+
* `status: 'queued'` claims behind the current holder, each with its
|
|
385
385
|
* `action`, `heldBy`, and `position`. Synced from the server's per-entity
|
|
386
|
-
* `
|
|
386
|
+
* `claim_queue` frame; empty when no one's waiting. Pair with
|
|
387
387
|
* `subscribe(...)` for change notifications.
|
|
388
388
|
*/
|
|
389
|
-
queueFor(target: PresenceTarget): readonly
|
|
389
|
+
queueFor(target: PresenceTarget): readonly Claim[];
|
|
390
390
|
/**
|
|
391
391
|
* Re-rank the wait queue on a target — move the listed waiters to the front
|
|
392
392
|
* in the given order; unlisted waiters keep their relative FIFO order behind
|
|
393
|
-
* them. Pass the `
|
|
394
|
-
* (each `
|
|
395
|
-
* it (a participant lacking the `
|
|
393
|
+
* them. Pass the `Claim[]` from `queueFor(target)` in the order you want
|
|
394
|
+
* (each `Claim` carries its `heldBy` + `id`). Privileged: the server gates
|
|
395
|
+
* it (a participant lacking the `claim.reorder` capability is denied), so
|
|
396
396
|
* this is fire-and-forget — the new order arrives reactively via `queueFor`.
|
|
397
397
|
*/
|
|
398
|
-
reorder(target: PresenceTarget, order: readonly
|
|
398
|
+
reorder(target: PresenceTarget, order: readonly Claim[]): void;
|
|
399
399
|
/**
|
|
400
400
|
* Framework-agnostic reactivity. Same contract as
|
|
401
401
|
* `PresenceStream.subscribe` — register a listener fired on every
|
|
@@ -405,30 +405,30 @@ export interface IntentStream {
|
|
|
405
405
|
*/
|
|
406
406
|
onChange(listener: () => void): () => void;
|
|
407
407
|
/**
|
|
408
|
-
* Observe server-side
|
|
409
|
-
* rejects an `
|
|
408
|
+
* Observe server-side claim rejections. Fires when the server
|
|
409
|
+
* rejects an `claims.writing(...)` / `announce(...)` call because
|
|
410
410
|
* another participant already holds an open claim on the same
|
|
411
411
|
* target (cooperative mutex → enforced at the server boundary).
|
|
412
412
|
*
|
|
413
413
|
* Use this to surface conflicts to the user:
|
|
414
414
|
* ```ts
|
|
415
|
-
* participant.
|
|
415
|
+
* participant.claims.onRejected((r) => {
|
|
416
416
|
* toast.error(`${r.heldBy} is editing — try again in a moment`);
|
|
417
417
|
* });
|
|
418
418
|
* ```
|
|
419
419
|
*
|
|
420
420
|
* Returns an unsubscribe fn.
|
|
421
421
|
*/
|
|
422
|
-
onRejected(listener: (rejection:
|
|
422
|
+
onRejected(listener: (rejection: ClaimRejection) => void): () => void;
|
|
423
423
|
/**
|
|
424
|
-
* Observe LOSING an
|
|
425
|
-
* server refused). Fires on the server's `
|
|
424
|
+
* Observe LOSING an claim you held — distinct from `onRejected` (a claim the
|
|
425
|
+
* server refused). Fires on the server's `claim_lost` frame, carrying why:
|
|
426
426
|
* `'preempted'` (a privileged participant evicted you) or `'expired'` (your
|
|
427
427
|
* TTL lapsed). Lets a holder react — re-plan vs re-claim — instead of
|
|
428
428
|
* silently discovering the lease gone via presence.
|
|
429
429
|
*
|
|
430
430
|
* ```ts
|
|
431
|
-
* participant.
|
|
431
|
+
* participant.claims.onLost((lost) => {
|
|
432
432
|
* if (lost.reason === 'preempted') replanAgainst(lost.target);
|
|
433
433
|
* else reclaim(lost.target);
|
|
434
434
|
* });
|
|
@@ -436,55 +436,29 @@ export interface IntentStream {
|
|
|
436
436
|
*
|
|
437
437
|
* Returns an unsubscribe fn.
|
|
438
438
|
*/
|
|
439
|
-
onLost(listener: (lost:
|
|
439
|
+
onLost(listener: (lost: ClaimLost) => void): () => void;
|
|
440
440
|
/**
|
|
441
|
-
* Async-iterable view of everyone else's open
|
|
441
|
+
* Async-iterable view of everyone else's open claims. Each
|
|
442
442
|
* iteration yields the current snapshot on every mutation.
|
|
443
443
|
*
|
|
444
444
|
* ```ts
|
|
445
|
-
* for await (const
|
|
446
|
-
* if (
|
|
445
|
+
* for await (const openClaims of participant.claims) {
|
|
446
|
+
* if (openClaims.some((i) => i.target.id === clauseId)) wait();
|
|
447
447
|
* }
|
|
448
448
|
* ```
|
|
449
449
|
*/
|
|
450
|
-
[Symbol.asyncIterator](): AsyncIterableIterator<ReadonlyArray<
|
|
450
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<ReadonlyArray<ActiveClaim>>;
|
|
451
451
|
}
|
|
452
452
|
/**
|
|
453
|
-
*
|
|
454
|
-
* `IntentStream.onRejected`. Server rejects an incoming claim when
|
|
455
|
-
* another participant already holds an open intent on the same target.
|
|
456
|
-
*/
|
|
457
|
-
export interface IntentRejection {
|
|
458
|
-
/** The rejected claim's id (the one the caller just tried to mint). */
|
|
459
|
-
readonly intentId: string;
|
|
460
|
-
/** Why the server rejected it — currently always `'conflict'`. */
|
|
461
|
-
readonly reason: 'conflict';
|
|
462
|
-
/** The target that's already held. */
|
|
463
|
-
readonly target: {
|
|
464
|
-
readonly entityType: string;
|
|
465
|
-
readonly entityId: string;
|
|
466
|
-
readonly path?: string;
|
|
467
|
-
readonly range?: TargetRange;
|
|
468
|
-
readonly field?: string;
|
|
469
|
-
readonly meta?: Record<string, unknown>;
|
|
470
|
-
};
|
|
471
|
-
/** Participant id holding the existing claim. */
|
|
472
|
-
readonly heldBy: string;
|
|
473
|
-
/** The existing claim's id (for audit / retry correlation). */
|
|
474
|
-
readonly heldByIntentId: string;
|
|
475
|
-
/** When the existing claim expires (ms since epoch). */
|
|
476
|
-
readonly heldByExpiresAt: number;
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* You LOST an intent you were HOLDING — distinct from `IntentRejection` (a
|
|
453
|
+
* You LOST an claim you were HOLDING — distinct from `ClaimRejection` (a
|
|
480
454
|
* claim the server refused you). Delivered via `onLost`.
|
|
481
455
|
*/
|
|
482
|
-
export interface
|
|
456
|
+
export interface ClaimLost {
|
|
483
457
|
/** The held claim's id that you just lost. */
|
|
484
|
-
readonly
|
|
458
|
+
readonly claimId: string;
|
|
485
459
|
/**
|
|
486
460
|
* How you lost it. `'preempted'`: a privileged participant (one holding the
|
|
487
|
-
* `
|
|
461
|
+
* `claim.preempt` capability) evicted you and took the lease — its work now
|
|
488
462
|
* supersedes yours, so re-plan against the new holder rather than blindly
|
|
489
463
|
* re-claiming. `'expired'`: your TTL lapsed without finishing — re-claim if
|
|
490
464
|
* you still need it.
|
|
@@ -500,7 +474,7 @@ export interface IntentLost {
|
|
|
500
474
|
readonly meta?: Record<string, unknown>;
|
|
501
475
|
};
|
|
502
476
|
}
|
|
503
|
-
export interface
|
|
477
|
+
export interface ClaimDeclaration {
|
|
504
478
|
readonly target: EntityRef;
|
|
505
479
|
/** Human-readable reason — "rewriting title" / "restyling chart". */
|
|
506
480
|
readonly reason: string;
|
|
@@ -517,58 +491,100 @@ export interface IntentDeclaration {
|
|
|
517
491
|
*
|
|
518
492
|
* ```ts
|
|
519
493
|
* {
|
|
520
|
-
* await using work = participant.
|
|
521
|
-
* // ... do the work;
|
|
494
|
+
* await using work = participant.claims.analyzing(clause, { ttl: '3m' });
|
|
495
|
+
* // ... do the work; claim auto-revokes when the block exits
|
|
522
496
|
* }
|
|
523
497
|
* ```
|
|
524
498
|
*/
|
|
525
|
-
|
|
526
|
-
|
|
499
|
+
/**
|
|
500
|
+
* THE one claim handle. Returned by every claim door — the typed
|
|
501
|
+
* `ablo.<model>.claim({ id })` (rich: `data`/`readAt`/`target` populated) and
|
|
502
|
+
* the low-level `participant.claims.claim()` lease (minimal: `claimId` +
|
|
503
|
+
* `revoke`/`release`). Row-level fields are optional precisely because the
|
|
504
|
+
* low-level lease has no row snapshot; the model door fills them in.
|
|
505
|
+
*
|
|
506
|
+
* Implements `Symbol.asyncDispose` so callers can `await using claim = ...`
|
|
507
|
+
* and have it auto-release on scope exit.
|
|
508
|
+
*/
|
|
509
|
+
export interface ClaimHandle<T = Record<string, unknown>> extends AsyncDisposable {
|
|
510
|
+
readonly object: 'claim';
|
|
511
|
+
readonly claimId: string;
|
|
512
|
+
/**
|
|
513
|
+
* True when the grant came AFTER waiting in the server's FIFO line
|
|
514
|
+
* (`claim_granted`) — the authoritative "the row may have changed under us"
|
|
515
|
+
* signal. Absent for an immediate grant or a non-queued lease.
|
|
516
|
+
*/
|
|
517
|
+
readonly waited?: boolean;
|
|
518
|
+
/**
|
|
519
|
+
* Sync watermark of the held snapshot (`data` was read at this stamp). Writes
|
|
520
|
+
* carrying the handle use it as the `readAt` stale guard. Present for
|
|
521
|
+
* model-scoped claims; absent for low-level leases.
|
|
522
|
+
*/
|
|
523
|
+
readonly readAt?: number;
|
|
524
|
+
readonly target: {
|
|
525
|
+
readonly model: string;
|
|
526
|
+
readonly id: string;
|
|
527
|
+
readonly field?: string;
|
|
528
|
+
readonly path?: string;
|
|
529
|
+
readonly range?: TargetRange;
|
|
530
|
+
readonly meta?: Record<string, unknown>;
|
|
531
|
+
};
|
|
532
|
+
readonly action: string;
|
|
533
|
+
readonly description?: string;
|
|
534
|
+
/** Row snapshot — populated by `ablo.<model>.claim`; absent on low-level leases. */
|
|
535
|
+
readonly data?: T;
|
|
536
|
+
release(): Promise<void>;
|
|
527
537
|
revoke(): void;
|
|
528
538
|
}
|
|
529
|
-
export interface
|
|
539
|
+
export interface ActiveClaim extends ClaimDeclaration {
|
|
530
540
|
readonly id: string;
|
|
531
541
|
readonly heldBy: string;
|
|
532
542
|
/**
|
|
533
|
-
* Whether the holding participant is a
|
|
534
|
-
* First-class field so UIs can style "agent editing X"
|
|
535
|
-
* from "user editing X" without string-parsing `heldBy`.
|
|
543
|
+
* Whether the holding participant is a user (session), an agent, or a
|
|
544
|
+
* system actor. First-class field so UIs can style "agent editing X"
|
|
545
|
+
* differently from "user editing X" without string-parsing `heldBy`.
|
|
546
|
+
* Canonical `'user' | 'agent' | 'system'` — the presence/claim stream
|
|
547
|
+
* derives the value from the boolean `isAgent` wire flag (so it produces
|
|
548
|
+
* only `'user'`/`'agent'`), but the type stays the full union it shares
|
|
549
|
+
* with the HTTP claim surface and lease store.
|
|
536
550
|
*/
|
|
537
|
-
readonly participantKind:
|
|
551
|
+
readonly participantKind: ParticipantKind;
|
|
538
552
|
readonly description?: string;
|
|
539
|
-
|
|
540
|
-
readonly
|
|
553
|
+
/** Epoch-ms the claim was announced. */
|
|
554
|
+
readonly announcedAt: number;
|
|
555
|
+
/** Epoch-ms the server auto-expires it. */
|
|
556
|
+
readonly expiresAt: number;
|
|
541
557
|
}
|
|
542
558
|
/**
|
|
543
|
-
* Every lifecycle state of a coordination
|
|
559
|
+
* Every lifecycle state of a coordination claim, in one enum.
|
|
544
560
|
* `active` = the current holder (the lock). `queued` = waiting in the FIFO
|
|
545
561
|
* line behind the holder (carries `position`). The terminal states drop the
|
|
546
|
-
*
|
|
562
|
+
* claim from the synced set.
|
|
547
563
|
*/
|
|
548
|
-
export type
|
|
564
|
+
export type ClaimStatus = 'active' | 'queued' | 'committed' | 'expired' | 'canceled';
|
|
549
565
|
/** Options for waiting on a target to become free. */
|
|
550
|
-
export interface
|
|
566
|
+
export interface ClaimWaitOptions {
|
|
551
567
|
readonly timeout?: number;
|
|
552
568
|
readonly pollInterval?: number;
|
|
553
569
|
readonly signal?: AbortSignal;
|
|
554
570
|
}
|
|
555
571
|
/**
|
|
556
572
|
* The coordination state of one entity. Self-describing on the wire via
|
|
557
|
-
* `object: '
|
|
573
|
+
* `object: 'claim'`. Existence with `status: 'active'` *is* the lock;
|
|
558
574
|
* the fields *are* the awareness ("agent X is editing this until Y").
|
|
559
575
|
*
|
|
560
576
|
* Deliberately omits a Stripe-style `next_action`: a contender's only
|
|
561
577
|
* response is "wait until free, then re-read", and the runtime performs
|
|
562
578
|
* that uniformly — `claim` serializes behind the holder via the server
|
|
563
|
-
* FIFO queue (or low-level `
|
|
579
|
+
* FIFO queue (or low-level `claims.waitFor` to wait without claiming), and the
|
|
564
580
|
* stale-context guard forces the re-read. Encoding a constant instruction
|
|
565
581
|
* the engine always takes would be the kind of ceremony this object exists
|
|
566
582
|
* to remove.
|
|
567
583
|
*/
|
|
568
|
-
export interface
|
|
569
|
-
readonly object: '
|
|
584
|
+
export interface Claim {
|
|
585
|
+
readonly object: 'claim';
|
|
570
586
|
readonly id: string;
|
|
571
|
-
readonly status:
|
|
587
|
+
readonly status: ClaimStatus;
|
|
572
588
|
/** What is being coordinated. */
|
|
573
589
|
readonly target: EntityRef;
|
|
574
590
|
/** Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. */
|
|
@@ -577,14 +593,14 @@ export interface Intent {
|
|
|
577
593
|
readonly description?: string;
|
|
578
594
|
/** Participant holding it. */
|
|
579
595
|
readonly heldBy: string;
|
|
580
|
-
readonly participantKind:
|
|
596
|
+
readonly participantKind: ParticipantKind;
|
|
581
597
|
/**
|
|
582
|
-
*
|
|
598
|
+
* Epoch-ms the holder opened it. Optional until the lease wire carries
|
|
583
599
|
* it — derived shapes (e.g. mapped from a presence frame) may omit it.
|
|
584
600
|
*/
|
|
585
|
-
readonly createdAt?:
|
|
586
|
-
/**
|
|
587
|
-
readonly expiresAt:
|
|
601
|
+
readonly createdAt?: number;
|
|
602
|
+
/** Epoch-ms the server auto-expires it if the holder doesn't finish. */
|
|
603
|
+
readonly expiresAt: number;
|
|
588
604
|
/**
|
|
589
605
|
* 0-based place in the FIFO line — present only when `status: 'queued'`
|
|
590
606
|
* (`0` = next in line behind the holder). Absent for the active holder.
|
package/dist/types/streams.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ablo treats humans and agents as participants on live application
|
|
5
5
|
* entities. Participants announce what they are reading or editing,
|
|
6
|
-
* claim
|
|
6
|
+
* claim before writing, and capture context watermarks before
|
|
7
7
|
* long-running AI work. The customer keeps their own schema, agent
|
|
8
8
|
* stack, tools, prompts, and product policy; the sync engine provides
|
|
9
9
|
* the shared coordination substrate.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* The inputs are two functions:
|
|
6
6
|
*
|
|
7
7
|
* - `subscribe(onChange): unsubscribe` — the existing reactivity
|
|
8
|
-
* primitive on `PresenceStream` / `
|
|
8
|
+
* primitive on `PresenceStream` / `ClaimStream`. We register a
|
|
9
9
|
* listener that enqueues a value every time the source mutates;
|
|
10
10
|
* we tear it down in `return()`.
|
|
11
11
|
* - `getSnapshot()` — read the latest value to hand to the
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* The inputs are two functions:
|
|
6
6
|
*
|
|
7
7
|
* - `subscribe(onChange): unsubscribe` — the existing reactivity
|
|
8
|
-
* primitive on `PresenceStream` / `
|
|
8
|
+
* primitive on `PresenceStream` / `ClaimStream`. We register a
|
|
9
9
|
* listener that enqueues a value every time the source mutates;
|
|
10
10
|
* we tear it down in `return()`.
|
|
11
11
|
* - `getSnapshot()` — read the latest value to hand to the
|
package/dist/wire/frames.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface CommitOperation {
|
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Client → Server single named-mutation frame. The named-mutator write
|
|
55
|
-
* primitive (
|
|
55
|
+
* primitive (claim + args), as opposed to the raw-op {@link CommitMessage}
|
|
56
56
|
* batch. Server-side mutator dispatch resolves `mutatorName` against the
|
|
57
57
|
* host-provided registry.
|
|
58
58
|
*/
|
|
@@ -78,7 +78,7 @@ export interface CommitMessage {
|
|
|
78
78
|
/**
|
|
79
79
|
* Dormant agent-task lineage field. The SDK no longer populates it —
|
|
80
80
|
* turns/tasks were removed and write attribution now rides on the
|
|
81
|
-
* claim (`
|
|
81
|
+
* claim (`claim`) id plus the server-stamped actor/capability. Kept
|
|
82
82
|
* optional for wire-compat; when present the Hub still validates and
|
|
83
83
|
* threads it onto `caused_by_task_id`, but client writes leave it
|
|
84
84
|
* `null` (the audit pane treats null as "no prompt-side context").
|
package/docs/api.md
CHANGED
|
@@ -120,18 +120,18 @@ coordination surface is `claim.state({ id })` / `claim.queue({ id })` /
|
|
|
120
120
|
|
|
121
121
|
| Field | Type | Description |
|
|
122
122
|
|---|---|---|
|
|
123
|
-
| `object` | `'
|
|
123
|
+
| `object` | `'claim'` | String representing the object's type. |
|
|
124
124
|
| `id` | string | Unique identifier for the claim. |
|
|
125
125
|
| `status` | `'active' \| 'queued' \| 'committed' \| 'expired' \| 'canceled'` | The whole lifecycle, in one field. `active` is the holder; `queued` is a waiter in the FIFO line behind it. |
|
|
126
126
|
| `target` | `{ type, id, field? }` | What is being coordinated. |
|
|
127
127
|
| `action` | string | Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. |
|
|
128
128
|
| `heldBy` | string | Participant id holding the claim. |
|
|
129
|
-
| `participantKind` | `'
|
|
129
|
+
| `participantKind` | `'user' \| 'agent' \| 'system'` | Who's behind it — a human (`user`), an AI (`agent`), or automated infrastructure (`system`). |
|
|
130
130
|
| `expiresAt` | string | Ms-epoch at which the server auto-expires it if the holder doesn't finish. |
|
|
131
131
|
|
|
132
132
|
```json
|
|
133
133
|
{
|
|
134
|
-
"object": "
|
|
134
|
+
"object": "claim",
|
|
135
135
|
"id": "claim_3MtwBwLkdIwHu7ix",
|
|
136
136
|
"status": "active",
|
|
137
137
|
"target": { "type": "weatherReports", "id": "report_stockholm", "field": "status" },
|
package/docs/client-behavior.md
CHANGED
|
@@ -29,6 +29,7 @@ Common options:
|
|
|
29
29
|
|---|---|
|
|
30
30
|
| `schema` | Required for typed model clients. |
|
|
31
31
|
| `apiKey` | Bearer credential for trusted server runtimes. Defaults to `ABLO_API_KEY` when available. |
|
|
32
|
+
| `databaseUrl` | Optional, server-only. Registers your Postgres directly (the connection-string path). Pass it explicitly — it is **not** auto-read from the environment. Omit it for a signed Data Source endpoint or the hosted sandbox. The SDK throws if it sees this in a browser. |
|
|
32
33
|
| `baseURL` | Override the hosted sync endpoint for staging or private deployments. |
|
|
33
34
|
| `persistence` | `memory` by default. Use `indexeddb` for a durable browser cache that survives reloads. |
|
|
34
35
|
| `fetch` | Custom fetch implementation for tests or non-standard runtimes. |
|
|
@@ -36,9 +37,11 @@ Common options:
|
|
|
36
37
|
| `defaultQuery` | Extra query parameters attached to every HTTP request. |
|
|
37
38
|
| `dangerouslyAllowBrowser` | Required before sending an API key from browser code. Prefer a server route instead. |
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
`databaseUrl` is an optional, server-only constructor option. It is **not**
|
|
41
|
+
auto-read from the environment — pass it explicitly to register your Postgres
|
|
42
|
+
directly (the connection-string path). Omit it when you expose a signed
|
|
43
|
+
[Data Source](./data-sources.md) endpoint, or when trying Ablo against the hosted
|
|
44
|
+
sandbox.
|
|
42
45
|
|
|
43
46
|
## Model Methods
|
|
44
47
|
|
package/docs/coordination.md
CHANGED
|
@@ -29,7 +29,7 @@ make:
|
|
|
29
29
|
|
|
30
30
|
| layer | kind | what it does | enforces? |
|
|
31
31
|
|---|---|---|---|
|
|
32
|
-
| **Presence** (`claim.state`, observers) | observation | Broadcasts who is working where, live. Renders cursors / "agent X is editing." | **No.** Advisory only — it never blocks or rejects a write. |
|
|
32
|
+
| **Presence** (`claim.state`, observers) | observation | Broadcasts who is working where, live. Renders cursors / "agent X is editing." Reading or claiming a row auto-enrolls you in its sync group, so `claim.state({ id })` observes co-participants from any client (browser or Node agent) with no manual subscribe step. | **No.** Advisory only — it never blocks or rejects a write. |
|
|
33
33
|
| **Claim** (`claim`/`claim.queue`/`claim.release`) | pessimistic | Reserves a row for one participant. Foreign writers are rejected server-side; contenders join a fair FIFO queue. | **Yes**, between participants — mutual exclusion. |
|
|
34
34
|
| **Stale-context** (`readAt` + `onStale`) | optimistic (LWW) | On commit, rejects a write whose snapshot is older than the row's latest delta. Last-writer-wins detection. | **Yes**, against time — lost-update detection. |
|
|
35
35
|
|
|
@@ -70,7 +70,7 @@ a model row. It's what `claim.state()` returns and what observers render.
|
|
|
70
70
|
| `target` | `EntityRef` | What is being coordinated (`{ model, id, field? }`). |
|
|
71
71
|
| `action` | `string` | Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. |
|
|
72
72
|
| `heldBy` | `string` | Participant holding (or waiting on) it (e.g. `'agent:forecaster'`). |
|
|
73
|
-
| `participantKind` | `'
|
|
73
|
+
| `participantKind` | `'user' \| 'agent' \| 'system'` | Who's behind it — a human (`user`), an AI (`agent`), or automated infrastructure (`system`). |
|
|
74
74
|
| `position` | `number?` | 0-based place in the FIFO line — present only when `status: 'queued'` (`0` = next behind the holder). |
|
|
75
75
|
| `createdAt` | `string?` | Ms-epoch the holder opened it. Optional — derived shapes may omit it. |
|
|
76
76
|
| `expiresAt` | `string` | Ms-epoch the server reclaims it if the holder goes **silent**. Renewed automatically while the holder's connection stays alive — a crash-cleanup floor, not a duration you size. |
|
|
@@ -167,6 +167,16 @@ ablo.<model>.claim.state({ id })
|
|
|
167
167
|
Read who's currently working on a row, for observers and UI. Synchronous and
|
|
168
168
|
reactive (it reads the local coordination snapshot). Never blocks.
|
|
169
169
|
|
|
170
|
+
**You don't subscribe to anything first.** Reading or claiming a row
|
|
171
|
+
automatically enrolls you in that row's sync group: reading it (including
|
|
172
|
+
`retrieve`/`get`, or `claim.state` itself) gives you **read-interest**, and
|
|
173
|
+
`claim`-ing it gives you a **pinned write-intent**. So `claim.state({ id })`
|
|
174
|
+
observes co-participants on that row from **any** client — a browser, a Server
|
|
175
|
+
Action, or a Node agent — and a holder sees its own claim, with no manual
|
|
176
|
+
subscribe step. There is no `participants.join` to call: the typed
|
|
177
|
+
`ablo.<model>` surface (read / `claim` / `claim.state` / `claim.queue`) is the
|
|
178
|
+
whole coordination API.
|
|
179
|
+
|
|
170
180
|
**Parameters**
|
|
171
181
|
|
|
172
182
|
| name | type | required | description |
|
|
@@ -306,7 +316,7 @@ inspect the `code`.
|
|
|
306
316
|
| `AbloClaimedError` | `claim_conflict` | An `update`/`delete` targets a row another participant holds — the server's pre-commit check rejected it. | — |
|
|
307
317
|
| `AbloClaimedError` | `entity_claimed` | Same conflict, from the commit guard backstop. | — |
|
|
308
318
|
| `AbloStaleContextError` | — | A guarded `update` (under a claim, or any write carrying `readAt`) targets a row that received deltas since the snapshot — your reasoning is stale. | `readAt`, `conflicts[]` |
|
|
309
|
-
| `AbloValidationError` | `model_claim_not_configured` | `claim` called on a model without collaboration
|
|
319
|
+
| `AbloValidationError` | `model_claim_not_configured` | `claim` called on a model proxy built without the collaboration runtime — an internal/advanced construction path. The standard `Ablo({ schema, apiKey })` client enables claiming for **every** model; there is no per-model claim config to add. | — |
|
|
310
320
|
| `AbloValidationError` | `entity_not_found` | The row id doesn't exist locally or on load. | — |
|
|
311
321
|
|
|
312
322
|
`AbloStaleContextError.conflicts` lists the `(model, id, observedSyncId)` rows
|
package/docs/data-sources.md
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# Connect Your Database
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
**In production, your database is the system of record.** Every synced model is
|
|
4
|
+
backed by your own Postgres; Ablo is the transaction layer on top of it. There
|
|
5
|
+
are two ways to connect, and they are the same product with the same writes — the
|
|
6
|
+
only difference is where your database credential lives:
|
|
7
7
|
|
|
8
8
|
| | How Ablo reaches your Postgres | Use when |
|
|
9
9
|
|---|---|---|
|
|
10
|
-
| **Connection string** (
|
|
10
|
+
| **Connection string** (primary) | You pass `databaseUrl` to `Ablo(...)` explicitly (it is never auto-read from the environment); Ablo registers the connection and commits each write directly, behind row-level security. | You can hand over a scoped connection string. |
|
|
11
11
|
| **Signed endpoint** | Your app exposes one route built from an ORM adapter; Ablo sends signed commit requests and your app writes its own database. | Database credentials must never leave your infrastructure. |
|
|
12
12
|
|
|
13
|
+
> Just trying Ablo? You don't need a database at all to start: the hosted
|
|
14
|
+
> **sandbox** can host rows in Ablo's test plane — pass an `apiKey` only and omit
|
|
15
|
+
> `databaseUrl`, like Stripe test mode. Connect your Postgres (either shape
|
|
16
|
+
> below) when you're ready for it to be the system of record.
|
|
17
|
+
|
|
13
18
|
Either way, you define an Ablo schema with `defineSchema`, `model`, and Zod. The
|
|
14
19
|
Ablo schema describes **only your synced, collaborative models** — the rows Ablo
|
|
15
20
|
coordinates and fans out in realtime. It is *not* your whole-database schema and
|
|
@@ -36,7 +41,7 @@ import { schema } from './ablo/schema';
|
|
|
36
41
|
export const ablo = Ablo({
|
|
37
42
|
schema,
|
|
38
43
|
apiKey: process.env.ABLO_API_KEY,
|
|
39
|
-
databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here
|
|
44
|
+
databaseUrl: process.env.DATABASE_URL, // your Postgres, passed explicitly — rows live here
|
|
40
45
|
});
|
|
41
46
|
```
|
|
42
47
|
|
|
@@ -50,6 +55,20 @@ On first connect the SDK registers the connection — sent once over TLS, stored
|
|
|
50
55
|
sealed, never returned by any API. From then on Ablo commits every confirmed
|
|
51
56
|
write directly to your database and reads canonical rows from it.
|
|
52
57
|
|
|
58
|
+
### A localhost Postgres can't be the system of record
|
|
59
|
+
|
|
60
|
+
This is the connection-string fact people hit first. Ablo's **cloud** registers
|
|
61
|
+
your connection string and connects to your Postgres **over the network**. A
|
|
62
|
+
`localhost` / private-range database (`127.0.0.1`, `192.168.*`, Docker's
|
|
63
|
+
`db:5432`) is unreachable from Ablo's side, so such connection strings are
|
|
64
|
+
**rejected**. Two escape hatches for local development against your own DB:
|
|
65
|
+
|
|
66
|
+
- **Expose a signed Data Source endpoint.** Your app — which *can* reach your
|
|
67
|
+
local DB — proxies Ablo's commits to it. See [Signed Endpoint](#signed-endpoint)
|
|
68
|
+
below. This is the right answer for "my dev DB stays on my machine."
|
|
69
|
+
- **Use the hosted sandbox.** Skip the database entirely: pass an `apiKey` only,
|
|
70
|
+
omit `databaseUrl`, and let Ablo's test plane host the rows while you build.
|
|
71
|
+
|
|
53
72
|
Safety requirements, enforced server-side before the first write:
|
|
54
73
|
|
|
55
74
|
- **Non-superuser role.** The connection must not be a superuser or hold
|
|
@@ -58,11 +77,12 @@ Safety requirements, enforced server-side before the first write:
|
|
|
58
77
|
- **Row-level security on synced tables.** `npx ablo migrate` provisions your
|
|
59
78
|
synced-model tables with `FORCE ROW LEVEL SECURITY` already applied; tables
|
|
60
79
|
you create yourself must do the same.
|
|
61
|
-
- **
|
|
62
|
-
address ranges are rejected.
|
|
80
|
+
- **Network-reachable host.** As above, connection strings resolving to loopback
|
|
81
|
+
or private address ranges are rejected — Ablo connects from its cloud.
|
|
63
82
|
|
|
64
83
|
`databaseUrl` is server-only: the SDK throws if it sees one in a browser-like
|
|
65
|
-
environment, and `dangerouslyAllowBrowser` does not override that.
|
|
84
|
+
environment, and `dangerouslyAllowBrowser` does not override that. It is also
|
|
85
|
+
never auto-read from the environment — pass it explicitly to `Ablo(...)`.
|
|
66
86
|
|
|
67
87
|
## Signed Endpoint
|
|
68
88
|
|