@code-yeongyu/senpi-codemode 2026.10.7 → 2026.10.9

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +67 -5
  3. package/package.json +5 -4
  4. package/src/bridge/http-server.ts +9 -1
  5. package/src/bridge/kernel-tools-protocol.ts +8 -0
  6. package/src/bridge/protocol.ts +4 -0
  7. package/src/bridges/eval-virtual-schemas.ts +22 -0
  8. package/src/environments/editable-check.ts +116 -0
  9. package/src/environments/install-lock.ts +223 -0
  10. package/src/environments/py-environment.ts +98 -0
  11. package/src/environments/py-installer.ts +247 -0
  12. package/src/environments/python-environments.ts +85 -0
  13. package/src/environments/revision-store.ts +110 -0
  14. package/src/extension/bridge-tool-call.ts +2 -0
  15. package/src/extension/cell-kernel-tools.ts +28 -0
  16. package/src/extension/kernel-registration.ts +5 -0
  17. package/src/extension/kernel-replacement.ts +20 -0
  18. package/src/extension/runtime-factory.ts +12 -0
  19. package/src/extension/session-manager-proxy.ts +6 -0
  20. package/src/extension/session-manager.ts +24 -2
  21. package/src/index.ts +1 -0
  22. package/src/kernels/jl/prelude.jl +6 -1
  23. package/src/kernels/js/context-manager.ts +15 -1
  24. package/src/kernels/js/kernel-tools-errors.js +2 -0
  25. package/src/kernels/js/kernel-tools-errors.ts +2 -0
  26. package/src/kernels/js/local-module-loader.ts +9 -4
  27. package/src/kernels/js/worker-runtime.js +3 -1
  28. package/src/kernels/js/workpool.js +5 -2
  29. package/src/kernels/py/kernel-contract.ts +11 -2
  30. package/src/kernels/py/kernel-tools-host.ts +97 -0
  31. package/src/kernels/py/kernel.ts +48 -10
  32. package/src/kernels/py/prelude.py +771 -18
  33. package/src/kernels/py/process.ts +15 -4
  34. package/src/kernels/py/startup.ts +58 -20
  35. package/src/kernels/py/transport.ts +49 -8
  36. package/src/kernels/rb/workpool.rb +7 -1
  37. package/src/kernels/sandbox/vendor/pi-codemode/LICENSE +21 -0
  38. package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +42 -0
  39. package/src/kernels/sandbox/vendor/pi-codemode/identifier.ts +14 -0
  40. package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +514 -0
  41. package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +431 -0
  42. package/src/kernels/sandbox/vendor/pi-codemode/runtime/protocol.ts +61 -0
  43. package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +231 -0
  44. package/src/kernels/sandbox/vendor/pi-codemode/types.ts +185 -0
  45. package/src/kernels/sandbox/vendor/pi-codemode/wasm.ts +37 -0
  46. package/src/kernels/shared/cell-source-at-start.ts +18 -0
  47. package/src/kernels/shared/kernel-tools-pump.ts +6 -0
  48. package/src/kernels/shared/process-cpu.ts +69 -0
  49. package/src/kernels/shared/subprocess-queue.ts +1 -0
  50. package/src/tool/cell-handler.ts +1 -1
  51. package/src/tool/eval-tool-options.ts +3 -0
  52. package/src/tool/load-cell.ts +73 -0
  53. package/src/tool/magic-cell-host.ts +65 -0
  54. package/src/tool/magic-cells.ts +65 -0
  55. package/src/tool/run-eval-cell.ts +30 -0
  56. package/src/tool/types.ts +27 -0
