Skip to content

Adopt the powerio 0.11.3 names - #71

Open
samtalki wants to merge 20 commits into
Power-Agent:mainfrom
samtalki:feat/powerio-0113
Open

samtalki wants to merge 20 commits into
Power-Agent:mainfrom
samtalki:feat/powerio-0113

Conversation

@samtalki

Copy link
Copy Markdown
Member

Summary

  • Pin powerio[...]>=0.11.3,<0.12 everywhere (15 places), and give every test module that needs the 0.11.3 names the same version floor, so an older powerio skips instead of failing.
  • Read a module's structural type through PioModule.type_name and build diagnostic records with powerio.diagnostic_records, both public in powerio 0.11.3. The private attribute read and the two copies in powermcp/solver_case.py are gone; powermcp/tellegen.py and pandapower/panda_mcp.py use the powerio names.

Stacked on #70, which stacks on #67 and #64. Draft until powerio 0.11.3 is on PyPI: until then the pin cannot resolve and CI cannot install the package.

Testing

python -m pytest tests -q on CPython 3.12.12 from the branch root, against a wheel built from the powerio 0.11.3 release stack:

Environment Result
powerio 0.11.3 stack wheel, all four floored modules running 424 passed, 20 skipped
released powerio 0.11.2 302 passed, 18 skipped, the four floored modules skipped with the version message

🤖 Generated with Claude Code

https://claude.ai/code/session_01TbZRpXL3GhRRgXrccNDWka

samtalki and others added 20 commits September 15, 2026 22:55
`resolve_solver_case` resolves one file or one serialized PowerIO IR document
into a typed module, selects a collection entry by `time_index` or
`scenario_id`, and hands a writer the balanced network an operating point
states. `OperatingPoint.network` supplies that network directly, so no
MATPOWER round trip narrows the state to a representable subset.

`pio-ir` and `pio` are the format tokens PowerIO accepts; a document carrying
neither is refused on PowerIO's own terms. Diagnostics follow PowerIO's
severity vocabulary: `error` fails validation, `remark` and `note` stay out of
the user-facing warning list.

`check_diagnostics`, `operating_point_module` and `select_entry` are public
because a second server module calls them. `value_type_name` isolates the one
structural-type read PowerIO publishes no accessor for.

`powermcp.sandbox` re-exports `staged_file_write` alongside the rest of the
path policy and states powerio's containment default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pandapower, PyPSA, ANDES, Egret and surge accept `powerio_ir` as the primary
argument, keep `network_json` as its alias, and splice the shared response
tail: `value_type`, `selection`, `diagnostics`, `warnings`, plus the emission
fidelity and the typed-edit report for the adapters that convert into their
own model.

PowerIO 0.11.2 hands a directory writer a path that does not exist yet, so
the PyPSA CSV export and the surge table export create the staging directory
before writing into it. ANDES and Egret install their one emitted case file
through `staged_file_write`, so a write that fails part way leaves whatever
stands at the destination untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fourteen declarations now read `powerio[mcp,matrix]>=0.11.2,<0.12`. The floor
is 0.11.2 because 0.11.1 added `OperatingPoint.network` and matrix `row_ids`,
and 0.11.2 sets the path-containment default this repository documents.
`GenX/requirements.txt` gains a pin: `GenX/server.py` imports
`powermcp.sandbox`, which imports powerio.

Version 0.4.0. The sdist excludes local virtual environments and uv build
caches, and `.gitignore` covers `.venv-*/`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PowerIO 0.11.2 confines MCP paths to the directory the process started in when
no root variable names one. A session-scoped autouse fixture names the
repository checkout, pytest's temporary base and the system temporary
directory, so a test writing under `tmp_path` is admitted. A test that sets its
own roots still overrides it, because `allowed_roots()` reads the environment
on every call.

`_containment_status` distinguishes an explicit root list from the implicit
default, reports the default as a setting to make rather than a policy already
chosen, and reports the refusal PowerIO raises for a root variable naming no
directory. The doctor's user-visible strings carry no decorative characters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The powerio and solver-boundary tests require powerio 0.11.2, so the matrix
tools name their axes unconditionally and a retired Package document is
asserted for what it is: not PowerIO IR.

`tests/test_transport.py` states why it exists. The SDK parses a string
argument whose text is JSON before validation, so only a real stdio session
proves a `powerio_ir` argument reaches the tool intact.

`tests/test_sdk_imports.py` covers PLEXOSDB, the one server directory it
missed. `tests/test_publish_workflow.py` asserts the sdist exclude list keeps
local virtual environments and build caches out of a release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The PowerIO section describes the server shipped in 0.11.2, names the
arguments every adapter takes, and states the containment default: with no
root variable set, paths stay beneath the directory the server process
started in.

Per-server READMEs describe what the import tools accept, which is serialized
PowerIO IR or one selected entry of a collection. The retired `Package`,
`model-json`, `package_json` and package `study_commit` formats keep their
migration note: re-parse the original case and pass its `powerio_ir`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
stdout carries JSON-RPC once a server runs, so the ANDES startup notes go to
its logger, which writes to stderr, and the two PowerWorld error reports go to
stderr explicitly.

`GenX/server.py` keeps the repository root on `sys.path` for the process
lifetime and says why: GenX carries no `__init__.py`, so its namespace
`__path__` is recomputed from `sys.path` on every attribute lookup and the
lazy `GenX.tool_logic` imports would fail without it.

