@code-yeongyu/senpi-codemode 2026.10.10-6 → 2026.10.10-8
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 +18 -0
- package/README.md +1 -0
- package/package.json +4 -4
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/reserved-dispatch.ts +13 -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/runtime-factory.ts +11 -10
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/kernels/js/worker-runtime.js +11 -0
- package/src/kernels/py/prelude.py +23 -0
- package/src/tool/cell-handler.ts +3 -0
- package/src/tool/cell-reserved-dispatch.ts +1 -0
- package/src/tool/run-eval-cell.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,12 +6,30 @@
|
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
8
|
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
### Removed
|
|
14
|
+
|
|
15
|
+
## [2026.10.10-8] - 2026-10-08
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `packages.install(manager, requirements, {timeout?})` in JavaScript and Python cells installs through the same session environment as `%pip install` / `%bun add` / `%npm add` and returns the install receipt; a stop cancels it and the timeout (600 seconds by default) fails it with `environment_install_timeout`.
|
|
22
|
+
|
|
9
23
|
- `tool_schema("eval:environments")` and `tool_schema("eval:isolation")` document the package magics (`%pip`, `%bun`/`%npm`, `%environment`, `%load`) and isolated cells on demand, with the error codes each one raises. Nothing is added to the eval prompt or its input schema.
|
|
10
24
|
|
|
11
25
|
### Changed
|
|
12
26
|
|
|
13
27
|
### Fixed
|
|
14
28
|
|
|
29
|
+
- The eval timing benchmark's spike retry no longer waits for a load at or above its own refusal ceiling: the settle target is capped at 70, so a run whose calm load was near 80 cannot start its retries straight into another spike and burn them all in seconds. On a host whose normal load is above 70, each retry now waits the full 15 minutes and then retries anyway; the attempt's own spike checks still decide what is kept. ([#2922](https://github.com/code-yeongyu/senpi/issues/2922))
|
|
30
|
+
|
|
31
|
+
- The eval timing benchmark no longer throws away a whole multi-block run when the host's 1-minute load goes over 80 partway through: only the block that saw the spike is discarded and re-run (up to 3 retries, each after the load has fallen back under 70). A runtime failure in the same block is never discarded with the spike. A block that spikes on every attempt is labelled, making the run inconclusive rather than refused, and the other blocks still report. Re-run blocks are listed in the JSON report's `retriedBlocks`. ([#2909](https://github.com/code-yeongyu/senpi/issues/2909))
|
|
32
|
+
|
|
15
33
|
### Removed
|
|
16
34
|
|
|
17
35
|
## [2026.10.10-6] - 2026-10-07
|
package/README.md
CHANGED
|
@@ -363,6 +363,7 @@ options object and asynchronous helpers are `await`-able.
|
|
|
363
363
|
| `write(path, content)` | Creates parent directories and writes text. `local://` paths persist in the session artifact root. |
|
|
364
364
|
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. Includes the session's `PI_*` values (see [Session environment](#session-environment)). |
|
|
365
365
|
| `tool.<name>(args)` | Invokes an active Senpi tool through the normal `pi.executeTool` pipeline and returns `{ text, images?, details?, hasError? }` in every kernel; image blocks arrive as `images[i] = { mimeType, dataBase64 }`. |
|
|
366
|
+
| `packages.install(manager, requirements, {timeout?})` (js, py) | The `%pip install` (py, manager `"pip"`) or `%bun add` / `%npm add` (js, `"bun"` / `"npm"`) installer as a call: same session environment, receipt (`py`: manager, mode, root, revision, requested, resolved, changed; `js`: installer, mode, revision, added, shadowed), cancellation by `stop` and `environment_install_*` codes; `timeout` defaults to 600 seconds and fails with `environment_install_timeout`. Python: `packages.install("pip", [...], timeout=...)`. |
|
|
366
367
|
| `tool_schema(name?)` | Returns a tool's parameter schema without calling it; omit `name` to list tool names. Five virtual entries document codemode itself on demand and are never listed: `eval:wait`, `eval:helpers`, `eval:kernel-tools`, `eval:environments` (the `%pip`, `%bun`/`%npm`, `%environment` and `%load` magics, their results and error codes) and `eval:isolation` (`isolate: true` cells, their limits and error codes). |
|
|
367
368
|
| `require(specifier)`, `createRequire(path)` (js) | CommonJS loading inside a cell, resolved like `import`: builtins (with or without `node:`), relative files and JSON from the cell's directory, bare packages from the session's project and then its managed package environment (`%bun add`/`%npm add`). A missing module raises the runtime's own `MODULE_NOT_FOUND` error. Like Node's `require`, it carries `require.resolve` (the same lookup the call uses, so both name the same copy; builtins resolve to the runtime's own id), `require.resolve.paths` and `require.cache` (the runtime's CommonJS cache). |
|
|
368
369
|
| `tool(fn, metadata?)` (js) | Registers a named function as a kernel tool for in-process children. `metadata.name` registers it under that name instead of the function's; arguments are still passed in the function's parameter order. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.10.10-
|
|
3
|
+
"version": "2026.10.10-8",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@babel/parser": "8.0.4",
|
|
36
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-
|
|
36
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-8",
|
|
37
37
|
"quickjs-wasi": "3.6.2",
|
|
38
38
|
"typebox": "1.3.34"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@code-yeongyu/senpi": "2026.10.10-
|
|
41
|
+
"@code-yeongyu/senpi": "2026.10.10-8"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@code-yeongyu/senpi": "2026.10.10-
|
|
44
|
+
"@code-yeongyu/senpi": "2026.10.10-8"
|
|
45
45
|
},
|
|
46
46
|
"keywords": [
|
|
47
47
|
"senpi",
|
package/src/bridge/reserved.ts
CHANGED
|
@@ -15,6 +15,8 @@ export const RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__" as const;
|
|
|
15
15
|
export const RESERVED_HANDLE_SEND_TOOL = "__handle_send__" as const;
|
|
16
16
|
/** `handle(node).control.cancel()`: idempotent for its epoch; a successor run is never touched. */
|
|
17
17
|
export const RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__" as const;
|
|
18
|
+
/** In-cell `packages.install(manager, requirements, {timeout?})`: the `%pip` / `%bun` / `%npm` installer as a call. */
|
|
19
|
+
export const RESERVED_PACKAGES_INSTALL_TOOL = "__packages_install__" as const;
|
|
18
20
|
/** Canonical oh-my-pi eval-timeout pause operation. */
|
|
19
21
|
export const TIMEOUT_PAUSE_OP = "timeout-pause" as const;
|
|
20
22
|
/** Canonical oh-my-pi eval-timeout resume operation. */
|
|
@@ -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,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
|
}
|
|
@@ -92,6 +92,15 @@ export async function createRuntime(
|
|
|
92
92
|
const sessionEnv = { ...sessionEnvironmentFrom(ctx), ...bundledBunPathEntry() };
|
|
93
93
|
const configuredPoolWidth = settings.parallelPoolWidth;
|
|
94
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;
|
|
95
104
|
const manager = await create({
|
|
96
105
|
sessionId,
|
|
97
106
|
ownerSessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -104,6 +113,7 @@ export async function createRuntime(
|
|
|
104
113
|
listTools: () => pi.getAllTools(),
|
|
105
114
|
complete,
|
|
106
115
|
...(handles === undefined ? {} : { handles }),
|
|
116
|
+
...(pythonEnvironments === undefined ? {} : { environments: { python: pythonEnvironments } }),
|
|
107
117
|
});
|
|
108
118
|
return {
|
|
109
119
|
sessionId,
|
|
@@ -127,16 +137,7 @@ export async function createRuntime(
|
|
|
127
137
|
env: { ...process.env, ...sessionEnv },
|
|
128
138
|
settings,
|
|
129
139
|
}),
|
|
130
|
-
...(
|
|
131
|
-
? {
|
|
132
|
-
pythonEnvironments: new PythonEnvironments({
|
|
133
|
-
artifactsDir: artifacts.dir,
|
|
134
|
-
cwd: ctx.cwd,
|
|
135
|
-
interpreter: availability.py.detected.path,
|
|
136
|
-
settings,
|
|
137
|
-
}),
|
|
138
|
-
}
|
|
139
|
-
: {}),
|
|
140
|
+
...(pythonEnvironments === undefined ? {} : { pythonEnvironments }),
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
143
|
|
|
@@ -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
|
}
|
|
@@ -162,6 +162,17 @@ export class JsWorkerRuntime {
|
|
|
162
162
|
return isPlainObject(opts) && opts.handle === true ? handles.handle(value) : value;
|
|
163
163
|
};
|
|
164
164
|
globalThis.wait = async (list, options) => await handles.wait(list, options);
|
|
165
|
+
// The `%bun add` / `%npm add` installer as a call: same environment, receipt, cancellation and codes (senpi row 38).
|
|
166
|
+
globalThis.packages = Object.freeze({
|
|
167
|
+
install: async (manager, requirements, options = {}) => {
|
|
168
|
+
if (!isPlainObject(options)) throw new TypeError("packages.install(): options must be an object, e.g. { timeout: 120 }");
|
|
169
|
+
return await this.#callTool("__packages_install__", {
|
|
170
|
+
manager,
|
|
171
|
+
requirements,
|
|
172
|
+
...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
});
|
|
165
176
|
globalThis.handle = value => handles.handle(value);
|
|
166
177
|
globalThis.tool = createToolNamespace(
|
|
167
178
|
(fn, metadata) => this.#tools.define(fn, metadata),
|
|
@@ -61,6 +61,7 @@ RESERVED_HANDLE_STATUS_TOOL = "__handle_status__"
|
|
|
61
61
|
RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__"
|
|
62
62
|
RESERVED_HANDLE_SEND_TOOL = "__handle_send__"
|
|
63
63
|
RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__"
|
|
64
|
+
RESERVED_PACKAGES_INSTALL_TOOL = "__packages_install__"
|
|
64
65
|
TIMEOUT_PAUSE_OP = "timeout-pause"
|
|
65
66
|
TIMEOUT_RESUME_OP = "timeout-resume"
|
|
66
67
|
|
|
@@ -1162,6 +1163,27 @@ def tool_schema(name: str | None = None) -> Any:
|
|
|
1162
1163
|
)
|
|
1163
1164
|
|
|
1164
1165
|
|
|
1166
|
+
class _Packages:
|
|
1167
|
+
"""packages.install(manager, requirements, *, timeout=600): the %pip installer as a call; returns its receipt."""
|
|
1168
|
+
|
|
1169
|
+
__slots__ = ()
|
|
1170
|
+
|
|
1171
|
+
def install(self, manager: str, requirements: Any, *, timeout: float | None = None) -> Any:
|
|
1172
|
+
args: dict[str, Any] = {"manager": manager, "requirements": requirements}
|
|
1173
|
+
if timeout is not None:
|
|
1174
|
+
args["timeout"] = timeout
|
|
1175
|
+
# A long install must not hit the 60 s socket cap; the host bounds it by the timeout (600 s by default).
|
|
1176
|
+
socket_timeout = (600 if timeout is None else float(timeout)) + _WAIT_SOCKET_GRACE_SECONDS
|
|
1177
|
+
return bridge_post(
|
|
1178
|
+
"/call",
|
|
1179
|
+
{"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_PACKAGES_INSTALL_TOOL, "args": args},
|
|
1180
|
+
socket_timeout=socket_timeout,
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
packages = _Packages()
|
|
1185
|
+
|
|
1186
|
+
|
|
1165
1187
|
def output(
|
|
1166
1188
|
*ids: str,
|
|
1167
1189
|
format: str = "raw",
|
|
@@ -1789,6 +1811,7 @@ USER_NS.update(
|
|
|
1789
1811
|
"workpool": workpool,
|
|
1790
1812
|
"output": output,
|
|
1791
1813
|
"tool_schema": tool_schema,
|
|
1814
|
+
"packages": packages,
|
|
1792
1815
|
"wait": wait,
|
|
1793
1816
|
"handle": handle,
|
|
1794
1817
|
"__senpi_magic": _magic,
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
|
14
14
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
15
15
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
16
16
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
17
|
+
import type { PackagesInstallEnvironments } from "../environments/packages-install.ts";
|
|
17
18
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
18
19
|
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
19
20
|
import {
|
|
@@ -54,6 +55,8 @@ export interface CellBridgeRuntime {
|
|
|
54
55
|
readonly handles?: HandleRegistry;
|
|
55
56
|
/** Absolute wall-clock deadline of this cell; a completion handle it creates is bounded by it. */
|
|
56
57
|
readonly hardDeadlineMs?: number;
|
|
58
|
+
/** A JS cell's session environment for `packages.install()`; Python cells reach theirs over the bridge. */
|
|
59
|
+
readonly environments?: PackagesInstallEnvironments;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export class CellHandler {
|
|
@@ -29,6 +29,7 @@ export function reservedDispatchContext(
|
|
|
29
29
|
marshalToolResult,
|
|
30
30
|
...(runtime.handles === undefined ? {} : { handles: runtime.handles }),
|
|
31
31
|
...(host === undefined ? {} : { evalHandleHost: host }),
|
|
32
|
+
...(runtime.environments === undefined ? {} : { environments: runtime.environments }),
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
10
10
|
import { resolveSandbox } from "../config/feature-settings.ts";
|
|
11
11
|
import { DEFAULT_FOREGROUND_WINDOW_SECONDS, defaultCodemodeSettings } from "../config/settings.ts";
|
|
12
|
+
import type { JsEnvironments } from "../environments/js-environments.ts";
|
|
13
|
+
import type { PackagesInstallEnvironments } from "../environments/packages-install.ts";
|
|
12
14
|
import {
|
|
13
15
|
KERNEL_TOOLS_CAPABILITIES,
|
|
14
16
|
type KernelToolsCapability,
|
|
@@ -241,6 +243,7 @@ async function executeCell(
|
|
|
241
243
|
...(kernelTools === undefined ? {} : { kernelTools }),
|
|
242
244
|
...(options.handles === undefined ? {} : { handles: options.handles }),
|
|
243
245
|
hardDeadlineMs: cell.startedAtMs + cell.hardLimitSeconds * 1_000,
|
|
246
|
+
...cellEnvironments(invocation.input.language, options),
|
|
244
247
|
});
|
|
245
248
|
handler = activeHandler;
|
|
246
249
|
cellManager.bindKernel(
|
|
@@ -364,3 +367,14 @@ function kernelToolsFor(kernel: EvalKernel): KernelToolsCapability | undefined {
|
|
|
364
367
|
invoke: (request, options) => withTools.invokeKernelTool(request, options),
|
|
365
368
|
} satisfies ExtensionKernelTools;
|
|
366
369
|
}
|
|
370
|
+
|
|
371
|
+
// Only JS cells call tools in-process; a Python cell reaches packages.install() over the bridge, whose session manager
|
|
372
|
+
// holds the same Python environment its magics use.
|
|
373
|
+
function cellEnvironments(
|
|
374
|
+
language: string,
|
|
375
|
+
options: { readonly jsEnvironments?: JsEnvironments },
|
|
376
|
+
): { readonly environments?: PackagesInstallEnvironments } {
|
|
377
|
+
if (language === "js" && options.jsEnvironments !== undefined)
|
|
378
|
+
return { environments: { js: options.jsEnvironments } };
|
|
379
|
+
return {};
|
|
380
|
+
}
|