<?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: Zaher Khateeb</title>
    <description>The latest articles on DEV Community by Zaher Khateeb (@zahere).</description>
    <link>https://dev.to/zahere</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%2F4030596%2Faf699966-fe1a-4785-bb3e-93ad76d87307.jpg</url>
      <title>DEV Community: Zaher Khateeb</title>
      <link>https://dev.to/zahere</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zahere"/>
    <language>en</language>
    <item>
      <title>I Paid an LLM to Sabotage My Multi-Agent System (On Purpose)</title>
      <dc:creator>Zaher Khateeb</dc:creator>
      <pubDate>Wed, 15 Jul 2026 17:00:02 +0000</pubDate>
      <link>https://dev.to/zahere/i-paid-an-llm-to-sabotage-my-multi-agent-system-on-purpose-1oo8</link>
      <guid>https://dev.to/zahere/i-paid-an-llm-to-sabotage-my-multi-agent-system-on-purpose-1oo8</guid>
      <description>&lt;p&gt;The most dangerous agent in a multi-agent system isn't the one that crashes. It's the one that keeps talking.&lt;/p&gt;

&lt;p&gt;I've spent the last year and a half building multi-agent infrastructure, and somewhere along the way I noticed something uncomfortable: the topology you pick for your agent mesh — who talks to whom — is quietly one of the biggest resilience decisions you make. And almost nobody tests it. We chaos-test our microservices, we kill pods in staging, we run game days for our databases. Then we wire five LLM agents together in whatever shape the framework tutorial suggested and ship it.&lt;/p&gt;

&lt;p&gt;So for the Nebius Serverless AI Builders Challenge I built &lt;strong&gt;Balagan&lt;/strong&gt; (Hebrew for "chaos" — if you know, you know): a chaos engineering harness for agent meshes. It injects faults into multi-agent systems and measures what happens to the quality of their collective decisions. This post covers what I built, why serverless was genuinely the right substrate for it rather than a checkbox, and what the heatmap says.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/zahere/balagan" rel="noopener noreferrer"&gt;https://github.com/zahere/balagan&lt;/a&gt; · runs in 60 seconds offline in &lt;code&gt;--mock&lt;/code&gt; mode, or end-to-end on Nebius Serverless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;Three topologies, same task, same model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flat&lt;/strong&gt; — all-to-all debate, two rounds, strict majority vote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hierarchical&lt;/strong&gt; — four workers answer independently; one aggregator issues the final verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ring&lt;/strong&gt; — sequential relay; each agent refines its predecessor's reasoning; the last agent decides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three conditions per topology:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline&lt;/strong&gt; — everyone behaves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crash-stop&lt;/strong&gt; — one random agent is silent for the whole trial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Byzantine&lt;/strong&gt; — one random agent stays chatty but is covertly instructed to work out the right answer and argue for the opposite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is my favorite implementation detail: &lt;strong&gt;the traitor is just a system prompt.&lt;/strong&gt; No forked model, no weight surgery — one prompt swap turns an honest verifier into a confident, articulate saboteur. If that doesn't slightly unsettle you about production agent systems, read it again.&lt;/p&gt;

&lt;p&gt;The panel is five Qwen2.5-7B-Instruct agents at temperature 0, and everything is seeded — the dataset regenerates byte-identically, the victim of each trial is drawn from a per-trial RNG, and 25 trials fill every (topology × fault) cell. The task is deliberately easy — claim verification with objective ground truth on a deterministic synthetic dataset. That's methodology, not laziness: when the fault-free condition sits at 96–100% accuracy, every point of degradation you see under faults is attributable to the fault, not to the task being hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why serverless, actually
&lt;/h2&gt;

