@frockbot/kernel-contracts 0.0.0 → 0.1.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/package.json +21 -6
- package/src/authoring.test.ts +143 -0
- package/src/authoring.ts +189 -0
- package/src/index.ts +11 -0
- package/src/isolate.test.ts +417 -0
- package/src/isolate.ts +704 -0
- package/src/model-invocation.ts +74 -0
- package/src/prompt-assembly.ts +54 -0
- package/src/send-to-user.test.ts +234 -0
- package/src/send-to-user.ts +384 -0
- package/src/session.test.ts +708 -0
- package/src/session.ts +521 -0
- package/src/skills.test.ts +123 -0
- package/src/skills.ts +164 -0
- package/src/tool-attachments.test.ts +100 -0
- package/src/tool-execution.ts +220 -0
- package/src/turn-history.test.ts +54 -0
- package/src/turn-history.ts +33 -0
- package/src/turn-type.test.ts +101 -0
- package/src/types.ts +1791 -0
- package/src/workspace.test.ts +913 -0
- package/src/workspace.ts +1176 -0
- package/tsconfig.json +14 -0
- package/README.md +0 -3
package/src/isolate.ts
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
// The Bot isolate boundary: every DTO that crosses between the Bot's Durable
|
|
2
|
+
// Object and a Dynamic Worker loaded for a non-first-party Package, plus the
|
|
3
|
+
// narrow interfaces the kernel declares for the host that mounts it.
|
|
4
|
+
//
|
|
5
|
+
// The shape follows `docs/plans/kernel-and-isolate.md` Step 4 with the three
|
|
6
|
+
// contract changes the Worker Loader spike forced
|
|
7
|
+
// (`docs/research/spike-worker-loader-from-do.md`):
|
|
8
|
+
//
|
|
9
|
+
// 1. `CAPABILITIES` cannot be an `RpcTarget` placed in `env` — workerd rejects
|
|
10
|
+
// it with `DataCloneError`. It is a loopback service binding minted with
|
|
11
|
+
// `ctx.exports.BotCapabilities({ props })`, so `BotCapabilitiesStub` is the
|
|
12
|
+
// call surface of a `WorkerEntrypoint`, not of an `RpcTarget`. Per-invocation
|
|
13
|
+
// narrowed objects are `RpcTarget`s *returned* from its methods.
|
|
14
|
+
// 2. `.get()` never throws; a broken artifact fails on the first RPC, so mount
|
|
15
|
+
// and `health()` are one guarded phase.
|
|
16
|
+
// 3. A reused loader id silently serves the first code, so the id is derived
|
|
17
|
+
// from the content address of the mounted modules and nothing else.
|
|
18
|
+
//
|
|
19
|
+
// Everything decoded here is untrusted: Bot-authored code produces the results
|
|
20
|
+
// and the capability requests, and the isolate produces the health report.
|
|
21
|
+
import type { TurnAdmissionV1 } from "./tool-execution.js";
|
|
22
|
+
import {
|
|
23
|
+
decodeTurnTypeV1,
|
|
24
|
+
type LlmStreamEvent,
|
|
25
|
+
type NormalizedModelRequest,
|
|
26
|
+
type ToolSchema,
|
|
27
|
+
} from "./types.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The wire contract version the kernel wrapper emits. Version 2 added
|
|
31
|
+
* per-tool turn admission; a version 1 isolate declares no admission and its
|
|
32
|
+
* tools are therefore offered on every turn type.
|
|
33
|
+
*/
|
|
34
|
+
export const ISOLATE_CONTRACT_VERSION = 2;
|
|
35
|
+
|
|
36
|
+
/** Every contract version the kernel still decodes. */
|
|
37
|
+
export type IsolateContractVersion = 1 | 2;
|
|
38
|
+
|
|
39
|
+
const ISOLATE_CONTRACT_VERSIONS: readonly IsolateContractVersion[] = [1, 2];
|
|
40
|
+
|
|
41
|
+
/** The upper bound on a single isolate invocation, enforced on both sides. */
|
|
42
|
+
export const ISOLATE_MAX_DEADLINE_MS = 60_000;
|
|
43
|
+
|
|
44
|
+
const MAX_ISOLATE_TOOLS = 64;
|
|
45
|
+
const MAX_ISOLATE_CAPABILITIES = 256;
|
|
46
|
+
const MAX_ISOLATE_CONTENT = 1_000_000;
|
|
47
|
+
const TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
48
|
+
|
|
49
|
+
export interface IsolateToolDescriptorV1 {
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
inputSchema: Record<string, unknown>;
|
|
53
|
+
idempotent: boolean;
|
|
54
|
+
/** Contract version 2 and later. Absent means every turn type. */
|
|
55
|
+
admission?: TurnAdmissionV1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface IsolateToolInvocationV1 {
|
|
59
|
+
schemaVersion: 1;
|
|
60
|
+
tool: string;
|
|
61
|
+
input: unknown;
|
|
62
|
+
botId: string;
|
|
63
|
+
sessionId: string;
|
|
64
|
+
runId: string;
|
|
65
|
+
turnId: string;
|
|
66
|
+
generationId: string;
|
|
67
|
+
deadlineMs: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface IsolateToolResultV1 {
|
|
71
|
+
schemaVersion: 1;
|
|
72
|
+
content: string;
|
|
73
|
+
isError: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface IsolateHealthV1 {
|
|
77
|
+
schemaVersion: 1;
|
|
78
|
+
ok: boolean;
|
|
79
|
+
packageId: string;
|
|
80
|
+
contractVersion: IsolateContractVersion;
|
|
81
|
+
tools: IsolateToolDescriptorV1[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** What `IDENTITY` carries into the isolate. Structured-clonable, never a stub. */
|
|
85
|
+
export interface IsolateIdentityV1 {
|
|
86
|
+
botId: string;
|
|
87
|
+
generationId: string;
|
|
88
|
+
packageId: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type IsolateCapabilityKindV1 =
|
|
92
|
+
"tool" | "model" | "memory" | "notification" | "computer";
|
|
93
|
+
|
|
94
|
+
export interface IsolateCapabilityDescriptorV1 {
|
|
95
|
+
capabilityId: string;
|
|
96
|
+
kind: IsolateCapabilityKindV1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface IsolateAuthorityRequestV1 {
|
|
100
|
+
capabilityId: string;
|
|
101
|
+
reason: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Self-modification never widens authority: the answer is always pending. */
|
|
105
|
+
export interface IsolatePendingDecisionV1 {
|
|
106
|
+
status: "pending-user-decision";
|
|
107
|
+
decisionId: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A capability call the authority could not serve. It is a declared variant,
|
|
112
|
+
* not an exception: an error thrown across the loopback binding would surface
|
|
113
|
+
* inside Bot code as an arbitrary host message, and Bot code has no contract
|
|
114
|
+
* for that. The reason is normalized and bounded.
|
|
115
|
+
*/
|
|
116
|
+
export interface IsolateCapabilityFailureV1 {
|
|
117
|
+
status: "unavailable";
|
|
118
|
+
reason: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type IsolateAuthorityOutcomeV1 =
|
|
122
|
+
IsolatePendingDecisionV1 | IsolateCapabilityFailureV1;
|
|
123
|
+
|
|
124
|
+
export type IsolateCapabilityListOutcomeV1 =
|
|
125
|
+
IsolateCapabilityDescriptorV1[] | IsolateCapabilityFailureV1;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* D6: model invocation as an Assignment-derived binding. Events cross the RPC
|
|
129
|
+
* boundary as an NDJSON byte stream — see `decodeIsolateModelEventV1`. A
|
|
130
|
+
* `ReadableStream` of JavaScript objects is not transferable over workerd RPC;
|
|
131
|
+
* a byte stream is, so the kernel encodes and the isolate decodes.
|
|
132
|
+
*/
|
|
133
|
+
export type IsolateModelInvocationV1 =
|
|
134
|
+
| {
|
|
135
|
+
status: "streaming";
|
|
136
|
+
requestId: string;
|
|
137
|
+
events: ReadableStream<Uint8Array>;
|
|
138
|
+
}
|
|
139
|
+
| IsolatePendingDecisionV1;
|
|
140
|
+
|
|
141
|
+
export type IsolateModelOutcomeV1 =
|
|
142
|
+
IsolateModelInvocationV1 | IsolateCapabilityFailureV1;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The wrapper `WorkerEntrypoint` the kernel generates. Bot code never
|
|
146
|
+
* implements this; it exports `tools` and `execute` and the wrapper adapts.
|
|
147
|
+
*/
|
|
148
|
+
export interface BotIsolateEntrypoint {
|
|
149
|
+
health(): Promise<IsolateHealthV1>;
|
|
150
|
+
execute(invocation: IsolateToolInvocationV1): Promise<IsolateToolResultV1>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The loopback service binding the Bot's Durable Object mints for one isolate.
|
|
155
|
+
* Every method is Assignment-derived: nothing here can hand out authority the
|
|
156
|
+
* Bot does not already hold.
|
|
157
|
+
*/
|
|
158
|
+
export interface BotCapabilitiesStub {
|
|
159
|
+
list(): Promise<IsolateCapabilityListOutcomeV1>;
|
|
160
|
+
/**
|
|
161
|
+
* D6 addendum. The kernel records the normalized request and acquires the
|
|
162
|
+
* credential lease through the existing provider path *before* forwarding.
|
|
163
|
+
* Without a matching enabled model Assignment the answer is a pending
|
|
164
|
+
* decision, never a grant.
|
|
165
|
+
*/
|
|
166
|
+
invokeModel(request: NormalizedModelRequest): Promise<IsolateModelOutcomeV1>;
|
|
167
|
+
requestAuthority(
|
|
168
|
+
request: IsolateAuthorityRequestV1,
|
|
169
|
+
): Promise<IsolateAuthorityOutcomeV1>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Everything Bot code can see. Nothing else is in scope: `globalOutbound` is
|
|
174
|
+
* null, so `Object.keys(env)` inside the isolate is exactly
|
|
175
|
+
* `["CAPABILITIES", "IDENTITY"]`.
|
|
176
|
+
*/
|
|
177
|
+
export interface BotIsolateEnv {
|
|
178
|
+
IDENTITY: IsolateIdentityV1;
|
|
179
|
+
CAPABILITIES: BotCapabilitiesStub;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface IsolateModuleMap {
|
|
183
|
+
[path: string]: { js: string };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface IsolateLoadInputV1 {
|
|
187
|
+
loaderId: string;
|
|
188
|
+
modules: IsolateModuleMap;
|
|
189
|
+
env: BotIsolateEnv;
|
|
190
|
+
limits: { cpuMs: number; subRequests: number };
|
|
191
|
+
compatibilityDate: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The kernel-declared isolate host. A runtime adapter implements it. */
|
|
195
|
+
export interface IsolateHost {
|
|
196
|
+
load(input: IsolateLoadInputV1): BotIsolateEntrypoint;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* D2. The loader identity, and nothing else — a reused id silently serves the
|
|
201
|
+
* first code, so every component here is content- or owner-derived. The hash
|
|
202
|
+
* covers the *mounted module set* (kernel wrapper text plus the Package
|
|
203
|
+
* artifact), so a wrapper change is a new isolate.
|
|
204
|
+
*/
|
|
205
|
+
export function isolateLoaderIdV1(input: {
|
|
206
|
+
userId: string;
|
|
207
|
+
botId: string;
|
|
208
|
+
artifactSetHash: string;
|
|
209
|
+
}): string {
|
|
210
|
+
const userId = boundedString(input.userId, "isolate loader userId", 256);
|
|
211
|
+
const botId = boundedString(input.botId, "isolate loader botId", 256);
|
|
212
|
+
const hash = boundedString(
|
|
213
|
+
input.artifactSetHash,
|
|
214
|
+
"isolate loader artifactSetHash",
|
|
215
|
+
128,
|
|
216
|
+
);
|
|
217
|
+
if (
|
|
218
|
+
/[:\s]/.test(userId) ||
|
|
219
|
+
/[:\s]/.test(botId) ||
|
|
220
|
+
!/^[0-9a-f]+$/.test(hash)
|
|
221
|
+
) {
|
|
222
|
+
throw new Error("isolate loader id components are invalid");
|
|
223
|
+
}
|
|
224
|
+
return `bot-package:${userId}:${botId}:${hash}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
228
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
229
|
+
throw new Error(`${label} must be an object`);
|
|
230
|
+
}
|
|
231
|
+
return value as Record<string, unknown>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function exactKeys(
|
|
235
|
+
value: Record<string, unknown>,
|
|
236
|
+
required: readonly string[],
|
|
237
|
+
label: string,
|
|
238
|
+
optional: readonly string[] = [],
|
|
239
|
+
): void {
|
|
240
|
+
const allowed = new Set<string>([...required, ...optional]);
|
|
241
|
+
if (
|
|
242
|
+
!required.every((key) => Object.hasOwn(value, key)) ||
|
|
243
|
+
!Object.keys(value).every((key) => allowed.has(key))
|
|
244
|
+
) {
|
|
245
|
+
throw new Error(`${label} has invalid fields`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function boundedString(
|
|
250
|
+
value: unknown,
|
|
251
|
+
label: string,
|
|
252
|
+
maximum: number,
|
|
253
|
+
allowEmpty = false,
|
|
254
|
+
): string {
|
|
255
|
+
if (
|
|
256
|
+
typeof value !== "string" ||
|
|
257
|
+
(!allowEmpty && value.length === 0) ||
|
|
258
|
+
value.length > maximum
|
|
259
|
+
) {
|
|
260
|
+
throw new Error(`${label} must be a bounded string`);
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function jsonValue(value: unknown, label: string, depth = 0): void {
|
|
266
|
+
if (depth > 16) throw new Error(`${label} is nested too deeply`);
|
|
267
|
+
if (value === null) return;
|
|
268
|
+
const kind = typeof value;
|
|
269
|
+
if (kind === "string" || kind === "boolean") return;
|
|
270
|
+
if (kind === "number") {
|
|
271
|
+
if (!Number.isFinite(value)) throw new Error(`${label} must be finite`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (Array.isArray(value)) {
|
|
275
|
+
value.forEach((entry, index) =>
|
|
276
|
+
jsonValue(entry, `${label}[${index}]`, depth + 1),
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (kind === "object") {
|
|
281
|
+
for (const [key, entry] of Object.entries(value as object)) {
|
|
282
|
+
jsonValue(entry, `${label}.${key}`, depth + 1);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
throw new Error(`${label} must be JSON`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The exact decoder for one turn admission declaration crossing the seam. */
|
|
290
|
+
export function decodeIsolateAdmissionV1(
|
|
291
|
+
input: unknown,
|
|
292
|
+
label = "isolate admission",
|
|
293
|
+
): TurnAdmissionV1 {
|
|
294
|
+
const value = record(input, label);
|
|
295
|
+
exactKeys(value, ["turnTypes"], label, ["subagentRoles"]);
|
|
296
|
+
if (!Array.isArray(value.turnTypes)) {
|
|
297
|
+
throw new Error(`${label}.turnTypes must be an array`);
|
|
298
|
+
}
|
|
299
|
+
if (value.turnTypes.length === 0) {
|
|
300
|
+
throw new Error(`${label}.turnTypes must not be empty`);
|
|
301
|
+
}
|
|
302
|
+
const turnTypes = value.turnTypes.map((turnType, index) =>
|
|
303
|
+
decodeTurnTypeV1(turnType, `${label}.turnTypes[${index}]`),
|
|
304
|
+
);
|
|
305
|
+
if (new Set(turnTypes).size !== turnTypes.length) {
|
|
306
|
+
throw new Error(`${label}.turnTypes contains duplicates`);
|
|
307
|
+
}
|
|
308
|
+
if (value.subagentRoles === undefined) return { turnTypes };
|
|
309
|
+
if (
|
|
310
|
+
!Array.isArray(value.subagentRoles) ||
|
|
311
|
+
value.subagentRoles.length === 0 ||
|
|
312
|
+
value.subagentRoles.length > ISOLATE_SUBAGENT_ROLE_LIMIT
|
|
313
|
+
) {
|
|
314
|
+
throw new Error(`${label}.subagentRoles must be a bounded array`);
|
|
315
|
+
}
|
|
316
|
+
const subagentRoles = value.subagentRoles.map((role, index) => {
|
|
317
|
+
if (
|
|
318
|
+
typeof role !== "string" ||
|
|
319
|
+
role.trim().length === 0 ||
|
|
320
|
+
role.length > ISOLATE_SUBAGENT_ROLE_MAX
|
|
321
|
+
) {
|
|
322
|
+
throw new Error(`${label}.subagentRoles[${index}] is invalid`);
|
|
323
|
+
}
|
|
324
|
+
return role;
|
|
325
|
+
});
|
|
326
|
+
if (new Set(subagentRoles).size !== subagentRoles.length) {
|
|
327
|
+
throw new Error(`${label}.subagentRoles contains duplicates`);
|
|
328
|
+
}
|
|
329
|
+
return { turnTypes, subagentRoles };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* How many roles one declaration may name, and how long a role name may be.
|
|
334
|
+
* The kernel bounds the string and reads no meaning into it: a role is a name
|
|
335
|
+
* a Package chose, exactly as a turn type is a name the kernel chose.
|
|
336
|
+
*/
|
|
337
|
+
const ISOLATE_SUBAGENT_ROLE_LIMIT = 16;
|
|
338
|
+
const ISOLATE_SUBAGENT_ROLE_MAX = 64;
|
|
339
|
+
|
|
340
|
+
export function decodeIsolateToolDescriptorV1(
|
|
341
|
+
input: unknown,
|
|
342
|
+
label = "isolate tool descriptor",
|
|
343
|
+
contractVersion: IsolateContractVersion = 1,
|
|
344
|
+
): IsolateToolDescriptorV1 {
|
|
345
|
+
const value = record(input, label);
|
|
346
|
+
exactKeys(
|
|
347
|
+
value,
|
|
348
|
+
[
|
|
349
|
+
"name",
|
|
350
|
+
"description",
|
|
351
|
+
"inputSchema",
|
|
352
|
+
"idempotent",
|
|
353
|
+
...(contractVersion >= 2 && Object.hasOwn(value, "admission")
|
|
354
|
+
? ["admission"]
|
|
355
|
+
: []),
|
|
356
|
+
],
|
|
357
|
+
label,
|
|
358
|
+
);
|
|
359
|
+
const name = boundedString(value.name, `${label}.name`, 64);
|
|
360
|
+
if (!TOOL_NAME.test(name)) throw new Error(`${label}.name is invalid`);
|
|
361
|
+
const description = boundedString(
|
|
362
|
+
value.description,
|
|
363
|
+
`${label}.description`,
|
|
364
|
+
2048,
|
|
365
|
+
true,
|
|
366
|
+
);
|
|
367
|
+
const inputSchema = record(value.inputSchema, `${label}.inputSchema`);
|
|
368
|
+
jsonValue(inputSchema, `${label}.inputSchema`);
|
|
369
|
+
if (typeof value.idempotent !== "boolean") {
|
|
370
|
+
throw new Error(`${label}.idempotent must be a boolean`);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
name,
|
|
374
|
+
description,
|
|
375
|
+
inputSchema,
|
|
376
|
+
idempotent: value.idempotent,
|
|
377
|
+
...(contractVersion >= 2 && value.admission !== undefined
|
|
378
|
+
? {
|
|
379
|
+
admission: decodeIsolateAdmissionV1(
|
|
380
|
+
value.admission,
|
|
381
|
+
`${label}.admission`,
|
|
382
|
+
),
|
|
383
|
+
}
|
|
384
|
+
: {}),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function decodeIsolateToolInvocationV1(
|
|
389
|
+
input: unknown,
|
|
390
|
+
label = "isolate tool invocation",
|
|
391
|
+
): IsolateToolInvocationV1 {
|
|
392
|
+
const value = record(input, label);
|
|
393
|
+
exactKeys(
|
|
394
|
+
value,
|
|
395
|
+
[
|
|
396
|
+
"schemaVersion",
|
|
397
|
+
"tool",
|
|
398
|
+
"input",
|
|
399
|
+
"botId",
|
|
400
|
+
"sessionId",
|
|
401
|
+
"runId",
|
|
402
|
+
"turnId",
|
|
403
|
+
"generationId",
|
|
404
|
+
"deadlineMs",
|
|
405
|
+
],
|
|
406
|
+
label,
|
|
407
|
+
);
|
|
408
|
+
if (value.schemaVersion !== 1) {
|
|
409
|
+
throw new Error(`${label}.schemaVersion is unsupported`);
|
|
410
|
+
}
|
|
411
|
+
const tool = boundedString(value.tool, `${label}.tool`, 64);
|
|
412
|
+
if (!TOOL_NAME.test(tool)) throw new Error(`${label}.tool is invalid`);
|
|
413
|
+
jsonValue(value.input, `${label}.input`);
|
|
414
|
+
const deadlineMs = value.deadlineMs;
|
|
415
|
+
if (
|
|
416
|
+
!Number.isSafeInteger(deadlineMs) ||
|
|
417
|
+
(deadlineMs as number) <= 0 ||
|
|
418
|
+
(deadlineMs as number) > ISOLATE_MAX_DEADLINE_MS
|
|
419
|
+
) {
|
|
420
|
+
throw new Error(`${label}.deadlineMs is out of range`);
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
schemaVersion: 1,
|
|
424
|
+
tool,
|
|
425
|
+
input: value.input,
|
|
426
|
+
botId: boundedString(value.botId, `${label}.botId`, 256),
|
|
427
|
+
sessionId: boundedString(value.sessionId, `${label}.sessionId`, 257),
|
|
428
|
+
runId: boundedString(value.runId, `${label}.runId`, 128),
|
|
429
|
+
turnId: boundedString(value.turnId, `${label}.turnId`, 128),
|
|
430
|
+
generationId: boundedString(
|
|
431
|
+
value.generationId,
|
|
432
|
+
`${label}.generationId`,
|
|
433
|
+
256,
|
|
434
|
+
),
|
|
435
|
+
deadlineMs: deadlineMs as number,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function decodeIsolateToolResultV1(
|
|
440
|
+
input: unknown,
|
|
441
|
+
label = "isolate tool result",
|
|
442
|
+
): IsolateToolResultV1 {
|
|
443
|
+
const value = record(input, label);
|
|
444
|
+
exactKeys(value, ["schemaVersion", "content", "isError"], label);
|
|
445
|
+
if (value.schemaVersion !== 1) {
|
|
446
|
+
throw new Error(`${label}.schemaVersion is unsupported`);
|
|
447
|
+
}
|
|
448
|
+
if (typeof value.isError !== "boolean") {
|
|
449
|
+
throw new Error(`${label}.isError must be a boolean`);
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
schemaVersion: 1,
|
|
453
|
+
content: boundedString(
|
|
454
|
+
value.content,
|
|
455
|
+
`${label}.content`,
|
|
456
|
+
MAX_ISOLATE_CONTENT,
|
|
457
|
+
true,
|
|
458
|
+
),
|
|
459
|
+
isError: value.isError,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function decodeIsolateHealthV1(
|
|
464
|
+
input: unknown,
|
|
465
|
+
label = "isolate health",
|
|
466
|
+
): IsolateHealthV1 {
|
|
467
|
+
const value = record(input, label);
|
|
468
|
+
exactKeys(
|
|
469
|
+
value,
|
|
470
|
+
["schemaVersion", "ok", "packageId", "contractVersion", "tools"],
|
|
471
|
+
label,
|
|
472
|
+
);
|
|
473
|
+
if (value.schemaVersion !== 1) {
|
|
474
|
+
throw new Error(`${label}.schemaVersion is unsupported`);
|
|
475
|
+
}
|
|
476
|
+
const contractVersion = ISOLATE_CONTRACT_VERSIONS.find(
|
|
477
|
+
(candidate) => candidate === value.contractVersion,
|
|
478
|
+
);
|
|
479
|
+
if (contractVersion === undefined) {
|
|
480
|
+
throw new Error(`${label}.contractVersion is unsupported`);
|
|
481
|
+
}
|
|
482
|
+
if (typeof value.ok !== "boolean") {
|
|
483
|
+
throw new Error(`${label}.ok must be a boolean`);
|
|
484
|
+
}
|
|
485
|
+
if (!Array.isArray(value.tools)) {
|
|
486
|
+
throw new Error(`${label}.tools must be an array`);
|
|
487
|
+
}
|
|
488
|
+
if (value.tools.length > MAX_ISOLATE_TOOLS) {
|
|
489
|
+
throw new Error(`${label}.tools exceeds its bound`);
|
|
490
|
+
}
|
|
491
|
+
const tools = value.tools.map((tool, index) =>
|
|
492
|
+
decodeIsolateToolDescriptorV1(
|
|
493
|
+
tool,
|
|
494
|
+
`${label}.tools[${index}]`,
|
|
495
|
+
contractVersion,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
|
|
499
|
+
throw new Error(`${label}.tools contains duplicate names`);
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
schemaVersion: 1,
|
|
503
|
+
ok: value.ok,
|
|
504
|
+
packageId: boundedString(value.packageId, `${label}.packageId`, 128),
|
|
505
|
+
contractVersion,
|
|
506
|
+
tools,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function decodeIsolateIdentityV1(
|
|
511
|
+
input: unknown,
|
|
512
|
+
label = "isolate identity",
|
|
513
|
+
): IsolateIdentityV1 {
|
|
514
|
+
const value = record(input, label);
|
|
515
|
+
exactKeys(value, ["botId", "generationId", "packageId"], label);
|
|
516
|
+
return {
|
|
517
|
+
botId: boundedString(value.botId, `${label}.botId`, 256),
|
|
518
|
+
generationId: boundedString(
|
|
519
|
+
value.generationId,
|
|
520
|
+
`${label}.generationId`,
|
|
521
|
+
256,
|
|
522
|
+
),
|
|
523
|
+
packageId: boundedString(value.packageId, `${label}.packageId`, 128),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const CAPABILITY_KINDS: readonly IsolateCapabilityKindV1[] = [
|
|
528
|
+
"tool",
|
|
529
|
+
"model",
|
|
530
|
+
"memory",
|
|
531
|
+
"notification",
|
|
532
|
+
"computer",
|
|
533
|
+
];
|
|
534
|
+
|
|
535
|
+
export function decodeIsolateCapabilityDescriptorV1(
|
|
536
|
+
input: unknown,
|
|
537
|
+
label = "isolate capability",
|
|
538
|
+
): IsolateCapabilityDescriptorV1 {
|
|
539
|
+
const value = record(input, label);
|
|
540
|
+
exactKeys(value, ["capabilityId", "kind"], label);
|
|
541
|
+
const kind = CAPABILITY_KINDS.find((candidate) => candidate === value.kind);
|
|
542
|
+
if (!kind) throw new Error(`${label}.kind is invalid`);
|
|
543
|
+
return {
|
|
544
|
+
capabilityId: boundedString(
|
|
545
|
+
value.capabilityId,
|
|
546
|
+
`${label}.capabilityId`,
|
|
547
|
+
256,
|
|
548
|
+
),
|
|
549
|
+
kind,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function decodeIsolateCapabilityListV1(
|
|
554
|
+
input: unknown,
|
|
555
|
+
label = "isolate capability list",
|
|
556
|
+
): IsolateCapabilityDescriptorV1[] {
|
|
557
|
+
if (!Array.isArray(input)) throw new Error(`${label} must be an array`);
|
|
558
|
+
if (input.length > MAX_ISOLATE_CAPABILITIES) {
|
|
559
|
+
throw new Error(`${label} exceeds its bound`);
|
|
560
|
+
}
|
|
561
|
+
return input.map((entry, index) =>
|
|
562
|
+
decodeIsolateCapabilityDescriptorV1(entry, `${label}[${index}]`),
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function decodeIsolateAuthorityRequestV1(
|
|
567
|
+
input: unknown,
|
|
568
|
+
label = "isolate authority request",
|
|
569
|
+
): IsolateAuthorityRequestV1 {
|
|
570
|
+
const value = record(input, label);
|
|
571
|
+
exactKeys(value, ["capabilityId", "reason"], label);
|
|
572
|
+
return {
|
|
573
|
+
capabilityId: boundedString(
|
|
574
|
+
value.capabilityId,
|
|
575
|
+
`${label}.capabilityId`,
|
|
576
|
+
256,
|
|
577
|
+
),
|
|
578
|
+
reason: boundedString(value.reason, `${label}.reason`, 2048, true),
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function decodeIsolatePendingDecisionV1(
|
|
583
|
+
input: unknown,
|
|
584
|
+
label = "isolate pending decision",
|
|
585
|
+
): IsolatePendingDecisionV1 {
|
|
586
|
+
const value = record(input, label);
|
|
587
|
+
exactKeys(value, ["status", "decisionId"], label);
|
|
588
|
+
if (value.status !== "pending-user-decision") {
|
|
589
|
+
throw new Error(`${label}.status must be pending-user-decision`);
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
status: "pending-user-decision",
|
|
593
|
+
decisionId: boundedString(value.decisionId, `${label}.decisionId`, 256),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** The exact decoder for the declared failure variant. */
|
|
598
|
+
export function decodeIsolateCapabilityFailureV1(
|
|
599
|
+
input: unknown,
|
|
600
|
+
label = "isolate capability failure",
|
|
601
|
+
): IsolateCapabilityFailureV1 {
|
|
602
|
+
const value = record(input, label);
|
|
603
|
+
exactKeys(value, ["status", "reason"], label);
|
|
604
|
+
if (value.status !== "unavailable") {
|
|
605
|
+
throw new Error(`${label}.status must be unavailable`);
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
status: "unavailable",
|
|
609
|
+
reason: boundedString(value.reason, `${label}.reason`, 512),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* One line of the `invokeModel` NDJSON stream. The isolate decodes each line
|
|
615
|
+
* before handing it to Bot code, and the kernel decodes nothing on the way out
|
|
616
|
+
* because it authored the event.
|
|
617
|
+
*/
|
|
618
|
+
export function decodeIsolateModelEventV1(
|
|
619
|
+
input: unknown,
|
|
620
|
+
label = "isolate model event",
|
|
621
|
+
): LlmStreamEvent {
|
|
622
|
+
const value = record(input, label);
|
|
623
|
+
if (value.type === "text-delta") {
|
|
624
|
+
exactKeys(value, ["type", "text"], label);
|
|
625
|
+
return {
|
|
626
|
+
type: "text-delta",
|
|
627
|
+
text: boundedString(
|
|
628
|
+
value.text,
|
|
629
|
+
`${label}.text`,
|
|
630
|
+
MAX_ISOLATE_CONTENT,
|
|
631
|
+
true,
|
|
632
|
+
),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (value.type === "finish") {
|
|
636
|
+
exactKeys(value, ["type", "reason"], label);
|
|
637
|
+
if (
|
|
638
|
+
value.reason !== "completed" &&
|
|
639
|
+
value.reason !== "tool-calls" &&
|
|
640
|
+
value.reason !== "max-tokens"
|
|
641
|
+
) {
|
|
642
|
+
throw new Error(`${label}.reason is invalid`);
|
|
643
|
+
}
|
|
644
|
+
return { type: "finish", reason: value.reason };
|
|
645
|
+
}
|
|
646
|
+
if (value.type === "tool-call") {
|
|
647
|
+
exactKeys(value, ["type", "call"], label);
|
|
648
|
+
const call = record(value.call, `${label}.call`);
|
|
649
|
+
exactKeys(call, ["id", "name", "input"], `${label}.call`);
|
|
650
|
+
jsonValue(call.input, `${label}.call.input`);
|
|
651
|
+
return {
|
|
652
|
+
type: "tool-call",
|
|
653
|
+
call: {
|
|
654
|
+
id: boundedString(call.id, `${label}.call.id`, 256),
|
|
655
|
+
name: boundedString(call.name, `${label}.call.name`, 128),
|
|
656
|
+
input: call.input,
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
throw new Error(`${label}.type is invalid`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function decodeIsolateModelInvocationV1(
|
|
664
|
+
input: unknown,
|
|
665
|
+
label = "isolate model invocation",
|
|
666
|
+
): IsolateModelInvocationV1 {
|
|
667
|
+
const value = record(input, label);
|
|
668
|
+
if (value.status === "pending-user-decision") {
|
|
669
|
+
return decodeIsolatePendingDecisionV1(value, label);
|
|
670
|
+
}
|
|
671
|
+
exactKeys(value, ["status", "requestId", "events"], label);
|
|
672
|
+
if (value.status !== "streaming") {
|
|
673
|
+
throw new Error(`${label}.status is invalid`);
|
|
674
|
+
}
|
|
675
|
+
const events = value.events;
|
|
676
|
+
if (
|
|
677
|
+
!events ||
|
|
678
|
+
typeof events !== "object" ||
|
|
679
|
+
typeof (events as ReadableStream).getReader !== "function"
|
|
680
|
+
) {
|
|
681
|
+
throw new Error(`${label}.events must be a readable stream`);
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
status: "streaming",
|
|
685
|
+
requestId: boundedString(value.requestId, `${label}.requestId`, 256),
|
|
686
|
+
events: events as ReadableStream<Uint8Array>,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Encodes one model event as a line of the isolate's NDJSON stream. */
|
|
691
|
+
export function encodeIsolateModelEventLineV1(event: LlmStreamEvent): string {
|
|
692
|
+
return `${JSON.stringify(event)}\n`;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** A tool descriptor projected onto the kernel's tool schema. */
|
|
696
|
+
export function isolateToolSchemaV1(
|
|
697
|
+
descriptor: IsolateToolDescriptorV1,
|
|
698
|
+
): ToolSchema {
|
|
699
|
+
return {
|
|
700
|
+
name: descriptor.name,
|
|
701
|
+
description: descriptor.description,
|
|
702
|
+
inputSchema: descriptor.inputSchema,
|
|
703
|
+
};
|
|
704
|
+
}
|