<?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: Ahmedulla</title>
    <description>The latest articles on DEV Community by Ahmedulla (@ahmed18).</description>
    <link>https://dev.to/ahmed18</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%2F4191528%2F5e3862a7-f705-4bc1-bff9-4edbc2d1a3a2.png</url>
      <title>DEV Community: Ahmedulla</title>
      <link>https://dev.to/ahmed18</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmed18"/>
    <language>en</language>
    <item>
      <title>Blast Radius: many alerts, one cause — building AI on-call agents on a graph</title>
      <dc:creator>Ahmedulla</dc:creator>
      <pubDate>Sun, 18 Oct 2026 05:50:39 +0000</pubDate>
      <link>https://dev.to/ahmed18/eight-services-alerting-one-cause-building-ai-on-call-agents-on-a-graph-15d7</link>
      <guid>https://dev.to/ahmed18/eight-services-alerting-one-cause-building-ai-on-call-agents-on-a-graph-15d7</guid>
      <description>&lt;p&gt;It's 2 AM. Your phone goes off. Checkout is failing. So is the frontend, the payment service, the proxy in front of&lt;br&gt;
everything, and a couple of services you've never touched. A wall of alerts, and only one of them is the actual problem.&lt;/p&gt;

&lt;p&gt;Before you can fix anything, you have to answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Which service is the cause&lt;/strong&gt;, and which ones are just victims?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changed&lt;/strong&gt; recently near it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt; should be woken up?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of those answers sits in one place. They're in the &lt;em&gt;connections&lt;/em&gt;. Service A calls B, B reads from database C, C&lt;br&gt;
was changed by a pull request two hours ago, the author of that PR is on the team that owns C, and this week that&lt;br&gt;
team's on-call is someone else entirely. That's a graph problem. So I built the agents on a graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast Radius&lt;/strong&gt; is a small team of AI on-call agents that use &lt;a href="https://www.falkordb.com/" rel="noopener noreferrer"&gt;FalkorDB&lt;/a&gt; as their&lt;br&gt;
context, their memory and their audit trail. I built it for Graph Hacks 2026 (WeMakeDevs × FalkorDB). This post&lt;br&gt;
walks through the graph model, the queries doing the real work, how the agents remember past incidents, and how I&lt;br&gt;
kept the whole thing running on free-tier LLMs.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/AgentAhmed/blast-radius" rel="noopener noreferrer"&gt;https://github.com/AgentAhmed/blast-radius&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Agent questions are graph questions
&lt;/h2&gt;

&lt;p&gt;Here's what the investigator agent needs to know during an incident, and the shape of the query behind each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Graph shape&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What breaks if &lt;code&gt;payment&lt;/code&gt; fails?&lt;/td&gt;
&lt;td&gt;variable-length path, walked &lt;em&gt;backwards&lt;/em&gt; along dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What could cause &lt;em&gt;all&lt;/em&gt; of these alerts?&lt;/td&gt;
&lt;td&gt;variable-length path &lt;em&gt;forward&lt;/em&gt; from every alerting service, counted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What changed near it?&lt;/td&gt;
&lt;td&gt;one hop from component to &lt;code&gt;PullRequest&lt;/code&gt;, filtered by time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who's on call for it?&lt;/td&gt;
&lt;td&gt;component → team → on-call rotation → person&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Have we seen this before?&lt;/td&gt;
&lt;td&gt;vector search over past incidents, re-ranked by graph structure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In SQL, most of these are recursive CTEs or several round trips. In Cypher, each one is a few lines. That matters&lt;br&gt;
twice: an LLM can call it as a tool, and a human can read it when the agent shows its working.&lt;/p&gt;
&lt;h2&gt;
  
  
  The graph model
&lt;/h2&gt;

