<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: rizkynandapr</title>
    <description>The latest articles on DEV Community by rizkynandapr (@rizkynandapr).</description>
    <link>https://dev.to/rizkynandapr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4137312%2Ff89bfe21-1cb9-4d5b-9478-240c93bd90c2.jpg</url>
      <title>DEV Community: rizkynandapr</title>
      <link>https://dev.to/rizkynandapr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rizkynandapr"/>
    <language>en</language>
    <item>
      <title>8 of my AI agent's 30 test calls failed. Every one was my fault.</title>
      <dc:creator>rizkynandapr</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:38:07 +0000</pubDate>
      <link>https://dev.to/rizkynandapr/8-of-my-ai-agents-30-test-calls-failed-every-one-was-my-fault-3kff</link>
      <guid>https://dev.to/rizkynandapr/8-of-my-ai-agents-30-test-calls-failed-every-one-was-my-fault-3kff</guid>
      <description>&lt;p&gt;The record said there was a gas leak. It did not say where.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urgency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"emergency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"emergency_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callback_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"street"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"postal_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"escalated"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My AI intake agent wrote that after a caller said their furnace was out and the basement "kind of smells like eggs." It told them to leave the house and call 911 from outside, which was the right thing to say. Then it handed an on-call dispatcher an emergency with no address and no phone number. Nobody could be sent. Nobody could call back.&lt;/p&gt;

&lt;p&gt;The model did exactly what I told it to. That was the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building
&lt;/h2&gt;

&lt;p&gt;An intake agent for home service contractors in the US: HVAC, plumbing, roofing. It answers the phone (or an SMS, or a web form), works out how urgent the problem is, collects what a dispatcher needs, and writes one JSON record at the end that goes into the contractor's system.&lt;/p&gt;

&lt;p&gt;Before letting it near a real caller I wrote 30 scripted test calls, ten per trade. The caller's lines are fixed. The agent replies turn by turn, and a runner scores the result on nine criteria. Four are checked by plain code: is the record valid against the schema (C1), is the urgency right (C2), are the required fields filled (C3), is the emergency type right (C4). The other five need a second model as judge, and I haven't run those yet. Everything below is the deterministic half, run against gpt-5.5 through a local gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before I had a baseline, one test failed three different ways
&lt;/h2&gt;

&lt;p&gt;I started with a single case, &lt;code&gt;hvac-01&lt;/code&gt;, the gas leak above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First failure: "no intake object found."&lt;/strong&gt; Except there was one. The agent had written a perfectly reasonable JSON object. My checker looks for a &lt;code&gt;schema_version&lt;/code&gt; field to recognise the record, and it wasn't there.&lt;/p&gt;

&lt;p&gt;It wasn't there because my prompt said this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;At the end of every conversation, emit a single JSON object conforming to &lt;code&gt;01-intake-agent/schema/intake.schema.json&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a file path. The model can't open files. It never saw the schema, so it invented its own shape from the field tables in the prompt: trade fields dumped at the top level, &lt;code&gt;"gas_smell"&lt;/code&gt; where the enum says &lt;code&gt;"gas"&lt;/code&gt;, no &lt;code&gt;outcome&lt;/code&gt; at all. Fixing &lt;code&gt;schema_version&lt;/code&gt; alone would have failed three more ways.&lt;/p&gt;

&lt;p&gt;The fix was to stop describing the schema and start including it. The runner now reads the schema file and injects it into the prompt, the same file the validator loads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;schema_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8-sig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intake_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# the real code wraps it in a json fence
&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;guardrails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trade_module&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;validator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Draft202012Validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema_text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract the agent reads and the contract it's graded on can't drift apart anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second failure: the record at the top of this post.&lt;/strong&gt; My emergency guardrail, G4, said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If any of these appears at any point in the conversation, stop the intake immediately. Do not finish your question. Do not collect remaining fields.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It never said when to start again. So when the caller said "Okay, I'm outside," safe and still on the line, the agent asked for nothing. It obeyed.&lt;/p&gt;