&lt;p&gt;Here's the shape of the workload: 225 independent trials, each a burst of 5–10 LLM calls, embarrassingly parallel, needed exactly once per model. That's a terrible fit for a rented GPU box (idle between bursts, babysitting required) and a perfect fit for two serverless primitives doing what they're each for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Nebius Serverless Endpoint&lt;/strong&gt; serves Qwen2.5-7B-Instruct via vLLM — the GPU lives exactly as long as the model needs to be hot, and it's an OpenAI-compatible URL, so the harness doesn't know or care that it's on Nebius.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Nebius Serverless Job&lt;/strong&gt; runs the sweep itself — a &lt;strong&gt;CPU-only&lt;/strong&gt; container that fans out trials with bounded concurrency, then releases compute the moment the sweep completes. Note the split: every GPU minute I pay for is a minute the model is actually serving tokens. The orchestration costs cents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the punchline the timing data handed me: the full 225-trial sweep — 1,400 endpoint calls, 339k tokens — took &lt;strong&gt;63 seconds&lt;/strong&gt; against one L40S. The endpoint's ~7-minute cold start took longer than the entire experiment it served. At that ratio, the economics aren't "GPU box vs serverless," they're "why would anyone babysit a lease for a one-minute burst." The measured bill for &lt;em&gt;everything in this post&lt;/em&gt; — two full 225-trial sweeps, the demo run, a smoke test, and several kill-and-recover demos — was &lt;strong&gt;$3.64&lt;/strong&gt; at the billing snapshot: $3.50 of GPU endpoint, &lt;strong&gt;four cents&lt;/strong&gt; of CPU sweep jobs, and less than a cent of Object Storage for the checkpoint layer the whole resilience story depends on. Call it six dollars all-in with the video-recording session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that would have killed the demo
&lt;/h2&gt;

&lt;p&gt;My first version checkpointed to local disk. Reasonable, boring, wrong.&lt;/p&gt;

&lt;p&gt;A Serverless Job's filesystem is released when the job VM goes away. So a local checkpoint survives a crash &lt;em&gt;inside&lt;/em&gt; the process — and not the one failure that actually matters here, a cancelled job. My centerpiece demo would have printed &lt;code&gt;resume: 0 already complete&lt;/code&gt; on camera and I would have deserved it.&lt;/p&gt;

&lt;p&gt;The fix reframes the whole design: &lt;strong&gt;Object Storage is the checkpoint of record.&lt;/strong&gt; And since S3 has no append, rewriting one big JSONL from a dozen concurrent workers would be a lost-update race waiting to happen. So each trial becomes its own object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3://&amp;lt;bucket&amp;gt;/balagan/full/trials/&amp;lt;trial_id&amp;gt;.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resume is now a LIST call. Every write is atomic. Re-running a trial overwrites only itself. What started as a workaround for a missing filesystem turned out to be the more correct design — which is a lesson I keep re-learning about serverless: the constraint is usually telling you something true.&lt;/p&gt;

