LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnAdvanced Agents & RetrievalAgent Failure & Recovery
🤖HardLLM Agents & Tool Use

Agent Failure & Recovery

Add validation checks, retries, checkpointed recovery, state reconciliation, loop breakers, and graceful degradation when LLM agents hallucinate, stall, or drift from their tools.

48 min read
Learning path
Step 132 of 196 in the full curriculum
Agent Memory & PersistenceRecursive Language Models (RLM)

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

A deploy-status agent gets one plain question: "What happened to run RUN-842?" It should look up a CI (continuous integration) run, read the failing job, and give you a short summary. Instead, it might call a tool that doesn't exist, send the wrong field name, read the same result until its budget runs out, or say a rollback succeeded after the write timed out. The response can still sound calm and complete.

Agent memory can carry scoped context across that run, but it can't decide whether a recalled note is still true, whether a timed-out write committed, or whether a generated tool call is safe. An agent runtime is the trusted loop around the model: it validates proposals, executes allowed tools, records observations, and checks claims before they leave the system. ReAct interleaved reasoning and actions,[1]Reference 1ReAct: Synergizing Reasoning and Acting in Language Models.https://arxiv.org/abs/2210.03629 while Toolformer explored learned API use.[2]Reference 2Toolformer: Language Models Can Teach Themselves to Use Tools.https://arxiv.org/abs/2302.04761 Neither makes those runtime checks optional.

We'll stay with RUN-842 and follow one failure at a time. Start by separating a plausible answer from verified state, then add validation, retry policy, circuit breakers, checkpointed resume, and bounded fallback. Success isn't "never fail." It means keeping failure visible, bounded, and reversible before a generated claim or an uncertain write reaches a user.

Why is "fail transparently" a better goal than "never fail" for production agents?

Answer

LLM agents are probabilistic and depend on tools, state, prompts, and external services. You can't eliminate every bad trajectory. You can make failures visible, bounded, reversible, and recoverable before they harm users or trigger duplicate side effects.

Why agent failures require runtime checks

Once a model can choose tools, tests alone can't police every generated path. Ask what must be checked while RUN-842 is in flight. The comparison makes the extra runtime boundary visible:

Failure ModeTraditional SoftwareLLM AgentRecovery Strategy
Invalid inputRejected with error messageAgent hallucinates a "valid" responseValidation checks: Schema checks before tool execution
Infinite loopCPU spin or runaway resource useAgent keeps calling the same tool with slight variationsSemantic Loop Detection: Hash canonicalized actions and compare intent similarity
Service unavailableConnection timeoutAgent fabricates the API responseCircuit Breaker: Fail fast and fallback to static logic
Logic errorWrong result, sometimes unnoticedPlausible but unsupported answer or tool choiceVerifier + source check: Validate against deterministic evidence where available

The extra risk comes from generated actions and answers. A small input or sampling change can make the same agent choose a different tool or interpret the same observation as success. Boundary checks catch known errors; runtime signals also need to catch a trajectory that is quietly getting worse.

You can't unit-test every conversational path. Offline eval harnesses still matter, but they miss generated branches you didn't sample. Runtime guardrails fill that gap with schema checks, allowlists, step limits, loop detection, budgets, verifiers, and side-effect reconciliation. When the model drifts, the surrounding system either steers it back or stops before the next untrusted write.

What moves from test time to runtime when software becomes agentic?

Answer

Some correctness checks must run during execution: schema validation, tool allowlists, step limits, loop detection, budget guards, verifier checks, and side-effect reconciliation. Offline tests still matter, but they can't cover every generated action path.

Why small per-step errors compound

Put numbers on the risk before naming the equation. A task with 40 required steps can succeed less than half the time when each step succeeds 98% of the time. In the toy model, every step is required and the steps are independent, so task success is:

P(success)≈pnP(\text{success}) \approx p^{n}P(success)≈pn

Here, p is the success probability for one step and n is the number of required steps. The product decays fast, even when each individual step looks reliable:

Per-step success p5 steps20 steps50 steps
99%95%82%61%
95%77%36%8%
90%59%12%0.5%

A 95%-reliable step can look acceptable in a one-shot demo and become nearly useless in this 50-step model. Real failures are rarely independent or equally likely. One expired credential can break every later call, while a checkpoint can make some failures recoverable. Treat the equation as a warning about long chains, not as a production reliability estimator.

The toy model isn't a benchmark. METR's March 2025 task-horizon analysis measured the length of software tasks agents could complete at a given reliability level. On that suite, Claude 3.7 Sonnet reached roughly one hour of human task time at 50% reliability.[3]Reference 3Measuring AI Ability to Complete Long Taskshttps://metr.org/blog/2025-03-19-measuring-ai-ability-to-complete-long-tasks/ That's a dated snapshot of one model on one benchmark, not a per-step estimate or a current leaderboard claim. It still shows why a capability demo and a dependable long-running workflow are different engineering targets.

The τ\tauτ-bench paper gives the same warning from another angle. Its retail and airline tasks involve an agent, a user, and tools. GPT-4o got below 25% pass^8 in retail: fewer than a quarter of tasks succeeded on all eight sampled trials.[4]Reference 4Tau-Bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domainshttps://arxiv.org/abs/2406.12045 A system can look capable on one run and still be inconsistent across repeats.

Every defense in this lesson attacks that product in one of two ways. It can raise per-step reliability p with validation, action contracts, or structured feedback. Or it can shrink the number of unrecovered steps with retries, fallbacks, checkpoints, loop breakers, and escalation. You can't make p = 1, so build a system that survives the steps where p < 1.

An agent step succeeds 98% of the time. Why might a 40-step task still fail more than half the time?

Answer

Dependent steps multiply: 0.98^40 is about 0.45, so the task succeeds under half the time even though each step looks reliable. Long trajectories amplify small per-step error rates, which is why recovery and bounding matter more than raising single-step accuracy alone.

The deploy-status agent

The equation explains why a long trajectory is fragile. Now trace one short task so each boundary has a concrete place in the story. The ReAct agent loop supplies the plan-act-observe pattern, and function calling lets a model propose a call against a tool schema.

A single ReAct-style agent receives a developer question, plans a tool call, executes it, and observes the result:

  1. Developer asks: "What happened to run RUN-842?"
  2. Agent reasons: "I need to query CI status for run RUN-842."
  3. Agent acts: Calls get_run with {"id": "RUN-842"}.
  4. Agent observes: "Status: failed, failing job: unit-tests."
  5. Agent answers: "Run RUN-842 failed in unit-tests."

That happy path hides three separate decisions: choose a registered tool, serialize arguments that satisfy its contract, and stop only after verified evidence arrives. Any one can fail. The sections that follow keep the same run ID while adding one symptom and one defense at a time.

Classify failure before choosing recovery

Recovery follows the boundary that failed, not a label such as "the model was wrong." A timeout shows why labels aren't enough: a read may be safe to retry, while a write must be reconciled before any replay. Use the observable evidence to choose the first safe response:

Failed boundaryObservable evidenceSafe first response
ProposalUnknown tool, invalid arguments, forbidden actionReject before execution; return structured correction feedback
DependencyTimeout, rate limit, or temporary service failureRetry only if the fault is transient and the operation is safe to repeat
External stateTimeout after a write; local state disagrees with source of truthReconcile using an operation ID or idempotency key before replay
ProgressRepeated call, repeated intent, or exhausted budgetStop, narrow capabilities, or escalate with trace evidence
ResultEmpty, stale, or unsupported answerValidate evidence; degrade explicitly rather than inventing an answer

Read the table as an incident triage order. First ask whether a side effect may already have happened. If not, classify the dependency fault, then look for a trusted checkpoint before deciding to stop or escalate.

Diagram showing Failed step, Write uncertain?, Reconcile, and Transient fault?.
Failed step, Write uncertain?, Reconcile, and Transient fault?.

Recovery choice depends on side-effect uncertainty, fault class, and checkpoint validity. Retry is only one branch.

A deploy agent says "deployment approved" because an earlier CI pass was hallucinated. Which boundary failed, and what should catch it before the reply reaches a user?

Answer

The result boundary failed because the answer lacked verified evidence; persisted false context may also have contaminated state. Validate the CI handoff against source-of-truth data before downstream use. If any write has uncertain status, reconcile it with an idempotency key before replay.

Worked example: the wrong parameter

Start with a malformed proposal that never reaches external state. get_run expects {"id": "RUN-842"}, but the deploy-status agent sends {"run_id": "RUN-842"} instead. The API rejects it with a 400 Bad Request: Missing required parameter 'id'.

A naive agent sees the error and sends the exact same call again. Nothing changed, so it gets the same 400. That's the "just try again" anti-pattern: without a reason it can act on, the agent has no basis for changing its proposal.

