<?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: Clinton Adedeji</title>
    <description>The latest articles on DEV Community by Clinton Adedeji (@clinnet).</description>
    <link>https://dev.to/clinnet</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3857818%2F95249d8f-60b2-4f71-8c2c-471cff3b5417.png</url>
      <title>DEV Community: Clinton Adedeji</title>
      <link>https://dev.to/clinnet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/clinnet"/>
    <language>en</language>
    <item>
      <title>I Built a Multi-Agent AI Runtime in Go Because Python Wasn't an Option</title>
      <dc:creator>Clinton Adedeji</dc:creator>
      <pubDate>Sat, 04 Apr 2026 21:16:39 +0000</pubDate>
      <link>https://dev.to/clinnet/i-built-a-multi-agent-ai-runtime-in-go-because-python-wasnt-an-option-2ioi</link>
      <guid>https://dev.to/clinnet/i-built-a-multi-agent-ai-runtime-in-go-because-python-wasnt-an-option-2ioi</guid>
      <description>&lt;h2&gt;
  
  
  The idea that started everything
&lt;/h2&gt;

&lt;p&gt;Some weeks ago, I was thinking about Infrastructure as Code.&lt;/p&gt;

&lt;p&gt;The reason IaC became so widely adopted is not because it's technically superior to clicking through a cloud console. It's because it removed the barrier between intent and execution. You write what you want, not how to do it. A DevOps engineer doesn't need to understand the internals of how an EC2 instance is provisioned — they write a YAML file, and the machine figures it out.&lt;/p&gt;

&lt;p&gt;I started wondering: why doesn't this exist for AI agents?&lt;/p&gt;

&lt;p&gt;If I want to run a multi-agent workflow today, I have two choices. I learn Python and use LangGraph or CrewAI, or I build my own tooling from scratch. Neither option is satisfying. The first forces me into an ecosystem and a language I might not want. The second means rebuilding primitives every time.&lt;/p&gt;

&lt;p&gt;What if I could write a YAML file that described what I wanted — which agents, which tools, which LLM providers — and a runtime would just handle the rest? What if a non-developer could read that file and understand what the system does? What if I didn't have to understand how an agent works internally before I could use one?&lt;/p&gt;

&lt;p&gt;That question became Routex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go, not Python
&lt;/h2&gt;

&lt;p&gt;Every AI agent framework that exists today is written in Python. LangChain, LangGraph, CrewAI, AutoGen — Python all the way down. And for good reason: Python has the richest ML ecosystem, the most tutorials, and the lowest barrier to entry for data scientists.&lt;/p&gt;

&lt;p&gt;But as a Go developer. And I kept thinking: Go should be a natural fit for this.&lt;/p&gt;

&lt;p&gt;Here's why. An AI agent is fundamentally a concurrent system. An agent waits for an LLM response, executes tools, waits for tool results, calls the LLM again. Multiple agents run in parallel, passing results to each other through a dependency graph. This is exactly what Go was designed for.&lt;/p&gt;

&lt;p&gt;Goroutines are cheap enough that you can run one per agent without thinking about thread pool sizing. Channels give you typed, safe communication between agents without shared state. The context package gives you cancellation and timeout propagation that flows naturally through the entire call stack. You get a single, statically compiled binary you can deploy anywhere without a runtime, a virtualenv, or a requirements.txt.&lt;/p&gt;

&lt;p&gt;Go already had everything the problem needed — it just didn't have the framework yet.&lt;br&gt;
So I built it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Routex looks like to a user
&lt;/h2&gt;

&lt;p&gt;The core idea is that you should be able to describe an entire multi-agent crew in a YAML file, run it with a single command, and get results — without writing a single line of Go.&lt;br&gt;
Here's what that looks like:&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="s"&gt;agents.yaml&lt;/span&gt;

&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research-crew"&lt;/span&gt;
  &lt;span class="na"&gt;llm_provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic"&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5-20251001"&lt;/span&gt;
  &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env:ANTHROPIC_API_KEY"&lt;/span&gt;

&lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Compare&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;top&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Go&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;web&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;frameworks&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2026"&lt;/span&gt;

&lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;researcher"&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;researcher"&lt;/span&gt;
    &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Find&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;detailed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;information&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;about&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Go&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;web&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;frameworks"&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web_search"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wikipedia"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer"&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer"&lt;/span&gt;
    &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;clear,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;structured&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;report&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;research"&lt;/span&gt;
    &lt;span class="na"&gt;depends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;researcher"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web_search"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wikipedia"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;routex run agents.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire user experience for the common case. The researcher runs first, uses web search and Wikipedia to gather information, then the writer agent picks up those results and produces a report. The dependency is declared — depends: ["researcher"] — and the runtime handles the ordering automatically.&lt;/p&gt;

&lt;p&gt;A non-developer can read this file and understand exactly what it does. A developer can extend it with custom tools, different LLM providers per agent, Redis-backed memory, and OpenTelemetry tracing — all from YAML, all without touching the runtime code.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The technical core: goroutines, channels, and a topological scheduler
&lt;/h2&gt;

&lt;p&gt;Under the YAML surface, Routex is built on three Go primitives: goroutines, channels, and a topological sort.&lt;/p&gt;

&lt;p&gt;Each agent is a long-lived goroutine. It sits waiting on an Inbox channel. The scheduler sends it a task, it runs its thinking loop — calling the LLM, executing tools, calling the LLM again — and sends its result back through an Output channel. This model maps so naturally onto Go that the core agent loop is less than fifty lines.&lt;/p&gt;

&lt;p&gt;The scheduler uses Kahn's algorithm to determine execution order. It builds a dependency graph from your YAML, identifies which agents have no dependencies, and runs them all in parallel as the first "wave." When that wave completes, it identifies agents whose dependencies are now satisfied and runs those. This continues until all agents have run.&lt;/p&gt;

&lt;p&gt;In practice, this means independent agents run concurrently without you having to think about it. If you have three researcher agents gathering data about different topics, they all run almost the same time. The writer agent waits until all three are done, then synthesizes their results in a single pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I didn't plan for: what happens when an agent fails
&lt;/h2&gt;

&lt;p&gt;I finished the scheduler and felt good about it. Then I realised I had a problem.&lt;/p&gt;

&lt;p&gt;LLM calls fail. They time out, hit rate limits, return malformed responses. If an agent fails halfway through a crew run, every agent that depends on it gets the wrong answer — or no answer at all. The writer agent would try to synthesise results that don't exist. The whole run corrupts silently.&lt;/p&gt;

&lt;p&gt;I needed a way to handle failure that was more principled than wrapping everything in a retry loop.&lt;/p&gt;

&lt;p&gt;I started reading about how Erlang handles this problem. Erlang was built by Ericsson in the 1980s for telephone switches — systems that cannot go down. Their solution was the supervision tree: every process is watched by a supervisor, and when a process crashes, the supervisor decides what to do based on a policy. The philosophy is "&lt;strong&gt;let it crash&lt;/strong&gt;" — don't write defensive code trying to handle every possible failure, just let things fail fast and trust the supervisor to recover cleanly.&lt;/p&gt;

&lt;p&gt;This maps perfectly onto agents. An agent fails — the supervisor checks its policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;one_for_one&lt;/code&gt; — restart only this agent, leave the others running&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;one_for_all&lt;/code&gt; — restart the entire crew&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rest_for_one&lt;/code&gt; — restart this agent and everything that depends on it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The supervisor also tracks a restart budget. If an agent crashes three times within one minute, the supervisor stops trying and declares it permanently failed rather than looping forever burning API tokens.&lt;/p&gt;

&lt;p&gt;In Routex, you configure this in one line:&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;agents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;researcher"&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;researcher"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;one_for_one"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bug that taught me everything about channel protocols
&lt;/h2&gt;

&lt;p&gt;Here is a story about a bug I will not forget.&lt;/p&gt;

&lt;p&gt;I had finished the supervisor. Restart policies were working. The supervisor correctly restarted failed agents. I was feeling very good about myself.&lt;/p&gt;

