@code-yeongyu/senpi-codemode 2026.10.8 → 2026.10.10-2
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 +58 -0
- package/README.md +103 -7
- package/package.json +6 -5
- package/src/bridge/http-server.ts +9 -1
- package/src/bridge/protocol.ts +4 -0
- package/src/bridges/eval-virtual-schemas.ts +1 -0
- 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/project-resolves.ts +54 -0
- package/src/environments/py-installer.ts +64 -4
- package/src/environments/python-environments.ts +85 -0
- package/src/environments/revision-store.ts +38 -7
- package/src/extension/bridge-tool-call.ts +2 -0
- package/src/extension/cell-kernel-tools.ts +28 -0
- package/src/extension/kernel-replacement.ts +20 -0
- package/src/extension/runtime-factory.ts +44 -2
- package/src/extension/session-manager-proxy.ts +6 -0
- package/src/extension/session-manager.ts +18 -3
- package/src/index.ts +2 -0
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/jl/prelude.jl +6 -1
- package/src/kernels/js/cell-dispatch.ts +44 -0
- package/src/kernels/js/context-manager.ts +25 -15
- package/src/kernels/js/host-entries.ts +62 -0
- package/src/kernels/js/kernel-contract.ts +2 -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/worker-core.js +3 -0
- package/src/kernels/js/worker-package-resolve.js +153 -0
- package/src/kernels/js/worker-runtime.js +4 -1
- package/src/kernels/js/worker-startup.ts +1 -0
- 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 +122 -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 +116 -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/prompt/eval-prompt.ts +8 -1
- package/src/tool/cell-handler.ts +10 -12
- 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/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/run-eval-cell.ts +57 -0
- package/src/tool/types.ts +48 -1
|
@@ -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,
|
|
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
|
|
|
@@ -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
|
+
}
|
|
@@ -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,12 +2,15 @@ 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 { withoutUntrustedInterpreter } from "../config/project-trust.ts";
|
|
5
6
|
import {
|
|
6
7
|
type CodemodeSettings,
|
|
7
8
|
loadCodemodeSettings,
|
|
8
9
|
type ResolvedCodemodeSettings,
|
|
9
10
|
resolveEnabledLanguages,
|
|
10
11
|
} from "../config/settings.ts";
|
|
12
|
+
import { JsEnvironments } from "../environments/js-environments.ts";
|
|
13
|
+
import { PythonEnvironments } from "../environments/python-environments.ts";
|
|
11
14
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
12
15
|
import {
|
|
13
16
|
createInterpreterDetector,
|
|
@@ -47,6 +50,8 @@ export type SessionRuntime = {
|
|
|
47
50
|
readonly artifactsDir: string;
|
|
48
51
|
readonly executeTool: AgentExecuteTool;
|
|
49
52
|
readonly spawns: boolean;
|
|
53
|
+
readonly pythonEnvironments?: PythonEnvironments;
|
|
54
|
+
readonly jsEnvironments: JsEnvironments;
|
|
50
55
|
};
|
|
51
56
|
|
|
52
57
|
export async function createRuntime(
|
|
@@ -58,11 +63,23 @@ export async function createRuntime(
|
|
|
58
63
|
handles?: HandleRegistry,
|
|
59
64
|
): Promise<SessionRuntime> {
|
|
60
65
|
const loaded = await loadCodemodeSettings({ cwd: ctx.cwd });
|
|
66
|
+
const trusted = withoutUntrustedInterpreter(
|
|
67
|
+
loaded.settings,
|
|
68
|
+
loaded.source,
|
|
69
|
+
ctx.cwd,
|
|
70
|
+
typeof ctx.isProjectTrusted === "function" ? () => ctx.isProjectTrusted() : undefined,
|
|
71
|
+
);
|
|
61
72
|
const settings: ResolvedCodemodeSettings = {
|
|
62
|
-
...
|
|
63
|
-
languages: resolveEnabledLanguages(
|
|
73
|
+
...trusted.settings,
|
|
74
|
+
languages: resolveEnabledLanguages(trusted.settings),
|
|
64
75
|
};
|
|
65
76
|
const availability = await getInterpreterAvailability(settings, createInterpreterDetector());
|
|
77
|
+
const pyReason = availability.py.detected.ok ? undefined : availability.py.detected.reason;
|
|
78
|
+
reportSettingsProblems(ctx, [
|
|
79
|
+
...loaded.warnings,
|
|
80
|
+
...(trusted.warning === undefined ? [] : [trusted.warning]),
|
|
81
|
+
...(pyReason === undefined ? [] : [pyReason]),
|
|
82
|
+
]);
|
|
66
83
|
const enabledLanguages = enabledLanguagesFrom(settings, availability);
|
|
67
84
|
const artifacts = resolveSessionArtifactsDir(ctx.sessionManager.getSessionFile());
|
|
68
85
|
const activeTools = new Set(pi.getActiveTools());
|
|
@@ -96,9 +113,34 @@ export async function createRuntime(
|
|
|
96
113
|
artifactsDir: artifacts.dir,
|
|
97
114
|
executeTool,
|
|
98
115
|
spawns: activeTools.has(settings.taskTools.task),
|
|
116
|
+
jsEnvironments: new JsEnvironments({
|
|
117
|
+
artifactsDir: artifacts.dir,
|
|
118
|
+
cwd: ctx.cwd,
|
|
119
|
+
runtime: jsRuntimeInfo().name,
|
|
120
|
+
env: { ...process.env, ...sessionEnv },
|
|
121
|
+
settings,
|
|
122
|
+
}),
|
|
123
|
+
...(availability.py.detected.ok && enabledLanguages.py
|
|
124
|
+
? {
|
|
125
|
+
pythonEnvironments: new PythonEnvironments({
|
|
126
|
+
artifactsDir: artifacts.dir,
|
|
127
|
+
cwd: ctx.cwd,
|
|
128
|
+
interpreter: availability.py.detected.path,
|
|
129
|
+
settings,
|
|
130
|
+
}),
|
|
131
|
+
}
|
|
132
|
+
: {}),
|
|
99
133
|
};
|
|
100
134
|
}
|
|
101
135
|
|
|
136
|
+
/** Settings problems reach the user: a notice with a UI, otherwise stderr (print and RPC runs). */
|
|
137
|
+
function reportSettingsProblems(ctx: ExtensionContext, problems: readonly string[]): void {
|
|
138
|
+
for (const problem of problems) {
|
|
139
|
+
if (ctx.hasUI) ctx.ui.notify(`[senpi-codemode] ${problem}`, "warning");
|
|
140
|
+
else console.error(`[senpi-codemode] ${problem}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
export function createExecuteTool(pi: CodemodeRuntimeAPI, activeTools?: ReadonlySet<string>): AgentExecuteTool {
|
|
103
145
|
// A cell names tools directly and cannot run tool_search first, so eval opts in to
|
|
104
146
|
// lazy activation. Eligibility still belongs to the extension that registered the tool.
|
|
@@ -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;
|
|
@@ -3,14 +3,17 @@ import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
|
3
3
|
import { type BridgeServerHandle, startBridgeServer } from "../bridge/http-server.ts";
|
|
4
4
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
5
5
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
6
|
+
import { resolveKernelToolsEnabled } from "../config/feature-settings.ts";
|
|
6
7
|
import { resolveKernelMemoryThresholds } from "../config/memory-settings.ts";
|
|
7
8
|
import { collectOrphanedChildren } from "../host-sdk.ts";
|
|
8
9
|
import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
|
|
9
|
-
import type { KernelToolsDescribeResult } from "../kernels/js/kernel-tools-types.ts";
|
|
10
|
+
import type { KernelToolsCapability, KernelToolsDescribeResult } from "../kernels/js/kernel-tools-types.ts";
|
|
11
|
+
import type { SessionEnvironment } from "../kernels/session-env.ts";
|
|
10
12
|
import type { KernelLifecycle } from "../kernels/shared/kernel-death.ts";
|
|
11
13
|
import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
|
|
12
14
|
import { type BridgeDispatchContext, dispatchBridgeCompletion } from "./bridge-dispatch.ts";
|
|
13
15
|
import { type BridgeToolCallRequest, routeBridgeToolCall } from "./bridge-tool-call.ts";
|
|
16
|
+
import { CellKernelTools } from "./cell-kernel-tools.ts";
|
|
14
17
|
import { parkWhenIdle } from "./idle-parking-kernel.ts";
|
|
15
18
|
import {
|
|
16
19
|
javaScriptKernelMemory,
|
|
@@ -65,6 +68,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
65
68
|
readonly #registrations: SessionKernelRegistrations;
|
|
66
69
|
#kernelCreations = new Map<EvalLanguage, Promise<EvalKernel>>();
|
|
67
70
|
#onMessageRefs = new Map<EvalLanguage, (message: KernelToHostMessage) => void>();
|
|
71
|
+
readonly #cellKernelTools = new CellKernelTools();
|
|
68
72
|
#context: ExtensionContext | undefined;
|
|
69
73
|
#generation = 0;
|
|
70
74
|
#disposePromise: Promise<void> | undefined;
|
|
@@ -91,7 +95,13 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
async #call(request: BridgeToolCallRequest): Promise<unknown> {
|
|
94
|
-
return await
|
|
98
|
+
return await this.#cellKernelTools.run(request.cellToken, () =>
|
|
99
|
+
routeBridgeToolCall(this.#options, request, this.#context?.evalHandleHost),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
bindCellKernelTools(token: string, capability: KernelToolsCapability): () => void {
|
|
104
|
+
return this.#cellKernelTools.bind(token, capability);
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
async getKernel(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): Promise<EvalKernel> {
|
|
@@ -253,6 +263,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
253
263
|
foreignLanguageNames: () => this.#foreignKernelToolNames(),
|
|
254
264
|
memory: resolveKernelMemoryThresholds(this.#options.settings.memory),
|
|
255
265
|
collectOrphanedChildren,
|
|
266
|
+
...(resolveKernelToolsEnabled(this.#options.settings) ? {} : { kernelToolsEnabled: false }),
|
|
256
267
|
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
257
268
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
258
269
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
@@ -268,10 +279,14 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
268
279
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
269
280
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
270
281
|
};
|
|
282
|
+
// The Python prelude reads this when a cell defines an @tool; only a disabled setting adds it.
|
|
283
|
+
const sessionEnv: SessionEnvironment | undefined = resolveKernelToolsEnabled(this.#options.settings)
|
|
284
|
+
? this.#options.sessionEnv
|
|
285
|
+
: { ...this.#options.sessionEnv, SENPI_CODEMODE_KERNEL_TOOLS: "0" };
|
|
271
286
|
const shared = {
|
|
272
287
|
sessionId: this.#options.sessionId,
|
|
273
288
|
cwd: this.#options.cwd,
|
|
274
|
-
...(
|
|
289
|
+
...(sessionEnv ? { sessionEnv } : {}),
|
|
275
290
|
connection,
|
|
276
291
|
onMessage,
|
|
277
292
|
...lifecycle,
|
package/src/index.ts
CHANGED
|
@@ -157,6 +157,8 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
157
157
|
complete,
|
|
158
158
|
settings: runtime.settings,
|
|
159
159
|
artifactsDir: runtime.artifactsDir,
|
|
160
|
+
...(runtime.pythonEnvironments === undefined ? {} : { pythonEnvironments: runtime.pythonEnvironments }),
|
|
161
|
+
jsEnvironments: runtime.jsEnvironments,
|
|
160
162
|
cellManager,
|
|
161
163
|
handles,
|
|
162
164
|
executionTracker: manager,
|
|
@@ -20,6 +20,8 @@ export interface InterpreterDetected {
|
|
|
20
20
|
|
|
21
21
|
export interface InterpreterUnavailable {
|
|
22
22
|
readonly ok: false;
|
|
23
|
+
/** Why a configured interpreter could not be used; unset when detection simply found none. */
|
|
24
|
+
readonly reason?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export type InterpreterDetection = InterpreterDetected | InterpreterUnavailable;
|
|
@@ -43,6 +45,8 @@ export interface CreateInterpreterDetectorOptions {
|
|
|
43
45
|
|
|
44
46
|
export interface InterpreterDetector {
|
|
45
47
|
detect(language: CodemodeLanguage): Promise<InterpreterDetection>;
|
|
48
|
+
/** Probes exactly this executable (no PATH candidates, no splitting on spaces). */
|
|
49
|
+
detectExplicit(path: string): Promise<InterpreterDetection>;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
export interface LanguageAvailability {
|
|
@@ -74,6 +78,9 @@ export function createInterpreterDetector(options: CreateInterpreterDetectorOpti
|
|
|
74
78
|
cache.set(language, pending);
|
|
75
79
|
return pending;
|
|
76
80
|
},
|
|
81
|
+
detectExplicit(path) {
|
|
82
|
+
return probeExplicit(path, probe);
|
|
83
|
+
},
|
|
77
84
|
};
|
|
78
85
|
}
|
|
79
86
|
|
|
@@ -82,7 +89,7 @@ export async function getInterpreterAvailability(
|
|
|
82
89
|
detector: InterpreterDetector,
|
|
83
90
|
): Promise<InterpreterAvailability> {
|
|
84
91
|
return {
|
|
85
|
-
py: await
|
|
92
|
+
py: await pythonAvailability(settings, detector),
|
|
86
93
|
js: await availabilityFor("js", settings.languages.js, detector),
|
|
87
94
|
rb: await availabilityFor("rb", settings.languages.rb, detector),
|
|
88
95
|
jl: await availabilityFor("jl", settings.languages.jl, detector),
|
|
@@ -100,6 +107,32 @@ async function availabilityFor(
|
|
|
100
107
|
};
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
/** `languages.pyInterpreter`, when set, is the only interpreter Python uses; a path that does not answer makes Python unavailable. */
|
|
111
|
+
async function pythonAvailability(
|
|
112
|
+
settings: CodemodeSettings,
|
|
113
|
+
detector: InterpreterDetector,
|
|
114
|
+
): Promise<LanguageAvailability> {
|
|
115
|
+
const configured = settings.languages.pyInterpreter;
|
|
116
|
+
if (configured === undefined || !settings.languages.py)
|
|
117
|
+
return await availabilityFor("py", settings.languages.py, detector);
|
|
118
|
+
return { enabled: true, detected: await detector.detectExplicit(configured) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function probeExplicit(path: string, probe: ExecFileProbe): Promise<InterpreterDetection> {
|
|
122
|
+
const reason = (detail: string): InterpreterUnavailable => ({
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: `languages.pyInterpreter "${path}" ${detail}; Python is unavailable in this session`,
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
const result = await probe(path, ["--version"], { timeoutMs: probeTimeoutMs });
|
|
128
|
+
const version = parseVersion(`${result.stdout}\n${result.stderr}`);
|
|
129
|
+
if (version === null) return reason("did not report a Python version");
|
|
130
|
+
return { ok: true, path, version, resolvedPath: path };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return reason(`could not run (${error instanceof Error ? error.message.split("\n")[0] : String(error)})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
103
136
|
async function detectUncached(
|
|
104
137
|
language: CodemodeLanguage,
|
|
105
138
|
hostPlatform: NodeJS.Platform,
|
|
@@ -210,9 +210,14 @@ function Base.getproperty(pool::SenpiWorkpool, name::Symbol)
|
|
|
210
210
|
getfield(pool, name)
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
-
function workpool(agent::AbstractDict, name::AbstractString; mode=nothing)
|
|
213
|
+
function workpool(agent::AbstractDict, name::AbstractString; mode=nothing, tools=nothing)
|
|
214
214
|
args = Dict{String, Any}("op" => "create", "agent" => agent, "name" => name)
|
|
215
215
|
mode !== nothing && (args["mode"] = mode)
|
|
216
|
+
if tools !== nothing
|
|
217
|
+
(tools isa AbstractVector && all(name -> name isa AbstractString, tools)) ||
|
|
218
|
+
throw(SenpiBridgeError("workpool(tools=...) takes a vector of tool names; got $(typeof(tools))", "invalid_tools"))
|
|
219
|
+
args["tools"] = collect(String, tools)
|
|
220
|
+
end
|
|
216
221
|
result = senpi_workpool_call(args)
|
|
217
222
|
details = get(result, "details", nothing)
|
|
218
223
|
if details isa AbstractDict
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { HostToKernelMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { HostCellExecutor } from "../../tool/types.ts";
|
|
3
|
+
import { inputAtStart } from "../shared/cell-source-at-start.ts";
|
|
4
|
+
import { refusedEntry } from "./host-entries.ts";
|
|
5
|
+
import {
|
|
6
|
+
type JavaScriptKernelOptions,
|
|
7
|
+
type JavaScriptRunInput,
|
|
8
|
+
resolveKernelToolNameSource,
|
|
9
|
+
} from "./kernel-contract.ts";
|
|
10
|
+
import type { LocalModuleLoader } from "./local-module-loader.ts";
|
|
11
|
+
|
|
12
|
+
export type CellDispatch =
|
|
13
|
+
| { readonly kind: "host"; readonly host: HostCellExecutor }
|
|
14
|
+
| { readonly kind: "worker"; readonly frames: readonly HostToKernelMessage[] };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Decides at the cell's turn how it runs: as a host entry (an install, or a turn-time refusal such as an unreadable
|
|
18
|
+
* `%load` file) or in the worker, with the frames the worker needs (the current kernel-tool names, then the cell).
|
|
19
|
+
*/
|
|
20
|
+
export function dispatchCell(
|
|
21
|
+
queued: JavaScriptRunInput,
|
|
22
|
+
loader: LocalModuleLoader,
|
|
23
|
+
options: Pick<JavaScriptKernelOptions, "hostToolNames" | "foreignLanguageNames">,
|
|
24
|
+
): CellDispatch {
|
|
25
|
+
const input = inputAtStart(queued);
|
|
26
|
+
if ("refused" in input) return { kind: "host", host: refusedEntry(input.refused) };
|
|
27
|
+
if (input.host !== undefined) return { kind: "host", host: input.host };
|
|
28
|
+
return {
|
|
29
|
+
kind: "worker",
|
|
30
|
+
frames: [
|
|
31
|
+
{
|
|
32
|
+
type: "kernel-tools-names",
|
|
33
|
+
hostToolNames: resolveKernelToolNameSource(options.hostToolNames),
|
|
34
|
+
foreignLanguageNames: resolveKernelToolNameSource(options.foreignLanguageNames),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: "run",
|
|
38
|
+
cellId: input.cellId,
|
|
39
|
+
code: loader.prepareCell(input.code, input.kernelPreludes, input.sourceFile, input.packageRoot?.()),
|
|
40
|
+
timeoutMs: input.timeoutMs,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -3,14 +3,15 @@ import { CHILD_LIFECYCLE_OP, INTERRUPT_ACK_OP } from "../../bridge/reserved.ts";
|
|
|
3
3
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
4
4
|
import { KernelToolHostPump } from "../shared/kernel-tools-pump.ts";
|
|
5
5
|
import { ActiveCellControl } from "./active-cell-control.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { dispatchCell } from "./cell-dispatch.ts";
|
|
7
|
+
import { HostEntries } from "./host-entries.ts";
|
|
8
|
+
import { DEFAULT_INTERRUPT_BOUNDS, JS_INTERRUPT_GRACE_MS, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
7
9
|
import {
|
|
8
10
|
assertJavaScriptKernelOpen,
|
|
9
11
|
type JavaScriptKernelMode,
|
|
10
12
|
type JavaScriptRunInput,
|
|
11
13
|
type LifecycleState,
|
|
12
14
|
type ResultMessage,
|
|
13
|
-
resolveKernelToolNameSource,
|
|
14
15
|
type ToolCallMessage,
|
|
15
16
|
} from "./kernel-contract.ts";
|
|
16
17
|
import { type JavaScriptMemoryReading, KernelMemoryBridge } from "./kernel-memory-bridge.ts";
|
|
@@ -21,7 +22,7 @@ import type {
|
|
|
21
22
|
KernelToolsInvokeRequest,
|
|
22
23
|
} from "./kernel-tools-types.ts";
|
|
23
24
|
import { type JavaScriptKernelOptions, LocalModuleLoader } from "./local-module-loader.ts";
|
|
24
|
-
import { JavaScriptRunQueue } from "./run-queue.ts";
|
|
25
|
+
import { JavaScriptRunQueue, type PendingJavaScriptRun } from "./run-queue.ts";
|
|
25
26
|
import { ToolCallQueue } from "./tool-call-queue.ts";
|
|
26
27
|
import { WorkerChildren } from "./worker-children.ts";
|
|
27
28
|
import { crashedResult } from "./worker-host.ts";
|
|
@@ -34,6 +35,15 @@ export type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
|
34
35
|
export { type JavaScriptWorkerEntryUrlOptions, resolveJsWorkerEntryUrl } from "./worker-startup.ts";
|
|
35
36
|
|
|
36
37
|
export class JavaScriptKernel {
|
|
38
|
+
readonly #hostEntries = new HostEntries<PendingJavaScriptRun>({
|
|
39
|
+
emit: (run, message) => (run.input.onMessage ?? this.#options.onMessage)?.(message),
|
|
40
|
+
settle: (run, result) => {
|
|
41
|
+
if (!this.#runs.releaseActive(run)) return;
|
|
42
|
+
this.#runs.settle(run, result);
|
|
43
|
+
this.#startNext();
|
|
44
|
+
},
|
|
45
|
+
durationMs: (run) => this.#runs.durationMs(run, performance.now()),
|
|
46
|
+
});
|
|
37
47
|
readonly #options: JavaScriptKernelOptions;
|
|
38
48
|
readonly #moduleLoader: LocalModuleLoader;
|
|
39
49
|
readonly #slot: WorkerSlot;
|
|
@@ -128,6 +138,7 @@ export class JavaScriptKernel {
|
|
|
128
138
|
const cancelled = this.cancelQueued(cellId, reason);
|
|
129
139
|
return { stateRetained: Promise.resolve(true), ...(cancelled ? {} : { note: "cell not found" }) };
|
|
130
140
|
}
|
|
141
|
+
if (active && this.#hostEntries.abort(active, reason)) return { stateRetained: Promise.resolve(true) };
|
|
131
142
|
if (!active) {
|
|
132
143
|
// A worker still stuck in startup is not a healthy idle worker: retiring it is the only recovery.
|
|
133
144
|
const wedgedInStartup = this.#slot.startingUp;
|
|
@@ -162,11 +173,14 @@ export class JavaScriptKernel {
|
|
|
162
173
|
if (this.#closePromise) return await this.#closePromise;
|
|
163
174
|
this.#slot.postMessage({ type: "close" });
|
|
164
175
|
this.#lifecycle = "closing";
|
|
176
|
+
const graceMs = this.#options.interruptBounds?.graceMs ?? JS_INTERRUPT_GRACE_MS;
|
|
177
|
+
const hostEntriesStopped = this.#hostEntries.stopAll("JS kernel closed", graceMs);
|
|
165
178
|
this.#runs.settleAll("JS kernel closed");
|
|
166
179
|
this.#kernelTools.rejectAll(kernelToolError("kernel_tool_stale", "JS kernel closed"));
|
|
167
180
|
this.#toolCalls.clear();
|
|
168
181
|
const recovery = this.#recovery.inFlight;
|
|
169
182
|
const closePromise = (async () => {
|
|
183
|
+
await hostEntriesStopped;
|
|
170
184
|
if (recovery) await recovery;
|
|
171
185
|
await this.#terminate();
|
|
172
186
|
})().finally(() => {
|
|
@@ -195,18 +209,13 @@ export class JavaScriptKernel {
|
|
|
195
209
|
if (this.#lifecycle !== "open" || this.#runs.active || !this.#slot.present) return;
|
|
196
210
|
const next = this.#runs.startNext(performance.now());
|
|
197
211
|
if (!next) return;
|
|
212
|
+
const dispatch = dispatchCell(next.input, this.#moduleLoader, this.#options);
|
|
213
|
+
if (dispatch.kind === "host") {
|
|
214
|
+
this.#hostEntries.start(next, dispatch.host);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
198
217
|
this.#activeCell.arm(next);
|
|
199
|
-
this.#slot.postMessage(
|
|
200
|
-
type: "kernel-tools-names",
|
|
201
|
-
hostToolNames: resolveKernelToolNameSource(this.#options.hostToolNames),
|
|
202
|
-
foreignLanguageNames: resolveKernelToolNameSource(this.#options.foreignLanguageNames),
|
|
203
|
-
});
|
|
204
|
-
this.#slot.postMessage({
|
|
205
|
-
type: "run",
|
|
206
|
-
cellId: next.input.cellId,
|
|
207
|
-
code: this.#moduleLoader.prepareCell(next.input.code, next.input.kernelPreludes),
|
|
208
|
-
timeoutMs: next.input.timeoutMs,
|
|
209
|
-
});
|
|
218
|
+
for (const frame of dispatch.frames) this.#slot.postMessage(frame);
|
|
210
219
|
}
|
|
211
220
|
|
|
212
221
|
async #restartAfterStop(): Promise<void> {
|
|
@@ -247,7 +256,8 @@ export class JavaScriptKernel {
|
|
|
247
256
|
this.#activeCell.disarm();
|
|
248
257
|
this.#kernelTools.rejectAll(kernelToolError("kernel_tool_stale", error.message));
|
|
249
258
|
this.#memory.workerLost(error);
|
|
250
|
-
|
|
259
|
+
// A crash mid-install stops the install (process group killed, nothing published); it settles itself once stopped.
|
|
260
|
+
if (active && !this.#hostEntries.abort(active, `JavaScript worker crashed: ${error.message}`)) {
|
|
251
261
|
this.#runs.releaseActive(active);
|
|
252
262
|
this.#runs.settle(
|
|
253
263
|
active,
|