@code-yeongyu/senpi-codemode 2026.10.9 → 2026.10.10-10

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 (96) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +161 -11
  3. package/package.json +6 -5
  4. package/src/bridge/protocol.ts +8 -3
  5. package/src/bridge/reserved.ts +2 -0
  6. package/src/bridges/eval-environment-schemas.ts +48 -0
  7. package/src/bridges/eval-virtual-schemas.ts +11 -4
  8. package/src/bridges/reserved-dispatch.ts +13 -1
  9. package/src/config/executable-settings.json +5 -0
  10. package/src/config/project-trust.ts +51 -0
  11. package/src/config/settings-overrides.ts +2 -0
  12. package/src/environments/js-environments.ts +208 -0
  13. package/src/environments/js-installer.ts +268 -0
  14. package/src/environments/js-revision-files.ts +91 -0
  15. package/src/environments/no-links.ts +132 -0
  16. package/src/environments/packages-install.ts +98 -0
  17. package/src/environments/project-resolves.ts +54 -0
  18. package/src/environments/py-environment.ts +7 -3
  19. package/src/environments/py-installer.ts +5 -1
  20. package/src/environments/python-environments.ts +1 -1
  21. package/src/environments/revision-store.ts +20 -4
  22. package/src/extension/bridge-tool-call.ts +5 -1
  23. package/src/extension/eval-notifier.ts +13 -2
  24. package/src/extension/kernel-registration.ts +7 -2
  25. package/src/extension/runtime-factory.ts +51 -13
  26. package/src/extension/runtime-info.ts +6 -2
  27. package/src/extension/session-manager-contract.ts +3 -0
  28. package/src/extension/session-manager.ts +16 -2
  29. package/src/index.ts +3 -2
  30. package/src/interpreters/detect.ts +34 -1
  31. package/src/kernels/AGENTS.md +2 -0
  32. package/src/kernels/js/cell-dispatch.ts +44 -0
  33. package/src/kernels/js/cell-ownership.js +268 -0
  34. package/src/kernels/js/cell-run-context.js +89 -0
  35. package/src/kernels/js/context-manager.ts +46 -51
  36. package/src/kernels/js/control-frames.ts +24 -0
  37. package/src/kernels/js/host-entries.ts +62 -0
  38. package/src/kernels/js/kernel-contract.ts +16 -1
  39. package/src/kernels/js/kernel-memory-bridge.ts +1 -1
  40. package/src/kernels/js/kernel-memory.ts +79 -0
  41. package/src/kernels/js/kernel-tools-registry.js +1 -0
  42. package/src/kernels/js/local-module-loader.ts +17 -6
  43. package/src/kernels/js/process-drain-scan.d.ts +8 -0
  44. package/src/kernels/js/process-drain-scan.js +28 -0
  45. package/src/kernels/js/process-entry.js +466 -0
  46. package/src/kernels/js/process-memory-host.ts +83 -0
  47. package/src/kernels/js/process-worker.ts +298 -0
  48. package/src/kernels/js/rejection-reports.js +61 -0
  49. package/src/kernels/js/run-queue.ts +7 -0
  50. package/src/kernels/js/worker-core.js +53 -8
  51. package/src/kernels/js/worker-cwd.js +5 -2
  52. package/src/kernels/js/worker-host.ts +9 -2
  53. package/src/kernels/js/worker-indirect-eval.js +37 -13
  54. package/src/kernels/js/worker-package-resolve.js +153 -0
  55. package/src/kernels/js/worker-require.d.ts +19 -0
  56. package/src/kernels/js/worker-require.js +52 -0
  57. package/src/kernels/js/worker-runtime.js +43 -9
  58. package/src/kernels/js/worker-shadow-guard.js +2 -6
  59. package/src/kernels/js/worker-shell-capture.js +5 -0
  60. package/src/kernels/js/worker-slot.ts +9 -0
  61. package/src/kernels/js/worker-startup.ts +49 -1
  62. package/src/kernels/js/worker-webview.js +15 -1
  63. package/src/kernels/py/host-cell.ts +8 -31
  64. package/src/kernels/py/prelude.py +33 -2
  65. package/src/kernels/py/process.ts +4 -0
  66. package/src/kernels/sandbox/sandbox-cell.ts +141 -0
  67. package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
  68. package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
  69. package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
  70. package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
  71. package/src/kernels/shared/host-cell.ts +57 -0
  72. package/src/kernels/shared/subprocess-process.ts +14 -0
  73. package/src/prompt/eval-prompt.ts +8 -1
  74. package/src/tool/AGENTS.md +1 -0
  75. package/src/tool/cell-handler.ts +12 -11
  76. package/src/tool/cell-reserved-dispatch.ts +1 -0
  77. package/src/tool/cell-runtime.ts +4 -2
  78. package/src/tool/detached-cell-contract.ts +3 -0
  79. package/src/tool/detached-cell-manager.ts +4 -8
  80. package/src/tool/detached-cell-notification.ts +11 -63
  81. package/src/tool/detached-cell-snapshot.ts +17 -0
  82. package/src/tool/detached-eval-result.ts +13 -7
  83. package/src/tool/detached-notification-queue.ts +1 -2
  84. package/src/tool/eval-request.ts +23 -1
  85. package/src/tool/eval-tool-options.ts +3 -0
  86. package/src/tool/eval-tool.ts +17 -4
  87. package/src/tool/interrupt-note.ts +4 -5
  88. package/src/tool/live-headline.ts +60 -0
  89. package/src/tool/load-cell.ts +18 -3
  90. package/src/tool/magic-cell-host.ts +60 -2
  91. package/src/tool/magic-cells.ts +12 -2
  92. package/src/tool/managed-cell.ts +0 -4
  93. package/src/tool/render.ts +74 -15
  94. package/src/tool/run-eval-cell.ts +46 -4
  95. package/src/tool/runtime-label.ts +2 -1
  96. package/src/tool/types.ts +23 -1
