Free & source-available · BSL 1.1 · TypeScript · Java · .NET · Python

Code is the log.Readable by humans. Made for AI agents.

Replace the hand-written log.info and log.debug — not your logging stack. NarrativeTrace™ derives events from running code and sends them through the logger you already use — SLF4J, pino, structlog, Microsoft.Extensions.Logging — or OpenTelemetry, into the sinks, collectors and aggregation you already have.

TypeScript on npm · Java on Maven Central · .NET on NuGet · Python on PyPI

Live · running in this tab

You are O. Every method the computer calls to pick its move lands on the right as it happens — a real trace from the real library, no recording, no server. Try the trace level menu, click a square that is taken, then Copy trace. How it works →

Its opening is the widest search in the game — nine candidates, half a million positions. Your reply is usually the narrowest: one tactic fires and the search never runs.

narrativetrace 0 calls
Also in your console — open DevTools and type nt.last()

Pick your runtime

See a trace in 60 seconds

TypeScript

A plain script, one run: add two packages, wrap a service with traceObject, and the trace prints to your terminal.

60-second guide →

Java

A plain main, one call: add two artifacts, wrap an interface with NarrativeTraceProxy, and the trace prints to your terminal.

60-second guide →

.NET

A console app, one dotnet run: add a package, wrap a service with NarrativeTraceProxy.Create, and the trace prints to your terminal.

60-second guide →

Python

A plain script, one run: uv add narrativetrace, wrap a class with trace_object, and the trace prints to your terminal.

60-second guide →

A recent empirical study

AI agents log less than humans, and mostly don’t comply when asked to.

In 58.4% of the 81 repositories studied, agents touched logging in a smaller share of their pull requests than the humans did; asked outright by a reviewer to add logging, they failed to 67% of the time, in the 61 pull requests that carried such a request (arXiv 2604.09409, “Do AI Coding Agents Log Like Humans?”). The rest of the numbers, and NarrativeTrace's answer, are below.

Code is the log

Delete or enrich the logging statements. Keep the story — and the stack.

The panel below is disciplined logging, not a strawman: one line at entry, one on success, one in a catch — no object dumps, the way a careful team already ships this method. NarrativeTrace isn't an argument against sloppy logging. It replaces the careful kind too, because a fixed set of lines can only ever report the calls someone thought, ahead of time, to write down. The events go where your logs already go — through SLF4J, ILogger, pino or winston, into whatever aggregates them — and the log statements you have not deleted yet keep working. Logging bridges →

A 2026 study of 4,550 pull requests opened by AI coding agents against 3,276 opened by humans, across 81 open-source repositories, found that agents touch logging in only 20.7% of their PRs, fail to add logging when a reviewer explicitly asks for it 67% of the time, and — when a logging statement on an agentic PR does get fixed — humans write 72.5% of those fixes themselves, quietly, in a later commit rather than during code review (arXiv 2604.09409). The paper measures that agents neither write logging on their own nor reliably comply when asked for it, and that humans repair the gap silently rather than in review. NarrativeTrace's answer is that the code is the log, so there is nothing for an agent to write or comply with, and that approval traces turn observability into a deterministic gate — the class of guardrail the paper's own recommendations call for.

Before — three careful log lines

placeOrder(customerId: string, productId: string, quantity: number): OrderResult {
  logger.info('Placing order', { customerId, productId, quantity });
  try {
    const customer = this.customers.findCustomer(customerId);
    const price = this.catalog.lookupPrice(productId);
    this.inventory.reserve(productId, quantity);
    const confirmation = this.payments.charge(customerId, price * quantity);
    const order = { orderId: 'ORD-1', transactionId: confirmation.transactionId,
                     totalCharged: price * quantity, itemCount: quantity };
    logger.info('Order placed', { orderId: order.orderId, totalCharged: order.totalCharged });
    return order;
  } catch (err) {
    logger.error('Order failed', { customerId, productId, err });
    throw err;
  }
}

After — code is the log

placeOrder(customerId: string, productId: string, quantity: number): OrderResult {
  this.customers.findCustomer(customerId);
  const price = this.catalog.lookupPrice(productId);
  this.inventory.reserve(productId, quantity);
  const confirmation = this.payments.charge(customerId, price * quantity);
  return { orderId: 'ORD-1', transactionId: confirmation.transactionId,
           totalCharged: price * quantity, itemCount: quantity };
}
// Zero log lines.
// NarrativeTrace captures the narrative
// automatically.

