diff --git a/packages/coding-agent/docs/rlm-runtime.md b/packages/coding-agent/docs/rlm-runtime.md index 4de095cf01..a3ceaf35dd 100644 --- a/packages/coding-agent/docs/rlm-runtime.md +++ b/packages/coding-agent/docs/rlm-runtime.md @@ -76,7 +76,7 @@ The Python side does not call providers or implement an agent loop. The kernel is created lazily on first IPython use. Python resolution is: 1. `PRIME_AGENT_KERNEL_PYTHON`, when it can import `ipykernel`; -2. `~/.prime/agent/kernel-venv/bin/python`, bootstrapped with `uv`; or +2. `~/.prime/agent/kernel-venv/bin/python` (`Scripts\python.exe` on Windows), bootstrapped with `uv`; or 3. the XDG data location when `~/.prime` is not writable. The managed environment includes Python 3.11, `ipykernel`, and `prime-agent-runtime`. A bootstrap marker detects stale environments. diff --git a/packages/coding-agent/src/core/kernel/bootstrap.ts b/packages/coding-agent/src/core/kernel/bootstrap.ts index 9b12b4b413..79ffc0e250 100644 --- a/packages/coding-agent/src/core/kernel/bootstrap.ts +++ b/packages/coding-agent/src/core/kernel/bootstrap.ts @@ -119,6 +119,10 @@ function expandHome(filePath: string): string { return filePath; } +export function venvPython(venv: string): string { + return process.platform === "win32" ? path.join(venv, "Scripts", "python.exe") : path.join(venv, "bin", "python"); +} + function fileContentHash(filePath: string): string { try { return `sha256:${createHash("sha256").update(readFileSync(filePath)).digest("hex")}`; @@ -725,7 +729,7 @@ async function bootstrapVenv( ): Promise { await mkdir(path.dirname(venv), { recursive: true }); const uv = await ensureUv(options); - const python = path.join(venv, "bin", "python"); + const python = venvPython(venv); const sourceDir = await resolveRuntimeSourceDir(); const runtimeRequirement = sourceDir ?? RUNTIME_REQUIREMENT; const runtimeIdentity = await resolveRuntimeIdentity(); @@ -886,7 +890,7 @@ async function ensureKernelPythonUncached( } const venv = await resolveWritableKernelVenvDir(); - const python = path.join(venv, "bin", "python"); + const python = venvPython(venv); const runtimeIdentity = await resolveRuntimeIdentity(); if (await kernelReady(python, venv, runtimeIdentity, pythonSkills)) return python; diff --git a/packages/coding-agent/test/ipython-bootstrap.test.ts b/packages/coding-agent/test/ipython-bootstrap.test.ts index 8aba99b463..fa5c23ba20 100644 --- a/packages/coding-agent/test/ipython-bootstrap.test.ts +++ b/packages/coding-agent/test/ipython-bootstrap.test.ts @@ -3,6 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, describe, expect, it } from "vitest"; +import { venvPython } from "../src/core/kernel/bootstrap.js"; import { KernelManager } from "../src/core/kernel/index.js"; import { buildRlmBootstrapCode } from "../src/core/tools/ipython.js"; @@ -44,7 +45,7 @@ describe("IPython RLM bootstrap", () => { function resolveKernelPython(): string | null { const candidates = [ process.env.PRIME_AGENT_KERNEL_PYTHON, - join(homedir(), ".prime", "agent", "kernel-venv", "bin", "python"), + venvPython(join(homedir(), ".prime", "agent", "kernel-venv")), ].filter((p): p is string => Boolean(p)); for (const python of candidates) { if (!existsSync(python)) continue; diff --git a/packages/coding-agent/test/kernel-bootstrap.test.ts b/packages/coding-agent/test/kernel-bootstrap.test.ts index eeb2fc0d15..bed16ba120 100644 --- a/packages/coding-agent/test/kernel-bootstrap.test.ts +++ b/packages/coding-agent/test/kernel-bootstrap.test.ts @@ -10,6 +10,7 @@ import { getKernelVenvDir, type KernelPythonSkill, resolveRuntimeIdentity, + venvPython, } from "../src/core/kernel/bootstrap.js"; let tempDir = ""; @@ -174,6 +175,21 @@ describe("kernel bootstrap", () => { expect(getKernelVenvDir()).toBe(venv); }); + it("resolves the venv interpreter using the host platform layout", () => { + const originalPlatform = process.platform; + const setPlatform = (value: NodeJS.Platform) => + Object.defineProperty(process, "platform", { value, configurable: true }); + try { + setPlatform("win32"); + expect(venvPython("C:\\venv")).toBe(join("C:\\venv", "Scripts", "python.exe")); + + setPlatform("linux"); + expect(venvPython("/venv")).toBe(join("/venv", "bin", "python")); + } finally { + setPlatform(originalPlatform); + } + }); + it("bootstraps a missing venv with uv, ipykernel, prime-agent-runtime, and default extra packages", async () => { const logPath = installFakeUv(); const venv = join(tempDir, "kernel-venv"); diff --git a/packages/coding-agent/test/kernel-state-roundtrip.test.ts b/packages/coding-agent/test/kernel-state-roundtrip.test.ts index e79d88d927..4b2b23522d 100644 --- a/packages/coding-agent/test/kernel-state-roundtrip.test.ts +++ b/packages/coding-agent/test/kernel-state-roundtrip.test.ts @@ -3,13 +3,14 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { venvPython } from "../src/core/kernel/bootstrap.js"; import { KernelManager } from "../src/core/kernel/index.js"; /** Find a python that can launch an ipykernel and has dill, or null to skip. */ function resolveKernelPython(): string | null { const candidates = [ process.env.PRIME_AGENT_KERNEL_PYTHON, - join(homedir(), ".prime", "agent", "kernel-venv", "bin", "python"), + venvPython(join(homedir(), ".prime", "agent", "kernel-venv")), ].filter((p): p is string => Boolean(p)); for (const python of candidates) { if (!existsSync(python)) continue;