Code Interpreter

Code execution is one dispatcher across languages and backends. Send a language and a code string: the daemon routes it to a runtime and answers when the run finishes.

The runtime is a native REPL, an embedded IPython kernel, or an external Jupyter-compatible server. The dispatcher picks one per language and falls back through what it finds, so a request never has to name a runtime.

A code session keeps an interpreter process alive between calls: what one call defines, the next one sees. Without a session, each run is a one-off in a process that is discarded afterwards.

For a shell command rather than a code cell, use Commands. The v1 and v2 routes side by side are in Migration from 1.x.

What it needs

GET /v1/capabilities describes the plane under code_interpreter:

  • statusready once either interpreter answers, absent when neither does
  • backend — the layer that runs Python: native, kernel, or endpoint
  • kindscode, nodejs, and jupyter where a kernel serves Python
  • default_python, default_node — the binaries the native tier spawns

The native tier needs python3 or node on PATH; the kernel tier also needs an importable ipykernel. The daemon selects an execution tier based on the runtimes available.

Run a snippet

curl -X POST "$BASE_URL/v2/code/execute" \
  -H "Content-Type: application/json" \
  -d '{"language": "python", "code": "print(sum([1, 2, 3]))"}'

Request body:

{
  "language": "python",
  "code": "print(sum([1, 2, 3]))"
}
curl -X POST "$BASE_URL/v1/code/execute" \
  -H "Content-Type: application/json" \
  -d '{"language": "python", "code": "print(sum([1, 2, 3]))"}'

Request body:

{
  "language": "python",
  "code": "print(sum([1, 2, 3]))"
}

The body takes these fields:

FieldValuesMeaning
languagepython, javascript, requiredWhich interpreter runs the code
codestring, requiredThe code to run
session_idstringSession to run in; absent means a one-off
statefulbooleantrue alone opens a session and returns its id
timeout1–900 s (default 30)How long the run may take
cwdstringWorking directory; fixed when the process starts
userstringAccount to run as; only root can switch

language also accepts python3, node, nodejs, and js, and a field the route does not know is ignored. A user this daemon cannot become is not a rejection: the run answers HTTP 200 with status: "error" and a ProcessError output saying only root can change identity. A kernel ignores user and runs as the daemon's own account.

data carries:

  • language, code — the resolved language and the code that ran
  • statusok, error, or timeout
  • outputs — the notebook output list, one entry per thing the run produced
  • stdout, stderr — each stream on its own; empty is "" on v2, null on v1
  • exit_code0 when the code completed, 1 after an error or a timeout
  • execution_count — the session's cell counter, from 1
  • session_id — the session that ran it; null for a one-off

status is the outcome of the run, and success in the envelope follows it. An exception or a timeout inside the code is still an HTTP 200, with success: false.

An empty code string is the one field the surfaces read differently: v2 rejects it with a 422, v1 runs an empty cell and answers 200 with no outputs.

outputs is the notebook output list:

output_typeCarries
streamname (stdout or stderr) and text
execute_resultdata with text/plain: the last expression's value
display_datadata with image/png and other mime bundles
errorename, evalue, traceback

JavaScript is the same request with another language: console.log([1, 2, 3].reduce((a, b) => a + b, 0)); prints 6 the same way.

Two language-specific routes run the same code with their own session semantics:

Runtime info

curl "$BASE_URL/v2/code/info"
curl "$BASE_URL/v1/code/info"

Both surfaces answer with the same body — which backend, languages, and limits are in effect:

  • backend — the layer that runs Python here: native, kernel, or endpoint
  • kinds — the runtimes present: python, nodejs, plus jupyter with a kernel
  • languages — what language accepts
  • python, node{available, version} each
  • max_sessions — how many sessions can be open at once
  • default_timeout, max_timeout30 and 900
  • prewarmed — how many warm processes are waiting; 0 by default

Keep variables across calls

A session_id the daemon has not seen is created on the spot, and stateful: true without one returns a generated id. Session state lives in memory: it survives calls within the session, not an aiod restart.

RoutePurposeNotes
POST /v2/code/sessionsOpen one up frontlanguage, plus session_id, cwd, user
GET /v2/code/sessionsList themKeyed by session id
GET /v2/code/sessions/{id}Read oneAn unknown id is a 404
DELETE /v2/code/sessions/{id}End oneAn unknown id is a 200 with deleted: false
DELETE /v2/code/sessionsEnd all of themAnswers cleaned_sessions

A session entry carries:

  • session_id, language, cwd — what it is and where it runs
  • created_at, last_used — milliseconds since the epoch
  • age_seconds — seconds since the last run
  • max_idle_time — milliseconds of idle time before it is closed
  • stateidle or executing

A kernel-backed Python session is listed here too, carrying its kernel_name instead of a max_idle_time.

The following example makes two calls in the same session. Aio is the envelope-aware helper from Examples:

Python
TypeScript
sb = Aio(BASE_URL)

# 1. Load the data once; the session keeps it.
sb.post(
    "/v2/code/execute",
    language="python",
    session_id="analysis",
    code="import statistics\nrows = [120, 135, 150, 142, 168, 180]",
)