Feed the error back as structured feedback rather than a raw exception string. The agent can compare its proposal with the tool schema, correct the key name, and try again. This is a repair retry: validation supplies new information before another model call. It doesn't require the longer-lived episodic reflection used by Reflexion, which we revisit later.

The corrected trace is short enough to inspect by hand:

text
1Step 1: get_run({"run_id": "RUN-842"}) -> 400 Bad Request: Missing required parameter 'id'. 2Step 2: Agent reflects: "I used 'run_id' but the schema requires 'id'." 3Step 3: get_run({"id": "RUN-842"}) -> 200 OK: Status failed, failing job unit-tests.

The retry must carry new information. The same prompt with the same context will likely produce the same wrong answer. You need to tell the model exactly what went wrong in a format it can act on.

Why is the second get_run({"id": "RUN-842"}) attempt different from a blind retry?

Answer

The retry includes new diagnostic information: the last call used run_id, but the schema requires id. That changes the next generation. A blind retry repeats the same context and hopes sampling changes the outcome.

Validate arguments before execution

Strict JSON schema validation gives this failure a clean boundary. Model APIs often support tool calling or structured outputs, but you still validate arguments before execution and apply authorization separately. Production code often uses Pydantic or Zod. The standard-library function below makes the contract visible: get_run accepts only {"id": "RUN-842"}, rejects unknown keys, and returns a one-line correction instead of executing.

validate-run-tool-arguments.py
1import re 2 3class ToolCallError(Exception): 4 pass 5 6RUN_ID = re.compile(r"^RUN-[0-9]{3,}$") 7 8def validate_tool_call(raw_args: dict[str, object]) -> dict[str, str]: 9 unknown = [key for key in raw_args if key != "id"] 10 if unknown: 11 raise ToolCallError("get_run requires valid field: id") 12 run_id = raw_args.get("id") 13 if not isinstance(run_id, str) or not RUN_ID.fullmatch(run_id): 14 raise ToolCallError("get_run requires valid field: id") 15 return {"id": run_id} 16 17try: 18 validate_tool_call({"run_id": "RUN-842"}) 19except ToolCallError as exc: 20 print("rejected:", exc) 21print("accepted:", validate_tool_call({"id": "RUN-842"})["id"])
Output
1rejected: get_run requires valid field: id 2accepted: RUN-842

Common mistake: Returning a raw Python traceback to the LLM. Stack traces are noisy, and the model will often retry the same mistake after skimming them. Agentic debugging means sending the smallest root-cause sentence that changes the next proposal, not a 50-line trace.

The agent called get_run({"run_id": "RUN-842"}), and the tool returned Missing required parameter 'id'. What feedback should the next retry receive?

Answer

The retry should receive structured feedback that names the mismatch: "The tool requires id, but the last call used run_id." The model needs that new information; repeating the same prompt or dumping a raw traceback is likely to repeat the failure.

Tool call hallucination

Shape validation still isn't enough. A payload can be well-formed and name a tool that no runtime has registered. Tool misuse has two concrete forms: the model invents a tool, or it emits malformed arguments for a real one. When context is incomplete, a plausible-sounding name can replace a request for clarification.

For example, our deploy-status agent might output:

text
1// Model generates this: 2{"tool": "search_runs", "args": {"query": "run RUN-842", "format": "json"}} 3// But the actual API expects: 4{"tool": "get_run", "params": {"id": "RUN-842"}}

search_runs sounds reasonable, but it isn't in the registry. Without an allowlist check, the runtime either sends the call to a nonexistent endpoint or lets the model infer a nearby capability. Either response creates an error the agent may misread.

Reject calls outside the tool registry

Use the same schema validation from above, plus a tool allowlist. Before executing any tool call, check whether the requested tool name exists in the registered tool set. If it doesn't, return a clear error: Tool 'search_runs' is not available. Available tools: get_run, get_logs, suggest_alternative.

This transforms an ambiguous failure into structured feedback the agent can use to self-correct.

Why should hallucinated tools be rejected before any execution attempt?

Answer

The tool name is part of the security and capability boundary. If search_runs isn't registered, the runtime shouldn't infer a nearby endpoint or let the model improvise. It should reject the call and show the allowed tool names.

Infinite loops (the "stuck agent")

Fixing one proposal doesn't guarantee progress. A valid call can repeat forever, either exactly or with new wording. A deploy-status agent might produce this trace:

text
1Step 1: get_run({"id": "RUN-842"}) -> "Failed in unit-tests" 2Step 2: get_run({"id": "RUN-842"}) -> "Failed in unit-tests" 3Step 3: get_run({"id": "RUN-842"}) -> "Failed in unit-tests" 4... (continues forever)

Or with semantic rephrasing:

text
1Step 1: get_run({"id": "RUN-842"}) -> "Failed in unit-tests" 2Step 2: search("status for run RUN-842") -> "Failed in unit-tests" 3Step 3: search("RUN-842 failing job") -> "Failed in unit-tests" 4Step 4: search("where is run RUN-842") -> "Failed in unit-tests" 5... (continues forever)

The missing ingredient is an explicit progress signal. A vague stopping condition, weak error feedback, or absent state check lets the model propose the same approach with slightly different wording. The model isn't required to prove that its new action differs from the failed one, so the runtime has to detect repetition.

Detect repeated actions before the hard cap

A step cap is too late. LoopBreaker.check looks at a proposed tool name and arguments before the call runs. It combines exact-call hashes, same-tool streaks, and approximate intent similarity, then returns True when the next call would cross a loop threshold.

defense-strategy.py
1import difflib 2import hashlib 3import json 4import re 5 6class LoopBreaker: 7 def __init__( 8 self, 9 max_steps: int = 15, 10 max_identical: int = 3, 11 same_tool_streak: int = 5, 12 semantic_threshold: float = 0.9, 13 ): 14 self.max_steps = max_steps 15 self.max_identical = max_identical 16 self.same_tool_streak = same_tool_streak 17 self.semantic_threshold = semantic_threshold 18 self.tool_call_history: list[dict[str, str]] = [] 19 20 def _stable_call_hash(self, tool_name: str, args: dict[str, object]) -> str: 21 canonical_args = json.dumps( 22 args, 23 sort_keys=True, 24 default=str, 25 separators=(",", ":"), 26 ) 27 return hashlib.sha256(f"{tool_name}:{canonical_args}".encode("utf-8")).hexdigest() 28 29 def _intent_text(self, tool_name: str, args: dict[str, object]) -> str: 30 argument_text = " ".join(str(value).strip().lower() for value in args.values()) 31 identifiers = sorted(set(re.findall(r"\b[a-z]+-\d+\b", argument_text))) 32 combined = f"{tool_name.replace('_', ' ')} {argument_text}" 33 34 # Normalize tool-specific phrasing into a task-level intent. 35 if identifiers and "run" in combined: 36 return f"lookup run {' '.join(identifiers)}" 37 38 fields = [tool_name.replace("_", " ")] 39 fields.extend(identifiers) 40 fields.extend( 41 str(args[key]).strip().lower() 42 for key in ("query", "prompt", "text", "task") 43 if args.get(key) 44 ) 45 return " ".join(fields) 46 47 def check(self, tool_name: str, args: dict[str, object]) -> bool: 48 """Returns True if a loop is detected.""" 49 call = { 50 "tool": tool_name, 51 "args_hash": self._stable_call_hash(tool_name, args), 52 "intent_text": self._intent_text(tool_name, args), 53 } 54 55 if len(self.tool_call_history) >= self.max_steps: 56 return True 57 58 identical_attempts = 1 + sum( 59 1 for h in self.tool_call_history if h["args_hash"] == call["args_hash"] 60 ) 61 if identical_attempts >= self.max_identical: 62 return True 63 64 same_tool_attempts = 1 65 for h in reversed(self.tool_call_history): 66 if h["tool"] != tool_name: 67 break 68 same_tool_attempts += 1 69 if same_tool_attempts >= self.same_tool_streak: 70 return True 71 72 recent_window = self.tool_call_history[-(self.same_tool_streak - 1):] 73 similar_attempts = 1 + sum( 74 1 75 for h in recent_window 76 if h["intent_text"] 77 and difflib.SequenceMatcher( 78 None, h["intent_text"], call["intent_text"] 79 ).ratio() >= self.semantic_threshold 80 ) 81 if similar_attempts >= self.max_identical: 82 return True 83 84 # Only append after checks to avoid polluting history with the failing call 85 self.tool_call_history.append(call) 86 return False 87 88breaker = LoopBreaker(max_identical=3) 89print("first:", breaker.check("get_run", {"id": "RUN-842"})) 90print("second:", breaker.check("get_run", {"id": "RUN-842"})) 91print("third:", breaker.check("get_run", {"id": "RUN-842"})) 92 93cross_tool = LoopBreaker(max_identical=2) 94print("cross-tool first:", cross_tool.check("get_run", {"id": "RUN-842"})) 95print("cross-tool repeat:", cross_tool.check("search", {"query": "where is run RUN-842"}))
Output
1first: False 2second: False 3third: True 4cross-tool first: False 5cross-tool repeat: True

