# How the Eval Tool Runs Persistent Python and JavaScript Kernels in Oh-My-Pi

> Discover how the Eval tool runs persistent Python and JavaScript kernels in Oh-My-Pi using Node.js worker threads and subprocesses for stateful notebooks.

- Repository: [Can Bölük/oh-my-pi](https://github.com/can1357/oh-my-pi)
- Tags: internals
- Published: 2026-05-21

---

**The eval tool maintains persistent execution contexts by running JavaScript in a Node.js worker thread and Python in a long-lived subprocess, enabling stateful multi-cell notebooks with shared variables and imports across calls.**

The **eval tool** in the `can1357/oh-my-pi` coding agent provides a unified interface for executing code cells in both Python and JavaScript. Unlike ephemeral execution environments that reset after each call, this implementation maintains **persistent kernels** that preserve state, imports, and variables across multiple evaluations. By leveraging a worker thread for JavaScript and a subprocess for Python, the tool delivers a Jupyter-like notebook experience within the agent's architecture.

## Architecture Overview

The eval tool implements two distinct backend strategies behind a common interface defined in [`src/eval/backend.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/backend.ts):

| Language | Kernel Implementation | Process Model | Communication Protocol |
|----------|----------------------|---------------|------------------------|
| **JavaScript** | `JsRuntime` | In-process **worker thread** ([`worker-entry.ts`](https://github.com/can1357/oh-my-pi/blob/main/worker-entry.ts)) | [`worker-protocol.ts`](https://github.com/can1357/oh-my-pi/blob/main/worker-protocol.ts) (postMessage) |
| **Python** | `PythonKernel` | Separate **OS subprocess** ([`runner.py`](https://github.com/can1357/oh-my-pi/blob/main/runner.py) + [`kernel.ts`](https://github.com/can1357/oh-my-pi/blob/main/kernel.ts)) | NDJSON over **stdin/stdout** |

The `EvalTool` class defined in [`src/tools/eval.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/tools/eval.ts) consumes these backends through a unified `ExecutorBackend` interface, selecting the appropriate kernel based on the `language` property of the execution cell.

## JavaScript Kernel: In-Process Worker Thread

The JavaScript backend executes code within a sandboxed `vm` context running inside a dedicated Node.js worker thread, ensuring isolation while maintaining access to the host's tool ecosystem.

### Worker Entry and Core Components

The kernel lifecycle begins in [`src/eval/js/worker-entry.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/js/worker-entry.ts), which instantiates a `WorkerCore` class from [`src/eval/js/worker-core.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/js/worker-core.ts). The `WorkerCore` orchestrates a single-threaded `JsRuntime` and manages the run queue. The actual execution environment lives in [`src/eval/js/shared/runtime.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/js/shared/runtime.ts), which implements the sandboxed `vm`-based runtime respecting the session's current working directory, settings, and abort signals.

Communication between the host and worker uses strict message types defined in [`src/eval/js/worker-protocol.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/js/worker-protocol.ts):

```ts
export type WorkerInbound = { type: "init" | "run" | "tool-reply" | "close"; … };
export type WorkerOutbound = { type: "ready" | "text" | "display" | "result" | "closed"; … };

```

### Execution Flow and State Persistence

When `EvalTool.run()` invokes `jsBackend.execute()` (from [`src/eval/js/index.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/js/index.ts)), the flow proceeds through `executeJs()` in [`src/eval/js/executor.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/js/executor.ts), which creates an `OutputSink` for streaming results and calls `executeInVmContext()`.

Because the worker thread persists for the entire session, **variables and imports remain available** across successive calls. The session can be explicitly reset by passing `reset: true` in the `ExecOpts`, which causes `WorkerCore` to tear down and recreate the JavaScript runtime.

## Python Kernel: Persistent Subprocess

The Python backend maintains a long-running subprocess that communicates via newline-delimited JSON (NDJSON), enabling robust handling of long-running computations and clean cancellation.

### Subprocess Lifecycle and NDJSON Protocol

The `PythonKernel` class in [`src/eval/py/kernel.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/py/kernel.ts) manages the subprocess lifecycle. When `pythonBackend.execute()` is called (from [`src/eval/py/index.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/py/index.ts)), it invokes `executePython()` in [`src/eval/py/executor.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/py/executor.ts), which retrieves or creates a kernel instance from a global `sessions` map.

If no kernel exists for the session, `PythonKernel.start()` spawns a new process:

1. The [`runner.py`](https://github.com/can1357/oh-my-pi/blob/main/runner.py) script (imported via `import … with { type: "text" }`) is written to `RUNNER_CACHE_DIR`
2. The detected Python interpreter launches the script with a filtered environment (`filterEnv` from [`src/eval/py/runtime.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/py/runtime.ts))
3. Parent and child communicate via NDJSON frames over stdin/stdout with types: `"stdout"`, `"stderr"`, `"display"`, `"result"`, and `"error"`

Each `executePython` call sends a `{"type":"run", "code":…}` frame and awaits the corresponding result, allowing the subprocess to maintain Jupyter-like state between executions.

### Robust Cancellation Handling

The Python kernel implements graceful degradation for cancellation. When an `AbortSignal` triggers:

1. The kernel sends `SIGINT` to the subprocess, triggering `KeyboardInterrupt` inside Python
2. If the process does not exit within `INTERRUPT_ESCALATION_MS`, the kernel escalates to `SIGTERM` and finally `SIGKILL`

This ensures that runaway Python code cannot deadlock the agent while preserving the ability to catch interrupts within the Python runtime.

## Unified Backend Interface