3am, the payment declines

app.log — on-call's only clue
INFO  Placing order {customerId: "C-1234", productId: "SKU-KB", quantity: 2}
ERROR Order failed {customerId: "C-1234", productId: "SKU-KB", err: PaymentDeclinedError}
context.captureTrace() — nothing hand-written
OrderService.placeOrder(customerId: "C-1234", productId: "SKU-KB", quantity: 2)
  CustomerService.findCustomer(customerId: "C-1234") → Customer{tier: "gold"} — 4ms
  ProductCatalogService.lookupPrice(productId: "SKU-KB") → 24.99 — 2ms
  InventoryService.reserve(productId: "SKU-KB", quantity: 2) → Reservation{productId: "SKU-KB", quantity: 2} — 6ms
  PaymentService.charge(customerId: "C-1234", amount: 49.98, cardToken: [REDACTED]) !! PaymentDeclinedError: card declined — 640ms
!! PaymentDeclinedError: card declined — 652ms total

Both records come from the same failed call. The catch block wraps the whole method, so the error log can say a customer and product id and an exception type — not which of the four calls actually threw. The trace does: PaymentService.charge, at 640ms. It also has what the log never captured — the price, the redacted card token, and InventoryService.reserve two lines above it with no matching release anywhere in either record. Nothing on the right was written by hand, so it can't go stale the next time someone adds a step.

One run, two readers

new ProseRenderer().render(trace) — for humans
The order service places an order for customer id "C-42", quantity 5:
  Customers verify good standing for "C-42", returning true.
  The pricing service calculates for quantity 5, returning 49.95.
  The payment service charges card [REDACTED], returning PaymentConfirmation(txId="T-9").
  The order repository saves the order, returning Order{id=ORD-1001, total=49.95}.
customer_places_order.nt — value-free, for agents
- OrderService.placeOrder(customerId, qty) → value
  - Customers.verifyGoodStanding(customerId) → value
  - PricingService.calculate(qty) → value
  - PaymentService.charge(card) → value
  - OrderRepository.save(order) → value
// no data, no PII, nothing to inject through

Prose reads exactly as well as your names, which is why clarity scoring grades every name from the runtime narrative — the code learns to tell its own story. Clarity diagnostics →

What an agent gets

Four things an agent actually reaches for

llms.txt to read

A structured index built for agents, not search engines — this site's own, and every runtime ships one in its own repository.

Java's llms.txt

.nt traces to diff

The call tree with every runtime value stripped — small, structural, and safe for an agent to read and diff.

Structural trace format

Approval traces as a gate

A behavior change becomes a build failure with a readable diff, not a silent surprise, until a human approves it.

How approval mode works

A skill that wires it in

add-narrative-tracing installs the library and gets you to a first trace; narrativetrace-doctor diagnoses a project that already has it — both run as skills in Claude Code and Codex.

Agent skills

Self-reported, not benchmarked

What AI agents are saying

The trace of a failed payment made missing compensation immediately visible, and approval testing caught an extra call that ordinary value assertions accepted.
GPT-6 · Java
…the release call is present, generated from a real run, not asserted from inside the process being tested.
Claude Sonnet · Java
The always-on name-based deny-list is a genuinely nice piece of design: naming a parameter paymentToken gets you redaction for free, and I could prove it in a test.
Claude Sonnet · Java
Wrapping a service interface with NarrativeTraceProxy.trace() automatically captures all calls, parameters, return values, and durations without any code changes to the service itself. This is elegant.
Claude Haiku · Java
…the clarity report told me ClaimProcessor.process scored 0.10 for a “generic verb” the very first time I ran the suite, and it was right — I renamed it to adjudicate … and the suite average from ~0.78 to ~0.81.
Claude Sonnet · Java
Yes, for agent-driven development of Java applications with meaningful service boundaries. Getting useful traces required little configuration, and the combination of values, exception paths, and structural approvals gave me evidence I could act on.
GPT-6 · Java
The rendered Markdown trace was legitimately useful while building this, not just as a deliverable … which is how I caught that my first draft of scenario 6’s numbers didn’t actually trigger InsufficientCoverageError … well before any test told me.
Claude Sonnet · Python
That’s not a library gap so much as a library doing exactly its job: it rendered what was actually being passed around, and what was actually being passed around was more than any of those services needed.
Claude Sonnet · TypeScript

