TypeError in file_reducer when subagent returns default 'files' state #64

Closed
opened 2026-02-16 08:19:41 -05:00 by yindo · 1 comment
Owner

Originally created by @ape-x on GitHub (Oct 8, 2025).

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
  • This is not related to the langchain-community package.
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

Example Code

import uuid
from dotenv import load_dotenv
from typing import Any

from deepagents import create_deep_agent
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import StateGraph, START, MessagesState
from langchain.agents import create_agent

from wiki import wiki_tools
from boards import board_tools  
from repos import repo_tools
from search import search_tools
from client import AzureDevOpsClient

all_azure_devops_tools = wiki_tools + board_tools + repo_tools + search_tools

load_dotenv()

AZURE_OPENAI_ENDPOINT = #PRIVATE
AZURE_OPENAI_API_VERSION = "2024-12-01-preview"

class VerbosePlanningMiddleware(AgentMiddleware):
        def __init__(self):
            super().__init__()
            self.processed_message_count = 0
            self.last_todos = []
            self.last_current_step = None
            self._seen_ids = set()
    
        def before_model(self, state: AgentState) -> dict[str, Any] | None:
            return state
    
        def modify_model_request(self, request, agent_state: AgentState, runtime):
            for m in agent_state.get("messages", []):
                mid = getattr(m, "id", None) or id(m)
                if mid in self._seen_ids:
                    continue
                self._seen_ids.add(mid)
                for tc in getattr(m, "tool_calls", []) or []:
                    if tc.get("name") == "task":
                        args = tc.get("args", {})
                        print(f"[SUBAGENT] {args.get('subagent_type')} :: {args.get('description')}")
            return request
    
        def after_model(self, state, runtime):
            messages = state.get('messages', []) if isinstance(state, dict) else getattr(state, 'messages', [])
            new_messages = messages[self.processed_message_count:]
            self.processed_message_count = len(messages)
            latest_todo_call = None
    
            for message in new_messages:
                if hasattr(message, 'tool_calls') and message.tool_calls:
                    for tool_call in message.tool_calls:
                        if 'todo' in tool_call.get('name', '').lower():
                            latest_todo_call = tool_call
                            break
    
            current_step = self._determine_current_step(messages, new_messages)
    
            if current_step and current_step != self.last_current_step:
                print(f"\n🎯 === STEP CHANGED ===")
                if self.last_current_step:
                    print(f"Previous: {self.last_current_step}")
                print(f"Current:  {current_step}")
                print("=== END STEP UPDATE ===\n")
                self.last_current_step = current_step
                if isinstance(state, dict):
                    state['current_step'] = current_step
    
            if latest_todo_call:
                args = latest_todo_call.get('args', {})
                if 'todos' in args:
                    current_todos = args['todos']
                    if current_todos != self.last_todos:
                        print(f"\n🔧 === TODO LIST UPDATED ===")
                        if self.last_todos:
                            print("📋 Changes:")
                            for i, (old, new) in enumerate(zip(self.last_todos, current_todos), 1):
                                old_status = old.get('status', 'pending')
                                new_status = new.get('status', 'pending')
                                if old_status != new_status:
                                    print(f"  {i}. {old.get('content', 'N/A')}: {old_status} → {new_status}")
                        print(f"\n📝 Current Todo List:")
    
    




model = init_chat_model(
    model="azure_openai:gpt-4.1",
    max_tokens=4096,
    openai_api_version=AZURE_OPENAI_API_VERSION,
    azure_endpoint=AZURE_OPENAI_ENDPOINT,
    temperature=0.0,
    azure_deployment="gpt-4.1",
)


ado = {
    "name": "azure_devops",
    "description": "Azure DevOps specialist: boards (work items, queries, backlogs, sprints, states), repos (branches, commits, pull requests), pipelines (builds, releases), wikis, code search, WIQL queries.",
    "prompt": """You are a precise Azure DevOps execution agent.
Always act on Azure DevOps related requests. Use your tools to fetch data (boards, work items, PRs, repos, pipelines, wikis).
Return raw factual lists when asked for listings. No generic apologies – attempt tool usage first. If a tool errors, summarize the error and suggest next steps.""",
    "tools": all_azure_devops_tools
}

