<?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: Michael S.</title>
    <description>The latest articles on DEV Community by Michael S. (@michael_agentic).</description>
    <link>https://dev.to/michael_agentic</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%2F3990402%2Fb2e71e2e-7d9a-4201-af19-dc3a32f53421.jpg</url>
      <title>DEV Community: Michael S.</title>
      <link>https://dev.to/michael_agentic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/michael_agentic"/>
    <language>en</language>
    <item>
      <title>Why your agent's tool list keeps drifting away from your database</title>
      <dc:creator>Michael S.</dc:creator>
      <pubDate>Tue, 11 Aug 2026 08:56:17 +0000</pubDate>
      <link>https://dev.to/michael_agentic/why-your-agents-tool-list-keeps-drifting-away-from-your-database-3hmb</link>
      <guid>https://dev.to/michael_agentic/why-your-agents-tool-list-keeps-drifting-away-from-your-database-3hmb</guid>
      <description>&lt;p&gt;The usual pitch for GraphQL and agents is that the model requests exactly the fields it needs, so you stop over-fetching. That's a bandwidth argument. Bandwidth stopped being the bottleneck in an agent loop roughly the moment agent loops existed — the model is about to spend four seconds and real money thinking about whatever comes back, so 8KB on the wire is not what's hurting you.&lt;/p&gt;

&lt;p&gt;Nearly every GraphQL-for-agents post opens this way, which is why anyone who has shipped one of these stops reading at paragraph two. There's a real argument underneath. It's less flattering to everyone involved, GraphQL included.&lt;/p&gt;

&lt;h2&gt;
  
  
  You are maintaining two schemas
&lt;/h2&gt;

&lt;p&gt;You have Postgres, and Postgres has a schema. It's authoritative, machine-readable, and updated every time you migrate, because that's what migrating means.&lt;/p&gt;

&lt;p&gt;You also have an agent with forty tools, each carrying a name, a description, a JSON Schema for its arguments, and a response shape that lives in someone's head. All of it written by hand, deployed on a cadence that has nothing to do with the database.&lt;/p&gt;

&lt;p&gt;That second pile is also a schema. It's lossy, and it has no mechanism for noticing when the first one moves. Nobody sat down and decided to maintain two schemas — you decided to write forty tools, and this is what forty tools turns out to mean eight months later.&lt;/p&gt;

&lt;p&gt;The complaint you usually hear about this is verbosity, which is the wrong complaint. The real problem is that the drift is silent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change in Postgres&lt;/th&gt;
&lt;th&gt;Hand-written REST tool&lt;/th&gt;
&lt;th&gt;Generated GraphQL schema&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Column added&lt;/td&gt;
&lt;td&gt;Invisible until a human edits the tool&lt;/td&gt;
&lt;td&gt;Queryable immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column renamed&lt;/td&gt;
&lt;td&gt;500, or silently drops the field&lt;/td&gt;
&lt;td&gt;Validation error, by name, before execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table added&lt;/td&gt;
&lt;td&gt;New endpoint, new tool, new description, new deploy&lt;/td&gt;
&lt;td&gt;Queryable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission tightened&lt;/td&gt;
&lt;td&gt;Tool still advertises it; fails at runtime if you're lucky&lt;/td&gt;
&lt;td&gt;Field vanishes from that role's schema&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Column two is where it hurts, because those failures land at runtime, inside a loop, as a strange tool result the model then reasons its way around. Agents are unreasonably good at reasoning around broken tools. It retries, rephrases, tries a neighbouring tool, and eventually hands you a confident summary built on a field that stopped existing in March. You find out when a customer asks why the numbers moved.&lt;/p&gt;

&lt;p&gt;Of those four rows, the renamed column is the one I'd bet on causing your next incident. A rename is the change least likely to trigger anyone's instinct to go check the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introspection, and why "just introspect" is wrong
&lt;/h2&gt;

&lt;p&gt;Introspection is the fix for drift — the agent asks what exists instead of being told in advance. The problem is that introspection is exhaustive by design, and generated schemas are enormous.&lt;/p&gt;

&lt;p&gt;Concrete numbers need a concrete stack, so from here on the examples are Nhost — Postgres with a Hasura GraphQL engine on top, which I'll come back to at the end. Any generator in this class behaves the same way, so translate the commands as needed.&lt;/p&gt;

