@frockbot/kernel-contracts 0.1.3 → 0.2.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/src/isolate.ts CHANGED
@@ -19,6 +19,12 @@
19
19
  // Everything decoded here is untrusted: Bot-authored code produces the results
20
20
  // and the capability requests, and the isolate produces the health report.
21
21
  import type { TurnAdmissionV1 } from "./tool-execution.js";
22
+ import {
23
+ BOT_ISOLATE_HOOK_EVENTS_V1,
24
+ isBotIsolateHookEventNameV1,
25
+ type BotIsolateHookEventNameV1,
26
+ type LoopEventPayloadMapV1,
27
+ } from "./loop-events.js";
22
28
  import {
23
29
  decodeTurnTypeV1,
24
30
  type LlmStreamEvent,
@@ -28,21 +34,20 @@ import {
28
34
 
29
35
  /**
30
36
  * 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.
37
+ * per-tool turn admission. Version 3 added declared loop hooks and hook RPC.
33
38
  */
34
- export const ISOLATE_CONTRACT_VERSION = 2;
39
+ export const ISOLATE_CONTRACT_VERSION = 3;
35
40
 
36
41
  /** Every contract version the kernel still decodes. */
37
- export type IsolateContractVersion = 1 | 2;
42
+ export type IsolateContractVersion = 1 | 2 | 3;
38
43
 
39
- const ISOLATE_CONTRACT_VERSIONS: readonly IsolateContractVersion[] = [1, 2];
44
+ const ISOLATE_CONTRACT_VERSIONS: readonly IsolateContractVersion[] = [1, 2, 3];
40
45
 
41
46
  /** The upper bound on a single isolate invocation, enforced on both sides. */
42
47
  export const ISOLATE_MAX_DEADLINE_MS = 60_000;
43
48
 
44
49
  const MAX_ISOLATE_TOOLS = 64;
45
- const MAX_ISOLATE_CAPABILITIES = 256;
50
+ const MAX_ISOLATE_CONNECTIONS = 100;
46
51
  const MAX_ISOLATE_CONTENT = 1_000_000;
47
52
  const TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
48
53
 
@@ -73,12 +78,32 @@ export interface IsolateToolResultV1 {
73
78
  isError: boolean;
74
79
  }
75
80
 
81
+ export interface IsolateHookInvocationV1<
82
+ Event extends BotIsolateHookEventNameV1 = BotIsolateHookEventNameV1,
83
+ > {
84
+ schemaVersion: 1;
85
+ event: Event;
86
+ payload: LoopEventPayloadMapV1[Event];
87
+ botId: string;
88
+ sessionId: string;
89
+ runId: string;
90
+ turnId: string;
91
+ generationId: string;
92
+ deadlineMs: number;
93
+ }
94
+
95
+ export type IsolateHookResultV1 =
96
+ | { schemaVersion: 1; status: "unchanged" }
97
+ | { schemaVersion: 1; status: "replaced"; replacement: unknown };
98
+
76
99
  export interface IsolateHealthV1 {
77
100
  schemaVersion: 1;
78
101
  ok: boolean;
79
102
  packageId: string;
80
103
  contractVersion: IsolateContractVersion;
81
104
  tools: IsolateToolDescriptorV1[];
105
+ /** Contract version 3 and later. */
106
+ hooks?: BotIsolateHookEventNameV1[];
82
107
  }
83
108
 
84
109
  /** What `IDENTITY` carries into the isolate. Structured-clonable, never a stub. */
@@ -88,25 +113,6 @@ export interface IsolateIdentityV1 {
88
113
  packageId: string;
89
114
  }
90
115
 
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
116
  /**
111
117
  * A capability call the authority could not serve. It is a declared variant,
112
118
  * not an exception: an error thrown across the loopback binding would surface
@@ -118,14 +124,121 @@ export interface IsolateCapabilityFailureV1 {
118
124
  reason: string;
119
125
  }
120
126
 
121
- export type IsolateAuthorityOutcomeV1 =
122
- IsolatePendingDecisionV1 | IsolateCapabilityFailureV1;
127
+ /** One safe Connection projection. Credentials and credential references never cross. */
128
+ export interface IsolateConnectionV1 {
129
+ connectionId: string;
130
+ packageId: string;
131
+ connectionTypeId: string;
132
+ displayName: string;
133
+ generation: string;
134
+ safeMetadata: Record<string, unknown>;
135
+ }
136
+
137
+ /** The Bot's configured model, resolved through one of its ready Connections. */
138
+ export interface IsolateModelBindingV1 {
139
+ connectionId: string;
140
+ packageId: string;
141
+ provider: string;
142
+ providerModelId: string;
143
+ connectionGeneration: string;
144
+ catalogGeneration?: string;
145
+ }
146
+
147
+ /** The per-Bot authority every Package in one Composition sees identically. */
148
+ export interface IsolateCapabilityListV1 {
149
+ status: "available";
150
+ connections: IsolateConnectionV1[];
151
+ model?: IsolateModelBindingV1;
152
+ tools: true;
153
+ memory: boolean;
154
+ workspace: boolean;
155
+ notify: true;
156
+ schedule: true;
157
+ }
123
158
 
124
159
  export type IsolateCapabilityListOutcomeV1 =
125
- IsolateCapabilityDescriptorV1[] | IsolateCapabilityFailureV1;
160
+ IsolateCapabilityListV1 | IsolateCapabilityFailureV1;
161
+
162
+ /** An opaque, short-lived reference to a Connection — never its credential. */
163
+ export interface IsolateConnectionLeaseV1 {
164
+ status: "available";
165
+ leaseId: string;
166
+ connectionId: string;
167
+ generation: string;
168
+ expiresAt: string;
169
+ }
170
+
171
+ export type IsolateConnectionOutcomeV1 =
172
+ IsolateConnectionLeaseV1 | IsolateCapabilityFailureV1;
173
+
174
+ export interface IsolateToolRequestV1 {
175
+ callId: string;
176
+ name: string;
177
+ input: unknown;
178
+ }
179
+
180
+ export type IsolateToolOutcomeV1 =
181
+ | { status: "completed"; content: string; isError: boolean }
182
+ | IsolateCapabilityFailureV1;
183
+
184
+ export type IsolateMemoryScopeV1 = "bot" | "user" | "project";
185
+ export type IsolateMemoryTierV1 = "profile" | "log" | "note";
186
+ export interface IsolateMemoryReadRequestV1 {
187
+ scope: IsolateMemoryScopeV1;
188
+ projectId?: string;
189
+ }
190
+ export interface IsolateMemoryWriteRequestV1 extends IsolateMemoryReadRequestV1 {
191
+ tier?: IsolateMemoryTierV1;
192
+ fact: string;
193
+ }
194
+ export type IsolateMemoryOutcomeV1 =
195
+ { status: "available"; value: unknown } | IsolateCapabilityFailureV1;
196
+
197
+ /** A root selector without a User id; the authority supplies its own User. */
198
+ export type IsolateWorkspaceRootV1 =
199
+ | { kind: "bot-instructions"; botId: string }
200
+ | { kind: "user-instructions" }
201
+ | { kind: "package-declared"; packageId: string; rootId: string };
202
+ export interface IsolateWorkspacePathV1 {
203
+ root: IsolateWorkspaceRootV1;
204
+ path: string;
205
+ }
206
+ export interface IsolateWorkspaceListRequestV1 {
207
+ root: IsolateWorkspaceRootV1;
208
+ prefix?: string;
209
+ cursor?: string;
210
+ limit?: number;
211
+ }
212
+ export interface IsolateWorkspaceWriteRequestV1 {
213
+ path: IsolateWorkspacePathV1;
214
+ bytes: Uint8Array;
215
+ expectedGenerationId: string | null;
216
+ mediaType?: string;
217
+ }
218
+ export interface IsolateWorkspaceDeleteRequestV1 {
219
+ path: IsolateWorkspacePathV1;
220
+ expectedGenerationId: string;
221
+ }
222
+ export type IsolateWorkspaceOutcomeV1 =
223
+ { status: "available"; value: unknown } | IsolateCapabilityFailureV1;
224
+
225
+ export interface IsolateNotificationRequestV1 {
226
+ notificationId: string;
227
+ title: string;
228
+ body: string;
229
+ }
230
+ export type IsolateNotificationOutcomeV1 =
231
+ { status: "recorded" } | IsolateCapabilityFailureV1;
232
+
233
+ /** A durable Routine operation attributed to one Package call. */
234
+ export interface IsolateScheduleRequestV1 {
235
+ callId: string;
236
+ input: unknown;
237
+ }
238
+ export type IsolateScheduleOutcomeV1 = IsolateToolOutcomeV1;
126
239
 
127
240
  /**
128
- * D6: model invocation as an Assignment-derived binding. Events cross the RPC
241
+ * Model invocation through the Bot's configured model binding. Events cross the RPC
129
242
  * boundary as an NDJSON byte stream — see `decodeIsolateModelEventV1`. A
130
243
  * `ReadableStream` of JavaScript objects is not transferable over workerd RPC;
131
244
  * a byte stream is, so the kernel encodes and the isolate decodes.
@@ -136,10 +249,9 @@ export type IsolateModelInvocationV1 =
136
249
  requestId: string;
137
250
  events: ReadableStream<Uint8Array>;
138
251
  }
139
- | IsolatePendingDecisionV1;
252
+ | IsolateCapabilityFailureV1;
140
253
 
141
- export type IsolateModelOutcomeV1 =
142
- IsolateModelInvocationV1 | IsolateCapabilityFailureV1;
254
+ export type IsolateModelOutcomeV1 = IsolateModelInvocationV1;
143
255
 
144
256
  /**
145
257
  * The wrapper `WorkerEntrypoint` the kernel generates. Bot code never
@@ -148,25 +260,126 @@ export type IsolateModelOutcomeV1 =
148
260
  export interface BotIsolateEntrypoint {
149
261
  health(): Promise<IsolateHealthV1>;
150
262
  execute(invocation: IsolateToolInvocationV1): Promise<IsolateToolResultV1>;
263
+ hook(invocation: IsolateHookInvocationV1): Promise<IsolateHookResultV1>;
151
264
  }
152
265
 
153
266
  /**
154
267
  * 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.
268
+ * Every method is Bot-authority-derived: nothing here can hand out authority
269
+ * the Bot does not already hold.
157
270
  */
158
271
  export interface BotCapabilitiesStub {
159
272
  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
273
  invokeModel(request: NormalizedModelRequest): Promise<IsolateModelOutcomeV1>;
167
- requestAuthority(
168
- request: IsolateAuthorityRequestV1,
169
- ): Promise<IsolateAuthorityOutcomeV1>;
274
+ invokeTool(request: IsolateToolRequestV1): Promise<IsolateToolOutcomeV1>;
275
+ memoryRead(
276
+ request: IsolateMemoryReadRequestV1,
277
+ ): Promise<IsolateMemoryOutcomeV1>;
278
+ memoryWrite(
279
+ request: IsolateMemoryWriteRequestV1,
280
+ ): Promise<IsolateMemoryOutcomeV1>;
281
+ memoryForget(
282
+ request: IsolateMemoryWriteRequestV1,
283
+ ): Promise<IsolateMemoryOutcomeV1>;
284
+ workspaceRead(
285
+ path: IsolateWorkspacePathV1,
286
+ ): Promise<IsolateWorkspaceOutcomeV1>;
287
+ workspaceList(
288
+ request: IsolateWorkspaceListRequestV1,
289
+ ): Promise<IsolateWorkspaceOutcomeV1>;
290
+ workspaceStat(
291
+ path: IsolateWorkspacePathV1,
292
+ ): Promise<IsolateWorkspaceOutcomeV1>;
293
+ workspaceWrite(
294
+ request: IsolateWorkspaceWriteRequestV1,
295
+ ): Promise<IsolateWorkspaceOutcomeV1>;
296
+ workspaceDelete(
297
+ request: IsolateWorkspaceDeleteRequestV1,
298
+ ): Promise<IsolateWorkspaceOutcomeV1>;
299
+ connection(connectionId: string): Promise<IsolateConnectionOutcomeV1>;
300
+ notify(
301
+ request: IsolateNotificationRequestV1,
302
+ ): Promise<IsolateNotificationOutcomeV1>;
303
+ schedule(
304
+ request: IsolateScheduleRequestV1,
305
+ ): Promise<IsolateScheduleOutcomeV1>;
306
+ }
307
+
308
+ /** The model outcome Bot-authored `package.js` receives after wrapper narrowing. */
309
+ export type BotPackageModelOutcomeV1 =
310
+ | {
311
+ status: "streaming";
312
+ requestId: string;
313
+ events: AsyncIterable<LlmStreamEvent>;
314
+ }
315
+ | IsolateCapabilityFailureV1;
316
+
317
+ /**
318
+ * The common exact `ctx` passed to a Bot-authored Package's tool and hook.
319
+ * The model-facing declarations are generated from these interfaces, and the
320
+ * wrapper's implementation is compile- and test-checked against the same
321
+ * keys.
322
+ */
323
+ export interface BotPackageContextV1 {
324
+ readonly tool?: string;
325
+ readonly event?: BotIsolateHookEventNameV1;
326
+ readonly botId: string;
327
+ readonly sessionId: string;
328
+ readonly runId: string;
329
+ readonly turnId: string;
330
+ readonly generationId: string;
331
+ readonly packageId: string;
332
+ readonly deadlineMs: number;
333
+ readonly bindings: string[];
334
+ readonly capabilities: {
335
+ list(): Promise<IsolateCapabilityListOutcomeV1>;
336
+ };
337
+ readonly model: {
338
+ invoke(request: NormalizedModelRequest): Promise<BotPackageModelOutcomeV1>;
339
+ };
340
+ readonly tools: {
341
+ /** Runs through the trusted registry's active-Composition and deny guards. */
342
+ invoke(request: IsolateToolRequestV1): Promise<IsolateToolOutcomeV1>;
343
+ };
344
+ readonly memory: {
345
+ read(request: IsolateMemoryReadRequestV1): Promise<IsolateMemoryOutcomeV1>;
346
+ write(
347
+ request: IsolateMemoryWriteRequestV1,
348
+ ): Promise<IsolateMemoryOutcomeV1>;
349
+ forget(
350
+ request: IsolateMemoryWriteRequestV1,
351
+ ): Promise<IsolateMemoryOutcomeV1>;
352
+ };
353
+ readonly workspace: {
354
+ read(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>;
355
+ list(
356
+ request: IsolateWorkspaceListRequestV1,
357
+ ): Promise<IsolateWorkspaceOutcomeV1>;
358
+ stat(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>;
359
+ write(
360
+ request: IsolateWorkspaceWriteRequestV1,
361
+ ): Promise<IsolateWorkspaceOutcomeV1>;
362
+ delete(
363
+ request: IsolateWorkspaceDeleteRequestV1,
364
+ ): Promise<IsolateWorkspaceOutcomeV1>;
365
+ };
366
+ connection(connectionId: string): Promise<IsolateConnectionOutcomeV1>;
367
+ notify(
368
+ request: IsolateNotificationRequestV1,
369
+ ): Promise<IsolateNotificationOutcomeV1>;
370
+ schedule(
371
+ request: IsolateScheduleRequestV1,
372
+ ): Promise<IsolateScheduleOutcomeV1>;
373
+ }
374
+
375
+ export interface BotPackageExecutionContextV1 extends BotPackageContextV1 {
376
+ readonly tool: string;
377
+ readonly event?: never;
378
+ }
379
+
380
+ export interface BotPackageHookContextV1 extends BotPackageContextV1 {
381
+ readonly tool?: never;
382
+ readonly event: BotIsolateHookEventNameV1;
170
383
  }
171
384
 
172
385
  /**
@@ -198,30 +411,25 @@ export interface IsolateHost {
198
411
 
199
412
  /**
200
413
  * 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.
414
+ * first code and `env`, so every component here is content- or owner-derived.
415
+ * The hash covers the mounted wrapper and Package artifact plus the digest of
416
+ * every baked-in binding: User, Bot, Composition generation, Connections, and
417
+ * resolved model. The User prefix independently prevents cross-User reuse.
204
418
  */
205
419
  export function isolateLoaderIdV1(input: {
206
420
  userId: string;
207
- botId: string;
208
421
  artifactSetHash: string;
209
422
  }): string {
210
423
  const userId = boundedString(input.userId, "isolate loader userId", 256);
211
- const botId = boundedString(input.botId, "isolate loader botId", 256);
212
424
  const hash = boundedString(
213
425
  input.artifactSetHash,
214
426
  "isolate loader artifactSetHash",
215
427
  128,
216
428
  );
217
- if (
218
- /[:\s]/.test(userId) ||
219
- /[:\s]/.test(botId) ||
220
- !/^[0-9a-f]+$/.test(hash)
221
- ) {
429
+ if (/[:\s]/.test(userId) || !/^[0-9a-f]+$/.test(hash)) {
222
430
  throw new Error("isolate loader id components are invalid");
223
431
  }
224
- return `bot-package:${userId}:${botId}:${hash}`;
432
+ return `bot-package:${userId}:${hash}`;
225
433
  }
226
434
 
227
435
  function record(value: unknown, label: string): Record<string, unknown> {
@@ -460,25 +668,114 @@ export function decodeIsolateToolResultV1(
460
668
  };
461
669
  }
462
670
 
463
- export function decodeIsolateHealthV1(
671
+ export function decodeIsolateHookInvocationV1(
464
672
  input: unknown,
465
- label = "isolate health",
466
- ): IsolateHealthV1 {
673
+ label = "isolate hook invocation",
674
+ ): IsolateHookInvocationV1 {
467
675
  const value = record(input, label);
468
676
  exactKeys(
469
677
  value,
470
- ["schemaVersion", "ok", "packageId", "contractVersion", "tools"],
678
+ [
679
+ "schemaVersion",
680
+ "event",
681
+ "payload",
682
+ "botId",
683
+ "sessionId",
684
+ "runId",
685
+ "turnId",
686
+ "generationId",
687
+ "deadlineMs",
688
+ ],
471
689
  label,
472
690
  );
473
691
  if (value.schemaVersion !== 1) {
474
692
  throw new Error(`${label}.schemaVersion is unsupported`);
475
693
  }
694
+ if (!isBotIsolateHookEventNameV1(value.event)) {
695
+ throw new Error(`${label}.event is invalid`);
696
+ }
697
+ // The host authored the event snapshot. The event-specific replacement is
698
+ // decoded on the return seam; here JSON validation prevents a live object
699
+ // from being smuggled into Bot code by a future host caller.
700
+ jsonValue(value.payload, `${label}.payload`);
701
+ const deadlineMs = value.deadlineMs;
702
+ if (
703
+ !Number.isSafeInteger(deadlineMs) ||
704
+ (deadlineMs as number) <= 0 ||
705
+ (deadlineMs as number) > ISOLATE_MAX_DEADLINE_MS
706
+ ) {
707
+ throw new Error(`${label}.deadlineMs is out of range`);
708
+ }
709
+ return {
710
+ schemaVersion: 1,
711
+ event: value.event,
712
+ payload: value.payload as LoopEventPayloadMapV1[BotIsolateHookEventNameV1],
713
+ botId: boundedString(value.botId, `${label}.botId`, 256),
714
+ sessionId: boundedString(value.sessionId, `${label}.sessionId`, 257),
715
+ runId: boundedString(value.runId, `${label}.runId`, 128),
716
+ turnId: boundedString(value.turnId, `${label}.turnId`, 128),
717
+ generationId: boundedString(
718
+ value.generationId,
719
+ `${label}.generationId`,
720
+ 256,
721
+ ),
722
+ deadlineMs: deadlineMs as number,
723
+ };
724
+ }
725
+
726
+ export function decodeIsolateHookResultV1(
727
+ input: unknown,
728
+ label = "isolate hook result",
729
+ ): IsolateHookResultV1 {
730
+ const value = record(input, label);
731
+ if (value.status === "unchanged") {
732
+ exactKeys(value, ["schemaVersion", "status"], label);
733
+ if (value.schemaVersion !== 1) {
734
+ throw new Error(`${label}.schemaVersion is unsupported`);
735
+ }
736
+ return { schemaVersion: 1, status: "unchanged" };
737
+ }
738
+ exactKeys(value, ["schemaVersion", "status", "replacement"], label);
739
+ if (value.schemaVersion !== 1) {
740
+ throw new Error(`${label}.schemaVersion is unsupported`);
741
+ }
742
+ if (value.status !== "replaced") {
743
+ throw new Error(`${label}.status is invalid`);
744
+ }
745
+ jsonValue(value.replacement, `${label}.replacement`);
746
+ return {
747
+ schemaVersion: 1,
748
+ status: "replaced",
749
+ replacement: value.replacement,
750
+ };
751
+ }
752
+
753
+ export function decodeIsolateHealthV1(
754
+ input: unknown,
755
+ label = "isolate health",
756
+ ): IsolateHealthV1 {
757
+ const value = record(input, label);
758
+ if (value.schemaVersion !== 1) {
759
+ throw new Error(`${label}.schemaVersion is unsupported`);
760
+ }
476
761
  const contractVersion = ISOLATE_CONTRACT_VERSIONS.find(
477
762
  (candidate) => candidate === value.contractVersion,
478
763
  );
479
764
  if (contractVersion === undefined) {
480
765
  throw new Error(`${label}.contractVersion is unsupported`);
481
766
  }
767
+ exactKeys(
768
+ value,
769
+ [
770
+ "schemaVersion",
771
+ "ok",
772
+ "packageId",
773
+ "contractVersion",
774
+ "tools",
775
+ ...(contractVersion >= 3 ? ["hooks"] : []),
776
+ ],
777
+ label,
778
+ );
482
779
  if (typeof value.ok !== "boolean") {
483
780
  throw new Error(`${label}.ok must be a boolean`);
484
781
  }
@@ -498,12 +795,32 @@ export function decodeIsolateHealthV1(
498
795
  if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
499
796
  throw new Error(`${label}.tools contains duplicate names`);
500
797
  }
798
+ if (
799
+ contractVersion >= 3 &&
800
+ (!Array.isArray(value.hooks) ||
801
+ value.hooks.length > BOT_ISOLATE_HOOK_EVENTS_V1.length)
802
+ ) {
803
+ throw new Error(`${label}.hooks must be a bounded array`);
804
+ }
805
+ const hooks =
806
+ contractVersion < 3
807
+ ? []
808
+ : (value.hooks as unknown[]).map((hook, index) => {
809
+ if (!isBotIsolateHookEventNameV1(hook)) {
810
+ throw new Error(`${label}.hooks[${index}] is invalid`);
811
+ }
812
+ return hook;
813
+ });
814
+ if (new Set(hooks).size !== hooks.length) {
815
+ throw new Error(`${label}.hooks contains duplicates`);
816
+ }
501
817
  return {
502
818
  schemaVersion: 1,
503
819
  ok: value.ok,
504
820
  packageId: boundedString(value.packageId, `${label}.packageId`, 128),
505
821
  contractVersion,
506
822
  tools,
823
+ hooks,
507
824
  };
508
825
  }
509
826
 
@@ -524,73 +841,344 @@ export function decodeIsolateIdentityV1(
524
841
  };
525
842
  }
526
843
 
527
- const CAPABILITY_KINDS: readonly IsolateCapabilityKindV1[] = [
528
- "tool",
529
- "model",
530
- "memory",
531
- "notification",
532
- "computer",
533
- ];
844
+ export function decodeIsolateConnectionV1(
845
+ input: unknown,
846
+ label = "isolate Connection",
847
+ ): IsolateConnectionV1 {
848
+ const value = record(input, label);
849
+ exactKeys(
850
+ value,
851
+ [
852
+ "connectionId",
853
+ "packageId",
854
+ "connectionTypeId",
855
+ "displayName",
856
+ "generation",
857
+ "safeMetadata",
858
+ ],
859
+ label,
860
+ );
861
+ const safeMetadata = record(value.safeMetadata, `${label}.safeMetadata`);
862
+ jsonValue(safeMetadata, `${label}.safeMetadata`);
863
+ return {
864
+ connectionId: boundedString(
865
+ value.connectionId,
866
+ `${label}.connectionId`,
867
+ 256,
868
+ ),
869
+ packageId: boundedString(value.packageId, `${label}.packageId`, 128),
870
+ connectionTypeId: boundedString(
871
+ value.connectionTypeId,
872
+ `${label}.connectionTypeId`,
873
+ 128,
874
+ ),
875
+ displayName: boundedString(value.displayName, `${label}.displayName`, 256),
876
+ generation: boundedString(value.generation, `${label}.generation`, 256),
877
+ safeMetadata,
878
+ };
879
+ }
534
880
 
535
- export function decodeIsolateCapabilityDescriptorV1(
881
+ export function decodeIsolateModelBindingV1(
536
882
  input: unknown,
537
- label = "isolate capability",
538
- ): IsolateCapabilityDescriptorV1 {
883
+ label = "isolate model binding",
884
+ ): IsolateModelBindingV1 {
539
885
  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`);
886
+ exactKeys(
887
+ value,
888
+ [
889
+ "connectionId",
890
+ "packageId",
891
+ "provider",
892
+ "providerModelId",
893
+ "connectionGeneration",
894
+ ],
895
+ label,
896
+ ["catalogGeneration"],
897
+ );
543
898
  return {
544
- capabilityId: boundedString(
545
- value.capabilityId,
546
- `${label}.capabilityId`,
899
+ connectionId: boundedString(
900
+ value.connectionId,
901
+ `${label}.connectionId`,
902
+ 256,
903
+ ),
904
+ packageId: boundedString(value.packageId, `${label}.packageId`, 128),
905
+ provider: boundedString(value.provider, `${label}.provider`, 128),
906
+ providerModelId: boundedString(
907
+ value.providerModelId,
908
+ `${label}.providerModelId`,
909
+ 256,
910
+ ),
911
+ connectionGeneration: boundedString(
912
+ value.connectionGeneration,
913
+ `${label}.connectionGeneration`,
547
914
  256,
548
915
  ),
549
- kind,
916
+ ...(value.catalogGeneration === undefined
917
+ ? {}
918
+ : {
919
+ catalogGeneration: boundedString(
920
+ value.catalogGeneration,
921
+ `${label}.catalogGeneration`,
922
+ 256,
923
+ ),
924
+ }),
550
925
  };
551
926
  }
552
927
 
553
928
  export function decodeIsolateCapabilityListV1(
554
929
  input: unknown,
555
930
  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}]`),
931
+ ): IsolateCapabilityListV1 {
932
+ const value = record(input, label);
933
+ exactKeys(
934
+ value,
935
+ [
936
+ "status",
937
+ "connections",
938
+ "tools",
939
+ "memory",
940
+ "workspace",
941
+ "notify",
942
+ "schedule",
943
+ ],
944
+ label,
945
+ ["model"],
563
946
  );
947
+ if (
948
+ value.status !== "available" ||
949
+ value.tools !== true ||
950
+ value.notify !== true ||
951
+ value.schedule !== true ||
952
+ typeof value.memory !== "boolean" ||
953
+ typeof value.workspace !== "boolean" ||
954
+ !Array.isArray(value.connections) ||
955
+ value.connections.length > MAX_ISOLATE_CONNECTIONS
956
+ ) {
957
+ throw new Error(`${label} is invalid`);
958
+ }
959
+ return {
960
+ status: "available",
961
+ connections: value.connections.map((connection, index) =>
962
+ decodeIsolateConnectionV1(connection, `${label}.connections[${index}]`),
963
+ ),
964
+ ...(value.model === undefined
965
+ ? {}
966
+ : { model: decodeIsolateModelBindingV1(value.model, `${label}.model`) }),
967
+ tools: true,
968
+ memory: value.memory,
969
+ workspace: value.workspace,
970
+ notify: true,
971
+ schedule: true,
972
+ };
973
+ }
974
+
975
+ export function decodeIsolateToolRequestV1(
976
+ input: unknown,
977
+ label = "isolate tool request",
978
+ ): IsolateToolRequestV1 {
979
+ const value = record(input, label);
980
+ exactKeys(value, ["callId", "name", "input"], label);
981
+ jsonValue(value.input, `${label}.input`);
982
+ return {
983
+ callId: boundedString(value.callId, `${label}.callId`, 256),
984
+ name: boundedString(value.name, `${label}.name`, 128),
985
+ input: value.input,
986
+ };
987
+ }
988
+
989
+ export function decodeIsolateScheduleRequestV1(
990
+ input: unknown,
991
+ label = "isolate schedule request",
992
+ ): IsolateScheduleRequestV1 {
993
+ const value = record(input, label);
994
+ exactKeys(value, ["callId", "input"], label);
995
+ jsonValue(value.input, `${label}.input`);
996
+ return {
997
+ callId: boundedString(value.callId, `${label}.callId`, 256),
998
+ input: value.input,
999
+ };
1000
+ }
1001
+
1002
+ const MEMORY_SCOPES = ["bot", "user", "project"] as const;
1003
+ const MEMORY_TIERS = ["profile", "log", "note"] as const;
1004
+
1005
+ export function decodeIsolateMemoryReadRequestV1(
1006
+ input: unknown,
1007
+ label = "isolate Memory read request",
1008
+ ): IsolateMemoryReadRequestV1 {
1009
+ const value = record(input, label);
1010
+ exactKeys(value, ["scope"], label, ["projectId"]);
1011
+ const scope = MEMORY_SCOPES.find((candidate) => candidate === value.scope);
1012
+ if (!scope) throw new Error(`${label}.scope is invalid`);
1013
+ if (scope === "project" && value.projectId === undefined) {
1014
+ throw new Error(`${label}.projectId is required`);
1015
+ }
1016
+ return {
1017
+ scope,
1018
+ ...(value.projectId === undefined
1019
+ ? {}
1020
+ : {
1021
+ projectId: boundedString(value.projectId, `${label}.projectId`, 128),
1022
+ }),
1023
+ };
1024
+ }
1025
+
1026
+ export function decodeIsolateMemoryWriteRequestV1(
1027
+ input: unknown,
1028
+ label = "isolate Memory write request",
1029
+ ): IsolateMemoryWriteRequestV1 {
1030
+ const value = record(input, label);
1031
+ exactKeys(value, ["scope", "fact"], label, ["projectId", "tier"]);
1032
+ const scope = MEMORY_SCOPES.find((candidate) => candidate === value.scope);
1033
+ const tier =
1034
+ value.tier === undefined
1035
+ ? undefined
1036
+ : MEMORY_TIERS.find((candidate) => candidate === value.tier);
1037
+ if (!scope || (value.tier !== undefined && !tier)) {
1038
+ throw new Error(`${label} is invalid`);
1039
+ }
1040
+ if (scope === "project" && value.projectId === undefined) {
1041
+ throw new Error(`${label}.projectId is required`);
1042
+ }
1043
+ return {
1044
+ scope,
1045
+ ...(value.projectId === undefined
1046
+ ? {}
1047
+ : {
1048
+ projectId: boundedString(value.projectId, `${label}.projectId`, 128),
1049
+ }),
1050
+ ...(tier ? { tier } : {}),
1051
+ fact: boundedString(value.fact, `${label}.fact`, 2_000),
1052
+ };
1053
+ }
1054
+
1055
+ function decodeIsolateWorkspaceRootV1(
1056
+ input: unknown,
1057
+ label: string,
1058
+ ): IsolateWorkspaceRootV1 {
1059
+ const value = record(input, label);
1060
+ if (value.kind === "user-instructions") {
1061
+ exactKeys(value, ["kind"], label);
1062
+ return { kind: "user-instructions" };
1063
+ }
1064
+ if (value.kind === "bot-instructions") {
1065
+ exactKeys(value, ["kind", "botId"], label);
1066
+ return {
1067
+ kind: "bot-instructions",
1068
+ botId: boundedString(value.botId, `${label}.botId`, 256),
1069
+ };
1070
+ }
1071
+ if (value.kind === "package-declared") {
1072
+ exactKeys(value, ["kind", "packageId", "rootId"], label);
1073
+ return {
1074
+ kind: "package-declared",
1075
+ packageId: boundedString(value.packageId, `${label}.packageId`, 128),
1076
+ rootId: boundedString(value.rootId, `${label}.rootId`, 128),
1077
+ };
1078
+ }
1079
+ throw new Error(`${label}.kind is invalid`);
1080
+ }
1081
+
1082
+ export function decodeIsolateWorkspacePathV1(
1083
+ input: unknown,
1084
+ label = "isolate Workspace path",
1085
+ ): IsolateWorkspacePathV1 {
1086
+ const value = record(input, label);
1087
+ exactKeys(value, ["root", "path"], label);
1088
+ return {
1089
+ root: decodeIsolateWorkspaceRootV1(value.root, `${label}.root`),
1090
+ path: boundedString(value.path, `${label}.path`, 1_024, true),
1091
+ };
564
1092
  }
565
1093
 
566
- export function decodeIsolateAuthorityRequestV1(
1094
+ export function decodeIsolateWorkspaceListRequestV1(
567
1095
  input: unknown,
568
- label = "isolate authority request",
569
- ): IsolateAuthorityRequestV1 {
1096
+ label = "isolate Workspace list request",
1097
+ ): IsolateWorkspaceListRequestV1 {
570
1098
  const value = record(input, label);
571
- exactKeys(value, ["capabilityId", "reason"], label);
1099
+ exactKeys(value, ["root"], label, ["prefix", "cursor", "limit"]);
1100
+ const limit = value.limit;
1101
+ if (
1102
+ limit !== undefined &&
1103
+ (!Number.isSafeInteger(limit) ||
1104
+ (limit as number) <= 0 ||
1105
+ (limit as number) > 1_000)
1106
+ ) {
1107
+ throw new Error(`${label}.limit is invalid`);
1108
+ }
572
1109
  return {
573
- capabilityId: boundedString(
574
- value.capabilityId,
575
- `${label}.capabilityId`,
1110
+ root: decodeIsolateWorkspaceRootV1(value.root, `${label}.root`),
1111
+ ...(value.prefix === undefined
1112
+ ? {}
1113
+ : {
1114
+ prefix: boundedString(value.prefix, `${label}.prefix`, 1_024, true),
1115
+ }),
1116
+ ...(value.cursor === undefined
1117
+ ? {}
1118
+ : { cursor: boundedString(value.cursor, `${label}.cursor`, 1_024) }),
1119
+ ...(limit === undefined ? {} : { limit: limit as number }),
1120
+ };
1121
+ }
1122
+
1123
+ export function decodeIsolateWorkspaceWriteRequestV1(
1124
+ input: unknown,
1125
+ label = "isolate Workspace write request",
1126
+ ): IsolateWorkspaceWriteRequestV1 {
1127
+ const value = record(input, label);
1128
+ exactKeys(value, ["path", "bytes", "expectedGenerationId"], label, [
1129
+ "mediaType",
1130
+ ]);
1131
+ if (!(value.bytes instanceof Uint8Array)) {
1132
+ throw new Error(`${label}.bytes must be bytes`);
1133
+ }
1134
+ if (
1135
+ value.expectedGenerationId !== null &&
1136
+ typeof value.expectedGenerationId !== "string"
1137
+ ) {
1138
+ throw new Error(`${label}.expectedGenerationId is invalid`);
1139
+ }
1140
+ return {
1141
+ path: decodeIsolateWorkspacePathV1(value.path, `${label}.path`),
1142
+ bytes: value.bytes,
1143
+ expectedGenerationId: value.expectedGenerationId as string | null,
1144
+ ...(value.mediaType === undefined
1145
+ ? {}
1146
+ : {
1147
+ mediaType: boundedString(value.mediaType, `${label}.mediaType`, 256),
1148
+ }),
1149
+ };
1150
+ }
1151
+
1152
+ export function decodeIsolateWorkspaceDeleteRequestV1(
1153
+ input: unknown,
1154
+ label = "isolate Workspace delete request",
1155
+ ): IsolateWorkspaceDeleteRequestV1 {
1156
+ const value = record(input, label);
1157
+ exactKeys(value, ["path", "expectedGenerationId"], label);
1158
+ return {
1159
+ path: decodeIsolateWorkspacePathV1(value.path, `${label}.path`),
1160
+ expectedGenerationId: boundedString(
1161
+ value.expectedGenerationId,
1162
+ `${label}.expectedGenerationId`,
576
1163
  256,
577
1164
  ),
578
- reason: boundedString(value.reason, `${label}.reason`, 2048, true),
579
1165
  };
580
1166
  }
581
1167
 
582
- export function decodeIsolatePendingDecisionV1(
1168
+ export function decodeIsolateNotificationRequestV1(
583
1169
  input: unknown,
584
- label = "isolate pending decision",
585
- ): IsolatePendingDecisionV1 {
1170
+ label = "isolate notification request",
1171
+ ): IsolateNotificationRequestV1 {
586
1172
  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
- }
1173
+ exactKeys(value, ["notificationId", "title", "body"], label);
591
1174
  return {
592
- status: "pending-user-decision",
593
- decisionId: boundedString(value.decisionId, `${label}.decisionId`, 256),
1175
+ notificationId: boundedString(
1176
+ value.notificationId,
1177
+ `${label}.notificationId`,
1178
+ 256,
1179
+ ),
1180
+ title: boundedString(value.title, `${label}.title`, 160),
1181
+ body: boundedString(value.body, `${label}.body`, 500),
594
1182
  };
595
1183
  }
596
1184
 
@@ -665,8 +1253,8 @@ export function decodeIsolateModelInvocationV1(
665
1253
  label = "isolate model invocation",
666
1254
  ): IsolateModelInvocationV1 {
667
1255
  const value = record(input, label);
668
- if (value.status === "pending-user-decision") {
669
- return decodeIsolatePendingDecisionV1(value, label);
1256
+ if (value.status === "unavailable") {
1257
+ return decodeIsolateCapabilityFailureV1(value, label);
670
1258
  }
671
1259
  exactKeys(value, ["status", "requestId", "events"], label);
672
1260
  if (value.status !== "streaming") {