langchain-empty-response-retry
A small, model-agnostic LangChain agent middleware that retries with a short corrective nudge when the model returns a successful completion with no text and no tool calls.
Why
Models occasionally return an empty completion, most often at the end of long tool-calling chains. The agent loop treats it as a finished turn and the user receives nothing. Because no exception is raised, ModelRetryMiddleware never fires.
EmptyResponseRetryMiddleware detects this by content, not by provider-specific finish reasons, and retries the request with a short nudge.
Install
pip install langchain-empty-response-retry
Usage
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_empty_response_retry import EmptyResponseRetryMiddleware
@tool
def search(query: str) -> str:
"""Search the knowledge base."""
return "Refunds take 5 days."
agent = create_agent(
model="openai:gpt-4o", # any chat model
tools=[search],
middleware=[EmptyResponseRetryMiddleware(max_retries=2)],
)
result = agent.invoke({"messages": [{"role": "user", "content": "How long do refunds take?"}]})
What a retry looks like
On an empty completion the request is retried with one nudge appended. Only the accepted response is kept in the agent state.
model call 1: [Human: "How long do refunds take?"] -> AIMessage("")
model call 2: [Human: "How long do refunds take?",
Human: "Your previous response was empty. Respond to ..."] -> AIMessage("About 5 days.")
final state: [Human: "How long do refunds take?", AI: "About 5 days."]
Failing loudly or using a fallback reply
# Raise EmptyModelResponseError after the last retry:
EmptyResponseRetryMiddleware(max_retries=1, on_exhausted="error")
# Or reply with fixed text instead of an empty message:
EmptyResponseRetryMiddleware(on_exhausted="Sorry, I couldn't generate a reply. Please try again.")
How it works
On every model call the middleware:
- Calls the handler. A response is not empty if it has text, any
tool_callsorinvalid_tool_calls, or astructured_response. Reasoning-only responses count as empty. - If empty, retries with
original messages + [nudge]. The blank attempt is not included (some providers reject empty assistant turns) and nudges do not accumulate across attempts. - When retries are exhausted,
on_exhausteddecides the outcome.
The predicate is exported for reuse:
from langchain_core.messages import AIMessage
from langchain_empty_response_retry import is_empty_ai_message
is_empty_ai_message(AIMessage(content=" ")) # True
Configuration
| Constructor arg | Default | Description |
|---|---|---|
max_retries |
2 |
Retries after the first attempt. 0 disables retries but still applies on_exhausted. |
nudge |
see DEFAULT_NUDGE |
Text of the corrective message. |
nudge_role |
"human" |
"human" or "system". Mid-conversation system messages are rejected by some providers. |
on_exhausted |
"return" |
"return" returns the last response unchanged, "error" raises EmptyModelResponseError, any other string is returned to the user as the reply. |
is_empty |
None |
Custom predicate (AIMessage) -> bool replacing the default check. |
Ordering
Middleware order is outermost first. Recommended placement:
- Place inside any middleware that trims or rewrites message history, so the nudge is not trimmed away.
- Put a
ModelCallLimitMiddlewareoutermost as a hard ceiling; retries across several guards multiply model calls.
Limitations
- Streaming: the retry happens after the first attempt has already streamed. Final state is correct, but a live
stream_mode="messages"consumer may have seen the rejected attempt. - Does not fix blank responses caused by context overflow or hard safety blocks; retries will also be blank.
- Each retry is a full model call. Keep
max_retriessmall.
Development
pip install -e ".[dev]"
pytest
License
MIT
Metadata
Release files for langchain-empty-response-retry 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| langchain_empty_response_retry-0.1.0.tar.gz | 7.7 kB | Without provenance | Details for langchain_empty_response_retry-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| langchain_empty_response_retry-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for langchain_empty_response_retry-0.1.0-py3-none-any.whl |
Total release size: 14.7 kB
Release files / langchain_empty_response_retry-0.1.0.tar.gz
| Download URL | langchain_empty_response_retry-0.1.0.tar.gz |
|---|---|
| Size | 7.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f467cd93e4ca24ffd7fc91238daf779df108e230936ee033d030a8bf3fb10738
|
|
BLAKE2b-256 checksum How to use checksums |
555823c42f097fc1826f66e64d8bdf10c12c51a6565f35d4f396a998df42188c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / langchain_empty_response_retry-0.1.0-py3-none-any.whl
| Download URL | langchain_empty_response_retry-0.1.0-py3-none-any.whl |
|---|---|
| Size | 7.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
21bece0ac060f607706624007c576bd7029e323de695f5505c5de16362026a69
|
|
BLAKE2b-256 checksum How to use checksums |
8dec32f1d4a64740ac7c3d693acc7fa6ea645abcdcf9b07e95048eca8285ab13
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Security for langchain-empty-response-retry 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.