@abloatai/ablo 0.8.0 → 0.9.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.
Files changed (165) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +33 -28
  3. package/dist/BaseSyncedStore.d.ts +83 -0
  4. package/dist/BaseSyncedStore.js +194 -2
  5. package/dist/Model.d.ts +42 -0
  6. package/dist/Model.js +103 -44
  7. package/dist/agent/session.js +3 -3
  8. package/dist/ai-sdk/coordination-context.js +4 -0
  9. package/dist/ai-sdk/index.d.ts +56 -47
  10. package/dist/ai-sdk/index.js +56 -47
  11. package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
  12. package/dist/ai-sdk/intent-broadcast.js +11 -4
  13. package/dist/ai-sdk/wrap.d.ts +14 -11
  14. package/dist/ai-sdk/wrap.js +11 -13
  15. package/dist/auth/credentialSource.d.ts +34 -0
  16. package/dist/auth/credentialSource.js +63 -0
  17. package/dist/auth/index.d.ts +2 -22
  18. package/dist/auth/index.js +4 -42
  19. package/dist/auth/schemas.d.ts +35 -0
  20. package/dist/auth/schemas.js +53 -0
  21. package/dist/client/Ablo.d.ts +160 -42
  22. package/dist/client/Ablo.js +145 -75
  23. package/dist/client/ApiClient.d.ts +20 -4
  24. package/dist/client/ApiClient.js +166 -28
  25. package/dist/client/auth.d.ts +14 -5
  26. package/dist/client/auth.js +60 -7
  27. package/dist/client/createInternalComponents.d.ts +2 -0
  28. package/dist/client/createInternalComponents.js +8 -1
  29. package/dist/client/createModelProxy.d.ts +130 -66
  30. package/dist/client/createModelProxy.js +152 -49
  31. package/dist/client/httpClient.d.ts +71 -0
  32. package/dist/client/httpClient.js +69 -0
  33. package/dist/client/identity.d.ts +2 -6
  34. package/dist/client/identity.js +49 -11
  35. package/dist/client/index.d.ts +1 -0
  36. package/dist/client/index.js +1 -0
  37. package/dist/client/registerDataSource.d.ts +3 -3
  38. package/dist/client/registerDataSource.js +11 -9
  39. package/dist/client/validateAbloOptions.js +1 -1
  40. package/dist/core/DatabaseManager.js +30 -2
  41. package/dist/core/openIDBWithTimeout.d.ts +36 -0
  42. package/dist/core/openIDBWithTimeout.js +88 -1
  43. package/dist/errorCodes.d.ts +70 -1
  44. package/dist/errorCodes.js +108 -9
  45. package/dist/errors.d.ts +2 -2
  46. package/dist/errors.js +72 -22
  47. package/dist/index.d.ts +17 -8
  48. package/dist/index.js +15 -6
  49. package/dist/keys/index.d.ts +16 -1
  50. package/dist/keys/index.js +26 -6
  51. package/dist/mutators/UndoManager.d.ts +158 -50
  52. package/dist/mutators/UndoManager.js +345 -22
  53. package/dist/mutators/inverseOp.d.ts +129 -0
  54. package/dist/mutators/inverseOp.js +74 -0
  55. package/dist/mutators/readerActions.d.ts +1 -1
  56. package/dist/mutators/undoApply.d.ts +42 -0
  57. package/dist/mutators/undoApply.js +143 -0
  58. package/dist/query/client.d.ts +10 -9
  59. package/dist/query/client.js +3 -6
  60. package/dist/react/AbloProvider.d.ts +23 -126
  61. package/dist/react/AbloProvider.js +62 -199
  62. package/dist/react/context.d.ts +31 -0
  63. package/dist/react/useAblo.d.ts +2 -2
  64. package/dist/react/useCurrentUserId.d.ts +1 -1
  65. package/dist/react/useCurrentUserId.js +1 -1
  66. package/dist/react/useMutators.js +19 -12
  67. package/dist/schema/ddl.d.ts +34 -3
  68. package/dist/schema/ddl.js +162 -4
  69. package/dist/schema/index.d.ts +5 -1
  70. package/dist/schema/index.js +13 -1
  71. package/dist/schema/model.d.ts +11 -0
  72. package/dist/schema/model.js +2 -0
  73. package/dist/schema/openapi.d.ts +28 -0
  74. package/dist/schema/openapi.js +118 -0
  75. package/dist/schema/plane.d.ts +23 -0
  76. package/dist/schema/plane.js +19 -0
  77. package/dist/schema/relation.d.ts +20 -0
  78. package/dist/schema/serialize.d.ts +4 -0
  79. package/dist/schema/serialize.js +4 -0
  80. package/dist/schema/sync-delta-row.d.ts +157 -0
  81. package/dist/schema/sync-delta-row.js +102 -0
  82. package/dist/schema/sync-delta-wire.d.ts +180 -0
  83. package/dist/schema/sync-delta-wire.js +102 -0
  84. package/dist/server/adapter.d.ts +156 -0
  85. package/dist/server/adapter.js +19 -0
  86. package/dist/server/commit.d.ts +82 -0
  87. package/dist/server/commit.js +1 -0
  88. package/dist/server/index.d.ts +14 -0
  89. package/dist/server/index.js +1 -0
  90. package/dist/server/next.d.ts +51 -0
  91. package/dist/server/next.js +47 -0
  92. package/dist/server/read-config.d.ts +60 -0
  93. package/dist/server/read-config.js +8 -0
  94. package/dist/server/storage-mode.d.ts +17 -0
  95. package/dist/server/storage-mode.js +12 -0
  96. package/dist/source/adapter.d.ts +65 -0
  97. package/dist/source/adapter.js +20 -0
  98. package/dist/source/adapters/drizzle.d.ts +43 -0
  99. package/dist/source/adapters/drizzle.js +185 -0
  100. package/dist/source/adapters/memory.d.ts +12 -0
  101. package/dist/source/adapters/memory.js +114 -0
  102. package/dist/source/adapters/prisma.d.ts +57 -0
  103. package/dist/source/adapters/prisma.js +176 -0
  104. package/dist/source/conformance.d.ts +32 -0
  105. package/dist/source/conformance.js +134 -0
  106. package/dist/source/contract.d.ts +144 -0
  107. package/dist/source/contract.js +99 -0
  108. package/dist/source/index.d.ts +62 -10
  109. package/dist/source/index.js +99 -0
  110. package/dist/source/migrations.d.ts +14 -0
  111. package/dist/source/migrations.js +39 -0
  112. package/dist/source/next.d.ts +33 -0
  113. package/dist/source/next.js +26 -0
  114. package/dist/sync/BootstrapHelper.d.ts +10 -0
  115. package/dist/sync/BootstrapHelper.js +10 -15
  116. package/dist/sync/ConnectionManager.d.ts +55 -1
  117. package/dist/sync/ConnectionManager.js +155 -16
  118. package/dist/sync/HydrationCoordinator.d.ts +93 -17
  119. package/dist/sync/HydrationCoordinator.js +238 -39
  120. package/dist/sync/NetworkProbe.d.ts +58 -24
  121. package/dist/sync/NetworkProbe.js +118 -42
  122. package/dist/sync/SyncWebSocket.d.ts +45 -70
  123. package/dist/sync/SyncWebSocket.js +70 -36
  124. package/dist/sync/createIntentStream.js +10 -1
  125. package/dist/types/streams.d.ts +9 -0
  126. package/dist/utils/mobx-setup.js +1 -0
  127. package/dist/webhooks/events.d.ts +38 -0
  128. package/dist/webhooks/events.js +40 -0
  129. package/dist/webhooks/index.d.ts +10 -0
  130. package/dist/webhooks/index.js +10 -0
  131. package/dist/wire/errorEnvelope.d.ts +34 -0
  132. package/dist/wire/errorEnvelope.js +86 -0
  133. package/dist/wire/frames.d.ts +119 -0
  134. package/dist/wire/frames.js +1 -0
  135. package/dist/wire/index.d.ts +24 -0
  136. package/dist/wire/index.js +21 -0
  137. package/dist/wire/listEnvelope.d.ts +45 -0
  138. package/dist/wire/listEnvelope.js +17 -0
  139. package/docs/api.md +47 -44
  140. package/docs/cli.md +44 -44
  141. package/docs/client-behavior.md +30 -30
  142. package/docs/coordination.md +33 -36
  143. package/docs/data-sources.md +35 -15
  144. package/docs/examples/agent-human.md +45 -43
  145. package/docs/examples/ai-sdk-tool.md +20 -16
  146. package/docs/examples/existing-python-backend.md +16 -12
  147. package/docs/examples/nextjs.md +14 -12
  148. package/docs/examples/scoped-agent.md +1 -1
  149. package/docs/examples/server-agent.md +24 -21
  150. package/docs/guarantees.md +15 -13
  151. package/docs/index.md +2 -2
  152. package/docs/integration-guide.md +30 -30
  153. package/docs/interaction-model.md +19 -23
  154. package/docs/mcp/claude-code.md +3 -3
  155. package/docs/mcp/cursor.md +1 -1
  156. package/docs/mcp/windsurf.md +2 -2
  157. package/docs/mcp.md +6 -6
  158. package/docs/quickstart.md +41 -31
  159. package/docs/react.md +13 -9
  160. package/docs/schema-contract.md +12 -10
  161. package/docs/the-loop.md +21 -0
  162. package/examples/data-source/README.md +4 -5
  163. package/examples/data-source/customer-server.ts +27 -25
  164. package/llms.txt +28 -5
  165. package/package.json +43 -3
