stage()
stage(name, *, optional=False, module=None) is a dual sync/async context manager that must be called inside an active story(). It registers a StageRecord on the story runtime, emits StageStarted and (on success) StageCompleted, and records its duration.
module — like story()'s parameter of the same name — is auto-detected from the caller's frame if not given, and shown by ConsoleRenderer; pass it explicitly when stage() is opened from inside a wrapper.
Sync usage
from runtime_narrative import story, stage
with story("Process Order"):
with stage("Validate Input") as record:
print(f"Stage index: {record.stage_index}")
with stage("Charge Payment"):
pass
with stage("Send Confirmation"):
pass
__enter__ returns a StageRecord:
| Field | Type | Description |
|---|---|---|
name | str | Stage name |
started_at | datetime | When the stage opened |
ended_at | datetime | None | When the stage closed |
duration_seconds | float | None | Elapsed time, set on close |
completed | bool | True when the stage exited cleanly |
failed | bool | True when the stage propagated an exception |
stage_index | int | Zero-based position in the story's stage list |
parent_stage_name | str | None | Name of the enclosing stage, or None |
module | str | The module that opened the stage, auto-detected or explicit |
Async usage
import asyncio
from runtime_narrative import story, stage
async def process_order(order_id: str) -> None:
async with story("Process Order"):
async with stage("Fetch Order"):
await asyncio.sleep(0.1)
async with stage("Process Payment"):
await asyncio.sleep(0.2)
async with stage("Dispatch"):
await asyncio.sleep(0.05)
asyncio.run(process_order("ORD-001"))
Nesting stages
Stages may nest arbitrarily. The inner stage records parent_stage_name pointing to the outer stage, which allows renderers and analyzers to reconstruct a tree view of the execution.
from runtime_narrative import story, stage
with story("Deploy Service"):
with stage("Build"):
with stage("Compile"):
pass
with stage("Run Unit Tests"):
pass
with stage("Package"):
pass
with stage("Release"):
with stage("Push Artifacts"):
pass
with stage("Update Config"):
pass
In the event stream, StageStarted for "Compile" has parent_stage_name="Build". Each inner stage also appears in the flat StoryRuntime.stages list, and in build_stage_timeline().
Calling stage() outside a story
stage() raises RuntimeError immediately if there is no active story in the current context. Two escape hatches are available for library code or background jobs that may run with or without an enclosing story:
has_active_story() probe
Check before entering a stage:
from runtime_narrative import has_active_story, stage
def my_lib_function():
if has_active_story():
with stage("My Lib Work"):
_do_work()
else:
_do_work()
stage(optional=True)
Silently skips instrumentation when no story is active; behaves normally when inside one:
from runtime_narrative import stage
def my_lib_function():
with stage("My Lib Work", optional=True):
_do_work() # instrumented inside a story, plain call outside
Run: uv run python examples/optional_stage.py
Next step
Continue to Decorators to wrap functions in story()/stage() without restructuring call sites, or revisit story() for the full parameter reference.