Background
PR #25 adds LangGraph tracing support and introduces invoke_agent spans for LangGraph nodes. This issue proposes we discuss the right semantic model before settling on an approach, because LangGraph nodes can represent fundamentally different things.
Two cases in practice
Case 1: A LangGraph node wrapping a genuine ReAct agent (create_react_agent)
examples/agent/single_node_agent.py
agent = create_react_agent(
llm, tools=[multiply, add], name="math_agent"
).with_config(
{
"metadata": {
"agent_name": "math_agent",
"session_id": session_id,
},
}
)
def run_agent(state: MessagesState) -> dict:
result = agent.invoke({"messages": state["messages"]})
return {"messages": result["messages"]}
builder = StateGraph(MessagesState)
builder.add_node("math_agent", run_agent)
Here the node wraps a genuine agent: it has tools, runs a decide → act → observe loop driven by the LLM, and the developer has explicitly named it via agent_name metadata. An invoke_agent math_agent span is appropriate and well-justified.
Case 2: A LangGraph node that is a plain Python function calling an LLM
examples/workflow/main.py
def researcher(state: GraphState) -> dict:
response = llm.invoke(
[
SystemMessage(content="You are a research assistant. Provide 2-3 factual sentences."),
HumanMessage(content=state["messages"][-1].content),
]
)
return {"research": response.content, "messages": [response]}
def summariser(state: GraphState) -> dict:
response = llm.invoke(
[
SystemMessage(content="You are an expert summariser. Condense the text below into one clear sentence."),
HumanMessage(content=state["research"]),
]
)
return {"messages": [response]}
builder = StateGraph(GraphState)
builder.add_node("researcher", researcher)
builder.add_node("summariser", summariser)
builder.add_edge(START, "researcher")
builder.add_edge("researcher", "summariser")
builder.add_edge("summariser", END)
Here both nodes are statically wired pipeline steps. There are no tools, no autonomous reasoning, no self-directed execution. The LLM is called directly with a hardcoded system prompt. The graph topology is fully determined by the developer
at build time, not by the LLM at runtime.
The question
The current semconv defines invoke_agent as: GenAI models can be trained to use tools to access real-time information... This combination of reasoning, logic, and access to external information... invokes the concept of an agent.
A plain pipeline node like researcher or summariser does not meet this definition. See comment
What OpenInference, Langfuse actually do:
- OpenInference sets openinference.span.kind = AGENT only when "agent" appears in the run name — otherwise CHAIN.
- Langfuse similarly maps to its own "agent" observation type only when "agent" appears in the name or class path — otherwise "chain".
Options for discussion
| Option |
Model |
Trade-off |
| A |
Only emit invoke_agent when the node wraps a genuine agent (tools + agentic loop, e.g. create_react_agent) |
Semantically precise; plain pipeline steps get no agent span |
| B |
Emit invoke_agent for every LangGraph node |
Broad coverage; conflates pipeline steps with agents; gen_ai.agent.name often empty |
| C |
Emit invoke_workflow for the whole graph, invoke_agent only for genuine agent nodes, and a new step or chain span for plain pipeline nodes |
Most expressive; requires defining a new span type |
| D |
Emit invoke_workflow for the whole graph + chat spans only for the LLM calls inside nodes; skip a node-level span entirely |
Minimal; loses node boundary visibility |
The key question is: should the semconv define a span type for a "graph node that is a deterministic pipeline step"? Something like invoke_step or reusing a chain concept? Or should we simply not instrument these at the node level and rely on invoke_workflow + chat spans?
Background
PR #25 adds LangGraph tracing support and introduces invoke_agent spans for LangGraph nodes. This issue proposes we discuss the right semantic model before settling on an approach, because LangGraph nodes can represent fundamentally different things.
Two cases in practice
Case 1: A LangGraph node wrapping a genuine ReAct agent (create_react_agent)
examples/agent/single_node_agent.py
Here the node wraps a genuine agent: it has tools, runs a decide → act → observe loop driven by the LLM, and the developer has explicitly named it via agent_name metadata. An invoke_agent math_agent span is appropriate and well-justified.
Case 2: A LangGraph node that is a plain Python function calling an LLM
examples/workflow/main.py
Here both nodes are statically wired pipeline steps. There are no tools, no autonomous reasoning, no self-directed execution. The LLM is called directly with a hardcoded system prompt. The graph topology is fully determined by the developer
at build time, not by the LLM at runtime.
The question
The current semconv defines invoke_agent as: GenAI models can be trained to use tools to access real-time information... This combination of reasoning, logic, and access to external information... invokes the concept of an agent.
A plain pipeline node like researcher or summariser does not meet this definition. See comment
What OpenInference, Langfuse actually do:
Options for discussion
invoke_agentwhen the node wraps a genuine agent (tools + agentic loop, e.g.create_react_agent)invoke_agentfor every LangGraph nodegen_ai.agent.nameoften emptyinvoke_workflowfor the whole graph,invoke_agentonly for genuine agent nodes, and a newsteporchainspan for plain pipeline nodesinvoke_workflowfor the whole graph +chatspans only for the LLM calls inside nodes; skip a node-level span entirelyThe key question is: should the semconv define a span type for a "graph node that is a deterministic pipeline step"? Something like invoke_step or reusing a chain concept? Or should we simply not instrument these at the node level and rely on invoke_workflow + chat spans?