@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/frockbot.json +34 -0
- package/package.json +34 -6
- package/src/catalog.test.ts +82 -0
- package/src/catalog.ts +112 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/runtime.test.ts +526 -0
- package/src/runtime.ts +122 -0
- package/src/user.test.ts +378 -0
- package/src/user.ts +401 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
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