The thresholds include the current attempt. That detail matters: an off-by-one bug burns one more tool call, token round-trip, and latency spike before the breaker trips.

The task-level normalization makes the runnable guard catch get_run({"id": "RUN-842"}) versus search("where is run RUN-842") even though the tool names and argument keys differ. For broader production traffic, replace or augment this narrow normalization with embeddings over a compact task and state summary. Evaluate that semantic detector on both true loops and legitimate follow-up actions against the same entity.

Common mistake: Assuming that max_steps alone is enough to prevent loops. A step limit of 15 doesn't stop an agent from wasting 15 steps (and thousands of tokens) on a futile loop. Semantic loop detection stops the bleeding early, often after 2-3 repeated attempts.

Why does the loop breaker track both exact argument hashes and approximate intent text?

Answer

Exact call hashes catch identical repeats such as get_run({"id": "RUN-842"}) without confusing a different tool that happens to accept the same ID. Intent text catches semantic repeats such as search("status for RUN-842"), search("RUN-842 failing job"), and search("what happened to run RUN-842"). Agents often loop by rephrasing, so both signals matter.

Why does the loop breaker append a call only after checks pass?

Answer

Rejected calls shouldn't pollute the normal history. If you record failed loop attempts as successful progress, downstream logic may think the agent advanced. The breaker should block the current call, report the loop reason, and let recovery decide whether to summarize, replan, fallback, or escalate.

Token budget exhaustion

A loop can stop making progress before it hits the hard step cap, while still consuming context and money. Consider a 20-step task whose runtime appends every tool call and observation to one transcript. Each prompt grows as history accumulates, and later turns resend most of what came before. The final step includes the previous 19 steps, so total input spend can grow roughly quadratically rather than like 20 isolated calls.

Left unchecked, that growth can overflow the context window, trigger provider rejection or truncation, and inflate the API bill. A subtle loop can burn through tokens long before an operator sees a clear error.

Use soft and hard budgets

Treat budget as two layers. A hard limit is a circuit breaker: if tokens or estimated cost cross the cap, the run stops. Soft management watches the window earlier and compact validated state before that stop is the only option.

TokenBudget.consume records the input and output tokens from a completed generation, then raises BudgetExhausted if a cap is crossed. Pair that post-call accounting with a preflight context estimate and a provider-side output cap so one generation can't overshoot too far.

defense-strategy-2.py
1class BudgetExhausted(Exception): 2 pass 3 4class TokenBudget: 5 def __init__( 6 self, 7 max_tokens: int = 100_000, 8 max_cost_usd: float = 1.0, 9 pricing: dict[str, dict[str, float]] | None = None, 10 ): 11 self.max_tokens = max_tokens 12 self.max_cost = max_cost_usd 13 self.used_tokens = 0 14 self.estimated_cost = 0.0 15 self.pricing = pricing or {} 16 17 def consume(self, input_tokens: int, output_tokens: int, model: str): 18 """Updates token counts and raises an error if budget is exceeded.""" 19 self.used_tokens += input_tokens + output_tokens 20 self.estimated_cost += self._calculate_cost(input_tokens, output_tokens, model) 21 22 # Hard stop to prevent runaway billing 23 if self.used_tokens > self.max_tokens: 24 raise BudgetExhausted(f"Token limit exceeded: {self.used_tokens}/{self.max_tokens}") 25 if self.estimated_cost > self.max_cost: 26 raise BudgetExhausted(f"Cost limit exceeded: ${self.estimated_cost:.2f}/${self.max_cost:.2f}") 27 28 def _calculate_cost(self, input_tokens: int, output_tokens: int, model: str) -> float: 29 rates = self.pricing.get(model) 30 if rates is None: 31 raise BudgetExhausted(f"Pricing missing for model: {model}") 32 33 input_cost = (input_tokens / 1_000_000) * rates["input"] 34 output_cost = (output_tokens / 1_000_000) * rates["output"] 35 return input_cost + output_cost 36 37budget = TokenBudget( 38 max_tokens=1_000, 39 max_cost_usd=0.01, 40 pricing={"agent-model": {"input": 1.0, "output": 2.0}}, 41) 42budget.consume(200, 50, "agent-model") 43print("used tokens:", budget.used_tokens) 44try: 45 budget.consume(900, 20, "agent-model") 46except BudgetExhausted as exc: 47 print("blocked:", str(exc).split(":")[0])
Output
1used tokens: 250 2blocked: Token limit exceeded

Production tip: When the budget approaches a configured soft threshold, stop optional exploration and compact only validated facts and pending work. A generated summary is lossy and can't replace the durable record of writes, approvals, or idempotency keys.

An agent has a 100,000-token budget and reaches 82,000 tokens before finishing. Should the system wait for the hard limit, summarize, or retry from scratch?

Answer

Stop optional exploration and compact validated context before the hard limit. The soft threshold protects latency and cost while preserving useful state. Retrying from scratch wastes work, and waiting for the hard limit risks an abrupt failure after the user has already waited.

Why can a 20-step agent run cost more than 20 separate one-step calls?

Answer

Each later step often resends much of the prior trajectory: user request, thoughts, tool calls, observations, and summaries. Prompt size grows with the run, so total input tokens can grow roughly quadratically unless the runtime summarizes or truncates state.

Cascading failures in multi-agent systems

Budgets bound one run. A multi-agent graph adds another risk: a bad handoff can turn one local mistake into a chain of confident decisions. In a supervisor-worker DAG (directed acyclic graph), failures usually spread because later nodes trust unsupported output or because local state drifts from the source of truth after a partial side effect.

Here's a failure chain on RUN-842:

  1. Monitor agent: "CI check passed" (invents a test result that never happened).
  2. Gate agent: "Tests passed, so approve deployment" (treats the invented check as source-of-truth state).
  3. Responder agent: "Run RUN-842 passed yesterday" (sends a confident user-facing answer grounded in false state).

A second version is worse because it looks operationally healthy. Suppose rollback_deploy times out after the deployment controller already committed the rollback. If the orchestrator records that step as failed and retries, the duplicate side effect leaves agent state out of sync with production.

Validate handoffs and reconcile writes

Pass agent output across a boundary only after verification, and never treat a timed-out write as "nothing happened."

Format and authorization checks should be deterministic. Operational facts should be checked against their source of truth. A model-based critic can flag candidates for review, but it can't prove that RUN-842 passed or that a rollback committed. If validation fails, halt and either request a corrected proposal or escalate. For side-effecting calls, add read-after-write reconciliation and idempotency keys before telling downstream agents the action succeeded.

validated_handoff runs those checks. Each validator returns a ValidationResult. If every check passes, the original output continues. If any check fails, the function returns a structured error for retry or review instead of poisoning the gate agent.

defense-strategy-3.py
1from dataclasses import dataclass 2from typing import Callable 3 4@dataclass 5class ValidationResult: 6 valid: bool 7 reason: str = "" 8 9Validator = Callable[[dict[str, object]], ValidationResult] 10 11def validated_handoff( 12 output: dict[str, object], 13 validators: list[Validator], 14) -> dict[str, object]: 15 """Validate agent output before passing it to the next agent.""" 16 for validator in validators: 17 result = validator(output) 18 if not result.valid: 19 return { 20 "status": "validation_failed", 21 "error": result.reason, 22 "fallback": "Request human review of this output", 23 } 24 return output 25 26def require_ci_evidence(output: dict[str, object]) -> ValidationResult: 27 if output.get("claim") == "ci_passed" and output.get("ci_status") != "passed": 28 return ValidationResult(False, "CI claim lacks source-of-truth evidence") 29 return ValidationResult(True) 30 31blocked = validated_handoff( 32 {"claim": "ci_passed", "run_id": "RUN-842", "ci_status": "missing"}, 33 [require_ci_evidence], 34) 35print(blocked["status"], blocked["error"]) 36passed = validated_handoff( 37 {"claim": "ci_passed", "run_id": "RUN-842", "ci_status": "passed"}, 38 [require_ci_evidence], 39) 40print("handoff:", passed["claim"])
Output
1validation_failed CI claim lacks source-of-truth evidence 2handoff: ci_passed

External service failures

External faults split into transient failures (429, timeouts, brief 503s) and outages that persist after the retry window. An agent feels the difference more sharply than a single-request client because one user question can fan out into several model and tool calls. Late turns also carry more context, so a run can hit a token-per-minute quota even when request count looks normal.

Retry only classified transient faults