&lt;p&gt;One table in a generated schema doesn't produce one type. It produces the table type, then &lt;code&gt;_bool_exp&lt;/code&gt;, &lt;code&gt;_order_by&lt;/code&gt;, &lt;code&gt;_insert_input&lt;/code&gt;, &lt;code&gt;_set_input&lt;/code&gt;, &lt;code&gt;_inc_input&lt;/code&gt; if anything is numeric, &lt;code&gt;_on_conflict&lt;/code&gt;, &lt;code&gt;_constraint&lt;/code&gt;, &lt;code&gt;_update_column&lt;/code&gt;, &lt;code&gt;_select_column&lt;/code&gt;, &lt;code&gt;_mutation_response&lt;/code&gt;, &lt;code&gt;_aggregate&lt;/code&gt;, &lt;code&gt;_aggregate_fields&lt;/code&gt;, and a statistical family of &lt;code&gt;_avg_fields&lt;/code&gt;, &lt;code&gt;_max_fields&lt;/code&gt;, &lt;code&gt;_min_fields&lt;/code&gt;, &lt;code&gt;_sum_fields&lt;/code&gt;, &lt;code&gt;_stddev_fields&lt;/code&gt;, &lt;code&gt;_stddev_pop_fields&lt;/code&gt;, &lt;code&gt;_var_samp_fields&lt;/code&gt; and friends. Every table you own carries a type for computing population variance, in case that ever comes up.&lt;/p&gt;

&lt;p&gt;That's north of twenty generated types per table, before any business logic exists. I won't quote a figure from my own project, because the only figure that matters is yours. It's two commands away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nhost schema dump &lt;span class="nt"&gt;--role&lt;/span&gt; admin &lt;span class="nt"&gt;-o&lt;/span&gt; schema.admin.graphqls
nhost schema dump &lt;span class="nt"&gt;--role&lt;/span&gt; user  &lt;span class="nt"&gt;-o&lt;/span&gt; schema.user.graphqls
&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; schema.&lt;span class="k"&gt;*&lt;/span&gt;.graphqls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run those, then put both files through a tokenizer. The admin number is usually an unpleasant surprise. What matters more is the gap between the two — that gap is this entire post, expressed in bytes.&lt;/p&gt;

&lt;p&gt;Piping raw introspection into a context window converts a schema advantage into a token bill. Anyone telling you otherwise is demoing four tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema is the permission boundary
&lt;/h2&gt;

&lt;p&gt;The property that makes any of this worth the trouble rarely comes up in these posts: roles don't only filter rows, they filter the schema. A role without select permission on &lt;code&gt;internal_notes&lt;/code&gt; doesn't get an authorization error when it asks — that field isn't in its version of the type. No permission on a table, and the role doesn't see the table at all.&lt;/p&gt;

&lt;p&gt;Introspecting as &lt;code&gt;agent&lt;/code&gt; therefore returns a genuinely different, much smaller document than introspecting as &lt;code&gt;admin&lt;/code&gt;. The defaults help too. In a zero-trust setup a new role starts with access to nothing and you grant your way up, which compares well against adding a fortieth endpoint to a REST service where the default is whatever the surrounding handlers happened to do.&lt;/p&gt;

&lt;p&gt;Narrowing the role collapses two problems into one, because the context bloat and the blast radius shrink together. You stop hand-curating a tool list and start writing a permission set, and the tool list falls out of it.&lt;/p&gt;

&lt;p&gt;A select permission on &lt;code&gt;invoices&lt;/code&gt; for a role called &lt;code&gt;agent&lt;/code&gt; looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent&lt;/span&gt;
&lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invoices&lt;/span&gt;
&lt;span class="na"&gt;permission&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;# internal_notes is not here. for this role it does not exist.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;id&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;amount&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
  &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;organization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;_eq&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-Hasura-User-Id&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tenant scoping there is a row filter evaluated in the data layer, keyed off a session variable that arrived in a signed JWT. It isn't a &lt;code&gt;WHERE&lt;/code&gt; clause the agent was trusted to remember. And &lt;code&gt;internal_notes&lt;/code&gt; isn't hidden so much as absent — there's no prompt injection that extracts a field the type system doesn't contain. The &lt;code&gt;limit&lt;/code&gt; is doing more work than it looks like, and I'll come back to it.&lt;/p&gt;

