feat(11-B): hero_tracing.py SDK — span emission for Python flows #16

Merged
timur merged 1 commit from feat/11-phase-b-hero-tracing-sdk into development 2026-05-05 12:17:16 +00:00
Owner

Summary

Phase B of hero_logic#11. Pure-Python (stdlib only) SDK that flow authors import. The executor (Phase C) embeds it via include_str! and stages it on startup at ~/.hero/var/flows/sdk/hero_tracing.py, then sets PYTHONPATH so flows can from hero_tracing import flow, instrument.

Public API

API Purpose
@flow(name, inputs, description) Decorator. Stamps __hero_flow__ metadata; doesn't execute anything itself.
flow.step(name, **tags) Context manager — child span.
flow.span(name, **tags) Generic alias for step.
flow.current_span Active span, or _NullSpan if none open.
flow.Failed(reason) Exception that marks span failed with a clean reason (no traceback).
instrument(client) __getattr__-based proxy — every public method opens an rpc:{Class}.{method} span.

Wire protocol — JSONL over UDS at HERO_FLOW_SPAN_SOCK

Four event types: span_start, span_tag, span_log, span_end. Times in ms-since-epoch. Documented in the module docstring as the source of truth for the Rust-side parser landing in Phase C.

Standalone mode

When HERO_FLOW_SPAN_SOCK is unset every helper is a silent no-op. Flow authors can python my_flow.py for local development without the executor.

Robustness

  • Per-writer lock — concurrent threads can't interleave bytes mid-line.
  • Dead-socket handling — if the executor closes the socket mid-run (wall-clock timeout), the writer marks itself dead and silently drops subsequent events. Better than surfacing broken-pipe inside the user's flow.
  • Param/result serialization truncates at 4 KiB.

Test plan

  • python3 -m unittest discover -s crates/hero_logic/sdk/python/tests — 16/16 pass.
  • Coverage: standalone no-ops; nested steps with parent linkage; flow.Failed vs arbitrary exception (clean reason vs traceback); tag/log events; HERO_FLOW_PARENT_SPAN env-var propagation (sub-flow case); instrument() including private-method passthrough + nested inheritance; _bootstrap_run happy + failed paths; threaded concurrency (each thread gets its own root).

