<?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: Harshavardhan Katkam</title>
    <description>The latest articles on DEV Community by Harshavardhan Katkam (@harshavardhan_katkam).</description>
    <link>https://dev.to/harshavardhan_katkam</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%2F3895644%2F19991469-56af-4b2e-82ef-71ed558444bf.jpg</url>
      <title>DEV Community: Harshavardhan Katkam</title>
      <link>https://dev.to/harshavardhan_katkam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harshavardhan_katkam"/>
    <language>en</language>
    <item>
      <title>Day 03: Your repo does not fit in the context window. Now what?</title>
      <dc:creator>Harshavardhan Katkam</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:19:13 +0000</pubDate>
      <link>https://dev.to/harshavardhan_katkam/day-03-your-repo-does-not-fit-in-the-context-window-now-what-2l5a</link>
      <guid>https://dev.to/harshavardhan_katkam/day-03-your-repo-does-not-fit-in-the-context-window-now-what-2l5a</guid>
      <description>&lt;p&gt;&lt;em&gt;Handing cartographer files one at a time had been working, so I did the obvious next thing and wrote the four lines that walk a repository and concatenate every source file into one string. Large context windows exist precisely so you can stop being clever about this.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Then, out of habit, I printed &lt;code&gt;len()&lt;/code&gt; on the string before sending it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I never sent it. Not because it would definitely have failed — some of it would have fit — but because I could not answer either of the two questions that immediately followed. How many tokens is that, actually? And what does one run of this cost? I had been building an agent for two days with no number attached to any part of it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;So today's build is not a feature. It is a tape measure and a meter. What they tell you is the constraint the entire project exists to work around, and it is better to meet it with a &lt;code&gt;print()&lt;/code&gt; than with an invoice.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Code for this day:&lt;/strong&gt; &lt;a href="https://github.com/Harshavardhan17/cartographer/tree/day-03" rel="noopener noreferrer"&gt;cartographer at day-03&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A repository is measured in &lt;strong&gt;characters on disk&lt;/strong&gt;; a model bills in &lt;strong&gt;tokens&lt;/strong&gt;, the chunks of text it reads and writes. The ratio between them is not a constant you can look up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not estimate with a tokenizer library from a different model family.&lt;/strong&gt; You get a precise number for the wrong vocabulary, because each family splits text into tokens its own way, and the error is systematic rather than random.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two samples and a subtraction&lt;/strong&gt; give you the real ratio and cancel the fixed per-request overhead that a single sample would silently bake in.&lt;/li&gt;
&lt;li&gt;The context window, the most one request can carry, is the wall you notice. &lt;strong&gt;The token count is the wall you hit first&lt;/strong&gt;, because a transcript re-sends itself on every call.&lt;/li&gt;
&lt;li&gt;Input and output are priced differently, so &lt;strong&gt;a meter that adds the two token counts together tells you nothing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The conclusion is not "wait for a bigger window". It is that &lt;strong&gt;the agent has to choose what to read&lt;/strong&gt;. That is a feature, not a workaround.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measure the repository first
&lt;/h2&gt;

&lt;p&gt;Walking a repository is easy. Walking it &lt;em&gt;correctly&lt;/em&gt; is where the first mistake lives.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;source_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SOURCE_SUFFIXES&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;SKIP_DIRECTORIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intersection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;source_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both live in a new module, &lt;code&gt;src/cartographer/budget.py&lt;/code&gt;, which knows about files and arithmetic and nothing about HTTP. &lt;code&gt;rglob("*")&lt;/code&gt; walks every path under the root. A file survives only if its extension is in &lt;code&gt;SOURCE_SUFFIXES&lt;/code&gt; (Python, docs, config) and none of its folder names below the root is in &lt;code&gt;SKIP_DIRECTORIES&lt;/code&gt;; the set intersection checks them all at once. &lt;code&gt;measure&lt;/code&gt; returns the file count and the total characters, ignoring bytes that are not valid text.&lt;/p&gt;

&lt;p&gt;The skip list is doing more work than the walk. Leave &lt;code&gt;.venv&lt;/code&gt; and &lt;code&gt;node_modules&lt;/code&gt; out of it and you are not measuring a project, you are measuring its dependencies, which is a much larger number that says nothing about the code. &lt;strong&gt;Measure the code someone wrote, not everything installed beside it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run that on any repository you have lying around before reading further. The character count is real and takes seconds to get.&lt;/p&gt;

&lt;h2&gt;
  
  
  The estimate everybody gets wrong
&lt;/h2&gt;

&lt;p&gt;Characters are not what you are billed for. So you need a ratio, and there are two popular ways to get one, both wrong.&lt;/p&gt;

&lt;p&gt;The first is &lt;code&gt;characters // 4&lt;/code&gt;. It is a folk constant, it is not specific to your model, and it is not specific to source code, which tokenizes differently from prose because of indentation, punctuation and long snake_case identifiers.&lt;/p&gt;

&lt;p&gt;The second is worse, because it feels rigorous: install a tokenizer library, encode the repository, report the count. That library ships one vendor's vocabulary. If you are calling a model from a different family, you have computed a precise number for a tokenizer nobody in your request is using. Precision is not accuracy, and the error is not noise you can average away — it is a systematic bias in a direction you cannot see from the outside, because you do not have the vocabulary the server used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two samples and a subtraction
&lt;/h2&gt;

&lt;p&gt;There is an authoritative counter available and it costs almost nothing to query: the server tells you what it counted, in &lt;code&gt;usage.prompt_tokens&lt;/code&gt;, on every response.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;post&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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="n"&gt;max_tokens&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&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;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;post&lt;/code&gt; is the old body of &lt;code&gt;ask&lt;/code&gt; in &lt;code&gt;src/cartographer/agent.py&lt;/code&gt;, now its own function so it can return the whole response and accept an optional &lt;code&gt;max_tokens&lt;/code&gt;; &lt;code&gt;ask&lt;/code&gt; now calls it and keeps its old signature. One request, capped at a single output token, because we are measuring input, not generating an answer.&lt;/p&gt;

&lt;p&gt;Now the naive move is to send one sample and divide. Do not: &lt;code&gt;prompt_tokens&lt;/code&gt; counts the whole rendered request, including the chat template the server wraps your message in. On a short sample that fixed overhead is a large fraction of the total, and you would fold it permanently into your ratio.&lt;/p&gt;

&lt;p&gt;Send two samples of very different sizes instead, and take the slope. The constant cancels.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chars_per_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;large&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;large&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;large&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two requests, two output tokens, and you have a ratio measured against the model you are actually calling. It goes stale the moment you change models, which is the argument for it being a function rather than a constant in a config file.&lt;/p&gt;

&lt;p&gt;The test that matters is offline: give it a counter with a known overhead of 11 tokens and a known true ratio of 4, and assert the result is exactly 4.0. If the subtraction were wrong, that assertion would fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count input and output separately
&lt;/h2&gt;

&lt;p&gt;The other half of today is the meter, and it exists because the two token counts are not interchangeable.&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="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;input_per_million&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;output_per_million&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;record&lt;/code&gt; adds each response's &lt;code&gt;usage&lt;/code&gt; block to running totals, and &lt;code&gt;post&lt;/code&gt; calls it on every response, so no request can bypass the meter. A &lt;code&gt;dollars&lt;/code&gt; property multiplies each total by its own price per million, and &lt;code&gt;tests/test_budget.py&lt;/code&gt; pins all three pieces offline: the calibration, the skip list and the pricing. Every line is explained in &lt;a href="https://github.com/Harshavardhan17/cartographer/blob/main/docs/devnotes/day-03.md" rel="noopener noreferrer"&gt;today's dev notes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Input and output are billed at different rates on every provider I have used, so a single total is not convertible into money. Keeping them apart also tells you which lever to pull: the transcript grows and every file gets re-sent, while the output stays a paragraph.&lt;/p&gt;

&lt;p&gt;The rates themselves come from environment variables and default to zero. No price literal goes into the source, because published rates change without anyone editing your repo, and a stale number in version control is worse than no number. On a free model the dollars are honestly zero and the token counts are still true, which is the point: tokens are the thing that runs out, against the window and against rate limits, long before money does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to make a repo fit
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Send the whole repo&lt;/th&gt;
&lt;th&gt;Index it all up front&lt;/th&gt;
&lt;th&gt;Let the agent choose what to read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;You pay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the whole repo, on every call&lt;/td&gt;
&lt;td&gt;one embedding pass (every file turned into searchable numbers), then cheap lookups&lt;/td&gt;
&lt;td&gt;only for the files it opened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Breaks when&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the repo outgrows the window, which is soon&lt;/td&gt;
&lt;td&gt;the index goes stale on the next commit&lt;/td&gt;
&lt;td&gt;the agent chooses badly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Right when&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;one question, one small repo&lt;/td&gt;
&lt;td&gt;stable code, many repeated queries&lt;/td&gt;
&lt;td&gt;large repo, open-ended questions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third column is the one this project needs, and not reluctantly. A map made from everything is not a map: it is the territory again. Choosing what matters is the actual product, and the constraint is what forces the design to be honest about it.&lt;/p&gt;

&lt;p&gt;My numbers for the pinned evaluation repositories live in &lt;code&gt;docs/measurements.md&lt;/code&gt;, with a commit SHA beside each one (click 8.5.0 came to roughly half a million tokens), because a measurement against a moving branch is not a measurement and a blog post is the wrong place to freeze it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure before you design.&lt;/strong&gt; A few seconds of &lt;code&gt;len()&lt;/code&gt; will tell you whether the architecture you are imagining is even legal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibrate against the model you call.&lt;/strong&gt; Two samples, subtract, never a folk constant and never another vendor's tokenizer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep prices out of source.&lt;/strong&gt; Environment variables, defaulting to zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Print the meter on every run&lt;/strong&gt;, including free-tier runs. The habit is the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat selection as the feature.&lt;/strong&gt; The moment you accept the agent must choose what to read, tools stop being a convenience and become the product.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;If this was useful, a clap helps other people find it. I would like to know how other people size this up: do you estimate agent cost before you build, or find out from the first surprising bill?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All views in this post are my own. The scenarios are illustrative composites, and code examples are not drawn from any specific codebase.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>agents</category>
    </item>
    <item>
      <title>Day 02: An LLM is a stateless function. Everything else is you</title>
      <dc:creator>Harshavardhan Katkam</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:16:50 +0000</pubDate>
      <link>https://dev.to/harshavardhan_katkam/day-02-an-llm-is-a-stateless-function-everything-else-is-you-2m07</link>
      <guid>https://dev.to/harshavardhan_katkam/day-02-an-llm-is-a-stateless-function-everything-else-is-you-2m07</guid>
      <description>&lt;p&gt;&lt;em&gt;I pointed the forty-line version of cartographer at a second file and asked the obvious follow-up: how does this one relate to the last one?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It asked me which file I meant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My first instinct was that I had broken something. A flag I had dropped, a session id I had forgotten to thread through. I went looking for it for longer than I am comfortable admitting. There is no session id. There was never going to be one. The endpoint had no idea I had called it ninety seconds earlier, and no amount of configuration was going to give it one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every piece of memory you have ever seen in an AI product is somebody, somewhere, deciding what goes into an array before a POST. That is the entire mechanism. Once you have seen it you cannot unsee it, and a surprising amount of vocabulary stops being mysterious.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Code for this day:&lt;/strong&gt; &lt;a href="https://github.com/Harshavardhan17/cartographer/tree/day-02" rel="noopener noreferrer"&gt;cartographer at day-02&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model is &lt;strong&gt;a pure function of the array you send&lt;/strong&gt;. It retains nothing between requests, and nothing you can configure changes that.&lt;/li&gt;
&lt;li&gt;The forty-line version sent one message. Now it sends a list, and &lt;strong&gt;the caller owns that list&lt;/strong&gt; — no library, no service, no hidden field.&lt;/li&gt;
&lt;li&gt;Append the &lt;strong&gt;assistant's reply as well as the user's&lt;/strong&gt;. Omitting it produces a subtler bug than amnesia.&lt;/li&gt;
&lt;li&gt;Your agent now has &lt;strong&gt;exactly one piece of state&lt;/strong&gt;, and it is a JSON file. Debug it with &lt;code&gt;cat&lt;/code&gt; before you reach for anything else.&lt;/li&gt;
&lt;li&gt;Re-sending a transcript is &lt;strong&gt;quadratic&lt;/strong&gt;: ten equal turns bill you fifty-five turns' worth of input, not ten.&lt;/li&gt;
&lt;li&gt;"Memory" is a product word. &lt;strong&gt;Every implementation of it is a policy for what to put in the array&lt;/strong&gt;, and choosing that policy well is most of the rest of this series.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The follow-up that goes nowhere
&lt;/h2&gt;

