@code-yeongyu/senpi-codemode 2026.7.25-2
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/CHANGELOG.md +250 -0
- package/LICENSE +22 -0
- package/README.md +161 -0
- package/package.json +58 -0
- package/src/bridge/http-server.ts +236 -0
- package/src/bridge/protocol.ts +198 -0
- package/src/bridge/reserved.ts +9 -0
- package/src/bridges/agent-bridge.ts +197 -0
- package/src/bridges/output-bridge.ts +96 -0
- package/src/bridges/schema-injection.ts +3 -0
- package/src/codemode/runtime.ts +258 -0
- package/src/codemode/tools.ts +106 -0
- package/src/completion/handler.ts +192 -0
- package/src/completion/tool-bridge.ts +55 -0
- package/src/config/settings.ts +215 -0
- package/src/extension/runtime-factory.ts +114 -0
- package/src/extension/session-manager-proxy.ts +116 -0
- package/src/extension/session-manager.ts +215 -0
- package/src/host-sdk.ts +1 -0
- package/src/index.ts +181 -0
- package/src/interpreters/detect.ts +161 -0
- package/src/kernels/jl/kernel.ts +37 -0
- package/src/kernels/jl/prelude.jl +283 -0
- package/src/kernels/jl/runner.jl +327 -0
- package/src/kernels/js/context-manager.ts +296 -0
- package/src/kernels/js/inline-worker-entry.js +23 -0
- package/src/kernels/js/inline-worker.ts +15 -0
- package/src/kernels/js/kernel-contract.ts +38 -0
- package/src/kernels/js/local-module-loader.ts +108 -0
- package/src/kernels/js/prelude.ts +15 -0
- package/src/kernels/js/rewrite-imports.ts +164 -0
- package/src/kernels/js/run-queue.ts +82 -0
- package/src/kernels/js/worker-core.d.ts +18 -0
- package/src/kernels/js/worker-core.js +94 -0
- package/src/kernels/js/worker-entry.js +23 -0
- package/src/kernels/js/worker-host.ts +117 -0
- package/src/kernels/js/worker-indirect-eval.js +88 -0
- package/src/kernels/js/worker-runtime.js +401 -0
- package/src/kernels/py/kernel-contract.ts +32 -0
- package/src/kernels/py/kernel.ts +290 -0
- package/src/kernels/py/prelude.py +954 -0
- package/src/kernels/py/process.ts +119 -0
- package/src/kernels/py/transport.ts +237 -0
- package/src/kernels/rb/kernel.ts +26 -0
- package/src/kernels/rb/prelude.rb +270 -0
- package/src/kernels/rb/runner.rb +204 -0
- package/src/kernels/shared/subprocess-contract.ts +22 -0
- package/src/kernels/shared/subprocess-kernel.ts +266 -0
- package/src/kernels/shared/subprocess-process.ts +174 -0
- package/src/kernels/shared/subprocess-queue.ts +101 -0
- package/src/kernels/shared/subprocess-run.ts +98 -0
- package/src/output/output-meta.ts +89 -0
- package/src/output/streaming-output.ts +296 -0
- package/src/prompt/eval-prompt.ts +319 -0
- package/src/timeouts/bridge-timeout.ts +16 -0
- package/src/timeouts/idle-timeout.ts +84 -0
- package/src/tool/cell-handler.ts +279 -0
- package/src/tool/eval-tool.ts +285 -0
- package/src/tool/image.ts +274 -0
- package/src/tool/json-tree.ts +247 -0
- package/src/tool/render.ts +876 -0
- package/src/tool/status-events.ts +12 -0
- package/src/tool/types.ts +114 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
|
|
3
|
+
import {
|
|
4
|
+
assertJavaScriptKernelOpen,
|
|
5
|
+
type JavaScriptKernelMode,
|
|
6
|
+
type JavaScriptRunInput,
|
|
7
|
+
type LifecycleState,
|
|
8
|
+
type ResultMessage,
|
|
9
|
+
type ToolCallMessage,
|
|
10
|
+
} from "./kernel-contract.ts";
|
|
11
|
+
import { type JavaScriptKernelOptions, LocalModuleLoader, localBridgeConnection } from "./local-module-loader.ts";
|
|
12
|
+
import { JavaScriptRunQueue, type PendingJavaScriptRun, stoppedResult } from "./run-queue.ts";
|
|
13
|
+
import { bridgeError, spawnNodeWorker, WorkerStartupCancelledError, waitForReady } from "./worker-host.ts";
|
|
14
|
+
|
|
15
|
+
export { JavaScriptKernelClosedError, type JavaScriptKernelMode, type JavaScriptRunInput } from "./kernel-contract.ts";
|
|
16
|
+
export type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
17
|
+
|
|
18
|
+
export class JavaScriptKernel {
|
|
19
|
+
readonly #options: JavaScriptKernelOptions;
|
|
20
|
+
readonly #moduleLoader: LocalModuleLoader;
|
|
21
|
+
#worker: WorkerLike | null = null;
|
|
22
|
+
#mode: JavaScriptKernelMode = "worker";
|
|
23
|
+
#lifecycle: LifecycleState = "open";
|
|
24
|
+
#ready: Promise<void> | null = null;
|
|
25
|
+
#startupAbort: AbortController | null = null;
|
|
26
|
+
#generation = 0;
|
|
27
|
+
#activation: Promise<void> | null = null;
|
|
28
|
+
#recovery: Promise<void> | null = null;
|
|
29
|
+
#closePromise: Promise<void> | null = null;
|
|
30
|
+
readonly #runs = new JavaScriptRunQueue();
|
|
31
|
+
#timeout: NodeJS.Timeout | null = null;
|
|
32
|
+
#toolWaiters: Array<(message: ToolCallMessage) => void> = [];
|
|
33
|
+
#pendingToolCalls: ToolCallMessage[] = [];
|
|
34
|
+
|
|
35
|
+
constructor(options: JavaScriptKernelOptions) {
|
|
36
|
+
this.#options = options;
|
|
37
|
+
this.#moduleLoader = new LocalModuleLoader(options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get mode(): JavaScriptKernelMode {
|
|
41
|
+
return this.#mode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async run(input: JavaScriptRunInput): Promise<ResultMessage> {
|
|
45
|
+
assertJavaScriptKernelOpen(this.#lifecycle, "run");
|
|
46
|
+
const promise = this.#runs.enqueue(input);
|
|
47
|
+
this.#activate();
|
|
48
|
+
return await promise;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async interrupt(reason = "interrupted"): Promise<void> {
|
|
52
|
+
assertJavaScriptKernelOpen(this.#lifecycle, "interrupt");
|
|
53
|
+
const active = this.#runs.active;
|
|
54
|
+
const target = this.#runs.takeInterruptTarget();
|
|
55
|
+
if (!target) return;
|
|
56
|
+
if (target === active) this.#clearTimeout();
|
|
57
|
+
this.#runs.settle(target, stoppedResult(target.input.cellId, `JS cell interrupted: ${reason}`));
|
|
58
|
+
await this.#restartAfterStop();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async reset(): Promise<void> {
|
|
62
|
+
assertJavaScriptKernelOpen(this.#lifecycle, "reset");
|
|
63
|
+
await this.#terminate();
|
|
64
|
+
assertJavaScriptKernelOpen(this.#lifecycle, "reset");
|
|
65
|
+
await this.#ensureReady();
|
|
66
|
+
this.#startNext();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
deliverToolReply(message: Extract<HostToKernelMessage, { type: "tool-reply" }>): void {
|
|
70
|
+
if (this.#lifecycle === "open") this.#worker?.postMessage(message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async nextToolCall(): Promise<ToolCallMessage> {
|
|
74
|
+
const pending = this.#pendingToolCalls.shift();
|
|
75
|
+
if (pending) return pending;
|
|
76
|
+
return await new Promise((resolve) => this.#toolWaiters.push(resolve));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async close(): Promise<void> {
|
|
80
|
+
if (this.#closePromise) return await this.#closePromise;
|
|
81
|
+
this.#worker?.postMessage({ type: "close" });
|
|
82
|
+
this.#lifecycle = "closing";
|
|
83
|
+
this.#runs.settleAll("JS kernel closed");
|
|
84
|
+
const recovery = this.#recovery;
|
|
85
|
+
const closePromise = (async () => {
|
|
86
|
+
if (recovery) await recovery;
|
|
87
|
+
await this.#terminate();
|
|
88
|
+
})().finally(() => {
|
|
89
|
+
this.#lifecycle = "closed";
|
|
90
|
+
});
|
|
91
|
+
this.#closePromise = closePromise;
|
|
92
|
+
return await closePromise;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#activate(): void {
|
|
96
|
+
if (this.#activation || this.#lifecycle !== "open" || this.#runs.active || !this.#runs.hasWaiting) return;
|
|
97
|
+
const activation = this.#activateWhenReady();
|
|
98
|
+
this.#activation = activation;
|
|
99
|
+
void activation.then(() => {
|
|
100
|
+
if (this.#activation === activation) this.#activation = null;
|
|
101
|
+
if (this.#lifecycle === "open" && !this.#runs.active && this.#runs.hasWaiting) this.#activate();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async #activateWhenReady(): Promise<void> {
|
|
106
|
+
try {
|
|
107
|
+
await this.#ensureReady();
|
|
108
|
+
if (this.#lifecycle === "open") this.#startNext();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error instanceof WorkerStartupCancelledError) return;
|
|
111
|
+
this.#runs.rejectWaiting(error instanceof Error ? error : new Error(String(error)));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async #ensureReady(): Promise<void> {
|
|
116
|
+
assertJavaScriptKernelOpen(this.#lifecycle, "run");
|
|
117
|
+
if (!this.#ready) {
|
|
118
|
+
const generation = ++this.#generation;
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
this.#startupAbort = controller;
|
|
121
|
+
const ready = this.#startWorker(generation, controller.signal);
|
|
122
|
+
this.#ready = ready;
|
|
123
|
+
void ready.then(
|
|
124
|
+
() => {
|
|
125
|
+
if (this.#ready === ready) this.#startupAbort = null;
|
|
126
|
+
},
|
|
127
|
+
() => {
|
|
128
|
+
if (this.#ready === ready) {
|
|
129
|
+
this.#ready = null;
|
|
130
|
+
this.#startupAbort = null;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return await this.#ready;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async #startWorker(generation: number, signal: AbortSignal): Promise<void> {
|
|
139
|
+
let worker = this.#spawnWorker();
|
|
140
|
+
this.#publishWorker(worker, generation);
|
|
141
|
+
try {
|
|
142
|
+
await this.#initializeWorker(worker, signal);
|
|
143
|
+
return;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (!this.#isCurrent(worker, generation) || error instanceof WorkerStartupCancelledError) {
|
|
146
|
+
await worker.terminate();
|
|
147
|
+
throw new WorkerStartupCancelledError();
|
|
148
|
+
}
|
|
149
|
+
if (worker.mode === "inline") throw error;
|
|
150
|
+
this.#worker = null;
|
|
151
|
+
await worker.terminate();
|
|
152
|
+
}
|
|
153
|
+
if (this.#lifecycle !== "open" || generation !== this.#generation) throw new WorkerStartupCancelledError();
|
|
154
|
+
worker = createInlineWorker(this.#options.cwd, this.#options.parallelPoolWidth);
|
|
155
|
+
this.#publishWorker(worker, generation);
|
|
156
|
+
await this.#initializeWorker(worker, signal);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#spawnWorker(): WorkerLike {
|
|
160
|
+
try {
|
|
161
|
+
const url = this.#options.workerEntryUrl ?? new URL("./worker-entry.js", import.meta.url);
|
|
162
|
+
return spawnNodeWorker(url, this.#options.cwd, this.#options.parallelPoolWidth);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (!(error instanceof Error)) throw error;
|
|
165
|
+
return createInlineWorker(this.#options.cwd, this.#options.parallelPoolWidth);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#publishWorker(worker: WorkerLike, generation: number): void {
|
|
170
|
+
if (this.#lifecycle !== "open" || generation !== this.#generation) throw new WorkerStartupCancelledError();
|
|
171
|
+
this.#worker = worker;
|
|
172
|
+
this.#mode = worker.mode;
|
|
173
|
+
worker.onMessage((message) => {
|
|
174
|
+
if (this.#isCurrent(worker, generation)) this.#handleMessage(message);
|
|
175
|
+
});
|
|
176
|
+
worker.onError((error) => {
|
|
177
|
+
if (this.#isCurrent(worker, generation)) this.#handleCrash(error);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async #initializeWorker(worker: WorkerLike, signal: AbortSignal): Promise<void> {
|
|
182
|
+
const ready = waitForReady(worker, signal);
|
|
183
|
+
worker.postMessage({
|
|
184
|
+
type: "init",
|
|
185
|
+
sessionId: this.#options.sessionId,
|
|
186
|
+
connection: localBridgeConnection(this.#options),
|
|
187
|
+
});
|
|
188
|
+
await ready;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#isCurrent(worker: WorkerLike, generation: number): boolean {
|
|
192
|
+
return this.#lifecycle === "open" && this.#worker === worker && this.#generation === generation;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#startNext(): void {
|
|
196
|
+
if (this.#lifecycle !== "open" || this.#runs.active || !this.#worker) return;
|
|
197
|
+
const next = this.#runs.startNext(performance.now());
|
|
198
|
+
if (!next) return;
|
|
199
|
+
if (next.input.timeoutMs) {
|
|
200
|
+
this.#timeout = setTimeout(() => void this.#timeoutActive(next), next.input.timeoutMs);
|
|
201
|
+
}
|
|
202
|
+
this.#worker.postMessage({
|
|
203
|
+
type: "run",
|
|
204
|
+
cellId: next.input.cellId,
|
|
205
|
+
code: this.#moduleLoader.prepareCell(next.input.code),
|
|
206
|
+
timeoutMs: next.input.timeoutMs,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async #timeoutActive(run: PendingJavaScriptRun): Promise<void> {
|
|
211
|
+
if (!this.#runs.releaseActive(run)) return;
|
|
212
|
+
const durationMs = run.input.timeoutMs ?? 0;
|
|
213
|
+
this.#runs.settle(run, {
|
|
214
|
+
type: "result",
|
|
215
|
+
cellId: run.input.cellId,
|
|
216
|
+
ok: false,
|
|
217
|
+
error: { message: `JS cell timed out after ${durationMs}ms` },
|
|
218
|
+
durationMs,
|
|
219
|
+
});
|
|
220
|
+
await this.#restartAfterStop();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async #restartAfterStop(): Promise<void> {
|
|
224
|
+
if (this.#recovery) return await this.#recovery;
|
|
225
|
+
const recovery = this.#performRestartAfterStop();
|
|
226
|
+
this.#recovery = recovery;
|
|
227
|
+
try {
|
|
228
|
+
await recovery;
|
|
229
|
+
} finally {
|
|
230
|
+
if (this.#recovery === recovery) this.#recovery = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async #performRestartAfterStop(): Promise<void> {
|
|
235
|
+
try {
|
|
236
|
+
await this.#terminate();
|
|
237
|
+
if (this.#lifecycle !== "open") return;
|
|
238
|
+
await this.#ensureReady();
|
|
239
|
+
if (this.#lifecycle === "open") this.#startNext();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error instanceof WorkerStartupCancelledError) return;
|
|
242
|
+
this.#runs.rejectWaiting(error instanceof Error ? error : new Error(String(error)));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#handleMessage(message: KernelToHostMessage): void {
|
|
247
|
+
this.#options.onMessage?.(message);
|
|
248
|
+
this.#runs.active?.input.onMessage?.(message);
|
|
249
|
+
if (message.type === "tool-call") {
|
|
250
|
+
const waiter = this.#toolWaiters.shift();
|
|
251
|
+
if (waiter) waiter(message);
|
|
252
|
+
else this.#pendingToolCalls.push(message);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (message.type !== "result") return;
|
|
256
|
+
const active = this.#runs.active;
|
|
257
|
+
if (!active || active.input.cellId !== message.cellId) return;
|
|
258
|
+
this.#clearTimeout();
|
|
259
|
+
this.#runs.releaseActive(active);
|
|
260
|
+
this.#runs.settle(active, message);
|
|
261
|
+
this.#startNext();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#handleCrash(error: Error): void {
|
|
265
|
+
const active = this.#runs.active;
|
|
266
|
+
if (!active && this.#startupAbort) return;
|
|
267
|
+
this.#clearTimeout();
|
|
268
|
+
if (active) {
|
|
269
|
+
this.#runs.releaseActive(active);
|
|
270
|
+
this.#runs.settle(active, {
|
|
271
|
+
type: "result",
|
|
272
|
+
cellId: active.input.cellId,
|
|
273
|
+
ok: false,
|
|
274
|
+
error: bridgeError(error),
|
|
275
|
+
durationMs: this.#runs.durationMs(active, performance.now()),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
void this.#restartAfterStop();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#clearTimeout(): void {
|
|
282
|
+
if (this.#timeout) clearTimeout(this.#timeout);
|
|
283
|
+
this.#timeout = null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async #terminate(): Promise<void> {
|
|
287
|
+
this.#clearTimeout();
|
|
288
|
+
this.#generation += 1;
|
|
289
|
+
this.#startupAbort?.abort();
|
|
290
|
+
this.#startupAbort = null;
|
|
291
|
+
this.#ready = null;
|
|
292
|
+
const worker = this.#worker;
|
|
293
|
+
this.#worker = null;
|
|
294
|
+
if (worker) await worker.terminate();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { createWorkerCore } from "./worker-core.js";
|
|
3
|
+
|
|
4
|
+
if (!parentPort) throw new Error("JS kernel inline fallback worker missing parentPort");
|
|
5
|
+
|
|
6
|
+
const transport = {
|
|
7
|
+
send(message) {
|
|
8
|
+
parentPort.postMessage(message);
|
|
9
|
+
},
|
|
10
|
+
onMessage(handler) {
|
|
11
|
+
parentPort.on("message", handler);
|
|
12
|
+
return () => parentPort.off("message", handler);
|
|
13
|
+
},
|
|
14
|
+
close() {
|
|
15
|
+
parentPort.close();
|
|
16
|
+
setTimeout(() => process.exit(0), 0);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
createWorkerCore(transport, {
|
|
21
|
+
cwd: workerData.cwd,
|
|
22
|
+
parallelPoolWidth: workerData.parallelPoolWidth,
|
|
23
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { JavaScriptKernelMode } from "./kernel-contract.ts";
|
|
3
|
+
import { spawnNodeWorker } from "./worker-host.ts";
|
|
4
|
+
|
|
5
|
+
export interface WorkerLike {
|
|
6
|
+
readonly mode: JavaScriptKernelMode;
|
|
7
|
+
postMessage(message: HostToKernelMessage): void;
|
|
8
|
+
onMessage(handler: (message: KernelToHostMessage) => void): () => void;
|
|
9
|
+
onError(handler: (error: Error) => void): () => void;
|
|
10
|
+
terminate(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createInlineWorker(cwd: string, parallelPoolWidth: number): WorkerLike {
|
|
14
|
+
return spawnNodeWorker(new URL("./inline-worker-entry.js", import.meta.url), cwd, parallelPoolWidth, "inline");
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
|
|
3
|
+
export type ResultMessage = Extract<KernelToHostMessage, { type: "result" }>;
|
|
4
|
+
export type ToolCallMessage = Extract<KernelToHostMessage, { type: "tool-call" }>;
|
|
5
|
+
|
|
6
|
+
export type JavaScriptKernelMode = "worker" | "inline";
|
|
7
|
+
|
|
8
|
+
export interface JavaScriptKernelOptions {
|
|
9
|
+
readonly sessionId: string;
|
|
10
|
+
readonly cwd: string;
|
|
11
|
+
readonly parallelPoolWidth: number;
|
|
12
|
+
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
13
|
+
readonly workerEntryUrl?: URL;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface JavaScriptRunInput {
|
|
17
|
+
readonly cellId: string;
|
|
18
|
+
readonly code: string;
|
|
19
|
+
readonly timeoutMs?: number;
|
|
20
|
+
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type KernelOperation = "run" | "reset" | "interrupt";
|
|
24
|
+
export type LifecycleState = "open" | "closing" | "closed";
|
|
25
|
+
|
|
26
|
+
export class JavaScriptKernelClosedError extends Error {
|
|
27
|
+
readonly name = "JavaScriptKernelClosedError";
|
|
28
|
+
readonly operation: KernelOperation;
|
|
29
|
+
|
|
30
|
+
constructor(operation: KernelOperation) {
|
|
31
|
+
super(`Cannot ${operation}: JavaScript kernel is closed`);
|
|
32
|
+
this.operation = operation;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function assertJavaScriptKernelOpen(lifecycle: LifecycleState, operation: KernelOperation): void {
|
|
37
|
+
if (lifecycle !== "open") throw new JavaScriptKernelClosedError(operation);
|
|
38
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { join, resolve, sep } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import type { BridgeConnectionConfig } from "../../bridge/protocol.ts";
|
|
4
|
+
import {
|
|
5
|
+
RESERVED_AGENT_TOOL,
|
|
6
|
+
RESERVED_OUTPUT_TOOL,
|
|
7
|
+
TIMEOUT_PAUSE_OP,
|
|
8
|
+
TIMEOUT_RESUME_OP,
|
|
9
|
+
} from "../../bridge/reserved.ts";
|
|
10
|
+
import type { JavaScriptKernelOptions as BaseJavaScriptKernelOptions } from "./kernel-contract.ts";
|
|
11
|
+
import { rewriteImports } from "./rewrite-imports.ts";
|
|
12
|
+
|
|
13
|
+
const PREPARED_CELL_PREFIX = "/*senpi:prepared-cell*/";
|
|
14
|
+
|
|
15
|
+
export interface LocalModuleLoaderOptions {
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
readonly localRoots?: Readonly<Record<string, string>>;
|
|
18
|
+
readonly artifactsDir?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type JavaScriptKernelOptions = BaseJavaScriptKernelOptions & LocalModuleLoaderOptions;
|
|
22
|
+
|
|
23
|
+
export function localBridgeConnection(options: LocalModuleLoaderOptions): BridgeConnectionConfig {
|
|
24
|
+
return {
|
|
25
|
+
port: 1,
|
|
26
|
+
token: "local",
|
|
27
|
+
...(options.localRoots ? { localRoots: { ...options.localRoots } } : {}),
|
|
28
|
+
...(options.artifactsDir ? { artifactsDir: options.artifactsDir } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type RuntimeModuleContext = {
|
|
33
|
+
readonly cwdUrl: string;
|
|
34
|
+
readonly localRootUrls: Readonly<Record<string, string>>;
|
|
35
|
+
readonly reservedAgentTool: string;
|
|
36
|
+
readonly reservedOutputTool: string;
|
|
37
|
+
readonly timeoutPauseOp: string;
|
|
38
|
+
readonly timeoutResumeOp: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function directoryUrl(directory: string): string {
|
|
42
|
+
return pathToFileURL(`${resolve(directory)}${sep}`).href;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runtimeContext(options: LocalModuleLoaderOptions): RuntimeModuleContext {
|
|
46
|
+
const roots: Record<string, string> = {};
|
|
47
|
+
for (const [scheme, root] of Object.entries(options.localRoots ?? {})) {
|
|
48
|
+
roots[scheme.toLowerCase()] = directoryUrl(root);
|
|
49
|
+
}
|
|
50
|
+
if (options.artifactsDir && roots.local === undefined) {
|
|
51
|
+
roots.local = directoryUrl(join(options.artifactsDir, "local"));
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
cwdUrl: directoryUrl(options.cwd),
|
|
55
|
+
localRootUrls: roots,
|
|
56
|
+
reservedAgentTool: RESERVED_AGENT_TOOL,
|
|
57
|
+
reservedOutputTool: RESERVED_OUTPUT_TOOL,
|
|
58
|
+
timeoutPauseOp: TIMEOUT_PAUSE_OP,
|
|
59
|
+
timeoutResumeOp: TIMEOUT_RESUME_OP,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loaderPrelude(context: RuntimeModuleContext): string {
|
|
64
|
+
const serialized = JSON.stringify(context);
|
|
65
|
+
return [
|
|
66
|
+
`globalThis.__senpi_module_context__ = ${serialized};`,
|
|
67
|
+
"globalThis.__senpi_reserved_agent_tool__ = globalThis.__senpi_module_context__.reservedAgentTool;",
|
|
68
|
+
"globalThis.__senpi_reserved_output_tool__ = globalThis.__senpi_module_context__.reservedOutputTool;",
|
|
69
|
+
"globalThis.__senpi_timeout_pause_op__ = globalThis.__senpi_module_context__.timeoutPauseOp;",
|
|
70
|
+
"globalThis.__senpi_timeout_resume_op__ = globalThis.__senpi_module_context__.timeoutResumeOp;",
|
|
71
|
+
"globalThis.__senpi_import__ = async (source, options) => {",
|
|
72
|
+
" const context = globalThis.__senpi_module_context__;",
|
|
73
|
+
" const specifier = String(source);",
|
|
74
|
+
" const match = /^([a-z][a-z0-9+.-]*):\\/\\/(.*)$/i.exec(specifier);",
|
|
75
|
+
" let target = specifier;",
|
|
76
|
+
" if (match) {",
|
|
77
|
+
" const scheme = match[1].toLowerCase();",
|
|
78
|
+
" const root = context.localRootUrls[scheme];",
|
|
79
|
+
" if (!root) throw new Error('Unsupported module protocol: ' + specifier);",
|
|
80
|
+
" let relative;",
|
|
81
|
+
" try { relative = decodeURIComponent(match[2].replaceAll('\\\\', '/')); }",
|
|
82
|
+
" catch { throw new Error('Invalid module URL encoding: ' + specifier); }",
|
|
83
|
+
" if (relative.startsWith('/') || relative.split('/').includes('..')) {",
|
|
84
|
+
" throw new Error('Module path escapes ' + scheme + ':// root: ' + specifier);",
|
|
85
|
+
" }",
|
|
86
|
+
" target = new URL(relative, root).href;",
|
|
87
|
+
" } else if (specifier.startsWith('./') || specifier.startsWith('../') || specifier === '.' || specifier === '..') {",
|
|
88
|
+
" target = new URL(specifier, context.cwdUrl).href;",
|
|
89
|
+
" } else if (specifier.startsWith('/') || /^[A-Za-z]:[\\\\/]/.test(specifier)) {",
|
|
90
|
+
" const urlModule = await import('node:url');",
|
|
91
|
+
" target = urlModule.pathToFileURL(specifier).href;",
|
|
92
|
+
" }",
|
|
93
|
+
" return options === undefined ? import(target) : import(target, options);",
|
|
94
|
+
"};",
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class LocalModuleLoader {
|
|
99
|
+
readonly #prelude: string;
|
|
100
|
+
|
|
101
|
+
constructor(options: LocalModuleLoaderOptions) {
|
|
102
|
+
this.#prelude = loaderPrelude(runtimeContext(options));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
prepareCell(code: string): string {
|
|
106
|
+
return `${PREPARED_CELL_PREFIX}${JSON.stringify({ prelude: this.#prelude, code: rewriteImports(code) })}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const JAVASCRIPT_KERNEL_PRELUDE = [
|
|
2
|
+
"print(...values): write stdout text.",
|
|
3
|
+
"display(value): emit JSON, image, or markdown display output.",
|
|
4
|
+
"log(message): emit a progress log line.",
|
|
5
|
+
"phase(title): emit a progress phase.",
|
|
6
|
+
"env(key?, value?): read, set, or list environment values.",
|
|
7
|
+
"read(path, options?): read UTF-8 text; plain paths use cwd and local:// uses the session local root.",
|
|
8
|
+
"write(path, content): write UTF-8 or binary data and return the resolved path.",
|
|
9
|
+
"tool.<name>(args): request a host tool call through the bridge.",
|
|
10
|
+
"completion(prompt, options?): request a host completion bridge call.",
|
|
11
|
+
"output(...ids, options?): retrieve task output through the reserved output bridge.",
|
|
12
|
+
"agent(prompt, options?): delegate work through the reserved agent bridge.",
|
|
13
|
+
"parallel(thunks): run async thunks through the configured bounded pool; preserves order and rethrows the lowest-index error after all settle.",
|
|
14
|
+
"pipeline(items, ...stages): map items through staged async transforms with a barrier between stages.",
|
|
15
|
+
].join("\n");
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
import type { ImportDeclaration } from "@babel/types";
|
|
3
|
+
|
|
4
|
+
const DYNAMIC_IMPORT_CALLEE =
|
|
5
|
+
'(typeof __senpi_import__ === "function" ? __senpi_import__ : (specifier, options) => import(specifier, options))';
|
|
6
|
+
|
|
7
|
+
type AstNode = {
|
|
8
|
+
readonly type: string;
|
|
9
|
+
readonly start: number;
|
|
10
|
+
readonly end: number;
|
|
11
|
+
readonly value: Readonly<Record<string, unknown>>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type TextEdit = {
|
|
15
|
+
readonly start: number;
|
|
16
|
+
readonly end: number;
|
|
17
|
+
readonly text: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
class UnexpectedImportSpecifierError extends Error {
|
|
21
|
+
readonly name = "UnexpectedImportSpecifierError";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertNever(_value: never): never {
|
|
25
|
+
throw new UnexpectedImportSpecifierError("Unsupported import specifier");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
29
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nodeFrom(value: unknown): AstNode | undefined {
|
|
33
|
+
if (!isRecord(value)) return undefined;
|
|
34
|
+
const type = value.type;
|
|
35
|
+
const start = value.start;
|
|
36
|
+
const end = value.end;
|
|
37
|
+
if (typeof type !== "string" || typeof start !== "number" || typeof end !== "number") return undefined;
|
|
38
|
+
return { type, start, end, value };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseProgram(code: string): ReturnType<typeof parse> | undefined {
|
|
42
|
+
try {
|
|
43
|
+
return parse(code, {
|
|
44
|
+
sourceType: "module",
|
|
45
|
+
allowAwaitOutsideFunction: true,
|
|
46
|
+
allowReturnOutsideFunction: true,
|
|
47
|
+
allowImportExportEverywhere: true,
|
|
48
|
+
allowNewTargetOutsideFunction: true,
|
|
49
|
+
allowSuperOutsideMethod: true,
|
|
50
|
+
allowUndeclaredExports: true,
|
|
51
|
+
errorRecovery: true,
|
|
52
|
+
plugins: ["typescript", "importAttributes"],
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof SyntaxError) return undefined;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function walkNodes(root: unknown, visit: (node: AstNode) => void): void {
|
|
61
|
+
const stack: unknown[] = [root];
|
|
62
|
+
while (stack.length > 0) {
|
|
63
|
+
const current = stack.pop();
|
|
64
|
+
if (Array.isArray(current)) {
|
|
65
|
+
for (let index = current.length - 1; index >= 0; index -= 1) stack.push(current[index]);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!isRecord(current)) continue;
|
|
69
|
+
const node = nodeFrom(current);
|
|
70
|
+
if (node) visit(node);
|
|
71
|
+
for (const [key, value] of Object.entries(current)) {
|
|
72
|
+
if (key === "loc" || key === "extra" || key === "range" || key.endsWith("Comments")) continue;
|
|
73
|
+
if (value !== null && typeof value === "object") stack.push(value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function importOptions(node: ImportDeclaration): string | undefined {
|
|
79
|
+
if (!node.attributes || node.attributes.length === 0) return undefined;
|
|
80
|
+
const pairs = node.attributes.map((attribute) => {
|
|
81
|
+
const key = attribute.key.type === "Identifier" ? attribute.key.name : JSON.stringify(attribute.key.value);
|
|
82
|
+
return `${key}: ${JSON.stringify(attribute.value.value)}`;
|
|
83
|
+
});
|
|
84
|
+
return `{ with: { ${pairs.join(", ")} } }`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function importCall(source: string, options: string | undefined): string {
|
|
88
|
+
const sourceLiteral = JSON.stringify(source);
|
|
89
|
+
return options ? `__senpi_import__(${sourceLiteral}, ${options})` : `__senpi_import__(${sourceLiteral})`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function rewriteImportDeclaration(node: ImportDeclaration): string {
|
|
93
|
+
const call = importCall(node.source.value, importOptions(node));
|
|
94
|
+
let defaultName: string | undefined;
|
|
95
|
+
let namespaceName: string | undefined;
|
|
96
|
+
const namedBindings: Array<readonly [imported: string, local: string]> = [];
|
|
97
|
+
|
|
98
|
+
for (const specifier of node.specifiers) {
|
|
99
|
+
switch (specifier.type) {
|
|
100
|
+
case "ImportDefaultSpecifier":
|
|
101
|
+
defaultName = specifier.local.name;
|
|
102
|
+
break;
|
|
103
|
+
case "ImportNamespaceSpecifier":
|
|
104
|
+
namespaceName = specifier.local.name;
|
|
105
|
+
break;
|
|
106
|
+
case "ImportSpecifier": {
|
|
107
|
+
const imported =
|
|
108
|
+
specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
109
|
+
namedBindings.push([imported, specifier.local.name]);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
assertNever(specifier);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (namedBindings.length > 0) {
|
|
118
|
+
const named = namedBindings
|
|
119
|
+
.map(([imported, local]) => (imported === local ? imported : `${imported}: ${local}`))
|
|
120
|
+
.join(", ");
|
|
121
|
+
const bindings = defaultName ? `default: ${defaultName}, ${named}` : named;
|
|
122
|
+
return `const { ${bindings} } = await ${call};`;
|
|
123
|
+
}
|
|
124
|
+
if (namespaceName && defaultName) {
|
|
125
|
+
return `const ${namespaceName} = await ${call}; const ${defaultName} = ${namespaceName}.default;`;
|
|
126
|
+
}
|
|
127
|
+
if (namespaceName) return `const ${namespaceName} = await ${call};`;
|
|
128
|
+
if (defaultName) return `const ${defaultName} = (await ${call}).default;`;
|
|
129
|
+
return `await ${call};`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function dynamicImportEdit(node: AstNode): TextEdit | undefined {
|
|
133
|
+
if (node.type !== "CallExpression") return undefined;
|
|
134
|
+
const callee = nodeFrom(node.value.callee);
|
|
135
|
+
if (callee?.type !== "Import") return undefined;
|
|
136
|
+
return { start: callee.start, end: callee.end, text: DYNAMIC_IMPORT_CALLEE };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function applyEdits(code: string, edits: readonly TextEdit[]): string {
|
|
140
|
+
if (edits.length === 0) return code;
|
|
141
|
+
const descending = edits.toSorted((left, right) => right.start - left.start);
|
|
142
|
+
let output = code;
|
|
143
|
+
for (const edit of descending) {
|
|
144
|
+
output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
|
|
145
|
+
}
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function rewriteImports(code: string): string {
|
|
150
|
+
if (!code.includes("import")) return code;
|
|
151
|
+
const ast = parseProgram(code);
|
|
152
|
+
if (!ast) return code;
|
|
153
|
+
const edits: TextEdit[] = [];
|
|
154
|
+
|
|
155
|
+
for (const node of ast.program.body) {
|
|
156
|
+
if (node.type !== "ImportDeclaration" || typeof node.start !== "number" || typeof node.end !== "number") continue;
|
|
157
|
+
edits.push({ start: node.start, end: node.end, text: rewriteImportDeclaration(node) });
|
|
158
|
+
}
|
|
159
|
+
walkNodes(ast.program, (node) => {
|
|
160
|
+
const edit = dynamicImportEdit(node);
|
|
161
|
+
if (edit) edits.push(edit);
|
|
162
|
+
});
|
|
163
|
+
return applyEdits(code, edits);
|
|
164
|
+
}
|