<?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: Emmanuel Pierre Nnawuogo "Chuks"</title>
    <description>The latest articles on DEV Community by Emmanuel Pierre Nnawuogo "Chuks" (@cnpierrepapi).</description>
    <link>https://dev.to/cnpierrepapi</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%2F4103402%2F5ce11b6c-da23-43a5-a5f4-8dc0b8ab360b.jpg</url>
      <title>DEV Community: Emmanuel Pierre Nnawuogo "Chuks"</title>
      <link>https://dev.to/cnpierrepapi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cnpierrepapi"/>
    <language>en</language>
    <item>
      <title>Migragent #1</title>
      <dc:creator>Emmanuel Pierre Nnawuogo "Chuks"</dc:creator>
      <pubDate>Mon, 31 Aug 2026 23:17:43 +0000</pubDate>
      <link>https://dev.to/cnpierrepapi/migragent-1-54f9</link>
      <guid>https://dev.to/cnpierrepapi/migragent-1-54f9</guid>
      <description>&lt;p&gt;I spent most of a day last week arguing with a prompt.&lt;/p&gt;

&lt;p&gt;The agent had five tools. It used none of them. It answered from memory instead,&lt;br&gt;
politely and confidently, like a student who had not done the reading and was&lt;br&gt;
hoping nobody would ask.&lt;/p&gt;

&lt;p&gt;The prompt was fine. The tools were fine. The bug was three layers down in how I&lt;br&gt;
was building the request, and by the time I found it I had rewritten the prompt&lt;br&gt;
twice and the tool descriptions once.&lt;/p&gt;

&lt;p&gt;So here is the whole thing, because it cost me a day and it should cost you about&lt;br&gt;
four minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;Google's Agent Development Kit lets you bring your own model class. You subclass&lt;br&gt;
BaseLlm, ADK hands you an LlmRequest, and you turn that into whatever your&lt;br&gt;
endpoint wants. I do this because every model call in my system goes through one&lt;br&gt;
retry path, and I did not want the chattiest caller in the building opening its&lt;br&gt;
own connection with its own opinions about rate limits.&lt;/p&gt;

&lt;p&gt;On that LlmRequest is a config object. It holds 35 fields in one flat list.&lt;br&gt;
Temperature is in there. So is maxOutputTokens. So are your tools, your system&lt;br&gt;
instruction and your safety settings.&lt;/p&gt;

&lt;p&gt;Those 35 fields do not all go to the same place.&lt;/p&gt;

&lt;p&gt;Six of them belong at the top level of a Vertex generateContent request. Three&lt;br&gt;
mean something to the client library and nothing at all to the server. The other&lt;br&gt;
26 go inside generationConfig.&lt;/p&gt;

&lt;p&gt;Nothing on the object says which is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did, which is the obvious thing
&lt;/h2&gt;

&lt;p&gt;I took the config, dumped it, and put the whole lot in generationConfig. One&lt;br&gt;
line. It looks right. It parses. It runs.&lt;/p&gt;

&lt;p&gt;That call succeeds. You get a 200 back, and a response with normal looking text&lt;br&gt;
in it. Your tools are sitting inside generationConfig where the server is not&lt;br&gt;
looking, so the model was never offered them, so it answered the question it was&lt;br&gt;
asked using what it already knew.&lt;/p&gt;

&lt;p&gt;Think of it as a form with 35 boxes, where six of them have to be torn off and&lt;br&gt;
posted to a different address. The form does not say which six. Fill it in wrong&lt;br&gt;
and it is still accepted, cheerfully, and you find out much later that the&lt;br&gt;
important half never arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it took a day
&lt;/h2&gt;

&lt;p&gt;The failure has no edges.&lt;/p&gt;

&lt;p&gt;No warning. No unknown field error. No empty candidate. No exception anywhere.&lt;br&gt;
The model behaved correctly for the entire day. It was asked a question with no&lt;br&gt;
tools attached, so it answered the question.&lt;/p&gt;

&lt;p&gt;And a missing tool call looks exactly like a model deciding not to call a tool,&lt;br&gt;
which models do all the time. That is the trap. Every other time you see that&lt;br&gt;
symptom it really is the prompt, so you go and fight the prompt, and the prompt&lt;br&gt;
fights back by being innocent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;A list, written out by name, rather than a guess:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TOP_LEVEL = {"tools", "toolConfig", "systemInstruction", "safetySettings",
             "cachedContent", "labels"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Six fields. That is the complete set on google-genai 2.x. Everything else that is&lt;br&gt;
not client-only is a sampling setting and goes in generationConfig.&lt;/p&gt;

&lt;p&gt;There is a second half to this, and it is the friendlier one. Some fields on that&lt;br&gt;
config are for the library and never for the wire: httpOptions,&lt;br&gt;
automaticFunctionCalling, shouldReturnHttpResponse. Forward those and Vertex&lt;br&gt;
rejects the request outright for having unknown fields. That one you find in&lt;br&gt;
about a minute, because it shouts.&lt;/p&gt;

&lt;p&gt;Same underlying gap, pointing the other way. Part of this object is for the&lt;br&gt;
client and part is for the server, and there is no mark on it anywhere saying&lt;br&gt;
where the line runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A thing I got wrong while writing this up
&lt;/h2&gt;

&lt;p&gt;My own notes on this had two field names in that client-only list that do not&lt;br&gt;
exist. One is not on the type at all. The other is real but lives one level down,&lt;br&gt;
inside HttpOptions.&lt;/p&gt;

&lt;p&gt;Neither could have changed anything, because a field that never appears can never&lt;br&gt;
be forwarded, which is exactly why they sat in my code for weeks looking correct.&lt;br&gt;
I only caught them because I went to check the list against the type before&lt;br&gt;
filing the issue, which took two minutes and should have been the first thing I&lt;br&gt;
did rather than the last.&lt;/p&gt;

&lt;p&gt;Lists written from watching things fail record what you saw. They do not record&lt;br&gt;
what is there. Worth knowing the difference before you publish one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;p&gt;Filed as google/adk-python issue #6880, with a reproduction that runs offline. No&lt;br&gt;
project, no credentials, no network. It just prints the config keys and shows the&lt;br&gt;
tools landing in the wrong half.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/google/adk-python/issues/6880" rel="noopener noreferrer"&gt;https://github.com/google/adk-python/issues/6880&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are writing a custom BaseLlm, copy the six names above and move on with&lt;br&gt;
your day. If you are not, you will never meet this, which is probably why it has&lt;br&gt;
sat there.&lt;/p&gt;




&lt;p&gt;I wrote this piece for the purposes of entering the All Things Agentic Hackathon.&lt;br&gt;
It came out of building MIGRAGENT, an agent that reads official immigration and&lt;br&gt;
licensing sources every day and turns them into a guide where every line carries&lt;br&gt;
the sentence it came from and the date it was read.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://migragent.onenept.com" rel="noopener noreferrer"&gt;https://migragent.onenept.com&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AllThingsAgenticHackathon
&lt;/h1&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