package/dist/Model.js CHANGED
@@ -357,8 +357,81 @@ export class Model {
357
357
  timestamp: new Date(),
358
358
  };
359
359
  }
360
+ /**
361
+ * Safely assign each field of `data` onto this instance, skipping `id`,
362
+ * unknown keys, MobX computed accessors, and getter-only (read-only)
363
+ * properties, and coercing date fields. Shared by `updateFromData`
364
+ * (hydration) and `applyChanges` (local user update).
365
+ *
366
+ * Change tracking is EXPLICIT, not magic: for every field actually
367
+ * written, `onWrite(key, oldValue, newValue)` is invoked with the value
368
+ * captured immediately before assignment. `applyChanges` passes a hook
369
+ * that records the change in `modifiedProperties`; `updateFromData`
370
+ * passes none (hydration must not generate outbound mutations). This
371
+ * is the single source of mutation tracking now that the `mobx-setup`
372
+ * `observe()` bridge has been removed (one write path: the SDK proxy).
373
+ */
374
+ assignFieldsFromData(data, onWrite) {
375
+ // Update properties with safety checks for read-only/computed accessors
376
+ for (const [key, raw] of Object.entries(data)) {
377
+ if (key === 'id')
378
+ continue;
379
+ // Only attempt to set if the property exists on instance or prototype
380
+ if (!(this.hasOwnProperty(key) || key in this))
381
+ continue;
382
+ // Never assign to MobX computed properties (they may expose a setter that throws)
383
+ try {
384
+ if (isComputedProp(this, key)) {
385
+ continue;
386
+ }
387
+ }
388
+ catch {
389
+ // If MobX internals are unavailable for some reason, fall back to descriptor checks below
390
+ }
391
+ // Resolve property descriptor from own or prototype chain
392
+ const ownDesc = Object.getOwnPropertyDescriptor(this, key);
393
+ let desc = ownDesc;
394
+ if (!desc) {
395
+ let proto = Object.getPrototypeOf(this);
396
+ while (proto && proto !== Object.prototype && !desc) {
397
+ desc = Object.getOwnPropertyDescriptor(proto, key);
398
+ proto = Object.getPrototypeOf(proto);
399
+ }
400
+ }
401
+ // Determine writability: allow if data descriptor writable, or accessor with setter
402
+ const writable = desc
403
+ ? ('writable' in desc && !!desc.writable) ||
404
+ ('set' in desc && typeof desc.set === 'function')
405
+ : true;
406
+ if (!writable) {
407
+ // Skip read-only accessor properties (getter-only)
408
+ continue;
409
+ }
410
+ // Handle date conversions
411
+ const value = (key === 'createdAt' || key === 'updatedAt' || key === 'archivedAt') && raw
412
+ ? new Date(raw)
413
+ : raw;
414
+ // Capture the pre-write value BEFORE assignment so trackers
415
+ // (undo inverse, getChanges) see the true previous value.
416
+ const oldValue = onWrite ? this[key] : undefined;
417
+ // Dynamic property assignment - use indexed access
418
+ this[key] = value;
419
+ onWrite?.(key, oldValue, value);
420
+ }
421
+ }
360
422
  /**
361
423
  * Update from raw data (hydration)
424
+ *
425
+ * Used for inbound server deltas and pool upserts. Change tracking is
426
+ * deliberately suppressed: hydration writes must NOT land in
427
+ * `modifiedProperties`, otherwise applying a server delta would queue a
428
+ * brand-new outbound mutation and the record would echo forever. For a
429
+ * LOCAL user edit, use `applyChanges` instead.
430
+ *
431
+ * Suppression is belt-and-suspenders: we pass no `onWrite` hook AND
432
+ * clear/restore `modifiedProperties` around the assignment, so any
433
+ * remaining `mobx-setup` `observe()` side-channel writes are discarded
434
+ * too. (The clear/restore is a harmless no-op once that bridge is gone.)
362
435
  */
