@abloatai/ablo 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +2 -1
- package/dist/BaseSyncedStore.d.ts +75 -0
- package/dist/BaseSyncedStore.js +193 -8
- package/dist/Database.d.ts +10 -2
- package/dist/Database.js +15 -1
- package/dist/SyncClient.d.ts +12 -1
- package/dist/SyncClient.js +110 -26
- package/dist/agent/Agent.d.ts +9 -9
- package/dist/agent/Agent.js +16 -16
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
- package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
- package/dist/ai-sdk/coordination-context.d.ts +9 -9
- package/dist/ai-sdk/coordination-context.js +8 -8
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.js +1 -1
- package/dist/ai-sdk/wrap.d.ts +4 -4
- package/dist/ai-sdk/wrap.js +4 -4
- package/dist/api/index.d.ts +2 -2
- package/dist/cli.cjs +254 -48
- package/dist/client/Ablo.d.ts +30 -63
- package/dist/client/Ablo.js +108 -102
- package/dist/client/ApiClient.d.ts +6 -5
- package/dist/client/ApiClient.js +83 -62
- package/dist/client/createModelProxy.d.ts +16 -54
- package/dist/client/createModelProxy.js +44 -16
- package/dist/client/httpClient.d.ts +2 -0
- package/dist/client/httpClient.js +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/writeOptionsSchema.d.ts +4 -4
- package/dist/client/writeOptionsSchema.js +4 -4
- package/dist/coordination/schema.d.ts +249 -38
- package/dist/coordination/schema.js +172 -39
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +4 -4
- package/dist/errorCodes.d.ts +9 -9
- package/dist/errorCodes.js +15 -15
- package/dist/errors.d.ts +51 -2
- package/dist/errors.js +94 -5
- package/dist/interfaces/index.d.ts +8 -4
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/types.d.ts +13 -13
- package/dist/policy/types.js +8 -8
- package/dist/react/AbloProvider.d.ts +51 -4
- package/dist/react/AbloProvider.js +95 -11
- package/dist/react/context.d.ts +26 -9
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +4 -4
- package/dist/react/useAblo.js +5 -5
- package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
- package/dist/react/useClaim.js +42 -0
- package/dist/schema/index.js +1 -1
- package/dist/schema/sugar.d.ts +3 -3
- package/dist/schema/sugar.js +3 -3
- package/dist/schema/sync-delta-wire.d.ts +8 -8
- package/dist/server/commit.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.d.ts +162 -0
- package/dist/sync/AreaOfInterestManager.js +233 -0
- package/dist/sync/BootstrapHelper.d.ts +9 -1
- package/dist/sync/BootstrapHelper.js +15 -5
- package/dist/sync/NetworkProbe.d.ts +1 -1
- package/dist/sync/NetworkProbe.js +1 -1
- package/dist/sync/SyncWebSocket.d.ts +59 -25
- package/dist/sync/SyncWebSocket.js +123 -26
- package/dist/sync/awaitClaimGrant.d.ts +40 -0
- package/dist/sync/awaitClaimGrant.js +86 -0
- package/dist/sync/createClaimStream.d.ts +34 -0
- package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
- package/dist/sync/createPresenceStream.js +3 -2
- package/dist/sync/participants.d.ts +10 -10
- package/dist/sync/participants.js +17 -10
- package/dist/sync/schemas.d.ts +8 -8
- package/dist/transactions/TransactionQueue.d.ts +12 -0
- package/dist/transactions/TransactionQueue.js +126 -8
- package/dist/types/global.d.ts +10 -10
- package/dist/types/global.js +3 -3
- package/dist/types/index.d.ts +9 -7
- package/dist/types/index.js +2 -2
- package/dist/types/streams.d.ts +114 -98
- package/dist/types/streams.js +1 -1
- package/dist/utils/asyncIterator.d.ts +1 -1
- package/dist/utils/asyncIterator.js +1 -1
- package/dist/wire/frames.d.ts +2 -2
- package/package.json +3 -2
- package/dist/react/useIntent.js +0 -42
- package/dist/sync/awaitIntentGrant.d.ts +0 -40
- package/dist/sync/awaitIntentGrant.js +0 -62
- package/dist/sync/createIntentStream.d.ts +0 -34
package/dist/SyncClient.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Send mutations to server via API client
|
|
8
8
|
* - Handle conflict resolution for local changes
|
|
9
9
|
*/
|
|
10
|
+
import { runInAction } from 'mobx';
|
|
10
11
|
import { ModelScope } from './ObjectPool.js';
|
|
11
12
|
// ModelRegistry instance accessed via this.objectPool.registry
|
|
12
13
|
import { LoadStrategy } from './types/index.js';
|
|
@@ -17,6 +18,31 @@ import { NetworkMonitor } from './NetworkMonitor.js';
|
|
|
17
18
|
import { TransactionQueue } from './transactions/TransactionQueue.js';
|
|
18
19
|
import { OptimisticEchoTracker, } from './transactions/OptimisticEchoTracker.js';
|
|
19
20
|
import { SyncPosition } from './sync/syncPosition.js';
|
|
21
|
+
/**
|
|
22
|
+
* Is the raw snapshot record strictly newer than the pooled model? Compares
|
|
23
|
+
* server-stamped `updatedAt` (the engine has no numeric row version; the delta
|
|
24
|
+
* pipeline is arrival-ordered last-write-wins). An undefined incoming time is
|
|
25
|
+
* treated as NOT newer (don't clobber a known row); an undefined existing time
|
|
26
|
+
* means the pool row is unversioned, so the incoming record wins. Used by the
|
|
27
|
+
* scoped hydrate-on-enter apply to drop snapshot rows a live delta already
|
|
28
|
+
* advanced past the snapshot watermark.
|
|
29
|
+
*/
|
|
30
|
+
function rawRecordIsNewer(data, existing) {
|
|
31
|
+
const raw = data.updatedAt;
|
|
32
|
+
const inMs = raw instanceof Date
|
|
33
|
+
? raw.getTime()
|
|
34
|
+
: typeof raw === 'string'
|
|
35
|
+
? (Number.isNaN(Date.parse(raw)) ? undefined : Date.parse(raw))
|
|
36
|
+
: typeof raw === 'number'
|
|
37
|
+
? raw
|
|
38
|
+
: undefined;
|
|
39
|
+
const exMs = existing.updatedAt instanceof Date ? existing.updatedAt.getTime() : undefined;
|
|
40
|
+
if (inMs === undefined)
|
|
41
|
+
return false;
|
|
42
|
+
if (exMs === undefined)
|
|
43
|
+
return true;
|
|
44
|
+
return inMs > exMs;
|
|
45
|
+
}
|
|
20
46
|
export class SyncClient extends EventEmitter {
|
|
21
47
|
objectPool;
|
|
22
48
|
database;
|
|
@@ -292,7 +318,10 @@ export class SyncClient extends EventEmitter {
|
|
|
292
318
|
// server processes it, we drain on rollback too so a stale id
|
|
293
319
|
// doesn't permanently silence a foreign delta sharing the same id
|
|
294
320
|
// (vanishingly unlikely for UUIDs, but cheap insurance).
|
|
295
|
-
this.transactionQueue.on('transaction:created', (tx) =>
|
|
321
|
+
this.transactionQueue.on('transaction:created', (tx) => {
|
|
322
|
+
if (!tx.localOnly)
|
|
323
|
+
this.echoTracker.markPending(tx.id);
|
|
324
|
+
});
|
|
296
325
|
this.transactionQueue.on('optimistic:rollback', (event) => {
|
|
297
326
|
this.echoTracker.drainOnRollback(event.transaction.id);
|
|
298
327
|
});
|
|
@@ -654,6 +683,20 @@ export class SyncClient extends EventEmitter {
|
|
|
654
683
|
* @see src/sync-engine/types/TrackableModel.ts for change capture pattern
|
|
655
684
|
*/
|
|
656
685
|
mutate(type, model, poolAction, writeOptions) {
|
|
686
|
+
// No-op UPDATE guard (O(1)). An update with no dirty fields would travel
|
|
687
|
+
// to the server, get dropped by `coalesceOperations` Rule 4 (empty input),
|
|
688
|
+
// and — if it was the only op — come back as `lastSyncId: 0`. That trips
|
|
689
|
+
// `captureCommitZeroSyncId` (false-positive Sentry anomaly) AND parks the
|
|
690
|
+
// tx in `awaiting_delta` for a 30s reconciliation timeout on a write that
|
|
691
|
+
// changed nothing. `Model.hasChanges` reads `modifiedProperties.size`, so
|
|
692
|
+
// this costs O(1) with no allocation (vs. O(N) materializing getChanges()).
|
|
693
|
+
//
|
|
694
|
+
// Strict `=== false` is deliberate: `rowAsModel` only casts, so a non-Model
|
|
695
|
+
// object can reach here with `hasChanges === undefined`. `undefined === false`
|
|
696
|
+
// is false → we fall through to the normal path rather than risk dropping a
|
|
697
|
+
// real write. Only a genuine Model with an empty dirty-set is skipped.
|
|
698
|
+
if (type === 'update' && model.hasChanges === false)
|
|
699
|
+
return;
|
|
657
700
|
// CRITICAL FIX: Capture changes BEFORE pool action
|
|
658
701
|
// Pool operations (especially upsert) can clear _local changes
|
|
659
702
|
// By capturing first, we ensure changes are never lost
|
|
@@ -720,6 +763,13 @@ export class SyncClient extends EventEmitter {
|
|
|
720
763
|
const capturedChanges = changes && Object.keys(changes).length > 0
|
|
721
764
|
? Object.freeze({ ...changes })
|
|
722
765
|
: this.captureModelChanges(model);
|
|
766
|
+
// No-op UPDATE guard: neither an explicit change set nor model dirty-fields.
|
|
767
|
+
// `captureModelChanges` already returns undefined for an empty dirty-set, so
|
|
768
|
+
// an undefined here means there is genuinely nothing to send — skip rather
|
|
769
|
+
// than emit an empty-input update that the server coalesces to lastSyncId 0
|
|
770
|
+
// (see the same guard in `mutate`).
|
|
771
|
+
if (capturedChanges === undefined)
|
|
772
|
+
return;
|
|
723
773
|
this.objectPool.upsert(model, ModelScope.live);
|
|
724
774
|
this.queueMutation({ type: 'update', model, timestamp: new Date(), capturedChanges });
|
|
725
775
|
this.notifyObservers({
|
|
@@ -1552,25 +1602,39 @@ export class SyncClient extends EventEmitter {
|
|
|
1552
1602
|
break;
|
|
1553
1603
|
}
|
|
1554
1604
|
}
|
|
1555
|
-
//
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
//
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1605
|
+
// Reveal the whole frame in ONE MobX action. `addBatch`/`upsertBatch`/
|
|
1606
|
+
// `removeBatch`/`updateScope` are each individually `action`-wrapped,
|
|
1607
|
+
// so calling them sequentially flushes reactions at every action
|
|
1608
|
+
// boundary — a catch-up frame that adds + updates + removes would fire
|
|
1609
|
+
// every dependent reaction (the decks gallery, each open editor) 3-4×
|
|
1610
|
+
// in a row, re-rendering and re-sorting on each. Wrapping them in a
|
|
1611
|
+
// single outer `runInAction` defers all reaction flushes to ONE
|
|
1612
|
+
// boundary: dependents recompute exactly once regardless of how many
|
|
1613
|
+
// models or how many op-kinds the frame touched. This is the MobX
|
|
1614
|
+
// equivalent of Replicache's "atomically reveal the new state" — the
|
|
1615
|
+
// app never observes a partially-applied frame.
|
|
1616
|
+
runInAction(() => {
|
|
1617
|
+
if (modelsToAdd.length > 0)
|
|
1618
|
+
this.objectPool.addBatch(modelsToAdd, ModelScope.live);
|
|
1619
|
+
if (modelsToUpsert.length > 0)
|
|
1620
|
+
this.objectPool.upsertBatch(modelsToUpsert, ModelScope.live);
|
|
1621
|
+
if (idsToRemove.length > 0)
|
|
1622
|
+
this.objectPool.removeBatch(idsToRemove);
|
|
1623
|
+
for (const id of idsToArchive)
|
|
1624
|
+
this.objectPool.updateScope(id, ModelScope.archived);
|
|
1625
|
+
// Emit changed model types so QueryProcessor can auto-invalidate.
|
|
1626
|
+
// Kept inside the action so any observable query-cache state it
|
|
1627
|
+
// flips is part of the same atomic reveal.
|
|
1628
|
+
const changedTypes = new Set(dbResults.map(r => r.modelName));
|
|
1629
|
+
if (changedTypes.size > 0)
|
|
1630
|
+
this.emit('models:changed', changedTypes);
|
|
1631
|
+
});
|
|
1568
1632
|
}
|
|
1569
1633
|
/**
|
|
1570
1634
|
* Apply bootstrap data to the ObjectPool with ghost removal.
|
|
1571
1635
|
* Owns: model creation, batch upsert, ghost detection + removal.
|
|
1572
1636
|
*/
|
|
1573
|
-
applyBootstrapDataToPool(bootstrapData, protectedIds) {
|
|
1637
|
+
applyBootstrapDataToPool(bootstrapData, protectedIds, options) {
|
|
1574
1638
|
if (!bootstrapData.models) {
|
|
1575
1639
|
return { added: 0, updated: 0, removed: 0, skipped: 0, healed: 0 };
|
|
1576
1640
|
}
|
|
@@ -1605,6 +1669,19 @@ export class SyncClient extends EventEmitter {
|
|
|
1605
1669
|
const recordId = data.id;
|
|
1606
1670
|
if (recordId)
|
|
1607
1671
|
idsForType.add(recordId);
|
|
1672
|
+
// Scoped backfill (P4 hydrate-on-enter): a subset snapshot is taken at
|
|
1673
|
+
// a server watermark. If a concurrent live delta already advanced this
|
|
1674
|
+
// row past the snapshot, skip it — `createFromData` mutates the pooled
|
|
1675
|
+
// model IN PLACE (the "keep instances alive" Linear pattern), so this
|
|
1676
|
+
// version guard MUST run BEFORE it; an upsert-layer guard would be too
|
|
1677
|
+
// late, the row would already be clobbered.
|
|
1678
|
+
if (options?.scoped && recordId) {
|
|
1679
|
+
const existing = this.objectPool.get(recordId);
|
|
1680
|
+
if (existing && !rawRecordIsNewer(data, existing)) {
|
|
1681
|
+
skippedCount++;
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1608
1685
|
try {
|
|
1609
1686
|
const model = this.objectPool.createFromData(data);
|
|
1610
1687
|
if (model)
|
|
@@ -1615,23 +1692,30 @@ export class SyncClient extends EventEmitter {
|
|
|
1615
1692
|
}
|
|
1616
1693
|
}
|
|
1617
1694
|
}
|
|
1618
|
-
//
|
|
1695
|
+
// Upsert. The scoped stale-skip above already guarded the version, so a
|
|
1696
|
+
// plain upsert is correct here for both paths.
|
|
1619
1697
|
const beforeSize = this.objectPool.size;
|
|
1620
1698
|
this.objectPool.upsertBatch(allModels, ModelScope.live);
|
|
1621
1699
|
const addedCount = this.objectPool.size - beforeSize;
|
|
1622
1700
|
const updatedCount = allModels.length - addedCount;
|
|
1623
|
-
// Ghost removal — remove pool entities not in server snapshot
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1701
|
+
// Ghost removal — remove pool entities not in the server snapshot. Only
|
|
1702
|
+
// valid for a FULL bootstrap, where the snapshot is authoritative for each
|
|
1703
|
+
// returned type. A SCOPED subset snapshot must NOT remove rows of the same
|
|
1704
|
+
// type that belong to other (unhydrated) groups.
|
|
1705
|
+
let removedCount = 0;
|
|
1706
|
+
if (!options?.scoped) {
|
|
1707
|
+
const ghostIds = [];
|
|
1708
|
+
for (const [modelType, serverIds] of serverIdsByType) {
|
|
1709
|
+
const poolIds = this.objectPool.getIdsByModelType(modelType);
|
|
1710
|
+
if (!poolIds)
|
|
1711
|
+
continue;
|
|
1712
|
+
for (const poolId of poolIds) {
|
|
1713
|
+
if (!serverIds.has(poolId) && !protectedIds?.has(poolId))
|
|
1714
|
+
ghostIds.push(poolId);
|
|
1715
|
+
}
|
|
1632
1716
|
}
|
|
1717
|
+
removedCount = this.objectPool.removeBatch(ghostIds);
|
|
1633
1718
|
}
|
|
1634
|
-
const removedCount = this.objectPool.removeBatch(ghostIds);
|
|
1635
1719
|
// Emit changed model types so QueryProcessor can auto-invalidate
|
|
1636
1720
|
const changedTypes = new Set(Object.keys(bootstrapData.models));
|
|
1637
1721
|
if (changedTypes.size > 0)
|
package/dist/agent/Agent.d.ts
CHANGED
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
* for custom integrations outside the AI SDK.
|
|
41
41
|
*/
|
|
42
42
|
import type { PresenceAnnouncer, AgentContext } from './types.js';
|
|
43
|
-
import type { Activity,
|
|
43
|
+
import type { Activity, WireClaim } from '../types/streams.js';
|
|
44
44
|
import { createAgentSession } from './session.js';
|
|
45
45
|
export type { AgentContext } from './types.js';
|
|
46
|
-
export type {
|
|
46
|
+
export type { WireClaim } from '../types/streams.js';
|
|
47
47
|
/**
|
|
48
48
|
* Shape returned by the sync server's REST `/api/presence` endpoint.
|
|
49
49
|
*
|
|
@@ -62,7 +62,7 @@ interface WirePeer {
|
|
|
62
62
|
activity?: Activity;
|
|
63
63
|
updatedAt?: number;
|
|
64
64
|
organizationId?: string;
|
|
65
|
-
|
|
65
|
+
activeClaims?: WireClaim[];
|
|
66
66
|
}
|
|
67
67
|
import type { SyncLogger } from '../interfaces/index.js';
|
|
68
68
|
export interface AgentOptions {
|
|
@@ -125,13 +125,13 @@ export interface FreshnessCheck {
|
|
|
125
125
|
/** Human-readable summary — feed this back to the LLM when stale. */
|
|
126
126
|
summary?: string;
|
|
127
127
|
/**
|
|
128
|
-
* Pending-mutation
|
|
129
|
-
* entity (self-
|
|
128
|
+
* Pending-mutation claims from OTHER participants targeting this
|
|
129
|
+
* entity (self-claims filtered out). Empty = no one else is
|
|
130
130
|
* currently generating against this entity. Non-empty is ADVISORY
|
|
131
131
|
* — the agent can proceed, wait, or defer. Stale-read protection
|
|
132
132
|
* that predates committed deltas.
|
|
133
133
|
*/
|
|
134
|
-
|
|
134
|
+
pendingClaims?: WireClaim[];
|
|
135
135
|
}
|
|
136
136
|
/** Subset of AI SDK's ModelMessage — structural. */
|
|
137
137
|
export interface AgentMessage {
|
|
@@ -299,13 +299,13 @@ export declare class Agent implements PresenceAnnouncer {
|
|
|
299
299
|
*/
|
|
300
300
|
checkFreshness(entityType: string, entityId: string, lastSeenAt: number): Promise<FreshnessCheck>;
|
|
301
301
|
/**
|
|
302
|
-
* Pull the org's presence, filter to
|
|
303
|
-
* entity (self-
|
|
302
|
+
* Pull the org's presence, filter to claims targeting the given
|
|
303
|
+
* entity (self-claims excluded). Advisory — returns empty on any
|
|
304
304
|
* error so `checkFreshness` stays usable when the presence endpoint
|
|
305
305
|
* is down. Case-insensitive match on entityType + entityId to absorb
|
|
306
306
|
* PascalCase / lowercase divergence.
|
|
307
307
|
*/
|
|
308
|
-
private
|
|
308
|
+
private fetchPendingClaimsFor;
|
|
309
309
|
/**
|
|
310
310
|
* Build a `prepareStep` hook for AI SDK's generateText / streamText /
|
|
311
311
|
* ToolLoopAgent. Called before each step — injects a system message
|
package/dist/agent/Agent.js
CHANGED
|
@@ -201,14 +201,14 @@ export class Agent {
|
|
|
201
201
|
*/
|
|
202
202
|
async checkFreshness(entityType, entityId, lastSeenAt) {
|
|
203
203
|
// Parallel fan-out: freshness (entity state vs lastSeenAt) + pending
|
|
204
|
-
//
|
|
204
|
+
// claims (other agents about to mutate). Both are advisory — if
|
|
205
205
|
// either request fails the check still returns a usable result.
|
|
206
|
-
const [queryRes,
|
|
206
|
+
const [queryRes, pendingClaims] = await Promise.all([
|
|
207
207
|
this.request('POST', '/api/sync/query', {
|
|
208
208
|
organizationId: this.opts.organizationId,
|
|
209
209
|
queries: [{ model: entityType, ids: [entityId] }],
|
|
210
210
|
}).catch((err) => ({ ok: false, status: 0, _err: err })),
|
|
211
|
-
this.
|
|
211
|
+
this.fetchPendingClaimsFor(entityType, entityId),
|
|
212
212
|
]);
|
|
213
213
|
try {
|
|
214
214
|
const res = queryRes;
|
|
@@ -217,7 +217,7 @@ export class Agent {
|
|
|
217
217
|
stale: false,
|
|
218
218
|
reason: 'ok',
|
|
219
219
|
summary: `Freshness check inconclusive: ${('status' in res ? res.status : 'error')}`,
|
|
220
|
-
|
|
220
|
+
pendingClaims,
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
223
|
const body = (await res.json());
|
|
@@ -227,7 +227,7 @@ export class Agent {
|
|
|
227
227
|
stale: true,
|
|
228
228
|
reason: 'not_found',
|
|
229
229
|
summary: `${entityType} ${entityId} no longer exists. Another actor may have deleted it.`,
|
|
230
|
-
|
|
230
|
+
pendingClaims,
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
233
|
const entity = rows[0];
|
|
@@ -251,7 +251,7 @@ export class Agent {
|
|
|
251
251
|
summary: `${entityType} ${entityId} was modified by ${lastModifiedBy ?? 'another actor'} ` +
|
|
252
252
|
`${ago}s ago. Your planned change is based on stale state. ` +
|
|
253
253
|
`Re-read the entity and adjust your approach.`,
|
|
254
|
-
|
|
254
|
+
pendingClaims,
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
257
|
return {
|
|
@@ -260,7 +260,7 @@ export class Agent {
|
|
|
260
260
|
currentState: entity,
|
|
261
261
|
lastModifiedBy,
|
|
262
262
|
lastModifiedAt,
|
|
263
|
-
|
|
263
|
+
pendingClaims,
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
266
|
catch (err) {
|
|
@@ -270,29 +270,29 @@ export class Agent {
|
|
|
270
270
|
stale: false,
|
|
271
271
|
reason: 'ok',
|
|
272
272
|
summary: `Freshness check error: ${err.message}`,
|
|
273
|
-
|
|
273
|
+
pendingClaims,
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
/**
|
|
278
|
-
* Pull the org's presence, filter to
|
|
279
|
-
* entity (self-
|
|
278
|
+
* Pull the org's presence, filter to claims targeting the given
|
|
279
|
+
* entity (self-claims excluded). Advisory — returns empty on any
|
|
280
280
|
* error so `checkFreshness` stays usable when the presence endpoint
|
|
281
281
|
* is down. Case-insensitive match on entityType + entityId to absorb
|
|
282
282
|
* PascalCase / lowercase divergence.
|
|
283
283
|
*/
|
|
284
|
-
async
|
|
284
|
+
async fetchPendingClaimsFor(entityType, entityId) {
|
|
285
285
|
const etLower = entityType.toLowerCase();
|
|
286
286
|
const idLower = entityId.toLowerCase();
|
|
287
287
|
const entries = await this.fetchPresence(true);
|
|
288
288
|
const result = [];
|
|
289
289
|
for (const entry of entries) {
|
|
290
|
-
if (!entry.
|
|
290
|
+
if (!entry.activeClaims)
|
|
291
291
|
continue;
|
|
292
|
-
for (const
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
result.push(
|
|
292
|
+
for (const claim of entry.activeClaims) {
|
|
293
|
+
if (claim.entityType.toLowerCase() === etLower &&
|
|
294
|
+
claim.entityId.toLowerCase() === idLower) {
|
|
295
|
+
result.push(claim);
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
}
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ─────────────────────────────────────────────────────────────────────────
|
|
9
9
|
* Use the unified `Ablo({...})` factory directly with `kind: 'agent'`.
|
|
10
10
|
* The factory holds the WebSocket, reactive subscriptions, mutations, and
|
|
11
|
-
* presence/
|
|
11
|
+
* presence/claims — same surface as a browser user, just with a
|
|
12
12
|
* server-issued capability token instead of session cookies.
|
|
13
13
|
*
|
|
14
14
|
* ```ts
|
package/dist/agent/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ─────────────────────────────────────────────────────────────────────────
|
|
9
9
|
* Use the unified `Ablo({...})` factory directly with `kind: 'agent'`.
|
|
10
10
|
* The factory holds the WebSocket, reactive subscriptions, mutations, and
|
|
11
|
-
* presence/
|
|
11
|
+
* presence/claims — same surface as a browser user, just with a
|
|
12
12
|
* server-issued capability token instead of session cookies.
|
|
13
13
|
*
|
|
14
14
|
* ```ts
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
// const ctx: Agent.Context = { perception };
|
|
123
123
|
// const s: Agent.SessionOptions = { ... };
|
|
124
124
|
//
|
|
125
|
-
// Everything else (Activity, Claim, Peer,
|
|
125
|
+
// Everything else (Activity, Claim, Peer, ActiveClaim, ...)
|
|
126
126
|
// lives on the `Ablo.*` namespace via
|
|
127
127
|
// `import type { Ablo } from '@abloatai/ablo'`.
|
|
128
128
|
export { Agent } from './Agent.js';
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent-SDK abstractions. The engine's data vocabulary
|
|
3
|
-
* (`Peer`, `Activity`, `
|
|
3
|
+
* (`Peer`, `Activity`, `Claim`, `ActiveClaim`,
|
|
4
4
|
* `PresenceUpdatePayload`, `PresenceKind`) lives in
|
|
5
5
|
* `../types/streams.ts`. This file holds only the bits that are
|
|
6
6
|
* specific to the agent module: the `PresenceAnnouncer` abstraction
|
package/dist/agent/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent-SDK abstractions. The engine's data vocabulary
|
|
3
|
-
* (`Peer`, `Activity`, `
|
|
3
|
+
* (`Peer`, `Activity`, `Claim`, `ActiveClaim`,
|
|
4
4
|
* `PresenceUpdatePayload`, `PresenceKind`) lives in
|
|
5
5
|
* `../types/streams.ts`. This file holds only the bits that are
|
|
6
6
|
* specific to the agent module: the `PresenceAnnouncer` abstraction
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Claim broadcast middleware — wraps a language model so the agent
|
|
3
3
|
* declares "I'm about to edit entity X" over the sync engine's
|
|
4
|
-
*
|
|
4
|
+
* claim primitive at stream start, and abandons the claim at
|
|
5
5
|
* stream end.
|
|
6
6
|
*
|
|
7
7
|
* Cross-cutting by design — composes via the AI SDK's
|
|
@@ -13,20 +13,20 @@
|
|
|
13
13
|
* the package's own `SyncAgent`. No app-specific assumptions —
|
|
14
14
|
* Ablo's web app uses this, but so can any consumer of `@abloatai/ablo`.
|
|
15
15
|
*
|
|
16
|
-
* Cost: one WS frame at stream start (`
|
|
17
|
-
* (`
|
|
16
|
+
* Cost: one WS frame at stream start (`claim_begin`), one at end
|
|
17
|
+
* (`claim_abandon`). No DB I/O, no extra LLM tokens.
|
|
18
18
|
*/
|
|
19
19
|
import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
|
|
20
20
|
import type { Ablo } from '../client/Ablo.js';
|
|
21
21
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
22
22
|
/**
|
|
23
|
-
* Target entity for the
|
|
23
|
+
* Target entity for the claim broadcast.
|
|
24
24
|
*
|
|
25
25
|
* `entityType` is a free-form string — convention is the schema's
|
|
26
26
|
* typename (e.g. `'SlideDeck'`, `'Task'`, `'Matter'`) so peers can
|
|
27
27
|
* filter consistently. The wire format treats it opaquely.
|
|
28
28
|
*/
|
|
29
|
-
export interface
|
|
29
|
+
export interface ClaimTarget {
|
|
30
30
|
readonly entityType: string;
|
|
31
31
|
readonly entityId: string;
|
|
32
32
|
/** Optional path for file/document-like targets. */
|
|
@@ -52,11 +52,11 @@ export interface IntentTarget {
|
|
|
52
52
|
*/
|
|
53
53
|
readonly estimatedMs?: number;
|
|
54
54
|
}
|
|
55
|
-
export interface
|
|
55
|
+
export interface ClaimBroadcastMiddlewareOptions<R extends SchemaRecord = SchemaRecord> {
|
|
56
56
|
/** Connected Ablo. Null disables the middleware (no-op). */
|
|
57
57
|
readonly agent: Ablo<R> | null;
|
|
58
58
|
/** Target entity. Null skips the broadcast (purely conversational). */
|
|
59
|
-
readonly target:
|
|
59
|
+
readonly target: ClaimTarget | null;
|
|
60
60
|
/**
|
|
61
61
|
* Action verb describing what the agent is doing. Convention:
|
|
62
62
|
* `'edit'`, `'read'`, `'review'`, `'generate'`. Default `'edit'`.
|
|
@@ -64,7 +64,7 @@ export interface IntentBroadcastMiddlewareOptions<R extends SchemaRecord = Schem
|
|
|
64
64
|
readonly action?: string;
|
|
65
65
|
/**
|
|
66
66
|
* Peer-visible explanation of the specific work this model call is about to
|
|
67
|
-
* perform. Surfaces to other agents through `
|
|
67
|
+
* perform. Surfaces to other agents through `ActiveClaim.description`.
|
|
68
68
|
*/
|
|
69
69
|
readonly description?: string;
|
|
70
70
|
}
|
|
@@ -79,4 +79,4 @@ export interface IntentBroadcastMiddlewareOptions<R extends SchemaRecord = Schem
|
|
|
79
79
|
* widened version collapses model proxies to an index signature
|
|
80
80
|
* that clashes with the named methods (`ready`, `dispose`, etc.).
|
|
81
81
|
*/
|
|
82
|
-
export declare function
|
|
82
|
+
export declare function claimBroadcastMiddleware<R extends SchemaRecord = SchemaRecord>(options: ClaimBroadcastMiddlewareOptions<R>): LanguageModelV3Middleware;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Claim broadcast middleware — wraps a language model so the agent
|
|
3
3
|
* declares "I'm about to edit entity X" over the sync engine's
|
|
4
|
-
*
|
|
4
|
+
* claim primitive at stream start, and abandons the claim at
|
|
5
5
|
* stream end.
|
|
6
6
|
*
|
|
7
7
|
* Cross-cutting by design — composes via the AI SDK's
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* the package's own `SyncAgent`. No app-specific assumptions —
|
|
14
14
|
* Ablo's web app uses this, but so can any consumer of `@abloatai/ablo`.
|
|
15
15
|
*
|
|
16
|
-
* Cost: one WS frame at stream start (`
|
|
17
|
-
* (`
|
|
16
|
+
* Cost: one WS frame at stream start (`claim_begin`), one at end
|
|
17
|
+
* (`claim_abandon`). No DB I/O, no extra LLM tokens.
|
|
18
18
|
*/
|
|
19
19
|
/**
|
|
20
20
|
* Build the middleware. When `agent` or `target` is null, returns a
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
* widened version collapses model proxies to an index signature
|
|
28
28
|
* that clashes with the named methods (`ready`, `dispose`, etc.).
|
|
29
29
|
*/
|
|
30
|
-
export function
|
|
30
|
+
export function claimBroadcastMiddleware(options) {
|
|
31
31
|
const { agent, target } = options;
|
|
32
32
|
const action = options.action ?? 'edit';
|
|
33
33
|
const description = options.description;
|
|
34
34
|
const openClaim = () => {
|
|
35
35
|
if (!agent || !target)
|
|
36
36
|
return null;
|
|
37
|
-
return agent.
|
|
37
|
+
return agent.claims.claim({
|
|
38
38
|
type: target.entityType,
|
|
39
39
|
id: target.entityId,
|
|
40
40
|
path: target.path,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Coordination context middleware — reads peer
|
|
2
|
+
* Coordination context middleware — reads peer claims on the same
|
|
3
3
|
* entity from the sync engine's presence stream and injects a brief
|
|
4
4
|
* coordination note into the prompt before the LLM call.
|
|
5
5
|
*
|
|
6
|
-
* The complement of `
|
|
6
|
+
* The complement of `claim-broadcast.ts`: that one declares what
|
|
7
7
|
* THIS agent is about to do; this one reads what OTHERS are doing
|
|
8
8
|
* and tells the LLM about it. Together they make multiplayer-with-
|
|
9
9
|
* AI structurally real — the AI knows when a human or another
|
|
@@ -23,28 +23,28 @@
|
|
|
23
23
|
import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
|
|
24
24
|
import type { Ablo } from '../client/Ablo.js';
|
|
25
25
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
26
|
-
import type {
|
|
26
|
+
import type { ClaimTarget } from './claim-broadcast.js';
|
|
27
27
|
export interface CoordinationContextMiddlewareOptions<R extends SchemaRecord = SchemaRecord> {
|
|
28
28
|
readonly agent: Ablo<R> | null;
|
|
29
|
-
readonly target:
|
|
29
|
+
readonly target: ClaimTarget | null;
|
|
30
30
|
/**
|
|
31
|
-
* Optional
|
|
31
|
+
* Optional claimId(s) to exclude from the read — typically this
|
|
32
32
|
* agent's own active claim so the coordination note doesn't tell
|
|
33
33
|
* the AI "you yourself are editing this." When middleware is
|
|
34
|
-
* composed with `
|
|
34
|
+
* composed with `claimBroadcastMiddleware` in the standard order,
|
|
35
35
|
* `transformParams` runs BEFORE the broadcast's `wrapStream`
|
|
36
36
|
* declares its claim, so the agent's own claim isn't yet in the
|
|
37
37
|
* cached presence and self-filtering isn't needed. The hook is
|
|
38
38
|
* here for callers that compose differently or for fleet
|
|
39
|
-
* coordination (filter sibling worker
|
|
39
|
+
* coordination (filter sibling worker claims).
|
|
40
40
|
*/
|
|
41
|
-
readonly
|
|
41
|
+
readonly excludeClaimIds?: readonly string[];
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Build the middleware. When `agent` or `target` is null, returns a
|
|
45
45
|
* pass-through.
|
|
46
46
|
*
|
|
47
|
-
* Generic over the schema record — see `
|
|
47
|
+
* Generic over the schema record — see `claimBroadcastMiddleware`
|
|
48
48
|
* for why `Ablo<S>` and `Ablo<SchemaRecord>` aren't structurally
|
|
49
49
|
* assignable.
|
|
50
50
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Coordination context middleware — reads peer
|
|
2
|
+
* Coordination context middleware — reads peer claims on the same
|
|
3
3
|
* entity from the sync engine's presence stream and injects a brief
|
|
4
4
|
* coordination note into the prompt before the LLM call.
|
|
5
5
|
*
|
|
6
|
-
* The complement of `
|
|
6
|
+
* The complement of `claim-broadcast.ts`: that one declares what
|
|
7
7
|
* THIS agent is about to do; this one reads what OTHERS are doing
|
|
8
8
|
* and tells the LLM about it. Together they make multiplayer-with-
|
|
9
9
|
* AI structurally real — the AI knows when a human or another
|
|
@@ -24,24 +24,24 @@
|
|
|
24
24
|
* Build the middleware. When `agent` or `target` is null, returns a
|
|
25
25
|
* pass-through.
|
|
26
26
|
*
|
|
27
|
-
* Generic over the schema record — see `
|
|
27
|
+
* Generic over the schema record — see `claimBroadcastMiddleware`
|
|
28
28
|
* for why `Ablo<S>` and `Ablo<SchemaRecord>` aren't structurally
|
|
29
29
|
* assignable.
|
|
30
30
|
*/
|
|
31
31
|
export function coordinationContextMiddleware(options) {
|
|
32
32
|
const { agent, target } = options;
|
|
33
|
-
const
|
|
33
|
+
const excludeClaimIds = new Set(options.excludeClaimIds ?? []);
|
|
34
34
|
return {
|
|
35
35
|
specificationVersion: 'v3',
|
|
36
36
|
transformParams: async ({ params }) => {
|
|
37
37
|
if (!agent || !target)
|
|
38
38
|
return params;
|
|
39
|
-
// Read peer
|
|
40
|
-
// against the engine's reactive
|
|
41
|
-
const peerClaims = agent.
|
|
39
|
+
// Read peer claims on the same target. Synchronous lookup
|
|
40
|
+
// against the engine's reactive claims.others array — no I/O.
|
|
41
|
+
const peerClaims = agent.claims.others.filter((claim) => claim.target.type === target.entityType &&
|
|
42
42
|
claim.target.id === target.entityId &&
|
|
43
43
|
targetsOverlap(claim.target, target) &&
|
|
44
|
-
!
|
|
44
|
+
!excludeClaimIds.has(claim.id));
|
|
45
45
|
if (peerClaims.length === 0)
|
|
46
46
|
return params;
|
|
47
47
|
const note = formatCoordinationNote(peerClaims, target);
|
package/dist/ai-sdk/index.d.ts
CHANGED
|
@@ -72,6 +72,6 @@
|
|
|
72
72
|
* to one entity before any tool is chosen; tool implementations stay exactly
|
|
73
73
|
* the same.
|
|
74
74
|
*/
|
|
75
|
-
export {
|
|
75
|
+
export { claimBroadcastMiddleware, type ClaimTarget, type ClaimBroadcastMiddlewareOptions, } from './claim-broadcast.js';
|
|
76
76
|
export { coordinationContextMiddleware, type CoordinationContextMiddlewareOptions, } from './coordination-context.js';
|
|
77
77
|
export { wrapWithMultiplayer, type WrapWithMultiplayerOptions } from './wrap.js';
|
package/dist/ai-sdk/index.js
CHANGED
|
@@ -72,6 +72,6 @@
|
|
|
72
72
|
* to one entity before any tool is chosen; tool implementations stay exactly
|
|
73
73
|
* the same.
|
|
74
74
|
*/
|
|
75
|
-
export {
|
|
75
|
+
export { claimBroadcastMiddleware, } from './claim-broadcast.js';
|
|
76
76
|
export { coordinationContextMiddleware, } from './coordination-context.js';
|
|
77
77
|
export { wrapWithMultiplayer } from './wrap.js';
|
package/dist/ai-sdk/wrap.d.ts
CHANGED
|
@@ -31,14 +31,14 @@ import { wrapLanguageModel } from 'ai';
|
|
|
31
31
|
import type { LanguageModelV3, LanguageModelV3Middleware } from '@ai-sdk/provider';
|
|
32
32
|
import type { Ablo } from '../client/Ablo.js';
|
|
33
33
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
34
|
-
import { type
|
|
34
|
+
import { type ClaimTarget } from './claim-broadcast.js';
|
|
35
35
|
export interface WrapWithMultiplayerOptions {
|
|
36
36
|
/** The base language model to wrap. Consumer brings their own. */
|
|
37
37
|
readonly model: LanguageModelV3;
|
|
38
38
|
/** Connected SyncAgent. Null = pass-through wrap (no broadcast, no read). */
|
|
39
39
|
readonly agent: Ablo<SchemaRecord> | null;
|
|
40
40
|
/** Target entity. Null = pass-through wrap. */
|
|
41
|
-
readonly target:
|
|
41
|
+
readonly target: ClaimTarget | null;
|
|
42
42
|
/**
|
|
43
43
|
* Optional action verb for the broadcast. Default `'edit'`.
|
|
44
44
|
* Convention: `'edit'`, `'read'`, `'review'`, `'generate'`.
|
|
@@ -50,11 +50,11 @@ export interface WrapWithMultiplayerOptions {
|
|
|
50
50
|
*/
|
|
51
51
|
readonly description?: string;
|
|
52
52
|
/**
|
|
53
|
-
* Optional
|
|
53
|
+
* Optional claimIds to exclude from the coordination-context
|
|
54
54
|
* read — typically the caller's own claim if they're composing
|
|
55
55
|
* multiple wrappings. Most consumers leave this empty.
|
|
56
56
|
*/
|
|
57
|
-
readonly
|
|
57
|
+
readonly excludeClaimIds?: readonly string[];
|
|
58
58
|
/**
|
|
59
59
|
* Optional extra middleware to compose. Runs in the order given,
|
|
60
60
|
* INSIDE the multiplayer middlewares (so the multiplayer wrap is
|