Skip to content

Explore data in a persistent kernel

This guide shows you how to have an agent load something expensive once and then ask several questions of it, instead of paying the load on every question.

The eval tool runs a persistent interpreter. Variables, imports and loaded data survive between calls.

A one-shot python -c pays the setup on every question. Loading a dataframe costs seconds and a hundred megabytes; charting it costs milliseconds. When every question costs the load, an agent asks fewer questions than it should — and each answer comes back as text it then has to re-parse.

Load data/runs.csv and tell me the p95 duration by agent.

import pandas as pd
runs = pd.read_csv("data/runs.csv")
len(runs)

The last expression is echoed the way a notebook does, so len(runs) reports the row count without a print.

Then, in a separate call:

runs.groupby("agent")["duration_ms"].quantile(0.95).sort_values()

runs is still loaded. A pandas frame comes back rendered as a table rather than as a repr.

import matplotlib.pyplot as plt
runs.groupby("agent")["duration_ms"].mean().plot.bar()

Figures are captured after the cell and returned as images, which the transcript renders. You see the chart; so does the agent.

Figures are closed after capture. An uncollected figure would be redrawn and re-sent by the next cell too, so a five-cell session would emit the first chart five times.

These are rewritten into plain code rather than erroring:

You type What happens
%pip install x Runs pip, then clears the module cache so the next import sees it
%cd path os.chdir
!command A captured subprocess
%%bash The rest of the cell as a shell script
%%timeit 1000 timed runs

The %pip cache eviction is the one that matters: installing into a live interpreter does nothing for a module that was already imported, so without it the version you just installed is not the one running.

paths = tool("search", {"pattern": "TODO", "path": "lib"})
for path in paths.splitlines()[:100]:
contents = tool("read", {"path": path})
...

tool(name, args) re-enters the agent’s own tool registry. That means a cell can fan out over a hundred files, or delegate through task, without the model spending a turn per item.

A bridged call suspends the cell’s timeout while it is outstanding. The budget measures inactivity, not wall clock — a cell that fans out to subagents and waits four minutes is not hung, and killing it mid-fanout would lose the whole kernel’s state.

eval(code: "…", reset: true)

Discards the interpreter and starts a clean one. Everything loaded is gone, which is the point.

In the conversation’s enclosure when it has one, on the host otherwise.

A persistent interpreter driven by a model reading an untrusted repo is exactly what enclosure-only execution exists for: a one-shot bash call at least ends, while a kernel is a shell that remembers.

It does not boot an enclosure to get there — the same rule that stops a rig tab auto-starting. If you want the kernel enclosed, open the conversation’s shell first. See Give an agent a machine to test on.

  • One cell at a time per kernel. Two cells racing in one interpreter is two halves of two programs interleaved in one namespace.
  • Output is capped. A cell in a print loop is truncated rather than allowed to fill the context window.
  • Four images per cell. A plotting loop can emit a figure per iteration, and every image is a large fixed cost in the request that carries it.
  • The kernel dies with the run. One still holding a dataframe after the conversation ended is a leak.