@code-yeongyu/senpi-codemode 2026.9.27 → 2026.9.28-3

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 CHANGED
@@ -12,6 +12,82 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.28-3] - 2026-09-28
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ - `new Bun.WebView()` in a JavaScript eval cell no longer fails with `Bun.WebView with backend "chrome" is only available on the main thread` (every call on Windows and Linux, `backend: "chrome"` on macOS). Cells see a `Bun` whose `WebView` (also through `import { WebView } from "bun"`) hands Chrome-backed views to the process main thread with the same API: navigation, input, `evaluate`, screenshots, `cdp()` and its events, `console` capture, `url`/`title`/`loading`, `close()` and `await using`. The macOS default (WebKit) stays a native view in the kernel worker. `Bun.WebView.closeAll()` in a cell closes only that kernel's views. ([#2248](https://github.com/code-yeongyu/senpi/issues/2248))
26
+
27
+ ### Removed
28
+
29
+ ## [2026.9.28-2] - 2026-09-28
30
+
31
+ ### Breaking Changes
32
+
33
+ ### Added
34
+
35
+ ### Changed
36
+
37
+ ### Fixed
38
+
39
+ ### Removed
40
+
41
+ ## [2026.9.28] - 2026-09-28
42
+
43
+ ### Breaking Changes
44
+
45
+ ### Added
46
+
47
+ ### Changed
48
+
49
+ ### Fixed
50
+
51
+ ### Removed
52
+
53
+ ## [2026.9.27-4] - 2026-09-27
54
+
55
+ ### Breaking Changes
56
+
57
+ ### Added
58
+
59
+ ### Changed
60
+
61
+ ### Fixed
62
+
63
+ ### Removed
64
+
65
+ ## [2026.9.27-3] - 2026-09-27
66
+
67
+ ### Breaking Changes
68
+
69
+ ### Added
70
+
71
+ ### Changed
72
+
73
+ ### Fixed
74
+
75
+ ### Removed
76
+
77
+ ## [2026.9.27-2] - 2026-09-27
78
+
79
+ ### Breaking Changes
80
+
81
+ ### Added
82
+
83
+ - The eval kernels install each active tool's `kernelPrelude`: its JavaScript or Python statements run before a cell whenever one of the prelude's exports is missing, a deactivated tool's exports are removed before the next cell, and each prelude's documentation line joins the eval prompt's helper list. Exports that would shadow a built-in helper are rejected. ([#2178](https://github.com/code-yeongyu/senpi/pull/2178))
84
+
85
+ ### Changed
86
+
87
+ ### Fixed
88
+
89
+ ### Removed
90
+
15
91
  ## [2026.9.27] - 2026-09-27
16
92
 
17
93
  ### Breaking Changes
package/README.md CHANGED
@@ -162,6 +162,13 @@ into the cell carries the tool's expected parameters, so the cell can correct th
162
162
  arguments and retry instead of falling back to one-at-a-time tool calls.
163
163
  `tool_schema()` exposes the same catalog up front.
164
164
 
165
+ A tool may also contribute globals of its own through `ToolDefinition.kernelPrelude`
166
+ (JavaScript and Python snippets that call the ordinary `tool.<name>()`, one
167
+ documentation line, and the exported names). While the tool is active, each
168
+ JavaScript and Python cell installs any missing export first and the eval prompt
169
+ lists the documentation line; once the tool is deactivated, the next cell deletes
170
+ those names. Exports that shadow a built-in helper are rejected by the host.
171
+
165
172
  `agent()` is available only when the configured task tool is active in the
166
173
  session. `output()` similarly requires the configured task-output tool and
167
174
  returns immediately: a running task reports its current status, while completed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.9.27",
3
+ "version": "2026.9.28-3",
4
4
  "description": "Source-only senpi extension package for codemode evaluation tools",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -30,14 +30,14 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@babel/parser": "8.0.4",
33
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.27",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.28-3",
34
34
  "typebox": "1.3.34"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.9.27"
37
+ "@code-yeongyu/senpi": "2026.9.28-3"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.9.27"
40
+ "@code-yeongyu/senpi": "2026.9.28-3"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -37,6 +37,15 @@ const hostToKernelMessageSchema = Type.Union([
37
37
  cellId: Type.String({ minLength: 1 }),
38
38
  code: Type.String(),
39
39
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
40
+ /** Python only: active tool globals to install before the cell and deactivated ones to remove. */
41
+ preludes: Type.Optional(
42
+ Type.Object({
43
+ install: Type.Array(
44
+ Type.Object({ exports: Type.Array(Type.String({ minLength: 1 })), python: Type.String() }),
45
+ ),
46
+ remove: Type.Array(Type.String({ minLength: 1 })),
47
+ }),
48
+ ),
40
49
  }),
41
50
  Type.Object({
42
51
  type: Type.Literal("tool-reply"),
@@ -57,6 +66,19 @@ const hostToKernelMessageSchema = Type.Union([
57
66
  Type.Object({
58
67
  type: Type.Literal("close"),
59
68
  }),
69
+ /** JS only: the kernel's private port to the main-thread Bun.WebView service, sent in the transfer list. */
70
+ Type.Object({
71
+ type: Type.Literal("webview-port"),
72
+ requestId: Type.String({ minLength: 1 }),
73
+ ok: Type.Literal(true),
74
+ port: Type.Unknown(),
75
+ }),
76
+ Type.Object({
77
+ type: Type.Literal("webview-port"),
78
+ requestId: Type.String({ minLength: 1 }),
79
+ ok: Type.Literal(false),
80
+ error: bridgeErrorSchema,
81
+ }),
60
82
  ...kernelToolHostToKernelSchemas,
61
83
  ]);
62
84
 
@@ -97,6 +119,7 @@ const kernelToHostMessageSchema = Type.Union([
97
119
  durationMs: Type.Integer({ minimum: 0 }),
98
120
  }),
99
121
  Type.Object({ type: Type.Literal("closed") }),
122
+ Type.Object({ type: Type.Literal("webview-connect"), requestId: Type.String({ minLength: 1 }) }),
100
123
  ...kernelToolKernelToHostSchemas,
101
124
  ]);
102
125
 