&lt;p&gt;Then I ran a test with a researcher agent that was configured to fail on its first attempt and recover on the second. I kicked it off and watched the logs. The supervisor saw the failure. It applied the &lt;code&gt;one_for_one&lt;/code&gt; policy. It restarted the agent goroutine. The logs said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supervisor: agent &lt;span class="s2"&gt;"researcher"&lt;/span&gt; restarted
agent &lt;span class="s2"&gt;"researcher"&lt;/span&gt;: waiting &lt;span class="k"&gt;for &lt;/span&gt;message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then... nothing. The application just sat there.&lt;/p&gt;

&lt;p&gt;No timeout error. No panic. No LLM calls. Just silence.&lt;/p&gt;

&lt;p&gt;I stared at my terminal for a full two minutes assuming the LLM was being slow. I went and made tea. I came back. Still nothing. I started wondering if the Anthropic API was down. I checked the Anthropic status page. Everything was fine.&lt;/p&gt;

&lt;p&gt;I added more logging. The agent was alive — it was sitting in its select loop, genuinely waiting for a message on its Inbox channel. It had been restarted correctly. The problem was that the scheduler had no idea.&lt;/p&gt;

&lt;p&gt;The scheduler had sent the original task to the agent's Inbox before the failure. The agent crashed mid-run. The supervisor restarted a fresh agent goroutine. That fresh goroutine was now sitting patiently waiting for a new task to arrive on the channel — which it never would, because from the scheduler's perspective, the task had already been sent. The scheduler was blocked waiting for a result from the old goroutine that no longer existed.&lt;/p&gt;

&lt;p&gt;Two goroutines. Both alive. Both waiting. Neither knowing the other was waiting. A perfect deadlock dressed up as a slow LLM.&lt;/p&gt;

&lt;p&gt;The fix was the &lt;code&gt;FailureReport&lt;/code&gt; / &lt;code&gt;Decision&lt;/code&gt; protocol. The scheduler now never moves on after a failure until the supervisor explicitly tells it what to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Scheduler sends this when an agent fails&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;FailureReport&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AgentID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Err&lt;/span&gt;     &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Reply&lt;/span&gt;   &lt;span class="k"&gt;chan&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;Decision&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Supervisor responds with this&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Decision&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AgentID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Retry&lt;/span&gt;   &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;Err&lt;/span&gt;     &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an agent fails, the scheduler sends a &lt;code&gt;FailureReport&lt;/code&gt; and blocks on the &lt;code&gt;Reply&lt;/code&gt; channel. The supervisor restarts the agent, then sends &lt;code&gt;Decision{Retry: true}&lt;/code&gt; back. The scheduler receives this, re-sends the original task to the agent's &lt;code&gt;Inbox&lt;/code&gt;, and waits for the result again.&lt;/p&gt;

&lt;p&gt;Now the scheduler always knows. The agent always gets its message. And I no longer spend time checking the Anthropic status page when my own code is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel tool calls: the LLM asked for three tools at once
&lt;/h2&gt;

&lt;p&gt;When a language model responds with a tool call, most agent frameworks execute it, wait for the result, then call the LLM again. One tool at a time, sequentially.&lt;/p&gt;

&lt;p&gt;But modern LLMs can request multiple tools in a single response when those tools are independent. Claude might decide it needs to search the web, read a file, and query Wikipedia simultaneously — and return all three requests in one response. Running them sequentially wastes time.&lt;/p&gt;

&lt;p&gt;In Routex, when the LLM returns multiple tool calls, they all execute concurrently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;toolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toExecute&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;toExecute&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolCallRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toolResult&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All results are appended to history in order before the next LLM call. From the LLM's perspective, it asked for three tools and got three results — the parallelism is invisible to it, but the wall-clock time is the slowest single tool rather than the sum of all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling LLM APIs directly with net/http
&lt;/h2&gt;

