@code-yeongyu/senpi-codemode 2026.10.10 → 2026.10.11
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 +196 -0
- package/README.md +113 -11
- package/package.json +6 -5
- package/src/bridge/memory-protocol.ts +8 -1
- package/src/bridge/protocol.ts +9 -4
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +52 -0
- package/src/bridges/eval-virtual-schemas.ts +11 -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/editable-check.ts +1 -1
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/py-environment.ts +7 -3
- package/src/environments/py-installer.ts +5 -1
- package/src/environments/python-environments.ts +1 -1
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/runtime-factory.ts +42 -13
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-cwd.ts +25 -9
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +20 -6
- package/src/index.ts +2 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/globals.jl +128 -0
- package/src/kernels/jl/prelude.jl +15 -2
- package/src/kernels/jl/runner.jl +35 -143
- 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 +23 -24
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/group-signal-notice.js +117 -0
- package/src/kernels/js/inline-worker.ts +2 -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 +8 -0
- 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-tree-host.ts +4 -8
- package/src/kernels/js/process-tree.d.ts +11 -0
- package/src/kernels/js/process-tree.js +55 -10
- 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/windows-tree-kill-host.ts +117 -0
- package/src/kernels/js/windows-tree-kill.d.ts +19 -0
- package/src/kernels/js/windows-tree-kill.js +100 -0
- package/src/kernels/js/worker-core.js +51 -8
- package/src/kernels/js/worker-cwd.js +46 -12
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- 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 +66 -13
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +9 -0
- package/src/kernels/js/worker-slot.ts +8 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/py/prelude.py +69 -27
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/py/transport.ts +4 -0
- package/src/kernels/rb/runner.rb +9 -19
- package/src/kernels/sandbox/sandbox-cell.ts +26 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
- package/src/kernels/shared/bun-ffi.ts +24 -0
- package/src/kernels/shared/kernel-memory-host.ts +30 -7
- package/src/kernels/shared/process-cpu.ts +7 -3
- package/src/kernels/shared/process-group-cpu.ts +6 -3
- package/src/kernels/shared/subprocess-kernel.ts +29 -3
- package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
- package/src/kernels/shared/subprocess-process.ts +24 -2
- package/src/kernels/shared/subprocess-queue.ts +1 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +3 -0
- 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-tool.ts +4 -2
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render-agent.ts +110 -0
- package/src/tool/render-blocks.ts +252 -0
- package/src/tool/render-call.ts +130 -0
- package/src/tool/render-cell.ts +165 -0
- package/src/tool/render-detail.ts +152 -0
- package/src/tool/render-live.ts +288 -0
- package/src/tool/render-result.ts +202 -0
- package/src/tool/render-status.ts +166 -0
- package/src/tool/render-tail.ts +139 -0
- package/src/tool/render.ts +3 -1069
- package/src/tool/run-eval-cell.ts +24 -14
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +13 -2
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { AgentToolResult, EvalHandleHost } from "@code-yeongyu/senpi";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
RESERVED_AGENT_TOOL,
|
|
4
|
+
RESERVED_OUTPUT_TOOL,
|
|
5
|
+
RESERVED_PACKAGES_INSTALL_TOOL,
|
|
6
|
+
RESERVED_SCHEMA_TOOL,
|
|
7
|
+
} from "../bridge/reserved.ts";
|
|
8
|
+
import { type PackagesInstallEnvironments, runPackagesInstall } from "../environments/packages-install.ts";
|
|
3
9
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
4
10
|
import type { EvalStatusEvent, ExecuteTool } from "../tool/types.ts";
|
|
5
11
|
import { type AgentExecuteTool, runEvalAgent } from "./agent-bridge.ts";
|
|
@@ -21,6 +27,8 @@ export interface ReservedDispatchContext {
|
|
|
21
27
|
readonly handles?: HandleRegistry;
|
|
22
28
|
/** `ctx.evalHandleHost` at dispatch time; agent/workpool refs need it, completion refs do not. */
|
|
23
29
|
readonly evalHandleHost?: EvalHandleHost;
|
|
30
|
+
/** The calling cell's session environments; `packages.install()` fails with environment_installer_unavailable without one. */
|
|
31
|
+
readonly environments?: PackagesInstallEnvironments;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
class SchemaUnavailableError extends Error {
|
|
@@ -36,6 +44,7 @@ export function isReservedToolName(toolName: string): boolean {
|
|
|
36
44
|
toolName === RESERVED_AGENT_TOOL ||
|
|
37
45
|
toolName === RESERVED_OUTPUT_TOOL ||
|
|
38
46
|
toolName === RESERVED_SCHEMA_TOOL ||
|
|
47
|
+
toolName === RESERVED_PACKAGES_INSTALL_TOOL ||
|
|
39
48
|
isHandleToolName(toolName)
|
|
40
49
|
);
|
|
41
50
|
}
|
|
@@ -66,6 +75,9 @@ export async function runReservedTool(toolName: string, context: ReservedDispatc
|
|
|
66
75
|
marshalToolResult: context.marshalToolResult,
|
|
67
76
|
});
|
|
68
77
|
}
|
|
78
|
+
if (toolName === RESERVED_PACKAGES_INSTALL_TOOL) {
|
|
79
|
+
return await runPackagesInstall(context.args, context.environments ?? {}, context.signal);
|
|
80
|
+
}
|
|
69
81
|
if (toolName === RESERVED_SCHEMA_TOOL) {
|
|
70
82
|
const listTools = context.listTools;
|
|
71
83
|
if (listTools === undefined) throw new SchemaUnavailableError();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import executableSettings from "./executable-settings.json" with { type: "json" };
|
|
3
|
+
import type { CodemodeSettings } from "./settings.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Settings that name an executable run at session start (an interpreter's `--version` probe, then the kernel). The
|
|
7
|
+
* one list of them lives in `executable-settings.json`, which senpi's project-trust check also reads: a project
|
|
8
|
+
* codemode file that sets any of them asks for trust. Without trust they are dropped with a warning and never run.
|
|
9
|
+
* Trust is asked only when the project's own file sets one, so a host that cannot answer still starts sessions;
|
|
10
|
+
* such a host counts as not trusting the project.
|
|
11
|
+
*/
|
|
12
|
+
export const PROJECT_EXECUTABLE_SETTINGS: readonly string[] = executableSettings.projectExecutableSettings;
|
|
13
|
+
|
|
14
|
+
export function withoutUntrustedInterpreter<Settings extends CodemodeSettings>(
|
|
15
|
+
settings: Settings,
|
|
16
|
+
source: string | null,
|
|
17
|
+
cwd: string,
|
|
18
|
+
isProjectTrusted: (() => boolean) | undefined,
|
|
19
|
+
): { readonly settings: Settings; readonly warning?: string } {
|
|
20
|
+
const projectFile = join(cwd, ".senpi", "codemode.json");
|
|
21
|
+
if (source !== projectFile) return { settings };
|
|
22
|
+
const named = PROJECT_EXECUTABLE_SETTINGS.filter((path) => valueAt(settings, path) !== undefined);
|
|
23
|
+
if (named.length === 0 || isProjectTrusted?.() === true) return { settings };
|
|
24
|
+
let trimmed: Settings = settings;
|
|
25
|
+
for (const path of named) trimmed = withoutPath(trimmed, path);
|
|
26
|
+
const list = named.map((path) => `${path} "${String(valueAt(settings, path))}"`).join(", ");
|
|
27
|
+
return {
|
|
28
|
+
settings: trimmed,
|
|
29
|
+
warning: `${list} in ${projectFile} is ignored because this project is not trusted; it was not run`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function valueAt(value: unknown, path: string): unknown {
|
|
34
|
+
let current: unknown = value;
|
|
35
|
+
for (const key of path.split(".")) {
|
|
36
|
+
if (typeof current !== "object" || current === null) return undefined;
|
|
37
|
+
current = Reflect.get(current, key);
|
|
38
|
+
}
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function withoutPath<T>(value: T, path: string): T {
|
|
43
|
+
const copy = structuredClone(value);
|
|
44
|
+
const keys = path.split(".");
|
|
45
|
+
const last = keys.pop();
|
|
46
|
+
let parent: unknown = copy;
|
|
47
|
+
for (const key of keys)
|
|
48
|
+
parent = typeof parent === "object" && parent !== null ? Reflect.get(parent, key) : undefined;
|
|
49
|
+
if (last !== undefined && typeof parent === "object" && parent !== null) Reflect.deleteProperty(parent, last);
|
|
50
|
+
return copy;
|
|
51
|
+
}
|
|
@@ -20,6 +20,8 @@ export function resolveEnabledLanguages(
|
|
|
20
20
|
env: Environment = process.env,
|
|
21
21
|
): CodemodeSettings["languages"] {
|
|
22
22
|
return {
|
|
23
|
+
// Keep the non-flag keys (languages.pyInterpreter); only the four enable flags take env overrides.
|
|
24
|
+
...settings.languages,
|
|
23
25
|
py: resolveLanguage(settings.languages.py, env[languageEnvironmentFlags.py]),
|
|
24
26
|
js: resolveLanguage(settings.languages.js, env[languageEnvironmentFlags.js]),
|
|
25
27
|
rb: resolveLanguage(settings.languages.rb, env[languageEnvironmentFlags.rb]),
|
|
@@ -60,7 +60,7 @@ async function refuseOutsideLinks(root: string, dir: string): Promise<void> {
|
|
|
60
60
|
export async function assertInstalledInRevision(stdout: string, staging: string): Promise<void> {
|
|
61
61
|
const line = stdout.split("\n").find((entry) => entry.startsWith("Successfully installed "));
|
|
62
62
|
if (line === undefined) return;
|
|
63
|
-
const present = new Set((await readdir(staging).catch(()
|
|
63
|
+
const present = new Set((await readdir(staging).catch((): string[] => [])).flatMap(installedName));
|
|
64
64
|
for (const token of line.slice("Successfully installed ".length).trim().split(/\s+/u)) {
|
|
65
65
|
const name = token.slice(0, token.lastIndexOf("-"));
|
|
66
66
|
if (!present.has(canonicalName(name))) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { JsEnvironments, JsInstallReceipt } from "./js-environments.ts";
|
|
2
|
+
import type { InstallReceipt } from "./py-environment.ts";
|
|
3
|
+
import { EnvironmentError } from "./py-installer.ts";
|
|
4
|
+
import type { PythonEnvironments } from "./python-environments.ts";
|
|
5
|
+
|
|
6
|
+
export interface PackagesInstallEnvironments {
|
|
7
|
+
readonly python?: PythonEnvironments;
|
|
8
|
+
readonly js?: JsEnvironments;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const PACKAGES_INSTALL_DEFAULT_TIMEOUT_SECONDS = 600;
|
|
12
|
+
|
|
13
|
+
interface PackagesInstallRequest {
|
|
14
|
+
readonly manager: "pip" | "bun" | "npm";
|
|
15
|
+
readonly requirements: readonly string[];
|
|
16
|
+
readonly timeoutSeconds: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function invalid(message: string): EnvironmentError {
|
|
20
|
+
return new EnvironmentError("environment_install_failed", `packages.install(): ${message}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseRequest(args: unknown): PackagesInstallRequest {
|
|
24
|
+
if (typeof args !== "object" || args === null) throw invalid("expected (manager, requirements, {timeout?})");
|
|
25
|
+
const manager = "manager" in args ? args.manager : undefined;
|
|
26
|
+
if (manager !== "pip" && manager !== "bun" && manager !== "npm") {
|
|
27
|
+
throw invalid('manager must be "pip" (Python) or "bun" / "npm" (JavaScript)');
|
|
28
|
+
}
|
|
29
|
+
const raw = "requirements" in args ? args.requirements : undefined;
|
|
30
|
+
const list = typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : undefined;
|
|
31
|
+
if (
|
|
32
|
+
list === undefined ||
|
|
33
|
+
list.length === 0 ||
|
|
34
|
+
!list.every((item) => typeof item === "string" && item.trim() !== "")
|
|
35
|
+
) {
|
|
36
|
+
throw invalid("requirements must be a non-empty string or a non-empty list of non-empty strings");
|
|
37
|
+
}
|
|
38
|
+
const timeout = "timeout" in args ? args.timeout : undefined;
|
|
39
|
+
if (timeout !== undefined && (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0)) {
|
|
40
|
+
throw invalid("timeout must be a positive number of seconds");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
manager,
|
|
44
|
+
requirements: list.map((item: string) => item.trim()),
|
|
45
|
+
timeoutSeconds: timeout ?? PACKAGES_INSTALL_DEFAULT_TIMEOUT_SECONDS,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function jsArguments(requirements: readonly string[]): string {
|
|
50
|
+
for (const requirement of requirements) {
|
|
51
|
+
if (/\s/.test(requirement)) throw invalid(`a package spec cannot contain whitespace: ${requirement}`);
|
|
52
|
+
}
|
|
53
|
+
return requirements.join(" ");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `packages.install(manager, requirements, {timeout?})`: the in-cell form of `%pip install` / `%bun add` /
|
|
58
|
+
* `%npm add`. It drives the same session environment the magic cells use and returns its receipt; a stop of the
|
|
59
|
+
* owning cell cancels it (environment_install_cancelled), and the timeout fails it with environment_install_timeout.
|
|
60
|
+
* pip receives the requirement list as separate arguments, never re-split. The installer's output is not streamed
|
|
61
|
+
* into the cell: the receipt is the result, and a failure carries the installer's stderr tail.
|
|
62
|
+
*/
|
|
63
|
+
export async function runPackagesInstall(
|
|
64
|
+
args: unknown,
|
|
65
|
+
environments: PackagesInstallEnvironments,
|
|
66
|
+
signal: AbortSignal | undefined,
|
|
67
|
+
): Promise<InstallReceipt | JsInstallReceipt> {
|
|
68
|
+
const request = parseRequest(args);
|
|
69
|
+
const { python, js } = environments;
|
|
70
|
+
const manager = request.manager;
|
|
71
|
+
let install: ((signal: AbortSignal) => Promise<InstallReceipt | JsInstallReceipt>) | undefined;
|
|
72
|
+
if (manager === "pip") {
|
|
73
|
+
if (python !== undefined) install = (signal) => python.install(request.requirements, signal);
|
|
74
|
+
} else if (js !== undefined) {
|
|
75
|
+
install = (signal) => js.install(jsArguments(request.requirements), signal, undefined, manager);
|
|
76
|
+
}
|
|
77
|
+
if (install === undefined) {
|
|
78
|
+
throw new EnvironmentError(
|
|
79
|
+
"environment_installer_unavailable",
|
|
80
|
+
manager === "pip"
|
|
81
|
+
? "pip installs need this session to have a Python interpreter"
|
|
82
|
+
: "bun/npm installs run only in a JavaScript cell",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const timeout = AbortSignal.timeout(request.timeoutSeconds * 1_000);
|
|
86
|
+
const combined = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
87
|
+
try {
|
|
88
|
+
return await install(combined);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (timeout.aborted && !(signal?.aborted ?? false)) {
|
|
91
|
+
throw new EnvironmentError(
|
|
92
|
+
"environment_install_timeout",
|
|
93
|
+
`the install did not finish within ${request.timeoutSeconds}s and was stopped`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { assertInstalledInRevision, assertNoEditableInstalls } from "./editable-check.ts";
|
|
6
|
-
import { parsePipRequirements, runPipInstall } from "./py-installer.ts";
|
|
6
|
+
import { normalizePipInstall, parsePipRequirements, runPipInstall } from "./py-installer.ts";
|
|
7
7
|
import { publishNextRevision, type Revision } from "./revision-store.ts";
|
|
8
8
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
@@ -40,12 +40,16 @@ export async function installPythonPackages(input: {
|
|
|
40
40
|
readonly base: string;
|
|
41
41
|
readonly mode: EnvironmentMode;
|
|
42
42
|
readonly interpreter: string;
|
|
43
|
-
|
|
43
|
+
/** `%pip` argument text, or the requirement list `packages.install()` passes without re-parsing. */
|
|
44
|
+
readonly requirements: string | readonly string[];
|
|
44
45
|
readonly cwd: string;
|
|
45
46
|
readonly signal: AbortSignal;
|
|
46
47
|
readonly onOutput?: (stream: "stdout" | "stderr", data: string) => void;
|
|
47
48
|
}): Promise<InstallReceipt> {
|
|
48
|
-
const requested =
|
|
49
|
+
const requested =
|
|
50
|
+
typeof input.requirements === "string"
|
|
51
|
+
? parsePipRequirements(input.requirements)
|
|
52
|
+
: normalizePipInstall(["install", ...input.requirements]);
|
|
49
53
|
let stdout = "";
|
|
50
54
|
const { revision } = await publishNextRevision(
|
|
51
55
|
input.base,
|
|
@@ -125,7 +125,11 @@ export function splitShellWords(text: string, posix = process.platform !== "win3
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export function parsePipRequirements(text: string): string[] {
|
|
128
|
-
|
|
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[] {
|
|
129
133
|
const command = args[0] === "install" ? args.slice(1) : undefined;
|
|
130
134
|
if (command === undefined) {
|
|
131
135
|
throw new EnvironmentError("environment_install_failed", "only `%pip install <requirements>` is supported");
|
|
@@ -19,7 +19,10 @@ export interface BridgeToolCallRequest {
|
|
|
19
19
|
// and ordinary tool results are marshalled to { text, images, details, hasError } — the raw
|
|
20
20
|
// { content } shape left python cells unable to reach tool.read image blocks.
|
|
21
21
|
export async function routeBridgeToolCall(
|
|
22
|
-
options: Pick<
|
|
22
|
+
options: Pick<
|
|
23
|
+
CreateCodemodeSessionManagerOptions,
|
|
24
|
+
"executeTool" | "listTools" | "settings" | "handles" | "environments"
|
|
25
|
+
>,
|
|
23
26
|
request: BridgeToolCallRequest,
|
|
24
27
|
evalHandleHost?: EvalHandleHost,
|
|
25
28
|
): Promise<unknown> {
|
|
@@ -39,5 +42,6 @@ export async function routeBridgeToolCall(
|
|
|
39
42
|
marshalToolResult,
|
|
40
43
|
...(options.handles === undefined ? {} : { handles: options.handles }),
|
|
41
44
|
...(evalHandleHost === undefined ? {} : { evalHandleHost }),
|
|
45
|
+
...(options.environments === undefined ? {} : { environments: options.environments }),
|
|
42
46
|
});
|
|
43
47
|
}
|
|
@@ -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
|
|
|
@@ -2,6 +2,8 @@ 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,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
getInterpreterAvailability,
|
|
17
19
|
type InterpreterAvailability,
|
|
18
20
|
} from "../interpreters/detect.ts";
|
|
21
|
+
import { processRuntimeInfo } from "../kernels/js/process-worker.ts";
|
|
19
22
|
import { sessionEnvironmentFrom } from "../kernels/session-env.ts";
|
|
20
23
|
import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
|
|
21
24
|
import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
|
|
@@ -62,12 +65,25 @@ export async function createRuntime(
|
|
|
62
65
|
handles?: HandleRegistry,
|
|
63
66
|
): Promise<SessionRuntime> {
|
|
64
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
|
+
);
|
|
65
74
|
const settings: ResolvedCodemodeSettings = {
|
|
66
|
-
...
|
|
67
|
-
languages: resolveEnabledLanguages(
|
|
75
|
+
...trusted.settings,
|
|
76
|
+
languages: resolveEnabledLanguages(trusted.settings),
|
|
68
77
|
};
|
|
69
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
|
+
]);
|
|
70
85
|
const enabledLanguages = enabledLanguagesFrom(settings, availability);
|
|
86
|
+
const jsProcessIsolation = resolveJsIsolation(settings) === "process";
|
|
71
87
|
const artifacts = resolveSessionArtifactsDir(ctx.sessionManager.getSessionFile());
|
|
72
88
|
const activeTools = new Set(pi.getActiveTools());
|
|
73
89
|
const executeTool = createExecuteTool(pi, activeTools);
|
|
@@ -76,6 +92,15 @@ export async function createRuntime(
|
|
|
76
92
|
const sessionEnv = { ...sessionEnvironmentFrom(ctx), ...bundledBunPathEntry() };
|
|
77
93
|
const configuredPoolWidth = settings.parallelPoolWidth;
|
|
78
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;
|
|
79
104
|
const manager = await create({
|
|
80
105
|
sessionId,
|
|
81
106
|
ownerSessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -88,6 +113,7 @@ export async function createRuntime(
|
|
|
88
113
|
listTools: () => pi.getAllTools(),
|
|
89
114
|
complete,
|
|
90
115
|
...(handles === undefined ? {} : { handles }),
|
|
116
|
+
...(pythonEnvironments === undefined ? {} : { environments: { python: pythonEnvironments } }),
|
|
91
117
|
});
|
|
92
118
|
return {
|
|
93
119
|
sessionId,
|
|
@@ -95,7 +121,11 @@ export async function createRuntime(
|
|
|
95
121
|
parallelPoolWidth,
|
|
96
122
|
manager,
|
|
97
123
|
enabledLanguages,
|
|
98
|
-
runtimes: runtimesFromAvailability(
|
|
124
|
+
runtimes: runtimesFromAvailability(
|
|
125
|
+
availability,
|
|
126
|
+
(jsProcessIsolation ? processRuntimeInfo(undefined) : undefined) ??
|
|
127
|
+
jsRuntimeInfo(process.versions, process.execPath, jsProcessIsolation),
|
|
128
|
+
),
|
|
99
129
|
settings,
|
|
100
130
|
artifactsDir: artifacts.dir,
|
|
101
131
|
executeTool,
|
|
@@ -107,19 +137,18 @@ export async function createRuntime(
|
|
|
107
137
|
env: { ...process.env, ...sessionEnv },
|
|
108
138
|
settings,
|
|
109
139
|
}),
|
|
110
|
-
...(
|
|
111
|
-
? {
|
|
112
|
-
pythonEnvironments: new PythonEnvironments({
|
|
113
|
-
artifactsDir: artifacts.dir,
|
|
114
|
-
cwd: ctx.cwd,
|
|
115
|
-
interpreter: availability.py.detected.path,
|
|
116
|
-
settings,
|
|
117
|
-
}),
|
|
118
|
-
}
|
|
119
|
-
: {}),
|
|
140
|
+
...(pythonEnvironments === undefined ? {} : { pythonEnvironments }),
|
|
120
141
|
};
|
|
121
142
|
}
|
|
122
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
|
+
|
|
123
152
|
export function createExecuteTool(pi: CodemodeRuntimeAPI, activeTools?: ReadonlySet<string>): AgentExecuteTool {
|
|
124
153
|
// A cell names tools directly and cannot run tool_search first, so eval opts in to
|
|
125
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". */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
1
2
|
import { stat } from "node:fs/promises";
|
|
2
3
|
|
|
3
4
|
// Only these codes mean the directory is gone or was never one; anything else (EACCES, ELOOP,
|
|
@@ -21,15 +22,30 @@ export async function assertSessionCwdAvailable(cwd: string): Promise<void> {
|
|
|
21
22
|
try {
|
|
22
23
|
isDirectory = (await stat(cwd)).isDirectory();
|
|
23
24
|
} catch (error) {
|
|
24
|
-
|
|
25
|
-
error instanceof Error &&
|
|
26
|
-
"code" in error &&
|
|
27
|
-
typeof error.code === "string" &&
|
|
28
|
-
MISSING_DIRECTORY_CODES.has(error.code)
|
|
29
|
-
) {
|
|
30
|
-
throw new CodemodeSessionCwdUnavailableError(cwd, error.code);
|
|
31
|
-
}
|
|
32
|
-
throw error;
|
|
25
|
+
throw sessionCwdStatError(cwd, error);
|
|
33
26
|
}
|
|
34
27
|
if (!isDirectory) throw new CodemodeSessionCwdUnavailableError(cwd, "not a directory");
|
|
35
28
|
}
|
|
29
|
+
|
|
30
|
+
/** Warm reuse checks every cell without a filesystem thread-pool round trip. */
|
|
31
|
+
export function assertSessionCwdAvailableSync(cwd: string): void {
|
|
32
|
+
let isDirectory: boolean;
|
|
33
|
+
try {
|
|
34
|
+
isDirectory = statSync(cwd).isDirectory();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw sessionCwdStatError(cwd, error);
|
|
37
|
+
}
|
|
38
|
+
if (!isDirectory) throw new CodemodeSessionCwdUnavailableError(cwd, "not a directory");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sessionCwdStatError(cwd: string, error: unknown): unknown {
|
|
42
|
+
if (
|
|
43
|
+
error instanceof Error &&
|
|
44
|
+
"code" in error &&
|
|
45
|
+
typeof error.code === "string" &&
|
|
46
|
+
MISSING_DIRECTORY_CODES.has(error.code)
|
|
47
|
+
) {
|
|
48
|
+
return new CodemodeSessionCwdUnavailableError(cwd, error.code);
|
|
49
|
+
}
|
|
50
|
+
return error;
|
|
51
|
+
}
|
|
@@ -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,12 +1,15 @@
|
|
|
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
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";
|
|
@@ -21,7 +24,7 @@ import {
|
|
|
21
24
|
} from "./kernel-registration.ts";
|
|
22
25
|
import type { RegisteredKernelSource } from "./kernel-registry.ts";
|
|
23
26
|
import { ReplaceableKernel } from "./kernel-replacement.ts";
|
|
24
|
-
import { assertSessionCwdAvailable } from "./session-cwd.ts";
|
|
27
|
+
import { assertSessionCwdAvailable, assertSessionCwdAvailableSync } from "./session-cwd.ts";
|
|
25
28
|
import type {
|
|
26
29
|
BridgeEndpoint,
|
|
27
30
|
CodemodeSessionManager,
|
|
@@ -111,7 +114,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
111
114
|
this.#onMessageRefs.set(language, onMessage);
|
|
112
115
|
const existing = this.#kernels.get(language);
|
|
113
116
|
if (existing) {
|
|
114
|
-
|
|
117
|
+
assertSessionCwdAvailableSync(this.#options.cwd);
|
|
115
118
|
return existing;
|
|
116
119
|
}
|
|
117
120
|
const pending = this.#kernelCreations.get(language);
|
|
@@ -252,6 +255,8 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
252
255
|
this.#options.localRoots ??
|
|
253
256
|
(this.#options.artifactsDir ? { local: join(this.#options.artifactsDir, "local") } : undefined);
|
|
254
257
|
if (language === "js") {
|
|
258
|
+
const memoryThresholds = resolveKernelMemoryThresholds(this.#options.settings.memory);
|
|
259
|
+
const processIsolation = resolveJsIsolation(this.#options.settings) === "process";
|
|
255
260
|
const kernel = new JavaScriptKernel({
|
|
256
261
|
sessionId: this.#options.sessionId,
|
|
257
262
|
cwd: this.#options.cwd,
|
|
@@ -259,8 +264,13 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
259
264
|
onMessage,
|
|
260
265
|
hostToolNames: () => this.#options.listTools?.().map((tool) => tool.name) ?? [],
|
|
261
266
|
foreignLanguageNames: () => this.#foreignKernelToolNames(),
|
|
262
|
-
memory:
|
|
267
|
+
memory: memoryThresholds,
|
|
263
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 }),
|
|
264
274
|
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
265
275
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
266
276
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
@@ -276,10 +286,14 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
276
286
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
277
287
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
278
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" };
|
|
279
293
|
const shared = {
|
|
280
294
|
sessionId: this.#options.sessionId,
|
|
281
295
|
cwd: this.#options.cwd,
|
|
282
|
-
...(
|
|
296
|
+
...(sessionEnv ? { sessionEnv } : {}),
|
|
283
297
|
connection,
|
|
284
298
|
onMessage,
|
|
285
299
|
...lifecycle,
|
|
@@ -297,8 +311,8 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
297
311
|
/** Only an already running JavaScript kernel can define a colliding name; none is started for this. */
|
|
298
312
|
#javaScriptKernelToolsDescribe(names: readonly string[]): Promise<KernelToolsDescribeResult> | undefined {
|
|
299
313
|
const js = this.#kernels.get("js");
|
|
300
|
-
if (!js ||
|
|
301
|
-
return
|
|
314
|
+
if (!js || typeof js.describeKernelTools !== "function") return undefined;
|
|
315
|
+
return js.describeKernelTools(names);
|
|
302
316
|
}
|
|
303
317
|
|
|
304
318
|
#foreignKernelToolNames(): string[] {
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
resolveMaxDetachedCells,
|
|
11
11
|
resolveRunBudgetSeconds,
|
|
12
12
|
} from "./config/settings.ts";
|
|
13
|
-
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
13
|
+
import { type EvalNotificationContent, EvalNotifier } from "./extension/eval-notifier.ts";
|
|
14
14
|
import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
|
|
15
15
|
import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
|
|
16
16
|
import { hostLine, modelIdFrom } from "./extension/host-facts.ts";
|
|
@@ -57,7 +57,7 @@ export interface CodemodeExtensionAPI {
|
|
|
57
57
|
getActiveTools(): string[];
|
|
58
58
|
getAllTools(): readonly EvalSchemaToolInfo[];
|
|
59
59
|
sendMessage(
|
|
60
|
-
message: { customType: string; content:
|
|
60
|
+
message: { customType: string; content: EvalNotificationContent; display: boolean },
|
|
61
61
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
62
62
|
): void;
|
|
63
63
|
/** Optional host event bus; a host without one turns extension event emission into a harmless no-op. */
|