&lt;p&gt;G4 now has a section called the safety-critical minimum. Once the caller confirms they're out, ask exactly two things, one at a time: the address, and a number to call back. Nothing else. If they panic or hang up, write what you have and mark it &lt;code&gt;abandoned&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third failure, and this one no score caught.&lt;/strong&gt; I was reading the transcript to debug the second problem and saw this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CALLER: Okay hang on
AGENT:  { "schema_version": "1.0", "trade": "hvac", "urgency": "emergency", ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller said "hang on" and the agent answered with raw JSON. It did it three times in one call. On a voice line, the text-to-speech layer reads whatever the agent returns. So somebody standing in their yard next to a gas leak would have heard a JSON object read out loud.&lt;/p&gt;

&lt;p&gt;C1 passed anyway, because the checker reads the agent's last turn first and the last turn happened to contain exactly one object. None of my nine criteria looks for this. I only found it because I was reading a transcript for a different reason.&lt;/p&gt;

&lt;p&gt;The prompt now says the record is written once, when the call is actually over, and that "hang on" and "one sec" are not the end of a call.&lt;/p&gt;

&lt;h2&gt;
  
  
  First full run: 22 of 30
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;hvac-01&lt;/code&gt; passing I ran all thirty. Eight failed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;What the checker said&lt;/th&gt;
&lt;th&gt;What was actually wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;plm-01, plm-10, rf-02&lt;/td&gt;
&lt;td&gt;missing callback_number, service_address&lt;/td&gt;
&lt;td&gt;the scripted caller never says them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hvac-03, plm-02&lt;/td&gt;
&lt;td&gt;missing property_type&lt;/td&gt;
&lt;td&gt;the prompt forbids guessing, and never says that "my kitchen sink" means residential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hvac-09&lt;/td&gt;
&lt;td&gt;expected same_day, got routine&lt;/td&gt;
&lt;td&gt;nothing in the kit says a repeat failure is urgent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rf-10&lt;/td&gt;
&lt;td&gt;expected same_day, got routine&lt;/td&gt;
&lt;td&gt;roofing triage has no line for commercial tenants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rf-01&lt;/td&gt;
&lt;td&gt;expected eme  rgency_type null, got "other"&lt;/td&gt;
&lt;td&gt;I never defined what emergency_type means&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row is the o ne that stings. Three emergency test cases demanded a phone number and an address that no line in the script ever supplied. No agent could pass them, however well it asked. I'd already found four cases like that in the HVAC file while debugging &lt;code&gt;hvac-01&lt;/code&gt;, fixed those, and assumed plumbing and roofing were fine. I hadn't checked. That's seven of thirty cases, almost a quarter of the suite, that were impossible to pass. I would have spent days tuning the prompt against them.&lt;/p&gt;

&lt;p&gt;The two urgency failures are a different kind of embarrassing. A customer calling because the tech was there on Tuesday and it's doing the same thing again is the call a contractor most needs to see today. A leak over a shop's storeroom costs the tenant money by the hour. I knew both of these. I had never written them down, so the agent had nothing to go on and picked the mildest reading. Both are now rules in the core prompt that outrank the trade modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  None of the eight were the model disobeying
&lt;/h2&gt;

&lt;p&gt;This is the thing I keep coming back to. Every failure was a place where my kit said two things that couldn't both be true, and the model picked one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Collect the required fields" and "stop collecting fields" (G4)&lt;/li&gt;
&lt;li&gt;"Unknown values are null" and a schema where &lt;code&gt;call_meta.started_at&lt;/code&gt; must be a string&lt;/li&gt;
&lt;li&gt;"Never guess" and "property_type is required," with callers who never say "residential"&lt;/li&gt;
&lt;li&gt;"Required: callback_number" and a script with no phone number in it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the agent "ignores" an instruction now, the first thing I do is look for the other sentence in my own prompt that it followed instead. So far it's always been there.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the test harness is the bug
&lt;/h2&gt;

&lt;p&gt;Two more, and both were in the runner, not the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The nudge that manufactured a failure.&lt;/strong&gt; At the end of each case the runner sends one last message, &lt;code&gt;[caller disconnected]&lt;/code&gt;, so an agent that got cut off still writes its record. It sent it every time, unconditionally.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rf-05&lt;/code&gt; is a web form. Name, phone, address and the problem all arrive in the first message. The agent wrote a complete, correct record straight away. Then the runner told it the caller had disconnected, and it wrote a second record with every field null. The checker reads the last turn first. The empty record won.&lt;/p&gt;

&lt;p&gt;That case had passed on two earlier runs and failed on the third. For a while it looked like randomness. It was a trap in the harness that finally got sprung.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Nudge only when the agent hasn't written a record yet.
&lt;/span&gt;&lt;span class="n"&gt;already&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;already&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[caller disconnected]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The quota wall that deleted everything.&lt;/strong&gt; Partway through a roofing run my API quota ran out. The runner retried three times, then died with a Python traceback, and the six cases it had already scored were gone because it only wrote the report at the very end.&lt;/p&gt;

&lt;p&gt;If you run evals on a subscription or a free tier, this will happen to you. Now the runner recognises a quota error (including a 429 that the gateway wraps as a 503), stops cleanly, writes everything it has with &lt;code&gt;"complete": false&lt;/code&gt;, and has a &lt;code&gt;--resume&lt;/code&gt; flag that skips cases already scored.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke after the fixes
&lt;/h2&gt;

&lt;p&gt;For the &lt;code&gt;emergency_type&lt;/code&gt; problem I added a rule: it names which G4 emergency branch fired, and it's null if none did. &lt;code&gt;rf-01&lt;/code&gt;, water pouring through a ceiling during a storm, started passing.&lt;/p&gt;

&lt;p&gt;On the next run &lt;code&gt;rf-02&lt;/code&gt; failed instead. A bedroom ceiling "bulging down, like a balloon full of water" came back as an emergency with &lt;code&gt;emergency_type: null&lt;/code&gt;. The agent had checked the G4 table, found rows for gas, electrical, flooding, carbon monoxide and injury, found nothing structural, and did what I'd just told it to. The schema had &lt;code&gt;structural&lt;/code&gt; in its enum. G4 never did. I'd fixed one contradiction and exposed the next one.&lt;/p&gt;

&lt;p&gt;G4 has a structural row now, including "don't puncture it or try to drain it," which is what you actually want a caller to hear about a ceiling full of water.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;First full run: 22/30.&lt;/li&gt;
&lt;li&gt;After fixing the kit: 29/30 in the next full run.&lt;/li&gt;
&lt;li&gt;After the last two fixes (the structural row and the runner nudge): HVAC 10/10, plumbing 10/10, and roofing 6 of 6 before my monthly quota ran out. The last four roofing cases passed on the previous version. I haven't rerun them on this one yet, and I'll update this post when I do.&lt;/li&gt;
&lt;li&gt;The five judge-scored criteria haven't been run at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I'm not going to tell you it's 30/30.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're building one of these
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't reference your schema by file path. Put it in the prompt, loaded from the same file your validator uses.&lt;/li&gt;
&lt;li&gt;Read the transcripts of cases that pass. My worst bug was in one.&lt;/li&gt;
&lt;li&gt;Check that every test case can actually be passed. Every required field has to appear somewhere in the caller's lines.&lt;/li&gt;
&lt;li&gt;For any guardrail that says "stop," write down what happens after stopping.&lt;/li&gt;
&lt;li&gt;Make the runner survive a quota error and save partial results. You'll need it the first time it matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's one thing I still don't measure. My agent says "Leave building now" and "Stay out of building," dropping the articles, like a telegram. It's correct and it passes every check I have. It also sounds like a robot to someone who is scared. None of my deterministic checks catch it, so that's what the judge run is for.&lt;/p&gt;

&lt;p&gt;I turned the check that would have caught that first record into a free n8n workflow. It pages a human when an intake agent logs an emergency with no address or callback number: &lt;a href="https://github.com/rizkynandapr/n8n-intake-record-guard" rel="noopener noreferrer"&gt;https://github.com/rizkynandapr/n8n-intake-record-guard&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>promptengineering</category>
    </item>
  </channel>
</rss>