&lt;p&gt;Each organisation gets its own graph (&lt;code&gt;org_&amp;lt;tenant&amp;gt;&lt;/code&gt;) in the same FalkorDB instance, so memory never leaks between&lt;br&gt;
companies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Topology:&lt;/strong&gt; &lt;code&gt;Component&lt;/code&gt; nodes (services, datastores, Kafka topics) with &lt;code&gt;CALLS&lt;/code&gt;, &lt;code&gt;READS_WRITES&lt;/code&gt;, &lt;code&gt;PUBLISHES&lt;/code&gt; and
&lt;code&gt;CONSUMES&lt;/code&gt; edges, plus &lt;code&gt;SERVES&lt;/code&gt; edges to the user flows they power (checkout, browse, cart).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;People and ownership:&lt;/strong&gt; &lt;code&gt;Person&lt;/code&gt; -&lt;code&gt;MEMBER_OF&lt;/code&gt;-&amp;gt; &lt;code&gt;Team&lt;/code&gt; -&lt;code&gt;OWNS&lt;/code&gt;-&amp;gt; &lt;code&gt;Component&lt;/code&gt;, and on-call shifts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change:&lt;/strong&gt; &lt;code&gt;PullRequest&lt;/code&gt; -&lt;code&gt;MODIFIES&lt;/code&gt;-&amp;gt; &lt;code&gt;Component&lt;/code&gt;, &lt;code&gt;Person&lt;/code&gt; -&lt;code&gt;AUTHORED&lt;/code&gt;-&amp;gt; &lt;code&gt;PullRequest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge:&lt;/strong&gt; design decisions (ADRs), runbooks and documents, linked to the components they describe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; &lt;code&gt;Incident&lt;/code&gt;, &lt;code&gt;Alert&lt;/code&gt;, &lt;code&gt;Action&lt;/code&gt; and &lt;code&gt;Lesson&lt;/code&gt;, with &lt;code&gt;ROOT_CAUSE&lt;/code&gt;, &lt;code&gt;IMPACTED&lt;/code&gt; and &lt;code&gt;RESOLVED_BY&lt;/code&gt; edges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agents themselves:&lt;/strong&gt; &lt;code&gt;AgentRun&lt;/code&gt; and &lt;code&gt;Step&lt;/code&gt; nodes for every LLM call and tool call, &lt;code&gt;Task&lt;/code&gt; nodes for hand-offs,
and &lt;code&gt;SUPPORTED_BY&lt;/code&gt; edges from a finding to the exact things it cites.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three modelling decisions mattered more than the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent state lives in the same graph as the data.&lt;/strong&gt; A step that ran a query has &lt;code&gt;TOUCHED&lt;/code&gt; edges to the nodes it&lt;br&gt;
read. "Show me every incident where the agent looked at this PR" is just a query, not a log search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence is edges, not text.&lt;/strong&gt; A finding doesn't only &lt;em&gt;say&lt;/em&gt; a PR caused it. It has a &lt;code&gt;SUPPORTED_BY&lt;/code&gt; edge to that&lt;br&gt;
PR. The UI can draw it, and the agent can't cite something that doesn't exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timestamps are stored as numbers but shown to the LLM as "2h ago".&lt;/strong&gt; Small models are surprisingly bad at epoch&lt;br&gt;
arithmetic. This one change fixed several wrong answers in testing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The root-cause query
&lt;/h2&gt;

&lt;p&gt;This is the heart of the project. Given the alerting components, find what they have in common &lt;em&gt;downstream&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;UNWIND&lt;/span&gt; &lt;span class="n"&gt;$alerting&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;alerting_name&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;alerting_name&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:CALLS&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;READS_WRITES&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;PUBLISHES&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;CONSUMES&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;covers&lt;/span&gt;
&lt;span class="k"&gt;OPTIONAL&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:CALLS&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;READS_WRITES&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;PUBLISHES&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;CONSUMES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;d:&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;d.name&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$alerting&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;covers&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;alerting_deps&lt;/span&gt;
&lt;span class="k"&gt;OPTIONAL&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:MODIFIES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;pr:&lt;/span&gt;&lt;span class="n"&gt;PullRequest&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pr.merged_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;$since&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;covers&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alerting_deps&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;recent_changes&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;c.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;covers&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c.name&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$alerting&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_alerting&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alerting_deps&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recent_changes&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;covers&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_alerting&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alerting_deps&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recent_changes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;$limit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ordering reads the way an SRE thinks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;covers:&lt;/strong&gt; how many alerting services depend on it. The real cause should explain &lt;em&gt;all&lt;/em&gt; the symptoms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;is_alerting:&lt;/strong&gt; it's showing symptoms itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;alerting_deps&lt;/strong&gt; (fewer is better): if its own dependencies are alerting, it's probably a victim too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recent_changes:&lt;/strong&gt; something merged nearby recently is suspicious.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;*0..6&lt;/code&gt; is deliberate. Zero hops means an alerting service can be its own root cause.&lt;/p&gt;

&lt;p&gt;My favourite test case is the OpenTelemetry Demo's &lt;code&gt;kafkaQueueProblems&lt;/code&gt; fault. The services that alert,&lt;br&gt;
&lt;code&gt;accounting&lt;/code&gt; and &lt;code&gt;fraud-detection&lt;/code&gt;, don't call each other. The only thing they share is the Kafka topic &lt;code&gt;orders&lt;/code&gt;,&lt;br&gt;
and &lt;strong&gt;the topic never alerts&lt;/strong&gt;. Searching logs would never point at it. The query does, because both consumers&lt;br&gt;
depend on it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Don't let the LLM ignore the graph
&lt;/h2&gt;

