Tool invocation error with empty error message when using `InjectedState` + `Command` return in async tool

LangGraph version: 1.0.9

I have an async tool that uses InjectedState to inject a namespace from graph state, and returns a Command to update state alongside the tool message. When the tool is called by the LLM, I consistently get the following ToolMessage:

Error invoking tool 'knowledge_search' with kwargs {'query': '...'} with error:
 
Please fix the error and try again.

Notice the {error} slot is empty — which means the underlying exception has no message (str(e) == ""). The generic message comes from TOOL_INVOCATION_ERROR_TEMPLATE in the LangGraph source.

Tool definition

@tool
async def knowledge_search(
    query: Annotated[str, "The search query to find relevant knowledge"],
    namespace: Annotated[str, InjectedState("namespace")],
    tool_call_id: Annotated[str, InjectedToolCallId],
) -> str | Command:
    """Search the business knowledge base."""
    if not namespace:
        return "Knowledge base is not available."

    results = await knowledge_similarity_search(namespace=namespace, query=query, n=3)

    if not results:
        return "No relevant information found in the knowledge base."

    sources = [doc.get("metadata", {}).get("title") or doc.get("metadata", {}).get("source") for doc in results]
    content = "\n\n".join([doc["content"] for doc in results])

    return Command(
        update={
            "knowledge_source": sources,
            "messages": [ToolMessage(content=content, tool_call_id=tool_call_id)],
        }
    )

What is the possible reason for this error? Any help appreciated. Happy to share more context.

hi @meharaz733

can you try one of these fixes?

  • namespace: Annotated[Optional[str], InjectedState("namespace")] (recommended - your runtime guard already handles the empty case)
  • Default the state field to "" so None is never injected
  • Always seed namespace in the initial graph input

Root cause

The empty {error} slot is not “the exception has no message” - it’s a corner case in LangGraph’s error filtering. The flow:

  1. ToolNode injects state["namespace"] and validates the call against the tool’s args schema.
  2. Pydantic raises ValidationError because the value is None (or missing) but you typed it as Annotated[str, ...].
  3. _filter_validation_errors (libs/prebuilt/langgraph/prebuilt/tool_node.py) drops every error whose loc[0] is in the injected-args set - to hide system-injected params from the LLM.
  4. Every failing field in your case is namespace, which is injected → filtered_errors == [].
  5. ToolInvocationError.__init__ does "\n".join([]) → empty string → empty {error} slot.

Your if not namespace: guard never runs because validation fails before the body executes. Command, async, and InjectedToolCallId are all red herrings.

Debug confirmation in 30s

Set handle_tool_errors=False on the ToolNode (or catch ToolInvocationError and inspect .source / .filtered_errors) - you’ll see the real Pydantic message: Input should be a valid string [input_value=None]

Thank you so much:)