@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Claim observability — a `ClaimLog` you can print or assert on.** A new
8
+ `observability` provider hook lets you tap every claim event and stale-write
9
+ collision the client sees. Hand `new ClaimLog()` to `Ablo({ observability })`
10
+ and it collects an ordered, readable log — `formatClaim` / `formatConflict`
11
+ render one line per event, and `collisions()` returns the conflicts for eval
12
+ assertions. New exports: `ClaimLog`, `formatClaim`, `formatConflict`,
13
+ `noopObservability`, and the types `ClaimLogEntry`, `ClaimEvent`,
14
+ `ConflictEvent`, `SyncObservabilityProvider`. Spread `noopObservability` to
15
+ override only the hooks you care about.
16
+
17
+ **AWS-shaped CLI credential store + `ablo config`.** Local CLI state is now split
18
+ into two files, matching `~/.aws/config` vs `~/.aws/credentials`: `config.json`
19
+ holds non-secret settings (active environment + active project) and is safe to
20
+ print or let an agent read; `credentials.json` holds the keys (0600, never
21
+ printed), keyed by project profile then environment. Per-project profiles follow
22
+ Stripe's model — `ablo projects use <slug>` selects the active profile, and a
23
+ key's project is fixed at mint so selecting a project never re-scopes an existing
24
+ key. `ablo status` now reports the resolved profile and environment.
25
+
26
+ **Schema JSON-column reconciliation.** `generateJsonColumnReconciliation` (new
27
+ export) emits the DDL to reconcile JSON-backed columns when adopting or evolving
28
+ an existing schema.
29
+
30
+ **Breaking (0.x):**
31
+ - The claim handle type `ClaimHandle` is renamed to **`Claim`**, and its
32
+ identifier field is `id` (was `claimId`). Update type imports and any code
33
+ reading `.claimId`.
34
+ - The ai-sdk `claimBroadcastMiddleware` (and `./ai-sdk/claim-broadcast`) is
35
+ removed — coordination broadcast is handled by `coordinationContextMiddleware`.
36
+ Import `ClaimTarget` from the package root or `@abloatai/ablo` ai-sdk's
37
+ `coordination-context` instead of `claim-broadcast`. The inline-claim option is
38
+ `reason` (not the pre-0.12 `action`); the ai-sdk docs are corrected to match.
39
+
40
+ ## 0.18.0
41
+
42
+ ### Minor Changes
43
+
44
+ - **Client observability — `debug` / `logLevel`, off by default.** The SDK used to
45
+ emit a `debug` line per model and per property during schema registration (a
46
+ firehose). It now defaults to a quiet `warn` threshold and exposes two new
47
+ `Ablo()` options to opt back in:
48
+ - `logLevel: 'debug' | 'info' | 'warn' | 'error' | 'silent'` — `'info'` surfaces
49
+ coordination and connection events without the per-model registration noise;
50
+ `'debug'` is everything. Precedence: explicit `logLevel` → `debug: true` →
51
+ `ABLO_LOG_LEVEL` env → default `warn`. Supplying your own `logger` bypasses both.
52
+ - `debug: boolean` — shorthand for `logLevel: 'debug'`.
53
+
54
+ Coordination is now traceable at `info`: claims that are **rejected** or **lost**
55
+ (preempted/expired), and your position **advancing in a claim queue**, each log
56
+ once per change with a readable target (`documents:abc.title`) — quiet lowercase
57
+ lines, no shouty tags.
58
+
59
+ **New: canonical wire-egress contract export.** `errorEnvelope`, `statusForType`,
60
+ and the `ErrorEnvelope` type are now exported from the package root. Server
61
+ consumers (e.g. a self-hosted sync server) can assert against the one source of
62
+ truth for the error-envelope shape and the `AbloError`-subclass→HTTP-status
63
+ table instead of keeping a copy that silently drifts.
64
+
65
+ **Structured CLI error rendering.** CLI failures render as a titled block with a
66
+ reason code and per-code remediation (`--verbose` for the stack) instead of a
67
+ console wall-of-text; `AbloError.toString()` produces a leak-proof one-liner.
68
+
69
+ **`ABLO_API_KEY` resolution + sandbox key scopes.** The key is now resolved from
70
+ `.env.local` / `.env` (not just the process env), and sandbox keys are granted
71
+ `schema:push` by default so `ablo push` works out of the box in a fresh sandbox.
72
+
3
73
  ## 0.17.0