&lt;p&gt;The common alternative is a line in a system prompt reading &lt;em&gt;"Only query data belonging to the current user's organization."&lt;/em&gt; That's a request, not a control. It sits in the same context window as untrusted user text, with no enforcement and no audit trail, and its failure rate against adversarial input is well documented by now.&lt;/p&gt;

&lt;p&gt;Hand-rolled REST can enforce isolation properly, obviously, and the question was never whether it can. It's whether it does so in one declarative place every query passes through, or across forty handlers where one is quietly missing a &lt;code&gt;WHERE&lt;/code&gt; clause. Usually it's the latter, and usually the missing one is an endpoint someone added for a quick admin report that was never meant to be permanent.&lt;/p&gt;

&lt;p&gt;None of this comes free either. Deep permission filters compile into the generated SQL, where they can get slow in ways that don't surface until they do. The tell is a query that runs fast as &lt;code&gt;admin&lt;/code&gt; and slow as &lt;code&gt;user&lt;/code&gt; against identical data. Nhost documents this one and points at Postgres JIT compilation as the usual culprit. Worth knowing before you conclude that GraphQL is slow, because it isn't — your permission tree is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that break
&lt;/h2&gt;

&lt;p&gt;Agents write queries no human would type, which produces failure modes GraphQL alone mostly doesn't have. Unbounded lists are the worst of them, because the model has no intuition for cardinality — &lt;code&gt;users&lt;/code&gt; is a word to it, not four hundred thousand rows. That's what the &lt;code&gt;limit&lt;/code&gt; in the permission was for, and it belongs on every select permission a non-human role has, not just the ones you got around to.&lt;/p&gt;

&lt;p&gt;Nesting is close behind, since it costs the agent nothing to write and so it writes it. The containment is a depth limit: on Nhost that's &lt;code&gt;maxDepthQueries&lt;/code&gt; under &lt;code&gt;[graphql.security]&lt;/code&gt;, on a plan that includes it. Self-hosted Hasura puts it behind a tier too, so if yours doesn't have it, you're building a gateway. Set it lower than feels comfortable. An agent has no legitimate reason to go four levels deep, and the queries where it wants to are exactly the ones you'd rather it didn't run.&lt;/p&gt;

&lt;p&gt;The same config block has &lt;code&gt;forbidAdminSecret&lt;/code&gt;, which rejects any request carrying an admin-secret header. Turn it on and "the agent accidentally ran as admin" becomes structurally impossible instead of something you audit for afterwards. The docs warn that enabling it can break deployments, which is a polite way of saying you probably have something running as admin right now that you've forgotten about.&lt;/p&gt;

&lt;p&gt;Mutations are the uglier half, since GraphQL will happily let you insert into three tables in a single call. The partial-failure semantics of that are about as well specified as you'd expect from a spec that mostly declined to have opinions about errors. An agent handed a partial success will improvise.&lt;/p&gt;

&lt;p&gt;I avoid finding out what it improvises: one mutation, one table, one call, an idempotency key. It's boring, and I'm aware it's avoidance rather than a solution. Give the agent a read-only role by default too, with writes behind a second role it has to explicitly assume — introspection makes exploration cheap, and models are curious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I haven't solved
&lt;/h2&gt;

&lt;p&gt;The thing I don't have a good answer for is role sprawl, and it follows directly from everything above. Narrow roles are how you control context and permissions at the same time. Follow that far enough and you get a role per agent task, then per-tenant variations, then one for the reporting agent that's &lt;em&gt;almost&lt;/em&gt; the support agent's role. Now permission metadata is a thing you maintain by hand.&lt;/p&gt;

&lt;p&gt;Which is, you'll notice, structurally the same problem as the hand-maintained tool list. It's smaller, declarative, and lives in one place, so it's a better version of the problem. But I'd be overselling to call it solved, and if you've found a clean pattern here, I'd like to hear it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to ignore all of this
&lt;/h2&gt;