@@ -0,0 +1,65 @@
1
+ import type { PythonEnvironments } from "../environments/python-environments.ts";
2
+ import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
3
+ import { MagicCellError, parseMagicCell } from "./magic-cells.ts";
4
+ import type { EvalLanguage, HostCellExecutor } from "./types.ts";
5
+
6
+ export type MagicCellPlan =
7
+ | { readonly kind: "ordinary" }
8
+ | { readonly kind: "host"; readonly executor: HostCellExecutor }
9
+ | { readonly kind: "refused"; readonly message: string }
10
+ | { readonly kind: "load"; readonly target: string };
11
+
12
+ export function planMagicCell(
13
+ language: EvalLanguage,
14
+ code: string,
15
+ environments: PythonEnvironments | undefined,
16
+ ): MagicCellPlan {
17
+ let magic: ReturnType<typeof parseMagicCell>;
18
+ try {
19
+ magic = parseMagicCell(language, code);
20
+ } catch (error) {
21
+ if (error instanceof MagicCellError) return { kind: "refused", message: error.message };
22
+ throw error;
23
+ }
24
+ if (magic === undefined) return { kind: "ordinary" };
25
+ if (magic.kind === "load") return { kind: "load", target: magic.target };
26
+ if (environments === undefined) {
27
+ return {
28
+ kind: "refused",
29
+ message: "environment_installer_unavailable: this session has no Python interpreter to install packages for",
30
+ };
31
+ }
32
+ if (magic.kind === "environment") {
33
+ const mode = magic.mode;
34
+ return {
35
+ kind: "host",
36
+ executor: async () => {
37
+ const root = await environments.setMode(mode);
38
+ return { ok: true, valueRepr: `environment: ${mode} (${root})` };
39
+ },
40
+ };
41
+ }
42
+ const requirements = magic.args;
43
+ return {
44
+ kind: "host",
45
+ // Install time is parked time for the run budget, the same accounting as a bridge call.
46
+ executor: async ({ signal, emit }) => {
47
+ emit({ type: "status", event: { op: TIMEOUT_PAUSE_OP } });
48
+ try {
49
+ const receipt = await environments.install(requirements, signal, (stream, data) =>
50
+ emit({ type: "text", stream, data }),
51
+ );
52
+ const packages = receipt.resolved.length > 0 ? receipt.resolved.join(", ") : "nothing new";
53
+ return {
54
+ ok: true,
55
+ valueRepr: `installed ${packages} into ${receipt.mode} (revision ${receipt.revision}); already-imported modules stay cached until reset`,
56
+ };
57
+ } catch (error) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ return { ok: false, error: { message } };
60
+ } finally {
61
+ emit({ type: "status", event: { op: TIMEOUT_RESUME_OP } });
62
+ }
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,65 @@
1
+ import type { EvalLanguage } from "./types.ts";
2
+
3
+ export type MagicCell =
4
+ | { readonly kind: "pip"; readonly args: string }
5
+ | { readonly kind: "environment"; readonly mode: "managed" | "project" }
6
+ | { readonly kind: "load"; readonly target: string };
7
+
8
+ const HOST_MAGICS = ["pip", "environment", "load"] as const;
9
+ const LOAD_LANGUAGES: ReadonlySet<EvalLanguage> = new Set(["py", "js"]);
10
+ const COMMENT_PREFIX: Partial<Record<EvalLanguage, string>> = { py: "#", js: "//" };
11
+ type HostMagic = (typeof HOST_MAGICS)[number];
12
+
13
+ export class MagicCellError extends Error {
14
+ readonly name = "MagicCellError";
15
+ }
16
+
17
+ function joinContinuations(lines: readonly string[]): string[] {
18
+ const joined: string[] = [];
19
+ let pending: string | undefined;
20
+ for (const line of lines) {
21
+ const current = pending === undefined ? line : `${pending} ${line.trim()}`;
22
+ if (current.trimEnd().endsWith("\\")) pending = current.trimEnd().slice(0, -1).trimEnd();
23
+ else {
24
+ joined.push(current);
25
+ pending = undefined;
26
+ }
27
+ }
28
+ if (pending !== undefined) joined.push(pending);
29
+ return joined;
30
+ }
31
+
32
+ function hostMagicOf(language: EvalLanguage, line: string): HostMagic | undefined {
33
+ const match = /^%([A-Za-z]+)(?:\s|$)/.exec(line.trim());
34
+ const name = match?.[1];
35
+ const magic = HOST_MAGICS.find((candidate) => candidate === name);
36
+ if (magic === "load") return LOAD_LANGUAGES.has(language) ? magic : undefined;
37
+ return language === "py" ? magic : undefined;
38
+ }
39
+
40
+ /**
41
+ * A cell whose first code line (blank and comment lines skipped) is a host magic runs on the host instead of
42
+ * the interpreter: `%pip` and `%environment` in Python, `%load` in Python and JavaScript. In Python a trailing
43
+ * backslash continues the line. Any other cell is ordinary code, so a magic-looking line later in the cell
44
+ * (say, inside a string) is left alone. A magic followed by more code is refused, because the host step must
45
+ * finish before the code that depends on it runs.
46
+ */
47
+ export function parseMagicCell(language: EvalLanguage, code: string): MagicCell | undefined {
48
+ const comment = COMMENT_PREFIX[language];
49
+ // Comment lines go before Python's backslash continuations are joined: Python never continues a comment line.
50
+ const raw = code.split("\n").filter((line) => comment === undefined || !line.trim().startsWith(comment));
51
+ const lines = (language === "py" ? joinContinuations(raw) : raw).filter((line) => line.trim() !== "");
52
+ const first = lines[0] ?? "";
53
+ const magic = hostMagicOf(language, first);
54
+ if (magic === undefined) return undefined;
55
+ if (lines.length > 1)
56
+ throw new MagicCellError(`put %${magic} on its own cell, then run the code that uses it in the next cell`);
57
+ const args = first.trim().slice(`%${magic}`.length).trim();
58
+ if (magic === "pip") return { kind: "pip", args };
59
+ if (magic === "load") {
60
+ if (args === "") throw new MagicCellError("%load takes one argument: the path of the file to run");
61
+ return { kind: "load", target: args };
62
+ }
63
+ if (args === "managed" || args === "project") return { kind: "environment", mode: args };
64
+ throw new MagicCellError("%environment takes one argument: managed or project");
65
+ }
@@ -23,6 +23,8 @@ import { EvalKernelResetRefusedError } from "./eval-kernel-reset-refused-error.t
23
23
  import { evalTimeoutBehavior } from "./eval-request.ts";
24
24
  import type { CreateEvalToolOptions, EvalCellInvocation } from "./eval-tool-options.ts";
25
25
  import { describeTimeoutState } from "./interrupt-note.ts";
26
+ import { loadCell } from "./load-cell.ts";
27
+ import { planMagicCell } from "./magic-cell-host.ts";
26
28
  import type { EvalKernel, EvalToolDetails } from "./types.ts";
27
29
 
28
30
  export async function runEvalCell(
@@ -205,11 +207,21 @@ async function executeCell(
205
207
  const pending = handler.handle(message);
206
208
  void pending.catch((error: unknown) => execution.cancel(error));
207
209
  };
210
+ let releaseCellKernelTools: (() => void) | undefined;
208
211
  try {
209
212
  const kernel = await execution.wait(options.kernelManager.getKernel(invocation.input.language, onMessage));
210
213
  // Computed before the handler so its construction-time snapshot captures this cell's capability.
211
214
  // Worker messages later restore that snapshot before calling host tools (#1754, #2512).
212
215
  const kernelTools = kernelToolsFor(kernel);
216
+ // Subprocess kernels call the host over the bridge, outside this async context. Their calls carry a fresh secret
217
+ // minted for this run and sent only to this cell's kernel, never the model-visible cell id, so code in another
218
+ // kernel cannot name this cell into its tools. JS calls already run inside this cell's scope (#1754).
219
+ const bridgeCellToken =
220
+ kernelTools === undefined || invocation.input.language === "js" ? undefined : randomUUID();
221
+ releaseCellKernelTools =
222
+ kernelTools === undefined || bridgeCellToken === undefined
223
+ ? undefined
224
+ : options.kernelManager.bindCellKernelTools?.(bridgeCellToken, kernelTools);
213
225
  const runBound = async (): Promise<AgentToolResult<EvalToolDetails>> => {
214
226
  const queue = kernel.queueSnapshot();
215
227
  state.queuedBehind = [...(queue.activeCellId === null ? [] : [queue.activeCellId]), ...queue.queuedCellIds];
@@ -249,10 +261,26 @@ async function executeCell(
249
261
  }
250
262
  if (invocation.input.reset) await execution.wait(kernel.reset());
251
263
  execution.setKernel(kernel);
264
+ const magic = planMagicCell(invocation.input.language, invocation.input.code, options.pythonEnvironments);
265
+ // Resolved when the cell's turn comes in the kernel's queue: a %load reads the file the cells ahead of it
266
+ // wrote, and a refusal settles in queue order like any cell.
267
+ const loadOptions = { cwd: invocation.ctx.cwd, artifactsDir: options.artifactsDir };
268
+ const resolveAtStart =
269
+ magic.kind === "load"
270
+ ? () => loadCell(magic.target, loadOptions)
271
+ : magic.kind === "refused"
272
+ ? () => ({ ok: false as const, message: magic.message })
273
+ : undefined;
274
+ const environments = invocation.input.language === "py" ? options.pythonEnvironments : undefined;
275
+ const envRoot = environments === undefined ? undefined : () => environments.activeRoot ?? "";
252
276
  const result = await execution.wait(
253
277
  kernel.run({
254
278
  cellId: invocation.cellId,
255
279
  code: invocation.input.code,
280
+ ...(resolveAtStart === undefined ? {} : { resolveAtStart }),
281
+ ...(magic.kind === "host" ? { host: magic.executor } : {}),
282
+ ...(envRoot === undefined ? {} : { envRoot }),
283
+ ...(bridgeCellToken === undefined ? {} : { bridgeCellToken }),
256
284
  kernelPreludes: options.kernelPreludes?.(),
257
285
  onMessage,
258
286
  onStarted: () => {
@@ -285,6 +313,8 @@ async function executeCell(
285
313
  if (error instanceof Error && error.name === "TimeoutError") throw await describeTimeoutState(error, execution);
286
314
  throw error;
287
315
  } finally {
316
+ // First, so nothing below can skip it: from here on a host call carrying this run's secret gets no kernel tools.
317
+ releaseCellKernelTools?.();
288
318
  state.active = false;
289
319
  bridgeAbortController.abort();
290
320
  execution.finish();
package/src/tool/types.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  DEFAULT_RUN_BUDGET_SECONDS,
9
9
  defaultCodemodeSettings,
10
10
  } from "../config/settings.ts";
11
+ import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
11
12
  import type { TruncationMeta } from "../output/output-meta.ts";
12
13
 
13
14
  export const evalLanguageOrder = ["js", "py", "rb", "jl"] as const;
@@ -167,8 +168,29 @@ export interface EvalKernelRunInput {
167
168
  readonly onMessage?: (message: KernelToHostMessage) => void;
168
169
  /** Globals of the tools active when the cell was submitted; kernels without preludes ignore them. */
169
170
  readonly kernelPreludes?: readonly KernelPreludeContribution[];
171
+ /**
172
+ * Python only: read when the cell starts running (not when it was queued), so a cell queued behind `%pip`
173
+ * sees the revision it published. `""` means no environment: the previous revision leaves the import path.
174
+ */
175
+ readonly envRoot?: () => string;
176
+ /** The file a `%load` cell runs: tracebacks name it and its relative imports resolve from its directory. */
177
+ readonly sourceFile?: string;
178
+ /**
179
+ * Called when the cell's turn comes in the kernel's queue, never earlier: what to run instead of `code` (a `%load`
180
+ * cell's file), or a refusal that settles the cell as failed in queue order.
181
+ */
182
+ readonly resolveAtStart?: () => CellSourceAtStart;
183
+ /**
184
+ * Subprocess kernels attach this to the cell's host calls, and the host gives those calls the cell's kernel tools.
185
+ * A fresh secret per run, sent only to the kernel that runs the cell; never a model-visible id.
186
+ */
187
+ readonly bridgeCellToken?: string;
170
188
  }
171
189
 
190
+ export type CellSourceAtStart =
191
+ | { readonly ok: true; readonly code: string; readonly sourceFile?: string }
192
+ | { readonly ok: false; readonly message: string };
193
+
172
194
  export interface KernelInterruptHandle {
173
195
  /** Resolves once the kernel knows whether user state survived the interrupt. */
174
196
  readonly stateRetained: Promise<boolean>;
@@ -205,6 +227,11 @@ export interface EvalKernelManager {
205
227
  * Identity-checked, so releasing a superseded listener never unbinds a newer cell's listener.
206
228
  */
207
229
  releaseKernelListener?(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): void;
230
+ /**
231
+ * Binds a running cell's kernel-tools capability to the host calls that carry `token` (subprocess kernels reach
232
+ * the host over the bridge, outside the cell's async context). The returned release runs when the cell settles.
233
+ */
234
+ bindCellKernelTools?(token: string, capability: KernelToolsCapability): () => void;
208
235
  }
209
236
 
210
237
  export type ExecuteTool = (