{"id":47890407,"url":"https://github.com/Ad3bay0c/routex","last_synced_at":"2026-04-12T06:01:11.857Z","repository":{"id":348258382,"uuid":"1192573100","full_name":"Ad3bay0c/routex","owner":"Ad3bay0c","description":"Lightweight AI agent runtime for Go. Define multi-agent crews in YAML, run them with goroutines and channels, and let the runtime handle scheduling, parallelism, retries, and observability — without leaving the Go ecosystem.","archived":false,"fork":false,"pushed_at":"2026-03-31T13:30:04.000Z","size":282,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-31T13:33:52.000Z","etag":null,"topics":["ai-agents","golang","llm","mcp","multi-agent"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Ad3bay0c.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-26T10:54:58.000Z","updated_at":"2026-03-31T13:26:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Ad3bay0c/routex","commit_stats":null,"previous_names":["ad3bay0c/routex"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Ad3bay0c/routex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ad3bay0c%2Froutex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ad3bay0c%2Froutex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ad3bay0c%2Froutex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ad3bay0c%2Froutex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Ad3bay0c","download_url":"https://codeload.github.com/Ad3bay0c/routex/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ad3bay0c%2Froutex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31705574,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-12T05:11:36.334Z","status":"ssl_error","status_checked_at":"2026-04-12T05:11:27.332Z","response_time":58,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","golang","llm","mcp","multi-agent"],"created_at":"2026-04-04T03:00:37.431Z","updated_at":"2026-04-12T06:01:11.849Z","avatar_url":"https://github.com/Ad3bay0c.png","language":"Go","readme":"```\n  ██████╗  ██████╗ ██╗   ██╗████████╗███████╗██╗  ██╗\n  ██╔══██╗██╔═══██╗██║   ██║╚══██╔══╝██╔════╝╚██╗██╔╝\n  ██████╔╝██║   ██║██║   ██║   ██║   █████╗   ╚███╔╝\n  ██╔══██╗██║   ██║██║   ██║   ██║   ██╔══╝   ██╔██╗\n  ██║  ██║╚██████╔╝╚██████╔╝   ██║   ███████╗██╔╝ ██╗\n  ╚═╝  ╚═╝ ╚═════╝  ╚═════╝    ╚═╝   ╚══════╝╚═╝  ╚═╝\n\n  lightweight AI agent runtime for Go\n```\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/Ad3bay0c/routex.svg)](https://pkg.go.dev/github.com/Ad3bay0c/routex)\n[![Go Report Card](https://goreportcard.com/badge/github.com/Ad3bay0c/routex)](https://goreportcard.com/report/github.com/Ad3bay0c/routex)\n[![codecov](https://codecov.io/github/Ad3bay0c/routex/graph/badge.svg?token=G9LZCMA2EC)](https://codecov.io/github/Ad3bay0c/routex)\n[![GitHub stars](https://img.shields.io/github/stars/Ad3bay0c/routex?style=social)](https://github.com/Ad3bay0c/routex/stargazers)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)\n\n**A lightweight AI agent runtime for Go.**\n\nRoutex lets you build, run, and supervise multi-agent AI crews using the primitives Go developers already know — goroutines, channels, and interfaces. Define your crew in a YAML file or pure Go code, wire in any LLM provider and tools, and let the runtime handle scheduling, parallelism, retries, memory, and observability.\n\n```bash\ngo install github.com/Ad3bay0c/routex/cmd/routex@latest\nroutex init my-crew \u0026\u0026 cd my-crew\ncp .env.example .env   # add your API key\nroutex run agents.yaml\n```\n\nTo depend on Routex from your own Go program (not the CLI):\n\n```bash\ngo get github.com/Ad3bay0c/routex@latest\n```\n\nSee [Using it as a library](#using-it-as-a-library) for imports and examples.\n\n---\n\n## Why Routex?\n\nThe AI agent ecosystem is almost entirely Python. Frameworks like LangGraph and CrewAI are powerful but they carry a Python runtime, slow cold starts, and async complexity that does not belong in production Go services.\n\nRoutex is built for Go developers who want agentic capabilities without leaving the Go ecosystem.\n\n|                     | LangGraph  | CrewAI     | **Routex**                |\n| ------------------- | ---------- | ---------- | ------------------------- |\n| Language            | Python     | Python     | **Go**                    |\n| Concurrency model   | asyncio    | asyncio    | **goroutines + channels** |\n| Agent supervision   | none       | none       | **Erlang-style OTP tree** |\n| Binary size         | heavy      | heavy      | **~10 MB single binary**  |\n| Cold start          | ~2–5 s     | ~2–5 s     | **~50 ms**                |\n| Multi-LLM per agent | no         | no         | **yes**                   |\n| OpenTelemetry       | partial    | partial    | **first-class**           |\n| Deploy target       | Python env | Python env | **any OS, Docker, K8s**   |\n\n---\n\n## Table of Contents\n\n- [Concepts](#concepts)\n- [Quickstart](#quickstart)\n  - [Using the CLI](#using-the-cli)\n  - [Using it as a library](#using-it-as-a-library)\n- [YAML reference](#yaml-reference)\n- [Built-in tools](#built-in-tools)\n- [LLM providers](#llm-providers)\n- [Multi-LLM crews](#multi-llm-crews)\n- [Memory backends](#memory-backends)\n- [MCP tool servers](#mcp-tool-servers)\n- [Restart policies](#restart-policies)\n- [Observability](#observability)\n- [Writing a custom tool](#writing-a-custom-tool)\n- [CLI reference](#cli-reference)\n- [GitHub Actions](#github-actions)\n- [Environment variables](#environment-variables)\n- [Repo layout](#repo-layout)\n- [Roadmap](#roadmap)\n\n---\n\n## Concepts\n\nRoutex borrows ideas from operating systems and applies them to AI agents:\n\n**Agent** — a long-lived goroutine with a brain (LLM), a memory scope, and a set of tools. Agents wait on an `Inbox` channel, process one task at a time, and send results back through typed channels. They never share state.\n\n**Crew** — a collection of agents that work together on a task. Agents declare `depends_on` relationships; the scheduler turns these into a DAG and runs independent agents in parallel.\n\n**Scheduler** — performs topological sort (Kahn's algorithm) on the dependency graph. Agents with no dependencies run immediately in parallel. Each subsequent wave starts only when the previous wave completes. Detects cycles at startup, before any agent runs.\n\n**Supervisor** — watches agents via a dedicated `notify` channel. When an agent fails, the scheduler asks the supervisor what to do. The supervisor checks the agent's restart budget and returns a decision: retry or give up. This design means the scheduler never advances a wave past a failed agent — it blocks until the supervisor resolves the failure.\n\n**Tool** — anything an agent can call: web search, file read/write, HTTP request, translation, image generation. Implement one interface, register once, available to any agent.\n\n**Memory** — each agent has a namespaced key-value and message history store. In-memory by default; Redis for persistence across runs.\n\n**Runtime** — the orchestrator. Builds the agent graph, wires in the LLM adapters, starts the supervisor, hands tasks to the scheduler, and collects results.\n\n---\n\n## Quickstart\n\n### Using the CLI\n\nThe fastest path — no Go code required.\n\n```bash\n# Install\ngo install github.com/Ad3bay0c/routex/cmd/routex@latest\n\n# Scaffold a new project\nroutex init my-research-crew\ncd my-research-crew\n\n# Set up environment\ncp .env.example .env\n# Edit .env — add your ANTHROPIC_API_KEY\n\n# Edit the generated agents.yaml file\n\n# Validate the config\nroutex validate agents.yaml\n\n# See the execution plan before running\nroutex run agents.yaml --dry-run\n\n# Run it\nroutex run agents.yaml\n```\n\nOverride the task inline without editing YAML:\n\n```bash\nroutex run agents.yaml --task \"Compare the latest releases of Go and Rust\"\n```\n\nUse a different env file (e.g. production secrets injected by your platform):\n\n```bash\nroutex run agents.yaml --env-file .env.staging --output ./reports/result.md\n```\n\n---\n\n### Using it as a library\n\nImport Routex into any Go application. Task input can come from an HTTP request, message queue, database — anywhere.\n\n**Add the module to your project** (this is the library dependency — unlike `go install`, which only builds the `routex` CLI):\n\n```bash\ngo get github.com/Ad3bay0c/routex@latest\n```\n\nThen import the root package and any subpackages you need (see below). If you add imports first, `go mod tidy` will record the module as well.\n\n| Goal                          | Command                                                    |\n| ----------------------------- | ---------------------------------------------------------- |\n| Use Routex from your Go code  | `go get github.com/Ad3bay0c/routex@version`                |\n| Install the `routex` CLI only | `go install github.com/Ad3bay0c/routex/cmd/routex@version` |\n\n**Tool imports — opt in to what you need**\n\nRoutex's built-in tools are in separate sub-packages so you only compile what you use. Import `tools/all` for everything, or pick individual packages for a leaner binary:\n\n```go\n// All 11 built-in tools (convenience — larger binary)\nimport _ \"github.com/Ad3bay0c/routex/tools/all\"\n\n// Or import only what you need (smaller binary, fewer dependencies)\nimport (\n    _ \"github.com/Ad3bay0c/routex/tools/file\"   // read_file, write_file\n    _ \"github.com/Ad3bay0c/routex/tools/search\" // web_search, brave_search, wikipedia\n    _ \"github.com/Ad3bay0c/routex/tools/web\"    // http_request, read_url, scrape\n    // omit tools/ai   → no OpenAI/Anthropic SDK dependency\n    // omit tools/comms → no SendGrid/Resend dependency\n)\n```\n\n**Option 1 — YAML-driven (recommended for most use cases):**\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/Ad3bay0c/routex\"\n    _ \"github.com/Ad3bay0c/routex/tools/all\" // register all built-in tools\n)\n\nfunc main() {\n    ctx := context.Background()\n\n    // LoadConfig reads agents.yaml, loads .env via env_file:,\n    // resolves all env: references, and validates the agent graph.\n    rt, err := routex.LoadConfig(\"agents.yaml\")\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    result, err := rt.StartAndRun(ctx)\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    fmt.Printf(\"Done in %s — %d tokens\\n\", result.Duration, result.TokensUsed)\n    rt.Stop()\n}\n```\n\n**Option 2 — override task at runtime:**\n\n```go\nrt, _ := routex.LoadConfig(\"agents.yaml\")\n\n// Task comes from an HTTP request, not from YAML\nrt.SetTask(routex.Task{\n    Input: r.FormValue(\"topic\"),\n})\n\n// Or pass directly to LoadConfig\nrt, _ := routex.LoadConfig(\"agents.yaml\",\n    routex.WithTaskInput(r.FormValue(\"topic\")),\n    routex.WithEnvFile(\".env.prod\"),\n)\n\nresult, _ := rt.StartAndRun(ctx)\n```\n\n**Option 3 — fully programmatic (no YAML):**\n\n```go\nimport (\n    \"github.com/Ad3bay0c/routex\"\n    \"github.com/Ad3bay0c/routex/agents\"\n    \"github.com/Ad3bay0c/routex/tools/search\"\n    \"github.com/Ad3bay0c/routex/tools/file\"\n)\n\nrt, _ := routex.NewRuntime(routex.Config{\n    Name: \"my-crew\",\n    LLM: routex.LLMConfig{\n        Provider: \"anthropic\",\n        Model:    \"claude-sonnet-4-6\",\n        APIKey:   os.Getenv(\"ANTHROPIC_API_KEY\"),\n    },\n})\n\nrt.RegisterTool(search.WebSearch())\nrt.RegisterTool(file.WriteFileIn(\"./outputs\"))\n\nrt.AddAgent(agents.AgentConfig{\n    ID:    \"researcher\",\n    Role:  agents.Researcher,\n    Goal:  \"Find recent news about Go 1.24\",\n    Tools: []string{\"web_search\"},\n})\n\nrt.AddAgent(agents.AgentConfig{\n    ID:        \"writer\",\n    Role:      agents.Writer,\n    Goal:      \"Write a summary report and save it\",\n    Tools:     []string{\"write_file\"},\n    DependsOn: []string{\"researcher\"},\n})\n\nresult, _ := rt.StartAndRun(ctx)\n```\n\n---\n\n## YAML reference\n\n```yaml\nruntime:\n  name: \"my-crew\" # identifies this crew in logs and traces\n  llm_provider: \"anthropic\" # anthropic | openai | ollama\n  model: \"claude-sonnet-4-6\" # model name for the chosen provider\n  api_key: \"env:ANTHROPIC_API_KEY\" # env:VAR reads from environment\n  log_level: \"info\" # debug | info | warn | error\n  env_file: \".\" # DEVELOPMENT ONLY — load .env next to this file\n  # base_url:   \"env:CUSTOM_ENDPOINT\"  # override LLM API endpoint\n\ntask:\n  input: \"Research the latest Go releases\"\n  output_file: \"env:OUTPUT_FILE\" # env: works in any string field\n  max_duration: \"5m\" # Go duration string — 30s, 5m, 1h\n\ntools:\n  # Built-in tools — declare here, auto-registered by the runtime\n  - name: \"web_search\" # DuckDuckGo, free, no key\n\n  - name: \"brave_search\" # Higher quality, 2,000 free/month\n    api_key: \"env:BRAVE_API_KEY\"\n    max_results: 5\n\n  - name: \"wikipedia\" # Free, no key needed\n    extra:\n      language: \"en\"\n\n  - name: \"http_request\" # Call any REST API\n    api_key: \"env:MY_API_KEY\" # sent as X-Api-Key header\n    extra:\n      bearer_token: \"env:MY_TOKEN\" # sent as Authorization: Bearer\n      query_api_key: \"env:OWM_KEY\" # sent as query param (e.g. OpenWeatherMap)\n      query_api_key_name: \"appid\" # the param name (?appid=KEY)\n      param_units: \"metric\" # default query param on every request\n      header_Accept: \"application/json\"\n\n  - name: \"write_file\"\n    base_dir: \"./outputs\" # agents can only write inside this dir\n\n  - name: \"read_file\"\n    base_dir: \"./data\"\n\n  - name: \"read_url\" # Fetch and strip HTML from any URL\n\n  - name: \"scrape\" # JS-rendered pages via ScrapingBee\n    api_key: \"env:SCRAPINGBEE_API_KEY\"\n\n  - name: \"summarise\" # LLM-powered text compression\n    api_key: \"env:ANTHROPIC_API_KEY\"\n\n  - name: \"translate\" # DeepL API, 500k chars/month free\n    api_key: \"env:DEEPL_API_KEY\"\n\n  - name: \"generate_image\" # DALL-E 3 via OpenAI\n    api_key: \"env:OPENAI_API_KEY\"\n    base_dir: \"./outputs/images\"\n\n  - name: \"send_email\" # SendGrid or Resend\n    api_key: \"env:SENDGRID_API_KEY\"\n    extra:\n      provider: \"sendgrid\" # sendgrid | resend\n      from_email: \"agent@example.com\"\n      from_name: \"Routex Agent\"\n\n  - name: \"mcp\" # MCP server call\n    extra:\n      server_url: \"http://localhost:3000\"\n      server_name: \"github\"\n\n  - name: \"mcp\"\n    extra:\n      server_url: \"http://localhost:3001\"\n      server_name: \"postgres\"\n\nagents:\n  - id: \"researcher\" # unique within this crew\n    role: \"researcher\" # planner | researcher | writer | critic | executor\n    goal: \"Find and summarise recent Go releases\"\n    tools: [\"web_search\", \"read_url\"]\n    restart: \"one_for_one\" # one_for_one | one_for_all | rest_for_one\n    max_retries: 2\n    timeout: \"90s\"\n    # depends_on: []                   # list of agent IDs that must complete first\n    # max_duplicate_tool_calls: 2      # redirect LLM after N identical tool calls (default: 2)\n    # max_total_tool_calls: 20         # absolute tool call budget per attempt (default: 20)\n\n    # Per-agent LLM override — uses a different model from the runtime default\n    # llm:\n    #   provider: \"openai\"\n    #   model:    \"gpt-4o\"\n    #   api_key:  \"env:OPENAI_API_KEY\"\n\n  - id: \"writer\"\n    role: \"writer\"\n    goal: \"Write a markdown report. Save it as report.md\"\n    tools: [\"write_file\"]\n    depends_on: [\"researcher\"] # starts only after researcher finishes\n    restart: \"one_for_one\"\n    max_retries: 2\n    timeout: \"120s\"\n\nmemory:\n  backend: \"inmem\" # inmem | redis\n  ttl: \"1h\"\n  # redis_url: \"env:REDIS_URL\"         # required when backend is \"redis\"\n\nobservability:\n  tracing: false\n  metrics: false\n  # jaeger_endpoint: \"env:OTEL_EXPORTER_OTLP_ENDPOINT\"  # e.g. http://localhost:4318\n  # metrics_addr:    \":9090\"           # curl localhost:9090/metrics\n```\n\n### The `env:` prefix\n\nAny string value in the YAML can read from the environment:\n\n```yaml\napi_key: \"env:ANTHROPIC_API_KEY\" # reads os.Getenv(\"ANTHROPIC_API_KEY\")\noutput_file: \"env:OUTPUT_PATH\" # works in any string field\nmodel: \"env:LLM_MODEL\" # even model names\n```\n\nThis means your `agents.yaml` file can be committed to git with zero secrets in it.\n\n---\n\n## Built-in tools\n\nAll tools are registered automatically when listed in the `tools:` section of `agents.yaml`. No `RegisterTool()` call needed for built-ins.\n\n### Search \u0026 Data\n\n| Tool           | Key needed            | Free tier           | Description                                 |\n| -------------- | --------------------- | ------------------- | ------------------------------------------- |\n| `web_search`   | none                  | unlimited           | DuckDuckGo Instant Answers                  |\n| `brave_search` | `BRAVE_API_KEY`       | 2,000/month         | Structured web results with publication age |\n| `wikipedia`    | none                  | unlimited           | Wikipedia article summaries, 300+ languages |\n| `scrape`       | `SCRAPINGBEE_API_KEY` | 1,000 credits/month | JS-rendered page content                    |\n\n### Web \u0026 HTTP\n\n| Tool           | Key needed   | Description                                       |\n| -------------- | ------------ | ------------------------------------------------- |\n| `read_url`     | none         | Fetch and strip HTML from any URL                 |\n| `http_request` | configurable | Call any REST API — GET, POST, PUT, PATCH, DELETE |\n\n`http_request` supports four authentication patterns:\n\n```yaml\n# 1. Query string key (OpenWeatherMap, Google Maps)\nextra:\n  query_api_key:      \"env:OWM_KEY\"\n  query_api_key_name: \"appid\"         # → ?appid=KEY on every request\n\n# 2. Bearer token (GitHub, most REST APIs)\nextra:\n  bearer_token: \"env:GITHUB_TOKEN\"   # → Authorization: Bearer TOKEN\n\n# 3. API key header\napi_key: \"env:MY_KEY\"                # → X-Api-Key: KEY\n\n# 4. Custom header\nextra:\n  header_X-Custom-Header: \"value\"    # → X-Custom-Header: value\n  header_API-KEY: \"value\"    # → API-KEY: value\n```\n\nDefault query params (sent on every request):\n\n```yaml\nextra:\n  param_units: \"metric\" # → ?units=metric on every request\n  param_lang: \"en\"\n```\n\n### File\n\n| Tool         | Config     | Description                           |\n| ------------ | ---------- | ------------------------------------- |\n| `write_file` | `base_dir` | Write files — sandboxed to `base_dir` |\n| `read_file`  | `base_dir` | Read files — sandboxed to `base_dir`  |\n\nPath traversal (`../`) is blocked in both tools. Agents can only read or write inside the configured `base_dir`.\n\n### AI \u0026 Generation\n\n| Tool             | Key needed          | Description                                                 |\n| ---------------- | ------------------- | ----------------------------------------------------------- |\n| `summarise`      | `ANTHROPIC_API_KEY` | LLM-powered text compression (paragraph, bullets, one-line) |\n| `translate`      | `DEEPL_API_KEY`     | DeepL translation, 30+ languages, 500k chars/month free     |\n| `generate_image` | `OPENAI_API_KEY`    | DALL-E 3 image generation, saves to disk                    |\n\n### Communication\n\n| Tool         | Key needed                             | Providers        | Description                         |\n| ------------ | -------------------------------------- | ---------------- | ----------------------------------- |\n| `send_email` | `SENDGRID_API_KEY` or `RESEND_API_KEY` | SendGrid, Resend | Send emails with plain text or HTML |\n\nSwap providers with one line — no code changes:\n\n```yaml\n- name: \"send_email\"\n  api_key: \"env:RESEND_API_KEY\"\n  extra:\n    provider: \"resend\" # was \"sendgrid\"\n    from_email: \"agent@yourdomain.com\"\n```\n\n---\n\n## LLM providers\n\nSet the provider at the runtime level — all agents inherit it by default:\n\n```yaml\nruntime:\n  llm_provider: \"anthropic\" # or \"openai\" or \"ollama\"\n  model: \"claude-sonnet-4-6\"\n  api_key: \"env:ANTHROPIC_API_KEY\"\n```\n\n| Provider    | Models                                                              | Notes                                  |\n| ----------- | ------------------------------------------------------------------- | -------------------------------------- |\n| `anthropic` | `claude-sonnet-4-6`, `claude-haiku-4-5-20251001`, `claude-opus-4-6` | Default                                |\n| `openai`    | `gpt-4o`, `gpt-4o-mini`, `o1`, `o3-mini`                            | Also works with OpenAI-compatible APIs |\n| `ollama`    | `llama3`, `mistral`, `phi3`, any installed model                    | Local inference, no API key needed     |\n\nSwitch providers by changing two lines in YAML — zero code changes.\n\n---\n\n## Multi-LLM crews\n\nEach agent can use a different LLM provider and model from the runtime default. This lets you assign the right model to each task — use a fast, cheap model for mechanical work and a powerful model only where it matters.\n\n```yaml\nruntime:\n  llm_provider: \"anthropic\"\n  model: \"claude-haiku-4-5-20251001\" # default — fast and cheap\n  api_key: \"env:ANTHROPIC_API_KEY\"\n\nagents:\n  # Researcher inherits Haiku — data fetching doesn't need a powerful model\n  - id: \"researcher\"\n    role: \"researcher\"\n    goal: \"Fetch weather data from the OpenWeatherMap API\"\n    tools: [\"http_request\"]\n\n  # Comparator uses GPT-4o — synthesis benefits from a larger model\n  - id: \"comparator\"\n    role: \"writer\"\n    goal: \"Compare the weather data and write a detailed analysis\"\n    tools: []\n    depends_on: [\"researcher\"]\n    llm:\n      provider: \"openai\"\n      model: \"gpt-4o\"\n      api_key: \"env:OPENAI_API_KEY\"\n\n  # Fact-checker uses Anthropic Sonnet — higher quality critique\n  - id: \"fact_checker\"\n    role: \"critic\"\n    goal: \"Verify the analysis and write the final report\"\n    tools: [\"write_file\"]\n    depends_on: [\"comparator\"]\n    llm:\n      provider: \"anthropic\"\n      model: \"claude-sonnet-4-6\"\n      api_key: \"env:ANTHROPIC_API_KEY\"\n```\n\n---\n\n## Memory backends\n\nAgents share a namespaced memory store. Each agent's history and outputs are stored under its own key — agents cannot accidentally read each other's working memory.\n\n```yaml\n# In-memory (default) — no setup, resets between runs\nmemory:\n  backend: \"inmem\"\n  ttl:     \"1h\"\n\n# Redis — persists across runs, shared between instances\nmemory:\n  backend:   \"redis\"\n  ttl:       \"24h\"\n  redis_url: \"env:REDIS_URL\"\n```\n\nStart Redis locally:\n\n```bash\ndocker run -p 6379:6379 redis:alpine\nexport REDIS_URL=redis://localhost:6379\n```\n\n---\n\n## MCP tool servers\n\nRoutex connects to any [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server at startup and registers all tools it exposes — making them available to agents exactly like built-in tools. Any MCP-compatible server works: the official GitHub MCP server, the Postgres MCP server, a custom server, or any server from the MCP registry.\n\n```yaml\ntools:\n  - name: \"mcp\"\n    extra:\n      server_url: \"http://localhost:3000\" # required — your MCP server URL\n      server_name: \"github\" # optional label for logs\n\n  # Multiple servers supported — each is one entry\n  - name: \"mcp\"\n    extra:\n      server_url: \"http://localhost:3001\"\n      server_name: \"postgres\"\n```\n\nTools from the server are discovered automatically via `tools/list` at startup. Agents use them by name just like built-ins:\n\n```yaml\nagents:\n  - id: \"researcher\"\n    role: \"researcher\"\n    goal: \"Search GitHub for Go MCP servers\"\n    tools: [\"search_repos\", \"get_user\"] # ← names returned by the MCP server\n```\n\n**Authentication** — most MCP servers require credentials. Pass them as headers using the `header_*` prefix, which supports the `env:` resolver:\n\n```yaml\ntools:\n  - name: \"mcp\"\n    extra:\n      server_url: \"http://localhost:3000\"\n      server_name: \"github\"\n      header_Authorization: \"env:GITHUB_TOKEN\" # → Authorization: Bearer ghp_xxx\n      header_X-Api-Key: \"env:MY_API_KEY\" # → X-Api-Key: abc123\n```\n\n**Name collisions** — if two servers expose a tool with the same name (e.g. both have `search`), the second is automatically prefixed with its `server_name`: `postgres.search`.\n\n**`routex tools list`** only shows built-in tools. MCP tools are discovered at runtime — run `routex run agents.yaml --dry-run` to see all available tools after the server connection is made.\n\n---\n\n## Restart policies\n\nRoutex supervision is modelled after Erlang/OTP. Set per agent:\n\n```yaml\n- id: \"researcher\"\n  restart: \"one_for_one\" # default\n```\n\n| Policy         | When an agent fails...                                    | Use when...                                               |\n| -------------- | --------------------------------------------------------- | --------------------------------------------------------- |\n| `one_for_one`  | Restart only that agent                                   | Agents are independent                                    |\n| `one_for_all`  | Restart the entire crew                                   | Agents share state and a partial crew gives wrong results |\n| `rest_for_one` | Restart the failed agent and all agents that depend on it | Pipeline — downstream agents need fresh upstream output   |\n\nThe restart budget is controlled at the runtime level:\n\n```go\nsupervisor.New(agents, policy,\n    3,             // max restarts per agent\n    time.Minute,   // within this sliding window\n)\n```\n\nAfter `maxRestarts` failures within the window, the supervisor declares the agent permanently failed and propagates the error to the caller.\n\n---\n\n## Observability\n\nEnable tracing and metrics in `agents.yaml`:\n\n```yaml\nobservability:\n  tracing: true\n  jaeger_endpoint: \"http://localhost:4318\" # OTLP HTTP — Jaeger v1.35+\n\n  metrics: true\n  metrics_addr: \":9090\"\n```\n\n**Start Jaeger locally:**\n\n```bash\ndocker run \\\n  -p 16686:16686 \\\n  -p 4318:4318 \\\n  jaegertracing/all-in-one\n\nopen http://localhost:16686\n```\n\nEvery run produces a trace tree:\n\n```\nroutex.run                        ← entire crew run\n  routex.agent [researcher]       ← one agent's execution\n    routex.llm.complete           ← single LLM call\n    routex.tool.execute [http_request]\n    routex.llm.complete           ← follow-up after tool result\n  routex.agent [comparator]\n    routex.llm.complete\n```\n\n**Prometheus metrics at `:9090/metrics`:**\n\n```\nroutex_tokens_total{agent_id,provider}\nroutex_tool_calls_total{tool_name,status}\nroutex_tool_duration_seconds{tool_name}\nroutex_agent_duration_seconds{agent_id,role}\nroutex_run_duration_seconds{runtime_name}\nroutex_agent_failures_total{agent_id}\n```\n\nAll observe methods are nil-safe — disabling tracing or metrics costs nothing beyond a nil check.\n\n---\n\n## Writing a custom tool\n\nAny struct that implement the `Tool` interface can be registered:\n\n```go\ntype Tool interface {\n    Name()    string\n    Schema()  tools.Schema\n    Execute(ctx context.Context, input json.RawMessage) (json.RawMessage, error)\n}\n```\n\n**Step 1 — implement the interface:**\n\n```go\n// tools/mytools/db_query.go\npackage mytools\n\nimport (\n    \"context\"\n    \"database/sql\"\n    \"encoding/json\"\n    \"fmt\"\n\n    \"github.com/Ad3bay0c/routex/tools\"\n)\n\ntype DBQueryTool struct {\n    db *sql.DB\n}\n\nfunc (t *DBQueryTool) Name() string { return \"db_query\" }\n\nfunc (t *DBQueryTool) Schema() tools.Schema {\n    return tools.Schema{\n        Description: \"Run a read-only SQL query against the application database. \" +\n            \"Returns results as a JSON array of objects.\",\n        Parameters: map[string]tools.Parameter{\n            \"query\": {\n                Type:        \"string\",\n                Description: \"A read-only SQL SELECT query. No INSERT, UPDATE, or DELETE.\",\n                Required:    true,\n            },\n        },\n    }\n}\n\ntype dbQueryInput struct {\n    Query string `json:\"query\"`\n}\n\nfunc (t *DBQueryTool) Execute(ctx context.Context, input json.RawMessage) (json.RawMessage, error) {\n    var params dbQueryInput\n    if err := json.Unmarshal(input, \u0026params); err != nil {\n        return nil, fmt.Errorf(\"db_query: invalid input: %w\", err)\n    }\n    if params.Query == \"\" {\n        return nil, fmt.Errorf(\"db_query: query is required\")\n    }\n\n    rows, err := t.db.QueryContext(ctx, params.Query)\n    if err != nil {\n        return nil, fmt.Errorf(\"db_query: %w\", err)\n    }\n    defer rows.Close()\n\n    // Convert rows to []map[string]any and marshal to JSON\n    results, err := rowsToJSON(rows)\n    if err != nil {\n        return nil, fmt.Errorf(\"db_query: %w\", err)\n    }\n\n    return json.Marshal(results)\n}\n\n// compile-time check\nvar _ tools.Tool = (*DBQueryTool)(nil)\n```\n\n**Step 2 — register it:**\n\n```go\nrt, _ := routex.LoadConfig(\"agents.yaml\")\nrt.RegisterTool(\u0026mytools.DBQueryTool{db: db})\nresult, _ := rt.StartAndRun(ctx)\n```\n\n**Step 3 — declare it in agents.yaml:**\n\n```yaml\ntools:\n  - name: \"db_query\" # no api_key or extra needed for custom tools\n\nagents:\n  - id: \"analyst\"\n    role: \"researcher\"\n    goal: \"Query the orders table for last week's top products\"\n    tools: [\"db_query\"]\n```\n\nCustom tools are prioritised over built-ins — if you register a tool named `web_search`, it replaces the built-in.\n\n---\n\n## CLI reference\n\n```\nroutex \u003ccommand\u003e [flags]\n\nCommands:\n  run       Run an agent crew\n  validate  Validate config without running\n  tools     Manage built-in tools\n  init      Scaffold a new project\n  version   Print version info\n```\n\n### `routex run`\n\n```\nroutex run \u003cagents.yaml\u003e [flags]\n\nFlags:\n  -e, --env-file   \u003cpath\u003e      Load .env file (overrides env_file: in YAML)\n  -t, --task       \u003ctext\u003e      Override task input\n  -o, --output     \u003cpath\u003e      Override output file path\n  -T, --timeout    \u003cduration\u003e  Override max_duration (e.g. 10m, 30s)\n  -l, --log-level  \u003clevel\u003e     debug | info | warn | error\n      --dry-run                Print execution plan and exit\n      --json                   Output result as JSON\n\nExamples:\n  routex run agents.yaml\n  routex run agents.yaml -t \"Latest Go security advisories\"\n  routex run agents.yaml -e .env.staging -o ./reports/$(date +%Y%m%d).md\n  routex run agents.yaml --dry-run\n  routex run agents.yaml --json | jq '.agents[] | select(.error != null)'\n```\n\nThe `--dry-run` flag validates the full config and prints the execution plan:\n\n```\nroutex dry-run  agents.yaml\n\n  wave 1\n    lagos_weather                    ← no deps, runs first (in parallel)\n    london_weather                   ← no deps, runs first (in parallel)\n  wave 2\n    comparator       ← lagos_weather, london_weather  [openai / gpt-4o]\n  wave 3\n    fact_checker     ← comparator\n\nConfig is valid. Run without --dry-run to execute.\n```\n\n### `routex validate`\n\n```\nroutex validate \u003cagents.yaml\u003e [flags]\n\nFlags:\n  -e, --env-file  \u003cpath\u003e   Load env file before validation\n      --json               Output result as JSON (exit code 0=valid, 1=invalid)\n\n# CI usage\nroutex validate agents.yaml --json | jq .\n```\n\n### `routex tools list`\n\n```\nroutex tools list [flags]\n\nFlags:\n  --json   Machine-readable output\n\nExample output:\n  Built-in tools (11)\n\n  NAME                       DESCRIPTION\n  ─────────────────────────  ─────────────────────────────────────────────\n  brave_search               Search the web using Brave Search for accurat...\n  generate_image             Generate an image from a text description usin...\n  http_request               Make an HTTP request to any REST API endpoint...\n  read_file                  Read the content of a local file...\n  read_url                   Fetch and strip HTML from any URL...\n  scrape                     Fetch JS-rendered page content via ScrapingBee...\n  send_email                 Send an email via SendGrid or Resend...\n  summarise                  Compress long text using Claude Haiku...\n  translate                  Translate text using DeepL...\n  web_search                 Search the web via DuckDuckGo (free, no key)...\n  wikipedia                  Fetch a Wikipedia article summary...\n  write_file                 Write content to a file safely...\n```\n\n### `routex init`\n\n```\nroutex init [dirname]\n\nScaffolds:\n  agents.yaml     — starter config (two agents, web_search + write_file)\n  .env.example    — template for required keys\n  .gitignore      — ignores .env and output files\n  main.go         — minimal Go entrypoint\n\nExample:\n  routex init weather-crew\n  cd weather-crew\n  cp .env.example .env \u0026\u0026 vim .env\n  go mod init github.com/yourname/weather-crew\n  go mod tidy\n  routex run agents.yaml\n```\n\n### `routex version`\n\n```\nroutex version [--json]\n\nOutput:\n  routex 1.0.0\n  go     go1.22.0\n  os     linux/amd64\n```\n\n**Typo correction** — Routex suggests the closest command when you make a mistake:\n\n```\n$ routex runn agents.yaml\nroutex: unknown command \"runn\"\n\nDid you mean this?\n        routex run\n\n$ routex run agents.yaml --timout 5m\nerror: unknown flag: --timout\n\n        Did you mean  --timeout  ?\n```\n\n---\n\n## GitHub Actions\n\nYou can run Routex in CI the same way you run it locally: check out the repo (with `agents.yaml`), install the CLI, set API keys as **secrets**, then invoke `routex run`. The repository ships a **composite action** that runs `go install` and `routex run` with the same flags as the CLI.\n\n**Requirements:** use a **Linux** runner (`ubuntu-latest` is typical). Set **encrypted secrets** on the repository or organization for any keys your crew needs (for example `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`). Do not put secrets in `with:` inputs. **Fork PRs** usually cannot access upstream secrets, so avoid running full agent jobs on untrusted PRs unless you have a deliberate, reviewed policy.\n\n### Option A — composite action (`uses: …/routex@v…`)\n\nPin the action to a **release tag** so `go install` uses the same version as the action (when `routex_ref` is left empty, install uses `github.action_ref`).\n\n```yaml\nname: Routex crew\non:\n  workflow_dispatch:\n\njobs:\n  run:\n    runs-on: ubuntu-latest\n    env:\n      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: Ad3bay0c/routex@main # pin a semver tag (e.g. v1.0.0) for reproducible installs\n        with:\n          config: agents.yaml\n          task: Summarise the README in three bullet points.\n          output: report.md\n          timeout: 15m\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: routex-output\n          path: report.md\n```\n\nInputs map to CLI flags: `config` (required), optional `task`, `env_file`, `output`, `timeout`, `log_level`, `working_directory`, `go_version`, `json_output`, `dry_run`, and optional `routex_ref` to override the install revision.\n\n### Option B — `go install` without the composite action\n\n```yaml\njobs:\n  run:\n    runs-on: ubuntu-latest\n    env:\n      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-go@v5\n        with:\n          go-version: \"1.25\"\n\n      - name: Install routex\n        run: go install github.com/Ad3bay0c/routex/cmd/routex@latest\n\n      - name: Run\n        run: |\n          echo \"$(go env GOPATH)/bin\" \u003e\u003e \"$GITHUB_PATH\"\n          routex run agents.yaml -t \"Your task\" -o report.md\n```\n\n### Safety and cost\n\nKeep **timeouts** and YAML `max_duration` reasonable; use **`dry_run: true`** (composite) or `--dry-run` in CI when you only need validation. Restrict **tools** and tokens in configs that run in CI (least privilege). LLM calls are **paid** per run—treat workflows like any other production job hitting external APIs.\n\n---\n\n## Environment variables\n\n| Variable                      | Used by                         | Description                                             |\n| ----------------------------- | ------------------------------- | ------------------------------------------------------- |\n| `ANTHROPIC_API_KEY`           | runtime, summarise              | Anthropic API key                                       |\n| `OPENAI_API_KEY`              | openai provider, generate_image | OpenAI API key                                          |\n| `BRAVE_API_KEY`               | brave_search                    | Brave Search API key                                    |\n| `SCRAPINGBEE_API_KEY`         | scrape                          | ScrapingBee API key                                     |\n| `DEEPL_API_KEY`               | translate                       | DeepL API key (append `:fx` for free tier)              |\n| `SENDGRID_API_KEY`            | send_email                      | SendGrid API key                                        |\n| `RESEND_API_KEY`              | send_email                      | Resend API key                                          |\n| `OPENWEATHER_API_KEY`         | http_request                    | OpenWeatherMap key (passed via query param)             |\n| `REDIS_URL`                   | memory                          | Redis connection URL                                    |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | observe                         | OTLP trace endpoint                                     |\n| `ROUTEX_METRICS_ADDR`         | observe                         | Prometheus metrics address (default `:9090`)            |\n| `ROUTEX_TASK`                 | config                          | Overrides `task.input` in YAML                          |\n| `ROUTEX_ENV_FILE`             | config                          | Overrides `env_file:` in YAML (set by CLI `--env-file`) |\n\n**Development vs Production:**\n\nUse `env_file: \".\"` in `agents.yaml` to load a `.env` file during development:\n\n```yaml\nruntime:\n  env_file: \".\" # DEVELOPMENT ONLY — remove in production\n```\n\nIn production, inject secrets through your platform instead:\n\n```bash\n# Docker\ndocker run -e ANTHROPIC_API_KEY=sk-ant-... myimage\n\n# Kubernetes\nkubectl create secret generic routex-secrets \\\n  --from-literal=ANTHROPIC_API_KEY=sk-ant-...\n```\n\n---\n\n## Repo layout\n\n```\nroutex/\n├── runtime.go                  # Runtime — Start, Run, Stop, ExecutionPlan\n├── config.go                   # LoadConfig, YAML parsing, env: resolution\n├── task.go                     # Task and Result public types\n│\n├── agents/\n│   ├── agent.go                # Agent goroutine — think loop, tool dedup, retry\n│   ├── agent_config.go         # AgentConfig, RestartPolicy, per-agent LLM\n│   ├── roles.go                # Planner, Researcher, Writer, Critic, Executor\n│   ├── memory.go               # Agent memory key helpers\n│   └── observe.go              # AgentTracer and AgentMetrics interfaces\n│\n├── llm/\n│   ├── adapter.go              # Adapter interface, Request, Response, Config\n│   ├── anthropic.go            # Anthropic provider (claude-*)\n│   ├── openai.go               # OpenAI provider + compatible APIs\n│   └── ollama.go               # Local Ollama provider\n│\n├── memory/\n│   ├── store.go                # MemoryStore interface\n│   ├── inmem.go                # In-memory backend (default)\n│   └── redis.go                # Redis backend\n│\n├── tools/\n│   ├── tool.go                 # Tool interface, Registry, Schema, Parameter\n│   ├── builtin.go              # RegisterBuiltin, Resolve, ListBuiltins\n│   ├── search/\n│   │   ├── web_search.go       # DuckDuckGo (free)\n│   │   ├── brave_search.go     # Brave Search API\n│   │   └── wikipedia.go        # Wikipedia REST API\n│   ├── web/\n│   │   ├── read_url.go         # HTML fetcher + stripper\n│   │   ├── scrape.go           # JS-rendered pages via ScrapingBee\n│   │   └── http_request.go     # Generic HTTP client\n│   ├── file/\n│   │   ├── write_file.go       # Sandboxed file writer\n│   │   └── read_file.go        # Sandboxed file reader\n│   ├── ai/\n│   │   ├── summarise.go        # LLM text compression\n│   │   ├── translate.go        # DeepL translation\n│   │   └── generate_image.go   # DALL-E 3 image generation\n│   └── comms/\n│       └── send_email.go       # SendGrid + Resend (two providers, one interface)\n│\n├── observe/\n│   ├── tracer.go               # OpenTelemetry spans — run, agent, LLM, tool\n│   └── metrics.go              # Prometheus counters and histograms\n│\n├── internal/\n│   ├── scheduler/\n│   │   └── scheduler.go        # Kahn's sort, wave execution, failure cooperation\n│   └── supervisor/\n│       └── supervisor.go       # Erlang-style restart, FailureReport/Decision protocol\n│\n├── cmd/routex/\n│   ├── main.go                 # CLI entrypoint\n│   ├── cli.go                  # Command dispatcher, flag parser, did-you-mean\n│   ├── cmd_run.go              # routex run\n│   ├── cmd_validate.go         # routex validate\n│   ├── cmd_tools.go            # routex tools list\n│   ├── cmd_init.go             # routex init\n│   ├── cmd_version.go          # routex version\n│   └── suggest.go              # Levenshtein distance for typo correction\n│\n└── examples/\n    ├── yaml-driven/            # Minimal YAML-based example\n    ├── programmatic/           # Pure Go API example\n    ├── search-and-data/        # Group 1 tools — brave_search, wikipedia, scrape\n    ├── ai-generation/          # Group 2 tools — summarise, translate, generate_image\n    ├── comms-and-storage/      # Group 3 tools — send_email, read_file, http_request\n    └── weather-compare/        # Multi-LLM crew — parallel agents, fact-checker\n```\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for how to add tools, LLM providers, memory backends, and more.\n\n---\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n## Blog\n\n- [I Built a Multi-Agent AI Runtime in Go Because Python Wasn't an Option](https://dev.to/clinnet/i-built-a-multi-agent-ai-runtime-in-go-because-python-wasnt-an-option-2ioi)\n","funding_links":[],"categories":["Artificial Intelligence"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAd3bay0c%2Froutex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FAd3bay0c%2Froutex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAd3bay0c%2Froutex/lists"}