Skip to main content

Model execution as human-readable stories with lean/rich failure diagnostics and optional LLM analysis

Project description

runtime-narrative

Turn any Python execution into a traceable story composed of named stages. Get minimal logs when everything works — and surgical, LLM-powered diagnostics the moment something breaks.

▶ Story started: Import Customers
✔ Load CSV           0.012s
✔ Validate Data      0.004s

❌ Failure at: Insert Records

  ValueError: duplicate customer id
  Location:   app/db.py:47  insert_row
  Code:       raise ValueError("duplicate customer id")
  Chain:      ValueError ← sqlite3.IntegrityError

  ## Exact Why
  A record with the same customer_id already exists (UNIQUE constraint).

  ## Targeted Fix
  Use INSERT OR IGNORE, or check for an existing row before inserting.

This README is a fast on-ramp. For complete API reference, every renderer/analyzer/integration in depth, and the full event schema, see WIKI.md.

Why

  • Stories and stagesstory()/stage() are dual sync/async context managers (or decorators, or auto-instrumentation) that need no restructuring of existing code.
  • Sub-story tracing — nest a story() inside an active one (e.g. an API call triggering a DB query) and it auto-links as a traceable child with its own success/failure and duration — no new API.
  • Lean by default, rich on demand — a compressed stack summary and exact failure frame always; local-variable capture with automatic secret redaction only when you ask for it.
  • Optional LLM diagnosis — Ollama, any OpenAI-compatible endpoint, or Anthropic Claude can turn a traceback into an exact-cause explanation and a targeted fix.
  • Bring your own everything — any object with handle(event) is a renderer; any object with analyze_failure(...) is an analyzer. Console, JSON, SQLite, OpenTelemetry, Prometheus, HTML, webhooks, and stdlib logging capture all ship built in.

Installation

pip install runtime-narrative

Optional extras unlock additional renderers and integrations:

Extra What it installs
console typer — colored terminal output in ConsoleRenderer
fastapi starletteRuntimeNarrativeMiddleware
otel opentelemetry-api, opentelemetry-sdkOtelRenderer, OtelLogRenderer, OtelMetricsRenderer
prometheus prometheus-clientPrometheusRenderer
anthropic anthropicAnthropicFailureAnalyzer
django django — Django ASGI/WSGI middleware
celery celeryNarrativeTask, connect_narrative
grpc grpcio — gRPC server interceptors
structlog structlog — richer default ConsoleRenderer style for captured logging output
all Everything above
pip install "runtime-narrative[console,fastapi,anthropic]"

Quick start

from runtime_narrative import story, stage

with story("Import Customers"):
    with stage("Load CSV"):
        rows = load_csv("customers.csv")
    with stage("Validate Data"):
        validate(rows)
    with stage("Insert Records"):
        db.insert(rows)

ConsoleRenderer is the default and needs no configuration — on failure it prints the exact frame, a source snippet, the exception chain, and a compressed stack summary. story/stage are dual sync/async: async with works identically for async code.


Sub-stories: end-to-end call tracing

Open a story() while another is already active (in the same sync/async context) and it automatically becomes a linked sub-story — inheriting the parent's renderers/diagnostics/analyzer, carrying parent_story_id/root_story_id, and succeeding or failing independently:

async def execute_query(sql: str):
    async with story(f"DB: {sql}"):           # auto-linked to whatever story is active
        async with stage("Execute Query"):
            await conn.execute(sql)

async def create_order():
    async with story("POST /orders"):
        async with stage("Persist Order"):
            await execute_query("INSERT INTO orders ...")

ConsoleRenderer renders the resulting call tree directly, tagging every line with a [short_id], coloring a whole story family consistently, and indenting by nesting depth:

[ad8cc2] ▶ Story started: POST /orders
  [ad8cc2] ▶ Stage started: Persist Order
    [d17c63] ▶ Story started: DB: INSERT orders
      [d17c63] ▶ Stage started: Execute Query
      [d17c63] ✔ Stage completed: Execute Query (0.021s)
    [d17c63] ▶ Story ended: SUCCESS (0.034s)
  [ad8cc2] ✔ Stage completed: Persist Order (0.034s)
[ad8cc2] ▶ Story ended: SUCCESS (0.052s)

This holds up under concurrency for free: asyncio.Task copies ContextVar state at creation and each OS thread starts with a fresh top-level context, so many concurrent callers sharing one helper (like execute_query above) never cross-link into each other's tree.