Phase plan (#11 split)

  • A — schema additive (#15)
  • B — this PRhero_tracing.py SDK
  • C — executor rewrite + 4 new RPCs + Tier 0 sandbox + SSE (depends on A + B)
  • D — migration tool + delete legacy DAG types/files (depends on C)

A and B are mergeable in any order. C combines them.

🤖 Generated with Claude Code

## Summary Phase B of hero_logic#11. Pure-Python (stdlib only) SDK that flow authors import. The executor (Phase C) embeds it via `include_str!` and stages it on startup at `~/.hero/var/flows/sdk/hero_tracing.py`, then sets `PYTHONPATH` so flows can `from hero_tracing import flow, instrument`. ## Public API | API | Purpose | |---|---| | `@flow(name, inputs, description)` | Decorator. Stamps `__hero_flow__` metadata; doesn't execute anything itself. | | `flow.step(name, **tags)` | Context manager — child span. | | `flow.span(name, **tags)` | Generic alias for `step`. | | `flow.current_span` | Active span, or `_NullSpan` if none open. | | `flow.Failed(reason)` | Exception that marks span failed with a clean reason (no traceback). | | `instrument(client)` | `__getattr__`-based proxy — every public method opens an `rpc:{Class}.{method}` span. | ## Wire protocol — JSONL over UDS at `HERO_FLOW_SPAN_SOCK` Four event types: `span_start`, `span_tag`, `span_log`, `span_end`. Times in ms-since-epoch. Documented in the module docstring as the source of truth for the Rust-side parser landing in Phase C. ## Standalone mode When `HERO_FLOW_SPAN_SOCK` is unset every helper is a silent no-op. Flow authors can `python my_flow.py` for local development without the executor. ## Robustness - Per-writer lock — concurrent threads can't interleave bytes mid-line. - Dead-socket handling — if the executor closes the socket mid-run (wall-clock timeout), the writer marks itself dead and silently drops subsequent events. Better than surfacing broken-pipe inside the user's flow. - Param/result serialization truncates at 4 KiB. ## Test plan - [x] `python3 -m unittest discover -s crates/hero_logic/sdk/python/tests` — 16/16 pass. - Coverage: standalone no-ops; nested steps with parent linkage; `flow.Failed` vs arbitrary exception (clean reason vs traceback); tag/log events; `HERO_FLOW_PARENT_SPAN` env-var propagation (sub-flow case); `instrument()` including private-method passthrough + nested inheritance; `_bootstrap_run` happy + failed paths; threaded concurrency (each thread gets its own root). ## Phase plan (#11 split) - A — schema additive (#15) - **B — this PR** — `hero_tracing.py` SDK - C — executor rewrite + 4 new RPCs + Tier 0 sandbox + SSE (depends on A + B) - D — migration tool + delete legacy DAG types/files (depends on C) A and B are mergeable in any order. C combines them. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Phase B of hero_logic#11. Pure-Python (stdlib only) SDK that flow authors
import to get @flow / flow.step / flow.current_span / instrument(). The
executor (Phase C) embeds this file via include_str! and stages it on
startup at ~/.hero/var/flows/sdk/hero_tracing.py.

Wire protocol — JSONL over Unix domain socket pointed at by
HERO_FLOW_SPAN_SOCK. Four event types: span_start, span_tag, span_log,
span_end. All times in ms-since-epoch. Documented in the module
docstring so a Rust-side parser can be written from the same source of
truth.

Public surface:

- @flow(name=, inputs=, description=) — decorator. Stamps __hero_flow__
  metadata onto the function so the executor can introspect inputs
  without running the flow. Calling the decorated function directly is a
  no-op of the original, so flows stay testable from a REPL.

- flow.step(name, **tags) / flow.span(name, **tags) — context managers
  that open a child span. Uses contextvars so async tasks and threads
  inherit parent correctly.

- flow.current_span — descriptor returning the open span or a no-op
  _NullSpan (so flow.current_span.tag(...) is unconditional).

- flow.Failed(reason) — exception that marks the span failed with a
  clean reason instead of a traceback. Any other exception captures full
  traceback into the span's error field.

- instrument(client) — __getattr__-based proxy that wraps every public
  method with an "rpc:{Class}.{method}" span. Service-agnostic; works
  against any generated client without per-client knowledge. Private
  (_-prefixed) methods pass through unwrapped.

- _bootstrap_run(entry, input_data) — internal entry the executor's
  bootstrap stub will call to open the root span around the @flow
  function.

Standalone-mode behavior: when HERO_FLOW_SPAN_SOCK is unset, every
helper is a silent no-op. A flow author can `python my_flow.py` for
local development without the executor running.

Robustness: socket writes are thread-safe (per-writer lock). If the
executor closes the socket mid-run (e.g. wall-clock timeout fired), the
writer marks itself dead and silently drops subsequent events rather
than raising broken-pipe inside the user's flow. Param/result
serialization truncates at 4 KiB to keep the span tree readable.

Tests: 16 unittest cases across 5 classes — standalone-mode no-ops,
single + nested + failed steps, tag/log events, parent_span_id env-var
propagation (sub-flow case), instrument() wrapping including
private-method passthrough and parent inheritance, _bootstrap_run, and
threaded concurrency. Run with:

  python3 -m unittest discover -s crates/hero_logic/sdk/python/tests

Refs hero_logic#11 (Story 1: Foundation), hero_logic#10 (epic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
timur merged commit f57963f507 into development 2026-05-05 12:17:16 +00:00
timur referenced this pull request from a commit 2026-05-05 12:17:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
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_logic!16
No description provided.