&lt;p&gt;(The Nebius cookbook's own &lt;code&gt;train-and-serve&lt;/code&gt; example uses Object Storage as the handoff from a training Job &lt;strong&gt;to&lt;/strong&gt; a serving Endpoint. Balagan runs the same pattern inverted — the Endpoint feeds the Job, and the bucket carries the experiment's memory instead of a model's weights.)&lt;/p&gt;

&lt;p&gt;The unexpected dividend: the sweep is now &lt;strong&gt;preemption-proof&lt;/strong&gt;, so it runs on preemptible capacity with one flag — a preempted job resumes exactly like a cancelled one. The resilience feature turned out to be the cost feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  A chaos harness had better survive chaos
&lt;/h2&gt;

&lt;p&gt;Every endpoint call carries exponential-backoff retry. A trial that dies records an error row instead of taking down the sweep. And with the checkpoint outside the job, you can do this: launch the sweep as a Serverless Job, cancel it mid-run, resubmit the identical spec — and watch the first log line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[balagan] run 'kill-demo': 225 trials total | resume: 140 already complete | 85 to run | mode: endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resubmitted job then ran exactly those 85 — not one finished trial re-ran, not one was lost.&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%2Fo6guk9og52zeyeusgt4y.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%2Fo6guk9og52zeyeusgt4y.png" alt="The moment after the kill: identical resubmit, and the checkpoint in Object Storage answers — 140 already complete, 85 to run." width="800" height="389"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The moment after the kill: identical resubmit, and the checkpoint in Object Storage answers — 140 already complete, 85 to run.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I killed my own benchmark mid-flight and it shrugged. A resilience benchmark that is itself resilient felt like the only honest way to build this.&lt;/p&gt;

&lt;p&gt;(One production note that earns its keep: the sweep got so fast against a healthy endpoint that I had to add a deliberately slow &lt;code&gt;kill-demo&lt;/code&gt; profile just to have time to kill it. Chaos engineering problem #1: your system recovering faster than you can break it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What the heatmap says
&lt;/h2&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%2Fdcndv47kd7qr4swplzyz.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%2Fdcndv47kd7qr4swplzyz.png" alt="The resilience heatmap: 225 trials, 25 per cell, zero trial-level errors. Green survives; red is where a topology meets the fault it can't absorb." width="800" height="467"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The resilience heatmap: 225 trials, 25 per cell, zero trial-level errors. Green survives; red is where a topology meets the fault it can't absorb.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;225 trials, 25 per cell, zero trial-level errors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topology&lt;/th&gt;
&lt;th&gt;baseline&lt;/th&gt;
&lt;th&gt;crash&lt;/th&gt;
&lt;th&gt;byzantine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;flat&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hierarchical&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ring&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things jump out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flat majority voting absorbed everything I threw at it&lt;/strong&gt; — 100% under both a crashed agent and a live saboteur. It also posted 96% on the &lt;em&gt;fault-free&lt;/em&gt; baseline: one trial ended with the vote deadlocked, no majority either way. Five agents arguing is a distributed system even when nobody's attacking it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hierarchical's single point of failure isn't a metaphor — it's arithmetic.&lt;/strong&gt; One random victim among five agents kills the aggregator once in five trials, an undecided mesh scores as wrong, and the measured accuracy under crash is: 80%. Exactly the structural prediction. But here's the twist I didn't expect — the byzantine condition hurt the hierarchy &lt;em&gt;less&lt;/em&gt; than plain crash did (92% vs 80%). A saboteur in the aggregator seat turns out to be an imperfect liar, and honest worker reports drag it toward the truth besides. In a hierarchy, an incompetent traitor is survivable; a dead boss is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ring is the fault-type victim.&lt;/strong&gt; It degrades monotonically — 96% → 92% → 88% — and it's weakest under byzantine, because whoever speaks last owns the verdict, and a saboteur landing late in the chain gets the final word with nobody left to overrule it.&lt;/p&gt;

&lt;p&gt;And the price tag on flat's immunity: &lt;strong&gt;9.3 LLM calls and ~2,600 tokens per trial, versus hierarchical's 4.7 calls and ~930 tokens.&lt;/strong&gt; Fault-tolerance-by-majority costs roughly 2.8× the tokens. That's a real engineering trade, and now it has numbers.&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%2Fo0ee74jl8bov9q64f4uv.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%2Fo0ee74jl8bov9q64f4uv.png" alt="The price of resilience: each line drops from fault-free accuracy to worst-fault accuracy. Flat barely drops — and sits at 2.8× the tokens." width="800" height="489"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The price of resilience: each line drops from fault-free to worst-fault accuracy. Flat barely drops — at 2.8× the tokens.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One more chart earned its place, because it inverts an instinct: &lt;strong&gt;in agent meshes, getting &lt;em&gt;faster&lt;/em&gt; can be a failure symptom.&lt;/strong&gt; A hierarchical trial whose aggregator is dead returns quickly — there's nobody left to deliberate. A ring with a crashed member is a shorter ring. If you alert on latency spikes but not latency &lt;em&gt;drops&lt;/em&gt;, a whole class of mesh failures walks right past your monitoring.&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%2Fksabu7p3zv5q1ncgg1dq.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%2Fksabu7p3zv5q1ncgg1dq.png" alt="After the " width="800" height="442"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Latency under fault: dead aggregators answer fast. If you only alert on latency spikes, this failure class walks past you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because a single 225-trial sweep costs about a minute, I ran the whole thing &lt;strong&gt;twice&lt;/strong&gt; — second time with a hardened verdict parser — as a stability check (committed in &lt;code&gt;results/stability_check/&lt;/code&gt;). Individual cells wobble by ±1 trial, which is what n=25 sampling does, but the invariant reproduced exactly: &lt;strong&gt;ten aggregator deaths across the two runs, ten total losses.&lt;/strong&gt; The second run also taught me something the first missed: one flat/crash trial ended in a 2–2 tie, because killing one of five agents doesn't just remove a voter — it removes &lt;em&gt;oddness&lt;/em&gt;. And a census of every failure across both runs: faulted meshes almost never decide &lt;em&gt;wrong&lt;/em&gt;; they fail to decide at all. Except under byzantine — those failures are confidently wrong. Your monitoring can catch silence; it takes more to catch confidence.&lt;/p&gt;

&lt;p&gt;The one-sentence takeaway: &lt;strong&gt;fault &lt;em&gt;type&lt;/em&gt; matters more than fault &lt;em&gt;count&lt;/em&gt;.&lt;/strong&gt; One faulty agent out of five moved accuracy anywhere from zero points to twenty depending on &lt;em&gt;which&lt;/em&gt; failure mode and &lt;em&gt;which&lt;/em&gt; topology — a topology that shrugs off a crashed agent can be quietly poisoned by a Byzantine one, and vice versa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On serverless, your checkpoint cannot live on the box.&lt;/strong&gt; The job VM is cattle. Object Storage, one object per trial, resume by LIST — sixty-odd lines, and it's the difference between a benchmark and a demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic mock mode paid for itself instantly.&lt;/strong&gt; The entire pipeline is verifiable offline in seconds for $0, which meant my paid GPU minutes debugged integration, never logic. (The one bug that reached Nebius — an &lt;code&gt;openai&lt;/code&gt;/&lt;code&gt;httpx&lt;/code&gt; version conflict — was caught by a 10-trial smoke job in the first five minutes, exactly what smoke tests are for.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Byzantine agents are one prompt away.&lt;/strong&gt; Sit with that before you wire untrusted context into your mesh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the fault-free baseline at the ceiling.&lt;/strong&gt; My first task mix included letter-counting claims — tokenization-hostile, so a 7B model flubs them sober. That would have confounded "the fault broke it" with "the task was hard." Easy task, clean signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The surprise was the ranking flip.&lt;/strong&gt; I expected byzantine to be strictly worse than crash everywhere. For hierarchical it was the opposite — at temperature 0, an LLM instructed to lie consistently still leaks the truth, while a dead aggregator decides nothing, always. Falsified expectations are what the harness is for.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;First, find your own system in the grid: if you run a supervisor/orchestrator framework, you live in the &lt;strong&gt;hierarchical&lt;/strong&gt; row; a sequential chain or pipeline is the &lt;strong&gt;ring&lt;/strong&gt; row; a debate-or-swarm-with-a-vote is the &lt;strong&gt;flat&lt;/strong&gt; row. Your crashed agent is the teammate that timed out or hit a rate limit; your byzantine agent is the one whose context got poisoned by injected input. This isn't a metaphor — the repo ships a &lt;strong&gt;LangGraph adapter&lt;/strong&gt; that runs the same three topologies as compiled &lt;code&gt;StateGraph&lt;/code&gt;s (&lt;code&gt;lg-flat&lt;/code&gt;, &lt;code&gt;lg-hierarchical&lt;/code&gt;, &lt;code&gt;lg-ring&lt;/code&gt;), so "bring your own topology" is literal.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pip install .&lt;/code&gt;, &lt;code&gt;balagan run --config configs/demo.yaml --mock&lt;/code&gt; for the free tour; the &lt;a href="https://github.com/zahere/balagan/blob/main/nebius/RUNBOOK.md" rel="noopener noreferrer"&gt;runbook&lt;/a&gt; takes you to the full Nebius deployment in ~30 minutes. Swap in your own topology by implementing one async function — or one &lt;code&gt;StateGraph&lt;/code&gt;. I'd genuinely love to see someone's production mesh shape in that heatmap.&lt;/p&gt;

&lt;p&gt;Balagan is v0.1 of a bigger idea — next up: mid-protocol crash points, partition faults, and formally verifying topology protocols &lt;em&gt;before&lt;/em&gt; empirically breaking them. If chaos engineering for agent systems is your kind of problem, the repo is open and so are my DMs.&lt;/p&gt;

</description>
      <category>nebiusserverlesschallenge</category>
      <category>llm</category>
      <category>multiagent</category>
      <category>chaosengineering</category>
    </item>
  </channel>
</rss>
