praxis-agent-sdk 0.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- praxis_agent_sdk-0.11.0/.gitignore +66 -0
- praxis_agent_sdk-0.11.0/PKG-INFO +397 -0
- praxis_agent_sdk-0.11.0/README.md +369 -0
- praxis_agent_sdk-0.11.0/examples/01_quickstart.py +62 -0
- praxis_agent_sdk-0.11.0/examples/02_streaming.py +87 -0
- praxis_agent_sdk-0.11.0/examples/03_client_tools.py +153 -0
- praxis_agent_sdk-0.11.0/examples/04_permissions.py +124 -0
- praxis_agent_sdk-0.11.0/examples/05_structured_output.py +191 -0
- praxis_agent_sdk-0.11.0/examples/06_multi_provider.py +148 -0
- praxis_agent_sdk-0.11.0/examples/07_sessions_and_fork.py +114 -0
- praxis_agent_sdk-0.11.0/examples/08_steering_and_interrupt.py +150 -0
- praxis_agent_sdk-0.11.0/examples/09_subagents.py +126 -0
- praxis_agent_sdk-0.11.0/examples/10_concurrent_sessions.py +142 -0
- praxis_agent_sdk-0.11.0/examples/11_budgets_and_cost.py +155 -0
- praxis_agent_sdk-0.11.0/examples/12_error_handling.py +147 -0
- praxis_agent_sdk-0.11.0/examples/13_fastapi_service.py +317 -0
- praxis_agent_sdk-0.11.0/examples/14_hooks_and_settings.py +179 -0
- praxis_agent_sdk-0.11.0/examples/15_skills.py +182 -0
- praxis_agent_sdk-0.11.0/examples/16_mcp_servers.py +215 -0
- praxis_agent_sdk-0.11.0/examples/17_sandbox_and_scopes.py +215 -0
- praxis_agent_sdk-0.11.0/examples/18_jobs_todos_queue.py +312 -0
- praxis_agent_sdk-0.11.0/examples/README.md +62 -0
- praxis_agent_sdk-0.11.0/examples/agent_factory_migration.py +355 -0
- praxis_agent_sdk-0.11.0/examples/smoke.py +108 -0
- praxis_agent_sdk-0.11.0/pyproject.toml +92 -0
- praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/__init__.py +131 -0
- praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/_bundled.py +3 -0
- praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/_internal/__init__.py +1 -0
- praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/_internal/transport.py +669 -0
- praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/client.py +1180 -0
- praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/errors.py +54 -0
- praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/py.typed +0 -0
- praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/tools.py +144 -0
- praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/types.py +545 -0
- praxis_agent_sdk-0.11.0/tests/fakeagent.py +169 -0
- praxis_agent_sdk-0.11.0/tests/sdk_contracts.py +68 -0
- praxis_agent_sdk-0.11.0/tests/test_client.py +765 -0
- praxis_agent_sdk-0.11.0/tests/test_contract.py +556 -0
- praxis_agent_sdk-0.11.0/tests/test_endpoint.py +401 -0
- praxis_agent_sdk-0.11.0/tests/test_packaging.py +322 -0
- praxis_agent_sdk-0.11.0/tests/test_structured_output.py +204 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
.codegraph/
|
|
2
|
+
.playwright-mcp/
|
|
3
|
+
.worktrees/
|
|
4
|
+
.praxis/worktrees/
|
|
5
|
+
.claude/
|
|
6
|
+
.codex/
|
|
7
|
+
.superpowers/
|
|
8
|
+
.DS_Store
|
|
9
|
+
/bin/
|
|
10
|
+
/client
|
|
11
|
+
/praxis-ui
|
|
12
|
+
/praxis-native
|
|
13
|
+
*.test
|
|
14
|
+
/cachetest
|
|
15
|
+
race-test-output.txt
|
|
16
|
+
tui/.tmp_hybrid_out.txt
|
|
17
|
+
__debug*
|
|
18
|
+
|
|
19
|
+
# superpowers planning artifacts (kept local, not shipped)
|
|
20
|
+
docs/superpowers/
|
|
21
|
+
# Root-anchored: an unanchored `plans/` also matched docs/plans/, so a design plan
|
|
22
|
+
# committed there was silently ignored. Only the scratch dir at the repo root is local.
|
|
23
|
+
/plans/
|
|
24
|
+
# docs/ is TRACKED on purpose. It previously carried a blanket `/docs/` ignore so
|
|
25
|
+
# new docs stayed local, but that made `git add docs/...` exit 0 while doing
|
|
26
|
+
# nothing — a doc describing shipped behavior could silently diverge from the code
|
|
27
|
+
# it documents, which is exactly what happened to AUTOMODE-ARCHITECTURE.md. Docs
|
|
28
|
+
# that describe behavior are part of the change that alters that behavior. Keep
|
|
29
|
+
# genuinely local scratch under docs/superpowers/ or /plans/ above.
|
|
30
|
+
|
|
31
|
+
# goreleaser build output
|
|
32
|
+
/dist/
|
|
33
|
+
|
|
34
|
+
# Python SDK build artifacts. _bin/ is load-bearing: build_wheels.py drops a
|
|
35
|
+
# ~40MB prx binary there before each wheel build, and committing it would put a
|
|
36
|
+
# platform-specific binary in the source tree.
|
|
37
|
+
python/src/praxis_agent_sdk/_bin/
|
|
38
|
+
python/dist/
|
|
39
|
+
python/.venv/
|
|
40
|
+
python/.pytest_cache/
|
|
41
|
+
__pycache__/
|
|
42
|
+
*.py[cod]
|
|
43
|
+
.mypy_cache/
|
|
44
|
+
.ruff_cache/
|
|
45
|
+
# uv.lock is deliberately NOT committed: the package has zero runtime
|
|
46
|
+
# dependencies, so the lock pins only dev tools, and CI installs with pip and never
|
|
47
|
+
# reads it. A lockfile nothing consumes is a file that silently goes stale.
|
|
48
|
+
python/uv.lock
|
|
49
|
+
# Generated per wheel build by scripts/build_wheels.py: it names the prx version
|
|
50
|
+
# that build bundled. Committing it would make it a lie as soon as the binary is
|
|
51
|
+
# rebuilt, and the SDK treats its absence as "no bundled binary".
|
|
52
|
+
python/src/praxis_agent_sdk/_bundled.py
|
|
53
|
+
|
|
54
|
+
# Intermediate FileDescriptorSet from `make cursorpb` (agent.desc is the source of truth).
|
|
55
|
+
ai/cursorpb/agent.fdset
|
|
56
|
+
|
|
57
|
+
# Serena agent tooling state (local cache/config, not source)
|
|
58
|
+
.serena/
|
|
59
|
+
node_modules
|
|
60
|
+
# TypeScript SDK build output (regenerated by `npm run build`).
|
|
61
|
+
typescript/dist/
|
|
62
|
+
typescript/*.tgz
|
|
63
|
+
dist/
|
|
64
|
+
|
|
65
|
+
# Local prx builds (go build ./cmd/prx at the repo root).
|
|
66
|
+
/prx
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: praxis-agent-sdk
|
|
3
|
+
Version: 0.11.0
|
|
4
|
+
Summary: Python SDK for the Praxis coding-agent harness — one agent API across every model and provider
|
|
5
|
+
Project-URL: Homepage, https://github.com/Facets-cloud/praxis-harness
|
|
6
|
+
Project-URL: Source, https://github.com/Facets-cloud/praxis-harness/tree/master/python
|
|
7
|
+
Project-URL: Issues, https://github.com/Facets-cloud/praxis-harness/issues
|
|
8
|
+
Author: Facets Cloud
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: agent,anthropic,bedrock,coding-agent,gemini,llm,openai,vertex
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
26
|
+
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# praxis-agent-sdk
|
|
30
|
+
|
|
31
|
+
Python SDK for the [Praxis](https://github.com/Facets-cloud/praxis-harness)
|
|
32
|
+
coding-agent harness. One agent API, **every model and provider** the harness
|
|
33
|
+
supports — Anthropic, OpenAI (including Codex), Google Gemini, Vertex, AWS
|
|
34
|
+
Bedrock, Z.AI, and anything else in its registry — with real tools, subagents,
|
|
35
|
+
sessions, MCP, skills, and automatic context compaction.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# the SDK, straight from the repo
|
|
39
|
+
pip install "git+https://github.com/Facets-cloud/praxis-harness.git@master#subdirectory=python"
|
|
40
|
+
# plus the binary it drives (a git install does not carry one — see below)
|
|
41
|
+
GOPRIVATE=github.com/Facets-cloud go install github.com/Facets-cloud/praxis-harness/cmd/prx@latest
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
There is **no Node.js requirement**, no `npm install -g`, and no separate CLI to
|
|
45
|
+
keep in version sync. Zero runtime dependencies, so it cannot conflict with your
|
|
46
|
+
service's tree.
|
|
47
|
+
|
|
48
|
+
## Interactive login
|
|
49
|
+
|
|
50
|
+
The SDK can drive the same local login flow as the TUI. Discovery exposes the
|
|
51
|
+
currently selectable providers and the providers intentionally marked as coming
|
|
52
|
+
soon; it never silently initiates a disabled provider.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from praxis_agent_sdk import PraxisClient, PraxisOptions
|
|
56
|
+
|
|
57
|
+
async with PraxisClient(PraxisOptions()) as agent:
|
|
58
|
+
providers = await agent.get_login_providers()
|
|
59
|
+
assert not next(p for p in providers if p.id == "anthropic").coming_soon
|
|
60
|
+
|
|
61
|
+
challenge = await agent.login_challenge("anthropic")
|
|
62
|
+
# Open challenge.auth_url, or collect the API key / redirect URL described
|
|
63
|
+
# by challenge.prompt_message, then keep the opaque challenge unchanged.
|
|
64
|
+
completion_value = input(challenge.prompt_message)
|
|
65
|
+
result = await agent.complete_login(challenge, completion_value)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`LoginChallenge.verifier` and `.state` are OAuth continuation values. They are
|
|
69
|
+
hidden from `repr`, but `complete_login()` needs the original object; do not
|
|
70
|
+
reconstruct it from display fields. A coming-soon provider is returned by
|
|
71
|
+
`get_login_providers()` with `coming_soon=True` and is rejected by both login
|
|
72
|
+
methods.
|
|
73
|
+
|
|
74
|
+
## Installing
|
|
75
|
+
|
|
76
|
+
### From git — supported, and what to use today
|
|
77
|
+
|
|
78
|
+
The package lives in the `python/` subdirectory of the harness repo, so every
|
|
79
|
+
installer needs to be told that.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# pip
|
|
83
|
+
pip install "git+https://github.com/Facets-cloud/praxis-harness.git@master#subdirectory=python"
|
|
84
|
+
pip install "git+ssh://git@github.com/Facets-cloud/praxis-harness.git@master#subdirectory=python"
|
|
85
|
+
|
|
86
|
+
# uv
|
|
87
|
+
uv add praxis-agent-sdk --git https://github.com/Facets-cloud/praxis-harness --subdirectory python
|
|
88
|
+
|
|
89
|
+
# a local checkout, editable
|
|
90
|
+
pip install -e /path/to/praxis-harness/python
|
|
91
|
+
uv add --editable /path/to/praxis-harness/python
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Pin a release rather than tracking `master` in anything you deploy — replace
|
|
95
|
+
`@master` with a tag or commit, e.g. `@python-v0.2.0`.
|
|
96
|
+
|
|
97
|
+
**A git or path install does NOT include the binary.** Nothing compiles Go during a
|
|
98
|
+
Python install, and `_bin/` is gitignored on purpose so a platform-specific binary
|
|
99
|
+
never enters the source tree. `bundled_prx_version()` returns `None` there, which is
|
|
100
|
+
the honest answer. Supply `prx` by any one of:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# build it from the same checkout
|
|
104
|
+
cd /path/to/praxis-harness && make build && export PRAXIS_BIN=$PWD/bin/prx
|
|
105
|
+
|
|
106
|
+
# or install it with Homebrew (macOS, Linux)
|
|
107
|
+
brew install facets-cloud/tap/prx
|
|
108
|
+
|
|
109
|
+
# or install it with Go
|
|
110
|
+
GOPRIVATE=github.com/Facets-cloud go install github.com/Facets-cloud/praxis-harness/cmd/prx@latest
|
|
111
|
+
|
|
112
|
+
# or take a release archive, then put prx on PATH
|
|
113
|
+
# https://github.com/Facets-cloud/praxis-harness-releases/releases
|
|
114
|
+
|
|
115
|
+
# or point at it per session, no environment needed
|
|
116
|
+
python -c "from praxis_agent_sdk import PraxisOptions; PraxisOptions(executable='/path/to/prx')"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If `prx` cannot be found, the error names every one of these fixes rather than
|
|
120
|
+
saying "not found".
|
|
121
|
+
|
|
122
|
+
### From a release wheel — binary included, no PyPI needed
|
|
123
|
+
|
|
124
|
+
Every `python-v*` tag attaches platform wheels to its GitHub release, and a wheel
|
|
125
|
+
bundles the matching `prx`. That is strictly better than a git install for a
|
|
126
|
+
consumer, because nothing else has to be supplied:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
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
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Pick the wheel matching your platform from
|
|
133
|
+
<https://github.com/Facets-cloud/praxis-harness-releases/releases>. `bundled_prx_version()`
|
|
134
|
+
then reports the exact `prx` it carries.
|
|
135
|
+
|
|
136
|
+
### From PyPI
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
uv add praxis-agent-sdk # uv project
|
|
140
|
+
uv pip install praxis-agent-sdk # uv, pip-style
|
|
141
|
+
pip install praxis-agent-sdk # pip
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Platform wheels for macOS (arm64, x86_64) and Linux (x86_64, aarch64) each bundle
|
|
145
|
+
the matching `prx`, so pip/uv picks the right one and you need nothing else.
|
|
146
|
+
Releases from 0.11.0 on are published to PyPI; for an older version use a release
|
|
147
|
+
wheel or the git install above.
|
|
148
|
+
|
|
149
|
+
Resolution order: `PraxisOptions.executable` → `$PRAXIS_BIN` → the wheel's bundled
|
|
150
|
+
binary → `prx` on `$PATH`. Bundled beats `$PATH` deliberately — a wheel pins a
|
|
151
|
+
binary it was tested against, and silently preferring a stray `prx` makes version
|
|
152
|
+
skew invisible.
|
|
153
|
+
|
|
154
|
+
### Adding it to a `uv` service
|
|
155
|
+
|
|
156
|
+
```toml
|
|
157
|
+
# pyproject.toml
|
|
158
|
+
dependencies = ["praxis-agent-sdk>=0.2.0"]
|
|
159
|
+
|
|
160
|
+
# while unpublished, pin the source:
|
|
161
|
+
[tool.uv.sources]
|
|
162
|
+
praxis-agent-sdk = { git = "https://github.com/Facets-cloud/praxis-harness", subdirectory = "python" }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
uv lock && uv sync
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Then swap the Node layer for the binary in your image:
|
|
170
|
+
|
|
171
|
+
```dockerfile
|
|
172
|
+
# was: RUN npm install -g @anthropic-ai/claude-code
|
|
173
|
+
COPY --from=praxis /usr/local/bin/prx /usr/local/bin/prx
|
|
174
|
+
ENV PRAXIS_BIN=/usr/local/bin/prx
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The SDK's floor is Python 3.10 so it never forces a consumer's floor upward;
|
|
178
|
+
`uv lock`/`uv sync`/`uv run` are verified against 3.14.
|
|
179
|
+
|
|
180
|
+
## Quick start
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
import asyncio
|
|
184
|
+
from praxis_agent_sdk import PraxisOptions, query, AssistantMessage
|
|
185
|
+
|
|
186
|
+
async def main():
|
|
187
|
+
async for message in query(
|
|
188
|
+
"read README.md and summarise it in three bullets",
|
|
189
|
+
PraxisOptions(model="claude-opus-5", cwd="/path/to/repo"),
|
|
190
|
+
):
|
|
191
|
+
if isinstance(message, AssistantMessage):
|
|
192
|
+
print(message.text)
|
|
193
|
+
|
|
194
|
+
asyncio.run(main())
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Change one string to change provider — the model id resolves its own provider:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
PraxisOptions(model="gpt-5.6-terra") # OpenAI
|
|
201
|
+
PraxisOptions(model="gemini-2.5-pro") # Google
|
|
202
|
+
PraxisOptions(model="claude-opus-5") # Anthropic
|
|
203
|
+
PraxisOptions(model="glm-4.6") # Z.AI
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Credentials
|
|
207
|
+
|
|
208
|
+
**Nothing to configure.** If this machine is logged in — `prx login`, or the TUI's
|
|
209
|
+
provider picker — the agent authenticates from that login, whichever kind it is:
|
|
210
|
+
OAuth (a Claude or ChatGPT subscription) or an API key, resolved per provider from
|
|
211
|
+
the model you asked for. Provider keys in the environment (`ANTHROPIC_API_KEY`,
|
|
212
|
+
`OPENAI_CODEX_API_KEY`, …) keep working too; the child inherits your environment.
|
|
213
|
+
|
|
214
|
+
What the SDK does for you is keep that login reachable when your process gives the
|
|
215
|
+
agent a different `HOME`:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
# An isolated runtime: its own HOME, its own sessions, its own settings.
|
|
219
|
+
PraxisOptions(env={"HOME": str(runtime / "home")}, session_dir=str(runtime / "sessions"))
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Praxis reads credentials from `$HOME/.praxis/agent/auth.json`, so a re-homed child
|
|
223
|
+
would resolve an empty store and fail to start with *no credential for provider* —
|
|
224
|
+
even though you are logged in. The SDK detects that the child would look somewhere
|
|
225
|
+
else and pins `PRAXIS_CODING_AGENT_AUTH_FILE` at the store this process uses. Only
|
|
226
|
+
the credential store is shared: sessions, skills, memories, and analytics stay under
|
|
227
|
+
the `HOME` you chose.
|
|
228
|
+
|
|
229
|
+
What travels is the store, never a token the SDK copied out of it — that is what
|
|
230
|
+
keeps OAuth working, since the harness still holds the refresh token, the account
|
|
231
|
+
id, and the credential type that decides a subscription's context window.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# Authenticate from a specific store (a mounted secret, a service account's login).
|
|
235
|
+
PraxisOptions(credentials="/run/secrets/praxis-auth.json")
|
|
236
|
+
|
|
237
|
+
# Or opt out and let the child resolve its own credentials from the env you gave it.
|
|
238
|
+
PraxisOptions(env={"HOME": sandbox}, inherit_credentials=False)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## A live session
|
|
242
|
+
|
|
243
|
+
`query()` is one prompt. `PraxisClient` is a conversation you can steer.
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from praxis_agent_sdk import PraxisClient, PraxisOptions, AssistantMessage
|
|
247
|
+
|
|
248
|
+
async with PraxisClient(PraxisOptions(model="claude-opus-5", cwd=repo)) as agent:
|
|
249
|
+
async for msg in agent.prompt("find the retry logic and explain it"):
|
|
250
|
+
if isinstance(msg, AssistantMessage):
|
|
251
|
+
print(msg.text)
|
|
252
|
+
|
|
253
|
+
# Mid-turn course correction — the agent picks this up at its next tool
|
|
254
|
+
# boundary instead of you interrupting and restating the whole task.
|
|
255
|
+
await agent.steer("only look under internal/, skip the tests")
|
|
256
|
+
|
|
257
|
+
await agent.set_model("gpt-5.6-terra") # switch models, keep history
|
|
258
|
+
usage, cost_usd = await agent.usage()
|
|
259
|
+
print(f"{usage.billed} tokens, ${cost_usd:.4f}")
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Exact plan approval
|
|
263
|
+
|
|
264
|
+
Plan approval is bound to the proposal you reviewed. Enter plan mode, propose a
|
|
265
|
+
plan, fetch that proposal by its opaque id, then pass the same id to approval.
|
|
266
|
+
A proposal that changed, expired, or was never reviewed is rejected.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
async with PraxisClient(PraxisOptions(permission_mode="plan")) as agent:
|
|
270
|
+
await agent.set_plan_mode(True)
|
|
271
|
+
proposal = await agent.propose_plan()
|
|
272
|
+
proposal_id = proposal["proposalId"]
|
|
273
|
+
content = await agent.get_plan_proposal(proposal_id)
|
|
274
|
+
print(content)
|
|
275
|
+
await agent.approve_plan("ship", proposal_id)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## MCP server configuration
|
|
279
|
+
|
|
280
|
+
Use `persist_mcp_server()` to save a server for a later session without starting a
|
|
281
|
+
process now. Use `add_mcp_server()` when the current conversation needs it
|
|
282
|
+
immediately; it connects the server and persists the same configuration.
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
async with PraxisClient(PraxisOptions()) as agent:
|
|
286
|
+
path = await agent.persist_mcp_server("docs", "npx", "-y", "docs-mcp-server")
|
|
287
|
+
print(f"saved MCP configuration in {path}")
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Tools that run in your process
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
from praxis_agent_sdk import PraxisOptions, PraxisClient, tool
|
|
294
|
+
|
|
295
|
+
@tool("get_deployment", "Look up a deployment by id", tier="read")
|
|
296
|
+
async def get_deployment(deployment_id: str) -> str:
|
|
297
|
+
return await api.fetch(deployment_id)
|
|
298
|
+
|
|
299
|
+
@tool("scale_service", "Change a service's replica count", tier="write")
|
|
300
|
+
async def scale_service(service: str, replicas: int) -> str:
|
|
301
|
+
await api.scale(service, replicas)
|
|
302
|
+
return f"scaled {service} to {replicas}"
|
|
303
|
+
|
|
304
|
+
options = PraxisOptions(model="claude-opus-5", tools=[get_deployment, scale_service])
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`tier` is load-bearing:
|
|
308
|
+
|
|
309
|
+
| tier | meaning | approval |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| `read` | no side effects | auto-approved, never prompts |
|
|
312
|
+
| `write` | mutates something | needs a permission decision |
|
|
313
|
+
| `exec` | runs commands | needs a permission decision |
|
|
314
|
+
|
|
315
|
+
A `write`/`exec` tool without a `can_use_tool` callback is **denied**, not
|
|
316
|
+
stalled — the harness resolves the prompt locally rather than blocking a turn on
|
|
317
|
+
an answer that is not coming.
|
|
318
|
+
|
|
319
|
+
## Permissions
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
from praxis_agent_sdk import PermissionDecision, PraxisOptions
|
|
323
|
+
|
|
324
|
+
async def can_use_tool(request):
|
|
325
|
+
if request.tool_name == "bash" and request.destructive:
|
|
326
|
+
return PermissionDecision(allow=False, reason="no destructive shell in prod")
|
|
327
|
+
return PermissionDecision(allow=True)
|
|
328
|
+
|
|
329
|
+
PraxisOptions(model="claude-opus-5", permission_mode="ask", can_use_tool=can_use_tool)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Permission modes: `auto` (default — classifier-gated, reads free), `ask`
|
|
333
|
+
(everything non-read prompts), `yolo` (allow, minus deterministic red lines),
|
|
334
|
+
`plan` (read-only planning).
|
|
335
|
+
|
|
336
|
+
## Migrating from `claude-agent-sdk`
|
|
337
|
+
|
|
338
|
+
| Claude Agent SDK | Praxis SDK |
|
|
339
|
+
|---|---|
|
|
340
|
+
| `ClaudeAgentOptions` | `PraxisOptions` |
|
|
341
|
+
| `ClaudeSDKClient` | `PraxisClient` |
|
|
342
|
+
| `query(prompt, options)` | `query(prompt, options)` |
|
|
343
|
+
| `@tool(name, desc, schema)` | `@tool(name, desc, schema, tier=...)` |
|
|
344
|
+
| `create_sdk_mcp_server(...)` | deleted — pass `tools=[...]` |
|
|
345
|
+
| `options.mcp_servers={"x": server}` | `tools=[...]` for in-process; `mcp_config=` for real MCP servers |
|
|
346
|
+
| `can_use_tool` → `PermissionResultAllow/Deny` | `can_use_tool` → `PermissionDecision(allow=...)` |
|
|
347
|
+
| `options.model` (Anthropic only) | `options.model` (any provider) |
|
|
348
|
+
| `options.max_turns` | `options.max_turns` |
|
|
349
|
+
| `options.permission_mode` | `options.permission_mode` |
|
|
350
|
+
| `options.setting_sources` | `options.settings`, `no_rules`, `no_skills` |
|
|
351
|
+
| `ResultMessage.total_cost_usd` | `ResultMessage`, or `await client.usage()` |
|
|
352
|
+
| requires Node + `@anthropic-ai/claude-code` | nothing — the binary ships in the wheel |
|
|
353
|
+
|
|
354
|
+
The full port guide, including the wire protocol, is in
|
|
355
|
+
[`docs/design/PYTHON-SDK.md`](../docs/design/PYTHON-SDK.md).
|
|
356
|
+
|
|
357
|
+
## Using a specific binary
|
|
358
|
+
|
|
359
|
+
Resolution order: `PraxisOptions.executable` → `$PRAXIS_BIN` → the wheel's
|
|
360
|
+
bundled binary → `prx` on `$PATH`.
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
PRAXIS_BIN=/usr/local/bin/prx python app.py
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Development
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
cd .. && make build # do this FIRST: the contract tests pin ./bin/prx
|
|
370
|
+
cd python
|
|
371
|
+
uv venv && uv pip install -e '.[dev]' # or python -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
372
|
+
uv run pytest # whole suite, ~16s (contract tests drive the real binary)
|
|
373
|
+
uv run pytest -v --durations=0 # per-test progress; use this if a run looks stuck
|
|
374
|
+
uv run ruff check . && uv run mypy src/praxis_agent_sdk
|
|
375
|
+
uv build # sdist + wheel
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The test suite spawns a real subprocess speaking the real protocol, so framing
|
|
379
|
+
and correlation bugs fail the tests rather than reaching production.
|
|
380
|
+
`tests/test_contract.py` additionally drives the REAL binary, which is what stops
|
|
381
|
+
the Go and Python halves of the protocol drifting.
|
|
382
|
+
|
|
383
|
+
Three things about running it:
|
|
384
|
+
|
|
385
|
+
- **The contract tests use `../bin/prx` and nothing else.** They deliberately do
|
|
386
|
+
NOT fall back to a `prx` on `$PATH`: a stale binary answers a newly added command
|
|
387
|
+
with `unknown command: X`, which reads like a protocol bug rather than a missing
|
|
388
|
+
`make build`. Without the built binary they skip and say so. `PRAXIS_CONTRACT_BIN`
|
|
389
|
+
overrides the path for CI layouts that build elsewhere.
|
|
390
|
+
- **No test may hang.** `pytest-timeout` caps every test at 60s and dumps the stack
|
|
391
|
+
of all threads, because a hang blocks the whole suite and `-q` prints its summary
|
|
392
|
+
only at the end — so a slow suite is indistinguishable from a stuck one.
|
|
393
|
+
- **`PRAXIS_TEST_BUILD=1` opts into the packaging build test**, which provisions an
|
|
394
|
+
isolated environment and needs the network. It is skipped by default so the suite
|
|
395
|
+
stays hermetic.
|
|
396
|
+
|
|
397
|
+
Runnable examples for every capability are in [`examples/`](examples/README.md).
|