Skip to content

FE-1468: Generate pydantic models from the CLI's protocol schemas - #9262

Merged
kube merged 5 commits into
mainfrom
cf/fe-1468-python-pydantic-models-from-cli-schemas
Aug 21, 2026
Merged

FE-1468: Generate pydantic models from the CLI's protocol schemas#9262
kube merged 5 commits into
mainfrom
cf/fe-1468-python-pydantic-models-from-cli-schemas

Conversation

@kube

@kube kube commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

The bindings returned dict[str, Any], and apps/petrinaut-opt read the study description field by field with nothing checking it; a protocol change first showed up as a failing study. This PR makes the protocol's response shapes one definition every language consumes: Zod schemas in petrinaut-core, published by the CLI as JSON Schema, generated into pydantic models for Python.

The repository already generates types across this boundary in the other direction (pydantic → openapi.json@local/petrinaut-optimizer-client). This is that pipeline reversed, with the same regenerate-and-diff guard.

Sits on FE-1467 (#9261) in stack #9269; FE-1412 (#9229) is above.

🔗 Related links

🔍 What does this change?

  • petrinaut-core: the response shapes become Zod schemas (petrinautOptimizationDescribeParameterSchema, …DescribeResultSchema, …ReplicateSchema, …EvaluateResultSchema). The existing types are now z.infer of them and are unchanged; 880 core and 39 CLI tests pass untouched. Patch changeset.
  • petrinaut-cli: a codegen task writes the checked-in schemas/optimization-protocol.schema.json, piped through oxfmt so formatting and regeneration agree. CI regenerates and diffs it.
  • petrinaut-python: codegen runs datamodel-codegen over that document into src/petrinaut/models.py. describe() returns OptimizationDescribeResult and evaluate() returns OptimizationEvaluateResult, validated in JSON-strict mode: a string where a number belongs is a contract bug and raises PetrinautProtocolError, while enums still parse from their wire strings. py.typed is added — without it no checker trusted the package's annotations. pydantic becomes the package's first runtime dependency.
  • petrinaut-opt: _parse_description keeps only the semantic rules a schema cannot express (bound ordering, log-scale domains, duplicates, study limits); the shape validation moves to the models. pydantic is declared directly instead of arriving through FastAPI.
  • Both Python packages move to typeCheckingMode = "strict". After the typed rewrite, strict reported 7 errors (41 on the old code); all fixed.

Review fixes. The describe response schema declares what the domain already guarantees: an int parameter's minimum/maximum/step are integers, and study.seedsPerTrial is bounded to the CLI's 1–100 range, so the generated models carry those constraints. PetrinautSession's docstring states the validation seam: model-protocol methods return the CLI's JSON verbatim as dicts, optimization methods return schema-validated models. The README example reads full.replicates by attribute, matching the model. The Docker image builds only the bindings' dependency closure (--filter '@local/petrinaut-python^...'): the bindings themselves have nothing to build in the image, and their codegen needs uv, which the Node-only builder stage lacks — this is what fixed the stack's Docker build.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an existing publishable library, and I have added a changeset. Patch: additive schema exports, derived types unchanged.

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR: the bindings README documents the models and the codegen guard.

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • affected the execution graph, and the turbo.json's have been updated to reflect this: petrinaut-cli#codegen (outputs schemas/**) and petrinaut-python#codegen (outputs models.py, depends on the CLI's codegen).

🛡 What tests cover this?

  • Core 880, CLI 39: unchanged, proving the type derivation is behaviour-compatible.
  • Bindings 24, including: a bool, null, or string objective is a schema violation that closes the session; an Infinity that Python's lax JSON parser admits stays a recoverable PetrinautRunError. The two e2e tests drive the built CLI through the typed methods.
  • Service 75. basedpyright strict and ruff pass in both packages. Regenerating twice produces no diff.

❓ How to test this?

  1. turbo run codegen --filter @local/petrinaut-python then git diff --exit-code
  2. Change a response schema in petrinaut-core/src/optimization.ts and rerun step 1: the diff is what Python consumers see.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Aug 21, 2026 12:17pm
