Skip to content

fix(agents): read LangGraphAgent reply text from content blocks - #8087

Open
gjija wants to merge 1 commit into
google:mainfrom
gjija:fix-langgraph-reply-blocks
Open

gjija wants to merge 1 commit into
google:mainfrom
gjija:fix-langgraph-reply-blocks

Conversation

@gjija

@gjija gjija commented Nov 10, 2026

Copy link
Copy Markdown

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • Closes: N/A
  • Related: N/A

2. Or, if no issue exists, describe the change:

Problem:

LangGraphAgent._run_async_impl takes final_state['messages'][-1].content and passes it straight to types.Part.from_text(text=...). In LangChain, BaseMessage.content is str | list[str | dict]. Chat models return the list form in common setups: Gemini 3 models through ChatGoogleGenerativeAI (langchain-google-genai 4.x returns text blocks for them; langchain-ai/langchain-google#1460 shows the streamed form), Vertex with a thinking budget (langchain-ai/langchain-google#1621), and any model set to output_version="v1". Then Part.from_text fails validation and the agent raises on every turn instead of answering:

pydantic_core._pydantic_core.ValidationError: 1 validation error for Part
text
  Input should be a valid string [type=string_type, input_value=[{'type': 'reasoning', 'r...'text': 'Sunny today.'}], input_type=list]

Repro (a real compiled graph through InMemoryRunner; the node returns what such a model returns):

import asyncio

from google.adk.agents.langgraph_agent import LangGraphAgent
from google.adk.runners import InMemoryRunner
from google.genai import types
from langchain_core.messages import AIMessage
from langgraph.graph import END, START, MessagesState, StateGraph


def reply(state):
  blocks = [
      {"type": "reasoning", "reasoning": "Checking the forecast."},
      {"type": "text", "text": "Sunny today."},
  ]
  return {"messages": [AIMessage(content=blocks)]}


async def main():
  builder = StateGraph(MessagesState)
  builder.add_node("reply", reply)
  builder.add_edge(START, "reply")
  builder.add_edge("reply", END)
  agent = LangGraphAgent(name="weather", graph=builder.compile())
  runner = InMemoryRunner(agent=agent, app_name="app")
  session = await runner.session_service.create_session(app_name="app", user_id="u")
  async for event in runner.run_async(
      user_id="u", session_id=session.id, new_message=types.UserContent("Weather?")
  ):
    print("reply:", event.content.parts[0].text)


asyncio.run(main())

On main it raises the ValidationError above.

Solution:

When the content is a list, use the message's .text property. It keeps plain strings and type == "text" blocks and joins them into a str. String content is passed through as before, so graphs that work today are unchanged.

.text is a property on every langchain-core 1.x, which ADK's langgraph>=1.0.10 floor requires. On langchain-core 0.3, which installs from when ADK required langgraph 0.x can still have, .text is a method; string content never reaches it there, so those installs keep working.

Non-text blocks (reasoning, images) are left out of the reply. Mapping reasoning blocks to ADK thought parts would be a separate change.

With this change the repro prints:

reply: Sunny today.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

New test test_langgraph_agent_reads_text_from_content_blocks in tests/unittests/agents/test_langgraph_agent.py, next to test_langgraph_agent_runs_real_compiled_state_graph. It runs a real compiled StateGraph(MessagesState) whose node returns a reasoning block and a text block.

Without the source change it fails with the ValidationError above (1 failed, 10 passed). With it:

$ pytest tests/unittests/agents/test_langgraph_agent.py
11 passed

The whole tests/unittests/agents folder: 975 passed, 1 xfailed (974 passed on main). I also ran the file on langgraph 0.4.8 with langchain-core 0.3.79: the 10 existing tests pass with the change, as on main; the new list test fails there on both, since 0.3 has no .text property. pyink and isort leave both files unchanged.

Manual End-to-End (E2E) Tests:

The repro above stands in for a manual run: a real compiled LangGraph graph through InMemoryRunner, with the node returning the block content a Gemini 3 or output_version="v1" model gives, in place of a live model call. Versions: langgraph 1.2.20, langchain-core 1.7.6.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

A list with no text blocks gives an empty text part instead of a crash.

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.
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.

2 participants