&lt;p&gt;Here is the version I wrote first, and it is worth staring at because it looks correct.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;turn&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="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Broken. Do not ship this.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&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;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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A list of messages goes in. A reply comes out. The signature reads like a conversation.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;messages + [...]&lt;/code&gt; builds a throwaway list, sends it, and drops it. The caller's list is never touched, so every call starts from the same place. This survives a read-through precisely because the type annotation implies history — you have to notice that nothing ever grows.&lt;/p&gt;

&lt;p&gt;The endpoint this series uses is the OpenAI-shaped chat completions endpoint, and on that shape a request is self-contained. Whatever you did not put in the array did not happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  There is no session. There is a list.
&lt;/h2&gt;

&lt;p&gt;The fix is three lines and it is the whole idea of the day.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;turn&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="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ask&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="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;reply&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It mutates the list it was handed. That is deliberate: there is one transcript, the caller holds it, and every append happens in one function you can put a breakpoint in.&lt;/p&gt;

&lt;p&gt;The second append is the line people drop, and its failure mode is worse than no memory at all. Leave it out and the model receives a list of questions with no answers attached — a transcript in which it apparently never spoke. So it re-answers things it already answered, contradicts a conclusion it reached two turns ago, and generally behaves like a model that is bad at reasoning. You will spend an evening rewriting the system prompt, the standing instructions in the first message, before you find it.&lt;/p&gt;

&lt;p&gt;Making both appends the responsibility of one function is not style. It removes the state where only half the exchange got recorded.&lt;/p&gt;

&lt;p&gt;Around &lt;code&gt;turn&lt;/code&gt;, the rest of &lt;code&gt;src/cartographer/agent.py&lt;/code&gt; changes less than you would expect. Yesterday's template goes, replaced by &lt;code&gt;SYSTEM&lt;/code&gt;, the system message that opens every list and tells the model it is mapping a repository one file at a time and should refer back to earlier files by name. &lt;code&gt;ask&lt;/code&gt; keeps its body but now takes the whole list and sends it as the &lt;code&gt;messages&lt;/code&gt; field, so it no longer decides what the model sees. &lt;code&gt;main&lt;/code&gt; turns every path on the command line into one turn, then asks a closing question that only a model holding every earlier file can answer: which would you read first? &lt;code&gt;tests/test_agent.py&lt;/code&gt; is replaced by a test that swaps &lt;code&gt;ask&lt;/code&gt; for a fake and asserts both the order of roles and that the second call saw four messages. &lt;a href="https://github.com/Harshavardhan17/cartographer/blob/main/docs/devnotes/day-02.md" rel="noopener noreferrer"&gt;Today's dev notes&lt;/a&gt; walk through every line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your agent has exactly one piece of state
&lt;/h2&gt;

&lt;p&gt;Because the model is a pure function of that array, the array is the complete state of your program. So write it down:&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;TRANSCRIPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&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;dumps&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="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line ends &lt;code&gt;main&lt;/code&gt;, and &lt;code&gt;transcript.json&lt;/code&gt; goes into &lt;code&gt;.gitignore&lt;/code&gt;, because it holds whole source files and is rebuilt on every run. This is the highest-value five minutes of the day, and almost nobody's tutorial does it. When the output is wrong, the question you actually need answered is not &lt;em&gt;why did the model say that&lt;/em&gt;. It is &lt;em&gt;what did the model see&lt;/em&gt;. There is exactly one artifact that answers it, and now it is on disk.&lt;/p&gt;

&lt;p&gt;Nearly every bad answer I have chased at this stage has been visible in that file within thirty seconds: a file that was never appended, a source read as an empty string because the path was relative to the wrong directory, a system message clobbered by a later assignment. None of those are model problems, and all of them look like model problems until you open the transcript.&lt;/p&gt;

&lt;p&gt;It also makes runs replayable. The input is a JSON array; you can re-send it byte for byte, change one message, and send it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bill is a triangle, not a line
&lt;/h2&gt;

&lt;p&gt;Now the part that gets expensive. Every call carries the whole array, so you pay for turn one again on turn two, and again on turn three.&lt;/p&gt;

&lt;p&gt;If each turn adds roughly &lt;em&gt;T&lt;/em&gt; tokens, the chunks of text a model reads and bills by, the &lt;em&gt;n*th request carries about *nT&lt;/em&gt; input tokens, and the run as a whole costs &lt;em&gt;T·n(n+1)/2&lt;/em&gt;. Ten turns is fifty-five turns' worth of input. Twenty turns is two hundred and ten. The curve is a triangle, and you are billed for its area.&lt;/p&gt;

&lt;p&gt;For a chat assistant that is survivable, because a turn is a sentence. For an agent reading source files, each turn is an entire file, so &lt;em&gt;T&lt;/em&gt; is enormous and the triangle gets frightening at single-digit &lt;em&gt;n&lt;/em&gt;. That is a measurement problem before it is a design problem, which is what tomorrow is about.&lt;/p&gt;

&lt;p&gt;There are three honest strategies, and they trade against each other:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Send everything&lt;/th&gt;
&lt;th&gt;Summarise older turns&lt;/th&gt;
&lt;th&gt;Keep pointers, re-read on demand&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input cost over n turns&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;grows with the square&lt;/td&gt;
&lt;td&gt;flat once the cap bites&lt;/td&gt;
&lt;td&gt;flat, plus one call per lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What you give up&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the context window, then your budget&lt;/td&gt;
&lt;td&gt;detail you did not know mattered&lt;/td&gt;
&lt;td&gt;round trips and latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Right when&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;short runs, and while learning&lt;/td&gt;
&lt;td&gt;chat, where old turns are chatty&lt;/td&gt;
&lt;td&gt;the content is on disk and cheap to re-read&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a codebase the third column is clearly right — the repository is still sitting there, and re-reading a file costs a disk read. That is the direction this project goes, and it is why the agent eventually has to be able to fetch things for itself rather than being handed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pass the transcript as an argument.&lt;/strong&gt; The moment it becomes a hidden attribute on an object, you stop being able to see what you sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append both sides in one function.&lt;/strong&gt; Make the half-recorded exchange unrepresentable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dump the array to disk on every run&lt;/strong&gt;, from the first day. It is four lines and it is your only observability until much later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert on the sequence of roles in a test.&lt;/strong&gt; Mine checks that two turns produce system, user, assistant, user, assistant — it would have caught the broken version above immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not install a memory library&lt;/strong&gt; until you can name which column of that table you need. All three are a page of code; the hard part is the choice, and no library makes it for you.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;If this was useful, a clap helps other people find it. I am curious about the other side of this one: what is the longest-running agent conversation you have kept alive, and what did you eventually have to throw away to keep it affordable?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All views in this post are my own. The scenarios are illustrative composites, and code examples are not drawn from any specific codebase.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>agents</category>
    </item>
    <item>
      <title>Day 01: Why your first agent should be 40 lines, not a framework</title>
      <dc:creator>Harshavardhan Katkam</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:14:39 +0000</pubDate>
      <link>https://dev.to/harshavardhan_katkam/day-01-why-your-first-agent-should-be-40-lines-not-a-framework-53l6</link>
      <guid>https://dev.to/harshavardhan_katkam/day-01-why-your-first-agent-should-be-40-lines-not-a-framework-53l6</guid>
      <description>&lt;p&gt;&lt;em&gt;The first agent I ever got working, I did not understand. I had installed a framework, pasted eleven lines out of its quickstart, and watched a correct answer appear. Then I changed the prompt slightly and it printed nothing. Not an error. An empty string, exit code zero.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I spent the next forty minutes in a debugger. Nine frames down, past a callback manager and two abstract base classes, I found what I had been hunting: a dictionary getting built, and an HTTP POST. That was it. That was the whole thing the framework had been doing for me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The bug was not what bothered me. What bothered me was that I could not have guessed where to look, because I had never once seen the request. I had learned a vocabulary — chains, runnables, callbacks — sitting on top of a fact I had never been shown.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;So the first working version of cartographer, the codebase-mapping agent I am building over thirty days, has nothing in the way. One file, one dependency, forty lines.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Code for this day:&lt;/strong&gt; &lt;a href="https://github.com/Harshavardhan17/cartographer/tree/day-01" rel="noopener noreferrer"&gt;cartographer at day-01&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An agent is &lt;strong&gt;an HTTP POST in a loop&lt;/strong&gt;. Everything else is retries, ergonomics and scheduling.&lt;/li&gt;
&lt;li&gt;Frameworks abstract &lt;strong&gt;the part that was already easy&lt;/strong&gt; and hide the part you need visible when it breaks.&lt;/li&gt;
&lt;li&gt;Day one is &lt;strong&gt;forty lines&lt;/strong&gt;: read a file, post it, print the reply. &lt;code&gt;httpx&lt;/code&gt; is the only import that is not standard library.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;raise_for_status()&lt;/code&gt; before you touch the body, or &lt;strong&gt;every auth and rate-limit failure reaches you as &lt;code&gt;KeyError: 'choices'&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The reason to start raw is not purity. It is that &lt;strong&gt;you will end up reading a framework's source anyway&lt;/strong&gt;, and this is the map you read it with.&lt;/li&gt;
&lt;li&gt;The honest ending: it works, and &lt;strong&gt;the output is prose nobody can check&lt;/strong&gt;. That, not the absent framework, is the real problem.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  An agent is a POST. That is the entire secret.
&lt;/h2&gt;

&lt;p&gt;Strip every layer off and what reaches the network is one request with two fields that matter: a model id, and a list of messages, each one a role (&lt;code&gt;system&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt; or &lt;code&gt;assistant&lt;/code&gt;) plus its text. What comes back is JSON, and the part you want is buried at a fixed address inside it. Every model provider worth using speaks this shape, which is why one endpoint can front hundreds of models.&lt;/p&gt;

&lt;p&gt;That is not a simplification for teaching purposes. It is the actual protocol. The rest of this series — tools, protocols, graphs, memory, retrieval — is thirty days of arguing about &lt;strong&gt;which strings go into that array and in what order&lt;/strong&gt;. If you do not have the array clearly in your head, none of the later arguments have anywhere to land.&lt;/p&gt;

&lt;h2&gt;
  
  
  The forty lines
&lt;/h2&gt;

&lt;p&gt;I am running everything in this series through &lt;a href="https://openrouter.ai/docs" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;, one endpoint at &lt;code&gt;https://openrouter.ai/api/v1&lt;/code&gt; speaking the OpenAI wire protocol, on free models — so anyone can reproduce the whole thing for nothing. Nothing here is specific to that choice; change the URL and the model string and it works elsewhere.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;OPENROUTER_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://openrouter.ai/api/v1/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CARTOGRAPHER_MODEL&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;nvidia/nemotron-3-super-120b-a12b:free&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/cartographer/agent.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are reading one file from a repository you have never seen.

Say what this file is for, what it depends on, and what would break if it
were deleted. Where the file does not say, answer that you cannot tell.

--- {path} ---
{source}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;OPENROUTER_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&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;model&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;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="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;120.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cannot find &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&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="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TARGET&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all of &lt;code&gt;src/cartographer/agent.py&lt;/code&gt;, the one file today adds, and it reads top to bottom in four parts.&lt;/p&gt;

&lt;p&gt;The constants say where and what. &lt;code&gt;OPENROUTER_URL&lt;/code&gt; is the chat completions endpoint, the address that takes a list of messages and returns a reply. &lt;code&gt;MODEL&lt;/code&gt; reads the model id from an environment variable, with a default. &lt;code&gt;TARGET&lt;/code&gt; is the one file to read.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PROMPT&lt;/code&gt; is a template. &lt;code&gt;{path}&lt;/code&gt; and &lt;code&gt;{source}&lt;/code&gt; are placeholders that &lt;code&gt;.format()&lt;/code&gt; fills with the file's name and contents. Its instructions ask three concrete questions and, deliberately, permit "I cannot tell", because a model that is not allowed to say that fills the gap with something plausible.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ask&lt;/code&gt; is the model call. It posts a JSON body with exactly two fields, the model id and a one-message list, with the key in an &lt;code&gt;Authorization&lt;/code&gt; header and a two-minute timeout, because free models are slow. It checks the status, parses the reply, and digs the text out of &lt;code&gt;choices[0].message.content&lt;/code&gt;. The &lt;code&gt;str()&lt;/code&gt; around that lookup is for the type checker, which will not accept an untyped value from a function promising a string.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;main&lt;/code&gt; checks the file exists, fills the template, asks, and prints. The &lt;code&gt;if __name__&lt;/code&gt; guard runs &lt;code&gt;main&lt;/code&gt; only when the file is executed as a program, so importing it from a test sends nothing.&lt;/p&gt;

&lt;p&gt;The second new file, &lt;code&gt;tests/test_agent.py&lt;/code&gt;, checks without a network that the filled prompt contains the file's text and the permission to be uncertain. &lt;a href="https://github.com/Harshavardhan17/cartographer/blob/main/docs/devnotes/day-01.md" rel="noopener noreferrer"&gt;Today's dev notes&lt;/a&gt; explain every line, including the commands to run it.&lt;/p&gt;

&lt;p&gt;The target is hardcoded to the file you are looking at, so the agent's first job is to describe itself. That is a deliberately easy first test: you already know the right answer, so you can judge the output the moment it appears.&lt;/p&gt;

&lt;p&gt;The model id comes from the environment with a default rather than being pinned in source, because free-tier availability churns constantly. When one model is busy you swap it in your shell, not in a commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three failures a framework would have swallowed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No key.&lt;/strong&gt; &lt;code&gt;os.environ['OPENROUTER_API_KEY']&lt;/code&gt; raises &lt;code&gt;KeyError&lt;/code&gt; at the exact line that needed it. Subscript, not &lt;code&gt;.get()&lt;/code&gt;. A missing credential should not be allowed to become an empty string that travels three functions before failing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A bad key, or a busy model.&lt;/strong&gt; Without &lt;code&gt;raise_for_status()&lt;/code&gt;, a &lt;code&gt;401&lt;/code&gt; or a &lt;code&gt;429&lt;/code&gt; gives you a perfectly valid JSON body containing an error object and no &lt;code&gt;choices&lt;/code&gt; — so the failure surfaces as &lt;code&gt;KeyError: 'choices'&lt;/code&gt; from the last line of &lt;code&gt;ask&lt;/code&gt;, pointing at the one place where nothing is wrong. One method call converts that into an exception naming the status code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An empty answer.&lt;/strong&gt; &lt;code&gt;choices&lt;/code&gt; can come back as an empty list. Under raw HTTP you see that immediately, because you are the one doing the indexing. Under a framework you get &lt;code&gt;None&lt;/code&gt; from a helper and start wondering about your prompt.&lt;/p&gt;

&lt;p&gt;None of these are exotic. They are the first three things that happen to everybody, and all three are cheap to diagnose only because there is no layer between the traceback and the wire.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when is a framework right?
&lt;/h2&gt;

&lt;p&gt;Not never. It is right at the point where you are paying for something you have already built badly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Raw HTTP&lt;/th&gt;
&lt;th&gt;Provider SDK&lt;/th&gt;
&lt;th&gt;Agent framework&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;You own&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the request body&lt;/td&gt;
&lt;td&gt;the retry and typing layer&lt;/td&gt;
&lt;td&gt;almost nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;You see when it breaks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;status code and body&lt;/td&gt;
&lt;td&gt;a typed exception&lt;/td&gt;
&lt;td&gt;a stack you have not read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Costs you&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;boilerplate per feature&lt;/td&gt;
&lt;td&gt;portability across providers&lt;/td&gt;
&lt;td&gt;the shape of your program&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Right when&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;learning, or the loop &lt;em&gt;is&lt;/em&gt; the product&lt;/td&gt;
&lt;td&gt;one provider, production traffic&lt;/td&gt;
&lt;td&gt;you need durability, branching and resumption, and know why&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third column becomes correct later in this series, when the agent has to survive a crash halfway through a scan and resume where it stopped. Writing that yourself is a bad use of a month. But arriving there having never seen the request means you cannot debug it, and you will need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  It works, and I cannot tell if it is right
&lt;/h2&gt;

&lt;p&gt;Running it prints a fluent paragraph about &lt;code&gt;agent.py&lt;/code&gt;. It reads well. It is also completely unverifiable: there is no field to assert on, no count to compare, nothing a test can hold. Prose is unfalsifiable by construction, and a mapping tool whose output cannot be checked is a tool nobody should trust with a codebase they do not know.&lt;/p&gt;

&lt;p&gt;That gap is the honest state of the project tonight, and it is a more interesting problem than any missing framework. A framework would have produced the same unverifiable paragraph, faster, from behind a wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write the POST by hand once&lt;/strong&gt;, before installing anything. Half an hour, and it never stops paying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put &lt;code&gt;raise_for_status()&lt;/code&gt; in from the start.&lt;/strong&gt; It is the difference between a diagnosis and a hunt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the key with a subscript&lt;/strong&gt;, not &lt;code&gt;.get()&lt;/code&gt;. Fail at the boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point the first version at a file you already understand&lt;/strong&gt;, so bad output is obvious rather than plausible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adopt the framework the day you can name the feature you need from it&lt;/strong&gt; — resumption, branching, durability — and not one day earlier.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;If this was useful, a clap helps other people find it. I want to hear the other side too: what was the first thing that made you reach for an agent framework, and was it something you had already tried to build yourself?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All views in this post are my own. The scenarios are illustrative composites, and code examples are not drawn from any specific codebase.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>python</category>
    </item>
    <item>
      <title>Day 00a: Six ideas that explain every AI agent, before any code</title>
      <dc:creator>Harshavardhan Katkam</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:12:03 +0000</pubDate>
      <link>https://dev.to/harshavardhan_katkam/day-00a-six-ideas-that-explain-every-ai-agent-before-any-code-35c7</link>
      <guid>https://dev.to/harshavardhan_katkam/day-00a-six-ideas-that-explain-every-ai-agent-before-any-code-35c7</guid>
      <description>&lt;p&gt;&lt;em&gt;Somewhere in the first hour of reading about AI agents, you meet a diagram. A box labelled Thought, an arrow to Action, an arrow to Observation, and an arrow curling back to the start. The captions say the agent reasons, plans and decides. It makes building one sound like work for a research lab.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Then someone asks a plain question: when the agent "thinks", what does the model actually receive? The honest answer is a list of messages, written as JSON, sent in an ordinary web request. The reply is more JSON. Nothing in between remembers anything.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;That gap, between the vocabulary and the mechanism, is what this primer closes: six ideas, explained from zero, each with something you can see for yourself.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A model call is text in, text out&lt;/strong&gt;, over HTTP. It predicts likely text, which is why it can be fluently wrong.&lt;/li&gt;
&lt;li&gt;Models read and are priced in &lt;strong&gt;tokens&lt;/strong&gt;, chunks often smaller than a word.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The context window&lt;/strong&gt; is the most one call can carry. Anything outside it does not exist for the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model remembers nothing between calls.&lt;/strong&gt; Your code resends the conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calling does not let the model act.&lt;/strong&gt; It lets the model ask your code to act.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An agent is a loop&lt;/strong&gt; around a model call: ask, run any requested tool, send the result back, repeat until it answers. Your code owns the limits.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What you will learn.&lt;/strong&gt; The six ideas, tried for real where possible, and the vocabulary for the next thirty days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites.&lt;/strong&gt; None to read it. You have used a chat assistant, and you can read a little Python. The optional "Try it yourself" boxes need a macOS or Linux terminal (or WSL on Windows) with &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;python3&lt;/code&gt;, and a free &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; account: one web address that forwards requests to hundreds of models, some of them free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time.&lt;/strong&gt; About twenty-five minutes to read, fifteen more for the experiments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to read it.&lt;/strong&gt; Each idea has the same parts: plain words, an everyday picture, what goes over the wire, the common mistake, why it matters. At "Predict, then check", decide before opening the reveal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get a free OpenRouter key
&lt;/h3&gt;

&lt;p&gt;The experiments need an &lt;strong&gt;API key&lt;/strong&gt;, a secret string that tells OpenRouter which account is asking. Getting one takes two minutes and no payment.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;openrouter.ai&lt;/a&gt; and sign up, with an email address or one of the sign-in buttons it offers.&lt;/li&gt;
&lt;li&gt;Open &lt;a href="https://openrouter.ai/keys" rel="noopener noreferrer"&gt;openrouter.ai/keys&lt;/a&gt; and click &lt;strong&gt;Create Key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name it something you will recognise, such as &lt;code&gt;cartographer&lt;/code&gt;. Leave the credit limit empty; &lt;code&gt;:free&lt;/code&gt; models cost nothing.&lt;/li&gt;
&lt;li&gt;Copy the key at once. It is shown only one time.&lt;/li&gt;
&lt;li&gt;Free models need no payment, but they are rate-limited: a busy one may ask you to wait.&lt;/li&gt;
&lt;li&gt;Never paste the key into a file, a chat or a screenshot. Load it straight into your terminal instead:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-rs&lt;/span&gt; &lt;span class="s2"&gt;"?OpenRouter key: "&lt;/span&gt; k &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;unset &lt;/span&gt;k
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the key when asked; nothing shows as you paste. That is zsh, the macOS default; in bash, start with &lt;code&gt;read -rsp "OpenRouter key: " k&lt;/code&gt;. The key now sits in an &lt;strong&gt;environment variable&lt;/strong&gt;, a named value your terminal hands to the commands it runs, never printed and never saved. It lasts until you close that terminal, and Day 01 loads it the same way.&lt;/p&gt;

&lt;p&gt;If the key ever leaks, delete it on the Keys page and create a new one. The old one stops working the moment you delete it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The picture the word suggests
&lt;/h2&gt;

&lt;p&gt;The word "agent" suggests an assistant that remembers what you told it, looks at your files, and goes off and does things. Each part of that picture is wrong in a way that decides how you build one. It does not remember: your code resends everything. It cannot see your files, only the text you send. It cannot act: it can only ask, and your code acts. The six ideas below replace the picture one piece at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 1: A model call is text in, text out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In plain words.&lt;/strong&gt; Your code sends an HTTP request to a &lt;strong&gt;provider&lt;/strong&gt;, the service that runs the model, holding a model name and a list of messages. The useful part of the response is one field: the generated text. The model does not browse, run code or open files. It returns the text it predicts should come next, one small piece at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An everyday picture.&lt;/strong&gt; The word suggestions above your phone keyboard: the same job, guessing what comes next, at an enormously larger scale.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ph4i4dcs0aq34vqqlij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ph4i4dcs0aq34vqqlij.png" alt=" " width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the wire.&lt;/strong&gt; A complete request. Each message has a &lt;strong&gt;role&lt;/strong&gt; saying who wrote it: &lt;code&gt;user&lt;/code&gt; for you, &lt;code&gt;assistant&lt;/code&gt; for the model, &lt;code&gt;system&lt;/code&gt; for standing instructions at the top.&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nvidia/nemotron-3-super-120b-a12b:free"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name one Python web framework, in one word."&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="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;The response, trimmed to the fields that matter. The shape is real; your wording and numbers will differ.&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;"choices"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Flask"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"finish_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stop"&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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usage"&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;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;number&amp;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;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;number&amp;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;"total_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;number&amp;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="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;Three fields carry everything. &lt;code&gt;choices[0].message.content&lt;/code&gt; is the answer: the first entry of &lt;code&gt;choices&lt;/code&gt;, its &lt;code&gt;message&lt;/code&gt;, its &lt;code&gt;content&lt;/code&gt;. &lt;code&gt;finish_reason&lt;/code&gt; says why it stopped: &lt;code&gt;stop&lt;/code&gt; means finished, &lt;code&gt;length&lt;/code&gt; means cut off at a size limit. &lt;code&gt;usage&lt;/code&gt; counts tokens, Idea 2.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Try it yourself.&lt;/strong&gt; One real model call. Paste it into the terminal where you loaded your key.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://openrouter.ai/api/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "nvidia/nemotron-3-super-120b-a12b:free", "messages": [{"role": "user", "content": "Name one Python web framework, in one word."}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;curl -s &amp;lt;url&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sends an HTTP request to the endpoint, quietly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-H "Authorization: Bearer ..."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A header carrying your key, read from the environment variable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-H "Content-Type: application/json"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Says the body is JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-d '{...}'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The body: the model name and the messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python3 -m json.tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prints the reply neatly; it changes nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Predict, then check.&lt;/strong&gt; How much of what comes back will be the answer itself?&lt;/p&gt;

&lt;p&gt;Check your prediction&lt;br&gt;
One word, inside a page of bookkeeping: an id, the model name, the finish reason, the token counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you should see.&lt;/strong&gt; After a few seconds, JSON with a framework name in &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;finish_reason&lt;/code&gt; set to &lt;code&gt;stop&lt;/code&gt;, and three counts under &lt;code&gt;usage&lt;/code&gt;. A &lt;code&gt;reasoning&lt;/code&gt; field, if present, is not the answer. If you see an error instead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You see&lt;/th&gt;
&lt;th&gt;It means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The key is missing or wrong in this terminal; run the &lt;code&gt;read&lt;/code&gt; line again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;429&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The free model is busy; wait a little and retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The model id is unknown or unavailable&lt;/td&gt;
&lt;td&gt;Free models come and go; swap in any id ending in &lt;code&gt;:free&lt;/code&gt; from &lt;a href="https://openrouter.ai/models" rel="noopener noreferrer"&gt;openrouter.ai/models&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake.&lt;/strong&gt; Treating a fluent answer as a checked one. The model produces what is &lt;em&gt;likely&lt;/em&gt;, not what is &lt;em&gt;verified&lt;/em&gt;, so when it does not know, it still produces something likely. A confident, plausible, wrong answer is &lt;strong&gt;hallucination&lt;/strong&gt;. No prompt removes it entirely. &lt;strong&gt;Grounding&lt;/strong&gt;, making the model answer from text you gave it, reduces it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why it matters for agents.&lt;/strong&gt; Everything an agent does is built from this call, and grounding is why cartographer, the agent this series builds, only cites code it has read off disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 2: Tokens are the unit everything is measured in
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In plain words.&lt;/strong&gt; Models do not read characters or words. Text is split into &lt;strong&gt;tokens&lt;/strong&gt;: often a whole short word, sometimes a fragment of a long one, sometimes one punctuation mark. The model reads and writes tokens, and paid providers charge per token both ways. The splitter is the model's &lt;strong&gt;tokenizer&lt;/strong&gt;, and different models split the same text differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An everyday picture.&lt;/strong&gt; The data on a phone plan: what you pay for, and what runs out. Tokens are both the price and the limit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F289w8s4ohhlxvz10frgz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F289w8s4ohhlxvz10frgz.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the wire.&lt;/strong&gt; You send text; the provider reports the cost in &lt;code&gt;usage&lt;/code&gt;: &lt;code&gt;prompt_tokens&lt;/code&gt; for what you sent, &lt;code&gt;completion_tokens&lt;/code&gt; for what the model wrote, &lt;code&gt;total_tokens&lt;/code&gt; for the sum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A worked example by hand.&lt;/strong&gt; A tokenizer might keep "the" whole and cut "unbelievably" into pieces like "un", "believ" and "ably"; where it cuts depends on the model. So eight words need not be eight tokens, and code, with its brackets and indentation, usually costs more than its word count suggests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predict, then check.&lt;/strong&gt; Your question in the first experiment had eight words. Will &lt;code&gt;prompt_tokens&lt;/code&gt; say 8?&lt;/p&gt;

&lt;p&gt;Check your prediction&lt;br&gt;
Very likely not. Words and tokens do not line up one to one, and the provider also counts the formatting that wraps your message. Run the experiment again and read the real number.&lt;/p&gt;

&lt;p&gt;To watch a tokenizer cut text live, try &lt;a href="https://tiktokenizer.vercel.app/" rel="noopener noreferrer"&gt;Tiktokenizer&lt;/a&gt;; its counts are for particular models, so they illustrate, not predict.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake.&lt;/strong&gt; Estimating cost or size in words. Providers bill and limit in tokens, and only &lt;code&gt;usage&lt;/code&gt; tells you the true count.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why it matters for agents.&lt;/strong&gt; An agent makes many calls, each billed and limited in tokens. Day 03 measures a real repository instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 3: The context window is one page
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In plain words.&lt;/strong&gt; Every model has a &lt;strong&gt;context window&lt;/strong&gt;: the most tokens one request can hold, your messages and the reply together. It is not memory and not storage. It is the size of the one page the model sees, and anything off that page does not exist for that call. Sizes differ by model, so check yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An everyday picture.&lt;/strong&gt; An exam where you bring one sheet of notes and write your answer on the same sheet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhiassmz4jsnfmt13pv6k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhiassmz4jsnfmt13pv6k.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A worked example by hand.&lt;/strong&gt; Toy numbers, chosen for easy arithmetic, not taken from any real model. Pretend the window is 100 tokens.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What is on the page&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Running total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instructions (the system message)&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One source file&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your question&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Room left for the reply&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Add a second 60-token file and the input alone is 145: it does not fit, and the request fails with an error that no rewording fixes. Keep one file but ask for a long explanation, and the reply runs out of room after 15 tokens, stopping mid-sentence with &lt;code&gt;finish_reason&lt;/code&gt; set to &lt;code&gt;length&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake.&lt;/strong&gt; Thinking a better prompt fixes a full window. The problem is arithmetic, so the fix is sending less: a slice instead of the whole file, a summary, only the files that matter. Chat apps may quietly drop your oldest messages to make room, which is why a very long chat seems to forget its start.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why it matters for agents.&lt;/strong&gt; A real codebase is far bigger than any page, so an agent that reads code must choose what goes on it. That choice is most of what this series builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 4: The model remembers nothing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In plain words.&lt;/strong&gt; Send two requests in a row and the second knows nothing about the first; the endpoint keeps no conversation for you. A chat feels continuous because your code keeps the conversation as a list and resends all of it, plus the new message, every time. The model is not remembering. It is rereading. This is what &lt;strong&gt;stateless&lt;/strong&gt; means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An everyday picture.&lt;/strong&gt; A help line where a different person answers every call and nobody keeps notes. To continue, you read the whole conversation out again, every time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10f3axy31731qbhigbb5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10f3axy31731qbhigbb5.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predict, then check.&lt;/strong&gt; Three separate requests: the first tells the model a name, the second asks for it back, the third sends the whole conversation in one list. What will the second answer?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Try it yourself: prove it forgets.&lt;/strong&gt; Three requests, each printing only the answer.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://openrouter.ai/api/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "nvidia/nemotron-3-super-120b-a12b:free", "messages": [{"role": "user", "content": "My name is Asha. Reply only with OK."}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['choices'][0]['message']['content'])"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://openrouter.ai/api/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "nvidia/nemotron-3-super-120b-a12b:free", "messages": [{"role": "user", "content": "What is my name?"}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['choices'][0]['message']['content'])"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://openrouter.ai/api/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "nvidia/nemotron-3-super-120b-a12b:free", "messages": [{"role": "user", "content": "My name is Asha. Reply only with OK."}, {"role": "assistant", "content": "OK"}, {"role": "user", "content": "What is my name?"}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['choices'][0]['message']['content'])"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you should see&lt;br&gt;
The first replies OK. The second says some version of "I don't know your name". The third says Asha. Wording varies; the pattern does not. Nothing changed between the second and third except what you sent. If the second names Asha, it guessed: rerun it. If a command prints a KeyError, the request failed: swap the last line for python3 -m json.tool to see the error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A worked example by hand.&lt;/strong&gt; Toy numbers again: every question is 10 tokens, every answer 20.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Turn&lt;/th&gt;
&lt;th&gt;What your code sends&lt;/th&gt;
&lt;th&gt;Input tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Q1, A1, Q2&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Q1, A1, Q2, A2, Q3&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You typed 30 tokens of questions but sent 120 tokens of input, and the gap widens every turn.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake.&lt;/strong&gt; "But my chat app remembers me." The app does, not the model: it saves notes and puts them back into the request. Some providers offer APIs that store a conversation for you, but the model still receives all of it on every call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why it matters for agents.&lt;/strong&gt; An agent's working memory is a list your code owns: what goes back in, what gets cut, what it costs. Day 02 builds it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 5: Tool calling lets the model ask, not act
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In plain words.&lt;/strong&gt; If the model only produces text, how does an agent read a file? In the request, you describe functions it may ask for, each a &lt;strong&gt;tool&lt;/strong&gt; with a name, a description and its arguments (its &lt;strong&gt;tool schema&lt;/strong&gt;). The model can then reply with a &lt;strong&gt;tool call&lt;/strong&gt;, a structured request to run one, instead of an answer. Your code runs it and sends the result back as a new message. The model asked; your program acted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An everyday picture.&lt;/strong&gt; A doctor on the phone says "take your temperature and tell me the number". You hold the thermometer and report back. The doctor only asked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpz17lo2szg5h8nsfx8er.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpz17lo2szg5h8nsfx8er.png" alt=" " width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the wire.&lt;/strong&gt; The request gains a &lt;code&gt;tools&lt;/code&gt; list. One entry, in the format Day 06 uses:&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;"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;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"function"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read_file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read one file from the repository and return its text."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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;"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;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"path"&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;"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;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Path relative to the repository root."&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required"&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="s2"&gt;"path"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the model wants it, &lt;code&gt;content&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;finish_reason&lt;/code&gt; is &lt;code&gt;tool_calls&lt;/code&gt;, and the message carries the request:&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;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&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;"tool_calls"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"function"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read_file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;src/app.py&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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="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="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;Your code appends that message, runs &lt;code&gt;read_file&lt;/code&gt;, and appends the result with the role &lt;code&gt;tool&lt;/code&gt;, tied to the request by its id:&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;the text of src/app.py&amp;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;Then it sends everything again. The id here is made up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predict, then check.&lt;/strong&gt; Look at &lt;code&gt;arguments&lt;/code&gt;. Is it a JSON object?&lt;/p&gt;

&lt;p&gt;Check your prediction&lt;br&gt;
No. It is a string containing JSON, written by the model like any other text. Your code must parse it and expect it to be malformed, name a missing file, or ask for a tool you never described.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake.&lt;/strong&gt; Saying the model "ran" a tool. It can only request what you described; nothing runs unless your code runs it. One reply can request several tools, and each gets its own result message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why it matters for agents.&lt;/strong&gt; This is where the control sits: the tools you describe are all the agent can do, and checking each request is your job. Day 06 makes this round trip by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 6: An agent is a loop
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In plain words.&lt;/strong&gt; Put the last two ideas together and you have an agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send the conversation and the list of available tools.&lt;/li&gt;
&lt;li&gt;If the reply is an answer, stop.&lt;/li&gt;
&lt;li&gt;If the reply is a tool request, run it, append the result to the conversation, and go back to step 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the whole of it. The model chooses the next step. Your code decides the rest: which tools exist, how many times the loop may run, what happens at that limit. Each pass is a &lt;strong&gt;turn&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxgkzri1yfto3vrgrjm1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxgkzri1yfto3vrgrjm1.png" alt=" " width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An everyday picture.&lt;/strong&gt; The phone doctor asks for your temperature, then about your throat, then diagnoses. You decide how many tests you will do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the wire: one run, traced by hand.&lt;/strong&gt; An agent with &lt;code&gt;list_files&lt;/code&gt; and &lt;code&gt;read_file&lt;/code&gt; tools is asked for a project's entry point.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Turn&lt;/th&gt;
&lt;th&gt;Your code sends&lt;/th&gt;
&lt;th&gt;The model replies&lt;/th&gt;
&lt;th&gt;Your code then&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;system, your task, the tools&lt;/td&gt;
&lt;td&gt;tool call: &lt;code&gt;list_files&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;runs it, appends the file list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;all of the above, plus that result&lt;/td&gt;
&lt;td&gt;tool call: &lt;code&gt;read_file&lt;/code&gt; on one file&lt;/td&gt;
&lt;td&gt;runs it, appends the file's text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;all of the above, plus that result&lt;/td&gt;
&lt;td&gt;an answer, &lt;code&gt;finish_reason&lt;/code&gt; &lt;code&gt;stop&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;stops and prints it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every turn sends everything before it: Idea 4, inside the loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predict, then check.&lt;/strong&gt; What happens if the model never replies with an answer, and keeps asking for one more file?&lt;/p&gt;

&lt;p&gt;Check your prediction&lt;br&gt;
Nothing in the model stops it. The loop runs, spending tokens, until your code stops it, which is why every real loop has a ceiling, such as a maximum number of turns.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake.&lt;/strong&gt; Reading "the agent decided" as something beyond this loop. The model returned a tool request and the loop ran it. Thought, Action, Observation is this loop drawn with bigger words.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why it matters for agents.&lt;/strong&gt; A loop the model can keep alive needs a limit you control. Day 06 builds the loop with two such limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an agent is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a chatbot.&lt;/strong&gt; A chat window is an interface. Some chat apps run a tool loop behind it, and the loop is what makes the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a workflow.&lt;/strong&gt; A workflow is a fixed sequence your code decides in advance, and it is often the better choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a smarter model.&lt;/strong&gt; It is the same model, with a loop and tools around it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The question that sorts them is who decides the next step:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Who decides the next step&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;th&gt;Breaks when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chat model&lt;/td&gt;
&lt;td&gt;You, every turn&lt;/td&gt;
&lt;td&gt;A single question&lt;/td&gt;
&lt;td&gt;The task needs many steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow&lt;/td&gt;
&lt;td&gt;Your code, fixed in advance&lt;/td&gt;
&lt;td&gt;Predictable, repeatable jobs&lt;/td&gt;
&lt;td&gt;The path depends on what it finds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;The model, inside a loop you bound&lt;/td&gt;
&lt;td&gt;The next step depends on the last result&lt;/td&gt;
&lt;td&gt;You cannot afford it to explore&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reading an unfamiliar codebase is the third kind: you cannot know the second file to open until you have read the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Words you will meet in the next thirty days
&lt;/h2&gt;

&lt;p&gt;Every later post defines its terms again, in this wording.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Built on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;The program that predicts text; you reach it over the network&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;The service that runs models and answers your requests&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;The web address a request is sent to&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API key&lt;/td&gt;
&lt;td&gt;A secret string that identifies your account; never write it in a file&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message&lt;/td&gt;
&lt;td&gt;One entry in the conversation: a role plus its text&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role&lt;/td&gt;
&lt;td&gt;Who wrote a message: &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;assistant&lt;/code&gt; or &lt;code&gt;tool&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt&lt;/td&gt;
&lt;td&gt;The text you send for the model to respond to&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System prompt&lt;/td&gt;
&lt;td&gt;Standing instructions in the &lt;code&gt;system&lt;/code&gt; message at the top&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination&lt;/td&gt;
&lt;td&gt;A fluent answer that is not backed by anything the model was given&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grounding&lt;/td&gt;
&lt;td&gt;Making the model answer from text you supplied&lt;/td&gt;
&lt;td&gt;Day 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transcript&lt;/td&gt;
&lt;td&gt;The conversation list your code keeps and resends&lt;/td&gt;
&lt;td&gt;Day 02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateless&lt;/td&gt;
&lt;td&gt;Keeps nothing between calls&lt;/td&gt;
&lt;td&gt;Day 02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token&lt;/td&gt;
&lt;td&gt;The chunk of text a model reads, writes and is billed in&lt;/td&gt;
&lt;td&gt;Day 03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context window&lt;/td&gt;
&lt;td&gt;The most tokens one request and its reply can hold&lt;/td&gt;
&lt;td&gt;Day 03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;Making the model reply in a fixed JSON shape you can check&lt;/td&gt;
&lt;td&gt;Day 04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON Schema&lt;/td&gt;
&lt;td&gt;A standard way to describe the shape a piece of JSON must have&lt;/td&gt;
&lt;td&gt;Day 04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temperature&lt;/td&gt;
&lt;td&gt;A setting that controls how much randomness goes into picking each token&lt;/td&gt;
&lt;td&gt;Day 05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool&lt;/td&gt;
&lt;td&gt;A function you describe to the model by name, description and arguments&lt;/td&gt;
&lt;td&gt;Day 06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool call&lt;/td&gt;
&lt;td&gt;The model's structured request to run one of your tools&lt;/td&gt;
&lt;td&gt;Day 06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turn&lt;/td&gt;
&lt;td&gt;One pass of the agent loop: send, reply, act&lt;/td&gt;
&lt;td&gt;Day 06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;td&gt;Model Context Protocol, a standard way to offer tools to many agents&lt;/td&gt;
&lt;td&gt;Day 09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph&lt;/td&gt;
&lt;td&gt;An agent written as named steps and the edges between them&lt;/td&gt;
&lt;td&gt;Day 15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Everything the agent has accumulated so far, in one place&lt;/td&gt;
&lt;td&gt;Day 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint&lt;/td&gt;
&lt;td&gt;A saved copy of state, so a run can resume after a crash&lt;/td&gt;
&lt;td&gt;Day 19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human in the loop&lt;/td&gt;
&lt;td&gt;Pausing the agent for a person to approve a step&lt;/td&gt;
&lt;td&gt;Day 20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;What is kept beyond one conversation, and how it is put back in&lt;/td&gt;
&lt;td&gt;Day 22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;Fetching only the relevant text, so it fits on the page&lt;/td&gt;
&lt;td&gt;Day 23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eval&lt;/td&gt;
&lt;td&gt;A repeatable test of the agent's answers&lt;/td&gt;
&lt;td&gt;Day 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trace&lt;/td&gt;
&lt;td&gt;A record of every call and tool run in one agent run&lt;/td&gt;
&lt;td&gt;Day 26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrail&lt;/td&gt;
&lt;td&gt;A limit on what the agent is allowed to do&lt;/td&gt;
&lt;td&gt;Day 29&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And a translation for what you will read elsewhere:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;When someone says&lt;/th&gt;
&lt;th&gt;It means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"The agent thought about it"&lt;/td&gt;
&lt;td&gt;The model generated text in reply to the conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The agent decided to read a file"&lt;/td&gt;
&lt;td&gt;The model returned a tool call, and the loop ran it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The agent remembers"&lt;/td&gt;
&lt;td&gt;Your code resent earlier messages, or put saved notes back into the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The agent forgot"&lt;/td&gt;
&lt;td&gt;That text was not in the request it was sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The agent used a tool"&lt;/td&gt;
&lt;td&gt;Your code ran a function the model asked for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The agent observed the result"&lt;/td&gt;
&lt;td&gt;Your code appended the tool's output as a message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The context is full"&lt;/td&gt;
&lt;td&gt;The request plus the reply would exceed the window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"It hallucinated"&lt;/td&gt;
&lt;td&gt;It produced likely text that nothing it was given supports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Thought, Action, Observation"&lt;/td&gt;
&lt;td&gt;Reply, tool call, tool result: the same loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where each idea comes back
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Idea&lt;/th&gt;
&lt;th&gt;Where you build it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Text in, text out&lt;/td&gt;
&lt;td&gt;Day 01, the raw call; Day 04, making the text checkable; Day 05, how each next piece is picked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Tokens&lt;/td&gt;
&lt;td&gt;Day 03, measuring a real repository and a cost meter; Day 27, cost control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. The context window&lt;/td&gt;
&lt;td&gt;Day 03, the hard wall; Day 23, retrieval as a tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. The model remembers nothing&lt;/td&gt;
&lt;td&gt;Day 02, the transcript; Day 22, memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Tool calling&lt;/td&gt;
&lt;td&gt;Day 06, by hand; Day 08, tool descriptions; Days 09 to 13, MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. The agent loop&lt;/td&gt;
&lt;td&gt;Day 06, the loop and its limits; Days 15 to 21, the loop as a graph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Answer each before opening it. If one will not come, reread that idea.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; A model confidently describes a function that does not exist. Why, and what reduces it?&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
It produces likely text, not checked text (Idea 1). Give it the real source to answer from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; Your question is twelve words. Will prompt_tokens be 12?&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
Almost certainly not. Tokens are not words, and formatting counts too (Idea 2).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; A toy window holds 100 tokens. Instructions 15, a file 70, a question 10. How much room is left for the reply, and what if it needs more?&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
5 tokens. The reply is cut off with finish_reason length (Idea 3). Send less.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.&lt;/strong&gt; Turn two of a chat clearly "remembers" turn one. Where does that memory live?&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
In your program's list, resent with every request (Idea 4).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5.&lt;/strong&gt; Questions are 10 tokens, answers 20. How many input tokens does turn three send?&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
70: two questions, two answers, the new question (Idea 4).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6.&lt;/strong&gt; An agent "read a file". Who opened it?&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
Your code, after the model returned a tool call (Idea 5).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.&lt;/strong&gt; What is in a tool call's arguments field, and why check it?&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
A string of JSON the model wrote; it can be malformed or name things that do not exist (Idea 5).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8.&lt;/strong&gt; What stops an agent loop running forever, and who owns it?&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
A limit in your code, such as a maximum number of turns (Idea 6).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9.&lt;/strong&gt; What one question separates a chatbot, a workflow and an agent?&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
Who decides the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;"The agent decided" means the model returned a tool request and the loop ran it.&lt;/li&gt;
&lt;li&gt;When something "forgets", look at the list it was sent.&lt;/li&gt;
&lt;li&gt;When something is too big, it is a context-window problem; no prompt fixes it.&lt;/li&gt;
&lt;li&gt;When an answer is confidently wrong, ask what text it was given.&lt;/li&gt;
&lt;li&gt;Trust the &lt;code&gt;usage&lt;/code&gt; block, not an estimate.&lt;/li&gt;
&lt;li&gt;Your code owns the ceiling on every loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Day 01 writes the first agent: the request from Idea 1, sent from about forty lines of Python, reading the same &lt;code&gt;content&lt;/code&gt; field. &lt;a href="https://github.com/Harshavardhan17/cartographer/blob/main/docs/devnotes/day-00a.md" rel="noopener noreferrer"&gt;Today's dev notes&lt;/a&gt; list what Day 01 needs on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://openrouter.ai/docs/api-reference/overview" rel="noopener noreferrer"&gt;OpenRouter API reference&lt;/a&gt;: the fields used above.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://openrouter.ai/docs/guides/features/tool-calling" rel="noopener noreferrer"&gt;OpenRouter tool calling&lt;/a&gt;: the full round trip.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://openrouter.ai/docs/api-reference/errors" rel="noopener noreferrer"&gt;OpenRouter error codes&lt;/a&gt;: every status code.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Building effective agents&lt;/a&gt;: workflows versus agents.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Which of these six surprised you, or which one did the explainers you have read get wrong? It tells me which idea needs more room in the posts that follow.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All views in this post are my own. The scenarios are illustrative composites, and code examples are not drawn from any specific codebase.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>python</category>
    </item>
    <item>
      <title>Day 00: Learn AI agents by building one, in 30 days</title>
      <dc:creator>Harshavardhan Katkam</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:03:46 +0000</pubDate>
      <link>https://dev.to/harshavardhan_katkam/day-00-learn-ai-agents-by-building-one-in-30-days-28fl</link>
      <guid>https://dev.to/harshavardhan_katkam/day-00-learn-ai-agents-by-building-one-in-30-days-28fl</guid>
      <description>&lt;p&gt;&lt;em&gt;Someone hands you an empty file and says: build an agent. Perhaps you have read the explainers and can recite that an agent is "an LLM in a loop with tools". Perhaps you have read none of them, and the word only means software that does things on its own. Either way the cursor blinks, and you do not know what the first line should be.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Most people in that spot open a framework's quickstart. Three lines later something impressive happens, and the interesting part — the loop, the decision about what to send, the thing that goes wrong at scale — happened inside a function you never opened. That is not your fault. Almost everything written about agents demonstrates a library, so what it teaches is vocabulary rather than mechanism.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is a thirty-day path to closing that gap. One concept a day, each one built by hand before any framework touches it, all of it assembling into a single real tool by the end. You will write the agent loop before you use a framework that hides it, and by the time you do adopt one you will be able to say what it is doing for you.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thirty days, one concept each&lt;/strong&gt;, from a bare model call to a production-shaped agent with evaluation and cost control.&lt;/li&gt;
&lt;li&gt;Everything is built &lt;strong&gt;by hand first&lt;/strong&gt;. Week one uses no SDK and no framework — forty lines of Python over raw HTTP.&lt;/li&gt;
&lt;li&gt;It all assembles into one real tool: &lt;strong&gt;cartographer&lt;/strong&gt;, an agent that reads a codebase you have never seen and tells you where to start.&lt;/li&gt;
&lt;li&gt;Every day ships &lt;strong&gt;build instructions you can follow&lt;/strong&gt;, and the code is public: &lt;a href="https://github.com/Harshavardhan17/cartographer" rel="noopener noreferrer"&gt;github.com/Harshavardhan17/cartographer&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Every model call runs on a &lt;strong&gt;free tier&lt;/strong&gt;. Following along costs nothing.&lt;/li&gt;
&lt;li&gt;Day 25 scores the finished agent against repositories with &lt;strong&gt;known-good answers&lt;/strong&gt;. If it turns out to be bad at this, that is the post.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why build one thing instead of thirty demos
&lt;/h2&gt;

&lt;p&gt;Most tutorials teach agent concepts with a fresh toy per concept: a weather tool for tool calling, a PDF chatbot for retrieval. Each one works, and none of them teach you much, because a toy cannot push back. You never hit the wall that makes the concept necessary — so you learn the API and not the reason.&lt;/p&gt;

&lt;p&gt;So this series builds a single tool across all thirty days, chosen because it fights you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cartographer&lt;/strong&gt; points at a repository you have never seen and returns an onboarding map: where execution actually begins, which small fraction of the files carry the system, one data path traced end to end, and where your first change would go. Everyone who has joined a team knows the week this replaces.&lt;/p&gt;

&lt;p&gt;It is a good teacher because of one property: &lt;strong&gt;a repository does not fit&lt;/strong&gt;. A mid-sized open-source project runs to several million tokens, the chunks of text a model reads and is billed by. There is no context window, the most one request can hold, large enough, no prompt clever enough, and no bigger model next year that makes it go away. The agent must decide what to read.&lt;/p&gt;

&lt;p&gt;That single constraint turns every concept in the curriculum from a demonstration into a necessity. Tool calling becomes the only way in. Retrieval stops being a stage you were told to add. Cost control becomes arithmetic. You meet each idea at the moment you need it, which is the only way any of it sticks.&lt;/p&gt;

&lt;p&gt;It has a second useful property: &lt;strong&gt;there is a right answer&lt;/strong&gt;. Point it at a codebase you know well and you can check the map, which is what makes a real evaluation possible on Day 25 instead of a vibe-based "seems to work".&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build, week by week
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Week one: the model, with nothing in front of it.&lt;/strong&gt; The agent loop in about forty lines of Python over raw HTTP, with no SDK and no agent library. You will handle the field that separates a finished answer from a truncated one, build conversation memory by hand and see why "memory" is a misleading word, do the token arithmetic that governs every design decision later, make the model return typed records instead of prose, and hand-roll tool calling so you have seen exactly what crosses the wire. By Friday you have a working agent and a clear list of everything wrong with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week two: MCP.&lt;/strong&gt; The protocol that lets any editor or client talk to your agent instead of it living inside one script. This is a good moment to learn it, because MCP removed its initialization handshake and protocol-level sessions in the &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/changelog" rel="noopener noreferrer"&gt;2026-07-28 revision&lt;/a&gt; — which means a large share of what is currently written about how MCP works describes a protocol that no longer exists. You will learn the version that is true today, build a server worth keeping, and handle the security question that arrives the moment an agent reads code it did not write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week three: LangGraph, and why a loop is not enough.&lt;/strong&gt; Reading a codebase is not a pipeline. You read a file, fail to understand it, and go back for more context — that is a cycle, and a cycle is the thing a chain cannot express. You will also add the two features that turn a script into software: surviving a crash four thousand files into a scan, and stopping to ask a human before doing something consequential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week four: the half that decides whether it is real.&lt;/strong&gt; Memory as three separate problems rather than one word. Retrieval as a tool the agent chooses to call. An evaluation set with known-good answers, so "did it get worse?" has an answer. Tracing, cost routing, and the seven ways this breaks in production: the part most tutorials skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to follow along
&lt;/h2&gt;

&lt;p&gt;Each day has two pieces: a post explaining the concept and why it matters, and &lt;a href="https://github.com/Harshavardhan17/cartographer/blob/main/docs/devnotes/day-00.md" rel="noopener noreferrer"&gt;dev notes&lt;/a&gt; that say which file to create in which folder, what to type into it, and what every line does.&lt;/p&gt;

&lt;p&gt;Today's build has no agent code. It creates the repository, and every file in it has a job. &lt;code&gt;pyproject.toml&lt;/code&gt; names the project, its Python version and its one runtime dependency. &lt;code&gt;src/cartographer/__init__.py&lt;/code&gt; makes the folder an importable package and holds the version. &lt;code&gt;.gitignore&lt;/code&gt; keeps the environment, caches and any secrets out of history. &lt;code&gt;LICENSE&lt;/code&gt; makes the code reusable. &lt;code&gt;README.md&lt;/code&gt; says what the tool will do and admits nothing works yet. &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; lints, type-checks and tests every push, and &lt;code&gt;tests/test_package.py&lt;/code&gt; is one real test. It also clones click 8.5.0, the repository the agent studies all series, pinned so every day measures the same code.&lt;/p&gt;

&lt;p&gt;You need Python 3.12, a terminal, and about an hour a day. Every model call runs against free models through one OpenAI-compatible endpoint, so there is no bill.&lt;/p&gt;

&lt;p&gt;You do not need prior agent experience. If agents are new to you, read the primer first, "Six ideas that explain every AI agent, before any code": about fifteen minutes, no code, covering what a model call is, tokens, the context window, why the model remembers nothing, tool calling, and the loop that makes it an agent. If they are familiar, start at Day 01. You do need to be comfortable reading Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rules for the writing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nothing gets described that has not been built.&lt;/strong&gt; The code ships before the post does. When something does not work, that is the post, and there will be several, because thirty days of building something hard is not thirty days of wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No invented numbers.&lt;/strong&gt; Any claim about accuracy comes from the Day 25 evaluation, against repositories with known-good answers, with the methodology published alongside it. Where a figure is illustrative, the sentence will say so.&lt;/p&gt;

&lt;p&gt;Tomorrow: the forty-line agent, and why your first one should not be a framework.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you have lost a week to an unfamiliar codebase — and most of us have — which part actually cost you the time: finding the entry point, or working out which files mattered? The answer changes what gets built first, so I would genuinely like to know.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All views in this post are my own. The scenarios are illustrative composites, and code examples are not drawn from any specific codebase.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agentiai</category>
      <category>agents</category>
    </item>
    <item>
      <title>The Senior Developer Mindset: How to Turn Abstract Ideas Into Production Code</title>
      <dc:creator>Harshavardhan Katkam</dc:creator>
      <pubDate>Thu, 04 Jun 2026 08:04:16 +0000</pubDate>
      <link>https://dev.to/harshavardhan_katkam/the-senior-developer-mindset-how-to-turn-abstract-ideas-into-production-code-3gl0</link>
      <guid>https://dev.to/harshavardhan_katkam/the-senior-developer-mindset-how-to-turn-abstract-ideas-into-production-code-3gl0</guid>
      <description>&lt;p&gt;&lt;em&gt;The single skill in my career that has compounded the most isn't a language, a framework, or a system design pattern. It's the ability to take a vague, half-formed idea — a feature request from a PM, a "we should make this resilient" from an architect, a new project ask from an account, a half-formed integration plan that crosses three teams — and turn it into something shipped, working, and useful.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the skill nobody teaches in a course. It's also one of the most rewarding to develop, because it's what allows you to grow into the engineer your team can hand ambiguous problems to with confidence. Here's the mindset, the framework, and the lessons that helped me grow toward it.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Skill That Doesn't Have a Name
&lt;/h2&gt;

&lt;p&gt;Every engineer learns to write code. Most learn to debug, design, and ship. Few practice the skill that comes before all of that — the skill of taking ambiguity and giving it shape.&lt;/p&gt;

&lt;p&gt;You've felt this if you've ever been on the receiving end of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"We need the dashboard to feel faster."&lt;/li&gt;
&lt;li&gt;"We should split this service — it's getting unwieldy."&lt;/li&gt;
&lt;li&gt;"Can we deliver a POC for the client conversation by Friday?"&lt;/li&gt;
&lt;li&gt;"Let's partner with team X to ship this end-to-end."&lt;/li&gt;
&lt;li&gt;"Can we set up a new module for this account?"&lt;/li&gt;
&lt;li&gt;"The system should be more robust."&lt;/li&gt;
&lt;li&gt;"There has to be a better way to do this."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of these are features. Some are architectural decisions. Some are new projects within an account. Some are cross-team integrations. None of them is a specification. The gap between any of them and a working outcome is where most real engineering effort goes — and the gap is the same shape whether the work is one PR or six months across three teams.&lt;/p&gt;

&lt;p&gt;What follows is the framework I've built from doing this work. I'm sharing it in case it saves you a few of the wrong turns I took.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Abstract Ideas Come From
&lt;/h2&gt;

&lt;p&gt;Not all abstract ideas are equal. Each source has its own failure mode and its own opportunity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source 1 — Product / Business.&lt;/strong&gt; PMs, BAs, and clients describe outcomes ("users should be able to..."). They rarely describe systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Trap:&lt;/em&gt; building exactly what they asked for, only to discover what they meant was different. &lt;em&gt;Opportunity:&lt;/em&gt; catching the misalignment before any work begins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Source 2 — Technical Leadership.&lt;/strong&gt; Architects and tech leads describe direction ("we should make this more resilient," "we need to migrate off X"). Vision, but rarely a roadmap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Trap:&lt;/em&gt; treating the suggestion as either a command (over-investing) or a passing thought (ignoring it). &lt;em&gt;Opportunity:&lt;/em&gt; turning the suggestion into a concrete proposal that leadership can green-light or refine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Source 3 — Your Own Observations.&lt;/strong&gt; The bug keeps coming back. The deployment keeps breaking. The on-call rotation keeps paging at 2 AM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Trap:&lt;/em&gt; complaining without doing, or doing something massive without communicating. &lt;em&gt;Opportunity:&lt;/em&gt; building the smallest fix or proof-of-concept that demonstrates value, then asking forgiveness rather than permission.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Source 4 — Improvisations That Become POCs.&lt;/strong&gt; Some of the best work I've shipped started as a quick script, an internal tool, an idea jotted during a meeting.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Trap:&lt;/em&gt; treating it as throwaway and never investing in making it solid. &lt;em&gt;Opportunity:&lt;/em&gt; recognizing when an improvisation is actually a POC for something bigger.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The framework that follows works for any of these — feature, architectural, project-level, or cross-team.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before the Framework: Understand the Terrain
&lt;/h2&gt;

&lt;p&gt;Before you can translate an abstract idea into anything real, you need to understand the system the work will live inside. Whether the "system" is a codebase, an architecture, an account, or a set of stakeholders — the principle is the same.&lt;/p&gt;

&lt;p&gt;It's tempting to jump from "great idea" to "let's start" without pausing to ask: &lt;em&gt;what's already here, why is it set up this way, and what will my work actually touch?&lt;/em&gt; I've fallen into this many times, and the result is fairly predictable. A clean new piece of code that conflicts with an existing pattern. An architectural change that breaks a downstream system nobody mentioned. A new project that quietly steps on another team's roadmap.&lt;/p&gt;

&lt;p&gt;Slow down at the start. Before any meaningful translation work, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What does the existing system look like?&lt;/strong&gt; Read the code. Trace the data flow. Read the contracts between services. Talk to the teams adjacent to your work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why was it built this way?&lt;/strong&gt; Git history, old PRs, design docs, informal Slack threads. The current state reflects past decisions you don't see in the present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's the messy part nobody wants to touch?&lt;/strong&gt; Every system has zones of accumulated complexity. Knowing where they are tells you where the risks live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who knows this best?&lt;/strong&gt; Find that person. Ten minutes with them saves a week of trial and error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This stage is unglamorous. It can feel like you're not making progress. But the time you invest in understanding the terrain compounds — every later decision becomes faster and less risky.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sometimes You Have to Take Ownership of the Mess
&lt;/h3&gt;

&lt;p&gt;Sometimes the existing structure isn't just unfamiliar — it's tangled, undocumented, and getting in the way. Code paths nobody fully understands. Configuration buried across services. A "we'll clean it up later" that's been there for two years.&lt;/p&gt;

&lt;p&gt;It's tempting to route around it. The more rewarding move is often to engage with it. That can look like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documenting what you've understood, even if no one asked you to&lt;/li&gt;
&lt;li&gt;Refactoring the parts you've had to touch, leaving them better than you found them&lt;/li&gt;
&lt;li&gt;Naming the problem out loud — in a writeup, a Slack thread, a one-on-one — so it's visible instead of invisible&lt;/li&gt;
&lt;li&gt;Proposing a small, low-risk cleanup as a side-quest to the main work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not every mess needs cleaning, and not every cleanup is yours to drive. But occasionally picking up the mop, when the cost is fair and the impact is real, tends to be one of the most rewarding kinds of work — and quietly earns trust over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Framework: Five Stages
&lt;/h2&gt;

&lt;p&gt;Translation work has five stages. They're not always linear — sometimes you loop back — but they're always present. The discipline is not skipping any of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: Capture
&lt;/h3&gt;

&lt;p&gt;Write down the idea verbatim. Not your interpretation. The exact words.&lt;/p&gt;

&lt;p&gt;If a PM said it, copy from Slack. If an architect said it, summarize back and confirm. If it's your own observation, note the symptom that triggered it. Your brain immediately starts interpreting — capturing the original wording gives you something to come back to when you realize three days in that you've drifted.&lt;/p&gt;

&lt;p&gt;Every translation effort starts with one written line that nobody can later argue about. That line is your anchor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Clarify
&lt;/h3&gt;

&lt;p&gt;Find the real problem, not the stated one.&lt;/p&gt;

&lt;p&gt;The stated request is rarely the real problem. "The dashboard should feel faster" might mean: data takes too long, layout shifts on render, no loading indicator, comparison to a competitor, slow connection. Five interpretations, five different solutions ranging from one day to one quarter.&lt;/p&gt;