363
436
  updateFromData(data) {
364
437
  if (this.isDisposed) {
@@ -367,52 +440,10 @@ export class Model {
367
440
  });
368
441
  }
369
442
  runInAction(() => {
370
- // Temporarily disable change tracking
371
443
  const originalTracking = this.modifiedProperties;
372
444
  this.modifiedProperties = new Map();
373
- // Update properties with safety checks for read-only/computed accessors
374
- for (const [key, raw] of Object.entries(data)) {
375
- if (key === 'id')
376
- continue;
377
- // Only attempt to set if the property exists on instance or prototype
378
- if (!(this.hasOwnProperty(key) || key in this))
379
- continue;
380
- // Never assign to MobX computed properties (they may expose a setter that throws)
381
- try {
382
- if (isComputedProp(this, key)) {
383
- continue;
384
- }
385
- }
386
- catch {
387
- // If MobX internals are unavailable for some reason, fall back to descriptor checks below
388
- }
389
- // Resolve property descriptor from own or prototype chain
390
- const ownDesc = Object.getOwnPropertyDescriptor(this, key);
391
- let desc = ownDesc;
392
- if (!desc) {
393
- let proto = Object.getPrototypeOf(this);
394
- while (proto && proto !== Object.prototype && !desc) {
395
- desc = Object.getOwnPropertyDescriptor(proto, key);
396
- proto = Object.getPrototypeOf(proto);
397
- }
398
- }
399
- // Determine writability: allow if data descriptor writable, or accessor with setter
400
- const writable = desc
401
- ? ('writable' in desc && !!desc.writable) ||
402
- ('set' in desc && typeof desc.set === 'function')
403
- : true;
404
- if (!writable) {
405
- // Skip read-only accessor properties (getter-only)
406
- continue;
407
- }
408
- // Handle date conversions
409
- const value = (key === 'createdAt' || key === 'updatedAt' || key === 'archivedAt') && raw
410
- ? new Date(raw)
411
- : raw;
412
- // Dynamic property assignment for hydration - use indexed access
413
- this[key] = value;
414
- }
415
- // Restore change tracking
445
+ // No `onWrite` this call records nothing itself.
446
+ this.assignFieldsFromData(data);
416
447
  this.modifiedProperties = originalTracking;
417
448
  });