@@ -1,3 +1,4 @@
1
+ import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
1
2
  import { type Static, Type } from "typebox";
2
3
  import { Check, Errors } from "typebox/value";
3
4
 
@@ -12,6 +13,7 @@ export interface EvalSchemaToolInfo {
12
13
  readonly name: string;
13
14
  readonly description?: string | undefined;
14
15
  readonly parameters?: unknown;
16
+ readonly kernelPrelude?: KernelPreludeContribution | undefined;
15
17
  }
16
18
 
17
19
  export interface RunEvalSchemaOptions {
@@ -0,0 +1,30 @@
1
+ import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
2
+ import type { CodemodeRuntimeAPI } from "./runtime-factory.ts";
3
+
4
+ type ToolRegistry = Pick<CodemodeRuntimeAPI, "getActiveTools" | "getAllTools">;
5
+
6
+ /**
7
+ * Kernel globals of the tools active right now. Read per cell, so a tool activated by `tool_search`, a by-name
8
+ * call, or a command shows up in the next cell and a deactivated one disappears, whatever the registration order.
9
+ */
10
+ export function activeKernelPreludes(pi: ToolRegistry): readonly KernelPreludeContribution[] {
11
+ const active = new Set(pi.getActiveTools());
12
+ return pi.getAllTools().flatMap((tool) => (tool.kernelPrelude && active.has(tool.name) ? [tool.kernelPrelude] : []));
13
+ }
14
+
15
+ /** Identity of the prompt's contribution doc lines; the eval tool re-registers only when it changes. */
16
+ export function kernelPreludeDocsKey(preludes: readonly KernelPreludeContribution[]): string {
17
+ return preludes.map((prelude) => prelude.documentation).join("\n");
18
+ }
19
+
20
+ /**
21
+ * The contributions the eval description documents. Like the monitor probe, an unreadable registry (the loader's
22
+ * action methods throw until the runtime binds) teaches nothing; the per-cell read still surfaces any error.
23
+ */
24
+ export function promptKernelPreludes(pi: ToolRegistry): readonly KernelPreludeContribution[] {
25
+ try {
26
+ return activeKernelPreludes(pi);
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  import { EvalNotifier } from "./extension/eval-notifier.ts";
14
14
  import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
15
15
  import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
16
+ import { activeKernelPreludes, kernelPreludeDocsKey, promptKernelPreludes } from "./extension/kernel-preludes.ts";
16
17
  import {
17
18
  createExecuteTool,
18
19
  createRuntime,
@@ -43,7 +44,7 @@ const SESSION_LIFECYCLE_EVENTS = [
43
44
 
44
45
  type SessionLifecycleEvent = (typeof SESSION_LIFECYCLE_EVENTS)[number];
45
46
 
46
- type CodemodeEvent = SessionLifecycleEvent | "model_select";
47
+ type CodemodeEvent = SessionLifecycleEvent | "model_select" | "turn_start";
47
48
 
48
49
  export interface CodemodeExtensionAPI {
49
50
  registerTool(tool: ReturnType<typeof createEvalTool>): void;
@@ -91,6 +92,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
91
92
  let activeModelId: string | undefined;
92
93
  let activeContext: ExtensionContext | undefined;
93
94
  let activeCells: EvalDetachedCellManager | undefined;
95
+ let promptPreludeDocs = "";
94
96
  const notifier = new EvalNotifier({
95
97
  sendMessage: (message, notifyOptions) => pi.sendMessage(message, notifyOptions),
96
98
  getContext: () => activeContext,
@@ -134,6 +136,8 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
134
136
  // reaches this before the runtime is bound). An unreadable registry means "do not teach
135
137
  // a tool we cannot confirm"; session_start / model_select re-register once it is live.
136
138
  const monitor = monitorIsRegistered(pi);
139
+ const preludes = promptKernelPreludes(pi);
140
+ promptPreludeDocs = kernelPreludeDocsKey(preludes);
137
141
  pi.registerTool(
138
142
  createEvalTool({
139
143
  enabledLanguages: runtime.enabledLanguages,
@@ -156,6 +160,8 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
156
160
  spawnDefaultAgent: runtime.settings.taskTools.task,
157
161
  hostLine: hostLine(),
158
162
  runtimes: runtime.runtimes,
163
+ kernelPreludes: () => activeKernelPreludes(pi),
164
+ promptKernelPreludes: preludes,
159
165
  ...(bunSkillPath === undefined ? {} : { bunSkillPath }),
160
166
  ...(modelId === undefined ? {} : { modelId }),
161
167
  }),
@@ -252,6 +258,14 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
252
258
  if (cellManager === undefined) return;
253
259
  registerEvalForRuntime(runtime, modelId, cellManager);
254
260
  });
261
+ // Tool activation has no event of its own; the next turn re-documents a changed contribution set.
262
+ pi.on("turn_start", async () => {
263
+ const runtime = activeRuntime;
264
+ const cellManager = activeCells;
265
+ if (runtime === undefined || cellManager === undefined) return;
266
+ if (kernelPreludeDocsKey(promptKernelPreludes(pi)) === promptPreludeDocs) return;
267
+ registerEvalForRuntime(runtime, activeModelId, cellManager);
268
+ });
255
269
  }
256
270
 
257
271
  function hostLine(): string {
@@ -195,7 +195,7 @@ export class JavaScriptKernel {
195
195
  this.#slot.postMessage({
196
196
  type: "run",
197
197
  cellId: next.input.cellId,
198
- code: this.#moduleLoader.prepareCell(next.input.code),
198
+ code: this.#moduleLoader.prepareCell(next.input.code, next.input.kernelPreludes),
199
199
  timeoutMs: next.input.timeoutMs,
200
200
  });
201
201
  }
@@ -1,5 +1,6 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
+ import type { MessagePort } from "node:worker_threads";
3
4
  import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
4
5
  import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
5
6
  import type { JavaScriptKernelMode } from "./kernel-contract.ts";
@@ -18,7 +19,7 @@ export function resolveInlineWorkerEntryUrl(options: JavaScriptInlineWorkerEntry
18
19
 
19
20
  export interface WorkerLike {
20
21
  readonly mode: JavaScriptKernelMode;
21
- postMessage(message: HostToKernelMessage): void;
22
+ postMessage(message: HostToKernelMessage, transfer?: readonly MessagePort[]): void;
22
23
  onMessage(handler: (message: KernelToHostMessage) => void): () => void;
23
24
  onError(handler: (error: Error) => void): () => void;
24
25
  terminate(): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import { join, resolve, sep } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
+ import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
3
4
  import type { BridgeConnectionConfig } from "../../bridge/protocol.ts";
4
5
  import {
5
6
  RESERVED_AGENT_TOOL,
@@ -8,6 +9,7 @@ import {
8
9
  TIMEOUT_PAUSE_OP,
9
10
  TIMEOUT_RESUME_OP,
10
11
  } from "../../bridge/reserved.ts";
12
+ import { type KernelPreludePlan, KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
11
13
  import type { JavaScriptKernelOptions as BaseJavaScriptKernelOptions } from "./kernel-contract.ts";
12
14
  import { rewriteImports } from "./rewrite-imports.ts";
13
15
 
@@ -75,6 +77,8 @@ function loaderPrelude(context: RuntimeModuleContext): string {
75
77
  "globalThis.__senpi_import__ = async (source, options) => {",
76
78
  " const context = globalThis.__senpi_module_context__;",
77
79
  " const specifier = String(source);",
80
+ " const kernelBun = globalThis[Symbol.for('senpi.kernel.bun')];",
81
+ " if (specifier === 'bun' && kernelBun) return { ...(await import('bun')), WebView: kernelBun.WebView, default: kernelBun };",
78
82
  " const match = /^([a-z][a-z0-9+.-]*):\\/\\/(.*)$/i.exec(specifier);",
79
83
  " let target = specifier;",
80
84
  " if (match) {",
@@ -99,14 +103,26 @@ function loaderPrelude(context: RuntimeModuleContext): string {
99
103
  ].join("\n");
100
104
  }
101
105
 
106
+ /** Removes deactivated exports, then installs each contribution only while one of its exports is missing. */
107
+ function contributionPrelude(plan: KernelPreludePlan): string {
108
+ const removals = plan.remove.map((name) => `delete globalThis[${JSON.stringify(name)}];`);
109
+ const installs = plan.install.map((contribution) => {
110
+ const missing = contribution.exports.map((name) => `!globalThis[${JSON.stringify(name)}]`).join(" || ");
111
+ return `if (${missing}) {\n${contribution.javascript}\n}`;
112
+ });
113
+ return [...removals, ...installs].join("\n");
114
+ }
115
+
102
116
  export class LocalModuleLoader {
103
117
  readonly #prelude: string;
118
+ readonly #contributions = new KernelPreludeTracker();
104
119
 
105
120
  constructor(options: LocalModuleLoaderOptions) {
106
121
  this.#prelude = loaderPrelude(runtimeContext(options));
107
122
  }
108
123
 
109
- prepareCell(code: string): string {
110
- return `${PREPARED_CELL_PREFIX}${JSON.stringify({ prelude: this.#prelude, code: rewriteImports(code) })}`;
124
+ prepareCell(code: string, contributions: readonly KernelPreludeContribution[] = []): string {
125
+ const prelude = `${this.#prelude}\n${contributionPrelude(this.#contributions.plan(contributions))}`;
126
+ return `${PREPARED_CELL_PREFIX}${JSON.stringify({ prelude, code: rewriteImports(code) })}`;
111
127
  }
112
128
  }
@@ -0,0 +1,59 @@
1
+ import type { MessagePort } from "node:worker_threads";
2
+ import type { WebViewServiceConnection } from "@code-yeongyu/senpi";
3
+ import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
4
+ import { bridgeError } from "./worker-host.ts";
5
+
6
+ export type WebViewConnector = () => Promise<WebViewServiceConnection>;
7
+
8
+ // Loaded on the first WebView a cell asks for, so starting a kernel never pays for the host barrel.
9
+ const connectThroughHost: WebViewConnector = async () => (await import("@code-yeongyu/senpi")).connectWebViewService();
10
+
11
+ /**
12
+ * The WebView clients one worker generation asked for. Chrome-backed `Bun.WebView`s only run on the
13
+ * process main thread, so the worker gets a private port to the main-thread service; retiring the
14
+ * generation (reset, close, crash, timeout) releases every view created through those ports.
15
+ */
16
+ export class KernelWebViewClients {
17
+ readonly #post: (message: HostToKernelMessage, transfer: readonly MessagePort[]) => void;
18
+ readonly #connect: WebViewConnector;
19
+ readonly #connections = new Set<WebViewServiceConnection>();
20
+ #retired = false;
21
+
22
+ constructor(
23
+ post: (message: HostToKernelMessage, transfer: readonly MessagePort[]) => void,
24
+ connect: WebViewConnector = connectThroughHost,
25
+ ) {
26
+ this.#post = post;
27
+ this.#connect = connect;
28
+ }
29
+
30
+ consume(message: KernelToHostMessage): boolean {
31
+ if (message.type !== "webview-connect") return false;
32
+ void this.#grant(message.requestId);
33
+ return true;
34
+ }
35
+
36
+ async release(): Promise<void> {
37
+ this.#retired = true;
38
+ const connections = [...this.#connections];
39
+ this.#connections.clear();
40
+ await Promise.allSettled(connections.map((connection) => connection.release()));
41
+ }
42
+
43
+ async #grant(requestId: string): Promise<void> {
44
+ let connection: WebViewServiceConnection;
45
+ try {
46
+ connection = await this.#connect();
47
+ } catch (error) {
48
+ const cause = error instanceof Error ? error : new Error(String(error));
49
+ if (!this.#retired) this.#post({ type: "webview-port", requestId, ok: false, error: bridgeError(cause) }, []);
50
+ return;
51
+ }
52
+ if (this.#retired) {
53
+ await connection.release();
54
+ return;
55
+ }
56
+ this.#connections.add(connection);
57
+ this.#post({ type: "webview-port", requestId, ok: true, port: connection.port }, [connection.port]);
58
+ }
59
+ }
@@ -3,6 +3,7 @@ import { kernelToolError } from "./kernel-tools-errors.js";
3
3
  import { createKernelToolPump } from "./kernel-tools-pump.js";
4
4
  import { hostDeniedError, hostToolRefusal } from "./kernel-tools-scope.js";
5
5
  import { JsWorkerRuntime } from "./worker-runtime.js";
6
+ import { installKernelWebView } from "./worker-webview.js";
6
7
 
7
8
  // Mirrors INTERRUPT_ACK_OP and CHILD_LIFECYCLE_OP in src/bridge/reserved.ts (this worker file cannot import TypeScript).
8
9
  const INTERRUPT_ACK_OP = "interrupt-ack";
@@ -25,6 +26,7 @@ export function createWorkerCore(transport, options) {
25
26
  let runtime = null;
26
27
  let activeCell = null;
27
28
  const pendingTools = new Map();
29
+ const pendingWebViewPorts = new Map();
28
30
  const nestedInvokes = new Map();
29
31
  const kernelTools = createKernelToolPump({
30
32
  getRuntime: () => runtime,
@@ -73,6 +75,13 @@ export function createWorkerCore(transport, options) {
73
75
  return await promise;
74
76
  }
75
77
 
78
+ function requestWebViewPort() {
79
+ const requestId = crypto.randomUUID();
80
+ const promise = new Promise((resolve, reject) => pendingWebViewPorts.set(requestId, { resolve, reject }));
81
+ emit({ type: "webview-connect", requestId });
82
+ return promise;
83
+ }
84
+
76
85
  function interruptCell(reason) {
77
86
  if (!activeCell || !runtime) return;
78
87
  emit({ type: "status", event: { op: INTERRUPT_ACK_OP, cellId: activeCell.cellId } });
@@ -92,8 +101,16 @@ export function createWorkerCore(transport, options) {
92
101
  runtime?.kernelTools.setCollisionNames(message.hostToolNames ?? [], message.foreignLanguageNames ?? []);
93
102
  return;
94
103
  }
104
+ if (message.type === "webview-port") {
105
+ const pending = pendingWebViewPorts.get(message.requestId);
106
+ pendingWebViewPorts.delete(message.requestId);
107
+ if (message.ok) pending?.resolve(message.port);
108
+ else pending?.reject(errorFromBridge(message.error));
109
+ return;
110
+ }
95
111
  if (message.type === "init") {
96
112
  applySessionEnvironment(message.sessionEnv);
113
+ installKernelWebView(requestWebViewPort);
97
114
  runtime = new JsWorkerRuntime({
98
115
  cwd: options.cwd,
99
116
  parallelPoolWidth: options.parallelPoolWidth,
@@ -88,7 +88,8 @@ export function bridgeError(error: Error): {
88
88
  function wrapNodeWorker(worker: Worker, mode: JavaScriptKernelMode): WorkerLike {
89
89
  return {
90
90
  mode,
91
- postMessage: (message) => worker.postMessage(message),
91
+ postMessage: (message, transfer) =>
92
+ worker.postMessage(message, transfer === undefined ? undefined : [...transfer]),
92
93
  onMessage(handler) {
93
94
  const listener = (message: KernelToHostMessage): void => handler(message);
94
95
  worker.on("message", listener);
@@ -6,6 +6,7 @@ import { encodeDisplayImage, resolveDisplayOps } from "./display-image.js";
6
6
  import { terminateProcessTrees } from "./process-tree.js";
7
7
  import { awaitMaybePromise, indirectEval, wrapUserCode } from "./worker-indirect-eval.js";
8
8
  import { installShellCapture } from "./worker-shell-capture.js";
9
+ import { bindKernelBun } from "./worker-webview.js";
9
10
  import { createWorkpool } from "./workpool.js";
10
11
  import { inKernelToolInvoke } from "./kernel-tools-context.js";
11
12
  import { kernelToolError } from "./kernel-tools-errors.js";
@@ -57,7 +58,7 @@ export class JsWorkerRuntime {
57
58
  ({ prelude, code: cellCode } = prepared);
58
59
  }
59
60
  if (prelude) indirectEval(prelude, `${cellId}:prelude`);
60
- const value = await awaitMaybePromise(indirectEval(wrapUserCode(cellCode), cellId));
61
+ const value = await awaitMaybePromise(indirectEval(bindKernelBun(wrapUserCode(cellCode)), cellId));
61
62
  await this.#drainPendingDisplays();
62
63
  return value;
63
64
  } finally {
@@ -1,8 +1,10 @@
1
+ import type { MessagePort } from "node:worker_threads";
1
2
  import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
2
3
  import type { WorkerLike } from "./inline-worker.ts";
3
4
  import { retireWorker, type WorkerRetirement } from "./interrupt-bounds.ts";
4
5
  import type { JavaScriptKernelMode } from "./kernel-contract.ts";
5
6
  import type { JavaScriptKernelOptions } from "./local-module-loader.ts";
7
+ import { KernelWebViewClients } from "./webview-host.ts";
6
8
  import { WorkerStartupCancelledError } from "./worker-host.ts";
7
9
  import { startWorkerWithInlineFallback } from "./worker-startup.ts";
8
10
 
@@ -17,6 +19,7 @@ export class WorkerSlot {
17
19
  readonly #options: JavaScriptKernelOptions;
18
20
  readonly #listeners: WorkerSlotListeners;
19
21
  #worker: WorkerLike | null = null;
22
+ #webViews: KernelWebViewClients | null = null;
20
23
  #mode: JavaScriptKernelMode = "worker";
21
24
  #generation = 0;
22
25
  #ready: Promise<void> | null = null;
@@ -43,8 +46,8 @@ export class WorkerSlot {
43
46
  return this.#generation;
44
47
  }
45
48
 
46
- postMessage(message: HostToKernelMessage): void {
47
- this.#worker?.postMessage(message);
49
+ postMessage(message: HostToKernelMessage, transfer?: readonly MessagePort[]): void {
50
+ this.#worker?.postMessage(message, transfer);
48
51
  }
49
52
 
50
53
  async ensureReady(): Promise<void> {
@@ -89,16 +92,22 @@ export class WorkerSlot {
89
92
  this.#startupAbort = null;
90
93
  this.#ready = null;
91
94
  const worker = this.#worker;
95
+ const webViews = this.#webViews;
92
96
  this.#worker = null;
93
- if (!worker) return "terminated";
94
- return await retireWorker(worker);
97
+ this.#webViews = null;
98
+ const retirement = worker ? await retireWorker(worker) : "terminated";
99
+ await webViews?.release();
100
+ return retirement;
95
101
  }
96
102
 
97
103
  #publish(worker: WorkerLike, generation: number): void {
98
104
  if (!this.#listeners.isOpen() || generation !== this.#generation) throw new WorkerStartupCancelledError();
99
105
  this.#worker = worker;
106
+ const webViews = new KernelWebViewClients((message, transfer) => worker.postMessage(message, transfer));
107
+ this.#webViews = webViews;
100
108
  worker.onMessage((message) => {
101
- if (this.#isCurrent(worker, generation)) this.#listeners.onMessage(message);
109
+ if (!this.#isCurrent(worker, generation) || webViews.consume(message)) return;
110
+ this.#listeners.onMessage(message);
102
111
  });
103
112
  worker.onError((error) => {
104
113
  if (this.#isCurrent(worker, generation)) this.#listeners.onCrash(error);
@@ -0,0 +1,79 @@
1
+ // The kernel worker's end of its private port to the main-thread Bun.WebView service. Mirrors
2
+ // the message shapes in packages/coding-agent/src/core/webview/webview-wire.ts (this worker file
3
+ // cannot import TypeScript); keep the two in sync.
4
+
5
+ export const DELIVER_EVENT = Symbol("senpi.webview.deliver");
6
+
7
+ export function webViewError(wire) {
8
+ const error = new Error(wire?.message ?? "Bun.WebView request failed");
9
+ if (typeof wire?.name === "string") error.name = wire.name;
10
+ if (typeof wire?.code === "string") error.code = wire.code;
11
+ return error;
12
+ }
13
+
14
+ export class WebViewPortClient {
15
+ #port;
16
+ #pending = new Map();
17
+ #views = new Map();
18
+ #nextId = 1;
19
+ #closed = false;
20
+ #onClosed;
21
+
22
+ constructor(port, onClosed) {
23
+ this.#port = port;
24
+ this.#onClosed = onClosed;
25
+ port.on("message", message => this.#receive(message));
26
+ port.on("close", () => this.#fail(new Error("Bun.WebView service connection closed")));
27
+ }
28
+
29
+ register(viewId, view) {
30
+ this.#views.set(viewId, new WeakRef(view));
31
+ }
32
+
33
+ forget(viewId) {
34
+ this.#views.delete(viewId);
35
+ }
36
+
37
+ request(message) {
38
+ if (this.#closed) return Promise.reject(new Error("Bun.WebView service connection closed"));
39
+ const id = this.#nextId++;
40
+ return new Promise((resolve, reject) => {
41
+ this.#pending.set(id, { resolve, reject });
42
+ try {
43
+ this.#port.postMessage({ ...message, id });
44
+ } catch (error) {
45
+ this.#pending.delete(id);
46
+ reject(error);
47
+ }
48
+ });
49
+ }
50
+
51
+ post(message) {
52
+ if (this.#closed) return;
53
+ try {
54
+ this.#port.postMessage(message);
55
+ } catch {
56
+ // A closed port has already released every view on the service side.
57
+ }
58
+ }
59
+
60
+ #receive(message) {
61
+ if (message?.kind === "reply") {
62
+ const pending = this.#pending.get(message.id);
63
+ if (!pending) return;
64
+ this.#pending.delete(message.id);
65
+ if (message.ok) pending.resolve({ value: message.value, state: message.state });
66
+ else pending.reject(webViewError(message.error));
67
+ return;
68
+ }
69
+ if (message?.kind === "event") this.#views.get(message.viewId)?.deref()?.[DELIVER_EVENT](message.event);
70
+ }
71
+
72
+ #fail(error) {
73
+ if (this.#closed) return;
74
+ this.#closed = true;
75
+ for (const pending of this.#pending.values()) pending.reject(error);
76
+ this.#pending.clear();
77
+ this.#onClosed();
78
+ }
79
+ }
@@ -0,0 +1,204 @@
1
+ import { DELIVER_EVENT, WebViewPortClient, webViewError } from "./worker-webview-client.js";
2
+
3
+ // Bun constructs "chrome"-backend WebViews only on the process main thread, and this kernel runs in
4
+ // a worker where `globalThis.Bun` and `Bun.WebView` are non-writable. Cells therefore run with a
5
+ // `Bun` parameter bound to a stand-in whose `WebView` forwards Chrome-backed views to the
6
+ // main-thread service over a private port; every other `Bun.*` member is the real one.
7
+ const KERNEL_BUN = Symbol.for("senpi.kernel.bun");
8
+
9
+ export function bindKernelBun(source) {
10
+ if (!globalThis[KERNEL_BUN]) return source;
11
+ return `((Bun) => ${source})(globalThis[Symbol.for("senpi.kernel.bun")])`;
12
+ }
13
+
14
+ export function installKernelWebView(requestPort) {
15
+ const bun = globalThis.Bun;
16
+ if (typeof bun !== "object" || bun === null || typeof bun.WebView !== "function") return;
17
+ globalThis[KERNEL_BUN] = shadowBun(bun, createKernelWebView(bun.WebView, requestPort));
18
+ }
19
+
20
+ function shadowBun(real, WebView) {
21
+ return new Proxy(
22
+ {},
23
+ {
24
+ get: (_target, key) => (key === "WebView" ? WebView : Reflect.get(real, key)),
25
+ set: (_target, key, value) => Reflect.set(real, key, value),
26
+ has: (_target, key) => key === "WebView" || Reflect.has(real, key),
27
+ ownKeys: () => Reflect.ownKeys(real),
28
+ getOwnPropertyDescriptor: (_target, key) => {
29
+ if (key === "WebView") return { value: WebView, writable: false, enumerable: true, configurable: true };
30
+ const descriptor = Reflect.getOwnPropertyDescriptor(real, key);
31
+ return descriptor && { ...descriptor, configurable: true };
32
+ },
33
+ },
34
+ );
35
+ }
36
+
37
+ // The macOS default (WebKit) stays a native worker view: its host process serves any thread.
38
+ function needsMainThread(options) {
39
+ const backend = options?.backend;
40
+ if (backend === undefined) return process.platform !== "darwin";
41
+ return backend === "chrome" || (typeof backend === "object" && backend !== null && backend.type === "chrome");
42
+ }
43
+
44
+ function closedError(method) {
45
+ const error = new Error(`Invalid state: WebView.${method}: view is closed`);
46
+ error.code = "ERR_INVALID_STATE";
47
+ return error;
48
+ }
49
+
50
+ function createKernelWebView(NativeWebView, requestPort) {
51
+ let connecting = null;
52
+ const proxied = new WeakSet();
53
+ const live = new Set();
54
+ const natives = new Set();
55
+ const finalizer = new FinalizationRegistry(({ client, viewId }) => client.post({ kind: "close", viewId }));
56
+
57
+ function connect() {
58
+ if (!connecting) {
59
+ const attempt = requestPort().then(port => new WebViewPortClient(port, () => (connecting = null)));
60
+ attempt.catch(() => {
61
+ if (connecting === attempt) connecting = null;
62
+ });
63
+ connecting = attempt;
64
+ }
65
+ return connecting;
66
+ }
67
+
68
+ class WebView extends EventTarget {
69
+ static closeAll() {
70
+ for (const view of [...live]) view.close();
71
+ for (const ref of [...natives]) ref.deref()?.close();
72
+ natives.clear();
73
+ }
74
+
75
+ static [Symbol.hasInstance](value) {
76
+ return value instanceof NativeWebView || proxied.has(value);
77
+ }
78
+
79
+ #viewId = crypto.randomUUID();
80
+ #state = { url: "", title: "", loading: false };
81
+ #listening = new Set();
82
+ #console;
83
+ #ready;
84
+ #closed = false;
85
+ onNavigated = null;
86
+ onNavigationFailed = null;
87
+
88
+ constructor(options = {}) {
89
+ if (!needsMainThread(options)) {
90
+ const view = new NativeWebView(options);
91
+ natives.add(new WeakRef(view));
92
+ return view;
93
+ }
94
+ super();
95
+ const { console: capture, ...rest } = options;
96
+ this.#console = capture;
97
+ proxied.add(this);
98
+ live.add(this);
99
+ const viewId = this.#viewId;
100
+ this.#ready = connect().then(async client => {
101
+ client.register(viewId, this);
102
+ finalizer.register(this, { client, viewId }, this);
103
+ const reply = await client.request({ kind: "create", viewId, options: rest, captureConsole: capture !== undefined });
104
+ this.#apply(reply.state);
105
+ return client;
106
+ });
107
+ this.#ready.catch(() => {});
108
+ }
109
+
110
+ get url() {
111
+ return this.#state.url;
112
+ }
113
+
114
+ get title() {
115
+ return this.#state.title;
116
+ }
117
+
118
+ get loading() {
119
+ return this.#state.loading;
120
+ }
121
+
122
+ navigate(url) { return this.#call("navigate", [url]); }
123
+ evaluate(script) { return this.#call("evaluate", [script]); }
124
+ click(...args) { return this.#call("click", args); }
125
+ type(text) { return this.#call("type", [text]); }
126
+ press(...args) { return this.#call("press", args); }
127
+ scroll(dx, dy) { return this.#call("scroll", [dx, dy]); }
128
+ scrollTo(...args) { return this.#call("scrollTo", args); }
129
+ resize(width, height) { return this.#call("resize", [width, height]); }
130
+ back() { return this.#call("back", []); }
131
+ forward() { return this.#call("forward", []); }
132
+ reload() { return this.#call("reload", []); }
133
+ cdp(...args) { return this.#call("cdp", args); }
134
+
135
+ async screenshot(options) {
136
+ const value = await this.#call("screenshot", [options]);
137
+ if (options?.encoding === "buffer" && value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
138
+ return value;
139
+ }
140
+
141
+ addEventListener(type, listener, options) {
142
+ super.addEventListener(type, listener, options);
143
+ if (this.#closed || this.#listening.has(type)) return;
144
+ this.#listening.add(type);
145
+ const viewId = this.#viewId;
146
+ this.#ready.then(client => client.post({ kind: "listen", viewId, event: type }), () => {});
147
+ }
148
+
149
+ close() {
150
+ if (this.#closed) return;
151
+ this.#closed = true;
152
+ live.delete(this);
153
+ const viewId = this.#viewId;
154
+ this.#ready.then(
155
+ client => {
156
+ client.post({ kind: "close", viewId });
157
+ client.forget(viewId);
158
+ finalizer.unregister(this);
159
+ },
160
+ () => {},
161
+ );
162
+ }
163
+
164
+ [Symbol.dispose]() {
165
+ this.close();
166
+ }
167
+
168
+ [Symbol.asyncDispose]() {
169
+ this.close();
170
+ }
171
+
172
+ [DELIVER_EVENT](event) {
173
+ if (event.type === "console") {
174
+ const [type, ...args] = event.args;
175
+ if (typeof this.#console === "function") this.#console(type, ...args);
176
+ else this.#console?.[type]?.(...args);
177
+ } else if (event.type === "navigated") {
178
+ this.#state = { ...this.#state, url: event.url, title: event.title };
179
+ this.onNavigated?.(event.url, event.title);
180
+ } else if (event.type === "navigation-failed") {
181
+ this.onNavigationFailed?.(webViewError(event.error));
182
+ } else if (event.type === "dom") {
183
+ this.dispatchEvent(new MessageEvent(event.name, { data: event.data }));
184
+ }
185
+ }
186
+
187
+ async #call(method, args) {
188
+ if (this.#closed) throw closedError(method);
189
+ const client = await this.#ready;
190
+ if (this.#closed) throw closedError(method);
191
+ const trimmed = [...args];
192
+ while (trimmed.length > 0 && trimmed.at(-1) === undefined) trimmed.pop();
193
+ const reply = await client.request({ kind: "call", viewId: this.#viewId, method, args: trimmed });
194
+ this.#apply(reply.state);
195
+ return reply.value;
196
+ }
197
+
198
+ #apply(state) {
199
+ if (state) this.#state = state;
200
+ }
201
+ }
202
+
203
+ return WebView;
204
+ }
@@ -1,6 +1,7 @@
1
1
  import type { KernelInterruptHandle } from "../../tool/types.ts";
2
2
  import type { KernelToolsInvokeOptions } from "../js/kernel-tools-types.ts";
3
3
  import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
4
+ import { KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
4
5
  import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
5
6
  import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
6
7
 
@@ -20,6 +21,7 @@ export class PythonKernel {
20
21
  #retirement: Promise<void> | null = null;
21
22
  #closePromise: Promise<void> | null = null;
22
23
  #failure: Error | null = null;
24
+ readonly #preludes = new KernelPreludeTracker();
23
25
  #generation = 0;
24
26
  #closed = false;
25
27
 
@@ -180,7 +182,10 @@ export class PythonKernel {
180
182
  if (timeoutMs !== undefined)
181
183
  pending.timeoutTimer = setTimeout(() => this.#timeoutRun(pending, timeoutMs), timeoutMs);
182
184
  try {
183
- this.#transport?.run(pending.input);
185
+ this.#transport?.run({
186
+ ...pending.input,
187
+ preludePlan: this.#preludes.plan(pending.input.kernelPreludes ?? []),
188
+ });
184
189
  } catch (error) {
185
190
  const failure = error instanceof Error ? error : new Error(String(error));
186
191
  this.#rejectRun(pending, failure);
@@ -1020,7 +1020,19 @@ async def run_code(code: Any, want_value: bool) -> Any:
1020
1020
  return None
1021
1021
 
1022
1022
 
1023
- def run_cell(cell_id: str, code: str) -> None:
1023
+ def apply_preludes(preludes: Any) -> None:
1024
+ # Host-computed per cell: globals of tools deactivated since the last cell are dropped,
1025
+ # and an active tool's snippet runs only while one of its exports is missing.
1026
+ if not isinstance(preludes, dict):
1027
+ return
1028
+ for name in preludes.get("remove", []):
1029
+ USER_NS.pop(name, None)
1030
+ for contribution in preludes.get("install", []):
1031
+ if any(name not in USER_NS for name in contribution.get("exports", [])):
1032
+ exec(compile(contribution.get("python", ""), "<kernel-prelude>", "exec"), USER_NS)
1033
+
1034
+
1035
+ def run_cell(cell_id: str, code: str, preludes: Any = None) -> None:
1024
1036
  start = time.monotonic()
1025
1037
  stdout = io.StringIO()
1026
1038
  stderr = io.StringIO()
@@ -1029,6 +1041,7 @@ def run_cell(cell_id: str, code: str) -> None:
1029
1041
  signal.signal(signal.SIGINT, signal.default_int_handler)
1030
1042
  try:
1031
1043
  with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
1044
+ apply_preludes(preludes)
1032
1045
  body, expression = compile_cell(code)
1033
1046
  LOOP.run_until_complete(run_code(body, False))
1034
1047
  value = LOOP.run_until_complete(run_code(expression, True))
@@ -1075,7 +1088,7 @@ def handle(message: dict[str, Any]) -> bool:
1075
1088
  emit({"type": "ready"})
1076
1089
  return True
1077
1090
  if message_type == "run":
1078
- run_cell(str(message.get("cellId", "")), str(message.get("code", "")))
1091
+ run_cell(str(message.get("cellId", "")), str(message.get("code", "")), message.get("preludes"))
1079
1092
  return True
1080
1093
  if message_type == "close":
1081
1094
  emit({"type": "closed"})
@@ -9,6 +9,7 @@ import {
9
9
  type KernelToHostMessage,
10
10
  } from "../../bridge/protocol.ts";
11
11
  import { applySessionEnvironment, type SessionEnvironment } from "../session-env.ts";
12
+ import type { KernelPreludePlan } from "../shared/kernel-prelude-plan.ts";
12
13
  import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
13
14
  import {
14
15
  defaultSpawn,
@@ -30,6 +31,7 @@ export interface PythonTransportRunInput {
30
31
  readonly cellId: string;
31
32
  readonly code: string;
32
33
  readonly timeoutMs?: number;
34
+ readonly preludePlan?: KernelPreludePlan;
33
35
  }
34
36
 
35
37
  export interface PythonTransportOptions {
@@ -115,7 +117,11 @@ export class PythonKernelTransport {
115
117
  }
116
118
 
117
119
  run(input: PythonTransportRunInput): void {
118
- this.#write({ type: "run", cellId: input.cellId, code: input.code, timeoutMs: input.timeoutMs });
120
+ const preludes = input.preludePlan && {
121
+ install: input.preludePlan.install.map(({ exports, python }) => ({ exports: [...exports], python })),
122
+ remove: [...input.preludePlan.remove],
123
+ };
124
+ this.#write({ type: "run", cellId: input.cellId, code: input.code, timeoutMs: input.timeoutMs, preludes });
119
125
  }
120
126
 
121
127
  interrupt(reason: string): void {
@@ -0,0 +1,19 @@
1
+ import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
2
+
3
+ /** What a kernel applies before a cell: contributions to (re)install and exports whose tool is no longer active. */
4
+ export interface KernelPreludePlan {
5
+ readonly install: readonly KernelPreludeContribution[];
6
+ readonly remove: readonly string[];
7
+ }
8
+
9
+ /** Remembers the exports one kernel was last told to hold, so a deactivated tool's globals are removed. */
10
+ export class KernelPreludeTracker {
11
+ #installed: ReadonlySet<string> = new Set();
12
+
13
+ plan(contributions: readonly KernelPreludeContribution[]): KernelPreludePlan {
14
+ const active = new Set(contributions.flatMap((contribution) => contribution.exports));
15
+ const remove = [...this.#installed].filter((name) => !active.has(name));
16
+ this.#installed = active;
17
+ return { install: contributions, remove };
18
+ }
19
+ }
@@ -113,6 +113,8 @@ export class SubprocessRunQueue {
113
113
  case "kernel-tool-invoke-reply":
114
114
  onMessage?.(message);
115
115
  return false;
116
+ case "webview-connect":
117
+ return false;
116
118
  default: {
117
119
  const exhaustive: never = message;
118
120
  return exhaustive;
@@ -69,7 +69,8 @@ log(message) → None
69
69
  Progress line above the status tree.
70
70
  phase(title) → None
71
71
  Phase grouping subsequent status lines.
72
- \`\`\`
72
+ {{#if kernelPreludeDocs}}{{kernelPreludeDocs}}
73
+ {{/if}}\`\`\`
73
74
  </prelude>
74
75
  {{#if spawns}}
75
76
  <workflow>
@@ -1,3 +1,4 @@
1
+ import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
1
2
  import { DEFAULT_MAX_DETACHED_CELLS, DEFAULT_RUN_BUDGET_SECONDS } from "../config/settings.ts";
2
3
  import type { EvalRuntimeInfo } from "../tool/types.ts";
3
4
  import { EVAL_PROMPT_TEMPLATE } from "./eval-prompt-template.ts";
@@ -32,6 +33,8 @@ export interface EvalPromptOptions {
32
33
  readonly runBudgetSeconds?: number;
33
34
  /** Global background capacity advertised to the model. */
34
35
  readonly maxDetachedCells?: number;
36
+ /** Active tools' kernel globals; each documentation line joins the prelude helper list. */
37
+ readonly kernelPreludes?: readonly KernelPreludeContribution[];
35
38
  }
36
39
 
37
40
  /** Prompt dialect for the eval-first batching emphasis. */
@@ -99,6 +102,7 @@ export function buildEvalPrompt(
99
102
  bunSkillPath: options.bunSkillPath ?? "",
100
103
  runBudgetSeconds: String(options.runBudgetSeconds ?? DEFAULT_RUN_BUDGET_SECONDS),
101
104
  maxDetachedCells: String(options.maxDetachedCells ?? DEFAULT_MAX_DETACHED_CELLS),
105
+ kernelPreludeDocs: (options.kernelPreludes ?? []).map((prelude) => prelude.documentation).join("\n"),
102
106
  };
103
107
  const description = renderTemplate(EVAL_PROMPT_TEMPLATE, context)
104
108
  .replace(/\n{3,}/g, "\n\n")
@@ -34,6 +34,7 @@ await Bun.write("page.png", await view.screenshot());
34
34
  ```
35
35
 
36
36
  - macOS: system WebKit, nothing to install. macOS/Linux/Windows: can drive installed Chrome/Chromium/Edge.
37
+ - In the eval js kernel, Chrome-backed views (the default off macOS, or `backend: "chrome"`) are served by the process main thread; the API is the same, a constructor error surfaces on the first awaited call, and `Bun.WebView.closeAll()` closes only this kernel's views. A kernel reset closes every view the kernel left open.
37
38
  - Clicks/scrolls are trusted user input (`event.isTrusted === true`).
38
39
  - Extends `EventTarget`; screenshots are `Blob`s; `.cdp(method, params?)` escape hatch for raw Chrome DevTools Protocol.
39
40
 
@@ -1,4 +1,10 @@
1
- import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext, ToolDefinition } from "@code-yeongyu/senpi";
1
+ import type {
2
+ AgentToolResult,
3
+ AgentToolUpdateCallback,
4
+ ExtensionContext,
5
+ KernelPreludeContribution,
6
+ ToolDefinition,
7
+ } from "@code-yeongyu/senpi";
2
8
  import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
3
9
  import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
4
10
  import type { ResolvedCodemodeSettings } from "../config/settings.ts";
@@ -58,6 +64,10 @@ export interface CreateEvalToolOptions {
58
64
  readonly runtimes?: EvalRuntimes;
59
65
  /** Absolute path of the active bun-1-4 skill; the prompt names it as MUST READ on a bun kernel. */
60
66
  readonly bunSkillPath?: string;
67
+ /** Kernel globals of the tools active when a cell is submitted; read once per cell. */
68
+ readonly kernelPreludes?: () => readonly KernelPreludeContribution[];
69
+ /** Contributions whose documentation lines the description lists; snapshot taken when the tool is (re)registered. */
70
+ readonly promptKernelPreludes?: readonly KernelPreludeContribution[];
61
71
  }
62
72
 
63
73
  export interface EvalCellInvocation {
@@ -54,6 +54,7 @@ export function createEvalTool(options: CreateEvalToolOptions) {
54
54
  ...(options.hostLine === undefined ? {} : { hostLine: options.hostLine }),
55
55
  ...(options.runtimes?.js === undefined ? {} : { jsRuntime: options.runtimes.js }),
56
56
  ...(options.bunSkillPath === undefined ? {} : { bunSkillPath: options.bunSkillPath }),
57
+ ...(options.promptKernelPreludes === undefined ? {} : { kernelPreludes: options.promptKernelPreludes }),
57
58
  });
58
59
  const languages = enabledLanguageList(options.enabledLanguages);
59
60
  const cellManager =
@@ -251,6 +251,7 @@ async function executeCell(
251
251
  kernel.run({
252
252
  cellId: invocation.cellId,
253
253
  code: invocation.input.code,
254
+ kernelPreludes: options.kernelPreludes?.(),
254
255
  onMessage,
255
256
  onStarted: () => {
256
257
  cellManager.markRunning(cell);
package/src/tool/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentToolResult, AgentToolUpdateCallback } from "@code-yeongyu/senpi";
1
+ import type { AgentToolResult, AgentToolUpdateCallback, KernelPreludeContribution } from "@code-yeongyu/senpi";
2
2
  import { type TSchema, type TUnsafe, Type } from "typebox";
3
3
  import type { HostToKernelMessage, KernelToHostMessage } from "../bridge/protocol.ts";
4
4
  import {
@@ -141,6 +141,8 @@ export interface EvalKernelRunInput {
141
141
  readonly timeoutMs?: number;
142
142
  readonly onStarted?: () => void;
143
143
  readonly onMessage?: (message: KernelToHostMessage) => void;
144
+ /** Globals of the tools active when the cell was submitted; kernels without preludes ignore them. */
145
+ readonly kernelPreludes?: readonly KernelPreludeContribution[];
144
146
  }
145
147
 
146
148
  export interface KernelInterruptHandle {