惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
雷峰网
雷峰网
小众软件
小众软件
博客园 - 【当耐特】
J
Java Code Geeks
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
Engineering at Meta
Engineering at Meta
aimingoo的专栏
aimingoo的专栏
A
About on SuperTechFans
H
Help Net Security
博客园_首页
P
Proofpoint News Feed
GbyAI
GbyAI
大猫的无限游戏
大猫的无限游戏
N
Netflix TechBlog - Medium
爱范儿
爱范儿
MyScale Blog
MyScale Blog
Blog — PlanetScale
Blog — PlanetScale
The Cloudflare Blog
MongoDB | Blog
MongoDB | Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Google DeepMind News
Google DeepMind News

LangChain Forum - Topics tagged intro-to-langgraph

Does HITL approval actually bind the action that reaches the executor? Production use of PostgresSaver: service-owned migrations and checkpoint retention in Node.js How are you guys stopping agents from silent loops/stampedes? Standard step_counters feel like garbage Introducing Langshark a local-first companion observability tool for self-hosted Langgraph deployments Help Needed : Best practices for client-side metadata filtering in custom checkpoint implementations? How do you actually decide between LangGraph, AutoGen, and which model to use for a task? Feedback requested: evaluating commit-time action validity in LangGraph workflows DeltaChannel + RemoveMessage message compression — how to express deletions with incremental storage? Research Q: what blocks your first LangGraph prod deploy? (not a pitch) A LangGraph checkpointer with provable thread deletion (signed erasure receipts) — feedback welcome Adding an end-to-end example: from locally-served model to agentic flow orchestration Deep Agent state cumbersome Sanity-check my LangGraph design before product demo LangGraph orchestration Problem Re-execute / restart from a specific task in a functional API AttributeError: 'AsyncSandboxClient' object has no attribute 'get_template' What is the equivalent of a transaction log for agent systems? Null-drift: A bare-metal O(1) Memory Store for continuous LangGraph agents Interrupt does not work correctly in LangGraph Pre-interrupt() code re-runs on resume — anti-pattern, or is there a sanctioned way to detect resume? Would pre-inference routing help long-context agent workflows? Improving citation accuracy and reducing hallucinations in custom Parent-Child RAG pipeline (Gemma3:4B + FAISS+BM25 + Cross-encoder reranker) WikipediaLoader endup in JSONDecodeError Using LangGraph interrupt for multi-step wizards with branching — right tool or wrong abstraction? Built NORNR for spend governance in agent workflows LangGraph + PostgreSQL: Chat history and summarization best practice Discussion about why LangGraph JS ToolNode doesn’t inject ToolRuntime.state like Python does, and what the correct workaround or intended design pattern is. No cost displayed in LangSmith when using LiteLLM + LangGraph LiteLLM Router in LangChain: Missing Model Name and Cost in LangSmith Traces I find langgraph chat in documents useful, how can I hook it up as mcp or skill or a tool for the Claude Code?
Chat model returns empty content when I inject a delayed ...
vicky70 · 2025-10-26 · via LangChain Forum - Topics tagged intro-to-langgraph

October 26, 2025, 8:20am 1

Hey!

I have a scheduler that triggers a tool function later (outside the original user–model exchange).
When the scheduled time comes, it sends a ToolMessage back into the chat model — but the model returns an empty message (content="") instead of responding.

This is the flow of messages:

[HumanMessage] → [AIMessage] → [ToolMessage] → [AIMessage] → … → [ToolMessage (from scheduled function)]

The final ToolMessage is the one created when the scheduled callback fires.
That’s where the issue happens — the model simply returns an empty string.

The Scheduled Callback:

def _task_reminder_callback(thread_id: str, task_description: str):
    print(f"\n\n === AGENT: TASK REMINDER! (for thread: {thread_id}) === ")
    print(f" TASK: {task_description} ")
    print(" ======================================================= \n\n")

    config = {"configurable": {"thread_id": thread_id, "user_id": "user_123"}}
    workflow_instance = app_state.get("WORKFLOW")

    state = workflow_instance.get_state(config)

    # Find the most recent tool call for schedule_reminder
    tool_call_id = None
    if state and state.values and "messages" in state.values:
        for msg in reversed(state.values["messages"]):
            if hasattr(msg, 'tool_calls') and msg.tool_calls:
                for tool_call in msg.tool_calls:
                    if tool_call.get('name') == 'schedule_reminder':
                        tool_call_id = tool_call.get('id')
                        break
            if tool_call_id:
                break

    print(f'[DEBUG] tool_call_id: {tool_call_id}')

    if workflow_instance:
        try:
            reminder_message = ToolMessage(
                content=(
                    f"REMINDER: generate a natural sounding message to remind user "
                    f"that it's time to do this task:\n\n {task_description}.\n\n"
                ),
                tool_call_id=tool_call_id
            )

            result = workflow_instance.invoke({"messages": reminder_message}, config=config)

            print(f" === Successfully invoked workflow for reminder in thread {thread_id} === \n\n {result} === \n\n")
        except Exception as e:
            print(f" !!! ERROR in _task_reminder_callback for thread {thread_id}: {e} !!! \n\n")
    else:
        print(f" !!! ERROR: WORKFLOW not initialized. Cannot run reminder for thread {thread_id} !!!")