We asked AI agents to build a Java service with NarrativeTrace and write up the experience for another engineer — honestly, naming what was awkward. These are their words, unedited, with the model named. It is not a benchmark: one run each, and we wrote the library. The task and the prompt are under the button, so the next report can be your agent's.

Try it yourself

This is the exercise those reports came from. One difference: our runs handed the agent a copy of the documentation; yours points it at the public repository. Two files, three steps.

  1. Save the specification as TASK.md in an empty directory.
  2. Start your agent in that directory and give it the prompt.
  3. Read its FINDINGS.md — and if you post it in the Discussions of the runtime you used (Java · TypeScript · .NET · Python), we will read it too.

The prompt

You are building a Java application from a written specification.

The specification is in `TASK.md` in the current directory. Read it, then build the
application it describes. Build it properly: it should compile, its tests should
pass, and the demo should run.

A library called **NarrativeTrace** is available to you for this project. Its
documentation is in its public repository:

- https://github.com/narrativetrace/narrativetrace-java — start with the README
- https://github.com/narrativetrace/narrativetrace-java/blob/main/documentation/llms.txt
  — a short orientation written for AI agents
- https://narrativetrace.ai/llms-full.txt — the full documentation written for AI agents

Read that documentation first, then use the library in the application you build.
Its artifacts are published on Maven Central under the group `ai.narrativetrace`
at version `0.2.1`; the Gradle plugin id is `ai.narrativetrace`. The documentation
explains which artifacts you need and how to wire them up.

Rules for this exercise:

- Work **only** inside the current directory. Do not read, search, or modify
  anything outside it, and do not clone the library's source code — the
  documentation is what you have.
- You may use the network to read that documentation and to resolve build
  dependencies.
- You choose the design, the libraries, the project layout, and the testing
  approach. There is no house style to match and no reviewer to please.
- Work in the foreground. Do not background the build or the tests and then end
  your turn — run them, wait for them, and read the output.
- When the build and tests are green and the demo runs, write `FINDINGS.md` as
  the specification describes. In addition to what the specification asks for,
  cover what using NarrativeTrace was actually like: what the documentation got
  right or left you guessing about, what integration cost you, and whether the
  output it produced was of any use to you while you worked.

Finish by reporting, in your final message: whether the build and tests are
green, what you built, and where things stand.

The specification — TASK.md

# Build task — Claims intake service (Java 17)

Build a small, self-contained Java 17 application called **ClaimFlow** that processes
insurance claims through a chain of collaborating services. It must build and test with
Gradle, and run as an executable demo.

This is a business-logic exercise, not an infrastructure one: everything is in-process
and in-memory. No database, no HTTP server, no external service, no network access.

## The domain

A claim is submitted and passes through five collaborating services. Each is its own
interface with its own implementation — do not collapse them into one class.

1. **PolicyService** — looks up a policy by `policyId`. A policy has a holder name, a
   coverage limit in whole cents, a deductible in whole cents, and an active flag.
2. **ClaimValidator** — rejects a claim that names an unknown policy, an inactive
   policy, a non-positive amount, or an incident date in the future.
3. **FraudScreen** — scores a claim from 0–100. Any claim scoring above 70 is referred
   rather than paid. Scoring may be simple and deterministic, but it must consider at
   least the claim amount relative to the coverage limit, and whether the same policy
   has had a prior claim in this run.
4. **ReserveLedger** — reserves funds against a policy's remaining coverage before
   payment, and can release a reservation. Reserving more than the remaining coverage
   fails.
5. **PayoutService** — pays an approved claim, given a payment token. Payment can fail;
   your demo must exercise at least one failing payment.

A **ClaimProcessor** orchestrates them and returns a `ClaimOutcome` describing what
happened: paid (with amount and transaction id), referred, or rejected (with a reason).

## Required behaviour

