From 5e579e3b0d547bfa353f6b3d1ed6dc653c468c69 Mon Sep 17 00:00:00 2001 From: gjija Date: Tue, 10 Nov 2026 00:02:02 +0000 Subject: [PATCH] fix(agents): read LangGraphAgent reply text from content blocks LangGraphAgent passed the last message's `content` straight to `Part.from_text`. LangChain message content can be a list of content blocks, for example a reasoning block and a text block. `Part.from_text` then fails pydantic validation and the agent raises instead of answering. When the content is a list, use the message's `.text` property, which keeps only the text. String content is passed through unchanged, so installs still on langchain-core 0.3, where `.text` is a method, keep working. --- src/google/adk/agents/langgraph_agent.py | 7 +++- .../unittests/agents/test_langgraph_agent.py | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/google/adk/agents/langgraph_agent.py b/src/google/adk/agents/langgraph_agent.py index f1dd1ed8775..875cdf48d8a 100644 --- a/src/google/adk/agents/langgraph_agent.py +++ b/src/google/adk/agents/langgraph_agent.py @@ -140,7 +140,12 @@ async def _run_async_impl( # Use the Runnable final_state = await self.graph.ainvoke({'messages': messages}, config) - result = final_state['messages'][-1].content + message = final_state['messages'][-1] + # Content can be a list of blocks (e.g. reasoning and text); `.text` keeps + # only the text. String content is passed through unchanged. + result = ( + message.content if isinstance(message.content, str) else message.text + ) result_event = Event( invocation_id=ctx.invocation_id, diff --git a/tests/unittests/agents/test_langgraph_agent.py b/tests/unittests/agents/test_langgraph_agent.py index e7b73a7b253..514919f0cae 100644 --- a/tests/unittests/agents/test_langgraph_agent.py +++ b/tests/unittests/agents/test_langgraph_agent.py @@ -261,6 +261,40 @@ def respond(state: MessagesState) -> dict[str, list[AIMessage]]: assert observed_messages[0].content == "test system prompt" +@pytest.mark.asyncio +async def test_langgraph_agent_reads_text_from_content_blocks(): + """A reply whose content is a list of blocks yields its text.""" + + def respond(state: MessagesState) -> dict[str, list[AIMessage]]: + del state + return { + "messages": [ + AIMessage( + content=[ + {"type": "reasoning", "reasoning": "thinking"}, + {"type": "text", "text": "Sunny today."}, + ] + ) + ] + } + + graph_builder = StateGraph(MessagesState) + graph_builder.add_node("respond", respond) + graph_builder.set_entry_point("respond") + graph_builder.set_finish_point("respond") + agent = LangGraphAgent(name="weather_agent", graph=graph_builder.compile()) + + events = [ + event + async for event in agent.run_async( + _make_parent_context("test_app", "test_user", "session-id") + ) + ] + + assert len(events) == 1 + assert events[0].content.parts[0].text == "Sunny today." + + def test_get_thread_id_is_stable_across_processes(): """A literal digest also rejects a swap to a per-process-salted hash.""" assert (