<?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: Akash Pal</title>
    <description>The latest articles on DEV Community by Akash Pal (@akashpal).</description>
    <link>https://dev.to/akashpal</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%2F141600%2F85e73546-83c9-443b-b965-725add6daea2.png</url>
      <title>DEV Community: Akash Pal</title>
      <link>https://dev.to/akashpal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akashpal"/>
    <language>en</language>
    <item>
      <title>Part 7: Iterating to Green: Real Bugs, and When You'd Actually Reach for a Framework</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:44:00 +0000</pubDate>
      <link>https://dev.to/akashpal/part-7-iterating-to-green-real-bugs-and-when-youd-actually-reach-for-a-framework-3np1</link>
      <guid>https://dev.to/akashpal/part-7-iterating-to-green-real-bugs-and-when-youd-actually-reach-for-a-framework-3np1</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 7 (final) of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh"&gt;Part 6&lt;/a&gt; (observability). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The other six parts described the finished design. This one is about what "finished" actually took — the real bugs the eval set caught, and the two questions every agent build eventually has to answer honestly: do you need more than one agent, and do you need a framework. Full detail on everything below: &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/docs/iteration-log.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/iteration-log.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The iteration log, condensed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Exact trajectory matching was the wrong check.&lt;/strong&gt; First eval run: 12/21 passed. Most failures were the agent correctly sending a confirmation email where the eval only expected a lookup — correct behavior, wrong assertion. Fix: switched the harness from exact-array equality to ordered-subsequence matching (every expected tool must appear, in order; extra steps in between are fine). Still catches a missing, reordered, or wrong tool. Stops false-failing on benign non-determinism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No retry/backoff meant a transient error crashed the whole run.&lt;/strong&gt; A &lt;code&gt;503 — model overloaded&lt;/code&gt; on case 1 took down the entire eval harness. Fixed with exponential backoff on &lt;code&gt;429&lt;/code&gt;/&lt;code&gt;503&lt;/code&gt; specifically, plus inter-case pacing to stay under free-tier rate limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A &lt;code&gt;-latest&lt;/code&gt; model alias silently rolled onto a stricter quota.&lt;/strong&gt; &lt;code&gt;gemini-flash-latest&lt;/code&gt; worked, then started failing with a &lt;strong&gt;20 requests/day&lt;/strong&gt; cap after quietly resolving to a newer model. Fixed by pinning an explicit model version instead of an alias, after checking the provider's live usage dashboard for actual quota — 25x more headroom on the pinned model. The takeaway generalizes past this one provider: "latest" aliases optimize for capability, not quota stability, and what they resolve to changes over time without your code changing at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A testing artifact that looked like a real bug.&lt;/strong&gt; Piping multiple answers into the interactive CLI via &lt;code&gt;printf "a\nb\n" | npm run agent&lt;/code&gt; intermittently hung after the first prompt. Root cause: a Node.js &lt;code&gt;readline/promises&lt;/code&gt; quirk with quickly-closing piped stdin — confirmed as a testing artifact, not a real bug, by replaying the same input through an actual pseudo-terminal, where it worked every time. Fixed anyway by reusing a single shared &lt;code&gt;readline.Interface&lt;/code&gt; instead of opening/closing one per prompt — more correct regardless of the original symptom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The model claimed a refund was proposed without ever proposing one.&lt;/strong&gt; The big one — covered in full in Part 5. Two eval cases showed &lt;code&gt;outcome: refund_proposed&lt;/code&gt; with a trajectory that never called &lt;code&gt;issue_refund&lt;/code&gt;. Fixed at two layers: an explicit prompt line, and — the layer that actually matters — a code-level check (&lt;code&gt;enforceOutcomeIntegrity&lt;/code&gt;) that verifies a real &lt;code&gt;confirmation_id&lt;/code&gt; exists in state before trusting the model's own claim.&lt;/p&gt;

&lt;p&gt;The honest note attached to that fifth finding in the actual log: it was verified with a single targeted re-run, not a full eval pass, specifically to conserve free-tier API quota. That gap is left visible in the repo on purpose — claiming a full pass without having run it would be the same category of mistake as finding #5 itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single agent, not multi-agent — and why that's arithmetic, not instinct
&lt;/h2&gt;

&lt;p&gt;Multi-agent (a coordinator dispatching to specialist workers) is a &lt;strong&gt;cost decision&lt;/strong&gt;, not an architectural preference. A coordinator + worker split costs roughly 3–8x a single agent's tokens and latency per run — workers see fewer tools each, which sharpens tool selection, but you're paying for that in every single run, not just the hard ones.&lt;/p&gt;

&lt;p&gt;Three signals should all be true before paying that multiplier: the task genuinely splits into specialized roles that each benefit from a sharper, narrower prompt; subtasks can run in parallel for a real speed win, not just cleaner code; and volume × accuracy improvement actually outweighs the cost multiplier — checked with real numbers, not intuition.&lt;/p&gt;

&lt;p&gt;At low volume, the math tends to favor a single agent even with a real accuracy edge from splitting — the multiplier isn't earned back. At high volume, the same accuracy edge can save real money, because escalation-cost reduction scales with volume in a way the fixed multiplier doesn't. This repo's own agent stays single, deliberately: five tools is well under the point where an agent starts confusing tool names, and nothing about the ticket volume this reference build targets justifies paying a multiplier for a split that would mostly just look tidier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework selection — now that you've seen the raw version
&lt;/h2&gt;

&lt;p&gt;Having built the loop by hand, the actual question a framework answers becomes concrete instead of abstract: does &lt;em&gt;this&lt;/em&gt; problem need multi-agent orchestration, streaming UI wiring, an integrated tracing ecosystem, or shared abstraction across a team? If none of those apply, the ~100-line loop this series built &lt;em&gt;is&lt;/em&gt; the production version, not a placeholder for one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw SDK (this series)&lt;/td&gt;
&lt;td&gt;Learning the loop, simple single-agent tools&lt;/td&gt;
&lt;td&gt;Minimal abstraction, provider-locked, most control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangGraph&lt;/td&gt;
&lt;td&gt;Complex state, multi-agent, widest enterprise adoption&lt;/td&gt;
&lt;td&gt;More setup cost; strongest observability ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mastra&lt;/td&gt;
&lt;td&gt;TypeScript-native teams&lt;/td&gt;
&lt;td&gt;Smaller ecosystem, natural fit for a JS/Next.js background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel AI SDK&lt;/td&gt;
&lt;td&gt;Streaming UI tightly coupled to a Next.js frontend&lt;/td&gt;
&lt;td&gt;Less suited to long-running, durable multi-step workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrewAI&lt;/td&gt;
&lt;td&gt;Standing up role-based multi-agent crews quickly&lt;/td&gt;
&lt;td&gt;Opinionated structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google ADK&lt;/td&gt;
&lt;td&gt;Teams committed to GCP/Gemini&lt;/td&gt;
&lt;td&gt;Less model-agnostic than LangGraph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LlamaIndex&lt;/td&gt;
&lt;td&gt;RAG-heavy use cases, large document corpora&lt;/td&gt;
&lt;td&gt;Less general-purpose outside retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where this leaves the repo
&lt;/h2&gt;

