@abloatai/ablo 0.3.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 +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ablo/sync-engine-internal/agent — Agent SDK helpers
|
|
3
|
+
*
|
|
4
|
+
* Two entry points depending on agent lifetime:
|
|
5
|
+
*
|
|
6
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
7
|
+
* LONG-LIVED AGENT (browser, daemon, persistent Node process)
|
|
8
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
* Use the unified `Ablo({...})` factory directly with `kind: 'agent'`.
|
|
10
|
+
* The factory holds the WebSocket, reactive subscriptions, mutations, and
|
|
11
|
+
* presence/intents — same surface as a browser user, just with a
|
|
12
|
+
* server-issued capability token instead of session cookies.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import Ablo from '@ablo/sync-engine';
|
|
16
|
+
*
|
|
17
|
+
* const ablo = Ablo({
|
|
18
|
+
* schema,
|
|
19
|
+
* url: 'wss://api.example.com',
|
|
20
|
+
* organizationId,
|
|
21
|
+
* kind: 'agent',
|
|
22
|
+
* agentId: 'reviewer-bot',
|
|
23
|
+
* capabilityToken: mintedToken,
|
|
24
|
+
* syncGroups: [`org:${organizationId}`],
|
|
25
|
+
* inMemory: true, // Node has no IndexedDB
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* await ablo.ready();
|
|
29
|
+
* for (const task of ablo.tasks.list({ where: { status: 'pending_review' } })) {
|
|
30
|
+
* await ablo.tasks.update(task.id, { status: 'reviewed' });
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* For server-side caching across requests, use {@link Agent.session}
|
|
35
|
+
* — it caches one engine per identity and refreshes capability tokens
|
|
36
|
+
* before expiry.
|
|
37
|
+
*
|
|
38
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
39
|
+
* SHORT-LIVED AGENT (SQS consumer, serverless, API route)
|
|
40
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
* Use {@link Agent}. Stateless REST hooks that slot directly into
|
|
42
|
+
* the Vercel AI SDK's `generateText` / `streamText`. No WebSocket. Ideal
|
|
43
|
+
* for agent-worker jobs that process one task and exit.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { generateText, tool, stepCountIs } from 'ai';
|
|
47
|
+
* import { Agent } from '@ablo/sync-engine-internal/agent';
|
|
48
|
+
*
|
|
49
|
+
* const perception = new Agent({
|
|
50
|
+
* syncServerUrl: 'http://localhost:8080',
|
|
51
|
+
* agentId: job.id,
|
|
52
|
+
* organizationId: job.organizationId,
|
|
53
|
+
* syncGroups: [`org:${job.organizationId}`],
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* await generateText({
|
|
57
|
+
* model: 'anthropic/claude-sonnet-4.5',
|
|
58
|
+
* messages,
|
|
59
|
+
* stopWhen: stepCountIs(10),
|
|
60
|
+
* tools: {
|
|
61
|
+
* updateSlide: perception.wrapTool(tool({ ... }), {
|
|
62
|
+
* entityType: 'Slide',
|
|
63
|
+
* getEntityId: (args) => args.id,
|
|
64
|
+
* }),
|
|
65
|
+
* },
|
|
66
|
+
* prepareStep: perception.prepareStep(),
|
|
67
|
+
* onStepFinish: perception.onStepFinish(),
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
72
|
+
* IDIOMATIC TOOL PATTERN (ported from vercel-labs/open-agents)
|
|
73
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
74
|
+
* Tools are factory functions that pull ambient state from
|
|
75
|
+
* `experimental_context`. The caller builds an {@link AgentContext} once
|
|
76
|
+
* and passes it to `generateText`; every tool reaches in.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* import { tool } from 'ai';
|
|
80
|
+
* import { z } from 'zod';
|
|
81
|
+
* import { Agent, type AgentContext } from '@ablo/sync-engine-internal/agent';
|
|
82
|
+
*
|
|
83
|
+
* export const updateSlideTool = () => tool({
|
|
84
|
+
* description: 'Update a slide title',
|
|
85
|
+
* inputSchema: z.object({ id: z.string(), title: z.string() }),
|
|
86
|
+
* execute: async (args, { experimental_context }) => {
|
|
87
|
+
* const perception = Agent.fromContext(experimental_context, 'updateSlide');
|
|
88
|
+
* const check = await perception.checkFreshness('Slide', args.id, Date.now() - 5000);
|
|
89
|
+
* if (check.stale) return check.summary;
|
|
90
|
+
* return { ok: true };
|
|
91
|
+
* },
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
96
|
+
* COMPOSED (long-lived `Ablo({kind:'agent'})` + Agent together)
|
|
97
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
98
|
+
* If you have a long-lived `Ablo` instance AND want AI SDK hooks, pass it
|
|
99
|
+
* as the `announcer` to Agent — presence announcements route
|
|
100
|
+
* through the existing WebSocket instead of opening new HTTP calls.
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* const ablo = Ablo({ kind: 'agent', schema, capabilityToken, ... });
|
|
104
|
+
* await ablo.ready();
|
|
105
|
+
*
|
|
106
|
+
* const perception = new Agent({
|
|
107
|
+
* ...sharedConfig,
|
|
108
|
+
* announcer: ablo, // reuse the WebSocket
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* Both `Ablo` and `Agent` implement the
|
|
113
|
+
* {@link PresenceAnnouncer} interface.
|
|
114
|
+
*/
|
|
115
|
+
// ── The entire `/agent` surface — one symbol ────────────────────────────
|
|
116
|
+
//
|
|
117
|
+
// `Agent` is the class AND the namespace for its types. Reach for
|
|
118
|
+
// options, context, and session options via dot access:
|
|
119
|
+
//
|
|
120
|
+
// import { Agent } from '@ablo/sync-engine-internal/agent';
|
|
121
|
+
// const opts: Agent.Options = { ... };
|
|
122
|
+
// const ctx: Agent.Context = { perception };
|
|
123
|
+
// const s: Agent.SessionOptions = { ... };
|
|
124
|
+
//
|
|
125
|
+
// Everything else (Activity, Claim, Turn, Peer, ActiveIntent, ...)
|
|
126
|
+
// lives on the `Ablo.*` namespace via
|
|
127
|
+
// `import type { Ablo } from '@ablo/sync-engine'`.
|
|
128
|
+
export { Agent } from './Agent.js';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent session — cache + lifecycle for server-side `SyncAgent`s.
|
|
3
|
+
*
|
|
4
|
+
* Captures the pattern every server-side consumer needs:
|
|
5
|
+
* 1. Cache `SyncAgent` instances per (org, user, surface, target).
|
|
6
|
+
* 2. Re-mint capabilities before TTL elapses.
|
|
7
|
+
* 3. Align the SyncAgent ctor's `syncGroups` with the cap allowlist
|
|
8
|
+
* so the upgrade-time intersection is non-empty (avoid the
|
|
9
|
+
* silent black-hole-broadcast bug).
|
|
10
|
+
* 4. Connect / disconnect / dispose lifecycle.
|
|
11
|
+
*
|
|
12
|
+
* What's generic, what isn't:
|
|
13
|
+
* - Cache, TTL, sync_groups alignment, lifecycle: SAME for every
|
|
14
|
+
* consumer. Lives here.
|
|
15
|
+
* - Cap mint: AUTH-FLOW-SPECIFIC. Every consumer has a different
|
|
16
|
+
* way to obtain a token (Better Auth cookie forwarding, API key
|
|
17
|
+
* exchange, OAuth, etc.). Consumer provides via the
|
|
18
|
+
* `issueToken` callback.
|
|
19
|
+
*
|
|
20
|
+
* The helper itself imports nothing app-specific. Open-source-clean.
|
|
21
|
+
*/
|
|
22
|
+
import { Ablo } from '../client/Ablo.js';
|
|
23
|
+
import type { Schema, SchemaRecord } from '../schema/schema.js';
|
|
24
|
+
interface IssuedToken {
|
|
25
|
+
readonly token: string;
|
|
26
|
+
readonly expiresAtMs: number;
|
|
27
|
+
/**
|
|
28
|
+
* Sync groups allowed by this capability. Must include every group
|
|
29
|
+
* the agent will subscribe to — the upgrade-time intersection of
|
|
30
|
+
* (allowed) ∩ (requested) determines effective subscription. Returning
|
|
31
|
+
* a list that doesn't include the needed groups produces an empty
|
|
32
|
+
* intersection and silent broadcast failure.
|
|
33
|
+
*/
|
|
34
|
+
readonly syncGroups: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
interface AgentIdentity {
|
|
37
|
+
readonly userId: string;
|
|
38
|
+
readonly organizationId: string;
|
|
39
|
+
/**
|
|
40
|
+
* Surface class — `'chat'`, `'mcp'`, `'agent_worker'`, etc. Session
|
|
41
|
+
* caches per surface so two surfaces don't share token or WS.
|
|
42
|
+
*/
|
|
43
|
+
readonly surfaceClass: string;
|
|
44
|
+
readonly target?: {
|
|
45
|
+
readonly entityType: string;
|
|
46
|
+
readonly entityId: string;
|
|
47
|
+
} | null;
|
|
48
|
+
}
|
|
49
|
+
export interface AgentSessionOptions<R extends SchemaRecord = SchemaRecord> {
|
|
50
|
+
/** Sync-server WebSocket URL — `wss://sync.example.com` or `ws://localhost:3001`. */
|
|
51
|
+
readonly syncServerUrl: string;
|
|
52
|
+
/** Schema for the typed model proxy on the returned Ablo. After
|
|
53
|
+
* the dual-engine collapse, `Ablo({kind:'agent'})` is the unified
|
|
54
|
+
* factory and requires the schema to expose
|
|
55
|
+
* `agent.<model>.create/update/delete`. */
|
|
56
|
+
readonly schema: Schema<R>;
|
|
57
|
+
/**
|
|
58
|
+
* Token-issuing callback. Called on cache miss / expiry. Owns the
|
|
59
|
+
* consumer's auth flow (Better Auth cookies, API key exchange, OAuth,
|
|
60
|
+
* etc.) so the engine stays auth-flow-agnostic.
|
|
61
|
+
*/
|
|
62
|
+
readonly issueToken: (identity: AgentIdentity) => Promise<IssuedToken>;
|
|
63
|
+
/**
|
|
64
|
+
* Soft window before actual expiry to re-mint. Defaults to 30s.
|
|
65
|
+
* Avoids races between mint-time and clock-skew at use-time.
|
|
66
|
+
*/
|
|
67
|
+
readonly reissueBufferMs?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Optional agent-id strategy. Default: `${surfaceClass}:${userId}`.
|
|
70
|
+
* Override when the consumer wants different attribution shape.
|
|
71
|
+
*/
|
|
72
|
+
readonly agentIdFor?: (identity: AgentIdentity) => string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Returns a session whose `getAgent` method handles cache, mint,
|
|
76
|
+
* sync_groups alignment, and lifecycle. Call `disposeAll()` from
|
|
77
|
+
* the consumer's process shutdown hook.
|
|
78
|
+
*
|
|
79
|
+
* Threading: the session is intended to be a long-lived singleton
|
|
80
|
+
* shared across requests. The cache is keyed precisely so two
|
|
81
|
+
* concurrent requests for the same (user, org, surface, target)
|
|
82
|
+
* share one agent + one WS, while different requests get
|
|
83
|
+
* independent agents.
|
|
84
|
+
*/
|
|
85
|
+
export declare function createAgentSession<R extends SchemaRecord = SchemaRecord>(options: AgentSessionOptions<R>): {
|
|
86
|
+
getAgent: (identity: AgentIdentity) => Promise<Ablo<R>>;
|
|
87
|
+
evict: (identity: AgentIdentity) => void;
|
|
88
|
+
disposeAll: () => void;
|
|
89
|
+
};
|
|
90
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent session — cache + lifecycle for server-side `SyncAgent`s.
|
|
3
|
+
*
|
|
4
|
+
* Captures the pattern every server-side consumer needs:
|
|
5
|
+
* 1. Cache `SyncAgent` instances per (org, user, surface, target).
|
|
6
|
+
* 2. Re-mint capabilities before TTL elapses.
|
|
7
|
+
* 3. Align the SyncAgent ctor's `syncGroups` with the cap allowlist
|
|
8
|
+
* so the upgrade-time intersection is non-empty (avoid the
|
|
9
|
+
* silent black-hole-broadcast bug).
|
|
10
|
+
* 4. Connect / disconnect / dispose lifecycle.
|
|
11
|
+
*
|
|
12
|
+
* What's generic, what isn't:
|
|
13
|
+
* - Cache, TTL, sync_groups alignment, lifecycle: SAME for every
|
|
14
|
+
* consumer. Lives here.
|
|
15
|
+
* - Cap mint: AUTH-FLOW-SPECIFIC. Every consumer has a different
|
|
16
|
+
* way to obtain a token (Better Auth cookie forwarding, API key
|
|
17
|
+
* exchange, OAuth, etc.). Consumer provides via the
|
|
18
|
+
* `issueToken` callback.
|
|
19
|
+
*
|
|
20
|
+
* The helper itself imports nothing app-specific. Open-source-clean.
|
|
21
|
+
*/
|
|
22
|
+
import { Ablo } from '../client/Ablo.js';
|
|
23
|
+
/**
|
|
24
|
+
* Returns a session whose `getAgent` method handles cache, mint,
|
|
25
|
+
* sync_groups alignment, and lifecycle. Call `disposeAll()` from
|
|
26
|
+
* the consumer's process shutdown hook.
|
|
27
|
+
*
|
|
28
|
+
* Threading: the session is intended to be a long-lived singleton
|
|
29
|
+
* shared across requests. The cache is keyed precisely so two
|
|
30
|
+
* concurrent requests for the same (user, org, surface, target)
|
|
31
|
+
* share one agent + one WS, while different requests get
|
|
32
|
+
* independent agents.
|
|
33
|
+
*/
|
|
34
|
+
export function createAgentSession(options) {
|
|
35
|
+
const reissueBufferMs = options.reissueBufferMs ?? 30_000;
|
|
36
|
+
const agentIdFor = options.agentIdFor ??
|
|
37
|
+
((id) => `${id.surfaceClass}:${id.userId}`);
|
|
38
|
+
const cacheByKey = new Map();
|
|
39
|
+
function cacheKey(id) {
|
|
40
|
+
const targetSeg = id.target
|
|
41
|
+
? `:${id.target.entityType}:${id.target.entityId}`
|
|
42
|
+
: '';
|
|
43
|
+
return `${id.organizationId}:${id.userId}:${id.surfaceClass}${targetSeg}`;
|
|
44
|
+
}
|
|
45
|
+
async function getAgent(identity) {
|
|
46
|
+
const key = cacheKey(identity);
|
|
47
|
+
const cached = cacheByKey.get(key);
|
|
48
|
+
if (cached && cached.expiresAtMs - Date.now() > reissueBufferMs) {
|
|
49
|
+
return cached.agent;
|
|
50
|
+
}
|
|
51
|
+
// Best-effort cleanup of stale agent — don't let a stuck cached
|
|
52
|
+
// entry block fresh issuance.
|
|
53
|
+
if (cached) {
|
|
54
|
+
try {
|
|
55
|
+
await cached.agent.dispose();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* ignore */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const minted = await options.issueToken(identity);
|
|
62
|
+
// Sync_groups alignment is the load-bearing detail. The SDK
|
|
63
|
+
// ctor's `syncGroups` and the cap mint's `syncGroups`
|
|
64
|
+
// MUST overlap or the upgrade intersection is empty and every
|
|
65
|
+
// broadcast filter returns false. Use the cap's allowed list
|
|
66
|
+
// verbatim — the caller controlled what went in there, so it's
|
|
67
|
+
// exactly what the SDK should request.
|
|
68
|
+
// `AbloOptions` exposes the URL as `baseURL` (resolved by
|
|
69
|
+
// `resolveBaseURL`). Earlier code passed `url:` here — `Ablo()`
|
|
70
|
+
// silently dropped the unknown field (the cast below masked the
|
|
71
|
+
// type error) and `resolveBaseURL` fell through to the hardcoded
|
|
72
|
+
// default `wss://api.ablo.cloud` (now `wss://mesh.ablo.finance`).
|
|
73
|
+
// Staging surfaced the bug 2026-05-07 — DNS lookup hit the wrong
|
|
74
|
+
// host even though the caller threaded `syncServerUrl` through
|
|
75
|
+
// correctly. Forward as `baseURL` so the caller's URL is the only
|
|
76
|
+
// source of truth and the package default never silently applies.
|
|
77
|
+
const wsUrl = toWsUrl(options.syncServerUrl);
|
|
78
|
+
const agentOptions = {
|
|
79
|
+
baseURL: wsUrl,
|
|
80
|
+
schema: options.schema,
|
|
81
|
+
kind: 'agent',
|
|
82
|
+
capabilityToken: minted.token,
|
|
83
|
+
agentId: agentIdFor(identity),
|
|
84
|
+
organizationId: identity.organizationId,
|
|
85
|
+
syncGroups: [...minted.syncGroups],
|
|
86
|
+
// Agents run in Node — no IDB available, no need for it.
|
|
87
|
+
inMemory: true,
|
|
88
|
+
};
|
|
89
|
+
const agent = Ablo(agentOptions);
|
|
90
|
+
try {
|
|
91
|
+
await agent.ready();
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const e = err;
|
|
95
|
+
const code = e.cause?.code;
|
|
96
|
+
const causeMsg = e.cause?.message;
|
|
97
|
+
// Best-effort dispose so the failed agent doesn't leak ws state.
|
|
98
|
+
try {
|
|
99
|
+
await agent.dispose();
|
|
100
|
+
}
|
|
101
|
+
catch { /* ignore */ }
|
|
102
|
+
// Use console.error directly (rather than the engine logger)
|
|
103
|
+
// because this path may run before the per-agent logger is
|
|
104
|
+
// attached. The structured fields match the cap-mint logger in
|
|
105
|
+
// `connectAgent.ts` so a single search picks both up.
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.error('[Agent.session] ws bootstrap failed', {
|
|
108
|
+
url: wsUrl,
|
|
109
|
+
surfaceClass: identity.surfaceClass,
|
|
110
|
+
orgId: identity.organizationId,
|
|
111
|
+
userId: identity.userId,
|
|
112
|
+
code,
|
|
113
|
+
causeMsg,
|
|
114
|
+
err,
|
|
115
|
+
});
|
|
116
|
+
throw new Error(`ws bootstrap ${wsUrl} failed: ${e.message ?? 'bootstrap failed'}` +
|
|
117
|
+
(code ? ` (${code})` : ''));
|
|
118
|
+
}
|
|
119
|
+
cacheByKey.set(key, { agent, expiresAtMs: minted.expiresAtMs });
|
|
120
|
+
return agent;
|
|
121
|
+
}
|
|
122
|
+
function disposeAll() {
|
|
123
|
+
for (const { agent } of cacheByKey.values()) {
|
|
124
|
+
try {
|
|
125
|
+
void agent.dispose();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* ignore */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
cacheByKey.clear();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Eject a specific cached agent — useful when the consumer knows
|
|
135
|
+
* the underlying token is invalidated (revocation, role change)
|
|
136
|
+
* and wants the next `getAgent` call to mint fresh.
|
|
137
|
+
*/
|
|
138
|
+
function evict(identity) {
|
|
139
|
+
const key = cacheKey(identity);
|
|
140
|
+
const cached = cacheByKey.get(key);
|
|
141
|
+
if (cached) {
|
|
142
|
+
try {
|
|
143
|
+
void cached.agent.dispose();
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* ignore */
|
|
147
|
+
}
|
|
148
|
+
cacheByKey.delete(key);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { getAgent, evict, disposeAll };
|
|
152
|
+
}
|
|
153
|
+
/** `https://host` → `wss://host`; `http://host` → `ws://host`. */
|
|
154
|
+
function toWsUrl(url) {
|
|
155
|
+
return url.replace(/^http/, 'ws');
|
|
156
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-SDK abstractions. The engine's data vocabulary
|
|
3
|
+
* (`Peer`, `Activity`, `IntentClaim`, `ActiveIntent`,
|
|
4
|
+
* `PresenceUpdatePayload`, `PresenceKind`) lives in
|
|
5
|
+
* `../types/streams.ts`. This file holds only the bits that are
|
|
6
|
+
* specific to the agent module: the `PresenceAnnouncer` abstraction
|
|
7
|
+
* (transport-agnostic announce contract) and `AgentContext` (the AI
|
|
8
|
+
* SDK `experimental_context` bag).
|
|
9
|
+
*/
|
|
10
|
+
import type { Activity } from '../types/streams.js';
|
|
11
|
+
/**
|
|
12
|
+
* A minimal interface for announcing presence — abstract over WebSocket
|
|
13
|
+
* (`Ablo({kind: 'agent'})`) and REST (`Agent`). Both
|
|
14
|
+
* implementations satisfy this, so higher-level code can depend on it
|
|
15
|
+
* without caring about transport.
|
|
16
|
+
*/
|
|
17
|
+
export interface PresenceAnnouncer {
|
|
18
|
+
announce(status: 'online' | 'away' | 'offline', activity?: Activity): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Ambient context threaded into AI SDK tools via `experimental_context`.
|
|
22
|
+
*
|
|
23
|
+
* The pattern: the caller constructs an AgentContext once per agent
|
|
24
|
+
* invocation and passes it as `experimental_context`. Each tool's
|
|
25
|
+
* `execute` function extracts what it needs from
|
|
26
|
+
* `options.experimental_context` instead of closing over module-level
|
|
27
|
+
* state.
|
|
28
|
+
*
|
|
29
|
+
* Benefits over closure-based tool wiring:
|
|
30
|
+
* - Tools are framework-agnostic module exports (portable across agents)
|
|
31
|
+
* - The context is typed in one place, not scattered across closures
|
|
32
|
+
* - New tools can access any field without changing tool signatures
|
|
33
|
+
*
|
|
34
|
+
* Ported from the vercel-labs/open-agents pattern.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { generateText, tool } from 'ai';
|
|
38
|
+
* import { Agent, type AgentContext } from '@ablo/sync-engine-internal/agent';
|
|
39
|
+
*
|
|
40
|
+
* const updateSlideTool = () => tool({
|
|
41
|
+
* inputSchema: z.object({ id: z.string(), title: z.string() }),
|
|
42
|
+
* execute: async (args, { experimental_context }) => {
|
|
43
|
+
* const perception = Agent.fromContext(experimental_context);
|
|
44
|
+
* const check = await perception.checkFreshness('Slide', args.id, Date.now() - 5000);
|
|
45
|
+
* if (check.stale) return check.summary;
|
|
46
|
+
* // ... actual mutation
|
|
47
|
+
* },
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* await generateText({
|
|
51
|
+
* model: 'anthropic/claude-sonnet-4.5',
|
|
52
|
+
* tools: { updateSlide: updateSlideTool() },
|
|
53
|
+
* experimental_context: { perception, organizationId, userId } satisfies AgentContext,
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Consumers can extend AgentContext via module augmentation or by
|
|
58
|
+
* intersecting with their own context type.
|
|
59
|
+
*/
|
|
60
|
+
export interface AgentContext {
|
|
61
|
+
/** Presence / freshness / AI SDK hook primitives. Required. */
|
|
62
|
+
perception: PresenceAnnouncer & {
|
|
63
|
+
checkFreshness?: (entityType: string, entityId: string, lastSeenAt: number) => Promise<unknown>;
|
|
64
|
+
};
|
|
65
|
+
/** Organization scope for all operations. */
|
|
66
|
+
organizationId?: string;
|
|
67
|
+
/** User or agent identifier — format: "agent:<id>" for agents. */
|
|
68
|
+
userId?: string;
|
|
69
|
+
/** Sync groups the agent belongs to. */
|
|
70
|
+
syncGroups?: string[];
|
|
71
|
+
/** Allow extension with product-specific fields. */
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-SDK abstractions. The engine's data vocabulary
|
|
3
|
+
* (`Peer`, `Activity`, `IntentClaim`, `ActiveIntent`,
|
|
4
|
+
* `PresenceUpdatePayload`, `PresenceKind`) lives in
|
|
5
|
+
* `../types/streams.ts`. This file holds only the bits that are
|
|
6
|
+
* specific to the agent module: the `PresenceAnnouncer` abstraction
|
|
7
|
+
* (transport-agnostic announce contract) and `AgentContext` (the AI
|
|
8
|
+
* SDK `experimental_context` bag).
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordination context middleware — reads peer intents on the same
|
|
3
|
+
* entity from the sync engine's presence stream and injects a brief
|
|
4
|
+
* coordination note into the prompt before the LLM call.
|
|
5
|
+
*
|
|
6
|
+
* The complement of `intent-broadcast.ts`: that one declares what
|
|
7
|
+
* THIS agent is about to do; this one reads what OTHERS are doing
|
|
8
|
+
* and tells the LLM about it. Together they make multiplayer-with-
|
|
9
|
+
* AI structurally real — the AI knows when a human or another
|
|
10
|
+
* agent is mid-edit and can defer / phrase its work as
|
|
11
|
+
* "while you finish that, I'll …" / suggest waiting / coordinate
|
|
12
|
+
* explicitly.
|
|
13
|
+
*
|
|
14
|
+
* Open-source-clean: depends only on `@ai-sdk/provider` types and
|
|
15
|
+
* the package's own `SyncAgent`. Consumers compose via the AI
|
|
16
|
+
* SDK's `wrapLanguageModel`.
|
|
17
|
+
*
|
|
18
|
+
* Cost: zero extra LLM calls (read happens locally from the agent's
|
|
19
|
+
* cached presence stream — already in memory from the WS subscription).
|
|
20
|
+
* Adds a few sentences to the system prompt (typically <100 tokens)
|
|
21
|
+
* only when peers are actively editing.
|
|
22
|
+
*/
|
|
23
|
+
import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
|
|
24
|
+
import type { Ablo } from '../client/Ablo.js';
|
|
25
|
+
import type { SchemaRecord } from '../schema/schema.js';
|
|
26
|
+
import type { IntentTarget } from './intent-broadcast.js';
|
|
27
|
+
export interface CoordinationContextMiddlewareOptions<R extends SchemaRecord = SchemaRecord> {
|
|
28
|
+
readonly agent: Ablo<R> | null;
|
|
29
|
+
readonly target: IntentTarget | null;
|
|
30
|
+
/**
|
|
31
|
+
* Optional intentId(s) to exclude from the read — typically this
|
|
32
|
+
* agent's own active claim so the coordination note doesn't tell
|
|
33
|
+
* the AI "you yourself are editing this." When middleware is
|
|
34
|
+
* composed with `intentBroadcastMiddleware` in the standard order,
|
|
35
|
+
* `transformParams` runs BEFORE the broadcast's `wrapStream`
|
|
36
|
+
* declares its claim, so the agent's own claim isn't yet in the
|
|
37
|
+
* cached presence and self-filtering isn't needed. The hook is
|
|
38
|
+
* here for callers that compose differently or for fleet
|
|
39
|
+
* coordination (filter sibling worker intents).
|
|
40
|
+
*/
|
|
41
|
+
readonly excludeIntentIds?: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the middleware. When `agent` or `target` is null, returns a
|
|
45
|
+
* pass-through.
|
|
46
|
+
*
|
|
47
|
+
* Generic over the schema record — see `intentBroadcastMiddleware`
|
|
48
|
+
* for why `Ablo<S>` and `Ablo<SchemaRecord>` aren't structurally
|
|
49
|
+
* assignable.
|
|
50
|
+
*/
|
|
51
|
+
export declare function coordinationContextMiddleware<R extends SchemaRecord = SchemaRecord>(options: CoordinationContextMiddlewareOptions<R>): LanguageModelV3Middleware;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordination context middleware — reads peer intents on the same
|
|
3
|
+
* entity from the sync engine's presence stream and injects a brief
|
|
4
|
+
* coordination note into the prompt before the LLM call.
|
|
5
|
+
*
|
|
6
|
+
* The complement of `intent-broadcast.ts`: that one declares what
|
|
7
|
+
* THIS agent is about to do; this one reads what OTHERS are doing
|
|
8
|
+
* and tells the LLM about it. Together they make multiplayer-with-
|
|
9
|
+
* AI structurally real — the AI knows when a human or another
|
|
10
|
+
* agent is mid-edit and can defer / phrase its work as
|
|
11
|
+
* "while you finish that, I'll …" / suggest waiting / coordinate
|
|
12
|
+
* explicitly.
|
|
13
|
+
*
|
|
14
|
+
* Open-source-clean: depends only on `@ai-sdk/provider` types and
|
|
15
|
+
* the package's own `SyncAgent`. Consumers compose via the AI
|
|
16
|
+
* SDK's `wrapLanguageModel`.
|
|
17
|
+
*
|
|
18
|
+
* Cost: zero extra LLM calls (read happens locally from the agent's
|
|
19
|
+
* cached presence stream — already in memory from the WS subscription).
|
|
20
|
+
* Adds a few sentences to the system prompt (typically <100 tokens)
|
|
21
|
+
* only when peers are actively editing.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Build the middleware. When `agent` or `target` is null, returns a
|
|
25
|
+
* pass-through.
|
|
26
|
+
*
|
|
27
|
+
* Generic over the schema record — see `intentBroadcastMiddleware`
|
|
28
|
+
* for why `Ablo<S>` and `Ablo<SchemaRecord>` aren't structurally
|
|
29
|
+
* assignable.
|
|
30
|
+
*/
|
|
31
|
+
export function coordinationContextMiddleware(options) {
|
|
32
|
+
const { agent, target } = options;
|
|
33
|
+
const excludeIntentIds = new Set(options.excludeIntentIds ?? []);
|
|
34
|
+
return {
|
|
35
|
+
specificationVersion: 'v3',
|
|
36
|
+
transformParams: async ({ params }) => {
|
|
37
|
+
if (!agent || !target)
|
|
38
|
+
return params;
|
|
39
|
+
// Read peer intents on the same target. Synchronous lookup
|
|
40
|
+
// against the engine's reactive intents.others array — no I/O.
|
|
41
|
+
const peerClaims = agent.intents.others.filter((claim) => claim.target.type === target.entityType &&
|
|
42
|
+
claim.target.id === target.entityId &&
|
|
43
|
+
targetsOverlap(claim.target, target) &&
|
|
44
|
+
!excludeIntentIds.has(claim.id));
|
|
45
|
+
if (peerClaims.length === 0)
|
|
46
|
+
return params;
|
|
47
|
+
const note = formatCoordinationNote(peerClaims, target);
|
|
48
|
+
return injectSystemNote(params, note);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function hasSubtarget(target) {
|
|
53
|
+
return Boolean(target.path || target.field || target.range);
|
|
54
|
+
}
|
|
55
|
+
function rangesOverlap(a, b) {
|
|
56
|
+
return a.startLine <= b.endLine && b.startLine <= a.endLine;
|
|
57
|
+
}
|
|
58
|
+
function targetsOverlap(claimTarget, target) {
|
|
59
|
+
if (!hasSubtarget(claimTarget) || !hasSubtarget(target))
|
|
60
|
+
return true;
|
|
61
|
+
if (claimTarget.path &&
|
|
62
|
+
target.path &&
|
|
63
|
+
claimTarget.path.toLowerCase() !== target.path.toLowerCase()) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const fieldOverlaps = !claimTarget.field ||
|
|
67
|
+
!target.field ||
|
|
68
|
+
claimTarget.field.toLowerCase() === target.field.toLowerCase();
|
|
69
|
+
const rangeOverlaps = !claimTarget.range ||
|
|
70
|
+
!target.range ||
|
|
71
|
+
rangesOverlap(claimTarget.range, target.range);
|
|
72
|
+
return fieldOverlaps && rangeOverlaps;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Format a one-paragraph coordination note for the LLM. Includes
|
|
76
|
+
* who's editing and what (when known). Kept short — the goal is
|
|
77
|
+
* "AI knows," not "AI gets a wall of text."
|
|
78
|
+
*/
|
|
79
|
+
function formatCoordinationNote(claims, target) {
|
|
80
|
+
const entityLabel = target.entityType.toLowerCase();
|
|
81
|
+
if (claims.length === 1) {
|
|
82
|
+
const c = claims[0];
|
|
83
|
+
return (`<multiplayer_context>\n` +
|
|
84
|
+
`Another participant is currently editing this ${entityLabel}. ` +
|
|
85
|
+
`Action declared: ${c.reason}. ` +
|
|
86
|
+
`Defer to their concurrent changes when reasonable, or note your work as complementary to theirs. ` +
|
|
87
|
+
`Avoid stomping their in-flight edits.\n` +
|
|
88
|
+
`</multiplayer_context>`);
|
|
89
|
+
}
|
|
90
|
+
const actions = Array.from(new Set(claims.map((c) => c.reason))).join(', ');
|
|
91
|
+
return (`<multiplayer_context>\n` +
|
|
92
|
+
`${claims.length} other participants are currently editing this ${entityLabel}. ` +
|
|
93
|
+
`Active actions: ${actions}. ` +
|
|
94
|
+
`Coordinate with their in-flight work — defer where reasonable, ` +
|
|
95
|
+
`or describe your work as complementary.\n` +
|
|
96
|
+
`</multiplayer_context>`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Append a system-role message to the prompt array. The AI SDK's
|
|
100
|
+
* `LanguageModelV3Prompt` is an ordered list of messages.
|
|
101
|
+
*/
|
|
102
|
+
function injectSystemNote(params, note) {
|
|
103
|
+
return {
|
|
104
|
+
...params,
|
|
105
|
+
prompt: [...params.prompt, { role: 'system', content: note }],
|
|
106
|
+
};
|
|
107
|
+
}
|