# 2. Ask a question about it; `rows` is still there.
run = sb.post(
    "/v2/code/execute",
    language="python",
    session_id="analysis",
    code="print(round(statistics.mean(rows), 1))",
)
print(run["stdout"].strip(), run["execution_count"])   # 149.2 2

# 3. Drop the session when the task is done.
sb.delete("/v2/code/sessions/analysis")

Sessions are listed and ended per language, on the route that owns them:

RoutePurposeNotes
GET /v1/nodejs/sessionsList the JavaScript sessionsPOST opens one up front
DELETE /v1/nodejs/sessions/{id}End oneAnswers deleted
GET /v1/jupyter/sessionsList the kernel-backed Python onesWhere ipykernel is installed
DELETE /v1/jupyter/sessions/{id}End oneA native Python session only idles out

Two calls sharing a session, through the SDK:

Python
TypeScript
client = Sandbox(base_url=BASE_URL)

# 1. Load the data once; the session keeps it.
client.code.execute_code(
    language="python",
    session_id="analysis",
    code="import statistics\nrows = [120, 135, 150, 142, 168, 180]",
)

# 2. Ask a question about it; `rows` is still there.
run = client.code.execute_code(
    language="python",
    session_id="analysis",
    code="print(round(statistics.mean(rows), 1))",
).data
print(run.stdout.strip(), run.execution_count)   # 149.2 2

# 3. Drop the session when the task is done.
client.jupyter.delete_session("analysis")

Which SDK calls reach these routes unchanged is listed in 1.x SDK compatibility.

Where a kernel serves Python, a code session and a Jupyter session with the same id are one namespace: {"session_id": "s1"} on the code route and on /v1/jupyter/execute reach the same kernel, and either side can end it.

Patterns for an agent

Most runs are one-offs.

ScenarioCallThen
print(sum(...))one-offRead stdout from the same response
Iterating on a dataseta named sessionReuse the id; the variables are still there
while True: passwith a timeoutstatus: "timeout"; see the table below
An exceptionanyFeed outputs[].traceback back to the model
Ruby, Go, anything elsea commandSee More languages

Timeouts and limits

A run that exceeds its timeout returns status: "timeout". What happens next, and the resource cost, depend on the tier:

TierOn timeoutLimits
Native REPLexecution timed out after 2000ms; session state was reset20 sessions, closed after 1800 s idle
Embedded kernelA KeyboardInterrupt, then execution timed out after 2000ms and was interrupted5 sessions, closed after 300 s idle

The native tier kills the interpreter five seconds past the deadline, so a run that ends inside that grace still returns its output; the session stays open and its namespace starts empty again. The kernel interrupts the cell instead, and keeps both the kernel and everything defined before it.

The two limits come from AIO_CODE_MAX_SESSIONS / AIO_CODE_SESSION_TIMEOUT_SECS and AIO_KERNEL_MAX_SESSIONS / AIO_KERNEL_SESSION_TIMEOUT_SECS. One session past the limit is a 429: Maximum number of sessions (20) reached, or Maximum number of kernel sessions (5) reached. Existing sessions are never evicted.

How it picks a backend

For Python, the daemon tries three backends in order:

  1. The external Jupyter-compatible endpoint at AIO_JUPYTER_ENDPOINT, if reachable
  2. An embedded kernel, if ipykernel is installed
  3. The native Python REPL

JavaScript always runs on the native REPL and needs only node on PATH. Nothing is warmed by default: AIO_CODE_PREWARM, which pools native JavaScript harnesses, and AIO_KERNEL_PREWARM are both 0.

AIO_CODE_BACKEND (auto, native, or kernel) pins one Python backend and skips this order.

More languages

language accepts python (also python3) and javascript (also node, nodejs, js); anything else is a 422. To support other languages, choose one of these approaches, in order of effort:

  • Any interpreter, no state. Run it through Commands, as ruby -e "puts 1". Nothing to configure; you give up only session state and the notebook-shaped output.
  • A different Python or Node. PYTHON_VERSION / NODE_VERSION pick which python3 / node on PATH the native tier spawns. kernel_name on /v1/jupyter picks among the installed kernels. See Jupyter.
  • Add a language to the code plane. The native tier is one small, stdlib-only harness per language, compiled into the binary (harness.py, harness.js).

The daemon writes {"code", "timeout_ms"} lines to the harness's stdin and reads {"stdout", "stderr", "result", "error"} lines from its stdout, one process per session.

Adding a language means writing that harness for its interpreter and registering it in the daemon's Language list. The HTTP surface, sessions, timeouts, and limits are shared.

Error handling

An exception, a timeout, or a dead interpreter is an HTTP 200 with success: false and status error or timeout. Only a bad request carries an error status code:

ConditionResult
Unsupported language, missing code, timeout outside 1–900422
A cwd that is not a directory422
An empty code string422 on v2, 200 on v1
No interpreter for the language503, naming what it looked for
Session limit reached429
Unknown session id on GET .../sessions/{id}404

A v2 refusal puts the reason in message: Unsupported language 'ruby'. Supported: python, javascript. A v1 refusal carries that same sentence in the errors list, one entry per rejected field, with location: ["body", "language"] and type: "enum".

See Error Handling for the cross-plane conventions.