feat(coding-agent/eval): added local python-runner subprocess execution

- Replaced Python execution with a local `python -u runner.py` subprocess and NDJSON stdin/stdout framing.
- Removed shared-gateway architecture, including coordinator lifecycle APIs, `useSharedGateway` wiring, and `jupyter` CLI/actions.
- Simplified setup checks to a plain Python 3 availability probe and removed automatic dependency-install fallbacks.
- Updated kernel cancellation and display processing to use status frames, SIGINT/SIGTERM escalation, and normalized output coercion.
- Added `python-runner` integration and display tests while deleting legacy websocket and kernel lifecycle test suites.
This commit is contained in:
can1357
2026-05-12 09:09:24 +02:00
parent dcf3f1c8a3
commit 8d144e17ec
26 changed files with 1735 additions and 3004 deletions
@@ -1,106 +0,0 @@
/**
* Jupyter CLI command handlers.
*
* Handles `omp jupyter` subcommand for managing the shared Python gateway.
*/
import { APP_NAME } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { getGatewayStatus, shutdownSharedGateway } from "../eval/py/gateway-coordinator";
export type JupyterAction = "kill" | "status";
export interface JupyterCommandArgs {
action: JupyterAction;
}
export function parseJupyterArgs(args: string[]): JupyterCommandArgs | undefined {
if (args.length === 0 || args[0] !== "jupyter") {
return undefined;
}
const action = args[1] as JupyterAction | undefined;
if (!action || !["kill", "status"].includes(action)) {
return { action: "status" };
}
return { action };
}
export async function runJupyterCommand(cmd: JupyterCommandArgs): Promise<void> {
switch (cmd.action) {
case "kill":
await runKill();
break;
case "status":
await runStatus();
break;
}
}
async function runKill(): Promise<void> {
const status = await getGatewayStatus();
if (!status.active) {
console.log(chalk.dim("No Jupyter gateway is running"));
return;
}
console.log(`Killing Jupyter gateway (PID ${status.pid})...`);
await shutdownSharedGateway();
console.log(chalk.green("Jupyter gateway stopped"));
}
async function runStatus(): Promise<void> {
const status = await getGatewayStatus();
if (!status.active) {
console.log(chalk.dim("No Jupyter gateway is running"));
return;
}
console.log(chalk.bold("Jupyter Gateway Status\n"));
console.log(` ${chalk.green("●")} Running`);
console.log(` PID: ${status.pid}`);
console.log(` URL: ${status.url}`);
if (status.uptime !== null) {
console.log(` Uptime: ${formatUptime(status.uptime)}`);
}
if (status.pythonPath) {
console.log(` Python: ${status.pythonPath}`);
}
if (status.venvPath) {
console.log(` Venv: ${status.venvPath}`);
}
}
function formatUptime(ms: number): string {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
}
if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
return `${seconds}s`;
}
export function printJupyterHelp(): void {
console.log(`${chalk.bold(`${APP_NAME} jupyter`)} - Manage the shared Jupyter gateway
${chalk.bold("Usage:")}
${APP_NAME} jupyter <command>
${chalk.bold("Commands:")}
status Show gateway status (default)
kill Stop the running gateway
${chalk.bold("Examples:")}
${APP_NAME} jupyter # Show status
${APP_NAME} jupyter status # Show status
${APP_NAME} jupyter kill # Stop the gateway
`);
}
+14 -161
View File
@@ -21,7 +21,6 @@ export interface SetupCommandArgs {
const VALID_COMPONENTS: SetupComponent[] = ["python", "stt"];
const PYTHON_PACKAGES = ["jupyter_kernel_gateway", "ipykernel"];
const MANAGED_PYTHON_ENV = getPythonEnvDir();
/**
@@ -65,10 +64,6 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
interface PythonCheckResult {
available: boolean;
pythonPath?: string;
uvPath?: string;
pipPath?: string;
missingPackages: string[];
installedPackages: string[];
usingManagedEnv?: boolean;
managedEnvPath?: string;
}
@@ -85,8 +80,6 @@ function managedPythonPath(): string {
async function checkPythonSetup(): Promise<PythonCheckResult> {
const result: PythonCheckResult = {
available: false,
missingPackages: [],
installedPackages: [],
managedEnvPath: MANAGED_PYTHON_ENV,
};
@@ -94,109 +87,24 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
const managedPath = managedPythonPath();
const hasManagedEnv = await Bun.file(managedPath).exists();
result.uvPath = $which("uv") ?? undefined;
result.pipPath = $which("pip3") ?? $which("pip") ?? undefined;
const candidates = [systemPythonPath, hasManagedEnv ? managedPath : undefined].filter(
(candidate): candidate is string => !!candidate,
);
if (candidates.length === 0) {
const pythonPath = systemPythonPath ?? (hasManagedEnv ? managedPath : undefined);
if (!pythonPath) {
return result;
}
result.pythonPath = systemPythonPath ?? managedPath;
let bestMatch = {
pythonPath: candidates[0],
missingPackages: [...PYTHON_PACKAGES],
installedPackages: [] as string[],
usingManagedEnv: candidates[0] === managedPath,
};
for (const pythonPath of candidates) {
const installedPackages: string[] = [];
const missingPackages: string[] = [];
for (const pkg of PYTHON_PACKAGES) {
const moduleName = pkg === "jupyter_kernel_gateway" ? "kernel_gateway" : pkg;
const script = `import importlib.util; raise SystemExit(0 if importlib.util.find_spec('${moduleName}') else 1)`;
const check = await $`${pythonPath} -c ${script}`.quiet().nothrow();
if (check.exitCode === 0) {
installedPackages.push(pkg);
} else {
missingPackages.push(pkg);
}
}
if (missingPackages.length < bestMatch.missingPackages.length) {
bestMatch = {
pythonPath,
missingPackages,
installedPackages,
usingManagedEnv: pythonPath === managedPath,
};
}
if (missingPackages.length === 0) {
result.available = true;
result.pythonPath = pythonPath;
result.missingPackages = missingPackages;
result.installedPackages = installedPackages;
result.usingManagedEnv = pythonPath === managedPath;
return result;
}
}
result.pythonPath = bestMatch.pythonPath;
result.missingPackages = bestMatch.missingPackages;
result.installedPackages = bestMatch.installedPackages;
result.usingManagedEnv = bestMatch.usingManagedEnv;
const probe = await $`${pythonPath} -c "import sys;sys.exit(0)"`.quiet().nothrow();
result.pythonPath = pythonPath;
result.available = probe.exitCode === 0;
result.usingManagedEnv = pythonPath === managedPath;
return result;
}
/**
* Install Python packages using uv (preferred) or pip.
*/
async function installPythonPackages(
packages: string[],
pythonPath: string,
uvPath?: string,
pipPath?: string,
): Promise<{ success: boolean; usedManagedEnv: boolean }> {
if (uvPath) {
console.log(chalk.dim(`Installing via uv: ${packages.join(" ")}`));
const result = await $`${uvPath} pip install ${packages}`.nothrow();
if (result.exitCode === 0) {
return { success: true, usedManagedEnv: false };
}
}
if (pipPath) {
console.log(chalk.dim(`Installing via pip: ${packages.join(" ")}`));
const result = await $`${pipPath} install ${packages}`.nothrow();
if (result.exitCode === 0) {
return { success: true, usedManagedEnv: false };
}
}
console.log(chalk.dim(`Falling back to managed virtual environment: ${MANAGED_PYTHON_ENV}`));
if (uvPath) {
const createEnv = await $`${uvPath} venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
if (createEnv.exitCode !== 0) {
return { success: false, usedManagedEnv: true };
}
const installInManagedEnv = await $`${uvPath} pip install --python ${MANAGED_PYTHON_ENV} ${packages}`.nothrow();
return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
}
const createEnv = await $`${pythonPath} -m venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
if (createEnv.exitCode !== 0) {
return { success: false, usedManagedEnv: true };
}
const managedPython = managedPythonPath();
const installInManagedEnv = await $`${managedPython} -m pip install ${packages}`.nothrow();
return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
}
// Python installation helper removed: the subprocess runner has no Python
// package dependencies beyond a working interpreter. `omp setup python --check`
// remains as a probe; users install optional libs (pandas, matplotlib, ...)
// directly via pip or the in-process `%pip` magic.
/**
* Run the setup command.
@@ -232,67 +140,13 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
console.log(chalk.dim(`Using managed environment: ${check.managedEnvPath}`));
}
if (check.uvPath) {
console.log(chalk.dim(`uv: ${check.uvPath}`));
} else if (check.pipPath) {
console.log(chalk.dim(`pip: ${check.pipPath}`));
}
if (check.installedPackages.length > 0) {
console.log(chalk.green(`${theme.status.success} Installed: ${check.installedPackages.join(", ")}`));
}
if (check.missingPackages.length === 0) {
if (check.available) {
console.log(chalk.green(`\n${theme.status.success} Python execution is ready`));
return;
}
console.log(chalk.yellow(`${theme.status.warning} Missing: ${check.missingPackages.join(", ")}`));
if (flags.check) {
process.exit(1);
}
if (!check.uvPath && !check.pipPath) {
console.error(chalk.red(`\n${theme.status.error} No package manager found`));
console.error(chalk.dim("Install uv (recommended) or pip:"));
console.error(chalk.dim(" curl -LsSf https://astral.sh/uv/install.sh | sh"));
process.exit(1);
}
console.log("");
const install = await installPythonPackages(check.missingPackages, check.pythonPath, check.uvPath, check.pipPath);
if (!install.success) {
console.error(chalk.red(`\n${theme.status.error} Installation failed`));
console.error(chalk.dim("Try installing manually:"));
if (install.usedManagedEnv) {
if (check.uvPath) {
console.error(chalk.dim(` uv venv ${MANAGED_PYTHON_ENV}`));
console.error(
chalk.dim(` uv pip install --python ${MANAGED_PYTHON_ENV} ${check.missingPackages.join(" ")}`),
);
} else {
console.error(chalk.dim(` ${check.pythonPath} -m venv ${MANAGED_PYTHON_ENV}`));
console.error(chalk.dim(` ${managedPythonPath()} -m pip install ${check.missingPackages.join(" ")}`));
}
} else {
console.error(chalk.dim(` ${check.uvPath ? "uv pip" : "pip"} install ${check.missingPackages.join(" ")}`));
}
process.exit(1);
}
const recheck = await checkPythonSetup();
if (recheck.available) {
console.log(chalk.green(`\n${theme.status.success} Python execution is ready`));
if (recheck.usingManagedEnv) {
console.log(chalk.dim(`Managed Python environment: ${recheck.managedEnvPath}`));
}
} else {
console.error(chalk.red(`\n${theme.status.error} Setup incomplete`));
console.error(chalk.dim(`Still missing: ${recheck.missingPackages.join(", ")}`));
process.exit(1);
}
console.error(chalk.red(`\n${theme.status.error} Python interpreter reported failure`));
process.exit(1);
}
async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promise<void> {
@@ -359,9 +213,8 @@ ${chalk.bold("Usage:")}
${APP_NAME} setup <component> [options]
${chalk.bold("Components:")}
python Install Jupyter kernel dependencies for Python code execution
python Verify a Python 3 interpreter is reachable for code execution
stt Install speech-to-text dependencies (openai-whisper, recording tools)
Packages: ${PYTHON_PACKAGES.join(", ")}
${chalk.bold("Options:")}
-c, --check Check if dependencies are installed without installing