&lt;p&gt;If you have six endpoints, write six tools and skip everything above, because at that size this is over-engineering and you'll feel silly. The threshold isn't a table count but a rate of change. Once your schema moves faster than a human remembers to update tool descriptions, manual maintenance has already lost — you just haven't hit the bug yet.&lt;/p&gt;

&lt;p&gt;Most of the argument evaporates if your data layer isn't relational, since the permission-filtered-schema property is doing the heavy lifting. That property comes from a mature permission layer over a relational store, not from GraphQL as a language. GraphQL over microservices with hand-written resolvers gives you the schema and none of the enforcement, which is the worst version of this trade available.&lt;/p&gt;

&lt;p&gt;And if you're here because agents supposedly work better with GraphQL, they don't especially. Models write mediocre GraphQL alongside their mediocre SQL and their mediocre REST calls. The gain was never query quality — it's that the surface stays honest without anyone maintaining it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concretely
&lt;/h2&gt;

&lt;p&gt;The setup I've been describing is Postgres with a permission-aware GraphQL engine on top, which is what &lt;a href="https://nhost.io" rel="noopener noreferrer"&gt;Nhost&lt;/a&gt; packages — Postgres, GraphQL, auth, storage, functions, event triggers. The part that matters here is that auth and the permission layer are the same system. So &lt;code&gt;x-hasura-user-id&lt;/code&gt; in that row filter is a claim from a signed token, not something an application layer asserted and hoped was right.&lt;/p&gt;

&lt;p&gt;It doesn't solve the context problem, and it won't stop your agent writing a stupid query. You still have to design the roles, which is where the actual work lives. Most of the value in any of this is one afternoon spent deciding what the &lt;code&gt;agent&lt;/code&gt; role is allowed to see, and no platform does that afternoon for you.&lt;/p&gt;

&lt;p&gt;What you get is the boundary in one declarative place instead of forty handlers, and a schema that can't drift from its permissions because they're the same object.&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>ai</category>
      <category>postgres</category>
      <category>agents</category>
    </item>
    <item>
      <title>What you actually need to ship an AI agent</title>
      <dc:creator>Michael S.</dc:creator>
      <pubDate>Thu, 18 Jun 2026 07:37:30 +0000</pubDate>
      <link>https://dev.to/michael_agentic/what-you-actually-need-to-ship-an-ai-agent-3a0h</link>
      <guid>https://dev.to/michael_agentic/what-you-actually-need-to-ship-an-ai-agent-3a0h</guid>
      <description>&lt;p&gt;Everyone's building agents. Half of them are running. The other half have "active plans."&lt;/p&gt;

&lt;p&gt;I've been in both camps. The difference isn't the model. Models have been good enough for a while now. It's everything around the model that nobody talks about in tutorials because tutorials end when the demo works.&lt;/p&gt;

&lt;p&gt;This is the stuff that bit me. Take it or leave it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why build an agent at all
&lt;/h2&gt;

&lt;p&gt;Worth asking before you pick a framework.&lt;/p&gt;

&lt;p&gt;The cases where agents actually make sense — and I mean actually get used, not just demoed — are pretty narrow:&lt;/p&gt;

&lt;p&gt;You have work that's too variable for a simple automation but too repetitive for a human to do 500 times a day. Customer support triage where the agent needs to know who the user is, what plan they're on, what happened in their last three sessions. Internal ops: pull from four systems, write a Slack summary, done. SaaS features where "AI that knows your account" is the actual value, not a generic chatbot bolted on.&lt;/p&gt;

&lt;p&gt;What all of these have in common: the agent needs to remember things. Needs to know who's asking. Needs to not lose its mind when a tool call fails or an LLM provider has a bad afternoon.&lt;/p&gt;

&lt;p&gt;Everything below is about making that work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Orchestration
&lt;/h2&gt;

&lt;p&gt;Use LangGraph. Not because it's elegant (it's not always), but because it handles the stuff that kills you in production and nothing else does it as well right now.&lt;/p&gt;