Run: uv run python examples/substory_db_call.py — full reference: WIKI §21


Capture existing logging calls

NarrativeLogHandler folds logging.warning()/.error() into the same event pipeline as story()/stage() — one stream instead of two, tagged with the story/stage it happened in:

import logging
from runtime_narrative import NarrativeLogHandler

logging.getLogger().addHandler(NarrativeLogHandler(level=logging.WARNING))

extra={...} becomes structured fields; with the structlog extra installed, ConsoleRenderer renders them with structlog's own default style (colored level, timestamp, key=value fields):

logger.warning("cache miss", extra={"order_id": "ORD-42"})
# [d9e653] 2026-07-01T16:28:34 [warning  ] cache miss   order_id=ORD-42 stage=Fetch

Customize per-level prefixes or plug in your own renderer:

ConsoleRenderer(level_icons={"warning": "⚠ ", "error": "✗ "})

Route different story families to different styles or destinations with FilteredRenderer(predicate, renderer) — every event carries story_name:

from runtime_narrative import ConsoleRenderer, FilteredRenderer

renderers = [
    FilteredRenderer(lambda e: e.story_name.startswith("GET "), ConsoleRenderer()),
    FilteredRenderer(lambda e: not e.story_name.startswith("GET "), ConsoleRenderer(level_icons={"error": "✗ "})),
]

Run: uv run python examples/logging_bridge.py, uv run python examples/structured_log_routing.py — full reference: WIKI §21


Story outcomes: fold the access log into the story line

StoryCompleted carries an outcome label. The FastAPI/Starlette and Django middlewares set it automatically from the response status, and ConsoleRenderer then renders a single self-contained line per request — story name, result, and HTTP status together:

[d7678e] ▶ Story started: GET /api/call
[d7678e] ▶ Story ended: GET /api/call - SUCCESS (200 OK, 0.023s)

Since the story line now carries everything the server access log would print, you can silence the duplicate line — e.g. uvicorn.run(app, access_log=False).

Outside middleware, set an outcome on any story yourself:

from runtime_narrative import http_outcome, story

with story("GET /api/call") as runtime:
    ...
    runtime.set_outcome(http_outcome(200))   # or any short label, e.g. "3 rows"

Rich logs to a file, and no more flooded polling loops

ConsoleRenderer writes its colored, human-readable output to any file-like object, not just the terminal — so the same troubleshooting-friendly narrative can land in a log file alongside the structured JSON stream:

with open("narrative.log.txt", "a", encoding="utf-8") as log_file:
    with story("Import Pipeline", renderers=[ConsoleRenderer(output=log_file), JsonRenderer("narrative.log")]):
        ...

Every auto-instrumentation entry point (FastAPI/Starlette middleware, Django middleware, Celery, gRPC interceptors) picks this up automatically via two environment variables, with no code changes:

RUNTIME_NARRATIVE_RICH_LOG_FILE=/var/log/app/narrative.log.txt   # rich console output also goes here
RUNTIME_NARRATIVE_RICH_LOG_CONSOLE=0                             # optional: stop echoing it to the terminal too

Long-running, poll-heavy stages (status checks every couple of seconds during a big upload or pipeline run) no longer flood the log either. CoalescingRenderer wraps any other renderer and collapses a run of identical back-to-back stages into one summary line — total call count and total time, not one line per poll:

from runtime_narrative import ConsoleRenderer, CoalescingRenderer

with story("Process Upload", renderers=[CoalescingRenderer(ConsoleRenderer())]):
    for _ in range(45):
        with stage("Check Pipeline Status"):
            poll()
[abcdef] ▶ Stage started: Check Pipeline Status
[abcdef] ✔ Stage completed: Check Pipeline Status (2.010s)
[abcdef] ▶ Stage started: Check Pipeline Status
[abcdef] ✔ Stage completed: Check Pipeline Status (2.005s)
[abcdef] 'Check Pipeline Status' repeated 43 more times (45 total) over 90.400s (avg 2.009s/call)

Every ConsoleRenderer line also carries a YYYY-MM-DD HH:MM:SS.mmm timestamp, and the module that opened a story or stage — auto-detected with a single cheap frame lookup, no stack walking — shown once on Story started and again only when a stage transitions to a different module, so multi-module workflows stay traceable without repeating the tag on every line:

