Skip to main content
Execution

Python

Execute Python source, files, modules, and package workflows in agentOS.

agentOS runs CPython 3.13 as a first-class VM execution engine. Python shares the VM filesystem, process tree, networking, permissions, and limits with agents, Bash, JavaScript, and installed software.

Letting an agent write code instead of chaining one tool call per step is called Code Mode. It has a few advantages over driving Bash:

  • Fewer tokens: Ten chained operations cost one round trip, not ten.
  • Real data processing: Comprehensions and pandas instead of jq and awk.
  • The package ecosystem: pip install whatever the task needs.
  • Concurrency: asyncio instead of shell job control.

Evaluate an expression

evaluate() returns a JSON-serializable value.

import { AgentOs } from "@rivet-dev/agentos";

const runtime = await AgentOs.create();

try {
	const result = await runtime.python.evaluate<number>("21 * 2");
	console.log(result.outcome === "succeeded" ? result.value : result.error); // 42
} finally {
	await runtime.dispose();
}

A value the JSON encoder can’t represent fails with evaluation_serialization_failed rather than silently losing the value.

Execute code

execute() runs source and captures its output instead of returning a value.

import { AgentOs } from "@rivet-dev/agentos";

const runtime = await AgentOs.create();

try {
	const result = await runtime.python.execute(`print("hello from agentOS")`, {
		output: { capture: "all" },
	});
	console.log(result.outcome === "succeeded" ? result.stdout : result.error); // "hello from agentOS\n"
} finally {
	await runtime.dispose();
}

Executions are ephemeral, so capture stdio only when you want it — "stderr" for diagnostics, "all" for both streams. onStdout/onStderr stream live and work independently of capture.

Pass data into code

inputs hands host values to the guest as real objects, so data never gets interpolated into source.

import { AgentOs } from "@rivet-dev/agentos";

const runtime = await AgentOs.create();

try {
	const result = await runtime.python.evaluate<number>(
		"sum(item['price'] for item in inputs['items'])",
		{ inputs: { items: [{ price: 10 }, { price: 32 }] } },
	);
	console.log(result.outcome === "succeeded" ? result.value : result.error); // 42
} finally {
	await runtime.dispose();
}

Keep state between calls

Pass a contextId to keep globals, functions, imports, and modules alive in one interpreter.

import { AgentOs } from "@rivet-dev/agentos";

const runtime = await AgentOs.create();

try {
	await runtime.createContext("analysis");

	await runtime.python.execute("answer = 40", { contextId: "analysis" });

	const result = await runtime.python.evaluate<number>("answer + 2", {
		contextId: "analysis",
	});
	console.log(result.outcome === "succeeded" ? result.value : result.error); // 42

	// Delete an idle context when you are done with it. `contexts.reset()`
	// is the other option: it keeps the id and drops only the interpreter state.
	await runtime.contexts.delete("analysis");
} finally {
	await runtime.dispose();
}

Only inline execute()/evaluate() retain memory. Files, modules, installs, and Bash may pass the same id, but they run fresh. A context runs one operation at a time — reusing a busy contextId fails immediately.

Files, modules, and async

import { AgentOs } from "@rivet-dev/agentos";

const runtime = await AgentOs.create();

try {
	await runtime.python.executeFile("/workspace/report.py", {
		args: ["--json"],
	});
	await runtime.python.executeModule("http.server", { args: ["8000"] }); // python -m

	// Inline Python supports top-level await, `async for`, and `async with`.
	await runtime.createContext("async");
	const result = await runtime.python.evaluate<string>("await fetch_data()", {
		contextId: "async",
	});
	console.log(result.outcome === "succeeded" ? result.value : result.error);
} finally {
	await runtime.dispose();
}

Awaited work counts against the deadline; unawaited asyncio tasks are not promised to survive between operations.

Install packages

import { AgentOs } from "@rivet-dev/agentos";

const runtime = await AgentOs.create();

try {
	const installed = await runtime.python.install(["requests==2.32.4"], {
		upgrade: true,
	});
	console.log(installed.outcome); // "succeeded"

	await runtime.python.install({ requirementsFile: "requirements.txt" });
} finally {
	await runtime.dispose();
}