&lt;p&gt;Every LLM SDK is just an HTTP client under the hood. Both the Anthropic and OpenAI adapters in Routex use &lt;code&gt;net/http&lt;/code&gt; directly — no &lt;code&gt;anthropic-sdk-go&lt;/code&gt;, no &lt;code&gt;go-openai&lt;/code&gt; in &lt;code&gt;go.mod&lt;/code&gt;. The wire format is straightforward JSON over HTTP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"/v1/messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-api-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"anthropic-version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"2023-06-01"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"content-type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire Anthropic adapter setup. Removing the SDKs dropped several megabytes of transitive dependencies from the binary and made the HTTP layer completely transparent — no SDK abstractions, no version mismatches, no wrapping errors in SDK-specific types. When the API changes, you update a struct. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-LLM crews: different models for different jobs
&lt;/h2&gt;

&lt;p&gt;One pattern that emerges naturally from the YAML-driven design is using different LLM providers for different agents:&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;agents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;researcher"&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;researcher"&lt;/span&gt;
    &lt;span class="na"&gt;llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic"&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5-20251001"&lt;/span&gt;  &lt;span class="c1"&gt;# fast, cheap&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer"&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer"&lt;/span&gt;
    &lt;span class="na"&gt;llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai"&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o"&lt;/span&gt;                      &lt;span class="c1"&gt;# more capable&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critic"&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critic"&lt;/span&gt;
    &lt;span class="na"&gt;llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama"&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3"&lt;/span&gt;                      &lt;span class="c1"&gt;# local, free&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent has its own LLM configuration. You can run Claude for research, GPT-4o for writing, and a local Llama model for review — all in the same crew, all declared in YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP: connecting to the entire ecosystem
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol is Anthropic's open standard for connecting LLMs to external tools via JSON-RPC. Any MCP-compatible server exposes a standard interface that Routex can connect to at startup:&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;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp"&lt;/span&gt;
    &lt;span class="na"&gt;extra&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;server_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;           &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000"&lt;/span&gt;
      &lt;span class="na"&gt;server_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;          &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github"&lt;/span&gt;
      &lt;span class="na"&gt;header_Authorization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env:GITHUB_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Routex connects, calls &lt;code&gt;tools/list&lt;/code&gt; to discover everything the server exposes, and registers each tool automatically. From that point, agents use them exactly like built-in tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the build looked like
&lt;/h2&gt;

&lt;p&gt;The project went through several distinct phases, each with its own surprises.&lt;/p&gt;

&lt;p&gt;The YAML config and basic agent loop came together relatively quickly. The topological scheduler took longer — mostly spent making sure cycles were detected cleanly and parallel waves executed correctly. The supervisor was the hardest part by far — not the restart logic itself, but making the channel protocol between the scheduler and supervisor airtight. The deadlock story above is the most vivid evidence of that difficulty.&lt;/p&gt;

&lt;p&gt;Parallel tool calls came late in the project, after I noticed the LLM was sometimes requesting multiple tools in one response and the runtime was silently discarding all but the first. Once I understood the pattern, the implementation was clean — but the change rippled through the history format, both LLM adapters, and the agent loop simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd do differently&lt;/strong&gt;: Start with the supervision model. I bolted it on after the scheduler was built, which meant retrofitting the channel protocol. If I were starting again, I'd design the scheduler–supervisor communication contract first and build everything else around it. The deadlock I described above would likely never have happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Routex v1.0.1 is available now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/Ad3bay0c/routex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or install the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/Ad3bay0c/routex/cmd/routex@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scaffold a new project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;routex init my-crew
&lt;span class="nb"&gt;cd &lt;/span&gt;my-crew
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the generated &lt;code&gt;agents.yaml&lt;/code&gt; and copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; and update the correct environment values, then run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;routex run agents.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're a Go developer who has been watching the AI agent ecosystem from the sidelines — Routex is for you.&lt;/p&gt;




&lt;p&gt;Routex is open source under the MIT License. Source, examples, and documentation: &lt;a href="https://github.com/Ad3bay0c/routex" rel="noopener noreferrer"&gt;https://github.com/Ad3bay0c/routex&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>go</category>
      <category>llm</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