418
449
  // Mark as persisted if updating existing model
@@ -421,6 +452,34 @@ export class Model {
421
452
  }
422
453
  this.didUpdate();
423
454
  }
455
+ /**
456
+ * Apply a LOCAL user-initiated update from a data object — the write
457
+ * path for `proxy.update({ id, data })`, which is the ONE AND ONLY way
458
+ * application code mutates synced fields.
459
+ *
460
+ * Unlike `updateFromData` (hydration, untracked), this records every
461
+ * written field in `modifiedProperties` via `propertyChanged`, so
462
+ * `getChanges()` / the transaction queue send the edited fields to the
463
+ * server and the undo system gets a correct pre-write baseline.
464
+ * Recording is EXPLICIT here (via the `onWrite` hook) — it does not rely
465
+ * on any MobX `observe()` side-channel.
466
+ *
467
+ * `_originalData` is intentionally NOT reset here: it stays as the
468
+ * last-persisted baseline until `clearChanges()` runs on sync-ack.
469
+ */
470
+ applyChanges(data) {
471
+ if (this.isDisposed) {
472
+ throw new AbloValidationError('Cannot update disposed model', {
473
+ code: 'model_disposed',
474
+ });
475
+ }
476
+ runInAction(() => {
477
+ this.assignFieldsFromData(data, (key, oldValue, newValue) => {
478
+ this.propertyChanged(key, oldValue, newValue);
479
+ });
480
+ });
481
+ this.didUpdate();
482
+ }
424
483
  /**
425
484
  * Serialize to JSON
426
485
  * This method should not trigger MobX reactions since it's used for serialization
@@ -69,9 +69,9 @@ export function createAgentSession(options) {
69
69
  // `AbloOptions` exposes the URL as `baseURL` (resolved by
70
70
  // `resolveBaseURL`). Earlier code passed `url:` here — `Ablo()`
71
71
  // silently dropped the unknown field (the cast below masked the
72
- // type error) and `resolveBaseURL` fell through to the hardcoded
73
- // default `wss://api.ablo.cloud` (now `wss://mesh.ablo.finance`).
74
- // Staging surfaced the bug 2026-05-07 — DNS lookup hit the wrong
72
+ // type error) and `resolveBaseURL` fell through to the hosted
73
+ // default `wss://api.abloatai.com`. Staging surfaced the bug
74
+ // 2026-05-07 — DNS lookup hit the wrong
75
75
  // host even though the caller threaded `syncServerUrl` through
76
76
  // correctly. Forward as `baseURL` so the caller's URL is the only
77
77
  // source of truth and the package default never silently applies.
@@ -80,17 +80,21 @@ function formatCoordinationNote(claims, target) {
80
80
  const entityLabel = target.entityType.toLowerCase();
81
81
  if (claims.length === 1) {
82
82
  const c = claims[0];
83
+ const details = c.description ? `Declared work: ${c.description}. ` : '';
83
84
  return (`<multiplayer_context>\n` +
84
85
  `Another participant is currently editing this ${entityLabel}. ` +
85
86
  `Action declared: ${c.reason}. ` +
87
+ details +
86
88
  `Defer to their concurrent changes when reasonable, or note your work as complementary to theirs. ` +
87
89
  `Avoid stomping their in-flight edits.\n` +
88
90
  `</multiplayer_context>`);
89
91
  }
90
92
  const actions = Array.from(new Set(claims.map((c) => c.reason))).join(', ');
93
+ const descriptions = Array.from(new Set(claims.map((c) => c.description).filter(Boolean))).join('; ');
91
94
  return (`<multiplayer_context>\n` +
92
95
  `${claims.length} other participants are currently editing this ${entityLabel}. ` +
93
96
  `Active actions: ${actions}. ` +
97
+ (descriptions ? `Declared work: ${descriptions}. ` : '') +
94
98
  `Coordinate with their in-flight work — defer where reasonable, ` +
95
99
  `or describe your work as complementary.\n` +
96
100
  `</multiplayer_context>`);
@@ -1,67 +1,76 @@
1
1
  /**
2
- * `@abloatai/ablo/ai-sdk` multiplayer-with-AI as language model
3
- * middleware.
2
+ * Ablo + AI SDK tools.
4
3
  *
5
- * Two cross-cutting middlewares for any AI SDK consumer using
6
- * `streamText` / `generateText`:
4
+ * The base pattern is intentionally one object all the way down:
7
5
  *
8
- * - `intentBroadcastMiddleware` agent declares what it's about
9
- * to mutate via `intent_begin`, abandons the claim at stream end.
10
- * Peers see the broadcast in their presence stream's
11
- * `activeIntents` field and can defer / yield / surface
12
- * "agent is editing this entity right now."
13
- *
14
- * - `coordinationContextMiddleware` — reads peer intents from local
15
- * presence cache before the LLM call, injects a
16
- * `<multiplayer_context>` system note when peers are editing
17
- * the same entity. The AI gets coordination awareness without
18
- * extra round-trips.
19
- *
20
- * Compose them with the AI SDK's `wrapLanguageModel`:
6
+ * 1. AI SDK `inputSchema` describes what the model may send.
7
+ * 2. `ablo.<model>.update({ id, data, claim })` performs the write.
8
+ * 3. `claim.description` tells humans and other agents what the tool is doing.
21
9
  *
22
10
  * ```ts
23
- * import { wrapLanguageModel, streamText } from 'ai';
24
- * import {
25
- * intentBroadcastMiddleware,
26
- * coordinationContextMiddleware,
27
- * } from '@abloatai/ablo/ai-sdk';
11
+ * import { tool, streamText } from 'ai';
12
+ * import { z } from 'zod';
28
13
  *
29
- * const target = { entityType: 'SlideDeck', entityId: 'deck-abc' };
14
+ * const renameTask = tool({
15
+ * description: 'Rename a task.',
16
+ * inputSchema: z.object({
17
+ * id: z.string().describe('Task id'),
18
+ * title: z.string().describe('New task title'),
19
+ * description: z
20
+ * .string()
21
+ * .describe('Why this rename is being made'),
22
+ * }),
23
+ * execute: async ({ id, title, description }) => {
24
+ * await ablo.tasks.update({
25
+ * id,
26
+ * data: { title },
27
+ * wait: 'confirmed',
28
+ * claim: {
29
+ * field: 'title',
30
+ * action: 'renaming',
31
+ * description,
32
+ * },
33
+ * });
30
34
  *
31
- * const wrappedModel = wrapLanguageModel({
32
- * model: anthropic('claude-opus-4-7'),
33
- * middleware: [
34
- * coordinationContextMiddleware({ agent, target }),
35
- * intentBroadcastMiddleware({ agent, target }),
36
- * ],
35
+ * return { id, title };
36
+ * },
37
37
  * });
38
38
  *
39
- * // Consumer keeps full control over messages, tools, system prompt:
40
- * const result = streamText({
41
- * model: wrappedModel,
42
- * messages: [...],
43
- * tools: { ... },
44
- * system: '...',
39
+ * await streamText({
40
+ * model,
41
+ * messages,
42
+ * tools: { renameTask },
45
43
  * });
46
44
  * ```
47
45
  *
48
- * Or use the convenience composition for the common case:
46
+ * That is the common case. A claim passed directly to `update` is acquired,
47
+ * attached to the write, and released by the SDK.
49
48
  *
50
- * ```ts
51
- * import { wrapWithMultiplayer } from '@abloatai/ablo/ai-sdk';
49
+ * For multi-step tools, take one handle and release it when the tool is done:
52
50
  *
53
- * const wrappedModel = wrapWithMultiplayer({
54
- * model: anthropic('claude-opus-4-7'),
55
- * agent,
56
- * target: { entityType: 'SlideDeck', entityId: 'deck-abc' },
51
+ * ```ts
52
+ * const claim = await ablo.tasks.claim({
53
+ * id,
54
+ * action: 'rewriting',
55
+ * description: 'Rewriting the task brief before updating follow-up fields.',
56
+ * ttl: '2m',
57
57
  * });
58
+ *
59
+ * try {
60
+ * await ablo.tasks.update({ id, data: { title }, claim });
61
+ * await ablo.tasks.update({ id, data: { description: brief }, claim });
62
+ * } finally {
63
+ * await claim.release();
64
+ * }
58
65
  * ```
59
66
  *
60
- * Order matters: `coordinationContextMiddleware`'s `transformParams`
61
- * runs at param-transform time (before the model call), reading peer
62
- * intents *before* this agent's broadcast lands in its own cache.
63
- * `intentBroadcastMiddleware`'s `wrapStream` runs around the actual
64
- * call. Self-claim doesn't pollute the peer-intent read.
67
+ * `claim.state`, `claim.queue`, and `claim.reorder` are coordination reads and
68
+ * scheduler controls. They are useful for UI or operators, but normal AI tools
69
+ * should start with `update({ id, data, claim })` or a manual claim handle.
70
+ *
71
+ * `wrapWithMultiplayer` is optional. Use it when the whole model call is scoped
72
+ * to one entity before any tool is chosen; tool implementations stay exactly
73
+ * the same.
65
74
  */
