horde.py — modo agente (herramientas) sobre AI Horde

Fecha: 21/9/2026 · PC: GTX 1050 2GB · Modelo default: aphrodite/TheDrummer/Skyfall-31B-v4.2

El problema y la idea

AI Horde (el swarm de voluntarios) sirve modelos sin censura (Skyfall-31B, Cydonia-24B, gemma-heretic, etc.) pero no emite tool_calls: su API solo devuelve texto libre. Un agente necesita invocar herramientas y seguir iterando.

Solución: el agente no corre en el swarm — corre en tu PC. El modelo solo devuelve JSON; tu máquina ejecuta, le devuelve la salida real, y repite. Esto se emula 100% en el cliente (horde.py), sin cambiar nada del lado del swarm.

Protocolo JSON

El modelo responde únicamente en uno de estos dos formatos:

{"tool": "shell", "command": "el comando a ejecutar"}

→ tu PC ejecuta el comando localmente y el resultado vuelve a la conversación.

{"answer": "tu respuesta al usuario", "done": true}

→ el agente termina y se muestra el resultado final.

Cómo funciona el loop (agent_run)

  1. Prompt inicial: AGENT_SYSTEM (le explica al modelo el protocolo JSON) + la tarea del usuario en el formato de turnos.
  2. ask_horde() → envía POST /generate/text/async y hace poll a GET /generate/text/status/{id} hasta done=true (máx. 6 min).
  3. Shorthands locales (para que el modelo emita comandos cortos y no dumps de HTML):
    • links URL → imprime los href/src únicos de una página (máx. 250).
    • sitemap URL → imprime los <loc> de un sitemap, siguiendo sub-sitemaps (máx. 250).
    • Ambos devuelven mensaje de error textual si la URL falla (no rompen el loop).
  4. Anti-repetición (la causa de “nunca sirve”): antes el modelo repetía el mismo requests.get hasta el infinito porque no recordaba lo que ya había corrido. Ahora:
    • cada comando ejecutado entra en una lista y se le inyecta al prompt: === comandos YA EJECUTADOS (no los repitas) ===.
    • si el modelo propone un comando idéntico a uno previo, se bloquea localmente y se le pide otra vía.
  5. Recuperación de JSON truncado: con max_length=96 (para responder rápido), a veces el JSON queda cortado a mitad (ej. {"answer":[...). extract_json() repara: cierra llaves/corchetes faltantes, descarta cadenas cortadas y recupera el comando de una tool call incompleta. Además acepta respuestas finales con claves answer, links o urls (no solo done).
  6. Si es done o trae una respuesta (answer/links/urls, sin tool) → imprime y termina.
  7. Si el modelo no siguió el formato → se le avisa con una corrección y reintenta (2 veces); en --auto, ante fallos seguidos muestra lo dicho y corta.
  8. Máximo 8 turnos por tarea.

Modo compacto (clave para anónimo): la key anónima de Horde (0 kudos) solo acepta pedidos ≤512 tokens totales y hay que responder rápido. Para que el agente funcione sin registrarte:

  • el prompt se arma con build_agent_prompt(): tarea completa + lista de “ya ejecutaste” + última salida recortada (300 chars), tope de 850 chars.
  • la generación (max_length) se fija en 96 tokens.
  • así cada llamada queda en ~350 tokens, bajo el límite de 512 → sin KudosUpfront.

El historial (turns) se rearma completo en cada llamada al swarm (Horde es stateless: no guarda sesión; todo va en el prompt).

Flags de uso

python3 horde.py --agent "tarea" [modelo]       # agente, pide confirmar cada comando
python3 horde.py --agent --auto "tarea"         # ejecuta sin confirmar
python3 horde.py "prompt" [modelo] [max_length] # modo normal (texto plano)
python3 horde.py --models                       # listar modelos vivos del swarm

Ejemplos reales:

python3 horde.py --agent "crea un archivo ~/hola.txt y guardá un haiku sobre el asado"
python3 horde.py --agent --auto "listá la RAM usada y el top 3 de procesos"

Seguridad

  • Por defecto pide confirmación (y/N) antes de cada comando.
  • Con --auto se ejecuta solo, pero hay una lista BLOCKED de comandos que siempre rechaza: rm -rf /, mkfs, escritura a /dev/sd*, shutdown, reboot, dd if=, chmod -R 777 /, git push -f, crontab -r, kill -9 -1.
  • En modo confirmado un comando de la lista solo muestra un aviso primero.
  • Todos los comandos corren con timeout 30s y salida truncada.

Configuración por variable de entorno

export HORDE_API_KEY="tu-key-personal"   # ojo: "0000000000" = anónimo (baja prioridad)
export HORDE_MODEL="aphrodite/TheDrummer/Skyfall-31B-v4.2"

El default por código es Skyfall-31B-v4.2 (2 workers en el swarm, emite JSON de herramienta correctamente). Cuidado con el id: tiene que existir hoy en el swarm (verificalo con --models) — modelos viejos/mal escritos quedan en cola para siempre.

Bonus: horde_proxy.py (pi ↔ Horde)

El mismo truco, expuesto como gateway OpenAI-compatible para que pi (u otro cliente del AI SDK) use el swarm como provider:

  • Endpoint http://127.0.0.1:8765/v1/chat/completions (SSE, formato OpenAI).
  • Traduce tool_calls de OpenAI → JSON simple para el modelo, y al revés.
  • Provider horde ya registrado en ~/.pi/agent/models.json.
  • Modo compacto: si el prompt estimado supera ~480 tokens, reconstruye un prompt mínimo para caber en el límite anónimo de Horde (512 tokens).
  • Reintentos internos (3 por llamada).
python3 horde_proxy.py                      # levanta el proxy en 127.0.0.1:8765
pi --provider horde --model "aphrodite/TheDrummer/Skyfall-31B-v4.2" --print "ejecuta date"

Limitaciones conocidas (cola anónima)

  • Latencia 1-2 min por llamada según demanda; cada tarea de agente son 2-4 llamadas.
  • La salida del modelo es no determinista: a veces tool call limpio, a veces meta-texto o vacío.
  • El contexto se recorta (últimas 2 salidas) para caber en 512 tokens: tareas largas de muchas herramientas pierden memoria de los pasos viejos.
  • Rate limit 429 en ráfagas.

Funciona decente para tareas cortas (1-3 herramientas, como date, ls, cat). Para tareas largas o cuando deja de andar por demanda, las mejoras gratuitas son: registrarse en stablehorde.net (≥25 kudos = más prioridad, quita el tope de 512) y/o correr un worker con tu GTX 1050 (koboldcpp sirviendo Llama-3.2-1B/3B) para ganar kudos por job + uptime.