Skip to content

Eight Real Agent Failures, Explained

Before you start

Prerequisite: Lesson 29 — The eight architectural decisions, which gives you the vocabulary this lesson assumes — session directories, tickets, tool registries — without re-explaining them. After this lesson, you can: distinguish a passing test suite from a working system, and name the specific gap between them — the code paths and model behaviors that offline, scripted tests structurally cannot exercise.

The afternoon before this class, the checklist looked complete: framework working (tickets, manifests, state machine), 148 tests passing in six seconds, three scenarios implemented, scripted offline runs of all three clean, make demo-ch5 producing the expected output. Ready to ship v0.1.0.

Then the switch flipped to real LLMs, and over the next six hours, eight separate failures hit in sequence — each one caused by a different, specific misconception about how language models actually interact with tool frameworks, not by the framework being broken.

The question underneath all eight

Here's what to hold onto as you read: "my tests pass" and "my code works" are different claims. A test suite only exercises the code paths someone wrote a test for. A real LLM, given a real prompt, will find every path nobody thought to test — and it will do it today, not eventually.

#SymptomRoot cause
1make demo-ch5-real crashesTool registry API mismatch
2Wrong models used despite .envConfig prefix required
3No session artifacts after runTempdir evaporates on exit
4Research scenario: silent "No report written"Planner split to non-existent half
5Reviewer scenario: "no source found"LLM ignored inlined code, looked for a file
6Reviewer produces polished reviewOf code that doesn't exist
7Reviewer calls the right tool, review still wrongTool got fabricated source
8Everything works — but is it real?Dataflow integrity check needed
Failure 1 — a method that doesn't exist

AttributeError: 'ToolRegistry' object has no attribute 'list_tools'. Demo code called tools.list_tools(); the registry never had that method. The offline tests never exercised this path, because the offline demos don't print the tool list. Fixed with a defensive fallback: try the call, fall back to sorted(tools._tools.keys()) on AttributeError. The lesson: demo code that runs at the top of a user-facing script is a common blind spot for a test suite that only exercises the library underneath it.

Failure 2 — the .env that wasn't read

Output showed the default models (Qwen3-Next-80B-A3B-Thinking, Qwen/Qwen3-32B) instead of the ones set in .env (MiniMaxAI/MiniMax-M2.5, Qwen/Qwen3-235B-A22B-Instruct-2507). Config.from_env() reads SOVEREIGN_AGENT_LLM_PLANNER_MODEL, not the short name PLANNER used in the .env file. Config prefixes exist to prevent collisions between unrelated tools sharing an environment — but that safety trades away discoverability. The fix isn't a code change, it's documenting the full variable names in .env.example, not the aspirational short ones.

Failure 3 — sessions that evaporated

make example-research-real finished, and ls sessions/ returned "No such file or directory." The demo code used tempfile.TemporaryDirectory(), which deletes itself the moment the Python process exits — taking every artifact from the run with it. Fixed by persisting real runs under ./sessions/ and reserving the tempdir for offline/scripted mode only. If you can't inspect what happened after a run, you can't debug anything. Tests can afford to evaporate their fixtures. Demos cannot.

Failure 4 — the structured half that wasn't there

The research assistant ran and produced: outcome: handoff_to_structured, (No report written.). The planner had split the task into three subgoals and assigned two of them to a structured half — except the research scenario has no structured half at all. The handoff went into the void. The planner's prompt lists "loop or structured" as options regardless of what's actually wired into the scenario; it doesn't check. Fixed with explicit task constraints: "This entire task stays in the LOOP half. Do NOT assign subgoals to the structured half." First instance of a pattern that recurs through this whole list — the model reaching for a capability the scenario doesn't need, because nothing told it the capability wasn't available.

Failure 5 — the LLM that wouldn't see inlined code

