MusePi

Eval Tool Python Backend

This document describes the Python execution stack in packages/coding-agent. It covers tool behavior, runner lifecycle, environment handling, execution semantics, output rendering, supported magics, and operational failure modes.

Scope and Key Files

What eval’s Python backend is

The eval tool executes one or more Python cells inside a retained python subprocess that speaks NDJSON over stdin/stdout. No Jupyter gateway and no extra pip dependencies are required — a vanilla Python 3.8+ interpreter is enough. Rich display() output (PIL, pandas, plotly, matplotlib figures) keeps working because the wrapper implements MIME-bundle dispatch.

Tool params:

{
  cells: Array<{
    language: "py" | "js";
    code: string;
    title?: string;
    timeout?: number; // seconds, clamped to 1..3600, default 30. Inactivity budget — see "Cell timeout".
    reset?: boolean; // reset this cell's selected runtime before execution
  }>;
}

The tool is concurrency = "exclusive" for a session, so calls do not overlap.

Kernel lifecycle

Each Python kernel is a single subprocess: <resolved-python> -u <runner.py>. The runner is bundled with the host binary (Bun text import), written to an omp-python-runner cache under the OS temp directory once per script hash, and reused by subsequent spawns.

Kernel startup sequence:

  1. Availability check (checkPythonKernelAvailability) — verifies that a Python interpreter resolves and runs.
  2. Spawn python -u runner.py with filtered env and cwd.
  3. Send an init request that runs os.chdir(cwd), injects env entries, and adds cwd to sys.path.
  4. Execute PYTHON_PRELUDE (idempotent — only initializes once per process).

Kernel shutdown:

Wire protocol (NDJSON, host ↔ runner)

One JSON object per line, UTF-8, \n terminated.

Host → runner:

{"id": "<reqId>", "code": "<source>", "silent": false, "storeHistory": true, "cwd": "<optional>", "env": {"KEY": "VAL"}}
{"type": "exit"}

Runner → host:

{"type": "started",  "id": "<reqId>"}
{"type": "stdout",   "id": "<reqId>", "data": "..."}
{"type": "stderr",   "id": "<reqId>", "data": "..."}
{"type": "display",  "id": "<reqId>", "bundle": {<mime>: <value>}}
{"type": "result",   "id": "<reqId>", "bundle": {<mime>: <value>}}
{"type": "error",    "id": "<reqId>", "ename": "...", "evalue": "...", "traceback": ["..."]}
{"type": "done",     "id": "<reqId>", "status": "ok"|"error", "executionCount": N, "cancelled": false}

Status events the prelude emits (e.g. _emit_status("find", count=…)) ship inside display bundles under application/x-omp-status so the existing TUI status renderer keeps working.

Magics

The runner’s source transformer rewrites IPython-style magics to plain Python calls before parsing. Supported set:

Magic Effect
%pip <args> python -m pip <args> with live streaming output. Newly installed packages are evicted from sys.modules so the next import picks up the fresh install.
%cd <path> os.chdir(path) (with ~ expansion); emits status event.
%pwd Returns os.getcwd().
%ls [path] Returns sorted(os.listdir(path)).
%env [KEY[=VAL]] List, read, or set env vars (matches prelude env() semantics).
%set_env KEY VALUE Set os.environ[KEY].
%time <expr> / %timeit <expr> Time the expression; emits status event with elapsed ms.
%who / %whos List user-namespace names.
%reset Clear user globals and re-inject prelude.
%load <path> Read a file into a fresh cell and execute.
%run <path> runpy.run_path and merge globals back.
%%bash / %%sh Run the cell body via bash/sh.
%%capture [name] Run body with stdout/stderr captured into name.
%%timeit Time the cell body.
%%writefile <path> Write body to file.
!cmd / var = !cmd Run command via subprocess shell; returns an SList-style result with .n / .s helpers.
var = %name args Assignment forms work for line magics and !cmd.

Unknown magic names raise NameError: UsageError: ... inside the cell.

Session persistence semantics

python.kernelMode controls retained kernel reuse:

