Problem
When a CompiledSubAgent returns structured data alongside its final message (e.g., via additional_kwargs or by setting a ToolMessage.artifact), the upstream _return_command_with_state_update in subagents.py discards everything except .text:
message_text = result["messages"][-1].text.rstrip() if result["messages"][-1].text else ""
return Command(
update={
**state_update,
"messages": [ToolMessage(message_text, tool_call_id=tool_call_id)],
}
)
This means there is no supported way for a CompiledSubAgent to return structured data to the parent agent through the ToolMessage. The structured_response state key is explicitly excluded via _EXCLUDED_STATE_KEYS, additional_kwargs on the last message are dropped, and -- critically -- if content is a list of standard content blocks rather than a plain string, the non-text blocks are also lost.
LangChain Core's standard content blocks provide a typed, provider-agnostic way to carry structured data alongside text in a single message (e.g., NonStandardContentBlock for application-specific payloads). A CompiledSubAgent could naturally use this:
content = [
{"type": "text", "text": "Summary of results..."},
{"type": "non_standard", "value": {"sql": "SELECT ...", "tier": 2, "rows": 14}},
]
return {"messages": [AIMessage(content=content)]}
But because _return_command_with_state_update extracts .text (which flattens to only TextContentBlock entries), the NonStandardContentBlock is silently discarded.
The docs say:
The structured object itself is not returned to the parent agent. When using structured output with subagents, include the structured data in the ToolMessage.
But for CompiledSubAgent, users don't control the ToolMessage construction -- the framework creates it from .text only. The guidance is impossible to follow for compiled subagents, whether using additional_kwargs, artifact, or standard content blocks.
Use case
I have a CompiledSubAgent that runs a multi-node LangGraph pipeline. The final node produces a human-readable summary as the message content, plus a structured payload containing intermediate results built across the various nodes. The parent agent needs the summary for conversation, but the API consumer needs the structured payload for other purposes.
Today the only options are:
- Serialize the structured data into the message text (increases parent context token cost, parent LLM may post-process it).
- Monkey-patch
_build_task_tool to propagate additional_kwargs or artifact (fragile, breaks on upstream changes).
Neither is great.
Requested feature
_return_command_with_state_update should forward the full content from the subagent's last message (preserving content blocks when present) rather than flattening to .text. It should also propagate artifact when the last message carries one. This would let CompiledSubAgent authors use any of LangChain Core's existing mechanisms -- content blocks, artifact, or plain text -- to return structured data alongside a human-readable summary.
Proposed solution
The most aligned fix is to forward the full content value (preserving content blocks when present) rather than flattening to .text. A one-line change in _return_command_with_state_update in deepagents/middleware/subagents.py:
def _return_command_with_state_update(result: dict, tool_call_id: str) -> Command:
if "messages" not in result:
error_msg = (
"CompiledSubAgent must return a state containing a 'messages' key. "
"Custom StateGraphs used with CompiledSubAgent should include 'messages' "
"in their state schema to communicate results back to the main agent."
)
raise ValueError(error_msg)
state_update = {k: v for k, v in result.items() if k not in _EXCLUDED_STATE_KEYS}
last_msg = result["messages"][-1]
# Forward full content (preserves content blocks) instead of flattening to .text
raw_content = last_msg.content
if isinstance(raw_content, str):
raw_content = raw_content.rstrip()
# Propagate artifact if the last message carries one
artifact = getattr(last_msg, "artifact", None)
return Command(
update={
**state_update,
"messages": [ToolMessage(content=raw_content, tool_call_id=tool_call_id, artifact=artifact)],
}
)
Why this works:
ToolMessage.content already accepts both str and list[dict] (content blocks). This is part of the LangChain Core message contract.
- When
content is a plain string (the common case today), behavior is identical to the current code.
- When
content is a list of standard content blocks, they are preserved. The parent agent's LLM sees the TextContentBlock entries as text, while NonStandardContentBlock entries survive for programmatic access by API consumers.
artifact propagation covers the case where a CompiledSubAgent uses ToolMessage as its final message type.
Subagent graph usage (content blocks approach)
The final node in a CompiledSubAgent graph could use standard content blocks:
from langchain_core.messages import AIMessage
return {
"messages": [AIMessage(content=[
{"type": "text", "text": "inf_member -- Tier 2 (Guided), 14 rows\n\nSQL:\n..."},
{"type": "non_standard", "value": {
"sql": "SELECT state, COUNT(*) ...",
"confidence_tier": 2,
"data": [{"state": "CA", "count": 5}, ...],
"columns": ["state", "count"],
}},
])],
}
Or use artifact on a ToolMessage:
from langchain_core.messages import ToolMessage
return {
"messages": [ToolMessage(content=summary, artifact=structured_payload, tool_call_id=...)],
}
Or continue using a plain string content (existing behavior, no change needed):
from langchain_core.messages import AIMessage
return {
"messages": [AIMessage(content="Just a text summary, no structured data.")],
}
All three patterns would work with the proposed change.
Additional context
Problem
When a
CompiledSubAgentreturns structured data alongside its final message (e.g., viaadditional_kwargsor by setting aToolMessage.artifact), the upstream_return_command_with_state_updateinsubagents.pydiscards everything except.text:This means there is no supported way for a
CompiledSubAgentto return structured data to the parent agent through theToolMessage. Thestructured_responsestate key is explicitly excluded via_EXCLUDED_STATE_KEYS,additional_kwargson the last message are dropped, and -- critically -- ifcontentis a list of standard content blocks rather than a plain string, the non-text blocks are also lost.LangChain Core's standard content blocks provide a typed, provider-agnostic way to carry structured data alongside text in a single message (e.g.,
NonStandardContentBlockfor application-specific payloads). ACompiledSubAgentcould naturally use this:But because
_return_command_with_state_updateextracts.text(which flattens to onlyTextContentBlockentries), theNonStandardContentBlockis silently discarded.The docs say:
But for
CompiledSubAgent, users don't control theToolMessageconstruction -- the framework creates it from.textonly. The guidance is impossible to follow for compiled subagents, whether usingadditional_kwargs,artifact, or standard content blocks.Use case
I have a
CompiledSubAgentthat runs a multi-node LangGraph pipeline. The final node produces a human-readable summary as the message content, plus a structured payload containing intermediate results built across the various nodes. The parent agent needs the summary for conversation, but the API consumer needs the structured payload for other purposes.Today the only options are:
_build_task_toolto propagateadditional_kwargsorartifact(fragile, breaks on upstream changes).Neither is great.
Requested feature
_return_command_with_state_updateshould forward the fullcontentfrom the subagent's last message (preserving content blocks when present) rather than flattening to.text. It should also propagateartifactwhen the last message carries one. This would letCompiledSubAgentauthors use any of LangChain Core's existing mechanisms -- content blocks, artifact, or plain text -- to return structured data alongside a human-readable summary.Proposed solution
The most aligned fix is to forward the full
contentvalue (preserving content blocks when present) rather than flattening to.text. A one-line change in_return_command_with_state_updateindeepagents/middleware/subagents.py:Why this works:
ToolMessage.contentalready accepts bothstrandlist[dict](content blocks). This is part of the LangChain Core message contract.contentis a plain string (the common case today), behavior is identical to the current code.contentis a list of standard content blocks, they are preserved. The parent agent's LLM sees theTextContentBlockentries as text, whileNonStandardContentBlockentries survive for programmatic access by API consumers.artifactpropagation covers the case where aCompiledSubAgentusesToolMessageas its final message type.Subagent graph usage (content blocks approach)
The final node in a
CompiledSubAgentgraph could use standard content blocks:Or use
artifacton aToolMessage:Or continue using a plain string content (existing behavior, no change needed):
All three patterns would work with the proposed change.
Additional context
_EXCLUDED_STATE_KEYScomment in the source explicitly calls out thatstructured_responsehas "no clear meaning for returning them from a subagent to the main agent" -- but bothToolMessage.artifactand standard content blocks are well-defined mechanisms in LangChain Core that already exist and would be natural fits here.ToolMessage.artifactis documented in LangChain Core and is already used by tools that need to return data that shouldn't be sent to the model but should be available to downstream code.NonStandardContentBlockis specifically designed as an escape hatch for application-specific payloads. Forwarding fullcontentinstead of.textwould makeCompiledSubAgentconsistent with how content blocks work everywhere else in LangChain.