Trace Spans
This page is the reference for the spans agrag records when you pass it a
tracer. Every span name, attribute and provider value below is what the
library emits today.
Tracing is opt-in per object. A tracer of None opens no recorded span and
costs nothing, and agrag never installs a global provider: the tracer is the
one you inject. See Trace an Agent Run for
how to build and pass one.
The LLM span tree
Every BAML-backed path produces the same three spans, nested. The request
span is written after the attempt finishes, from the data BAML collected, so it
carries the real request and response.
agrag.extraction.extract_chunk (INTERNAL)
agrag.extraction.baml (INTERNAL)
agrag.llm.call (INTERNAL)
agrag.llm.attempt (INTERNAL)
agrag.llm.request (CLIENT, kind LLM)
| Span | Kind | Attributes |
|---|---|---|
agrag.llm.call | INTERNAL | agrag.llm.function, agrag.llm.attempt_count |
agrag.llm.attempt | INTERNAL | agrag.llm.attempt (1-based) |
agrag.llm.request | CLIENT | the table below |
agrag.llm.function is the BAML function name, such as
ExtractEntitiesAndRelations. The attempt span records the exception when an
attempt fails. The call span is ERROR when the call as a whole fails.
Ingestion spans
| Span | Attributes |
|---|---|
agrag.ingestion.load_document | agrag.source_uri, agrag.loader_name, agrag.normalization (JSON) |
agrag.ingestion.chunk_document | agrag.document_key, agrag.chunker.strategy, agrag.chunker.hash, agrag.chunker.settings (JSON), agrag.chunker.rule (a rule index or fallback), agrag.chunks_produced |
agrag.storage.embed_chunks | agrag.embedding.heading_context (bool, whether the embedder got the heading path) |
Domain spans by path
Each LLM path also opens one small domain span carrying its own ids and counts.
| Path | Domain span | Attributes |
|---|---|---|
| Extraction, BAML | agrag.extraction.baml | agrag.chunk_id, agrag.entities_extracted, agrag.relations_extracted, agrag.extraction.heading_context (bool, whether a section line was sent) |
| Extraction, GLiNER | agrag.extraction.gliner, and agrag.extraction.model_load around the one-time model build | the same counts; agrag.model on the load span |
| Extraction, escalating | agrag.extraction.escalating | agrag.chunk_id, agrag.escalated (bool) |
| Resolution | agrag.resolution.llm_verify | agrag.pair_count, agrag.match_count, agrag.uncertain_count, agrag.request_failed (bool) |
| Merge | agrag.merge.resolve_description | agrag.description_count, agrag.summarized (bool) |
| Community | agrag.community.report_batch | agrag.batch_size |
| Chunk retrieval | agrag.retrieval.hydrate_parents | agrag.parent_count, agrag.result.count, retrieval.documents (JSON array of attached parent chunks) |
| Text2cypher | agrag.retrieval.generate_cypher | agrag.is_repair (bool) |
| Judge | agrag.eval.judge | agrag.eval.judge_model |
Two of these are conditional, and that is deliberate:
agrag.merge.resolve_descriptionopens only when the field actually conflicted, that is, when more than one distinct candidate exists. A field every mention agrees on adds no span.agrag.retrieval.generate_cypheropens on every generation, andagrag.is_repairtells a first attempt from a retry after a failure.
agrag.resolution.llm_verify never carries an ERROR status. A failed LLM
call resolves to NO_MATCH, not an error, so the span stays UNSET and sets
agrag.request_failed instead.
The retrieval span tree
One search call fans out to the recipe's retrievers concurrently, so their
spans are siblings under the root. The agrag.graphdb.*, agrag.vectordb.*
and agrag.embedding.* spans nest below these whenever the caller passed the
same tracer to the stores and the embedder.
agrag.retrieval.search RETRIEVER
agrag.retrieval.entity RETRIEVER, one per retriever
agrag.retrieval.allowed_entity_ids only with a document scope
agrag.retrieval.entity_search
agrag.retrieval.vector_search one per adaptive pass
agrag.retrieval.active_member_ids
agrag.retrieval.hydrate_entities
agrag.retrieval.resolve_entity per hit that needs the walk
agrag.retrieval.resolved_entity_search
agrag.retrieval.chunk RETRIEVER
agrag.retrieval.vector_search
agrag.retrieval.hydrate_chunks
agrag.retrieval.community RETRIEVER
agrag.retrieval.text2cypher RETRIEVER
agrag.retrieval.generate_cypher
agrag.retrieval.execute_cypher one per attempt
agrag.retrieval.fuse
agrag.retrieval.bfs RETRIEVER, when recipe.bfs
agrag.retrieval.community_expand when recipe.community_expand
agrag.retrieval.rerank.cross_encoder or rerank.node_distance
agrag.retrieval.rerank.model_load only on a cache miss
agrag.retrieval.find_entity and agrag.retrieval.traverse are RETRIEVER
roots for direct calls that bypass search;
agrag.retrieval.list_relationship_types returns strings and carries no kind.
Inside an agent run, the whole tree nests under the OpenInference TOOL span
of the tool call that made it.
Attributes
| Span | Attributes |
|---|---|
agrag.retrieval.search | agrag.recipe.methods, agrag.recipe.limit, agrag.recipe.bfs, agrag.recipe.community_expand, agrag.recipe.reranker, agrag.failed_methods (when a method failed) |
retrievers (entity, chunk, community, text2cypher, bfs) | agrag.limit; bfs also agrag.seed_count, agrag.depth, agrag.direction; text2cypher also agrag.cypher |
agrag.retrieval.find_entity / traverse | agrag.seed_ids, agrag.relation_type, agrag.direction, agrag.depth, agrag.limit, agrag.community_expand on traverse |
agrag.retrieval.list_relationship_types | agrag.seed_ids, agrag.relation_type_filter, agrag.direction, agrag.result_count, agrag.result_types |
agrag.retrieval.vector_search | agrag.collection, agrag.store (vector_store or graph_native), agrag.labels, agrag.limit, agrag.hit_count, agrag.hit_ids, agrag.hit_scores |
agrag.retrieval.entity_search / resolved_entity_search | agrag.limit, agrag.passes, agrag.hit_count |
agrag.retrieval.hydrate_* | agrag.requested_count, agrag.hydrated_count |
agrag.retrieval.resolve_entity | agrag.entity_id, agrag.hops, agrag.resolved_id |
agrag.retrieval.allowed_entity_ids | agrag.document_count, agrag.entity_count |
agrag.retrieval.active_member_ids | agrag.requested_count, agrag.superseded_count |
agrag.retrieval.execute_cypher | agrag.is_repair, agrag.row_count |
agrag.retrieval.fuse | agrag.rrf_k, agrag.fuse.methods, agrag.fuse.method_counts |
agrag.retrieval.community_expand / community_context | agrag.seed_count, agrag.top_k, agrag.added_count on community_expand |
agrag.retrieval.rerank.cross_encoder | agrag.model, agrag.input_count, agrag.min_score (when set), agrag.skipped (when the extra is missing) |
agrag.retrieval.rerank.model_load | agrag.model |
agrag.retrieval.rerank.node_distance | agrag.seed_count, agrag.input_count, agrag.path_query_count |
Result attributes
Every span that returns a list records its results in full:
agrag.result_count, and arraysagrag.result_ids,agrag.result_scores,agrag.result_kindsandagrag.result_textsholding every result, with no text cap. Array attributes are not subject to the SDK's per-span attribute limit.- For the first 20 results, the OpenInference flattened names
retrieval.documents.N.document.id,.document.content,.document.scoreand.document.metadata(a JSON object withkindandmethod), which trace viewers draw as a retrieval panel.
The query and the scope are always recorded: input.value and agrag.query
carry the query text, and agrag.filters carries the scope as JSON, an empty
scope included. A step that falls back to empty (a failed hydration, a
reranker without its extra) records the exception as an event on the
narrowest span and that span's status stays UNSET.
The agrag.llm.request span
This is the only span a BAML path tags with the OpenInference LLM kind.
| Attribute | Value |
|---|---|
openinference.span.kind | "LLM" |
| span kind | CLIENT |
llm.provider | OpenInference value mapped from BAML's provider id, table below |
llm.model_name | the model named in the response body, else the model in the request body |
llm.token_count.prompt / .completion | Usage.input_tokens / output_tokens, omitted when None |
llm.token_count.total | prompt plus completion, only when both are present |
llm.token_count.prompt_details.cache_read | Usage.cached_input_tokens, only when greater than 0 |
agrag.llm.usage_missing | true when the call succeeded and every usage field was None |
input.value, input.mime_type | the request body text, application/json |
output.value, output.mime_type | the response body text, application/json, or text/plain if it does not parse |
http.response.status_code | the response status |
error.type | the status as a string, when it is 400 or higher |
agrag.llm.baml_provider | BAML's raw provider id |
agrag.llm.client_name | the client that made the request |
agrag.llm.requested_model | the model in the request body |
agrag.llm.selected | whether BAML used this call's reply |
The span's status is ERROR when the status is 400 or higher, or when the call
got no response at all.
Provider mapping
BAML's provider ids map to the same OpenInference values the agent path emits, so the same configuration reports the same provider either way.
| BAML provider id | llm.provider |
|---|---|
openai, openai-generic, openai-responses | openai |
azure-openai | azure |
anthropic | anthropic |
google-ai, vertex-ai | google |
aws-bedrock | aws |
An id outside this table is recorded unchanged, so a newly added provider still produces a usable span.
Counting calls and tokens
To count the LLM calls in a trace, count the spans tagged with the OpenInference LLM kind:
def count_llm_calls(spans):
return sum(
1
for span in spans
if span.attributes.get("openinference.span.kind") == "LLM"
)
Token counts live only on agrag.llm.request spans and on the agent and judge
LLM spans. The parent spans never carry them, so summing
llm.token_count.prompt over every span with that kind is exact and nothing
counts twice.
What is recorded, and what is not
Nothing withholds text. agrag.llm.request records the full request and
response bodies, and the agent and judge spans record the question, tool inputs
and evidence text. The OpenInference redaction environment variables no longer
change any span, on any path. To record less, scrub attributes in a
SpanProcessor you configure, or run without a tracer.
Two things are never recorded on any span: the request headers, which carry the provider key, and the request URL.
Known limits
- An unparsable reply looks successful. BAML coerces a reply it cannot
parse into an empty result rather than raising, so the span's status is
UNSET.output.valueis the evidence that the reply was not usable. - Retries below BAML are invisible. LangChain and the provider SDKs retry
internally, and one agent or judge LLM span can cover several HTTP requests.
The
agrag.llm.requestspans, read from BAML's collector, are exact for the requests BAML itself made. - Token semantics were verified against an OpenAI-compatible endpoint only.
For
anthropic,google-ai,vertex-ai,aws-bedrockandazure-openai, the counts are whatever BAML'sUsagereports. - The SDK caps attributes at 128 by default. A long agent run can reach that
and have attributes silently dropped. Raise
SpanLimits.max_attributeson your provider and use a sampling processor in production. - BAML's error text includes the request URL. It reaches
output.value, exception events andStageFailure.error_message. Never put a provider key in abase_urlquery string.