@abloatai/ablo 0.8.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.
- package/CHANGELOG.md +40 -1
- package/README.md +32 -27
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +172 -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 +86 -50
- package/dist/mutators/UndoManager.js +129 -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/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 +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- 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 +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -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 +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -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 +1 -1
- 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/client/Ablo.js
CHANGED
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* const sync = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
12
12
|
*
|
|
13
13
|
* const reports = sync.reports.list({ where: { status: 'todo' } });
|
|
14
|
-
* await sync.reports.create({ title: 'Fix bug' });
|
|
15
|
-
* await sync.reports.update(
|
|
16
|
-
*
|
|
14
|
+
* await sync.reports.create({ data: { title: 'Fix bug' } });
|
|
15
|
+
* await sync.reports.update({
|
|
16
|
+
* id: reportId,
|
|
17
|
+
* data: { status: 'ready' },
|
|
18
|
+
* });
|
|
19
|
+
* await sync.reports.delete({ id: reportId });
|
|
17
20
|
*/
|
|
18
21
|
import { z } from 'zod';
|
|
19
22
|
import { AbloClaimedError, AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, hasWireCode, toAbloError } from '../errors.js';
|
|
@@ -23,6 +26,7 @@ import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, no
|
|
|
23
26
|
import { alwaysOnline } from '../adapters/alwaysOnline.js';
|
|
24
27
|
import { validateAbloOptions } from './validateAbloOptions.js';
|
|
25
28
|
import { exchangeApiKey } from '../auth/index.js';
|
|
29
|
+
import { createAuthCredentialSource } from '../auth/credentialSource.js';
|
|
26
30
|
import { createInternalComponents } from './createInternalComponents.js';
|
|
27
31
|
import { resolveParticipantIdentity } from './identity.js';
|
|
28
32
|
import { Model } from '../Model.js';
|
|
@@ -646,6 +650,37 @@ function createDefaultMutationDispatcher(executor) {
|
|
|
646
650
|
},
|
|
647
651
|
};
|
|
648
652
|
}
|
|
653
|
+
// ── Auth normalization ─────────────────────────────────────────────────────
|
|
654
|
+
/**
|
|
655
|
+
* The one resolver the credential lifecycle needs: an async `() => token | null`,
|
|
656
|
+
* or `null` when auth is static (a plain long-lived `apiKey` with no refresh —
|
|
657
|
+
* the common case). Only the short-lived per-user path sets this, via `getToken`
|
|
658
|
+
* (the primitive) or `authEndpoint` (sugar that POSTs for `{ token }`).
|
|
659
|
+
*/
|
|
660
|
+
function resolveCredentialResolver(options) {
|
|
661
|
+
if (options.getToken)
|
|
662
|
+
return options.getToken;
|
|
663
|
+
if (options.authEndpoint) {
|
|
664
|
+
const endpoint = options.authEndpoint;
|
|
665
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
666
|
+
return async () => {
|
|
667
|
+
// The endpoint lives on the consumer's OWN backend and is authed by the
|
|
668
|
+
// user's session cookie (hence `credentials: 'include'`); it returns the
|
|
669
|
+
// `ek_` to carry to the sync-server. A non-OK response is terminal
|
|
670
|
+
// (`null` → sign out), matching the `getToken` contract.
|
|
671
|
+
const res = await fetchImpl(endpoint, {
|
|
672
|
+
method: 'POST',
|
|
673
|
+
credentials: 'include',
|
|
674
|
+
headers: { 'Content-Type': 'application/json' },
|
|
675
|
+
});
|
|
676
|
+
if (!res.ok)
|
|
677
|
+
return null;
|
|
678
|
+
const body = (await res.json());
|
|
679
|
+
return body.token ?? null;
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
649
684
|
export function Ablo(options) {
|
|
650
685
|
if (options.schema == null) {
|
|
651
686
|
return createProtocolClient(options);
|
|
@@ -655,6 +690,12 @@ export function Ablo(options) {
|
|
|
655
690
|
const authInput = { options, env };
|
|
656
691
|
const configuredApiKey = resolveApiKey(authInput);
|
|
657
692
|
const configuredAuthToken = resolveAuthToken(authInput);
|
|
693
|
+
// The client OWNS its credential lifecycle (not the React layer): this resolver
|
|
694
|
+
// drives both the reactive re-mint (FSM `credential_stale`) and the proactive
|
|
695
|
+
// refresh timer + wake/online/focus triggers. Null for the common static
|
|
696
|
+
// `apiKey` path — no refresh needed.
|
|
697
|
+
const credentialResolver = resolveCredentialResolver(options);
|
|
698
|
+
const authCredentials = createAuthCredentialSource(internalOptions.capabilityToken ?? configuredAuthToken);
|
|
658
699
|
const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
|
|
659
700
|
assertBrowserSafety({
|
|
660
701
|
apiKey: configuredApiKey,
|
|
@@ -724,7 +765,12 @@ export function Ablo(options) {
|
|
|
724
765
|
// the schema-to-Model-class translation depends on private
|
|
725
766
|
// helpers (`createDynamicModelClass`, `unwrapZodType`, etc.)
|
|
726
767
|
// that aren't worth pulling into the components module.
|
|
727
|
-
const { modelRegistry, objectPool, bootstrapHelper, database, syncClient, hydration, } = createInternalComponents({
|
|
768
|
+
const { modelRegistry, objectPool, bootstrapHelper, database, syncClient, hydration, } = createInternalComponents({
|
|
769
|
+
schema,
|
|
770
|
+
url,
|
|
771
|
+
options: internalOptions,
|
|
772
|
+
auth: authCredentials,
|
|
773
|
+
});
|
|
728
774
|
registerModelsFromSchema(schema, modelRegistry);
|
|
729
775
|
// 5. BaseSyncedStore handles the initialization orchestration
|
|
730
776
|
// (open DB → hydrate IDB → connect WS → fetch bootstrap → hydrate again →
|
|
@@ -742,7 +788,15 @@ export function Ablo(options) {
|
|
|
742
788
|
modelRegistry,
|
|
743
789
|
schema,
|
|
744
790
|
url,
|
|
791
|
+
auth: authCredentials,
|
|
745
792
|
});
|
|
793
|
+
// Hand the credential lifecycle to the client (refresher + proactive refresh
|
|
794
|
+
// timer + wake/online/focus re-mint). Installed once here so refresh works for
|
|
795
|
+
// ANY consumer of `Ablo({ auth })` — not only those who render `<AbloProvider>`.
|
|
796
|
+
// The first mint happens in `ready()` so the first connection carries a token.
|
|
797
|
+
if (credentialResolver) {
|
|
798
|
+
store.startCredentialLifecycle(credentialResolver);
|
|
799
|
+
}
|
|
746
800
|
// Wire the store back into the default executor's lazy getter (see
|
|
747
801
|
// `storeHolder` above). The executor was constructed before the store
|
|
748
802
|
// existed; this late binding closes the loop so commits dispatch over
|
|
@@ -812,16 +866,6 @@ export function Ablo(options) {
|
|
|
812
866
|
// source of truth. No duplicate closure variables.
|
|
813
867
|
let _readyPromise = null;
|
|
814
868
|
let _refreshScheduler = null;
|
|
815
|
-
let currentCapabilityToken = internalOptions.capabilityToken ?? configuredAuthToken ?? undefined;
|
|
816
|
-
// Wire the cap token into HydrationCoordinator's HTTP path. Without
|
|
817
|
-
// this, `ablo.<model>.load(...)` / `ablo.<model>.retrieve(...)` go
|
|
818
|
-
// through `postQuery` with `credentials: 'include'` only — fine in
|
|
819
|
-
// browsers (session cookies), but Node consumers (agent-worker)
|
|
820
|
-
// have no cookies and the request lands with no credential at all.
|
|
821
|
-
// The WS path was already wired (token rides the upgrade URL); this
|
|
822
|
-
// closes the gap on HTTP. Closure-over-binding so cap rotation
|
|
823
|
-
// (`applyRotatedToken` in the refresh scheduler below) propagates.
|
|
824
|
-
hydration.setCapabilityTokenProvider(() => currentCapabilityToken ?? null);
|
|
825
869
|
async function ready() {
|
|
826
870
|
if (_readyPromise)
|
|
827
871
|
return _readyPromise;
|
|
@@ -831,6 +875,20 @@ export function Ablo(options) {
|
|
|
831
875
|
}
|
|
832
876
|
_readyPromise = (async () => {
|
|
833
877
|
try {
|
|
878
|
+
// Mint the FIRST access credential before we connect, so the initial
|
|
879
|
+
// WebSocket upgrade + bootstrap carry a valid bearer (no tokenless first
|
|
880
|
+
// connect that has to self-heal). Only when a refreshing resolver is
|
|
881
|
+
// wired AND no static credential is already present. Contract mirrors
|
|
882
|
+
// `getToken`: `null` ⇒ the login is gone (terminal — fail ready so the
|
|
883
|
+
// app shows sign-in); a THROW ⇒ transient (rethrown; autoStart swallows
|
|
884
|
+
// and the lifecycle's online/wake triggers retry).
|
|
885
|
+
if (credentialResolver && !authCredentials.getAuthToken()) {
|
|
886
|
+
const token = await credentialResolver();
|
|
887
|
+
if (!token) {
|
|
888
|
+
throw new AbloAuthenticationError('Auth resolver returned null before connect — the user is not signed in.', { code: 'auth_no_credentials' });
|
|
889
|
+
}
|
|
890
|
+
authCredentials.setAuthToken(token);
|
|
891
|
+
}
|
|
834
892
|
// Register the caller's own database for write-back BEFORE bootstrap, so
|
|
835
893
|
// the server resolves this org's data plane to the customer's DB rather
|
|
836
894
|
// than serving an empty/wrong store. The org is derived server-side from
|
|
@@ -856,20 +914,15 @@ export function Ablo(options) {
|
|
|
856
914
|
// Resolve identity against the LIVE token, not the construction-time
|
|
857
915
|
// `configuredAuthToken`. Consumers using `getToken` (apps/web) never
|
|
858
916
|
// pass `authToken` at construction — they call `setAuthToken()` before
|
|
859
|
-
// `ready()`, which updates
|
|
917
|
+
// `ready()`, which updates the shared credential source. Reading the frozen
|
|
860
918
|
// `configuredAuthToken` here made `/auth/identity` fire with no Bearer
|
|
861
919
|
// (→ `no_matching_provider` / `session_expired`) even though the JWT
|
|
862
|
-
// was present. Mirrors
|
|
863
|
-
//
|
|
864
|
-
configuredAuthToken:
|
|
920
|
+
// was present. Mirrors every other transport by reading the shared
|
|
921
|
+
// credential source.
|
|
922
|
+
configuredAuthToken: authCredentials.getAuthToken() ?? configuredAuthToken,
|
|
865
923
|
bootstrapHelper,
|
|
924
|
+
auth: authCredentials,
|
|
866
925
|
logger,
|
|
867
|
-
applyRotatedToken: (token) => {
|
|
868
|
-
currentCapabilityToken = token;
|
|
869
|
-
bootstrapHelper.setAuthToken(token);
|
|
870
|
-
const ws = store.getSyncWebSocket();
|
|
871
|
-
ws?.setCapabilityToken(token);
|
|
872
|
-
},
|
|
873
926
|
});
|
|
874
927
|
const { userId, accountScope, teamIds, capabilityToken, syncGroups, participantKind, } = resolved;
|
|
875
928
|
// Fail-loud guard: detect the degenerate "no real sync groups
|
|
@@ -895,8 +948,6 @@ export function Ablo(options) {
|
|
|
895
948
|
'`["org:${orgId}", "user:${userId}"]`) or verify your auth ' +
|
|
896
949
|
'provider populates them. See packages/sync-engine/src/client/identity.ts.', { participantKind, resolvedSyncGroups });
|
|
897
950
|
}
|
|
898
|
-
currentCapabilityToken = capabilityToken;
|
|
899
|
-
bootstrapHelper.setAuthToken(capabilityToken);
|
|
900
951
|
if (resolved.refreshScheduler) {
|
|
901
952
|
_refreshScheduler = resolved.refreshScheduler;
|
|
902
953
|
}
|
|
@@ -962,6 +1013,16 @@ export function Ablo(options) {
|
|
|
962
1013
|
httpStatus: error.httpStatus,
|
|
963
1014
|
error: error.message,
|
|
964
1015
|
});
|
|
1016
|
+
// Clear the memo so a FUTURE `ready()` re-attempts bootstrap instead of
|
|
1017
|
+
// replaying this rejection forever. Bootstrap failures here are
|
|
1018
|
+
// transient by nature — offline, an IndexedDB open timeout, a bootstrap
|
|
1019
|
+
// fetch hiccup — and used to brick the engine until a full page reload
|
|
1020
|
+
// because line ~2013 (`if (_readyPromise) return _readyPromise`) handed
|
|
1021
|
+
// every later caller this same dead promise. Nulling it lets the
|
|
1022
|
+
// provider's online/wake/retry triggers drive a clean re-bootstrap.
|
|
1023
|
+
// (The terminal `_validationError` branch above intentionally stays
|
|
1024
|
+
// cached — config can't change without recreating the engine.)
|
|
1025
|
+
_readyPromise = null;
|
|
965
1026
|
throw error;
|
|
966
1027
|
}
|
|
967
1028
|
})();
|
|
@@ -992,14 +1053,7 @@ export function Ablo(options) {
|
|
|
992
1053
|
}
|
|
993
1054
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
994
1055
|
function authHeaders() {
|
|
995
|
-
|
|
996
|
-
if (currentCapabilityToken) {
|
|
997
|
-
headers.Authorization = `Bearer ${currentCapabilityToken}`;
|
|
998
|
-
}
|
|
999
|
-
else if (configuredAuthToken) {
|
|
1000
|
-
headers.Authorization = `Bearer ${configuredAuthToken}`;
|
|
1001
|
-
}
|
|
1002
|
-
return headers;
|
|
1056
|
+
return authCredentials.withAuthHeaders({ 'Content-Type': 'application/json' });
|
|
1003
1057
|
}
|
|
1004
1058
|
function createClientTxId(idempotencyKey) {
|
|
1005
1059
|
if (idempotencyKey && idempotencyKey.length > 0)
|
|
@@ -1048,11 +1102,15 @@ export function Ablo(options) {
|
|
|
1048
1102
|
return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
|
|
1049
1103
|
}
|
|
1050
1104
|
function modelClaimFromActive(intent) {
|
|
1105
|
+
const description = typeof intent.target.meta?.description === 'string'
|
|
1106
|
+
? intent.target.meta.description
|
|
1107
|
+
: undefined;
|
|
1051
1108
|
return {
|
|
1052
1109
|
id: intent.id,
|
|
1053
1110
|
actor: intent.heldBy,
|
|
1054
1111
|
participantKind: intent.participantKind,
|
|
1055
1112
|
action: intent.reason,
|
|
1113
|
+
...(description ? { description } : {}),
|
|
1056
1114
|
field: intent.target.field,
|
|
1057
1115
|
status: 'active',
|
|
1058
1116
|
expiresAt: intent.expiresAt,
|
|
@@ -1072,6 +1130,7 @@ export function Ablo(options) {
|
|
|
1072
1130
|
actor: intent.heldBy,
|
|
1073
1131
|
participantKind: intent.participantKind,
|
|
1074
1132
|
action: intent.action,
|
|
1133
|
+
...(intent.description ? { description: intent.description } : {}),
|
|
1075
1134
|
field: intent.target.field,
|
|
1076
1135
|
status: 'queued',
|
|
1077
1136
|
position: intent.position,
|
|
@@ -1320,7 +1379,6 @@ export function Ablo(options) {
|
|
|
1320
1379
|
const res = await fetchImpl(`${bootstrapHelper.baseUrl}/sync/query`, {
|
|
1321
1380
|
method: 'POST',
|
|
1322
1381
|
headers: authHeaders(),
|
|
1323
|
-
credentials: 'include',
|
|
1324
1382
|
body: JSON.stringify({
|
|
1325
1383
|
queries: [
|
|
1326
1384
|
{
|
|
@@ -1362,59 +1420,59 @@ export function Ablo(options) {
|
|
|
1362
1420
|
}
|
|
1363
1421
|
function model(name) {
|
|
1364
1422
|
return {
|
|
1365
|
-
retrieve(
|
|
1366
|
-
return retrieveModel(name, id,
|
|
1423
|
+
retrieve(params) {
|
|
1424
|
+
return retrieveModel(name, params.id, params);
|
|
1367
1425
|
},
|
|
1368
|
-
async create(
|
|
1369
|
-
const id =
|
|
1370
|
-
await applyClaimedPolicy({ model: name, id },
|
|
1426
|
+
async create(params) {
|
|
1427
|
+
const id = params.id ?? createModelId();
|
|
1428
|
+
await applyClaimedPolicy({ model: name, id }, params);
|
|
1371
1429
|
return commits.create({
|
|
1372
|
-
intent:
|
|
1373
|
-
idempotencyKey:
|
|
1374
|
-
readAt:
|
|
1375
|
-
onStale:
|
|
1376
|
-
wait:
|
|
1430
|
+
intent: params.intent,
|
|
1431
|
+
idempotencyKey: params.idempotencyKey,
|
|
1432
|
+
readAt: params.readAt,
|
|
1433
|
+
onStale: params.onStale,
|
|
1434
|
+
wait: params.wait,
|
|
1377
1435
|
operations: [
|
|
1378
1436
|
{
|
|
1379
1437
|
action: 'create',
|
|
1380
1438
|
model: name,
|
|
1381
1439
|
id,
|
|
1382
|
-
data,
|
|
1440
|
+
data: params.data,
|
|
1383
1441
|
},
|
|
1384
1442
|
],
|
|
1385
1443
|
});
|
|
1386
1444
|
},
|
|
1387
|
-
async update(
|
|
1388
|
-
await applyClaimedPolicy({ model: name, id },
|
|
1445
|
+
async update(params) {
|
|
1446
|
+
await applyClaimedPolicy({ model: name, id: params.id }, params);
|
|
1389
1447
|
return commits.create({
|
|
1390
|
-
intent:
|
|
1391
|
-
idempotencyKey:
|
|
1392
|
-
readAt:
|
|
1393
|
-
onStale:
|
|
1394
|
-
wait:
|
|
1448
|
+
intent: params.intent,
|
|
1449
|
+
idempotencyKey: params.idempotencyKey,
|
|
1450
|
+
readAt: params.readAt,
|
|
1451
|
+
onStale: params.onStale,
|
|
1452
|
+
wait: params.wait,
|
|
1395
1453
|
operations: [
|
|
1396
1454
|
{
|
|
1397
1455
|
action: 'update',
|
|
1398
1456
|
model: name,
|
|
1399
|
-
id,
|
|
1400
|
-
data,
|
|
1457
|
+
id: params.id,
|
|
1458
|
+
data: params.data,
|
|
1401
1459
|
},
|
|
1402
1460
|
],
|
|
1403
1461
|
});
|
|
1404
1462
|
},
|
|
1405
|
-
async delete(
|
|
1406
|
-
await applyClaimedPolicy({ model: name, id },
|
|
1463
|
+
async delete(params) {
|
|
1464
|
+
await applyClaimedPolicy({ model: name, id: params.id }, params);
|
|
1407
1465
|
return commits.create({
|
|
1408
|
-
intent:
|
|
1409
|
-
idempotencyKey:
|
|
1410
|
-
readAt:
|
|
1411
|
-
onStale:
|
|
1412
|
-
wait:
|
|
1466
|
+
intent: params.intent,
|
|
1467
|
+
idempotencyKey: params.idempotencyKey,
|
|
1468
|
+
readAt: params.readAt,
|
|
1469
|
+
onStale: params.onStale,
|
|
1470
|
+
wait: params.wait,
|
|
1413
1471
|
operations: [
|
|
1414
1472
|
{
|
|
1415
1473
|
action: 'delete',
|
|
1416
1474
|
model: name,
|
|
1417
|
-
id,
|
|
1475
|
+
id: params.id,
|
|
1418
1476
|
},
|
|
1419
1477
|
],
|
|
1420
1478
|
});
|
|
@@ -1426,14 +1484,29 @@ export function Ablo(options) {
|
|
|
1426
1484
|
ready,
|
|
1427
1485
|
waitForFlush,
|
|
1428
1486
|
setAuthToken(token) {
|
|
1429
|
-
//
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1487
|
+
// The single credential source is read lazily by bootstrap HTTP,
|
|
1488
|
+
// lazy query HTTP, network probes, and WebSocket reconnect URL auth.
|
|
1489
|
+
// Updating it here is enough for the next request/connect to use the
|
|
1490
|
+
// refreshed token; no per-transport patching.
|
|
1491
|
+
authCredentials.setAuthToken(token);
|
|
1492
|
+
// A fresh credential is useless to a connection parked in offline /
|
|
1493
|
+
// backoff / auth_blocked until the next probe trigger — so kick one now.
|
|
1494
|
+
// Harmless while connected (the FSM ignores the nudge there).
|
|
1495
|
+
store.nudgeReconnect();
|
|
1496
|
+
},
|
|
1497
|
+
async getAuthToken() {
|
|
1498
|
+
// The live short-lived bearer (set via `setAuthToken`/`getToken` refresh)
|
|
1499
|
+
// is the canonical credential; fall back to a configured API key.
|
|
1500
|
+
return (authCredentials.getAuthToken() ??
|
|
1501
|
+
(await resolveApiKeyValue(configuredApiKey)) ??
|
|
1502
|
+
configuredAuthToken ??
|
|
1503
|
+
null);
|
|
1504
|
+
},
|
|
1505
|
+
setCredentialRefresher(refresher) {
|
|
1506
|
+
store.setCredentialRefresher(refresher);
|
|
1507
|
+
},
|
|
1508
|
+
nudgeReconnect() {
|
|
1509
|
+
store.nudgeReconnect();
|
|
1437
1510
|
},
|
|
1438
1511
|
sessions: {
|
|
1439
1512
|
// Stripe `ephemeralKeys.create` shape: a BACKEND (holding `sk_`) mints a
|
|
@@ -1579,10 +1652,7 @@ export function Ablo(options) {
|
|
|
1579
1652
|
async beginTurn(beginOptions) {
|
|
1580
1653
|
const baseUrl = url.replace(/\/+$/, '');
|
|
1581
1654
|
const turnUrl = `${baseUrl.replace(/^ws/, 'http')}/api/agent/turn`;
|
|
1582
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
1583
|
-
if (currentCapabilityToken) {
|
|
1584
|
-
headers.Authorization = `Bearer ${currentCapabilityToken}`;
|
|
1585
|
-
}
|
|
1655
|
+
const headers = authCredentials.withAuthHeaders({ 'Content-Type': 'application/json' });
|
|
1586
1656
|
const res = await fetch(turnUrl, {
|
|
1587
1657
|
method: 'POST',
|
|
1588
1658
|
headers,
|
|
@@ -200,12 +200,20 @@ export interface AgentModelMutationOptions extends Omit<ModelMutationOptions, 'i
|
|
|
200
200
|
} | null;
|
|
201
201
|
}
|
|
202
202
|
export interface AgentModelClient<T = Record<string, unknown>> {
|
|
203
|
-
retrieve(
|
|
204
|
-
|
|
203
|
+
retrieve(params: AgentModelReadOptions & {
|
|
204
|
+
readonly id: string;
|
|
205
|
+
}): Promise<ModelRead<T>>;
|
|
206
|
+
create(params: AgentModelMutationOptions & {
|
|
207
|
+
readonly data: Record<string, unknown>;
|
|
205
208
|
readonly id?: string | null;
|
|
206
209
|
}): Promise<CommitReceipt>;
|
|
207
|
-
update(
|
|
208
|
-
|
|
210
|
+
update(params: AgentModelMutationOptions & {
|
|
211
|
+
readonly id: string;
|
|
212
|
+
readonly data: Record<string, unknown>;
|
|
213
|
+
}): Promise<CommitReceipt>;
|
|
214
|
+
delete(params: AgentModelMutationOptions & {
|
|
215
|
+
readonly id: string;
|
|
216
|
+
}): Promise<CommitReceipt>;
|
|
209
217
|
}
|
|
210
218
|
export interface AgentRunContext {
|
|
211
219
|
readonly task: Task;
|
|
@@ -228,5 +236,13 @@ export interface AbloApi {
|
|
|
228
236
|
agent(id: string, options: AgentOptions): Agent;
|
|
229
237
|
model<T = Record<string, unknown>>(name: string): ModelClient<T>;
|
|
230
238
|
beginTurn(options: TaskCreateOptions): Promise<Turn>;
|
|
239
|
+
/**
|
|
240
|
+
* Resolve the active bearer credential this client authenticates with — the
|
|
241
|
+
* same token its own requests carry in `Authorization`. Returns `null` when
|
|
242
|
+
* no credential is configured. Async because the API key may be supplied as
|
|
243
|
+
* an async setter. Use it to authenticate side-band requests to the same
|
|
244
|
+
* sync-server (e.g. the S3 presign endpoint) without re-minting.
|
|
245
|
+
*/
|
|
246
|
+
getAuthToken(): Promise<string | null>;
|
|
231
247
|
}
|
|
232
248
|
export declare function createProtocolClient(options: AbloApiClientOptions): AbloApi;
|
package/dist/client/ApiClient.js
CHANGED
|
@@ -171,32 +171,36 @@ export function createProtocolClient(options) {
|
|
|
171
171
|
function createAgentModelClient(agentClient, name) {
|
|
172
172
|
const base = agentClient.model(name);
|
|
173
173
|
return {
|
|
174
|
-
retrieve(
|
|
174
|
+
retrieve(params) {
|
|
175
175
|
// Reads are never blocked by a claim (coordination.md): a claim
|
|
176
176
|
// serializes WRITERS, not readers. So — unlike the create/update/
|
|
177
177
|
// delete paths below — retrieve does NOT apply the agent claimed
|
|
178
178
|
// default; options pass through and the read path's `'return'`
|
|
179
179
|
// default keeps a claimed row readable. A caller can still opt into
|
|
180
180
|
// gating with an explicit `ifClaimed` (developer's choice).
|
|
181
|
-
return base.retrieve(
|
|
181
|
+
return base.retrieve(params);
|
|
182
182
|
},
|
|
183
|
-
create(
|
|
184
|
-
const id =
|
|
185
|
-
return withAgentIntent(agentClient, name, id,
|
|
186
|
-
...stripAgentRuntimeOptions(
|
|
183
|
+
create(params) {
|
|
184
|
+
const id = params.id ?? createModelId();
|
|
185
|
+
return withAgentIntent(agentClient, name, id, params, (commitIntent) => base.create({
|
|
186
|
+
...stripAgentRuntimeOptions(params),
|
|
187
187
|
id,
|
|
188
|
+
data: params.data,
|
|
188
189
|
intent: commitIntent,
|
|
189
190
|
}));
|
|
190
191
|
},
|
|
191
|
-
update(
|
|
192
|
-
return withAgentIntent(agentClient, name, id,
|
|
193
|
-
...stripAgentRuntimeOptions(
|
|
192
|
+
update(params) {
|
|
193
|
+
return withAgentIntent(agentClient, name, params.id, params, (commitIntent) => base.update({
|
|
194
|
+
...stripAgentRuntimeOptions(params),
|
|
195
|
+
id: params.id,
|
|
196
|
+
data: params.data,
|
|
194
197
|
intent: commitIntent,
|
|
195
198
|
}));
|
|
196
199
|
},
|
|
197
|
-
delete(
|
|
198
|
-
return withAgentIntent(agentClient, name, id,
|
|
199
|
-
...stripAgentRuntimeOptions(
|
|
200
|
+
delete(params) {
|
|
201
|
+
return withAgentIntent(agentClient, name, params.id, params, (commitIntent) => base.delete({
|
|
202
|
+
...stripAgentRuntimeOptions(params),
|
|
203
|
+
id: params.id,
|
|
200
204
|
intent: commitIntent,
|
|
201
205
|
}));
|
|
202
206
|
},
|
|
@@ -562,14 +566,39 @@ export function createProtocolClient(options) {
|
|
|
562
566
|
return waitForNoIntents(target, options);
|
|
563
567
|
},
|
|
564
568
|
};
|
|
565
|
-
async function
|
|
566
|
-
|
|
567
|
-
|
|
569
|
+
async function listModel(modelName, options) {
|
|
570
|
+
const params = new URLSearchParams();
|
|
571
|
+
if (options?.limit !== undefined)
|
|
572
|
+
params.set('limit', String(options.limit));
|
|
573
|
+
if (options?.orderBy) {
|
|
574
|
+
const [col, dir] = Object.entries(options.orderBy)[0] ?? [];
|
|
575
|
+
if (col) {
|
|
576
|
+
params.set('order_by', col);
|
|
577
|
+
if (dir === 'desc')
|
|
578
|
+
params.set('order', 'desc');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// The collection route turns any non-reserved query param into an equality
|
|
582
|
+
// filter (`?status=todo`). The wire is AND-only equality — matches what a
|
|
583
|
+
// stateless reactor needs; richer predicates stay on the stateful path.
|
|
584
|
+
if (options?.where && typeof options.where === 'object') {
|
|
585
|
+
for (const [k, v] of Object.entries(options.where)) {
|
|
586
|
+
if (v !== undefined && v !== null && typeof v !== 'object')
|
|
587
|
+
params.set(k, String(v));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const qs = params.toString();
|
|
591
|
+
const res = await requestJson(`/v1/models/${encodeURIComponent(modelName)}${qs ? `?${qs}` : ''}`, { method: 'GET' });
|
|
592
|
+
return res.data ?? [];
|
|
593
|
+
}
|
|
594
|
+
async function retrieveModel(modelName, params) {
|
|
595
|
+
await applyClaimedPolicy({ model: modelName, id: params.id }, params);
|
|
596
|
+
const query = await requestJson(`/v1/models/${encodeURIComponent(modelName)}/${encodeURIComponent(params.id)}`, {
|
|
568
597
|
method: 'GET',
|
|
569
598
|
});
|
|
570
599
|
const data = query.data;
|
|
571
600
|
if (!data) {
|
|
572
|
-
throw new AbloValidationError(`Model row not found: ${modelName}/${id}`, { code: 'model_not_found' });
|
|
601
|
+
throw new AbloValidationError(`Model row not found: ${modelName}/${params.id}`, { code: 'model_not_found' });
|
|
573
602
|
}
|
|
574
603
|
return {
|
|
575
604
|
data,
|
|
@@ -622,22 +651,126 @@ export function createProtocolClient(options) {
|
|
|
622
651
|
};
|
|
623
652
|
}
|
|
624
653
|
function model(name) {
|
|
654
|
+
// Durable lease + FIFO wait-line over HTTP (the existing claim routes). A
|
|
655
|
+
// claim is server state, not a subscription — acquire/hold/release are plain
|
|
656
|
+
// request/response, so a stateless agent participates in coordination too.
|
|
657
|
+
const claimPath = (id) => `/v1/models/${encodeURIComponent(name)}/${encodeURIComponent(id)}/claim`;
|
|
658
|
+
const isClaimHandle = (value) => typeof value === 'object' &&
|
|
659
|
+
value !== null &&
|
|
660
|
+
value.object === 'claim' &&
|
|
661
|
+
typeof value.claimId === 'string' &&
|
|
662
|
+
typeof value.release === 'function';
|
|
663
|
+
const claimMeta = (options) => {
|
|
664
|
+
if (!options?.description)
|
|
665
|
+
return options?.meta;
|
|
666
|
+
return { ...(options.meta ?? {}), description: options.description };
|
|
667
|
+
};
|
|
668
|
+
const acquireClaim = async (params) => {
|
|
669
|
+
const body = await requestJson(claimPath(params.id), {
|
|
670
|
+
method: 'POST',
|
|
671
|
+
body: JSON.stringify({
|
|
672
|
+
action: params.action ?? 'editing',
|
|
673
|
+
...(params.ttl !== undefined ? { ttl: params.ttl } : {}),
|
|
674
|
+
...(params.description !== undefined ? { description: params.description } : {}),
|
|
675
|
+
...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
|
|
676
|
+
// `wait` (default true) → queue behind the holder; false → fail-fast
|
|
677
|
+
// with AbloClaimedError (work-distribution dedup).
|
|
678
|
+
queue: params.wait ?? true,
|
|
679
|
+
}),
|
|
680
|
+
});
|
|
681
|
+
if (body.status === 'queued') {
|
|
682
|
+
throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position ?? 0}. ` +
|
|
683
|
+
`The HTTP client cannot await the grant without a WebSocket.`, { code: 'intent_queued' });
|
|
684
|
+
}
|
|
685
|
+
return body.intent?.id ?? body.id ?? body.intentId ?? createIntentId();
|
|
686
|
+
};
|
|
687
|
+
const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
|
|
688
|
+
async function claimImpl(params) {
|
|
689
|
+
const claimId = await acquireClaim(params);
|
|
690
|
+
const { data } = await retrieveModel(name, { id: params.id });
|
|
691
|
+
const release = () => releaseClaim(params);
|
|
692
|
+
return {
|
|
693
|
+
object: 'claim',
|
|
694
|
+
claimId,
|
|
695
|
+
target: {
|
|
696
|
+
model: name,
|
|
697
|
+
id: params.id,
|
|
698
|
+
...(params.field ? { field: params.field } : {}),
|
|
699
|
+
...(params.path ? { path: params.path } : {}),
|
|
700
|
+
...(params.range ? { range: params.range } : {}),
|
|
701
|
+
...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
|
|
702
|
+
},
|
|
703
|
+
action: params.action ?? 'editing',
|
|
704
|
+
...(params.description ? { description: params.description } : {}),
|
|
705
|
+
data,
|
|
706
|
+
release,
|
|
707
|
+
revoke: () => {
|
|
708
|
+
void release().catch(() => { });
|
|
709
|
+
},
|
|
710
|
+
[Symbol.asyncDispose]: release,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const intentsForEntity = async (params) => requestJson(`/v1/intents?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(params.field)}` : ''}`, { method: 'GET' });
|
|
714
|
+
const claim = Object.assign(claimImpl, {
|
|
715
|
+
release: releaseClaim,
|
|
716
|
+
state: async (params) => {
|
|
717
|
+
const res = await intentsForEntity(params);
|
|
718
|
+
return res.intents?.[0] ?? null;
|
|
719
|
+
},
|
|
720
|
+
queue: async (params) => {
|
|
721
|
+
const res = await intentsForEntity(params);
|
|
722
|
+
return { object: 'list', data: res.queue ?? [] };
|
|
723
|
+
},
|
|
724
|
+
reorder: async (params) => {
|
|
725
|
+
await requestJson(`${claimPath(params.id)}/reorder`, {
|
|
726
|
+
method: 'POST',
|
|
727
|
+
// The reorder route's payload is `{ heldBy, intentId }[]` — Intent's id
|
|
728
|
+
// IS the intentId.
|
|
729
|
+
body: JSON.stringify({ order: params.order.map((i) => ({ heldBy: i.heldBy, intentId: i.id })) }),
|
|
730
|
+
});
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
const withMutationClaim = async (id, input, run) => {
|
|
734
|
+
const claimInput = input?.claim;
|
|
735
|
+
if (!claimInput)
|
|
736
|
+
return run(input);
|
|
737
|
+
if (isClaimHandle(claimInput)) {
|
|
738
|
+
return run({ ...input, intent: { id: claimInput.claimId }, claim: undefined });
|
|
739
|
+
}
|
|
740
|
+
const claimId = await acquireClaim({ id, ...claimInput });
|
|
741
|
+
try {
|
|
742
|
+
return await run({ ...input, intent: { id: claimId }, claim: undefined });
|
|
743
|
+
}
|
|
744
|
+
finally {
|
|
745
|
+
await releaseClaim({ id }).catch(() => { });
|
|
746
|
+
}
|
|
747
|
+
};
|
|
625
748
|
return {
|
|
626
|
-
|
|
627
|
-
|
|
749
|
+
claim,
|
|
750
|
+
retrieve(params) {
|
|
751
|
+
return retrieveModel(name, params);
|
|
628
752
|
},
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
await applyClaimedPolicy({ model: name, id }, mutationOptions);
|
|
632
|
-
return mutateModel('create', name, id, data, mutationOptions);
|
|
753
|
+
list(options) {
|
|
754
|
+
return listModel(name, options);
|
|
633
755
|
},
|
|
634
|
-
async
|
|
635
|
-
|
|
636
|
-
return
|
|
756
|
+
async create(params) {
|
|
757
|
+
const id = params.id ?? createModelId();
|
|
758
|
+
return withMutationClaim(id, params, async (options) => {
|
|
759
|
+
await applyClaimedPolicy({ model: name, id }, options);
|
|
760
|
+
return mutateModel('create', name, id, params.data, options);
|
|
761
|
+
});
|
|
637
762
|
},
|
|
638
|
-
async
|
|
639
|
-
|
|
640
|
-
|
|
763
|
+
async update(params) {
|
|
764
|
+
return withMutationClaim(params.id, params, async (options) => {
|
|
765
|
+
await applyClaimedPolicy({ model: name, id: params.id }, options);
|
|
766
|
+
return mutateModel('update', name, params.id, params.data, options);
|
|
767
|
+
});
|
|
768
|
+
},
|
|
769
|
+
async delete(params) {
|
|
770
|
+
return withMutationClaim(params.id, params, async (options) => {
|
|
771
|
+
await applyClaimedPolicy({ model: name, id: params.id }, options);
|
|
772
|
+
return mutateModel('delete', name, params.id, undefined, options);
|
|
773
|
+
});
|
|
641
774
|
},
|
|
642
775
|
};
|
|
643
776
|
}
|
|
@@ -652,6 +785,11 @@ export function createProtocolClient(options) {
|
|
|
652
785
|
commits,
|
|
653
786
|
model,
|
|
654
787
|
agent: createAgent,
|
|
788
|
+
async getAuthToken() {
|
|
789
|
+
// Mirror `authHeaders()`: a configured API key wins, else the
|
|
790
|
+
// construction-time auth token. Resolve the (possibly async) key setter.
|
|
791
|
+
return (await resolveApiKeyValue(configuredApiKey)) ?? configuredAuthToken ?? null;
|
|
792
|
+
},
|
|
655
793
|
async beginTurn(turnOptions) {
|
|
656
794
|
const task = await tasks.create(turnOptions);
|
|
657
795
|
let closed = false;
|