4
74
 
5
75
  ### Minor Changes
@@ -505,10 +505,11 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
505
505
  * 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
506
506
  * probe finds the key stale (`credential_stale`) or on a nudge.
507
507
  * 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
508
- * timer inside the TTL, plus re-mint on OS wake / network-online / tab
509
- * focus. Browser-only triggers are env-gated, so Node/agent hosts get
510
- * only the timer (a no-op there agents use a static `apiKey`, no
511
- * resolver, so this is never called for them).
508
+ * timer inside the TTL, plus re-mint on OS wake. The ENTIRE proactive
509
+ * block is browser-gated (`typeof window`): server/SSR has no socket to
510
+ * keep warm and the resolver is browser-oriented, so arming it in Node
511
+ * would fire a relative-URL fetch and throw. (Agents pass a static
512
+ * `apiKey` with no resolver, so this method is never called for them.)
512
513
  *
513
514
  * Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
514
515
  * never call a refresh method. Idempotent (a second call replaces the first);
@@ -739,9 +739,21 @@ export class BaseSyncedStore {
739
739
  // A throw = transient (offline / mint endpoint unreachable / 5xx). The
740
740
  // login may be perfectly valid; never sign out for this — back off and
741
741
  // retry. Mirrors the `getToken` throw-vs-null contract end-to-end.
742
- getContext().logger.warn('[BaseSyncedStore] Access-credential re-mint failed (transient)', {
743
- error: error?.message,
744
- });
742
+ const message = error?.message ?? String(error);
743
+ // A relative-URL resolver invoked server-side (Node fetch has no origin
744
+ // to resolve against) emits the opaque "Failed to parse URL" / "Only
745
+ // absolute URLs are supported". Translate it into something actionable
746
+ // instead of a mystery transient blip — the proactive refresh is now
747
+ // browser-only, so hitting this means the resolver fired from SSR/RSC or
748
+ // a server route.
749
+ if (typeof window === 'undefined' && /parse URL|absolute URLs?/i.test(message)) {
750
+ getContext().logger.warn('credential resolver ran on the server with a relative URL — Node fetch needs an absolute URL. ' +
751
+ 'Refresh the Ablo client in the browser, or build an absolute URL server-side ' +
752
+ "(e.g. new URL('/api/ablo-session', process.env.NEXT_PUBLIC_APP_URL)).", { error: message });
753
+ }
754
+ else {
755
+ getContext().logger.warn('access-credential re-mint failed (transient)', { error: message });
756
+ }
745
757
  return 'network_error';
746
758
  }
747
759
  })();
