Disposition of the alpha-RTC agent feedback (10 issues)#
Source: pystorms/.../alpha_openswmm_findings_and_rerun.md — an agent’s report
after driving the gym MCP tools to optimize reactive RTC on the alpha network.
This records, per issue: whether the root-cause was accurate, what was
implemented, and what was deferred (with rationale). Validated and
tested in the openswmm conda env.
Summary#
# |
Issue |
Disposition |
|---|---|---|
1 |
Complex params rejected as JSON strings |
Fixed (defensive coercion); root-cause corrected |
2 |
EnvConfig/ObservationSpec not discoverable |
Fixed (schema + features + examples + better errors) |
3 |
No first-class single/baseline policy eval |
Deferred (documented workaround) |
4 |
Baseline corner can be missing from front |
Partly (documented + guidance in capabilities); seeding deferred |
5 |
Monotonic projection undocumented |
Fixed (capability note; decode already returns projected) |
6 |
“CSO” reward semantics ambiguous |
Fixed (capability note; tag-resolver deferred) |
7 |
Results payload bloat |
Deferred (documented: read |
8 |
Path/concurrency under-documented |
Deferred (doc-only) |
9 |
Silent algorithm/budget fallback |
Fixed (resolved settings echoed everywhere; #1 removes the trigger) |
10 |
Dashboards not discoverable/offline |
Deferred (skills/dashboard layer, out of MCP-tool scope) |
Implemented#
#1 — JSON-string object/array params (critical)#
Root-cause (corrected twice). The report blamed a missing schema type; on
FastMCP 3.2.4 the schema is in fact typed (optimization is
{"anyOf":[{"type":"object",...},{"type":"null"}],"default":null}). The real
cause is one layer deeper: FastMCP validates each tool argument against the
function signature with Pydantic, at bind time, so a stringified argument is
rejected with Input should be a valid dictionary [type=dict_type, ..., input_type=str] before the tool body runs. A first attempt that decoded the
string inside the function body therefore never executed for the failing path
— the string was rejected during binding. (Confirmed by driving
mcp.call_tool("gym_start_optimization", {"optimization": "{...}"}).)
Fix (the correct layer). A Pydantic BeforeValidator,
decode_json_if_str, that runs during binding: a string argument is
json.loads-decoded there, so Pydantic then validates the resulting
object/array; non-strings pass through; bad JSON yields a clear validation
error. Exposed as reusable annotated parameter types in gym_support/config.py
— JsonObject, JsonObjectRequired, JsonFloatMatrix, JsonFloatVector,
JsonStrListRequired — and applied to every complex gym_* param:
optimization/config/policy/action (gym_runs),
config (gym_envs create/validate), and
front/reference_point/ideal_point/reference_front/weights/indicators/job_ids
(gym_scoring). So optimization='{"algorithm":"nsga2","budget":600}' now binds
and runs NSGA-II — it can no longer be lost to a bind failure.
Crucially, the client-facing schema is unchanged (still object|null /
array|null): the validator decodes strings transparently, so clients that send
proper objects are unaffected and clients that stringify now also work. The
function-body coerce_json_param helper is retained as defence for direct/
internal callers (e.g. _parse_config), but the binding-layer validator is what
fixes the FastMCP path.
Tests: test_gym_scoring.py::test_pareto_filter_accepts_json_string_front_via_binding_layer
(through the real mcp.call_tool binding layer),
test_gym_config.py::test_decode_json_if_str_validator_runs_inside_pydantic_binding
and ::test_coerce_json_param_*,
test_gym_envs.py::test_json_string_config_is_accepted,
test_gym_jobs.py::test_start_optimization_accepts_json_string_and_echoes_resolved.
#9 — Silent algorithm/budget fallback / verifiability#
OptimizationConfig.resolved() returns the applied settings (algorithm, budget,
seed, plus population_size for MOEAs / grid_levels for grid). It is now
echoed in the start response and every gym_get_job snapshot (optimization
block) and in result.json / gym_get_job_results — previously these lived
only in job.json. Combined with #1 (the optimization arg now binds, instead of
being dropped → defaulting to random_search/50), the silent-downgrade path is
closed: a malformed optimization raises rather than falling back.
Tests: test_gym_jobs.py::test_optimization_config_resolved_includes_only_relevant_fields
and …_accepts_json_string_and_echoes_resolved.
#2 — EnvConfig / ObservationSpec discoverability#
gym_list_capabilities now also returns: env_config_schema
(EnvConfig.model_json_schema()), observation_spec_schema,
observation_features (the 15 valid observations keys, from the single source
of truth OBSERVATION_FEATURES), and one worked examples config per common
env_type (cip/rtc/control_curve/schedule). The config-validation error now,
when the failure mentions observations, enumerates the valid feature keys and
states they map element-ID lists, not IDs directly — fixing the exact
three-try loop the agent hit (observations.J1 Extra inputs are not permitted).
Tests: test_gym_envs.py::test_list_capabilities_surfaces_env_and_observation_schema,
…::test_observations_validation_error_enumerates_feature_keys,
test_gym_config.py::test_observation_features_are_real_observation_spec_fields.
#5 / #6 — Projection + CSO semantics (docs)#
Added to gym_list_capabilities notes: (#5) the exact monotonic projection —
nondecreasing = left-to-right cumulative max, nonincreasing = cumulative min,
applied at decode time; gym_decode_policy already returns the projected
y_values that ran. (#6) cso_volume sums node overflow; weir/relief spill
is a link flow → use uncontrolled_discharge with link_ids; never
cso_volume for weir CSO. Also added the #4 baseline guidance (below) as a note.
Deferred (with rationale)#
#3 — First-class single/baseline policy evaluation#
Real gap: gym_run_episode can’t run a chosen decision vector for a Box-action
policy env (control_curve/schedule/market), so measuring the all-open baseline
needs the degenerate y_low=y_high=1.0 trick. A clean fix is a new
gym_evaluate_policy(name|config, decision_vector) tool (or a typed PolicySpec
for gym_run_episode). Deferred as a new tool surface; the documented workaround
(pin y_low=y_high) works today and is now described in the capabilities notes.
#4 — Baseline (dominating corner) missing from the front#
Real correctness gap for control_curve/design search: random/MOEA sampling may
never hit the exact all-y_high baseline, so the reported Pareto front can omit
the true CSO-optimal corner. Implemented now: a capabilities note instructing
the agent to measure the baseline separately and union it in.
Deferred: seeding the initial population with the y_init/extreme corners
inside the search runners (jobs.py::_run_*) — a search-loop change wanted its
own tests; the union-the-baseline guidance covers correctness in the meantime.
#7 — Results payload size#
gym_get_job_results inlines every Pareto point’s full decision dict. A compact
objectives-only mode + pagination is worthwhile but additive API surface;
deferred. The canonical cheap source is documented: each job’s
output_dir/evaluations.jsonl and result.json.
#8 — Path/concurrency docs#
Doc-only: inp_path/config_dir must be readable by the server process; jobs
run concurrently and share CPU (submit sequentially for best wall-clock).
Deferred to the user-guide.
#10 — Dashboard discoverability / offline rendering#
Concerns the skills/dashboard layer (templates, viz.figures, CDN-vs-inline
Plotly, no-WebGL desktop viewer), not the gym MCP tools. Valid, but a separate
surface; deferred to the skills package.
Incidental finding (not in the 10)#
test_gym_jobs.py::test_nsga2_produces_nondominated_front is flaky:
_run_platypus seeds only numpy (np.random.default_rng(opt.seed) is used by
random_search), not Platypus’s global random, so MOEA runs are
non-deterministic and the np.allclose-based Pareto-index reconstruction in
_assemble_result can occasionally include a near-duplicate dominated point.
Recommended follow-up: seed Platypus for reproducibility (carefully, given the
global-RNG/thread-safety concern) and/or make the Pareto-index reconstruction
exact. Left unchanged here (pre-existing, outside this feedback’s scope).