2026-07-03 12:03:41.208 [abcdef] ▶ Story started: Process Upload (app.routes.upload)
2026-07-03 12:03:41.209 [abcdef]   ▶ Stage started: Validate Input (app.validators)
2026-07-03 12:03:41.210 [abcdef]   ✔ Stage completed: Validate Input (0.001s)
2026-07-03 12:03:41.211 [abcdef]   ▶ Stage started: Insert Record (app.db)

Run: uv run python examples/colorful_errors_and_emojis.py — full reference: WIKI §10.1, §10.1b


Feature tour

Everything below works the same way in every context (sync/async, decorators, auto-instrumentation, any framework middleware). One line each here; full detail and every parameter in the Wiki.

Area What you get Full reference
Decorators @runtime_narrative_story / @runtime_narrative_stage — wrap functions without restructuring call sites WIKI §7
Auto-instrumentation @narrative_class, @no_stage, instrument_module(), auto_instrument() — instrument classes/modules with zero call-site changes WIKI §8
Failure diagnostics Lean/rich modes, production traceback caps, secret redaction, FailureDiagnosticsConfig WIKI §9
Failure analyzers OllamaFailureAnalyzer, LLMFailureAnalyzer, AnthropicFailureAnalyzer, DeduplicatingAnalyzer, background_analysis=True WIKI §9, §16
Renderers ConsoleRenderer (optional file output=), JsonRenderer/RotatingJsonRenderer, HtmlReportRenderer, SqliteStoryRenderer, OtelRenderer/OtelLogRenderer/OtelMetricsRenderer, PrometheusRenderer, AlertRoutingRenderer, FilteredRenderer, CoalescingRenderer WIKI §10
Framework integrations FastAPI/Starlette middleware, Django ASGI/WSGI middleware, Celery task base class, gRPC interceptors WIKI §11
Async task groups NarrativeTaskGroup — concurrent asyncio tasks under one shared story WIKI §12
Persistence & CLI SqliteStoryRenderer + runtime-narrative failures / runtime-narrative story <id> WIKI §13
Testing StoryRecorder — dual sync/async assertion API, no output produced WIKI §14
dry_run mode Suppress stage-body exceptions; verify instrumentation wiring with no side effects WIKI §15
Custom renderers/analyzers Any handle(event) object is a renderer; any analyze_failure(...) object is an analyzer WIKI §17, §18
Utilities has_active_story(), stage(optional=True) for library code that may run with or without a story WIKI §6
StoryRuntime.record_failure() Record a failure in saga/rollback flows without owning exception propagation WIKI §5
Event schema All seven event dataclasses and their fields WIKI §20

Examples

Every script under examples/ is runnable as-is: uv run python examples/<name>.py.

Core

Script Demonstrates
success.py Minimal story()/stage() API, no decorators, a success path
basic.py @runtime_narrative_story/@runtime_narrative_stage decorators, a failure path
basic_ollama.py Same failure path with OllamaFailureAnalyzer attached

Sub-stories and logging (newest features)

Script Demonstrates
substory_db_call.py Nested story() auto-linking as a sub-story (API call → DB call)
logging_bridge.py NarrativeLogHandler folding logging calls into the story pipeline
structured_log_routing.py extra= fields, level_icons, and FilteredRenderer per-story-family routing

Auto-instrumentation

Script Demonstrates
narrative_class.py @narrative_class and @no_stage
instrument_module.py instrument_module() on an existing module
auto_instrument.py auto_instrument() import-hook, zero call-site changes

Failure diagnostics and analysis

Script Demonstrates
diagnostics_config.py FailureDiagnosticsConfig — rich mode, redaction, production caps
background_analysis.py background_analysis=True — non-blocking LLM analysis
anthropic_analyzer.py AnthropicFailureAnalyzer + DeduplicatingAnalyzer

Renderers and observability

Script Demonstrates
html_report.py HtmlReportRenderer — self-contained HTML report
sqlite_persistence.py SqliteStoryRenderer + the runtime-narrative CLI
otel_tracing.py OtelRenderer, OtelLogRenderer, OtelMetricsRenderer
alert_routing.py AlertRoutingRenderer — async webhook fan-out
colorful_errors_and_emojis.py ConsoleRenderer's built-in color + level_icons emoji across log levels and a failure

Framework integrations and concurrency

