wrapAgent callback, every withSpan anywhere in the call tree attaches to the active run via AsyncLocalStorage (Node) or contextvars (Python). You do not pass runId down through function arguments. Split your agent across as many files as you like.
- Node.js
- Python
Why this works
wrapAgent opens an AsyncLocalStorage (Node) / contextvars.Context (Python) that carries the active runId. Every withSpan reads that context and attaches to the same run. The context propagates across await boundaries, across imports, across helper functions — anywhere the async chain reaches.
When the context breaks
| Situation | What happens | Fix |
|---|---|---|
worker_threads / ProcessPoolExecutor / new process | Context doesn’t cross the boundary | Capture runId, pass it in, use joinRun |
setImmediate / bare .then() that escapes the awaited tree | Span is dropped | Keep work inside the awaited chain |
| HTTP to another service | Context doesn’t cross the wire | Use propagationHeaders() + middleware |