&lt;p&gt;Everything in this series is a real, committed artifact — not a cleaned-up retelling. The &lt;a href="https://github.com/akash-pal/agent-from-scratch#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt; has the full architecture diagrams, quickstart, and real example runs (screenshots included, not staged). &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/AGENTS.md" rel="noopener noreferrer"&gt;AGENTS.md&lt;/a&gt; documents the hard constraints for anyone — human or AI coding agent — extending this repo later: no framework, mock data only, gated tools stay gated, guardrails live in code.&lt;/p&gt;

&lt;p&gt;It's explicitly a reference build, not production software — mock data, a single free-tier model, no persistence, no auth. If you take one thing from the whole series, take the build order, not the code: pin the use case, write tool contracts as specs, build the eval set before the agent exists, write the smallest loop that could work, iterate against real failures, gate the consequential actions. The framework question comes last, not first — and by the time you're actually equipped to answer it, you often don't need to.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Series index: &lt;a href="https://dev.to/akashpal/part-1-what-makes-something-an-agent-and-why-we-built-this-without-a-framework-5ccg"&gt;Part 1&lt;/a&gt; (what makes something an agent) · &lt;a href="https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462"&gt;Part 2&lt;/a&gt; (use case &amp;amp; tool contracts) · &lt;a href="https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk"&gt;Part 3&lt;/a&gt; (the eval set) · &lt;a href="https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6"&gt;Part 4&lt;/a&gt; (the raw loop) · &lt;a href="https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j"&gt;Part 5&lt;/a&gt; (guardrails) · &lt;a href="https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh"&gt;Part 6&lt;/a&gt; (observability) · Part 7 (this post).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 6: Observability for AI Agents: Tracing, Metrics, and Drift</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:58 +0000</pubDate>
      <link>https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh</link>
      <guid>https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 6 of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j"&gt;Part 5&lt;/a&gt; (guardrails). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Run the eval set" and "is this agent healthy right now" are different questions, and it's easy to only build infrastructure for the first one. Eval sets run offline, on cases you already thought of. Production traffic doesn't ask permission to send you a ticket type you didn't anticipate. Observability is what tells you when that's happening — and it's also, unglamorously, what makes offline evaluation possible in the first place: you can't debug a failing eval case without knowing what the agent actually did, step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimum trace payload
&lt;/h2&gt;

&lt;p&gt;Every tool call in this build logs a structured record — &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/trace.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/trace.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TraceStep&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;trace_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;step_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;args_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// hashed, never raw args&lt;/span&gt;
  &lt;span class="nl"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;result_summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details here that look small and aren't:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;args_hash&lt;/code&gt;, not raw &lt;code&gt;args&lt;/code&gt;.&lt;/strong&gt; This trace log is meant to be safe to keep around, ship to a monitoring system, or paste into a bug report — none of which should require thinking about what secrets might be embedded in a tool call's arguments. Hashing means you can still confirm two calls used identical arguments (for debugging idempotency, for instance) without ever persisting the actual values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hashArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;8&lt;/span&gt;&lt;span class="p"&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;strong&gt;&lt;code&gt;result_summary&lt;/code&gt;, truncated.&lt;/strong&gt; Full tool results can be large (a &lt;code&gt;kb_search&lt;/code&gt; returning full article bodies, for instance) — logging the whole thing on every step makes trace output unreadable and bloats whatever's storing it. &lt;code&gt;summarizeResult&lt;/code&gt; takes the first few fields and truncates long values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_FIELD_LEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;summarizeResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Readable in a terminal, structured everywhere else
&lt;/h2&gt;

&lt;p&gt;The trace log doubles as CLI output — this build's whole point is being inspectable, so watching an agent run in real time matters. Early on, that meant a raw JSON blob per line, which is technically complete and practically unreadable. The fix was a small formatting pass, not a new logging system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TraceStep&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;→&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tok`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DIM&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;step_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CYAN&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DIM&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`      &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result_summary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output in an actual terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  [1] order_lookup (0ms, 1077→23 tok)
      order_id="ord_1005"  status="processing"  items=[...]  total_usd=45
  [2] kb_search (1ms, 1268→20 tok)
      articles=[...]  relevance_scores=[0.48,0.24,0.24]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Colors auto-disable when &lt;code&gt;stdout&lt;/code&gt; isn't a real TTY (&lt;code&gt;process.stdout.isTTY&lt;/code&gt;), so piping this to a file or a CI log doesn't leave you with literal escape-code garbage — small thing, but the kind of small thing that makes the difference between a trace log people actually read and one they ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three production monitoring layers
&lt;/h2&gt;

&lt;p&gt;Trace-per-step is the foundation, but three distinct layers sit on top of it, each answering a different question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace logging&lt;/strong&gt; — every run, every tool call: input, response, latency, cost. This is what you already have from the payload above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online metrics&lt;/strong&gt; — aggregate numbers over time: success rate, escalation rate, average tool calls per run, average token cost per run. This is where you'd notice, for instance, that escalation rate crept from 8% to 15% over a week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift detection&lt;/strong&gt; — compare those metrics week over week. This one's easy to skip and shouldn't be: model provider updates cause silent regressions with &lt;strong&gt;zero code changes on your side&lt;/strong&gt;. The agent that scored 95% on your eval set last month can start failing differently this month because the underlying model changed, not because anything you wrote did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This repo doesn't implement online metrics or drift detection — it's a CLI reference build with no persistent request volume to aggregate — but the trace payload is written specifically so those layers could be built on top of it without changing the tracing code itself. That's the actual design goal: the minimum payload isn't "the metrics you need now," it's "the raw material any metrics system would need later."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for debugging, not just monitoring
&lt;/h2&gt;

&lt;p&gt;Go back to Part 3's eval failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[FAIL] hard_04 (hard)
    - trajectory: expected [order_lookup, refund_eligibility, issue_refund] as a subsequence, got [order_lookup, refund_eligibility]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That failure message exists because of tracing, full stop. Without a structured, step-by-step record of what tools got called, "the eval failed" would be all you'd know — not &lt;em&gt;why&lt;/em&gt;. The actual bug behind that failure (Part 5's phantom refund proposal) was findable specifically because the trajectory was visible, not just the final pass/fail.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-7-iterating-to-green-real-bugs-and-when-youd-actually-reach-for-a-framework-3np1"&gt;Part 7: Iterating to Green: Real Bugs, and When You'd Actually Reach for a Framework →&lt;/a&gt;&lt;/strong&gt; closes the series: the full iteration log — every real bug found running this agent against the eval set, what fixed each one, and when you'd actually reach for a framework instead of this raw loop.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>observability</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 5: Guardrails That Live in Code, Not the Prompt</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:53 +0000</pubDate>
      <link>https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j</link>
      <guid>https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 5 of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6"&gt;Part 4&lt;/a&gt; (the loop). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the finding this whole article is built around: partway through eval iteration, the agent started reporting that a refund had been proposed — a clean, plausible-sounding message — &lt;strong&gt;without ever having called the tool that proposes refunds.&lt;/strong&gt; No approval was ever requested. No confirmation existed. The model just said it happened.&lt;/p&gt;

&lt;p&gt;That's the failure mode this part is about, and the fix is the actual argument for why guardrails belong in code, not in prompt text alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy as code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/policy.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/policy.ts&lt;/code&gt;&lt;/a&gt; is a plain data object — an allowlist, an approval list, rate limits, and regex patterns — checked by the agent loop, not asked of the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;allowTools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_lookup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund_eligibility&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue_refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kb_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;send_email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;requireApprovalFor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue_refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;send_email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;rateLimits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxToolCallsPerRun&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxCostPerRunUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;autoEscalatePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;legal_threat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;lawyer|attorney|sue|legal action|better business bureau|&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;bbb&lt;/span&gt;&lt;span class="se"&gt;\b)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fraud_flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;fraud|unauthorized|without my permission|didn't authorize|stolen card&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;duplicate_ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;already submitted|second ticket|duplicate ticket|already reported&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of these is enforced &lt;em&gt;outside&lt;/em&gt; the LLM's control. The model can't talk its way past &lt;code&gt;requireApprovalFor&lt;/code&gt; — the loop checks it before executing the tool, full stop. The &lt;code&gt;autoEscalatePatterns&lt;/code&gt; regexes run against the raw ticket text before the model is even called (this is that &lt;code&gt;expected_trajectory: []&lt;/code&gt; behavior from Part 3's edge-case bucket) — a legal threat or fraud flag never reaches the LLM at all, straight to a human queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three human-review patterns
&lt;/h2&gt;

&lt;p&gt;Not every consequential action needs the same review pattern. This build uses &lt;strong&gt;pre-action approval&lt;/strong&gt; — human approves before execution — for &lt;code&gt;issue_refund&lt;/code&gt; and &lt;code&gt;send_email&lt;/code&gt;, because both are rare, high-stakes, and hard to reverse. Two other patterns exist for different risk profiles and are worth knowing even though this particular agent doesn't need them: &lt;strong&gt;sampling review&lt;/strong&gt; (agent acts autonomously, a random 10–20% gets human QA — appropriate for high-frequency, low-individual-risk actions) and &lt;strong&gt;confidence routing&lt;/strong&gt; (only route to a human when the model's self-reported confidence is genuinely calibrated — risky if that confidence isn't actually reliable, which it usually isn't by default).&lt;/p&gt;

&lt;p&gt;The gate itself, from &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/approval.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/approval.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cliApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApprovalFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRl&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`\n[APPROVAL REQUIRED] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;)\nApprove? (y/n) `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And how the loop applies it before any gated tool runs (from &lt;code&gt;agent.ts&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;requiresApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;approvalFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;resultPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected_by_human_approval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;isError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a human says no, the tool never executes — the model gets an error result back and has to handle that, same as any other tool failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: a claim without a fact behind it
&lt;/h2&gt;

&lt;p&gt;Here's what actually happened. Two eval cases (&lt;code&gt;hard_04&lt;/code&gt;, &lt;code&gt;hard_06&lt;/code&gt;) came back reporting &lt;code&gt;outcome: refund_proposed&lt;/code&gt;, but their tool-call trajectory was only &lt;code&gt;[order_lookup, refund_eligibility]&lt;/code&gt; — &lt;code&gt;issue_refund&lt;/code&gt; was never in the list. The model had generated a final message starting with &lt;code&gt;REFUND_PROPOSED:&lt;/code&gt; directly, skipping the tool call that's supposed to &lt;em&gt;cause&lt;/em&gt; that outcome.&lt;/p&gt;

&lt;p&gt;Why this matters: the outcome parser trusted the model's own &lt;code&gt;REFUND_PROPOSED:&lt;/code&gt; prefix as ground truth. Nothing checked that the claim corresponded to an actual, approved, executed tool call. A "propose a refund" statement is exactly the kind of business-critical, consequential claim that shouldn't rest entirely on the model choosing to comply with a prompt instruction — and here, it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — two layers, not one
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Layer one, prompt:&lt;/strong&gt; add an explicit line the model can't miss (already shown in Part 4's system prompt) — &lt;em&gt;"never state REFUND_PROPOSED unless you actually called issue_refund and it succeeded in this run."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer two, code — the one that actually matters:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Guardrail enforced in code, not just the prompt: a model can claim&lt;/span&gt;
&lt;span class="c1"&gt;// REFUND_PROPOSED in text without ever having called issue_refund.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;enforceOutcomeIntegrity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentOutcome&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentOutcome&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund_proposed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refund_confirmation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ESCALATED: model claimed REFUND_PROPOSED without ever calling issue_refund (policy violation) — original: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&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;state.artifacts.refund_confirmation_id&lt;/code&gt; is only ever set in one place — when &lt;code&gt;issue_refund&lt;/code&gt; actually succeeds (&lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/memory.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/memory.ts&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recordStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrajectoryStep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trajectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue_refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmation_id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refund_confirmation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmation_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;enforceOutcomeIntegrity&lt;/code&gt; isn't trusting the model's words at all — it's checking a fact that can only exist if the gated, approved tool call actually happened. If the model claims &lt;code&gt;REFUND_PROPOSED&lt;/code&gt; without that fact present, the run gets downgraded to &lt;code&gt;escalated&lt;/code&gt;, regardless of how convincing the model's text sounded.&lt;/p&gt;

&lt;p&gt;This is the actual lesson, stated plainly: &lt;strong&gt;any claim your agent makes about a consequential action should be verifiable from state the code controls, not trusted from text the model generated.&lt;/strong&gt; The prompt fix alone would probably have reduced the frequency of this bug. It wouldn't have made it impossible. The code guardrail does.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh"&gt;Part 6: Observability for AI Agents: Tracing, Metrics, and Drift →&lt;/a&gt;&lt;/strong&gt; covers observability — the trace payload this build logs on every single step, and why "run the eval set" isn't the same question as "is this healthy in production."&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>security</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 4: The Raw ReAct Loop: ~100 Lines, No Framework</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:50 +0000</pubDate>
      <link>https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6</link>
      <guid>https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk"&gt;Part 3&lt;/a&gt; (the eval set). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the part everyone reaches for a framework to skip. Here's the argument for not doing that, at least the first time: if you can't explain what your agent loop does in plain English, no framework is going to fix that — it's just going to make the loop harder to see.&lt;/p&gt;

&lt;p&gt;Here's &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/agent.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/agent.ts&lt;/code&gt;&lt;/a&gt;, trimmed to the actual loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;approvalFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApprovalFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxSteps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AgentResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Guardrail check happens BEFORE any model call — see Part 5.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;escalatePattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matchAutoEscalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;escalatePattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ESCALATED: auto-escalated — "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;escalatePattern&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Content&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ticketToUserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withRetry&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;systemInstruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COMPANY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;functionDeclarations&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCalls&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// No tool call — the model produced a final answer. Done.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;enforceOutcomeIntegrity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseOutcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Otherwise: execute the requested tool(s), feed results back, loop again.&lt;/span&gt;
    &lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&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="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responseParts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeToolWithGuardrails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;approvalFn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Part 5&lt;/span&gt;
      &lt;span class="nx"&gt;responseParts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;functionResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;responseParts&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ESCALATED: max_steps_exceeded (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; steps)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The real file inlines tool execution, approval gating, and trace logging directly in the loop rather than a separate function — shown split out here for readability. &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/agent.ts" rel="noopener noreferrer"&gt;Full file on GitHub&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;That's the whole thing: call the model, check if it wants a tool, if yes run it and loop, if no return the final answer. No state machine, no graph, no separate orchestrator abstraction — a &lt;code&gt;for&lt;/code&gt; loop and an &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six-element system prompt
&lt;/h2&gt;

&lt;p&gt;The system prompt is the other half of the loop, and it's treated as a spec with required parts, not free-form prose. From &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/systemPrompt.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/systemPrompt.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are the support agent for {company}. You are not a human agent — say so if asked.

GOAL: resolve or correctly escalate every ticket in as few steps as possible.

TOOLS: order_lookup (read order state) · refund_eligibility (check BEFORE
proposing a refund) · issue_refund (gated) · kb_search (use before any factual
answer) · send_email (gated, delivers the resolution).

POLICY: never call issue_refund without a prior refund_eligibility=true result.
Never fabricate an answer kb_search did not return. Never state REFUND_PROPOSED
unless issue_refund actually succeeded in this run.

UNCERTAINTY: if you are not confident, say so explicitly and escalate — do not guess.

DONE: emit a final response only once resolved, a refund is proposed, or the
ticket is escalated. Always start with RESOLVED:, REFUND_PROPOSED:, or
ESCALATED: so the outcome can be parsed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six required elements: &lt;strong&gt;identity&lt;/strong&gt; (who it is / who it serves), &lt;strong&gt;one-sentence goal&lt;/strong&gt;, &lt;strong&gt;tool list + when to use each&lt;/strong&gt;, &lt;strong&gt;explicit policy&lt;/strong&gt; (what it can't do), &lt;strong&gt;how to signal uncertainty&lt;/strong&gt;, &lt;strong&gt;how to signal done&lt;/strong&gt;. Skip any one of these and you'll find it in your eval failures — a prompt missing an explicit "how to signal done" section is, empirically, the single biggest source of unparseable final responses.&lt;/p&gt;

&lt;p&gt;Notice that &lt;code&gt;POLICY&lt;/code&gt; line about &lt;code&gt;REFUND_PROPOSED&lt;/code&gt; — &lt;em&gt;"never state REFUND_PROPOSED unless issue_refund actually succeeded."&lt;/em&gt; That sentence exists because, without it, the model did exactly that: described a refund as proposed without ever calling the tool. The full story — and why the fix required more than just this prompt line — is Part 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory: one state object, not four systems
&lt;/h2&gt;

&lt;p&gt;"Memory" sounds like it should mean a vector database. Here it's a single object, updated by a reducer — &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/memory.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/memory.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;working&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;                          &lt;span class="c1"&gt;// this run only&lt;/span&gt;
  &lt;span class="nl"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;trajectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrajectoryStep&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;         &lt;span class="c1"&gt;// rolling&lt;/span&gt;
  &lt;span class="nl"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// exact, structured&lt;/span&gt;
  &lt;span class="nl"&gt;long_term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preferred_channel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// stable, mock in this repo&lt;/span&gt;
  &lt;span class="nl"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four layers, ordered by increasing persistence and complexity: &lt;strong&gt;working&lt;/strong&gt; (this run only — the current ticket, ephemeral), &lt;strong&gt;conversation&lt;/strong&gt; (the trajectory so far), &lt;strong&gt;artifacts&lt;/strong&gt; (exact structured facts that need precise recall later, like a refund confirmation ID — never fuzzy semantic search for something like this), &lt;strong&gt;long-term&lt;/strong&gt; (stable facts across sessions — read-only mock data here, since this repo has no real persistence layer).&lt;/p&gt;

&lt;p&gt;The decision rule worth internalizing: reach for the cheapest layer first. In-context conversation memory is sufficient for most single-session agents. An external structured store is for facts that must survive across runs. Vector/semantic memory is the most expensive, highest-maintenance option, and should be justified, not defaulted to — critically, &lt;strong&gt;never&lt;/strong&gt; for anything with financial or access-control consequences, where "approximately right" retrieval is the wrong guarantee. A refund confirmation ID goes in &lt;code&gt;artifacts&lt;/code&gt; as an exact string, not embedded and semantically searched for later.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j"&gt;Part 5: Guardrails That Live in Code, Not the Prompt →&lt;/a&gt;&lt;/strong&gt; covers the guardrail layer this loop leans on for every tool call: the policy-as-code approval gate, and the real bug it was added specifically to catch.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 3: Build the Eval Set Before the Agent Exists</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:48 +0000</pubDate>
      <link>https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk</link>
      <guid>https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462"&gt;Part 2&lt;/a&gt; (tool contracts). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the ordering that trips people up: build the eval set &lt;strong&gt;before&lt;/strong&gt; the agent loop exists. Not after, not alongside — before. It feels backwards. You can't run an eval against an agent that doesn't exist yet.&lt;/p&gt;

&lt;p&gt;That's exactly the point. If you write the eval set after the agent is working, you're unconsciously grading against whatever the agent already does. Cases you didn't think to write are cases your agent silently fails on forever. Writing 21 cases against a &lt;em&gt;specification&lt;/em&gt; (the use case and tool contracts from Part 2) means you're measuring against a real target, not tuning your eval to match your own demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eval set: &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/eval/cases.json" rel="noopener noreferrer"&gt;&lt;code&gt;eval/cases.json&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;21 cases, three buckets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Shipping-status lookups, simple KB questions, a cancelled-order info request, one no-KB-match case that must escalate rather than fabricate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Refund eligibility inside/outside the 30-day window, multi-item orders where only one item is refunded, boundary cases just past the window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Legal-threat, fraud-flag, and duplicate-ticket patterns — must auto-escalate with &lt;strong&gt;zero&lt;/strong&gt; tool/LLM calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A sample case, checking both the outcome &lt;em&gt;and&lt;/em&gt; the trajectory that produced it:&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;"case_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;"hard_03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticket"&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;"ticket_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;"hard_03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wrong size shoes, keep the socks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The running shoes from order ord_1004 are the wrong size. I want a refund for just the shoes, not the socks."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer_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;"cust_002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"order_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;"ord_1004"&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;"expected_trajectory"&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;"order_lookup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund_eligibility"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"issue_refund"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund_proposed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_max_steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policy_checks"&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="s2"&gt;"refund amount reflects only the shoe item (~$74), not the full order total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"issue_refund gated behind human approval"&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 things being checked per case, not just "did the answer look right":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Outcome&lt;/strong&gt; — did it land on &lt;code&gt;resolved&lt;/code&gt; / &lt;code&gt;refund_proposed&lt;/code&gt; / &lt;code&gt;escalated&lt;/code&gt; correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trajectory&lt;/strong&gt; — did it call the &lt;em&gt;right tools, in the right order&lt;/em&gt;. This is what catches "right answer, wrong path" failures — an agent that proposes a refund without ever checking eligibility first, for instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy checks&lt;/strong&gt; — case-specific assertions, like "never call &lt;code&gt;issue_refund&lt;/code&gt; for an already-cancelled order."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why trajectory matters as much as outcome
&lt;/h2&gt;

&lt;p&gt;An agent can land on the correct final answer through a broken process. If &lt;code&gt;hard_03&lt;/code&gt;'s agent calls &lt;code&gt;issue_refund&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; &lt;code&gt;refund_eligibility&lt;/code&gt;, and just happens to guess an eligible amount, checking only the final outcome would call that a pass. Checking the trajectory catches it. This maps directly to something the underlying guide's own evaluation framework insists on: different build steps need different kinds of evaluation, and trajectory correctness is its own category, separate from "did the final answer look okay."&lt;/p&gt;

&lt;p&gt;One design choice worth flagging honestly: the &lt;em&gt;first version&lt;/em&gt; of this trajectory check used exact array equality (&lt;code&gt;JSON.stringify(actual) === JSON.stringify(expected)&lt;/code&gt;), and it was wrong — real LLM runs are non-deterministic enough that this produced false failures on completely correct agent behavior. Part 6 covers exactly what broke and the fix (ordered-subsequence matching instead of exact equality), because it's a genuinely useful lesson on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge bucket exists to test the guardrail, not the model
&lt;/h2&gt;

&lt;p&gt;The three edge cases — legal threat, fraud flag, duplicate ticket — aren't testing whether the LLM is smart enough to recognize a threat. They're testing whether the &lt;strong&gt;auto-escalate pattern match&lt;/strong&gt; (a plain regex check, covered in Part 5) correctly intercepts these tickets &lt;em&gt;before&lt;/em&gt; any model call happens at all:&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;"case_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;"edge_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticket"&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;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Final notice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...lawyer involved...Better Business Bureau."&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;"expected_trajectory"&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;"expected_outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"escalated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_max_steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policy_checks"&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;"auto-escalated before any LLM/tool call (legal_threat pattern)"&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;&lt;code&gt;expected_trajectory: []&lt;/code&gt; and &lt;code&gt;expected_max_steps: 0&lt;/code&gt; are the actual assertion: this ticket should never reach the LLM at all. That's a guardrail test, not a model-capability test — a meaningfully different thing to verify, and one worth keeping separate from the easy/hard buckets conceptually even though they live in the same file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real harness run looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[PASS] easy_03 (easy)
[FAIL] hard_04 (hard)
    - trajectory: expected [order_lookup, refund_eligibility, issue_refund] as a subsequence, got [order_lookup, refund_eligibility]
[PASS] edge_01 (edge)

=== Summary: 12/21 passed ===
easy: 6/12  |  hard: 3/6  |  edge: 3/3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;hard_04&lt;/code&gt; failure is real — it's the same run that surfaced the most significant bug in the whole build (Part 6 covers it in full: the agent claimed a refund was proposed &lt;em&gt;without ever calling the tool that proposes it&lt;/em&gt;). This is what Step 3 buys you: a fixed, pre-written target to run against, so failures like this show up as a clear diff instead of a vague "something feels off."&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6"&gt;Part 4: The Raw ReAct Loop: ~100 Lines, No Framework →&lt;/a&gt;&lt;/strong&gt; covers the actual agent loop — the raw ReAct implementation these 21 cases get run against, with no framework in sight.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>testing</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 2: Pinning the Use Case and Writing Tool Contracts Like Specs</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:46 +0000</pubDate>
      <link>https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462</link>
      <guid>https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of a series building a support-ticket agent with no framework. &lt;a href="https://dev.to/akashpal/part-1-what-makes-something-an-agent-and-why-we-built-this-without-a-framework-5ccg"&gt;Part 1&lt;/a&gt; covered why. This part covers Steps 1–2 of the build order: pinning the use case, and writing tool contracts. Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before any code, two documents: &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/docs/use-case.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/use-case.md&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/docs/tool-contracts.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/tool-contracts.md&lt;/code&gt;&lt;/a&gt;. Skipping this step is the single most common reason teams end up with an agent nobody trusts — not because the idea was bad, but because nothing downstream (evals, prompts, memory) had a fixed target to hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: pin the use case
&lt;/h2&gt;

&lt;p&gt;Four gates, filled in before writing a line of code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bounded input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One support ticket: &lt;code&gt;{ subject, body, customer_id, order_id? }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bounded output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exactly one of: &lt;code&gt;resolved&lt;/code&gt;, &lt;code&gt;refund_proposed&lt;/code&gt; (pending approval), &lt;code&gt;escalated&lt;/code&gt; (with a reason)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Success metric&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Resolution rate &amp;gt; 85% without escalation; escalation rate &amp;lt; 10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tool count cap matters more than it looks. An agent given 10+ tools starts hallucinating tool names and picking the wrong one — a cognitive load problem, not a dependency problem. Keeping this agent to 5 tools, covering exactly three request types (order status, refunds, KB lookups), keeps every run in the healthy 3–8 tool-call range instead of ballooning into a system that needs to be split into multiple specialist agents. (Part 7 covers the actual cost math for when a split &lt;em&gt;is&lt;/em&gt; worth it.)&lt;/p&gt;

&lt;p&gt;Bounded output matters too: &lt;code&gt;resolved&lt;/code&gt; / &lt;code&gt;refund_proposed&lt;/code&gt; / &lt;code&gt;escalated&lt;/code&gt; isn't just documentation — it becomes a literal parseable prefix (&lt;code&gt;RESOLVED:&lt;/code&gt;, &lt;code&gt;REFUND_PROPOSED:&lt;/code&gt;, &lt;code&gt;ESCALATED:&lt;/code&gt;) that the agent's final message must start with. Part 4 shows exactly how that gets parsed, and Part 5 shows why trusting that string alone turned out to be a real bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: tool contracts are a schema, not a docstring
&lt;/h2&gt;

&lt;p&gt;This is the part that's easy to under-invest in. A tool's &lt;code&gt;description&lt;/code&gt; field isn't a comment for future developers — it's the only thing the LLM reads to decide &lt;em&gt;when&lt;/em&gt; to call the tool. Treat it as a specification.&lt;/p&gt;

&lt;p&gt;Here's the actual &lt;code&gt;refund_eligibility&lt;/code&gt; definition from &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/tools/index.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/tools/index.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund_eligibility&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check whether an order is eligible for a refund BEFORE ever proposing one. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Orders are eligible only if delivered and within a 30-day window of order_date. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cancelled orders are already auto-refunded and are never eligible for a manual &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund. Returns eligible, max_amount_usd, and a policy_ref explaining the decision.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;input_schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Customer's stated reason for the refund request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reason&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the 30-day window is stated &lt;strong&gt;explicitly in the tool description&lt;/strong&gt;, not just mentioned once in the system prompt. That's not redundancy — it's a finding from later iteration (full story in Part 6): policy logic that lives only in the system prompt gets missed under load. It has to be load-bearing in the tool itself.&lt;/p&gt;

&lt;p&gt;The full contract table, matching each tool to its schema and failure mode:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;order_lookup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ order_id }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ status, items, total_usd, order_date }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;order not found → structured error, not a guess&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refund_eligibility&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ order_id, reason }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ eligible, max_amount_usd, policy_ref }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cancelled/undelivered → &lt;code&gt;eligible=false&lt;/code&gt; with a reason; ambiguous → escalate, never assume &lt;code&gt;true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;issue_refund&lt;/code&gt; &lt;em&gt;(gated)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ order_id, amount_usd, idempotency_key }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ confirmation_id }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;idempotency_key&lt;/code&gt; required — prevents double-refund on retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kb_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ query }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ articles: [...], relevance_scores }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no match → explicit empty result; agent must not fabricate an answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;send_email&lt;/code&gt; &lt;em&gt;(gated)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ to, subject, body, ticket_id }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ sent, message_id }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gated behind pre-action approval (Part 5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three details worth calling out specifically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency keys aren't optional.&lt;/strong&gt; &lt;code&gt;issue_refund&lt;/code&gt; requires one because retries happen — a flaky network call, a model retrying after a transient error — and a refund tool without idempotency protection means a retry can double-refund a customer. This is enforced in the tool executor itself, not assumed from good behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/tools/issueRefund.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issuedRefunds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// idempotency_key -&amp;gt; confirmation_id&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;issueRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotency_key&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idempotency_key_required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issuedRefunds&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="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;confirmation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;replayed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="c1"&gt;// ... issue the refund, store it under idempotencyKey&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure modes are structured, not guessed.&lt;/strong&gt; &lt;code&gt;order_lookup&lt;/code&gt; on a missing order returns &lt;code&gt;{ error: "order_not_found", order_id }&lt;/code&gt;, not an empty object the model has to interpret. &lt;code&gt;kb_search&lt;/code&gt; on no matches returns an explicit empty &lt;code&gt;articles: []&lt;/code&gt;, specifically so the system prompt can say "if this comes back empty, escalate — don't fabricate an answer."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gated tools are marked at the contract level.&lt;/strong&gt; &lt;code&gt;issue_refund&lt;/code&gt; and &lt;code&gt;send_email&lt;/code&gt; carry &lt;code&gt;gated: true&lt;/code&gt; right in their definition. That flag is read by the policy layer (Part 5) to decide which tool calls need a human in the loop before they execute — the gating decision starts here, at the contract, not somewhere downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data source: mock, but shaped like something real
&lt;/h2&gt;

&lt;p&gt;All five tools read from an in-memory mock store — &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/data/mockData.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/data/mockData.ts&lt;/code&gt;&lt;/a&gt;: five customers, ten orders spanning inside/outside the refund window, five KB articles. It's deliberately fake data (this repo is a reference build, not a production system — more on that in the repo's own README), but the schemas are shaped to mirror a real commerce API's &lt;code&gt;order&lt;/code&gt;/&lt;code&gt;return&lt;/code&gt;/line-item structure, so swapping in a real backend later is a data-layer change, not a tool-contract rewrite.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk"&gt;Part 3: Build the Eval Set Before the Agent Exists →&lt;/a&gt;&lt;/strong&gt; covers building the eval set — 21 cases written &lt;em&gt;before&lt;/em&gt; the agent loop existed, and why that ordering is the actual point, not a nice-to-have.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 1: What Makes Something an Agent (and Why We Built This Without a Framework)</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:44 +0000</pubDate>
      <link>https://dev.to/akashpal/part-1-what-makes-something-an-agent-and-why-we-built-this-without-a-framework-5ccg</link>
      <guid>https://dev.to/akashpal/part-1-what-makes-something-an-agent-and-why-we-built-this-without-a-framework-5ccg</guid>
      <description>&lt;p&gt;Most agent tutorials reach for a framework on line one — LangChain, LangGraph, CrewAI, pick one. This series does the opposite. Over seven parts, we build a real support-ticket agent with &lt;strong&gt;no agent framework at all&lt;/strong&gt;: a hand-written loop against a raw model SDK, explicit guardrails, and an eval set built &lt;em&gt;before&lt;/em&gt; the agent exists.&lt;/p&gt;

&lt;p&gt;The full code is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;akash-pal/agent-from-scratch&lt;/a&gt;&lt;/strong&gt;. Every article in this series links back to the specific files it's talking about — this isn't a toy snippet series, it's a walkthrough of one real (if intentionally small) build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chatbot vs. agent
&lt;/h2&gt;

&lt;p&gt;A chatbot answers once per turn. You send a message, it sends one back, done.&lt;/p&gt;

&lt;p&gt;An agent runs a &lt;strong&gt;loop&lt;/strong&gt;: it decides an action, executes a tool, observes the result, and decides again — repeating until the task is done or it gives up. This is the classic &lt;strong&gt;ReAct&lt;/strong&gt; pattern (Reason + Act):&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%2Fhhiygje71ylzh6btk4yo.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%2Fhhiygje71ylzh6btk4yo.png" alt="ReAct Pattern" width="477" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That loop is the entire reason the rest of this series exists. Because the agent calls tools autonomously and repeatedly, small errors compound across steps in a way a single chatbot turn never does. Propose a refund on the wrong order, and unlike a chatbot giving a wrong answer once, an agent might act on that mistake — call a tool, change state, send an email. The infrastructure this series builds — evals, tracing, guardrails, memory design — exists to &lt;strong&gt;bound and catch that compounding failure&lt;/strong&gt;, not because agents are inherently harder to prompt.&lt;/p&gt;