66
75
  export { intentBroadcastMiddleware, type IntentTarget, type IntentBroadcastMiddlewareOptions, } from './intent-broadcast.js';
67
76
  export { coordinationContextMiddleware, type CoordinationContextMiddlewareOptions, } from './coordination-context.js';
@@ -1,67 +1,76 @@
1
1
  /**
2
- * `@abloatai/ablo/ai-sdk` multiplayer-with-AI as language model
3
- * middleware.
2
+ * Ablo + AI SDK tools.
4
3
  *
5
- * Two cross-cutting middlewares for any AI SDK consumer using
6
- * `streamText` / `generateText`:
4
+ * The base pattern is intentionally one object all the way down:
7
5
  *
8
- * - `intentBroadcastMiddleware` agent declares what it's about
9
- * to mutate via `intent_begin`, abandons the claim at stream end.
10
- * Peers see the broadcast in their presence stream's
11
- * `activeIntents` field and can defer / yield / surface
12
- * "agent is editing this entity right now."
13
- *
14
- * - `coordinationContextMiddleware` — reads peer intents from local
15
- * presence cache before the LLM call, injects a
16
- * `<multiplayer_context>` system note when peers are editing
17
- * the same entity. The AI gets coordination awareness without
18
- * extra round-trips.
19
- *
20
- * Compose them with the AI SDK's `wrapLanguageModel`:
6
+ * 1. AI SDK `inputSchema` describes what the model may send.
7
+ * 2. `ablo.<model>.update({ id, data, claim })` performs the write.
8
+ * 3. `claim.description` tells humans and other agents what the tool is doing.
21
9
  *
22
10
  * ```ts
23
- * import { wrapLanguageModel, streamText } from 'ai';
24
- * import {
25
- * intentBroadcastMiddleware,
26
- * coordinationContextMiddleware,
27
- * } from '@abloatai/ablo/ai-sdk';
11
+ * import { tool, streamText } from 'ai';
12
+ * import { z } from 'zod';
28
13
  *
29
- * const target = { entityType: 'SlideDeck', entityId: 'deck-abc' };
14
+ * const renameTask = tool({
15
+ * description: 'Rename a task.',
16
+ * inputSchema: z.object({
17
+ * id: z.string().describe('Task id'),
18
+ * title: z.string().describe('New task title'),
19
+ * description: z
20
+ * .string()
21
+ * .describe('Why this rename is being made'),
22
+ * }),
23
+ * execute: async ({ id, title, description }) => {
24
+ * await ablo.tasks.update({
25
+ * id,
26
+ * data: { title },
27
+ * wait: 'confirmed',
28
+ * claim: {
29
+ * field: 'title',
30
+ * action: 'renaming',
31
+ * description,
32
+ * },
33
+ * });
30
34
  *
31
- * const wrappedModel = wrapLanguageModel({
32
- * model: anthropic('claude-opus-4-7'),
33
- * middleware: [
34
- * coordinationContextMiddleware({ agent, target }),
35
- * intentBroadcastMiddleware({ agent, target }),
36
- * ],
35
+ * return { id, title };
36
+ * },
37
37
  * });
38
38
  *
39
- * // Consumer keeps full control over messages, tools, system prompt:
40
- * const result = streamText({
41
- * model: wrappedModel,
42
- * messages: [...],
43
- * tools: { ... },
44
- * system: '...',
39
+ * await streamText({
40
+ * model,
41
+ * messages,
42
+ * tools: { renameTask },
45
43
  * });
46
44
  * ```
47
45
  *
48
- * Or use the convenience composition for the common case:
46
+ * That is the common case. A claim passed directly to `update` is acquired,
47
+ * attached to the write, and released by the SDK.
49
48
  *
50
- * ```ts
51
- * import { wrapWithMultiplayer } from '@abloatai/ablo/ai-sdk';
49
+ * For multi-step tools, take one handle and release it when the tool is done:
52
50
  *
53
- * const wrappedModel = wrapWithMultiplayer({
54
- * model: anthropic('claude-opus-4-7'),
55
- * agent,
56
- * target: { entityType: 'SlideDeck', entityId: 'deck-abc' },
51
+ * ```ts
52
+ * const claim = await ablo.tasks.claim({
53
+ * id,
54
+ * action: 'rewriting',
55
+ * description: 'Rewriting the task brief before updating follow-up fields.',
56
+ * ttl: '2m',
57
57
  * });
58
+ *
59
+ * try {
60
+ * await ablo.tasks.update({ id, data: { title }, claim });
61
+ * await ablo.tasks.update({ id, data: { description: brief }, claim });
62
+ * } finally {
63
+ * await claim.release();
64
+ * }
58
65
  * ```
59
66
  *
60
- * Order matters: `coordinationContextMiddleware`'s `transformParams`
61
- * runs at param-transform time (before the model call), reading peer
62
- * intents *before* this agent's broadcast lands in its own cache.
63
- * `intentBroadcastMiddleware`'s `wrapStream` runs around the actual
64
- * call. Self-claim doesn't pollute the peer-intent read.
67
+ * `claim.state`, `claim.queue`, and `claim.reorder` are coordination reads and
68
+ * scheduler controls. They are useful for UI or operators, but normal AI tools
69
+ * should start with `update({ id, data, claim })` or a manual claim handle.
70
+ *
71
+ * `wrapWithMultiplayer` is optional. Use it when the whole model call is scoped
72
+ * to one entity before any tool is chosen; tool implementations stay exactly
73
+ * the same.
65
74
  */