Before adding a retry, ask two questions: is the fault transient, and is the operation safe to repeat? For transient network and rate-limit errors, use exponential backoff with jitter. Immediate retries tend to hit the same overload; jitter spreads attempts across time instead of letting a fleet retry in lockstep.[5]Reference 5Exponential Backoff And Jitterhttps://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/

Amazon Web Services (AWS)'s "full jitter" form sleeps a random time in [0, min(cap, base * 2**(attempt-1))]. A 429, 502/503, or timeout is only a retry candidate: provider guidance must permit the retry, and the operation must be safe to repeat. Respect Retry-After when available. Keep a 400 schema error or 401 auth failure off this path.

The retry helper classifies those faults, then retries with full jitter. Its fake clock exposes two retries without making you wait on real sleeps. Libraries such as Tenacity wrap the same idea; the point is the classification boundary, not the decorator.

retry-with-full-jitter.py
1import random 2 3class RetryableModelError(Exception): 4 pass 5 6class RateLimitExceeded(RetryableModelError): 7 pass 8 9class UpstreamTimeout(RetryableModelError): 10 pass 11 12class UpstreamUnavailable(RetryableModelError): 13 pass 14 15def normalize_provider_error(exc: Exception) -> Exception: 16 status_code = getattr(exc, "status_code", None) 17 message = str(exc).lower() 18 if status_code == 429 or "rate limit" in message: 19 return RateLimitExceeded(str(exc)) 20 if isinstance(exc, TimeoutError) or "timeout" in message: 21 return UpstreamTimeout(str(exc)) 22 if status_code in {502, 503, 504} or "temporarily unavailable" in message: 23 return UpstreamUnavailable(str(exc)) 24 return exc 25 26def call_llm_with_retry( 27 call, 28 *, 29 attempts: int = 3, 30 base: float = 1.0, 31 cap: float = 30.0, 32 rng: random.Random | None = None, 33 sleep=lambda _seconds: None, 34): 35 """Retry transient faults with full jitter. Deterministic failures raise immediately.""" 36 rng = rng or random.Random(0) 37 last_error: Exception | None = None 38 for attempt in range(1, attempts + 1): 39 try: 40 return call() 41 except Exception as exc: 42 normalized = normalize_provider_error(exc) 43 last_error = normalized 44 retryable = isinstance(normalized, RetryableModelError) 45 if not retryable or attempt == attempts: 46 raise normalized from exc 47 delay = min(cap, base * (2 ** (attempt - 1))) 48 sleep(rng.uniform(0, delay)) 49 raise last_error or RuntimeError("retry loop exited without a result") 50 51class FakeClock: 52 def __init__(self) -> None: 53 self.slept: list[float] = [] 54 55 def sleep(self, seconds: float) -> None: 56 self.slept.append(seconds) 57 58def make_flaky(failures_before_success: int): 59 remaining = {"n": failures_before_success} 60 61 def call() -> str: 62 if remaining["n"] > 0: 63 remaining["n"] -= 1 64 exc = Exception("rate limit") 65 exc.status_code = 429 66 raise exc 67 return "RUN-842 failed in unit-tests" 68 69 return call 70 71clock = FakeClock() 72result = call_llm_with_retry( 73 make_flaky(2), 74 attempts=3, 75 rng=random.Random(1), 76 sleep=clock.sleep, 77) 78print("result:", result) 79print("retries:", len(clock.slept))
Output
1result: RUN-842 failed in unit-tests 2retries: 2

Notice what this helper doesn't repair. If the model keeps making the same schema mistake, the next attempt needs better information instead of more time. A concise validation error or tool-rejection reason is usually enough for that local repair. Reflexion is a broader, memory-backed pattern for learning from task feedback across trials, not a name for every corrected retry.

Prefer typed SDK exceptions and provider response headers in production. The normalizer above keeps a message fallback only to show the classification boundary without coupling the lesson to one SDK.

Failover must preserve the payload contract

A 429 or dead endpoint may route traffic to a backup model from the same provider or a second provider. That can improve availability, but it changes the correctness question: can the backup produce the same contract? Strict-schema support, JSON wrappers, field names, and function arguments vary across models and providers.

Validate primary and fallback responses against one canonical schema before dispatch. Give each route an adapter that maps its native output into that schema. Without that adapter, a malformed or plausible-but-wrong argument can reach a tool. If the fallback can't satisfy the contract, correct or escalate the validation failure. Never forward the payload unchanged.

Retry, resume, or roll back?

External faults are where retry logic usually starts, but the same decision tree applies everywhere. A retry is appropriate only when the failure is transient and nothing important has committed. A 429, 503, or dropped connection usually fits that pattern. A poisoned context or partial side effect doesn't.

Suppose the agent believes "rollback queued" even though the tool call timed out. Replaying the next step can duplicate production changes or make later reasoning depend on false state. The transport error tells you how the call ended, not what happened in the outside world.

Durable runtimes handle this with checkpoints rather than blind replay. LangGraph persists graph state as checkpoints keyed by thread_id and resumes from a saved super-step boundary.[6]Reference 6LangGraph Persistencehttps://docs.langchain.com/oss/python/langgraph/persistence Temporal persists workflow execution state in event history and replays deterministic workflow code after failures.[7]Reference 7Temporal Workflow Execution Overviewhttps://docs.temporal.io/workflow-execution Both patterns let you resume from the last confirmed-good boundary instead of restarting a long run from scratch.

Use three recovery paths:

  • Retry when the failure is transient and the step is side-effect free.
  • Resume from a checkpoint when prior state is still valid but expensive to recompute.
  • Reconcile or roll back when the failure happened around a non-idempotent side effect such as triggering a rollback, mutating a feature flag, or posting an incident update.

The small classifier below turns that distinction into an executable choice. An uncertain write never enters the ordinary retry path, even when its transport error looks transient.

choose-recovery-path.py
1def recovery_path(error: str, side_effect_status: str, checkpoint_valid: bool) -> str: 2 if side_effect_status == "uncertain": 3 return "reconcile before replay" 4 if checkpoint_valid and error == "worker_restarted": 5 return "resume checkpoint" 6 if side_effect_status == "none" and error in {"429", "503", "timeout"}: 7 return "retry with backoff" 8 return "stop or escalate" 9 10print("lookup timeout:", recovery_path("timeout", "none", False)) 11print("rollback timeout:", recovery_path("timeout", "uncertain", True))
Output
1lookup timeout: retry with backoff 2rollback timeout: reconcile before replay
Two timed-out RUN-842 tool calls side by side. get_run has no side effect, so recovery retries with jitter. rollback_deploy is an uncertain write with idempotency key rb-842-1, so recovery reconciles against the deployment controller before any replay.
Both calls timed out on RUN-842. The lookup can retry because nothing committed. The rollback can't, because the deployment controller may already have applied rb-842-1.

Production tip: Pair checkpoints with idempotency keys for outbound side effects. Resume must be safe even if the previous attempt failed after the external system committed but before your agent recorded success.

A rollback tool times out after the deployment controller may have committed the rollback. Why is blind retry wrong, and what should the orchestrator do instead?

Answer

Blind retry can issue a duplicate rollback because the timeout doesn't prove the side effect failed. Reconcile against the deployment controller with the rollback idempotency key, update durable state with the confirmed outcome, then resume from a checkpoint consistent with that outcome.

Which failures are good retry candidates, and which failures need a different recovery path?

Answer

Retry transient, side-effect-free failures such as rate limits, brief 503s, and timeouts before a write commits. Use structured feedback for schema mistakes, circuit breakers for persistent dependency outages, checkpoints for long-running state, and reconciliation for possible side effects.

Action-contract disconnect

Ask whether the action that will execute still matches the task the system accepted. A dangerous failure happens when the agent's proposed operation diverges from that contract. The two common states are a wrong tool choice despite a valid request and wrong argument serialization despite choosing the right tool.

Consider our deploy-status agent. Its structured intent says lookup_run for RUN-842. However, the emitted tool call might target a create_release tool or provide a different run ID, producing irrelevant results while the response text still sounds plausible.

Generated explanations don't constrain generated tool payloads. A model may describe a reasonable plan yet emit an unsupported parameter or a different identifier. Validate the executable payload against the task contract instead of asking for hidden reasoning or trusting a confident explanation.

Admit only actions that match the contract

The primary defense is execution verification: compare the proposed tool call to a structured task contract before execution. A critic model can help triage ambiguous output, but deterministic tool, identifier, permission, and schema checks should block clear violations.

