@abloatai/ablo 0.10.0 → 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.
Files changed (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -1
  3. package/dist/BaseSyncedStore.d.ts +75 -0
  4. package/dist/BaseSyncedStore.js +193 -8
  5. package/dist/Database.d.ts +10 -2
  6. package/dist/Database.js +15 -1
  7. package/dist/SyncClient.d.ts +12 -1
  8. package/dist/SyncClient.js +110 -26
  9. package/dist/agent/Agent.d.ts +9 -9
  10. package/dist/agent/Agent.js +16 -16
  11. package/dist/agent/index.d.ts +1 -1
  12. package/dist/agent/index.js +2 -2
  13. package/dist/agent/types.d.ts +1 -1
  14. package/dist/agent/types.js +1 -1
  15. package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
  16. package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
  17. package/dist/ai-sdk/coordination-context.d.ts +9 -9
  18. package/dist/ai-sdk/coordination-context.js +8 -8
  19. package/dist/ai-sdk/index.d.ts +1 -1
  20. package/dist/ai-sdk/index.js +1 -1
  21. package/dist/ai-sdk/wrap.d.ts +4 -4
  22. package/dist/ai-sdk/wrap.js +4 -4
  23. package/dist/api/index.d.ts +2 -2
  24. package/dist/cli.cjs +254 -48
  25. package/dist/client/Ablo.d.ts +30 -63
  26. package/dist/client/Ablo.js +108 -102
  27. package/dist/client/ApiClient.d.ts +6 -5
  28. package/dist/client/ApiClient.js +83 -62
  29. package/dist/client/createModelProxy.d.ts +16 -54
  30. package/dist/client/createModelProxy.js +44 -16
  31. package/dist/client/httpClient.d.ts +2 -0
  32. package/dist/client/httpClient.js +1 -1
  33. package/dist/client/index.d.ts +3 -3
  34. package/dist/client/writeOptionsSchema.d.ts +4 -4
  35. package/dist/client/writeOptionsSchema.js +4 -4
  36. package/dist/coordination/schema.d.ts +249 -38
  37. package/dist/coordination/schema.js +172 -39
  38. package/dist/core/index.d.ts +2 -2
  39. package/dist/core/index.js +4 -4
  40. package/dist/errorCodes.d.ts +9 -9
  41. package/dist/errorCodes.js +15 -15
  42. package/dist/errors.d.ts +51 -2
  43. package/dist/errors.js +94 -5
  44. package/dist/interfaces/index.d.ts +8 -4
  45. package/dist/policy/index.d.ts +1 -1
  46. package/dist/policy/types.d.ts +13 -13
  47. package/dist/policy/types.js +8 -8
  48. package/dist/react/AbloProvider.d.ts +51 -4
  49. package/dist/react/AbloProvider.js +95 -11
  50. package/dist/react/context.d.ts +26 -9
  51. package/dist/react/context.js +2 -2
  52. package/dist/react/index.d.ts +4 -4
  53. package/dist/react/index.js +4 -4
  54. package/dist/react/useAblo.js +5 -5
  55. package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
  56. package/dist/react/useClaim.js +42 -0
  57. package/dist/schema/index.js +1 -1
  58. package/dist/schema/sugar.d.ts +3 -3
  59. package/dist/schema/sugar.js +3 -3
  60. package/dist/schema/sync-delta-wire.d.ts +8 -8
  61. package/dist/server/commit.d.ts +2 -2
  62. package/dist/sync/AreaOfInterestManager.d.ts +162 -0
  63. package/dist/sync/AreaOfInterestManager.js +233 -0
  64. package/dist/sync/BootstrapHelper.d.ts +9 -1
  65. package/dist/sync/BootstrapHelper.js +15 -5
  66. package/dist/sync/NetworkProbe.d.ts +1 -1
  67. package/dist/sync/NetworkProbe.js +1 -1
  68. package/dist/sync/SyncWebSocket.d.ts +59 -25
  69. package/dist/sync/SyncWebSocket.js +123 -26
  70. package/dist/sync/awaitClaimGrant.d.ts +40 -0
  71. package/dist/sync/awaitClaimGrant.js +86 -0
  72. package/dist/sync/createClaimStream.d.ts +34 -0
  73. package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
  74. package/dist/sync/createPresenceStream.js +3 -2
  75. package/dist/sync/participants.d.ts +10 -10
  76. package/dist/sync/participants.js +17 -10
  77. package/dist/sync/schemas.d.ts +8 -8
  78. package/dist/transactions/TransactionQueue.d.ts +12 -0
  79. package/dist/transactions/TransactionQueue.js +126 -8
  80. package/dist/types/global.d.ts +10 -10
  81. package/dist/types/global.js +3 -3
  82. package/dist/types/index.d.ts +9 -7
  83. package/dist/types/index.js +2 -2
  84. package/dist/types/streams.d.ts +114 -98
  85. package/dist/types/streams.js +1 -1
  86. package/dist/utils/asyncIterator.d.ts +1 -1
  87. package/dist/utils/asyncIterator.js +1 -1
  88. package/dist/wire/frames.d.ts +2 -2
  89. package/docs/migration.md +52 -0
  90. package/package.json +3 -2
  91. package/dist/react/useIntent.js +0 -42
  92. package/dist/sync/awaitIntentGrant.d.ts +0 -40
  93. package/dist/sync/awaitIntentGrant.js +0 -62
  94. package/dist/sync/createIntentStream.d.ts +0 -34
@@ -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 intent before writing, and capture context watermarks before
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, IntentClaim, PresenceKind } from '../coordination/schema.js';
13
- export type { TargetRange, OnStaleMode, IntentClaim, PresenceKind };
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: 'human' | 'agent';
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 intents this participant has declared. */
301
- readonly activeIntents?: ReadonlyArray<IntentClaim>;
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
- * Intent broadcasts — "I'm about to do X on Y." Broadcasts flow on
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 intent doesn't enforce exclusion; it
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 intent announcement
318
- * (`intents.analyzing`, `.drafting`, etc.).
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 `IntentStream` below
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 IntentOptions {
325
+ export interface ClaimLeaseOptions {
326
326
  /**
327
- * How long before the server auto-expires this intent if the
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 IntentOptions {
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 (`intent_acquired` if the
353
- * target was free, `intent_granted` once promoted to the head of the line).
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 `awaitIntentGrant`.
356
+ * (`ablo.<model>.claim`), which pair this flag with `awaitClaimGrant`.
357
357
  */
358
358
  readonly queue?: boolean;
359
359
  }
360
- export interface IntentStream {
360
+ export interface ClaimStream {
361
361
  /**
362
- * Claim an exclusive intent on a target. Returns a handle — call
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 `intent_rejected` when another participant
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): Claim;
375
+ claim(target: PresenceTarget, opts?: ClaimOptions): ClaimHandle;
376
376
  /**
377
- * Reactive view of every other participant's active intents.
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<ActiveIntent>;
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'` intents behind the current holder, each with its
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
- * `intent_queue` frame; empty when no one's waiting. Pair with
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 Intent[];
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 `Intent[]` from `queueFor(target)` in the order you want
394
- * (each `Intent` carries its `heldBy` + `id`). Privileged: the server gates
395
- * it (a participant lacking the `intent.reorder` capability is denied), so
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 Intent[]): void;
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 intent rejections. Fires when the server
409
- * rejects an `intents.writing(...)` / `announce(...)` call because
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.intents.onRejected((r) => {
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: IntentRejection) => void): () => void;
422
+ onRejected(listener: (rejection: ClaimRejection) => void): () => void;
423
423
  /**
424
- * Observe LOSING an intent you held — distinct from `onRejected` (a claim the
425
- * server refused). Fires on the server's `intent_lost` frame, carrying why:
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.intents.onLost((lost) => {
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: IntentLost) => void): () => void;
439
+ onLost(listener: (lost: ClaimLost) => void): () => void;
440
440
  /**
441
- * Async-iterable view of everyone else's open intents. Each
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 openIntents of participant.intents) {
446
- * if (openIntents.some((i) => i.target.id === clauseId)) wait();
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<ActiveIntent>>;
450
+ [Symbol.asyncIterator](): AsyncIterableIterator<ReadonlyArray<ActiveClaim>>;
451
451
  }
452
452
  /**
453
- * Shape of an `intent_rejected` event delivered to
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 IntentLost {
456
+ export interface ClaimLost {
483
457
  /** The held claim's id that you just lost. */
484
- readonly intentId: string;
458
+ readonly claimId: string;
485
459
  /**
486
460
  * How you lost it. `'preempted'`: a privileged participant (one holding the
487
- * `intent.preempt` capability) evicted you and took the lease — its work now
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 IntentDeclaration {
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.intents.analyzing(clause, { ttl: '3m' });
521
- * // ... do the work; intent auto-revokes when the block exits
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
- export interface Claim extends AsyncDisposable {
526
- readonly id: string;
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 ActiveIntent extends IntentDeclaration {
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 human (session) or an agent.
534
- * First-class field so UIs can style "agent editing X" differently
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: 'human' | 'agent';
551
+ readonly participantKind: ParticipantKind;
538
552
  readonly description?: string;
539
- readonly announcedAt: string;
540
- readonly expiresAt: string;
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 intent, in one enum.
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
- * intent from the synced set.
562
+ * claim from the synced set.
547
563
  */
548
- export type IntentStatus = 'active' | 'queued' | 'committed' | 'expired' | 'canceled';
564
+ export type ClaimStatus = 'active' | 'queued' | 'committed' | 'expired' | 'canceled';
549
565
  /** Options for waiting on a target to become free. */
550
- export interface IntentWaitOptions {
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: 'intent'`. Existence with `status: 'active'` *is* the lock;
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 `intents.waitFor` to wait without claiming), and the
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 Intent {
569
- readonly object: 'intent';
584
+ export interface Claim {
585
+ readonly object: 'claim';
570
586
  readonly id: string;
571
- readonly status: IntentStatus;
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: 'human' | 'agent';
596
+ readonly participantKind: ParticipantKind;
581
597
  /**
582
- * Ms-epoch the holder opened it. Optional until the lease wire carries
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?: string;
586
- /** Ms-epoch the server auto-expires it if the holder doesn't finish. */
587
- readonly expiresAt: string;
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.
@@ -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 intent before writing, and capture context watermarks before
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` / `IntentStream`. We register a
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` / `IntentStream`. We register a
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
@@ -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 (intent + args), as opposed to the raw-op {@link CommitMessage}
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 (`intent`) id plus the server-stamped actor/capability. Kept
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/migration.md CHANGED
@@ -11,6 +11,7 @@ change when you upgrade.
11
11
 
12
12
  | Version | What changed | What to do |
13
13
  |---|---|---|
14
+ | **0.10.0** | Environment enum renamed `test`/`live` → `sandbox`/`production` | Update code that branches on the environment (e.g. source `mode`): `'test'`→`'sandbox'`, `'live'`→`'production'`. Key prefixes `sk_test_`/`sk_live_` are unchanged |
14
15
  | **0.9.2** | `turn` primitive + agent-work `tasks` resource removed | Coordinate with `claim`; mint a scoped session instead of `agent().run()` |
15
16
  | **0.9.2** | `intents` deprecated in favor of `claim` | Use `ablo.<model>.claim`; `ablo.intents` is now `@internal` |
16
17
  | **0.9.0** | One options object per verb | `update(id, data, opts)` → `update({ id, data, ...opts })` |
@@ -23,6 +24,57 @@ change when you upgrade.
23
24
 
24
25
  ---
25
26
 
27
+ ## 0.10.0 — environment enum `sandbox` / `production`; stateless HTTP transport
28
+
29
+ ### Environment enum rename (the only breaking change)
30
+
31
+ The canonical environment values are now **`production`** and **`sandbox`** (was
32
+ `live` and `test`). This is a *vocabulary* change at the type/API layer — the
33
+ on-the-wire key prefixes are **unchanged**: keys are still `sk_test_…` /
34
+ `sk_live_…` and parse exactly as before. What changed is the enum you see in
35
+ code: `Environment`, the source-handler `mode` field, and `ApiKeyEnv` now read
36
+ `production` / `sandbox`.
37
+
38
+ You only need to act if your code branches on the environment value — most
39
+ commonly a Data Source handler keyed on `mode`. The mapping is exactly
40
+ `test → sandbox`, `live → production`:
41
+
42
+ ```diff
43
+ const handler = createSourceHandler({
44
+ read: async ({ mode }) => {
45
+ - const db = mode === 'test' ? testDb : liveDb;
46
+ + const db = mode === 'sandbox' ? sandboxDb : productionDb;
47
+ // …
48
+ },
49
+ });
50
+ ```
51
+
52
+ `commit` now also forwards `projectId`, `accountScope`, and `environment` to
53
+ source resolvers, so per-project and per-environment traffic can be routed to
54
+ distinct stores.
55
+
56
+ > **CLI note:** the legacy single-file config that stored `test`/`live` key
57
+ > buckets is no longer auto-migrated. If `ablo status` can't find your keys after
58
+ > upgrading, re-run `ablo login` to write the current `sandbox`/`production`
59
+ > layout.
60
+
61
+ ### New (non-breaking): `transport: 'http'`
62
+
63
+ `Ablo({ transport: 'http' })` returns a stateless `AbloHttpClient` for
64
+ server-side actors (agents, workers, serverless): the same `ablo.<model>` surface
65
+ and `claim` coordination, but each call is one HTTP round-trip with identity on
66
+ the Bearer credential — no websocket, no local synced pool. The return type
67
+ narrows, so stateful-only APIs (`get` / `getAll` / `onChange`) become compile
68
+ errors instead of latent runtime gaps. Existing code keeps the default
69
+ `'websocket'` transport, unchanged.
70
+
71
+ ```ts
72
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: 'http' });
73
+ await ablo.tasks.update({ id, data: { status: 'done' } });
74
+ ```
75
+
76
+ ---
77
+
26
78
  ## 0.9.2 — `turn` / agent-`tasks` removed; `intents` deprecated
27
79
 
28
80
  The SDK's coordination surface is now exactly two things: `ablo.<model>` writes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.10.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
  }
@@ -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>;