66
75
  export { intentBroadcastMiddleware, } from './intent-broadcast.js';
67
76
  export { coordinationContextMiddleware, } from './coordination-context.js';
@@ -62,6 +62,11 @@ export interface IntentBroadcastMiddlewareOptions<R extends SchemaRecord = Schem
62
62
  * `'edit'`, `'read'`, `'review'`, `'generate'`. Default `'edit'`.
63
63
  */
64
64
  readonly action?: string;
65
+ /**
66
+ * Peer-visible explanation of the specific work this model call is about to
67
+ * perform. Surfaces to other agents through `ActiveIntent.description`.
68
+ */
69
+ readonly description?: string;
65
70
  }
66
71
  /**
67
72
  * Build the middleware. When `agent` or `target` is null, returns a
@@ -30,16 +30,23 @@
30
30
  export function intentBroadcastMiddleware(options) {
31
31
  const { agent, target } = options;
32
32
  const action = options.action ?? 'edit';
33
- const openClaim = () => agent && target
34
- ? agent.intents.claim({
33
+ const description = options.description;
34
+ const openClaim = () => {
35
+ if (!agent || !target)
36
+ return null;
37
+ return agent.intents.claim({
35
38
  type: target.entityType,
36
39
  id: target.entityId,
37
40
  path: target.path,
38
41
  range: target.range,
39
42
  field: target.field,
40
43
  meta: target.meta,
41
- }, { reason: action, ttl: target.estimatedMs ?? 60_000 })
42
- : null;
44
+ }, {
45
+ reason: action,
46
+ description,
47
+ ttl: target.estimatedMs ?? 60_000,
48
+ });
49
+ };
43
50
  return {
44
51
  specificationVersion: 'v3',
45
52
  // The AI SDK's middleware contract passes a no-arg `doStream` /
@@ -1,23 +1,21 @@
1
1
  /**
2
- * Convenience composition for the common case — wraps a language
3
- * model with both multiplayer middlewares (intent broadcast +
4
- * coordination context) in the right order.
2
+ * Optional model wrapper for entity-scoped turns.
5
3
  *
6
- * Consumers who want full control over middleware composition (add
7
- * caching / observability / their own custom middleware) should use
8
- * the factories directly: `intentBroadcastMiddleware`,
9
- * `coordinationContextMiddleware`. This helper is the one-liner for
10
- * the 90% case.
4
+ * Tool implementations do not change. Keep tools as normal AI SDK tools; use
5
+ * `ablo.<model>.update({ id, data, claim })` inside `execute`. This wrapper is
6
+ * only for the surrounding model call, when the UI already knows "this turn is
7
+ * about deck_abc" before the model chooses a tool.
11
8
  *
12
- * Stays explicit about its scope wraps the MODEL only. Consumer
13
- * keeps full control over their `streamText` / `generateText` call
14
- * (messages, tools, system prompt, provider options, onFinish, etc.).
9
+ * It declares one realtime claim while the model is generating and injects a
10
+ * short note if someone else is already working on the same target.
15
11
  *
16
12
  * ```ts
17
13
  * const wrapped = wrapWithMultiplayer({
18
14
  * model: anthropic('claude-opus-4-7'),
19
15
  * agent,
20
16
  * target: { entityType: 'SlideDeck', entityId: 'deck-abc' },
17
+ * action: 'renaming',
18
+ * description: 'Renaming the deck title to match the project brief.',
21
19
  * });
22
20
  *
23
21
  * const result = streamText({
@@ -46,6 +44,11 @@ export interface WrapWithMultiplayerOptions {
46
44
  * Convention: `'edit'`, `'read'`, `'review'`, `'generate'`.
47
45
  */
