@abloatai/ablo 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/BaseSyncedStore.d.ts +5 -4
  3. package/dist/BaseSyncedStore.js +38 -14
  4. package/dist/NetworkMonitor.js +3 -1
  5. package/dist/ObjectPool.d.ts +14 -1
  6. package/dist/ObjectPool.js +8 -1
  7. package/dist/SyncClient.js +7 -1
  8. package/dist/SyncEngineContext.js +2 -0
  9. package/dist/ai-sdk/coordination-context.d.ts +2 -1
  10. package/dist/ai-sdk/coordination-context.js +3 -3
  11. package/dist/ai-sdk/index.d.ts +3 -4
  12. package/dist/ai-sdk/index.js +2 -3
  13. package/dist/ai-sdk/wrap.d.ts +13 -18
  14. package/dist/ai-sdk/wrap.js +2 -6
  15. package/dist/cli.cjs +459 -205
  16. package/dist/client/Ablo.d.ts +116 -23
  17. package/dist/client/Ablo.js +128 -38
  18. package/dist/client/ApiClient.d.ts +2 -2
  19. package/dist/client/ApiClient.js +27 -32
  20. package/dist/client/auth.d.ts +26 -0
  21. package/dist/client/auth.js +209 -1
  22. package/dist/client/createModelProxy.d.ts +16 -11
  23. package/dist/client/createModelProxy.js +15 -15
  24. package/dist/client/sessionMint.d.ts +10 -0
  25. package/dist/client/sessionMint.js +8 -1
  26. package/dist/coordination/schema.d.ts +10 -10
  27. package/dist/coordination/schema.js +7 -8
  28. package/dist/coordination/trace.d.ts +91 -0
  29. package/dist/coordination/trace.js +147 -0
  30. package/dist/core/StoreManager.js +3 -1
  31. package/dist/errorCodes.d.ts +2 -0
  32. package/dist/errorCodes.js +2 -0
  33. package/dist/errors.d.ts +10 -2
  34. package/dist/errors.js +20 -9
  35. package/dist/index.d.ts +7 -1
  36. package/dist/index.js +12 -0
  37. package/dist/interfaces/index.d.ts +45 -2
  38. package/dist/policy/types.d.ts +33 -9
  39. package/dist/policy/types.js +42 -11
  40. package/dist/react/AbloProvider.d.ts +2 -2
  41. package/dist/schema/ddl.d.ts +36 -0
  42. package/dist/schema/ddl.js +66 -0
  43. package/dist/schema/index.d.ts +1 -1
  44. package/dist/schema/index.js +1 -1
  45. package/dist/surface.d.ts +1 -1
  46. package/dist/surface.js +2 -0
  47. package/dist/sync/HydrationCoordinator.js +7 -3
  48. package/dist/sync/SyncWebSocket.d.ts +11 -2
  49. package/dist/sync/SyncWebSocket.js +68 -4
  50. package/dist/sync/awaitClaimGrant.js +9 -2
  51. package/dist/sync/createClaimStream.d.ts +9 -2
  52. package/dist/sync/createClaimStream.js +41 -7
  53. package/dist/sync/participants.d.ts +12 -11
  54. package/dist/sync/participants.js +5 -5
  55. package/dist/transactions/TransactionQueue.d.ts +1 -0
  56. package/dist/transactions/TransactionQueue.js +40 -3
  57. package/dist/types/streams.d.ts +57 -135
  58. package/dist/wire/errorEnvelope.d.ts +13 -0
  59. package/docs/coordination.md +16 -0
  60. package/docs/debugging.md +194 -0
  61. package/docs/index.md +1 -0
  62. package/package.json +1 -1
  63. package/dist/ai-sdk/claim-broadcast.d.ts +0 -83
  64. package/dist/ai-sdk/claim-broadcast.js +0 -79