@@ -7,6 +7,7 @@ import {
7
7
  type ToolDefinition,
8
8
  type ToolRenderResultOptions,
9
9
  truncateToVisualLines,
10
+ visibleWidth,
10
11
  } from "@code-yeongyu/senpi";
11
12
  import type { TruncationMeta } from "../output/output-meta.ts";
12
13
  import { highlightedCode } from "./code-preview.ts";
@@ -21,6 +22,7 @@ import {
21
22
  JSON_TREE_SCALAR_LEN_EXPANDED,
22
23
  renderJsonTreeLines,
23
24
  } from "./json-tree.ts";
25
+ import { knownLanguage, leadsWithHeadline, liveHeadline } from "./live-headline.ts";
24
26
  import { formatRuntimeBadge } from "./runtime-label.ts";
25
27
  import { codePointPrefix, formatDuration, renderToolCallWidget } from "./tool-widgets.ts";
26
28
  import type {
@@ -341,18 +343,37 @@ function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges
341
343
  environment.spinnerFrame ?? Math.floor((cellElapsedMs(cell, environment) ?? 0) / LIVE_RENDER_TICK_MS),
342
344
  );
343
345
  const runtimeBadge = cell.runtime === undefined ? "" : ` (${formatRuntimeBadge(cell.language, cell.runtime)})`;
344
- let header = `eval ${cell.language}${runtimeBadge} ${presentation.label} ${presentation.icon}`;
346
+ let header = leadsWithHeadline(cell.status)
347
+ ? `eval ${cell.language}${runtimeBadge} ${presentation.label}`
348
+ : `eval ${cell.language}${runtimeBadge} ${presentation.label} ${presentation.icon}`;
345
349
  if (cell.queuedBehind !== undefined && cell.queuedBehind.length > 0)