48
46
  readonly action?: string;
47
+ /**
48
+ * Peer-visible explanation of the specific work this model call is about to
49
+ * perform. Other agents receive it in their coordination context.
50
+ */
51
+ readonly description?: string;
49
52
  /**
50
53
  * Optional intentIds to exclude from the coordination-context
51
54
  * read — typically the caller's own claim if they're composing
@@ -1,23 +1,21 @@
1
1
  /**
2
- * Convenience composition for the common case — wraps a language
3
- * model with both multiplayer middlewares (intent broadcast +
4
- * coordination context) in the right order.
2
+ * Optional model wrapper for entity-scoped turns.
5
3
  *
6
- * Consumers who want full control over middleware composition (add
7
- * caching / observability / their own custom middleware) should use
8
- * the factories directly: `intentBroadcastMiddleware`,
9
- * `coordinationContextMiddleware`. This helper is the one-liner for
10
- * the 90% case.
4
+ * Tool implementations do not change. Keep tools as normal AI SDK tools; use
5
+ * `ablo.<model>.update({ id, data, claim })` inside `execute`. This wrapper is
6
+ * only for the surrounding model call, when the UI already knows "this turn is
7
+ * about deck_abc" before the model chooses a tool.
11
8
  *
12
- * Stays explicit about its scope wraps the MODEL only. Consumer
13
- * keeps full control over their `streamText` / `generateText` call
14
- * (messages, tools, system prompt, provider options, onFinish, etc.).
9
+ * It declares one realtime claim while the model is generating and injects a
10
+ * short note if someone else is already working on the same target.
15
11
  *
16
12
  * ```ts
17
13
  * const wrapped = wrapWithMultiplayer({
18
14
  * model: anthropic('claude-opus-4-7'),
19
15
  * agent,
20
16
  * target: { entityType: 'SlideDeck', entityId: 'deck-abc' },
17
+ * action: 'renaming',
18
+ * description: 'Renaming the deck title to match the project brief.',
21
19
  * });
22
20
  *
23
21
  * const result = streamText({
@@ -33,12 +31,12 @@ import { wrapLanguageModel } from 'ai';
33
31
  import { intentBroadcastMiddleware, } from './intent-broadcast.js';
34
32
  import { coordinationContextMiddleware } from './coordination-context.js';
35
33
  export function wrapWithMultiplayer(options) {
36
- const { model, agent, target, action, excludeIntentIds, extraMiddleware } = options;
34
+ const { model, agent, target, action, description, excludeIntentIds, extraMiddleware } = options;
37
35
  return wrapLanguageModel({
38
36
  model,
39
37
  middleware: [
40
38
  coordinationContextMiddleware({ agent, target, excludeIntentIds }),
41
- intentBroadcastMiddleware({ agent, target, action }),
39
+ intentBroadcastMiddleware({ agent, target, action, description }),
42
40
  ...(extraMiddleware ?? []),
43
41
  ],
44
42
  });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Single mutable source for the SDK's active bearer credential.
3
+ *
4
+ * Every transport should read from this object at request/connect time:
5
+ * bootstrap HTTP, lazy query HTTP, identity/probe HTTP, and WebSocket URL
6
+ * auth. Token refresh writes here once; consumers observe the new value
7
+ * through their getter without being manually patched one by one.
8
+ */
9
+ /**
10
+ * WebSocket subprotocols used to carry the bearer credential OUT of the URL.
11
+ *
12
+ * Browsers cannot set an `Authorization` header on a WebSocket, so the SDK
13
+ * offers the token as a `Sec-WebSocket-Protocol` value — `ablo.bearer.<token>` —
14
+ * alongside the real `ablo.sync.v1` protocol the server selects. This keeps the
15
+ * credential out of the query string, which ALB access logs, proxies, and
16
+ * browser history capture. The server reads the token from the subprotocol and
17
+ * echoes back ONLY `ablo.sync.v1`, never the token-bearing value. Shared with
18
+ * the sync-server so client and server can never drift on the wire format.
19
+ */
20
+ export declare const WS_BEARER_SUBPROTOCOL_PREFIX = "ablo.bearer.";
21
+ export declare const WS_SYNC_SUBPROTOCOL = "ablo.sync.v1";
22
+ export interface AuthCredentialSource {
23
+ getAuthToken(): string | null;
24
+ setAuthToken(token: string | null | undefined): void;
25
+ authorizationHeader(): string | undefined;
26
+ withAuthHeaders(headers?: Record<string, string>): Record<string, string>;
27
+ applyAuthQueryParam(params: URLSearchParams, paramName?: string): void;
28
+ }
29
+ export type AuthTokenGetter = () => string | null | undefined;
30
+ export declare function createAuthCredentialSource(initialToken?: string | null): AuthCredentialSource;
31
+ export declare function resolveAuthToken(getAuthToken?: AuthTokenGetter, fallbackToken?: string | null): string | undefined;
32
+ export declare function authorizationHeaderForToken(token: string | null | undefined): string | undefined;
33
+ export declare function withAuthHeaders(getAuthToken: AuthTokenGetter | undefined, headers?: Record<string, string>, fallbackToken?: string | null): Record<string, string>;
34
+ export declare function applyAuthToQueryParams(params: URLSearchParams, getAuthToken: AuthTokenGetter | undefined, paramName?: string, fallbackToken?: string | null): void;