PLEXOSDB's notes on its shared import name say what the module is: a
re-export of the `plexosdb-mcp` server object under that same name. The
install picker's instruction line and the `powermcp` package docstring carry
no decorative characters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
powerio 0.11.2 ships a cp39-abi3 manylinux wheel serving Python 3.9 through
3.14, and the only other Rust dependency, surge-py, lives in an opt-in extra
CI never installs. Without the toolchain step a future wheel gap fails loudly
instead of building from an sdist.

The suite spawns roughly forty subprocesses per job with 60 second timeouts,
so stacked pushes on a busy runner turn a timeout into a failure. The
concurrency group cancels superseded pull request runs and leaves pushes to
main alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
powermcp/tellegen.py exchanges PowerIO IR with the tellegen binary over stdin
and stdout: the powerio server parses, Tellegen solves, and the solution comes
back as a PowerIO module every other tool reads. The tools are capabilities,
contract, solve, solve_module, plan and the Study family. Applying a Study
proposal binds a recommendation to the Study and stays a human action, so
study_run refuses it.

The binary resolves from POWERMCP_TELLEGEN_BINARY, then the tellegen.binary
config key, then PATH. A multiconductor value and a module PowerIO marks with
an error are both refused before the process starts. The tellegen extra is
empty because the CLI is a Rust binary rather than a pip package.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The binary is not a Python package, so the import probe says nothing about it.
The doctor runs the configured executable's capabilities subcommand and reports
green only when it exits zero and prints JSON. Output decodes as UTF-8 with
replacement so a non-UTF-8 console cannot raise UnicodeDecodeError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tests/data/fake_tellegen.py stands in for the compiled binary and speaks the
same JSON protocol, so CI needs no Rust toolchain. One test runs against a real
build and skips unless TELLEGEN_BIN names one.

The checked-arguments table in tests/test_sandbox.py lists the three helpers
every tellegen tool reaches the filesystem through, and test_windows_only_flags
names tellegen among the servers that run everywhere.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
powermcp/TELLEGEN.md covers configuration, the Study bundle format, revision
binding and cancellation. The README section states how to build the CLI, point
PowerMCP at it, and read the response tail, and the PowerIO section names
Tellegen as a consumer of the same IR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
powermcp/errors.py names the result shape every bundled tool reports:
{"status": "success", ...} on success and {"status": "error", "message": ...}
on failure. run_tool applies it to a tool body, passing the text of a refused
path or a rejected argument straight through and logging anything else with
its traceback before reporting it by type.

The GenX server runs each tool through run_tool and returns tool_success /
tool_error, as do the tool_logic helpers whose dicts the tools hand back
unchanged. A caller reads status once and has no second failure path to
handle from this connector.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbZRpXL3GhRRgXrccNDWka
The PowerFactory tools serialize their result to a JSON string, so the shape
travels in the string rather than in a dict. Each one now carries "status" of
"success" or "error" plus a message, through tool_success and tool_error and
through _reported, which maps a DIgSILENTAgent (ok, message) pair onto it.

run_pipeline's step report is handed back as a tool result by run_simulation
and run_custom_case, so it carries the same two keys alongside its per-step
records; a stopped pipeline names the step that stopped it. read_results_csv
still returns raw CSV text on success and now reports a missing or unreadable
file through the error shape.

tests/test_powerfactory_results.py covers both branches against a substituted
agent, since PowerFactory itself is Windows-only vendor software.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbZRpXL3GhRRgXrccNDWka
Both tools run through run_tool, so an r2x failure reaches the caller as
{"status": "error", "message": "<type>: <text>"} rather than as an MCP protocol
error, and the traceback is logged to stderr instead of the JSON-RPC channel.
The refused-path branch and the success branch use the same two keys.

Path containment stays inside each tool function, where tests/test_sandbox.py
reads it from the source.

tests/test_plexosdb_results.py substitutes the r2x packages with fakes that
raise, since r2x needs a real PLEXOS XML study and is not installed in CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbZRpXL3GhRRgXrccNDWka
core/engine.py built the py_dss_interface DSS object at import, which loads the
OpenDSS engine library. core/server.py imported that module, so a machine
without a loadable engine could not start the server at all, not even to list
its tools. get_dss() builds the instance and points dss_tools at it on the
first call, and compile_opendss_file is the one entry that reaches it: every
other tool requires a compiled circuit first.

The OpenDSS tools now report through the shared shape, so a "status" of
"success" carries the tabular data under "payload" and a failure carries a
message, matching the other bundled servers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbZRpXL3GhRRgXrccNDWka
Three module-scope sys.exit calls took the process down on a missing mcp,
matplotlib or spicelib, so an install without the optional extras could not
start the server or list its tools. The MCP SDK is a plain import, since the
runner reports a missing SDK before it launches the file. matplotlib is
imported inside the plot tool, after selecting the Agg backend, and spicelib
inside the two tools that read .raw files; each names the package to install
when it is absent. Only those two tools need either one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbZRpXL3GhRRgXrccNDWka
The root README gains a "Tool results" section stating the shape every tool in
this repository reports and pointing at powermcp/errors.py, and the package
layout lists that module.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbZRpXL3GhRRgXrccNDWka
powerio 0.11.3 publishes PioModule.type_name, the canonical structural
name of a module's value, and the top-level diagnostic_record and
diagnostic_records converters. The property replaces the read of the
private _inner._type_name, and the two converters replace PowerMCP's own
copies, which produced the same keys in the same order.

The dependency floor moves to 0.11.3 in every place that declares it. Every
test module that reaches the typed powerio APIs now requires that floor, so
an older powerio skips those modules instead of failing inside them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbZRpXL3GhRRgXrccNDWka
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbZRpXL3GhRRgXrccNDWka
@samtalki
samtalki marked this pull request as ready for review September 17, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant