@code-yeongyu/senpi-codemode 2026.9.16 → 2026.9.17

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,48 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.17] - 2026-09-17
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ - `kernelTools.invoke(request, options?)` accepts a per-call execution scope for the nested host calls the invoked closure makes: `{ scope: { tools: { allow?: string[], deny?: string[] } } }`. While that invocation is active, a `tool.<name>()` outside the scope is refused inside the worker with `kernel_tool_host_denied` carrying `{ tool, call_id, reason: "allow" | "deny" }`: the closure sees a rejected promise, the refusal never reaches the host bridge, and the parent's own cells and queue keep the parent's full tool surface. `deny` wins over `allow`, an `allow` list refuses every host tool it does not name, a malformed list fails closed, and the scope lives only for that call — it is dropped when the call settles (including interrupt and reset) and is never persisted. The second argument still accepts a bare `AbortSignal`, and a call without a scope posts exactly the message it always did. Consumers detect the feature through `kernelTools.capabilities.invokeScope === true`; `KERNEL_TOOLS_CAPABILITIES`, `KernelToolsCapabilities`, `KernelToolsInvokeOptions`, `KernelToolsInvokeScope`, `KernelToolsHostScope`, `KernelToolHostDenial` and `KernelToolHostDenialReason` are exported ([#1731](https://github.com/code-yeongyu/senpi/issues/1731)).
22
+
23
+ ### Changed
24
+
25
+ - Kernel-tools types bind to coding-agent's `ExtensionKernelTools` / `KernelToolInvokeOptions` / `KernelToolInvokeScope` (`KERNEL_TOOLS_CAPABILITIES satisfies ExtensionKernelTools["capabilities"]`) so the implementation cannot drift from the host declaration ([#1731](https://github.com/code-yeongyu/senpi/issues/1731)).
26
+
27
+ ### Fixed
28
+
29
+ ### Removed
30
+
31
+ ## [2026.9.16-3] - 2026-09-16
32
+
33
+ ### Breaking Changes
34
+
35
+ ### Added
36
+
37
+ ### Changed
38
+
39
+ ### Fixed
40
+
41
+ ### Removed
42
+
43
+ ## [2026.9.16-2] - 2026-09-16
44
+
45
+ ### Breaking Changes
46
+
47
+ ### Added
48
+
49
+ ### Changed
50
+
51
+ ### Fixed
52
+
53
+ - The JavaScript kernel-tool capability now reaches host tools called from inside a running eval cell. A cell that registers `tool(fn)` can hand those functions to its in-process children through `task`/`agent`/`workpool`, because `ExtensionContext.kernelTools` resolves for the duration of each host tool call the cell makes. It was undefined at the dispatch point — the worker's message loop ran outside the `kernelToolsStorage` scope that only covered the awaited run chain — so every grant was refused with `tools_unavailable` and the capability shipped in the previous release could not be used ([#1754](https://github.com/code-yeongyu/senpi/issues/1754)).
54
+
55
+ ### Removed
56
+
15
57
  ## [2026.9.16] - 2026-09-16
16
58
 
17
59
  ### Breaking Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.9.16",
3
+ "version": "2026.9.17",
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.16",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.17",
34
34
  "typebox": "1.3.27"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.9.16"
37
+ "@code-yeongyu/senpi": "2026.9.17"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.9.16"
40
+ "@code-yeongyu/senpi": "2026.9.17"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -1,10 +1,28 @@
1
1
  import { type Static, Type } from "typebox";
2
2
 
3
+ /** Payload of a `kernel_tool_host_denied` refusal: the host tool, the invoking call, the reason (#1731). */
4
+ const kernelToolHostDenialSchema = Type.Object({
5
+ tool: Type.String({ minLength: 1 }),
6
+ call_id: Type.String({ minLength: 1 }),
7
+ reason: Type.Union([Type.Literal("allow"), Type.Literal("deny")]),
8
+ });
9
+
3
10
  const kernelToolErrorSchema = Type.Object({
4
11
  message: Type.String(),
5
12
  name: Type.Optional(Type.String()),
6
13
  stack: Type.Optional(Type.String()),
7
14
  code: Type.Optional(Type.String()),
15
+ details: Type.Optional(kernelToolHostDenialSchema),
16
+ });
17
+
18
+ /** Per-call execution scope for the nested host calls the invoked closure makes (#1731). */
19
+ export const kernelToolInvokeScopeSchema = Type.Object({
20
+ tools: Type.Optional(
21
+ Type.Object({
22
+ allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
23
+ deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
24
+ }),
25
+ ),
8
26
  });
9
27
 
10
28
  export const kernelToolDescriptorSchema = Type.Object({
@@ -43,6 +61,7 @@ export const kernelToolHostToKernelSchemas = [
43
61
  definition_revision: Type.Integer({ minimum: 1 }),
44
62
  args: Type.Unknown(),
45
63
  call_id: Type.String({ minLength: 1 }),
64
+ scope: Type.Optional(kernelToolInvokeScopeSchema),
46
65
  }),
47
66
  Type.Object({
48
67
  type: Type.Literal("kernel-tool-cancel"),
package/src/index.ts CHANGED
@@ -263,10 +263,17 @@ function modelIdFrom(event: unknown): string | undefined {
263
263
  }
264
264
 
265
265
  export {
266
+ KERNEL_TOOLS_CAPABILITIES,
266
267
  KERNEL_TOOLS_UNSUPPORTED,
267
268
  type KernelToolDescriptor,
269
+ type KernelToolHostDenial,
270
+ type KernelToolHostDenialReason,
271
+ type KernelToolsCapabilities,
268
272
  type KernelToolsCapability,
269
273
  type KernelToolsDescribeResult,
274
+ type KernelToolsHostScope,
275
+ type KernelToolsInvokeOptions,
270
276
  type KernelToolsInvokeRequest,
277
+ type KernelToolsInvokeScope,
271
278
  } from "./kernels/js/kernel-tools-types.ts";
272
279
  export { enabledLanguagesFrom };
@@ -13,7 +13,11 @@ import {
13
13
  } from "./kernel-contract.ts";
14
14
  import { kernelToolError } from "./kernel-tools-errors.ts";
15
15
  import { KernelToolHostPump } from "./kernel-tools-host.ts";
16
- import type { KernelToolsDescribeResult, KernelToolsInvokeRequest } from "./kernel-tools-types.ts";
16
+ import type {
17
+ KernelToolsDescribeResult,
18
+ KernelToolsInvokeOptions,
19
+ KernelToolsInvokeRequest,
20
+ } from "./kernel-tools-types.ts";
17
21
  import { type JavaScriptKernelOptions, LocalModuleLoader } from "./local-module-loader.ts";
18
22
  import { terminateProcessTrees } from "./process-tree-host.ts";
19
23
  import { JavaScriptRunQueue, type PendingJavaScriptRun, stoppedResult } from "./run-queue.ts";
@@ -72,8 +76,11 @@ export class JavaScriptKernel {
72
76
  return this.#kernelTools.describe(names);
73
77
  }
74
78
 
75
- invokeKernelTool(request: KernelToolsInvokeRequest, signal?: AbortSignal): Promise<unknown> {
76
- return this.#kernelTools.invoke(request, signal);
79
+ invokeKernelTool(
80
+ request: KernelToolsInvokeRequest,
81
+ options?: AbortSignal | KernelToolsInvokeOptions,
82
+ ): Promise<unknown> {
83
+ return this.#kernelTools.invoke(request, options);
77
84
  }
78
85
 
79
86
  async run(input: JavaScriptRunInput): Promise<ResultMessage> {
@@ -1,3 +1,5 @@
1
+ import type { KernelToolsInvokeScope } from "./kernel-tools-types.ts";
2
+
1
3
  export const kernelToolCallContext: {
2
4
  run<T>(store: KernelToolCallStore, fn: () => T): T;
3
5
  getStore(): KernelToolCallStore | undefined;
@@ -8,6 +10,8 @@ export type KernelToolCallStore = {
8
10
  readonly callId: string;
9
11
  readonly generation: number;
10
12
  readonly signal: AbortSignal;
13
+ /** Host tools this call's nested bridge calls may reach; absent means the parent's full surface. */
14
+ readonly scope?: KernelToolsInvokeScope;
11
15
  };
12
16
 
13
17
  export function inKernelToolInvoke(): boolean;
@@ -7,16 +7,19 @@ export const KERNEL_TOOL_ERROR_CODES = Object.freeze([
7
7
  "kernel_tool_missing",
8
8
  "kernel_tool_failed",
9
9
  "kernel_tool_recursion",
10
+ "kernel_tool_host_denied",
10
11
  ]);
11
12
 
12
13
  export class KernelToolError extends Error {
13
- constructor(code, message) {
14
+ constructor(code, message, details) {
14
15
  super(message);
15
16
  this.name = "KernelToolError";
16
17
  this.code = code;
18
+ // Only `kernel_tool_host_denied` carries one today: { tool, call_id, reason }.
19
+ if (details !== undefined) this.details = details;
17
20
  }
18
21
  }
19
22
 
20
- export function kernelToolError(code, message) {
21
- return new KernelToolError(code, message);
23
+ export function kernelToolError(code, message, details) {
24
+ return new KernelToolError(code, message, details);
22
25
  }
@@ -7,20 +7,38 @@ export const KERNEL_TOOL_ERROR_CODES = [
7
7
  "kernel_tool_missing",
8
8
  "kernel_tool_failed",
9
9
  "kernel_tool_recursion",
10
+ "kernel_tool_host_denied",
10
11
  ] as const;
11
12
 
12
13
  export type KernelToolErrorCode = (typeof KERNEL_TOOL_ERROR_CODES)[number];
13
14
 
15
+ /** Why a nested host call was refused: it is outside the call's allow list, or named by its deny list. */
16
+ export type KernelToolHostDenialReason = "allow" | "deny";
17
+
18
+ /** Payload carried by `kernel_tool_host_denied`: the host tool, the invoking kernel-tool call, the reason. */
19
+ export type KernelToolHostDenial = {
20
+ readonly tool: string;
21
+ readonly call_id: string;
22
+ readonly reason: KernelToolHostDenialReason;
23
+ };
24
+
14
25
  export class KernelToolError extends Error {
15
26
  readonly name = "KernelToolError";
16
27
  readonly code: KernelToolErrorCode;
28
+ /** Structured payload for the codes that carry one; only `kernel_tool_host_denied` does today. */
29
+ readonly details?: KernelToolHostDenial;
17
30
 
18
- constructor(code: KernelToolErrorCode, message: string) {
31
+ constructor(code: KernelToolErrorCode, message: string, details?: KernelToolHostDenial) {
19
32
  super(message);
20
33
  this.code = code;
34
+ if (details !== undefined) this.details = details;
21
35
  }
22
36
  }
23
37
 
24
- export function kernelToolError(code: KernelToolErrorCode, message: string): KernelToolError {
25
- return new KernelToolError(code, message);
38
+ export function kernelToolError(
39
+ code: KernelToolErrorCode,
40
+ message: string,
41
+ details?: KernelToolHostDenial,
42
+ ): KernelToolError {
43
+ return new KernelToolError(code, message, details);
26
44
  }
@@ -2,7 +2,12 @@ import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/prot
2
2
  import { generateCorrelationId } from "../../bridge/protocol.ts";
3
3
  import { RESERVED_AGENT_TOOL } from "../../bridge/reserved.ts";
4
4
  import { kernelToolError } from "./kernel-tools-errors.ts";
5
- import type { KernelToolsDescribeResult, KernelToolsInvokeRequest } from "./kernel-tools-types.ts";
5
+ import type {
6
+ KernelToolsDescribeResult,
7
+ KernelToolsInvokeOptions,
8
+ KernelToolsInvokeRequest,
9
+ KernelToolsInvokeScope,
10
+ } from "./kernel-tools-types.ts";
6
11
 
7
12
  type KernelToolReply = Extract<
8
13
  KernelToHostMessage,
@@ -59,7 +64,13 @@ export class KernelToolHostPump {
59
64
  return { results: reply.results as KernelToolsDescribeResult["results"] };
60
65
  }
61
66
 
62
- async invoke(request: KernelToolsInvokeRequest, signal?: AbortSignal): Promise<unknown> {
67
+ /**
68
+ * `options` is the caller's abort signal, or `{ signal?, scope? }` where `scope` bounds the host
69
+ * tools the invoked closure may reach during this call only. A call without a scope posts exactly
70
+ * the message it always did (#1731).
71
+ */
72
+ async invoke(request: KernelToolsInvokeRequest, options?: AbortSignal | KernelToolsInvokeOptions): Promise<unknown> {
73
+ const { signal, scope } = normalizeInvokeOptions(options);
63
74
  this.events.dispatchEvent(new Event("nestedInvoke"));
64
75
  const reply = await this.#request(
65
76
  {
@@ -70,13 +81,14 @@ export class KernelToolHostPump {
70
81
  definition_revision: request.definition_revision,
71
82
  args: request.args,
72
83
  call_id: request.call_id,
84
+ ...wireScope(scope),
73
85
  },
74
86
  signal,
75
87
  );
76
88
  if (reply.type !== "kernel-tool-invoke-reply") {
77
89
  throw kernelToolError("kernel_tool_failed", "unexpected kernel-tool invoke reply");
78
90
  }
79
- if (!reply.ok) throw kernelToolError(codeOf(reply.error.code), reply.error.message);
91
+ if (!reply.ok) throw kernelToolError(codeOf(reply.error.code), reply.error.message, reply.error.details);
80
92
  return reply.value;
81
93
  }
82
94
 
@@ -112,6 +124,33 @@ export class KernelToolHostPump {
112
124
  }
113
125
  }
114
126
 
127
+ /**
128
+ * The scope as the protocol carries it: own copies of the caller's lists, and nothing at all when the
129
+ * caller named no host tools, so an unscoped call posts exactly the message it always did.
130
+ */
131
+ function wireScope(scope?: KernelToolsInvokeScope): { scope?: { tools: { allow?: string[]; deny?: string[] } } } {
132
+ const tools = scope?.tools;
133
+ if (tools === undefined) return {};
134
+ if (tools.allow === undefined && tools.deny === undefined) return {};
135
+ return {
136
+ scope: {
137
+ tools: {
138
+ ...(tools.allow === undefined ? {} : { allow: [...tools.allow] }),
139
+ ...(tools.deny === undefined ? {} : { deny: [...tools.deny] }),
140
+ },
141
+ },
142
+ };
143
+ }
144
+
145
+ function normalizeInvokeOptions(options?: AbortSignal | KernelToolsInvokeOptions): KernelToolsInvokeOptions {
146
+ if (options === undefined) return {};
147
+ return isAbortSignal(options) ? { signal: options } : options;
148
+ }
149
+
150
+ function isAbortSignal(options: AbortSignal | KernelToolsInvokeOptions): options is AbortSignal {
151
+ return options instanceof AbortSignal || "aborted" in options;
152
+ }
153
+
115
154
  function codeOf(
116
155
  code: string | undefined,
117
156
  ):
@@ -119,12 +158,14 @@ function codeOf(
119
158
  | "kernel_tool_stale"
120
159
  | "kernel_tool_missing"
121
160
  | "kernel_tool_recursion"
161
+ | "kernel_tool_host_denied"
122
162
  | "tools_unavailable"
123
163
  | "invalid_tool_definition" {
124
164
  if (
125
165
  code === "kernel_tool_stale" ||
126
166
  code === "kernel_tool_missing" ||
127
167
  code === "kernel_tool_recursion" ||
168
+ code === "kernel_tool_host_denied" ||
128
169
  code === "tools_unavailable" ||
129
170
  code === "invalid_tool_definition"
130
171
  ) {
@@ -21,7 +21,15 @@ export function createKernelToolPump({ getRuntime, emit, nestedInvokes }) {
21
21
  const runtime = getRuntime();
22
22
  if (!runtime?.kernelTools) throw kernelToolError("tools_unavailable", "JS runtime not initialized");
23
23
  const value = await kernelToolCallContext.run(
24
- { pendingTools, callId: message.call_id, generation: message.kernel_generation, signal: controller.signal },
24
+ // The scope lives only in this store: it is gone once the call settles, and it never
25
+ // reaches the top-level cell context or the next invocation (#1731).
26
+ {
27
+ pendingTools,
28
+ callId: message.call_id,
29
+ generation: message.kernel_generation,
30
+ signal: controller.signal,
31
+ scope: message.scope,
32
+ },
25
33
  () =>
26
34
  runtime.kernelTools.invoke(
27
35
  {
@@ -91,15 +99,21 @@ function pumpError(error) {
91
99
  message: error.message,
92
100
  stack: error.stack,
93
101
  ...(typeof error.code === "string" ? { code: error.code } : {}),
102
+ ...(isPlainRecord(error.details) ? { details: error.details } : {}),
94
103
  };
95
104
  }
96
105
  return { message: String(error) };
97
106
  }
98
107
 
108
+ function isPlainRecord(value) {
109
+ return value !== null && typeof value === "object" && !Array.isArray(value);
110
+ }
111
+
99
112
  function errorFrom(error) {
100
113
  const result = new Error(error.message);
101
114
  if (error.name) result.name = error.name;
102
115
  if (error.stack) result.stack = error.stack;
103
116
  if (typeof error.code === "string") result.code = error.code;
117
+ if (isPlainRecord(error.details)) result.details = error.details;
104
118
  return result;
105
119
  }
@@ -0,0 +1,9 @@
1
+ import type { KernelToolError, KernelToolHostDenialReason } from "./kernel-tools-errors.ts";
2
+ import type { KernelToolsInvokeScope } from "./kernel-tools-types.ts";
3
+
4
+ export function hostToolRefusal(
5
+ scope: KernelToolsInvokeScope | undefined,
6
+ toolName: string,
7
+ ): KernelToolHostDenialReason | null;
8
+
9
+ export function hostDeniedError(toolName: string, callId: string, reason: KernelToolHostDenialReason): KernelToolError;
@@ -0,0 +1,34 @@
1
+ import { kernelToolError } from "./kernel-tools-errors.js";
2
+
3
+ /**
4
+ * Call-scoped host-tool policy for one kernel-tool invocation (#1731). A call without a tool scope
5
+ * refuses nothing (today's behavior); `deny` wins over `allow`; an `allow` list refuses every host
6
+ * tool it does not name; a malformed list fails closed instead of widening the call's reach.
7
+ */
8
+ export function hostToolRefusal(scope, toolName) {
9
+ const tools = scope?.tools;
10
+ if (tools === null || typeof tools !== "object") return null;
11
+ if (tools.deny !== undefined) {
12
+ const deny = nameList(tools.deny);
13
+ if (deny === null || deny.includes(toolName)) return "deny";
14
+ }
15
+ if (tools.allow !== undefined) {
16
+ const allow = nameList(tools.allow);
17
+ if (allow === null || !allow.includes(toolName)) return "allow";
18
+ }
19
+ return null;
20
+ }
21
+
22
+ /** The typed refusal the closure sees and the invoking call reports. */
23
+ export function hostDeniedError(toolName, callId, reason) {
24
+ return kernelToolError(
25
+ "kernel_tool_host_denied",
26
+ `Host tool is outside this kernel tool call's scope: ${toolName} (${reason})`,
27
+ { tool: toolName, call_id: callId, reason },
28
+ );
29
+ }
30
+
31
+ function nameList(value) {
32
+ if (!Array.isArray(value)) return null;
33
+ return value.every((name) => typeof name === "string") ? value : null;
34
+ }
@@ -1,6 +1,7 @@
1
- import type { KernelToolErrorCode } from "./kernel-tools-errors.ts";
1
+ import type { ExtensionKernelTools, KernelToolInvokeOptions, KernelToolInvokeScope } from "@code-yeongyu/senpi";
2
+ import type { KernelToolErrorCode, KernelToolHostDenial, KernelToolHostDenialReason } from "./kernel-tools-errors.ts";
2
3
 
3
- export type { KernelToolErrorCode };
4
+ export type { KernelToolErrorCode, KernelToolHostDenial, KernelToolHostDenialReason };
4
5
 
5
6
  export type KernelToolDescriptor = {
6
7
  readonly name: string;
@@ -31,9 +32,22 @@ export type KernelToolsDescribeResult = {
31
32
  readonly results: readonly KernelToolsDescribeEntry[];
32
33
  };
33
34
 
35
+ /** Host declaration (#1731); aliases so this package cannot drift from `@code-yeongyu/senpi`. */
36
+ export type KernelToolsHostScope = NonNullable<KernelToolInvokeScope["tools"]>;
37
+ export type KernelToolsInvokeScope = KernelToolInvokeScope;
38
+ export type KernelToolsInvokeOptions = KernelToolInvokeOptions;
39
+
40
+ /** Stable capability markers a consumer gates on before sending an option this runtime may not know. */
41
+ export type KernelToolsCapabilities = ExtensionKernelTools["capabilities"];
42
+
43
+ export const KERNEL_TOOLS_CAPABILITIES = Object.freeze({
44
+ invokeScope: true as const,
45
+ }) satisfies ExtensionKernelTools["capabilities"];
46
+
34
47
  export type KernelToolsCapability = {
48
+ readonly capabilities: ExtensionKernelTools["capabilities"];
35
49
  describe(names: readonly string[]): Promise<KernelToolsDescribeResult>;
36
- invoke(request: KernelToolsInvokeRequest, signal?: AbortSignal): Promise<unknown>;
50
+ invoke: ExtensionKernelTools["invoke"];
37
51
  };
38
52
 
39
53
  export const KERNEL_TOOLS_UNSUPPORTED = {
@@ -1,6 +1,7 @@
1
1
  import { kernelToolCallContext } from "./kernel-tools-context.js";
2
2
  import { kernelToolError } from "./kernel-tools-errors.js";
3
3
  import { createKernelToolPump } from "./kernel-tools-pump.js";
4
+ import { hostDeniedError, hostToolRefusal } from "./kernel-tools-scope.js";
4
5
  import { JsWorkerRuntime } from "./worker-runtime.js";
5
6
 
6
7
  // Mirrors INTERRUPT_ACK_OP and CHILD_LIFECYCLE_OP in src/bridge/reserved.ts (this worker file cannot import TypeScript).
@@ -59,6 +60,12 @@ export function createWorkerCore(transport, options) {
59
60
  const nested = kernelToolCallContext.getStore();
60
61
  if (!nested && activeCell?.interruption) throw activeCell.interruption;
61
62
  if (nested?.signal.aborted) throw nested.signal.reason;
63
+ // A scoped kernel-tool call is refused here, before anything reaches the host bridge, so the
64
+ // closure sees a rejected promise and the parent's own cells keep their full tool surface (#1731).
65
+ if (nested) {
66
+ const refusal = hostToolRefusal(nested.scope, toolName);
67
+ if (refusal) throw hostDeniedError(toolName, nested.callId, refusal);
68
+ }
62
69
  const bag = nested?.pendingTools ?? pendingTools;
63
70
  const callId = `js-${crypto.randomUUID()}`;
64
71
  const promise = new Promise((resolve, reject) => bag.set(callId, { resolve, reject }));
@@ -1,4 +1,5 @@
1
1
  import type { KernelInterruptHandle } from "../../tool/types.ts";
2
+ import type { KernelToolsInvokeOptions } from "../js/kernel-tools-types.ts";
2
3
  import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
3
4
  import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
4
5
  import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
@@ -40,7 +41,7 @@ export class PythonKernel {
40
41
  return rejectKernelToolsUnavailable();
41
42
  }
42
43
 
43
- invokeKernelTool(_request: unknown, _signal?: AbortSignal): Promise<never> {
44
+ invokeKernelTool(_request: unknown, _options?: AbortSignal | KernelToolsInvokeOptions): Promise<never> {
44
45
  return rejectKernelToolsUnavailable();
45
46
  }
46
47
 
@@ -1,6 +1,7 @@
1
1
  import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
2
2
  import { decodeBridgeFrame, encodeBridgeFrame, isKernelToHostMessage } from "../../bridge/protocol.ts";
3
3
  import type { KernelInterruptHandle } from "../../tool/types.ts";
4
+ import type { KernelToolsInvokeOptions } from "../js/kernel-tools-types.ts";
4
5
  import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
5
6
  import { applySessionEnvironment } from "../session-env.ts";
6
7
  import type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
@@ -78,7 +79,7 @@ export class SubprocessKernel {
78
79
  return rejectKernelToolsUnavailable();
79
80
  }
80
81
 
81
- invokeKernelTool(_request: unknown, _signal?: AbortSignal): Promise<never> {
82
+ invokeKernelTool(_request: unknown, _options?: AbortSignal | KernelToolsInvokeOptions): Promise<never> {
82
83
  return rejectKernelToolsUnavailable();
83
84
  }
84
85
 
@@ -1,4 +1,9 @@
1
- import { type AgentToolResult, type ExtensionContext, sanitizeTerminalLabel } from "@code-yeongyu/senpi";
1
+ import {
2
+ type AgentToolResult,
3
+ type ExtensionContext,
4
+ kernelToolsStorage,
5
+ sanitizeTerminalLabel,
6
+ } from "@code-yeongyu/senpi";
2
7
  import type { KernelToHostMessage } from "../bridge/protocol.ts";
3
8
  import { RESERVED_SCHEMA_TOOL } from "../bridge/reserved.ts";
4
9
  import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
@@ -8,6 +13,7 @@ import { appendSchemaHint } from "../bridges/schema-hint.ts";
8
13
  import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
9
14
  import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
10
15
  import type { ResolvedCodemodeSettings } from "../config/settings.ts";
16
+ import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
11
17
  import {
12
18
  boundToolCallArgs,
13
19
  capCodePoints,
@@ -39,6 +45,8 @@ export interface CellBridgeRuntime {
39
45
  readonly ctx: ExtensionContext;
40
46
  readonly artifactPath?: string;
41
47
  readonly imageResizer?: EvalImageResizer;
48
+ /** This cell's live kernel-tool capability; only a JS kernel has one (#1754). */
49
+ readonly kernelTools?: KernelToolsCapability;
42
50
  }
43
51
 
44
52
  export class CellHandler {
@@ -81,7 +89,7 @@ export class CellHandler {
81
89
  this.#resultBuilder.display(message);
82
90
  return;
83
91
  case "tool-call": {
84
- const pending = this.#handleToolCall(message);
92
+ const pending = this.#dispatchToolCall(message);
85
93
  this.#state.pendingBridgeCalls.push(pending);
86
94
  await pending;
87
95
  return;
@@ -114,6 +122,19 @@ export class CellHandler {
114
122
  return this.#resultBuilder.liveResult();
115
123
  }
116
124
 
125
+ /**
126
+ * The worker's message loop fires outside the async context `run-eval-cell.ts` enters around the
127
+ * awaited run chain, so the capability has to be entered here — around the whole dispatch, including
128
+ * the reserved agent()/output() bridges where a host task tool resolves the parent's kernel tools —
129
+ * for exactly the duration of each host tool call this cell makes (#1754). Without a capability
130
+ * (py/rb/jl) the store stays empty and `ctx.kernelTools` remains undefined.
131
+ */
132
+ async #dispatchToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
133
+ const kernelTools = this.#runtime.kernelTools;
134
+ if (kernelTools === undefined) return await this.#handleToolCall(message);
135
+ return await kernelToolsStorage.run(kernelTools, async () => await this.#handleToolCall(message));
136
+ }
137
+
117
138
  async #handleToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
118
139
  const startedAt = Date.now();
119
140
  const metric = createToolCallMetric(message.toolName, startedAt);
@@ -1,8 +1,17 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { join } from "node:path";
3
- import { type AgentToolResult, type ExtensionContext, kernelToolsStorage } from "@code-yeongyu/senpi";
3
+ import {
4
+ type AgentToolResult,
5
+ type ExtensionContext,
6
+ type ExtensionKernelTools,
7
+ kernelToolsStorage,
8
+ } from "@code-yeongyu/senpi";
4
9
  import { DEFAULT_FOREGROUND_WINDOW_SECONDS, defaultCodemodeSettings } from "../config/settings.ts";
5
- import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
10
+ import {
11
+ KERNEL_TOOLS_CAPABILITIES,
12
+ type KernelToolsCapability,
13
+ type KernelToolsDescribeResult,
14
+ } from "../kernels/js/kernel-tools-types.ts";
6
15
  import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
7
16
  import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
8
17
  import { CellHandler, type CellState } from "./cell-handler.ts";
@@ -167,6 +176,9 @@ async function executeCell(
167
176
  void pending.catch((error: unknown) => execution.cancel(error));
168
177
  }),
169
178
  );
179
+ // Computed before the handler so the cell's capability can be entered per host tool call from the
180
+ // worker's message loop, which runs outside the `kernelToolsStorage.run` context below (#1754).
181
+ const kernelTools = jsKernelTools(kernel, invocation.input.language);
170
182
  const runBound = async (): Promise<AgentToolResult<EvalToolDetails>> => {
171
183
  execution.setKernel(kernel);
172
184
  const activeHandler = new CellHandler(kernel, state, {
@@ -179,6 +191,7 @@ async function executeCell(
179
191
  ? {}
180
192
  : { artifactPath: join(options.artifactsDir, `eval-${randomUUID()}.log`) }),
181
193
  ...(options.imageResizer === undefined ? {} : { imageResizer: options.imageResizer }),
194
+ ...(kernelTools === undefined ? {} : { kernelTools }),
182
195
  });
183
196
  handler = activeHandler;
184
197
  cellManager.markRunning(
@@ -198,7 +211,6 @@ async function executeCell(
198
211
  await execution.wait(Promise.all(state.pendingBridgeCalls));
199
212
  return await handler.finalize(result);
200
213
  };
201
- const kernelTools = jsKernelTools(kernel, invocation.input.language);
202
214
  return kernelTools ? await kernelToolsStorage.run(kernelTools, runBound) : await runBound();
203
215
  } catch (error) {
204
216
  if (handler && error instanceof Error && error.name === "CodemodeSessionDisposedError")
@@ -217,11 +229,12 @@ function jsKernelTools(kernel: EvalKernel, language: string): KernelToolsCapabil
217
229
  if (language !== "js") return undefined;
218
230
  if (!("describeKernelTools" in kernel) || typeof kernel.describeKernelTools !== "function") return undefined;
219
231
  const js = kernel as EvalKernel & {
220
- describeKernelTools: KernelToolsCapability["describe"];
221
- invokeKernelTool: KernelToolsCapability["invoke"];
232
+ describeKernelTools: (names: readonly string[]) => Promise<KernelToolsDescribeResult>;
233
+ invokeKernelTool: ExtensionKernelTools["invoke"];
222
234
  };
223
235
  return {
236
+ capabilities: KERNEL_TOOLS_CAPABILITIES,
224
237
  describe: (names) => js.describeKernelTools(names),
225
- invoke: (request, signal) => js.invokeKernelTool(request, signal),
226
- };
238
+ invoke: (request, options) => js.invokeKernelTool(request, options),
239
+ } satisfies ExtensionKernelTools;
227
240
  }