&lt;p&gt;The things that matter: state persists across crashes (you don't restart from zero when something hiccups). You can pause mid-execution, wait for a human to approve something, resume. Parallel tool calls without data races. Explicit control flow so you actually know what's running.&lt;/p&gt;

&lt;p&gt;Here's what setting up LangGraph with Postgres checkpointing actually looks like. This is the part that makes your agent survive crashes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.checkpoint.postgres&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PostgresSaver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg_pool&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ConnectionPool&lt;/span&gt;

&lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConnectionPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conninfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NHOST_DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;checkpointer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PostgresSaver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;act&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;act_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;act&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;act&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# every run is now resumable: crash mid-execution, pick up from last checkpoint
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;HumanMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;)]},&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;configurable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;  &lt;span class="c1"&gt;# per-user state
&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;thread_id&lt;/code&gt; is the key thing here. Pass the user's ID and every session is isolated, resumable, and persisted automatically.&lt;/p&gt;

&lt;p&gt;The thing nobody warns you about: you can massively over-engineer simple things with it. I've seen a FAQ chatbot end up as a 14-node state graph. LangGraph doesn't stop you from doing that. You have to stop yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to consider something else:&lt;/strong&gt; if you're TypeScript-first, look at Mastra before committing. TypeScript-native, growing fast, better DX in a few ways. If you're in an enterprise org that already runs Temporal for workflow orchestration, you might be better off building agent steps as Temporal activities than introducing another stateful runtime. LangGraph is the highest-confidence bet for a new project but it's not a law.&lt;/p&gt;




&lt;h2&gt;
  
  
  MCP and tools
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol, Anthropic, late 2024) is the right idea: one protocol for connecting agents to external services instead of custom glue for every tool. GitHub, Slack, Nhost, Google Drive — most have MCP servers now. You connect your agent once and swap tools without rewriting integrations.&lt;/p&gt;

&lt;p&gt;The ecosystem is real. The maturity is uneven and I want to be honest about that.&lt;/p&gt;

&lt;p&gt;Community MCP servers vary a lot. Some are solid and actively maintained. Some are a weekend project that hasn't been touched in eight months. A few have had genuinely bad security issues. One package shipped clean for 15 versions then added exfiltration code in version 16 (CVE-2025-6514, CVSS 9.6). Anthropic's own official Git MCP server shipped with three CVEs including one that got you RCE through prompt injection. Not a community project. Anthropic's reference implementation.&lt;/p&gt;

&lt;p&gt;Treat MCP servers like npm packages: pin versions, audit what they're doing, don't blindly trust community servers for anything that touches sensitive data.&lt;/p&gt;

&lt;p&gt;For your own internal business logic: write your own MCP servers. It's simpler than it sounds and means your agent talks to your own systems through the same interface as everything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory
&lt;/h2&gt;

&lt;p&gt;This is the part of the stack everyone underestimates — and I'm still figuring it out.&lt;/p&gt;

&lt;p&gt;Here's the actual problem: LLMs are stateless by default. Every API call starts from zero. For a demo this is fine. For an agent that's supposed to know who you are and remember that you hate long responses, it's not fine.&lt;/p&gt;

&lt;p&gt;Short-term memory (within a session) is handled by LangGraph's checkpointer. Store it in Postgres. Not interesting, just works.&lt;/p&gt;

&lt;p&gt;Long-term memory (across sessions) is where it gets real. You need two things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User session ends
       │
       ▼
 ┌─────────────────────────────────────────────┐
 │            What do we store?                │
 └──────────────┬──────────────────────────────┘
                │
       ┌────────┴─────────┐
       ▼                  ▼
  Structured           Semantic
  (Postgres)          (pgvector)
       │                  │
  "user prefers      "last month user
  short responses"    said their budget
  "plan: pro"         was under $10k"
  "timezone: UTC+2"   similarity search
       │                  │
       └────────┬─────────┘
                ▼
     injected into next session's
     system prompt or tool context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you go pure vector-only because it feels modern, you lose queryability and auditability. You end up with a blob of embeddings you can't inspect or debug. Use both.&lt;/p&gt;

