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.
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.
Java
A plain main, one call: add two artifacts, wrap an interface with
NarrativeTraceProxy, and the trace prints to your terminal.
.NET
A console app, one dotnet run: add a package, wrap a service with
NarrativeTraceProxy.Create, and the trace prints to your terminal.
Python
A plain script, one run: uv add narrativetrace, wrap a class with
trace_object, and the trace prints to your terminal.
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.
Before — three careful log lines
public OrderResult placeOrder(String customerId, String productId, int quantity) {
logger.info("Placing order for customer {} product {} quantity {}",
customerId, productId, quantity);
try {
var customer = customerService.findCustomer(customerId);
var price = catalogService.lookupPrice(productId);
inventoryService.reserve(productId, quantity);
var payment = paymentService.charge(customerId, price * quantity);
var result = new OrderResult("ORD-1", payment.transactionId(), price * quantity, quantity);
logger.info("Order placed: orderId={} totalCharged={}", result.orderId(), result.totalCharged());
return result;
} catch (Exception e) {
logger.error("Order failed for customer {} product {}", customerId, productId, e);
throw e;
}
}
After — code is the log
public OrderResult placeOrder(String customerId, String productId, int quantity) {
customerService.findCustomer(customerId);
var price = catalogService.lookupPrice(productId);
inventoryService.reserve(productId, quantity);
var payment = paymentService.charge(customerId, price * quantity);
return new OrderResult("ORD-1", payment.transactionId(), price * quantity, quantity);
}
// Zero log lines.
// NarrativeTrace captures the narrative
// automatically.
Before — three careful log lines
public OrderResult PlaceOrder(string customerId, string productId, int quantity)
{
_logger.LogInformation("Placing order for {Customer} product {Product} qty {Qty}",
customerId, productId, quantity);
try
{
var customer = _customers.FindCustomer(customerId);
var unitPrice = _catalog.LookupPrice(productId);
_inventory.Reserve(productId, quantity);
var payment = _payments.Charge(customerId, unitPrice * quantity, $"tok_{customer.Id}");
var order = new OrderResult(NextOrderId(), payment.TransactionId, unitPrice * quantity, quantity);
_logger.LogInformation("Order placed: {OrderId} total {Total}", order.OrderId, order.TotalCharged);
return order;
}
catch (Exception ex)
{
_logger.LogError(ex, "Order failed for {Customer} product {Product}", customerId, productId);
throw;
}
}
After — code is the log
public OrderResult PlaceOrder(string customerId, string productId, int quantity)
{
var customer = _customers.FindCustomer(customerId);
var unitPrice = _catalog.LookupPrice(productId);
_inventory.Reserve(productId, quantity);
var payment = _payments.Charge(customerId, unitPrice * quantity, $"tok_{customer.Id}");
return new OrderResult(NextOrderId(), payment.TransactionId, unitPrice * quantity, quantity);
}
// Zero log lines.
// NarrativeTrace captures the narrative
// automatically.
Before — three careful log lines
def place_order(self, customer_id: str, product_id: str, quantity: int) -> OrderResult:
logger.info("Placing order", extra={"customer_id": customer_id, "product_id": product_id,
"quantity": quantity})
try:
customer = self._customers.find_customer(customer_id)
price = self._catalog.lookup_price(product_id)
self._inventory.reserve(product_id, quantity)
confirmation = self._payments.charge(customer_id, price * quantity, f"tok_{customer.id}")
order = OrderResult("ORD-1", confirmation.transaction_id, price * quantity, quantity)
logger.info("Order placed", extra={"order_id": order.order_id, "total_charged": order.total_charged})
return order
except Exception:
logger.error("Order failed", extra={"customer_id": customer_id, "product_id": product_id},
exc_info=True)
raise
After — code is the log
def place_order(self, customer_id: str, product_id: str, quantity: int) -> OrderResult:
customer = self._customers.find_customer(customer_id)
price = self._catalog.lookup_price(product_id)
self._inventory.reserve(product_id, quantity)
confirmation = self._payments.charge(customer_id, price * quantity, f"tok_{customer.id}")
return OrderResult("ORD-1", confirmation.transaction_id, price * quantity, quantity)
# Zero log lines.
# NarrativeTrace captures the narrative
# automatically.
3am, the payment declines
INFO Placing order {customerId: "C-1234", productId: "SKU-KB", quantity: 2}
ERROR Order failed {customerId: "C-1234", productId: "SKU-KB", err: PaymentDeclinedError}
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
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}.
- 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 formatApproval 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 worksA 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.
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.
…the release call is present, generated from a real run, not asserted from inside the process being tested.
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.
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.
…the clarity report told meClaimProcessor.processscored 0.10 for a “generic verb” the very first time I ran the suite, and it was right — I renamed it toadjudicate… and the suite average from ~0.78 to ~0.81.
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.
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.
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.
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.
- Save the specification as
TASK.mdin an empty directory. - Start your agent in that directory and give it the prompt.
- 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
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.
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.
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 designThis 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
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) {}
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.
Wrap a service — or auto-wrap
var context = new ThreadLocalNarrativeContext();
var service = NarrativeTraceProxy.trace(
orderService, OrderService.class, context);
// or for Spring:
@EnableNarrativeTrace(
basePackages = "com.example.app")
Read the story
service.placeOrder("C-1234", "SKU-KB", 2);
System.out.println(
new IndentedTextRenderer()
.render(context.captureTrace()));
// JUnit 5/4: every test writes
// its own trace file.
All 17 artifacts are on
Maven Central
under ai.narrativetrace at the current release; the plugin id is
ai.narrativetrace. Java 17+, Gradle 8+.
Wrap a service — or auto-wrap
var context = new SyncNarrativeContext(
new NarrativeTraceConfig(TracingLevel.Detail));
var service = NarrativeTraceProxy
.Create<IOrderService>(new OrderService(), context);
// or for the DI container:
services.AddNarrativeTracing(options => options
.Namespaces("MyApp.Services", "MyApp.Domain"));
Read the story
service.PlaceOrder("C-1234", "SKU-KB", 2);
Console.WriteLine(
IndentedTextRenderer.Render(
context.CaptureTrace()));
// xUnit / NUnit: every test writes
// its own trace file.
Every package is live on
nuget.org at the
current release. Targets net10.0 and netstandard2.0, with
NarrativeTrace.Legacy reaching back to .NET Framework 4.8 — and no
compiler flag, because .NET keeps parameter names in metadata already.
Wrap a service — or narrate it
context = ContextVarNarrativeContext()
service = trace_object(OrderService(), context)
# or narrate a method by hand:
@narrated("Placing order of {quantity} {product_id} for customer {customer_id}")
def place_order(self, customer_id, product_id, quantity): ...
Read the story
service.place_order("C-1234", "SKU-KB", 2)
print(MarkdownRenderer().render(context.capture_trace()))
# pytest: every test writes
# its own trace file.
All 8 packages are on PyPI at the current release —
uv add narrativetrace. Python 3.12+;
@on_error stacks the same way for failure narration.
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.
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)
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.
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 diffsFind 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 diagnosticsAlso included: a domain glossary harvested from real runs, and the same trace rendered as prose in Chinese, Spanish or Portuguese. Glossary & translated traces →
Before your architect asks
Fits the stack you have. Says so in writing.
ILogger, pino and winston
OpenTelemetrynarratives as spans, in every runtime
~1.9 µsper traced call on the JVM, measured; ~40 ns inactive
Redaction@NotTraced plus name-based deny-lists, on by default
Value separationthe architecture behind the .nt trace
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.
level:'off' · measured 2026-09-07Under 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
.nttraces (Java, .NET); approval mode (Java) - Domain glossary, translated traces, OpenTelemetry
Pro Early access
Cross-trace analytics, AI integration, and compliance for teams.
- Flow summaries & path frequency analysis
- Migration diffs with risk classification
- Runtime dependency graphs
- Audit & SecOps events with policy engine (Java)
- MCP server & tiered AI output for coding agents — coming soon
- Approval governance: semantic diffs, PR review bots, signed baselines — planned
- Clarity trending, AI-assisted renaming, dead-code detection — planned
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
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
ILogger ShippedEvery 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.
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.