- Rust 40.4%
- JavaScript 18.2%
- Python 15.9%
- HTML 12.5%
- Shell 7.8%
- Other 5.2%
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
|
||
|---|---|---|
| .forgejo/workflows | ||
| crates | ||
| examples | ||
| scripts | ||
| .gitignore | ||
| buildenv.sh | ||
| Cargo.lock | ||
| Cargo.toml | ||
| Makefile | ||
| PRD.md | ||
| README.md | ||
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 replayflow.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 payloadask_user.text / number / choice / multi_choice / confirm— UI-flavored helpers overflow.pauseinstrument(client)— opt-in per-RPC spansflow.Failed— clean "this didn't work" exception for spansflow.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
- Author writes a
@flow-decorated Python function; saves as aWorkflow+ firstWorkflowVersion. play_startvalidates inputs, writes aPlay, binds/tmp/spans-{sid}.sock, spawnspython3withPYTHONPATHpointing at the staged SDK + generated clients.- The subprocess imports
hero_tracing, the boot stub calls the@flow-decorated entry; every step emits JSONL span events over the socket. - The server persists spans incrementally and pushes them via SSE to the admin UI's graph view.
- If a step calls
flow.pause(...)→ subprocess exits clean,Play.status = awaiting_resume, aResumeRequestis persisted. - UI (or webhook / cron / another service) calls
play_resume(play_sid, resume_id, payload)→ server spawns a fresh subprocess; cached step outputs short-circuit@flowcalls; 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 §4–5.
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.oschemais 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) andengine/span_socket.rs(listener, persistence). - Seed flows in
crates/hero_logic/src/seed_flows/are working reference implementations of the authoring patterns.service_agent.pyis 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