How do you stop a LangChain agent from retrying?
Published
A model can request another tool call. Your application decides whether it may run. Use a tool-call limit, handle its error and report an incomplete task honestly.
Explanation & code
LangChain 1.5.11 with predetermined model messages and a fake support service. Three is our demo ceiling. This limits model-requested tool calls, not time, cost or HTTP retries inside a tool. With error behavior, an oversized parallel batch raises before that batch executes. A hung tool needs a separate timeout.
Understand it. Then fix it.
The model asks; the application enforces
An AI agent uses a model to choose tools: functions exposed by the application. In our test, the model repeatedly requests lookup for support ticket 42. The fake service returns unavailable. These are predetermined model messages driving the real framework, not a live model incident or a claim that every model loops.
Use the native tool-call limiter
LangChain is a framework for model-based applications. Its middleware runs checks around agent execution. This global limit has no toolName filter: it counts requested tool invocations across one agent run. Select error explicitly; the default continue blocks excess calls while allowing the model to continue. model is a configured tool-capable model; read is our lookup function. Real data access also requires server-side authorization.
import { createAgent, tool, toolCallLimitMiddleware, ToolCallLimitExceededError as LimitError } from 'langchain';
import { z } from 'zod';
export function makeSupportAgent(model, read) {
const lookup = tool(({ ticket }) => read(ticket), {
name: 'lookup', description: 'Read the status of my support ticket',
schema: z.object({ ticket: z.string() }),
});
return createAgent({
model, tools: [lookup],
middleware: [toolCallLimitMiddleware({
runLimit: 3, exitBehavior: 'error',
})],
});
}Return an incomplete result at the application boundary
Catch only the limit error. Do not label unrelated failures as budget exhaustion. answered here means a final model reply was returned; it is not a general proof of correctness. The video shortens this to the relevant try/catch. LimitError is an import alias for ToolCallLimitExceededError, not a custom error class.
// LimitError is imported above.
export async function runSupport(agent, request) {
try {
const result = await agent.invoke(request);
return { status: 'answered', message: result.messages.at(-1).content };
} catch (error) {
if (!(error instanceof LimitError)) throw error;
return { status: 'incomplete', reason: 'tool_limit' };
}
}Predict the result before running the fixture
Three sequential failed lookups execute. The fourth model response requests another lookup, but the middleware raises before it runs. There are four model calls and three tool executions in this test. A successful first lookup instead produces a final answer after one tool call and two model calls. Three is a ceiling, not a target.
// Sequential failure fixture
{ status: "incomplete", reason: "tool_limit" }
// Successful first lookup fixture
{ status: "answered", message: "Ticket 42 is open" }A count cap is not a complete budget
One model response can request several tools. Under error behavior, a batch exceeding the remaining limit raises before any tool in that batch runs. Calls do not measure seconds or dollars, and internal retries inside a tool are not separate agent tool requests. Add task-appropriate deadlines, cancellation, cost controls and result validation separately. Five offline tests verify the shown boundaries.
Code blocks are teaching excerpts. Keep the surrounding error handling and application requirements.
Save the code excerpts ↓Read the full transcript
LangChain is a framework for apps that use language models. Why is my agent still calling a broken tool? An agent uses a model to choose tools: functions your app can run. In this test, it keeps asking for the same failed lookup. Can I just tell the model to stop retrying? A prompt is an instruction. Enforce the limit in code. This middleware checks tool requests before they run. Run limit three allows at most three tool calls per run. Error stops the run if another request exceeds that limit. Same test: three lookups return unavailable. The fourth request reaches the limit check, but never runs the tool. Catch that specific limit error. Our app returns incomplete, not a made-up answer. Other errors still get thrown. What if the first lookup works? The model can answer and finish after one tool call. Three is a ceiling, not a target. This counts calls, not seconds or dollars. A hanging tool needs a separate timeout. Three failed calls. Zero answers. Our robot would like a promotion to senior support.