Instrumentation
Nested LLM traces with spans and generations
Build parent-child LLM traces for retrievers, tools, agents, and model calls in TypeScript or Python, with error and flush handling.
Nested LLM traces show the order and parent-child relationship of every step in a request. Use them when one root trace contains retrieval, tool calls, routing, or more than one model generation. The result is a tree you can inspect from the root request down to the slow or failed operation.
Choose the right node type
A Currai trace tree uses three main node types:
- A trace is the root unit of work, such as one chat turn or agent run.
- A span records non-model work, including retrieval, tool execution, and application code.
- A generation records a model call, including the model, input, output, parameters, token usage, and cost.
Create the first span or generation from the trace. To create a child node, call
span() or generation() on an existing span. Currai assigns the trace ID and
parent observation ID when the child is created.
Build a nested trace in TypeScript
This example records retrieval as a span, reranking as a generation inside that span, and the final answer as a generation on the root trace.
const trace = currai.trace({
name: "rag-answer",
userId,
sessionId,
input: { question },
});
const retrieval = trace.span({
name: "retrieval.search",
input: { question, limit: 8 },
});
try {
const candidates = await vectorStore.search(question, { limit: 8 });
const rerank = retrieval.generation({
name: "retrieval.rerank",
model: "gpt-4o-mini",
input: { question, candidates },
});
const ranked = await rerankDocuments(question, candidates);
rerank.end({ output: ranked });
retrieval.end({ output: { documentIds: ranked.map((doc) => doc.id) } });
const answer = trace.generation({
name: "answer.generate",
model: "gpt-4o",
input: { question, context: ranked },
});
const response = await generateAnswer(question, ranked);
answer.end({ output: response.text, usage: response.usage });
trace.update({ output: response.text });
} catch (error) {
retrieval.end({
level: "ERROR",
statusMessage: error instanceof Error ? error.message : String(error),
});
throw error;
} finally {
await currai.flushAsync();
}The trace appears with this shape:
rag-answer (trace)
├── retrieval.search (span)
│ └── retrieval.rerank (generation)
└── answer.generate (generation)The final answer is not a child of retrieval because it represents the next top-level stage in the request. The reranker is a child of retrieval because it is part of choosing documents.
Build the same tree in Python
The Python SDK uses the same data model and snake_case field names.
trace = currai.trace(
name="rag-answer",
user_id=user_id,
session_id=session_id,
input={"question": question},
)
retrieval = trace.span(
name="retrieval.search",
input={"question": question, "limit": 8},
)
try:
candidates = vector_store.search(question, limit=8)
rerank = retrieval.generation(
name="retrieval.rerank",
model="gpt-4o-mini",
input={"question": question, "candidates": candidates},
)
ranked = rerank_documents(question, candidates)
rerank.end(output=ranked)
retrieval.end(output={"document_ids": [doc.id for doc in ranked]})
answer = trace.generation(
name="answer.generate",
model="gpt-4o",
input={"question": question, "context": ranked},
)
response = generate_answer(question, ranked)
answer.end(output=response.text, usage=response.usage)
trace.update(output=response.text)
except Exception as error:
retrieval.end(level="ERROR", status_message=str(error))
raise
finally:
currai.flush()End every node where the work finishes
Call end() on each span and generation after its operation completes. This
records the output and end time on the correct node. End a parent span after its
children finish so its duration covers the complete nested operation.
For failed work, end the active node with level: "ERROR" and a useful status
message before rethrowing. The failed node then stays in the trace tree with the
context that led to the error.
Short-lived scripts, jobs, and serverless handlers must flush before the process
exits. Use await currai.flushAsync() in TypeScript or currai.flush() in
synchronous Python code. A missing flush can make a correctly built tree appear
incomplete because queued events never reached the ingestion endpoint.
Nest tools and agent loops
A span can contain more spans and generations, so the same pattern works for an agent loop:
const iteration = trace.span({ name: "agent.iteration", input: state });
const decision = iteration.generation({
name: "agent.choose-tool",
model: "gpt-4o-mini",
input: state,
});
decision.end({ output: toolChoice });
const tool = iteration.span({
name: `tool.${toolChoice.name}`,
input: toolChoice.arguments,
});
tool.end({ output: toolResult });
iteration.end({ output: { tool: toolChoice.name } });Use stable, descriptive names such as retrieval.search, tool.weather, or
agent.choose-tool. Put request-specific values in input or metadata instead of
the name. Stable names make latency and error comparisons useful across traces.
OpenTelemetry parent context
If your service already uses OpenTelemetry, create spans inside the active OTel context and export them to Currai. OTel propagates the parent context for you, so you do not need to rebuild the tree with Currai SDK objects. Avoid instrumenting the same operation with both approaches unless you intend to store two copies.
See OpenTelemetry ingestion for exporter configuration and GenAI attribute mapping.
Troubleshoot a flat or incomplete tree
If every observation appears at the root, check that child nodes are created
from the parent span rather than from the trace. For example, use
retrieval.generation(...), not trace.generation(...), when the model call is
part of retrieval.
If a child is missing, confirm that its end() call runs on success and error
paths, then confirm the client flushes before shutdown. If durations look wrong,
end child nodes before their parent. For more ingestion checks, see
Troubleshooting.
Next: track token usage and cost on generations.