@@ -769,10 +781,11 @@ export class BaseSyncedStore {
769
781
  * 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
770
782
  * probe finds the key stale (`credential_stale`) or on a nudge.
771
783
  * 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
772
- * timer inside the TTL, plus re-mint on OS wake / network-online / tab
773
- * focus. Browser-only triggers are env-gated, so Node/agent hosts get
774
- * only the timer (a no-op there agents use a static `apiKey`, no
775
- * resolver, so this is never called for them).
784
+ * timer inside the TTL, plus re-mint on OS wake. The ENTIRE proactive
785
+ * block is browser-gated (`typeof window`): server/SSR has no socket to
786
+ * keep warm and the resolver is browser-oriented, so arming it in Node
787
+ * would fire a relative-URL fetch and throw. (Agents pass a static
788
+ * `apiKey` with no resolver, so this method is never called for them.)
776
789
  *
777
790
  * Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
778
791
  * never call a refresh method. Idempotent (a second call replaces the first);
@@ -807,14 +820,25 @@ export class BaseSyncedStore {
807
820
  // 'network_error' → transient (offline / mint hiccup); the next timer tick
808
821
  // or the FSM's own probe retries. Never sign out for it.
809
822
  };
810
- // Comfortably inside the 15m `ek_` TTL; a missed (background-throttled) tick
811
- // is recovered by the next, or by the reactive probe. The timer is the sole
812
- // proactive PRE-ROLL it keeps the key warm ahead of expiry even while the
813
- // socket sits healthy-`connected` (a state the FSM never probes unprompted).
814
- const REFRESH_INTERVAL_MS = 10 * 60 * 1000;
815
- const timer = setInterval(() => void refresh(), REFRESH_INTERVAL_MS);
816
- const teardowns = [() => clearInterval(timer)];
823
+ const teardowns = [];
824
+ // The ENTIRE proactive pre-roll is BROWSER-ONLY. On the server (Next.js
825
+ // SSR/RSC evaluating the `providers` module) there is no live socket to keep
826
+ // warm AND the scaffolded credential resolver is browser-oriented (a
827
+ // relative-URL `fetch('/api/ablo-session')`). Arming the timer server-side
828
+ // fires that resolver in Node, where fetch has no origin to resolve a
829
+ // relative URL against "Failed to parse URL" on every tick. Browser-only
830
+ // refresh is the unanimous vendor model (Supabase `autoRefreshToken: isBrowser()`,
831
+ // Clerk/Ably/Stripe refresh client-side). The reactive re-mint hook
832
+ // (`setCredentialRefresher` above) stays UNCONDITIONAL: it only fires on a
833
+ // real connection probe, which can't happen during a bare SSR module eval.
817
834
  if (typeof window !== 'undefined') {
835
+ // Comfortably inside the 15m `ek_` TTL; a missed (background-throttled)
836
+ // tick is recovered by the next, or by the reactive probe. The timer is
837
+ // the sole proactive PRE-ROLL — it keeps the key warm ahead of expiry even
838
+ // while the socket sits healthy-`connected` (a state the FSM never probes).
839
+ const REFRESH_INTERVAL_MS = 10 * 60 * 1000;
840
+ const timer = setInterval(() => void refresh(), REFRESH_INTERVAL_MS);
841
+ teardowns.push(() => clearInterval(timer));
818
842
  // OS-wake (desktop only): the Electron shell bridges `powerMonitor`
819
843
  // 'resume' to this DOM event. This is the ONE event-trigger the lifecycle
820
844
  // still owns, because `visibilitychange` does NOT fire on wake-from-sleep
@@ -31,7 +31,9 @@ export class NetworkMonitor extends EventEmitter {
31
31
  const wasOnline = this.isOnline;
32
32
  this.isOnline = false;
33
33
  if (wasOnline) {
34
- getContext().logger.warn('Network connection lost');
34
+ // Symmetric with 'Network connection restored' (info) — expected,
35
+ // transient connectivity state, not an actionable warning.
36
+ getContext().logger.info('Network connection lost');
35
37
  this.emit('offline');
36
38
  }
37
39
  };
@@ -111,7 +111,20 @@ export declare class ObjectPool {
111
111
  __class?: string;
112
112
  modelName?: string;
113
113
  id?: string;
114
- }, ModelClass?: new (data: Record<string, unknown>) => Model): Model | null;
114
+ }, ModelClass?: new (data: Record<string, unknown>) => Model, opts?: {
115
+ /**
116
+ * Throw (instead of warn + return null) when the wire names a model
117
+ * this client never registered. Passed by the developer-await read
118
+ * path (`HydrationCoordinator`'s network leg) so a schema collision
119
+ * — the org's pushed schema names `Document` but this client only
120
+ * registered `documents` — surfaces AT the row that failed, with the
121
+ * known-model list and `ablo status` pointer, instead of bubbling up
122
+ * three layers later as a misleading `entity_not_found`. Left off
123
+ * (default) for the continuous delta loop and IDB-cache hydration,
124
+ * where a single unknown typename must not wedge the whole batch.
125
+ */
126
+ strict?: boolean;
127
+ }): Model | null;
115
128
  /**
116
129
  * Clear the object pool
117
130
  * @param options.preserveObserved - If true, keep models that are being observed by React
@@ -603,7 +603,7 @@ export class ObjectPool {
603
603
  create(typename, data) {
604
604
  return this.createFromData({ ...data, __typename: typename });
605
605
  }
606
- createFromData(data, ModelClass) {
606
+ createFromData(data, ModelClass, opts) {
607
607
  // Support multiple model identifier fields for backwards compatibility
608
608
  const modelName = data.__typename ?? data.__class ?? data.modelName ?? 'Unknown';
609
609
  const Constructor = ModelClass || this.registry.getModelByName(modelName);
@@ -613,6 +613,13 @@ export class ObjectPool {
613
613
  getContext().modelDebugLogger?.logError('Unknown', 'CREATE', 'No model identifier found', data);
614
614
  return null;
615
615
  }
616
+ if (opts?.strict) {
617
+ const known = this.registry.getRegisteredModelNames();
618
+ throw new AbloValidationError(`Model "${modelName}" is not registered on this client` +
619
+ (known.length ? ` (known: ${known.join(', ')})` : '') +
620
+ `. The schema pushed to this org may differ from your local ` +
621
+ `schema — run \`ablo status\` to compare.`, { code: 'model_not_registered' });
622
+ }
616
623
  getContext().logger.warn(`ObjectPool.createFromData: No constructor found for model "${modelName}"`, { data });
617
624
  getContext().modelDebugLogger?.logError(modelName, 'CREATE', `No constructor found for model "${modelName}"`, data);
618
625
  return null;
@@ -161,8 +161,14 @@ export class SyncClient extends EventEmitter {
161
161
  // (e.g. `AbloValidationError` with `code: 'schema_...'`,
162
162
  // `AbloServerError` with `httpStatus: 500`). Falling back to
163
163
  // generic message lets us still see unstructured errors.
164
+ // Mechanic-level breadcrumb only. The authoritative, user-facing
165
+ // reason is logged once at `warn` by `TransactionQueue.handleFailure`
166
+ // (`Permanent error - rolling back`). Logging the same typed cause
167
+ // again here at `warn` is what produced three identical dumps per
168
+ // rejected write — keep it at `debug` so the rollback mechanics are
169
+ // available when debugging but don't double the console noise.
164
170
  const abloErr = error instanceof AbloError ? error : undefined;
165
- getContext().logger.warn('[SyncClient.rollback]', {
171
+ getContext().logger.debug('[SyncClient.rollback]', {
166
172
  txType: transaction.type,
167
173
  modelName: transaction.modelName,
168
174
  modelId: transaction.modelId.slice(0, 12),
@@ -28,6 +28,8 @@ export const noopObservability = {
28
28
  captureWebSocketError() { },
29
29
  captureOfflineFlushFailure() { },
30
30
  captureSelfHealing() { },
31
+ captureClaim() { },
32
+ captureConflict() { },
31
33
  captureCommitZeroSyncId() { },
32
34
  startSpan(_name, _op, fn, _attributes) {
33
35
  return fn();
@@ -23,7 +23,8 @@
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 { ClaimTarget } from './claim-broadcast.js';
26
+ import type { ClaimTarget } from '../types/streams.js';
27
+ export type { ClaimTarget };
27
28
  export interface CoordinationContextMiddlewareOptions<R extends SchemaRecord = SchemaRecord> {
28
29
  readonly agent: Ablo<R> | null;
29
30
  readonly target: ClaimTarget | null;
@@ -38,8 +38,8 @@ export function coordinationContextMiddleware(options) {
38
38
  return params;
39
39
  // Read peer claims on the same target. Synchronous lookup
40
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
- claim.target.id === target.entityId &&
41
+ const peerClaims = agent.claims.others.filter((claim) => claim.target.type === target.type &&
42
+ claim.target.id === target.id &&
43
43
  targetsOverlap(claim.target, target) &&
44
44
  !excludeClaimIds.has(claim.id));
45
45
  if (peerClaims.length === 0)
@@ -77,7 +77,7 @@ function targetsOverlap(claimTarget, target) {
77
77
  * "AI knows," not "AI gets a wall of text."
78
78
  */
