revert: feat(langchain): ls_agent_type tag on create_agent calls (#37249)

This commit is contained in:
ccurme
2026-05-08 09:24:11 -04:00
committed by GitHub
parent 85a5a04210
commit 9c48a120b9
3 changed files with 87 additions and 117 deletions

View File

@@ -1647,12 +1647,7 @@ def create_agent(
# Set recursion limit to 9_999
# https://github.com/langchain-ai/langgraph/issues/7313
config: RunnableConfig = {
"recursion_limit": 9_999,
"configurable": {
"ls_agent_type": "root",
},
}
config: RunnableConfig = {"recursion_limit": 9_999}
config["metadata"] = {"ls_integration": "langchain_create_agent"}
if name:
config["metadata"]["lc_agent_name"] = name

View File

@@ -16,17 +16,12 @@ configurations.
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Annotated, Any
from unittest.mock import MagicMock
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool
from langgraph.prebuilt import InjectedStore
from langgraph.store.memory import InMemoryStore
from langsmith import Client
from langsmith.run_helpers import tracing_context
from langchain.agents import create_agent
from langchain.agents.middleware.types import AgentMiddleware, AgentState
@@ -847,109 +842,3 @@ async def test_combined_injected_state_runtime_store_async() -> None:
# Verify store was injected and writable
assert injected_data["store"] is not None
assert injected_data["store_write_success"] is True
def test_ls_agent_type_is_trace_only_metadata() -> None:
"""Test that ls_agent_type is added to metadata on tracing only, not in streamed chunks."""
# Capture metadata from regular callback handler (simulates streamed metadata)
captured_callback_metadata: list[dict[str, Any]] = []
class CaptureHandler(BaseCallbackHandler):
def on_chain_start(
self,
serialized: dict[str, Any],
inputs: dict[str, Any],
*,
run_id: str,
parent_run_id: str | None = None,
tags: list[str] | None = None,
metadata: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
captured_callback_metadata.append({"tags": tags, "metadata": metadata})
# Create a mock client to capture what gets sent to LangSmith
mock_session = MagicMock()
mock_client = Client(session=mock_session, api_key="test", auto_batch_tracing=False)
agent = create_agent(
model=FakeToolCallingModel(tool_calls=[[], []]),
tools=[],
system_prompt="You are a helpful assistant.",
)
# Use tracing_context to enable tracing with the mock client
with tracing_context(client=mock_client, enabled=True):
agent.invoke(
{"messages": [HumanMessage("hi?")]},
config={"callbacks": [CaptureHandler()]},
)
# Verify that ls_agent_type is NOT in the regular callback metadata
# (it should only go to the tracer via langsmith_inheritable_metadata)
assert len(captured_callback_metadata) > 0
for captured in captured_callback_metadata:
metadata = captured.get("metadata") or {}
assert metadata.get("ls_agent_type") is None, (
f"ls_agent_type should not be in callback metadata, but got: {metadata}"
)
# Verify that ls_agent_type IS in the tracer metadata (sent to LangSmith)
# Get the POST requests to the LangSmith API
posts = []
for call in mock_session.request.mock_calls:
if call.args and call.args[0] == "POST":
body = json.loads(call.kwargs["data"])
if "post" in body:
posts.extend(body["post"])
else:
posts.append(body)
assert len(posts) >= 1
# Find the root run (the agent execution)
root_post = posts[0]
metadata = root_post.get("extra", {}).get("metadata", {})
assert metadata.get("ls_agent_type") == "root", (
f"ls_agent_type should be 'root' in tracer metadata, but got: {metadata}"
)
def test_ls_agent_type_is_overridable() -> None:
"""Test that ls_agent_type can be overridden via configurable in invoke config."""
# Create a mock client to capture what gets sent to LangSmith
mock_session = MagicMock()
mock_client = Client(session=mock_session, api_key="test", auto_batch_tracing=False)
agent = create_agent(
model=FakeToolCallingModel(tool_calls=[[], []]),
tools=[],
system_prompt="You are a helpful assistant.",
)
# Use tracing_context to enable tracing with the mock client
with tracing_context(client=mock_client, enabled=True):
agent.invoke(
{"messages": [HumanMessage("hi?")]},
config={"configurable": {"ls_agent_type": "subagent", "custom_key": "custom_value"}},
)
# Verify that ls_agent_type is overridden and configurable is merged in the tracer metadata
posts = []
for call in mock_session.request.mock_calls:
if call.args and call.args[0] == "POST":
body = json.loads(call.kwargs["data"])
if "post" in body:
posts.extend(body["post"])
else:
posts.append(body)
assert len(posts) >= 1
root_post = posts[0]
metadata = root_post.get("extra", {}).get("metadata", {})
assert metadata.get("ls_agent_type") == "subagent", (
f"ls_agent_type should be 'subagent' in tracer metadata, but got: {metadata}"
)
# Verify that the additional configurable key is merged into metadata
assert metadata.get("custom_key") == "custom_value", (
f"custom_key should be 'custom_value' in tracer metadata, but got: {metadata}"
)

View File

@@ -0,0 +1,86 @@
"""Regression tests for subagent stream event propagation.
Reproduces a bug where `create_agent` set ``ls_agent_type`` inside the
parent agent's ``configurable`` and, as a side effect, ``updates``,
``values``, and ``custom`` stream events from sub-agents invoked through
tools were dropped during ``stream(..., subgraphs=True)``.
"""
from __future__ import annotations
from langchain_core.messages import HumanMessage, ToolCall
from langchain_core.tools import tool
from langchain.agents import create_agent
from tests.unit_tests.agents.model import FakeToolCallingModel
def _make_subagent_caller_tool():
"""Build a subagent and a tool that invokes it."""
subagent = create_agent(
model=FakeToolCallingModel(tool_calls=[[]]),
name="subagent",
)
@tool
def call_subagent(query: str) -> str:
"""Delegate the query to a sub-agent."""
result = subagent.invoke({"messages": [HumanMessage(query)]})
return result["messages"][-1].text
return call_subagent
def _make_parent_agent(call_subagent_tool) -> object:
parent_tool_calls: list[list[ToolCall]] = [
[{"args": {"query": "hi"}, "id": "call_1", "name": "call_subagent"}],
[],
]
return create_agent(
model=FakeToolCallingModel(tool_calls=parent_tool_calls),
tools=[call_subagent_tool],
name="parent",
)
def test_subagent_updates_emitted_when_streaming_with_subgraphs() -> None:
"""`updates` events from a tool-invoked sub-agent must be streamed.
Without the fix, the parent agent's ``configurable`` overrode the
streaming machinery's per-run state, suppressing ``updates`` events
from any sub-graph invoked inside a tool.
"""
call_subagent_tool = _make_subagent_caller_tool()
parent = _make_parent_agent(call_subagent_tool)
subagent_update_events = []
for namespace, mode, _data in parent.stream(
{"messages": [HumanMessage("hi")]},
stream_mode=["updates", "messages"],
subgraphs=True,
):
if mode == "updates" and namespace:
subagent_update_events.append(namespace)
assert subagent_update_events, (
"expected `updates` events from the sub-agent's subgraph namespace, but none were emitted"
)
async def test_subagent_updates_emitted_when_astreaming_with_subgraphs() -> None:
"""Async counterpart of the sync regression test."""
call_subagent_tool = _make_subagent_caller_tool()
parent = _make_parent_agent(call_subagent_tool)
subagent_update_events = []
async for namespace, mode, _data in parent.astream(
{"messages": [HumanMessage("hi")]},
stream_mode=["updates", "messages"],
subgraphs=True,
):
if mode == "updates" and namespace:
subagent_update_events.append(namespace)
assert subagent_update_events, (
"expected `updates` events from the sub-agent's subgraph namespace, but none were emitted"
)