An agent processing refund requests for a PhonePe-style payments app gets a slightly ambiguous transaction ID. Instead of asking for clarification, it calls the refund tool, gets an error, calls it again with a guess at the correct ID, gets another error, and tries again. And again. Forty-three tool calls later, someone notices the bill for that one support ticket costs more than the refund itself, and three of those attempted calls actually went through, because nobody built in a check for whether the same refund had already been issued. Nothing about this agent was unintelligent. The underlying model reasoned reasonably well at every single step. What was missing was engineering: a limit on how many times it could retry, a check for whether an action had already succeeded before trying it again, and a point at which an uncertain action should have stopped and asked a human instead of guessing. This is the central lesson of this module. A capable model is necessary but not sufficient to build a production agent. Many production agent failures are not primarily model-intelligence failures. They come from missing guardrails, unreliable tools, poor state handling, or uncontrolled execution, and the gap between an agent that works in a demo and one that survives contact with real users is often determined as much by reliability engineering as by model capability. > 📌 **Remember:** A production agent failure is often caused by a missing guardrail > rather than poor reasoning: no retry limit, no idempotency check, no human checkpoint, > or no mechanism for recovering state. ### What makes an agent different from a regular LLM call As covered in the LLM Fundamentals module, a single prompt-response call takes one question and returns one answer. An **AI agent** is a system where the model can take an action, observe the result of that action, and decide what to do next based on that result, repeating this cycle until the task is done. The model is not just answering, it is deciding what to do, checking what happened, and adjusting. This loop is often called **ReAct**, short for Reason and Act, in the research literature. ReAct is one important agent pattern, reasoning about the next action, taking that action, observing the result, and continuing from the new information. It is a pattern, not a synonym for every agent architecture. Frameworks such as LangGraph, covered later in this module, can implement a ReAct-style loop, but they are not limited to it, the same graph structure can just as easily represent a deterministic workflow, a branching decision tree, an approval pause, or a fixed retry sequence. Reason: what should I do next given the goal and what I know so far? | v Act: call a tool (check balance, issue refund, send message) | v Observe: what did the tool actually return? | v (loop back to Reason, or stop if the task is done) This diagram sits directly beneath the explanation above, showing the same three-step cycle described in words: reasoning about the next action, acting on it through a tool, and observing the real result before deciding whether to continue or stop. An agent's **trajectory** is the ordered sequence of decisions, tool calls, observations, and state transitions produced during one run. That term reappears later in this module around logging, and again in the Evaluation module, so it is worth fixing now: a trajectory is not the final answer, it is the entire path the agent took to get there. ### Deterministic workflows vs autonomous agents Not every AI-powered feature needs full agent autonomy, and defaulting to maximum autonomy is a common source of unreliability. A **deterministic workflow** is a fixed sequence of steps you define in code, where an LLM call might handle one or two steps inside that sequence, but the overall path is not decided by the model at runtime. An **autonomous agent** decides its own sequence of actions based on the situation, which is more flexible but harder to predict and harder to test. Most production systems need less autonomy than a demo suggests. If you already know the steps a task requires, checking a policy document, then checking an account status, then issuing a refund, hardcode that sequence and use the LLM only for the specific judgment calls inside it. Reserve full agent autonomy for tasks where the correct sequence of steps genuinely cannot be known in advance. > 📌 **Engineering Decision:** Default to a deterministic workflow with the LLM handling > only the steps that genuinely require judgment. Move to a fully autonomous agent only > once you can show a fixed sequence cannot handle the variation the task actually > requires. ### Three kinds of failure an agent system can have Before going further, it helps to separate three failure categories this module will keep returning to, because each one is fixed differently. A **model failure** is the model itself choosing wrong given correct, well designed tools, the wrong tool, wrong arguments, an incorrect decision given the information it had. A **tool or infrastructure failure** is something breaking outside the model's control, a timeout, a rate limit, a malformed response, a network drop. A **control failure** is neither of those, it is the absence of a guardrail around the loop itself, no step limit, no approval gate, no idempotency check, no way to recover lost state. | Failure Type | Example | Fixed By | |:---|:---|:---| | Model failure | Wrong tool selected given clear options | Better tool design, evaluation | | Tool/infrastructure failure | Timeout, rate limit, malformed response | Retry, backoff, timeout handling | | Control failure | Infinite loop, no approval, duplicate action | Limits, idempotency, approval, state design | Nearly everything in this module maps to fixing one of these three categories, and recognizing which one you are looking at is usually the fastest way to diagnose a real agent failure. Worth flagging now, because the next section will complicate it slightly: a lot of what looks like a model failure, the model "picking the wrong tool," actually traces back to a design decision on your side, not a limitation of the model's reasoning. ---
An agent is only as reliable as the tools it can call. Many agent failures that look like "the model got confused" can actually originate in poor tool design: ambiguous tool boundaries, overlapping responsibilities, weak descriptions, or unclear parameter schemas. Better tool design reduces the number of decisions the model has to infer incorrectly, and this section is where most of those problems are prevented before they ever reach the model. ### How function calling actually works **Function calling** is the mechanism by which a model can request that a specific function be run, with specific arguments, instead of just generating text. The model does not execute the function itself, it returns a structured request, your application validates and runs the actual function, and the result is fed back to the model as the next piece of context in the conversation. User | v Agent application | v LLM reasons about the next step | v LLM returns a structured tool call request | v Application validates the call <- this step is easy to skip, and shouldn't be | v Tool executes | v Structured result returned to the LLM That validation step deserves its own emphasis. A model returning a syntactically valid tool call is not the same thing as that call being safe or authorized to run. A model producing `{"transaction_id": "TXN123", "reason": "customer requested"}` for `issue_refund` does not mean your application should execute it without question. Before a side-effecting tool actually runs, the request should pass through schema validation, authorization, any relevant business-rule checks, and an idempotency check, in that order, not straight from model output to execution. > 📌 **Remember:** Never treat a model's tool selection as authorization. The application, > not the model, is responsible for independently enforcing permissions, business rules, > and safety constraints before executing a side-effecting tool. ```python refund_tool_schema = { "name": "issue_refund", "description": ( "Issues a refund for a completed transaction. Use this only after " "confirming the transaction exists and has not already been refunded. " "Do not use this to check refund eligibility, use check_refund_eligibility for that." ), "parameters": { "type": "object", "properties": { "transaction_id": { "type": "string", "description": "The exact transaction ID, format TXN followed by 12 digits." }, "reason": { "type": "string", "description": "Short reason for the refund, shown in the audit log." } }, "required": ["transaction_id", "reason"] } } ``` > **Note:** The `description` field is not documentation for a human reading the code, it > is the primary signal the model uses to decide when and how to call this tool. A vague > description like "handles refunds" gives the model far less to work with than one that > states exactly when to use it and when not to. ### Why narrow, well-described tools beat broad, vague ones A tool named `handle_transaction` that can check status, issue refunds, and send notifications depending on which arguments you pass it is harder for a model to use correctly than three separate tools, each doing exactly one thing. When tool responsibilities overlap or a single tool tries to do too much, the model has to guess which mode you meant, and it will sometimes guess wrong. * Give each tool one clear responsibility, not several bundled behind a mode flag. * Write the description to say explicitly when to use this tool and, where relevant, when not to, pointing at the correct tool instead. * Keep parameter names literal and specific, `transaction_id` rather than `id`, so the model does not have to infer what an ambiguous field is supposed to contain. > 🔴 **Common Mistake:** Two tools with overlapping responsibilities and similar > descriptions, for example a `refund_transaction` tool and a `reverse_payment` tool that > both technically do the same thing. The model picks inconsistently between them because > nothing in either description tells it which one is correct for a given situation. The > fix: merge overlapping tools into one, or make the descriptions mutually exclusive about > when each applies. ### Structured tool outputs and schema validation A tool call is only half of the reliability picture. What the tool returns also needs structure, and that structure needs to be validated before your system trusts it, especially before feeding it back into the next reasoning step. ```python from pydantic import BaseModel, ValidationError class RefundResult(BaseModel): """ Defines the exact shape a refund tool result must have. Validating against this before returning the result to the agent catches a malformed or unexpected response before it corrupts the next step of the reasoning loop. """ success: bool refund_id: str | None amount: float already_refunded: bool def issue_refund(transaction_id, reason): raw_result = payments_api.refund(transaction_id, reason) try: # Validate the raw API response against the expected schema before # it is trusted anywhere else in the agent loop return RefundResult(**raw_result).model_dump() except ValidationError as e: # A malformed result should surface as a clear tool error, not # silently pass through and confuse the next reasoning step return {"success": False, "error": f"Malformed tool response: {e}"} ``` > **Note:** `already_refunded` is deliberately part of the schema, not an afterthought. > As covered later in this module, checking whether an action already happened before > retrying it is the core idea behind idempotency, and that check only works if the tool > result actually reports it. ### Eligibility is not authorization For a scenario like this refund tool, confirming a transaction is eligible for a refund and confirming this specific request is authorized to proceed are two separate checks, and it is worth not collapsing them into one. `check_refund_eligibility` answers "does policy allow a refund here at all." A separate authorization step answers "is this specific agent, in this specific context, permitted to actually issue it, and does it need human sign-off first." The full sequence for a side-effecting action like this looks like eligibility check, then authorization, then a check for whether approval is required, then an idempotency check, only then execution. Approval and idempotency are both covered in full later in this module, they are introduced here because this is where they fit into the request path. ### Tool permissions and least privilege Reliability is not only about preventing accidental loops or bad retries, it is also about limiting what an agent is capable of doing in the first place. An agent should never receive every tool the wider application has available, only the ones its specific task actually requires. ``` Support Agent Toolset ├── get_transaction_status allowed ├── check_refund_eligibility allowed ├── issue_refund allowed, routed through approval ├── delete_account not exposed to this agent ├── modify_payment_credentials not exposed to this agent └── admin_database_query not exposed to this agent ``` Enforce this in application code, at the layer that decides which tool definitions get sent to the model in the first place, and again at execution time. Never rely on the system prompt alone, an instruction telling the model "you are not allowed to delete accounts" is not a security boundary, it is a suggestion the model may fail to follow. If a tool should never be called by a given agent, do not give that agent the ability to call it at all. ---
Tool calling, then state, then orchestration, then validation, then reliability, in that order, is the real engineering focus of building an agent. State comes right after tool calling for a reason: before you can meaningfully orchestrate a loop, checkpoint it, or add limits to it, you need to know exactly what information that loop is carrying at every step. ### What actually belongs in agent state **Agent state** is the complete set of information the agent carries forward from one step of the loop to the next, everything it needs to decide what to do next without re-deriving it from scratch each time. ``` Agent State ├── goal the task the agent is trying to complete ├── conversation_context relevant history so far ├── tool_calls the sequence of actions taken ├── tool_results what each action actually returned ├── step_count completed reason-act loop iterations ├── tool_attempt_count individual tool execution attempts this step ├── retry_count retries for the current logical operation ├── pending_approval whether a high-risk action is awaiting human sign-off ├── task_status in_progress, complete, blocked, or failed └── error_state the most recent error, if any, and its type ``` Notice `step_count`, `tool_attempt_count`, and `retry_count` are listed separately rather than folded into one number. This distinction matters more than it looks: `step_count` is completed agent loop iterations, one reason-act-observe cycle. `tool_attempt_count` is how many times a tool was actually invoked, which can be higher than the step count if a single step retries internally. `retry_count` tracks retries specifically for the current logical operation, covered in full in the reliability section later in this module. If you collapse these into a single counter, a hard step limit and a retry limit end up fighting over the same number, and neither one does its job cleanly. Not everything belongs in state. Raw, unstructured internal reasoning text is usually not something you want carried forward as state to be inspected or branched on, that belongs in a log, covered later in this module, not in the state object the graph reasons over. Large binary content, full document text that was only needed for one step, or anything that can be cheaply re-fetched when needed is also usually better left out of state and referenced instead. Whether a given piece of state should be treated as append-only, like `tool_calls` growing with each step, or replaced outright, like `task_status` changing from `in_progress` to `complete`, is a decision worth making explicitly for each field rather than leaving to convention. Getting this wrong is a common source of bugs where a later step overwrites information an earlier step still needed. > 📌 **Remember:** Good checkpointing, covered later in this module, only works as well as > the state design underneath it. A checkpoint saves whatever is in state, if state is > missing something the agent needs to resume correctly, the checkpoint will not fix that. ### Why explicit state design pays off before orchestration Once state is designed deliberately, the orchestration layer covered next has a clear contract to work against, rather than an implicit, ad hoc bag of variables that grows however each step happens to need it. This is what makes the difference between an agent loop you can reason about and test, and one where every change risks breaking something elsewhere in a hidden way. ---
With tools designed and state defined, you need a way to run the reason-act-observe loop with explicit control over transitions and error handling, rather than an unstructured while loop that is hard to test or reason about. ### Why explicit control matters more than framework choice **LangGraph** models an agent as a graph: nodes represent steps, edges represent transitions between them, and state is passed explicitly between nodes rather than implicitly hidden inside a loop. This matters because it makes the agent's behavior inspectable and testable, you can look at the graph and know every possible path the agent can take, rather than trusting that a free-form loop will behave correctly. As covered above, LangGraph is a general graph-based orchestration approach, capable of representing a ReAct-style loop but not limited to it. > **Note:** The code below is an illustrative pattern showing how a typed state schema > and explicit conditional transitions fit together in a graph-based orchestrator. > LangGraph's exact API surface changes across versions, treat the pattern as the lesson, > and check current LangGraph documentation for the exact syntax at the time you build. ```python from typing import TypedDict from langgraph.graph import StateGraph, END class AgentState(TypedDict): """ A typed contract for what this agent carries between steps, following the state design covered earlier in this module. Declaring this explicitly, instead of passing a raw dict, catches a missing or mistyped field at development time rather than deep inside a run. step_count is incremented exactly once per completed reason-act cycle, inside act_node below, so it always means "completed cycles", not "tool calls attempted" if a retry happens inside a single cycle. task_complete is set explicitly by check_completion_node below, it is never inferred implicitly from a tool result alone. """ goal: str step_count: int max_steps: int next_action: dict | None last_result: dict | None task_complete: bool def reason_node(state: AgentState) -> AgentState: # Decide the next action based on current state and the goal next_action = decide_next_step(state) return {**state, "next_action": next_action} def act_node(state: AgentState) -> AgentState: # Execute the chosen tool and record the result in state. # step_count increments here, once per completed cycle through this # node, regardless of how many retries happened inside execute_tool. result = execute_tool(state["next_action"]) return {**state, "last_result": result, "step_count": state["step_count"] + 1} def check_completion_node(state: AgentState) -> AgentState: # Explicit node whose only job is deciding whether the goal is met. # task_complete is never set anywhere else, this is the single place # that owns it, which is what makes should_continue's condition below # actually reachable and testable in isolation. is_done = evaluate_goal_completion(state["goal"], state["last_result"]) return {**state, "task_complete": is_done} def should_continue(state: AgentState) -> str: # Explicit stopping conditions, not left to the model to decide alone if state["step_count"] >= state["max_steps"]: return "stop" if state["last_result"] and state["last_result"].get("success") and state["task_complete"]: return "stop" return "continue" graph = StateGraph(AgentState) graph.add_node("reason", reason_node) graph.add_node("act", act_node) graph.add_node("check_completion", check_completion_node) graph.add_edge("act", "check_completion") graph.add_conditional_edges("check_completion", should_continue, {"continue": "reason", "stop": END}) graph.set_entry_point("reason") ``` > **Note:** `should_continue` is where a hard step limit lives directly in the graph > structure, not as an afterthought bolted onto the model's own judgment. This is one of > the reliability guardrails covered in full later in this module, introduced here > because the graph structure is exactly where it belongs. Notice it reads `max_steps` > straight out of the typed state defined above, this is the payoff of designing state > explicitly before writing the orchestration layer. Giving completion detection its own > node, rather than leaving `task_complete` to be set implicitly somewhere inside > `act_node`, means you can test "did the agent correctly recognize the goal was met" as > its own unit, separately from "did the tool call succeed." ### When to add a second agent, and when not to **CrewAI** and similar frameworks coordinate multiple specialized agents working together, for example one agent that researches and another that writes. This adds real complexity: more moving parts, more places for a handoff to fail, and a much harder system to debug when something goes wrong. > 📌 **Engineering Decision:** Start with a single agent, a small toolset, and reliability > guardrails from day one. Move to multiple agents only once you can demonstrate, not > assume, that a single agent genuinely cannot handle the task's variation or scope. Most tasks that seem to need multiple specialized agents actually need one agent with better tool design and a clearer prompt. Reach for multi-agent orchestration only after a single well-built agent has actually been tried and shown to fall short, not as a default starting architecture. ---
So far, tools have been functions defined directly inside your codebase. The **Model Context Protocol**, or **MCP**, standardizes how an agent connects to tools that live outside its own codebase, in a separate service, without needing custom integration code for each one. ### MCP architecture at a working level MCP defines a host-client-server architecture. A **host** is the application the user interacts with, your agent application. A **client** lives inside the host and manages the connection to one MCP server. A **server** exposes capabilities to that client, most commonly **tools**, functions the agent can call, but a server can also expose resources and prompts as additional capability types. The host can connect to several servers at once through separate clients, each one exposing a different set of tools. Host (your agent app) | +-- Client A --- Server A (internal HR tools) | +-- Client B --- Server B (payments tools) This diagram follows the role definitions above: one host application, multiple clients, each client connected to one independent server that exposes its own set of capabilities, so an agent can draw on tools from several separate systems without custom glue code for each. The practical benefit is that a tool built as an MCP server can be reused across any MCP compatible agent, and a team can build and own their own server independently of the agent application that ends up calling it. > ⚠️ **Security:** MCP standardizes tool connectivity, it does not automatically make a > tool safe, authorized, idempotent, or reliable. Connecting to an MCP server is not a > substitute for the schema validation, authorization, least-privilege access, timeout > handling, retry logic, and approval gates covered throughout this module. Every one of > those controls still needs to exist around an MCP tool call, exactly as it would around > a tool defined directly in your own codebase. ### Building a simple MCP server > **Note:** This is an illustrative MCP server pattern. Conceptually, an MCP server needs > to expose a mechanism for capability discovery and a mechanism for tool invocation, the > exact SDK method names, signatures, and interfaces vary by MCP implementation and > version, so check current MCP documentation for the exact syntax at build time. ```python from mcp.server import Server from mcp.types import Tool, TextContent server = Server("refund-tools") @server.list_tools() async def list_tools(): # Advertises available tools to any connected client, using the same # kind of clear, specific description covered earlier in this module return [ Tool( name="check_refund_eligibility", description="Checks whether a transaction is eligible for refund based on policy rules.", inputSchema={ "type": "object", "properties": {"transaction_id": {"type": "string"}}, "required": ["transaction_id"] } ) ] @server.call_tool() async def call_tool(name, arguments): if name == "check_refund_eligibility": eligible = check_eligibility(arguments["transaction_id"]) return [TextContent(type="text", text=str(eligible))] raise ValueError(f"Unknown tool: {name}") ``` > **Note:** In this SDK, discovery and invocation happen to be named `list_tools` and > `call_tool`, one to advertise what the server can do, one to actually do it. A > connecting agent calls the discovery method first to learn available capabilities > before ever invoking one. ---
This section and the two that follow are the heart of this module. Tool design and state make an agent capable and inspectable. What follows is what makes it survivable in production, mostly by addressing the tool/infrastructure and control failure categories introduced earlier. ### Retries need a limit and a backoff, not infinite persistence A tool call can fail for reasons that have nothing to do with the agent's reasoning, a network blip, a rate limit, a momentarily overloaded downstream service, a tool or infrastructure failure rather than a model failure. Retrying a failed call is reasonable. Retrying it forever, or retrying it instantly in a tight loop, is how a transient blip turns into a cascading failure. ```python import time def call_with_retry(tool_fn, args, max_retries=3, base_delay=1.0): """ Retries a tool call up to max_retries times with exponential backoff. Stops immediately on a non-retryable error, only retries on failures that are plausibly transient, such as timeouts or rate limits. """ for attempt in range(max_retries): try: return tool_fn(**args) except (TimeoutError, RateLimitError) as e: if attempt == max_retries - 1: raise time.sleep(base_delay * (2 ** attempt)) # exponential backoff except ValidationError: raise # not a transient failure, retrying will not help raise RuntimeError("Retry loop exited without returning or raising") ``` > **Note:** The `except` clauses deliberately separate transient failures, worth > retrying, from failures like a validation error, which will happen again identically no > matter how many times you retry. Retrying a non-transient failure just wastes time and > money without changing the outcome. ### A timeout does not tell you what actually happened There is a sharper point buried inside the retry logic above, worth pulling out on its own because it is exactly what caused the failure in this module's opening scenario. A timeout tells you that you did not receive a response in time. It does not tell you whether the operation itself failed, succeeded, or is still in progress somewhere downstream. For a side-effecting call like `POST /issue-refund` or `POST /charge-card`, blindly retrying after a timeout risks running the operation a second time when the first attempt actually went through, the server just never told you. Request sent | v Timeout, no response received | v Did the operation actually happen? -> unknown | v Check status, or retry using an idempotency key | v Only now is a retry safe The rule that follows from this: retry a side-effecting operation only when it is idempotent, covered next, or when the system gives you a way to check whether the original attempt actually succeeded before you try again. Retrying blindly on the assumption that a timeout means "nothing happened" is exactly how the opening scenario's duplicate refund occurred. ### Idempotency: making retries safe **Idempotency** means that repeating the same operation, using the same idempotency identity, does not create additional unintended side effects, whether the underlying request runs once or several times. This is a slightly more precise statement than saying the result is identical every time, an idempotent API commonly returns the original resource or result on a duplicate request rather than literally repeating the action, the guarantee is about side effects, not necessarily about the response being byte-for-byte the same. The standard way to achieve this is an idempotency key: a unique identifier attached to a specific attempt, so the receiving system can recognize a duplicate and return the original result instead of repeating the action. A critical detail worth being precise about: the idempotency key represents the logical operation, not any single HTTP attempt underneath it. One logical refund can span several network-level attempts, and every one of those attempts must reuse the same key. Logical refund attempt (key generated once, here) | +-- HTTP attempt #1 -> timeout | (retry, same key) | +-- HTTP attempt #2 -> rate limited | (retry, same key) | +-- HTTP attempt #3 -> success, key stored by payments API ```python def issue_refund_idempotent(transaction_id, reason, idempotency_key): """ idempotency_key should be generated once per logical refund attempt, not regenerated on every retry. The payments API is expected to store keys it has already processed and return the original result for a duplicate key rather than issuing a second refund. """ return payments_api.refund( transaction_id=transaction_id, reason=reason, idempotency_key=idempotency_key ) ``` > 🔴 **Common Mistake:** Generating a new idempotency key on every retry attempt instead > of reusing one key for the whole logical attempt. This defeats the entire purpose, the > downstream system sees each retry as a brand new request and processes it again. The > fix: generate the key once before the first attempt, and pass that same key into every > retry of that same logical action. ### Timeout, cancellation, cost, and concurrency as separate ceilings It is worth being precise about which limit protects against which failure, because they are easy to blur together but do different jobs. A **timeout** bounds how long a single tool call is allowed to run before it is treated as failed. **Cancellation** is the mechanism for stopping an entire agent run cleanly if it exceeds a maximum allowed duration, not just a maximum step count, so it does not keep consuming resources indefinitely in the background. A **cost ceiling** bounds monetary exposure directly, tracked per run and checked before every new action, which is a different thing from either of the above: a handful of steps that each call an expensive external API or trigger a costly real-world action can still add up to an unacceptable bill even while comfortably inside a generous step count and well within any timeout. A **concurrency limit** bounds how many tool calls, or how many agent runs, are allowed to execute in parallel, an agent can technically stay within its step limit while still launching several expensive operations at once if nothing bounds parallel execution. | Limit | What It Bounds | |:---|:---| | Timeout | How long one tool call is allowed to run | | Cancellation | How long an entire agent run is allowed to continue | | Step limit | How many reasoning-action cycles are allowed | | Cost ceiling | How much money a run is allowed to spend | | Concurrency limit | How many operations run in parallel at once | A tool call, or an entire agent run, that never returns and is never cancelled is functionally as bad as one that errors outright, the difference is that a hung request also silently consumes resources indefinitely. Every tool call needs an explicit timeout, and every agent run needs its own cancellation path independent of step counting. ---
An agent processing refund requests for a PhonePe-style payments app gets a slightly ambiguous transaction ID. Instead o...
An agent is only as reliable as the tools it can call. Many agent failures that look like "the model got confused" can a...
Tool calling, then state, then orchestration, then validation, then reliability, in that order, is the real engineering ...
With tools designed and state defined, you need a way to run the reason-act-observe loop with explicit control over tran...
So far, tools have been functions defined directly inside your codebase. The Model Context Protocol, or MCP, standardize...
This section and the two that follow are the heart of this module. Tool design and state make an agent capable and inspe...
Retries and idempotency guard against a single action going wrong. This section guards against the agent as a whole goin...
An agent that fails partway through a multi-step task should not have to start over from nothing, and an agent about to ...
Every guardrail covered so far assumes you can actually see what the agent did. Without logging, none of it is debuggabl...
> Note: These commands assume a Linux or macOS shell, or WSL on Windows. The core lab > steps below use LangGraph and Py...
The reliability mental model Run-level ceilings bound the whole execution from outside. Loop detection and schema checks...
[image-1] Reliability guardrails surrounding the agent loop: [Style: Clean flat diagram on white background] -> [Title: ...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.