Script Demonstrates
middleware_skip_if.py RuntimeNarrativeMiddleware(skip_if=...) for FastAPI/Starlette
task_group.py NarrativeTaskGroup — concurrent asyncio tasks under one story
fastapi_app/ Full FastAPI demo app (uv run python -m examples.fastapi_app.run)
fastapi_ugly_traceback_demo.py The same deep async bug (route → orchestrator → retry-wrapped pricing engine → asyncio.gather fan-out → the actual TypeError), run once with no instrumentation and once with runtime-narrative — a raw ~35-frame traceback vs. one pinpointed line, source snippet, and locals

Testing and lifecycle utilities

Script Demonstrates
story_recorder.py StoryRecorder test assertion API
dry_run_mode.py dry_run=True — verify wiring with no side effects
optional_stage.py has_active_story() and stage(optional=True)
saga_record_failure.py StoryRuntime.record_failure() in a saga/rollback flow
stage_story_name.py story_name on StageStarted/StageCompleted

Environment variables

Variable Values Default Effect
RUNTIME_NARRATIVE_ENV development, production development Production caps tracebacks to 8 000 chars and forces lean mode
RUNTIME_NARRATIVE_FAILURE_DIAGNOSTICS lean, rich lean rich captures local variable values at the failing frames. Invalid values raise ValueError at story construction.
RUNTIME_NARRATIVE_ALLOW_RICH_IN_PRODUCTION 1, true off Bypass production safeguard; allow rich diagnostics in production
RUNTIME_NARRATIVE_MODEL model name string Default model for AnthropicFailureAnalyzer; also used by example scripts for OllamaFailureAnalyzer / LLMFailureAnalyzer
ANTHROPIC_API_KEY API key Required by AnthropicFailureAnalyzer; read automatically if api_key= is not passed
RUNTIME_NARRATIVE_RICH_LOG_FILE file path Adds a file-backed ConsoleRenderer writing rich, human-readable output to this path, on top of whichever renderer TTY detection selects. Read by every auto-instrumentation entry point (FastAPI/Starlette, Django, Celery, gRPC) when renderers= is omitted
RUNTIME_NARRATIVE_RICH_LOG_CONSOLE 1, 0 1 With RUNTIME_NARRATIVE_RICH_LOG_FILE set and stdout a TTY, 0 suppresses the terminal copy so the narrative goes to the file only
RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS seconds (float) 12 (OllamaFailureAnalyzer/LLMFailureAnalyzer), 30 (AnthropicFailureAnalyzer) Request timeout for the built-in failure analyzers. Raise it for slower or cold-loading local models (e.g. Ollama loading a multi-GB model from disk); on timeout or any other request failure the analyzer logs a logging.warning (module runtime_narrative.analyzers.*) and falls back to no LLM analysis rather than failing the story

Python compatibility

Python 3.9+. Async task groups (NarrativeTaskGroup) require no additional dependencies. Type hints use from __future__ import annotations throughout for compatibility with older typing syntax.


More

  • WIKI.md — complete reference: every parameter, every renderer, every event field.
  • CHANGELOG.md — what changed in each release.
  • ROADMAP.md — what's shipped and what's next.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

runtime_narrative-1.5.3.tar.gz (106.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

runtime_narrative-1.5.3-py3-none-any.whl (71.5 kB view details)

Uploaded Python 3

File details

Details for the file runtime_narrative-1.5.3.tar.gz.

File metadata

  • Download URL: runtime_narrative-1.5.3.tar.gz
  • Upload date:
  • Size: 106.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for runtime_narrative-1.5.3.tar.gz
Algorithm Hash digest
SHA256 357002cda31851509c21a7510d584180b43738b87c85afc0f80bf910ec09c4cd
MD5 9932e892be3efcaab90ac2ae676bb92d
BLAKE2b-256 c9326ce431bacd14cd1ff8ab26d8162c25e52a8a8588f0adb8021fd5842cf5a7

See more details on using hashes here.

File details

Details for the file runtime_narrative-1.5.3-py3-none-any.whl.

File metadata

File hashes

Hashes for runtime_narrative-1.5.3-py3-none-any.whl
Algorithm Hash digest
SHA256 5556e98909d2fcae20a273a4c21e8a36af915b58573a30e3e6297cc437ae7e08
MD5 6562c5ff5b572510d79649c779b4eb6f
BLAKE2b-256 5ef02d0fc6e0ffff24ca0bdd17c74a78a41ca2ed2ffc264d0548079fbee0eec8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page