> ## Documentation Index
> Fetch the complete documentation index at: https://notes.kodekloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Lab Walkthrough Build a Safe Agent

> Guide to hardening a Python tool-calling agent by returning tool errors as results and bounding retry loops for safe, production-ready behavior.

Time for another hands-on lab.

You already have a tool-calling agent that works on the happy path, but it has two critical failure modes that will hurt you in production:

* tools that crash (uncaught exceptions), and
* an unbounded loop that can retry forever (burning API credits).

This walkthrough shows how to harden a simple Python agent (single-file: `safe_agent.py`) so it behaves safely in production: tools return errors as results, and the main loop is bounded with a graceful final attempt.

Environment

* Working directory: `/root/code`
* File: `safe_agent.py`
* Prereqs: Python, virtual environment, OpenAI SDK (pre-installed)

<Frame>
  <img src="https://mintcdn.com/kodekloud-c4ac6d9a/WUKBeXdogksN49S5/images/AI-Agents-for-Beginners-OpenClaw-Case-Study/Building-AI-Agents/Lab-Walkthrough-Build-a-Safe-Agent/retro-neon-pixel-safe-agent-lab.jpg?fit=max&auto=format&n=WUKBeXdogksN49S5&q=85&s=1a4fce9ad39089addb8a3d5a6adddfa8" alt="A retro-style graphic with neon pixelated text reading &#x22;HANDS-ON LAB&#x22; and &#x22;BUILD A SAFE AGENT.&#x22; Below it is the subtitle: &#x22;Your tool-calling agent, hardened for production,&#x22; all on a dark grid background." width="1920" height="1080" data-path="images/AI-Agents-for-Beginners-OpenClaw-Case-Study/Building-AI-Agents/Lab-Walkthrough-Build-a-Safe-Agent/retro-neon-pixel-safe-agent-lab.jpg" />
</Frame>

Overview of the baseline (unsafeguarded)

* File to edit: `safe_agent.py`
* Start with this minimal agent: OpenAI client, a `check_calendar` tool, `execute_tool` dispatcher, and a `while True` loop. This is intentionally vulnerable so you can see the failure modes.

Baseline (unsafeguarded) example

```python theme={null}
from openai import OpenAI
import os, json

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

def check_calendar(date):
    return "10am: Standup, 2pm: Dentist"

def execute_tool(name, args):
    # Unsafeguarded: any exception raised inside a tool will propagate
    if name == "check_calendar":
        return check_calendar(**args)
    return "Unknown tool"

# Simplified run loop (unsafeguarded)
messages = [
    {"role": "system", "content": "You are an assistant that can call tools."},
    {"role": "user", "content": "What's on my calendar for 2023-10-01?"}
]

while True:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
    )
    msg = response.choices[0].message
    if msg.content:
        print(msg.content)
    if response.choices[0].finish_reason == "stop":
        break
    # If the model asks to call a tool and the tool raises, the whole script will crash.
```

Failure modes, fixes, and benefits

| Failure mode | Problem | Fix | Benefit |
| - | - | - | - |
| Tool crashes | Uncaught exceptions in tools crash the process and terminate the conversation | Wrap tool calls in `try`/`except` inside `execute_tool`; return a string like `"Error: ..."` instead of raising | Agent receives the error as a normal result and can adapt or retry |
| Infinite retries | `while True` loop may spin forever if model never signals finish or tools are flaky | Use a bounded loop (`for iteration in range(MAX_ITERATIONS)`) and a `for`-`else` to perform a final best-effort attempt | Prevents runaway API usage; ensures graceful exhaustion |

Failure mode #1 — tool crashes

* Problem: if a tool raises an exception, the entire script crashes, the user sees a traceback, and the conversation ends.
* Fix: catch exceptions in `execute_tool` and return a descriptive error string. The model will receive `"Error: ..."` as the tool output and can adjust behavior (try another tool, explain the problem, notify the user).

Add a flaky tool to test this behavior (it always raises), and update `execute_tool` to return an error string on exceptions.

Patch for safe tool execution

```python theme={null}
def flaky_tool(query):
    raise Exception("Service unavailable")

def execute_tool(name, args):
    try:
        if name == "check_calendar":
            return check_calendar(**args)
        if name == "flaky_tool":
            return flaky_tool(**args)
        return "Unknown tool"
    except Exception as e:
        # Return the error back to the agent as a string result
        return f"Error: {str(e)}. Try a different approach."
```

Wire `flaky_tool` into the tool list you provide the model (for example, in the system or tool description messages) and ask the agent to use it. The script should not crash — instead the model will receive a result beginning with `"Error"` and can recover.

<Callout icon="lightbulb" color="#1CB2FE">
  Return errors to the agent (as strings) rather than raising them from `execute_tool`. This lets the model handle failures gracefully and maintain conversation continuity.
</Callout>

Failure mode #2 — infinite loops

* Problem: `while True` exits only when the model signals finish (finish\_reason == "stop"). If tools are flaky or the model never returns a final finish, the agent can retry forever and burn credits.
* Fix: replace `while True` with a bounded loop (e.g., `for iteration in range(MAX_ITERATIONS)`). Use a `for`-`else` clause: the `else` block runs only when the loop exhausts iterations without a `break`. In this case, append a prompt asking for a best-effort answer and make one final completion call.

Main loop with iteration logging and graceful exhaustion