PLANNING_BIASED_INSTRUCTIONS = """
You are a high-level coordinator agent. Your ONLY job is to delegate tasks to specialized subagents. You MUST NOT perform tasks yourself.

Rules:
1. If the user request involves multiple steps OR they explicitly say 'plan', 'todo list', 'steps', you MUST FIRST call the write_todos tool.
2. The initial write_todos call must create a list with each distinct step as its own item.
3. Immediately mark the very first actionable step as in_progress.
4. For any task that requires using a tool (like accessing Azure DevOps), you MUST delegate it to the appropriate subagent using the 'task' tool.
5. To delegate, you MUST use the 'task' tool and specify the correct 'subagent_type'. For Azure DevOps work, use 'subagent_type="azure_devops"'.
6. If the user mentions planning and you do not create a todo list, you are violating instructions.
"""

agent = create_deep_agent(
    model=model,
    middleware=[VerbosePlanningMiddleware()],
    instructions=PLANNING_BIASED_INSTRUCTIONS,
    subagents=[ado]
)


def run_with_planning(user_text: str):
    thread_id = str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}, "recursion_limit":1000}

    print(f"Stream channels: {agent.stream_channels}")
    print("'write_todos' present?:", "write_todos" in agent.nodes["tools"].bound._tools_by_name)

    latest_todos: list[dict[str, Any]] | None = None
    current_step: str | None = None
    saw_todo = False

    input_messages = [
        SystemMessage(content="Remember: ALWAYS create a todo list first when user requests planning."),
        HumanMessage(content=user_text),
    ]

    print(f"--- Streaming run (thread_id={thread_id}) ---")
    for update in agent.stream({"messages": input_messages}, config=config):
        if "current_step" in update:
            current_step = update["current_step"]
            print(f"\n🎯 [Current Step]: {current_step}")

        if "VerbosePlanningMiddleware.after_model" in update:
            middleware_data = update["VerbosePlanningMiddleware.after_model"]
            print(f"🔧 [Middleware Data Type]: {type(middleware_data)}")
            print(f"🔧 [Middleware Data]: {middleware_data}")

            if isinstance(middleware_data, dict):
                if "current_step" in middleware_data:
                    current_step = middleware_data["current_step"]
                    print(f"\n🎯 [Current Step from Middleware]: {current_step}")

                if "step_info" in middleware_data:
                    step_info = middleware_data["step_info"]
                    print(f"📊 Step Info: {step_info}")

        step_related_keys = [k for k in update.keys() if 'step' in k.lower() or 'phase' in k.lower()]
        if step_related_keys:
            print(f"\n🔍 [Other step fields]: {step_related_keys}")
            for key in step_related_keys:
                print(f"  {key}: {update[key]}")

        if "todos" in update:
            saw_todo = True
            latest_todos = update["todos"]
            print("\n[Todo List Updated]")
            for i, todo in enumerate(latest_todos, 1):
                title = todo.get("title") or todo.get("task") or str(todo)
                status = todo.get("status", "unknown")
                print(f"  {i}. [{status}] {title}")

        if "messages" in update:
            for m in update["messages"]:
                mtype = getattr(m, "type", None)
                if mtype == "ai":
                    print(f"\n[AI] {m.content}")
                elif mtype == "tool":
                    print(f"\n[Tool Output] {m.content}")

    print("\n--- Stream complete ---")

    if current_step:
        print(f"\n🎯 Final Step: {current_step}")

    if not saw_todo:
        print("\n(No todos were created automatically.)")
    else:
        print("\nFinal todo snapshot:")
        for i, todo in enumerate(latest_todos, 1):
            title = todo.get("title") or todo.get("task") or str(todo)
            status = todo.get("status", "unknown")
            print(f"  {i}. [{status}] {title}")

    return {"todos": latest_todos, "current_step": current_step}


if __name__ == "__main__":
    user_prompt = (
        "create a todo list for the following task and execute it: give me a list of all board items from Azure DevOps. don't ask for anything else, just give me the list"
    )
    run_with_planning(user_prompt)

Error Message and Stack Trace (if applicable)

