Skip to main content

pal-runtime (Python)

The official Python SDK for PAL — the open-source runtime for production AI agents with VPC deployment, pre-run cost forecasts, policy budgets, and eval-in-CI.

Python 3.11+. Async-first (httpx + asyncio). Full parity with the TypeScript SDK's HTTP surface.


Install

Registry packages are not published yet. The distribution is named pal-runtime; do not pip install pal-sdk (that PyPI name belongs to an unrelated project). Install from the repo:

pip install "path/to/pal/packages/sdk-python"          # add [deepagents] for the extra below
# or in CI:
pip install "git+https://github.com/dshakes/pal.git#subdirectory=packages/sdk-python"

The import package is pal (from pal import PalClient); the CLI entry point is pal-runner.

Dependencies pulled in automatically: httpx>=0.27, pydantic>=2.0, grpcio>=1.62, protobuf>=5.0, opentelemetry-api>=1.20.

Optional extras:

Extra Pulls in For
deepagents deepagents>=0.6 Run a LangChain deepagents agent on a PAL microVM — see deepagents integration

Quick start

import asyncio
import os
from pal import PalClient

async def main():
    async with PalClient(
        api_key=os.environ["PAL_API_KEY"],
        # base_url defaults to PAL_API_URL, else http://localhost:8080
        # or set PAL_API_URL for a self-hosted instance
    ) as client:

        # 1. Create an agent
        agent = await client.agents.create(name="triage", description="Classifies support emails")

        # 2. Run it
        run = await client.runs.create(agent="triage", input={"email": "my invoice is wrong"})
        print(run.status, run.output)

        # 3. Stream events in real time
        async for event in client.runs.events(run.id):
            print(event.kind, event.data)

asyncio.run(main())

Client constructor

PalClient(
    base_url: str | None = None,    # default: PAL_API_URL or http://localhost:8080
    api_key: str | None = None,     # default: PAL_API_KEY
    *,
    timeout: float = 30.0,          # per-request timeout in seconds
    route_strategy: str | None = None,  # default: PAL_ROUTE_STRATEGY or "auto"
)

Use as a context manager (async with PalClient(...) as client) to ensure the underlying httpx connection pool is closed. Call await client.close() manually if not using a context manager.


Env vars

Var Purpose
PAL_API_URL Base URL of the control-plane API. Default http://localhost:8080; set it to your control plane
PAL_API_KEY Bearer token issued by POST /auth/login or the API-keys settings page
PAL_ROUTE_STRATEGY One of auto, cheap, best, fast. Sent as X-Pal-Route-Strategy. Default auto

API reference

Agents — client.agents

# Create
agent: AgentInfo = await client.agents.create(name="my-agent", description="...", labels={})

# Get
agent: AgentInfo = await client.agents.get("my-agent")

# List
resp: AgentListResponse = await client.agents.list(page_size=50, page_token=None)
# resp.agents: list[AgentInfo]
# resp.next_page_token: str | None

# Delete
await client.agents.delete("my-agent")

Runs — client.runs

# Create (returns Run)
run: Run = await client.runs.create(
    agent="triage",
    input={"email": "..."},
    labels={},
    idempotency_key=None,   # optional; derived from (run_id, step_id, attempt) in the engine
)

# Create with streaming (returns async iterator of StreamEvent)
stream = await client.runs.create(agent="triage", input={...}, stream=True)
async for event in stream:
    print(event.kind, event.data)

# Get, list, cancel
run: Run = await client.runs.get(run_id)
resp: RunListResponse = await client.runs.list(agent="triage", status="completed", page_size=50)
run: Run = await client.runs.cancel(run_id, reason="user cancelled")

# Stream events from a completed or live run
async for event in client.runs.events(run_id, from_seq=0, live=True):
    print(event.kind, event.data)

# Send a signal to a paused run (workflow approval / step_signal)
await client.runs.signal(run_id, name="approved", value={"comment": "lgtm"})

Sessions — client.sessions

# Create an interactive multi-turn session
session: Session = await client.sessions.create(agent="my-agent", metadata={})