Installs modify the VM-wide filesystem, so a package installed once is importable by every later execution in that VM, in any language. Only one npm/Python mutation runs at a time per VM; a concurrent install fails with execution_busy.

Background processes and web servers

spawn* starts a long-lived process and returns a pid. From there you get stdin, output, signals, and waiting.

import { AgentOs } from "@rivet-dev/agentos";

const runtime = await AgentOs.create({ permissions: { network: "allow" } });

try {
	const server = await runtime.python.spawnModule("http.server", {
		args: ["8000"],
		output: { retainEvents: true },
	});

	const response = await runtime.network.httpRequest({ port: 8000, path: "/" });
	console.log(response.status); // 200

	await runtime.process.readOutput(server.pid);
	await runtime.process.signal(server.pid, "SIGINT");
	await runtime.process.wait(server.pid);
} finally {
	await runtime.dispose();
}

A full Linux environment underneath

There is a real Linux environment behind all of this, shared by every language. Files and installed packages are immediately visible to Bash, JavaScript, agents, and other executions.

import { AgentOs } from "@rivet-dev/agentos";

const runtime = await AgentOs.create({ permissions: { network: "allow" } });

try {
	// Filesystem: pathlib and os use the VM's persistent filesystem.
	const files = await runtime.python.execute(
		`
from pathlib import Path

Path("/workspace/data.txt").write_text("hello")
print(Path("/workspace/data.txt").read_text())
		`,
		{ output: { capture: "all" } },
	);
	console.log(files.stdout); // "hello\n"

	// Process trees: subprocess spawns real guest processes.
	const processes = await runtime.python.execute(
		`
import subprocess

subprocess.run(["ls", "-la", "/workspace"], check=True)
		`,
		{ output: { capture: "all" } },
	);
	console.log(processes.stdout); // Directory listing for /workspace

	// Networking: sockets and HTTP clients go through the VM network policy.
	const status = await runtime.python.execute(
		`
import urllib.request

print(urllib.request.urlopen("https://example.com").status)
		`,
		{ output: { capture: "all" } },
	);
	console.log(status.stdout); // "200\n"
} finally {
	await runtime.dispose();
}

See Filesystem, Processes & Shells, and Networking & Previews.

Bindings

Python calls bindings as normal commands via subprocess, so host credentials stay outside the VM.

import { AgentOs, type Bindings } from "@rivet-dev/agentos";
import { z } from "zod";

// The handler runs on the host, so the API key never enters the VM.
const weather: Bindings = {
	name: "weather",
	description: "Weather data bindings",
	bindings: {
		forecast: {
			description: "Get the weather forecast for a city",
			inputSchema: z.object({ city: z.string() }),
			execute: async ({ city }: { city: string }) => {
				const res = await fetch(
					`https://api.weather.example/forecast?city=${city}&key=${process.env.WEATHER_API_KEY}`,
				);
				return res.json();
			},
		},
	},
};

// The collection is projected into the VM as an `agentos-weather` command.
const runtime = await AgentOs.create({ bindings: [weather] });

try {
	const result = await runtime.python.execute(
		`
import json
import subprocess

completed = subprocess.run(
    ["agentos-weather", "forecast", "--city", "Paris"],
    check=True,
    capture_output=True,
    text=True,
)
print(json.loads(completed.stdout)["result"])
		`,
		{ output: { capture: "all" } },
	);
	console.log(result.outcome === "succeeded" ? result.stdout : result.error);
} finally {
	await runtime.dispose();
}

Permissions, limits, and timeouts

Every operation inherits the VM permission policy and resource limits.

import { AgentOs } from "@rivet-dev/agentos";

const runtime = await AgentOs.create();

try {
	// timeoutMs is a wall-clock deadline for the whole operation. It expires
	// into a result rather than throwing, and never replaces the VM watchdogs.
	const result = await runtime.python.execute("import time; time.sleep(10)", {
		timeoutMs: 2_000,
	});
	console.log(result.outcome); // "timed_out"
} finally {
	await runtime.dispose();
}