79
79
  function formatCoordinationNote(claims, target) {
80
- const entityLabel = target.entityType.toLowerCase();
80
+ const entityLabel = target.type.toLowerCase();
81
81
  if (claims.length === 1) {
82
82
  const c = claims[0];
83
83
  const details = c.description ? `Declared work: ${c.description}. ` : '';
@@ -27,7 +27,7 @@
27
27
  * wait: 'confirmed',
28
28
  * claim: {
29
29
  * field: 'title',
30
- * action: 'renaming',
30
+ * reason: 'renaming',
31
31
  * description,
32
32
  * },
33
33
  * });
@@ -51,7 +51,7 @@
51
51
  * ```ts
52
52
  * const claim = await ablo.tasks.claim({
53
53
  * id,
54
- * action: 'rewriting',
54
+ * reason: 'rewriting',
55
55
  * description: 'Rewriting the task brief before updating follow-up fields.',
56
56
  * ttl: '2m',
57
57
  * });
@@ -72,6 +72,5 @@
72
72
  * to one entity before any tool is chosen; tool implementations stay exactly
73
73
  * the same.
74
74
  */
75
- export { claimBroadcastMiddleware, type ClaimTarget, type ClaimBroadcastMiddlewareOptions, } from './claim-broadcast.js';
76
- export { coordinationContextMiddleware, type CoordinationContextMiddlewareOptions, } from './coordination-context.js';
75
+ export { coordinationContextMiddleware, type CoordinationContextMiddlewareOptions, type ClaimTarget, } from './coordination-context.js';
77
76
  export { wrapWithMultiplayer, type WrapWithMultiplayerOptions } from './wrap.js';
@@ -27,7 +27,7 @@
27
27
  * wait: 'confirmed',
28
28
  * claim: {
29
29
  * field: 'title',
30
- * action: 'renaming',
30
+ * reason: 'renaming',
31
31
  * description,
32
32
  * },
33
33
  * });
@@ -51,7 +51,7 @@
51
51
  * ```ts
