@abloatai/ablo 0.7.0 → 0.9.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 (181) hide show
  1. package/CHANGELOG.md +72 -1
  2. package/README.md +80 -66
  3. package/dist/BaseSyncedStore.d.ts +73 -0
  4. package/dist/BaseSyncedStore.js +179 -5
  5. package/dist/Model.d.ts +42 -0
  6. package/dist/Model.js +103 -44
  7. package/dist/SyncEngineContext.d.ts +2 -1
  8. package/dist/SyncEngineContext.js +5 -3
  9. package/dist/agent/session.js +6 -5
  10. package/dist/ai-sdk/coordination-context.js +4 -0
  11. package/dist/ai-sdk/index.d.ts +56 -47
  12. package/dist/ai-sdk/index.js +56 -47
  13. package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
  14. package/dist/ai-sdk/intent-broadcast.js +11 -4
  15. package/dist/ai-sdk/wrap.d.ts +14 -11
  16. package/dist/ai-sdk/wrap.js +11 -13
  17. package/dist/auth/credentialSource.d.ts +34 -0
  18. package/dist/auth/credentialSource.js +63 -0
  19. package/dist/auth/index.d.ts +2 -22
  20. package/dist/auth/index.js +26 -36
  21. package/dist/auth/schemas.d.ts +35 -0
  22. package/dist/auth/schemas.js +53 -0
  23. package/dist/client/Ablo.d.ts +259 -33
  24. package/dist/client/Ablo.js +276 -73
  25. package/dist/client/ApiClient.d.ts +52 -4
  26. package/dist/client/ApiClient.js +236 -66
  27. package/dist/client/auth.d.ts +21 -2
  28. package/dist/client/auth.js +77 -5
  29. package/dist/client/createInternalComponents.d.ts +2 -0
  30. package/dist/client/createInternalComponents.js +8 -1
  31. package/dist/client/createModelProxy.d.ts +187 -79
  32. package/dist/client/createModelProxy.js +203 -68
  33. package/dist/client/httpClient.d.ts +71 -0
  34. package/dist/client/httpClient.js +69 -0
  35. package/dist/client/identity.d.ts +2 -6
  36. package/dist/client/identity.js +63 -11
  37. package/dist/client/index.d.ts +1 -0
  38. package/dist/client/index.js +1 -0
  39. package/dist/client/registerDataSource.d.ts +19 -0
  40. package/dist/client/registerDataSource.js +59 -0
  41. package/dist/client/validateAbloOptions.d.ts +2 -1
  42. package/dist/client/validateAbloOptions.js +8 -7
  43. package/dist/core/DatabaseManager.js +30 -2
  44. package/dist/core/openIDBWithTimeout.d.ts +36 -0
  45. package/dist/core/openIDBWithTimeout.js +88 -1
  46. package/dist/errorCodes.d.ts +92 -1
  47. package/dist/errorCodes.js +139 -7
  48. package/dist/errors.d.ts +54 -3
  49. package/dist/errors.js +192 -44
  50. package/dist/index.d.ts +23 -10
  51. package/dist/index.js +21 -8
  52. package/dist/keys/index.d.ts +76 -0
  53. package/dist/keys/index.js +171 -0
  54. package/dist/mutators/UndoManager.d.ts +86 -50
  55. package/dist/mutators/UndoManager.js +129 -22
  56. package/dist/mutators/inverseOp.d.ts +129 -0
  57. package/dist/mutators/inverseOp.js +74 -0
  58. package/dist/mutators/readerActions.d.ts +1 -1
  59. package/dist/mutators/undoApply.d.ts +42 -0
  60. package/dist/mutators/undoApply.js +143 -0
  61. package/dist/query/client.d.ts +10 -9
  62. package/dist/query/client.js +22 -14
  63. package/dist/react/AbloProvider.d.ts +23 -101
  64. package/dist/react/AbloProvider.js +61 -103
  65. package/dist/react/ClientSideSuspense.d.ts +1 -1
  66. package/dist/react/DefaultFallback.d.ts +1 -1
  67. package/dist/react/SyncGroupProvider.d.ts +1 -1
  68. package/dist/react/index.d.ts +3 -2
  69. package/dist/react/index.js +3 -2
  70. package/dist/react/useAblo.d.ts +4 -4
  71. package/dist/react/useAblo.js +10 -5
  72. package/dist/react/useCurrentUserId.d.ts +1 -1
  73. package/dist/react/useCurrentUserId.js +1 -1
  74. package/dist/react/useMutators.js +19 -12
  75. package/dist/react/useReactive.js +16 -3
  76. package/dist/schema/ddl.d.ts +26 -3
  77. package/dist/schema/ddl.js +152 -4
  78. package/dist/schema/index.d.ts +4 -0
  79. package/dist/schema/index.js +12 -0
  80. package/dist/schema/model.d.ts +11 -0
  81. package/dist/schema/model.js +2 -0
  82. package/dist/schema/openapi.d.ts +28 -0
  83. package/dist/schema/openapi.js +118 -0
  84. package/dist/schema/plane.d.ts +23 -0
  85. package/dist/schema/plane.js +19 -0
  86. package/dist/schema/relation.d.ts +20 -0
  87. package/dist/schema/serialize.d.ts +7 -3
  88. package/dist/schema/serialize.js +6 -2
  89. package/dist/schema/sync-delta-row.d.ts +157 -0
  90. package/dist/schema/sync-delta-row.js +102 -0
  91. package/dist/schema/sync-delta-wire.d.ts +180 -0
  92. package/dist/schema/sync-delta-wire.js +102 -0
  93. package/dist/server/adapter.d.ts +156 -0
  94. package/dist/server/adapter.js +19 -0
  95. package/dist/server/commit.d.ts +82 -0
  96. package/dist/server/commit.js +1 -0
  97. package/dist/server/index.d.ts +14 -0
  98. package/dist/server/index.js +1 -0
  99. package/dist/server/next.d.ts +51 -0
  100. package/dist/server/next.js +47 -0
  101. package/dist/server/read-config.d.ts +60 -0
  102. package/dist/server/read-config.js +8 -0
  103. package/dist/server/storage-mode.d.ts +17 -0
  104. package/dist/server/storage-mode.js +12 -0
  105. package/dist/source/adapter.d.ts +59 -0
  106. package/dist/source/adapter.js +19 -0
  107. package/dist/source/adapters/drizzle.d.ts +34 -0
  108. package/dist/source/adapters/drizzle.js +147 -0
  109. package/dist/source/adapters/memory.d.ts +12 -0
  110. package/dist/source/adapters/memory.js +114 -0
  111. package/dist/source/adapters/prisma.d.ts +57 -0
  112. package/dist/source/adapters/prisma.js +199 -0
  113. package/dist/source/conformance.d.ts +32 -0
  114. package/dist/source/conformance.js +134 -0
  115. package/dist/source/contract.d.ts +143 -0
  116. package/dist/source/contract.js +98 -0
  117. package/dist/source/index.d.ts +61 -10
  118. package/dist/source/index.js +98 -0
  119. package/dist/source/next.d.ts +33 -0
  120. package/dist/source/next.js +26 -0
  121. package/dist/sync/BootstrapHelper.d.ts +10 -0
  122. package/dist/sync/BootstrapHelper.js +56 -42
  123. package/dist/sync/ConnectionManager.d.ts +57 -1
  124. package/dist/sync/ConnectionManager.js +186 -11
  125. package/dist/sync/HydrationCoordinator.d.ts +93 -17
  126. package/dist/sync/HydrationCoordinator.js +241 -41
  127. package/dist/sync/NetworkProbe.d.ts +60 -18
  128. package/dist/sync/NetworkProbe.js +121 -23
  129. package/dist/sync/SyncWebSocket.d.ts +45 -70
  130. package/dist/sync/SyncWebSocket.js +113 -89
  131. package/dist/sync/createIntentStream.js +10 -1
  132. package/dist/sync/participants.js +5 -2
  133. package/dist/transactions/TransactionQueue.js +13 -1
  134. package/dist/types/streams.d.ts +9 -0
  135. package/dist/utils/mobx-setup.js +1 -0
  136. package/dist/webhooks/events.d.ts +38 -0
  137. package/dist/webhooks/events.js +40 -0
  138. package/dist/webhooks/index.d.ts +10 -0
  139. package/dist/webhooks/index.js +10 -0
  140. package/dist/wire/errorEnvelope.d.ts +34 -0
  141. package/dist/wire/errorEnvelope.js +86 -0
  142. package/dist/wire/frames.d.ts +119 -0
  143. package/dist/wire/frames.js +1 -0
  144. package/dist/wire/index.d.ts +24 -0
  145. package/dist/wire/index.js +21 -0
  146. package/dist/wire/listEnvelope.d.ts +45 -0
  147. package/dist/wire/listEnvelope.js +17 -0
  148. package/docs/api-keys.md +5 -5
  149. package/docs/api.md +125 -65
  150. package/docs/audit.md +16 -9
  151. package/docs/cli.md +57 -47
  152. package/docs/client-behavior.md +54 -40
  153. package/docs/coordination.md +66 -80
  154. package/docs/data-sources.md +56 -34
  155. package/docs/examples/agent-human.md +74 -28
  156. package/docs/examples/ai-sdk-tool.md +29 -22
  157. package/docs/examples/existing-python-backend.md +41 -26
  158. package/docs/examples/nextjs.md +32 -17
  159. package/docs/examples/scoped-agent.md +43 -28
  160. package/docs/examples/server-agent.md +40 -15
  161. package/docs/guarantees.md +38 -27
  162. package/docs/identity.md +65 -59
  163. package/docs/index.md +30 -19
  164. package/docs/integration-guide.md +78 -78
  165. package/docs/interaction-model.md +43 -35
  166. package/docs/mcp/claude-code.md +11 -19
  167. package/docs/mcp/cursor.md +7 -25
  168. package/docs/mcp/windsurf.md +7 -20
  169. package/docs/mcp.md +103 -26
  170. package/docs/quickstart.md +63 -61
  171. package/docs/react.md +24 -16
  172. package/docs/roadmap.md +13 -13
  173. package/docs/schema-contract.md +111 -0
  174. package/docs/the-loop.md +21 -0
  175. package/examples/README.md +8 -4
  176. package/examples/data-source/README.md +10 -7
  177. package/examples/data-source/customer-server.ts +27 -25
  178. package/examples/data-source/run.ts +4 -3
  179. package/examples/quickstart.ts +1 -1
  180. package/llms.txt +55 -21
  181. package/package.json +48 -3