Source code was inlined directly in the task prompt for the code-reviewer scenario. The trace showed read_file(path="workspace/source.py") → not found, then list_files(path="workspace") → empty, then complete_task(status="failed", reason="No source file found"). The model never called the analyzer tool at all — it saw the word "file" in a tool name, ignored the inlined source sitting right there in its own context, and went hunting the disk instead. The first fix attempt strengthened the prompt with explicit "do NOT try to read it from the workspace" language. The model called list_files twice anyway. When a directory-listing tool exists, models reflexively reach for it to "orient themselves" — training data is full of trajectories that start that way, and no prompt reliably overrides the reflex. The fix that actually worked: don't offer the tool. Narrow the registry to exactly what the scenario needs, same move as Decision 8 from the previous lesson.

Failure 6 — a perfect review of nothing

With the narrowed registry, the scenario ran clean: output written, complete_task called, every ticket green, every manifest verified. The review itself described functions named add, multiply, and divide, all "implemented correctly," zero issues found. The actual source file has no such functions — it has one function called do_everything_in_one_function, with wildcard imports and too many print statements. Root cause, found by logging exactly what the analysis tool received: a _TOOL_CALL_LOG capturing source_matches_sample == False on the first 80 characters, proving the LLM had fabricated 142 bytes of plausible-looking Python, run it through the analyzer, gotten 2 findings back on that fiction, and blended those findings with its own training-data knowledge into a fluent, wrong review. The fix: stop inlining source in prompts entirely. Seed it to workspace/sample.py on disk before the run, and give the tool a filename to read itself — no copying, no re-tokenization, no compression-induced drift between what the file says and what the model repeats.

Failure 7 — dataflow is not guaranteed by structure

With the file-on-disk pattern, the tool definitely read the real source. But nothing forced the LLM to actually use what the tool returned — it could still ignore four real findings and write a report describing four different, fabricated ones, and nothing in the framework's tickets or manifests would catch it. This is the gap between structural correctness (the tool was called, the manifest verified, the state advanced) and semantic correctness (the output of tool N was meaningfully used as input to tool N+1). Tickets guarantee the first. They say nothing about the second — which is exactly the gap this whole arc opened on in Lesson 27.

Failure 8 — the sentinel that got skipped

The final run: a correct, real code review, all four real findings present, dataflow verified clean. And still: ⚠ ipc/session_complete.json missing. The LLM wrote the review, then emitted a plain-text summary of what it had done instead of calling the formal complete_task sentinel. The session ended cleanly in practice, but the completion record was never written. Models sometimes skip protocol steps once the "real work" feels finished to them. The fix isn't a prompt arms race — it's accepting the behavior, treating a missing sentinel as informational rather than fatal, and considering a framework-level auto-close for the next version.

Failures 5, 6, and 7 aren't three unrelated bugs — they're one root behavior cascading through the pipeline, and the fix that broke the cascade solved two of the three at once:

One root behavior, three symptoms, one fix that closes two of them

Notice what the diagram makes explicit that the chronological list doesn't: the file-on-disk fix closes Failures 5 and 6 together, because both were downstream of the same root behavior. Failure 7 survives that fix untouched — it isn't caused by the same root behavior at all, it's the next layer of the problem, and it's the one that finally forces a real dataflow check instead of another prompt patch.

The final scoreboard

After all eight fixes, here's what the same three scenarios looked like running end-to-end against real LLMs, with full dataflow integrity in place:

demo-ch5-real 3.1s 2 tool calls ✓ minimal smoke research-assistant 8.2s 3 tool calls ✓ 2/2 citations verified

Every one of these now carries an integrity check that would catch a fabrication on any future run — not because the framework got smarter, but because each failure taught exactly what to check for.

Quick check — 148 tests passed and three scripted scenarios ran clean before any of these eight failures happened. What does that tell you about the risk that remained?

What to carry forward

Everything you just watched happen is what made the demo you're about to see possible. The next lesson runs these same three scenarios live, against real models, with nothing pre-cached — and then extracts the pattern that Failure 7 only named: how to write a dataflow integrity check that catches this class of silent failure before it ships.

Continue to Lesson 31

The live demo, run for real, plus the ~30-line dataflow integrity pattern every scenario needs — with no exceptions.

Have a question about this lesson?

Reply here and it goes straight to Rod. Same as replying to one of his emails.