&lt;p&gt;The LLM (a free-tier open model) decides which tools to call and writes the final finding. Early on, it would&lt;br&gt;
sometimes run the right query, get the right answer back, and then submit something else. Usually the loudest&lt;br&gt;
alerting service.&lt;/p&gt;

&lt;p&gt;So now the graph checks the answer. When the investigator submits a root cause, the coverage check runs again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If another component explains &lt;strong&gt;more&lt;/strong&gt; of the alerts, the answer goes back once, with numbers: "only 2 of 4
alerting components depend on &lt;code&gt;checkout&lt;/code&gt;, while &lt;code&gt;payment&lt;/code&gt; is a dependency of 4".&lt;/li&gt;
&lt;li&gt;After one rejection, the next answer is accepted. The graph gives a second opinion, it doesn't get a veto. The
model might know something the graph doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single check fixed every miss in my evaluation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Memory that makes the second incident faster
&lt;/h2&gt;

&lt;p&gt;When an incident is resolved, a scribe agent writes a postmortem and a lesson, and links them to the root cause and&lt;br&gt;
the fix. Next time something similar happens, the investigator calls the similar-incidents tool, which mixes vector&lt;br&gt;
search with graph structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;db.idx.vector.queryNodes&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Incident'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'embedding'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$k&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vecf32&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$vec&lt;/span&gt;&lt;span class="ss"&gt;))&lt;/span&gt; &lt;span class="k"&gt;YIELD&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;text_sim&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;i.id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;$exclude&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'resolved'&lt;/span&gt;
&lt;span class="k"&gt;OPTIONAL&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ROOT_CAUSE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;rc:&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;i.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rc.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixes&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
       &lt;span class="mf"&gt;0.6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;text_sim&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;same_root&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Text similarity alone mixes up "payment errors" and "payment latency". Adding "same root-cause component" and&lt;br&gt;
"overlapping impacted services" from the graph makes the match a lot sharper. One FalkorDB detail that cost me&lt;br&gt;
some time: &lt;code&gt;queryNodes&lt;/code&gt; returns cosine &lt;em&gt;distance&lt;/em&gt;, so similarity is &lt;code&gt;1 - score&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The embeddings are local (&lt;code&gt;bge-small-en-v1.5&lt;/code&gt; through fastembed). Recall costs nothing and never hits a rate limit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Agents that hand off work through the graph
&lt;/h2&gt;

&lt;p&gt;There are four agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Triage&lt;/strong&gt; groups alerts into an incident and sets the severity. No LLM: it's pure graph logic, so it's instant
and free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Investigator&lt;/strong&gt; finds the cause and acts on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scribe&lt;/strong&gt; writes memory after a human resolves the incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Company brain&lt;/strong&gt; answers questions like "why does checkout publish to Kafka, and who decided that?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They never call each other directly. They coordinate through &lt;code&gt;Task&lt;/code&gt; nodes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triage creates an &lt;code&gt;investigate&lt;/code&gt; task.&lt;/li&gt;
&lt;li&gt;A worker claims it with one Cypher query that grabs the oldest open task and marks it &lt;code&gt;claimed&lt;/code&gt;. FalkorDB applies
writes to a graph one at a time, so two workers can't claim the same task.&lt;/li&gt;
&lt;li&gt;When a human resolves the incident, a &lt;code&gt;record&lt;/code&gt; task hands it to the scribe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a worker dies, the task is still sitting in the graph, and anyone can see who is working on what.&lt;/p&gt;
&lt;h2&gt;
  
  
  Surviving free-tier rate limits
&lt;/h2&gt;

&lt;p&gt;I wanted this to run on free LLM tiers, which means rate limits aren't an edge case, they're normal. Two things make&lt;br&gt;
it work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A fallback chain.&lt;/strong&gt; Groq first, then any other configured provider (Gemini, OpenRouter, OpenAI, Claude, or any&lt;br&gt;
OpenAI-compatible endpoint), then a local Ollama model. Errors are classified (rate limited, bad request, bad key,&lt;br&gt;
model not found, provider down), and each model gets a cooldown instead of being hammered in a retry loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoints in the graph.&lt;/strong&gt; Every LLM response and every tool result is a &lt;code&gt;Step&lt;/code&gt; node, chained with &lt;code&gt;NEXT&lt;/code&gt;. When&lt;br&gt;
every provider is exhausted, the run is marked &lt;code&gt;paused&lt;/code&gt;. Later, the worker rebuilds the conversation from the stored&lt;br&gt;
steps and carries on exactly where it stopped. Nothing is re-queried and nothing is lost.&lt;/p&gt;