@@ -0,0 +1,194 @@
1
+ # Debugging & Logs
2
+
3
+ By default the SDK is quiet — it logs only warnings and errors. When you're building a human + agent flow and want to *see* the coordination happen (who claimed what, who's waiting in line, who got preempted), turn on Ablo's diagnostic logging. Every line is prefixed `[Ablo]` so it's obvious which output is ours in a console full of other tools.
4
+
5
+ ## Turn it on
6
+
7
+ ```ts
8
+ import Ablo from '@abloatai/ablo';
9
+ import { schema } from './ablo/schema';
10
+
11
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, debug: true });
12
+ ```
13
+
14
+ `debug: true` is the simple switch. For finer control use `logLevel`, or set it without touching code via the `ABLO_LOG_LEVEL` environment variable.
15
+
16
+ ```ts
17
+ Ablo({ schema, apiKey }) // quiet — warnings + errors only (default)
18
+ Ablo({ schema, apiKey, debug: true }) // everything (coordination + lifecycle)
19
+ Ablo({ schema, apiKey, logLevel: 'info' }) // coordination + connection, no per-model noise
20
+ ```
21
+
22
+ ```bash
23
+ ABLO_LOG_LEVEL=debug npm run dev # same, from the environment
24
+ ```
25
+
26
+ ### Levels
27
+
28
+ | Level | What it shows |
29
+ |---|---|
30
+ | `silent` | nothing |
31
+ | `error` | failures only |
32
+ | `warn` | **default** — warnings + errors |
33
+ | `info` | the above + the **coordination trace** (claims, grants, queueing) + connection state |
34
+ | `debug` | the above + internal lifecycle (per-model registration, store hydration) — the full firehose |
35
+
36
+ Precedence: an explicit `logLevel` wins, then `debug: true` (⇒ `debug`), then `ABLO_LOG_LEVEL`, then the `warn` default. `debug: false` (or omitting it) just means "don't raise the level."
37
+
38
+ > For watching coordination, **`logLevel: 'info'` is the sweet spot** — you get the claim trace without the per-model registration chatter that `debug` adds.
39
+
40
+ ## What you'll see — the coordination trace
41
+
42
+ These lines (all at `info`) let you watch the human + agent handover you built:
43
+
44
+ ```
45
+ [Ablo] claim: requesting documents:doc_42 for "editing" (will queue if contended)
46
+ [Ablo] claim: queued for documents:doc_42 — position 2 of 3, waiting
47
+ [Ablo] claim: granted 7f3c… — your turn (waited in queue)
48
+ [Ablo] claim: rejected documents:doc_42 — held by agent_writer
49
+ [Ablo] claim: lost documents:doc_42 (preempted or expired)
50
+ [Ablo] claim: released documents:doc_42
51
+ ```
52
+
53
+ Read it as the lifecycle of one claim:
54
+
55
+ - **`requesting`** — your code (or an agent) called `ablo.<model>.claim(...)`. `(will queue if contended)` appears when you passed `{ queue: true }`.
56
+ - **`queued … position N of M`** — the row was held, so you're waiting in the FIFO line. This is the "an agent is waiting behind a claim" moment; it re-logs only when your position changes, so you can watch it advance.
57
+ - **`granted … your turn`** — you reached the head of the line; the lease is now yours and the row may have changed while you waited.
58
+ - **`rejected … held by <who>`** — your claim was refused because someone else holds it (and the model's policy didn't let you in).
59
+ - **`lost`** — you held the lease and it was taken (preempted by a higher-priority writer, or it expired).
60
+ - **`released`** — you (or `await using`'s scope exit) gave the lease back.
61
+
62
+ ## Where the logs run
63
+
64
+ The coordination trace and the proactive credential refresh run **in the browser** (and any client that holds a live socket) — that's where the human + agent activity is. Server-side code that mints credentials or does one-shot reads won't emit the trace; it has no live session to narrate.
65
+
66
+ ## Bring your own logger
67
+
68
+ Pass a `logger` to route Ablo's output into your own logging stack (Pino, Sentry breadcrumbs, etc.). A custom logger bypasses `debug`/`logLevel` entirely — you decide what to do with each level.
69
+
70
+ ```ts
71
+ Ablo({
72
+ schema,
73
+ apiKey,
74
+ logger: {
75
+ debug: (...a) => {},
76
+ info: (...a) => myLogger.info({ ablo: a }),
77
+ warn: (...a) => myLogger.warn({ ablo: a }),
78
+ error: (...a) => myLogger.error({ ablo: a }),
79
+ },
80
+ });
81
+ ```
82
+
83
+ ## Read the coordination in code — the activity log
84
+
85
+ The console trace above is for *you*, at a terminal. To put the same activity **inside your app** — an activity feed, a "who's editing" badge, a Sentry breadcrumb trail — read it programmatically. Same events, three layers; pick by audience:
86
+
87
+ | Layer | You get | Best for |
88
+ |---|---|---|
89
+ | `logger` (above) | `[Ablo]` text lines | watching a terminal |
90
+ | `observability` | typed `ClaimEvent` / `ConflictEvent` objects | dashboards, alerting (Sentry / Datadog / OTel) |
91
+ | `ClaimLog` | an ordered, **reactive** list of both | rendering an activity feed on a page |
92
+
93
+ ### The events
94
+
95
+ Every claim state change is a `ClaimEvent`; every notify-instead-of-abort stale write (a write that succeeded but whose premise had moved) is a `ConflictEvent`:
96
+
97
+ ```ts
98
+ interface ClaimEvent {
99
+ phase: 'acquired' | 'queued' | 'granted' | 'lost' | 'rejected' | 'expired';
100
+ model?: string; id?: string; field?: string; // the claimed row
101
+ actor?: string; participantKind?: 'user' | 'agent' | 'system';
102
+ position?: number; // FIFO position, when queued
103
+ reason?: string; // why, on rejected
104
+ claimId?: string;
105
+ }
106
+
107
+ interface ConflictEvent {
108
+ clientTxId: string;
109
+ rows: { model: string; id: string; fields: string[]; writtenBy?: 'user' | 'agent' | 'system' }[];
110
+ }
111
+ ```
112
+
113
+ `phase` is past-tense — the state the claim just entered — and maps one-to-one to what arrives on the wire.
114
+
115
+ ### Collect them — `ClaimLog`
116
+
117
+ `ClaimLog` records both into an ordered list. Hand it to `observability`, then read it back:
118
+
119
+ ```ts
120
+ import Ablo, { ClaimLog } from '@abloatai/ablo';
121
+
122
+ const log = new ClaimLog();
123
+ const ablo = Ablo({ schema, apiKey, observability: log });
124
+
125
+ // …run the agents…
126
+ console.log(`${log}`); // a printable, ⚠-marked timeline
127
+ log.entries; // ClaimLogEntry[] — every event, in order, with a `.line`
128
+ log.collisions(); // just the rejected/lost claims + stale writes
129
+ ```
130
+
131
+ It's also the simplest way to **assert** coordination in a test — no log scraping:
132
+
133
+ ```ts
134
+ expect(log.collisions()).toHaveLength(0); // no one stepped on anyone
135
+ ```
136
+
137
+ ### Show it on a page — reactive
138
+
139
+ `ClaimLog.onChange` fires on every event and returns an unsubscribe — the exact shape `useSyncExternalStore` wants, so a live feed is a few lines:
140
+
141
+ ```tsx
142
+ import { useSyncExternalStore } from 'react';
143
+ import { ClaimLog } from '@abloatai/ablo';
144
+
145
+ function ActivityFeed({ log }: { log: ClaimLog }) {
146
+ const entries = useSyncExternalStore(log.onChange, () => log.entries);
147
+ return (
148
+ <ul>
149
+ {entries.map((e) => (
150
+ <li key={e.seq} className={e.collision ? 'text-amber-600' : undefined}>{e.line}</li>
151
+ ))}
152
+ </ul>
153
+ );
154
+ }
155
+ ```
156
+
157
+ > `ClaimLog` lives in browser memory: it starts empty on load and shows events that arrive while mounted. For a feed that survives reload, persist `entries` yourself — but for a live coordination panel, the in-memory log is exactly right.
158
+
159
+ For **"who holds *this* row right now"** (a badge, not a feed), don't use `ClaimLog` — read the reactive claim state directly. It re-renders on change with no extra wiring:
160
+
161
+ ```tsx
162
+ const holder = useAblo((ablo) => ablo.documents.claim.state({ id })); // Claim | null
163
+ ```
164
+
165
+ See [React](./react.md) and [Coordination](./coordination.md) for the claim-read APIs.
166
+
167
+ ### Route to your own backend
168
+
169
+ `ClaimLog` is one implementation of the `observability` slot. To push events into Sentry, Datadog, or OpenTelemetry instead, spread `noopObservability` and override just the two coordination hooks:
170
+
171
+ ```ts
172
+ import Ablo, { noopObservability } from '@abloatai/ablo';
173
+
174
+ const ablo = Ablo({
175
+ schema, apiKey,
176
+ observability: {
177
+ ...noopObservability,
178
+ captureClaim: (e) => {
179
+ if (e.phase === 'rejected') Sentry.captureMessage(`claim blocked: ${e.model}/${e.id} by ${e.actor}`);
180
+ },
181
+ captureConflict: (e) => Sentry.captureMessage(`stale write tx ${e.clientTxId} on ${e.rows.length} row(s)`),
182
+ },
183
+ });
184
+ ```
185
+
186
+ ## Errors
187
+
188
+ Ablo's thrown errors are typed and self-describing — `String(err)` (or logging it) yields one clean line, never a stack dump:
189
+
190
+ ```
191
+ AbloValidationError [model_required_field_missing]: A required field was absent. (see https://docs.abloatai.com/errors#model_required_field_missing) [request_id: req_8Fk2aQ]
192
+ ```
193
+
194
+ Branch on `err.code` (stable) — never on the message (rewordable). See [Client Behavior](./client-behavior.md) for the full error model and which codes are safe to retry.
package/docs/index.md CHANGED
@@ -48,6 +48,7 @@ Three things stay true no matter how you use Ablo:
48
48
  - [Interaction Model](./interaction-model.md) — The schema, claim, update, confirmation loop.
49
49
  - [API Reference](./api.md) — Model-by-model method shape.
50
50
  - [Client Behavior](./client-behavior.md) — Options, errors, retries, timeouts, and imports.
51
+ - [Debugging & Logs](./debugging.md) — Turn on the `[Ablo]` coordination trace (`debug` / `logLevel`) to watch claims, queueing, and grants while you build — or read the same activity in code via `ClaimLog` to render an activity feed.
51
52
  - [Connect Your Database](./data-sources.md) — Keep canonical rows in your app database without giving Ablo database credentials.
52
53
  - [React](./react.md) — Provider, hooks, and reactive reads for React apps.
53
54
  - [API Keys](./api-keys.md) — Bearer tokens for the public API.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "The Collaboration Layer For AI Agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,83 +0,0 @@
1
- /**
2
- * Claim broadcast middleware — wraps a language model so the agent
3
- * declares "I'm about to edit entity X" over the sync engine's
4
- * claim primitive at stream start, and abandons the claim at
5
- * stream end.
6
- *
7
- * Cross-cutting by design — composes via the AI SDK's
8
- * `wrapLanguageModel`. Same middleware works for every chat
9
- * surface, and for non-chat agent loops that share the AI SDK's
10
- * middleware interface (workers, MCP tools, autonomous loops).
11
- *
12
- * Open-source-clean: depends only on `@ai-sdk/provider` types and
13
- * the package's own `SyncAgent`. No app-specific assumptions —
14
- * Ablo's web app uses this, but so can any consumer of `@abloatai/ablo`.
15
- *
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
- */
19
- import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
20
- import type { Ablo } from '../client/Ablo.js';
21
- import type { SchemaRecord } from '../schema/schema.js';
22
- /**
23
- * Target entity for the claim broadcast.
24
- *
25
- * `entityType` is a free-form string — convention is the schema's
26
- * typename (e.g. `'SlideDeck'`, `'Task'`, `'Matter'`) so peers can
27
- * filter consistently. The wire format treats it opaquely.
28
- */
29
- export interface ClaimTarget {
30
- readonly entityType: string;
31
- readonly entityId: string;
32
- /** Optional path for file/document-like targets. */
33
- readonly path?: string;
34
- /** Optional line/column range for partial-entity coordination. */
35
- readonly range?: {
36
- readonly startLine: number;
37
- readonly endLine: number;
38
- readonly startColumn?: number;
39
- readonly endColumn?: number;
40
- };
41
- /**
42
- * Optional sub-field within the entity. Useful when the agent
43
- * knows it's only editing a specific field — peers can filter
44
- * on the field too.
45
- */
46
- readonly field?: string;
47
- /** App-defined structured metadata. Opaque to the core SDK. */
48
- readonly meta?: Record<string, unknown>;
49
- /**
50
- * Hint for the server-side TTL on the claim. Caps at 10 minutes
51
- * server-side; default 60s — typical chat turn.
52
- */
53
- readonly estimatedMs?: number;
54
- }
55
- export interface ClaimBroadcastMiddlewareOptions<R extends SchemaRecord = SchemaRecord> {
56
- /** Connected Ablo. Null disables the middleware (no-op). */
57
- readonly agent: Ablo<R> | null;
58
- /** Target entity. Null skips the broadcast (purely conversational). */
59
- readonly target: ClaimTarget | null;
60
- /**
61
- * Human-readable phase describing what the agent is doing. Convention:
62
- * `'edit'`, `'read'`, `'review'`, `'generate'`. Default `'edit'`. The same
63
- * `reason` field used on every claim surface.
64
- */
65
- readonly reason?: string;
66
- /**
67
- * Peer-visible explanation of the specific work this model call is about to
68
- * perform. Surfaces to other agents through `ActiveClaim.description`.
69
- */
70
- readonly description?: string;
71
- }
72
- /**
73
- * Build the middleware. When `agent` or `target` is null, returns a
74
- * pass-through — keeps call sites unconditional regardless of
75
- * whether the surface has an entity in scope.
76
- *
77
- * Generic over the schema record so callers passing
78
- * `Ablo<typeof schema>` don't have to widen — `Ablo<S>` and
79
- * `Ablo<SchemaRecord>` are structurally non-compatible because the
80
- * widened version collapses model proxies to an index signature
81
- * that clashes with the named methods (`ready`, `dispose`, etc.).
82
- */
83
- export declare function claimBroadcastMiddleware<R extends SchemaRecord = SchemaRecord>(options: ClaimBroadcastMiddlewareOptions<R>): LanguageModelV3Middleware;
@@ -1,79 +0,0 @@
1
- /**
2
- * Claim broadcast middleware — wraps a language model so the agent
3
- * declares "I'm about to edit entity X" over the sync engine's
4
- * claim primitive at stream start, and abandons the claim at
5
- * stream end.
6
- *
7
- * Cross-cutting by design — composes via the AI SDK's
8
- * `wrapLanguageModel`. Same middleware works for every chat
9
- * surface, and for non-chat agent loops that share the AI SDK's
10
- * middleware interface (workers, MCP tools, autonomous loops).
11
- *
12
- * Open-source-clean: depends only on `@ai-sdk/provider` types and
13
- * the package's own `SyncAgent`. No app-specific assumptions —
14
- * Ablo's web app uses this, but so can any consumer of `@abloatai/ablo`.
15
- *
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
- */
19
- /**
20
- * Build the middleware. When `agent` or `target` is null, returns a
21
- * pass-through — keeps call sites unconditional regardless of
22
- * whether the surface has an entity in scope.
23
- *
24
- * Generic over the schema record so callers passing
25
- * `Ablo<typeof schema>` don't have to widen — `Ablo<S>` and
26
- * `Ablo<SchemaRecord>` are structurally non-compatible because the
27
- * widened version collapses model proxies to an index signature
28
- * that clashes with the named methods (`ready`, `dispose`, etc.).
29
- */
30
- export function claimBroadcastMiddleware(options) {
31
- const { agent, target } = options;
32
- const reason = options.reason ?? 'edit';
33
- const description = options.description;
34
- const openClaim = () => {
35
- if (!agent || !target)
36
- return null;
37
- return agent.claims.claim({
38
- type: target.entityType,
39
- id: target.entityId,
40
- path: target.path,
41
- range: target.range,
42
- field: target.field,
43
- meta: target.meta,
44
- }, {
45
- reason,
46
- description,
47
- ttl: target.estimatedMs ?? 60_000,
48
- });
49
- };
50
- return {
51
- specificationVersion: 'v3',
52
- // The AI SDK's middleware contract passes a no-arg `doStream` /
53
- // `doGenerate` thunk — params have already been transformed by
54
- // any earlier `transformParams` middleware in the chain. We
55
- // open the claim, call the inner, abandon when the inner
56
- // resolves (or rejects).
57
- async wrapStream({ doStream }) {
58
- const handle = openClaim();
59
- try {
60
- return await doStream();
61
- }
62
- finally {
63
- // Always abandon — even on error. The server's TTL would
64
- // eventually clean up regardless, but explicit release means
65
- // peers see the claim drop the moment generation completes.
66
- handle?.revoke();
67
- }
68
- },
69
- async wrapGenerate({ doGenerate }) {
70
- const handle = openClaim();
71
- try {
72
- return await doGenerate();
73
- }
74
- finally {
75
- handle?.revoke();
76
- }
77
- },
78
- };
79
- }