@abloatai/ablo 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +2 -1
- 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 +254 -48
- package/dist/client/Ablo.d.ts +30 -63
- package/dist/client/Ablo.js +108 -102
- package/dist/client/ApiClient.d.ts +6 -5
- package/dist/client/ApiClient.js +83 -62
- package/dist/client/createModelProxy.d.ts +16 -54
- package/dist/client/createModelProxy.js +44 -16
- 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 +15 -15
- 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/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 +12 -0
- package/dist/transactions/TransactionQueue.js +126 -8
- package/dist/types/global.d.ts +10 -10
- package/dist/types/global.js +3 -3
- 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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The Collaboration Layer For AI Agents",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -221,6 +221,7 @@
|
|
|
221
221
|
"ts-morph": "^26.0.0",
|
|
222
222
|
"tsup": "^8.0.0",
|
|
223
223
|
"typescript": "^5.8.3",
|
|
224
|
-
"publint": "^0.3.21"
|
|
224
|
+
"publint": "^0.3.21",
|
|
225
|
+
"yjs": "^13.6.29"
|
|
225
226
|
}
|
|
226
227
|
}
|
package/dist/react/useIntent.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useCallback } from 'react';
|
|
3
|
-
import { useSyncContext } from './context.js';
|
|
4
|
-
import { AbloValidationError } from '../errors.js';
|
|
5
|
-
/**
|
|
6
|
-
* Named-intent invoker, typed via `ResolveIntents[IntentName]`.
|
|
7
|
-
*
|
|
8
|
-
* The consumer declares their intent vocabulary in the global:
|
|
9
|
-
*
|
|
10
|
-
* ```ts
|
|
11
|
-
* declare module '@abloatai/ablo' {
|
|
12
|
-
* interface Register {
|
|
13
|
-
* Intents: {
|
|
14
|
-
* editLayer: { slideId: string; layerId: string };
|
|
15
|
-
* generateWithAI: { entityId: string; tool: string };
|
|
16
|
-
* };
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* Then `useIntent('editLayer')` returns a function whose sole argument
|
|
22
|
-
* is the `editLayer` claim shape — no runtime checks, purely compile-
|
|
23
|
-
* time narrowing.
|
|
24
|
-
*
|
|
25
|
-
* The SDK doesn't own what happens next: the `beginIntent` function on
|
|
26
|
-
* the React context (supplied via `SyncProvider`) is where the intent
|
|
27
|
-
* claim turns into a network effect. A Node-backed consumer wires it
|
|
28
|
-
* through `SyncAgent.beginIntent`; a browser-backed consumer may
|
|
29
|
-
* broadcast it through their own WebSocket. This hook is pure sugar
|
|
30
|
-
* that adds the typed name + claim narrowing.
|
|
31
|
-
*/
|
|
32
|
-
export function useIntent(intentName) {
|
|
33
|
-
const { beginIntent } = useSyncContext();
|
|
34
|
-
return useCallback((claim) => {
|
|
35
|
-
if (!beginIntent) {
|
|
36
|
-
throw new AbloValidationError(`useIntent: no \`beginIntent\` wired into SyncProvider. Pass ` +
|
|
37
|
-
`a \`beginIntent\` prop (typically bound to your transport) ` +
|
|
38
|
-
`to enable intent invocations.`, { code: 'intent_not_wired' });
|
|
39
|
-
}
|
|
40
|
-
return beginIntent(intentName, claim);
|
|
41
|
-
}, [beginIntent, intentName]);
|
|
42
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `awaitIntentGrant` — the client side of the fair-queue handover.
|
|
3
|
-
*
|
|
4
|
-
* When a `claim` is contended, the server enqueues it and replies `queued`
|
|
5
|
-
* (HTTP 202 on `/v1/intents`, or `intent_queued` over WS). The grant is then
|
|
6
|
-
* PUSHED later over the WS as `intent_granted` when the claim reaches the head.
|
|
7
|
-
* This resolves once that frame arrives for our `intentId` — so the caller's
|
|
8
|
-
* `claim` promise stays pending (event-driven; no poll, no race) until it's
|
|
9
|
-
* actually our turn. Rejects on `intent_lost` (surfaced as `claim_lost`: the claim was taken away — TTL
|
|
10
|
-
* lapse on disconnect, revoke) or an optional timeout.
|
|
11
|
-
*
|
|
12
|
-
* Takes only a minimal `{ subscribe }` transport so it unit-tests against a
|
|
13
|
-
* fake; `SyncWebSocket` satisfies it structurally.
|
|
14
|
-
*/
|
|
15
|
-
export interface GrantTransport {
|
|
16
|
-
subscribe(event: 'intent_acquired' | 'intent_granted' | 'intent_lost' | 'intent_queued', handler: (payload: Record<string, unknown>) => void): () => void;
|
|
17
|
-
}
|
|
18
|
-
export interface IntentGrantInfo {
|
|
19
|
-
/**
|
|
20
|
-
* True when the grant arrived as `intent_granted` — i.e. the target was
|
|
21
|
-
* HELD when we asked and we waited in the FIFO line behind the holder.
|
|
22
|
-
* False for the immediate `intent_acquired` (target was free).
|
|
23
|
-
*
|
|
24
|
-
* Callers use this to know the row may have changed while we queued:
|
|
25
|
-
* intent VISIBILITY is entity-scoped (org-wide subscriptions receive no
|
|
26
|
-
* presence/intent fan-out — see Hub.broadcastPresenceChange), so the
|
|
27
|
-
* local coordination snapshot cannot be trusted to detect "we waited".
|
|
28
|
-
* The grant frame itself is the authoritative signal.
|
|
29
|
-
*/
|
|
30
|
-
readonly waited: boolean;
|
|
31
|
-
}
|
|
32
|
-
export declare function awaitIntentGrant(transport: GrantTransport, intentId: string, options?: {
|
|
33
|
-
timeoutMs?: number;
|
|
34
|
-
/**
|
|
35
|
-
* Backpressure: reject instead of waiting if, when we join the line, the
|
|
36
|
-
* server reports `position >= maxQueueDepth` (i.e. that many claims are
|
|
37
|
-
* already ahead of us). Omit to wait however deep the queue is.
|
|
38
|
-
*/
|
|
39
|
-
maxQueueDepth?: number;
|
|
40
|
-
}): Promise<IntentGrantInfo>;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `awaitIntentGrant` — the client side of the fair-queue handover.
|
|
3
|
-
*
|
|
4
|
-
* When a `claim` is contended, the server enqueues it and replies `queued`
|
|
5
|
-
* (HTTP 202 on `/v1/intents`, or `intent_queued` over WS). The grant is then
|
|
6
|
-
* PUSHED later over the WS as `intent_granted` when the claim reaches the head.
|
|
7
|
-
* This resolves once that frame arrives for our `intentId` — so the caller's
|
|
8
|
-
* `claim` promise stays pending (event-driven; no poll, no race) until it's
|
|
9
|
-
* actually our turn. Rejects on `intent_lost` (surfaced as `claim_lost`: the claim was taken away — TTL
|
|
10
|
-
* lapse on disconnect, revoke) or an optional timeout.
|
|
11
|
-
*
|
|
12
|
-
* Takes only a minimal `{ subscribe }` transport so it unit-tests against a
|
|
13
|
-
* fake; `SyncWebSocket` satisfies it structurally.
|
|
14
|
-
*/
|
|
15
|
-
import { AbloClaimedError } from '../errors.js';
|
|
16
|
-
export function awaitIntentGrant(transport, intentId, options) {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
const unsubs = [];
|
|
19
|
-
let timer;
|
|
20
|
-
const settle = (fn) => {
|
|
21
|
-
if (timer)
|
|
22
|
-
clearTimeout(timer);
|
|
23
|
-
for (const u of unsubs)
|
|
24
|
-
u();
|
|
25
|
-
fn();
|
|
26
|
-
};
|
|
27
|
-
// The target was free → `intent_acquired` (immediate); it was contended,
|
|
28
|
-
// we waited in line, and reached the head → `intent_granted`. Either frame
|
|
29
|
-
// means the lease is now ours; `waited` records which path it was.
|
|
30
|
-
unsubs.push(transport.subscribe('intent_acquired', (p) => {
|
|
31
|
-
if (p?.intentId === intentId)
|
|
32
|
-
settle(() => resolve({ waited: false }));
|
|
33
|
-
}));
|
|
34
|
-
unsubs.push(transport.subscribe('intent_granted', (p) => {
|
|
35
|
-
if (p?.intentId === intentId)
|
|
36
|
-
settle(() => resolve({ waited: true }));
|
|
37
|
-
}));
|
|
38
|
-
if (options?.maxQueueDepth !== undefined) {
|
|
39
|
-
const max = options.maxQueueDepth;
|
|
40
|
-
unsubs.push(transport.subscribe('intent_queued', (p) => {
|
|
41
|
-
if (p?.intentId !== intentId)
|
|
42
|
-
return;
|
|
43
|
-
const position = typeof p.position === 'number' ? p.position : 0;
|
|
44
|
-
if (position >= max) {
|
|
45
|
-
settle(() => reject(new AbloClaimedError(`Claim queue for ${intentId} is ${position} deep (max ${max}).`, { code: 'queue_too_deep' })));
|
|
46
|
-
}
|
|
47
|
-
}));
|
|
48
|
-
}
|
|
49
|
-
unsubs.push(transport.subscribe('intent_lost', (p) => {
|
|
50
|
-
if (p?.intentId === intentId) {
|
|
51
|
-
settle(() => reject(new AbloClaimedError(`Claim lost while queued for ${intentId}.`, {
|
|
52
|
-
code: 'claim_lost',
|
|
53
|
-
})));
|
|
54
|
-
}
|
|
55
|
-
}));
|
|
56
|
-
if (options?.timeoutMs && options.timeoutMs > 0) {
|
|
57
|
-
timer = setTimeout(() => {
|
|
58
|
-
settle(() => reject(new AbloClaimedError(`Timed out waiting for the queue grant on claim ${intentId}.`, { code: 'grant_timeout' })));
|
|
59
|
-
}, options.timeoutMs);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transport-driven IntentStream factory.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `createPresenceStream` — built directly on `SyncWebSocket`,
|
|
5
|
-
* no SyncAgent wrapper. Intents derive their `others` view from the
|
|
6
|
-
* same `presence_update` frames the presence stream consumes (the
|
|
7
|
-
* Hub piggybacks `activeIntents` on every presence frame). Outbound
|
|
8
|
-
* announce/revoke ride the same socket via `intent_begin` /
|
|
9
|
-
* `intent_abandon` frames.
|
|
10
|
-
*
|
|
11
|
-
* Wire contract (apps/sync-server/src/hub/types.ts):
|
|
12
|
-
* • Outbound: `{ type: 'intent_begin', payload: { intentId,
|
|
13
|
-
* entityType, entityId, action, field?, estimatedMs? } }`
|
|
14
|
-
* • Outbound: `{ type: 'intent_abandon', payload: { intentId,
|
|
15
|
-
* entityType?, entityId? } }`
|
|
16
|
-
* • Inbound (via presence): `event.activeIntents: IntentClaim[]`
|
|
17
|
-
* stamped with `declaredAt`, `expiresAt`.
|
|
18
|
-
* • Inbound: `intent_rejected` event with conflict metadata.
|
|
19
|
-
*
|
|
20
|
-
* After the dual-engine collapse (step #36), this is the only
|
|
21
|
-
* IntentStream factory in the SDK; the older compatibility path
|
|
22
|
-
* deletes.
|
|
23
|
-
*/
|
|
24
|
-
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
25
|
-
import type { IntentStream } from '../types/streams.js';
|
|
26
|
-
export interface IntentStreamConfig {
|
|
27
|
-
/** Identity used to filter our own active intents out of `others`. */
|
|
28
|
-
participantId: string;
|
|
29
|
-
}
|
|
30
|
-
export interface AttachableIntentStream extends IntentStream {
|
|
31
|
-
attach(transport: SyncWebSocket): void;
|
|
32
|
-
dispose(): void;
|
|
33
|
-
}
|
|
34
|
-
export declare function createIntentStream(config: IntentStreamConfig, transport?: SyncWebSocket | null): AttachableIntentStream;
|