&lt;p&gt;One bug I only found by testing on the real free tier: after a long pause, a resumed run sometimes kept calling&lt;br&gt;
query tools until it ran out of budget, and finished with no answer at all. The fix was small. Once the budget is&lt;br&gt;
reached, the model is offered &lt;strong&gt;only&lt;/strong&gt; the final-answer tool. It can't wander off any more, so it summarises what it&lt;br&gt;
already found.&lt;/p&gt;
&lt;h2&gt;
  
  
  Does it work?
&lt;/h2&gt;

&lt;p&gt;I wrote an evaluation that injects each of the OpenTelemetry Demo's 8 fault flags into a fresh graph, runs the real&lt;br&gt;
investigator, and scores it against hidden ground truth. Then it resolves the incident and injects the same fault&lt;br&gt;
again to test memory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;First occurrence&lt;/th&gt;
&lt;th&gt;Same fault again&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Root cause correct&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8/8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8/8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cited the incident it had just learned from&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7/8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg LLM calls / graph queries&lt;/td&gt;
&lt;td&gt;5.6 / 4.5&lt;/td&gt;
&lt;td&gt;5.0 / 4.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;During that run, the free tier forced 24 model fallbacks and 34 full pauses. Every paused run resumed from its&lt;br&gt;
checkpoint and finished.&lt;/p&gt;

&lt;p&gt;It's an honest but small test: 8 simulated faults on a realistic topology. Real incidents are messier, with partial&lt;br&gt;
outages and several things breaking at once.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real data, not just a demo
&lt;/h2&gt;

&lt;p&gt;A demo dataset only proves so much, so there are two ways in for real data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; point it at any public repo. Services and dependencies come from its compose files (including
hostnames in environment variables like &lt;code&gt;http://payment:8080&lt;/code&gt;), owners from &lt;code&gt;CODEOWNERS&lt;/code&gt;, and changes and experts
from merged PRs, mapped to components by file path. On the OpenTelemetry Demo repo it found 26 components and 43
dependencies. GitHub logins are stored as stable pseudonyms by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts:&lt;/strong&gt; an Alertmanager-compatible webhook, which Grafana can post to as well. Labels like &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt; or
a pod name (&lt;code&gt;checkout-5c9d7f-q2w8r&lt;/code&gt;) are matched to components. Anything that doesn't match is reported back,
never guessed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the agents act in real tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A GitHub issue&lt;/strong&gt; with the root cause, the evidence and the recommended fix. The token is fine-grained: one
repository, Issues permission only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Slack or Discord message&lt;/strong&gt; when the cause is identified (severity, root cause, confidence, who was paged, the
issue link), and another when it's resolved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every action has a fixed id per incident, so a retried or resumed run never pages twice or opens a duplicate issue.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give the LLM queries, not the database.&lt;/strong&gt;I gave the model sixteen named, tested graph tools instead of letting it write Cypher. Every query is one I can test and explain, the UI shows the exact query behind each step, and a model can't run a query I didn't write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the graph check the model&lt;/strong&gt;, not just feed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep agent state next to the data.&lt;/strong&gt; Checkpoints, hand-offs and evidence as nodes and edges gave me resume after
a crash, provenance and an audit trail almost for free.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Building dependencies from live OpenTelemetry traces instead of compose files, using resolved incidents to tune the&lt;br&gt;
ranking weights, and letting the agent run approved fixes (a feature-flag flip or a rollback) behind a human&lt;br&gt;
approval step.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It runs locally with Docker and a free Groq key, or Ollama with no key at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AgentAhmed/blast-radius &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;blast-radius
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="c"&gt;# then follow the Quick start in the README&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inject &lt;code&gt;paymentFailure&lt;/code&gt;, watch the investigator trace it, resolve it, then inject it again and watch it remember.&lt;/p&gt;

&lt;p&gt;Here's the full demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/GGMcWBU6z0c" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The code, the Cypher queries, a design document and the evaluation are all on GitHub:&lt;br&gt;
&lt;a href="https://github.com/AgentAhmed/blast-radius" rel="noopener noreferrer"&gt;https://github.com/AgentAhmed/blast-radius&lt;/a&gt;&lt;/p&gt;

</description>
      <category>falkordb</category>
      <category>ai</category>
      <category>devops</category>
      <category>graphdatabase</category>
    </item>
  </channel>
</rss>