validate-action-contract.py
1class ActionMismatch(Exception): 2 pass 3 4def validate_action_contract( 5 task_contract: dict[str, object], 6 tool_name: str, 7 tool_args: dict[str, object], 8 allowed_tools: set[str], 9) -> str: 10 if tool_name not in allowed_tools: 11 raise ActionMismatch("tool not allowed") 12 if task_contract["intent"] == "lookup_run" and tool_name != "get_run": 13 raise ActionMismatch("tool does not match intent") 14 if tool_args.get("id") != task_contract["run_id"]: 15 raise ActionMismatch("run ID changed") 16 return "admitted" 17 18contract = {"intent": "lookup_run", "run_id": "RUN-842"} 19print("lookup:", validate_action_contract(contract, "get_run", {"id": "RUN-842"}, {"get_run"})) 20try: 21 validate_action_contract(contract, "create_release", {}, {"get_run"}) 22except ActionMismatch as exc: 23 print("drift:", exc)
Output
1lookup: admitted 2drift: tool not allowed

Why is action-contract validation useful even if the model's explanation looks correct?

Answer

The explanation and tool payload are separate generated artifacts. A model can say "query the run database" while emitting a different tool or run identifier. The runtime should validate the executable payload against structured scope, not trust narration.

When a repair becomes Reflexion

Action validation fixes a malformed call. What if the call is valid but the strategy still produces no new evidence? The Reflexion paper describes an agent that turns task feedback into a verbal reflection, stores that reflection in episodic memory, and uses it to guide later trials.[8]Reference 8Reflexion: Language Agents with Verbal Reinforcement Learning.https://arxiv.org/abs/2303.11366 Its defining feature isn't the word "critique." It's the feedback-to-memory loop across attempts: Attempt -> task feedback -> verbal reflection -> episodic memory -> next attempt.

The wrong-field example doesn't need this machinery because its validator already provides the fix. Reflexion becomes useful when a failure is safe to revisit but the diagnosis is strategic rather than syntactic. Suppose the deploy agent repeatedly reads the same summary endpoint and still can't explain a failure:

text
1[Attempt] get_run({"id": "RUN-842"}) -> "failed in unit-tests" 2[Feedback] No new evidence: failure cause still unsupported. 3[Reflection] "The summary endpoint cannot reveal the assertion. Query the failed job logs next." 4[Memory] Store that lesson for the next trial on this task. 5[Next attempt] get_logs({"run_id": "RUN-842", "job": "unit-tests"})

The reflection changes strategy instead of merely repairing syntax. Scope memory carefully: a lesson learned for one repository, tool version, or incident can become stale or harmful elsewhere. Re-run tool allowlists, authorization, schema validation, and source-of-truth checks on the new proposal.

Reflection isn't proof: A verbal reflection remains generated guidance. It doesn't prove a factual claim or authorize a side effect.

What makes Reflexion different from a repair retry?

Answer

A repair retry uses immediate structured error feedback to correct the current proposal. Reflexion converts task feedback into a verbal lesson, stores it in episodic memory, and applies it to a later trial. It's useful for strategic failures that remain safe to revisit.

Most deterministic failures need only a structured error. Use memory-backed reflection for repeated or ambiguous failures that are safe to revisit and whose lessons have a clear scope. High-stakes or potentially committed effects should stop for approval or reconciliation instead.

When is a Reflexion loop overkill?

Answer

When the failure has a clear deterministic fix, such as "missing required field id." A concise structured error can steer the next proposal without adding generated reflection or memory. High-stakes writes need authorization or reconciliation, not self-critique.

The circuit breaker pattern

A circuit breaker stops a failing dependency from taking the rest of the agent with it. For RUN-842, that usually means failing fast once get_run or the model endpoint crosses a known error threshold, instead of letting every remaining step rediscover the outage.

The breaker has three states in its state machine: Closed lets requests flow and counts failures, Open fails fast while the dependency is unhealthy, and Half-open admits one probe after a cooldown.

Diagram showing Closed, Open, and Half-open.
Closed, Open, and Half-open.

Only dependency-health failures move the circuit toward Open. A schema or authorization error follows a correction path instead.

The labels above map to the runtime states: Closed is normal operation, Open avoids spending tokens on a dependency already known to be unhealthy, and Half-open tests recovery with one request. If that probe succeeds, traffic returns to Closed; if it fails, the circuit returns to Open.

Half-open keeps recovery controlled. Jumping from Open straight to Closed after a timeout would dump every queued agent call onto a still-failing API (the thundering-herd problem). One probe is cheaper than discovering the outage again with a burst of billable tokens.

Why should a circuit breaker use a Half-Open state instead of jumping directly from Open back to Closed after the cooldown?

Answer

Half-Open sends one probe request first. If it succeeds, traffic can resume; if it fails, the circuit reopens. Jumping straight to Closed can send many pending agent requests into an unhealthy dependency and create another outage or budget spike.

The implementation turns those two rules into state transitions. It gates Half-Open so only one probe runs at a time, then stamps each admitted call with a generation. Opening the circuit, or starting a new Half-Open generation, invalidates older in-flight completions. A stale success can't close a circuit that newer failures already opened.

the-circuit-breaker-pattern.py
1import asyncio 2import time 3 4class CircuitOpenError(Exception): 5 pass 6 7class CircuitBreaker: 8 """Prevents cascading failures by halting requests to failing services.""" 9 CLOSED = "closed" # Normal operation 10 OPEN = "open" # All calls fail fast 11 HALF_OPEN = "half_open" # Test with one call 12 13 def __init__(self, failure_threshold: int = 5, reset_timeout: float = 60.0): 14 self.state = self.CLOSED 15 self.failure_count = 0 16 self.failure_threshold = failure_threshold 17 self.reset_timeout = reset_timeout 18 self.last_failure_time = 0.0 19 self._lock = asyncio.Lock() 20 self._half_open_probe_in_flight = False 21 self._generation = 0 22 23 async def call(self, func, *args, **kwargs): 24 """Executes the function if the circuit is closed or half-open.""" 25 async with self._lock: 26 now = time.monotonic() 27 28 if self.state == self.OPEN: 29 if now - self.last_failure_time > self.reset_timeout: 30 self.state = self.HALF_OPEN 31 self._half_open_probe_in_flight = False 32 self._generation += 1 33 else: 34 raise CircuitOpenError("Circuit is open - failing fast") 35 36 if self.state == self.HALF_OPEN and self._half_open_probe_in_flight: 37 raise CircuitOpenError("Half-open probe already in flight") 38 39 is_half_open_probe = self.state == self.HALF_OPEN 40 if is_half_open_probe: 41 self._half_open_probe_in_flight = True 42 call_generation = self._generation 43 44 try: 45 result = await func(*args, **kwargs) 46 except Exception as exc: 47 async with self._lock: 48 if self._is_dependency_failure(exc): 49 self._on_failure(call_generation, is_half_open_probe) 50 elif call_generation == self._generation and is_half_open_probe: 51 self._half_open_probe_in_flight = False 52 raise 53 54 async with self._lock: 55 self._on_success(call_generation, is_half_open_probe) 56 return result 57 58 def _on_success(self, call_generation: int, was_half_open_probe: bool): 59 if call_generation != self._generation: 60 return 61 if self.state == self.HALF_OPEN and not was_half_open_probe: 62 return 63 self.failure_count = 0 64 self.state = self.CLOSED 65 self._half_open_probe_in_flight = False 66 67 def _on_failure(self, call_generation: int, was_half_open_probe: bool): 68 if call_generation != self._generation: 69 return 70 self.failure_count += 1 71 self.last_failure_time = time.monotonic() 72 if was_half_open_probe or self.failure_count >= self.failure_threshold: 73 self.state = self.OPEN 74 self._half_open_probe_in_flight = False 75 self._generation += 1 76 77 def _is_dependency_failure(self, exc: Exception) -> bool: 78 # Add provider-specific 429 and 5xx exceptions in production. 79 return isinstance(exc, (TimeoutError, ConnectionError)) 80 81async def verify_stale_success_cannot_close_newer_open_circuit(): 82 breaker = CircuitBreaker(failure_threshold=1) 83 old_call_started = asyncio.Event() 84 release_old_call = asyncio.Event() 85 86 async def old_success(): 87 old_call_started.set() 88 await release_old_call.wait() 89 return "old success" 90 91 async def newer_failure(): 92 raise TimeoutError("dependency failed") 93 94 old_task = asyncio.create_task(breaker.call(old_success)) 95 await old_call_started.wait() 96 97 try: 98 await breaker.call(newer_failure) 99 except TimeoutError: 100 pass 101 102 assert breaker.state == breaker.OPEN 103 release_old_call.set() 104 assert await old_task == "old success" 105 assert breaker.state == breaker.OPEN 106 107asyncio.run(verify_stale_success_cannot_close_newer_open_circuit()) 108print("stale success left circuit open")
Output
1stale success left circuit open

Count only failures that indicate dependency health, such as timeouts, connection failures, and selected 5xx responses. A 400 schema error or 401 auth failure needs correction or escalation, but it shouldn't open a dependency circuit.

