No description
  • Rust 40.4%
  • JavaScript 18.2%
  • Python 15.9%
  • HTML 12.5%
  • Shell 7.8%
  • Other 5.2%
Find a file
mik-tf ba74b2b094 chore(hero_logic): D-10 closure — canonical service.toml + service_base!() + cargo update + dep audit
Bring hero_logic into compliance with the workspace's canonical service shape
under D-10 (lhumina_code/hero_proc#102 +
runbook lhumina_code/hero_proc#105).

Shape:
  hero_logic is 3-binary / 2-crate — `hero_logic_server` is packed as a
  `[[bin]]` inside the `hero_logic` CLI crate at `src/bin/`, not its own
  crate. Two service.toml files (one per crate) both list all 3 binaries
  with their sockets; both ship `[[env]] PATH_ROOT default="~/hero"` per
  the s107 lesson + #105 runbook (hero_lib 30a0b34e requires PATH_ROOT
  in spawned env or paths.rs:38 panics).

Wiring:
  - crates/hero_logic/src/main.rs (CLI): `service_base!()` macro +
    `validate_service_toml(SERVICE_TOML)` + `handle_info_flag(SERVICE_TOML)`.
    Add `forward_env()` helper threading PATH_ROOT/PATH_VAR/PATH_BUILD/
    PATH_CODE/HERO_SOCKET_DIR into each `HeroService::env()` so the legacy
    `hero_logic --start` lifecycle still works under hero_lib 30a0b34e
    (s109 lesson #19 adapted to the `hero_service::HeroServices` abstraction).
    Override `HeroService::ui("hero_logic_admin").socket_name("admin.sock")`
    so the registered action matches the admin binary's actual socket
    (HeroService::ui defaults to `ui.sock` — pre-s76 _ui rebrand artifact).
  - crates/hero_logic/src/bin/hero_logic_server.rs: inline SERVICE_TOML/
    BUILD_NR consts with `include_str!("../../service.toml")` since the
    `service_base!()` macro hard-codes `../service.toml` which only works
    from `src/main.rs`. Add the triad + `print_startup_banner` + `prepare_sockets`.
  - crates/hero_logic_admin/src/main.rs: `service_base!()` + triad + banner
    + `prepare_sockets`. Fix stale module doc claiming `ui.sock` — admin
    actually binds `admin.sock`. Fix well-known manifest `"socket": "ui"`
    typo to `"admin"`.
  - crates/hero_logic_admin/Cargo.toml: add `herolib_core = { workspace = true }`.

Cascade:
  cargo update absorbs hero_lib 30a0b34e (PATH_ROOT-mandatory regime),
  hero_rpc df2f8b1b, hero_proc_sdk fce6ce2, hero_proxy b8e383c, plus
  transitives (tokio 1.52.2→1.52.3, tower-http 0.6.8→0.6.10, wasm-bindgen
  0.2.120→0.2.121, etc.). Removed sqlite/image stack (libsqlite3-sys,
  rsqlite-vfs, rusqlite, sqlite-wasm-rs, image, image-webp, png, etc.) —
  upstream herolib_core 30a0b34e no longer pulls them in.

  osis_server_generated.rs is regenerated by the cargo build cascade
  (let-Ok-chain idiom from upstream herolib_derive). Auto-generated per
  D-03; carried as part of this commit.

Dep audit (hero_logic crate, 5 zero-match strips):
  hero_proc_sdk, thiserror, tracing-subscriber, toml, parking_lot.
  Verified by grep across `crates/hero_logic/src/`. The hero_logic_admin
  crate has no zero-match deps (regex is genuinely used at routes.rs:708).

Verification (D-10 5/5):
  - service.toml ✓ — 2 files, deserialize as ServiceToml, list 3 binaries
    with sockets + `[[env]] PATH_ROOT`.
  - service_base!() triad ✓ — all 3 main.rs files.
  - lab infocheck ✓ — 0 findings (was 9).
  - Smoke ✓ — `lab service hero_logic_server --install --start` → 4/4
    (health, openrpc.json, well-known, system.ping); `lab service
    hero_logic_admin --install --start` → 2/2 (health, well-known).
    PATH_ROOT confirmed in spawned daemon env via `/proc/$pid/environ`.
  - cargo test ✓ — `cargo test --workspace --release --lib --bins` 50/50
    passed. One pre-existing integration test compile failure unrelated
    to this sweep: crates/hero_logic/tests/e2e_create_event.rs:31 refs
    `service_agent_v3.py` which was renamed to `service_agent.py` in
    commit ed2d74f (pre-sweep on origin/development) — out of D-10 scope.

Refs: lhumina_code/hero_proc#102
2026-05-17 23:09:36 -04:00
.forgejo/workflows ci: add build-linux.yaml + bundled fmt/clippy/musl-fix debt (#24) 2026-05-06 22:38:59 +00:00
crates chore(hero_logic): D-10 closure — canonical service.toml + service_base!() + cargo update + dep audit 2026-05-17 23:09:36 -04:00
examples feat(#29) + chore(#30): PRD-gap pass + hygiene sweep (#32) 2026-05-14 10:07:30 +00:00
scripts chore(bakeoff): split med_ok / med_all, add attempts + diagnosis columns 2026-05-14 12:06:03 +02:00
.gitignore feat(11-E): seed service_agent_v3 Python flow on startup (#22) 2026-05-06 17:35:14 +00:00
buildenv.sh refactor: rename hero_logic_ui -> hero_logic_admin (ui.sock -> admin.sock) (#26) 2026-05-08 02:06:06 +00:00
Cargo.lock chore(hero_logic): D-10 closure — canonical service.toml + service_base!() + cargo update + dep audit 2026-05-17 23:09:36 -04:00
Cargo.toml chore: bundled WIP — retry ladder, axum 0.8 routes, dep bumps, bakeoff script 2026-05-13 15:12:50 +02:00
Makefile feat(#29) + chore(#30): PRD-gap pass + hygiene sweep (#32) 2026-05-14 10:07:30 +00:00
PRD.md docs: add PRD + README; scaffold /examples; delete superseded flows-as-python.md 2026-05-13 15:12:34 +02:00
README.md docs: add PRD + README; scaffold /examples; delete superseded flows-as-python.md 2026-05-13 15:12:34 +02:00

hero_logic

A runtime for runnable, observable, resumable Python flows.

A flow is a Python function decorated with @flow(...). It has typed inputs and outputs. When you run it, hero_logic spawns a sandboxed Python subprocess, captures a live span tree of every step, persists the result, and renders the run as a graph you can read at any depth.

Flows compose recursively: a step calls another flow as a function (in-process, default) or as a separate Play (spawned, opt-in). Any step at any depth can pause — exit cleanly with state persisted — to wait for input from a user, a webhook, a scheduled trigger, or any other source. When a resume arrives, the play re-runs; every previously-completed step replays from cache, the pause returns the resume payload, execution continues. No long-blocked subprocesses, no lost work, no double side-effects.

Around the runtime: versioned workflows, saved input presets, per-version benchmarks, a searchable library of flows, and a web admin with Monaco source + live graph view + pause/resume input forms.

For the full design see PRD.md. This README is the orientation.


What problems it solves

  • Visibility into multi-step automation. Watch a complex run, live, as a tree — including nested sub-flows, fan-outs, retries, replays. A reader who doesn't know Python can still see what happened.
  • Resumable workflows without long-running processes. A flow can ask a user for a clarification, wait for an external webhook, or pause for a scheduled trigger — and the subprocess exits cleanly until the resume arrives.
  • Reproducible runs with cost / latency tracking. Pin Plays to specific versions; benchmark versions against realistic inputs; let the runtime pick the right version per request based on cost/speed/accuracy weights.
  • A growing library of solved problems. Every saved Workflow is a callable primitive. The agent searches the library before generating fresh code; user-confirmed flows save back as reusable entries.

The dominant current consumer is AI-agent orchestration (service_agent and friends), but hero_logic is not AI-specific. Anything that benefits from typed I/O + step visibility + saved inputs + benchmarks + pauses qualifies.


Core API (Python)

Authoring surface, all exported from hero_tracing:

from hero_tracing import flow, instrument, ask_user

@flow(name="my_flow", inputs={"prompt": {"type":"string","required":True}},
                     outputs={"answer": {"type":"string"}})
def main(prompt):
    ai = instrument(HeroAibrokerClient())               # opt-in per-RPC spans

    selection = flow.invoke("pick_service", prompt=prompt)        # in-process sub-flow
    if not selection["confident"]:
        selection = ask_user.choice("Which service?",             # pause for human input
                                    options=selection["candidates"])

    result = flow.invoke("run_against_service",                   # spawned sub-flow → own Play
                        service=selection, prompt=prompt, spawn=True)
    return {"answer": result["summary"]}
  • @flow — every call becomes a span; declares typed I/O; outputs are memoized for replay
  • flow.step(name, **tags) / flow.span(...) — context-manager spans (use sparingly; prefer @flow)
  • flow.invoke(name, **inputs, spawn=False) — call a sub-flow (in-process or spawned)
  • flow.pause(name, schema=..., ui=...) — generic pause; returns the resume payload
  • ask_user.text / number / choice / multi_choice / confirm — UI-flavored helpers over flow.pause
  • instrument(client) — opt-in per-RPC spans
  • flow.Failed — clean "this didn't work" exception for spans
  • flow.current_span.tag(k, v) / .log(text) — attach data to the current span

See PRD §3 for the full surface.


RPC surface (LogicService)

JSON-RPC 2.0 over HTTP/1.1 over UDS at ~/hero/var/sockets/hero_logic/rpc.sock. Generated typed clients live at ~/.hero/var/router/python/hero_logic_client.py.

workflow_create / workflow_get / workflow_list / workflow_update / workflow_delete
workflow_create_version / workflow_set_current_version / workflow_version_fetch
play_start / play_run_async / play_status / play_wait / play_cancel
play_resume / play_pending_resumes
example_upsert / example_fetch / example_to_input_data / example_list / example_delete
benchmark_list_for_workflow / benchmark_list_for_version / benchmark_latest_for_version
pick_version
flow_library_search

See PRD §8 for parameters and return shapes.


Where things live

hero_logic/
├── PRD.md                                 # full design spec
├── README.md                              # this file
├── examples/                              # E2E driver scripts (start play, respond to pauses, assert)
└── crates/
    ├── hero_logic/                        # CLI binary + RPC server binary + executor
    │   ├── src/main.rs                    # hero_logic CLI (--start / --stop / --info)
    │   ├── src/bin/hero_logic_server.rs   # JSON-RPC server (rpc.sock)
    │   ├── src/engine/                    # Python executor, span socket, replay
    │   ├── src/seed_flows/                # bundled @flow Python sources
    │   ├── schemas/logic/logic.oschema    # types + RPC source of truth
    │   └── sdk/python/hero_tracing.py     # embedded; staged on server startup
    └── hero_logic_admin/                  # Axum dashboard (admin.sock)
        ├── src/main.rs
        └── templates/                     # Askama HTML

Runtime paths the executor touches:

  • ~/hero/var/sockets/hero_logic/{rpc.sock,admin.sock} — service sockets
  • ~/.hero/var/flows/sdk/hero_tracing.py — staged on every server start
  • ~/.hero/var/router/python/ — generated RPC clients (owned by hero_router)
  • ~/.hero/var/plays/{play_sid}/work/ — per-Play workdir + replay cache files
  • /tmp/spans-{play_sid}.sock — per-Play span socket

Binaries

Binary Kind Socket Purpose
hero_logic cli Registration + control (--start / --stop / --info)
hero_logic_server server hero_logic/rpc.sock JSON-RPC, executor, span listener
hero_logic_admin admin hero_logic/admin.sock Web dashboard

All three follow the herolib_base pattern: service.toml at the crate root, embedded via service_base!(), validated and printed on startup, sockets prepared and stale-cleaned before bind. See PRD §13.


Build, install, run

make build          # cargo build --release
make install        # install binaries to ~/hero/bin
make run            # registers + starts via hero_proc (requires hero_proc running)
make dev            # run hero_logic_server in foreground, debug logging
make dev-ui         # run hero_logic_admin in foreground
make stop           # stop via hero_proc
make test           # cargo test --lib

hero_proc must be running before make run. Admin UI: http://localhost:9820 when proxied through hero_router, or via ~/hero/var/sockets/hero_logic/admin.sock.


How a run flows end-to-end

  1. Author writes a @flow-decorated Python function; saves as a Workflow + first WorkflowVersion.
  2. play_start validates inputs, writes a Play, binds /tmp/spans-{sid}.sock, spawns python3 with PYTHONPATH pointing at the staged SDK + generated clients.
  3. The subprocess imports hero_tracing, the boot stub calls the @flow-decorated entry; every step emits JSONL span events over the socket.
  4. The server persists spans incrementally and pushes them via SSE to the admin UI's graph view.
  5. If a step calls flow.pause(...) → subprocess exits clean, Play.status = awaiting_resume, a ResumeRequest is persisted.
  6. UI (or webhook / cron / another service) calls play_resume(play_sid, resume_id, payload) → server spawns a fresh subprocess; cached step outputs short-circuit @flow calls; the pause returns the resume payload; the run continues. Repeats per pause until the play reaches a terminal status.

For details on the replay contract and step memoization, see PRD §45.


Tips for AI agents picking this up

  • The PRD is the source of truth. Read it before changing anything structural.
  • crates/hero_logic/schemas/logic/logic.oschema is the source of truth for types + RPC. Edit the schema, regenerate, then implement handlers — not the other way around.
  • The Python authoring surface is crates/hero_logic/sdk/python/hero_tracing.py. Wire protocol lives there too.
  • The executor is crates/hero_logic/src/engine/python_executor.rs (spawn, sandbox, boot stub) and engine/span_socket.rs (listener, persistence).
  • Seed flows in crates/hero_logic/src/seed_flows/ are working reference implementations of the authoring patterns. service_agent.py is the primary example.
  • /examples/ is the E2E test surface — runnable scripts that drive a play through its full lifecycle including resumes.

License

Apache-2.0