```python theme={null}
MAX_ITERATIONS = 10
model = "gpt-4o-mini"

messages = [
    {"role": "system", "content": "You are an assistant that can call tools."},
    {"role": "user", "content": "What's on my calendar for 2023-10-01?"}
]

for iteration in range(1, MAX_ITERATIONS + 1):
    print(f"Iteration {iteration} of {MAX_ITERATIONS}")
    response = client.chat.completions.create(model=model, messages=messages)
    msg = response.choices[0].message
    finish_reason = response.choices[0].finish_reason

    if msg.content:
        print(msg.content)

    # If the model finished cleanly, stop retrying
    if finish_reason == "stop":
        break

    # Otherwise, the model likely asked to call a tool. Example flow:
    # - parse tool request from msg (e.g., JSON or a structured string)
    # - call execute_tool(tool_name, tool_args)
    # - append tool result to messages and continue the loop
    # (Parsing logic omitted for brevity.)

else:
    # Loop exhausted without a clean finish; ask for the best-effort answer and call the model once more
    messages.append({
        "role": "user",
        "content": "You've reached the max iterations. Give your best answer with what you have."
    })
    final = client.chat.completions.create(model=model, messages=messages)
    print(final.choices[0].message.content)
```

<Callout icon="warning" color="#FF6B6B">
  Do not use unbounded retry loops in production agents. Always cap retries (e.g., `MAX_ITERATIONS`) and provide a clear final attempt so users get a best-effort response instead of the process running indefinitely.
</Callout>

Putting it all together — a safer `safe_agent.py`

* The minimal improvements to apply:
  1. Add `flaky_tool` for testing.
  2. Make `execute_tool` return errors (string) instead of raising.
  3. Replace `while True` with a bounded loop and use `for`-`else` to perform a final completion if the agent never finishes.

Example complete script (combine the patches above):

```python theme={null}
# safe_agent.py
from openai import OpenAI
import os, json, time

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
model = "gpt-4o-mini"
MAX_ITERATIONS = 10

def check_calendar(date):
    # Simulated calendar lookup
    return "10am: Standup, 2pm: Dentist"

def flaky_tool(query):
    # Simulated flaky service for testing error handling
    raise Exception("Service unavailable")

def execute_tool(name, args):
    try:
        if name == "check_calendar":
            return check_calendar(**args)
        if name == "flaky_tool":
            return flaky_tool(**args)
        return "Unknown tool"
    except Exception as e:
        # Return errors as strings so the agent sees them as tool results
        return f"Error: {str(e)}. Try a different approach."

def parse_tool_request(message_text):
    """
    Example parse: expect the model to return a JSON object indicating
    a tool call like: {"tool": "check_calendar", "args": {"date": "2023-10-01"}}
    If parsing fails, return None.
    """
    try:
        obj = json.loads(message_text)
        if isinstance(obj, dict) and "tool" in obj:
            return obj["tool"], obj.get("args", {})
    except Exception:
        pass
    return None, None

# Seed conversation
messages = [
    {"role": "system", "content": "You are an assistant that can call tools. When you need to call a tool, respond with a JSON object like {\"tool\": \"name\", \"args\": {...}}."},
    {"role": "user", "content": "What's on my calendar for 2023-10-01? Use available tools."}
]

for iteration in range(1, MAX_ITERATIONS + 1):
    print(f"Iteration {iteration} of {MAX_ITERATIONS}")
    response = client.chat.completions.create(model=model, messages=messages)
    msg = response.choices[0].message
    finish_reason = response.choices[0].finish_reason

    if msg.content:
        print("Model output:", msg.content)

    # If the model finished cleanly, we are done
    if finish_reason == "stop":
        break

    # Try to parse a tool request from the model's message
    tool_name, tool_args = parse_tool_request(msg.content or "")
    if tool_name:
        print(f"Parsed tool request: {tool_name} with args {tool_args}")
        tool_result = execute_tool(tool_name, tool_args)
        # Append the tool's result back into the conversation for the model
        messages.append({"role": "tool", "name": tool_name, "content": tool_result})
        # Also append a short assistant message acknowledging the tool output so the model can continue
        messages.append({"role": "assistant", "content": f"Tool {tool_name} returned: {tool_result}"})
        # small backoff to avoid tight loops in testing
        time.sleep(0.5)
        continue

    # If we couldn't parse a tool call, append the model content and let it continue
    messages.append({"role": "assistant", "content": msg.content})

else:
    # Exhausted retries without a clean finish; ask for a best-effort answer once
    messages.append({
        "role": "user",
        "content": "You've reached the max iterations. Please give your best answer with what you have."
    })
    final = client.chat.completions.create(model=model, messages=messages)
    print("Final best-effort output:", final.choices[0].message.content)
```

Quick test suggestions

* Ask the agent to use `flaky_tool`. You should see:
  * iteration logs increment,
  * `execute_tool` returning `"Error: Service unavailable. Try a different approach."`,
  * the model receives the error string and should either try a different tool or produce a best-effort answer when iterations are exhausted.
* Verify that the process never crashes with a stack trace from `flaky_tool`.

Why these changes matter (summary)

* Returning errors as tool results keeps the conversation alive and allows the agent to recover.
* Bounded loops prevent runaway API usage and give you a chance to provide a final best-effort response.
* Together they make your tool-calling agent resilient and production-ready.

References and further reading

* [OpenAI API docs](https://platform.openai.com/docs)
* Python — exception handling: [https://docs.python.org/3/tutorial/errors.html](https://docs.python.org/3/tutorial/errors.html)
* Agent design patterns and structured tool calls — prefer structured outputs (JSON) for reliable parsing

Good luck! Run the script again after applying the patches — you should see iteration counters, handled tool errors, and graceful termination instead of crashes or infinite retries.

<CardGroup>
  <Card title="Watch Video" icon="video" cta="Learn more" href="https://learn.kodekloud.com/user/courses/ai-agents-for-beginner-openclaw-case-study/module/d77598d4-d1d3-4768-97da-03ead60bf984/lesson/c0c99b65-11fa-4cea-a5c0-883310083b29" />
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.