ERROR: Exception in ASGI application

  • Exception Group Traceback (most recent call last):
    | File "<python_install_path>\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 409, in run_asgi
    | result = await app( # type: ignore[func-returns-value]
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | self.scope, self.receive, self.send
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | )
    | ^
    | File "<python_install_path>\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 60, in call
    | return await self.app(scope, receive, send)
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | File "<python_install_path>\Lib\site-packages\fastapi\applications.py", line 1054, in call
    | await super().call(scope, receive, send)
    | File "<python_install_path>\Lib\site-packages\starlette\applications.py", line 112, in call
    | await self.middleware_stack(scope, receive, send)
    | File "<python_install_path>\Lib\site-packages\starlette\middleware\errors.py", line 187, in call
    | raise exc
    | File "<python_install_path>\Lib\site-packages\starlette\middleware\errors.py", line 165, in call
    | await self.app(scope, receive, _send)
    | File "<python_install_path>\Lib\site-packages\starlette\middleware\exceptions.py", line 62, in call
    | await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
    | File "<python_install_path>\Lib\site-packages\starlette_exception_handler.py", line 53, in wrapped_app
    | raise exc
    | File "<python_install_path>\Lib\site-packages\starlette_exception_handler.py", line 42, in wrapped_app
    | await app(scope, receive, sender)
    | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 715, in call
    | await self.middleware_stack(scope, receive, send)
    | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 735, in app
    | await route.handle(scope, receive, send)
    | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 288, in handle
    | await self.app(scope, receive, send)
    | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 76, in app
    | await wrap_app_handling_exceptions(app, request)(scope, receive, send)
    | File "<python_install_path>\Lib\site-packages\starlette_exception_handler.py", line 53, in wrapped_app
    | raise exc
    | File "<python_install_path>\Lib\site-packages\starlette_exception_handler.py", line 42, in wrapped_app
    | await app(scope, receive, sender)
    | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 74, in app
    | await response(scope, receive, send)
    | File "<python_install_path>\Lib\site-packages\starlette\responses.py", line 261, in call
    | async with anyio.create_task_group() as task_group:
    | ~~~~~~~~~~~~~~~~~~~~~~~^^
    | File "<python_install_path>\Lib\site-packages\anyio_backends_asyncio.py", line 772, in aexit
    | raise BaseExceptionGroup(
    | "unhandled errors in a TaskGroup", self._exceptions
    | ) from None
    | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
    +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    | File "<python_install_path>\Lib\site-packages\starlette\responses.py", line 264, in wrap
    | await func()
    | File "<python_install_path>\Lib\site-packages\starlette\responses.py", line 245, in stream_response
    | async for chunk in self.body_iterator:
    | ...<2 lines>...
    | await send({"type": "http.response.body", "body": chunk, "more_body": True})
    | File "<project_path>\worker_api.py", line 114, in format_stream
    | async for chunk in astream:
    | ...<107 lines>...
    | print(f"Unexpected chunk structure: {type(chunk)} - {chunk}")
    | File "<python_install_path>\Lib\site-packages\langgraph\pregel\main.py", line 2976, in astream
    | async for _ in runner.atick(
    | ...<13 lines>...
    | yield o
    | File "<python_install_path>\Lib\site-packages\langgraph\pregel_runner.py", line 401, in atick
    | _panic_or_proceed(
    | ~~~~~~~~~~~~~~~~~^
    | futures.done.union(f for f, t in futures.items() if t is not None),
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | timeout_exc_cls=asyncio.TimeoutError,
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | panic=reraise,
    | ^^^^^^^^^^^^^^
    | )
    | ^
    | File "<python_install_path>\Lib\site-packages\langgraph\pregel_runner.py", line 511, in _panic_or_proceed
    | raise exc
    | File "<python_install_path>\Lib\site-packages\langgraph\pregel_retry.py", line 137, in arun_with_retry
    | return await task.proc.ainvoke(task.input, config)
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | File "<python_install_path>\Lib\site-packages\langgraph_internal_runnable.py", line 712, in ainvoke
    | input = await step.ainvoke(input, config)
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | File "<python_install_path>\Lib\site-packages\langgraph_internal_runnable.py", line 474, in ainvoke
    | ret = await self.afunc(*args, **kwargs)
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | File "<python_install_path>\Lib\site-packages\langgraph\graph_branch.py", line 180, in _aroute
    | value = reader(config)
    | File "<python_install_path>\Lib\site-packages\langgraph\pregel_read.py", line 91, in do_read
    | return read(select, fresh)
    | File "<python_install_path>\Lib\site-packages\langgraph\pregel_algo.py", line 202, in local_read
    | cc.update(updated[k])
    | ~~~~~~~~~^^^^^^^^^^^^
    | File "<python_install_path>\Lib\site-packages\langgraph\channels\binop.py", line 93, in update
    | self.value = self.operator(self.value, value)
    | ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
    | File "<python_install_path>\Lib\site-packages\deepagents\state.py", line 21, in file_reducer
    | **return {**l, r}
    | ^^^^^^^^^^
    | TypeError: 'list' object is not a mapping
    | During task with name 'tools' and id '3e629c10-d527-fb72-0803-d0b1dc8a0768'
    +------------------------------------

Description

When a deepagent successfully delegates a task to a subagent using the task tool, the entire application crashes with a TypeError when the subagent completes its work and returns its result.

This error occurs specifically when the subagent's task does not involve file operations. The subagent executes its tools correctly (e.g., fetching data from an API) and generates a valid text-based report. However, the crash happens during the final step where the main agent tries to integrate the subagent's state back into its own.

The core of the issue lies in how state is passed from a completed subagent back to the main agent.

  1. Subagent State Return: When a subagent finishes, it returns its final state to the main agent. If the subagent performed no file operations, its internal files state channel appears to default to an empty list ([]).
  2. Main Agent State Update: The main agent receives the subagent's output, which includes the key-value pair 'files': []. It then attempts to update its own files state with this value.
  3. Reducer Failure: The state update is handled by the file_reducer function in deepagents/state.py. This function is defined as return {**l, **r}, which uses dictionary unpacking.
  4. Type Error: The reducer receives the current files state (a dictionary, l) and the incoming update (the list [], r). The operation {**l, **[]} fails because the ** operator cannot be used on a list, raising the TypeError: 'list' object is not a mapping.

How to reproduce

  1. Create a deepagent with at least one subagent.
  2. Assign the main agent a task that must be delegated to the subagent (e.g., "Use the azure_devops agent to fetch work items").
  3. Ensure the subagent's delegated task involves API calls or data processing, but no file I/O.
  4. The subagent will run and successfully generate a result.
  5. Observe that the application crashes with the TypeError as the main agent attempts to process the final ToolMessage from the task. The nature of the task should be long running, involving multiple ToolCalls.

Additionaly, a potential fix that stopped the issue for me was :
def file_reducer(l, r): # Gracefully handle non-dicitionary updates from subagents if not isinstance(r, dict): return l if not isinstance(l, dict): l = {} return {**l, **r}

System Info

langchain: 1.0.0a10
Python Version: 3.13.3

Originally created by @ape-x on GitHub (Oct 8, 2025). ### Checked other resources - [x] This is a bug, not a usage question. - [x] I added a clear and descriptive title that summarizes this issue. - [x] I used the GitHub search to find a similar question and didn't find it. - [x] I am sure that this is a bug in LangChain rather than my code. - [x] The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package). - [x] This is not related to the langchain-community package. - [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example). - [x] I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS. ### Example Code ``` import uuid from dotenv import load_dotenv from typing import Any from deepagents import create_deep_agent from langchain.chat_models import init_chat_model from langchain_core.messages import HumanMessage, SystemMessage from langgraph.graph import StateGraph, START, MessagesState from langchain.agents import create_agent from wiki import wiki_tools from boards import board_tools from repos import repo_tools from search import search_tools from client import AzureDevOpsClient all_azure_devops_tools = wiki_tools + board_tools + repo_tools + search_tools load_dotenv() AZURE_OPENAI_ENDPOINT = #PRIVATE AZURE_OPENAI_API_VERSION = "2024-12-01-preview" class VerbosePlanningMiddleware(AgentMiddleware): def __init__(self): super().__init__() self.processed_message_count = 0 self.last_todos = [] self.last_current_step = None self._seen_ids = set() def before_model(self, state: AgentState) -> dict[str, Any] | None: return state def modify_model_request(self, request, agent_state: AgentState, runtime): for m in agent_state.get("messages", []): mid = getattr(m, "id", None) or id(m) if mid in self._seen_ids: continue self._seen_ids.add(mid) for tc in getattr(m, "tool_calls", []) or []: if tc.get("name") == "task": args = tc.get("args", {}) print(f"[SUBAGENT] {args.get('subagent_type')} :: {args.get('description')}") return request def after_model(self, state, runtime): messages = state.get('messages', []) if isinstance(state, dict) else getattr(state, 'messages', []) new_messages = messages[self.processed_message_count:] self.processed_message_count = len(messages) latest_todo_call = None for message in new_messages: if hasattr(message, 'tool_calls') and message.tool_calls: for tool_call in message.tool_calls: if 'todo' in tool_call.get('name', '').lower(): latest_todo_call = tool_call break current_step = self._determine_current_step(messages, new_messages) if current_step and current_step != self.last_current_step: print(f"\n🎯 === STEP CHANGED ===") if self.last_current_step: print(f"Previous: {self.last_current_step}") print(f"Current: {current_step}") print("=== END STEP UPDATE ===\n") self.last_current_step = current_step if isinstance(state, dict): state['current_step'] = current_step if latest_todo_call: args = latest_todo_call.get('args', {}) if 'todos' in args: current_todos = args['todos'] if current_todos != self.last_todos: print(f"\n🔧 === TODO LIST UPDATED ===") if self.last_todos: print("📋 Changes:") for i, (old, new) in enumerate(zip(self.last_todos, current_todos), 1): old_status = old.get('status', 'pending') new_status = new.get('status', 'pending') if old_status != new_status: print(f" {i}. {old.get('content', 'N/A')}: {old_status} → {new_status}") print(f"\n📝 Current Todo List:") model = init_chat_model( model="azure_openai:gpt-4.1", max_tokens=4096, openai_api_version=AZURE_OPENAI_API_VERSION, azure_endpoint=AZURE_OPENAI_ENDPOINT, temperature=0.0, azure_deployment="gpt-4.1", ) ado = { "name": "azure_devops", "description": "Azure DevOps specialist: boards (work items, queries, backlogs, sprints, states), repos (branches, commits, pull requests), pipelines (builds, releases), wikis, code search, WIQL queries.", "prompt": """You are a precise Azure DevOps execution agent. Always act on Azure DevOps related requests. Use your tools to fetch data (boards, work items, PRs, repos, pipelines, wikis). Return raw factual lists when asked for listings. No generic apologies – attempt tool usage first. If a tool errors, summarize the error and suggest next steps.""", "tools": all_azure_devops_tools } PLANNING_BIASED_INSTRUCTIONS = """ You are a high-level coordinator agent. Your ONLY job is to delegate tasks to specialized subagents. You MUST NOT perform tasks yourself. Rules: 1. If the user request involves multiple steps OR they explicitly say 'plan', 'todo list', 'steps', you MUST FIRST call the write_todos tool. 2. The initial write_todos call must create a list with each distinct step as its own item. 3. Immediately mark the very first actionable step as in_progress. 4. For any task that requires using a tool (like accessing Azure DevOps), you MUST delegate it to the appropriate subagent using the 'task' tool. 5. To delegate, you MUST use the 'task' tool and specify the correct 'subagent_type'. For Azure DevOps work, use 'subagent_type="azure_devops"'. 6. If the user mentions planning and you do not create a todo list, you are violating instructions. """ agent = create_deep_agent( model=model, middleware=[VerbosePlanningMiddleware()], instructions=PLANNING_BIASED_INSTRUCTIONS, subagents=[ado] ) def run_with_planning(user_text: str): thread_id = str(uuid.uuid4()) config = {"configurable": {"thread_id": thread_id}, "recursion_limit":1000} print(f"Stream channels: {agent.stream_channels}") print("'write_todos' present?:", "write_todos" in agent.nodes["tools"].bound._tools_by_name) latest_todos: list[dict[str, Any]] | None = None current_step: str | None = None saw_todo = False input_messages = [ SystemMessage(content="Remember: ALWAYS create a todo list first when user requests planning."), HumanMessage(content=user_text), ] print(f"--- Streaming run (thread_id={thread_id}) ---") for update in agent.stream({"messages": input_messages}, config=config): if "current_step" in update: current_step = update["current_step"] print(f"\n🎯 [Current Step]: {current_step}") if "VerbosePlanningMiddleware.after_model" in update: middleware_data = update["VerbosePlanningMiddleware.after_model"] print(f"🔧 [Middleware Data Type]: {type(middleware_data)}") print(f"🔧 [Middleware Data]: {middleware_data}") if isinstance(middleware_data, dict): if "current_step" in middleware_data: current_step = middleware_data["current_step"] print(f"\n🎯 [Current Step from Middleware]: {current_step}") if "step_info" in middleware_data: step_info = middleware_data["step_info"] print(f"📊 Step Info: {step_info}") step_related_keys = [k for k in update.keys() if 'step' in k.lower() or 'phase' in k.lower()] if step_related_keys: print(f"\n🔍 [Other step fields]: {step_related_keys}") for key in step_related_keys: print(f" {key}: {update[key]}") if "todos" in update: saw_todo = True latest_todos = update["todos"] print("\n[Todo List Updated]") for i, todo in enumerate(latest_todos, 1): title = todo.get("title") or todo.get("task") or str(todo) status = todo.get("status", "unknown") print(f" {i}. [{status}] {title}") if "messages" in update: for m in update["messages"]: mtype = getattr(m, "type", None) if mtype == "ai": print(f"\n[AI] {m.content}") elif mtype == "tool": print(f"\n[Tool Output] {m.content}") print("\n--- Stream complete ---") if current_step: print(f"\n🎯 Final Step: {current_step}") if not saw_todo: print("\n(No todos were created automatically.)") else: print("\nFinal todo snapshot:") for i, todo in enumerate(latest_todos, 1): title = todo.get("title") or todo.get("task") or str(todo) status = todo.get("status", "unknown") print(f" {i}. [{status}] {title}") return {"todos": latest_todos, "current_step": current_step} if __name__ == "__main__": user_prompt = ( "create a todo list for the following task and execute it: give me a list of all board items from Azure DevOps. don't ask for anything else, just give me the list" ) run_with_planning(user_prompt) ``` ### Error Message and Stack Trace (if applicable) ERROR: Exception in ASGI application + Exception Group Traceback (most recent call last): | File "<python_install_path>\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 409, in run_asgi | result = await app( # type: ignore[func-returns-value] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | self.scope, self.receive, self.send | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ) | ^ | File "<python_install_path>\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 60, in __call__ | return await self.app(scope, receive, send) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "<python_install_path>\Lib\site-packages\fastapi\applications.py", line 1054, in __call__ | await super().__call__(scope, receive, send) | File "<python_install_path>\Lib\site-packages\starlette\applications.py", line 112, in __call__ | await self.middleware_stack(scope, receive, send) | File "<python_install_path>\Lib\site-packages\starlette\middleware\errors.py", line 187, in __call__ | raise exc | File "<python_install_path>\Lib\site-packages\starlette\middleware\errors.py", line 165, in __call__ | await self.app(scope, receive, _send) | File "<python_install_path>\Lib\site-packages\starlette\middleware\exceptions.py", line 62, in __call__ | await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) | File "<python_install_path>\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app | raise exc | File "<python_install_path>\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app | await app(scope, receive, sender) | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 715, in __call__ | await self.middleware_stack(scope, receive, send) | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 735, in app | await route.handle(scope, receive, send) | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 288, in handle | await self.app(scope, receive, send) | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 76, in app | await wrap_app_handling_exceptions(app, request)(scope, receive, send) | File "<python_install_path>\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app | raise exc | File "<python_install_path>\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app | await app(scope, receive, sender) | File "<python_install_path>\Lib\site-packages\starlette\routing.py", line 74, in app | await response(scope, receive, send) | File "<python_install_path>\Lib\site-packages\starlette\responses.py", line 261, in __call__ | async with anyio.create_task_group() as task_group: | ~~~~~~~~~~~~~~~~~~~~~~~^^ | File "<python_install_path>\Lib\site-packages\anyio\_backends\_asyncio.py", line 772, in __aexit__ | raise BaseExceptionGroup( | "unhandled errors in a TaskGroup", self._exceptions | ) from None | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception) +-+---------------- 1 ---------------- | Traceback (most recent call last): | File "<python_install_path>\Lib\site-packages\starlette\responses.py", line 264, in wrap | await func() | File "<python_install_path>\Lib\site-packages\starlette\responses.py", line 245, in stream_response | async for chunk in self.body_iterator: | ...<2 lines>... | await send({"type": "http.response.body", "body": chunk, "more_body": True}) | File "<project_path>\worker_api.py", line 114, in format_stream | async for chunk in astream: | ...<107 lines>... | print(f"Unexpected chunk structure: {type(chunk)} - {chunk}") | File "<python_install_path>\Lib\site-packages\langgraph\pregel\main.py", line 2976, in astream | async for _ in runner.atick( | ...<13 lines>... | yield o | File "<python_install_path>\Lib\site-packages\langgraph\pregel\_runner.py", line 401, in atick | _panic_or_proceed( | ~~~~~~~~~~~~~~~~~^ | futures.done.union(f for f, t in futures.items() if t is not None), | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | timeout_exc_cls=asyncio.TimeoutError, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | panic=reraise, | ^^^^^^^^^^^^^^ | ) | ^ | File "<python_install_path>\Lib\site-packages\langgraph\pregel\_runner.py", line 511, in _panic_or_proceed | raise exc | File "<python_install_path>\Lib\site-packages\langgraph\pregel\_retry.py", line 137, in arun_with_retry | return await task.proc.ainvoke(task.input, config) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "<python_install_path>\Lib\site-packages\langgraph\_internal\_runnable.py", line 712, in ainvoke | input = await step.ainvoke(input, config) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "<python_install_path>\Lib\site-packages\langgraph\_internal\_runnable.py", line 474, in ainvoke | ret = await self.afunc(*args, **kwargs) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "<python_install_path>\Lib\site-packages\langgraph\graph\_branch.py", line 180, in _aroute | value = reader(config) | File "<python_install_path>\Lib\site-packages\langgraph\pregel\_read.py", line 91, in do_read | return read(select, fresh) | File "<python_install_path>\Lib\site-packages\langgraph\pregel\_algo.py", line 202, in local_read | cc.update(updated[k]) | ~~~~~~~~~^^^^^^^^^^^^ | File "<python_install_path>\Lib\site-packages\langgraph\channels\binop.py", line 93, in update | self.value = self.operator(self.value, value) | ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^ | **File "<python_install_path>\Lib\site-packages\deepagents\state.py", line 21, in file_reducer** | **return {**l, **r}** | ^^^^^^^^^^ | **TypeError: 'list' object is not a mapping** | During task with name 'tools' and id '3e629c10-d527-fb72-0803-d0b1dc8a0768' +------------------------------------ ### Description When a deepagent successfully delegates a task to a subagent using the task tool, the entire application crashes with a TypeError when the subagent completes its work and returns its result. This error occurs specifically when the subagent's task does not involve file operations. The subagent executes its tools correctly (e.g., fetching data from an API) and generates a valid text-based report. However, the crash happens during the final step where the main agent tries to integrate the subagent's state back into its own. ### The core of the issue lies in how state is passed from a completed subagent back to the main agent. 1. Subagent State Return: When a subagent finishes, it returns its final state to the main agent. If the subagent performed no file operations, its internal files state channel appears to default to an empty list ([]). 2. Main Agent State Update: The main agent receives the subagent's output, which includes the key-value pair 'files': []. It then attempts to update its own files state with this value. 3. Reducer Failure: The state update is handled by the file_reducer function in deepagents/state.py. This function is defined as return {**l, **r}, which uses dictionary unpacking. 4. Type Error: The reducer receives the current files state (a dictionary, l) and the incoming update (the list [], r). The operation {**l, **[]} fails because the ** operator cannot be used on a list, raising the TypeError: 'list' object is not a mapping. ### How to reproduce 1. Create a deepagent with at least one subagent. 2. Assign the main agent a task that must be delegated to the subagent (e.g., "Use the azure_devops agent to fetch work items"). 3. Ensure the subagent's delegated task involves API calls or data processing, but no file I/O. 4. The subagent will run and successfully generate a result. 5. Observe that the application crashes with the TypeError as the main agent attempts to process the final ToolMessage from the task. The nature of the task should be long running, involving multiple ToolCalls. Additionaly, a potential fix that stopped the issue for me was : `def file_reducer(l, r): # Gracefully handle non-dicitionary updates from subagents if not isinstance(r, dict): return l if not isinstance(l, dict): l = {} return {**l, **r}` ### System Info langchain: 1.0.0a10 Python Version: 3.13.3
yindo closed this issue 2026-02-16 08:19:42 -05:00
Author
Owner

@nhuang-lc commented on GitHub (Oct 21, 2025):

Hey @ape-x, thanks for flagging this, and sorry for the delay in looking into this.

This is fixed on the latest version of deepagents >= 0.1.0

Here is a simplified code snippet that passes files into the main agent.

  • These files are passed to the subagent when it is invoked
  • The subagent doesn't do anything with these files
  • These same files are returned.
@tool(description="Use this tool to get the weather")
def get_weather(location: str):
    return f"The weather in {location} is sunny."

agent = create_deep_agent(
    model="claude-sonnet-4-20250514",
    system_prompt="Use the task tool to call a subagent.",
    subagents=[
        {
            "name": "weather",
            "description": "This subagent can get weather in cities.",
            "system_prompt": "Use the get_weather tool to get the weather in a city.",
            "tools": [get_weather],
        }
    ],
)
response = agent.invoke({
    "messages": [HumanMessage(content="What is the weather in Tokyo?")],
    "files": {
        "/weather.txt": {
            "content": "The weather in Tokyo is sunny.",
            "created_at": "2021-01-01",
            "modified_at": "2021-01-01",
        }
    }
})
print(response)

Removing the files from input altogether also works. In this case, there are no files, and there is no default empty list for the files value.

agent = create_deep_agent(
    model="claude-sonnet-4-20250514",
    system_prompt="Use the task tool to call a subagent.",
    subagents=[
        {
            "name": "weather",
            "description": "This subagent can get weather in cities.",
            "system_prompt": "Use the get_weather tool to get the weather in a city.",
            "tools": [get_weather],
        }
    ],
)
assert "task" in agent.nodes["tools"].bound._tools_by_name.keys()
response = agent.invoke({
    "messages": [HumanMessage(content="What is the weather in Tokyo?")],
})
print(response)

Please let me know if you run into any issues with this on the latest version, happy to help debug :)