# Send a message and get the synchronous response
msg: SessionMessage = await client.sessions.send_message(session_id, content="hello", role="user")

# Stream session events (SSE)
async for event in client.sessions.stream_events(session_id, from_seq=0):
    print(event)

# Get, list, close
session = await client.sessions.get(session_id)
resp: SessionListResponse = await client.sessions.list(agent="my-agent", status="active")
await client.sessions.close(session_id)

Connectors — client.connectors

# List installed connectors
resp: ConnectorListResponse = await client.connectors.list()

# Execute a connector action (e.g. send a Slack message)
result: ConnectorResult = await client.connectors.execute(
    "slack",
    "send_message",
    params={"channel": "#general", "text": "Hello from PAL!"},
)

# List available actions for a connector
actions: list[dict] = await client.connectors.list_actions("slack")

Building an agent (decorator style)

from pal import agent, step, tool
from pal.types import AgentContext

@agent("researcher", model="reasoning-large", description="Deep research agent")
async def researcher(input: dict, ctx: AgentContext) -> dict:
    # step() is durable: result is journalled and replayed on restart
    results = await step(
        "search",
        lambda: ctx.tools.web.search(input["query"]),
    )
    # parallel fan-out
    summaries = await step_map(
        "summarize",
        results[:5],
        lambda item, i: ctx.llm.complete(prompt=f"Summarize: {item}"),
        concurrency=3,
    )
    return {"summaries": summaries}

Step primitives

Function Behaviour
await step(name, fn, *, retry, timeout) Single durable step. fn is an async callable.
await step_map(name, items, fn, *, concurrency, retry, timeout) Fan-out over a list; each item gets its own journal entry name[i]
await step_race(name, fns, *, retry, timeout) Run multiple callables concurrently; return the first result, cancel the rest
await step_sleep(name, duration) Durable sleep. Duration strings: "30s", "5m", "2h"
await step_signal(name, *, timeout) Wait for an external signal. Only functional in the PAL production runtime

In local / dev mode all step primitives execute the function directly (no journalling). The production runtime installs a journal-aware runner via set_step_runtime().

Built-in tools

from pal.tools import tool   # singleton registry

@agent("worker", tools=[tool.web, tool.python])
async def worker(input: dict, ctx: AgentContext) -> dict:
    html = await ctx.tools.web.fetch("https://example.com")
    ...

Available: tool.web, tool.python, tool.fs, tool.browser. tool.all() returns all four.

Class-based pattern

from pal.agent import PalAgent
from pal.types import AgentContext

class MyAgent(PalAgent):
    name = "my-agent"
    model = "auto"

    async def run(self, input: dict, ctx: AgentContext) -> dict:
        ...

Errors

All errors inherit from PalError.

Exception Raised when
PalApiError(status_code, body) HTTP response is non-2xx
PalStepError(name, reason) Step execution fails
PalLlmError LLM call fails (budget, provider, timeout)
PalTimeoutError(name, duration) step() / step_sleep() timeout exceeded
PalValidationError Invalid agent name or config

Running an agent locally

python -m pal.runtime.runner --agent hello-world --input '{"name": "World"}'

The pal-runner entry point (installed by pip install) does the same:

pal-runner --agent hello-world --input '{"name": "World"}'

deepagents integration

pip install "path/to/pal/packages/sdk-python[deepagents]" adds a deepagents sandbox backend that runs the agent's shell and filesystem inside a PAL microVM instead of on the host. Egress allowlist, secret vending, and per-tenant quota are enforced by the runtime, not by the prompt — untrusted agent code never touches a bare shell (invariant #5).

from deepagents import create_deep_agent
from pal.integrations.deepagents_sandbox import PalSandbox

with PalSandbox.create(image_digest="sha256:...") as sandbox:
    agent = create_deep_agent(backend=sandbox)
    agent.invoke({"messages": [{"role": "user", "content": "run the tests"}]})

