Generated Python clients omit service methods — only rootobject CRUD is surfaced #29

Closed
opened 2026-04-21 11:03:06 +00:00 by timur · 2 comments
Owner

Problem

The Python clients auto-generated by hero_router (via router.python_client, backed by hero_rpc's generator) only emit CRUD methods for OSIS rootobjects. Custom methods declared on a service in the .oschema are dropped.

Concrete example — hero_logic

hero_logic declares ~30 custom methods on LogicService (see crates/hero_logic/schemas/logic/logic.oschema): play_start, play_status, workflow_version_fetch, workflow_clone, workflow_from_template, example_to_input_data, benchmark_list_for_version, pick_version, node_logs, node_retry, etc.

The generated client ~/.hero/var/router/python/hero_logic_client.py has 48 methods, all CRUD:

  • workflow_list, workflow_get, workflow_set, workflow_delete, workflow_find, workflow_exists
  • same six for play, workflowversion, example, benchmark
  • logicservice_list/_get/_set/... (meta-CRUD on the service record itself)

Zero of the actual LogicService trait methods are present. grep 'play_start\|workflow_clone\|workflow_from_template' ~/.hero/var/router/python/hero_logic_client.py → empty.

Impact

Every flow script in hero_logic (and every external caller wanting to drive a play from Python) has to hand-roll JSON-RPC over a raw Unix domain socket. The e2e_ux_test_flow.json, whiteboard_smoke_test_flow.json, benchmark_flow.json, optimize_flow.json templates all contain 30-60 lines of identical http.client.HTTPConnection + socket plumbing per Python node just to call play_start/play_status. The auto-generated client is technically present, but useless for anything except OSIS CRUD, so nobody imports it.

Expected behavior

HeroLogicClient should expose every service method declared in the .oschema. For example:

from hero_logic_client import HeroLogicClient
logic = HeroLogicClient()
play = logic.play_start(workflow_sid='00et', input_data=json.dumps({'prompt': task}), name='my-run')
while True:
    status = logic.play_status(play_sid=play['sid'])
    if status['status'] in ('success','failed'): break
version = logic.workflow_version_fetch(sid=status['workflow_version_sid'])

Instead of ~60 lines of http/socket plumbing.

Likely cause

Check crates/generator/src/python/ — the service-method generation branch probably exists for Rust clients but was never extended to Python (or it's there but only matches a specific method annotation that LogicService methods lack).

The OpenRPC spec at src/logic/core/openrpc.json DOES contain the LogicService methods (verified: jq '.methods | map(.name) | .[]' < openrpc.json | grep LogicService returns all 30+). So the input is complete — the output emission is what's missing.

Scope / definition of done

  1. Generated *_client.py exposes every method from the OpenRPC spec, not just {object}_{crud} shapes.
  2. Each method gets a typed signature from the OpenRPC param list and a short docstring from .description.
  3. Regenerate hero_logic_client.py and verify it now contains play_start, play_status, workflow_clone, workflow_version_fetch, etc.
  4. Spot-check: all 4 existing template-flows under hero_logic/templates/*.json can be rewritten to from hero_logic_client import HeroLogicClient instead of the raw-socket pattern. Size comparison in the PR description.
  5. No change to JavaScript or Rust client generation (they already handle this case — only Python is broken, per quick read).

Why this matters

As hero_logic flows grow (the current 6 built-in flows, plus every user-authored flow), every Python action script repeats the same 30-line boilerplate. This also gates any future work that lets users author flows as single scripts ("Level 2" in the companion architecture discussion — making linear procedural flows feel Python-native instead of DAG-shaped).

Consumers outside hero_logic are also affected: any external Python caller of hero_logic's play API today either skips the generated client or writes their own JSON-RPC shim.

  • Companion architecture thread (not an issue): "right boundary between hero_proc actions and hero_logic DAGs, and whether flows should be authored as Python instead of JSON". This client-codegen fix is a prerequisite for any conclusion there — procedural flows can't look clean until the client is complete.
## Problem The Python clients auto-generated by hero_router (via `router.python_client`, backed by hero_rpc's generator) only emit CRUD methods for OSIS rootobjects. Custom methods declared on a `service` in the .oschema are dropped. ### Concrete example — `hero_logic` `hero_logic` declares ~30 custom methods on `LogicService` (see `crates/hero_logic/schemas/logic/logic.oschema`): `play_start`, `play_status`, `workflow_version_fetch`, `workflow_clone`, `workflow_from_template`, `example_to_input_data`, `benchmark_list_for_version`, `pick_version`, `node_logs`, `node_retry`, etc. The generated client `~/.hero/var/router/python/hero_logic_client.py` has **48 methods**, all CRUD: - `workflow_list`, `workflow_get`, `workflow_set`, `workflow_delete`, `workflow_find`, `workflow_exists` - same six for `play`, `workflowversion`, `example`, `benchmark` - `logicservice_list`/`_get`/`_set`/... (meta-CRUD on the service record itself) **Zero** of the actual LogicService trait methods are present. `grep 'play_start\|workflow_clone\|workflow_from_template' ~/.hero/var/router/python/hero_logic_client.py` → empty. ## Impact Every flow script in hero_logic (and every external caller wanting to drive a play from Python) has to hand-roll JSON-RPC over a raw Unix domain socket. The `e2e_ux_test_flow.json`, `whiteboard_smoke_test_flow.json`, `benchmark_flow.json`, `optimize_flow.json` templates all contain 30-60 lines of identical `http.client.HTTPConnection` + socket plumbing per Python node just to call `play_start`/`play_status`. The auto-generated client is technically present, but useless for anything except OSIS CRUD, so nobody imports it. ## Expected behavior `HeroLogicClient` should expose every service method declared in the .oschema. For example: ```python from hero_logic_client import HeroLogicClient logic = HeroLogicClient() play = logic.play_start(workflow_sid='00et', input_data=json.dumps({'prompt': task}), name='my-run') while True: status = logic.play_status(play_sid=play['sid']) if status['status'] in ('success','failed'): break version = logic.workflow_version_fetch(sid=status['workflow_version_sid']) ``` Instead of ~60 lines of http/socket plumbing. ## Likely cause Check `crates/generator/src/python/` — the service-method generation branch probably exists for Rust clients but was never extended to Python (or it's there but only matches a specific method annotation that `LogicService` methods lack). The OpenRPC spec at `src/logic/core/openrpc.json` DOES contain the LogicService methods (verified: `jq '.methods | map(.name) | .[]' < openrpc.json | grep LogicService` returns all 30+). So the input is complete — the output emission is what's missing. ## Scope / definition of done 1. Generated `*_client.py` exposes every method from the OpenRPC spec, not just `{object}_{crud}` shapes. 2. Each method gets a typed signature from the OpenRPC param list and a short docstring from `.description`. 3. Regenerate `hero_logic_client.py` and verify it now contains `play_start`, `play_status`, `workflow_clone`, `workflow_version_fetch`, etc. 4. Spot-check: all 4 existing template-flows under `hero_logic/templates/*.json` can be rewritten to `from hero_logic_client import HeroLogicClient` instead of the raw-socket pattern. Size comparison in the PR description. 5. No change to JavaScript or Rust client generation (they already handle this case — only Python is broken, per quick read). ## Why this matters As hero_logic flows grow (the current 6 built-in flows, plus every user-authored flow), every Python action script repeats the same 30-line boilerplate. This also gates any future work that lets users author flows as single scripts ("Level 2" in the companion architecture discussion — making linear procedural flows feel Python-native instead of DAG-shaped). Consumers outside hero_logic are also affected: any external Python caller of hero_logic's play API today either skips the generated client or writes their own JSON-RPC shim. ## Related - Companion architecture thread (not an issue): "right boundary between hero_proc actions and hero_logic DAGs, and whether flows should be authored as Python instead of JSON". This client-codegen fix is a prerequisite for any conclusion there — procedural flows can't look clean until the client is complete.
Author
Owner

Tactical fix up for review in hero_router (the Python generator lives there, not in hero_rpc — the issue's "Likely cause" pointer was off): lhumina_code/hero_router#40

That PR:

  • Strips the PascalCase service prefix so LogicService.play_startplay_start in the generated client.
  • Prefers .description over .summary for docstrings (matches hero_rpc's oschema generator output).
  • Adds an examples/regen_python_client helper and uses it to regenerate ~/.hero/var/router/python/hero_logic_client.py from hero_logic/crates/hero_logic/src/logic/core/openrpc.json — DoD grep play_start|workflow_clone|workflow_version_fetch now matches.

Deliberately not in scope there (confirmed with instructions):

  • Rewriting the 4–5 hero_logic/templates/*.json flow templates — those are being deleted wholesale in hero_logic#13.
  • Moving Python codegen into hero_rpc itself (the architecturally-right home) — tracked separately in hero_rpc#31.
  • Fixing live rpc.discover cache drift — separate PR, tracked in hero_rpc#32. Until hero_logic is rebuilt or that lands, the runtime path (router.python_client against the live socket) still returns the CRUD-only client; the regenerated file confirms the generator fix itself is correct.

Will close this issue when hero_router#40 merges.

Tactical fix up for review in hero_router (the Python generator lives there, not in hero_rpc — the issue's "Likely cause" pointer was off): https://forge.ourworld.tf/lhumina_code/hero_router/pulls/40 That PR: - Strips the PascalCase service prefix so `LogicService.play_start` → `play_start` in the generated client. - Prefers `.description` over `.summary` for docstrings (matches hero_rpc's oschema generator output). - Adds an `examples/regen_python_client` helper and uses it to regenerate `~/.hero/var/router/python/hero_logic_client.py` from `hero_logic/crates/hero_logic/src/logic/core/openrpc.json` — DoD grep `play_start|workflow_clone|workflow_version_fetch` now matches. Deliberately *not* in scope there (confirmed with instructions): - Rewriting the 4–5 `hero_logic/templates/*.json` flow templates — those are being deleted wholesale in `hero_logic#13`. - Moving Python codegen into hero_rpc itself (the architecturally-right home) — tracked separately in `hero_rpc#31`. - Fixing live `rpc.discover` cache drift — separate PR, tracked in `hero_rpc#32`. Until hero_logic is rebuilt or that lands, the runtime path (`router.python_client` against the live socket) still returns the CRUD-only client; the regenerated file confirms the generator fix itself is correct. Will close this issue when hero_router#40 merges.
timur closed this issue 2026-04-21 21:17:51 +00:00
Author
Owner

Landed in hero_router@31ec54cd (via hero_router#40). The tactical generator fix is live on development:

  • Service methods emit as bare names (play_start, workflow_clone, …), not LogicService_*.
  • Docstrings now come from description (falling back to summary), so oschema-authored comments reach the generated client.
  • cargo run --example regen_python_client -- --spec <openrpc.json> --service <name> regenerates a service's cached Python files without rebuilding the backing binary.

Follow-ups still open:

  • hero_rpc#31 — move Python codegen to hero_rpc (architecturally-right home).
  • hero_rpc#32 — fix stale-cache drift on live rpc.discover so the runtime pipeline matches the static-spec pipeline used for verification here.

Closing this issue.

Landed in hero_router@31ec54cd (via hero_router#40). The tactical generator fix is live on `development`: - Service methods emit as bare names (`play_start`, `workflow_clone`, …), not `LogicService_*`. - Docstrings now come from `description` (falling back to `summary`), so oschema-authored comments reach the generated client. - `cargo run --example regen_python_client -- --spec <openrpc.json> --service <name>` regenerates a service's cached Python files without rebuilding the backing binary. Follow-ups still open: - `hero_rpc#31` — move Python codegen to hero_rpc (architecturally-right home). - `hero_rpc#32` — fix stale-cache drift on live `rpc.discover` so the runtime pipeline matches the static-spec pipeline used for verification here. Closing this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_rpc#29
No description provided.