346
350
  header += ` · queued behind ${cell.queuedBehind.map(sanitizeTerminalLabel).join(", ")}`;
351
+ else if (cell.queuedBehind !== undefined && cell.status === "queued")
352
+ header += ` · waiting for the ${cell.language} kernel to be ready`;
347
353
  const throughputBadge = badges.throughput === undefined ? undefined : formatThroughputBadge(badges.throughput);
348
354
  if (throughputBadge !== undefined) header += ` · ${throughputBadge}`;
349
355
  const elapsedMs = badges.throughput?.wallDurationMs ?? cellElapsedMs(cell, environment);
350
356
  if (elapsedMs !== undefined) header += ` · ${formatDuration(elapsedMs)}`;
351
357
  if (badges.reset) header += " · reset";
352
358
  if (badges.timeout !== undefined) header += ` · timeout ${badges.timeout}s`;
359
+ if (leadsWithHeadline(cell.status))
360
+ header = headlined(presentation.icon, cell.summary, cell.code, header, environment);
353
361
  return style(environment.theme, presentation.color, header);
354
362
  }
355
363
 
364
+ // An in-progress row leads with what the cell is doing (senpi#2802); collapsed, the headline is cut so the whole
365
+ // header stays on one line, and the code moves behind expand.
366
+ function headlined(
367
+ icon: string,
368
+ summary: string | undefined,
369
+ code: string | undefined,
370
+ rest: string,
371
+ environment: RenderEnvironment,
372
+ ): string {
373
+ const budget = environment.expanded ? undefined : environment.width - 3 - visibleWidth(`${icon} · ${rest}`);
374
+ return `${icon} ${liveHeadline(summary, code, budget)} · ${rest}`;
375
+ }
376
+
356
377
  function previewText(
357
378
  text: string,
358
379
  maxLines: number,
@@ -612,12 +633,23 @@ function renderAgentProgressEvents(events: readonly EvalStatusEvent[], environme
612
633
  }
613
634
 
614
635
  function renderCell(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string[] {
636
+ if (leadsWithHeadline(cell.status) && !environment.expanded && cell.output.trim().length === 0) {
637
+ const statusEvents = (cell.statusEvents ?? []).filter((event) => event.op !== "agent");
638
+ if (statusEvents.length === 0) {
639
+ // Nothing to frame yet: the collapsed live row is its headline alone, one line, no empty box.
640
+ const lines = renderPrefixed(cellHeader(cell, environment, badges), environment, LIVE_LINE_PREFIX);
641
+ const agentEvents = (cell.statusEvents ?? []).filter((event) => event.op === "agent");
642
+ if (agentEvents.length > 0) appendLines(lines, renderAgentProgressEvents(agentEvents, environment));
643
+ return lines;
644
+ }
645
+ }
615
646
  const lines = renderPrefixed(cellHeader(cell, environment, badges), environment, {
616
647
  prefix: "╭─ ",
617
648
  continuation: "│ ",
618
649
  color: "borderAccent",
619
650
  });
620
- if (cell.summary !== undefined) {
651
+ const headlined = leadsWithHeadline(cell.status);
652
+ if (cell.summary !== undefined && !headlined) {
621
653
  appendLines(
622
654
  lines,
623
655
  summaryVisualLines(cell.summary, Math.max(1, environment.width - 2), environment.expanded).map(
@@ -626,11 +658,14 @@ function renderCell(cell: EvalCellResult, environment: RenderEnvironment, badges
626
658
  );
627
659
  }
628
660
  const innerWidth = Math.max(1, environment.width - 2);
629
- const codePreview = previewText(
630
- highlightedCode(cell.code, cell.language, environment.theme, environment.repaint),
631
- environment.expanded ? Number.POSITIVE_INFINITY : CODE_PREVIEW_LINES,
632
- innerWidth,
633
- );
661
+ const codePreview =
662
+ headlined && !environment.expanded
663
+ ? { lines: [], skipped: 0 }
664
+ : previewText(
665
+ highlightedCode(cell.code, cell.language, environment.theme, environment.repaint),
666
+ environment.expanded ? Number.POSITIVE_INFINITY : CODE_PREVIEW_LINES,
667
+ innerWidth,
668
+ );
634
669
  if (codePreview.skipped > 0) {
635
670
  appendLines(
636
671
  lines,
@@ -879,6 +914,10 @@ function displaySummary(summary: string | undefined): string | undefined {
879
914
  return normalizeEvalSummary(summary);
880
915
  }
881
916
 
917
+ function cellIdSuffix(cellId: unknown): string {
918
+ return typeof cellId === "string" && cellId !== "" ? ` ${sanitizeTerminalLabel(cellId)}` : "";
919
+ }
920
+
882
921
  export function renderEvalCall(
883
922
  args: EvalToolRequest,
884
923
  theme: Theme | undefined,
@@ -892,15 +931,23 @@ export function renderEvalCall(
892
931
  return component;
893
932
  }
894
933
  if (!isEvalRunInput(args)) {
895
- const title = args.action === "list" ? "eval list" : `eval ${args.action} ${args.cell_id}`;
934
+ // While a peek/stop call streams in, `cell_id` can still be missing; the title is the action alone until it arrives.
935
+ const title = args.action === "list" ? "eval list" : `eval ${args.action}${cellIdSuffix(args.cell_id)}`;
896
936
  component.setBlocks([{ kind: "text", text: style(theme, "toolTitle", title) }]);
897
937
  return component;
898
938
  }
939
+ // While the model streams the call, `code` usually arrives before `language` and `summary`; a partial call still
940
+ // renders (the host would otherwise fall back to a raw key=value row).
941
+ const code = typeof args.code === "string" ? args.code : "";
942
+ const language = knownLanguage(args.language);
899
943
  if (theme === undefined && context.spinnerFrame === undefined) {
900
944
  const reset = args.reset === true ? " reset" : "";
901
945
  const timeout = args.timeout === undefined ? "" : ` timeout ${args.timeout}s`;
902
946
  component.setBlocks([
903
- { kind: "text", text: style(theme, "toolTitle", `eval ${args.language}${reset}${timeout}`) },
947
+ {
948
+ kind: "text",
949
+ text: style(theme, "toolTitle", `eval${language === undefined ? "" : ` ${language}`}${reset}${timeout}`),
950
+ },
904
951
  ...(displaySummary(args.summary) === undefined
905
952
  ? []
906
953
  : [summaryBlock(displaySummary(args.summary) ?? "", theme, context.expanded)]),
@@ -909,9 +956,11 @@ export function renderEvalCall(
909
956
  text: style(
910
957
  theme,
911
958
  "mdCodeBlock",
912
- args.code.trim().length > 0
913
- ? displayCode(args.code, args.language, context.argsComplete ? context.invalidate : undefined)
914
- : "...",
959
+ code.trim().length === 0
960
+ ? "..."
961
+ : language === undefined
962
+ ? code
963
+ : displayCode(code, language, context.argsComplete ? context.invalidate : undefined),
915
964
  ),
916
965
  maxVisualLines: context.expanded ? undefined : CODE_PREVIEW_LINES,
917
966
  collapseKind: "code",
@@ -933,11 +982,13 @@ export function renderEvalCall(
933
982
  now: renderNow(context),
934
983
  ...(context.argsComplete ? { repaint: context.invalidate } : {}),
935
984
  };
985
+ const summary = displaySummary(args.summary);
986
+ if (language === undefined) return streamingCallLines(summary, code, environment);
936
987
  const cell: EvalCellResult = {
937
988
  index: 0,
938
- ...(displaySummary(args.summary) === undefined ? {} : { summary: displaySummary(args.summary) ?? "" }),
939
- code: args.code,
940
- language: args.language,
989
+ ...(summary === undefined ? {} : { summary }),
990
+ code,
991
+ language,
941
992
  output: "",
942
993
  status: context.spinnerFrame === undefined ? "pending" : "running",
943
994
  };
@@ -952,6 +1003,14 @@ export function renderEvalCall(
952
1003
  return component;
953
1004
  }
954
1005
 
1006
+ /** A call whose language has not streamed in yet: the headline row alone, until the full frame can render. */
1007
+ function streamingCallLines(summary: string | undefined, code: string, environment: RenderEnvironment): string[] {
1008
+ const icon = environment.spinnerFrame === undefined ? "○" : spinner(environment.spinnerFrame);
1009
+ return renderPrefixed(headlined(icon, summary, code, "eval", environment), environment, LIVE_LINE_PREFIX);
1010
+ }
1011
+
1012
+ const LIVE_LINE_PREFIX: PrefixStyle = { prefix: "╶─ ", continuation: " ", color: "borderAccent" };
1013
+
955
1014
  export function renderEvalResult(
956
1015
  result: AgentToolResult<EvalResultDetails>,
957
1016
  options: ToolRenderResultOptions,
@@ -7,12 +7,16 @@ import {
7
7
  kernelToolsStorage,
8
8
  } from "@code-yeongyu/senpi";
9
9
  import type { KernelToHostMessage } from "../bridge/protocol.ts";
10
+ import { resolveSandbox } from "../config/feature-settings.ts";
10
11
  import { DEFAULT_FOREGROUND_WINDOW_SECONDS, defaultCodemodeSettings } from "../config/settings.ts";
12
+ import type { JsEnvironments } from "../environments/js-environments.ts";
13
+ import type { PackagesInstallEnvironments } from "../environments/packages-install.ts";
11
14
  import {
12
15
  KERNEL_TOOLS_CAPABILITIES,
13
16
  type KernelToolsCapability,
14
17
  type KernelToolsDescribeResult,
15
18
  } from "../kernels/js/kernel-tools-types.ts";
19
+ import { sandboxCellExecutor, sandboxRuntimeInfo } from "../kernels/sandbox/sandbox-cell.ts";
16
20
  import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
17
21
  import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
18
22
  import { CellHandler, type CellState } from "./cell-handler.ts";
@@ -42,7 +46,8 @@ export async function runEvalCell(
42
46
  const bridgeAbortController = new AbortController();
43
47
  const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
44
48
  const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
45
- const runtime = options.runtimes?.[invocation.input.language];
49
+ const runtime =
50
+ invocation.input.isolate === true ? sandboxRuntimeInfo() : options.runtimes?.[invocation.input.language];
46
51
  const state: CellState = {
47
52
  input: invocation.input,
48
53
  ...(runtime === undefined ? {} : { runtime }),
@@ -238,6 +243,7 @@ async function executeCell(
238
243
  ...(kernelTools === undefined ? {} : { kernelTools }),
239
244
  ...(options.handles === undefined ? {} : { handles: options.handles }),
240
245
  hardDeadlineMs: cell.startedAtMs + cell.hardLimitSeconds * 1_000,
246
+ ...cellEnvironments(invocation.input.language, options),
241
247
  });
242
248
  handler = activeHandler;
243
249
  cellManager.bindKernel(
@@ -261,25 +267,50 @@ async function executeCell(
261
267
  }
262
268
  if (invocation.input.reset) await execution.wait(kernel.reset());
263
269
  execution.setKernel(kernel);
264
- const magic = planMagicCell(invocation.input.language, invocation.input.code, options.pythonEnvironments);
270
+ const magic = planMagicCell(
271
+ invocation.input.language,
272
+ invocation.input.code,
273
+ options.pythonEnvironments,
274
+ options.jsEnvironments,
275
+ );
265
276
  // Resolved when the cell's turn comes in the kernel's queue: a %load reads the file the cells ahead of it
266
277
  // wrote, and a refusal settles in queue order like any cell.
267
278
  const loadOptions = { cwd: invocation.ctx.cwd, artifactsDir: options.artifactsDir };
268
- const resolveAtStart =
269
- magic.kind === "load"
279
+ // An isolated cell never runs a file outside its sandbox: %load there is refused.
280
+ const isolatedLoad = invocation.input.isolate === true && magic.kind === "load";
281
+ const resolveAtStart = isolatedLoad
282
+ ? () => ({
283
+ ok: false as const,
284
+ message:
285
+ "isolate: true cannot run a %load cell: an isolated cell sees no host files, and loading one would break that isolation. Run %load in a normal cell, or paste the code into the isolated cell.",
286
+ })
287
+ : magic.kind === "load"
270
288
  ? () => loadCell(magic.target, loadOptions)
271
289
  : magic.kind === "refused"
272
290
  ? () => ({ ok: false as const, message: magic.message })
273
291
  : undefined;
292
+ const isolated =
293
+ invocation.input.isolate === true && resolveAtStart === undefined
294
+ ? sandboxCellExecutor(invocation.input.code, {
295
+ sandbox: resolveSandbox(options.settings ?? {}),
296
+ executeTool: options.executeTool,
297
+ toolNames: () => (options.listTools?.() ?? []).map((tool) => tool.name),
298
+ describeTool: (name) => options.listTools?.().find((tool) => tool.name === name)?.description,
299
+ })
300
+ : undefined;
274
301
  const environments = invocation.input.language === "py" ? options.pythonEnvironments : undefined;
275
302
  const envRoot = environments === undefined ? undefined : () => environments.activeRoot ?? "";
303
+ const jsEnvironments = invocation.input.language === "js" ? options.jsEnvironments : undefined;
304
+ const packageRoot = jsEnvironments === undefined ? undefined : () => jsEnvironments.packageRoot;
276
305
  const result = await execution.wait(
277
306
  kernel.run({
278
307
  cellId: invocation.cellId,
279
308
  code: invocation.input.code,
280
309
  ...(resolveAtStart === undefined ? {} : { resolveAtStart }),
281
310
  ...(magic.kind === "host" ? { host: magic.executor } : {}),
311
+ ...(isolated === undefined ? {} : { host: isolated }),
282
312
  ...(envRoot === undefined ? {} : { envRoot }),
313
+ ...(packageRoot === undefined ? {} : { packageRoot }),
283
314
  ...(bridgeCellToken === undefined ? {} : { bridgeCellToken }),
284
315
  kernelPreludes: options.kernelPreludes?.(),
285
316
  onMessage,
@@ -336,3 +367,14 @@ function kernelToolsFor(kernel: EvalKernel): KernelToolsCapability | undefined {
336
367
  invoke: (request, options) => withTools.invokeKernelTool(request, options),
337
368
  } satisfies ExtensionKernelTools;
338
369
  }
370
+
371
+ // Only JS cells call tools in-process; a Python cell reaches packages.install() over the bridge, whose session manager
372
+ // holds the same Python environment its magics use.
373
+ function cellEnvironments(
374
+ language: string,
375
+ options: { readonly jsEnvironments?: JsEnvironments },
376
+ ): { readonly environments?: PackagesInstallEnvironments } {
377
+ if (language === "js" && options.jsEnvironments !== undefined)
378
+ return { environments: { js: options.jsEnvironments } };
379
+ return {};
380
+ }
@@ -10,7 +10,8 @@ const ELLIPSIS = "\u2026";
10
10
  * runtime name because node and bun are otherwise indistinguishable.
11
11
  */
12
12
  export function formatRuntimeBadge(language: EvalLanguage, runtime: EvalRuntimeInfo, home: string = homedir()): string {
13
- const label = language === "js" ? `${runtime.name} ${runtime.version}` : runtime.version;
13
+ const base = language === "js" ? `${runtime.name} ${runtime.version}` : runtime.version;
14
+ const label = runtime.isolation === undefined ? base : `${base}, ${runtime.isolation}`;
14
15
  if (runtime.path === undefined || runtime.path.length === 0) return label;
15
16
  return `${label}, ${minifyPath(runtime.path, home)}`;
16
17
  }
package/src/tool/types.ts CHANGED
@@ -52,6 +52,8 @@ export interface EvalToolInput {
52
52
  readonly timeout?: number;
53
53
  readonly on_timeout?: "detach" | "error";
54
54
  readonly reset?: boolean;
55
+ /** Run in a fresh sandbox VM instead of the persistent kernel; accepted only while `sandbox.enabled` is on. */
56
+ readonly isolate?: boolean;
55
57
  }
56
58
 
57
59
  export interface EvalListInput {
@@ -117,14 +119,26 @@ const fullEvalInputSchema = Type.Object(
117
119
  /** Runtime accepts a discriminated run/control union. */
118
120
  export type EvalInputSchema = TUnsafe<EvalToolRequest> & Pick<typeof fullEvalInputSchema, "properties">;
119
121
 
122
+ const ISOLATE_FIELD_DESCRIPTION =
123
+ "js only. Fresh sandbox per call: no persistence, no fs/net/process/timers; only tool.* plus print/display.";
124
+
120
125
  export function createEvalInputSchema(
121
126
  enabled: EnabledEvalLanguages,
122
127
  deadlines: EvalDeadlineSeconds = defaultEvalDeadlineSeconds,
128
+ options: { readonly sandbox?: boolean } = {},
123
129
  ): EvalInputSchema {
124
130
  const languages = enabledLanguageList(enabled);
125
131
  if (languages.length === 0) throw new Error("eval requires at least one enabled language");
126
132
  const languageSchema = evalLanguageUnion(languages);
127
- const properties = evalInputProperties(languageSchema, deadlines);
133
+ // The sandbox field exists in the schema only while sandbox cells are turned on, so the default schema
134
+ // (and the prompt built from it) stays exactly what it was.
135
+ const properties =
136
+ options.sandbox === true && languages.includes("js")
137
+ ? {
138
+ ...evalInputProperties(languageSchema, deadlines),
139
+ isolate: Type.Optional(Type.Boolean({ description: ISOLATE_FIELD_DESCRIPTION })),
140
+ }
141
+ : evalInputProperties(languageSchema, deadlines);
128
142
  return Type.Unsafe<EvalToolRequest>(
129
143
  Type.Object(properties, {
130
144
  // Keep branches self-contained for Mistral-hosted GLM (#2240).
@@ -175,6 +189,12 @@ export interface EvalKernelRunInput {
175
189
  readonly envRoot?: () => string;
176
190
  /** The file a `%load` cell runs: tracebacks name it and its relative imports resolve from its directory. */
177
191
  readonly sourceFile?: string;
192
+ /**
193
+ * JavaScript only: the session's managed package revision; bare imports that do not resolve from cwd fall back to
194
+ * it. Read when the cell starts running (not when it was queued), so a cell queued behind `%bun add` resolves the
195
+ * revision that install published.
196
+ */
197
+ readonly packageRoot?: () => string | undefined;
178
198
  /**
179
199
  * Called when the cell's turn comes in the kernel's queue, never earlier: what to run instead of `code` (a `%load`
180
200
  * cell's file), or a refusal that settles the cell as failed in queue order.
@@ -259,6 +279,8 @@ export interface EvalRuntimeInfo {
259
279
  readonly name: string;
260
280
  readonly version: string;
261
281
  readonly path?: string;
282
+ /** JS only: the kernel runs in a child process (`process`), or the cell ran in an isolated QuickJS VM (`sandbox`). */
283
+ readonly isolation?: "process" | "sandbox";
262
284
  }
263
285
 
264
286
  export type EvalRuntimes = Readonly<Partial<Record<EvalLanguage, EvalRuntimeInfo>>>;