<!-- Markdown mirror. Canonical: https://empryo.com/docs/tools/python-kernel -->

# Python kernel

> RLM mode gives the agent a Python interpreter that stays running between calls, so variables and imports survive. Off by default.

`python_kernel` is one Python interpreter, alive for as long as your workspace
is. A variable bound in one call is still bound in the next.

That is the whole difference from `shell`. Every `python -c` is a fresh process,
so a script that loads a dataframe pays for the load again on every question
asked about it, and the agent has to re-derive its own context each time.
With a kernel the agent loads once and interrogates across a whole turn:

```
python_kernel(code: "import pandas as pd; df = pd.read_csv('sales.csv'); df.shape")
→ (48211, 17)

python_kernel(code: "df.groupby('region').revenue.sum().sort_values().tail(3)")
→ region
  EMEA     1840221.55
  APAC     2110984.10
  NA       3922015.77
```

The second call never re-read the file.

## Turning it on

Off by default. It executes arbitrary code with your privileges, which is the
same power `shell` has, so it waits to be asked:

```json
"agentFeatures": { "pythonKernel": true }
```

- **TUI**, `/agent-features` → Python Kernel
- **Desktop**, Settings → Router → Behaviors → Python kernel
- **One run**, `EMPRYO_PYTHON_KERNEL=1 empryo --headless "…"`

Requires `python3` on PATH. Without one the tool is not offered at
all, so a model never offers to use something that cannot run.

## Parameters

| Field | Type | What it does |
|---|---|---|
| `code` | string | Python to run. The last expression's value comes back, like a REPL |
| `reset` | boolean | Clear every variable first (or on its own, to clear and stop) |
| `timeoutMs` | number | Give up after this many ms, default 120000, max 600000 |

Returns stdout, stderr and the value of the last expression. An exception comes
back as a failure with the traceback, and the interpreter stays alive.

## Behaviour worth knowing

- **IPython when it is importable**, rich reprs, `display`, `_`. Plain CPython
  otherwise, nothing to install either way.
- **One kernel per workspace.** Two tabs in the same repo share their globals,
  which is the point: the state belongs to the work, not to the chat lane.
- **cwd is the workspace root**, so relative paths mean what they look like.
- **A runaway loop is killed at the timeout** and the kernel restarts, rather
  than the turn hanging. The reply says so. The variables are gone with it.
- **Not a Jupyter client.** The protocol is JSON over the child's own stdio, so
  there is no `jupyter_client`, no ZMQ, and nothing to install before the first
  call works.

## When not to use it

Reach for `shell` for one-shot commands (`pytest`, `ruff`, a script you already
have). A kernel buys nothing there and costs you a process that outlives the
call. Reach for `project` for lint/typecheck/test, which already know your
toolchain. The kernel earns its place when step N+1 depends on what step N left
in memory.
