Hive ACP v0.1.0: Orquestación Multi-Agente y Multi-Provider
por Hugo Hernández Valdez

De un agente a un enjambre
En la primera parte construí hive-acp: un bridge que conecta un agente de IA a Telegram usando ACP y MCP. Un agente por chat, persistencia de contexto, y 5 herramientas MCP. Funcionaba, pero tenía una limitación fundamental: todo dependía de un solo agente haciendo todo el trabajo.
Una semana después, el proyecto cambió completamente. Ahora es un sistema de orquestación donde múltiples agentes de diferentes providers (Kiro, OpenCode) trabajan en paralelo, coordinados por un orquestador que delega tareas y reporta resultados. Este artículo cubre cómo llegué ahí.
El problema del agente único
Con un solo agente manejando todo — código, revisiones, planificación, preguntas generales — pasaban dos cosas:
- Timeouts: Tareas largas (análisis de código, generación de reportes) bloqueaban al agente por minutos. Si se colgaba, el bot entero dejaba de responder.
- Sin especialización: El mismo agente que escribía código también respondía "hola". No había forma de optimizar prompts por tipo de tarea.
La solución era obvia: un orquestador que delegue a subagentes especializados. Pero implementarlo requería resolver varios problemas de arquitectura.
Orquestación multi-agente
JobManager
El JobManager es el corazón de la orquestación. Recibe tareas, spawnea subagentes en paralelo, y emite eventos de progreso:
const job = jobManager.dispatch(chatId, [
{ agent: "hiveacp-coder", task: "Implementa la función X" },
{ agent: "hiveacp-reviewer", task: "Revisa el código de src/index.ts" },
]);
Cada tarea corre en un proceso aislado. Si un subagente se cuelga, los demás siguen trabajando. El orquestador recibe los resultados como [SUBAGENT RESULT] y los sintetiza para el usuario.
Visibilidad en tiempo real
Mientras los subagentes trabajan, el usuario ve el progreso en Telegram:
🤖 hiveacp-coder
⚙️ read
⚙️ grep
✅ write
Cada herramienta que usa el subagente aparece en una línea separada, con ícono de estado. El mensaje se actualiza en tiempo real y se borra cuando el subagente termina.
El flujo completo
Multi-provider: Kiro + OpenCode juntos
El cambio más interesante fue hacer que agentes de diferentes providers trabajen juntos. Un orquestador de OpenCode puede despachar tareas a subagentes de Kiro, y viceversa.
ProviderRegistry
El ProviderRegistry mapea nombres de agentes a sus providers:
const registry = new ProviderRegistry();
registry.addProvider("kiro", kiroProvider());
registry.addProvider("opencode", opencodeProvider());
// Agentes de Kiro (auto-descubiertos)
registry.addAgent("hiveacp-coder", "kiro", "Escribe código");
registry.addAgent("hiveacp-reviewer", "kiro", "Revisa código");
// Agentes de OpenCode (desde agents.json)
registry.addAgent("opencode-coder", "opencode", "Coder con OpenCode");
Cuando el JobManager despacha una tarea, resuelve el provider del agente desde el registry y spawnea el proceso correcto. El orquestador no necesita saber qué provider usa cada subagente.
El reto de OpenCode
Kiro soporta --agent <name> para seleccionar un agente por CLI. OpenCode no — sus agentes se definen como archivos Markdown en ~/.config/opencode/agents/ y no hay flag de selección.
La solución: el campo agentFlag en el CliProvider. Si el provider lo tiene, el AcpClient pasa el flag. Si no, las instrucciones del agente se leen del archivo .md y se prependen al prompt de la tarea:
// Kiro: --agent hiveacp-coder
const args = ["acp", "--trust-all-tools", "--agent", agentName];
// OpenCode: instrucciones en el prompt
const taskText = `[AGENT INSTRUCTIONS]\n${instructions}\n[END INSTRUCTIONS]\n\n${task}`;
HIVE_ORCHESTRATOR
En vez de elegir un "provider" (HIVE_PROVIDER=kiro), ahora eliges directamente qué agente es el orquestador:
HIVE_ORCHESTRATOR=opencode-orchestrator # usa OpenCode
HIVE_ORCHESTRATOR=hiveacp-orchestrator # usa Kiro
El sistema resuelve el provider automáticamente desde el registry.
Registro de agentes
¿Cómo sabe el sistema qué agentes existen y qué provider usa cada uno? Todo se define en un archivo centralizado ~/.hive-acp/agents.json:
[
{ "name": "hiveacp-coder", "provider": "kiro", "description": "Escribe código" },
{ "name": "hiveacp-reviewer", "provider": "kiro", "description": "Revisa código" },
{ "name": "opencode-coder", "provider": "opencode", "description": "Coder con OpenCode" }
]
Al arrancar, el ProviderRegistry lee este archivo y mapea cada agente a su provider. Cuando el orquestador llama agent_list, obtiene la lista completa. Cuando despacha una tarea a opencode-coder, el registry resuelve que debe usar el provider de OpenCode.
Inicialmente el sistema auto-descubría agentes leyendo archivos de ~/.kiro/agents/ y ~/.config/opencode/agents/. Pero eso causaba problemas: aparecían agentes que no eran de hive-acp (como asistentes personales), y no había forma de controlar qué se exponía al orquestador. Un archivo centralizado es más predecible — solo aparece lo que registras explícitamente.
Para crear un agente nuevo hay un CLI interactivo:
npm run create-agent
Pregunta nombre, descripción, prompt, skills, y provider. Crea el archivo en la carpeta correcta (JSON para Kiro, Markdown para OpenCode) y lo registra en agents.json automáticamente.
ChatAdapter: agnóstico de plataforma
El refactor más grande fue desacoplar todo de Telegram. Antes, las herramientas de screenshot e imágenes importaban grammy directamente. Ahora usan una interfaz ChatAdapter:
interface ChatAdapter {
getActiveContext(chatId?: number): ChatContext | null;
sendResponse(chatId: number, text: string): Promise<void>;
sendPhoto(chatId: number, filePath: string, caption?: string): Promise<void>;
sendFile(chatId: number, filePath: string, caption?: string): Promise<void>;
bindJobManager(jobManager: JobManager, pool: AcpPool): void;
start(): void;
stop(): void;
}
TelegramAdapter implementa esta interfaz. Para agregar Slack o Discord, solo hay que crear otro adapter — sin tocar tools, orquestación, ni lógica de negocio.
Dónde quedó grammy
Después del refactor, grammy solo se importa en dos archivos dentro de src/adapters/chat/telegram/. Todo lo demás trabaja con ChatAdapter.
Knowledge graph
Cada conversación extrae hechos como triples sujeto-predicado-objeto y los persiste en disco:
acme-api | uses | PostgreSQL
atlas | has | SendGrid integration
chronos | needs | i18n fix
Los triples se inyectan como contexto cuando se crea una nueva sesión o se despacha una tarea a un subagente. Tres herramientas MCP permiten al agente (o al usuario) gestionar la memoria:
memory_search— buscar hechosmemory_add— agregar un hechomemory_forget— olvidar hechos que coincidan
Streaming adaptativo
El streaming a Telegram fue el área con más bugs. Telegram no es un DOM — tiene rate limits, editMessageText falla si el contenido no cambió, y el Markdown de Telegram es un subconjunto limitado.
Los problemas que encontré
- Mensajes no entregados: El buffer se llenaba pero el debounce no disparaba antes del turn end. El
streamMsgIdse reseteaba a null y el mensaje se perdía. - Texto duplicado: OpenCode no emite
TurnEnd, así que los chunks de múltiples turns se concatenaban en un solo buffer. - Markdown roto: El agente usaba
**bold**(estándar) pero Telegram necesita*bold*. También escapaba caracteres para MarkdownV2 que no aplican en v1.
Las soluciones
Debounce adaptativo: 400ms cuando hay poco texto (feedback rápido), 1200ms cuando el buffer crece (menos edits, menos rate limiting).
Split automático: Cuando el buffer supera 3000 caracteres, se finaliza el mensaje actual con Markdown y se empieza uno nuevo. Evita el límite de 4096 de Telegram.
Normalización de Markdown: toTelegramMd() convierte **bold** a *bold* y limpia escapes de MarkdownV2, respetando code blocks.
Turn detection para OpenCode: Cuando llega un agent_message (fullMessage), se emite turn_message automáticamente — resolviendo el problema de providers que no emiten TurnEnd.
Reciclaje de clientes: Si un agente hace timeout o muere, se mata y se remueve del pool. El siguiente mensaje crea uno nuevo automáticamente.
Nuevas herramientas MCP
De 5 herramientas pasamos a 13:
| Categoría | Herramientas |
|---|---|
| Telegram | telegram_send_file, telegram_react |
| Context | context_save, context_show, context_clear |
| Memory | memory_search, memory_add, memory_forget |
| Orchestration | agent_list, agent_dispatch, agent_job, agent_cancel |
| Screenshot | screenshot_url |
| Images | images_search |
| Terminal | terminal_execute |
Cada categoría es un módulo independiente que registra sus tools y su función execute. Agregar una nueva es crear un archivo y registrarlo en index.ts.
NdJsonParser: framing con tests
El parsing de JSON-RPC sobre stdio estaba inline en el AcpClient — 20 líneas que acumulaban un buffer, buscaban newlines, y parseaban JSON. Lo extraje a un módulo NdJsonParser con 9 tests unitarios:
const parser = new NdJsonParser(
(msg) => handleMessage(msg),
(err) => log.warn("Parse error: %s", err.message),
);
// Feed raw chunks from stdout
process.stdout.on("data", (chunk) => parser.write(chunk));
Los tests cubren: líneas completas, chunks parciales, múltiples mensajes en un chunk, líneas vacías, JSON inválido, Buffer input, splits entre writes, reset, y trailing data sin newline.
Estructura final
src/
├── index.ts
├── acp/
│ ├── client.ts # ACP JSON-RPC client (stdio)
│ ├── framing.ts # NdJsonParser module
│ ├── pool.ts # Client pool with eviction and context
│ ├── registry.ts # ProviderRegistry
│ └── providers/
│ ├── types.ts # CliProvider / ResponseParser
│ ├── kiro.ts # Kiro provider
│ └── opencode.ts # OpenCode provider
├── adapters/
│ ├── chat/
│ │ ├── types.ts # ChatAdapter interface
│ │ └── telegram/
│ │ ├── adapter.ts # Telegram implementation
│ │ └── tools.ts # Telegram MCP tools
│ ├── context/tools.ts
│ ├── images/tools.ts
│ ├── screenshot/tools.ts
│ └── terminal/tools.ts
├── orchestration/
│ ├── job-manager.ts
│ ├── tools.ts
│ └── types.ts
├── memory/
│ ├── store.ts
│ ├── tools.ts
│ └── types.ts
├── mcp/
│ ├── bridge.ts
│ ├── handler.ts
│ └── types.ts
├── cli/create-agent.ts
├── skills/telegram-formatting/SKILL.md
└── utils/
Lecciones aprendidas
-
El orquestador no debe trabajar: La regla más importante. Si el orquestador ejecuta tareas él mismo, se convierte en un punto único de fallo. Delegar todo a subagentes lo mantiene liviano y resiliente.
-
Los parsers no deben saber de presentación: Tener escapes de Telegram Markdown en los parsers de ACP fue un error. La capa de protocolo debe devolver texto plano; la presentación es responsabilidad del adapter.
-
El streaming en Telegram es un campo minado: Rate limits, edits que fallan silenciosamente, Markdown incompatible, race conditions entre debounce y turn boundaries. Cada bug requirió logs específicos para diagnosticarlo.
-
agents.jsoncomo fuente de verdad: Auto-descubrir agentes de múltiples directorios causaba duplicados y agentes inesperados. Un archivo centralizado es más predecible. -
Los tests pagan rápido: Extraer el framing a un módulo con tests tomó 15 minutos. Encontrar un bug de framing sin tests habría tomado horas.
Qué sigue
- Seguridad: Path traversal en
fs/readTextFile, command injection enterminal/execute, input validation en el MCP handler - Más adapters: Slack y Discord usando la interfaz
ChatAdapter - Métricas: Duración de prompts, uso de tokens por agente, tasa de errores
- Streaming híbrido: Typing indicator para respuestas cortas, streaming solo cuando la respuesta tarda más de 3 segundos
Conclusión
En una semana, hive-acp pasó de ser un bridge single-agent a un sistema de orquestación multi-provider. El cambio más valioso no fue una feature específica sino la arquitectura: ChatAdapter para desacoplar plataformas, ProviderRegistry para mezclar providers, y JobManager para paralelizar trabajo.
Lo que empezó como "quiero usar mi agente desde el teléfono" se convirtió en "quiero que mis agentes trabajen juntos mientras yo estoy en otra cosa". Y eso cambia fundamentalmente cómo interactúo con IA para desarrollo.
El código sigue siendo open source en github.com/gouh/hive-acp.
Posts relacionados

Hive ACP: Mi Alternativa a OpenClaw para Conectar Agentes de IA a Telegram
Una exploración técnica de hive-acp, un bridge multi-agente que conecta un agente de IA a Telegram. Cubre la arquitectura, los protocolos ACP y MCP, persistencia de contexto, y las lecciones aprendidas después de dos intentos fallidos.

Scaffolding Typescript API - Parte 1
¿Cómo crear una estructura base de una API con typescript?