@blastlauncher/protocol 0.0.0-stage → 3.0.0-alpha.1
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/LICENSE +7 -0
- package/README.md +4 -2
- package/dist/capabilities.d.ts +584 -0
- package/dist/capabilities.js +544 -0
- package/dist/embedder-schema.d.ts +7 -0
- package/dist/embedder-schema.js +314 -0
- package/dist/embedder.d.ts +173 -0
- package/dist/embedder.js +151 -0
- package/dist/framing.d.ts +20 -0
- package/dist/framing.js +115 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/messages.d.ts +153 -0
- package/dist/messages.js +652 -0
- package/dist/provider-codec.d.ts +4 -0
- package/dist/provider-codec.js +57 -0
- package/dist/provider.d.ts +236 -0
- package/dist/provider.js +1 -0
- package/package.json +26 -4
- package/schema/embedder-v1.schema.json +3909 -0
package/dist/messages.js
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
// Host <-> runtime messages (docs/decisions/0001-process-and-scene-model.md).
|
|
2
|
+
// Everything crossing the process boundary is untrusted input, so the parse
|
|
3
|
+
// functions are total: they never throw on malformed input and report the first
|
|
4
|
+
// failure as `<field path>: <reason>`.
|
|
5
|
+
//
|
|
6
|
+
// Unknown fields are rejected at every level, nested objects included. Adding
|
|
7
|
+
// one is a protocol change, and an ignored typo is worse than an error.
|
|
8
|
+
import { decodeModelRequest } from "./provider-codec.js";
|
|
9
|
+
export const PROTOCOL_VERSION = 1;
|
|
10
|
+
// Validation. Readers return the typed value or throw a ValidationError, which
|
|
11
|
+
// the public parse functions turn into a result. Throwing keeps the readers
|
|
12
|
+
// free of per-field branching; the error is only ever constructed here.
|
|
13
|
+
class ValidationError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "ValidationError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function fail(path, reason) {
|
|
20
|
+
throw new ValidationError(path === "" ? reason : `${path}: ${reason}`);
|
|
21
|
+
}
|
|
22
|
+
/** Joins a key onto a field path; the root path is the empty string. */
|
|
23
|
+
function keyPath(path, key) {
|
|
24
|
+
return path === "" ? key : `${path}.${key}`;
|
|
25
|
+
}
|
|
26
|
+
function readObject(value, path) {
|
|
27
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
28
|
+
fail(path, "expected object");
|
|
29
|
+
}
|
|
30
|
+
const prototype = Object.getPrototypeOf(value);
|
|
31
|
+
if (prototype !== null && prototype !== Object.prototype)
|
|
32
|
+
fail(path, "expected plain object");
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function checkKeys(object, path, allowed) {
|
|
36
|
+
for (const key of Object.keys(object)) {
|
|
37
|
+
if (!allowed.includes(key))
|
|
38
|
+
fail(keyPath(path, key), "unexpected field");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function readString(value, path) {
|
|
42
|
+
if (typeof value !== "string")
|
|
43
|
+
fail(path, "expected string");
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function readNumber(value, path) {
|
|
47
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
48
|
+
fail(path, "expected number");
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function readBoolean(value, path) {
|
|
52
|
+
if (typeof value !== "boolean")
|
|
53
|
+
fail(path, "expected boolean");
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function readNonNegativeInteger(value, path) {
|
|
57
|
+
const number = readNumber(value, path);
|
|
58
|
+
if (!Number.isInteger(number) || number < 0)
|
|
59
|
+
fail(path, "expected non-negative integer");
|
|
60
|
+
return number;
|
|
61
|
+
}
|
|
62
|
+
/** `seen` rejects cycles: a recursive structure is not JSON and not a scene. */
|
|
63
|
+
function readJsonValue(value, path, seen) {
|
|
64
|
+
switch (typeof value) {
|
|
65
|
+
case "boolean":
|
|
66
|
+
case "string":
|
|
67
|
+
return value;
|
|
68
|
+
case "number":
|
|
69
|
+
if (!Number.isFinite(value))
|
|
70
|
+
fail(path, "expected finite number");
|
|
71
|
+
return value;
|
|
72
|
+
case "object": {
|
|
73
|
+
if (value === null)
|
|
74
|
+
return null;
|
|
75
|
+
if (seen.has(value))
|
|
76
|
+
fail(path, "expected acyclic value");
|
|
77
|
+
seen.add(value);
|
|
78
|
+
try {
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
return value.map((item, index) => readJsonValue(item, `${path}[${index}]`, seen));
|
|
81
|
+
}
|
|
82
|
+
const object = readObject(value, path);
|
|
83
|
+
const result = {};
|
|
84
|
+
for (const [key, item] of Object.entries(object)) {
|
|
85
|
+
result[key] = readJsonValue(item, keyPath(path, key), seen);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
seen.delete(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
fail(path, "expected JSON value");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function readJsonArray(value, path) {
|
|
98
|
+
if (!Array.isArray(value))
|
|
99
|
+
fail(path, "expected array");
|
|
100
|
+
const seen = new WeakSet();
|
|
101
|
+
seen.add(value);
|
|
102
|
+
return value.map((item, index) => readJsonValue(item, `${path}[${index}]`, seen));
|
|
103
|
+
}
|
|
104
|
+
function readJsonObject(value, path) {
|
|
105
|
+
const object = readObject(value, path);
|
|
106
|
+
const seen = new WeakSet();
|
|
107
|
+
seen.add(object);
|
|
108
|
+
const result = {};
|
|
109
|
+
for (const [key, item] of Object.entries(object)) {
|
|
110
|
+
result[key] = readJsonValue(item, keyPath(path, key), seen);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
function readStringRecord(value, path) {
|
|
115
|
+
const object = readObject(value, path);
|
|
116
|
+
const result = {};
|
|
117
|
+
for (const [key, item] of Object.entries(object)) {
|
|
118
|
+
result[key] = readString(item, keyPath(path, key));
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function readErrorInfo(value, path) {
|
|
123
|
+
const object = readObject(value, path);
|
|
124
|
+
checkKeys(object, path, ["code", "message"]);
|
|
125
|
+
return {
|
|
126
|
+
code: readString(object["code"], keyPath(path, "code")),
|
|
127
|
+
message: readString(object["message"], keyPath(path, "message")),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function readCommandMode(value, path) {
|
|
131
|
+
const mode = readString(value, path);
|
|
132
|
+
switch (mode) {
|
|
133
|
+
case "view":
|
|
134
|
+
case "no-view":
|
|
135
|
+
case "menu-bar":
|
|
136
|
+
case "model-provider":
|
|
137
|
+
return mode;
|
|
138
|
+
default:
|
|
139
|
+
fail(path, 'expected one of "view" | "no-view" | "menu-bar" | "model-provider"');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function readAppearance(value, path) {
|
|
143
|
+
const appearance = readString(value, path);
|
|
144
|
+
if (appearance === "light" || appearance === "dark")
|
|
145
|
+
return appearance;
|
|
146
|
+
fail(path, 'expected one of "light" | "dark"');
|
|
147
|
+
}
|
|
148
|
+
function readTextSize(value, path) {
|
|
149
|
+
const textSize = readString(value, path);
|
|
150
|
+
if (textSize === "medium" || textSize === "large")
|
|
151
|
+
return textSize;
|
|
152
|
+
fail(path, 'expected one of "medium" | "large"');
|
|
153
|
+
}
|
|
154
|
+
function readCommand(value, path) {
|
|
155
|
+
const object = readObject(value, path);
|
|
156
|
+
checkKeys(object, path, [
|
|
157
|
+
"extension",
|
|
158
|
+
"name",
|
|
159
|
+
"entry",
|
|
160
|
+
"mode",
|
|
161
|
+
"arguments",
|
|
162
|
+
"launchContext",
|
|
163
|
+
"fallbackText",
|
|
164
|
+
"draftValues",
|
|
165
|
+
]);
|
|
166
|
+
// Optional fields stay absent and never materialize as `undefined`, so a
|
|
167
|
+
// parsed message equals the message that was sent.
|
|
168
|
+
const commandArguments = object["arguments"];
|
|
169
|
+
const launchContext = object["launchContext"];
|
|
170
|
+
const draftValues = object["draftValues"];
|
|
171
|
+
return {
|
|
172
|
+
extension: readString(object["extension"], keyPath(path, "extension")),
|
|
173
|
+
name: readString(object["name"], keyPath(path, "name")),
|
|
174
|
+
entry: readString(object["entry"], keyPath(path, "entry")),
|
|
175
|
+
mode: readCommandMode(object["mode"], keyPath(path, "mode")),
|
|
176
|
+
...(commandArguments === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: { arguments: readStringRecord(commandArguments, keyPath(path, "arguments")) }),
|
|
179
|
+
...(launchContext === undefined
|
|
180
|
+
? {}
|
|
181
|
+
: { launchContext: readJsonObject(launchContext, keyPath(path, "launchContext")) }),
|
|
182
|
+
...(object["fallbackText"] === undefined
|
|
183
|
+
? {}
|
|
184
|
+
: { fallbackText: readString(object["fallbackText"], keyPath(path, "fallbackText")) }),
|
|
185
|
+
...(draftValues === undefined
|
|
186
|
+
? {}
|
|
187
|
+
: { draftValues: readJsonObject(draftValues, keyPath(path, "draftValues")) }),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function readAvailableAPIs(value, path) {
|
|
191
|
+
if (!Array.isArray(value))
|
|
192
|
+
fail(path, "expected array");
|
|
193
|
+
return value.map((api, index) => {
|
|
194
|
+
if (api !== "AI" && api !== "BrowserExtension")
|
|
195
|
+
fail(`${path}[${index}]`, "expected AI or BrowserExtension");
|
|
196
|
+
return api;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function readEnvironment(value, path) {
|
|
200
|
+
const object = readObject(value, path);
|
|
201
|
+
checkKeys(object, path, [
|
|
202
|
+
"extensionName",
|
|
203
|
+
"commandName",
|
|
204
|
+
"ownerOrAuthorName",
|
|
205
|
+
"assetsPath",
|
|
206
|
+
"supportPath",
|
|
207
|
+
"isDevelopment",
|
|
208
|
+
"appearance",
|
|
209
|
+
"textSize",
|
|
210
|
+
"availableAPIs",
|
|
211
|
+
]);
|
|
212
|
+
return {
|
|
213
|
+
extensionName: readString(object["extensionName"], keyPath(path, "extensionName")),
|
|
214
|
+
commandName: readString(object["commandName"], keyPath(path, "commandName")),
|
|
215
|
+
ownerOrAuthorName: readString(object["ownerOrAuthorName"], keyPath(path, "ownerOrAuthorName")),
|
|
216
|
+
assetsPath: readString(object["assetsPath"], keyPath(path, "assetsPath")),
|
|
217
|
+
supportPath: readString(object["supportPath"], keyPath(path, "supportPath")),
|
|
218
|
+
isDevelopment: readBoolean(object["isDevelopment"], keyPath(path, "isDevelopment")),
|
|
219
|
+
appearance: readAppearance(object["appearance"], keyPath(path, "appearance")),
|
|
220
|
+
textSize: readTextSize(object["textSize"], keyPath(path, "textSize")),
|
|
221
|
+
...(object["availableAPIs"] === undefined
|
|
222
|
+
? {}
|
|
223
|
+
: {
|
|
224
|
+
availableAPIs: readAvailableAPIs(object["availableAPIs"], keyPath(path, "availableAPIs")),
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** A handler ref reaches props only as the exact `{ $handler }` shape. */
|
|
229
|
+
function isHandlerRefCandidate(value) {
|
|
230
|
+
return (typeof value === "object" && value !== null && !Array.isArray(value) && "$handler" in value);
|
|
231
|
+
}
|
|
232
|
+
function readHandlerRef(value, path) {
|
|
233
|
+
const object = readObject(value, path);
|
|
234
|
+
checkKeys(object, path, ["$handler"]);
|
|
235
|
+
return { $handler: readString(object["$handler"], keyPath(path, "$handler")) };
|
|
236
|
+
}
|
|
237
|
+
function readProps(value, path) {
|
|
238
|
+
const object = readObject(value, path);
|
|
239
|
+
const seen = new WeakSet();
|
|
240
|
+
seen.add(object);
|
|
241
|
+
const result = {};
|
|
242
|
+
for (const [key, item] of Object.entries(object)) {
|
|
243
|
+
const at = keyPath(path, key);
|
|
244
|
+
result[key] = isHandlerRefCandidate(item)
|
|
245
|
+
? readHandlerRef(item, at)
|
|
246
|
+
: readJsonValue(item, at, seen);
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
function readSceneNode(value, path, seen) {
|
|
251
|
+
const object = readObject(value, path);
|
|
252
|
+
if (seen.has(object))
|
|
253
|
+
fail(path, "expected acyclic scene");
|
|
254
|
+
seen.add(object);
|
|
255
|
+
try {
|
|
256
|
+
checkKeys(object, path, ["id", "type", "props", "children"]);
|
|
257
|
+
const children = object["children"];
|
|
258
|
+
if (!Array.isArray(children))
|
|
259
|
+
fail(keyPath(path, "children"), "expected array");
|
|
260
|
+
return {
|
|
261
|
+
id: readString(object["id"], keyPath(path, "id")),
|
|
262
|
+
type: readString(object["type"], keyPath(path, "type")),
|
|
263
|
+
props: readProps(object["props"], keyPath(path, "props")),
|
|
264
|
+
children: children.map((child, index) => readSceneNode(child, `${keyPath(path, "children")}[${index}]`, seen)),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
seen.delete(object);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function readLogLevel(value, path) {
|
|
272
|
+
const level = readString(value, path);
|
|
273
|
+
switch (level) {
|
|
274
|
+
case "debug":
|
|
275
|
+
case "info":
|
|
276
|
+
case "warn":
|
|
277
|
+
case "error":
|
|
278
|
+
return level;
|
|
279
|
+
default:
|
|
280
|
+
fail(path, 'expected one of "debug" | "info" | "warn" | "error"');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function readModel(value, path) {
|
|
284
|
+
const model = readJsonObject(value, path);
|
|
285
|
+
checkKeys(model, path, [
|
|
286
|
+
"id",
|
|
287
|
+
"title",
|
|
288
|
+
"isLocal",
|
|
289
|
+
"icon",
|
|
290
|
+
"description",
|
|
291
|
+
"capabilities",
|
|
292
|
+
"contextWindow",
|
|
293
|
+
"sizeInBytes",
|
|
294
|
+
]);
|
|
295
|
+
readString(model["id"], `${path}.id`);
|
|
296
|
+
readString(model["title"], `${path}.title`);
|
|
297
|
+
for (const key of ["isLocal"])
|
|
298
|
+
if (model[key] !== undefined)
|
|
299
|
+
readBoolean(model[key], `${path}.${key}`);
|
|
300
|
+
for (const key of ["description"])
|
|
301
|
+
if (model[key] !== undefined)
|
|
302
|
+
readString(model[key], `${path}.${key}`);
|
|
303
|
+
for (const key of ["contextWindow", "sizeInBytes"])
|
|
304
|
+
if (model[key] !== undefined)
|
|
305
|
+
readNumber(model[key], `${path}.${key}`);
|
|
306
|
+
if (model["capabilities"] !== undefined) {
|
|
307
|
+
const caps = readJsonObject(model["capabilities"], `${path}.capabilities`);
|
|
308
|
+
checkKeys(caps, `${path}.capabilities`, [
|
|
309
|
+
"systemMessage",
|
|
310
|
+
"temperature",
|
|
311
|
+
"vision",
|
|
312
|
+
"tools",
|
|
313
|
+
"streaming",
|
|
314
|
+
"reasoningEffort",
|
|
315
|
+
]);
|
|
316
|
+
for (const [key, capability] of Object.entries(caps)) {
|
|
317
|
+
const config = readJsonObject(capability, `${path}.capabilities.${key}`);
|
|
318
|
+
if (key !== "vision")
|
|
319
|
+
readBoolean(config["supported"], `${path}.capabilities.${key}.supported`);
|
|
320
|
+
if (key === "vision" || key === "reasoningEffort") {
|
|
321
|
+
const list = config[key === "vision" ? "mediaTypes" : "options"];
|
|
322
|
+
if (!Array.isArray(list) || !list.every((item) => typeof item === "string"))
|
|
323
|
+
fail(`${path}.capabilities.${key}`, "expected string array");
|
|
324
|
+
if (key === "reasoningEffort")
|
|
325
|
+
readString(config["default"], `${path}.capabilities.${key}.default`);
|
|
326
|
+
else if (!list.every((item) => ["image/png", "image/jpeg", "image/webp", "image/gif"].includes(String(item))))
|
|
327
|
+
fail(`${path}.capabilities.vision.mediaTypes`, "unknown media type");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return model;
|
|
332
|
+
}
|
|
333
|
+
function readPart(value) {
|
|
334
|
+
const part = readJsonObject(value, "part");
|
|
335
|
+
const type = readString(part["type"], "part.type");
|
|
336
|
+
const required = {
|
|
337
|
+
"text-start": ["id"],
|
|
338
|
+
"text-end": ["id"],
|
|
339
|
+
text: ["text"],
|
|
340
|
+
"text-delta": [],
|
|
341
|
+
"reasoning-start": ["id"],
|
|
342
|
+
"reasoning-end": ["id"],
|
|
343
|
+
reasoning: ["text"],
|
|
344
|
+
"reasoning-delta": [],
|
|
345
|
+
source: ["id", "sourceType"],
|
|
346
|
+
file: [],
|
|
347
|
+
"tool-call-streaming-start": ["toolCallId", "toolName"],
|
|
348
|
+
"tool-call-delta": ["toolCallId", "toolName", "argsTextDelta"],
|
|
349
|
+
"tool-input-start": ["id", "toolName"],
|
|
350
|
+
"tool-input-end": ["id"],
|
|
351
|
+
"tool-input-delta": ["id", "delta"],
|
|
352
|
+
"tool-input-error": [],
|
|
353
|
+
"tool-call": ["toolCallId", "toolName"],
|
|
354
|
+
"tool-result": ["toolCallId", "toolName"],
|
|
355
|
+
"tool-error": ["toolCallId", "toolName"],
|
|
356
|
+
"tool-output-denied": ["toolCallId", "toolName"],
|
|
357
|
+
"tool-approval-request": ["approvalId"],
|
|
358
|
+
finish: ["finishReason"],
|
|
359
|
+
error: [],
|
|
360
|
+
abort: [],
|
|
361
|
+
start: [],
|
|
362
|
+
"stream-start": [],
|
|
363
|
+
"response-metadata": [],
|
|
364
|
+
"start-step": [],
|
|
365
|
+
"finish-step": [],
|
|
366
|
+
"reasoning-part-finish": [],
|
|
367
|
+
"step-start": [],
|
|
368
|
+
raw: [],
|
|
369
|
+
};
|
|
370
|
+
if (!Object.hasOwn(required, type))
|
|
371
|
+
fail("part.type", "unknown stream part");
|
|
372
|
+
for (const key of required[type])
|
|
373
|
+
readString(part[key], `part.${key}`);
|
|
374
|
+
for (const key of ["id", "text", "textDelta", "title", "filename"])
|
|
375
|
+
if (part[key] !== undefined)
|
|
376
|
+
readString(part[key], `part.${key}`);
|
|
377
|
+
const requiredValues = {
|
|
378
|
+
file: ["file"],
|
|
379
|
+
"tool-call": ["input"],
|
|
380
|
+
"tool-error": ["error"],
|
|
381
|
+
"tool-approval-request": ["toolCall"],
|
|
382
|
+
error: ["error"],
|
|
383
|
+
};
|
|
384
|
+
for (const key of requiredValues[type] ?? [])
|
|
385
|
+
if (!(key in part))
|
|
386
|
+
fail(`part.${key}`, "required field");
|
|
387
|
+
if (part["totalUsage"] !== undefined) {
|
|
388
|
+
const usage = readJsonObject(part["totalUsage"], "part.totalUsage");
|
|
389
|
+
for (const key of ["inputTokens", "outputTokens", "totalTokens"])
|
|
390
|
+
if (usage[key] !== undefined)
|
|
391
|
+
readNumber(usage[key], `part.totalUsage.${key}`);
|
|
392
|
+
}
|
|
393
|
+
if (type === "source") {
|
|
394
|
+
if (part["sourceType"] === "url")
|
|
395
|
+
readString(part["url"], "part.url");
|
|
396
|
+
else if (part["sourceType"] === "document")
|
|
397
|
+
readString(part["mediaType"], "part.mediaType");
|
|
398
|
+
else
|
|
399
|
+
fail("part.sourceType", "expected url or document");
|
|
400
|
+
}
|
|
401
|
+
if (type === "finish" &&
|
|
402
|
+
!["stop", "length", "content-filter", "tool-calls", "error", "other", "unknown"].includes(String(part["finishReason"])))
|
|
403
|
+
fail("part.finishReason", "unknown reason");
|
|
404
|
+
return part;
|
|
405
|
+
}
|
|
406
|
+
function buildHostMessage(object) {
|
|
407
|
+
const type = readString(object["type"], "type");
|
|
408
|
+
switch (type) {
|
|
409
|
+
case "provider.getModels":
|
|
410
|
+
case "provider.abort":
|
|
411
|
+
checkKeys(object, "", ["type", "id"]);
|
|
412
|
+
return { type, id: readString(object["id"], "id") };
|
|
413
|
+
case "provider.stream": {
|
|
414
|
+
checkKeys(object, "", ["type", "id", "model", "request"]);
|
|
415
|
+
const request = readJsonObject(object["request"], "request");
|
|
416
|
+
checkKeys(request, "request", [
|
|
417
|
+
"system",
|
|
418
|
+
"messages",
|
|
419
|
+
"temperature",
|
|
420
|
+
"tools",
|
|
421
|
+
"toolChoice",
|
|
422
|
+
"providerOptions",
|
|
423
|
+
]);
|
|
424
|
+
if (request["system"] !== undefined)
|
|
425
|
+
readString(request["system"], "request.system");
|
|
426
|
+
if (request["temperature"] !== undefined)
|
|
427
|
+
readNumber(request["temperature"], "request.temperature");
|
|
428
|
+
if (request["toolChoice"] !== undefined &&
|
|
429
|
+
request["toolChoice"] !== "auto" &&
|
|
430
|
+
request["toolChoice"] !== "required")
|
|
431
|
+
fail("request.toolChoice", "expected auto or required");
|
|
432
|
+
if (request["tools"] !== undefined) {
|
|
433
|
+
const tools = readJsonObject(request["tools"], "request.tools");
|
|
434
|
+
for (const [name, toolValue] of Object.entries(tools)) {
|
|
435
|
+
const tool = readJsonObject(toolValue, `request.tools.${name}`);
|
|
436
|
+
checkKeys(tool, `request.tools.${name}`, ["type", "description", "inputSchema"]);
|
|
437
|
+
if (tool["type"] !== undefined && tool["type"] !== "function")
|
|
438
|
+
fail(`request.tools.${name}.type`, "expected function");
|
|
439
|
+
if (tool["description"] !== undefined)
|
|
440
|
+
readString(tool["description"], `request.tools.${name}.description`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const options = readJsonObject(request["providerOptions"], "request.providerOptions");
|
|
444
|
+
for (const [name, settings] of Object.entries(options))
|
|
445
|
+
readJsonObject(settings, `request.providerOptions.${name}`);
|
|
446
|
+
const raycast = readJsonObject(options["raycast"], "request.providerOptions.raycast");
|
|
447
|
+
for (const key of ["locale", "currentDate", "sessionId"])
|
|
448
|
+
readString(raycast[key], `request.providerOptions.raycast.${key}`);
|
|
449
|
+
if (raycast["reasoningEffort"] !== undefined)
|
|
450
|
+
readString(raycast["reasoningEffort"], "request.providerOptions.raycast.reasoningEffort");
|
|
451
|
+
if (request["messages"] !== undefined) {
|
|
452
|
+
if (!Array.isArray(request["messages"]))
|
|
453
|
+
fail("request.messages", "expected array");
|
|
454
|
+
for (const item of request["messages"]) {
|
|
455
|
+
const message = readJsonObject(item, "request.messages[]");
|
|
456
|
+
checkKeys(message, "request.messages[]", ["role", "content"]);
|
|
457
|
+
if (!["system", "user", "assistant", "tool"].includes(String(message["role"])))
|
|
458
|
+
fail("request.messages[].role", "unknown role");
|
|
459
|
+
if (message["role"] === "system")
|
|
460
|
+
readString(message["content"], "request.messages[].content");
|
|
461
|
+
else if (!Array.isArray(message["content"]))
|
|
462
|
+
fail("request.messages[].content", "expected array");
|
|
463
|
+
else
|
|
464
|
+
for (const content of message["content"]) {
|
|
465
|
+
const part = readJsonObject(content, "request.messages[].content[]");
|
|
466
|
+
const kind = readString(part["type"], "request.messages[].content[].type");
|
|
467
|
+
const allowed = message["role"] === "user"
|
|
468
|
+
? ["text", "file"]
|
|
469
|
+
: message["role"] === "assistant"
|
|
470
|
+
? ["text", "file", "reasoning", "tool-call"]
|
|
471
|
+
: ["tool-result"];
|
|
472
|
+
if (!allowed.includes(kind))
|
|
473
|
+
fail("request.messages[].content[].type", "invalid content for role");
|
|
474
|
+
if (kind === "text" || kind === "reasoning")
|
|
475
|
+
readString(part["text"], "request.messages[].content[].text");
|
|
476
|
+
if (kind === "file")
|
|
477
|
+
readString(part["mediaType"], "request.messages[].content[].mediaType");
|
|
478
|
+
if (kind === "tool-call" || kind === "tool-result") {
|
|
479
|
+
for (const key of ["toolCallId", "toolName"])
|
|
480
|
+
readString(part[key], `request.messages[].content[].${key}`);
|
|
481
|
+
if (kind === "tool-call" && !("input" in part))
|
|
482
|
+
fail("request.messages[].content[].input", "required field");
|
|
483
|
+
if (kind === "tool-result") {
|
|
484
|
+
const output = readJsonObject(part["output"], "request.messages[].content[].output");
|
|
485
|
+
if (output["type"] !== "json" || !("value" in output))
|
|
486
|
+
fail("request.messages[].content[].output", "expected JSON output");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
decodeModelRequest(request);
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
fail("request", "invalid attachment encoding");
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
type,
|
|
500
|
+
id: readString(object["id"], "id"),
|
|
501
|
+
model: readModel(object["model"], "model"),
|
|
502
|
+
request,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
case "init": {
|
|
506
|
+
checkKeys(object, "", ["type", "protocol", "command", "preferences", "environment"]);
|
|
507
|
+
return {
|
|
508
|
+
type: "init",
|
|
509
|
+
protocol: readNumber(object["protocol"], "protocol"),
|
|
510
|
+
command: readCommand(object["command"], "command"),
|
|
511
|
+
preferences: readJsonObject(object["preferences"], "preferences"),
|
|
512
|
+
environment: readEnvironment(object["environment"], "environment"),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
case "event": {
|
|
516
|
+
checkKeys(object, "", ["type", "node", "handler", "args"]);
|
|
517
|
+
return {
|
|
518
|
+
type: "event",
|
|
519
|
+
node: readString(object["node"], "node"),
|
|
520
|
+
handler: readString(object["handler"], "handler"),
|
|
521
|
+
args: readJsonArray(object["args"], "args"),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
case "capability.progress": {
|
|
525
|
+
checkKeys(object, "", ["type", "id", "value"]);
|
|
526
|
+
return {
|
|
527
|
+
type: "capability.progress",
|
|
528
|
+
id: readString(object["id"], "id"),
|
|
529
|
+
value: readJsonValue(object["value"], "value", new WeakSet()),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
case "capability.result": {
|
|
533
|
+
const id = readString(object["id"], "id");
|
|
534
|
+
const ok = readBoolean(object["ok"], "ok");
|
|
535
|
+
if (ok) {
|
|
536
|
+
checkKeys(object, "", ["type", "id", "ok", "value"]);
|
|
537
|
+
return {
|
|
538
|
+
type: "capability.result",
|
|
539
|
+
id,
|
|
540
|
+
ok: true,
|
|
541
|
+
value: readJsonValue(object["value"], "value", new WeakSet()),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
checkKeys(object, "", ["type", "id", "ok", "error"]);
|
|
545
|
+
return {
|
|
546
|
+
type: "capability.result",
|
|
547
|
+
id,
|
|
548
|
+
ok: false,
|
|
549
|
+
error: readErrorInfo(object["error"], "error"),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
case "shutdown": {
|
|
553
|
+
checkKeys(object, "", ["type"]);
|
|
554
|
+
return { type: "shutdown" };
|
|
555
|
+
}
|
|
556
|
+
default:
|
|
557
|
+
fail("type", `unknown message type ${JSON.stringify(type)}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function buildRuntimeMessage(object) {
|
|
561
|
+
const type = readString(object["type"], "type");
|
|
562
|
+
switch (type) {
|
|
563
|
+
case "provider.models": {
|
|
564
|
+
checkKeys(object, "", ["type", "id", "models"]);
|
|
565
|
+
if (!Array.isArray(object["models"]))
|
|
566
|
+
fail("models", "expected array");
|
|
567
|
+
return {
|
|
568
|
+
type,
|
|
569
|
+
id: readString(object["id"], "id"),
|
|
570
|
+
models: object["models"].map((model, i) => readModel(model, `models[${i}]`)),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
case "provider.part": {
|
|
574
|
+
checkKeys(object, "", ["type", "id", "part"]);
|
|
575
|
+
return { type, id: readString(object["id"], "id"), part: readPart(object["part"]) };
|
|
576
|
+
}
|
|
577
|
+
case "provider.end":
|
|
578
|
+
checkKeys(object, "", ["type", "id"]);
|
|
579
|
+
return { type, id: readString(object["id"], "id") };
|
|
580
|
+
case "provider.error":
|
|
581
|
+
checkKeys(object, "", ["type", "id", "error"]);
|
|
582
|
+
return {
|
|
583
|
+
type,
|
|
584
|
+
id: readString(object["id"], "id"),
|
|
585
|
+
error: readErrorInfo(object["error"], "error"),
|
|
586
|
+
};
|
|
587
|
+
case "ready": {
|
|
588
|
+
checkKeys(object, "", ["type", "protocol"]);
|
|
589
|
+
return { type: "ready", protocol: readNumber(object["protocol"], "protocol") };
|
|
590
|
+
}
|
|
591
|
+
case "scene": {
|
|
592
|
+
checkKeys(object, "", ["type", "revision", "root"]);
|
|
593
|
+
const root = object["root"];
|
|
594
|
+
return {
|
|
595
|
+
type: "scene",
|
|
596
|
+
revision: readNonNegativeInteger(object["revision"], "revision"),
|
|
597
|
+
root: root === null ? null : readSceneNode(root, "root", new WeakSet()),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
case "capability.cancel": {
|
|
601
|
+
checkKeys(object, "", ["type", "id"]);
|
|
602
|
+
return { type: "capability.cancel", id: readString(object["id"], "id") };
|
|
603
|
+
}
|
|
604
|
+
case "capability.request": {
|
|
605
|
+
checkKeys(object, "", ["type", "id", "name", "params"]);
|
|
606
|
+
return {
|
|
607
|
+
type: "capability.request",
|
|
608
|
+
id: readString(object["id"], "id"),
|
|
609
|
+
name: readString(object["name"], "name"),
|
|
610
|
+
params: readJsonValue(object["params"], "params", new WeakSet()),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
case "log": {
|
|
614
|
+
checkKeys(object, "", ["type", "level", "message"]);
|
|
615
|
+
return {
|
|
616
|
+
type: "log",
|
|
617
|
+
level: readLogLevel(object["level"], "level"),
|
|
618
|
+
message: readString(object["message"], "message"),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
case "error": {
|
|
622
|
+
checkKeys(object, "", ["type", "code", "message", "fatal"]);
|
|
623
|
+
return {
|
|
624
|
+
type: "error",
|
|
625
|
+
code: readString(object["code"], "code"),
|
|
626
|
+
message: readString(object["message"], "message"),
|
|
627
|
+
fatal: readBoolean(object["fatal"], "fatal"),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
default:
|
|
631
|
+
fail("type", `unknown message type ${JSON.stringify(type)}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function parse(build) {
|
|
635
|
+
try {
|
|
636
|
+
return { ok: true, message: build() };
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
// Anything that is not a validation failure is a bug here, not bad input.
|
|
640
|
+
if (error instanceof ValidationError)
|
|
641
|
+
return { ok: false, error: error.message };
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/** Validates an untyped value as a host -> runtime message. */
|
|
646
|
+
export function parseHostMessage(value) {
|
|
647
|
+
return parse(() => buildHostMessage(readObject(value, "")));
|
|
648
|
+
}
|
|
649
|
+
/** Validates an untyped value as a runtime -> host message. */
|
|
650
|
+
export function parseRuntimeMessage(value) {
|
|
651
|
+
return parse(() => buildRuntimeMessage(readObject(value, "")));
|
|
652
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { JsonValue } from "./messages.js";
|
|
2
|
+
import type { ModelRequest } from "./provider.js";
|
|
3
|
+
export declare function encodeModelRequest(request: ModelRequest): Record<string, JsonValue>;
|
|
4
|
+
export declare function decodeModelRequest(value: Record<string, JsonValue>): ModelRequest;
|