agent-dispatch
Servidor MCP que permite a los agentes de Claude Code delegar tareas a agentes en otros directorios de proyectos, con envío paralelo, sesiones y trabajos asíncronos.
Documentación
agent-dispatch
Servidor MCP que permite a los agentes de Claude Code delegar tareas a agentes en otros directorios de proyectos.
Cada agente se ejecuta como una sesión separada de claude -p en su propio directorio de proyecto, heredando los servidores MCP, CLAUDE.md y herramientas de ese proyecto. El agente que llama solo recibe el resultado.
Los proyectos relacionados se pueden agrupar en un grupo — un resumen compartido más una lista de miembros — para que una sesión pueda coordinar el trabajo entre ellos (por ejemplo, repositorios de código + una puerta de enlace de infra/Portainer + una puerta de enlace de analytics).
Funciona con autenticación OAuth, clave API y suscripción de Claude.
Agentes de IA: este README es el documento canónico para usar la herramienta — configuración: Inicio rápido (cada paso tiene una verificación determinista), primera llamada:
dispatch, selección de herramientas: Qué herramienta usar, manejo de fallos: Recuperación de errores. ¿Trabajando en este repositorio? Consulta AGENTS.md.
Inicio rápido
Requisito previo: la CLI de Claude Code debe estar instalada y autenticada. Verifica primero:
claude --version # must print a version — if it fails, install Claude Code before continuing
Luego:
pip install agent-dispatch # or: pipx install agent-dispatch
# 1. Create config + register the MCP server with Claude Code (user scope)
agent-dispatch init
# 2. Register project directories as agents — REPLACE the example paths with
# real directories on your machine; they must exist (~ is expanded, relative
# paths are resolved). Descriptions are auto-generated from project files.
# No second project handy? Use the zero-setup block below instead.
agent-dispatch add infra ~/projects/infra
agent-dispatch add backend ~/projects/backend
# 3. Smoke test — dispatches a real task to the agent added in step 2 and prints
# the answer; exit 0 on success. Default task when none given:
# "What project is this? Describe in one sentence."
agent-dispatch test infra
# 4. Verify the whole install — prints "All checks passed." and exits 0 on success
agent-dispatch doctor
Alternativa sin configuración para los pasos 2–3 (no se necesita un segundo proyecto — registra el directorio actual):
agent-dispatch add self . && agent-dispatch test self "Say hello"
Cada sesión de Claude Code ahora tiene las herramientas de despacho. Verificación independiente: claude mcp list debe imprimir una línea que comience con agent-dispatch:. Desde dentro de una sesión de Claude Code, las primeras llamadas MCP son list_agents(), luego dispatch(...).
Si init falla al registrar el servidor MCP (imprime una advertencia en lugar de Registered MCP server), regístralo manualmente:
claude mcp add-json agent-dispatch "{\"type\":\"stdio\",\"command\":\"$(which agent-dispatch)\",\"args\":[\"serve\"]}" --scope user
Si test falla con un error de permisos (error_type: "permission"), otorga acceso a la herramienta y vuelve a probar:
agent-dispatch update infra --allowed-tools "Bash,Read,Grep" # least privilege
# or, if the agent needs everything (see SECURITY.md for the trade-off):
agent-dispatch update infra --permission-mode bypassPermissions
Cuándo despachar
Sí despacha cuando una tarea necesita herramientas, archivos o contexto de otro proyecto:
- Revisar registros de contenedores mediante el MCP de Portainer del agente de infraestructura
- Consultar una base de datos mediante el MCP de postgres del agente de base de datos
- Leer código o ejecutar pruebas en otro repositorio
No despaches cuando puedas hacerlo tú mismo — despachar inicia una sesión completa de Claude.
Referencia de herramientas MCP
list_agents
Lista todos los agentes configurados. Llama a esta primero para ver qué está disponible.
// Response (capability + permission fields shown only when populated)
[
{
"name": "infra",
"directory": "/home/user/projects/infra",
"description": "Infrastructure agent. MCP: portainer. Stack: Python, Docker",
"healthy": true,
"has_claude_md": true,
"has_mcp_config": true,
"mcp_servers": ["portainer", "postgres"],
"stacks": ["Python", "Docker"],
"dbs": ["Alembic"],
"capabilities": ["docker_logs", "deploy_debug"],
"risky_capabilities": ["restart_services"],
"permission_mode": "bypassPermissions",
"allowed_tools": ["Bash", "Read", "Grep"]
}
]
mcp_servers, stacks y dbs se detectan desde los archivos del proyecto del agente (.mcp.json, Dockerfile, pyproject.toml, Cargo.toml, prisma/, alembic.ini, etc.) para que los llamadores puedan elegir al agente correcto sin despachar una sonda.
inspect_agent
Búsqueda detallada económica — lee los archivos del agente sin iniciar una sesión de claude. Devuelve la configuración completa (tiempo de espera, modelo, presupuesto, modo de permisos, herramientas permitidas/no permitidas), MCP/stacks/BDs detectados, además de vistas previas breves de CLAUDE.md y README.md cuando están presentes.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
name | string | sí | Nombre del agente de list_agents |
preview_lines | int | no | Máximo de líneas de CLAUDE.md/README.md (predeterminado 40, máximo 200, 0 desactiva) |
Úsala antes de dispatch_async/dispatch para confirmar que un agente tiene las herramientas y el contexto para tu tarea — mucho más económica que un despacho de sonda.
Grupos
Un grupo agrupa agentes relacionados en un conjunto de trabajo entre proyectos — típicamente algunos repositorios de código más puertas de enlace de capacidades (un agente de infra con un MCP de Portainer, un agente de analytics con un navegador + Yandex Metrica). Permite que una sesión orquestadora coordine trabajo que abarca código, implementación y verificación.
Un grupo es una capa descriptiva, no un motor de ejecución — no hay enrutador ni máquina de estados. Tú eliges miembros leyendo sus pistas y coordinas con las herramientas de despacho normales. Dos campos de texto se dirigen a dos audiencias:
description— orientado al orquestador: cómo coordinar el grupo (el orden de los pasos, a quién llamar para qué). Expuesto porlist_groups/inspect_group, nunca inyectado en el prompt de un miembro.shared_context— hechos orientados al miembro (nombres de stacks, IDs de contadores, convenciones) que se mantienen independientemente de qué miembro los lea. Se antepone automáticamente alcontextde un miembro cuando pasasgroup=.
Los miembros referencian agentes por nombre; la membresía es de muchos a muchos (una puerta de enlace compartida puede pertenecer a varios grupos). Gestiona grupos con la CLI de agent-dispatch group (add/list/inspect/update/remove) o editando agents.yaml.
list_groups() — lectura económica, sin subprocesos, de cada grupo: descripción, número de miembros y la pista de use_for + salud de cada miembro. Un miembro cuyo agente fue eliminado se marca como "unknown": true en lugar de fallar.
inspect_group(name) — el resumen completo de un grupo: description, el shared_context completo y la lista de miembros. Para un análisis profundo de un miembro específico, llama a inspect_agent(member) — inspect_group deliberadamente sigue siendo una lectura económica de membresía.
Usar un grupo — pasa group= a dispatch (o por elemento en dispatch_parallel). El agente debe ser miembro; el shared_context de su grupo se adjunta automáticamente:
# From the shop-web codebase, hand the deploy to the infra gateway.
# The "shop" group's facts (stack name, counter id) are auto-attached.
dispatch(
agent="infra",
task="Redeploy the shop-web container",
caller="shop-web",
goal="ship the checkout fix",
group="shop",
)
group="" (el predeterminado) es idéntico byte por byte a un despacho normal — los hechos compartidos se integran en la cadena de context, por lo que la caché de resultados desambigua grupos automáticamente y las llamadas sin grupo no se ven afectadas.
dispatch
Delegación de tareas de una sola vez. Los resultados se almacenan en caché — las solicitudes idénticas dentro del TTL devuelven al instante.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
agent | string | sí | Nombre del agente de list_agents |
task | string | sí | Qué hacer — sé específico, el agente no tiene contexto de tu conversación |
context | string | no | Contexto adicional: mensajes de error, fragmentos de código, trazas de pila |
caller | string | no | Tu proyecto/rol — ayuda al agente a entender quién pregunta |
goal | string | no | Objetivo más amplio — ayuda al agente a tomar mejores decisiones |
response_format | string | no | "json" para solicitar un único valor JSON; el resultado analizado llega a parsed_result. Vacío = texto de formato libre. |
return_ref | bool | no | Cuando true, devuelve solo un ref + vista previa del resumen en lugar del texto completo del resultado. Usa fetch_result(ref) para cargar el texto completo bajo demanda. |
summary_chars | int | no | Máximo de caracteres del texto del resultado a incluir en la respuesta de referencia (predeterminado 500). |
timeout_seconds | int | no | Anulación de tiempo de espera única para esta llamada (0 = tiempo de espera configurado del agente; limitado a 10–7200). Sin necesidad de editar la configuración para tareas conocidas como largas. |
group | string | no | Nombre del grupo (de list_groups). El agente debe ser miembro; el shared_context del grupo (hechos orientados al miembro) se antepone automáticamente a context. Vacío = despacho normal. Consulta Grupos. |
# Call — recommended form (always include caller and goal)
dispatch(
agent="infra", # must exist in list_agents()
task="Check container logs for errors related to the scheduler service",
context="Error: TypeError at scheduler.py:42",
caller="backend", # your project/role
goal="debug production crash" # the broader objective
)
// Response (success)
{
"agent": "infra",
"success": true,
"result": "Found 3 errors in container logs: TypeError in scheduler.py:42...",
"session_id": "sess-abc-123",
"cost_usd": 0.02,
"duration_ms": 5000,
"num_turns": 2
}
// Response (failure — error_type helps you handle programmatically)
{
"agent": "infra",
"success": false,
"result": "",
"error": "Tool_use is not allowed in this permission mode\n\nHint: ...",
"error_type": "permission"
}
Valores de error_type: permission (herramienta/acción denegada), timeout, recursion (profundidad de despacho excedida), not_found (directorio o CLI faltante), budget (la CLI de claude detuvo la sesión en max_budget_usd), cli_error (otros fallos). Los errores de permisos y presupuesto incluyen una pista accionable.
Tiempos de espera reanudables: cada despacho nuevo preasigna un UUID de sesión (--session-id), por lo que un despacho con tiempo de espera agotado aún devuelve un session_id — la transcripción parcial sobrevive a la terminación. El error de tiempo de espera explica la recuperación: reanuda con dispatch_session(agent, "Continue where you left off", session_id=...), reintenta con un timeout_seconds mayor, o usa dispatch_async.
Visibilidad de herramientas denegadas: en modo no interactivo, la CLI de claude deniega automáticamente las herramientas que el agente no tiene permitido usar — el agente entonces a menudo "tiene éxito" con una respuesta como "Necesito tu permiso para una consulta de solo lectura". Cuando eso sucede, la respuesta lleva la señal determinista: denied_tools (analizado desde el permission_denials de la CLI) más un hint que explica que el resultado puede estar incompleto y cómo otorgar acceso. success permanece true — es una señal suave, no un fallo.
// Response (success, but a tool was blocked)
{
"agent": "analysis",
"success": true,
"result": "Here is the offline mapping. To finish I'd need to run one read-only query...",
"denied_tools": ["Bash"],
"hint": "1 tool call(s) were denied by permissions: Bash. The result may be incomplete..."
}
Salida JSON estructurada: pasa response_format="json" para pedir al agente un único valor JSON. El ejecutor añade un pie de instrucción ("responde con un único valor JSON válido, sin cercas, sin prosa") y en caso de éxito analiza la respuesta — el valor analizado llega a parsed_result. El texto crudo siempre está en result. Los fallos de análisis dejan parsed_result=None pero no fallan el despacho (modo suave).
// Response with response_format="json"
{
"agent": "infra",
"success": true,
"result": "{\"errors\": 3, \"first_at\": \"14:02\"}",
"parsed_result": {"errors": 3, "first_at": "14:02"}
}
Siempre pasa caller y goal — el agente despachado ve un prompt estructurado:
## Goal
debug production crash
## Dispatched by
backend
## Context
Error: TypeError at scheduler.py:42
## Task
Check container logs for recent errors related to the scheduler service
dispatch_session
Multiturno: continúa una conversación con un agente. La primera llamada inicia una sesión, pasa session_id de vuelta para continuar. Nunca se almacena en caché.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
agent | string | sí | Nombre del agente |
task | string | sí | Tarea o mensaje de seguimiento |
session_id | string | no | De la respuesta anterior — vacío para nueva sesión |
context | string | no | Contexto adicional |
caller | string | no | Quién está despachando |
goal | string | no | Objetivo más amplio |
timeout_seconds | int | no | Anulación de tiempo de espera única (0 = predeterminado del agente; limitado a 10–7200) |
dispatch_session es también la ruta de recuperación de tiempo de espera: un dispatch con tiempo agotado devuelve un session_id — pásalo aquí con task="Continue where you left off" para recuperar el trabajo parcial en lugar de reiniciar.
Turn 1: dispatch_session("infra", "List running containers")
→ session_id: "sess-abc"
Turn 2: dispatch_session("infra", "Restart the nginx one", session_id="sess-abc")
→ agent remembers previous context
dispatch_parallel
Ejecuta múltiples tareas concurrentemente. Mucho más rápido que llamadas secuenciales de dispatch.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
dispatches | string (JSON) | sí | Arreglo JSON de {"agent", "task", "context?", "caller?", "goal?", "response_format?", "return_ref?", "summary_chars?", "timeout_seconds?", "group?"} (un group por elemento valida la membresía de antemano e inyecta automáticamente su shared_context) |
aggregate | string | no | Nombre del agente para sintetizar todos los resultados en una sola respuesta |
Importante: dispatches es una cadena JSON, no una lista.
// Input
[
{"agent": "infra", "task": "check pod logs for errors", "caller": "backend", "goal": "debug crash"},
{"agent": "db", "task": "are all migrations applied?", "caller": "backend", "goal": "debug crash"}
]
// Response (without aggregate)
[
{"agent": "infra", "success": true, "result": "No errors in pod logs", ...},
{"agent": "db", "success": true, "result": "All migrations applied", ...}
]
// Response (with aggregate="backend")
{
"individual_results": [
{"agent": "infra", "success": true, "result": "No errors in pod logs", ...},
{"agent": "db", "success": true, "result": "All migrations applied", ...}
],
"aggregated": {
"agent": "backend",
"success": true,
"result": "Summary: all systems nominal. No pod errors, all migrations applied."
}
}
dispatch_stream
Igual que dispatch pero muestra progreso en vivo mientras el agente trabaja. Úsala para tareas de larga duración. No se almacena en caché.
Los parámetros son los mismos que dispatch excepto return_ref/summary_chars (la transmisión es incompatible con el modo de referencia) y group (la inyección de contexto de grupo solo se admite en dispatch y por elemento en dispatch_parallel).
dispatch_dialogue
Dos agentes colaboran a través de conversación multiturno. Nunca se almacena en caché.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
requester | string | sí | Agente con el problema/contexto |
responder | string | sí | Agente con la experiencia/herramientas |
topic | string | sí | Problema o pregunta a discutir |
max_rounds | int | no | Máximo de rondas de ida y vuelta (predeterminado: 3, máximo: 10) |
Cada ronda cuesta hasta 2 despachos. Los agentes señalan finalización con [RESOLVED].
// Response
{
"resolved": true,
"rounds": 2,
"total_cost_usd": 0.04,
"total_duration_ms": 12000,
"final_answer": "Staging had 1 pending migration. Applied successfully.",
"conversation": [
{"agent": "db", "role": "responder", "round": 1, "message": "Which environment?", "cost_usd": 0.01},
{"agent": "backend", "role": "requester", "round": 1, "message": "Staging", "cost_usd": 0.01},
{"agent": "db", "role": "responder", "round": 2, "message": "Applied. [RESOLVED]", "cost_usd": 0.01}
]
}
add_agent
Registra un nuevo directorio de proyecto como agente. La descripción se genera automáticamente desde los archivos del proyecto si se omite.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
name | string | sí | Nombre del agente (letras, dígitos, guiones, guiones bajos) |
directory | string | sí | Ruta a un directorio de proyecto existente (~ se expande, las rutas relativas se resuelven) |
description | string | no | Qué puede hacer este agente — se genera automáticamente si está vacío |
timeout | int | no | Tiempo de espera en segundos (0 = 300; este es un valor predeterminado literal, no settings.default_timeout) |
max_budget_usd | float | no | Costo máximo en USD por despacho (0 = heredar settings.default_max_budget_usd; sin límite solo cuando eso tampoco está configurado) |
permission_mode | string | no | Modo de permisos (p. ej. default, plan, bypassPermissions) |
allowed_tools | string | no | Herramientas permitidas separadas por comas (p. ej. "Bash,Read,Edit") |
disallowed_tools | string | no | Herramientas no permitidas separadas por comas |
capabilities | string | no | Etiquetas de capacidad separadas por comas (p. ej. "docker_logs,deploy_debug") |
risky_capabilities | string | no | Etiquetas de alto riesgo separadas por comas (p. ej. "restart_services") |
update_agent
Actualiza la configuración de un agente existente. Solo se cambian los campos no vacíos. Pasa "none" para limpiar un campo.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
name | string | sí | Nombre del agente a actualizar |
description | string | no | Nueva descripción |
timeout | int | no | Nuevo tiempo de espera (0 = no cambiar) |
max_budget_usd | float | no | Nuevo límite de presupuesto (0 = no cambiar; negativo limpia el límite por agente, después de lo cual se aplica settings.default_max_budget_usd) |
model | string | no | Anulación del modelo. "none" para limpiar |
permission_mode | string | no | Modo de permisos. "none" para limpiar |
allowed_tools | string | no | Separado por comas. "none" para limpiar |
disallowed_tools | string | no | Separado por comas. "none" para limpiar |
capabilities | string | no | Separado por comas. "none" para limpiar |
risky_capabilities | string | no | Separado por comas. "none" para limpiar |
Cambiar la configuración de un agente elimina los resultados en caché de ese agente — la clave de caché contiene el nombre del agente, por lo que un agente re-apuntado o con permisos cambiados seguiría respondiendo desde la configuración anterior durante el resto del TTL. Lo mismo se aplica a add_agent y remove_agent.
remove_agent
Elimina un agente de la configuración.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
name | string | sí | Nombre del agente a eliminar |
cache_stats / cache_clear
Ver la tasa de aciertos de caché y el tamaño, o limpiar todos los resultados en caché.
Referencias de resultados — return_ref + fetch_result
Para despachos cuyo texto de resultado es grande (auditorías, volcados de registros, búsquedas de código), pasar el texto completo de vuelta infla el contexto del agente que llama. Usa return_ref=True para obtener solo una pequeña referencia:
dispatch(agent="infra", task="audit every container", return_ref=True, summary_chars=200)
-> {"ref": "8f3a...e1", "agent": "infra", "success": true,
"size": 14823, "summary_chars": 200,
"summary": "Inspected 32 containers. Found 3 OOM kills in the last hour:\n- worker-3...",
"cost_usd": 0.08, "duration_ms": 9200}
// Later, when you actually need to read the result:
fetch_result(ref="8f3a...e1") -> full DispatchResult JSON
fetch_result(ref="8f3a...e1", max_chars=2000) -> truncated, plus {"truncated": true, "full_size": 14823}
Las referencias reutilizan el mismo almacenamiento que los trabajos de dispatch_async (bajo ~/.config/agent-dispatch/jobs/), por lo que cualquier job_id devuelto por dispatch_async también es un ref válido para fetch_result. parsed_result (cuando response_format="json" está configurado) es pequeño y siempre se incluye directamente en la respuesta de referencia — no se necesita una segunda búsqueda.
Despacho asíncrono — dispatch_async, dispatch_status, dispatch_wait, dispatch_cancel, dispatch_jobs, dispatch_gc
Cuando una tarea despachada va a tomar tiempo, no quieres bloquear tu propia ranura de herramienta durante minutos. El despacho asíncrono devuelve un job_id inmediatamente y te permite verificar cuando estés listo.
// 1. fire and forget (timeout_seconds= works here too for known-long tasks)
dispatch_async(agent="infra", task="audit every container log for OOM kills today")
-> {"job_id": "8f3a...e1", "status": "pending", "agent": "infra"}
// 2. do other work, then check progress (non-blocking)
// `progress` is a rolling tail of what the agent is doing right now
dispatch_status(job_id="8f3a...e1")
-> {"id": "8f3a...e1", "status": "running", "started_at": 1730000123.4,
"progress": ["Using tool: Bash", "Scanning container logs for OOM events..."], ...}
// 3. or block until done (timeout_seconds default: 60, capped at 3600)
dispatch_wait(job_id="8f3a...e1", timeout_seconds=120)
-> {"id": "8f3a...e1", "status": "done", "result": {"agent": "infra", "success": true, ...}}
// If the timeout fires, the job keeps running:
-> {"id": "...", "status": "running", "timed_out_waiting": true}
dispatch_cancel(job_id) cancela un trabajo pendiente, y también mata el subproceso claude de un trabajo en ejecución cuando el trabajo fue iniciado por la misma instancia del servidor (el trabajo se marca cancelled primero, para que la escritura final del trabajador no pueda deshacerlo; el trabajo parcial se pierde pero la cola de progreso se conserva). Un trabajo en ejecución iniciado por una ejecución anterior del servidor no se puede matar de forma segura y se deja terminar. La respuesta lleva un outcome de cancelled, cancelled_running, running (no propiedad de este servidor), already_terminal o not_found.
Los trabajadores asíncronos se ejecutan con transmisión bajo el capó: el archivo de trabajo mantiene una cola móvil (últimas 20 líneas, ~1 escritura/seg) de texto del asistente y eventos de uso de herramientas. dispatch_status lo muestra como progress mientras el trabajo se ejecuta y lo conserva después como un rastro de autopsia; dispatch_jobs muestra last_progress para trabajos en ejecución.
dispatch_jobs(status?) lista trabajos recientes como resúmenes (filtra por pending / running / done / failed / cancelled). dispatch_gc(max_age_days=7) purga trabajos terminales más antiguos que el umbral — los trabajos pendientes y en ejecución nunca se eliminan.
El estado del trabajo persiste en disco en ~/.config/agent-dispatch/jobs/ (anula con AGENT_DISPATCH_JOBS_DIR). Un archivo JSON por trabajo, escrito solo por el propietario (0o600) con escrituras atómicas — seguro de leer o ls mientras los trabajos están en vuelo. Los job_id proporcionados por el llamador se validan como hex de 32 caracteres antes de cualquier acceso a archivos (sin traversal de rutas). Al inicio, el servidor recupera trabajos que una instancia bloqueada abandonó: los running atascados más de una hora, y los pending más de 24 horas, se marcan failed para que dejen de ser consultados para siempre y se vuelvan recolectables por dispatch_gc. (El umbral de pending es deliberadamente largo — el directorio de trabajos es compartido por cada servidor en ejecución, por lo que un trabajo en cola detrás del límite de concurrencia de otro servidor no debe ser barrido.)
| Cuándo usar asíncrono | Cuándo usar dispatch |
|---|---|
| Tarea larga (minutos) — quieres seguir trabajando | Tarea corta — necesitas la respuesta ahora mismo |
| Varias tareas largas que recogerás después | Varias tareas cortas → dispatch_parallel |
| No te importa el almacenamiento en caché (cada llamada es un trabajo nuevo) | En caché por defecto — las solicitudes idénticas son gratuitas |
Qué Herramienta Usar
| Escenario | Herramienta |
|---|---|
| Pregunta rápida y puntual a otro proyecto | dispatch |
| Flujo de trabajo de varios pasos con seguimientos | dispatch_session |
| Necesitas respuestas de varios agentes a la vez | dispatch_parallel |
| Tarea larga, quieres ver el progreso | dispatch_stream |
| Dos agentes necesitan colaborar | dispatch_dialogue |
| Necesitas un resumen combinado de varios agentes | dispatch_parallel con aggregate |
| Tarea larga — no bloquees tu ranura de herramienta | dispatch_async + dispatch_wait |
| Verificar el progreso sin bloquear | dispatch_status |
| Tarea conocidamente larga, puntual | cualquier herramienta de despacho con timeout_seconds=... |
| Un despacho agotó el tiempo | dispatch_session con el session_id del error |
| Coordinar un conjunto de proyectos relacionados | define un grupo, luego dispatch(..., group=name) |
| Ver qué agentes forman un conjunto de trabajo | list_groups / inspect_group |
Recuperación de Errores
Los fallos son deterministas: verifica success, luego ramifica según error_type.
error_type | Significado | Recuperación |
|---|---|---|
permission | Se denegó una llamada de herramienta | update_agent(name, allowed_tools="Bash,Read") (privilegio mínimo) o update_agent(name, permission_mode="bypassPermissions"), luego re-despacha. El texto de error incluye una pista con la corrección exacta. |
timeout | Proceso eliminado en el tiempo de espera | Reanuda el trabajo parcial: dispatch_session(agent, "Continue where you left off", session_id=<from the error text>). O reintenta con un timeout_seconds= más grande, o usa dispatch_async. Un despacho transmitido que produjo su respuesta antes de la fecha límite devuelve esa respuesta con un hint en lugar de fallar. |
not_found | Directorio del agente o CLI de claude faltante | list_agents() → verifica healthy. Vuelve a agregar el agente con una ruta existente, o ejecuta agent-dispatch doctor para encontrar lo que falta. |
recursion | El anidamiento de despacho excedió max_dispatch_depth (predeterminado 3) | No despaches desde agentes despachados; si el anidamiento es intencional, aumenta max_dispatch_depth en la configuración. |
budget | La CLI de claude terminó la sesión en el límite de gasto de max_budget_usd — la respuesta está incompleta | Aumenta el límite (update_agent(name, max_budget_usd=2.0)), cambia a un model más barato, o divide la tarea. La sesión parcial es reanudable: dispatch_session(agent, "Continue where you left off", session_id=<from the result>). |
cli_error | Cualquier otra cosa del subproceso de claude | Lee el texto de error; ejecuta agent-dispatch doctor para problemas de entorno; reintenta una vez si es transitorio. |
Tres señales suaves que llegan con success: true:
denied_tools+hint— el agente terminó pero algunas llamadas de herramientas fueron bloqueadas; el resultado puede estar incompleto. Otorga acceso (ver la fila depermission) y re-despacha.parsed_result: nullconresponse_format="json"— la respuesta no era JSON válido; el texto sin procesar sigue enresult. Advertencia: un agente que no puede cumplir devuelve{"error": "<reason>"}— que se analiza correctamente — así que también verificaparsed_resultpara una clave de"error".budget_exceeded: true—cost_usdsuperó elmax_budget_usddel agente (o el predeterminado de la configuración) sin que la CLI detuviera la ejecución (el turno final puede exceder el límite). El despacho no falla — el dinero ya está gastado — pero un agente descontrolado ahora es visible. Ajusta la tarea, elige un modelo más barato, o aumenta el presupuesto. Una ejecución que la CLI sí detuvo falla conerror_type: "budget"en su lugar.
Los errores a nivel de herramienta (agente desconocido, entrada malformada) devuelven un sobre simple en lugar de un DispatchResult:
{"error": "Unknown agent: 'foo'. Available: infra, db, monitoring"}
Configuración
Config en ~/.config/agent-dispatch/agents.yaml (anulación: variable de entorno AGENT_DISPATCH_CONFIG):
agents:
infra:
directory: ~/projects/infra
description: "Infrastructure agent. MCP: portainer."
timeout: 300 # seconds, default: 300
capabilities: # capability labels, shown in list_agents
- docker_logs
- deploy_debug
risky_capabilities: # high-risk labels, surfaced for visibility
- restart_services
# model: sonnet # optional model override
# max_budget_usd: 1.0 # cost limit per dispatch
# permission_mode: bypassPermissions # one of: default | plan | bypassPermissions
# allowed_tools: # restrict which tools the agent can use
# - Read
# - Grep
# disallowed_tools: # block specific tools
# - Write
# Optional: bundle related agents into a cross-project working set.
# A descriptive layer — no router; the orchestrating session coordinates
# with the normal dispatch tools. See the Groups section above.
groups:
shop:
# ORCHESTRATOR-facing: how to coordinate the group. Surfaced by
# list_groups/inspect_group, NEVER injected into a member's prompt.
description: "After a code change: deploy via infra, then verify via analytics."
# MEMBER-facing facts, auto-prepended to dispatch(..., group="shop").
shared_context: |
Prod runs in Portainer stack "shop". Metrica counter 12345.
members: # reference agents above (many-to-many)
- agent: infra
use_for: deploy, restart, container logs
# - agent: backend
# use_for: orders/payments endpoints
settings:
default_timeout: 300
# default_permission_mode: bypassPermissions # inherited by all agents
# default_allowed_tools: # inherited when agent has none
# - Bash
# - Read
# - Edit
max_dispatch_depth: 3 # recursion protection
max_concurrency: 5 # max parallel claude -p processes (per dispatch path)
# job_retention_days: 30 # 0 (default) = never prune. See "Job retention" below.
cache:
enabled: true
ttl: 300 # seconds
max_size: 1000 # max cached entries; oldest evicted first (FIFO)
La configuración se recarga en cada llamada de herramienta — agrega agentes sin reiniciar.
Retención de trabajos
Cada dispatch_async y cada dispatch(..., return_ref=True) escribe un
registro en ~/.config/agent-dispatch/jobs/, y nada lo elimina por sí solo —
dispatch_gc debe ejecutarse manualmente. El directorio por lo tanto crece sin
límite, y dispatch_jobs más la recuperación de trabajos obsoletos que se ejecuta en cada
inicio del servidor leen y analizan cada archivo en él.
Configura job_retention_days para podar registros terminales (hechos/fallidos/cancelados) más antiguos
de N días cuando un servidor se inicia:
settings:
job_retention_days: 30
El valor predeterminado es 0 — desactivado — porque esos registros son tu propio historial de
despachos pasados y eliminarlos no se puede deshacer. Los trabajos pendientes y en ejecución nunca
se tocan.
agent-dispatch gc --days N y la herramienta dispatch_gc aplican la misma regla como una
acción puntual. Ambos eliminan inmediatamente y reportan el conteo; ninguno previsualiza, así que
verifica qué hay primero con agent-dispatch jobs.
Auto-Descripción
agent-dispatch add sin --description genera una a partir de:
CLAUDE.md— primer párrafo significativo (prioridad)README.md— primera línea sustancial (respaldo)pyproject.toml/package.json— descripción del proyecto.mcp.json— lista nombres de servidores MCP- Indicadores de stack — Docker, Rust, Go, Python, Node.js
- Indicadores de BD — Prisma, Alembic, migraciones
Capacidades Explícitas
La auto-descripción es útil, pero las capabilities explícitas hacen más claro para qué sirve cada agente. Agrega etiquetas de tarea cortas en snake_case a los agentes:
agent-dispatch update infra \
--capabilities docker_logs,deploy_debug \
--risky-capabilities restart_services
list_agents y inspect_agent muestran capabilities y risky_capabilities para que el llamador pueda elegir el agente correcto de un vistazo — risky_capabilities marca habilidades de mayor riesgo (p. ej. reiniciar servicios) para un escrutinio adicional.
Cómo Funciona
Your Claude Code session
│
├─ dispatch("infra", "find errors", caller="backend", goal="debug crash")
│
▼
agent-dispatch MCP server
├─ cache check → hit? return cached result
├─ semaphore → limit concurrent processes
└─ subprocess.run("claude -p ...", cwd=~/projects/infra/)
│
▼
New Claude Code session in ~/projects/infra/
├─ Inherits: CLAUDE.md, .mcp.json, project tools
├─ Receives structured prompt with goal/caller/context/task
└─ Returns result → cached for future identical requests
Seguridad
- Protección contra recursión — la variable de entorno
AGENT_DISPATCH_DEPTHrastrea el anidamiento. Límite predeterminado: 3. Mejor esfuerzo a través del límite de subprocesos (ver SECURITY.md). - Guardia contra inyección de argumentos — los campos CLI estructurados (
session_id,model,permission_mode, nombres de herramientas) que comienzan con-se rechazan para que no puedan introducir banderasclaudeadicionales. - Guardia contra recorrido de rutas — los valores
job_id/refproporcionados por el llamador se validan como hexadecimal de 32 caracteres antes de cualquier acceso al sistema de archivos. - Estado solo para el propietario — los archivos de trabajo (
0o600) yagents.yaml(0o600) se escriben solo para el propietario; sus directorios son0o700. - Control de costos —
max_budget_usdpor agente o global se pasa al CLIclaudecomo--max-budget-usd, de modo que un envío descontrolado se detiene en el límite y regresa comoerror_type: "budget"con unsession_idreanudable. Un exceso que supere el presupuesto sin detenerse se marca posteriormente conbudget_exceeded: truey una pista. - Concurrencia —
max_concurrency(predeterminado: 5) limita los procesosclaude -pparalelos. Nota: las rutas de envío síncronas y asíncronas usan semáforos separados, por lo que el peor caso total es2 × max_concurrency. - Tiempo de espera — por agente o global (predeterminado: 300s). Un envío en streaming ejecuta el agente en su propio grupo de procesos, por lo que el plazo mata todo el árbol: un proceso que el agente dejó corriendo en segundo plano no puede mantener el envío (y su espacio de concurrencia) abierto más allá del tiempo de espera.
- Caché — las solicitudes
(agent, task, context, caller, goal, response_format)idénticas devuelven resultados en caché, limitados porcache.max_size(la entrada más antigua se elimina primero). Solo se almacenan en caché los éxitos limpios: los fallos, los resultados condenied_toolsy los resultados marcados conbudget_exceededno se almacenan, por lo que la recuperación documentada de "otorgar acceso / aumentar el límite, luego reenviar" nunca recibe una respuesta obsoleta y limitada. Cambiar la configuración de un agente invalida sus entradas. Las sesiones y diálogos nunca se almacenan en caché. Un envíogroup=pliega elshared_contextdel grupo encontext, por lo que diferentes grupos se almacenan en caché por separado y un envío simple no se ve afectado. - Configuración duradera —
agents.yamlse escribe atómicamente (archivo temporal + renombrado), por lo que una escritura interrumpida nunca puede truncarlo. Cada ruta de mutación (tanto CLI como servidor MCP) también toma un bloqueo de asesoramiento entre procesos, por lo que las ediciones concurrentes no eliminan agentes entre sí. El bloqueo es de mejor esfuerzo por diseño: después de esperar 10 segundos, registra una advertencia y continúa de todos modos, porque un titular de bloqueo atascado no debe congelar el servidor MCP — por lo que en una configuración muy disputada es posible una actualización perdida, pero no una truncada.
Ver SECURITY.md para el modelo de amenazas completo (incluido el riesgo de escalada bypassPermissions y los archivos de trabajo en disco).
CLI
| Comando | Descripción |
|---|---|
agent-dispatch init | Crear configuración + registrar servidor MCP con Claude Code |
agent-dispatch add <name> <dir> | Agregar un agente (genera descripción automáticamente) |
agent-dispatch update <name> | Actualizar configuración del agente (permisos, tiempo de espera, modelo, etc.) |
agent-dispatch remove <name> | Eliminar un agente |
agent-dispatch list | Listar agentes con estado de salud y permisos |
agent-dispatch group <add|list|inspect|update|remove> | Gestionar grupos — conjuntos de trabajo de agentes entre proyectos |
agent-dispatch describe <name> | Mostrar configuración completa para un agente (herramientas de tres estados, archivos de proyecto) |
agent-dispatch test <name> [task] [--stream] | Probar un agente con un envío (--stream para progreso en vivo) |
agent-dispatch doctor | Diagnosticar instalación: CLI de Claude, registro MCP, salud del agente y membresía de grupos |
agent-dispatch jobs [--status --limit] | Listar trabajos de envío asíncronos (más recientes primero) |
agent-dispatch job <id> | Mostrar un trabajo: estado, cola de progreso, vista previa del resultado |
agent-dispatch cancel <id> | Cancelar un trabajo pendiente (trabajos en ejecución: use la herramienta MCP dispatch_cancel) |
agent-dispatch gc [--days N | --all] | Purgar trabajos terminales mayores de N días (predeterminado 7; --all purga cualquier edad) |
agent-dispatch serve | Iniciar servidor MCP (stdio, usado por Claude Code) |
Requisitos
- Python >= 3.10
- CLI de Claude Code instalado, autenticado y en
PATH(verificar:claude --version)
Licencia
MIT