Skip to content

[Discussion] How to model plain LangGraph nodes — invoke_agent, invoke_workflow, or a new span type? #28

@wrisa

Description

@wrisa

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions