The earlier parts show the agent working. This one collects what broke while I built it, each as symptom, cause, fix and lesson. The longest entry is a bug where the agent told me a website was unreachable when the real problem was a mismatched browser driver, because it shows two separate failures stacked on top of each other.
Prerequisites: Part 1 through Part 8. Each short entry links to the part that covers it in depth.
The centerpiece: "the site could not be reached"
I asked the agent to check whether a website was up. It routed to the web-crawler tools and called get_page_content. The tool result was this:
Page extraction failed: Message: session not created: This version of ChromeDriver only supports Chrome version 144
Current browser version is 155.0.8059.41
The model then told me, confidently, that the site "could not be reached".
Symptom: the agent reported a site as unreachable. Cause: nothing to do with the site. The browser driver and the browser were different major versions. Fix: two changes, because my first fix was wrong. Details below.
There are two bugs here and they have different owners. One is a tool bug (a driver mismatch). The other is an agent bug (a tool error became a false statement about the world). I'll take them in that order.
Reading the first line
The stack trace is long and mostly noise. The first line says everything: ChromeDriver supports Chrome 144, and the installed Chrome is 155. I had never touched Chrome's version. Chrome updates itself in the background, so one day it moved from 144 to 155 and the driver the crawler used did not.
That sent me to find which driver the crawler was actually using. The crawler preferred a ./chromedriver binary bundled in its project folder:
./chromedriver --version
# ChromeDriver 144.0.7559.109
The mismatch was confirmed. Chrome 155, driver 144. Selenium refuses to drive a browser whose major version differs from the driver's.
First fix: let Selenium Manager choose the driver
Selenium 4.6 and later ships a helper called Selenium Manager. When you don't hand Selenium a driver path, it detects your installed browser and downloads a matching driver into a cache (~/.cache/selenium). I was on Selenium 4.40, so the feature was already there; the crawler just never used it because it passed an explicit path.
The change was to stop passing a path and let Selenium resolve it:
driver = webdriver.Chrome(service=Service(), options=options)
I expected that to be the end of it. It failed again with a version error.
Why it still failed: a second stale copy
Selenium Manager logs what it decides. This time the log had a line I had not seen before, and it explained everything (path shortened):
The chromedriver version (145.0.7632.26) detected in PATH at <homebrew path>/chromedriver might not be compatible with the detected chrome version (155.0.8059.41); currently, chromedriver 155.0.8059.84 is recommended for chrome 155.*, so it is advised to delete the driver in PATH and retry
There was an old Homebrew chromedriver (version 145) on my PATH. Selenium Manager prefers a driver it finds on PATH, even when it warns that it is incompatible. So my "automatic" fix picked a stale 145 instead of downloading 155. Then the old code path caught the failure and fell back to the bundled 144 binary, which also failed. Two stale copies of the same program, each hiding the other: removing the bundled one would have exposed the Homebrew one, and vice versa.
I could have uninstalled the Homebrew driver. But that fixes my machine, not the program, and the next stale copy would break it again. I looked for a way to tell Selenium Manager to ignore PATH. Running selenium-manager --help listed --skip-driver-in-path, and the environment variable form is SE_SKIP_DRIVER_IN_PATH=true.
I tested that on the command line first, before touching code, by setting the variable for one run. It worked: the crawler downloaded a 155 driver and fetched the page.
Second fix: in code, with a fallback
Then I put it in the crawler. The idea, with the surrounding method trimmed:
@staticmethod
def _find_local_chromedriver() -> str | None:
"""Project root first, then system PATH and common Homebrew locations."""
# ...
# inside the driver setup:
# Selenium Manager would otherwise reuse a stale chromedriver found on PATH
# even when it doesn't match Chrome, so tell it to ignore PATH drivers.
os.environ.setdefault('SE_SKIP_DRIVER_IN_PATH', 'true')
try:
driver = webdriver.Chrome(service=Service(), options=options)
except WebDriverException as auto_error:
driver_path = self._find_local_chromedriver()
if not driver_path:
raise RuntimeError("Selenium Manager failed and no local chromedriver was found.") from auto_error
driver = webdriver.Chrome(service=Service(driver_path), options=options)
The order matters. Selenium Manager goes first, with PATH drivers ignored. The old lookup (bundled binary, then PATH, then common install locations) survives only as a fallback if Selenium Manager cannot run, for example offline. I shortened the error message and elided the helper's body in this excerpt; the real code is slightly longer.
After this, the same request worked: the crawler did a 5-page crawl of the site and the agent returned the page title. The first run after each Chrome update is slower because it downloads a new driver; after that the cached one is reused.
setdefault means an explicit SE_SKIP_DRIVER_IN_PATH in your environment still wins. If you ever want PATH drivers back, set it to false before launching.
The other bug: a tool error became a claim about the world
Fixing the driver fixed my machine. It did not fix what the agent did with the error. The tool said "session not created", and the model summarised that as "the site could not be reached". That is a false statement. It sounds plausible, it is the sort of thing a failing fetch often means, and nothing in the tool result supported it.
There is also a deeper mismatch. "Is this site up?" is a question about an HTTP response, and none of my tools returns one. get_page_content launches a real browser, renders the page and extracts text. When that fails, the failure could be the browser, the driver, a timeout, or the site. The model had no tool that answered the actual question, so it improvised from an error message.
The right fix is a tool shaped like the question: a tiny check_url that makes one HTTP request with httpx and returns the status code and how long it took. A failed request would then say "connection refused" or "timed out" for real. I chose not to add it to this starter, so the agent today can still make this mistake with a different tool error. Part 10 lists it as a next step.
What I took from this
- Read the first error line, not the stack trace. The first line named both versions.
- Two stale copies of the same binary can hide each other. If a fix changes the error but not the outcome, check what else is on
PATH. - Fix the cause, not the instance. An auto-updating browser will keep drifting. Resolving the driver at runtime means the next Chrome update does not break anything.
- Models turn tool errors into confident claims. Give the agent tools that answer the question being asked, and treat any "X is down" answer from a failing tool with suspicion.
The rest of the log
create_react_agent is deprecated
Symptom: a deprecation warning, and examples online that no longer matched. Cause: the prebuilt agent helper I started from was replaced. Fix: use create_agent and pass system_prompt= where older code passed prompt=. Lesson: check the date on a LangGraph tutorial. Covered in Part 1.
Invented dates from epoch timestamps
Symptom: the model reported file modification times as dates that were wrong or made up. Cause: the tools return raw epoch numbers and the 7B model does arithmetic badly. My first prompt told it to convert them to readable dates, which made it worse, because it now felt obliged to produce a date. Fix: tell it to quote the raw number and not convert. Lesson: don't ask a small model to do a computation you can avoid. Covered in Part 2.
Miscounted folders
The general route: vague, then wrong
Symptom: a question routed to the tool-less general agent got a vague answer, and a follow-up got a confidently wrong one. Cause: the model does not know the library I asked about, and had no tool to look it up. Fix: none that the model can do alone. Lesson: a route without tools can only answer from training data. Covered in Part 4.
The approval test hit the sandbox
Symptom: my approval test failed with security_error; piped input arrived out of order; a stray write proposal appeared and was stopped by the gate. Cause: the test path was outside the file server's sandbox, and piping answers into an interactive prompt is fragile. Fix: test inside the allowed directory and answer prompts in sequence. The gate did its job on the stray write. Lesson: the approval step is a usability safeguard; the server's sandbox is what actually restricts access. Covered in Part 6.
A truncated path with no marker
Symptom: the approval prompt showed a path cut off with nothing to say it was cut. Cause: plain slicing of the argument text. Fix: _clip in display.py, which appends … (+N chars) when it cuts. Lesson: a prompt that asks for consent must not hide what it is consenting to. Covered in Part 6.
Noisy logs and raw dumps
Symptom: MCP servers printed INFO logs into my terminal, and tool results dumped whole. Cause: the servers configure logging themselves and write to stderr. Fix: quiet_server.py launches a server with INFO logging disabled, and display.py prints compact, clipped output. Lesson: usability is part of the agent. Covered in Part 8.
A perfect eval score that meant little
Symptom: the routing eval scored full marks. Cause: the cases were too easy, one category per prompt with obvious keywords. Fix: a number I trusted less, and a to-do for harder cases. Lesson: a perfect score on a small set is a prompt to look at the set. Covered in Part 5.
Limits that stay
With this 7B model, a few problems are not bugs I can fix. It does not know libraries released after its training, it is unreliable at counting, and it will sometimes explain a tool error instead of reporting it. Prompts and routing reduce these; they do not remove them.
Recap
- The "site unreachable" answer was a ChromeDriver and Chrome version mismatch, not a site problem.
- Selenium Manager fixes drift, but only if it ignores stale drivers on
PATH(SE_SKIP_DRIVER_IN_PATH=true). - A tool error turned into a false claim: match tools to the question (a
check_urltool), and distrust "it's down". - Most other failures were small-model limits; some I fixed with prompts or code, some I left.
- Keep a log like this. It is the fastest way to see which problems repeat.
Next: Part 10