@frockbot/kernel-composition 0.0.0 → 0.1.1

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.
@@ -0,0 +1,247 @@
1
+ // The kernel-generated wrapper module (`index.js`) for a Bot isolate.
2
+ //
3
+ // This is composition, not contract: the kernel *generates* this text and
4
+ // content-addresses it together with `package.js`, so changing a byte of it is
5
+ // a new artifact set and therefore a new loader identity. Bot code never
6
+ // implements the wrapper; it exports `tools` and `execute` and the wrapper
7
+ // adapts, decodes the invocation, enforces the deadline, and hands user code a
8
+ // narrow `ctx` that names only what the isolate may do.
9
+ //
10
+ // The wrapper is emitted as plain JavaScript because it is a module in the
11
+ // loaded Worker's module map, not a source file this repository compiles.
12
+
13
+ /**
14
+ * The deadline guard, shared verbatim between the generated wrapper and the
15
+ * Bun test that proves it. Kept as source text so the tested function and the
16
+ * shipped function cannot drift.
17
+ */
18
+ export const BOT_ISOLATE_DEADLINE_SOURCE = `function withIsolateDeadline(work, deadlineMs) {
19
+ if (!Number.isSafeInteger(deadlineMs) || deadlineMs <= 0 || deadlineMs > 60000) {
20
+ return Promise.reject(new Error("isolate invocation deadline is out of range"));
21
+ }
22
+ let timer;
23
+ const expiry = new Promise(function (_resolve, reject) {
24
+ timer = setTimeout(function () {
25
+ reject(new Error("isolate invocation exceeded its deadline of " + deadlineMs + "ms"));
26
+ }, deadlineMs);
27
+ });
28
+ return Promise.race([Promise.resolve().then(work), expiry]).finally(function () {
29
+ clearTimeout(timer);
30
+ });
31
+ }`;
32
+
33
+ /**
34
+ * The invocation guard. The isolate re-decodes what the Durable Object sent:
35
+ * the boundary is crossed in both directions and both sides decode.
36
+ */
37
+ export const BOT_ISOLATE_INVOCATION_SOURCE = `var TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
38
+ var INVOCATION_KEYS = [
39
+ "schemaVersion",
40
+ "tool",
41
+ "input",
42
+ "botId",
43
+ "sessionId",
44
+ "runId",
45
+ "turnId",
46
+ "generationId",
47
+ "deadlineMs",
48
+ ];
49
+ function decodeInvocation(value) {
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
51
+ throw new Error("isolate tool invocation must be an object");
52
+ }
53
+ // Exact keys, like the outbound twin: an invocation carrying a field this
54
+ // contract does not declare is not this contract's invocation.
55
+ if (
56
+ Object.keys(value).length !== INVOCATION_KEYS.length ||
57
+ !INVOCATION_KEYS.every(function (key) {
58
+ return Object.hasOwn(value, key);
59
+ })
60
+ ) {
61
+ throw new Error("isolate tool invocation has invalid fields");
62
+ }
63
+ if (value.schemaVersion !== 1) {
64
+ throw new Error("isolate tool invocation schemaVersion is unsupported");
65
+ }
66
+ if (typeof value.tool !== "string" || !TOOL_NAME.test(value.tool)) {
67
+ throw new Error("isolate tool invocation tool is invalid");
68
+ }
69
+ for (const key of ["botId", "sessionId", "runId", "turnId", "generationId"]) {
70
+ if (typeof value[key] !== "string" || value[key].length === 0) {
71
+ throw new Error("isolate tool invocation " + key + " is invalid");
72
+ }
73
+ }
74
+ return value;
75
+ }`;
76
+
77
+ /** Decodes one NDJSON line of the `invokeModel` byte stream inside the isolate. */
78
+ export const BOT_ISOLATE_MODEL_SOURCE = `async function* modelEvents(stream) {
79
+ const reader = stream.getReader();
80
+ const decoder = new TextDecoder();
81
+ let buffer = "";
82
+ try {
83
+ for (;;) {
84
+ const chunk = await reader.read();
85
+ if (chunk.done) break;
86
+ buffer += decoder.decode(chunk.value, { stream: true });
87
+ let newline = buffer.indexOf("\\n");
88
+ while (newline >= 0) {
89
+ const line = buffer.slice(0, newline).trim();
90
+ buffer = buffer.slice(newline + 1);
91
+ if (line.length > 0) yield JSON.parse(line);
92
+ newline = buffer.indexOf("\\n");
93
+ }
94
+ }
95
+ const tail = buffer.trim();
96
+ if (tail.length > 0) yield JSON.parse(tail);
97
+ } finally {
98
+ reader.releaseLock();
99
+ }
100
+ }`;
101
+
102
+ /**
103
+ * The wrapper module text. Content-addressed with `package.js`; bump
104
+ * `BOT_ISOLATE_WRAPPER_VERSION` whenever this string changes so the mounted
105
+ * module set — and therefore the loader id — changes with it.
106
+ */
107
+ export const BOT_ISOLATE_WRAPPER_SOURCE = `// Generated by @frockbot/kernel-composition. Do not edit inside the isolate.
108
+ import { WorkerEntrypoint } from "cloudflare:workers";
109
+ import * as botPackage from "./package.js";
110
+
111
+ const CONTRACT_VERSION = 2;
112
+
113
+ ${BOT_ISOLATE_DEADLINE_SOURCE}
114
+
115
+ ${BOT_ISOLATE_INVOCATION_SOURCE}
116
+
117
+ ${BOT_ISOLATE_MODEL_SOURCE}
118
+
119
+ function declaredTools() {
120
+ const declared = Array.isArray(botPackage.tools) ? botPackage.tools : [];
121
+ if (declared.length === 0) {
122
+ throw new Error('package.js must export a non-empty "tools" array');
123
+ }
124
+ if (typeof botPackage.execute !== "function") {
125
+ throw new Error('package.js must export an "execute" function');
126
+ }
127
+ return declared.map(function (tool) {
128
+ if (!tool || typeof tool.name !== "string" || !TOOL_NAME.test(tool.name)) {
129
+ throw new Error("package.js declared a tool with an invalid name");
130
+ }
131
+ const schema =
132
+ tool.inputSchema && typeof tool.inputSchema === "object" && !Array.isArray(tool.inputSchema)
133
+ ? tool.inputSchema
134
+ : {};
135
+ // Contract version 2: a tool may name the turn types it is offered on.
136
+ // The kernel decodes and bounds it; the wrapper only carries it across.
137
+ const admission =
138
+ tool.admission && typeof tool.admission === "object"
139
+ ? {
140
+ turnTypes: tool.admission.turnTypes,
141
+ ...(tool.admission.subagentRoles
142
+ ? { subagentRoles: tool.admission.subagentRoles }
143
+ : {}),
144
+ }
145
+ : undefined;
146
+ return Object.assign(
147
+ {
148
+ name: tool.name,
149
+ description: typeof tool.description === "string" ? tool.description : "",
150
+ inputSchema: schema,
151
+ idempotent: tool.idempotent === true,
152
+ },
153
+ admission ? { admission: admission } : {},
154
+ );
155
+ });
156
+ }
157
+
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
+ };
188
+ }
189
+
190
+ export default class extends WorkerEntrypoint {
191
+ async health() {
192
+ return {
193
+ schemaVersion: 1,
194
+ ok: true,
195
+ packageId: this.env.IDENTITY.packageId,
196
+ contractVersion: CONTRACT_VERSION,
197
+ tools: declaredTools(),
198
+ };
199
+ }
200
+
201
+ async execute(rawInvocation) {
202
+ let invocation;
203
+ try {
204
+ invocation = decodeInvocation(rawInvocation);
205
+ } catch (error) {
206
+ return {
207
+ schemaVersion: 1,
208
+ content: String((error && error.message) || error),
209
+ isError: true,
210
+ };
211
+ }
212
+ try {
213
+ const context = narrowContext(this.env, invocation);
214
+ const value = await withIsolateDeadline(function () {
215
+ return botPackage.execute(invocation.tool, invocation.input, context);
216
+ }, invocation.deadlineMs);
217
+ return {
218
+ schemaVersion: 1,
219
+ content: typeof value === "string" ? value : JSON.stringify(value ?? null),
220
+ isError: false,
221
+ };
222
+ } catch (error) {
223
+ return {
224
+ schemaVersion: 1,
225
+ content: String((error && error.message) || error),
226
+ isError: true,
227
+ };
228
+ }
229
+ }
230
+ }
231
+ `;
232
+
233
+ /** Bumped with any change to the wrapper text; folded into the loader id. */
234
+ export const BOT_ISOLATE_WRAPPER_VERSION = "wrapper-v2";
235
+
236
+ export const BOT_ISOLATE_MAIN_MODULE = "index.js";
237
+ export const BOT_ISOLATE_PACKAGE_MODULE = "package.js";
238
+
239
+ /** The exactly-two-entry module map a Bot isolate mounts. */
240
+ export function botIsolateModuleMap(packageSource: string): {
241
+ [path: string]: { js: string };
242
+ } {
243
+ return {
244
+ [BOT_ISOLATE_MAIN_MODULE]: { js: BOT_ISOLATE_WRAPPER_SOURCE },
245
+ [BOT_ISOLATE_PACKAGE_MODULE]: { js: packageSource },
246
+ };
247
+ }