@frockbot/plugin-provider-frock-ai 0.0.0 → 0.3.13

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/src/user.ts ADDED
@@ -0,0 +1,401 @@
1
+ import type {
2
+ ConnectionView,
3
+ OperationReceiptV1,
4
+ UserConfigurationCommandV1,
5
+ UserSettingsViewV1,
6
+ } from "@frockbot/configuration-core";
7
+ import {
8
+ decodeConnectionCommandReceiptV1,
9
+ decodeConnectionCommandV1,
10
+ type ConnectionCommandReceiptV1,
11
+ type ConnectionCommandV1,
12
+ } from "@frockbot/connection-core";
13
+ import type { Plugin } from "cordis";
14
+ import {
15
+ FROCK_AI_CONNECTION_GENERATION,
16
+ FROCK_AI_CONNECTION_ID,
17
+ FROCK_AI_CONNECTION_TYPE_ID,
18
+ FROCK_AI_DEFAULT_MODEL,
19
+ FROCK_AI_PACKAGE_ID,
20
+ FROCK_AI_PROVIDER_TYPE,
21
+ frockAiStaticCatalogV1,
22
+ } from "./catalog.js";
23
+ import { defineUserBackendContribution } from "@frockbot/kernel-contracts/contributions";
24
+
25
+ // Durable storage keys. They read `flock-` because they are already written in
26
+ // every existing User's Durable Object; see the note in `catalog.ts`.
27
+ const BOOTSTRAP_KEY = "provider-flock-ai:bootstrap-v1";
28
+ const COMMAND_PREFIX = "provider-flock-ai:command:";
29
+ const PACKAGE_VERSION = "0.0.1";
30
+
31
+ interface FrockAiUserSettingsTransaction {
32
+ get<T>(key: string): Promise<T | undefined>;
33
+ put<T>(key: string, value: T): Promise<void>;
34
+ put(entries: Record<string, unknown>): Promise<void>;
35
+ }
36
+
37
+ interface FrockAiUserSettingsStorage extends FrockAiUserSettingsTransaction {
38
+ transaction<T>(
39
+ callback: (storage: FrockAiUserSettingsTransaction) => Promise<T>,
40
+ ): Promise<T>;
41
+ }
42
+
43
+ interface UserConfigurationReadBootstrap {
44
+ readonly packageId: string;
45
+ bootstrap(userId: string): Promise<void>;
46
+ }
47
+
48
+ interface FrockAiUserSettingsHost {
49
+ read(
50
+ userId: string,
51
+ storage: FrockAiUserSettingsTransaction,
52
+ ): Promise<UserSettingsViewV1>;
53
+ executeConfigurationCommand(
54
+ userId: string,
55
+ command: UserConfigurationCommandV1,
56
+ storage: FrockAiUserSettingsTransaction,
57
+ ): Promise<OperationReceiptV1>;
58
+ createConnection(
59
+ userId: string,
60
+ connection: ConnectionView,
61
+ storage: FrockAiUserSettingsTransaction,
62
+ ): Promise<ConnectionView>;
63
+ replaceConnection(
64
+ userId: string,
65
+ connectionId: string,
66
+ expectedGeneration: string | undefined,
67
+ connection: ConnectionView,
68
+ storage: FrockAiUserSettingsTransaction,
69
+ ): Promise<ConnectionView>;
70
+ getConnection(
71
+ userId: string,
72
+ connectionId: string,
73
+ storage: FrockAiUserSettingsTransaction,
74
+ ): Promise<ConnectionView | undefined>;
75
+ registerConfigurationReadBootstrap(
76
+ bootstrap: UserConfigurationReadBootstrap,
77
+ ): () => void;
78
+ }
79
+
80
+ interface FrockAiUserBackendHost {
81
+ storage: FrockAiUserSettingsStorage;
82
+ settings: FrockAiUserSettingsHost;
83
+ }
84
+
85
+ interface StoredBootstrapV1 {
86
+ schemaVersion: 1;
87
+ userId: string;
88
+ }
89
+
90
+ interface StoredCommandV1 {
91
+ accountId: string;
92
+ command: ConnectionCommandV1;
93
+ receipt: ConnectionCommandReceiptV1;
94
+ }
95
+
96
+ function decodeBootstrap(input: unknown): StoredBootstrapV1 {
97
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
98
+ throw new Error("Stored Frock AI bootstrap marker is invalid");
99
+ }
100
+ const value = input as Record<string, unknown>;
101
+ if (
102
+ Object.keys(value).length !== 2 ||
103
+ value.schemaVersion !== 1 ||
104
+ typeof value.userId !== "string" ||
105
+ !value.userId
106
+ ) {
107
+ throw new Error("Stored Frock AI bootstrap marker is invalid");
108
+ }
109
+ return { schemaVersion: 1, userId: value.userId };
110
+ }
111
+
112
+ function decodeStoredCommand(input: unknown): StoredCommandV1 {
113
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
114
+ throw new Error("Stored Frock AI command is invalid");
115
+ }
116
+ const value = input as Record<string, unknown>;
117
+ if (
118
+ Object.keys(value).length !== 3 ||
119
+ typeof value.accountId !== "string" ||
120
+ !value.accountId
121
+ ) {
122
+ throw new Error("Stored Frock AI command is invalid");
123
+ }
124
+ return {
125
+ accountId: value.accountId,
126
+ command: decodeConnectionCommandV1(value.command),
127
+ receipt: decodeConnectionCommandReceiptV1(value.receipt),
128
+ };
129
+ }
130
+
131
+ function ambientConnection(): ConnectionView {
132
+ return {
133
+ connectionId: FROCK_AI_CONNECTION_ID,
134
+ packageId: FROCK_AI_PACKAGE_ID,
135
+ connectionTypeId: FROCK_AI_CONNECTION_TYPE_ID,
136
+ displayName: "Frock AI",
137
+ state: "ready",
138
+ generation: FROCK_AI_CONNECTION_GENERATION,
139
+ providerType: FROCK_AI_PROVIDER_TYPE,
140
+ authorization: {
141
+ schemaVersion: 1,
142
+ kind: "ambient-native",
143
+ credential: {
144
+ schemaVersion: 1,
145
+ configured: true,
146
+ source: "ambient-native",
147
+ writable: false,
148
+ generation: FROCK_AI_CONNECTION_GENERATION,
149
+ },
150
+ },
151
+ modelCatalog: frockAiStaticCatalogV1(),
152
+ safeMetadata: { catalog: "static" },
153
+ };
154
+ }
155
+
156
+ function platformModelResolves(settings: UserSettingsViewV1): boolean {
157
+ const model = settings.platformModel;
158
+ if (!model) return false;
159
+ const connection = settings.connections.find(
160
+ (candidate) => candidate.connectionId === model.connectionId,
161
+ );
162
+ if (!connection || connection.state !== "ready") return false;
163
+ const installed = settings.packages.some(
164
+ (candidate) =>
165
+ candidate.packageId === connection.packageId &&
166
+ candidate.state === "installed",
167
+ );
168
+ return Boolean(
169
+ installed &&
170
+ connection.modelCatalog?.models.some(
171
+ (candidate) => candidate.providerModelId === model.providerModelId,
172
+ ),
173
+ );
174
+ }
175
+
176
+ function frockConnectionNeedsRepair(connection: ConnectionView): boolean {
177
+ return (
178
+ connection.state !== "ready" ||
179
+ connection.providerType !== FROCK_AI_PROVIDER_TYPE ||
180
+ !connection.modelCatalog?.models.some(
181
+ (candidate) => candidate.providerModelId === FROCK_AI_DEFAULT_MODEL,
182
+ )
183
+ );
184
+ }
185
+
186
+ function requireApplied(receipt: OperationReceiptV1): void {
187
+ if (receipt.status === "rejected") {
188
+ throw new Error(receipt.failure);
189
+ }
190
+ }
191
+
192
+ export class FrockAiUserBackendContribution implements UserConfigurationReadBootstrap {
193
+ readonly packageId = FROCK_AI_PACKAGE_ID;
194
+
195
+ constructor(private readonly host: FrockAiUserBackendHost) {}
196
+
197
+ async bootstrap(userId: string): Promise<void> {
198
+ await this.host.storage.transaction(async (storage) => {
199
+ const marker = await storage.get<unknown>(BOOTSTRAP_KEY);
200
+ if (marker !== undefined) {
201
+ const decoded = decodeBootstrap(marker);
202
+ if (decoded.userId !== userId) {
203
+ throw new Error("Frock AI bootstrap belongs to another User");
204
+ }
205
+ }
206
+
207
+ let settings = await this.host.settings.read(userId, storage);
208
+ const installation = settings.packages.find(
209
+ (candidate) => candidate.packageId === FROCK_AI_PACKAGE_ID,
210
+ );
211
+ if (
212
+ installation?.state !== "installed" ||
213
+ installation.version !== PACKAGE_VERSION
214
+ ) {
215
+ requireApplied(
216
+ await this.host.settings.executeConfigurationCommand(
217
+ userId,
218
+ {
219
+ schemaVersion: 1,
220
+ type: "user/install-package",
221
+ commandId: `flock-ai-repair-install-v1-${settings.revision}`,
222
+ expectedRevision: settings.revision,
223
+ packageId: FROCK_AI_PACKAGE_ID,
224
+ version: PACKAGE_VERSION,
225
+ },
226
+ storage,
227
+ ),
228
+ );
229
+ }
230
+
231
+ let connection = await this.host.settings.createConnection(
232
+ userId,
233
+ ambientConnection(),
234
+ storage,
235
+ );
236
+ if (
237
+ connection.packageId !== FROCK_AI_PACKAGE_ID ||
238
+ connection.connectionTypeId !== FROCK_AI_CONNECTION_TYPE_ID
239
+ ) {
240
+ throw new Error("Frock AI Connection identity is invalid");
241
+ }
242
+ if (frockConnectionNeedsRepair(connection)) {
243
+ await this.host.settings.replaceConnection(
244
+ userId,
245
+ FROCK_AI_CONNECTION_ID,
246
+ connection.generation,
247
+ ambientConnection(),
248
+ storage,
249
+ );
250
+ }
251
+
252
+ settings = await this.host.settings.read(userId, storage);
253
+ if (!platformModelResolves(settings)) {
254
+ requireApplied(
255
+ await this.host.settings.executeConfigurationCommand(
256
+ userId,
257
+ {
258
+ schemaVersion: 1,
259
+ type: "user/set-platform-model",
260
+ commandId: `flock-ai-repair-platform-model-v1-${settings.revision}`,
261
+ expectedRevision: settings.revision,
262
+ model: {
263
+ connectionId: FROCK_AI_CONNECTION_ID,
264
+ providerModelId: FROCK_AI_DEFAULT_MODEL,
265
+ },
266
+ },
267
+ storage,
268
+ ),
269
+ );
270
+ }
271
+ if (marker === undefined) {
272
+ await storage.put(BOOTSTRAP_KEY, { schemaVersion: 1, userId });
273
+ }
274
+ });
275
+ }
276
+
277
+ async executeConnection(
278
+ accountId: string,
279
+ input: unknown,
280
+ ): Promise<ConnectionCommandReceiptV1> {
281
+ const command = decodeConnectionCommandV1(input);
282
+ const key = `${COMMAND_PREFIX}${command.commandId}`;
283
+ return this.host.storage.transaction(async (storage) => {
284
+ const stored = await storage.get<unknown>(key);
285
+ if (stored !== undefined) {
286
+ const decoded = decodeStoredCommand(stored);
287
+ if (
288
+ decoded.accountId !== accountId ||
289
+ JSON.stringify(decoded.command) !== JSON.stringify(command)
290
+ ) {
291
+ throw new Error("Frock AI command id collision");
292
+ }
293
+ return decoded.receipt;
294
+ }
295
+ const connectionId =
296
+ "connectionId" in command
297
+ ? command.connectionId
298
+ : FROCK_AI_CONNECTION_ID;
299
+ let status: ConnectionCommandReceiptV1["status"] = "failed";
300
+ const current = await this.host.settings.getConnection(
301
+ accountId,
302
+ connectionId,
303
+ storage,
304
+ );
305
+ if (current?.packageId === FROCK_AI_PACKAGE_ID) {
306
+ if (command.type === "connection/refresh-models") {
307
+ await this.host.settings.replaceConnection(
308
+ accountId,
309
+ connectionId,
310
+ current.generation,
311
+ { ...current, modelCatalog: frockAiStaticCatalogV1() },
312
+ storage,
313
+ );
314
+ status = "applied";
315
+ } else if (command.type === "connection/update-label") {
316
+ await this.host.settings.replaceConnection(
317
+ accountId,
318
+ connectionId,
319
+ current.generation,
320
+ { ...current, displayName: command.label },
321
+ storage,
322
+ );
323
+ status = "applied";
324
+ }
325
+ }
326
+ const receipt: ConnectionCommandReceiptV1 = {
327
+ schemaVersion: 1,
328
+ commandId: command.commandId,
329
+ connectionId,
330
+ status,
331
+ };
332
+ await storage.put(key, { accountId, command, receipt });
333
+ return receipt;
334
+ });
335
+ }
336
+
337
+ async lookupConnectionCommand(
338
+ accountId: string,
339
+ commandId: string,
340
+ ): Promise<ConnectionCommandReceiptV1 | undefined> {
341
+ const stored = await this.host.storage.get<unknown>(
342
+ `${COMMAND_PREFIX}${commandId}`,
343
+ );
344
+ if (stored === undefined) return undefined;
345
+ const decoded = decodeStoredCommand(stored);
346
+ return decoded.accountId === accountId ? decoded.receipt : undefined;
347
+ }
348
+
349
+ async leaseModelCredential(): Promise<never> {
350
+ throw new Error("Frock AI uses its deployment binding, not a credential");
351
+ }
352
+
353
+ settleModelCredential(): Promise<void> {
354
+ return Promise.resolve();
355
+ }
356
+ }
357
+
358
+ export function createFrockAiUserBackendContribution(
359
+ host: FrockAiUserBackendHost,
360
+ ): FrockAiUserBackendContribution {
361
+ return new FrockAiUserBackendContribution(host);
362
+ }
363
+
364
+ export function createFrockAiUserBackendPlugin(
365
+ host: FrockAiUserBackendHost,
366
+ lifecycle: { mount(value: FrockAiUserBackendContribution): () => void },
367
+ ): Plugin {
368
+ return () => {
369
+ const contribution = createFrockAiUserBackendContribution(host);
370
+ const unregister =
371
+ host.settings.registerConfigurationReadBootstrap(contribution);
372
+ const dispose = lifecycle.mount(contribution);
373
+ return () => {
374
+ unregister();
375
+ dispose();
376
+ };
377
+ };
378
+ }
379
+
380
+ /**
381
+ * What an application hands this Contribution: the ambient Frock AI Connection, under the
382
+ * Package's own key so one wide host object can satisfy every Package's slice
383
+ * without their fields colliding.
384
+ */
385
+ export interface FrockAiUserApplicationHostV1 {
386
+ frockAi: FrockAiUserBackendHost;
387
+ }
388
+
389
+ /**
390
+ * The manifest's `user` entry, resolved by specifier. The
391
+ * application looks this descriptor up in its Contribution table; it never
392
+ * branches on which Package it belongs to.
393
+ */
394
+ export const userContribution = defineUserBackendContribution<
395
+ FrockAiUserApplicationHostV1,
396
+ FrockAiUserBackendContribution
397
+ >({
398
+ specifier: "@frockbot/plugin-provider-frock-ai/user",
399
+ create: (host, lifecycle) =>
400
+ createFrockAiUserBackendPlugin(host.frockAi, lifecycle),
401
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "allowImportingTsExtensions": true,
7
+ "resolveJsonModule": true,
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "lib": ["ES2023", "DOM"],
12
+ "types": ["bun"]
13
+ },
14
+ "include": ["src/**/*.ts"]
15
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @frockbot/plugin-provider-frock-ai
2
-
3
- Placeholder reserving this name. See https://github.com/timoconnellaus/frockbot.