@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.
- package/package.json +2 -2
- package/src/compiler.test.ts +17 -0
- package/src/compiler.ts +12 -2
- package/src/generation.test.ts +55 -0
- package/src/generation.ts +111 -7
- package/src/index.test.ts +233 -7
- package/src/isolate-host.test.ts +342 -14
- package/src/isolate-host.ts +303 -22
- package/src/isolate-wrapper.test.ts +35 -0
- package/src/isolate-wrapper.ts +155 -32
- package/src/manifest.ts +345 -31
package/src/isolate-wrapper.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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-
|
|
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";
|