Problem
When this scheduled function runs and I call workflow_instance.invoke() with a ToolMessage,
the chat model returns an empty content message AIMessage(content="") — it doesn’t “react” like it normally would when the tool message comes directly during conversation flow.
Expected behavior:
The model should interpret the tool message and respond naturally (e.g., “Hey, it’s time to do your scheduled task!”).

My Question:
How can I correctly re-inject a delayed ToolMessage into the workflow so that the model responds like it does during normal tool use?

Is there something special about how the tool_call_id or message history needs to be linked so the model knows what to do?
Or do I need to send a different type of message (like SystemMessage or AssistantMessage) when reactivating the model after a delay?

Hi @vicky70

have you tried this?

# 1) Build the ToolMessage with the SAME tool_call_id produced earlier by the model
reminder_message = ToolMessage(
    content=(
        f"REMINDER: generate a natural sounding message to remind the user "
        f"that it's time to do this task:\n\n{task_description}.\n\n"
    ),
    tool_call_id=tool_call_id,
    name="schedule_reminder",  # include tool name for clarity
)

# 2) Append it to the thread state AS IF it came from the 'tools' node
next_config = workflow_instance.update_state(
    config,
    {"messages": [reminder_message]},
    as_node="tools",  # drives the graph to the agent/model next
)

# 3) Resume the graph so the model reacts to the tool output
result = workflow_instance.invoke(None, config=next_config)

1 Like

vinishiru February 19, 2026, 7:11pm 3

Hello @vicky70 , have you successfully implemented this behavior?
I also have a tool that returns a partial result from the agent call, but in a later moment I try to inject a ToolMessage (with same tool_call_id), but the model generates a empty content response.

I have tried the @pawel-twardziak approach, but the model also generates an empty response, even when updating the graph state as it was resuming the result from the tool node.

I have also used a time-travel approach, search for the state when the tool was originally called, overwriting the tool result, as if it was completed as a direct call, but this way I lose any message exchanged between the user and the agent.

I had better results creating the tool message and a HumanMessage that instructs the model to continue the conversation, both on the same input message array, but oddly the model seens to ignore the ToolMessage data and generate a response that is not present on the appended ToolMessage.

Example:

messages = [
                    ToolMessage(
                        content=ai_agent_prompt_dto.prompt,
                        tool_call_id=ai_agent_prompt_dto.context[
                            "tool_call_id"
                        ], 
                    ),
                    HumanMessage(content="You have received the tool delayed result. You must continue with the conversation."),
                ]

Hey @vicky70

This usually happens because the graph isn’t actually being resumed in the same execution context the model expects.

When you inject a delayed ToolMessage, the model will only react if:

  1. The tool_call_id matches exactly the one from the original AIMessage.tool_calls.

  2. The message is appended as coming from the "tools" node.

  3. The graph is resumed from the correct next edge (not restarted with a fresh invoke).

In LangGraph, a ToolMessage by itself doesn’t trigger reasoning — it only works if the graph state machine thinks it just finished executing a tool. If that edge isn’t active anymore (because time passed or the flow completed), the model can legitimately return AIMessage(content="").

That empty response usually means:

“There is no pending tool result expected in this state.”

Why your HumanMessage workaround behaves oddly

When you add:

HumanMessage("You have received the tool delayed result...")

you’re effectively starting a new reasoning turn.
So the model ignores the tool linkage and just treats it like fresh input.

That’s why it doesn’t behave like native tool completion.

What works more reliably in production

Instead of trying to “resume” the original tool call hours later:

:white_check_mark: Treat the reminder as a new user event
→ Inject a HumanMessage like:

“System reminder: It’s time to do X.”

and let the agent respond normally.

This avoids fighting the internal state machine.

Why this happens (architecturally)

LangGraph’s execution model assumes:

AI → tool_call → ToolMessage → AI (immediate continuation)

It is not designed for:

AI → tool_call → [hours later] → ToolMessage → AI

Once the run is complete, there is no suspended edge waiting for that tool result.

In short:
You’re not doing it wrong — you’re hitting a lifecycle limitation.
Delayed tool reinjection only works if the graph is still in a waiting state. Otherwise, treat it as a new conversational turn.