Both kernels expose a common interface through [`src/eval/index.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/index.ts):

```ts
export * from "./backend";
export { default as jsBackend } from "./js";
export { default as pythonBackend } from "./py";

```

The `EvalTool` consumes these backends polymorphically:

```ts
// src/tools/eval.ts (simplified)
export class EvalTool implements Tool {
  async run(cell: EvalCell, opts: ExecOpts) {
    const backend = cell.language === "python"
      ? evalIndex.pythonBackend
      : evalIndex.jsBackend;
    return backend.execute(cell.code, execOptions);
  }
}

```

This abstraction allows client code to switch languages without changing the execution pattern, while both backends respect identical options for `deadlineMs`, `signal`, `artifactPath`, and streaming callbacks.

## Cross-Language Tool Bridge

Python cells can invoke host-side tools (such as `search`, `read`, or `bash`) through a bridge mechanism implemented in [`src/eval/py/tool-bridge.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/py/tool-bridge.ts). The system injects a small Python helper via [`src/eval/py/prelude.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/py/prelude.ts) that creates a proxy object named `tool`.

When Python code calls `tool.<name>(args)`, the bridge marshals the request over the NDJSON channel back to the host. The `PythonKernel` dispatcher forwards the call to the corresponding `Tool` implementation and returns the result to the Python subprocess. This mechanism ensures **feature parity** between JavaScript and Python cells regarding tool access.

## Practical Code Examples

### Executing Python with Tool Access

```ts
import { createToolSession } from "@oh-my-pi/pi-coding-agent";
import { EvalTool } from "@oh-my-pi/pi-coding-agent/tools/eval";

async function runPython() {
  const session = await createToolSession({ cwd: "/tmp/omp" });
  const evalTool = new EvalTool(session);

  const result = await evalTool.run({
    language: "python",
    code: `
      # The prelude automatically makes 'tool' available

      search_result = await tool.search({ query: "oh-my-pi" })
      print(f"Found {len(search_result.hits)} results")
    `,
  });

  console.log("Output:", result.output);
}
runPython();

```

### Executing JavaScript with State Persistence

```ts
import { EvalTool } from "@oh-my-pi/pi-coding-agent/tools/eval";

async function runJs() {
  const evalTool = new EvalTool(await createToolSession());

  // First cell: define a variable
  await evalTool.run({
    language: "javascript",
    code: `const sharedData = { count: 42 };`,
  });

  // Second cell: access the persisted variable
  const { output } = await evalTool.run({
    language: "javascript",
    code: `console.log("Count is:", sharedData.count);`,
  });

  console.log(output); // "Count is: 42"
}
runJs();

```

## Summary

- **Dual Runtime Strategy**: JavaScript executes in an in-process **worker thread** ([`worker-entry.ts`](https://github.com/can1357/oh-my-pi/blob/main/worker-entry.ts)), while Python runs in a separate **OS subprocess** ([`kernel.ts`](https://github.com/can1357/oh-my-pi/blob/main/kernel.ts)) communicating via NDJSON.
- **Stateful Execution**: Both kernels persist across `eval` calls, maintaining variables, imports, and file handles unless explicitly reset via `opts.reset`.
- **Unified Interface**: The `EvalTool` class in [`src/tools/eval.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/tools/eval.ts) abstracts backend differences, routing to `jsBackend` or `pythonBackend` based on the `language` flag.
- **Robust Cancellation**: Python implements a signal escalation strategy (`SIGINT` → `SIGTERM` → `SIGKILL`), while JavaScript relies on worker termination via `AbortSignal`.
- **Tool Ecosystem Access**: Python cells access host tools through a lightweight bridge ([`tool-bridge.ts`](https://github.com/can1357/oh-my-pi/blob/main/tool-bridge.ts) and [`prelude.ts`](https://github.com/can1357/oh-my-pi/blob/main/prelude.ts)) that marshals calls over the NDJSON protocol.

## Frequently Asked Questions

### How does the eval tool maintain state between code cells?

The eval tool maintains state by keeping kernels alive across executions. For JavaScript, the `WorkerCore` in [`src/eval/js/worker-core.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/js/worker-core.ts) preserves the `vm` context between calls. For Python, the `PythonKernel` in [`src/eval/py/kernel.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/py/kernel.ts) keeps the subprocess running, maintaining the interpreter's global namespace. Variables and imports defined in one cell remain accessible in subsequent cells until the session ends or `reset: true` is passed.

### What happens when I cancel a running Python cell?

When cancellation is requested via an `AbortSignal`, the `PythonKernel` first sends `SIGINT` to the subprocess, allowing Python code to catch `KeyboardInterrupt` and clean up. If the process does not terminate within `INTERRUPT_ESCALATION_MS`, the kernel escalates to `SIGTERM` and finally `SIGKILL` to ensure the agent remains responsive regardless of the subprocess state.

### Can Python code access the same tools as JavaScript?

Yes. Through the tool bridge implemented in [`src/eval/py/tool-bridge.ts`](https://github.com/can1357/oh-my-pi/blob/main/src/eval/py/tool-bridge.ts), Python cells can access the complete host tool ecosystem. The bridge injects a `tool` proxy object via [`prelude.ts`](https://github.com/can1357/oh-my-pi/blob/main/prelude.ts) that marshals function calls over the NDJSON channel to the host, executes the corresponding `Tool` implementation, and returns the result to the Python runtime.

### How do I force a kernel reset to clear all variables?

Pass `reset: true` in the `ExecOpts` when calling `EvalTool.run()`. For JavaScript, this triggers the recreation of the `WorkerCore` and `JsRuntime`. For Python, this causes the `PythonKernel` to terminate the existing subprocess and spawn a fresh one on the next execution, clearing all accumulated state.