PalSandbox implements deepagents' SandboxBackendProtocol. deepagents' BaseSandbox derives the full filesystem toolset (ls/read/write/edit/ glob/grep) from three primitives, so the backend is just those three mapped onto the runtime REST surface:

Backend method PAL endpoint
execute() POST /v1/runtime/vms/{id}/exec
upload_files()/download_files() base64 over the same exec channel
create() / close() POST /v1/runtime/schedule / DELETE /v1/runtime/vms/{id}

Constructors:

  • PalSandbox.create(image_digest=..., **spec) — schedules a fresh microVM and polls until it is running. Extra kwargs pass through to /v1/runtime/schedule (limits, egressRules, secrets, labels, ...). Terminates the VM on exit.
  • PalSandbox.connect(vm_id) — attaches to an already-running VM; does not terminate it on close.

Composes with deepagents' own CompositeBackend so state can outlive the VM — mount a store at /memories/ while everything else executes in the sandbox. See examples/deepagents_sandbox_agent.py for a runnable version that also wires RubricMiddleware for in-loop self-evaluation.

Reads PAL_API_URL / PAL_API_KEY like the main client; pass base_url / api_key explicitly to override.

create() defaults the entrypoint to sleep infinity. A sandbox is a long-lived exec target, so an image whose own entrypoint runs to completion (python:3.11-slim just starts a REPL and exits) would be reaped by the runtime before the agent could exec into it. Pass an explicit command to override.

Verified against a real VM. The full path — PalSandbox → control-plane → runtime-scheduler → runtime-manager → real container — is exercised end to end on the docker backend, covering execute, binary upload/download roundtrip, and the BaseSandbox-derived write/read/ls/edit/grep the agent actually calls. The Go-side regression test is services/control-plane/internal/handlers/runtime_exec_e2e_test.go (opt-in via PAL_RUNTIME_E2E=1). The Firecracker path specifically is not verifiable on macOS (no /dev/kvm) — that is covered by the microvm · integration CI job on a KVM runner. Exec routing differs between them: the manager sends Firecracker VMs to the in-guest harness and everything else through the backend.

Known ceilings. File transfer is base64-inline over the exec channel — fine for agent-sized files, but a large binary should move via object storage. A microVM image digest is required; the sandbox does not build one for you.


Parity note

The Python SDK's management surface (agents, runs, sessions, connectors, budgets, evals, experiments, marketplace, mcp, receipts, feedback, rehearsals, plus debug and support) is at parity with the TypeScript SDK's HTTP surface — all typed, none require raw client._request(...) calls. The agent definition layer (@agent, step, tool) mirrors the TypeScript surface except that zod schema validation is replaced by plain Python type hints and pydantic models.

Metadata

Release files for pal-runtime 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pal-runtime 0.1.0
File Size Uploaded Provenance Actions
pal_runtime-0.1.0.tar.gz 70.4 kB With provenance Details for pal_runtime-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pal-runtime 0.1.0
File Interpreter ABI Platform Actions
pal_runtime-0.1.0-py3-none-any.whl Python 3 none any Details for pal_runtime-0.1.0-py3-none-any.whl

Total release size: 127.9 kB

Release files / pal_runtime-0.1.0.tar.gz

Download URL pal_runtime-0.1.0.tar.gz
Size 70.4 kB
Tags Source
SHA-256 checksum
How to use checksums
460b2f5f16db891d8c6afc8d232e1a9b19c4d65d2ee01a78a6780ba0e5e83965
BLAKE2b-256 checksum
How to use checksums
9b4acf29f03078b65fc370bf7f2a15a20f85544d5c9b295a608653ae119a1d18
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Release files / pal_runtime-0.1.0-py3-none-any.whl

Download URL pal_runtime-0.1.0-py3-none-any.whl
Size 57.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3941ba4a3e6f0282d4e0d0a2f9cc38b99c6d414f481031906cf261fe8b235d55
BLAKE2b-256 checksum
How to use checksums
bb8de6615359d47aed8b4328176cc5adeae0cf67701dd1b28d11f98cd10678f5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Security for pal-runtime 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pal_runtime-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page