The generation check handles a race that the one-probe flag alone can't prevent. Several calls may already be running while the circuit is Closed. If their newer peer failures open the circuit, a slower success from the old generation may still return its own result, but it no longer has permission to reset shared breaker state. Only a success admitted in the current Closed generation or the current Half-Open probe can close the circuit.

Choose breaker scope deliberately. A per-process breaker protects only the worker that observed failures and may be enough for a local resource. A fleet-wide breaker needs coordination through shared state, a service mesh, or provider-side routing. If you build it with a store such as Redis, make state transitions atomic and decide how the system behaves when that store is unavailable.

When does a circuit breaker need coordinated state in a multi-server deployment?

Answer

When the intended protection boundary is the whole fleet. Otherwise one worker may open its circuit while peers keep sending traffic to the same failing dependency. Coordination can come from shared state, a mesh, or upstream routing; a deliberately local breaker need not share state.

Fallback chains: graceful degradation

What can you still say when the live path is down? Start with the most capable route, validate its evidence, and move to a narrower claim only when that route fails. The chain below makes that order explicit:

Diagram showing Live tools, Read-only tools, Timestamped cache, and Stop + handoff.
Live tools, Read-only tools, Timestamped cache, and Stop + handoff.

Each fallback narrows its claim contract. Cached context must expose freshness; the final stage admits that live status is unavailable.

Each step trades open-ended behavior for a narrower claim. If the full RUN-842 agent fails or the circuit opens, move to a read-only lookup, then a timestamped cache, then an honest "live status unavailable" message. Fallback isn't automatically safe. A cache from 40 minutes ago can't answer "did the rollback finish?"

Set requires_live_data=True and evidence becomes part of the result contract. In the example, live tools and the read-only path both fail. The cached answer is rejected because the question needs current data, so the chain returns a static handoff instead of a stale "looked healthy" sentence.

fallback-chains-graceful-degradation.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class StrategyResult: 5 text: str 6 has_live_evidence: bool 7 8class FallbackChain: 9 DEFAULT_RESPONSE = ( 10 "Live run status is unavailable right now. Please retry or contact on-call." 11 ) 12 13 def execute(self, task: str, requires_live_data: bool = False) -> str: 14 strategies = [ 15 ("primary_agent", self._full_agent_execution), 16 ("backup_agent", self._simplified_agent), 17 ("cached_context", self._cached_context), 18 ("static_fallback", self._static_fallback), 19 ] 20 for name, strategy in strategies: 21 try: 22 result = strategy(task) 23 except TimeoutError: 24 continue 25 if name == "static_fallback": 26 return result.text 27 if self._validate_result(result, requires_live_data): 28 return result.text 29 return self.DEFAULT_RESPONSE 30 31 def _validate_result( 32 self, 33 result: StrategyResult, 34 requires_live_data: bool, 35 ) -> bool: 36 if not result.text.strip(): 37 return False 38 if requires_live_data and not result.has_live_evidence: 39 return False 40 return True 41 42 def _full_agent_execution(self, task: str) -> StrategyResult: 43 raise TimeoutError("get_run timed out") 44 45 def _simplified_agent(self, task: str) -> StrategyResult: 46 raise TimeoutError("read-only get_run timed out") 47 48 def _cached_context(self, task: str) -> StrategyResult: 49 return StrategyResult( 50 "Cached: RUN-842 looked healthy 40m ago", 51 has_live_evidence=False, 52 ) 53 54 def _static_fallback(self, task: str) -> StrategyResult: 55 return StrategyResult(self.DEFAULT_RESPONSE, has_live_evidence=False) 56 57print( 58 FallbackChain().execute( 59 "What happened to run RUN-842?", 60 requires_live_data=True, 61 ) 62)
Output
1Live run status is unavailable right now. Please retry or contact on-call.

In the fallback chain, why should the system return the first response that passes validation instead of trying every lower-capability strategy too?

Answer

Once a response is valid, more attempts add latency, cost, and new failure chances without improving reliability. The chain should degrade only as needed: primary agent, backup agent, direct model, then static response.

What should each fallback stage validate before returning to the user?

Answer

At minimum: non-empty output, policy compliance, required evidence, freshness, and clear degraded-mode disclosure. A cached or static path must not answer a question that requires unavailable live data.

Pinned reminders are not controls

Long traces create a recall problem before they create a policy problem. Long-context studies show a positional bias: models often do best when relevant information appears near the beginning or end of the context window, and noticeably worse when it's buried in the middle.[9]Reference 9Lost in the Middle: How Language Models Use Long Contextshttps://aclanthology.org/2024.tacl-1.9/ As tool output and scratchpad text accumulate, critical constraints can drift into that low-salience region.

A pinned reminder repeats a short set of important instructions near the current task. This can help recall when a trace is long, but it isn't an enforcement boundary. Authorization checks, tool allowlists, and output validation still need to live outside the model. The earlier guardrails lesson separates model guidance from deterministic controls in more detail.

For RUN-842, a pinned block can remind the model not to invent CI status and to confirm writes. Repeating those lines at the end of a long trace may improve recall, but it still isn't a security boundary. Authorization, allowlists, and output checks live in code.

the-pinned-constraints-pattern.py
1class PinnedConstraintsAgent: 2 CRITICAL_CONSTRAINTS = """ 3CRITICAL CONSTRAINTS (must follow): 41. Never disclose PII (Personally Identifiable Information) 52. Always confirm actions that modify data 63. Never invent a CI or deploy status 74. Maximum 10 tool calls per user request 8""" 9 10 def __init__(self, base_system_prompt: str): 11 self.base_system_prompt = base_system_prompt 12 13 def build_prompt(self, conversation_history: list, current_task: str) -> str: 14 prompt_parts = [ 15 self.base_system_prompt, 16 "", 17 "=== CONVERSATION HISTORY ===", 18 self._format_history(conversation_history), 19 "", 20 "=== CURRENT TASK ===", 21 current_task, 22 "", 23 self.CRITICAL_CONSTRAINTS, 24 ] 25 return "\n".join(prompt_parts) 26 27 def _format_history(self, history: list) -> str: 28 return "\n".join(f"{msg['role']}: {msg['content']}" for msg in history[-10:]) 29 30agent = PinnedConstraintsAgent("You look up CI runs.") 31prompt = agent.build_prompt( 32 [{"role": "user", "content": "What happened to run RUN-842?"}], 33 "Summarize RUN-842 without guessing.", 34) 35after_task = prompt.split("=== CURRENT TASK ===", maxsplit=1)[1] 36print("pinned after task:", "CRITICAL CONSTRAINTS" in after_task) 37print("ends with cap:", after_task.strip().endswith("Maximum 10 tool calls per user request"))
Output
1pinned after task: True 2ends with cap: True

Production tip: Keep the block short, prioritize task-critical reminders, and evaluate whether repeating it improves behavior. Moving more policy text to the end doesn't create a stronger security boundary.

Pinned constraints say "Never disclose PII." Is that enough to enforce privacy?

Answer

No. Pinned constraints help recall by keeping rules near the end of the prompt, but they aren't a security boundary. Privacy still needs deterministic checks outside the model, such as authorization, tool allowlists, redaction, and output validation.

Which constraints belong in a pinned block, and which belong in code?

Answer

Short reminders that guide model behavior belong in the pinned block: ask before modifying data, don't guess, respect max tool calls. Hard guarantees belong in code: authorization, PII redaction, tool allowlists, budget caps, and final output validation.

Human-in-the-loop escalation

Some failures should end automation, not trigger another model call. When an agent encounters high-stakes ambiguity, repeated failures, or a potential safety issue, pause and route a bounded review packet to an authorized human reviewer.

The earlier human-in-the-loop architecture lesson introduced review checkpoints. Here, HITL is a recovery destination: pause a failed or risky run and send a bounded review packet to an authorized person. Review doesn't guarantee correctness. Approvals can become stale, and execution can drift from the reviewed proposal. Bind approval to the exact action and revalidate it at execution time.

When to escalate

Use measurable triggers rather than a vague feeling that the model is unsure:

  • A verifier or policy gate score crosses a documented review threshold
  • Repeated tool-call failures or validation rejections
  • A proposed write such as rollback, deploy, or delete
  • Step, budget, or circuit limits that stop automation

The fixture's three-retry limit and 0.7 risk threshold are examples, not universal defaults. Calibrate both against evaluated failures, reviewer load, and the cost of a missed escalation. Copy allowlisted fields into the review packet; don't forward raw model or tool payloads.

