Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .claude/skills/mobius-seed/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
4 changes: 2 additions & 2 deletions .claude/skills/mobius-seed/scripts/create_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
94 changes: 94 additions & 0 deletions .claude/skills/mobius-seed/scripts/find_agents.py
Original file line number Diff line number Diff line change
@@ -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 '<query>' [--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)
Comment thread
AaronGoldsmith marked this conversation as resolved.

# 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()
59 changes: 59 additions & 0 deletions src/mobius/agent_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
79 changes: 72 additions & 7 deletions src/mobius/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand All @@ -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()


Expand Down
12 changes: 10 additions & 2 deletions src/mobius/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,21 @@
);
"""

# 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,
task_embedding FLOAT[{dim}]
);
"""

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."""
Expand Down Expand Up @@ -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()
Expand Down
21 changes: 19 additions & 2 deletions src/mobius/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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."""
Expand All @@ -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)
Comment thread
AaronGoldsmith marked this conversation as resolved.

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(
Expand Down
Loading