diff --git a/libs/langchain_v1/langchain/agents/factory.py b/libs/langchain_v1/langchain/agents/factory.py index b700a6572b4..38c7a09224f 100644 --- a/libs/langchain_v1/langchain/agents/factory.py +++ b/libs/langchain_v1/langchain/agents/factory.py @@ -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 diff --git a/libs/langchain_v1/tests/unit_tests/agents/test_injected_runtime_create_agent.py b/libs/langchain_v1/tests/unit_tests/agents/test_injected_runtime_create_agent.py index c56aa5e95a5..ff8c88788c5 100644 --- a/libs/langchain_v1/tests/unit_tests/agents/test_injected_runtime_create_agent.py +++ b/libs/langchain_v1/tests/unit_tests/agents/test_injected_runtime_create_agent.py @@ -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}" - ) diff --git a/libs/langchain_v1/tests/unit_tests/agents/test_subagent_streaming.py b/libs/langchain_v1/tests/unit_tests/agents/test_subagent_streaming.py new file mode 100644 index 00000000000..b56cbface9f --- /dev/null +++ b/libs/langchain_v1/tests/unit_tests/agents/test_subagent_streaming.py @@ -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" + )