&lt;p&gt;Three components sit around the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; — each is a name + natural-language description + JSON input schema + an executor function. The description is what the LLM reads to decide when to call it — treat it as a specification, not documentation. (More on this in Part 2.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; — what the agent knows within a run and across runs. (Part 4.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrator&lt;/strong&gt; — the LLM (or a graph/state machine wrapping it) that decides which tool to call next based on the current state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why build it without a framework first
&lt;/h2&gt;

&lt;p&gt;Frameworks pay for themselves under specific conditions: multi-agent orchestration, streaming UI needs, integrated tracing, advanced retry/fallback logic, or a team of people who need a shared abstraction. None of those apply to a first agent built by one person to understand what's actually happening.&lt;/p&gt;

&lt;p&gt;If you can't tell whether LangGraph is adding value or hiding a bug, you don't yet know what LangGraph is doing for you. Writing the raw loop first — even a bad one — gives you a mental model to evaluate any framework against later. That's the actual thesis of this whole series: build the ~100-line version, understand every step of it, &lt;em&gt;then&lt;/em&gt; decide if you need more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build order this series follows
&lt;/h2&gt;

&lt;p&gt;Every part below produced a real, committed artifact in the repo, in this order — and the order is deliberate, not incidental:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin the use case&lt;/strong&gt; — bounded input, bounded output, a hard cap on tool count, one measurable success metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map tools &amp;amp; data sources&lt;/strong&gt; — write every tool contract before writing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the eval set&lt;/strong&gt; — 21 cases, written before the agent existed, so it measures against a real target instead of tuning to whatever the agent happens to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the minimal loop&lt;/strong&gt; — the raw ReAct loop, no framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the system prompt&lt;/strong&gt; — six required elements, versioned like code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate to green&lt;/strong&gt; — one change per fix, and a log of what broke and why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add human review&lt;/strong&gt; — approval gating on consequential actions, decided as an architecture choice, not bolted on after launch.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The use case: an e-commerce support-ticket agent
&lt;/h2&gt;

&lt;p&gt;The concrete system this series builds triages and resolves inbound customer support tickets — order status questions, refund requests, and knowledge-base lookups — for a fictional e-commerce company. Five tools, one agent, real guardrails:&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%2Fw6or53tlm6b0lpl46hdc.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%2Fw6or53tlm6b0lpl46hdc.png" alt="Architecture: an inbound ticket first checks an auto-escalate guard (fraud/legal/duplicate patterns), routing to either immediate escalation with zero LLM calls, or the agent loop, which reads/writes memory state and calls five tools — three run freely, two (issue_refund, send_email) are gated behind human approval" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;order_lookup&lt;/code&gt; and &lt;code&gt;kb_search&lt;/code&gt; are read-only and run freely. &lt;code&gt;issue_refund&lt;/code&gt; and &lt;code&gt;send_email&lt;/code&gt; are &lt;strong&gt;gated&lt;/strong&gt; — they require human approval before they execute, because they're consequential and hard to reverse. That distinction — which actions can run autonomously and which need a human in the loop — turns out to be an architecture decision made in Part 1 of the design, not a safety feature bolted on at the end. Part 7 covers exactly how that gating works.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462"&gt;Part 2: Pinning the Use Case and Writing Tool Contracts Like Specs →&lt;/a&gt;&lt;/strong&gt; covers pinning the use case and writing the five tool contracts — including the specific wording change that fixed a real eval failure later in the series.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>llm</category>
    </item>
    <item>
      <title>Scale Frontend Application: Journey from Zero to Millions of users.</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Wed, 12 Nov 2025 19:07:15 +0000</pubDate>
      <link>https://dev.to/akashpal/scale-frontend-application-journey-from-zero-to-millions-of-users-1697</link>
      <guid>https://dev.to/akashpal/scale-frontend-application-journey-from-zero-to-millions-of-users-1697</guid>
      <description>&lt;p&gt;Scalability problem arises when the system is fast for single user but slow under heavy load.&lt;/p&gt;

&lt;p&gt;A frontend application journey starts with a language and a framework to speed up development and setup.&lt;/p&gt;

&lt;p&gt;Browsers only understand HTML, CSS and Javascript. The application code is more developer friendly and it needs to be converted to browser compatible format. This is where bundlers come in.&lt;br&gt;
The bundlers minify code, optimize images sizes and leverages techniques such as dynamic import and code-splitting to reduce overall size of application. &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.amazonaws.com%2Fuploads%2Farticles%2Fld00k814vfrusqmcn025.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.amazonaws.com%2Fuploads%2Farticles%2Fld00k814vfrusqmcn025.png" alt="Dynamic Import" width="233" height="225"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2F89911yzzql49c2gsucuq.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.amazonaws.com%2Fuploads%2Farticles%2F89911yzzql49c2gsucuq.png" alt="Code Splitting" width="478" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Along with that it uses Tree shaking to analyse the dependency tree and remove any unused code from final bundle. This enables to improve the load times and overall performance.&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.amazonaws.com%2Fuploads%2Farticles%2Fz9kq9qakok258r376be4.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.amazonaws.com%2Fuploads%2Farticles%2Fz9kq9qakok258r376be4.png" alt="Tree Shaking" width="466" height="192"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fmzzo0b5tc9r3zbznkrik.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.amazonaws.com%2Fuploads%2Farticles%2Fmzzo0b5tc9r3zbznkrik.png" alt="Webpack Bundler" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The generated bundle is deployed to server. This is an application server which hosts the bundle.&lt;/p&gt;

&lt;p&gt;The application server IP address needs to be mapped to a DNS record.&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.amazonaws.com%2Fuploads%2Farticles%2Fnp5iy0aelm3nyj09w0vo.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.amazonaws.com%2Fuploads%2Farticles%2Fnp5iy0aelm3nyj09w0vo.png" alt="Application Server and DNS" width="614" height="147"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the application server is in India and the users are in North America then it would increase the latency for the users from North America to reach the server in India.&lt;/p&gt;

&lt;p&gt;CDN (Content Delivery Network) is used to cache requested assets in nearby geographical regions. A combination of single or multiple CDN providers can be used to increase fault tolerance. &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.amazonaws.com%2Fuploads%2Farticles%2Fjs5t7av9nzuqj681ppb2.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.amazonaws.com%2Fuploads%2Farticles%2Fjs5t7av9nzuqj681ppb2.png" alt="Application Server and DNS and CDN" width="800" height="159"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2F6xkqppsy8pm59xd7o3lq.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.amazonaws.com%2Fuploads%2Farticles%2F6xkqppsy8pm59xd7o3lq.png" alt="Map illustrating how CDN's distribute content closer to users" width="617" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With just one application server all the requests are directed to it.&lt;br&gt;
Think of a queue where users line up and each user is addressed at a time. In order to make the server available for all users, the application server can be scaled either Horizontally or Vertically.&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.amazonaws.com%2Fuploads%2Farticles%2F6buxf5gow6knceek4t2p.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.amazonaws.com%2Fuploads%2Farticles%2F6buxf5gow6knceek4t2p.png" alt="Vertical scaling and Horizontal scaling" width="637" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vertical scaling means adding more RAM and more CPU cores.&lt;br&gt;
Horizontal scaling means adding more application servers.&lt;/p&gt;

&lt;p&gt;Vertical scaling is easier which means a chef in a kitchen has more hands to address more users, but its more costly.&lt;/p&gt;

&lt;p&gt;Horizontal scaling is cheaper than adding more RAM and CPU, simply adding another instance of application server. However now the user requests have to be directed to either of the application servers. This needs to be done by a Load Balancer.&lt;/p&gt;

&lt;p&gt;The Load Balancer distributes the traffic among the application servers based on algorithms such as Round Robin or least connections.&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.amazonaws.com%2Fuploads%2Farticles%2Fop364o83jum0lavdj1ue.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.amazonaws.com%2Fuploads%2Farticles%2Fop364o83jum0lavdj1ue.png" alt="Application Server and DNS and CDN and load Balancer" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Round robin methods distributes serially the requests to each server. This might lead to a scenario where one application server is processing a request and round robin hands another request, the new request needs to wait for the current one.&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.amazonaws.com%2Fuploads%2Farticles%2Fdcwvetshr29d47wgwsrq.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.amazonaws.com%2Fuploads%2Farticles%2Fdcwvetshr29d47wgwsrq.png" alt="Round robin load balancing algorithm" width="722" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whereas, least used connection priorities which application server is available and redirects the request to that server.&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.amazonaws.com%2Fuploads%2Farticles%2Fc2kw8l5bo7wn7c6yyb8j.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.amazonaws.com%2Fuploads%2Farticles%2Fc2kw8l5bo7wn7c6yyb8j.png" alt="Least used connection load balancing" width="724" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nginx ("engine x") a application load balancer is widely used.&lt;/p&gt;

&lt;p&gt;If customers in a restaurant walk up to the chef to request for food then chaos erupts! A waiter ensures thats the requests are provided in a orderly fashion so the chef can focus on cooking.&lt;/p&gt;

&lt;p&gt;The waiter (nginx) serves that static frontend files and redirects the traffic to backend server (acting like a reverse proxy for backend API's)&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.amazonaws.com%2Fuploads%2Farticles%2Feaxh7341p257vlded9eo.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.amazonaws.com%2Fuploads%2Farticles%2Feaxh7341p257vlded9eo.png" alt="Nginx as reverse proxy" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order to ensure consistence between the application code developed locally and the one shipped to production environment a standard shipping mechanism is required - this provided by called Docker. Docker enables to pack all the application code and dependencies into a standardised box called containers.&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.amazonaws.com%2Fuploads%2Farticles%2F6x49d2syp98yil5eteu6.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.amazonaws.com%2Fuploads%2Farticles%2F6x49d2syp98yil5eteu6.png" alt="Container applications on top of docker" width="530" height="443"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fv1vg9vu3tlnaxse52pww.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.amazonaws.com%2Fuploads%2Farticles%2Fv1vg9vu3tlnaxse52pww.png" alt="Application Container" width="468" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of managing multiple application servers which are not may be utilised fully. All containers can reside within a single application server; thereby saving the cost of multiple servers and also ensuring consistency between local and deployed instance. &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.amazonaws.com%2Fuploads%2Farticles%2F6yol0c2fzzbhykjl76kk.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.amazonaws.com%2Fuploads%2Farticles%2F6yol0c2fzzbhykjl76kk.png" alt="Single application server with multiple containers" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A team of a few developers can continue using a single code base to manage the entire application. &lt;/p&gt;

&lt;p&gt;When each module is handled by a different team then splitting the monolithic codebase into fragments can help individual modules to be scaled separately and deployed without dependency. &lt;/p&gt;

&lt;p&gt;MicroFrontend Architecture enables to deploy each module separately and leverage dependent modules at runtime. Each module is deployed separately without any dependency.&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.amazonaws.com%2Fuploads%2Farticles%2Fils51zsprxby51zhejrl.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.amazonaws.com%2Fuploads%2Farticles%2Fils51zsprxby51zhejrl.png" alt="Monolith to Microfrontend" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ModuleFederation works on three core concepts - expose something, consume something and share dependencies.&lt;/p&gt;

&lt;p&gt;MicroFrontend solves a organizational problem not a technical problem. &lt;/p&gt;

&lt;h3&gt;
  
  
  MicroFrontend can be achieved using different techniques:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Publish each app as an NPM package&lt;/li&gt;
&lt;li&gt;Bundle each micro-app individually and load with a script tag&lt;/li&gt;
&lt;li&gt;Using micro frontend frameworks&lt;/li&gt;
&lt;li&gt;Using Module Federation with webpack 5&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  MicroFrontend brings in unique challenges like:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;How to setup inter-communication between individual modules?&lt;/li&gt;
&lt;li&gt;How to share data state ?&lt;/li&gt;
&lt;li&gt;How to fetch the types of federated modules ?&lt;/li&gt;
&lt;li&gt;How to unit test modules dependent on other federated modules ? &lt;/li&gt;
&lt;li&gt;How to improve the developer experience of connecting different federated modules ?&lt;/li&gt;
&lt;li&gt;How to improve the performance ?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We will explore these in future. For now thats all!! Add in your thoughts and queries in comments section.        &lt;/p&gt;

</description>
      <category>frontend</category>
      <category>microfrontend</category>
      <category>scaling</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
