commit 30e3d05bec59431feaae8c2fed2ac755b730ba28 Author: Danials Date: Mon Jun 15 12:22:54 2026 +0000 Initial commit: hermes-swarm skill diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26b14f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +__pycache__/ +*.py[cod] +*$py.class +*.so +.env +.venv/ +venv/ +swarm_outputs/ +*.log +.idea/ +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f38d67c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Hermes Swarm contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9aa63d2 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# hermes-swarm + +Multi-agent discussion swarm for Hermes Agent. Replace one expensive reasoning model with several cheap models that argue, refine, and converge. + +## What it does + +- Runs a round-robin discussion between Hermes profiles via Redis Pub/Sub. +- Each agent sees the full transcript before replying. +- First agent synthesizes the final answer. +- Saves transcript and final answer as Markdown. + +## Install + +```bash +git clone https://forgejo.redtask.ru/youruser/hermes-swarm.git +cd hermes-swarm +``` + +Install dependency: + +```bash +pip install redis +``` + +## Quick start + +1. Start Redis: + +```bash +docker compose -f templates/docker-compose.redis.yml up -d +``` + +2. Add Hermes profiles (see `templates/hermes-profiles.yaml`). + +3. Run: + +```bash +python3 scripts/swarm_chat.py \ + --topic "Refactor this function to use async SQLAlchemy" \ + --agents swarm-architect swarm-critic swarm-coder \ + --rounds 3 +``` + +## Environment variables + +| Variable | Default | +|----------|---------| +| `REDIS_URL` | `redis://localhost:***@dataclass | `SWARM_OUTPUT_DIR` | `./swarm_outputs` | +| `HERMES_WORKDIR` | `.` | +| `HERMES_CMD` | `hermes` | + +## Example use cases + +- **Code review**: feed a diff, let critic + reviewer + coder discuss. +- **Design decisions**: compare two approaches with architect + critic. +- **Test planning**: let tester + coder + architect write a test strategy. + +## License + +MIT diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..00874cd --- /dev/null +++ b/SKILL.md @@ -0,0 +1,160 @@ +--- +name: hermes-swarm +description: "Run a multi-agent discussion swarm using cheap models via Hermes CLI and Redis Pub/Sub." +version: 1.0.0 +author: Hermes Agent +license: MIT +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [swarm, multi-agent, delegation, redis, collaboration, reasoning, cheap-models] + related_skills: [subagent-driven-development, writing-plans, plan] +--- + +# hermes-swarm + +Use this skill when you want **several cheap models to discuss a problem and converge on a solution** instead of paying for one expensive reasoning model. + +## When to use + +- The task benefits from multiple perspectives (architect, critic, coder, tester). +- You have access to several small/cheap models via Hermes profiles. +- You want a reusable, self-hosted collaboration mechanism with no cloud orchestrator. +- You can run Redis locally or in Docker. + +## When NOT to use + +- One strong model is cheaper than 3-4 small calls (always benchmark cost). +- The task is trivial or one-shot. +- You need real-time synchronous chat between agents — this is round-robin, not live chat. + +## What you get + +``` +┌─────────────────────────────────────────────┐ +│ Redis Pub/Sub room │ +│ myproject:swarm: │ +└──────────────┬──────────────────────────────┘ + │ + ┌───────────┼───────────┐ + ▼ ▼ ▼ +swarm- swarm- swarm- +architect critic coder + │ │ │ + └───────────┴───────────┘ + │ + [synthesizer] → final.md + transcript.md +``` + +## Prerequisites + +1. **Hermes Agent** installed and `hermes` in `$PATH`. +2. **Redis** running locally or reachable via `REDIS_URL`. +3. At least **two Hermes profiles** configured with different models/roles. + +## Quick start + +### 1. Install Redis (Docker) + +```bash +docker run -d --name redis-swarm \ + -p 6379:6379 \ + redis:7-alpine +``` + +Or use the included `templates/docker-compose.redis.yml`. + +### 2. Create Hermes profiles + +Add a block like this to `~/.hermes/config.yaml` for each agent role: + +```yaml +profiles: + swarm-architect: + provider: openrouter # or any provider you use + model: google/gemma-3-12b-it:cheap + system_prompt: | + You are the architect in a multi-agent engineering discussion. + Look at the big picture, propose structure and design trade-offs. + swarm-critic: + provider: openrouter + model: deepseek/deepseek-v3:free + system_prompt: | + You are the critic. Challenge assumptions, find flaws and risks. + swarm-coder: + provider: openrouter + model: qwen/qwen-2.5-coder-32b-instruct + system_prompt: | + You are the coder. Turn ideas into concrete, working code. +``` + +> Tip: keep these profiles cheap. The whole point is to replace one expensive call with several cheap ones. + +### 3. Run the swarm + +```bash +cd /path/to/hermes-swarm +python3 scripts/swarm_chat.py \ + --topic "Design a Python LRU cache for HTTP responses" \ + --agents swarm-architect swarm-critic swarm-coder \ + --rounds 3 \ + --room myproject:swarm:lru_cache +``` + +Output files land in `./swarm_outputs/` by default: + +- `_final.md` — synthesized answer +- `_transcript.md` — full discussion + +### 4. Tune environment variables + +| Variable | Default | Meaning | +|----------|---------|---------| +| `REDIS_URL` | `redis://localhost:***@dataclass | `SWARM_OUTPUT_DIR` | `./swarm_outputs` | Where transcripts/final answers are saved | +| `HERMES_WORKDIR` | current directory | Working dir passed to each Hermes agent | +| `HERMES_CMD` | `hermes` | Hermes CLI binary | + +## Architecture + +The dispatcher runs **synchronously** in rounds: + +1. **Seed** the Redis room with the topic and agent list. +2. For each round, ask every agent to read the full history and reply. +3. After the last round, ask the first agent (synthesizer) to write the final answer. +4. Persist final answer and transcript to disk. + +All messages are stored in a Redis sorted set (`room:history`), so agents can read the full context even if they are restarted. + +## Extending roles + +Edit `scripts/swarm_chat.py` or pass custom role labels with `--roles`. Built-in roles: + +- `architect` — structure and trade-offs +- `critic` — flaws, edge cases, risks +- `coder` — concrete code +- `tester` — tests and verification +- `reviewer` — clarity and completeness +- `planner` — actionable steps + +## Cost tips + +- Start with `--rounds 2` and 2-3 agents to measure token usage. +- Use the cheapest models that understand your domain. +- Compare cost vs. a single strong model on the same task. +- Set `SWARM_OUTPUT_DIR` to your project folder so outputs become project artifacts. + +## Files + +``` +hermes-swarm/ +├── SKILL.md # this file +├── README.md # installation guide +├── scripts/ +│ └── swarm_chat.py # dispatcher +├── templates/ +│ ├── docker-compose.redis.yml # Redis setup +│ └── hermes-profiles.yaml # example profile config +└── examples/ + ├── code-review.sh # review a PR diff + └── design-decision.sh # compare two approaches +``` diff --git a/examples/code-review.sh b/examples/code-review.sh new file mode 100755 index 0000000..4afc587 --- /dev/null +++ b/examples/code-review.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Example: use the swarm to review a PR diff. + +cd "$(dirname "$0")/.." + +TOPIC=$(cat <<'EOF' +Review the following PR diff for correctness, performance, and style. +Only discuss the diff; do not invent unrelated improvements. + +EOF +) + +# Append the actual diff, e.g. from stdin or a file +if [ -p /dev/stdin ]; then + TOPIC="${TOPIC}$(cat)" +else + echo "Usage: cat diff.patch | ./examples/code-review.sh" + exit 1 +fi + +python3 scripts/swarm_chat.py \ + --topic "$TOPIC" \ + --agents swarm-critic swarm-reviewer swarm-coder \ + --roles critic reviewer coder \ + --rounds 2 \ + --room project:swarm:code_review diff --git a/examples/design-decision.sh b/examples/design-decision.sh new file mode 100755 index 0000000..b38fec9 --- /dev/null +++ b/examples/design-decision.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Example: use the swarm to decide between two approaches. + +cd "$(dirname "$0")/.." + +python3 scripts/swarm_chat.py \ + --topic "Should we use SQLAlchemy 2.0 style or 1.x style in our FastAPI project? Compare maintainability, async support, and migration cost." \ + --agents swarm-architect swarm-critic swarm-coder \ + --roles architect critic coder \ + --rounds 3 \ + --room project:swarm:design_decision diff --git a/scripts/swarm_chat.py b/scripts/swarm_chat.py new file mode 100644 index 0000000..916d36d --- /dev/null +++ b/scripts/swarm_chat.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python3 +""" +Hermes Swarm Chat — reusable multi-agent discussion over Redis Pub/Sub. + +Usage: + python3 swarm_chat.py \ + --topic "Design a Python function that caches HTTP responses" \ + --agents architect critic coder \ + --rounds 3 \ + --room myproject:swarm:demo + +Each agent is a separate Hermes profile. The dispatcher: + 1. seeds a Redis room with the topic, + 2. asks every agent in turn to read room history and respond, + 3. limits discussion to N rounds, + 4. asks the first agent (synthesizer) to write the final answer. + +Environment: + REDIS_URL Redis URI (default: redis://localhost:6379/0) + HERMES_CMD Hermes binary (default: hermes) + HERMES_WORKDIR Working directory passed to hermes chat (default: current dir) + SWARM_OUTPUT_DIR Where to save transcripts/final answer (default: ./swarm_outputs) +""" + +import argparse +import json +import os +import re +import subprocess +import sys +import time +import uuid +from dataclasses import dataclass, field +from pathlib import Path +from typing import List, Optional + +import redis + + +DEFAULT_ROOM_PREFIX = "hermes:swarm" +REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0") +HERMES_CMD = os.getenv("HERMES_CMD", "hermes") +HERMES_WORKDIR = os.getenv("HERMES_WORKDIR", os.getcwd()) +OUTPUT_DIR = Path(os.getenv("SWARM_OUTPUT_DIR", "./swarm_outputs")).resolve() + + +@dataclass +class SwarmMessage: + id: str + round: int + agent: str + role: str + content: str + timestamp: float = field(default_factory=time.time) + reply_to: Optional[str] = None + + def to_json(self) -> str: + return json.dumps({ + "id": self.id, + "round": self.round, + "agent": self.agent, + "role": self.role, + "content": self.content, + "timestamp": self.timestamp, + "reply_to": self.reply_to, + }, ensure_ascii=False, default=str) + + @classmethod + def from_dict(cls, d: dict) -> "SwarmMessage": + return cls( + id=d["id"], + round=d["round"], + agent=d["agent"], + role=d["role"], + content=d["content"], + timestamp=d.get("timestamp", time.time()), + reply_to=d.get("reply_to"), + ) + + +@dataclass +class AgentDef: + name: str # Hermes profile name, e.g. swarm-architect + role: str # human-readable role, e.g. architect + prompt: str # system role instruction + model: str = "" # informational only + + +class RedisRoom: + def __init__(self, room: str, redis_url: str = REDIS_URL): + self.room = room + self.control_key = f"{room}:control" + self.r = redis.from_url(redis_url, decode_responses=True) + self.pubsub = self.r.pubsub() + self.pubsub.subscribe(room) + + def post(self, msg: SwarmMessage) -> None: + self.r.publish(self.room, msg.to_json()) + # Also keep durable history sorted by timestamp in a Redis sorted set. + self.r.zadd(f"{self.room}:history", {msg.to_json(): msg.timestamp}) + + def history(self, since: float = 0) -> List[SwarmMessage]: + items = self.r.zrangebyscore(f"{self.room}:history", since, "+inf") + messages = [] + for raw in items: + try: + messages.append(SwarmMessage.from_dict(json.loads(raw))) + except Exception: + continue + return messages + + def clear(self) -> None: + self.r.delete(f"{self.room}:history") + self.r.delete(self.control_key) + + def signal_stop(self, reason: str = "rounds_exhausted") -> None: + self.r.set(self.control_key, json.dumps({"stop": True, "reason": reason})) + + def should_stop(self) -> bool: + raw = self.r.get(self.control_key) + if isinstance(raw, str) and raw: + try: + return json.loads(raw).get("stop", False) + except Exception: + pass + return False + + +class HermesAgentRunner: + """Runs a single Hermes agent via CLI and captures the text reply.""" + + def __init__(self, profile: str, timeout: int = 120, workdir: str = HERMES_WORKDIR, hermes_cmd: str = HERMES_CMD): + self.profile = profile + self.timeout = timeout + self.workdir = workdir + self.hermes_cmd = hermes_cmd + + def ask(self, prompt: str) -> str: + cmd = [ + self.hermes_cmd, "-p", self.profile, "chat", + "-q", prompt, + "--source", "swarm", + "-Q", # quiet: suppress banner/spinner + ] + env = os.environ.copy() + # Force working directory so agents see consistent paths. + env["HERMES_CWD"] = self.workdir + try: + result = subprocess.run( + cmd, + cwd=self.workdir, + env=env, + capture_output=True, + text=True, + timeout=self.timeout, + ) + output = result.stdout + result.stderr + # Strip ANSI escape codes. + output = re.sub(r"\x1b\[[0-9;]*m", "", output) + return self._extract_answer(output) + except subprocess.TimeoutExpired: + return f"[timeout: agent {self.profile} did not respond within {self.timeout}s]" + except Exception as e: + return f"[error running agent {self.profile}: {e}]" + + @staticmethod + def _extract_answer(output: str) -> str: + lines = output.splitlines() + drop_prefixes = ( + "Thinking", "Using model", "▔", "─", "┌", "└", "│", + "You:", "Hermes:", "◆", "⚕", "Running", "Tool result", + ) + cleaned = [] + for line in lines: + stripped = line.strip() + if not stripped: + continue + stripped = re.sub(r"\x1b\[[0-9;]*m", "", stripped) + if any(stripped.startswith(p) for p in drop_prefixes): + continue + if "session_id:" in stripped or "⚠ tirith" in stripped: + continue + cleaned.append(line) + while cleaned and not cleaned[-1].strip(): + cleaned.pop() + text = "\n".join(cleaned).strip() + if not text: + text = re.sub(r"\x1b\[[0-9;]*m", "", output).strip() + return text + + +class SwarmChat: + def __init__(self, room: RedisRoom, agents: List[AgentDef], topic: str, rounds: int = 3): + self.room = room + self.agents = agents + self.topic = topic + self.rounds = rounds + self.runner = HermesAgentRunner(profile="") # profile set per turn + + def _build_prompt(self, agent: AgentDef, round_no: int, history: List[SwarmMessage]) -> str: + transcript = [] + for m in history: + transcript.append(f"[Round {m.round} | {m.agent} ({m.role})]\n{m.content}\n") + transcript_text = "\n".join(transcript) if transcript else "(no messages yet)" + + return ( + f"You are participating in a multi-agent engineering discussion.\n\n" + f"YOUR ROLE: {agent.role}\n" + f"{agent.prompt}\n\n" + f"DISCUSSION TOPIC:\n{self.topic}\n\n" + f"CURRENT ROUND: {round_no} of {self.rounds}\n\n" + f"PREVIOUS MESSAGES IN THIS ROOM:\n{transcript_text}\n\n" + f"TASK:\n" + f"Read the previous messages, then contribute your perspective as the {agent.role}. " + f"Be concise (2-5 sentences). Do not repeat what others already said. " + f"If you disagree, explain why and propose a better approach. " + f"If the discussion has already converged, say so and add any final detail." + ) + + def run(self) -> List[SwarmMessage]: + seed = SwarmMessage( + id=f"seed-{uuid.uuid4().hex[:8]}", + round=0, + agent="dispatcher", + role="moderator", + content=f"Discussion topic: {self.topic}. Agents: {[a.role for a in self.agents]}. Maximum rounds: {self.rounds}.", + ) + self.room.post(seed) + + for r in range(1, self.rounds + 1): + if self.room.should_stop(): + print(f"[dispatcher] stop signal detected before round {r}") + break + print(f"\n=== Round {r}/{self.rounds} ===") + for agent in self.agents: + if self.room.should_stop(): + break + history = self.room.history(since=0) + prompt = self._build_prompt(agent, r, history) + self.runner.profile = agent.name + print(f"[ask {agent.name} ({agent.role})] ...", end="", flush=True) + answer = self.runner.ask(prompt) + print(f" ({len(answer)} chars)") + msg = SwarmMessage( + id=f"r{r}-{agent.role}-{uuid.uuid4().hex[:6]}", + round=r, + agent=agent.name, + role=agent.role, + content=answer, + ) + self.room.post(msg) + + self.room.signal_stop("rounds_exhausted") + return self.room.history(since=0) + + def synthesize(self) -> str: + history = self.room.history(since=0) + transcript = [] + for m in history: + if m.agent == "dispatcher": + continue + transcript.append(f"[{m.agent} ({m.role}), round {m.round}]\n{m.content}\n") + transcript_text = "\n".join(transcript) + + synthesizer = self.agents[0] + prompt = ( + f"You are the {synthesizer.role}. The multi-agent discussion below has finished.\n\n" + f"ORIGINAL TOPIC:\n{self.topic}\n\n" + f"FULL TRANSCRIPT:\n{transcript_text}\n\n" + f"TASK:\n" + f"Write a concise final answer (1-3 paragraphs) that incorporates the best ideas, resolves disagreements, " + f"and gives the user a concrete, actionable result. Do not introduce fictional tools or APIs." + ) + self.runner.profile = synthesizer.name + print(f"\n[synthesize via {synthesizer.name}] ...", end="", flush=True) + answer = self.runner.ask(prompt) + print(f" ({len(answer)} chars)") + return answer + + +def main(): + parser = argparse.ArgumentParser(description="Run a synchronous multi-agent discussion via Redis Pub/Sub.") + parser.add_argument("--topic", required=True, help="The discussion topic / task.") + parser.add_argument("--agents", nargs="+", required=True, + help="Hermes profile names to use as agents, e.g. swarm-architect swarm-critic swarm-coder") + parser.add_argument("--roles", nargs="+", default=None, + help="Optional role labels matching --agents. If omitted, derived from profile names.") + parser.add_argument("--rounds", type=int, default=3, help="Number of discussion rounds (default 3).") + parser.add_argument("--room", default=None, help="Redis room key. Default: hermes:swarm:.") + parser.add_argument("--no-synth", action="store_true", help="Skip final synthesis.") + parser.add_argument("--clear", action="store_true", help="Clear room history before starting.") + parser.add_argument("--timeout", type=int, default=120, help="Timeout per agent call in seconds.") + parser.add_argument("--output-dir", type=Path, default=None, + help="Directory for transcripts/final output. Default: SWARM_OUTPUT_DIR or ./swarm_outputs") + args = parser.parse_args() + + if len(args.agents) < 2: + print("Need at least 2 agents for a discussion.", file=sys.stderr) + sys.exit(1) + + room_name = args.room or f"{DEFAULT_ROOM_PREFIX}:{uuid.uuid4().hex[:8]}" + room = RedisRoom(room_name) + if args.clear: + room.clear() + + roles = args.roles or [a.replace("swarm-", "") for a in args.agents] + if len(roles) != len(args.agents): + print("--roles must match --agents length.", file=sys.stderr) + sys.exit(1) + + role_prompts = { + "architect": "Look at the big picture, propose structure and design trade-offs.", + "critic": "Challenge assumptions, find flaws, edge cases, and risks in others' proposals.", + "coder": "Turn ideas into concrete code or commands. Prefer working examples.", + "tester": "Identify what needs to be tested, write test cases, and verify edge cases.", + "reviewer": "Check the proposed solution for clarity, completeness, and maintainability.", + "planner": "Break the topic into actionable steps and suggest an execution order.", + "default": "Contribute relevant expertise and help the group converge on a good answer.", + } + + agents = [] + for profile, role in zip(args.agents, roles): + agents.append(AgentDef( + name=profile, + role=role, + prompt=role_prompts.get(role, role_prompts["default"]), + )) + + out_dir = args.output_dir or OUTPUT_DIR + out_dir.mkdir(parents=True, exist_ok=True) + + print(f"Room: {room_name}") + print(f"Agents: {[a.name + '(' + a.role + ')' for a in agents]}") + print(f"Rounds: {args.rounds}") + print(f"Output: {out_dir}") + + swarm = SwarmChat(room, agents, args.topic, args.rounds) + swarm.runner.timeout = args.timeout + history = swarm.run() + + print(f"\nTotal messages in room: {len(history)}") + + if not args.no_synth: + final = swarm.synthesize() + print("\n=== FINAL SYNTHESIS ===") + print(final) + out_file = out_dir / f"{room_name.replace(':', '_')}_final.md" + out_file.write_text(f"# {args.topic}\n\n{final}\n", encoding="utf-8") + print(f"\nSaved synthesis to {out_file}") + + tx_file = out_dir / f"{room_name.replace(':', '_')}_transcript.md" + lines = [f"# {args.topic}\n\n## Room: {room_name}\n\n"] + for m in history: + lines.append(f"### Round {m.round} — {m.agent} ({m.role})\n\n{m.content}\n\n") + tx_file.write_text("".join(lines), encoding="utf-8") + print(f"Saved transcript to {tx_file}") + + +if __name__ == "__main__": + main() diff --git a/templates/docker-compose.redis.yml b/templates/docker-compose.redis.yml new file mode 100644 index 0000000..fdd0689 --- /dev/null +++ b/templates/docker-compose.redis.yml @@ -0,0 +1,15 @@ +version: "3.8" + +services: + redis-swarm: + image: redis:7-alpine + container_name: redis-swarm + restart: unless-stopped + ports: + - "127.0.0.1:6379:6379" + command: redis-server --appendonly yes + volumes: + - redis-swarm-data:/data + +volumes: + redis-swarm-data: diff --git a/templates/hermes-profiles.yaml b/templates/hermes-profiles.yaml new file mode 100644 index 0000000..3b3216c --- /dev/null +++ b/templates/hermes-profiles.yaml @@ -0,0 +1,48 @@ +# Example Hermes profiles for the swarm skill. +# Copy relevant blocks into ~/.hermes/config.yaml under the `profiles:` key. + +profiles: + swarm-architect: + provider: openrouter + model: google/gemma-3-12b-it:cheap + system_prompt: | + You are the architect in a multi-agent engineering discussion. + Look at the big picture, propose structure and design trade-offs, + and help the group converge on a clean, maintainable design. + Be concise (2-5 sentences per reply). + + swarm-critic: + provider: openrouter + model: deepseek/deepseek-v3:free + system_prompt: | + You are the critic in a multi-agent engineering discussion. + Challenge assumptions, find flaws, edge cases, security risks, + and non-obvious downsides of every proposal. + Be concise (2-5 sentences per reply). + + swarm-coder: + provider: openrouter + model: qwen/qwen-2.5-coder-32b-instruct + system_prompt: | + You are the coder in a multi-agent engineering discussion. + Turn ideas into concrete, working code or commands. + Prefer minimal, readable examples that compile/run. + Be concise (2-5 sentences + short code blocks). + + swarm-tester: + provider: openrouter + model: qwen/qwen-2.5-coder-32b-instruct + system_prompt: | + You are the tester in a multi-agent engineering discussion. + Identify what needs to be tested, write test cases, + and verify edge cases and failure modes. + Be concise (2-5 sentences per reply). + + swarm-reviewer: + provider: openrouter + model: google/gemma-3-12b-it:cheap + system_prompt: | + You are the reviewer in a multi-agent engineering discussion. + Check the proposed solution for clarity, completeness, + maintainability, and consistency with the original problem. + Be concise (2-5 sentences per reply).