@code-yeongyu/senpi-codemode 2026.10.8 → 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.
- package/CHANGELOG.md +178 -0
- package/README.md +212 -14
- package/package.json +6 -5
- package/src/bridge/http-server.ts +9 -1
- package/src/bridge/protocol.ts +12 -3
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +48 -0
- package/src/bridges/eval-virtual-schemas.ts +12 -4
- package/src/bridges/reserved-dispatch.ts +13 -1
- package/src/config/executable-settings.json +5 -0
- package/src/config/project-trust.ts +51 -0
- package/src/config/settings-overrides.ts +2 -0
- package/src/environments/js-environments.ts +208 -0
- package/src/environments/js-installer.ts +268 -0
- package/src/environments/js-revision-files.ts +91 -0
- package/src/environments/no-links.ts +132 -0
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/project-resolves.ts +54 -0
- package/src/environments/py-environment.ts +7 -3
- package/src/environments/py-installer.ts +68 -4
- package/src/environments/python-environments.ts +85 -0
- package/src/environments/revision-store.ts +38 -7
- package/src/extension/bridge-tool-call.ts +7 -1
- package/src/extension/cell-kernel-tools.ts +28 -0
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/kernel-replacement.ts +20 -0
- package/src/extension/runtime-factory.ts +53 -3
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager-proxy.ts +6 -0
- package/src/extension/session-manager.ts +26 -4
- package/src/index.ts +4 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/prelude.jl +6 -1
- package/src/kernels/js/cell-dispatch.ts +44 -0
- package/src/kernels/js/cell-ownership.js +268 -0
- package/src/kernels/js/cell-run-context.js +89 -0
- package/src/kernels/js/context-manager.ts +48 -39
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/host-entries.ts +62 -0
- package/src/kernels/js/kernel-contract.ts +16 -1
- package/src/kernels/js/kernel-memory-bridge.ts +1 -1
- package/src/kernels/js/kernel-memory.ts +79 -0
- package/src/kernels/js/kernel-tools-registry.js +1 -0
- package/src/kernels/js/local-module-loader.ts +21 -5
- package/src/kernels/js/process-drain-scan.d.ts +8 -0
- package/src/kernels/js/process-drain-scan.js +28 -0
- package/src/kernels/js/process-entry.js +466 -0
- package/src/kernels/js/process-memory-host.ts +83 -0
- package/src/kernels/js/process-worker.ts +298 -0
- package/src/kernels/js/rejection-reports.js +61 -0
- package/src/kernels/js/run-queue.ts +7 -0
- package/src/kernels/js/worker-core.js +53 -8
- package/src/kernels/js/worker-cwd.js +5 -2
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-package-resolve.js +153 -0
- package/src/kernels/js/worker-require.d.ts +19 -0
- package/src/kernels/js/worker-require.js +52 -0
- package/src/kernels/js/worker-runtime.js +45 -9
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +5 -0
- package/src/kernels/js/worker-slot.ts +9 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/js/workpool.js +5 -2
- package/src/kernels/py/host-cell.ts +8 -31
- package/src/kernels/py/kernel.ts +8 -2
- package/src/kernels/py/prelude.py +145 -11
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/py/transport.ts +13 -1
- package/src/kernels/rb/workpool.rb +7 -1
- package/src/kernels/sandbox/sandbox-cell.ts +141 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
- package/src/kernels/shared/cell-source-at-start.ts +18 -0
- package/src/kernels/shared/host-cell.ts +57 -0
- package/src/kernels/shared/subprocess-process.ts +14 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +13 -12
- package/src/tool/cell-reserved-dispatch.ts +1 -0
- package/src/tool/cell-runtime.ts +4 -2
- package/src/tool/detached-cell-contract.ts +3 -0
- package/src/tool/detached-cell-manager.ts +4 -8
- package/src/tool/detached-cell-notification.ts +11 -63
- package/src/tool/detached-cell-snapshot.ts +17 -0
- package/src/tool/detached-eval-result.ts +13 -7
- package/src/tool/detached-notification-queue.ts +1 -2
- package/src/tool/eval-request.ts +23 -1
- package/src/tool/eval-tool-options.ts +6 -0
- package/src/tool/eval-tool.ts +17 -4
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/load-cell.ts +88 -0
- package/src/tool/magic-cell-host.ts +123 -0
- package/src/tool/magic-cells.ts +75 -0
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render.ts +74 -15
- package/src/tool/run-eval-cell.ts +73 -1
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +50 -1
|
@@ -61,11 +61,75 @@ const SHORT_ALIASES: Readonly<Record<string, string>> = {
|
|
|
61
61
|
const STDERR_TAIL_BYTES = 4_096;
|
|
62
62
|
const PIP_TREE_GRACE_MS = 2_000;
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Splits a %pip argument line the way the platform's shell would for these inputs: single and double quotes
|
|
66
|
+
* group (`"pkg[extra]>=1.0"`) and `#` at the start of a word begins a comment. A backslash escapes the next
|
|
67
|
+
* character only in POSIX mode; on Windows it is a path separator and stays as written. An unclosed quote is
|
|
68
|
+
* refused rather than guessed.
|
|
69
|
+
*/
|
|
70
|
+
const DOUBLE_QUOTE_ESCAPES: ReadonlySet<string> = new Set(['"', "\\", "$", "`"]);
|
|
71
|
+
|
|
72
|
+
export function splitShellWords(text: string, posix = process.platform !== "win32"): string[] {
|
|
73
|
+
const words: string[] = [];
|
|
74
|
+
let word = "";
|
|
75
|
+
let inWord = false;
|
|
76
|
+
let quote: "'" | '"' | undefined;
|
|
77
|
+
for (let index = 0; index < text.length; index++) {
|
|
78
|
+
const char = text[index] ?? "";
|
|
79
|
+
if (quote === "'") {
|
|
80
|
+
if (char === "'") quote = undefined;
|
|
81
|
+
else word += char;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (posix && char === "\\" && index + 1 < text.length) {
|
|
85
|
+
const next = text[index + 1] ?? "";
|
|
86
|
+
// A backslash-newline is a line continuation in a shell, inside double quotes or not: both characters go.
|
|
87
|
+
if (next === "\n") {
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Inside double quotes a shell only escapes these; any other backslash stays as written.
|
|
92
|
+
if (quote === '"' && !DOUBLE_QUOTE_ESCAPES.has(next)) {
|
|
93
|
+
word += char;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
index += 1;
|
|
97
|
+
word += next;
|
|
98
|
+
inWord = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (quote === '"') {
|
|
102
|
+
if (char === '"') quote = undefined;
|
|
103
|
+
else word += char;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (char === "'" || char === '"') {
|
|
107
|
+
quote = char;
|
|
108
|
+
inWord = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (/\s/u.test(char)) {
|
|
112
|
+
if (inWord) words.push(word);
|
|
113
|
+
word = "";
|
|
114
|
+
inWord = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === "#" && !inWord) break;
|
|
118
|
+
word += char;
|
|
119
|
+
inWord = true;
|
|
120
|
+
}
|
|
121
|
+
if (quote !== undefined)
|
|
122
|
+
throw new EnvironmentError("environment_install_failed", `unclosed ${quote} quote in %pip arguments`);
|
|
123
|
+
if (inWord) words.push(word);
|
|
124
|
+
return words;
|
|
125
|
+
}
|
|
126
|
+
|
|
64
127
|
export function parsePipRequirements(text: string): string[] {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
128
|
+
return normalizePipInstall(splitShellWords(text));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** `%pip` words already split (`["install", ...]`): `packages.install()` passes its list as-is, so nothing is re-quoted. */
|
|
132
|
+
export function normalizePipInstall(args: readonly string[]): string[] {
|
|
69
133
|
const command = args[0] === "install" ? args.slice(1) : undefined;
|
|
70
134
|
if (command === undefined) {
|
|
71
135
|
throw new EnvironmentError("environment_install_failed", "only `%pip install <requirements>` is supported");
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
2
|
+
import {
|
|
3
|
+
type EnvironmentMode,
|
|
4
|
+
type InstallReceipt,
|
|
5
|
+
installPythonPackages,
|
|
6
|
+
managedEnvironmentBase,
|
|
7
|
+
projectPythonBase,
|
|
8
|
+
pythonAbiTag,
|
|
9
|
+
} from "./py-environment.ts";
|
|
10
|
+
import { EnvironmentError } from "./py-installer.ts";
|
|
11
|
+
import { readActiveRevisionSync } from "./revision-store.ts";
|
|
12
|
+
|
|
13
|
+
export interface PythonEnvironmentsOptions {
|
|
14
|
+
readonly artifactsDir: string;
|
|
15
|
+
readonly cwd: string;
|
|
16
|
+
readonly interpreter: string;
|
|
17
|
+
readonly settings: Pick<ResolvedCodemodeSettings, "environments">;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PythonEnvironments {
|
|
21
|
+
readonly #options: PythonEnvironmentsOptions;
|
|
22
|
+
#mode: EnvironmentMode = "managed";
|
|
23
|
+
#currentBase: string | undefined;
|
|
24
|
+
#managedBase: Promise<string> | undefined;
|
|
25
|
+
|
|
26
|
+
constructor(options: PythonEnvironmentsOptions) {
|
|
27
|
+
this.#options = options;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get mode(): EnvironmentMode {
|
|
31
|
+
return this.#mode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The revision a Python cell imports from, read from the current mode's root each time a cell starts, so an
|
|
36
|
+
* install by another session sharing the project root is seen. Undefined while nothing is installed there.
|
|
37
|
+
*/
|
|
38
|
+
get activeRoot(): string | undefined {
|
|
39
|
+
return this.#currentBase === undefined ? undefined : readActiveRevisionSync(this.#currentBase)?.dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async setMode(mode: EnvironmentMode): Promise<string> {
|
|
43
|
+
const base = await this.#base(mode);
|
|
44
|
+
this.#mode = mode;
|
|
45
|
+
this.#currentBase = base;
|
|
46
|
+
return base;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async install(
|
|
50
|
+
requirements: string | readonly string[],
|
|
51
|
+
signal: AbortSignal,
|
|
52
|
+
onOutput?: (stream: "stdout" | "stderr", data: string) => void,
|
|
53
|
+
): Promise<InstallReceipt> {
|
|
54
|
+
if (this.#options.settings.environments?.autoProvision === false) {
|
|
55
|
+
throw new EnvironmentError(
|
|
56
|
+
"environment_installer_unavailable",
|
|
57
|
+
"installs are turned off for this project (environments.autoProvision is false)",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const mode = this.#mode;
|
|
61
|
+
const base = await this.#base(mode);
|
|
62
|
+
const receipt = await installPythonPackages({
|
|
63
|
+
base,
|
|
64
|
+
mode,
|
|
65
|
+
interpreter: this.#options.interpreter,
|
|
66
|
+
requirements,
|
|
67
|
+
cwd: this.#options.cwd,
|
|
68
|
+
signal,
|
|
69
|
+
...(onOutput === undefined ? {} : { onOutput }),
|
|
70
|
+
});
|
|
71
|
+
if (this.#mode === mode) this.#currentBase = base;
|
|
72
|
+
return receipt;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#base(mode: EnvironmentMode): Promise<string> {
|
|
76
|
+
if (mode === "project") return Promise.resolve(projectPythonBase(this.#options.cwd));
|
|
77
|
+
const configured = this.#options.settings.environments?.managedRoot;
|
|
78
|
+
this.#managedBase ??= pythonAbiTag(this.#options.interpreter).then((abi) =>
|
|
79
|
+
configured === undefined
|
|
80
|
+
? managedEnvironmentBase(this.#options.artifactsDir, "py", abi)
|
|
81
|
+
: managedEnvironmentBase(configured, "py", abi),
|
|
82
|
+
);
|
|
83
|
+
return this.#managedBase;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { chmod, cp, mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { type LockWaitNotice, withRootLock } from "./install-lock.ts";
|
|
6
|
+
import { assertLinkStaysInside, assertNoLinksBelow, recordedInstallSources } from "./no-links.ts";
|
|
5
7
|
import { EnvironmentError } from "./py-installer.ts";
|
|
6
8
|
|
|
7
9
|
const POINTER = "active";
|
|
@@ -12,11 +14,25 @@ export interface Revision {
|
|
|
12
14
|
readonly dir: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
/** The same read, synchronous: the pointer is one short file, read when a Python cell starts running. */
|
|
18
|
+
export function readActiveRevisionSync(base: string): Revision | undefined {
|
|
19
|
+
try {
|
|
20
|
+
return parsePointer(base, readFileSync(join(base, POINTER), "utf8"));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return undefined;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parsePointer(base: string, text: string): Revision | undefined {
|
|
28
|
+
const name = text.trim();
|
|
29
|
+
const match = REVISION.exec(name);
|
|
30
|
+
return match?.[1] === undefined ? undefined : { number: Number(match[1]), dir: join(base, name) };
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export async function readActiveRevision(base: string): Promise<Revision | undefined> {
|
|
16
34
|
try {
|
|
17
|
-
|
|
18
|
-
const match = REVISION.exec(name);
|
|
19
|
-
return match?.[1] === undefined ? undefined : { number: Number(match[1]), dir: join(base, name) };
|
|
35
|
+
return parsePointer(base, await readFile(join(base, POINTER), "utf8"));
|
|
20
36
|
} catch (error) {
|
|
21
37
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return undefined;
|
|
22
38
|
throw error;
|
|
@@ -35,7 +51,7 @@ export async function publishNextRevision(
|
|
|
35
51
|
signal?: AbortSignal,
|
|
36
52
|
onLockWait?: (notice: LockWaitNotice) => void,
|
|
37
53
|
): Promise<{ readonly revision: Revision; readonly previous: Revision | undefined }> {
|
|
38
|
-
await
|
|
54
|
+
await mkdirPrivate(base);
|
|
39
55
|
return withRootLock(
|
|
40
56
|
base,
|
|
41
57
|
async () => {
|
|
@@ -44,8 +60,18 @@ export async function publishNextRevision(
|
|
|
44
60
|
const staging = join(base, `.staging-rev-${number}-${process.pid}-${randomUUID()}`);
|
|
45
61
|
const dir = join(base, `rev-${number}`);
|
|
46
62
|
try {
|
|
47
|
-
if (previous === undefined) await mkdir(staging, {
|
|
48
|
-
else
|
|
63
|
+
if (previous === undefined) await mkdir(staging, { mode: 0o700 });
|
|
64
|
+
else {
|
|
65
|
+
await assertNoLinksBelow(base, previous.dir);
|
|
66
|
+
const sources = await recordedInstallSources(previous.dir);
|
|
67
|
+
await cp(previous.dir, staging, {
|
|
68
|
+
recursive: true,
|
|
69
|
+
verbatimSymlinks: true,
|
|
70
|
+
filter: async (source) => await assertLinkStaysInside(source, previous.dir, sources),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Explicit, not left to the umask: a revision holds the user's installed packages and carried registry config.
|
|
74
|
+
await chmod(staging, 0o700);
|
|
49
75
|
await build(staging, previous);
|
|
50
76
|
signal?.throwIfAborted();
|
|
51
77
|
await renameRetrying(staging, dir);
|
|
@@ -93,3 +119,8 @@ async function highestRevision(base: string): Promise<number> {
|
|
|
93
119
|
}
|
|
94
120
|
return highest;
|
|
95
121
|
}
|
|
122
|
+
|
|
123
|
+
/** Creates `dir` and any missing parents private to the user (0700; the umask can only narrow it further). */
|
|
124
|
+
export async function mkdirPrivate(dir: string): Promise<void> {
|
|
125
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
126
|
+
}
|
|
@@ -8,6 +8,8 @@ export interface BridgeToolCallRequest {
|
|
|
8
8
|
readonly toolName: string;
|
|
9
9
|
readonly args: unknown;
|
|
10
10
|
readonly callId: string;
|
|
11
|
+
/** The calling run's secret; absent for a call made outside any run (a plain thread), which gets no kernel tools. */
|
|
12
|
+
readonly cellToken?: string;
|
|
11
13
|
readonly signal: AbortSignal;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -17,7 +19,10 @@ export interface BridgeToolCallRequest {
|
|
|
17
19
|
// and ordinary tool results are marshalled to { text, images, details, hasError } — the raw
|
|
18
20
|
// { content } shape left python cells unable to reach tool.read image blocks.
|
|
19
21
|
export async function routeBridgeToolCall(
|
|
20
|
-
options: Pick<
|
|
22
|
+
options: Pick<
|
|
23
|
+
CreateCodemodeSessionManagerOptions,
|
|
24
|
+
"executeTool" | "listTools" | "settings" | "handles" | "environments"
|
|
25
|
+
>,
|
|
21
26
|
request: BridgeToolCallRequest,
|
|
22
27
|
evalHandleHost?: EvalHandleHost,
|
|
23
28
|
): Promise<unknown> {
|
|
@@ -37,5 +42,6 @@ export async function routeBridgeToolCall(
|
|
|
37
42
|
marshalToolResult,
|
|
38
43
|
...(options.handles === undefined ? {} : { handles: options.handles }),
|
|
39
44
|
...(evalHandleHost === undefined ? {} : { evalHandleHost }),
|
|
45
|
+
...(options.environments === undefined ? {} : { environments: options.environments }),
|
|
40
46
|
});
|
|
41
47
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { kernelToolsStorage } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kernel-tools capabilities of the subprocess-kernel cells running right now, keyed by a secret minted per run and
|
|
6
|
+
* sent only to the kernel running that cell. Those kernels (py/rb/jl) reach the host over the bridge, outside the
|
|
7
|
+
* submitting cell's async context, so a bridge call carries its run's secret and runs inside that run's capability:
|
|
8
|
+
* the same `kernelToolsStorage` scope the JS worker path enters per host call (#1754).
|
|
9
|
+
*/
|
|
10
|
+
export class CellKernelTools {
|
|
11
|
+
readonly #byToken = new Map<string, KernelToolsCapability>();
|
|
12
|
+
|
|
13
|
+
/** Makes `capability` the kernel tools of calls carrying `token` until the returned release runs. */
|
|
14
|
+
bind(token: string, capability: KernelToolsCapability): () => void {
|
|
15
|
+
this.#byToken.set(token, capability);
|
|
16
|
+
return () => {
|
|
17
|
+
if (this.#byToken.get(token) === capability) this.#byToken.delete(token);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Runs `call` with the capability bound to `token`; an absent, unknown or released token gets none. */
|
|
22
|
+
async run<T>(token: string | undefined, call: () => Promise<T>): Promise<T> {
|
|
23
|
+
const capability = token === undefined ? undefined : this.#byToken.get(token);
|
|
24
|
+
return capability === undefined
|
|
25
|
+
? await kernelToolsStorage.exit(call)
|
|
26
|
+
: await kernelToolsStorage.run(capability, call);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { EvalImageContent } from "../tool/image-resize.ts";
|
|
3
|
+
|
|
4
|
+
/** A notification's message content: plain text, or text and the cells' images in one message. */
|
|
5
|
+
export type EvalNotificationContent = string | ({ readonly type: "text"; readonly text: string } | EvalImageContent)[];
|
|
6
|
+
|
|
2
7
|
import type { EvalDetachedCellNotification, EvalDetachedCellNotifier } from "../tool/detached-cell-manager.ts";
|
|
3
8
|
|
|
4
9
|
const NON_INTERACTIVE_MODES = new Set(["print", "json"]);
|
|
@@ -11,7 +16,7 @@ export type EvalNotifyMode = "wake" | "next-turn" | "off";
|
|
|
11
16
|
export interface EvalNotifierDeps {
|
|
12
17
|
/** Deliver a model-visible notification without rendering synthetic user input. */
|
|
13
18
|
readonly sendMessage: (
|
|
14
|
-
message: { customType: string; content:
|
|
19
|
+
message: { customType: string; content: EvalNotificationContent; display: boolean },
|
|
15
20
|
options: { triggerTurn: boolean; deliverAs: "steer" | "followUp" },
|
|
16
21
|
) => void;
|
|
17
22
|
readonly getContext: () => ExtensionContext | undefined;
|
|
@@ -43,10 +48,16 @@ export class EvalNotifier implements EvalDetachedCellNotifier {
|
|
|
43
48
|
this.#deps.sendMessage(
|
|
44
49
|
{
|
|
45
50
|
customType: EVAL_NOTIFICATION_CUSTOM_TYPE,
|
|
46
|
-
content: pending
|
|
51
|
+
content: notificationContent(pending),
|
|
47
52
|
display: false,
|
|
48
53
|
},
|
|
49
54
|
{ triggerTurn: true, deliverAs: mode === "wake" ? "steer" : "followUp" },
|
|
50
55
|
);
|
|
51
56
|
}
|
|
52
57
|
}
|
|
58
|
+
|
|
59
|
+
function notificationContent(cells: readonly EvalDetachedCellNotification[]): EvalNotificationContent {
|
|
60
|
+
const text = cells.map((cell) => cell.content).join("\n\n");
|
|
61
|
+
const images = cells.flatMap((cell) => cell.images ?? []);
|
|
62
|
+
return images.length === 0 ? text : [{ type: "text", text }, ...images];
|
|
63
|
+
}
|
|
@@ -63,11 +63,16 @@ export class SessionKernelRegistrations {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function javaScriptKernelMemory(kernel: JavaScriptKernel): RegisteredKernelSource {
|
|
66
|
+
const measure = kernel.mode === "process" ? "footprint" : "heap";
|
|
66
67
|
return {
|
|
67
|
-
measure
|
|
68
|
+
measure,
|
|
68
69
|
lastLiveBytes: () => kernel.lastLiveBytes,
|
|
69
70
|
busy: () => kernel.queueSnapshot().activeCellId !== null,
|
|
70
|
-
queryMemory: () =>
|
|
71
|
+
queryMemory: async () => {
|
|
72
|
+
const reading = await kernel.queryMemory();
|
|
73
|
+
return reading === undefined ? undefined : { liveBytes: reading.liveBytes, measure: reading.measure };
|
|
74
|
+
},
|
|
75
|
+
...(measure === "footprint" ? { pid: () => kernel.processPid } : {}),
|
|
71
76
|
};
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
KernelInterruptHandle,
|
|
14
14
|
PendingCell,
|
|
15
15
|
} from "../tool/types.ts";
|
|
16
|
+
import { hasKernelTools, type KernelToolsMethods } from "./kernel-tools-probe.ts";
|
|
16
17
|
|
|
17
18
|
export type StartKernel = (lifecycle: Required<KernelLifecycle>) => Promise<EvalKernel>;
|
|
18
19
|
|
|
@@ -50,10 +51,29 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
50
51
|
#closed = false;
|
|
51
52
|
#closeFailure: unknown;
|
|
52
53
|
|
|
54
|
+
// Present only when the instance has kernel tools, so a probe sees what it would see on the instance itself.
|
|
55
|
+
declare readonly describeKernelTools?: KernelToolsMethods["describeKernelTools"];
|
|
56
|
+
declare readonly invokeKernelTool?: KernelToolsMethods["invokeKernelTool"];
|
|
57
|
+
|
|
53
58
|
private constructor(language: EvalLanguage, start: StartKernel, first: EvalKernel) {
|
|
54
59
|
this.#language = language;
|
|
55
60
|
this.#start = start;
|
|
56
61
|
this.#kernel = first;
|
|
62
|
+
// Python is the one subprocess kernel with kernel tools; rb and jl only carry tools_unavailable stubs to hide.
|
|
63
|
+
if (language === "py" && hasKernelTools(first)) {
|
|
64
|
+
// Always the CURRENT instance: after a replacement the old definitions are gone with the old interpreter.
|
|
65
|
+
const tools: KernelToolsMethods = {
|
|
66
|
+
describeKernelTools: (names) => this.#currentTools().describeKernelTools(names),
|
|
67
|
+
invokeKernelTool: (request, options) => this.#currentTools().invokeKernelTool(request, options),
|
|
68
|
+
};
|
|
69
|
+
Object.assign(this, tools);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#currentTools(): KernelToolsMethods {
|
|
74
|
+
const current = this.#kernel;
|
|
75
|
+
if (!hasKernelTools(current)) throw new Error(`The ${this.#language} kernel has no kernel tools`);
|
|
76
|
+
return current;
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
/** Starts the first instance; one that cannot hand back its queue is returned as it is. */
|
|
@@ -2,18 +2,23 @@ import { type ExtensionContext, withBundledBunCommands } from "@code-yeongyu/sen
|
|
|
2
2
|
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
3
3
|
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
4
4
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
5
|
+
import { resolveJsIsolation } from "../config/feature-settings.ts";
|
|
6
|
+
import { withoutUntrustedInterpreter } from "../config/project-trust.ts";
|
|
5
7
|
import {
|
|
6
8
|
type CodemodeSettings,
|
|
7
9
|
loadCodemodeSettings,
|
|
8
10
|
type ResolvedCodemodeSettings,
|
|
9
11
|
resolveEnabledLanguages,
|
|
10
12
|
} from "../config/settings.ts";
|
|
13
|
+
import { JsEnvironments } from "../environments/js-environments.ts";
|
|
14
|
+
import { PythonEnvironments } from "../environments/python-environments.ts";
|
|
11
15
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
12
16
|
import {
|
|
13
17
|
createInterpreterDetector,
|
|
14
18
|
getInterpreterAvailability,
|
|
15
19
|
type InterpreterAvailability,
|
|
16
20
|
} from "../interpreters/detect.ts";
|
|
21
|
+
import { processRuntimeInfo } from "../kernels/js/process-worker.ts";
|
|
17
22
|
import { sessionEnvironmentFrom } from "../kernels/session-env.ts";
|
|
18
23
|
import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
|
|
19
24
|
import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
|
|
@@ -47,6 +52,8 @@ export type SessionRuntime = {
|
|
|
47
52
|
readonly artifactsDir: string;
|
|
48
53
|
readonly executeTool: AgentExecuteTool;
|
|
49
54
|
readonly spawns: boolean;
|
|
55
|
+
readonly pythonEnvironments?: PythonEnvironments;
|
|
56
|
+
readonly jsEnvironments: JsEnvironments;
|
|
50
57
|
};
|
|
51
58
|
|
|
52
59
|
export async function createRuntime(
|
|
@@ -58,12 +65,25 @@ export async function createRuntime(
|
|
|
58
65
|
handles?: HandleRegistry,
|
|
59
66
|
): Promise<SessionRuntime> {
|
|
60
67
|
const loaded = await loadCodemodeSettings({ cwd: ctx.cwd });
|
|
68
|
+
const trusted = withoutUntrustedInterpreter(
|
|
69
|
+
loaded.settings,
|
|
70
|
+
loaded.source,
|
|
71
|
+
ctx.cwd,
|
|
72
|
+
typeof ctx.isProjectTrusted === "function" ? () => ctx.isProjectTrusted() : undefined,
|
|
73
|
+
);
|
|
61
74
|
const settings: ResolvedCodemodeSettings = {
|
|
62
|
-
...
|
|
63
|
-
languages: resolveEnabledLanguages(
|
|
75
|
+
...trusted.settings,
|
|
76
|
+
languages: resolveEnabledLanguages(trusted.settings),
|
|
64
77
|
};
|
|
65
78
|
const availability = await getInterpreterAvailability(settings, createInterpreterDetector());
|
|
79
|
+
const pyReason = availability.py.detected.ok ? undefined : availability.py.detected.reason;
|
|
80
|
+
reportSettingsProblems(ctx, [
|
|
81
|
+
...loaded.warnings,
|
|
82
|
+
...(trusted.warning === undefined ? [] : [trusted.warning]),
|
|
83
|
+
...(pyReason === undefined ? [] : [pyReason]),
|
|
84
|
+
]);
|
|
66
85
|
const enabledLanguages = enabledLanguagesFrom(settings, availability);
|
|
86
|
+
const jsProcessIsolation = resolveJsIsolation(settings) === "process";
|
|
67
87
|
const artifacts = resolveSessionArtifactsDir(ctx.sessionManager.getSessionFile());
|
|
68
88
|
const activeTools = new Set(pi.getActiveTools());
|
|
69
89
|
const executeTool = createExecuteTool(pi, activeTools);
|
|
@@ -72,6 +92,15 @@ export async function createRuntime(
|
|
|
72
92
|
const sessionEnv = { ...sessionEnvironmentFrom(ctx), ...bundledBunPathEntry() };
|
|
73
93
|
const configuredPoolWidth = settings.parallelPoolWidth;
|
|
74
94
|
const parallelPoolWidth = Number.isFinite(configuredPoolWidth) ? Math.max(1, Math.trunc(configuredPoolWidth)) : 1;
|
|
95
|
+
const pythonEnvironments =
|
|
96
|
+
availability.py.detected.ok && enabledLanguages.py
|
|
97
|
+
? new PythonEnvironments({
|
|
98
|
+
artifactsDir: artifacts.dir,
|
|
99
|
+
cwd: ctx.cwd,
|
|
100
|
+
interpreter: availability.py.detected.path,
|
|
101
|
+
settings,
|
|
102
|
+
})
|
|
103
|
+
: undefined;
|
|
75
104
|
const manager = await create({
|
|
76
105
|
sessionId,
|
|
77
106
|
ownerSessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -84,6 +113,7 @@ export async function createRuntime(
|
|
|
84
113
|
listTools: () => pi.getAllTools(),
|
|
85
114
|
complete,
|
|
86
115
|
...(handles === undefined ? {} : { handles }),
|
|
116
|
+
...(pythonEnvironments === undefined ? {} : { environments: { python: pythonEnvironments } }),
|
|
87
117
|
});
|
|
88
118
|
return {
|
|
89
119
|
sessionId,
|
|
@@ -91,14 +121,34 @@ export async function createRuntime(
|
|
|
91
121
|
parallelPoolWidth,
|
|
92
122
|
manager,
|
|
93
123
|
enabledLanguages,
|
|
94
|
-
runtimes: runtimesFromAvailability(
|
|
124
|
+
runtimes: runtimesFromAvailability(
|
|
125
|
+
availability,
|
|
126
|
+
(jsProcessIsolation ? processRuntimeInfo(undefined) : undefined) ??
|
|
127
|
+
jsRuntimeInfo(process.versions, process.execPath, jsProcessIsolation),
|
|
128
|
+
),
|
|
95
129
|
settings,
|
|
96
130
|
artifactsDir: artifacts.dir,
|
|
97
131
|
executeTool,
|
|
98
132
|
spawns: activeTools.has(settings.taskTools.task),
|
|
133
|
+
jsEnvironments: new JsEnvironments({
|
|
134
|
+
artifactsDir: artifacts.dir,
|
|
135
|
+
cwd: ctx.cwd,
|
|
136
|
+
runtime: jsRuntimeInfo().name,
|
|
137
|
+
env: { ...process.env, ...sessionEnv },
|
|
138
|
+
settings,
|
|
139
|
+
}),
|
|
140
|
+
...(pythonEnvironments === undefined ? {} : { pythonEnvironments }),
|
|
99
141
|
};
|
|
100
142
|
}
|
|
101
143
|
|
|
144
|
+
/** Settings problems reach the user: a notice with a UI, otherwise stderr (print and RPC runs). */
|
|
145
|
+
function reportSettingsProblems(ctx: ExtensionContext, problems: readonly string[]): void {
|
|
146
|
+
for (const problem of problems) {
|
|
147
|
+
if (ctx.hasUI) ctx.ui.notify(`[senpi-codemode] ${problem}`, "warning");
|
|
148
|
+
else console.error(`[senpi-codemode] ${problem}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
102
152
|
export function createExecuteTool(pi: CodemodeRuntimeAPI, activeTools?: ReadonlySet<string>): AgentExecuteTool {
|
|
103
153
|
// A cell names tools directly and cannot run tool_search first, so eval opts in to
|
|
104
154
|
// lazy activation. Eligibility still belongs to the extension that registered the tool.
|
|
@@ -10,10 +10,14 @@ export interface JsRuntimeVersions {
|
|
|
10
10
|
export function jsRuntimeInfo(
|
|
11
11
|
versions: JsRuntimeVersions = process.versions,
|
|
12
12
|
execPath: string = process.execPath,
|
|
13
|
+
processIsolation = false,
|
|
13
14
|
): EvalRuntimeInfo {
|
|
14
15
|
const bun = versions.bun;
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const base =
|
|
17
|
+
bun !== undefined && bun.length > 0
|
|
18
|
+
? { name: "bun", version: bun, path: execPath }
|
|
19
|
+
: { name: "node", version: versions.node, path: execPath };
|
|
20
|
+
return processIsolation ? { ...base, isolation: "process" } : base;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
/** Short host-line segment, e.g. "node 26.7.0" or "bun 1.4.0". */
|
|
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
|
2
2
|
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
3
3
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
4
4
|
import type { CodemodeSettings } from "../config/settings.ts";
|
|
5
|
+
import type { PackagesInstallEnvironments } from "../environments/packages-install.ts";
|
|
5
6
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
6
7
|
import type { InterpreterAvailability } from "../interpreters/detect.ts";
|
|
7
8
|
import type { SessionEnvironment } from "../kernels/session-env.ts";
|
|
@@ -42,4 +43,6 @@ export interface CreateCodemodeSessionManagerOptions {
|
|
|
42
43
|
readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
43
44
|
/** The session generation's handle registry; subprocess kernels reach `wait()`/`handle()` through the bridge. */
|
|
44
45
|
readonly handles?: HandleRegistry;
|
|
46
|
+
/** The Python environment `packages.install("pip", ...)` reaches from a Python cell over the bridge. */
|
|
47
|
+
readonly environments?: PackagesInstallEnvironments;
|
|
45
48
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
3
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
4
|
+
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
4
5
|
import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
|
|
5
6
|
import {
|
|
6
7
|
CodemodeSessionDisposedError,
|
|
@@ -106,6 +107,11 @@ export class SessionManagerProxy implements CodemodeSessionManager, EvalExecutio
|
|
|
106
107
|
this.#current?.releaseKernelListener?.(language, onMessage);
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
bindCellKernelTools(token: string, capability: KernelToolsCapability): () => void {
|
|
111
|
+
// Bound to the generation that runs the cell: a later generation never serves that cell's host calls.
|
|
112
|
+
return this.#current?.bindCellKernelTools?.(token, capability) ?? (() => undefined);
|
|
113
|
+
}
|
|
114
|
+
|
|
109
115
|
async complete(request: CompletionRequest, ctx: ExtensionContext): Promise<CompletionResult> {
|
|
110
116
|
this.assertEvalExecutionAllowed();
|
|
111
117
|
const current = this.#current;
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
3
|
+
import { readProcessFootprint } from "@code-yeongyu/senpi";
|
|
3
4
|
import { type BridgeServerHandle, startBridgeServer } from "../bridge/http-server.ts";
|
|
4
5
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
5
6
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
7
|
+
import { resolveJsIsolation, resolveKernelToolsEnabled } from "../config/feature-settings.ts";
|
|
6
8
|
import { resolveKernelMemoryThresholds } from "../config/memory-settings.ts";
|
|
7
9
|
import { collectOrphanedChildren } from "../host-sdk.ts";
|
|
8
10
|
import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
|
|
9
|
-
import type { KernelToolsDescribeResult } from "../kernels/js/kernel-tools-types.ts";
|
|
11
|
+
import type { KernelToolsCapability, KernelToolsDescribeResult } from "../kernels/js/kernel-tools-types.ts";
|
|
12
|
+
import type { SessionEnvironment } from "../kernels/session-env.ts";
|
|
10
13
|
import type { KernelLifecycle } from "../kernels/shared/kernel-death.ts";
|
|
11
14
|
import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
|
|
12
15
|
import { type BridgeDispatchContext, dispatchBridgeCompletion } from "./bridge-dispatch.ts";
|
|
13
16
|
import { type BridgeToolCallRequest, routeBridgeToolCall } from "./bridge-tool-call.ts";
|
|
17
|
+
import { CellKernelTools } from "./cell-kernel-tools.ts";
|
|
14
18
|
import { parkWhenIdle } from "./idle-parking-kernel.ts";
|
|
15
19
|
import {
|
|
16
20
|
javaScriptKernelMemory,
|
|
@@ -65,6 +69,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
65
69
|
readonly #registrations: SessionKernelRegistrations;
|
|
66
70
|
#kernelCreations = new Map<EvalLanguage, Promise<EvalKernel>>();
|
|
67
71
|
#onMessageRefs = new Map<EvalLanguage, (message: KernelToHostMessage) => void>();
|
|
72
|
+
readonly #cellKernelTools = new CellKernelTools();
|
|
68
73
|
#context: ExtensionContext | undefined;
|
|
69
74
|
#generation = 0;
|
|
70
75
|
#disposePromise: Promise<void> | undefined;
|
|
@@ -91,7 +96,13 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
async #call(request: BridgeToolCallRequest): Promise<unknown> {
|
|
94
|
-
return await
|
|
99
|
+
return await this.#cellKernelTools.run(request.cellToken, () =>
|
|
100
|
+
routeBridgeToolCall(this.#options, request, this.#context?.evalHandleHost),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
bindCellKernelTools(token: string, capability: KernelToolsCapability): () => void {
|
|
105
|
+
return this.#cellKernelTools.bind(token, capability);
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
async getKernel(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): Promise<EvalKernel> {
|
|
@@ -244,6 +255,8 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
244
255
|
this.#options.localRoots ??
|
|
245
256
|
(this.#options.artifactsDir ? { local: join(this.#options.artifactsDir, "local") } : undefined);
|
|
246
257
|
if (language === "js") {
|
|
258
|
+
const memoryThresholds = resolveKernelMemoryThresholds(this.#options.settings.memory);
|
|
259
|
+
const processIsolation = resolveJsIsolation(this.#options.settings) === "process";
|
|
247
260
|
const kernel = new JavaScriptKernel({
|
|
248
261
|
sessionId: this.#options.sessionId,
|
|
249
262
|
cwd: this.#options.cwd,
|
|
@@ -251,8 +264,13 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
251
264
|
onMessage,
|
|
252
265
|
hostToolNames: () => this.#options.listTools?.().map((tool) => tool.name) ?? [],
|
|
253
266
|
foreignLanguageNames: () => this.#foreignKernelToolNames(),
|
|
254
|
-
memory:
|
|
267
|
+
memory: memoryThresholds,
|
|
255
268
|
collectOrphanedChildren,
|
|
269
|
+
...(processIsolation ? { isolation: "process" as const } : {}),
|
|
270
|
+
...(processIsolation && memoryThresholds !== undefined
|
|
271
|
+
? { processMemory: { thresholds: memoryThresholds, readFootprint: readProcessFootprint } }
|
|
272
|
+
: {}),
|
|
273
|
+
...(resolveKernelToolsEnabled(this.#options.settings) ? {} : { kernelToolsEnabled: false }),
|
|
256
274
|
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
257
275
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
258
276
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
@@ -268,10 +286,14 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
268
286
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
269
287
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
270
288
|
};
|
|
289
|
+
// The Python prelude reads this when a cell defines an @tool; only a disabled setting adds it.
|
|
290
|
+
const sessionEnv: SessionEnvironment | undefined = resolveKernelToolsEnabled(this.#options.settings)
|
|
291
|
+
? this.#options.sessionEnv
|
|
292
|
+
: { ...this.#options.sessionEnv, SENPI_CODEMODE_KERNEL_TOOLS: "0" };
|
|
271
293
|
const shared = {
|
|
272
294
|
sessionId: this.#options.sessionId,
|
|
273
295
|
cwd: this.#options.cwd,
|
|
274
|
-
...(
|
|
296
|
+
...(sessionEnv ? { sessionEnv } : {}),
|
|
275
297
|
connection,
|
|
276
298
|
onMessage,
|
|
277
299
|
...lifecycle,
|