@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,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent — AI SDK v6 native hooks for agent awareness.
|
|
3
|
+
*
|
|
4
|
+
* Slots directly into generateText / streamText / ToolLoopAgent via three
|
|
5
|
+
* hooks: prepareStep (inject awareness before each step), onStepFinish
|
|
6
|
+
* (announce activity after each step), and wrapTool (wrap mutation tools
|
|
7
|
+
* with freshness checks). Stateless REST under the hood — works with any
|
|
8
|
+
* API model, no WebSocket.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { generateText, tool, stepCountIs } from 'ai';
|
|
12
|
+
* import { Agent } from '@ablo/sync-engine-internal/agent';
|
|
13
|
+
*
|
|
14
|
+
* const perception = new Agent({
|
|
15
|
+
* syncServerUrl: 'http://localhost:8080',
|
|
16
|
+
* agentId: 'researcher-1',
|
|
17
|
+
* organizationId: 'org-1',
|
|
18
|
+
* syncGroups: ['deal:abc'],
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* const result = await generateText({
|
|
22
|
+
* model: 'anthropic/claude-sonnet-4.5',
|
|
23
|
+
* messages,
|
|
24
|
+
* stopWhen: stepCountIs(10),
|
|
25
|
+
* tools: {
|
|
26
|
+
* updateSlide: perception.wrapTool(
|
|
27
|
+
* tool({
|
|
28
|
+
* inputSchema: z.object({ id: z.string(), title: z.string() }),
|
|
29
|
+
* execute: async ({ id, title }) => { ... },
|
|
30
|
+
* }),
|
|
31
|
+
* { entityType: 'Slide', getEntityId: (args) => args.id },
|
|
32
|
+
* ),
|
|
33
|
+
* },
|
|
34
|
+
* prepareStep: perception.prepareStep(), // injects awareness
|
|
35
|
+
* onStepFinish: perception.onStepFinish(), // announces activity
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Low-level primitives (gather, checkFreshness, announce) are also exposed
|
|
40
|
+
* for custom integrations outside the AI SDK.
|
|
41
|
+
*/
|
|
42
|
+
import { createAgentSession } from './session.js';
|
|
43
|
+
import { AbloValidationError } from '../errors.js';
|
|
44
|
+
// ── Agent ───────────────────────────────────────────────────────
|
|
45
|
+
// Console-backed default logger. Local to this module so the agent
|
|
46
|
+
// SDK doesn't take a transitive dependency on `getContext()` (which
|
|
47
|
+
// belongs to the web-app context, not standalone agent workers).
|
|
48
|
+
const consoleLogger = {
|
|
49
|
+
debug: (msg, ...args) => console.debug('[agent]', msg, ...args),
|
|
50
|
+
info: (msg, ...args) => console.info('[agent]', msg, ...args),
|
|
51
|
+
warn: (msg, ...args) => console.warn('[agent]', msg, ...args),
|
|
52
|
+
error: (msg, ...args) => console.error('[agent]', msg, ...args),
|
|
53
|
+
};
|
|
54
|
+
export class Agent {
|
|
55
|
+
opts;
|
|
56
|
+
constructor(options) {
|
|
57
|
+
this.opts = {
|
|
58
|
+
authToken: options.authToken,
|
|
59
|
+
announcer: options.announcer,
|
|
60
|
+
syncServerUrl: options.syncServerUrl.replace(/\/+$/, ''),
|
|
61
|
+
agentId: options.agentId,
|
|
62
|
+
organizationId: options.organizationId,
|
|
63
|
+
syncGroups: options.syncGroups,
|
|
64
|
+
fetch: options.fetch ?? globalThis.fetch.bind(globalThis),
|
|
65
|
+
timeoutMs: options.timeoutMs ?? 5_000,
|
|
66
|
+
logger: options.logger ?? consoleLogger,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build a long-lived agent session — caches `Ablo({kind:'agent'})`
|
|
71
|
+
* instances per `(org, user, surface, target)` and refreshes capability
|
|
72
|
+
* tokens before TTL elapses. Use on the server when the same agent
|
|
73
|
+
* identity handles many requests.
|
|
74
|
+
*
|
|
75
|
+
* Returns the cache, NOT an `Agent` instance: the long-lived path
|
|
76
|
+
* uses `Ablo({kind:'agent'})` over WebSocket, while the `Agent` class
|
|
77
|
+
* itself is the short-lived REST helper for AI SDK tool loops. The
|
|
78
|
+
* static method lives here so consumers reach for everything
|
|
79
|
+
* agent-related under one namespace.
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* const session = Agent.session({ syncServerUrl, schema, issueToken });
|
|
83
|
+
* const ablo = await session.getAgent({ userId, organizationId, surfaceClass });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
static session = createAgentSession;
|
|
87
|
+
/** The fully-qualified userId used on the wire: `agent:<agentId>`. */
|
|
88
|
+
get userId() {
|
|
89
|
+
return `agent:${this.opts.agentId}`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Extract the Agent instance from an AI SDK tool's
|
|
93
|
+
* `experimental_context`. Use inside tool `execute` functions to reach
|
|
94
|
+
* the perception without closure-capturing it.
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* execute: async (args, { experimental_context }) => {
|
|
98
|
+
* const perception = Agent.fromContext(experimental_context);
|
|
99
|
+
* const check = await perception.checkFreshness('Slide', args.id, lastSeenAt);
|
|
100
|
+
* // ...
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* Throws if the context is missing or doesn't contain an Agent.
|
|
105
|
+
* @param ctx The `experimental_context` passed to the tool.
|
|
106
|
+
* @param toolName Optional tool name for error messages.
|
|
107
|
+
*/
|
|
108
|
+
static fromContext(ctx, toolName) {
|
|
109
|
+
if (!ctx ||
|
|
110
|
+
typeof ctx !== 'object' ||
|
|
111
|
+
!('perception' in ctx) ||
|
|
112
|
+
!(ctx.perception instanceof Agent)) {
|
|
113
|
+
const where = toolName ? ` (tool: ${toolName})` : '';
|
|
114
|
+
throw new AbloValidationError(`Agent.fromContext: experimental_context must contain an Agent in \`perception\`.${where} ` +
|
|
115
|
+
`Set \`experimental_context: { perception } satisfies AgentContext\` when calling generateText/streamText.`, { code: 'agent_perception_missing_context' });
|
|
116
|
+
}
|
|
117
|
+
return ctx.perception;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Narrower variant of {@link fromContext} that returns `undefined` instead
|
|
121
|
+
* of throwing when perception isn't in context. Useful for tools where
|
|
122
|
+
* awareness is optional (e.g., read-only tools that work without it).
|
|
123
|
+
*/
|
|
124
|
+
static tryFromContext(ctx) {
|
|
125
|
+
if (!ctx ||
|
|
126
|
+
typeof ctx !== 'object' ||
|
|
127
|
+
!('perception' in ctx) ||
|
|
128
|
+
!(ctx.perception instanceof Agent)) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return ctx.perception;
|
|
132
|
+
}
|
|
133
|
+
// ── Outbound: announce activity ──────────────────────────────────────
|
|
134
|
+
/**
|
|
135
|
+
* Announce this agent's presence/activity. Fire-and-forget — logs errors
|
|
136
|
+
* but never throws (presence failures must not block the agent loop).
|
|
137
|
+
*
|
|
138
|
+
* If a `announcer` was provided (e.g. a connected SyncAgent), routes
|
|
139
|
+
* through it to reuse the WebSocket. Otherwise falls back to REST POST.
|
|
140
|
+
*/
|
|
141
|
+
async announce(status, activity) {
|
|
142
|
+
// Prefer injected announcer (WebSocket) over REST
|
|
143
|
+
if (this.opts.announcer) {
|
|
144
|
+
try {
|
|
145
|
+
await this.opts.announcer.announce(status, activity);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
this.opts.logger.warn('[perception] announcer error', {
|
|
149
|
+
error: err.message,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const res = await this.request('POST', '/api/presence', {
|
|
156
|
+
userId: this.userId,
|
|
157
|
+
organizationId: this.opts.organizationId,
|
|
158
|
+
status,
|
|
159
|
+
activity,
|
|
160
|
+
syncGroups: this.opts.syncGroups,
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
this.opts.logger.warn(`[perception] announce failed: ${res.status} ${res.statusText}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
this.opts.logger.warn('[perception] announce error', {
|
|
168
|
+
error: err.message,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ── Inbound: gather context for next LLM call ────────────────────────
|
|
173
|
+
/**
|
|
174
|
+
* Gather a snapshot of current activity by peers and format it as
|
|
175
|
+
* natural-language context for injection into the next LLM prompt.
|
|
176
|
+
*/
|
|
177
|
+
async gather(options) {
|
|
178
|
+
const opts = {
|
|
179
|
+
maxChars: 2000,
|
|
180
|
+
includePresence: true,
|
|
181
|
+
excludeSelf: true,
|
|
182
|
+
...options,
|
|
183
|
+
};
|
|
184
|
+
const snapshot = {
|
|
185
|
+
timestamp: Date.now(),
|
|
186
|
+
presence: [],
|
|
187
|
+
};
|
|
188
|
+
if (opts.includePresence) {
|
|
189
|
+
snapshot.presence = await this.fetchPresence(opts.excludeSelf);
|
|
190
|
+
}
|
|
191
|
+
const prompt = this.formatPrompt(snapshot, opts);
|
|
192
|
+
return { prompt, snapshot };
|
|
193
|
+
}
|
|
194
|
+
// ── Freshness check: run before mutations ────────────────────────────
|
|
195
|
+
/**
|
|
196
|
+
* Check if an entity was modified since `lastSeenAt`. Use before
|
|
197
|
+
* executing a mutation to detect stale state.
|
|
198
|
+
*
|
|
199
|
+
* Returns `{ stale: true, summary }` when the entity changed — feed
|
|
200
|
+
* `summary` back to the LLM as a tool result so it can adjust its plan.
|
|
201
|
+
*/
|
|
202
|
+
async checkFreshness(entityType, entityId, lastSeenAt) {
|
|
203
|
+
// Parallel fan-out: freshness (entity state vs lastSeenAt) + pending
|
|
204
|
+
// intents (other agents about to mutate). Both are advisory — if
|
|
205
|
+
// either request fails the check still returns a usable result.
|
|
206
|
+
const [queryRes, pendingIntents] = await Promise.all([
|
|
207
|
+
this.request('POST', '/api/sync/query', {
|
|
208
|
+
organizationId: this.opts.organizationId,
|
|
209
|
+
queries: [{ model: entityType, ids: [entityId] }],
|
|
210
|
+
}).catch((err) => ({ ok: false, status: 0, _err: err })),
|
|
211
|
+
this.fetchPendingIntentsFor(entityType, entityId),
|
|
212
|
+
]);
|
|
213
|
+
try {
|
|
214
|
+
const res = queryRes;
|
|
215
|
+
if (!('ok' in res) || !res.ok) {
|
|
216
|
+
return {
|
|
217
|
+
stale: false,
|
|
218
|
+
reason: 'ok',
|
|
219
|
+
summary: `Freshness check inconclusive: ${('status' in res ? res.status : 'error')}`,
|
|
220
|
+
pendingIntents,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const body = (await res.json());
|
|
224
|
+
const rows = body.results?.[0];
|
|
225
|
+
if (!rows || rows.length === 0) {
|
|
226
|
+
return {
|
|
227
|
+
stale: true,
|
|
228
|
+
reason: 'not_found',
|
|
229
|
+
summary: `${entityType} ${entityId} no longer exists. Another actor may have deleted it.`,
|
|
230
|
+
pendingIntents,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const entity = rows[0];
|
|
234
|
+
const updatedAtRaw = entity.updated_at ?? entity.updatedAt;
|
|
235
|
+
const lastModifiedBy = entity.updated_by ??
|
|
236
|
+
entity.updatedBy ??
|
|
237
|
+
entity.created_by;
|
|
238
|
+
const lastModifiedAt = typeof updatedAtRaw === 'string'
|
|
239
|
+
? Date.parse(updatedAtRaw)
|
|
240
|
+
: typeof updatedAtRaw === 'number'
|
|
241
|
+
? updatedAtRaw
|
|
242
|
+
: undefined;
|
|
243
|
+
if (lastModifiedAt !== undefined && lastModifiedAt > lastSeenAt) {
|
|
244
|
+
const ago = Math.round((Date.now() - lastModifiedAt) / 1000);
|
|
245
|
+
return {
|
|
246
|
+
stale: true,
|
|
247
|
+
reason: 'modified',
|
|
248
|
+
currentState: entity,
|
|
249
|
+
lastModifiedBy,
|
|
250
|
+
lastModifiedAt,
|
|
251
|
+
summary: `${entityType} ${entityId} was modified by ${lastModifiedBy ?? 'another actor'} ` +
|
|
252
|
+
`${ago}s ago. Your planned change is based on stale state. ` +
|
|
253
|
+
`Re-read the entity and adjust your approach.`,
|
|
254
|
+
pendingIntents,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
stale: false,
|
|
259
|
+
reason: 'ok',
|
|
260
|
+
currentState: entity,
|
|
261
|
+
lastModifiedBy,
|
|
262
|
+
lastModifiedAt,
|
|
263
|
+
pendingIntents,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
// Freshness check is advisory — on error, assume ok and let the
|
|
268
|
+
// mutation proceed. Better than blocking the agent on a flaky query.
|
|
269
|
+
return {
|
|
270
|
+
stale: false,
|
|
271
|
+
reason: 'ok',
|
|
272
|
+
summary: `Freshness check error: ${err.message}`,
|
|
273
|
+
pendingIntents,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Pull the org's presence, filter to intents targeting the given
|
|
279
|
+
* entity (self-intents excluded). Advisory — returns empty on any
|
|
280
|
+
* error so `checkFreshness` stays usable when the presence endpoint
|
|
281
|
+
* is down. Case-insensitive match on entityType + entityId to absorb
|
|
282
|
+
* PascalCase / lowercase divergence.
|
|
283
|
+
*/
|
|
284
|
+
async fetchPendingIntentsFor(entityType, entityId) {
|
|
285
|
+
const etLower = entityType.toLowerCase();
|
|
286
|
+
const idLower = entityId.toLowerCase();
|
|
287
|
+
const entries = await this.fetchPresence(true);
|
|
288
|
+
const result = [];
|
|
289
|
+
for (const entry of entries) {
|
|
290
|
+
if (!entry.activeIntents)
|
|
291
|
+
continue;
|
|
292
|
+
for (const intent of entry.activeIntents) {
|
|
293
|
+
if (intent.entityType.toLowerCase() === etLower &&
|
|
294
|
+
intent.entityId.toLowerCase() === idLower) {
|
|
295
|
+
result.push(intent);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
// ── AI SDK hooks ─────────────────────────────────────────────────────
|
|
302
|
+
/**
|
|
303
|
+
* Build a `prepareStep` hook for AI SDK's generateText / streamText /
|
|
304
|
+
* ToolLoopAgent. Called before each step — injects a system message
|
|
305
|
+
* summarizing what other agents are doing right now.
|
|
306
|
+
*
|
|
307
|
+
* ```ts
|
|
308
|
+
* const result = await generateText({
|
|
309
|
+
* // ...
|
|
310
|
+
* prepareStep: perception.prepareStep({ maxChars: 1500 }),
|
|
311
|
+
* });
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
prepareStep(options) {
|
|
315
|
+
const maxChars = options?.maxChars ?? 1500;
|
|
316
|
+
const focusFromToolCalls = options?.focusFromToolCalls;
|
|
317
|
+
const skipFirstStep = options?.skipFirstStep ?? false;
|
|
318
|
+
return async ({ stepNumber, steps, messages }) => {
|
|
319
|
+
if (skipFirstStep && stepNumber === 0)
|
|
320
|
+
return undefined;
|
|
321
|
+
// Derive focus entities from recent tool calls if configured
|
|
322
|
+
let focusEntities;
|
|
323
|
+
if (focusFromToolCalls && steps.length > 0) {
|
|
324
|
+
const focus = new Set();
|
|
325
|
+
for (const step of steps) {
|
|
326
|
+
for (const call of step.toolCalls ?? []) {
|
|
327
|
+
const tokens = focusFromToolCalls(call);
|
|
328
|
+
if (tokens)
|
|
329
|
+
tokens.forEach((t) => focus.add(t));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (focus.size > 0)
|
|
333
|
+
focusEntities = [...focus];
|
|
334
|
+
}
|
|
335
|
+
const { prompt } = await this.gather({ maxChars, focusEntities });
|
|
336
|
+
const awareness = { role: 'system', content: prompt };
|
|
337
|
+
return {
|
|
338
|
+
messages: [...messages, awareness],
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Build an `onStepFinish` hook for AI SDK. Called after each step —
|
|
344
|
+
* announces the agent's activity based on the tool calls that just ran.
|
|
345
|
+
*
|
|
346
|
+
* ```ts
|
|
347
|
+
* const result = await generateText({
|
|
348
|
+
* // ...
|
|
349
|
+
* onStepFinish: perception.onStepFinish(),
|
|
350
|
+
* });
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
onStepFinish(options) {
|
|
354
|
+
const resolveActivity = options?.activity ??
|
|
355
|
+
((ctx) => {
|
|
356
|
+
const lastCall = ctx.toolCalls?.[ctx.toolCalls.length - 1];
|
|
357
|
+
if (!lastCall)
|
|
358
|
+
return null;
|
|
359
|
+
return {
|
|
360
|
+
entityType: 'Tool',
|
|
361
|
+
entityId: lastCall.toolName,
|
|
362
|
+
action: 'executed',
|
|
363
|
+
detail: lastCall.toolName,
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
return async (ctx) => {
|
|
367
|
+
const activity = resolveActivity(ctx);
|
|
368
|
+
if (activity) {
|
|
369
|
+
await this.announce('online', activity);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Wrap an AI SDK tool to check entity freshness before executing. If the
|
|
375
|
+
* entity was modified by another actor since the LLM last saw it, returns
|
|
376
|
+
* a diff summary as the tool result instead of executing — the LLM adjusts
|
|
377
|
+
* its plan rather than blindly overwriting.
|
|
378
|
+
*
|
|
379
|
+
* ```ts
|
|
380
|
+
* tools: {
|
|
381
|
+
* updateSlide: perception.wrapTool(
|
|
382
|
+
* tool({ inputSchema: ..., execute: ... }),
|
|
383
|
+
* { entityType: 'Slide', getEntityId: (args) => args.id },
|
|
384
|
+
* ),
|
|
385
|
+
* }
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
wrapTool(originalTool, config) {
|
|
389
|
+
const originalExecute = originalTool.execute;
|
|
390
|
+
if (!originalExecute)
|
|
391
|
+
return originalTool;
|
|
392
|
+
const self = this;
|
|
393
|
+
const announceOnExecute = config.announceOnExecute ?? true;
|
|
394
|
+
const wrappedExecute = async (args, opts) => {
|
|
395
|
+
const entityId = config.getEntityId(args);
|
|
396
|
+
// No id → nothing to guard, just execute
|
|
397
|
+
if (!entityId) {
|
|
398
|
+
return originalExecute(args, opts);
|
|
399
|
+
}
|
|
400
|
+
// Freshness check (skipped when no baseline timestamp is provided)
|
|
401
|
+
const lastSeen = config.lastSeenAt?.(args, opts);
|
|
402
|
+
if (lastSeen !== undefined && lastSeen > 0) {
|
|
403
|
+
const check = await self.checkFreshness(config.entityType, entityId, lastSeen);
|
|
404
|
+
if (check.stale && check.summary) {
|
|
405
|
+
return check.summary;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Announce activity before executing (fire-and-forget)
|
|
409
|
+
if (announceOnExecute) {
|
|
410
|
+
void self.announce('online', {
|
|
411
|
+
entityType: config.entityType,
|
|
412
|
+
entityId,
|
|
413
|
+
action: 'editing',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return originalExecute(args, opts);
|
|
417
|
+
};
|
|
418
|
+
return {
|
|
419
|
+
...originalTool,
|
|
420
|
+
execute: wrappedExecute,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
// ── Internal ─────────────────────────────────────────────────────────
|
|
424
|
+
async fetchPresence(excludeSelf) {
|
|
425
|
+
try {
|
|
426
|
+
const url = `/api/presence?orgId=${encodeURIComponent(this.opts.organizationId)}`;
|
|
427
|
+
const res = await this.request('GET', url);
|
|
428
|
+
if (!res.ok)
|
|
429
|
+
return [];
|
|
430
|
+
const body = (await res.json());
|
|
431
|
+
const entries = body.entries ?? [];
|
|
432
|
+
// Filter by overlapping sync groups (presence API returns all org
|
|
433
|
+
// entries — the SDK narrows to our scope)
|
|
434
|
+
const ours = new Set(this.opts.syncGroups);
|
|
435
|
+
return entries.filter((e) => {
|
|
436
|
+
if (excludeSelf && e.userId === this.userId)
|
|
437
|
+
return false;
|
|
438
|
+
return (e.syncGroups ?? []).some((g) => ours.has(g));
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
formatPrompt(snapshot, opts) {
|
|
446
|
+
const lines = [];
|
|
447
|
+
const now = new Date(snapshot.timestamp).toISOString();
|
|
448
|
+
lines.push(`[Team context as of ${now}]`);
|
|
449
|
+
const focus = new Set(opts.focusEntities ?? []);
|
|
450
|
+
const hasFocus = focus.size > 0;
|
|
451
|
+
// Sort: focused entities first, then agents, then humans
|
|
452
|
+
const relevant = hasFocus
|
|
453
|
+
? snapshot.presence.filter((e) => e.activity && focus.has(`${e.activity.entityType}:${e.activity.entityId}`))
|
|
454
|
+
: snapshot.presence;
|
|
455
|
+
if (relevant.length === 0) {
|
|
456
|
+
lines.push('No other participants active in your scope.');
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
lines.push(hasFocus
|
|
460
|
+
? `Participants working on focused entities (${opts.focusEntities.join(', ')}):`
|
|
461
|
+
: `Active participants:`);
|
|
462
|
+
for (const entry of relevant) {
|
|
463
|
+
const role = entry.isAgent ? 'agent' : 'human';
|
|
464
|
+
const base = `- ${role} ${entry.userId} [${entry.status}]`;
|
|
465
|
+
if (entry.activity) {
|
|
466
|
+
const act = entry.activity;
|
|
467
|
+
const detail = act.detail ? ` (${act.detail})` : '';
|
|
468
|
+
lines.push(`${base}: ${act.action} ${act.entityType}:${act.entityId}${detail}`);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
lines.push(base);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (hasFocus && relevant.length < snapshot.presence.length) {
|
|
475
|
+
const others = snapshot.presence.length - relevant.length;
|
|
476
|
+
lines.push(`(${others} other participant${others === 1 ? '' : 's'} active on unrelated entities)`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
let result = lines.join('\n');
|
|
480
|
+
if (result.length > opts.maxChars) {
|
|
481
|
+
result = result.slice(0, opts.maxChars - 3) + '...';
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
async request(method, path, body) {
|
|
486
|
+
const url = `${this.opts.syncServerUrl}${path}`;
|
|
487
|
+
const headers = {
|
|
488
|
+
'Content-Type': 'application/json',
|
|
489
|
+
};
|
|
490
|
+
if (this.opts.authToken) {
|
|
491
|
+
headers.Authorization = `Bearer ${this.opts.authToken}`;
|
|
492
|
+
}
|
|
493
|
+
return this.opts.fetch(url, {
|
|
494
|
+
method,
|
|
495
|
+
headers,
|
|
496
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
497
|
+
signal: AbortSignal.timeout(this.opts.timeoutMs),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
export { Agent } from './Agent.js';
|