@ian-pascoe/pi-codemode 0.2.0 → 0.3.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.
- package/README.md +50 -13
- package/package.json +1 -1
- package/src/codemode-console-output.ts +11 -0
- package/src/codemode-observer-ui.ts +15 -36
- package/src/codemode-session-coordinator.ts +127 -90
- package/src/codemode-tool-catalog.ts +4 -6
- package/src/codemode-tool-contract.ts +37 -7
- package/src/codemode-tool-rendering.ts +71 -27
- package/src/codemode-worker-protocol.ts +141 -33
- package/src/codemode-worker.ts +283 -18
- package/src/pi-codemode-extension.ts +17 -21
- package/src/pi-tool-bridge.ts +5 -38
package/src/codemode-worker.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CODEMODE_CONSOLE_METHODS, type CodeModeConsoleEntry } from "./codemode-console-output.ts";
|
|
1
2
|
import {
|
|
2
3
|
CODEMODE_WORKER_MESSAGE_LIMIT_BYTES,
|
|
3
4
|
parseCodeModeWorkerRequest,
|
|
@@ -9,13 +10,27 @@ import {
|
|
|
9
10
|
} from "./codemode-worker-protocol.ts";
|
|
10
11
|
|
|
11
12
|
const CODEMODE_WORKER_READ_BUFFER_BYTES = 64 * 1024;
|
|
13
|
+
const CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES = 512;
|
|
12
14
|
|
|
13
15
|
type DenoByteReader = { read(buffer: Uint8Array): Promise<number | null> };
|
|
14
16
|
type DenoByteWriter = { write(buffer: Uint8Array): Promise<number> };
|
|
17
|
+
type DenoInspectOptions = {
|
|
18
|
+
readonly colors: false;
|
|
19
|
+
readonly getters: false;
|
|
20
|
+
readonly customInspect: false;
|
|
21
|
+
};
|
|
15
22
|
type CodeModeDenoNamespace = {
|
|
16
23
|
readonly args: readonly string[];
|
|
17
24
|
readonly stdin: DenoByteReader;
|
|
18
25
|
readonly stdout: DenoByteWriter;
|
|
26
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Deno.inspect is the captured hostile-value formatter; coordinator tests cover getters, coercion hooks, custom inspectors, and Proxies.
|
|
27
|
+
readonly inspect: (value: unknown, options: DenoInspectOptions) => string;
|
|
28
|
+
readonly internal: symbol;
|
|
29
|
+
readonly [key: symbol]:
|
|
30
|
+
| {
|
|
31
|
+
readonly inspectArgs: (args: readonly unknown[], options: DenoInspectOptions) => string;
|
|
32
|
+
}
|
|
33
|
+
| undefined;
|
|
19
34
|
readonly version: {
|
|
20
35
|
readonly deno: string;
|
|
21
36
|
readonly v8: string;
|
|
@@ -26,6 +41,11 @@ type CodeModeDenoNamespace = {
|
|
|
26
41
|
declare const Deno: CodeModeDenoNamespace;
|
|
27
42
|
|
|
28
43
|
const denoProcess = Deno;
|
|
44
|
+
const denoInspect = denoProcess.inspect;
|
|
45
|
+
const denoInternal = denoProcess[denoProcess.internal];
|
|
46
|
+
if (denoInternal === undefined)
|
|
47
|
+
throw new Error("Pi CodeMode: Deno Console formatter is unavailable");
|
|
48
|
+
const denoInspectArgs = denoInternal.inspectArgs;
|
|
29
49
|
const arrayIsArray = Array.isArray;
|
|
30
50
|
const arrayPrototype = Array.prototype;
|
|
31
51
|
const blobConstructor = Blob;
|
|
@@ -43,6 +63,11 @@ const numberFrom = Number;
|
|
|
43
63
|
const numberIsFinite = Number.isFinite;
|
|
44
64
|
const numberIsSafeInteger = Number.isSafeInteger;
|
|
45
65
|
const objectFreeze = Object.freeze;
|
|
66
|
+
const SAFE_DENO_INSPECT_OPTIONS = objectFreeze({
|
|
67
|
+
colors: false,
|
|
68
|
+
getters: false,
|
|
69
|
+
customInspect: false,
|
|
70
|
+
} as const);
|
|
46
71
|
const objectPrototype = Object.prototype;
|
|
47
72
|
const ownKeys = Reflect.ownKeys;
|
|
48
73
|
const queueRuntimeMicrotask = queueMicrotask.bind(globalThis);
|
|
@@ -159,6 +184,9 @@ type ActiveWorkerCell = {
|
|
|
159
184
|
readonly sessionId: string;
|
|
160
185
|
readonly cellId: string;
|
|
161
186
|
readonly pendingCalls: PendingGuestToolCall[];
|
|
187
|
+
readonly consoleEntries: CodeModeConsoleEntry[];
|
|
188
|
+
consoleBytes: number;
|
|
189
|
+
consoleOverflow: boolean;
|
|
162
190
|
batchSequence: number;
|
|
163
191
|
callSequence: number;
|
|
164
192
|
batchScheduled: boolean;
|
|
@@ -206,6 +234,113 @@ function isReservedNotebookBindingName(name: string): boolean {
|
|
|
206
234
|
return false;
|
|
207
235
|
}
|
|
208
236
|
|
|
237
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Cell Console calls accept arbitrary guest values; captured Deno.inspect disables getters and custom inspection, while coordinator hostile-value tests cover coercion hooks and Proxies.
|
|
238
|
+
function inspectGuestConsoleValue(value: unknown): string {
|
|
239
|
+
try {
|
|
240
|
+
return denoInspect(value, SAFE_DENO_INSPECT_OPTIONS);
|
|
241
|
+
} catch {
|
|
242
|
+
return "[Uninspectable value]";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatGuestConsoleArguments(args: readonly unknown[]): string | undefined {
|
|
247
|
+
if (args.length === 0) return "";
|
|
248
|
+
const first = args[0];
|
|
249
|
+
const maximumTextLength =
|
|
250
|
+
CODEMODE_WORKER_MESSAGE_LIMIT_BYTES - CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES;
|
|
251
|
+
if (isGuestString(first) && first.length >= maximumTextLength) return undefined;
|
|
252
|
+
if (args.length === 1 && isGuestString(first)) return first;
|
|
253
|
+
if (isGuestString(first) && !first.includes("%")) {
|
|
254
|
+
let text = first;
|
|
255
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
256
|
+
const value = args[index];
|
|
257
|
+
const rendered = isGuestString(value) ? value : inspectGuestConsoleValue(value);
|
|
258
|
+
if (text.length + rendered.length + 1 >= maximumTextLength) return undefined;
|
|
259
|
+
text += ` ${rendered}`;
|
|
260
|
+
}
|
|
261
|
+
return text;
|
|
262
|
+
}
|
|
263
|
+
if (!isGuestString(first)) {
|
|
264
|
+
let text = "";
|
|
265
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
266
|
+
const value = args[index];
|
|
267
|
+
const rendered = isGuestString(value) ? value : inspectGuestConsoleValue(value);
|
|
268
|
+
const separatorLength = index === 0 ? 0 : 1;
|
|
269
|
+
if (text.length + rendered.length + separatorLength >= maximumTextLength) return undefined;
|
|
270
|
+
if (separatorLength > 0) text += " ";
|
|
271
|
+
text += rendered;
|
|
272
|
+
}
|
|
273
|
+
return text;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const safeArgs: unknown[] = [first];
|
|
277
|
+
for (let index = 1; index < args.length; index += 1) safeArgs[index] = args[index];
|
|
278
|
+
let format = "";
|
|
279
|
+
let argumentIndex = 1;
|
|
280
|
+
for (let index = 0; index < first.length; index += 1) {
|
|
281
|
+
const character = first[index];
|
|
282
|
+
if (character !== "%" || index + 1 >= first.length) {
|
|
283
|
+
format += character;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const token = first[index + 1];
|
|
287
|
+
if (token === "%") {
|
|
288
|
+
format += "%%";
|
|
289
|
+
index += 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (
|
|
293
|
+
token !== "s" &&
|
|
294
|
+
token !== "d" &&
|
|
295
|
+
token !== "i" &&
|
|
296
|
+
token !== "f" &&
|
|
297
|
+
token !== "j" &&
|
|
298
|
+
token !== "o" &&
|
|
299
|
+
token !== "O" &&
|
|
300
|
+
token !== "c"
|
|
301
|
+
) {
|
|
302
|
+
format += `%${token}`;
|
|
303
|
+
index += 1;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const value = safeArgs[argumentIndex];
|
|
307
|
+
if (value !== undefined || argumentIndex < safeArgs.length) {
|
|
308
|
+
if (isGuestReference(value)) {
|
|
309
|
+
const inspected = inspectGuestConsoleValue(value);
|
|
310
|
+
if (inspected.length >= maximumTextLength) return undefined;
|
|
311
|
+
safeArgs[argumentIndex] = inspected;
|
|
312
|
+
format += token === "c" ? "%c" : "%s";
|
|
313
|
+
} else {
|
|
314
|
+
format += `%${token}`;
|
|
315
|
+
}
|
|
316
|
+
argumentIndex += 1;
|
|
317
|
+
} else {
|
|
318
|
+
format += `%${token}`;
|
|
319
|
+
}
|
|
320
|
+
index += 1;
|
|
321
|
+
}
|
|
322
|
+
safeArgs[0] = format;
|
|
323
|
+
for (let index = argumentIndex; index < safeArgs.length; index += 1) {
|
|
324
|
+
const value = safeArgs[index];
|
|
325
|
+
if (isGuestReference(value)) {
|
|
326
|
+
const inspected = inspectGuestConsoleValue(value);
|
|
327
|
+
if (inspected.length >= maximumTextLength) return undefined;
|
|
328
|
+
safeArgs[index] = inspected;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
let minimumFormattedBytes = 0;
|
|
332
|
+
for (const value of safeArgs) {
|
|
333
|
+
if (isGuestString(value)) minimumFormattedBytes += value.length;
|
|
334
|
+
if (
|
|
335
|
+
minimumFormattedBytes + CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
|
|
336
|
+
CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
|
|
337
|
+
) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return denoInspectArgs(safeArgs, SAFE_DENO_INSPECT_OPTIONS);
|
|
342
|
+
}
|
|
343
|
+
|
|
209
344
|
function assertNotebookBindingNameAvailable(name: string): void {
|
|
210
345
|
if (isReservedNotebookBindingName(name)) {
|
|
211
346
|
throw new typeErrorConstructor(`CodeMode Notebook Binding '${name}' is reserved`);
|
|
@@ -398,6 +533,35 @@ function utf8ByteLength(value: string): number {
|
|
|
398
533
|
return encodeUtf8(value).byteLength;
|
|
399
534
|
}
|
|
400
535
|
|
|
536
|
+
function utf8JsonStringByteLength(value: string): number {
|
|
537
|
+
let bytes = 2;
|
|
538
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
539
|
+
const codeUnit = value.charCodeAt(index);
|
|
540
|
+
if (codeUnit === 0x22 || codeUnit === 0x5c) bytes += 2;
|
|
541
|
+
else if (codeUnit <= 0x1f) {
|
|
542
|
+
bytes +=
|
|
543
|
+
codeUnit === 0x08 ||
|
|
544
|
+
codeUnit === 0x09 ||
|
|
545
|
+
codeUnit === 0x0a ||
|
|
546
|
+
codeUnit === 0x0c ||
|
|
547
|
+
codeUnit === 0x0d
|
|
548
|
+
? 2
|
|
549
|
+
: 6;
|
|
550
|
+
} else if (codeUnit <= 0x7f) bytes += 1;
|
|
551
|
+
else if (codeUnit <= 0x7ff) bytes += 2;
|
|
552
|
+
else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
553
|
+
const lowSurrogate = value.charCodeAt(index + 1);
|
|
554
|
+
if (lowSurrogate >= 0xdc00 && lowSurrogate <= 0xdfff) {
|
|
555
|
+
bytes += 4;
|
|
556
|
+
index += 1;
|
|
557
|
+
} else bytes += 6;
|
|
558
|
+
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) bytes += 6;
|
|
559
|
+
else bytes += 3;
|
|
560
|
+
if (bytes >= CODEMODE_WORKER_MESSAGE_LIMIT_BYTES) return bytes;
|
|
561
|
+
}
|
|
562
|
+
return bytes;
|
|
563
|
+
}
|
|
564
|
+
|
|
401
565
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: This is arbitrary guest ingress; descriptor-only traversal avoids invoking accessors, coercion, and guest-mutated methods. Coordinator hostile JSON tests cover the boundary.
|
|
402
566
|
function serializeGuestJson(value: unknown, allowUndefined: boolean): string | undefined {
|
|
403
567
|
const seen: object[] = [];
|
|
@@ -715,6 +879,50 @@ defineProperty(globalThis, "tools", {
|
|
|
715
879
|
value: tools,
|
|
716
880
|
writable: false,
|
|
717
881
|
});
|
|
882
|
+
const guestConsole = createObject(null);
|
|
883
|
+
for (const method of CODEMODE_CONSOLE_METHODS) {
|
|
884
|
+
defineProperty(guestConsole, method, {
|
|
885
|
+
configurable: false,
|
|
886
|
+
enumerable: true,
|
|
887
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Cell Console calls accept arbitrary guest values; formatGuestConsoleArguments safely inspects them before capture.
|
|
888
|
+
value: (...args: unknown[]): undefined => {
|
|
889
|
+
const cell = activeCell;
|
|
890
|
+
if (cell !== undefined && !cell.consoleOverflow) {
|
|
891
|
+
const text = formatGuestConsoleArguments(args);
|
|
892
|
+
if (text === undefined) {
|
|
893
|
+
cell.consoleEntries.length = 0;
|
|
894
|
+
cell.consoleOverflow = true;
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
const entryBytes = 19 + utf8JsonStringByteLength(method) + utf8JsonStringByteLength(text);
|
|
898
|
+
const nextConsoleBytes =
|
|
899
|
+
cell.consoleBytes + (cell.consoleEntries.length === 0 ? 0 : 1) + entryBytes;
|
|
900
|
+
if (
|
|
901
|
+
nextConsoleBytes + CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
|
|
902
|
+
CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
|
|
903
|
+
) {
|
|
904
|
+
cell.consoleEntries.length = 0;
|
|
905
|
+
cell.consoleOverflow = true;
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
cell.consoleEntries[cell.consoleEntries.length] = {
|
|
909
|
+
method,
|
|
910
|
+
text,
|
|
911
|
+
};
|
|
912
|
+
cell.consoleBytes = nextConsoleBytes;
|
|
913
|
+
}
|
|
914
|
+
return undefined;
|
|
915
|
+
},
|
|
916
|
+
writable: false,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
objectFreeze(guestConsole);
|
|
920
|
+
defineProperty(globalThis, "console", {
|
|
921
|
+
configurable: false,
|
|
922
|
+
enumerable: false,
|
|
923
|
+
value: guestConsole,
|
|
924
|
+
writable: false,
|
|
925
|
+
});
|
|
718
926
|
|
|
719
927
|
function disableGuestGlobal(name: string, value?: Readonly<typeof safeDenoIdentity>): void {
|
|
720
928
|
const descriptor = getOwnPropertyDescriptor(globalThis, name);
|
|
@@ -770,7 +978,6 @@ const safeDenoIdentity = objectFreeze({
|
|
|
770
978
|
disableGuestGlobal("Deno", safeDenoIdentity);
|
|
771
979
|
for (const unsafeGlobal of [
|
|
772
980
|
"process",
|
|
773
|
-
"console",
|
|
774
981
|
"alert",
|
|
775
982
|
"confirm",
|
|
776
983
|
"prompt",
|
|
@@ -889,42 +1096,97 @@ function scheduleCellFinish(cell: ActiveWorkerCell): void {
|
|
|
889
1096
|
return;
|
|
890
1097
|
}
|
|
891
1098
|
activeCell = undefined;
|
|
1099
|
+
const consoleEntries = cell.consoleEntries.length === 0 ? undefined : [...cell.consoleEntries];
|
|
892
1100
|
let response: CodeModeWorkerResponse;
|
|
893
|
-
if (cell.
|
|
894
|
-
const error = describeGuestError(cell.mainError);
|
|
895
|
-
const serializationFailure =
|
|
896
|
-
isGuestReference(cell.mainError) &&
|
|
897
|
-
(hasSerializationErrorInstance(cell.mainError) ||
|
|
898
|
-
internalToolErrorCode(cell.mainError) === "serialization");
|
|
1101
|
+
if (cell.consoleOverflow) {
|
|
899
1102
|
response = {
|
|
900
1103
|
version: 1,
|
|
901
1104
|
type: "cell-error",
|
|
902
1105
|
sessionId: cell.sessionId,
|
|
903
1106
|
cellId: cell.cellId,
|
|
904
|
-
error: {
|
|
905
|
-
code: serializationFailure ? "serialization" : "script",
|
|
906
|
-
message: renderGuestError(error),
|
|
907
|
-
},
|
|
1107
|
+
error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
|
|
908
1108
|
};
|
|
909
|
-
} else {
|
|
910
|
-
|
|
911
|
-
|
|
1109
|
+
} else if (cell.mainFailed) {
|
|
1110
|
+
const error = describeGuestError(cell.mainError);
|
|
1111
|
+
const serializationFailure =
|
|
1112
|
+
isGuestReference(cell.mainError) &&
|
|
1113
|
+
(hasSerializationErrorInstance(cell.mainError) ||
|
|
1114
|
+
internalToolErrorCode(cell.mainError) === "serialization");
|
|
1115
|
+
const message = renderGuestError(error);
|
|
1116
|
+
if (
|
|
1117
|
+
consoleEntries !== undefined &&
|
|
1118
|
+
cell.consoleBytes +
|
|
1119
|
+
utf8JsonStringByteLength(message) +
|
|
1120
|
+
CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
|
|
1121
|
+
CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
|
|
1122
|
+
) {
|
|
1123
|
+
response = {
|
|
1124
|
+
version: 1,
|
|
1125
|
+
type: "cell-error",
|
|
1126
|
+
sessionId: cell.sessionId,
|
|
1127
|
+
cellId: cell.cellId,
|
|
1128
|
+
error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
|
|
1129
|
+
};
|
|
1130
|
+
} else {
|
|
912
1131
|
const responseBase = {
|
|
913
1132
|
version: 1,
|
|
914
|
-
type: "cell-
|
|
1133
|
+
type: "cell-error",
|
|
915
1134
|
sessionId: cell.sessionId,
|
|
916
1135
|
cellId: cell.cellId,
|
|
1136
|
+
error: {
|
|
1137
|
+
code: serializationFailure ? "serialization" : "script",
|
|
1138
|
+
message,
|
|
1139
|
+
},
|
|
917
1140
|
} as const;
|
|
918
|
-
response =
|
|
1141
|
+
response =
|
|
1142
|
+
consoleEntries === undefined
|
|
1143
|
+
? responseBase
|
|
1144
|
+
: { ...responseBase, console: consoleEntries };
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
try {
|
|
1148
|
+
const resultJson = serializeGuestJson(cell.mainResult, true);
|
|
1149
|
+
if (
|
|
1150
|
+
consoleEntries !== undefined &&
|
|
1151
|
+
cell.consoleBytes +
|
|
1152
|
+
(resultJson === undefined ? 0 : utf8JsonStringByteLength(resultJson)) +
|
|
1153
|
+
CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
|
|
1154
|
+
CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
|
|
1155
|
+
) {
|
|
1156
|
+
response = {
|
|
1157
|
+
version: 1,
|
|
1158
|
+
type: "cell-error",
|
|
1159
|
+
sessionId: cell.sessionId,
|
|
1160
|
+
cellId: cell.cellId,
|
|
1161
|
+
error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
|
|
1162
|
+
};
|
|
1163
|
+
} else {
|
|
1164
|
+
const responseBase = {
|
|
1165
|
+
version: 1,
|
|
1166
|
+
type: "cell-result",
|
|
1167
|
+
sessionId: cell.sessionId,
|
|
1168
|
+
cellId: cell.cellId,
|
|
1169
|
+
} as const;
|
|
1170
|
+
const resultResponse =
|
|
1171
|
+
resultJson === undefined ? responseBase : { ...responseBase, resultJson };
|
|
1172
|
+
response =
|
|
1173
|
+
consoleEntries === undefined
|
|
1174
|
+
? resultResponse
|
|
1175
|
+
: { ...resultResponse, console: consoleEntries };
|
|
1176
|
+
}
|
|
919
1177
|
} catch (cause) {
|
|
920
1178
|
const error = describeGuestError(cause);
|
|
921
|
-
|
|
1179
|
+
const responseBase = {
|
|
922
1180
|
version: 1,
|
|
923
1181
|
type: "cell-error",
|
|
924
1182
|
sessionId: cell.sessionId,
|
|
925
1183
|
cellId: cell.cellId,
|
|
926
1184
|
error: { code: "serialization", message: renderGuestError(error) },
|
|
927
|
-
};
|
|
1185
|
+
} as const;
|
|
1186
|
+
response =
|
|
1187
|
+
consoleEntries === undefined
|
|
1188
|
+
? responseBase
|
|
1189
|
+
: { ...responseBase, console: consoleEntries };
|
|
928
1190
|
}
|
|
929
1191
|
}
|
|
930
1192
|
void enqueueWorkerResponse(response);
|
|
@@ -939,6 +1201,9 @@ function startWorkerCell(
|
|
|
939
1201
|
sessionId: request.sessionId,
|
|
940
1202
|
cellId: request.cellId,
|
|
941
1203
|
pendingCalls: [],
|
|
1204
|
+
consoleEntries: [],
|
|
1205
|
+
consoleBytes: 2,
|
|
1206
|
+
consoleOverflow: false,
|
|
942
1207
|
batchSequence: 0,
|
|
943
1208
|
callSequence: 0,
|
|
944
1209
|
batchScheduled: false,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
} from "./pi-tool-bridge.js";
|
|
40
40
|
|
|
41
41
|
const CODEMODE_EXECUTE_DESCRIPTION =
|
|
42
|
-
"Execute a TypeScript Cell in a persistent isolated Deno CodeMode Session. Reuse a Session ID to retain Notebook Bindings; a new Session reclaims the least-recently-used idle Session at capacity. Use the read-only tools object for registered Pi tools.";
|
|
42
|
+
"Execute a TypeScript Cell in a persistent isolated Deno CodeMode Session. Reuse a Session ID to retain Notebook Bindings; a new Session reclaims the least-recently-used idle Session at capacity. Use the read-only tools object for registered Pi tools. Return final result data with a top-level return statement. Reserve console.log, console.info, console.warn, console.error, and console.debug for diagnostics; captured output arrives only with terminal results.";
|
|
43
43
|
|
|
44
44
|
type PiCodeModeGeneration = {
|
|
45
45
|
readonly captured: CapturedPiAgentSession;
|
|
@@ -126,11 +126,11 @@ class PiCodeModeLifecycleController {
|
|
|
126
126
|
this.pi.on("session_start", async (_event, context) => this.startSession(context));
|
|
127
127
|
this.pi.on("before_agent_start", () => this.synchronizeCurrentGeneration());
|
|
128
128
|
this.pi.on("tool_execution_end", () => this.synchronizeCurrentGeneration());
|
|
129
|
-
this.pi.on("session_shutdown", async (
|
|
129
|
+
this.pi.on("session_shutdown", async () => this.shutdownSession());
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
private async startSession(context: ExtensionContext): Promise<void> {
|
|
133
|
-
await this.shutdownSession(
|
|
133
|
+
await this.shutdownSession();
|
|
134
134
|
const capturedResult = capturePiAgentSession(this.pi);
|
|
135
135
|
if (!capturedResult.ok) {
|
|
136
136
|
this.notifyWarning(context, capturedResult.warning);
|
|
@@ -247,7 +247,7 @@ class PiCodeModeLifecycleController {
|
|
|
247
247
|
} catch {
|
|
248
248
|
// Observer cleanup is presentation-only; execution resources still require release.
|
|
249
249
|
}
|
|
250
|
-
await coordinator.shutdown(
|
|
250
|
+
await coordinator.shutdown();
|
|
251
251
|
await sessionFiles.close();
|
|
252
252
|
if (this.generation === generation) this.generation = undefined;
|
|
253
253
|
this.notifyWarning(
|
|
@@ -389,12 +389,8 @@ class PiCodeModeLifecycleController {
|
|
|
389
389
|
now: CODEMODE_SYSTEM_RUNTIME.now,
|
|
390
390
|
signal: AbortSignal.any([batch.signal, terminationController.signal]),
|
|
391
391
|
onTerminate: () => terminationController.abort(),
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
Object.assign(bridgeOptions, { outerAssistantMessage });
|
|
395
|
-
}
|
|
396
|
-
if (batch.onUpdate !== undefined) {
|
|
397
|
-
Object.assign(bridgeOptions, {
|
|
392
|
+
...(outerAssistantMessage !== undefined && { outerAssistantMessage }),
|
|
393
|
+
...(batch.onUpdate !== undefined && {
|
|
398
394
|
onUpdate: (_callId: string, update: AgentToolResult<unknown>) => {
|
|
399
395
|
const outerUpdate: AgentToolResult<CodeModeResultDetails> = {
|
|
400
396
|
content: update.content,
|
|
@@ -402,8 +398,8 @@ class PiCodeModeLifecycleController {
|
|
|
402
398
|
};
|
|
403
399
|
batch.onUpdate?.(outerUpdate);
|
|
404
400
|
},
|
|
405
|
-
})
|
|
406
|
-
}
|
|
401
|
+
}),
|
|
402
|
+
};
|
|
407
403
|
const bridged = await executePiToolBridgeBatch(bridgeCaptured, bridgeOptions);
|
|
408
404
|
const bridgedResults = new Map<string, CodeModeNestedToolResult>(
|
|
409
405
|
bridged.calls.map((outcome) => [
|
|
@@ -431,16 +427,16 @@ class PiCodeModeLifecycleController {
|
|
|
431
427
|
"Pi CodeMode nested tool returned no result",
|
|
432
428
|
),
|
|
433
429
|
);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
430
|
+
return {
|
|
431
|
+
results,
|
|
432
|
+
presentation: bridged.presentation,
|
|
433
|
+
...(bridged.usage !== undefined && { usage: bridged.usage }),
|
|
434
|
+
...(bridged.addedToolNames.length > 0 && { addedToolNames: bridged.addedToolNames }),
|
|
435
|
+
...(bridged.terminate && { terminate: true }),
|
|
436
|
+
};
|
|
441
437
|
}
|
|
442
438
|
|
|
443
|
-
private async shutdownSession(
|
|
439
|
+
private async shutdownSession(): Promise<void> {
|
|
444
440
|
const generation = this.generation;
|
|
445
441
|
if (generation === undefined || !generation.active) return;
|
|
446
442
|
generation.active = false;
|
|
@@ -450,7 +446,7 @@ class PiCodeModeLifecycleController {
|
|
|
450
446
|
// Observer cleanup is presentation-only; execution resources still require release.
|
|
451
447
|
}
|
|
452
448
|
try {
|
|
453
|
-
await generation.coordinator.shutdown(
|
|
449
|
+
await generation.coordinator.shutdown();
|
|
454
450
|
} finally {
|
|
455
451
|
try {
|
|
456
452
|
await generation.sessionFiles.close();
|
package/src/pi-tool-bridge.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
parseCodeModeJsonValue,
|
|
13
13
|
type CodeModeJsonValue,
|
|
14
14
|
} from "./codemode-tool-contract.js";
|
|
15
|
+
import { addCodeModeUsage, boundedCodeModeElapsedMs } from "./codemode-session-coordinator.js";
|
|
15
16
|
import type { CapturedPiAgentSession } from "./pi-agent-session-capture.js";
|
|
16
17
|
|
|
17
18
|
const PI_TOOL_BRIDGE_RESULT_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
@@ -505,36 +506,6 @@ function translatePiToolResult(
|
|
|
505
506
|
};
|
|
506
507
|
}
|
|
507
508
|
|
|
508
|
-
function addUsage(left: Usage | undefined, right: Usage | undefined): Usage | undefined {
|
|
509
|
-
if (right === undefined) return left;
|
|
510
|
-
if (left === undefined) return right;
|
|
511
|
-
const reasoning =
|
|
512
|
-
left.reasoning === undefined && right.reasoning === undefined
|
|
513
|
-
? undefined
|
|
514
|
-
: (left.reasoning ?? 0) + (right.reasoning ?? 0);
|
|
515
|
-
const cacheWrite1h =
|
|
516
|
-
left.cacheWrite1h === undefined && right.cacheWrite1h === undefined
|
|
517
|
-
? undefined
|
|
518
|
-
: (left.cacheWrite1h ?? 0) + (right.cacheWrite1h ?? 0);
|
|
519
|
-
const combined: Usage = {
|
|
520
|
-
input: left.input + right.input,
|
|
521
|
-
output: left.output + right.output,
|
|
522
|
-
cacheRead: left.cacheRead + right.cacheRead,
|
|
523
|
-
cacheWrite: left.cacheWrite + right.cacheWrite,
|
|
524
|
-
totalTokens: left.totalTokens + right.totalTokens,
|
|
525
|
-
cost: {
|
|
526
|
-
input: left.cost.input + right.cost.input,
|
|
527
|
-
output: left.cost.output + right.cost.output,
|
|
528
|
-
cacheRead: left.cost.cacheRead + right.cost.cacheRead,
|
|
529
|
-
cacheWrite: left.cost.cacheWrite + right.cost.cacheWrite,
|
|
530
|
-
total: left.cost.total + right.cost.total,
|
|
531
|
-
},
|
|
532
|
-
};
|
|
533
|
-
if (cacheWrite1h !== undefined) combined.cacheWrite1h = cacheWrite1h;
|
|
534
|
-
if (reasoning !== undefined) combined.reasoning = reasoning;
|
|
535
|
-
return combined;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
509
|
function terminatedPiToolCall(callId: string): FinalizedPiToolCall {
|
|
539
510
|
return createBridgeFailure(
|
|
540
511
|
callId,
|
|
@@ -553,12 +524,6 @@ function presentationOutcome(
|
|
|
553
524
|
: "failed";
|
|
554
525
|
}
|
|
555
526
|
|
|
556
|
-
function elapsedMilliseconds(startedAt: number, finishedAt: number): number {
|
|
557
|
-
const elapsed = Math.round(finishedAt - startedAt);
|
|
558
|
-
if (!Number.isFinite(elapsed)) return 0;
|
|
559
|
-
return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed));
|
|
560
|
-
}
|
|
561
|
-
|
|
562
527
|
function timedFinalizedPiToolCall(
|
|
563
528
|
call: PiToolBridgeCall,
|
|
564
529
|
finalized: FinalizedPiToolCall,
|
|
@@ -569,7 +534,7 @@ function timedFinalizedPiToolCall(
|
|
|
569
534
|
finalized,
|
|
570
535
|
presentation: {
|
|
571
536
|
callId: call.callId,
|
|
572
|
-
elapsedMs:
|
|
537
|
+
elapsedMs: boundedCodeModeElapsedMs(startedAt, now()),
|
|
573
538
|
name: call.name,
|
|
574
539
|
outcome: presentationOutcome(finalized.outcome),
|
|
575
540
|
},
|
|
@@ -590,7 +555,9 @@ function collectPiToolBridgeBatch(
|
|
|
590
555
|
const addedToolNames: string[] = [];
|
|
591
556
|
const seenToolNames = new Set<string>();
|
|
592
557
|
for (const { finalized } of timedCalls) {
|
|
593
|
-
|
|
558
|
+
if (finalized.usage !== undefined) {
|
|
559
|
+
usage = addCodeModeUsage(usage, finalized.usage);
|
|
560
|
+
}
|
|
594
561
|
for (const toolName of finalized.addedToolNames) {
|
|
595
562
|
if (seenToolNames.has(toolName)) continue;
|
|
596
563
|
seenToolNames.add(toolName);
|