&lt;p&gt;Writing to both from inside an agent node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_memory_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# structured fact → Postgres
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nhost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;graphql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        mutation UpsertMemory($userId: uuid!, $key: String!, $value: String!) {
            insert_user_memory_one(
                object: {user_id: $userId, key: $key, value: $value},
                on_conflict: {constraint: user_memory_pkey, update_columns: [value]}
            ) { id }
        }
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_preference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# semantic memory → pgvector
&lt;/span&gt;    &lt;span class="n"&gt;embedding&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;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nhost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;graphql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        mutation InsertEmbedding($userId: uuid!, $content: String!, $embedding: vector!) {
            insert_memory_embedding_one(
                object: {user_id: $userId, content: $content, embedding: $embedding}
            ) { id }
        }
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not claiming this is the cleanest possible implementation. It's just what the actual write path looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What nobody tells you until you're deep in it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflicting memories.&lt;/strong&gt; User says "keep it short" in February. In April they say "I need more detail on this." Which one wins? There's no clean answer to this and I don't think anyone has one. You're making judgment calls in your memory logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hallucinated memories.&lt;/strong&gt; LLMs can "remember" things you never stored. This happens in production and it's unsettling when you first see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory bloat.&lt;/strong&gt; You can't just keep appending forever. At some point you need summarization, forgetting, or tiered retrieval. When exactly? What do you summarize vs keep verbatim? How do you decide what to drop? Open questions. Every team doing this seriously has custom logic.&lt;/p&gt;

&lt;p&gt;Mem0 is trying to solve some of this. Worth checking its current state. It was promising but I wouldn't call it "plug this in and you're done" yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The backend nobody plans for until it's too late
&lt;/h2&gt;

&lt;p&gt;Most agent tutorials treat the backend as "somewhere you store stuff." In practice your agent needs four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A database with vector search (memory, covered above)&lt;/li&gt;
&lt;li&gt;User identity: who is this person, what are they allowed to do, what's their plan tier&lt;/li&gt;
&lt;li&gt;File storage: documents they upload, things the agent generates, outputs you need to persist&lt;/li&gt;
&lt;li&gt;Serverless functions for your own business logic, because not every tool is a third-party API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can stitch these together from separate services. Supabase for the DB, something else for auth, S3 for files, Lambda for functions. It works. It's also four systems to maintain, four permission models to keep in sync, four things that can drift out of alignment.&lt;/p&gt;

&lt;p&gt;I use Nhost because it's all of this in one place: Postgres, pgvector, Auth, Storage, Functions, with a consistent permissions model and an MCP server so the agent can interact with all of it through a single interface. Less surface area, same capabilities.&lt;/p&gt;

&lt;p&gt;Not the only answer. The argument is coherence, not uniqueness.&lt;/p&gt;




&lt;h2&gt;
  
  
  LLM Gateway
&lt;/h2&gt;

&lt;p&gt;I skipped this on an earlier project. Then Anthropic had a three-hour incident and the product was down. That was the last time I skipped it.&lt;/p&gt;