Multi-cell behavior in a single tool call

Python cells run sequentially in the same selected Python kernel instance for that tool call.

If an intermediate cell fails:

reset=true is per cell and resets that language runtime before the cell executes.

Environment filtering and runtime resolution

Environment is filtered before launching the runner:

Runtime selection order (skipped entirely when the python.interpreter setting names an explicit executable):

  1. Active/located venv (VIRTUAL_ENV, then CONDA_PREFIX, then <cwd>/.venv, <cwd>/venv)
  2. Managed venv at ~/.musepi/python-env
  3. python or python3 on PATH

When a venv is selected, its bin/Scripts path is prepended to PATH.

The runner additionally receives PYTHONUNBUFFERED=1 and PYTHONIOENCODING=utf-8 so streamed output reaches the host promptly.

Tool availability and mode selection

eval.py / eval.js (both default true) plus optional boolean env flags PI_PY / PI_JS control eval backend exposure:

PI_PY and PI_JS use normal boolean flag parsing. Each flag, when set, overrides only its own setting; an unset flag falls back to its setting (eval.py / eval.js, both default true).

If Python preflight fails and eval.js is enabled, eval remains available for js cells; py cells fail with a Python-backend availability error.

Python prelude helpers include agent(prompt, *, agent="task", label=None, schema=None, schema_mode=None, isolated=None, apply=None, merge=None, handle=False). It synchronously calls the host bridge and returns final text, or parsed data when schema is supplied. schema_mode selects permissive or strict structured-output handling; the isolation/apply/merge flags control task worktree behavior. With handle=True, it returns a DAG node dict ({"text", "output", "handle", "id", "agent"}) whose handle is the recoverable agent://<id> URI; parsed output is also stored under "data" when available.

Execution flow and cancellation/timeout

Cell timeout

Each eval cell timeout is in seconds, defaults to 30, and is clamped to 1..3600. It is a wall-clock budget on the cell’s own work that the watchdog (IdleTimeout, src/eval/idle-timeout.ts) enforces, but it is suspended while a host-side agent()/parallel()/completion() bridge call is in flight: those calls emit synthetic pause/resume timeout-control status events (withBridgeTimeoutPause, src/eval/bridge-timeout.ts) that pause the watchdog entirely and start a fresh timeout window when control returns to the runtime, so a long fanout or a slow completion runs to completion instead of being killed mid-stream. Pause is reference-counted because parallel() can have multiple bridge calls in flight at once.

The pause/resume events are the sole mechanism that suspends the budget. Everything else the cell does — compute, stdout/stderr, log()/phase(), and ordinary (non-agent) tool calls — counts against timeout, so a cell that is not delegating to an agent/completion is bounded by a plain wall-clock timeout. The tool combines the caller abort signal, the session abort signal, and the watchdog’s signal with AbortSignal.any(...); no wall-clock deadline is passed to the backend, so neither runtime arms a competing fixed timer.

Kernel execution cancellation

On abort/timeout:

If the runner does not emit done within 5s of the interrupt (INTERRUPT_ESCALATION_MS — e.g. stuck in C code holding the GIL), the host shuts the subprocess down (escalating exitSIGTERMSIGKILL), the cell is annotated as kernel-killed, and the kernel is recreated on the next call.

stdin behavior

Interactive stdin is not supported. The runner does not forward input() prompts; user code that calls input() blocks until cancellation.

Output capture and rendering

Captured output classes

From runner frames:

Display MIME precedence:

  1. text/markdown
  2. text/plain
  3. text/html (converted to basic markdown)

Additionally captured as structured outputs:

Matplotlib

The runner sets MPLBACKEND=Agg as an environ default so figures render off-screen. After every cell, pyplot.get_fignums() is iterated; each figure is saved to PNG, emitted as an image/png display, and closed.

Storage and truncation

Output is streamed through OutputSink and may be persisted to artifact storage. Tool results can include truncation metadata and artifact://<id> for full output recovery.

Renderer behavior

Operational troubleshooting

Relevant environment variables