@frockbot/kernel-composition 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.
@@ -9,6 +9,10 @@
9
9
  //
10
10
  // The wrapper is emitted as plain JavaScript because it is a module in the
11
11
  // loaded Worker's module map, not a source file this repository compiles.
12
+ import {
13
+ BOT_ISOLATE_HOOK_EVENTS_V1,
14
+ type BotPackageContextV1,
15
+ } from "@frockbot/kernel-contracts";
12
16
 
13
17
  /**
14
18
  * The deadline guard, shared verbatim between the generated wrapper and the
@@ -35,6 +39,7 @@ export const BOT_ISOLATE_DEADLINE_SOURCE = `function withIsolateDeadline(work, d
35
39
  * the boundary is crossed in both directions and both sides decode.
36
40
  */
37
41
  export const BOT_ISOLATE_INVOCATION_SOURCE = `var TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
42
+ var HOOK_EVENTS = ${JSON.stringify(BOT_ISOLATE_HOOK_EVENTS_V1)};
38
43
  var INVOCATION_KEYS = [
39
44
  "schemaVersion",
40
45
  "tool",
@@ -72,6 +77,42 @@ function decodeInvocation(value) {
72
77
  }
73
78
  }
74
79
  return value;
80
+ }
81
+ var HOOK_INVOCATION_KEYS = [
82
+ "schemaVersion",
83
+ "event",
84
+ "payload",
85
+ "botId",
86
+ "sessionId",
87
+ "runId",
88
+ "turnId",
89
+ "generationId",
90
+ "deadlineMs",
91
+ ];
92
+ function decodeHookInvocation(value) {
93
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
94
+ throw new Error("isolate hook invocation must be an object");
95
+ }
96
+ if (
97
+ Object.keys(value).length !== HOOK_INVOCATION_KEYS.length ||
98
+ !HOOK_INVOCATION_KEYS.every(function (key) {
99
+ return Object.hasOwn(value, key);
100
+ })
101
+ ) {
102
+ throw new Error("isolate hook invocation has invalid fields");
103
+ }
104
+ if (value.schemaVersion !== 1 || !HOOK_EVENTS.includes(value.event)) {
105
+ throw new Error("isolate hook invocation is unsupported");
106
+ }
107
+ if (!value.payload || typeof value.payload !== "object" || Array.isArray(value.payload)) {
108
+ throw new Error("isolate hook invocation payload is invalid");
109
+ }
110
+ for (const key of ["botId", "sessionId", "runId", "turnId", "generationId"]) {
111
+ if (typeof value[key] !== "string" || value[key].length === 0) {
112
+ throw new Error("isolate hook invocation " + key + " is invalid");
113
+ }
114
+ }
115
+ return value;
75
116
  }`;
76
117
 
77
118
  /** Decodes one NDJSON line of the `invokeModel` byte stream inside the isolate. */
@@ -99,6 +140,86 @@ export const BOT_ISOLATE_MODEL_SOURCE = `async function* modelEvents(stream) {
99
140
  }
100
141
  }`;
101
142
 