@nhuang-lc commented on GitHub (Oct 21, 2025): Hey @ape-x, thanks for flagging this, and sorry for the delay in looking into this. This is fixed on the latest version of deepagents >= 0.1.0 Here is a simplified code snippet that passes files into the main agent. - These files are passed to the subagent when it is invoked - The subagent doesn't do anything with these files - These same files are returned. ``` @tool(description="Use this tool to get the weather") def get_weather(location: str): return f"The weather in {location} is sunny." agent = create_deep_agent( model="claude-sonnet-4-20250514", system_prompt="Use the task tool to call a subagent.", subagents=[ { "name": "weather", "description": "This subagent can get weather in cities.", "system_prompt": "Use the get_weather tool to get the weather in a city.", "tools": [get_weather], } ], ) response = agent.invoke({ "messages": [HumanMessage(content="What is the weather in Tokyo?")], "files": { "/weather.txt": { "content": "The weather in Tokyo is sunny.", "created_at": "2021-01-01", "modified_at": "2021-01-01", } } }) print(response) ``` Removing the files from input altogether also works. In this case, there are no files, and there is no default empty list for the files value. ``` agent = create_deep_agent( model="claude-sonnet-4-20250514", system_prompt="Use the task tool to call a subagent.", subagents=[ { "name": "weather", "description": "This subagent can get weather in cities.", "system_prompt": "Use the get_weather tool to get the weather in a city.", "tools": [get_weather], } ], ) assert "task" in agent.nodes["tools"].bound._tools_by_name.keys() response = agent.invoke({ "messages": [HumanMessage(content="What is the weather in Tokyo?")], }) print(response) ``` Please let me know if you run into any issues with this on the latest version, happy to help debug :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagents#64