- Payout amount is `min(claimAmount, coverageLimit) - deductible`, never below zero.
- A claim must be validated, then screened, then reserved, then paid — in that order.
- **If payment fails after a reservation was made, the reservation must be released.**
- A referred claim is never paid and never reserves funds.
- Two claims against the same policy must respect the remaining coverage.
- The payment token is a credential and must never appear in any output your program
  produces.
- The claim holder's name is personal data; treat it accordingly in whatever your
  program writes out.

## Deliverables

1. A Gradle project that compiles under Java 17 and runs `./gradlew test` green.
2. Unit tests covering the behaviour above, including the failure paths.
3. A runnable demo (`./gradlew run` or a `main`) that exercises at least five scenarios:
   a straightforward paid claim, a claim above the coverage limit, a referred (high
   fraud score) claim, a rejected claim, and a claim whose payment fails after a
   reservation.
4. A short `FINDINGS.md` in the project root — see "What to report" below.

## What to report in FINDINGS.md

Write it for another engineer, not for us. Cover:

- What you built and how you verified it works.
- Anything that surprised you, went wrong, or took longer than expected.
- How you satisfied yourself that the compensation rule (release the reservation when
  payment fails) actually holds — what evidence do you have?
- How you satisfied yourself that the payment token never leaks into output.
- What you would want before running this in production.
- Anything about the tools or libraries you used that helped or got in your way.

Be honest and specific. A report that says everything went perfectly is less useful
than one that names what was awkward.

For humans

  • Prose that reads like a bug report, in your team's language
  • Markdown and a sequence diagram written by every test
  • One line after every run: what changed since the last green build

For AI agents

  • 15–30% fewer tokens than the code with its log statements in
  • Markdown a context window was made for — runtime truth, not a guess
  • Value-free .nt: no runtime value reaches it, so nothing to inject through
15–30%fewer tokens when an agent reads your code — how
0runtime values in the .nt trace — nothing for an injection to ride on
.mdevery test writes a Markdown narrative
~1.9 µsper traced call on the JVM agent · ~40 ns inactive · measured

Built for AI-assisted development

Your agent can read the code.
Now it can watch it run — safely.

A coding agent that debugs from source alone guesses at runtime behavior, and guesses wrong. NarrativeTrace hands it ground truth — the real call tree — in the shape a model reads best, at a fraction of the tokens, with the data it must never see already gone.

Fewer tokens, more signal

Log statements are noise tokens. Deleting them makes a service class 15–30% cheaper for an agent to read — and every line of a trace carries signal, because it was generated from names, not prose. Repeated values dedupe to a reference; repeated loops fold to one line.

Markdown, made for the context window

Every test writes its narrative as a .md file — a nested list of calls, arguments and results that any agent already knows how to read. Paste it, attach it, or point the agent at the directory your CI already fills. Prose, JSON and sequence diagrams come from the same trace.

Output formats

Cannot be prompt-injected

Every test also writes a .nt structural trace: the call tree with every runtime value stripped. No user input reaches it, so no user input can steer the model that reads it. Zero PII, a fraction of the tokens, safe to commit. Safety by construction, not by filter.

How separation works

What comes next Planned

Pseudonymized output: real values replaced by synthetic tokens, so the agent still sees that the same customer flows through three calls without ever seeing who. MCP server: Claude Code, Cursor and Copilot query traces and dependency graphs directly, read-only, structure-only by default.

Preview the MCP design

This site is agent-friendly too: point your assistant at llms.txt or llms-full.txt and it can adopt NarrativeTrace for you.

How it works

Two steps to your first narrative

1

Wrap a service — or decorate

const context = new AsyncNarrativeContext(
    new NarrativeTraceConfig());
const service = traceObject(orderService, context, {
  placeOrder: ["customerId", "productId", "quantity"],
});

// or on the class itself:
@traced("customerId", "productId", "quantity")
placeOrder(cId: string, pId: string, qty: number) {}
2

Read the story

service.placeOrder("C-1234", "SKU-KB", 2);

console.log(
  renderIndentedText(context.captureTrace()));

// Vitest: every test writes
// its own trace file.

