praxis-agent-sdk
Python SDK for the Praxis coding-agent harness. One agent API, every model and provider the harness supports — Anthropic, OpenAI (including Codex), Google Gemini, Vertex, AWS Bedrock, Z.AI, and anything else in its registry — with real tools, subagents, sessions, MCP, skills, and automatic context compaction.
# the SDK, straight from the repo
pip install "git+https://github.com/Facets-cloud/praxis-harness.git@master#subdirectory=python"
# plus the binary it drives (a git install does not carry one — see below)
GOPRIVATE=github.com/Facets-cloud go install github.com/Facets-cloud/praxis-harness/cmd/prx@latest
There is no Node.js requirement, no npm install -g, and no separate CLI to
keep in version sync. Zero runtime dependencies, so it cannot conflict with your
service's tree.
Interactive login
The SDK can drive the same local login flow as the TUI. Discovery exposes the currently selectable providers and the providers intentionally marked as coming soon; it never silently initiates a disabled provider.
from praxis_agent_sdk import PraxisClient, PraxisOptions
async with PraxisClient(PraxisOptions()) as agent:
providers = await agent.get_login_providers()
assert not next(p for p in providers if p.id == "anthropic").coming_soon
challenge = await agent.login_challenge("anthropic")
# Open challenge.auth_url, or collect the API key / redirect URL described
# by challenge.prompt_message, then keep the opaque challenge unchanged.
completion_value = input(challenge.prompt_message)
result = await agent.complete_login(challenge, completion_value)
LoginChallenge.verifier and .state are OAuth continuation values. They are
hidden from repr, but complete_login() needs the original object; do not
reconstruct it from display fields. A coming-soon provider is returned by
get_login_providers() with coming_soon=True and is rejected by both login
methods.
Installing
From git — supported, and what to use today
The package lives in the python/ subdirectory of the harness repo, so every
installer needs to be told that.
# pip
pip install "git+https://github.com/Facets-cloud/praxis-harness.git@master#subdirectory=python"
pip install "git+ssh://git@github.com/Facets-cloud/praxis-harness.git@master#subdirectory=python"
# uv
uv add praxis-agent-sdk --git https://github.com/Facets-cloud/praxis-harness --subdirectory python
# a local checkout, editable
pip install -e /path/to/praxis-harness/python
uv add --editable /path/to/praxis-harness/python
Pin a release rather than tracking master in anything you deploy — replace
@master with a tag or commit, e.g. @python-v0.2.0.
A git or path install does NOT include the binary. Nothing compiles Go during a
Python install, and _bin/ is gitignored on purpose so a platform-specific binary
never enters the source tree. bundled_prx_version() returns None there, which is
the honest answer. Supply prx by any one of:
# build it from the same checkout
cd /path/to/praxis-harness && make build && export PRAXIS_BIN=$PWD/bin/prx
# or install it with Homebrew (macOS, Linux)
brew install facets-cloud/tap/prx
# or install it with Go
GOPRIVATE=github.com/Facets-cloud go install github.com/Facets-cloud/praxis-harness/cmd/prx@latest
# or take a release archive, then put prx on PATH
# https://github.com/Facets-cloud/praxis-harness-releases/releases
# or point at it per session, no environment needed
python -c "from praxis_agent_sdk import PraxisOptions; PraxisOptions(executable='/path/to/prx')"
If prx cannot be found, the error names every one of these fixes rather than
saying "not found".
From a release wheel — binary included, no PyPI needed
Every python-v* tag attaches platform wheels to its GitHub release, and a wheel
bundles the matching prx. That is strictly better than a git install for a
consumer, because nothing else has to be supplied:
pip install https://github.com/Facets-cloud/praxis-harness-releases/releases/download/python-v0.2.0/praxis_agent_sdk-0.2.0-py3-none-macosx_11_0_arm64.whl
Pick the wheel matching your platform from
https://github.com/Facets-cloud/praxis-harness-releases/releases. bundled_prx_version()
then reports the exact prx it carries.
From PyPI
uv add praxis-agent-sdk # uv project
uv pip install praxis-agent-sdk # uv, pip-style
pip install praxis-agent-sdk # pip
Platform wheels for macOS (arm64, x86_64) and Linux (x86_64, aarch64) each bundle
the matching prx, so pip/uv picks the right one and you need nothing else.
Releases from 0.11.0 on are published to PyPI; for an older version use a release
wheel or the git install above.
Resolution order: PraxisOptions.executable → $PRAXIS_BIN → the wheel's bundled
binary → prx on $PATH. Bundled beats $PATH deliberately — a wheel pins a
binary it was tested against, and silently preferring a stray prx makes version
skew invisible.
Adding it to a uv service
# pyproject.toml
dependencies = ["praxis-agent-sdk>=0.2.0"]
# while unpublished, pin the source:
[tool.uv.sources]
praxis-agent-sdk = { git = "https://github.com/Facets-cloud/praxis-harness", subdirectory = "python" }
uv lock && uv sync
Then swap the Node layer for the binary in your image:
# was: RUN npm install -g @anthropic-ai/claude-code
COPY --from=praxis /usr/local/bin/prx /usr/local/bin/prx
ENV PRAXIS_BIN=/usr/local/bin/prx
The SDK's floor is Python 3.10 so it never forces a consumer's floor upward;
uv lock/uv sync/uv run are verified against 3.14.
Quick start
import asyncio
from praxis_agent_sdk import PraxisOptions, query, AssistantMessage
async def main():
async for message in query(
"read README.md and summarise it in three bullets",
PraxisOptions(model="claude-opus-5", cwd="/path/to/repo"),
):
if isinstance(message, AssistantMessage):
print(message.text)
asyncio.run(main())
Change one string to change provider — the model id resolves its own provider:
PraxisOptions(model="gpt-5.6-terra") # OpenAI
PraxisOptions(model="gemini-2.5-pro") # Google
PraxisOptions(model="claude-opus-5") # Anthropic
PraxisOptions(model="glm-4.6") # Z.AI
Credentials
Nothing to configure. If this machine is logged in — prx login, or the TUI's
provider picker — the agent authenticates from that login, whichever kind it is:
OAuth (a Claude or ChatGPT subscription) or an API key, resolved per provider from
the model you asked for. Provider keys in the environment (ANTHROPIC_API_KEY,
OPENAI_CODEX_API_KEY, …) keep working too; the child inherits your environment.
What the SDK does for you is keep that login reachable when your process gives the
agent a different HOME:
# An isolated runtime: its own HOME, its own sessions, its own settings.
PraxisOptions(env={"HOME": str(runtime / "home")}, session_dir=str(runtime / "sessions"))
Praxis reads credentials from $HOME/.praxis/agent/auth.json, so a re-homed child
would resolve an empty store and fail to start with no credential for provider —
even though you are logged in. The SDK detects that the child would look somewhere
else and pins PRAXIS_CODING_AGENT_AUTH_FILE at the store this process uses. Only
the credential store is shared: sessions, skills, memories, and analytics stay under
the HOME you chose.
What travels is the store, never a token the SDK copied out of it — that is what keeps OAuth working, since the harness still holds the refresh token, the account id, and the credential type that decides a subscription's context window.
# Authenticate from a specific store (a mounted secret, a service account's login).
PraxisOptions(credentials="/run/secrets/praxis-auth.json")
# Or opt out and let the child resolve its own credentials from the env you gave it.
PraxisOptions(env={"HOME": sandbox}, inherit_credentials=False)
A live session
query() is one prompt. PraxisClient is a conversation you can steer.
from praxis_agent_sdk import PraxisClient, PraxisOptions, AssistantMessage
async with PraxisClient(PraxisOptions(model="claude-opus-5", cwd=repo)) as agent:
async for msg in agent.prompt("find the retry logic and explain it"):
if isinstance(msg, AssistantMessage):
print(msg.text)
# Mid-turn course correction — the agent picks this up at its next tool
# boundary instead of you interrupting and restating the whole task.
await agent.steer("only look under internal/, skip the tests")
await agent.set_model("gpt-5.6-terra") # switch models, keep history
usage, cost_usd = await agent.usage()
print(f"{usage.billed} tokens, ${cost_usd:.4f}")
Exact plan approval
Plan approval is bound to the proposal you reviewed. Enter plan mode, propose a plan, fetch that proposal by its opaque id, then pass the same id to approval. A proposal that changed, expired, or was never reviewed is rejected.
async with PraxisClient(PraxisOptions(permission_mode="plan")) as agent:
await agent.set_plan_mode(True)
proposal = await agent.propose_plan()
proposal_id = proposal["proposalId"]
content = await agent.get_plan_proposal(proposal_id)
print(content)
await agent.approve_plan("ship", proposal_id)
MCP server configuration
Use persist_mcp_server() to save a server for a later session without starting a
process now. Use add_mcp_server() when the current conversation needs it
immediately; it connects the server and persists the same configuration.
async with PraxisClient(PraxisOptions()) as agent:
path = await agent.persist_mcp_server("docs", "npx", "-y", "docs-mcp-server")
print(f"saved MCP configuration in {path}")
Tools that run in your process
from praxis_agent_sdk import PraxisOptions, PraxisClient, tool
@tool("get_deployment", "Look up a deployment by id", tier="read")
async def get_deployment(deployment_id: str) -> str:
return await api.fetch(deployment_id)
@tool("scale_service", "Change a service's replica count", tier="write")
async def scale_service(service: str, replicas: int) -> str:
await api.scale(service, replicas)
return f"scaled {service} to {replicas}"
options = PraxisOptions(model="claude-opus-5", tools=[get_deployment, scale_service])
tier is load-bearing:
| tier | meaning | approval |
|---|---|---|
read |
no side effects | auto-approved, never prompts |
write |
mutates something | needs a permission decision |
exec |
runs commands | needs a permission decision |
A write/exec tool without a can_use_tool callback is denied, not
stalled — the harness resolves the prompt locally rather than blocking a turn on
an answer that is not coming.
Permissions
from praxis_agent_sdk import PermissionDecision, PraxisOptions
async def can_use_tool(request):
if request.tool_name == "bash" and request.destructive:
return PermissionDecision(allow=False, reason="no destructive shell in prod")
return PermissionDecision(allow=True)
PraxisOptions(model="claude-opus-5", permission_mode="ask", can_use_tool=can_use_tool)
Permission modes: auto (default — classifier-gated, reads free), ask
(everything non-read prompts), yolo (allow, minus deterministic red lines),
plan (read-only planning).
Migrating from claude-agent-sdk
| Claude Agent SDK | Praxis SDK |
|---|---|
ClaudeAgentOptions |
PraxisOptions |
ClaudeSDKClient |
PraxisClient |
query(prompt, options) |
query(prompt, options) |
@tool(name, desc, schema) |
@tool(name, desc, schema, tier=...) |
create_sdk_mcp_server(...) |
deleted — pass tools=[...] |
options.mcp_servers={"x": server} |
tools=[...] for in-process; mcp_config= for real MCP servers |
can_use_tool → PermissionResultAllow/Deny |
can_use_tool → PermissionDecision(allow=...) |
options.model (Anthropic only) |
options.model (any provider) |
options.max_turns |
options.max_turns |
options.permission_mode |
options.permission_mode |
options.setting_sources |
options.settings, no_rules, no_skills |
ResultMessage.total_cost_usd |
ResultMessage, or await client.usage() |
requires Node + @anthropic-ai/claude-code |
nothing — the binary ships in the wheel |
The full port guide, including the wire protocol, is in
docs/design/PYTHON-SDK.md.
Using a specific binary
Resolution order: PraxisOptions.executable → $PRAXIS_BIN → the wheel's
bundled binary → prx on $PATH.
PRAXIS_BIN=/usr/local/bin/prx python app.py
Development
cd .. && make build # do this FIRST: the contract tests pin ./bin/prx
cd python
uv venv && uv pip install -e '.[dev]' # or python -m venv .venv && .venv/bin/pip install -e '.[dev]'
uv run pytest # whole suite, ~16s (contract tests drive the real binary)
uv run pytest -v --durations=0 # per-test progress; use this if a run looks stuck
uv run ruff check . && uv run mypy src/praxis_agent_sdk
uv build # sdist + wheel
The test suite spawns a real subprocess speaking the real protocol, so framing
and correlation bugs fail the tests rather than reaching production.
tests/test_contract.py additionally drives the REAL binary, which is what stops
the Go and Python halves of the protocol drifting.
Three things about running it:
- The contract tests use
../bin/prxand nothing else. They deliberately do NOT fall back to aprxon$PATH: a stale binary answers a newly added command withunknown command: X, which reads like a protocol bug rather than a missingmake build. Without the built binary they skip and say so.PRAXIS_CONTRACT_BINoverrides the path for CI layouts that build elsewhere. - No test may hang.
pytest-timeoutcaps every test at 60s and dumps the stack of all threads, because a hang blocks the whole suite and-qprints its summary only at the end — so a slow suite is indistinguishable from a stuck one. PRAXIS_TEST_BUILD=1opts into the packaging build test, which provisions an isolated environment and needs the network. It is skipped by default so the suite stays hermetic.
Runnable examples for every capability are in examples/.
Metadata
Release files for praxis-agent-sdk 0.11.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| praxis_agent_sdk-0.11.0.tar.gz | 106.9 kB | Without provenance | Details for praxis_agent_sdk-0.11.0.tar.gz |
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| praxis_agent_sdk-0.11.0-py3-none-manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details for praxis_agent_sdk-0.11.0-py3-none-manylinux2014_x86_64.whl |
| praxis_agent_sdk-0.11.0-py3-none-manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details for praxis_agent_sdk-0.11.0-py3-none-manylinux2014_aarch64.whl |
| praxis_agent_sdk-0.11.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details for praxis_agent_sdk-0.11.0-py3-none-macosx_11_0_arm64.whl |
| praxis_agent_sdk-0.11.0-py3-none-macosx_10_15_x86_64.whl | Python 3 | none | macOS 10.15+ x86-64 | Details for praxis_agent_sdk-0.11.0-py3-none-macosx_10_15_x86_64.whl |
Total release size: 157.5 MB
Release files / praxis_agent_sdk-0.11.0.tar.gz
| Download URL | praxis_agent_sdk-0.11.0.tar.gz |
|---|---|
| Size | 106.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4f154f9c61106034ec3a2a85cc945b309ae66ffe34aca50842814a53af3fe3bf
|
|
BLAKE2b-256 checksum How to use checksums |
983c0222d4200e20013baf48a2492b2ac30a1a24c581a3e2661f14858fe34ec5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / praxis_agent_sdk-0.11.0-py3-none-manylinux2014_x86_64.whl
| Download URL | praxis_agent_sdk-0.11.0-py3-none-manylinux2014_x86_64.whl |
|---|---|
| Size | 40.5 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
1979c821dae982f59b8eb5c292e7e2f8fdfef6d96c17291be8c941afd8693f66
|
|
BLAKE2b-256 checksum How to use checksums |
2420a336d099d023940f5951d61776eb798f648f3f3144b0d7201551a5885839
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / praxis_agent_sdk-0.11.0-py3-none-manylinux2014_aarch64.whl
| Download URL | praxis_agent_sdk-0.11.0-py3-none-manylinux2014_aarch64.whl |
|---|---|
| Size | 36.9 MB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
0329a3f40b590ed894732b91e643609c5e405386ae02a677b069932b13e4f7fa
|
|
BLAKE2b-256 checksum How to use checksums |
579fb112e3844fdaa94e09d41e1d2b9cddf3d0ad8ea933930b7f3e575feb7db8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / praxis_agent_sdk-0.11.0-py3-none-macosx_11_0_arm64.whl
| Download URL | praxis_agent_sdk-0.11.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 38.7 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
761008666400a135b463418bbeb5cdb4badda5435309583639e6a51da75d5c51
|
|
BLAKE2b-256 checksum How to use checksums |
b5991a4882a5e42814d86850113eacee209ca53abed65deb3e4a2490f888a4d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / praxis_agent_sdk-0.11.0-py3-none-macosx_10_15_x86_64.whl
| Download URL | praxis_agent_sdk-0.11.0-py3-none-macosx_10_15_x86_64.whl |
|---|---|
| Size | 41.4 MB |
| Tags | Python 3 macOS 10.15+ x86-64 |
|
SHA-256 checksum How to use checksums |
747c51ef7aed15574540972ae31427bdd6a9cbad099c4f759ffa3d6f417f525d
|
|
BLAKE2b-256 checksum How to use checksums |
40eea96aa961a1b75a0dc1cb94e3f3346d6d4b81aab631d077d4787f496c044e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Security for praxis-agent-sdk 0.11.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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
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.