FE-1270: Extract Python bindings for the Petrinaut CLI into @local/petrinaut-python - #9228
Open
kube wants to merge 1 commit into
Open
FE-1270: Extract Python bindings for the Petrinaut CLI into @local/petrinaut-python#9228kube wants to merge 1 commit into
kube wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
kube
force-pushed
the
cf/fe-1270-create-python-bindings-to-petrinaut-core
branch
from
August 17, 2026 23:51
c1e9f74 to
a2a4545
Compare
kube
force-pushed
the
cf/fe-1270-create-python-bindings-to-petrinaut-core
branch
from
August 17, 2026 23:56
a2a4545 to
cfbbb60
Compare
kube
marked this pull request as ready for review
August 17, 2026 23:56
PR SummaryCursor Bugbot is generating a summary for commit cfbbb60. Configure here. |
kube
force-pushed
the
cf/fe-1270-create-python-bindings-to-petrinaut-core
branch
from
August 18, 2026 02:01
8b047b8 to
7176be8
Compare
kube
force-pushed
the
cf/fe-1270-create-python-bindings-to-petrinaut-core
branch
from
August 18, 2026 15:18
7176be8 to
14efab7
Compare
kube
force-pushed
the
cf/fe-1270-create-python-bindings-to-petrinaut-core
branch
from
August 18, 2026 22:48
14efab7 to
4fe418d
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 31 changed files in this pull request and generated no new comments.
Suppressed comments (1)
libs/@hashintel/petrinaut-cli/README.md:15
- The architecture check described by this PR does not currently cover this new relationship.
libs/@local/petrinaut-arch-docs/architecture.config.ts:34-57registers only core, editor, and CLI;content/index.mdx:78-80still saysapps/petrinaut-optis outside the generated model; andlibs/@hashintel/petrinaut-cli/src/README.md:9-12still says the optimizer directly spawns the CLI. Consequently the generated docs remain stale and nooptimizer → python-bindingsedge is enforced. Register the optimizer and bindings with layer declarations and update the stale architecture prose as part of this extraction.
The [`@local/petrinaut-python`](../../@local/petrinaut-python/README.md)
bindings drive this CLI from Python: sessions, run requests, and optimization
studies.
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 31 changed files in this pull request and generated no new comments.
Suppressed comments (1)
libs/@local/petrinaut-python/src/petrinaut/optimization.py:14
- The stated architecture check cannot currently cover this new boundary:
libs/@local/petrinaut-arch-docs/architecture.config.ts:35-57registers only the core, editor, and CLI packages, whilesrc/extract.ts:158-165explicitly rejects Python packages because no extractor exists. As a result, the claimedoptimizer → python-bindingsedge is never generated or enforced, so a direct optimizer-to-CLI dependency would pass the architecture check. Add Python extraction/registration for both this package andapps/petrinaut-opt(with their layer declarations and graph wiring), or remove the claim and track that enforcement separately.
class OptimizationSession(PetrinautSession):
Contributor
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | Flame Graph |
representative_read_entity
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |
Adds libs/@local/petrinaut-python: a stdlib-only, POSIX-only package wrapping the CLI's JSON-lines protocol. PetrinautSession serves a model (from a file or over stdin) with healthz/metadata/run plus a generic request method; OptimizationSession adds describe/evaluate/objective over an opaque manifest, and evaluate returns per-seed replicates when the manifest asks for more than one seed. The code comes from petrinaut-opt's client: spawn scrubbing, bounded reads, and process-group shutdown are unchanged. petrinaut-opt consumes the package as an editable uv path dependency, mirrored as a workspace:* edge so Turbo pruning and change detection see it; its codegen inputs, the Docker python-deps/runner stages, and the deploy path filter follow. The old src/petrinaut_client.py, the PYTHON_INTEGRATION.md walkthrough, and examples/python_stdio.py are retired, and MODEL_EXAMPLES.md now uses the bindings. Two end-to-end pytests drive the real built CLI bundle, one per session type.
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 31 changed files in this pull request and generated no new comments.
Suppressed comments (2)
libs/@local/petrinaut-python/src/petrinaut/session.py:328
PetrinautClientErrormeans the process/transport is unusable (errors.py:6-7), andrequest()explicitly promises that this exception closes the session (lines 204-207). This branch deliberately leaves the session healthy, so callers cannot use the documented exception type to decide whether the session can be reused. Treat non-serializable params as caller input—consistent with the constructor’sValueErrors—rather than as a transport failure.
libs/@local/petrinaut-python/README.md:79- The uppercase constants are defaults, not constructor option names. A caller following this text and passing
BOOTSTRAP_TIMEOUT_SECONDS=will get an unexpected-keywordTypeError; namebootstrap_timeout_secondsandrequest_timeout_secondsas the options and identify the constants as their defaults.
- Bootstrap (spawn to readiness) and each protocol response have deadlines,
`BOOTSTRAP_TIMEOUT_SECONDS` and `PROTOCOL_READ_TIMEOUT_SECONDS`. Both are
constructor options.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🌟 What is the purpose of this PR?
Using Petrinaut from Python meant copying a subprocess wrapper out of the CLI's docs.
apps/petrinaut-optcarried its own client, which covered two of the CLI's five protocol methods and was private to that service. This PR extracts a package both can use.Stack #9226: FE-1410, FE-1411, FE-1408, and FE-1413 have merged, so this PR now sits on
mainand is the bottom of the stack. FE-1412 (#9229) is above it.🔗 Related links
🔍 What does this change?
New package
libs/@local/petrinaut-python(import namepetrinaut), stdlib-only and POSIX-only, internal to the monorepo:PetrinautSessionserves one model per process, from a file or over stdin, and exposeshealthz(),metadata(),run(), and arequest()method for any protocol method.OptimizationSessiondrives a study over a manifest the bindings treat as opaque JSON:describe_optimization(),objective(parameter_values), andevaluate(parameter_values).evaluatereturns the full result, so it carriesreplicateswhenever the manifest setsexecution.seedsPerTrialabove 1.PetrinautSession,OptimizationSession,PetrinautClientError,PetrinautProtocolError,PetrinautRunError.petrinaut-opt's client with one change: the read loop's three copies of the line-cap and UTF-8 decode blocks collapse into one local helper. A scrubbed child environment, bounded reads with deadlines, and process-group shutdown all behave as before.petrinaut-opt no longer knows what it is calling
The service depends on the bindings and nothing else. It never names the CLI, in code, tests, docs, configuration, or the image, so replacing what the bindings spawn touches only the bindings.
apps/petrinaut-optlose 928 lines and gain 132. The deleted client and its test are 775 of those, and the rest is the CLI leaving the service's vocabulary: comments, docstrings, two responsedetailstrings, and the architecture-docs@role.create_model()is inlined into its only caller. Its docstring called it a seam for API tests, but every test patchesinitialize_optimizerinstead, so nothing used it.CLI protocolsection documented the JSON-lines exchange, which the bindings own and the CLI's own manual covers; what remains describes the four session calls this service makes and what it does with the description.@hashintel/petrinaut-climoves from the bindings'devDependenciesto theirdependencies, which is what it always was: they spawn it. That is what lets the image stop naming it.@local/petrinaut-python, builds--filter '@local/petrinaut-python...', and takes thepetrinautexecutable from the closure's ownnode_modules/.bin. The bindings give their child a fixedPATH, so it is linked into/usr/local/bin.uv sync --no-editablebuilds the bindings into the virtualenv, so the runner copies the virtualenv alone. The editable install's requirement that the source sit at a matching absolute path in two stages is gone.NODE_OPTIONSnever reached the child, because the bindings scrub the environment, and the passthrough is renamedPETRINAUT_CHILD_NODE_OPTIONSso it names the bindings' child rather than a package.optimizerlayer with one edge,optimizer → python-bindings, and none tocli. At this layer the decoupling is a code fact, not yet a checked one.Review pass
initialize_optimizernow closes withgraceful=False, which is what the bindings' ownclose()contract requires of failure paths. It previously waited out the graceful EOF while holding the admission slot.TypeErrorescaped the documented exception types entirely.failed to communicate with the Petrinaut CLI, and other transport failures name the underlying error type. A stalled run and a broken pipe were previously indistinguishable in the logs.OptimizationSession.from_model_file(...)used to raiseValueError: provide exactly one of optimization_manifest or manifest_path, because the inherited constructor routed throughcls. The model constructors now return aPetrinautSession.COPY /repo/libswas carrying 227 petrinaut-core source files, the CLI's sources and test fixtures, and the bindings' tests into/opt/petrinaut-runtime, all readable to the sandboxed child that evaluates user-authored metric expressions.source_labelarguments, three hardcoded8 MiBstrings that could drift from the constants they describe, two section banners, unreadterminated/killedflags on the test double, one tautological test, and duplicated prose across the two READMEs. The bindings README now names the timeout and cap constants rather than quoting their values, states that a session serves one request at a time, and says how to put the executable onPATH.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
@local/petrinaut-pythonis private and consumed inside the monorepo.📜 Does this require a change to the docs?
The changes in this PR:
libs/@hashintel/petrinaut-cli/README.md. The CLI usage manual keeps the stdlib wrapper it gained in FE-1413; FE-1456 points it at the bindings once their own manual exists. In-app user-guide behaviour is unchanged.🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
turbo.json's have been updated to reflect thistest:unitdepends on the CLI'sbuild, since the end-to-end tests spawn the bundle.petrinaut-opt's codegen inputs include the bindings sources, narrowed to*.pyso bytecode does not change the cache key.🛡 What tests cover this?
from_model_fileandfrom_model,healthz/metadata/run, error frames and protocol errors, timeouts and shutdown, and the optimizer client suite that moved here. Every test from the deletedtest_petrinaut_client.pyis present; the one dropped test asserted that two private attributes equalled the constants they default to.PetrinautSessionrun of the SIR model, and oneOptimizationSessionstudy over the checked-in supply-chain manifest. They skip when the bundle ornodeis missing. FE-1412 adds a seeded study.apps/petrinaut-opt: 75 tests pass against the package.❓ How to test this?
turbo run test:unit --filter @local/petrinaut-pythoncd apps/petrinaut-opt && uv sync && uv run pytestuv run python -c "from petrinaut import OptimizationSession, PetrinautSession"🐾 Next steps
FE-1412 scales the client's per-response deadline by the seeds a study describes.
🤖 Generated with Claude Code