petrinaut Ready Ready Preview Aug 21, 2026 12:17pm
petrinaut-docs Ready Ready Preview Aug 21, 2026 12:17pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Aug 21, 2026 12:17pm

@github-actions github-actions Bot added area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team type/eng > backend Owned by the @backend team area/tests New or updated tests area/apps labels Aug 21, 2026
@kube
kube marked this pull request as ready for review August 21, 2026 00:14
Copilot AI balanced review requested due to automatic review settings August 21, 2026 00:14
@cursor

cursor Bot commented Aug 21, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches the optimization protocol boundary and Python session APIs (dicts → validated models, schema-mismatch closes the session). Docker turbo filter and codegen graph also change, so a drift or filter mistake can break builds or studies.

Overview
Publishes the optimization protocol’s describe/evaluate response shapes as Zod schemas in petrinaut-core (existing TS types are z.infer and unchanged), then generates a checked-in JSON Schema from the CLI and pydantic models for Python.

OptimizationSession.describe() / evaluate() now return OptimizationDescribeResult / OptimizationEvaluateResult. A result that fails the schema raises PetrinautProtocolError and closes the session; non-finite objectives stay a recoverable PetrinautRunError. pydantic is the bindings’ first runtime dependency.

petrinaut-opt drops hand-rolled shape checks in _parse_description and keeps only semantic rules (bounds, log scale, duplicates, study limits). Both Python packages switch to strict typechecking. Docker builds the bindings’ dependency closure only (@local/petrinaut-python^...) so the Node stage does not run Python codegen.

Reviewed by Cursor Bugbot for commit d309df3. Bugbot is set up for automated code reviews on this repo. Configure here.

@kube kube changed the title cf/fe 1468 python pydantic models from cli schemas FE-1468: Generate pydantic models from the CLI's protocol schemas Aug 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread libs/@hashintel/petrinaut-core/src/optimization.ts

Copy link
Copy Markdown
Contributor

@kube looks great too - thanks! I will approve once we fix the cursor-raised issue.

Copilot AI review requested due to automatic review settings August 21, 2026 08:27
@kube
kube force-pushed the cf/fe-1468-python-pydantic-models-from-cli-schemas branch from ecae501 to 3fb37f9 Compare August 21, 2026 08:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3fb37f9. Configure here.

Comment thread apps/petrinaut-opt/src/petrinaut_optimizer.py
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.
kube added 4 commits August 21, 2026 13:58
The method sat on a class already named OptimizationSession, while its
siblings evaluate() and objective() carry no prefix. describe() maps 1:1
to the protocol's optimization.describe. The previous name remains as a
class-level alias. Raised in review on #9228.
Encoding failures happen before anything is written: params or a
bootstrap payload that will not serialize to JSON raise TypeError, and a
payload over the line cap raises ValueError. Both previously raised
PetrinautClientError, whose contract says the process or transport is no
longer usable — a caller treating it as terminal would discard a healthy
session. The request id is now consumed only after a successful encode,
so a rejected request leaves the session exactly as it was. Raised by
Cursor and in review on #9228.
ruff and basedpyright are configured in each package's pyproject.toml and pinned as uv dev dependencies, so an editor and CI resolve the same versions with no editor-specific files. New lint:ruff and lint:types tasks are wired into the per-package probe in lint.yml.
The optimization protocol's response shapes become Zod schemas in petrinaut-core; the CLI publishes them as one JSON Schema document; the bindings generate pydantic models from it and validate every describe/evaluate result. Both Python packages move to typeCheckingMode strict.
@kube
kube added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit 1b6c095 Aug 21, 2026
102 of 129 checks passed
Base automatically changed from cf/fe-1467-python-lint-and-type-check to main August 21, 2026 14:52
@vercel
vercel Bot requested a review from a team as a code owner August 21, 2026 14:52
@kube
kube deleted the cf/fe-1468-python-pydantic-models-from-cli-schemas branch August 21, 2026 14:52
@github-actions github-actions Bot added the type/legal Owned by the @legal team label Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > backend Owned by the @backend team type/eng > frontend Owned by the @frontend team type/legal Owned by the @legal team

Development

Successfully merging this pull request may close these issues.

3 participants