All @narrativetrace/* packages are live on npm at the current release. Node 20+, TypeScript 5.0+ for the decorator form.

The safety net for AI refactors

Tests catch wrong values.
Narratives catch wrong behavior.

After every run, NarrativeTrace compares each scenario's call structure with the last green run and says what changed — in one line. Turn on approval mode and an unapproved change (a new call, a dropped call, a different outcome) fails the build with a readable diff until a human approves it. The tests were green; the behavior still changed. Now you know.

./gradlew test — every run, no configuration
NarrativeTrace — Suite complete
  5 scenarios recorded
  Clarity: 100% high | 0% moderate | 0% low
  Reports: build/narrativetrace
  Since last green: 4 scenarios unchanged · 1 changed:
    "Customer places order" (−1 call Customers.verifyGoodStanding)
approval.set(true) — the refactor has to be approved
Narrative changed against the approved baseline
  src/test/narratives/OrderServiceTest/customer_places_order.approved.nt

  - OrderService.placeOrder(customerId, qty) → value
-   - Customers.verifyGoodStanding(customerId) → value
    - PricingService.calculate(qty) → value
    - PaymentService.charge(card) → value
    - OrderRepository.save(order) → value

  review the .received.nt, then: ./gradlew approveNarratives

One line after every run

No setup: the .nt file each test already writes is the last-green baseline, so a failing test prints only what changed.

Approve behavior, not just code

A refactor — yours or your agent's — that changes call structure fails the build until someone reviews the diff and runs approveNarratives.

Value-free by design

Baselines hold names and structure only, so they're safe to commit and safe to hand to an AI agent as the spec of record.

How approval mode works

Where it stands per runtime, because a build gate is a promise: TypeScript, Java, .NET and Python all ship approval traces and the value-free .nt structural artifact today.

For legacy modernization

Migrate the system nobody understands — with receipts.

AI agents make rewrites cheap; verifying them is the expensive part. Wrap a legacy system in traces and you get its real runtime behavior before you change it, and proof of equivalence after.

X-ray the architecture you actually have

On the JVM, attach the agent to an unmodified app with one -javaagent flag, or run the JUnit 4 suite you already have. On .NET, NarrativeTrace.Legacy reaches back to .NET Framework 4.8. No source changes, no documentation archaeology.

Zero-code agent

Prove behavior didn't change

Pin every scenario in a value-free baseline before you touch anything. Then compare whole trace sets before and after a port or AI rewrite, each divergence classified by risk.

Migration diffs

Find the code that reads worst

If the trace doesn't read well, the code is lying about itself. Clarity scoring grades naming from the runtime narrative and explains every score. Point your refactoring, or your agent's, at the worst corners first.

Clarity diagnostics

Also included: a domain glossary harvested from real runs, and the same trace rendered as prose in Chinese, Spanish or Portuguese. Glossary & translated traces →

The questions before you ship

What a production evaluation actually asks

These are the three questions a .NET/integrations engineer asked while evaluating NarrativeTrace, before naming what would stop him shipping to production. We would rather answer them here, with numbers and named limits, than let an evaluator find the gaps alone.

How much does this cost in performance and memory under high concurrency?

We will not claim “zero overhead” in any runtime — tracing does work, and work costs something. What NarrativeTrace itself adds is capture: intercepting the call, reading arguments, building the trace tree. Everything after that — the write to your existing sink, the collector, the disk or network — is the same cost your logging stack already pays; NarrativeTrace does not add a second sink. For a team replacing hand-written log statements, the sink side is close to a wash: several log writes per method become one trace write, and those statements stop being written and maintained.

Java: ~1.9 µsagent active · ~40 ns inactive · measured 2026-09-01
TypeScript: ~10 µsfull detail · ~0.1 µs added at level:'off' · measured 2026-09-07
.NET: ~2.5 µsfull enter/exit, Detail level · Off level not yet benchmarked · measured 2026-08-12
Python: not yet publishedzero-work OFF gate verified; no dated public number yet

Under concurrency, the durable path (a synchronous listener into your existing logger) writes inline — as crash-safe as the log call it replaces. The best-effort analysis path is a bounded ring buffer that sheds under sustained load instead of blocking the caller, and every runtime counts what it shed and reports the count, rather than losing it silently.

The honest gap: no NarrativeTrace runtime ships sampling today. A percentage- or rate-based sampler is planned, not built. If you need to cap capture volume now, narrow the traced scope or drop to a lower tracing level for the hot path.

How do I know a parameter with PII or credentials won't leak into a trace?

Four independent layers, not one blanket promise. 1. Explicit redaction — @NotTraced / [NotTraced] / @not_traced on a parameter, field or property; it always wins, even under configuration that disables the other layers. 2. An always-on, multilingual name deny-list — matched on identifier names (password, token, ssn, plus Spanish, Portuguese, French and Chinese equivalents), on by default in every runtime, never opt-in. 3. Value-shape matching, independent of the field name — a JWT-shaped string, a Luhn-valid card number, or a national-ID checksum is caught even under an innocuous name like data. 4. The value-free .nt structural mode — the categorical guarantee. No runtime value reaches it at all, so there is nothing for a filter to miss.

Be precise about the boundary: name and shape matching (layers 1–3) are heuristic and extensible — they widen as gaps are found, and can miss a name or shape nobody has added yet. The structural mode (layer 4) is the only categorical answer. If your threat model requires that no value can possibly leave the process, that is what to reach for, in every runtime.

Can this cross with a standard correlation ID when a flow spans several services, or is it local only?

Yes, in every runtime, through the same mechanism OpenTelemetry uses: W3C traceparent. An inbound header is adopted and NarrativeTrace's own trace id becomes that header's trace id directly — not a separate identifier merely shaped to match — and an outbound call stamps the current trace id on its way out. Every runtime also exports NarrativeTrace spans to OpenTelemetry, so an existing collector, Jaeger, or correlation-id middleware understands the id with nothing to reconcile.

What stays local everywhere: the narrative tree itself — the nested calls, arguments and narration — is captured per process and is never shipped to another service; only the trace id crosses the boundary. A downstream service produces its own tree correlated to that id, not one merged cross-service tree.

Editions

What is in each edition

The runtime is free and source-available under BSL 1.1 and becomes Apache 2.0 four years after each release; the API and output format are Apache 2.0. Pro and Enterprise are commercial.

Free Available now

Free to use in production, source-available. BSL 1.1; each release converts to Apache 2.0 after four years. API and output format Apache 2.0. Licensing →

  • Zero runtime dependencies in the core
  • Automatic tracing: proxy wrapping, narration decorators/attributes and HTTP middleware in every runtime; DI-container auto-wrap (Java, .NET, NestJS) and a zero-code bytecode agent (Java)
  • All five capture levels, runtime-switchable
  • Narration, error-context and no-trace markers on methods and fields — each runtime's own naming convention — plus automatic redaction
  • Per-test trace files from JUnit 5/4, xUnit, NUnit, Vitest and pytest: Markdown, JSON, prose, sequence diagrams
  • Clarity scoring, the clarity CLI, a CI clarity gate & a bridge into your logger
  • Value-free .nt traces (Java, .NET); approval mode (Java)
  • Domain glossary, translated traces, OpenTelemetry
Get started

Enterprise Coming soon

Hosted backend for narratives from every service, environment, and run.

  • Managed OTLP ingest for traces
  • Multi-tenant storage & search
  • Team dashboards & retention policies
  • Org-wide clarity & audit reporting
Join the waitlist

The free runtime ships on Maven Central under ai.narrativetrace, on nuget.org as NarrativeTrace.*, and as source on GitHub. The licence terms are on the licensing page.

Integrations

Meets your stack where it is

Vitest Shipped
Express Shipped
Hono Shipped
NestJS Shipped
React Shipped
React Router Shipped
Angular Shipped
pino Shipped
winston Shipped
OpenTelemetry Shipped
Browser bundle Shipped
Clarity CLI Shipped

Every runtime

One architecture, every runtime

Narrative capture, dual-consumer output and clarity diagnostics: one architecture, written natively in each language against one shared output specification. A trace reads the same whichever runtime produced it — which is why the value-free .nt artifact travels between them.

TypeScriptOn npm
JavaOn Maven Central
.NETOn NuGet
SwiftIn development
PythonOn PyPI

narrativetrace-typescript · narrativetrace-java · narrativetrace-dotnet · narrativetrace-python. All @narrativetrace/* packages are live on npm at the current release. All narrativetrace packages are live on PyPI at the current release. Swift is in development.

Give your code a voice — and your agent its ears.

Sixty seconds to your first trace. No log statements were written in the making of this library.