@@ -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;
@@ -0,0 +1,63 @@
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 const WS_BEARER_SUBPROTOCOL_PREFIX = 'ablo.bearer.';
21
+ export const WS_SYNC_SUBPROTOCOL = 'ablo.sync.v1';
22
+ export function createAuthCredentialSource(initialToken) {
23
+ let authToken = normalizeToken(initialToken);
24
+ return {
25
+ getAuthToken: () => authToken,
26
+ setAuthToken(token) {
27
+ authToken = normalizeToken(token);
28
+ },
29
+ authorizationHeader() {
30
+ return authorizationHeaderForToken(authToken);
31
+ },
32
+ withAuthHeaders(headers = {}) {
33
+ const authorization = authorizationHeaderForToken(authToken);
34
+ return authorization ? { ...headers, Authorization: authorization } : { ...headers };
35
+ },
36
+ applyAuthQueryParam(params, paramName = 'authorization') {
37
+ applyAuthToQueryParams(params, () => authToken, paramName);
38
+ },
39
+ };
40
+ }
41
+ export function resolveAuthToken(getAuthToken, fallbackToken) {
42
+ return normalizeToken(getAuthToken?.() ?? fallbackToken) ?? undefined;
43
+ }
44
+ export function authorizationHeaderForToken(token) {
45
+ const normalized = normalizeToken(token);
46
+ return normalized ? `Bearer ${normalized}` : undefined;
47
+ }
48
+ export function withAuthHeaders(getAuthToken, headers = {}, fallbackToken) {
49
+ const authorization = authorizationHeaderForToken(resolveAuthToken(getAuthToken, fallbackToken));
50
+ return authorization ? { ...headers, Authorization: authorization } : { ...headers };
51
+ }
52
+ export function applyAuthToQueryParams(params, getAuthToken, paramName = 'authorization', fallbackToken) {
53
+ const authorization = authorizationHeaderForToken(resolveAuthToken(getAuthToken, fallbackToken));
54
+ if (authorization) {
55
+ params.set(paramName, authorization);
56
+ }
57
+ }
58
+ function normalizeToken(token) {
59
+ if (!token)
60
+ return null;
61
+ const trimmed = token.trim();
62
+ return trimmed.length > 0 ? trimmed : null;
63
+ }
@@ -11,21 +11,8 @@
11
11
  * SDKs hide their internal auth-handshake — the apiKey is the only
