@ian-pascoe/pi-codemode 0.1.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.
@@ -0,0 +1,480 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ /** Maximum UTF-8 bytes in one CodeMode worker protocol line, excluding its newline. */
4
+ export const CODEMODE_WORKER_MESSAGE_LIMIT_BYTES = 8 * 1024 * 1024;
5
+
6
+ const CODEMODE_WORKER_PROTOCOL_VERSION = 1;
7
+ const arrayIsArray = Array.isArray;
8
+ const jsonParse = JSON.parse;
9
+ const jsonStringify = JSON.stringify;
10
+ const objectKeys = Object.keys;
11
+
12
+ type CodeModeProtocolObject = { readonly [key: string]: CodeModeProtocolValue };
13
+ type CodeModeProtocolValue =
14
+ | null
15
+ | boolean
16
+ | number
17
+ | string
18
+ | readonly CodeModeProtocolValue[]
19
+ | CodeModeProtocolObject;
20
+ type CodeModeProtocolSlot = CodeModeProtocolValue | undefined;
21
+
22
+ /** A nested tool call emitted by a Deno Cell after one microtask drain. */
23
+ export type CodeModeWorkerToolCall = {
24
+ readonly callId: string;
25
+ readonly toolName: string;
26
+ readonly inputJson: string;
27
+ };
28
+
29
+ /** A catchable nested tool settlement returned to the Deno Cell. */
30
+ export type CodeModeWorkerToolSettlement =
31
+ | { readonly callId: string; readonly outcome: "success"; readonly resultJson: string }
32
+ | {
33
+ readonly callId: string;
34
+ readonly outcome: "error";
35
+ readonly error: { readonly code: string; readonly message: string };
36
+ };
37
+
38
+ /** Strict parent-to-process message for one persistent CodeMode Session. */
39
+ export type CodeModeWorkerRequest =
40
+ | {
41
+ readonly version: 1;
42
+ readonly type: "execute";
43
+ readonly sessionId: string;
44
+ readonly cellId: string;
45
+ readonly source: string;
46
+ readonly internalIdentifierPlaceholder: string;
47
+ readonly toolNames: readonly string[];
48
+ }
49
+ | {
50
+ readonly version: 1;
51
+ readonly type: "tool-results";
52
+ readonly sessionId: string;
53
+ readonly cellId: string;
54
+ readonly batchId: string;
55
+ readonly results: readonly CodeModeWorkerToolSettlement[];
56
+ }
57
+ | { readonly version: 1; readonly type: "shutdown"; readonly sessionId: string };
58
+
59
+ /** Stable worker-side Cell failures translated by the session coordinator. */
60
+ export type CodeModeWorkerCellErrorCode = "script" | "serialization" | "runtime";
61
+
62
+ /** Strict process-to-parent message for one persistent CodeMode Session. */
63
+ export type CodeModeWorkerResponse =
64
+ | { readonly version: 1; readonly type: "ready"; readonly sessionId: string }
65
+ | {
66
+ readonly version: 1;
67
+ readonly type: "tool-batch";
68
+ readonly sessionId: string;
69
+ readonly cellId: string;
70
+ readonly batchId: string;
71
+ readonly calls: readonly CodeModeWorkerToolCall[];
72
+ }
73
+ | {
74
+ readonly version: 1;
75
+ readonly type: "cell-result";
76
+ readonly sessionId: string;
77
+ readonly cellId: string;
78
+ readonly resultJson?: string;
79
+ }
80
+ | {
81
+ readonly version: 1;
82
+ readonly type: "cell-error";
83
+ readonly sessionId: string;
84
+ readonly cellId: string;
85
+ readonly error: { readonly code: CodeModeWorkerCellErrorCode; readonly message: string };
86
+ }
87
+ | {
88
+ readonly version: 1;
89
+ readonly type: "protocol-error";
90
+ readonly sessionId: string;
91
+ readonly message: string;
92
+ };
93
+
94
+ /** Expected result of parsing one untrusted CodeMode worker protocol line. */
95
+ export type CodeModeWorkerParseResult<T> =
96
+ | { readonly ok: true; readonly value: T }
97
+ | { readonly ok: false; readonly message: string };
98
+
99
+ function isRecord(value: CodeModeProtocolSlot): value is CodeModeProtocolObject {
100
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- SAFETY: JSON.parse has already limited the value to JSON primitives, arrays, and objects; this selects the object arm without coercion.
101
+ return typeof value === "object" && value !== null && !arrayIsArray(value);
102
+ }
103
+
104
+ function isString(value: CodeModeProtocolSlot): value is string {
105
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- SAFETY: JSON.parse has already limited the value to JSON data; this selects its string arm without coercion.
106
+ return typeof value === "string";
107
+ }
108
+
109
+ function isNonEmptyString(value: CodeModeProtocolSlot): value is string {
110
+ return isString(value) && value.length > 0;
111
+ }
112
+
113
+ function isUniqueNonEmptyStringArray(value: CodeModeProtocolSlot): value is readonly string[] {
114
+ if (!arrayIsArray(value)) return false;
115
+ for (let index = 0; index < value.length; index += 1) {
116
+ const candidate = value[index];
117
+ if (!isNonEmptyString(candidate)) return false;
118
+ for (let priorIndex = 0; priorIndex < index; priorIndex += 1) {
119
+ if (value[priorIndex] === candidate) return false;
120
+ }
121
+ }
122
+ return true;
123
+ }
124
+
125
+ function hasString(values: readonly string[], candidate: string): boolean {
126
+ for (let index = 0; index < values.length; index += 1) {
127
+ if (values[index] === candidate) return true;
128
+ }
129
+ return false;
130
+ }
131
+
132
+ function hasExactKeys(value: CodeModeProtocolObject, expected: readonly string[]): boolean {
133
+ const keys = objectKeys(value);
134
+ if (keys.length !== expected.length) return false;
135
+ for (let expectedIndex = 0; expectedIndex < expected.length; expectedIndex += 1) {
136
+ const expectedKey = expected[expectedIndex];
137
+ if (expectedKey === undefined) return false;
138
+ let found = false;
139
+ for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
140
+ if (keys[keyIndex] === expectedKey) {
141
+ found = true;
142
+ break;
143
+ }
144
+ }
145
+ if (!found) return false;
146
+ }
147
+ return true;
148
+ }
149
+
150
+ function parseProtocolJson(
151
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Raw process input is untrusted until the string and JSON checks below establish the protocol representation.
152
+ message: unknown,
153
+ subject: "request" | "response",
154
+ ): CodeModeWorkerParseResult<CodeModeProtocolValue> {
155
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- SAFETY: This is the sole raw process-line ingress; only primitive string input reaches JSON.parse and protocol tests cover non-string rejection.
156
+ if (typeof message !== "string")
157
+ return { ok: false, message: `CodeMode worker ${subject} must be JSON text` };
158
+ if (Buffer.byteLength(message, "utf8") > CODEMODE_WORKER_MESSAGE_LIMIT_BYTES) {
159
+ return { ok: false, message: `CodeMode worker ${subject} exceeds 8 MiB` };
160
+ }
161
+ try {
162
+ // SAFETY: Successful JSON.parse output is exactly the recursive JSON representation modeled by CodeModeProtocolValue.
163
+ return { ok: true, value: jsonParse(message) as CodeModeProtocolValue };
164
+ } catch {
165
+ return { ok: false, message: `CodeMode worker ${subject} is not valid JSON` };
166
+ }
167
+ }
168
+
169
+ function parseToolSettlement(
170
+ value: CodeModeProtocolSlot,
171
+ ): CodeModeWorkerToolSettlement | undefined {
172
+ if (!isRecord(value) || !isNonEmptyString(value.callId)) return undefined;
173
+ if (
174
+ value.outcome === "success" &&
175
+ hasExactKeys(value, ["callId", "outcome", "resultJson"]) &&
176
+ isString(value.resultJson)
177
+ ) {
178
+ return { callId: value.callId, outcome: "success", resultJson: value.resultJson };
179
+ }
180
+ if (
181
+ value.outcome === "error" &&
182
+ hasExactKeys(value, ["callId", "error", "outcome"]) &&
183
+ isRecord(value.error) &&
184
+ hasExactKeys(value.error, ["code", "message"]) &&
185
+ isNonEmptyString(value.error.code) &&
186
+ isNonEmptyString(value.error.message)
187
+ ) {
188
+ return {
189
+ callId: value.callId,
190
+ outcome: "error",
191
+ error: { code: value.error.code, message: value.error.message },
192
+ };
193
+ }
194
+ return undefined;
195
+ }
196
+
197
+ function parseToolResultsRequest(
198
+ decoded: CodeModeProtocolObject,
199
+ ): CodeModeWorkerRequest | undefined {
200
+ if (
201
+ !hasExactKeys(decoded, ["batchId", "cellId", "results", "sessionId", "type", "version"]) ||
202
+ !isNonEmptyString(decoded.sessionId) ||
203
+ !isNonEmptyString(decoded.cellId) ||
204
+ !isNonEmptyString(decoded.batchId) ||
205
+ !arrayIsArray(decoded.results)
206
+ ) {
207
+ return undefined;
208
+ }
209
+ const results: CodeModeWorkerToolSettlement[] = [];
210
+ const callIds: string[] = [];
211
+ for (let index = 0; index < decoded.results.length; index += 1) {
212
+ const result = parseToolSettlement(decoded.results[index]);
213
+ if (result === undefined || hasString(callIds, result.callId)) return undefined;
214
+ callIds[callIds.length] = result.callId;
215
+ results[results.length] = result;
216
+ }
217
+ return {
218
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
219
+ type: "tool-results",
220
+ sessionId: decoded.sessionId,
221
+ cellId: decoded.cellId,
222
+ batchId: decoded.batchId,
223
+ results,
224
+ };
225
+ }
226
+
227
+ /** Parses one strict, versioned, bounded parent-to-worker JSON line. */
228
+ export function parseCodeModeWorkerRequest(
229
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: This exported parser is the parent-to-worker protocol ingress and accepts arbitrary process input by design.
230
+ message: unknown,
231
+ ): CodeModeWorkerParseResult<CodeModeWorkerRequest> {
232
+ const parsed = parseProtocolJson(message, "request");
233
+ if (!parsed.ok) return parsed;
234
+ const decoded = parsed.value;
235
+ if (
236
+ !isRecord(decoded) ||
237
+ decoded.version !== CODEMODE_WORKER_PROTOCOL_VERSION ||
238
+ !isString(decoded.type)
239
+ ) {
240
+ return { ok: false, message: "CodeMode worker request has an invalid protocol shape" };
241
+ }
242
+
243
+ if (
244
+ decoded.type === "shutdown" &&
245
+ hasExactKeys(decoded, ["sessionId", "type", "version"]) &&
246
+ isNonEmptyString(decoded.sessionId)
247
+ ) {
248
+ return {
249
+ ok: true,
250
+ value: {
251
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
252
+ type: "shutdown",
253
+ sessionId: decoded.sessionId,
254
+ },
255
+ };
256
+ }
257
+ if (decoded.type === "tool-results") {
258
+ const request = parseToolResultsRequest(decoded);
259
+ return request === undefined
260
+ ? { ok: false, message: "CodeMode worker request has an invalid protocol shape" }
261
+ : { ok: true, value: request };
262
+ }
263
+ if (decoded.type === "execute") {
264
+ if (
265
+ hasExactKeys(decoded, [
266
+ "cellId",
267
+ "internalIdentifierPlaceholder",
268
+ "sessionId",
269
+ "source",
270
+ "toolNames",
271
+ "type",
272
+ "version",
273
+ ]) &&
274
+ isNonEmptyString(decoded.sessionId) &&
275
+ isNonEmptyString(decoded.cellId) &&
276
+ isString(decoded.source) &&
277
+ isNonEmptyString(decoded.internalIdentifierPlaceholder) &&
278
+ isUniqueNonEmptyStringArray(decoded.toolNames)
279
+ ) {
280
+ return {
281
+ ok: true,
282
+ value: {
283
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
284
+ type: "execute",
285
+ sessionId: decoded.sessionId,
286
+ cellId: decoded.cellId,
287
+ source: decoded.source,
288
+ internalIdentifierPlaceholder: decoded.internalIdentifierPlaceholder,
289
+ toolNames: decoded.toolNames,
290
+ },
291
+ };
292
+ }
293
+ }
294
+ return { ok: false, message: "CodeMode worker request has an invalid protocol shape" };
295
+ }
296
+
297
+ function parseWorkerError(
298
+ value: CodeModeProtocolSlot,
299
+ ): { readonly code: string; readonly message: string } | undefined {
300
+ if (
301
+ !isRecord(value) ||
302
+ !hasExactKeys(value, ["code", "message"]) ||
303
+ !isNonEmptyString(value.code) ||
304
+ !isNonEmptyString(value.message)
305
+ ) {
306
+ return undefined;
307
+ }
308
+ return { code: value.code, message: value.message };
309
+ }
310
+
311
+ function parseToolBatchResponse(
312
+ decoded: CodeModeProtocolObject,
313
+ ): CodeModeWorkerResponse | undefined {
314
+ if (
315
+ !hasExactKeys(decoded, ["batchId", "calls", "cellId", "sessionId", "type", "version"]) ||
316
+ !isNonEmptyString(decoded.sessionId) ||
317
+ !isNonEmptyString(decoded.cellId) ||
318
+ !isNonEmptyString(decoded.batchId) ||
319
+ !arrayIsArray(decoded.calls)
320
+ ) {
321
+ return undefined;
322
+ }
323
+ const calls: CodeModeWorkerToolCall[] = [];
324
+ const callIds: string[] = [];
325
+ for (let index = 0; index < decoded.calls.length; index += 1) {
326
+ const candidate = decoded.calls[index];
327
+ if (
328
+ !isRecord(candidate) ||
329
+ !hasExactKeys(candidate, ["callId", "inputJson", "toolName"]) ||
330
+ !isNonEmptyString(candidate.callId) ||
331
+ hasString(callIds, candidate.callId) ||
332
+ !isNonEmptyString(candidate.toolName) ||
333
+ !isString(candidate.inputJson)
334
+ ) {
335
+ return undefined;
336
+ }
337
+ callIds[callIds.length] = candidate.callId;
338
+ calls[calls.length] = {
339
+ callId: candidate.callId,
340
+ toolName: candidate.toolName,
341
+ inputJson: candidate.inputJson,
342
+ };
343
+ }
344
+ if (calls.length === 0) return undefined;
345
+ return {
346
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
347
+ type: "tool-batch",
348
+ sessionId: decoded.sessionId,
349
+ cellId: decoded.cellId,
350
+ batchId: decoded.batchId,
351
+ calls,
352
+ };
353
+ }
354
+
355
+ /** Parses one strict, versioned, bounded worker-to-parent JSON line. */
356
+ export function parseCodeModeWorkerResponse(
357
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: This exported parser is the worker-to-parent protocol ingress and accepts arbitrary process input by design.
358
+ message: unknown,
359
+ ): CodeModeWorkerParseResult<CodeModeWorkerResponse> {
360
+ const parsed = parseProtocolJson(message, "response");
361
+ if (!parsed.ok) return parsed;
362
+ const decoded = parsed.value;
363
+ if (
364
+ !isRecord(decoded) ||
365
+ decoded.version !== CODEMODE_WORKER_PROTOCOL_VERSION ||
366
+ !isString(decoded.type) ||
367
+ !isNonEmptyString(decoded.sessionId)
368
+ ) {
369
+ return { ok: false, message: "CodeMode worker response has an invalid protocol shape" };
370
+ }
371
+ if (decoded.type === "ready" && hasExactKeys(decoded, ["sessionId", "type", "version"])) {
372
+ return {
373
+ ok: true,
374
+ value: {
375
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
376
+ type: "ready",
377
+ sessionId: decoded.sessionId,
378
+ },
379
+ };
380
+ }
381
+ if (
382
+ decoded.type === "protocol-error" &&
383
+ hasExactKeys(decoded, ["message", "sessionId", "type", "version"]) &&
384
+ isNonEmptyString(decoded.message)
385
+ ) {
386
+ return {
387
+ ok: true,
388
+ value: {
389
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
390
+ type: "protocol-error",
391
+ sessionId: decoded.sessionId,
392
+ message: decoded.message,
393
+ },
394
+ };
395
+ }
396
+ if (decoded.type === "tool-batch") {
397
+ const response = parseToolBatchResponse(decoded);
398
+ return response === undefined
399
+ ? { ok: false, message: "CodeMode worker response has an invalid protocol shape" }
400
+ : { ok: true, value: response };
401
+ }
402
+ if (
403
+ decoded.type === "cell-result" &&
404
+ hasExactKeys(
405
+ decoded,
406
+ decoded.resultJson === undefined
407
+ ? ["cellId", "sessionId", "type", "version"]
408
+ : ["cellId", "resultJson", "sessionId", "type", "version"],
409
+ ) &&
410
+ isNonEmptyString(decoded.cellId) &&
411
+ (decoded.resultJson === undefined || isString(decoded.resultJson))
412
+ ) {
413
+ const value = {
414
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
415
+ type: "cell-result",
416
+ sessionId: decoded.sessionId,
417
+ cellId: decoded.cellId,
418
+ } as const;
419
+ return decoded.resultJson === undefined
420
+ ? { ok: true, value }
421
+ : { ok: true, value: { ...value, resultJson: decoded.resultJson } };
422
+ }
423
+ if (
424
+ decoded.type === "cell-error" &&
425
+ hasExactKeys(decoded, ["cellId", "error", "sessionId", "type", "version"]) &&
426
+ isNonEmptyString(decoded.cellId)
427
+ ) {
428
+ const error = parseWorkerError(decoded.error);
429
+ if (error !== undefined && ["script", "serialization", "runtime"].includes(error.code)) {
430
+ // SAFETY: The literal-membership check above refines the protocol string to the closed worker error code union.
431
+ const code = error.code as CodeModeWorkerCellErrorCode;
432
+ return {
433
+ ok: true,
434
+ value: {
435
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
436
+ type: "cell-error",
437
+ sessionId: decoded.sessionId,
438
+ cellId: decoded.cellId,
439
+ error: { code, message: error.message },
440
+ },
441
+ };
442
+ }
443
+ }
444
+ return { ok: false, message: "CodeMode worker response has an invalid protocol shape" };
445
+ }
446
+
447
+ /** Serializes one parent request without allowing an oversized protocol line. */
448
+ export function serializeCodeModeWorkerRequest(
449
+ request: CodeModeWorkerRequest,
450
+ ): CodeModeWorkerParseResult<string> {
451
+ const message = jsonStringify(request);
452
+ return Buffer.byteLength(message, "utf8") <= CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
453
+ ? { ok: true, value: message }
454
+ : { ok: false, message: "CodeMode worker request exceeds 8 MiB" };
455
+ }
456
+
457
+ /** Serializes one worker response, replacing oversized Cell output with a bounded error. */
458
+ export function serializeCodeModeWorkerResponse(response: CodeModeWorkerResponse): string {
459
+ const message = jsonStringify(response);
460
+ if (Buffer.byteLength(message, "utf8") <= CODEMODE_WORKER_MESSAGE_LIMIT_BYTES) return message;
461
+ if (
462
+ response.type === "cell-result" ||
463
+ response.type === "cell-error" ||
464
+ response.type === "tool-batch"
465
+ ) {
466
+ return jsonStringify({
467
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
468
+ type: "cell-error",
469
+ sessionId: response.sessionId,
470
+ cellId: response.cellId,
471
+ error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
472
+ } satisfies CodeModeWorkerResponse);
473
+ }
474
+ return jsonStringify({
475
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
476
+ type: "protocol-error",
477
+ sessionId: response.sessionId,
478
+ message: "CodeMode worker response exceeds 8 MiB",
479
+ } satisfies CodeModeWorkerResponse);
480
+ }