&lt;p&gt;What a gateway does: sits between your orchestrator and the model API. Handles fallback (Anthropic down → route to GPT-4o automatically), caching (same prompt hits cache instead of costing another call), per-session cost limits (a runaway agent loop can rack up hundreds of dollars before anyone notices, and that's not hypothetical), and load balancing across API keys at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LiteLLM&lt;/strong&gt; is what I'd start with. Open-source, self-hosted, 100+ providers behind a unified OpenAI-compatible API. Takes an afternoon to set up. Covers everything you need early on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portkey&lt;/strong&gt; when you need more: guardrails, PII redaction, audit trails, more sophisticated routing policies. Went fully open-source in early 2026.&lt;/p&gt;

&lt;p&gt;Rough heuristic I've seen cited: below ~$10K/month in LLM spend you can get away with a simple wrapper. Above that, treat the gateway as infrastructure, not an optional add-on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The model
&lt;/h2&gt;

&lt;p&gt;Pick Claude or GPT-4o and start. This layer is genuinely commoditizing.&lt;/p&gt;

&lt;p&gt;What still matters: tool calling reliability is not equal across models. Multi-step agentic tool use, long chains, recovery from bad tool outputs — Claude 3.5 Sonnet is the most consistent in my experience. GPT-4o is close. Open-weight models are better than they were and will keep getting better but they still lag on complex recovery scenarios. The gap narrowed. It's not gone.&lt;/p&gt;

&lt;p&gt;Cost at scale: route by step complexity. Simple classification or routing steps don't need the big model. Haiku or GPT-4o mini for those, expensive model for the reasoning steps. If your agent makes 25 LLM calls per session and 20 of them are simple, you're wasting money.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;This section didn't exist in the 2024 version of guides like this. It does now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The attack you need to understand: indirect prompt injection.&lt;/strong&gt; Your agent fetches a document. That document contains hidden instructions. The model executes them because it can't distinguish between content and instructions in context.&lt;/p&gt;

&lt;p&gt;This is not theoretical. Supabase's Cursor agent processed support tickets with embedded SQL to exfiltrate integration tokens. Attackers submitted support tickets containing the attack payload. The agent had privileged DB access and trusted what it read.&lt;/p&gt;

&lt;p&gt;Anthropic's own Git MCP server shipped CVEs that allowed RCE through prompt injection. Path traversal, argument injection, repo scoping bypass. If the reference implementation shipped with that, assume third-party community MCP servers are higher risk.&lt;/p&gt;

&lt;p&gt;There's no complete defense because this is architectural — it's a property of how LLM context windows work, not a bug you can patch. What you can do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Least privilege.&lt;/strong&gt; If the agent only needs to read, give it read access. Don't grant write "just in case."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat tool outputs as untrusted.&lt;/strong&gt; Validate and sanitize MCP responses before they enter the model context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate irreversible actions.&lt;/strong&gt; Before the agent sends an email or modifies a payment record, validate against policy. Require explicit user confirmation where stakes are high.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vet MCP servers.&lt;/strong&gt; Pin versions. Audit what they're doing before putting them near anything sensitive.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Observability
&lt;/h2&gt;

&lt;p&gt;I always wire this up last. I always regret it.&lt;/p&gt;

&lt;p&gt;Agents fail silently in ways that are hard to catch. A normal API returns a 500. An agent that retrieves the wrong memory or calls the wrong tool returns something that looks like a valid response. The failure is invisible until a user notices or until you look at the numbers and something's off.&lt;/p&gt;

&lt;p&gt;You need step-level traces: which node ran, what tool was called with what exact input, what it returned, what was in the prompt at that moment, how long each step took, what it cost. Not logs. Traces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangSmith&lt;/strong&gt; is what I use. Native to LangGraph, one environment variable, and the trace UI is genuinely good. The lock-in is real — it's LangChain's product. But nothing else is as functional for this specifically right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open-source alternative: Langtrace.&lt;/strong&gt; OpenTelemetry-compatible, you own the data, integrates with Grafana or Datadog. More setup, less polished UI.&lt;/p&gt;

&lt;p&gt;One thing to actually instrument yourself: correlation between agent traces and user sessions. You want to be able to take a user complaint, look up their session, and see the full chain of what happened. This doesn't come for free. Wire it up early.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's still a mess
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Memory architecture.&lt;/strong&gt; Nobody has the clean answer. How to handle conflicting signals, what to summarize vs keep, when to forget: every team doing this seriously has custom logic. If someone's selling you a complete solution, probe it hard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evals.&lt;/strong&gt; Most teams still rely on human review because automated evaluation of open-ended agent behavior is genuinely hard to build well. Building eval datasets that catch real regressions and not just happy-path behavior takes real investment most teams don't make until something goes wrong in prod.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent patterns.&lt;/strong&gt; Planner-executor setups, debate loops, agent hierarchies: people are using all of these in production. No consensus on when to use which. Evolving fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP security.&lt;/strong&gt; The protocol is less than two years old. CVEs are appearing. Stay current.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; A 20-step agent loop at 1,000 sessions/day is real money. Build token budget controls before you need them, not after you get the bill.&lt;/p&gt;




&lt;p&gt;The model is maybe 10% of why agents succeed or fail in production.&lt;/p&gt;

&lt;p&gt;The rest is whether it remembers things, whether it knows who it's talking to, whether you can see what it's doing when something goes wrong, and whether it doesn't get compromised by content it reads from the world.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stack: LangGraph (or Mastra) · Claude · LiteLLM/Portkey · Nhost (Postgres + pgvector + Auth + Storage + Functions) · MCP · LangSmith (or Langtrace) · Vercel AI SDK&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>postgres</category>
      <category>graphql</category>
    </item>
  </channel>
</rss>