143
+ const BOT_ISOLATE_CONTEXT_PROPERTY_SOURCE_V1 = {
144
+ tool: "invocation.tool",
145
+ event: "invocation.event",
146
+ botId: "invocation.botId",
147
+ sessionId: "invocation.sessionId",
148
+ runId: "invocation.runId",
149
+ turnId: "invocation.turnId",
150
+ generationId: "invocation.generationId",
151
+ packageId: "env.IDENTITY.packageId",
152
+ deadlineMs: "invocation.deadlineMs",
153
+ bindings: "Object.keys(env).sort()",
154
+ capabilities: `{
155
+ list: function () {
156
+ return capabilities.list();
157
+ },
158
+ }`,
159
+ model: `{
160
+ invoke: async function (request) {
161
+ const outcome = await capabilities.invokeModel(request);
162
+ if (!outcome || outcome.status !== "streaming") return outcome;
163
+ return {
164
+ status: "streaming",
165
+ requestId: outcome.requestId,
166
+ events: modelEvents(outcome.events),
167
+ };
168
+ },
169
+ }`,
170
+ tools: `{
171
+ invoke: function (request) {
172
+ return capabilities.invokeTool(request);
173
+ },
174
+ }`,
175
+ memory: `{
176
+ read: function (request) {
177
+ return capabilities.memoryRead(request);
178
+ },
179
+ write: function (request) {
180
+ return capabilities.memoryWrite(request);
181
+ },
182
+ forget: function (request) {
183
+ return capabilities.memoryForget(request);
184
+ },
185
+ }`,
186
+ workspace: `{
187
+ read: function (path) {
188
+ return capabilities.workspaceRead(path);
189
+ },
190
+ list: function (request) {
191
+ return capabilities.workspaceList(request);
192
+ },
193
+ stat: function (path) {
194
+ return capabilities.workspaceStat(path);
195
+ },
196
+ write: function (request) {
197
+ return capabilities.workspaceWrite(request);
198
+ },
199
+ delete: function (request) {
200
+ return capabilities.workspaceDelete(request);
201
+ },
202
+ }`,
203
+ connection:
204
+ "function (connectionId) { return capabilities.connection(connectionId); }",
205
+ notify: "function (request) { return capabilities.notify(request); }",
206
+ schedule: "function (request) { return capabilities.schedule(request); }",
207
+ } satisfies Record<keyof BotPackageContextV1, string>;
208
+
209
+ /** The keys the generated wrapper actually places on `ctx`. */
210
+ export const BOT_ISOLATE_NARROW_CONTEXT_KEYS_V1 = Object.keys(
211
+ BOT_ISOLATE_CONTEXT_PROPERTY_SOURCE_V1,
212
+ ) as Array<keyof BotPackageContextV1>;
213
+
214
+ export const BOT_ISOLATE_NARROW_CONTEXT_SOURCE_V1 = `function narrowContext(env, invocation) {
215
+ const capabilities = env.CAPABILITIES;
216
+ return {
217
+ ${Object.entries(BOT_ISOLATE_CONTEXT_PROPERTY_SOURCE_V1)
218
+ .map(([key, source]) => ` ${JSON.stringify(key)}: ${source},`)
219
+ .join("\n")}
220
+ };
221
+ }`;
222
+
102
223
  /**
103
224
  * The wrapper module text. Content-addressed with `package.js`; bump
104
225
  * `BOT_ISOLATE_WRAPPER_VERSION` whenever this string changes so the mounted
@@ -108,7 +229,7 @@ export const BOT_ISOLATE_WRAPPER_SOURCE = `// Generated by @frockbot/kernel-comp
108
229
  import { WorkerEntrypoint } from "cloudflare:workers";
109
230
  import * as botPackage from "./package.js";
110
231
 
111
- const CONTRACT_VERSION = 2;
232
+ const CONTRACT_VERSION = 3;
112
233
 
113
234
  ${BOT_ISOLATE_DEADLINE_SOURCE}
114
235
 
@@ -155,38 +276,24 @@ function declaredTools() {
155
276
  });
156
277
  }
157
278
 
158
- function narrowContext(env, invocation) {
159
- const capabilities = env.CAPABILITIES;
160
- return {
161
- tool: invocation.tool,
162
- botId: invocation.botId,
163
- sessionId: invocation.sessionId,
164
- runId: invocation.runId,
165
- turnId: invocation.turnId,
166
- generationId: invocation.generationId,
167
- packageId: env.IDENTITY.packageId,
168
- deadlineMs: invocation.deadlineMs,
169
- // What this isolate holds, by name. Everything else is out of scope:
170
- // globalOutbound is null and no host binding is in env.
171
- bindings: Object.keys(env).sort(),
172
- listCapabilities: function () {
173
- return capabilities.list();
174
- },
175
- requestAuthority: function (request) {
176
- return capabilities.requestAuthority(request);
177
- },
178
- invokeModel: async function (request) {
179
- const outcome = await capabilities.invokeModel(request);
180
- if (!outcome || outcome.status !== "streaming") return outcome;
181
- return {
182
- status: "streaming",
183
- requestId: outcome.requestId,
184
- events: modelEvents(outcome.events),
185
- };
186
- },
187
- };
279
+ function declaredHooks() {
280
+ if (botPackage.hooks === undefined) return [];
281
+ if (!botPackage.hooks || typeof botPackage.hooks !== "object" || Array.isArray(botPackage.hooks)) {
282
+ throw new Error('package.js "hooks" must be an object');
283
+ }
284
+ return Object.keys(botPackage.hooks).map(function (event) {
285
+ if (!HOOK_EVENTS.includes(event)) {
286
+ throw new Error('package.js declared an unsupported hook "' + event + '"');
287
+ }
288
+ if (typeof botPackage.hooks[event] !== "function") {
289
+ throw new Error('package.js hook "' + event + '" must be a function');
290
+ }
291
+ return event;
292
+ });
188
293
  }
189
294
 
295
+ ${BOT_ISOLATE_NARROW_CONTEXT_SOURCE_V1}
296
+
190
297
  export default class extends WorkerEntrypoint {
191
298
  async health() {
192
299
  return {
@@ -195,6 +302,7 @@ export default class extends WorkerEntrypoint {
195
302
  packageId: this.env.IDENTITY.packageId,
196
303
  contractVersion: CONTRACT_VERSION,
197
304
  tools: declaredTools(),
305
+ hooks: declaredHooks(),
198
306
  };
199
307
  }
200
308
 
@@ -227,11 +335,26 @@ export default class extends WorkerEntrypoint {
227
335
  };
228
336
  }
229
337
  }
338
+
339
+ async hook(rawInvocation) {
340
+ const invocation = decodeHookInvocation(rawInvocation);
341
+ const hooks = declaredHooks();
342
+ if (!hooks.includes(invocation.event)) {
343
+ throw new Error('package.js did not declare hook "' + invocation.event + '"');
344
+ }
345
+ const context = narrowContext(this.env, invocation);
346
+ const replacement = await withIsolateDeadline(function () {
347
+ return botPackage.hooks[invocation.event](invocation.payload, context);
348
+ }, invocation.deadlineMs);
349
+ return replacement === undefined
350
+ ? { schemaVersion: 1, status: "unchanged" }
351
+ : { schemaVersion: 1, status: "replaced", replacement: replacement };
352
+ }
230
353
  }
231
354
  `;
232
355
 
233
356
  /** Bumped with any change to the wrapper text; folded into the loader id. */
234
- export const BOT_ISOLATE_WRAPPER_VERSION = "wrapper-v2";
357
+ export const BOT_ISOLATE_WRAPPER_VERSION = "wrapper-v4";
235
358
 
236
359
  export const BOT_ISOLATE_MAIN_MODULE = "index.js";
237
360
  export const BOT_ISOLATE_PACKAGE_MODULE = "package.js";