▶ runtime-narrative

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

python
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:

FieldTypeDescription
namestrStage name
started_atdatetimeWhen the stage opened
ended_atdatetime | NoneWhen the stage closed
duration_secondsfloat | NoneElapsed time, set on close
completedboolTrue when the stage exited cleanly
failedboolTrue when the stage propagated an exception
stage_indexintZero-based position in the story's stage list
parent_stage_namestr | NoneName of the enclosing stage, or None
modulestrThe module that opened the stage, auto-detected or explicit

Async usage

python
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.

python
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:

python
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:

python
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.