Documentation Index
Fetch the complete documentation index at: https://docs.trodo.ai/docs/llms.txt
Use this file to discover all available pages before exploring further.
Inside a 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.
// agent.js — entry point
import trodo from 'trodo-node';
import { triage } from './triage.js';
import { research } from './research.js';
import { write } from './write.js';
trodo.init({ siteId: process.env.TRODO_SITE_ID });
export async function support(question) {
const { result } = await trodo.wrapAgent('support-agent', async (run) => {
run.setInput({ question });
const intent = await triage(question); // no runId argument
const facts = await research(intent); // no runId argument
const answer = await write(question, facts); // no runId argument
run.setOutput({ answer });
return answer;
});
return result;
}
// triage.js
import trodo from 'trodo-node';
export async function triage(question) {
return trodo.withSpan({ kind: 'agent', name: 'triage' }, async (span) => {
span.setInput({ question });
// ...LLM call auto-captured as a child span
return 'refund';
});
}
// research.js, write.js — same pattern: import trodo, call withSpan, done.
# agent.py — entry point
import trodo, os
from triage import triage
from research import research
from write import write
trodo.init(site_id=os.environ["TRODO_SITE_ID"])
def support(question):
with trodo.wrap_agent("support-agent") as run:
run.set_input({"question": question})
intent = triage(question) # no run_id argument
facts = research(intent) # no run_id argument
answer = write(question, facts) # no run_id argument
run.set_output({"answer": answer})
return answer
# triage.py
import trodo
def triage(question):
with trodo.span("triage", kind="agent") as span:
span.set_input({"question": question})
# LLM call auto-captured as a child span
return "refund"
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 |
See also