Repository navigation
Conversation
Signed-off-by: 1fanwang <1fannnw@gmail.com>
There was a problem hiding this comment.
🟢 Approval recommended
The changes are well-scoped to LangGraph nested workflow span classification and are backed by targeted regression tests (sync and async) covering the reported failure mode.
Pull request overview
This PR fixes LangChain/LangGraph workflow tracing so that named nested subgraphs emit their own invoke_workflow spans, instead of disappearing into the outer workflow span. It extends the “graph announcement” mechanism (introduced for nested agents) to all compiled graphs, while keeping agent graphs classified as invoke_agent and continuing to suppress ordinary nested RunnableSequence chains.
Changes:
- Extend graph announcements to cover non-agent compiled graphs, enabling nested subgraph workflow spans.
- Adjust chain-run classification precedence to honor explicit overrides and prefer announced nested workflows over inherited agent metadata.
- Add regression tests validating that a named nested subgraph produces an additional workflow span (sync + async).
File summaries
| File | Description |
|---|---|
| instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_workflow.py | New regression tests for nested LangGraph subgraph workflow spans and suppression behavior for non-graph nested chains. |
| instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_operation_mapping.py | Adds unit tests covering workflow override behavior when nested and precedence rules with announcements vs metadata. |
| instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/operation_mapping.py | Updates workflow/agent classification logic to allow nested workflow spans when a compiled-graph announcement is present. |
| instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py | Uses the generalized graph announcement (claim_graph) and threads announced_workflow into classification; supports workflow naming from announcements. |
| instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/agent_context.py | Generalizes pending context from “agent” to “graph” announcements and announces non-agent graphs during Pregel stream entry. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Pull request dashboard statusWaiting on the author · refreshed 2026-10-26 20:01 UTC Respond to 1 review item (e.g. link a commit, explain why not, ask a follow-up):
Status above doesn't look right?
|
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Assisted-by: GitHub Copilot CLI (GPT-5.6 Sol) Signed-off-by: 1fanwang <1fannnw@gmail.com>
Keep graph-owned metadata while using the current local-agent invocation contract. Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
This comment has been minimized.
This comment has been minimized.
| agent_announcement is not None, | ||
| ancestor_agent_names, | ||
| serialized=serialized, | ||
| metadata=metadata, |
The only conflict was in on_tool_start in callback_handler.py. Main replaced the manual tool argument parsing with tool_arguments_to_any_value, and this branch changed _find_agent_context to return three values. The merged code keeps both changes. Assisted-by: GitHub Copilot CLI (claude-sonnet-5.5) Signed-off-by: 1fanwang <1fannnw@gmail.com>
An enclosing task that sets otel_agent_span to False passes it to every nested callback, so a nested graph announced as a workflow was dropped although its own metadata did not set it. Announced workflows now read that opt-out from their own graph metadata. The otel_trace opt-out and announced agents are unchanged. Assisted-by: GitHub Copilot CLI (claude-sonnet-5.5) Signed-off-by: 1fanwang <1fannnw@gmail.com>
|
|
||
| # 3. Workflow / orchestration detection. | ||
| if _looks_like_workflow(serialized, metadata, parent_run_id): | ||
| if metadata and metadata.get(_META_WORKFLOW_SPAN): |
There was a problem hiding this comment.
Callback metadata is inherited by every child run, so without the top-level guard every chain under a run marked otel_workflow_span becomes an invoke_workflow span. E.g. graph.invoke(x, config={"metadata": {"otel_workflow_span": True}}) now reports each node as a workflow too. Chains and nodes will get their own span type (open-telemetry/semantic-conventions-genai#188), which is not workflow.
Could the override apply only on the top-level run (no parent_run_id) as before? Nested graphs don't need it: every Pregel.stream/astream is announced, and classify_chain_run returns invoke_workflow for an announced graph before it looks at the override - test_nested_subgraph_emits_workflow_spans already gets both spans without it. The workflow-override case in test_graph_child_with_local_operation_metadata would go. Test that showcases the problem:
def test_inherited_workflow_override_marks_only_top_level_run(
instrument_langchain: Any,
span_exporter: InMemorySpanExporter,
) -> None:
graph = _graph(_respond, name="outer_graph")
with instrument_langchain():
graph.invoke(
{"messages": [HumanMessage(content="hello")]},
config={"metadata": {"otel_workflow_span": True}},
)
assert [s.name for s in _workflow_spans(span_exporter)] == [
"invoke_workflow outer_graph"
]| or ( | ||
| not has_parent_agent | ||
| and ( | ||
| metadata.get(_META_AGENT_TYPE) | ||
| or metadata.get(_META_AGENT_SPAN) | ||
| ) | ||
| ) |
There was a problem hiding this comment.
This drops explicitly marked agents nested under an agent, not just inherited signals. A runnable with agent_type called from an agent graph used to get invoke_agent researcher; now it gets no span. Fails on this PR, passes on main:
def test_explicit_agent_nested_under_agent(
instrument_langchain: Any,
span_exporter: InMemorySpanExporter,
) -> None:
researcher = RunnableLambda(_respond).with_config(
run_name="researcher", metadata={"agent_type": "worker"}
)
supervisor = _graph(researcher, name="supervisor").with_config(
{"metadata": {"agent_name": "supervisor"}}
)
with instrument_langchain():
supervisor.invoke({"messages": [HumanMessage(content="hello")]})
assert {s.name for s in _agent_spans(span_exporter)} == {
"invoke_agent supervisor",
"invoke_agent researcher",
}|
|
||
| _pending: ContextVar[tuple[_PendingAgent, ...]] = ContextVar( | ||
| "otel_genai_pending_agents", default=() | ||
| _PendingAgent = _PendingGraph |
There was a problem hiding this comment.
nit: the alias only exists for test_conversation_id.py; consider switching that test to _PendingGraph and dropping it.
| _pending: ContextVar[tuple[_PendingAgent, ...]] = ContextVar( | |
| "otel_genai_pending_agents", default=() | |
| _PendingAgent = _PendingGraph |
| _attach_to_context=self._attach_to_context, | ||
| ) | ||
| agent.input_messages = make_input_message(inputs) | ||
| agent.conversation_id = conversation_id |
There was a problem hiding this comment.
nit: conversation_id is already passed to invoke_local_agent above.
| agent.conversation_id = conversation_id |
Assisted-by: GitHub Copilot CLI (Auto) Signed-off-by: 1fanwang <1fannnw@gmail.com>
Assisted-by: GitHub Copilot CLI (Auto) Signed-off-by: 1fanwang <1fannnw@gmail.com>
|
Hi @1fanwang — just a friendly reminder that this pull request is waiting on you. The dashboard status comment has the open items and is kept current.
|
Assisted-by: GitHub Copilot CLI (Auto) Signed-off-by: 1fanwang <1fannnw@gmail.com>
Assisted-by: GitHub Copilot CLI (Auto) Signed-off-by: 1fanwang <1fannnw@gmail.com>
Assisted-by: GitHub Copilot CLI (Auto) Signed-off-by: 1fanwang <1fannnw@gmail.com>

Description
Named LangGraph subgraphs disappear from traces, so their duration and outcome cannot be inspected separately. They now emit workflow spans while agent graphs keep one agent span and ordinary nested chains remain suppressed.
Graph entry points identify compiled graph roots, so metadata inherited from an enclosing graph does not create extra agent spans. A node that opts out of agent spans no longer hides a nested workflow, since that opt-out only says the node is not an agent. Opting out of tracing still applies.
Fixes #578
Type of change
How has this been tested?
The tests do not assert async parent links. #513 tracks why, and those assertions stay xfailed.
E2E
The reproducer uses real compiled graphs and the OpenTelemetry exporter with local message nodes. It requires no model service or credentials. The historical Before runtime was Python 3.12.9, langchain-core 1.6.9 and langgraph 1.2.14. The current After runs use Python 3.10.21/core 0.3.49/graph 0.3.18 and Python 3.14.7/core 1.7.0/graph 1.2.16. Its third case sets otel_agent_span to False on the node that runs the nested graph, and the nested graph inherits it.
Save the sources below beside the repository and run from its root with the indicated environments prepared. The historical Before command used its original py312 environment.
Before, at 7a576ee:
After, at bf40a11:
Reproducer source: nested_workflow_repro.py
The compatibility observation runs local compiled graphs and prints every emitted span with its conversation ID. Bound IDs fall back to graph metadata, runtime IDs take precedence, bound trace opt-outs hide the child graph, and internal writers emit no agent spans. Both runs use the same current commit above.
Reproducer source: compatibility_observation.py
The local conformance run could not execute because Weaver is not installed. CI runs this target.
Checklist
See CONTRIBUTING.md
for the style guide, changelog guidance, and more.