@frockbot/kernel-contracts 0.1.4 → 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 enabled-capability 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 derives from enabled capabilities: 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 capability 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
  /**
@@ -200,10 +413,8 @@ export interface IsolateHost {
200
413
  * D2. The loader identity, and nothing else — a reused id silently serves the
201
414
  * first code and `env`, so every component here is content- or owner-derived.
202
415
  * The hash covers the mounted wrapper and Package artifact plus the digest of
203
- * every baked-in binding: User, Bot, Composition generation, and enabled set.
204
- * Identical artifacts reuse an isolate only under identical authority; the
205
- * User prefix independently prevents cross-User reuse (AGENTS.md Package
206
- * composition; ADR 0019).
416
+ * every baked-in binding: User, Bot, Composition generation, Connections, and
417
+ * resolved model. The User prefix independently prevents cross-User reuse.
207
418
  */
208
419
  export function isolateLoaderIdV1(input: {
209
420
  userId: string;
@@ -457,25 +668,114 @@ export function decodeIsolateToolResultV1(
457
668
  };
458
669
  }
459
670
 
460
- export function decodeIsolateHealthV1(
671
+ export function decodeIsolateHookInvocationV1(
461
672
  input: unknown,
462
- label = "isolate health",
463
- ): IsolateHealthV1 {
673
+ label = "isolate hook invocation",
674
+ ): IsolateHookInvocationV1 {
464
675
  const value = record(input, label);
465
676
  exactKeys(
466
677
  value,
467
- ["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
+ ],
468
689
  label,
469
690
  );
470
691
  if (value.schemaVersion !== 1) {
471
692
  throw new Error(`${label}.schemaVersion is unsupported`);
472
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
+ }
473
761
  const contractVersion = ISOLATE_CONTRACT_VERSIONS.find(
474
762
  (candidate) => candidate === value.contractVersion,
475
763
  );
476
764
  if (contractVersion === undefined) {
477
765
  throw new Error(`${label}.contractVersion is unsupported`);
478
766
  }
767
+ exactKeys(
768
+ value,
769
+ [
770
+ "schemaVersion",
771
+ "ok",
772
+ "packageId",
773
+ "contractVersion",
774
+ "tools",
775
+ ...(contractVersion >= 3 ? ["hooks"] : []),
776
+ ],
777
+ label,
778
+ );
479
779
  if (typeof value.ok !== "boolean") {
480
780
  throw new Error(`${label}.ok must be a boolean`);
481
781
  }
@@ -495,12 +795,32 @@ export function decodeIsolateHealthV1(
495
795
  if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
496
796
  throw new Error(`${label}.tools contains duplicate names`);
497
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
+ }
498
817
  return {
499
818
  schemaVersion: 1,
500
819
  ok: value.ok,
501
820
  packageId: boundedString(value.packageId, `${label}.packageId`, 128),
502
821
  contractVersion,
503
822
  tools,
823
+ hooks,
504
824
  };
505
825
  }
506
826
 
@@ -521,73 +841,344 @@ export function decodeIsolateIdentityV1(
521
841
  };
522
842
  }
523
843
 
524
- const CAPABILITY_KINDS: readonly IsolateCapabilityKindV1[] = [
525
- "tool",
526
- "model",
527
- "memory",
528
- "notification",
529
- "computer",
530
- ];
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
+ }
531
880
 
532
- export function decodeIsolateCapabilityDescriptorV1(
881
+ export function decodeIsolateModelBindingV1(
533
882
  input: unknown,
534
- label = "isolate capability",
535
- ): IsolateCapabilityDescriptorV1 {
883
+ label = "isolate model binding",
884
+ ): IsolateModelBindingV1 {
536
885
  const value = record(input, label);
537
- exactKeys(value, ["capabilityId", "kind"], label);
538
- const kind = CAPABILITY_KINDS.find((candidate) => candidate === value.kind);
539
- 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
+ );
540
898
  return {
541
- capabilityId: boundedString(
542
- value.capabilityId,
543
- `${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`,
544
909
  256,
545
910
  ),
546
- kind,
911
+ connectionGeneration: boundedString(
912
+ value.connectionGeneration,
913
+ `${label}.connectionGeneration`,
914
+ 256,
915
+ ),
916
+ ...(value.catalogGeneration === undefined
917
+ ? {}
918
+ : {
919
+ catalogGeneration: boundedString(
920
+ value.catalogGeneration,
921
+ `${label}.catalogGeneration`,
922
+ 256,
923
+ ),
924
+ }),
547
925
  };
548
926
  }
549
927
 
550
928
  export function decodeIsolateCapabilityListV1(
551
929
  input: unknown,
552
930
  label = "isolate capability list",
553
- ): IsolateCapabilityDescriptorV1[] {
554
- if (!Array.isArray(input)) throw new Error(`${label} must be an array`);
555
- if (input.length > MAX_ISOLATE_CAPABILITIES) {
556
- throw new Error(`${label} exceeds its bound`);
557
- }
558
- return input.map((entry, index) =>
559
- 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"],
560
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
+ };
561
973
  }
562
974
 
563
- export function decodeIsolateAuthorityRequestV1(
975
+ export function decodeIsolateToolRequestV1(
564
976
  input: unknown,
565
- label = "isolate authority request",
566
- ): IsolateAuthorityRequestV1 {
977
+ label = "isolate tool request",
978
+ ): IsolateToolRequestV1 {
567
979
  const value = record(input, label);
568
- exactKeys(value, ["capabilityId", "reason"], label);
980
+ exactKeys(value, ["callId", "name", "input"], label);
981
+ jsonValue(value.input, `${label}.input`);
569
982
  return {
570
- capabilityId: boundedString(
571
- value.capabilityId,
572
- `${label}.capabilityId`,
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
+ };
1092
+ }
1093
+
1094
+ export function decodeIsolateWorkspaceListRequestV1(
1095
+ input: unknown,
1096
+ label = "isolate Workspace list request",
1097
+ ): IsolateWorkspaceListRequestV1 {
1098
+ const value = record(input, 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
+ }
1109
+ return {
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`,
573
1163
  256,
574
1164
  ),
575
- reason: boundedString(value.reason, `${label}.reason`, 2048, true),
576
1165
  };
577
1166
  }
578
1167
 
579
- export function decodeIsolatePendingDecisionV1(
1168
+ export function decodeIsolateNotificationRequestV1(
580
1169
  input: unknown,
581
- label = "isolate pending decision",
582
- ): IsolatePendingDecisionV1 {
1170
+ label = "isolate notification request",
1171
+ ): IsolateNotificationRequestV1 {
583
1172
  const value = record(input, label);
584
- exactKeys(value, ["status", "decisionId"], label);
585
- if (value.status !== "pending-user-decision") {
586
- throw new Error(`${label}.status must be pending-user-decision`);
587
- }
1173
+ exactKeys(value, ["notificationId", "title", "body"], label);
588
1174
  return {
589
- status: "pending-user-decision",
590
- 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),
591
1182
  };
592
1183
  }
593
1184
 
@@ -662,8 +1253,8 @@ export function decodeIsolateModelInvocationV1(
662
1253
  label = "isolate model invocation",
663
1254
  ): IsolateModelInvocationV1 {
664
1255
  const value = record(input, label);
665
- if (value.status === "pending-user-decision") {
666
- return decodeIsolatePendingDecisionV1(value, label);
1256
+ if (value.status === "unavailable") {
1257
+ return decodeIsolateCapabilityFailureV1(value, label);
667
1258
  }
668
1259
  exactKeys(value, ["status", "requestId", "events"], label);
669
1260
  if (value.status !== "streaming") {