Skip to content

Conversation

yiyuan-he
Copy link

@yiyuan-he yiyuan-he commented Sep 25, 2025

Description

This PR adds agent span support to the LangChain instrumentation following the OpenTelemetry Semantic Conventions for GenAI agent spans. It enables tracing of LangChain agent invocations, chain executions, and agent actions/decisions.

  • invoke_agent operation for agent invocations
  • Proper span naming: invoke_agent {agent_name} and chain {chain_name}
  • Agent-specific attributes: gen_ai.agent.name, gen_ai.operation.name

A follow-up PR will add tool execution spans (execute_tool operation).

Sidenote: Previous PR from our team can be closed.

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

  • Added unit tests for agent span creation and lifecycle
  • Added unit tests for chain span creation with proper parent-child
    relationships
  • Added unit tests for agent action and finish callbacks
  • All existing tests continue to pass
  • Tests verify proper span attributes according to semantic conventions

Does This PR Require a Core Repo Change?

  • Yes. - Link to PR:
  • No.

Checklist:

See contributing.md for styleguide, changelog guidelines, and more.

  • Followed the style guidelines of this project
  • Changelogs have been updated
  • Unit tests have been added
  • [] Documentation has been updated (README update may be needed in follow-up)

@yiyuan-he yiyuan-he requested a review from a team as a code owner September 25, 2025 16:05
elif "id" in serialized:
chain_name = serialized["id"][-1]

span = self.span_manager.create_chain_span(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make sure it creates internal span

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sound good - confirmed chain spans use SpanKind.INTERNAL here:

    def create_chain_span(
        self,
        run_id: UUID,
        parent_run_id: Optional[UUID],
        chain_name: str,
    ) -> Span:
        """Create a span for chain execution."""
        span = self._create_span(
            run_id=run_id,
            parent_run_id=parent_run_id,
            span_name=f"chain {chain_name}",
            kind=SpanKind.INTERNAL,
        )

)
span.set_attribute(
GenAI.GEN_AI_OPERATION_NAME,
"invoke_agent",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that the operation name might be changed in the future, if so, declaring it a constant for it might be better.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - I'm not sure if this semantic convention will change but agree that this should be moved to a constant since it's currently hardcoded in a few places.

GenAI.GEN_AI_OPERATION_NAME,
"invoke_agent",
)
if agent_name:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If agent_name is None, in that case, should a default value be passed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah makes sense, i'll default it to "unknown" for now. Open to suggestions on the naming though.

span = spans[0]
assert span.name == "chain TestAgent"
assert span.kind == SpanKind.INTERNAL
assert span.attributes.get(GenAI.GEN_AI_AGENT_NAME) == "TestAgent"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the above comment you mentioned that chain spans are internal operations and will not have an operation name but there you are adding assert statements for it, did I miss something?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your review! Let me try to clarify.

Regular chains don't have gen_ai.operation.name since they're just internal operations. However, Agent chains are different. In LangChain's architecture, agents are implemented as chains so we:

  1. create them using create_agent_span() (which creates an internal span)
  2. detect if it's actually an agent by checking for agent_name in metadata
  3. if it is an agent, we add the agent-specific attributes:
  • gen_ai.agent.name
  • gen_ai.operation.name = "invoke_agent"

I'll update the docstrings to make this distinction clearer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. Thanks for the clarification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants