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.
Files changed (41) hide show
  1. praxis_agent_sdk-0.11.0/.gitignore +66 -0
  2. praxis_agent_sdk-0.11.0/PKG-INFO +397 -0
  3. praxis_agent_sdk-0.11.0/README.md +369 -0
  4. praxis_agent_sdk-0.11.0/examples/01_quickstart.py +62 -0
  5. praxis_agent_sdk-0.11.0/examples/02_streaming.py +87 -0
  6. praxis_agent_sdk-0.11.0/examples/03_client_tools.py +153 -0
  7. praxis_agent_sdk-0.11.0/examples/04_permissions.py +124 -0
  8. praxis_agent_sdk-0.11.0/examples/05_structured_output.py +191 -0
  9. praxis_agent_sdk-0.11.0/examples/06_multi_provider.py +148 -0
  10. praxis_agent_sdk-0.11.0/examples/07_sessions_and_fork.py +114 -0
  11. praxis_agent_sdk-0.11.0/examples/08_steering_and_interrupt.py +150 -0
  12. praxis_agent_sdk-0.11.0/examples/09_subagents.py +126 -0
  13. praxis_agent_sdk-0.11.0/examples/10_concurrent_sessions.py +142 -0
  14. praxis_agent_sdk-0.11.0/examples/11_budgets_and_cost.py +155 -0
  15. praxis_agent_sdk-0.11.0/examples/12_error_handling.py +147 -0
  16. praxis_agent_sdk-0.11.0/examples/13_fastapi_service.py +317 -0
  17. praxis_agent_sdk-0.11.0/examples/14_hooks_and_settings.py +179 -0
  18. praxis_agent_sdk-0.11.0/examples/15_skills.py +182 -0
  19. praxis_agent_sdk-0.11.0/examples/16_mcp_servers.py +215 -0
  20. praxis_agent_sdk-0.11.0/examples/17_sandbox_and_scopes.py +215 -0
  21. praxis_agent_sdk-0.11.0/examples/18_jobs_todos_queue.py +312 -0
  22. praxis_agent_sdk-0.11.0/examples/README.md +62 -0
  23. praxis_agent_sdk-0.11.0/examples/agent_factory_migration.py +355 -0
  24. praxis_agent_sdk-0.11.0/examples/smoke.py +108 -0
  25. praxis_agent_sdk-0.11.0/pyproject.toml +92 -0
  26. praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/__init__.py +131 -0
  27. praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/_bundled.py +3 -0
  28. praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/_internal/__init__.py +1 -0
  29. praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/_internal/transport.py +669 -0
  30. praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/client.py +1180 -0
  31. praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/errors.py +54 -0
  32. praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/py.typed +0 -0
  33. praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/tools.py +144 -0
  34. praxis_agent_sdk-0.11.0/src/praxis_agent_sdk/types.py +545 -0
  35. praxis_agent_sdk-0.11.0/tests/fakeagent.py +169 -0
  36. praxis_agent_sdk-0.11.0/tests/sdk_contracts.py +68 -0
  37. praxis_agent_sdk-0.11.0/tests/test_client.py +765 -0
  38. praxis_agent_sdk-0.11.0/tests/test_contract.py +556 -0
  39. praxis_agent_sdk-0.11.0/tests/test_endpoint.py +401 -0
  40. praxis_agent_sdk-0.11.0/tests/test_packaging.py +322 -0
  41. 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).