{context} from it.
Spans carry that answer as a stated kind. Whoever produces the span says what it is, the
engine resolves it once at ingest, and every reader gets the same answer back on the wire as
spanKind. This is the same design as LangSmith’s run_type, Langfuse’s observation type, and
OpenInference’s openinference.span.kind.
The vocabulary
Kinds are mostly labels - with a few that carry real semantics:
retrievalhas a behavioral consequence. The output of every retrieval span in an interaction is what the RAG judges grade against as{context}- a Faithfulness or Context Relevancy scorer reads exactly those chunks.memoryis deliberately notretrieval, even though a memory read looks like a lookup.{context}means knowledge the answer should be grounded in; a recalled user preference is state, not grounding - feeding it to a groundedness judge would penalize answers for not citing it. Keeping the kinds apart lets the timeline and scorers treat “consulted the knowledge base” and “remembered the user” as the different acts they are.- The bare kind string
recallis deliberately NOT an alias: eval harnesses (Ragas, DeepEval) emit per-metric spans kindedrecall/precision/faithfulness- evaluator spans about a retrieval metric, not memory ops. Usememory_recall(or statememory) for a recall operation.
How a span gets its kind
Three paths, checked in this order - an earlier one always wins. 1. Stated explicitly. Both the root span and child spans takespan_kind:
span_kind to these:
record_memory/trace_memory drop (warned once per process, then at debug) when no
span is active, unlike record_tool_call/record_retrieval, which queue onto the next trace.
Framework integrations do the same: merged LLM steps arrive as llm, LangChain graph nodes as
chain, tool executions as tool, retriever runs as retrieval.
3. Inferred by the engine, as a fallback. A span that never stated a kind - a trace
recorded before span kinds existed, or a producer that just doesn’t say - still classifies,
by the engine’s inference ladder: a span with a model is llm, one carrying tool calls is
tool, the SDK’s auto-generated names (LLM Call N, Retrieval N, Memory ...) are
recognized, and everything else is chain. One nuance with a real consequence: a
retrieval-named span whose name contains the word memory/memories (retrieve_memories)
classifies as memory, so recalled user state never rides into the RAG judges’ {context} -
while Retrieval: memoranda index stays a retrieval. Old traces classify exactly as they
always did; inference is a best guess, never a fact.
Alias spellings from the other products’ vocabularies (OpenInference’s retriever,
Langfuse’s generation, every memory_* variant, …) fold onto the kinds above - the
table below is the reference, generated from the same
alias map the engine runs. Two deliberate absences: UNKNOWN (OpenInference / MLflow’s
default) is not an alias - it means “nothing was stated” and falls through to the ladder -
and bare recall is not one either, because eval harnesses emit per-metric spans literally
kinded “recall” (the retrieval metric), which must never classify as memory
(memory_recall works).
A stated kind always beats the ladder. A guardrail implemented as an LLM
call carries a model, which the ladder would read as
llm - stating
span_kind="guardrail" is what keeps it a guardrail. One thing the engine
will not infer: that a root span is an agent turn. A flat trace’s root
is the LLM call, so root-ness proves nothing - a root that really is an
agent turn should say so.Example: memory steps in a trace
One trace whose steps mix the three step families - a memory read (recalled user state), a knowledge retrieval, and a memory write:
The Execution Timeline: memory read and write steps in their own lane, next to the retrieval step.
Other products’ vocabularies just work
The engine folds every convention it knows onto its own vocabulary, so a span instrumented for another product classifies on arrival with no change by the producer:
On the OpenTelemetry ingest path the kind is read from the span’s own
attributes, in this order:
openinference.span.kind, gen_ai.operation.name,
mlflow.spanType, langfuse.observation.type, then gen_ai.tool.name (a span carrying one IS
the tool call) and finally the model attribute. Anything already instrumented with
OpenInference or the GenAI semconv arrives fully classified.
A word the engine does not recognize is ignored, not stored: the span falls back to the
inference ladder rather than carrying a “kind” nobody defined.
Kind vs. tool_calls
These answer different questions. span_kind="tool" says this span is one tool execution.
The root trace’s flat tool_calls[] list says this interaction made these calls - it is what
the built-in Tool failure check, trajectory matching, and the Tool quality column read. The SDK
maintains both for you (trace_tool_call writes the child span with its kind and appends to
the active span’s tool_calls list); if you build spans by hand, keep doing both.
