@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.
- package/CHANGELOG.md +46 -1
- package/README.md +33 -28
- package/dist/BaseSyncedStore.d.ts +83 -0
- package/dist/BaseSyncedStore.js +194 -2
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/agent/session.js +3 -3
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +4 -42
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +160 -42
- package/dist/client/Ablo.js +145 -75
- package/dist/client/ApiClient.d.ts +20 -4
- package/dist/client/ApiClient.js +166 -28
- package/dist/client/auth.d.ts +14 -5
- package/dist/client/auth.js +60 -7
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +130 -66
- package/dist/client/createModelProxy.js +152 -49
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +49 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +3 -3
- package/dist/client/registerDataSource.js +11 -9
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +70 -1
- package/dist/errorCodes.js +108 -9
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +72 -22
- package/dist/index.d.ts +17 -8
- package/dist/index.js +15 -6
- package/dist/keys/index.d.ts +16 -1
- package/dist/keys/index.js +26 -6
- package/dist/mutators/UndoManager.d.ts +158 -50
- package/dist/mutators/UndoManager.js +345 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +3 -6
- package/dist/react/AbloProvider.d.ts +23 -126
- package/dist/react/AbloProvider.js +62 -199
- package/dist/react/context.d.ts +31 -0
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/schema/ddl.d.ts +34 -3
- package/dist/schema/ddl.js +162 -4
- package/dist/schema/index.d.ts +5 -1
- package/dist/schema/index.js +13 -1
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +4 -0
- package/dist/schema/serialize.js +4 -0
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +65 -0
- package/dist/source/adapter.js +20 -0
- package/dist/source/adapters/drizzle.d.ts +43 -0
- package/dist/source/adapters/drizzle.js +185 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +176 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +144 -0
- package/dist/source/contract.js +99 -0
- package/dist/source/index.d.ts +62 -10
- package/dist/source/index.js +99 -0
- package/dist/source/migrations.d.ts +14 -0
- package/dist/source/migrations.js +39 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +10 -15
- package/dist/sync/ConnectionManager.d.ts +55 -1
- package/dist/sync/ConnectionManager.js +155 -16
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +238 -39
- package/dist/sync/NetworkProbe.d.ts +58 -24
- package/dist/sync/NetworkProbe.js +118 -42
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +70 -36
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api.md +47 -44
- package/docs/cli.md +44 -44
- package/docs/client-behavior.md +30 -30
- package/docs/coordination.md +33 -36
- package/docs/data-sources.md +35 -15
- package/docs/examples/agent-human.md +45 -43
- package/docs/examples/ai-sdk-tool.md +20 -16
- package/docs/examples/existing-python-backend.md +16 -12
- package/docs/examples/nextjs.md +14 -12
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/examples/server-agent.md +24 -21
- package/docs/guarantees.md +15 -13
- package/docs/index.md +2 -2
- package/docs/integration-guide.md +30 -30
- package/docs/interaction-model.md +19 -23
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +2 -2
- package/docs/mcp.md +6 -6
- package/docs/quickstart.md +41 -31
- package/docs/react.md +13 -9
- package/docs/schema-contract.md +12 -10
- package/docs/the-loop.md +21 -0
- package/examples/data-source/README.md +4 -5
- package/examples/data-source/customer-server.ts +27 -25
- package/llms.txt +28 -5
- 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
|
-
//
|
|
374
|
-
|
|
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
|
package/dist/agent/session.js
CHANGED
|
@@ -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
|
|
73
|
-
// default `wss://api.
|
|
74
|
-
//
|
|
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>`);
|
package/dist/ai-sdk/index.d.ts
CHANGED
|
@@ -1,67 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* middleware.
|
|
2
|
+
* Ablo + AI SDK tools.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* `streamText` / `generateText`:
|
|
4
|
+
* The base pattern is intentionally one object all the way down:
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* middleware: [
|
|
34
|
-
* coordinationContextMiddleware({ agent, target }),
|
|
35
|
-
* intentBroadcastMiddleware({ agent, target }),
|
|
36
|
-
* ],
|
|
35
|
+
* return { id, title };
|
|
36
|
+
* },
|
|
37
37
|
* });
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* tools: { ... },
|
|
44
|
-
* system: '...',
|
|
39
|
+
* await streamText({
|
|
40
|
+
* model,
|
|
41
|
+
* messages,
|
|
42
|
+
* tools: { renameTask },
|
|
45
43
|
* });
|
|
46
44
|
* ```
|
|
47
45
|
*
|
|
48
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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';
|
package/dist/ai-sdk/index.js
CHANGED
|
@@ -1,67 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* middleware.
|
|
2
|
+
* Ablo + AI SDK tools.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* `streamText` / `generateText`:
|
|
4
|
+
* The base pattern is intentionally one object all the way down:
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* middleware: [
|
|
34
|
-
* coordinationContextMiddleware({ agent, target }),
|
|
35
|
-
* intentBroadcastMiddleware({ agent, target }),
|
|
36
|
-
* ],
|
|
35
|
+
* return { id, title };
|
|
36
|
+
* },
|
|
37
37
|
* });
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* tools: { ... },
|
|
44
|
-
* system: '...',
|
|
39
|
+
* await streamText({
|
|
40
|
+
* model,
|
|
41
|
+
* messages,
|
|
42
|
+
* tools: { renameTask },
|
|
45
43
|
* });
|
|
46
44
|
* ```
|
|
47
45
|
*
|
|
48
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
|
34
|
-
|
|
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
|
-
}, {
|
|
42
|
-
|
|
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` /
|
package/dist/ai-sdk/wrap.d.ts
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
package/dist/ai-sdk/wrap.js
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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;
|