diff --git a/.claude/skills/mobius-seed/SKILL.md b/.claude/skills/mobius-seed/SKILL.md index 392c1a6..66747a8 100644 --- a/.claude/skills/mobius-seed/SKILL.md +++ b/.claude/skills/mobius-seed/SKILL.md @@ -24,10 +24,11 @@ The `agent_builder.py` module calls the Anthropic API (Opus) to generate agent p python -m mobius.cli init ``` -2. **Check what already exists:** +2. **Check what already exists.** Use semantic search to find agents similar to what you're about to create: ```bash -python -m mobius.cli agent list +python .claude/skills/mobius-seed/scripts/find_agents.py "description of what you want to create" --top 10 ``` +This returns JSON with the most relevant existing agents. Skip creating agents that are too similar to what's already there. 3. **Craft your agent definitions.** Think carefully about: - What makes a great system prompt for this specialization @@ -69,3 +70,4 @@ python -m mobius.cli agent list - **Vary approaches**: Give agents different problem-solving styles (e.g., "think step by step" vs "output code immediately") - **Be specific**: Generic prompts lose to specific ones in tournaments. "You are a Python expert who prioritizes readability" beats "You are a helpful coding assistant." - If the user gives you a codebase path, READ the codebase first and create agents tailored to its tech stack, patterns, and common tasks. +- If the user gives you a specific task instead of a specialization, create multiple agents that approach that task from genuinely different angles — vary problem-solving style, priorities, and trade-offs. diff --git a/.claude/skills/mobius-seed/scripts/create_agent.py b/.claude/skills/mobius-seed/scripts/create_agent.py index b42350e..05e0d20 100644 --- a/.claude/skills/mobius-seed/scripts/create_agent.py +++ b/.claude/skills/mobius-seed/scripts/create_agent.py @@ -42,8 +42,8 @@ def main(): sys.exit(1) config = get_config() - conn, _ = init_db(config) - registry = Registry(conn, config) + conn, vec_available = init_db(config) + registry = Registry(conn, config, vec_available) # Check for duplicates existing = registry.get_agent_by_slug(data["slug"]) diff --git a/.claude/skills/mobius-seed/scripts/find_agents.py b/.claude/skills/mobius-seed/scripts/find_agents.py new file mode 100644 index 0000000..5f03cf1 --- /dev/null +++ b/.claude/skills/mobius-seed/scripts/find_agents.py @@ -0,0 +1,94 @@ +"""Find agents semantically similar to a query. + +Usage: + python find_agents.py "build a REST API with authentication" + python find_agents.py "debug memory leaks" --top 5 + +Returns JSON array of matching agents ranked by relevance. +""" + +import json +import sys + +sys.path.insert(0, "src") + +from mobius.config import get_config +from mobius.db import init_db, vec_to_blob +from mobius.embedder import embed +from mobius.registry import Registry + + +def main(): + if len(sys.argv) < 2: + print("Usage: python find_agents.py '' [--top N]") + sys.exit(1) + + query = sys.argv[1] + top_k = 10 + if "--top" in sys.argv: + idx = sys.argv.index("--top") + if idx + 1 < len(sys.argv): + top_k = int(sys.argv[idx + 1]) + + config = get_config() + conn, vec_available = init_db(config) + + if not vec_available: + print(json.dumps({"error": "sqlite-vec not available"})) + sys.exit(1) + + # Check if agent_vec has any rows + count = conn.execute("SELECT COUNT(*) as cnt FROM agent_vec").fetchone()["cnt"] + if count == 0: + print(json.dumps({"error": "No agent embeddings found. Run backfill first."})) + sys.exit(1) + + # Embed query and search + query_vec = embed(query, config) + query_blob = vec_to_blob(query_vec) + + rows = conn.execute( + """ + SELECT av.id, av.distance + FROM agent_vec av + WHERE av.description_embedding MATCH ? + AND k = ? + ORDER BY av.distance + """, + (query_blob, top_k), + ).fetchall() + + if not rows: + print(json.dumps([])) + conn.close() + return + + # Fetch full agent details for matches + registry = Registry(conn, config, vec_available) + results = [] + for row in rows: + agent = registry.get_agent(row["id"]) + if agent is None: + continue + distance = row["distance"] + similarity = 1.0 - (distance**2 / 2.0) + results.append({ + "slug": agent.slug, + "name": agent.name, + "description": agent.description, + "provider": agent.provider, + "model": agent.model, + "specializations": agent.specializations, + "elo": round(agent.elo_rating), + "win_rate": round(agent.win_rate, 3), + "matches": agent.total_matches, + "champion": agent.is_champion, + "similarity": round(similarity, 4), + }) + + print(json.dumps(results, indent=2)) + conn.close() + + +if __name__ == "__main__": + main() diff --git a/src/mobius/agent_builder.py b/src/mobius/agent_builder.py index 86cad05..b4f37fe 100644 --- a/src/mobius/agent_builder.py +++ b/src/mobius/agent_builder.py @@ -348,6 +348,65 @@ async def crossbreed( logger.error("Invalid crossbred agent: %s", e) return None + async def diverge( + self, + specialization: str, + description: str, + n: int = 3, + provider: ProviderType = "anthropic", + model: str | None = None, + ) -> list[AgentRecord]: + """Generate N diverse agent variants for a single specialization.""" + import asyncio + + async def _generate_one(index: int) -> AgentRecord | None: + prompt = f"""Create a specialized agent for: {specialization} + +Description: {description} + +The agent should use provider "{provider}" and model "{model or 'use your best judgment for the provider'}". + +This is variant #{index + 1} of {n}. Make it distinct from other possible approaches — +try a different methodology, tone, or problem-solving strategy while staying on-topic.""" + + result = await run_judge( + prompt=prompt, + system_prompt=BUILDER_SYSTEM_PROMPT, + provider_name=self.builder_provider, + model=self.builder_model, + ) + if not result.success: + logger.error("Diverge variant %d failed: %s", index, result.error) + return None + + data = _parse_agent_json(result.output) + if data is None: + return None + + try: + tools = data.get("tools", ["Bash", "Read", "Grep", "Glob"]) + # Ensure "Bash" is always present and first in the tools list + if "Bash" in tools: + tools.remove("Bash") + tools.insert(0, "Bash") + + return AgentRecord( + name=data["name"], + slug=data["slug"], + description=data["description"], + system_prompt=data["system_prompt"], + provider=data.get("provider", provider), + model=data.get("model", model or "claude-haiku-4-5-20251001"), + tools=tools, + specializations=data.get("specializations", [specialization]), + ) + except Exception as e: + logger.error("Invalid diverge variant %d: %s", index, e) + return None + + results = await asyncio.gather(*[_generate_one(i) for i in range(n)]) + return [r for r in results if r is not None] + async def bootstrap( self, ) -> list[AgentRecord]: diff --git a/src/mobius/cli.py b/src/mobius/cli.py index 84e4ddc..0cfe1a3 100644 --- a/src/mobius/cli.py +++ b/src/mobius/cli.py @@ -52,7 +52,7 @@ def _get_components(): config = get_config() conn, vec_available = init_db(config) - registry = Registry(conn, config) + registry = Registry(conn, config, vec_available) tournament = Tournament(conn, config, registry) memory = Memory(conn, config, vec_available) selector = Selector(registry, memory, config) @@ -78,7 +78,7 @@ def init(verbose: bool = typer.Option(False, "--verbose", "-v")): from mobius.registry import Registry from mobius.seeds import DEFAULT_AGENTS - registry = Registry(conn, config) + registry = Registry(conn, config, vec_available) seeded = 0 for agent in DEFAULT_AGENTS: if not registry.get_agent_by_slug(agent.slug): @@ -158,10 +158,21 @@ def run( @app.command() def bootstrap( + n: int = typer.Option(3, "--agents", "-n", help="Number of agent variants per specialization (diverge mode)"), + strategy: str = typer.Option("default", "--strategy", "-s", help="Bootstrap strategy: 'default' or 'diverge'"), verbose: bool = typer.Option(False, "--verbose", "-v"), ): """Seed initial agents via the Agent Builder (Opus).""" _setup_logging(verbose) + + if n < 1: + console.print("[red]Error: --agents/-n must be >= 1.[/red]") + raise typer.Exit(1) + + if strategy not in ("default", "diverge"): + console.print(f"[red]Error: unknown strategy '{strategy}'. Must be 'default' or 'diverge'.[/red]") + raise typer.Exit(1) + config, conn, registry, *_ = _get_components()[:3] from mobius.agent_builder import AgentBuilder @@ -172,19 +183,73 @@ def bootstrap( raise typer.Exit(0) builder = AgentBuilder(config) - console.print("[bold]Bootstrapping agents via Opus...[/bold]") - agents = asyncio.run(builder.bootstrap()) + if strategy == "diverge": + from mobius.agent_builder import BOOTSTRAP_SPECIALIZATIONS + + console.print(f"[bold]Bootstrapping agents via diverge (n={n})...[/bold]") + champion_assigned = False + for spec, desc in BOOTSTRAP_SPECIALIZATIONS: + console.print(f"\n[bold]Diverging: {spec}[/bold]") + agents = asyncio.run(builder.diverge(spec, desc, n=n)) + for agent in agents: + if registry.get_agent_by_slug(agent.slug): + console.print(f"[yellow]Skipping {agent.slug} — already exists[/yellow]") + continue + if not champion_assigned: + agent.is_champion = True + champion_assigned = True + registry.create_agent(agent) + console.print(f"[green]Created: {agent.name} ({agent.provider}/{agent.model})[/green]") + else: + console.print("[bold]Bootstrapping agents via Opus...[/bold]") + agents = asyncio.run(builder.bootstrap()) + for agent in agents: + if registry.get_agent_by_slug(agent.slug): + console.print(f"[yellow]Skipping {agent.slug} — already exists[/yellow]") + continue + agent.is_champion = True + registry.create_agent(agent) + console.print(f"[green]Created: {agent.name} ({agent.provider}/{agent.model})[/green]") + + console.print(f"\n[bold green]Bootstrap complete.[/bold green]") + conn.close() + + +@app.command() +def diverge( + specialization: str = typer.Argument(..., help="Specialization to generate variants for"), + description: str = typer.Option("", "--desc", "-d", help="Description of the specialization"), + n: int = typer.Option(3, "--agents", "-n", help="Number of variants to generate"), + verbose: bool = typer.Option(False, "--verbose", "-v"), +): + """Generate N diverse agent variants for a single specialization.""" + _setup_logging(verbose) + + if n < 1: + console.print("[red]Error: --agents/-n must be >= 1.[/red]") + raise typer.Exit(1) + + config, conn, registry, *_ = _get_components()[:3] + from mobius.agent_builder import AgentBuilder + + builder = AgentBuilder(config) + console.print(f"[bold]Generating {n} variants for '{specialization}'...[/bold]") + + agents = asyncio.run(builder.diverge(specialization, description or specialization, n=n)) + + champion_assigned = False for agent in agents: - # Check for slug conflict if registry.get_agent_by_slug(agent.slug): console.print(f"[yellow]Skipping {agent.slug} — already exists[/yellow]") continue - agent.is_champion = True # First of their kind = champion + if not champion_assigned: + agent.is_champion = True + champion_assigned = True registry.create_agent(agent) console.print(f"[green]Created: {agent.name} ({agent.provider}/{agent.model})[/green]") - console.print(f"\n[bold green]Bootstrapped {len(agents)} agents.[/bold green]") + console.print(f"\n[bold green]Generated {len(agents)} variant(s).[/bold green]") conn.close() diff --git a/src/mobius/db.py b/src/mobius/db.py index e77d51d..67d4a7e 100644 --- a/src/mobius/db.py +++ b/src/mobius/db.py @@ -84,7 +84,7 @@ ); """ -# sqlite-vec virtual table created separately (requires extension) +# sqlite-vec virtual tables created separately (requires extension) VEC_TABLE_SQL = """ CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0( id TEXT PRIMARY KEY, @@ -92,6 +92,13 @@ ); """ +AGENT_VEC_TABLE_SQL = """ +CREATE VIRTUAL TABLE IF NOT EXISTS agent_vec USING vec0( + id TEXT PRIMARY KEY, + description_embedding FLOAT[{dim}] +); +""" + def _load_sqlite_vec(conn: sqlite3.Connection) -> bool: """Try to load the sqlite-vec extension. Returns True if successful.""" @@ -133,9 +140,10 @@ def init_db(config: MobiusConfig) -> tuple[sqlite3.Connection, bool]: # Create core schema conn.executescript(SCHEMA_SQL) - # Create vector table if extension is available + # Create vector tables if extension is available if vec_available: conn.execute(VEC_TABLE_SQL.format(dim=config.embedding_dim)) + conn.execute(AGENT_VEC_TABLE_SQL.format(dim=config.embedding_dim)) # Track schema version existing = conn.execute("SELECT version FROM schema_version").fetchone() diff --git a/src/mobius/registry.py b/src/mobius/registry.py index 50b1d38..a5c939b 100644 --- a/src/mobius/registry.py +++ b/src/mobius/registry.py @@ -8,7 +8,7 @@ from pathlib import Path from mobius.config import MobiusConfig -from mobius.db import dict_to_row, row_to_dict +from mobius.db import dict_to_row, row_to_dict, vec_to_blob from mobius.models import AgentRecord logger = logging.getLogger(__name__) @@ -17,9 +17,10 @@ class Registry: """Manages agent definitions in the database.""" - def __init__(self, conn: sqlite3.Connection, config: MobiusConfig): + def __init__(self, conn: sqlite3.Connection, config: MobiusConfig, vec_available: bool = False): self.conn = conn self.config = config + self.vec_available = vec_available def create_agent(self, agent: AgentRecord) -> AgentRecord: """Insert a new agent into the registry.""" @@ -30,10 +31,26 @@ def create_agent(self, agent: AgentRecord) -> AgentRecord: f"INSERT INTO agents ({cols}) VALUES ({placeholders})", list(row.values()), ) + + # Embed description for semantic search + if self.vec_available: + self._embed_agent(agent) + self.conn.commit() logger.info("Created agent: %s (%s)", agent.name, agent.slug) return agent + def _embed_agent(self, agent: AgentRecord) -> None: + """Embed an agent's description and store in agent_vec.""" + from mobius.embedder import embed + + text = f"{agent.name}: {agent.description}" + vec = embed(text, self.config) + self.conn.execute( + "INSERT OR REPLACE INTO agent_vec (id, description_embedding) VALUES (?, ?)", + (agent.id, vec_to_blob(vec)), + ) + def get_agent(self, agent_id: str) -> AgentRecord | None: """Fetch an agent by ID.""" row = self.conn.execute(