&lt;p&gt;The habit worth building: ask &lt;em&gt;"what does success look like?"&lt;/em&gt; and don't start work until you can describe success in measurable terms. Five questions I always ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who experiences this?&lt;/strong&gt; (Specific persona, not "the user")&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What are they trying to accomplish?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How will we know it's better?&lt;/strong&gt; (Measurable, even approximately)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's the cost of not doing this?&lt;/strong&gt; (If "nothing concrete," the priority is probably wrong)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What would the absolute simplest version look like?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I can't answer these five, I don't write code, draw architecture, or kick off a project. I go back to whoever raised the idea and have a conversation. This is rarely glamorous and often slightly awkward. It's also the single most valuable habit I've built.&lt;/p&gt;

&lt;h3&gt;
  
  
  Break It Down to Layman Terms — The Feynman Move
&lt;/h3&gt;

&lt;p&gt;A habit I've made non-negotiable: &lt;strong&gt;before I start anything, I make sure I can explain the problem in the simplest possible language, with an analogy a non-engineer could follow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the Feynman technique applied to engineering. If you can't explain something simply, you don't really understand it.&lt;/p&gt;

&lt;p&gt;In practice, my first move on any meaningful task is to ask either a peer or an AI tool: &lt;em&gt;"Explain this to me like I'm new to the system. Use an analogy if it helps."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the ask is "improve the consistency of our distributed cache," a layman framing might be: &lt;em&gt;"Imagine three people each have a copy of a shared notebook. When one writes something, how do the others find out, and how fast?"&lt;/em&gt; That sentence — minus the engineering vocabulary — anchors my thinking for everything that follows.&lt;/p&gt;

&lt;p&gt;I sometimes write the layman version at the top of a doc before any technical detail. It serves as a north star — every time the work feels tangled, I come back to that one sentence and check whether I'm still solving the problem I started with.&lt;/p&gt;

&lt;p&gt;The senior engineers I admire most can describe extremely complex problems in remarkably simple terms. That's not a coincidence. It's a habit, built deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: Constrain
&lt;/h3&gt;

&lt;p&gt;Decide what &lt;em&gt;not&lt;/em&gt; to build.&lt;/p&gt;

&lt;p&gt;This is the stage that's easiest to skip — and the one I've come to value most. Once you're convinced something is worth doing, the next question is: &lt;em&gt;given my time, my team's bandwidth, and the actual cost of getting this wrong — what's the smallest meaningful version?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Constraints to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; A week, a quarter, a spare hour?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; What's in, what's out, what's a "later"?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk:&lt;/strong&gt; What could go wrong, and what's the worst case?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversibility:&lt;/strong&gt; One-way door (hard to undo) or two-way door (easy to undo)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audience:&lt;/strong&gt; Who uses it, who maintains it, who approves it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful tool: &lt;strong&gt;the 3-day version.&lt;/strong&gt; Force yourself to design what you'd ship if you had three days, no more. Even if you eventually have three weeks, the 3-day version teaches you what's truly load-bearing and what's optional. Often, the 3-day version is what should ship first anyway.&lt;/p&gt;

&lt;p&gt;This stage prevents most "scope creep" — not by saying "no" to features after they're requested, but by deciding upfront, in writing, what's &lt;em&gt;not&lt;/em&gt; going to be built.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Lesson I Wish I'd Learned Earlier: Timebox, or Start Fresh
&lt;/h3&gt;

&lt;p&gt;For a long time, I had a habit I didn't recognize. When faced with an ambiguous problem, I'd dive into analysis. Open documentation, sketch designs, weigh trade-offs, talk to people, draw more diagrams. Hours would pass. The work felt productive — I was thinking deeply, after all.&lt;/p&gt;

&lt;p&gt;A tech lead I worked with eventually pointed it out: I was in analysis paralysis. I was so focused on getting the design &lt;em&gt;right&lt;/em&gt; before starting that I was burning hours on decisions that would have taken five minutes to validate with a quick prototype.&lt;/p&gt;

&lt;p&gt;The advice he gave me changed how I work: &lt;strong&gt;timebox the analysis, and if you're not converging, start fresh.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Practically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give yourself a fixed amount of time to clarify and constrain — usually no more than half a day for typical work&lt;/li&gt;
&lt;li&gt;If you're still spinning at the end of that window, stop&lt;/li&gt;
&lt;li&gt;Either commit to the best option you have and start building, or reset entirely with a blank page and 30 minutes — often the second pass is faster and clearer than the first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starting fresh feels like wasted work, but it's almost always faster than continuing to refine a tangled first pass. Your second attempt benefits from everything you learned in the first one, but isn't burdened by the dead-ends you got stuck on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 4: Construct
&lt;/h3&gt;

&lt;p&gt;Build the thing.&lt;/p&gt;

&lt;p&gt;By this point, you usually know what you're building, why, and what you're &lt;em&gt;not&lt;/em&gt; building. The actual construction is faster because the ambiguity has been resolved upfront. This is often the stage we think &lt;em&gt;is&lt;/em&gt; the job — but it's actually the stage where the upstream work pays off.&lt;/p&gt;

&lt;p&gt;A few principles I've internalized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decompose into demoable units.&lt;/strong&gt; If you can't show progress in 2–3 day increments, the unit is too big.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sketch before building.&lt;/strong&gt; A diagram, a flowchart, a written API contract — anything that lets you reason about the design before code, architecture, or commitments lock you in. Five minutes of sketching saves five hours of rework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use AI as a force multiplier, not a replacement.&lt;/strong&gt; AI tools accelerate boilerplate, surface edge cases, draft tests, review for blind spots. They do not make the design decisions or skip the clarify stage. The judgment is yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lean on your peers.&lt;/strong&gt; Quick gut-check messages — "does this approach make sense?" — to a senior peer or architect cost five minutes of their time and can save you days. The habit worth building is asking early and specifically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 5: Confirm
&lt;/h3&gt;

&lt;p&gt;Close the loop with whoever raised the idea.&lt;/p&gt;

&lt;p&gt;Once it's working — even partially — show it back. Not a deck. Not a status update. The actual thing.&lt;/p&gt;

&lt;p&gt;This stage catches the misalignments that survived all earlier stages. You'll find that what you built is 95% right and 5% wrong, and the 5% only became visible when they saw it. Better to find it now than after the next sprint.&lt;/p&gt;

&lt;p&gt;Sometimes this stage reveals that the original idea was wrong, and what they actually needed was different. That's the whole point of staging it iteratively. The faster you close the loop, the less wasted work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes the Framework Actually Work
&lt;/h2&gt;

&lt;p&gt;Three force multipliers worth investing in:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Dedicated, focused time.&lt;/strong&gt; Translation work cannot happen in 15-minute slots between meetings. The clarify and constrain stages especially require uninterrupted thinking. I block calendar time explicitly — not for "writing code," but for "translating idea X into a plan." When I started doing this deliberately, my output quality changed more than from any tool or technology I adopted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. AI as a thinking partner.&lt;/strong&gt; AI tools, used well, accelerate the framework. I lean on them for: generating "what could go wrong" lists (Stage 2), asking "what's the simplest version?" (Stage 3), boilerplate and test scaffolding (Stage 4), drafting demos and writeups (Stage 5). I do not lean on them for the judgment calls — whether to build, whether the simpler version is enough, whether the design will hold up. The AI removes friction around your thinking. It doesn't replace it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Mentorship — both ways.&lt;/strong&gt; Earlier in my career, having a senior to validate or redirect my translation work saved me from countless wrong turns. Now, increasingly, I'm the one being asked. Both directions of this exchange compound the skill. Senior engineers don't translate ambiguity better because they're smarter — they've just seen the same shapes of ambiguity hundreds of times. Mentorship is compressed exposure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Improvisations Compound
&lt;/h2&gt;

&lt;p&gt;Some of the highest-leverage work I've shipped started as something improvised — a small script, an internal tool, an idea jotted in a meeting. The pattern looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You notice a recurring annoyance — manual process, flaky test, something that keeps breaking&lt;/li&gt;
&lt;li&gt;You spend an hour or two building a quick fix&lt;/li&gt;
&lt;li&gt;The fix works for your case&lt;/li&gt;
&lt;li&gt;A teammate asks if it could work for theirs&lt;/li&gt;
&lt;li&gt;You generalize it — another half-day&lt;/li&gt;
&lt;li&gt;By the end of the month, three people are using it&lt;/li&gt;
&lt;li&gt;By the end of the quarter, it's the recommended approach&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this was on a roadmap. None of it was in a sprint plan. But it shipped because you noticed something, took it seriously, and applied the same translation framework you'd apply to any ask.&lt;/p&gt;

&lt;p&gt;This is, in my experience, where engineers create the most outsized value — not in the work that's handed to them, but in the work they spot and pursue. The framework still applies, just with you as both originator and implementer. The discipline keeps you from two equally common pitfalls: ignoring your ideas entirely, or over-investing in them without validation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Failure Modes I Watch in Myself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Skipping clarify because the request "seems clear."&lt;/strong&gt; It rarely is. The clearer it seems, the more likely you've assumed something the asker didn't say.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Building first, asking later.&lt;/strong&gt; A common reflex, and one I've fallen into many times. Once you've built, you're emotionally invested in the solution, and it gets harder to walk away from it even when it's the wrong shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Using AI to skip thinking.&lt;/strong&gt; AI can generate output that looks right but isn't the right thing. The framework — clarify, constrain, confirm — is exactly what AI tools cannot do for you. Don't outsource it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Skill Grows You Into
&lt;/h2&gt;

&lt;p&gt;If I had to point at the single thing this framework has unlocked, it's the chance to grow into what some teams call a &lt;strong&gt;feature anchor.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A feature anchor is the engineer the team can trust to take an ambiguous problem — feature, architectural change, new project, cross-team integration — and bring it to a working outcome. Not because they're the best coder. Not because they know the most. Because they're willing to ask the right questions, decide what to build and what to skip, work through the messy middle, and demo something useful at the end.&lt;/p&gt;

&lt;p&gt;That role is rarely assigned. It tends to grow on you quietly, as you keep running through this kind of process on enough problems that people start handing you the ambiguous work because they know it'll get done. It's one of the more rewarding milestones in an engineering career, because it's the moment the &lt;em&gt;judgment&lt;/em&gt; part of the job — not just the code — starts to compound.&lt;/p&gt;

&lt;p&gt;If becoming the engineer your team turns to when something fuzzy lands on the table sounds appealing — this skill, more than any specific technology, is the path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The framework is simple. The discipline of running it every time is what's hard.&lt;/p&gt;

&lt;p&gt;So much of growing as an engineer is not about the code. It's about everything that happens before and after the code. The clarify conversation that prevents weeks of rework. The patience to understand the existing system before changing it. The discipline of restating the problem in layman terms. The constraint exercise that ships something useful in days instead of months. The improvisation that becomes a team-wide tool. The willingness to demo half-built work and absorb feedback. The wisdom to timebox analysis and restart fresh. The willingness to take ownership of the messy parts when the team needs someone to.&lt;/p&gt;

&lt;p&gt;None of this is glamorous. None of it makes for a flashy pull request. But over time, it's the work that compounds the most.&lt;/p&gt;

&lt;p&gt;If you're earlier in your career and this resonates — try one stage of the framework deliberately this week. Pick one ambiguous ask, run it through clarify and constrain explicitly, and notice what changes.&lt;/p&gt;

&lt;p&gt;If you're further along — I'd love to hear what's missing or what you'd phrase differently. Every engineer I've learned from has a slightly different version of this, and I'm always curious to hear theirs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped, a clap goes a long way. And if you've shipped something that started as an improvisation, I'd love to hear that story.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All views in this post are my own. The framework is drawn from my own learning and not from any specific employer, project, or codebase. Examples are illustrative.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>productivity</category>
      <category>softwareengineering</category>
      <category>programming</category>
    </item>
    <item>
      <title>RestTemplate vs RestClient vs WebClient</title>
      <dc:creator>Harshavardhan Katkam</dc:creator>
      <pubDate>Fri, 24 Apr 2026 08:41:33 +0000</pubDate>
      <link>https://dev.to/harshavardhan_katkam/resttemplate-vs-restclient-vs-webclient-hcg</link>
      <guid>https://dev.to/harshavardhan_katkam/resttemplate-vs-restclient-vs-webclient-hcg</guid>
      <description>&lt;p&gt;The HTTP Client Showdown Every Spring Developer Needs to Understand&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you've been writing Spring applications for a while, you've probably had this conversation: "Should I use RestTemplate? Someone said it's deprecated. What about WebClient? And wait, what on earth is RestClient?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's settle this once and for all.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Short Version (For the Impatient)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Introduced&lt;/th&gt;
