@runtimed/node
v0.5.6
Published
Node.js bindings for controlling nteract runtimed notebooks and Python kernels.
Downloads
2,290
Readme
@runtimed/node
Run Python from Node in a kernel that stays alive between calls. Variables and imports persist, dependencies install mid-session via uv, and plots come back as image data in the cell result.
Before — new process every call, no memory:
execSync('python -c "import pandas as pd; df = pd.read_csv(\'data.csv\'); print(df.shape)"');
// next call: pandas re-imported, df gone, startup cost paid againAfter — one session, state sticks:
const { createNotebook } = require("@runtimed/node");
async function main() {
const session = await createNotebook({ workingDir: "./project" });
await session.runCell('import pandas as pd; df = pd.read_csv("data.csv")');
await session.runCell('df.groupby("region").sum()'); // df still there
await session.addDependencies(["seaborn"]); // no restart
await session.syncEnvironment(); // df survives this
const result = await session.runCell(
"import seaborn as sns; sns.heatmap(df.corr(numeric_only=True))",
);
// Plot arrives as a display_data output. `dataJson` carries base64 PNG under
// "image/png"; `blobUrlsJson` carries a daemon URL for the same bytes.
const plot = result.outputs.find((output) => output.outputType === "display_data");
console.log(Object.keys(JSON.parse(plot.dataJson))); // ["image/png", "text/plain", ...]
}
main().catch((error) => {
console.error(error);
process.exit(1);
});Built for agent loops where one session spans many turns. Session startup costs a couple of seconds and then every later call is cheap, so the win grows with the number of turns. Running a script once? Use a subprocess.
Node.js bindings for the nteract runtimed daemon. This package lets Node,
Bun, and other CommonJS-compatible runtimes create notebooks, run Python cells,
queue executions, read outputs, save notebooks, and manage notebook dependencies
through the same local daemon used by nteract desktop.
Embedding a notebook frontend
@runtimed/node/relay exposes the native byte pipe used by desktop notebook
hosts. The browser/WASM frontend remains the Automerge peer; Node owns the
daemon socket, handshake, framing, and liveness heartbeat and forwards opaque
typed frames to the browser transport.
const { createRelay } = require("@runtimed/node/relay");
const relay = await createRelay({
workingDir: process.cwd(),
ephemeral: true,
description: "embedded notebook",
});
relay.onFrame((frame) => browserTransport.send(frame));
browserTransport.on("message", (frame) => relay.send(frame));
browserTransport.on("close", () => relay.close());Hosts that supervise the daemon can use defaultSocketPath(),
socketPathForChannel("stable" | "nightly"), and
queryDaemonInfo({ socketPath }) from the same subpath. The explicit channel
resolver is useful when the host's release channel differs from the package's
compile-time default. The query returns null until the daemon is ready, so
the host does not need to duplicate the pool wire protocol just to probe
readiness.
RelaySession.info.daemonVersion is the identity carried by that notebook's
exact handshake. It is intentionally left undefined when an older daemon omits
it rather than being filled from a later pool query, which could race a daemon
restart. Treat that artifact version as diagnostic metadata. Compatibility is
determined by the negotiated protocol number and, for optional semantics, the
capabilities advertised by the connection. Use queryDaemonInfo() only for
readiness and diagnostics.
Frames include the one-byte notebook frame discriminator and omit the daemon socket's length prefix; an empty buffer is not a frame and is rejected. The relay subscribes to native delivery eagerly so bootstrap frames are retained. It invokes JavaScript frame handlers serially to preserve ordering, so handlers should forward or copy a frame promptly rather than perform expensive work inline.
Electron hosts can attach that relay directly to one transferred
MessagePortMain without opening a loopback listener:
const { MessageChannelMain } = require("electron");
const { createRelay } = require("@runtimed/node/relay");
const { serveElectronNotebookHost } = require("@runtimed/node/electron");
const relay = await createRelay({ notebookId: authorizedNotebookId });
const { port1, port2 } = new MessageChannelMain();
serveElectronNotebookHost({
port: port1,
relay,
handler: {
invoke(method, params) {
return invokeAuthorizedHostMethod(method, params);
},
},
});
browserWindow.webContents.postMessage("notebook:port", null, [port2]);@runtimed/node/electron validates the host-method allowlist and multiplexes
opaque protocol-numbered notebook frames, host requests, notifications, and
events over the port. The embedding host still owns notebook authorization,
path validation, dialogs, window lifecycle, and output CSP.
The relay is intentionally Electron-free: custom protocols, CSP, browser-peer
authentication, window lifecycle, and daemon installation remain
responsibilities of the embedding host. connectRelay(notebookId) is an
operator connection, not an authorization check. A host must authorize the
notebook ID itself and must not expose room discovery or relay creation directly
to an untrusted browser context.
close() is terminal cancellation. Natural daemon closure drains frames already
queued for JavaScript before notifying onClose; explicitly closing before a
browser peer subscribes discards the buffered bootstrap frames.
For a daemon-backed transport check during development:
RUNTIMED_SOCKET_PATH=/path/to/runtimed.sock \
pnpm --dir packages/runtimed-node smoke:relayInstall
npm install @runtimed/node@runtimed/node ships a small JavaScript wrapper plus TypeScript declarations.
The native binding is installed through an optional platform package such as
@runtimed/node-darwin-arm64 or @runtimed/node-linux-x64-gnu.
Basic Usage
const { createNotebook, defaultSocketPath } = require("@runtimed/node");
async function main() {
const session = await createNotebook({
runtime: "python",
workingDir: process.cwd(),
// Record these before the first cell runs.
dependencies: ["numpy", "matplotlib"],
description: "plotting smoke test",
});
try {
console.log("daemon socket:", defaultSocketPath());
await session.syncEnvironment();
const result = await session.runCell(`
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 6.28, 200)
plt.plot(x, np.sin(x))
plt.show()
`);
console.log(result.status);
console.log(result.outputs);
await session.saveNotebook();
} finally {
await session.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Notebook Dependencies
createNotebook() accepts dependencies so agent code can declare packages
up-front instead of failing the first import and retrying after addDependencies().
When packageManager is omitted, the daemon/user environment choice remains in
charge. Later dependency edits also infer the manager from the running kernel,
inline notebook metadata (uv, then conda, then pixi), or detected project
file, falling back to UV for fresh Python notebooks with no other signal. Pass
the native binding's PackageManager string enum ("uv", "conda", or
"pixi") only when you need to target a specific metadata section.
description can be used as a human-readable peer label for agent-created
sessions.
API Surface
defaultSocketPath()returns the socket path for the current nteract channel or theRUNTIMED_SOCKET_PATHoverride.socketPathForChannel("stable" | "nightly")returns a channel-specific daemon socket path.listActiveNotebooks(options)lists active daemon notebook rooms.createNotebook(options)creates a notebook and records optional first-call dependencies.openNotebook(notebookId, options)connects to an existing daemon notebook.openNotebookPath(path, options)opens a notebook file through the daemon.showNotebook(options)opens an active notebook or path in nteract Desktop, returning a structuredopened: falseresponse in headless environments.shutdownNotebook(notebookId, options)shuts down a notebook room by ID.getExecutionResult(executionId, options)reads a result by execution ID.Session.listCells()andSession.getCell(cellId)inspect notebook cells.Session.createCell(source, options),Session.setCell(cellId, options),Session.deleteCell(cellId), andSession.moveCell(cellId, options)provide direct notebook editing without MCP JSON round-trips.createCell()appends by default; passindex: 0to prepend orafterCellIdto insert after another cell.Session.executeCell(cellId, options)runs an existing code cell.Session.showNotebook()opens the session in nteract Desktop when a display is available.Session.interruptKernel(),Session.shutdownKernel(), andSession.restartKernel()manage the running kernel.Session.shutdownNotebook()shuts down this notebook room and closes the session.Session.runCell(source, options)appends, runs, and waits for a cell.Session.queueCell(source, options)appends a cell, queues it, and returns IDs.Session.waitForExecution(executionId, options)waits for queued work. PassonUpdate(progress)to receive resolved output snapshots while the execution is still running.Session.runtimeState$,Session.executionTransitions$,Session.executionViewChanges$,Session.cellChanges$,Session.broadcasts$, andSession.sessionStatus$expose the same projected event families used by the browser sync engine.Session.getExecutionView()returns the current materialized execution view: non-null notebook cell pointers, execution snapshots keyed byexecution_id, and the execution-ID-first queue projection. Use this for status surfaces; useexecutionViewChanges$when you need every pointer-clear transition.Session.addDependency(spec, { packageManager })/Session.addDependencies(specs, { packageManager })andSession.removeDependency(spec, { packageManager })/Session.removeDependencies(specs, { packageManager })edit notebook dependency metadata for UV, Conda, or Pixi. OmitpackageManagerto follow the notebook's running/configured manager. Batch variants use one CRDT metadata transaction.Session.getDependencyStatus()returns dependency metadata, fingerprint, and trust state in one call.Session.getRuntimeStatus()returns kernel lifecycle, activity, env source, and startup error details.Session.syncEnvironment()installs recorded notebook dependencies.Session.saveNotebook(path?)saves the notebook.Session.close()releases the daemon connection.
Daemon Requirements
The package talks to a local runtimed daemon over its Unix socket. In a
development checkout, run the per-worktree daemon before using the bindings:
cargo xtask dev-daemonPublished nteract desktop builds manage their own daemon. Set
RUNTIMED_SOCKET_PATH when you need to connect to a specific daemon instance.
Development Smoke Test
After building the native binding, run the daemon-backed API smoke test with:
RUNTIMED_SOCKET_PATH=/path/to/runtimed.sock pnpm --dir packages/runtimed-node smoke:apiWhen testing an out-of-tree N-API build, point the smoke script at it:
RUNTIMED_NODE_SMOKE_MODULE=/tmp/runtimed-node-napi-check/index.cjs \
RUNTIMED_SOCKET_PATH=/path/to/runtimed.sock \
pnpm --dir packages/runtimed-node smoke:apiPlatform Packages
The platform packages are implementation details and should normally be
installed through @runtimed/node:
@runtimed/node-darwin-arm64@runtimed/node-linux-x64-gnu
They contain only the compiled native .node binary for their target platform.