12
12
  * credential the consumer touches.
13
13
  */
14
- /** Server response shape matches Phase 1A + 1B wire output. */
15
- export interface CapabilityExchangeResponse {
16
- readonly capabilityId: string;
17
- readonly token: string;
18
- readonly expiresAt: string;
19
- readonly organizationId: string;
20
- readonly scope: {
21
- readonly organizationId: string;
22
- readonly syncGroups: readonly string[];
23
- readonly operations: readonly string[];
24
- readonly participantKind: 'user' | 'agent' | 'system';
25
- readonly participantId: string;
26
- };
27
- readonly userMeta: Record<string, unknown>;
28
- }
14
+ import { type CapabilityExchangeResponse, type IdentityResolveResponse } from './schemas.js';
15
+ export type { CapabilityExchangeResponse, IdentityResolveResponse } from './schemas.js';
29
16
  export interface ExchangeApiKeyRequest {
30
17
  readonly apiKey: string;
31
18
  readonly baseUrl: string;
@@ -41,13 +28,6 @@ export interface ExchangeApiKeyRequest {
41
28
  readonly timeoutMs?: number;
42
29
  }
43
30
  export declare function exchangeApiKey(options: ExchangeApiKeyRequest): Promise<CapabilityExchangeResponse>;
44
- export interface IdentityResolveResponse {
45
- readonly participantKind: 'user' | 'agent' | 'system';
46
- readonly participantId: string;
47
- readonly accountScope: string;
48
- readonly syncGroups: readonly string[];
49
- readonly userMeta: Record<string, unknown>;
50
- }
51
31
  export interface ResolveIdentityRequest {
52
32
  readonly baseUrl: string;
53
33
  readonly authToken?: string;
@@ -11,7 +11,8 @@
11
11
  * SDKs hide their internal auth-handshake — the apiKey is the only
12
12
  * credential the consumer touches.
13
13
  */
14
- import { AbloAuthenticationError } from '../errors.js';
14
+ import { parseCapabilityExchangeResponse, parseIdentityResolveResponse, } from './schemas.js';
15
+ import { AbloAuthenticationError, hasWireCode, translateHttpError } from '../errors.js';
15
16
  export async function exchangeApiKey(options) {
16
17
  if (!options.apiKey) {
17
18
  throw new AbloAuthenticationError('apiKey is required for capability exchange', { code: 'apikey_missing' });
@@ -59,33 +60,18 @@ export async function exchangeApiKey(options) {
59
60
  catch {
60
61
  // ignore — server returned non-JSON error
61
62
  }
62
- const errBody = body;
63
- throw new AbloAuthenticationError(`apiKey exchange rejected (${response.status}): ${errBody?.reason ?? response.statusText}`, {
64
- code: errBody?.error ?? 'exchange_failed',
65
- httpStatus: response.status,
66
- });
67
- }
68
- const raw = (await response.json());
69
- if (!isCapabilityExchangeResponse(raw)) {
70
- throw new AbloAuthenticationError('apiKey exchange response was malformed — missing required fields', { code: 'exchange_malformed_response' });
71
- }
72
- return raw;
73
- }
74
- function isCapabilityExchangeResponse(raw) {
75
- if (!raw || typeof raw !== 'object')
76
- return false;
77
- const o = raw;
78
- if (typeof o.token !== 'string')
79
- return false;
80
- if (typeof o.expiresAt !== 'string')
81
- return false;
82
- if (typeof o.organizationId !== 'string')
83
- return false;
84
- if (typeof o.scope !== 'object' || o.scope === null)
85
- return false;
86
- if (typeof o.userMeta !== 'object' || o.userMeta === null)
87
- return false;
88
- return true;
63
+ // Route through the canonical wire-error translator so the server's
64
+ // envelope (`code` + `message` + `doc_url`) propagates verbatim and maps to
65
+ // the right AbloError subclass — instead of the legacy `error`/`reason`
66
+ // shape this used to read (which the server no longer emits, collapsing
67
+ // every failure to a generic code with an empty message). Fall back to
68
+ // `exchange_failed` only when the body carried no recognizable code.
69
+ const requestId = response.headers.get('x-request-id') ?? undefined;
70
+ throw hasWireCode(body)
71
+ ? translateHttpError(response.status, body, requestId)
72
+ : new AbloAuthenticationError(`apiKey exchange rejected (${response.status})`, { code: 'exchange_failed', httpStatus: response.status });
73
+ }
74
+ return parseCapabilityExchangeResponse(await response.json());
89
75
  }
90
76
  /**
91
77
  * Resolve the caller's Ablo identity from the authenticated request
@@ -112,7 +98,6 @@ export async function resolveIdentity(options) {
112
98
  response = await fetcher(url, {
113
99
  method: 'GET',
114
100
  headers,
115
- credentials: 'include',
116
101
  signal: controller.signal,
117
102
  });
118
103
  }
@@ -130,13 +115,18 @@ export async function resolveIdentity(options) {
130
115
  catch {
131
116
  // ignore non-JSON auth errors
132
117
  }
133
- const errBody = body;
134
- throw new AbloAuthenticationError(`identity resolve rejected (${response.status}): ${errBody?.reason ?? response.statusText}`, {
135
- code: errBody?.error ?? 'identity_resolve_failed',
136
- httpStatus: response.status,
137
- });
138
- }
139
- return (await response.json());
118
+ // Canonical envelope translation (see `exchangeApiKey` above). This is what
119
+ // surfaces the sync-server's precise auth diagnosis e.g.
120
+ // `jwt_issuer_untrusted` with its full message — to the SDK consumer,
121
+ // instead of collapsing every 401 to `identity_resolve_failed` with an
122
+ // empty reason because the old parser looked for `error`/`reason` keys the
123
+ // server doesn't emit.
124
+ const requestId = response.headers.get('x-request-id') ?? undefined;
125
+ throw hasWireCode(body)
126
+ ? translateHttpError(response.status, body, requestId)
127
+ : new AbloAuthenticationError(`identity resolve rejected (${response.status})`, { code: 'identity_resolve_failed', httpStatus: response.status });
128
+ }
129
+ return parseIdentityResolveResponse(await response.json());
140
130
  }
141
131
  const DEFAULT_BUFFER_FLOOR_MS = 60_000;
142
132
  const DEFAULT_BUFFER_RATIO = 0.1;
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ export declare const AuthTokenSchema: z.ZodString;
3
+ export declare const CapabilityExchangeResponseSchema: z.ZodObject<{
4
+ capabilityId: z.ZodString;
5
+ token: z.ZodString;
6
+ expiresAt: z.ZodString;
7
+ organizationId: z.ZodString;
8
+ scope: z.ZodObject<{
9
+ organizationId: z.ZodString;
10
+ syncGroups: z.ZodArray<z.ZodString>;
11
+ operations: z.ZodArray<z.ZodString>;
12
+ participantKind: z.ZodEnum<{
13
+ user: "user";
14
+ agent: "agent";
15
+ system: "system";
16
+ }>;
17
+ participantId: z.ZodString;
18
+ }, z.core.$loose>;
19
+ userMeta: z.ZodRecord<z.ZodString, z.ZodUnknown>;
20
+ }, z.core.$loose>;
21
+ export type CapabilityExchangeResponse = z.infer<typeof CapabilityExchangeResponseSchema>;
22
+ export declare const IdentityResolveResponseSchema: z.ZodObject<{
23
+ participantKind: z.ZodEnum<{
24
+ user: "user";
25
+ agent: "agent";
26
+ system: "system";
27
+ }>;
28
+ participantId: z.ZodString;
29
+ accountScope: z.ZodString;
30
+ syncGroups: z.ZodArray<z.ZodString>;
31
+ userMeta: z.ZodRecord<z.ZodString, z.ZodUnknown>;
32
+ }, z.core.$loose>;
33
+ export type IdentityResolveResponse = z.infer<typeof IdentityResolveResponseSchema>;
34
+ export declare function parseCapabilityExchangeResponse(raw: unknown): CapabilityExchangeResponse;
35
+ export declare function parseIdentityResolveResponse(raw: unknown): IdentityResolveResponse;