Building TypeScript-Native Observability: Async Context and Execution Flow
A useful agent trace is not a list of timestamps. It is a causal tree. When a TypeScript agent retrieves documents in parallel, calls a model, retries a tool, and falls back to cached data, each operation needs a trace ID, its own span ID, and the correct parent span. Without those relationships, completion order is easily mistaken for execution structure. This article builds a small Node.js tracer to demonstrate the core mechanics: immutable async context, parent-child spans, reliable finalization, and a pluggable sink. It is intentionally smaller than a production observability library, but the design avoids several common mistakes found in minimal examples. Completion Order Is Not Causality Imagine three tools running in parallel: 80 ms search_tickets completes 100 ms load_account completes 120 ms search_docs completes Those timestamps describe completion order. The execution tree describes why the operations existed: research_agent └─ parallel_retrieval ├─ search_docs ├─ search_tickets └─ load_account Both views are useful, but only the tree preserves the relationship between the agent decision and its child tools. The normal JavaScript call stack cannot serve as that tree. Async work may resume later, execute concurrently, or outlive the function that scheduled it. Tracing therefore needs an explicit logical context. The Context We Need Each asynchronous branch needs two values: type TraceContext = { traceId : string ; parentSpanId : string | null ; }; When a new span starts, it reads the current context, records parentSpanId , creates its own spanId , and runs child work inside a new context whose parent is that span. In Node.js, AsyncLocalStorage provides the propagation primitive. It carries a value through normal asynchronous resources without adding trace parameters to every application function. Do not mutate one shared context object. Parallel siblings would race to replace the current span. Create a new context value for every nested span instead. Defin