&lt;th&gt;Style&lt;/th&gt;
&lt;th&gt;Status in 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RestTemplate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spring 3.0 (2009)&lt;/td&gt;
&lt;td&gt;Blocking, imperative&lt;/td&gt;
&lt;td&gt;In maintenance mode — not deprecated, but not evolving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebClient&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spring 5.0 (2017)&lt;/td&gt;
&lt;td&gt;Non-blocking, reactive&lt;/td&gt;
&lt;td&gt;Actively maintained, still the async champion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RestClient&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spring 6.1 (2023)&lt;/td&gt;
&lt;td&gt;Blocking, fluent&lt;/td&gt;
&lt;td&gt;The new recommended default for synchronous code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're starting a new project today and your code is synchronous — &lt;strong&gt;use RestClient&lt;/strong&gt;. If you're doing reactive or truly async I/O — &lt;strong&gt;use WebClient&lt;/strong&gt;. If you have an existing RestTemplate codebase — &lt;strong&gt;you don't have to migrate tomorrow&lt;/strong&gt;, but plan for it.&lt;/p&gt;

&lt;p&gt;Now let's dig into &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Quick History Lesson (Because Context Matters)
&lt;/h2&gt;

&lt;p&gt;To understand these three clients, you need to understand the problems each was born to solve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2009 — RestTemplate arrives.&lt;/strong&gt; Back then, calling REST APIs in Java was painful. You were stitching together &lt;code&gt;HttpURLConnection&lt;/code&gt;, Apache HttpClient, and custom serializers. RestTemplate wrapped all that ceremony behind a clean, synchronous API. It was revolutionary — and for a decade, it was &lt;em&gt;the&lt;/em&gt; way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2017 — WebClient arrives.&lt;/strong&gt; The reactive revolution (RxJava, Project Reactor) changed how we think about I/O. Microservices meant one request often fanned out into ten. Blocking threads became expensive. Spring introduced WebFlux and, with it, WebClient — a non-blocking HTTP client built on Reactor's &lt;code&gt;Mono&lt;/code&gt; and &lt;code&gt;Flux&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem? WebClient became the &lt;em&gt;only&lt;/em&gt; modern choice. If you wanted modern features (builder pattern, better error handling, interceptors that don't fight you), you had to adopt reactive types — even if your app had zero reactive code. Teams were writing &lt;code&gt;.block()&lt;/code&gt; everywhere and pretending they weren't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2023 — RestClient arrives.&lt;/strong&gt; The Spring team finally admitted the obvious: most applications are synchronous, and they deserve a modern, fluent, blocking API that doesn't force reactive types down their throat. RestClient is what RestTemplate would look like if we designed it today.&lt;/p&gt;




&lt;h2&gt;
  
  
  RestTemplate: The Veteran
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;RestTemplate&lt;/span&gt; &lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RestTemplateBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;restTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rootUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getForObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users/{id}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;postForObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's good:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everyone knows it. Zero learning curve.&lt;/li&gt;
&lt;li&gt;Massive ecosystem, battle-tested, boring in the best way.&lt;/li&gt;
&lt;li&gt;Works synchronously — matches how most business logic thinks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What hurts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API is &lt;em&gt;overloaded beyond recognition&lt;/em&gt;. Look at the Javadoc — &lt;code&gt;exchange&lt;/code&gt;, &lt;code&gt;execute&lt;/code&gt;, &lt;code&gt;getForObject&lt;/code&gt;, &lt;code&gt;getForEntity&lt;/code&gt;, &lt;code&gt;postForObject&lt;/code&gt;, &lt;code&gt;postForEntity&lt;/code&gt;, &lt;code&gt;postForLocation&lt;/code&gt;... twenty ways to do one thing.&lt;/li&gt;
&lt;li&gt;No fluent builder. Headers, query params, and body-building are awkward.&lt;/li&gt;
&lt;li&gt;Error handling defaults are surprising. A 4xx throws; a 2xx with no body returns null.&lt;/li&gt;
&lt;li&gt;Not evolving. New features go to RestClient and WebClient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Official status:&lt;/strong&gt; &lt;em&gt;Not deprecated.&lt;/em&gt; This is important. The Spring team has explicitly said RestTemplate will not be removed. But it's in maintenance mode — no new features, just bug fixes.&lt;/p&gt;




&lt;h2&gt;
  
  
  WebClient: The Reactive Powerhouse
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;WebClient&lt;/span&gt; &lt;span class="n"&gt;webClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;webClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;webClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users/{id}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bodyToMono&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Flux&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getAllUsers&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;webClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bodyToFlux&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's good:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Truly non-blocking. Perfect for high-concurrency fan-out (a single Netty event-loop thread can handle thousands of in-flight requests).&lt;/li&gt;
&lt;li&gt;Fluent, modern, discoverable API.&lt;/li&gt;
&lt;li&gt;First-class streaming support — &lt;code&gt;Flux&amp;lt;User&amp;gt;&lt;/code&gt; lets you process a 10GB response without loading it into memory.&lt;/li&gt;
&lt;li&gt;Backpressure. If you're consuming a fast producer from a slow consumer, WebClient handles it natively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What hurts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're now in Reactor-land. &lt;code&gt;Mono&lt;/code&gt;, &lt;code&gt;Flux&lt;/code&gt;, &lt;code&gt;flatMap&lt;/code&gt;, &lt;code&gt;zip&lt;/code&gt;, &lt;code&gt;onErrorResume&lt;/code&gt; — there's a real learning curve.&lt;/li&gt;
&lt;li&gt;Stack traces are ugly. Debugging reactive code is genuinely harder.&lt;/li&gt;
&lt;li&gt;If you &lt;code&gt;.block()&lt;/code&gt; everything, you get WebClient's complexity with RestTemplate's performance characteristics — the worst of both worlds.&lt;/li&gt;
&lt;li&gt;Needs &lt;code&gt;spring-webflux&lt;/code&gt; on the classpath even in a servlet app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When it shines:&lt;/strong&gt; WebFlux apps, streaming responses, SSE/WebSockets, fan-out aggregation where latency matters, any place you're genuinely doing async I/O.&lt;/p&gt;




&lt;h2&gt;
  
  
  RestClient: The Goldilocks Client
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;RestClient&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RestClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;restClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users/{id}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;getUserWithCustomErrorHandling&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users/{id}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;HttpStatusCode:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;is4xxClientError&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserNotFoundException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;})&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at that code. Notice something? It reads &lt;em&gt;exactly&lt;/em&gt; like WebClient — but there's not a &lt;code&gt;Mono&lt;/code&gt; or &lt;code&gt;Flux&lt;/code&gt; in sight. It's synchronous. It blocks. It returns the value directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's good:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same fluent API as WebClient, zero reactive types.&lt;/li&gt;
&lt;li&gt;Modern error handling with &lt;code&gt;.onStatus()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Can use any underlying HTTP client — JDK's &lt;code&gt;HttpClient&lt;/code&gt;, Apache, Jetty, Reactor Netty.&lt;/li&gt;
&lt;li&gt;Integrates cleanly with Spring's &lt;code&gt;@HttpExchange&lt;/code&gt; declarative clients (think Feign, but built in).&lt;/li&gt;
&lt;li&gt;Familiar mental model for anyone coming from RestTemplate or WebClient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What hurts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Newer. Smaller community base of examples and Stack Overflow answers (though that's changing fast).&lt;/li&gt;
&lt;li&gt;Not reactive. If you need non-blocking, it's the wrong tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; You can build a RestClient from an existing RestTemplate — a huge win for gradual migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;RestClient&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RestClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingRestTemplate&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All your existing interceptors, message converters, and error handlers come along for the ride.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Deep Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Syntax and Ergonomics
&lt;/h3&gt;

&lt;p&gt;RestClient and WebClient share near-identical fluent APIs. RestTemplate is the outlier with its overloaded method zoo.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Threading Model
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RestTemplate&lt;/strong&gt; — one thread per request, blocked until response arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RestClient&lt;/strong&gt; — same model (blocking), but the underlying transport is pluggable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebClient&lt;/strong&gt; — event-loop based, one thread handles many in-flight requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a typical CRUD service handling a few hundred req/sec on a well-sized thread pool, the difference is invisible. For a gateway aggregating 50 downstream calls per request at 10K RPS, WebClient wins by orders of magnitude.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Error Handling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;RestTemplate&lt;/strong&gt; throws &lt;code&gt;HttpClientErrorException&lt;/code&gt; / &lt;code&gt;HttpServerErrorException&lt;/code&gt; by default. You override a &lt;code&gt;ResponseErrorHandler&lt;/code&gt; globally to change it. Not fine-grained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RestClient and WebClient&lt;/strong&gt; offer per-request &lt;code&gt;.onStatus()&lt;/code&gt; handling — you decide what a 404 means &lt;em&gt;in the context of this specific call.&lt;/em&gt; Huge ergonomic win.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Testing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RestTemplate&lt;/strong&gt; — &lt;code&gt;MockRestServiceServer&lt;/code&gt; is mature and easy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebClient&lt;/strong&gt; — needs either &lt;code&gt;ExchangeFunction&lt;/code&gt; mocking or an &lt;code&gt;MockWebServer&lt;/code&gt; setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RestClient&lt;/strong&gt; — works with &lt;code&gt;MockRestServiceServer&lt;/code&gt; (shares the infrastructure). Big win for teams migrating.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Declarative Clients (the underrated feature)
&lt;/h3&gt;

&lt;p&gt;Both RestClient and WebClient support Spring's &lt;code&gt;@HttpExchange&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@HttpExchange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;UserApi&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@GetExchange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{id}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;@PostExchange&lt;/span&gt;
    &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it up once and you have a type-safe client — no implementation code to write. This is effectively what Feign gave us, now built into Spring. RestTemplate doesn't support this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Decision Framework
&lt;/h2&gt;

&lt;p&gt;Ask yourself three questions, in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Is your codebase reactive (WebFlux, R2DBC, Kafka reactive)?&lt;/strong&gt;&lt;br&gt;
→ Use &lt;strong&gt;WebClient&lt;/strong&gt;. Don't fight the paradigm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Do you need genuine non-blocking I/O for performance (high fan-out, streaming, SSE)?&lt;/strong&gt;&lt;br&gt;
→ Use &lt;strong&gt;WebClient&lt;/strong&gt;. The complexity pays for itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Everything else?&lt;/strong&gt;&lt;br&gt;
→ Use &lt;strong&gt;RestClient&lt;/strong&gt;. It's the modern synchronous default.&lt;/p&gt;

&lt;p&gt;And for legacy code on RestTemplate — there's no emergency. Plan a gradual migration as you touch code. &lt;code&gt;RestClient.create(restTemplate)&lt;/code&gt; makes incremental migration almost free.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Word on Performance
&lt;/h2&gt;

&lt;p&gt;I'll save you the benchmark drama: &lt;strong&gt;for synchronous workloads, RestTemplate and RestClient are effectively identical.&lt;/strong&gt; They share transport layers. The real performance story is WebClient vs. everything else in high-concurrency async scenarios — and even there, it only matters if you're actually constrained on threads.&lt;/p&gt;

&lt;p&gt;Don't pick WebClient for "performance" if your app does 50 RPS. You'll pay the complexity tax for benefits you'll never measure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes I See on Code Reviews
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Using WebClient with &lt;code&gt;.block()&lt;/code&gt; everywhere.&lt;/strong&gt; You're burning all of WebClient's benefits and eating its complexity. Switch to RestClient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creating a new RestTemplate/RestClient per request.&lt;/strong&gt; These are designed to be singletons. Inject the builder, build once, reuse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring timeouts.&lt;/strong&gt; Default timeouts are "forever." Always set connect and read timeouts — production will thank you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catching &lt;code&gt;RestClientException&lt;/code&gt; broadly.&lt;/strong&gt; Use &lt;code&gt;.onStatus()&lt;/code&gt; for semantic error handling. Catch specific exceptions at the boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not using &lt;code&gt;@HttpExchange&lt;/code&gt;.&lt;/strong&gt; If you're writing repetitive CRUD wrappers, you're writing code that Spring will generate for you.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Spring's HTTP client story went from "just use RestTemplate" to "use WebClient and suffer" to — finally — a clean, ergonomic split based on what your code actually needs. RestClient in particular is one of the best Spring improvements of the last five years. It quietly fixes the biggest complaint developers had without breaking anyone's existing code.&lt;/p&gt;

&lt;p&gt;If I had to sum it up in one sentence: &lt;strong&gt;use RestClient by default, reach for WebClient when you're genuinely reactive, and migrate off RestTemplate at your own pace.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it. No dogma, no hype. Pick the tool that matches your workload.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped, a clap goes a long way. And if you disagree — especially on the "don't migrate tomorrow" take for RestTemplate — I'd love to hear your war stories in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>spring</category>
      <category>springboot</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