when-to-escalate.py
1from dataclasses import dataclass 2from enum import Enum 3 4class EscalationReason(Enum): 5 POLICY_REVIEW = "policy_review" 6 REPEATED_FAILURES = "repeated_failures" 7 SAFETY_CHECK = "safety_check" 8 9@dataclass 10class EscalationRequest: 11 reason: EscalationReason 12 context: dict[str, object] 13 priority: int 14 suggested_action: str 15 16class HumanInTheLoop: 17 def __init__(self, risk_review_threshold: float = 0.7, max_retries: int = 3): 18 self.risk_review_threshold = risk_review_threshold 19 self.max_retries = max_retries 20 21 def should_escalate( 22 self, 23 agent_state: dict[str, object], 24 failure_count: int = 0, 25 policy_risk_score: float | None = None, 26 ) -> EscalationRequest | None: 27 if agent_state.get("action_type") in {"delete", "deploy", "rollback"}: 28 return EscalationRequest( 29 reason=EscalationReason.SAFETY_CHECK, 30 context=agent_state, 31 priority=1, 32 suggested_action="Approve the exact action hash before execution", 33 ) 34 if failure_count >= self.max_retries: 35 return EscalationRequest( 36 reason=EscalationReason.REPEATED_FAILURES, 37 context=agent_state, 38 priority=2, 39 suggested_action="Review failed attempts and provide guidance", 40 ) 41 if ( 42 policy_risk_score is not None 43 and policy_risk_score >= self.risk_review_threshold 44 ): 45 return EscalationRequest( 46 reason=EscalationReason.POLICY_REVIEW, 47 context=agent_state, 48 priority=3, 49 suggested_action="Review policy-flagged output", 50 ) 51 return None 52 53review = HumanInTheLoop() 54rollback = review.should_escalate( 55 { 56 "action_type": "rollback", 57 "run_id": "RUN-842", 58 "action_hash": "rb-842-1", 59 } 60) 61lookup = review.should_escalate( 62 {"action_type": "lookup", "run_id": "RUN-842"}, 63 failure_count=1, 64) 65print(rollback.reason.value if rollback else "none", rollback.priority if rollback else 0) 66print("lookup escalate:", lookup is not None)
Output
1safety_check 1 2lookup escalate: False

Escalation beats false success: An agent that says "I need help" is better than one that silently hallucinates a "Success" message. Design your escalation UX to make it clear when and why the agent failed, preserving user trust.

Name two concrete triggers that should escalate an agent run to a human reviewer.

Answer

Good triggers include repeated validation failures, a documented policy-score threshold, a proposed rollback or deploy, max-step exhaustion, or a circuit breaker opening on an important dependency. The trigger should be measurable, not raw model confidence.

What context should an escalation request preserve for the human reviewer?

Answer

It should include user request, current goal, last valid checkpoint, exact proposed action or action hash, failure history, redacted tool evidence, validation errors, source-of-truth links, and why automation stopped. The reviewer needs bounded context to decide, not a raw transcript dump.

Monitoring and alerting for agent systems

An agent can return 200 OK and still fail its task. Production systems therefore need specialized observability alongside standard APM (Application Performance Monitoring): latency and error rates matter, but they won't reveal a loop or an unsupported claim by themselves.

Track agent-specific metrics to catch technically successful responses that fail to deliver valid results. Semantic entropy techniques measure uncertainty across sampled answers and can help prioritize review for open-ended generations; they don't verify a tool result or replace source-of-truth checks.[10]Reference 10Detecting Hallucinations in Large Language Models Using Semantic Entropyhttps://www.nature.com/articles/s41586-024-07421-0

Agent observability metrics

MetricTypeWhat it measuresExample starting condition
Loop rateDerived ratioShare of traces where loop detection triggeredSustained increase over task-specific baseline
Recovery rateDerived ratioShare of intervened trajectories ending in a safe answer, fallback, or escalationDrops below recovery SLO
Fallback rateDerived ratioShare of requests routed to a lower-capability pathBurns degraded-mode SLO
Step countHistogramTool calls per user turnHigh percentile approaches configured step cap
Token usageHistogramInput plus output tokens per turnHigh percentile crosses soft budget
Validation rejection rateDerived ratioShare of validated outputs rejected by factual or schema checksSustained increase over task-specific baseline

Counters track cumulative events like loops, fallbacks, and validation failures. Histograms record distributions like step counts and token usage. Validation rejection rate and recovery rate are usually derived from validation and incident counters rather than stored directly as gauges. Define recovery rate as:

RecoveryRate=failed trajectories that ended safelyfailed trajectories that triggered an intervention\text{RecoveryRate} = \frac{\text{failed trajectories that ended safely}}{\text{failed trajectories that triggered an intervention}}RecoveryRate=failed trajectories that triggered an interventionfailed trajectories that ended safely​

For example, if 100 deploy-status traces hit a validation error, loop breaker, timeout, or fallback path, and 72 still return a safe answer or clean escalation, the recovery rate is 72%. This metric tells you whether your defenses are helping users instead of only detecting failures.

OpenTelemetry's v1.41 GenAI conventions defined model-client duration and token-usage metrics and marked that GenAI surface as Development.[11]Reference 11OpenTelemetry GenAI metrics, v1.41.0https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-metrics.md Pin a convention version in your instrumentation. Don't treat those names as a stable contract, and keep agent-level signals such as loop and recovery rate in your own documented namespace.

Eval hygiene for recovery systems

Offline harnesses measure recovery, but they can also leak gold into the agent. Treat expected trajectories, gold tool results, and grader labels as eval-only artifacts:

  • Pin fixtures and keep them outside the agent's tool, memory, and prompt paths during measurement.
  • Never write gold answers or expected tool traces into episodic or archival memory from the harness.
  • Feedback channels used in eval must match production. No hidden oracle tools that only exist in the test loop.
  • If recovery rate jumps after a fixture change, report possible contamination before celebrating the metric.

A recovery system that "learns" the grader isn't recovering; it's cheating the measurement.

Why is recovery rate more useful than validation-failure count alone?

Answer

Validation-failure count tells you how often agents trip a guard. Recovery rate tells you whether the guard leads to a safe answer, clean fallback, or proper escalation. A system that detects many failures but strands users still needs work.

The ratio is ordinary arithmetic. Production stacks often export the underlying counters through Prometheus. The snippet records the table's 100-trace example:

agent-observability-metrics.py
1class AgentMetrics: 2 def __init__(self) -> None: 3 self.interventions = 0 4 self.safe_recoveries = 0 5 6 def record_intervention(self, recovered: bool) -> None: 7 self.interventions += 1 8 if recovered: 9 self.safe_recoveries += 1 10 11 def recovery_rate(self) -> float: 12 return self.safe_recoveries / self.interventions 13 14metrics = AgentMetrics() 15for recovered in [True] * 72 + [False] * 28: 16 metrics.record_intervention(recovered) 17print(f"recovery rate: {metrics.recovery_rate():.0%}")
Output
1recovery rate: 72%

Example alert logic

Alert conditions must be calibrated by task and consequence. Useful starting conditions include:

  1. Loop rate rises over baseline: Inspect traces for ambiguous tools, broken progress state, repeated dependency results, or an overly strict detector before assigning cause.
  2. Fallback traffic burns its SLO: Check primary dependencies, validation changes, routing, and task mix. Separate an outage from an intentional product rollout.
  3. High-percentile steps or tokens approach hard caps: Tighten optional exploration or compact validated state before user-facing stops dominate.
  4. A circuit remains Open: Investigate dependency health, client and network paths, timeout budgets, and breaker sensitivity.

Send these agent-specific metrics to your observability and incident-response tooling alongside standard server metrics. Alert on a rising loop rate so operators can stop affected runs before they degrade service or drive runaway API costs.

Common misconceptions

Deterministic service habits become unsafe when the control flow is generated. Keep these four boundaries in view:

Blind retries: Retrying the same hallucinating LLM call often repeats the failure or produces a different wrong answer. Effective recovery changes the strategy by modifying the prompt, switching models, or reducing the task scope.

Timeouts: A timeout doesn't prove that a side effect failed. Replay can duplicate work or create inconsistent state, so use idempotency keys, checkpoints, and explicit reconciliation against the source of truth.

Step caps: A step limit is a crude safety net. It doesn't stop an agent from wasting 15 steps (and thousands of tokens) on a futile loop before the limit; semantic loop detection stops that waste earlier.

Generic exception handlers: Catching every exception silently creates "zombie agents" that continue with corrupted state. Keep failures explicit and route them through validation checks or circuit breakers instead of swallowing them.

Practice: diagnose the failure

Pause before reading the sketches. For each scenario, name the failed boundary, choose the first safe recovery path, and say what evidence would let the run continue.

  • Scenario 1: Your deploy-status agent calls get_run({"id": "RUN-842"}) three times in a row, gets the same answer each time, and keeps going.

  • Scenario 2: The agent's reasoning trace says "I will search the database" but the actual tool call is create_release({}).

  • Scenario 3: After a rollback tool times out, the orchestrator retries and the developer receives two rollback notifications.

  • Scenario 4: The agent has used 120,000 tokens on a single request and your limit is 100,000.

  • Scenario 5: The agent invents a tool called fetch_run_secret that doesn't exist in your tool registry.