52
52
  * const claim = await ablo.tasks.claim({
53
53
  * id,
54
- * action: 'rewriting',
54
+ * reason: 'rewriting',
55
55
  * description: 'Rewriting the task brief before updating follow-up fields.',
56
56
  * ttl: '2m',
57
57
  * });
@@ -72,6 +72,5 @@
72
72
  * to one entity before any tool is chosen; tool implementations stay exactly
73
73
  * the same.
74
74
  */
75
- export { claimBroadcastMiddleware, } from './claim-broadcast.js';
76
75
  export { coordinationContextMiddleware, } from './coordination-context.js';
77
76
  export { wrapWithMultiplayer } from './wrap.js';
@@ -14,8 +14,6 @@
14
14
  * model: anthropic('claude-opus-4-7'),
15
15
  * agent,
16
16
  * target: { entityType: 'SlideDeck', entityId: 'deck-abc' },
17
- * reason: 'renaming',
18
- * description: 'Renaming the deck title to match the project brief.',
19
17
  * });
20
18
  *
21
19
  * const result = streamText({
@@ -31,25 +29,22 @@ import { wrapLanguageModel } from 'ai';
31
29
  import type { LanguageModelV3, LanguageModelV3Middleware } from '@ai-sdk/provider';
32
30
  import type { Ablo } from '../client/Ablo.js';
33
31
  import type { SchemaRecord } from '../schema/schema.js';
34
- import { type ClaimTarget } from './claim-broadcast.js';
35
- export interface WrapWithMultiplayerOptions {
32
+ import { type ClaimTarget } from './coordination-context.js';
33
+ export interface WrapWithMultiplayerOptions<R extends SchemaRecord = SchemaRecord> {
36
34
  /** The base language model to wrap. Consumer brings their own. */
37
35
  readonly model: LanguageModelV3;
38
- /** Connected SyncAgent. Null = pass-through wrap (no broadcast, no read). */
39
- readonly agent: Ablo<SchemaRecord> | null;
40
- /** Target entity. Null = pass-through wrap. */
41
- readonly target: ClaimTarget | null;
42
- /**
43
- * Optional human-readable phase for the broadcast. Default `'edit'`.
44
- * Convention: `'edit'`, `'read'`, `'review'`, `'generate'`. The same
45
- * `reason` field used on every claim surface.
46
- */
47
- readonly reason?: string;
48
36
  /**
49
- * Peer-visible explanation of the specific work this model call is about to
50
- * perform. Other agents receive it in their coordination context.
37
+ * Connected Ablo. Null = pass-through wrap (no broadcast, no read).
38
+ *
39
+ * Generic over the schema record (like the two middlewares it composes) so a
40
+ * caller passing a typed `Ablo<typeof schema>` doesn't have to cast: `Ablo<S>`
41
+ * and `Ablo<SchemaRecord>` aren't structurally assignable (the widened version
42
+ * collapses model proxies to an index signature that clashes with the named
43
+ * methods `ready`/`dispose`/…).
51
44
  */
52
- readonly description?: string;
45
+ readonly agent: Ablo<R> | null;
46
+ /** Target entity. Null = pass-through wrap. */
47
+ readonly target: ClaimTarget | null;
53
48
  /**
54
49
  * Optional claimIds to exclude from the coordination-context
55
50
  * read — typically the caller's own claim if they're composing
@@ -68,4 +63,4 @@ export interface WrapWithMultiplayerOptions {
68
63
  */
69
64
  readonly extraMiddleware?: readonly LanguageModelV3Middleware[];
70
65
  }
71
- export declare function wrapWithMultiplayer(options: WrapWithMultiplayerOptions): ReturnType<typeof wrapLanguageModel>;
66
+ export declare function wrapWithMultiplayer<R extends SchemaRecord = SchemaRecord>(options: WrapWithMultiplayerOptions<R>): ReturnType<typeof wrapLanguageModel>;
@@ -14,8 +14,6 @@
14
14
  * model: anthropic('claude-opus-4-7'),
15
15
  * agent,
16
16
  * target: { entityType: 'SlideDeck', entityId: 'deck-abc' },
17
- * reason: 'renaming',
18
- * description: 'Renaming the deck title to match the project brief.',
19
17
  * });
20
18
  *
21
19
  * const result = streamText({
@@ -28,15 +26,13 @@
28
26
  * ```
29
27
  */
30
28
  import { wrapLanguageModel } from 'ai';
31
- import { claimBroadcastMiddleware, } from './claim-broadcast.js';
32
- import { coordinationContextMiddleware } from './coordination-context.js';
29
+ import { coordinationContextMiddleware, } from './coordination-context.js';
33
30
  export function wrapWithMultiplayer(options) {
34
- const { model, agent, target, reason, description, excludeClaimIds, extraMiddleware } = options;
31
+ const { model, agent, target, excludeClaimIds, extraMiddleware } = options;
35
32
  return wrapLanguageModel({
36
33
  model,
37
34
  middleware: [
38
35
  coordinationContextMiddleware({ agent, target, excludeClaimIds }),
39
- claimBroadcastMiddleware({ agent, target, reason, description }),
40
36
  ...(extraMiddleware ?? []),
41
37
  ],
42
38
  });