Solution sketches

  • Scenario 1: Progress failure. The LoopBreaker would catch the third identical call with args_hash deduplication. Also inspect why the completion condition didn't stop the run after a sufficient answer.

  • Scenario 2: Proposal failure. Compare the emitted tool with the structured task contract and allowlist. Block create_release when the accepted intent is deploy status. A critic can add review signal but can't replace deterministic capability checks.

  • Scenario 3: External-state failure. The timeout didn't mean "nothing happened." Use an idempotency key and read-after-write reconciliation before deciding whether any retry is necessary.

  • Scenario 4: Progress-budget failure. TokenBudget should stop the run at its hard limit. A configured soft threshold should have stopped optional exploration and compacted validated state earlier.

  • Scenario 5: Proposal failure. A tool allowlist rejects fetch_run_secret before execution and returns structured feedback: Tool 'fetch_run_secret' not available. Available tools: get_run, get_logs, suggest_alternative.

Which scenario creates the greatest operational risk, and why?

Answer

Scenario 3 can duplicate a real rollback. Loops and hallucinated tools waste resources, but replaying a timed-out non-idempotent action can mutate production state and corrupt the run history.

Agent failure controls

The same few boundaries recur across every example. Use this list as a design review for a runtime, then check each control against a trace rather than treating it as a slogan.

  1. In the independent, equal-probability toy model, n required steps succeed with probability p^n. Real trajectories add correlated faults and recovery boundaries, so measure task-level reliability directly.
  2. Generated failures can look successful. Build validation checks for claims and actions, not exception handlers alone.
  3. Classify the failed boundary: proposal, dependency, external state, progress, or result. Recovery follows evidence, not a generic "agent error."
  4. Loop detection needs multiple signals: exact-call hashing, approximate intent similarity, repeated-tool heuristics, step limits, and token budgets.
  5. Corrective feedback beats blind retry for deterministic mistakes. Reflexion stores task feedback as verbal memory for later trials; it doesn't authorize actions or prove claims.
  6. Circuit breakers limit repeated calls to unhealthy dependencies; they don't establish correctness.
  7. Fallback chains degrade capability explicitly. Live-data questions must stop or escalate when fallback paths lack evidence.
  8. Checkpointed recovery beats blind replay when state may already be poisoned or partially committed.
  9. Pinned reminders can improve recall in long contexts, but enforcement remains outside the model.
  10. Human review must be bound to exact actions and revalidated at execution; escalation alone isn't a guarantee.
  11. Agent-specific metrics such as loop rate, fallback rate, and token usage matter as much as latency and error rate.

Handoff to recursive long-context work

These controls bound a conventional agent loop: every proposal, tool result, checkpoint, and recovery action has a contract. The next lesson, Recursive Language Models, keeps long inputs outside the active window and delegates targeted sub-calls. Recursion changes how context is decomposed. It doesn't remove budgets, validation, or recovery. Each sub-call still needs a trusted input boundary and a bounded way to fail.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A workflow needs 40 independent required steps, each succeeding with probability 0.98, and has no recovery. What does the toy model predict?

Correct answer: Success is 0.98^40, about 45%, so long trajectories can fail often despite strong per-step reliability.

Every required step must pass in this simplified independent model, so probabilities multiply. Runtime validation, recovery, and shorter unrecovered chains address the task-level reliability gap.

2.One run encounters a 429 before any write, a 400 for missing required field id, and a rollback timeout after the controller may have committed. Which recovery routing is correct?

Correct answer: Back off with jitter for the 429, return structured schema feedback for the 400, and reconcile the rollback before replay.

Transient side-effect-free faults can retry. Deterministic proposal errors need new corrective information. A timed-out write has uncertain external state, so idempotency and source-of-truth reconciliation come before any replay.

3.An agent alternates get_run({id: RUN-842}) with rephrased searches for RUN-842 and receives no new evidence. Which detector catches the failure earliest without relying only on a hard step cap?

Correct answer: Exact tool-plus-argument hashes combined with normalized or embedding-based intent and progress signals.

Hashes catch byte-identical repeats; intent catches cross-tool paraphrases; progress prevents legitimate pagination from looking like a loop. A step limit remains a final bound, not early detection.

4.A 100,000-token run reaches 82,000 tokens while optional searches continue and each turn resends prior observations. What should happen next?

Correct answer: Stop optional exploration, compact validated facts and pending work, preflight the next context, and retain the hard cap.

Soft budget management prevents abrupt failure and accelerating cost before the hard breaker trips. Only validated durable state should survive compaction; raw or disproven turns can remain in external audit storage.

5.A dependency circuit opens in generation 8 while an older generation-7 request is still running. That old request later succeeds. How should breaker state change?

Correct answer: Return its result to that caller but ignore it for shared transitions; only current-generation success may close the circuit.

The completion is valid for its own request but stale as health evidence. A generation check prevents an older success from erasing newer failures and reopening traffic prematurely.

6.Live and read-only lookups time out. A cache says RUN-842 looked healthy 40 minutes ago, but the user asks whether rollback has finished now. Which fallback result is valid?

Correct answer: Reject the cache for this live-data claim and return an explicit unavailable or handoff response.

Fallback stages narrow the claim contract. A timestamped cache can answer some historical questions, but it cannot establish a current side effect; honest degradation is safer than stale certainty.

7.A prompt pins Always confirm actions that modify data, and the agent proposes a rollback after repeated validation failures. What must happen before execution?

Correct answer: Escalate to an authorized reviewer, bind approval to the exact action hash, and revalidate at execution.

Pinned reminders can improve recall but cannot enforce authorization. Repeated failures and production rollback are measurable review triggers, and approval must match the payload that will actually run.

8.A validator says get_run requires field id after the agent used run_id. When is a Reflexion memory loop unnecessary?

Correct answer: When immediate structured feedback identifies the deterministic repair and the next proposal can be revalidated directly.

A repair retry consumes explicit local diagnostics. Reflexion is useful for strategic lessons carried across evaluated trials, not a label for every corrected retry and never proof of external state.

9.Guardrail trigger volume is stable, but recovery rate falls from 82% to 46%. What should operators investigate first?

Correct answer: Whether fallback, verifier, dependency, or repair paths now rescue fewer intervened runs.

Stable intervention volume means detection still fires at roughly the same rate. Lower recovery means fewer affected trajectories end in a safe answer, valid fallback, or clean escalation.

10.A task contract says lookup_run for RUN-842, but the model narrates a lookup and emits create_release({}). What should the action boundary do?

Correct answer: Reject the payload before execution because tool, identifier, schema, and permission must match the structured contract.

Narration and executable payload are separate generated artifacts. Deterministic admission checks enforce the accepted task; checkpoints and rollback are not substitutes for preventing an unauthorized action.

10 questions remaining.

Next Step
Continue to Recursive Language Models (RLM)

Recovery policies keep one agent's state and actions bounded. RLM applies those controls to a programmable long-context loop that stores large inputs outside the active window and delegates targeted sub-calls under explicit budgets.

PreviousAgent Memory & Persistence
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

ReAct: Synergizing Reasoning and Acting in Language Models.

Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., & Cao, Y. · 2023 · ICLR 2023

https://arxiv.org/abs/2210.03629

Toolformer: Language Models Can Teach Themselves to Use Tools.

Schick, T., et al. · 2023 · NeurIPS 2023

https://arxiv.org/abs/2302.04761

Measuring AI Ability to Complete Long Tasks

Kwa, T., West, B., Becker, J., et al. (METR) · 2025

https://metr.org/blog/2025-03-19-measuring-ai-ability-to-complete-long-tasks/

Tau-Bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains

Yao, S., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2406.12045

Exponential Backoff And Jitter

Brooker, M. (AWS) · 2015

https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/

LangGraph Persistence

LangChain · 2026

https://docs.langchain.com/oss/python/langgraph/persistence

Temporal Workflow Execution Overview

Temporal Technologies · 2026

https://docs.temporal.io/workflow-execution

Reflexion: Language Agents with Verbal Reinforcement Learning.

Shinn, N., et al. · 2023

https://arxiv.org/abs/2303.11366

Lost in the Middle: How Language Models Use Long Contexts

Liu, N.F., et al. · 2024 · TACL 2024

https://aclanthology.org/2024.tacl-1.9/

Detecting Hallucinations in Large Language Models Using Semantic Entropy

Farquhar, S., et al. · 2024 · Nature

https://www.nature.com/articles/s41586-024-07421-0

OpenTelemetry GenAI metrics, v1.41.0

OpenTelemetry Authors · 2026

https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-metrics.md

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.