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

PyPI CI Python License

Servidor MCP que permite a los agentes de Claude Code delegar tareas a agentes en otros directorios de proyectos.

agent-dispatch mascot

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 sola 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 a través del MCP de Portainer del agente de infraestructura
  • Consultar una base de datos a través del 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"],
    "typical": {"median_seconds": 143.3, "p90_seconds": 178.3, "median_cost_usd": 0.2666}
  }
]

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.

typical se mide, no se declara: proviene de los despachos recientes de este agente en el registro de uso y aparece solo después de algunas ejecuciones. Úsalo para dimensionar timeout_seconds y para saber cuánto costará una llamada antes de hacerla. Está ausente cuando el registro está desactivado o el agente tiene muy poca historia — no hay datos es mejor que una duración típica derivada de dos ejecuciones.

inspect_agent

Consulta detallada y 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/pilas/BD detectados, más vistas previas cortas de CLAUDE.md y README.md cuando están presentes.

ParámetroTipoRequeridoDescripción
namestringsíNombre del agente de list_agents
preview_linesintnoMáx. de líneas de CLAUDE.md/README.md (predeterminado 40, máx. 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.

También lleva el instructions del agente (sus órdenes permanentes) y el bloque completo de typical — el recortado en list_agents más dispatches, failed y el desglose de outcomes:

"typical": {
  "dispatches": 24, "median_seconds": 143.3, "p90_seconds": 178.3,
  "median_cost_usd": 0.2666, "failed": 1, "outcomes": {"done": 21, "partial": 2}
}

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, despliegue 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 apuntan a dos audiencias:

  • description — orientado al orquestador: cómo coordinar el grupo (el orden de los pasos, a quién llamar para qué). Expuesto por list_groups/inspect_group, nunca inyectado en el prompt de un miembro.
  • shared_context — hechos orientados a miembros (nombres de pilas, IDs de contadores, convenciones) que se mantienen independientemente de qué miembro los lea. Se antepone automáticamente al context de un miembro cuando pasas group=.

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 viaja 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 simple — 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 regresan instantáneamente.

ParámetroTipoRequeridoDescripción
agentstringsíNombre del agente de list_agents
taskstringsíQué hacer — sé específico, el agente no tiene contexto de tu conversación
contextstringnoContexto adicional: mensajes de error, fragmentos de código, trazas de pila
callerstringnoTu proyecto/rol — ayuda al agente a entender quién pregunta
goalstringnoObjetivo más amplio — ayuda al agente a tomar mejores decisiones de compensación
response_formatstringno"json" para solicitar un único valor JSON; el resultado analizado llega a parsed_result. Vacío = texto de formato libre.
return_refboolnoCuando 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_charsintnoMáx. de caracteres del texto del resultado para incluir en la respuesta de referencia (predeterminado 500).
timeout_secondsintnoAnulación del tiempo de espera 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.
groupstringnoNombre del grupo (de list_groups). El agente debe ser miembro; el shared_context del grupo (hechos orientados a miembros) se antepone automáticamente a context. Vacío = despacho simple. 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,
  "outcome": "done"
}

// 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), usage_limit (la cuenta de Claude alcanzó su límite de tasa/sesión), cli_error (otros fallos). Los errores de permisos, presupuesto, límite de uso y tiempo de espera 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 interrupció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

El protocolo de despacho y outcome. claude -p por sí solo no sabe que está siendo dirigido por otro agente: ante una tarea ambigua, terminará felizmente con "¿Podrías aclarar qué servicio quieres decir?" — una ejecución facturada que no respondió nada, y una que una caché simple serviría de nuevo durante todo el TTL. Así que cada despacho también añade un protocolo de despacho corto al prompt del sistema del agente (--append-system-prompt, nunca mezclado con tu texto de tarea):

  • se ejecuta de forma no interactiva, despachado por caller, y nadie responderá una pregunta ni aprobará una acción — declara la suposición y continúa, toma la lectura más segura cuando dos difieren;
  • una herramienta denegada o algo faltante es algo que reportar, no algo en lo que detenerse — termina todo lo demás que sea posible;
  • su presupuesto de tiempo es aproximadamente el tiempo de espera del agente (y su límite de gasto, si se establece uno) — dimensiona el trabajo para que encaje; una respuesta parcial completa es mejor que una perfecta inacabada;
  • lidera con el resultado, luego la evidencia; enumera lo que no se pudo hacer y por qué (esto es lo que hace que un resumen de return_ref — la cabecera del texto — valga la pena leerlo);
  • termina con una línea, STATUS: done, STATUS: partial o STATUS: blocked.

Esa última línea se extrae de result a outcome — el veredicto propio del agente, determinista de verificar: "done" significa completo; "partial" / "blocked" significan que el agente dice que el trabajo está inacabado, el resultado nombra lo que falta, y un hint explica la continuación (generalmente dispatch_session(..., session_id=...)). Lee outcome antes de leer el texto. Nunca cambia success, los resultados de partial / blocked no se almacenan en caché, y un dispatch_parallel(..., aggregate=...) etiqueta a tales miembros para el agregador para que un informe bloqueado no se sintetice como uno terminado. Ausente cuando el agente no reportó uno — el protocolo está desactivado (settings.dispatch_protocol: false), se solicitó response_format="json" (el pie JSON gobierna la forma de la respuesta allí), o el agente simplemente lo omitió. Órdenes permanentes. Por agente instructions (configurado mediante add_agent / update_agent o agent-dispatch update <name> --instructions "...") sigue el protocolo del mismo prompt del sistema en cada despacho — "solo SQL de solo lectura", "nunca reinicies un stack a menos que la tarea lo indique", "responde con líneas de log exactas". A diferencia de context, que es por llamada, y a diferencia del CLAUDE.md propio del proyecto, que está escrito para una sesión interactiva, estas son las reglas para ser despachado.

// Response (success, but the agent says it did not finish)
{
  "agent": "infra",
  "success": true,
  "result": "Restarted horizon. Could not verify the queue drained: the redis container is not reachable from here.",
  "session_id": "sess-abc-123",
  "outcome": "partial",
  "hint": "The agent reports its work is PARTIAL — read the result for what is missing, then continue in the same session via dispatch_session(agent='infra', task='Continue where you left off', session_id='sess-abc-123') or re-dispatch with what it needed."
}

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ámetroTipoRequeridoDescripción
agentstringsíNombre del agente
taskstringsíTarea o mensaje de seguimiento
session_idstringnoDe la respuesta anterior — vacío para una sesión nueva
contextstringnoContexto adicional
callerstringnoQuién está despachando
goalstringnoObjetivo más amplio
timeout_secondsintnoAnulación de tiempo de espera única (0 = predeterminado del agente; limitado a 10–7200)

dispatch_session es también la ruta de recuperación por tiempo de espera: un dispatch que agotó el tiempo 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 de forma concurrente. Mucho más rápido que llamadas secuenciales a dispatch.

ParámetroTipoRequeridoDescripción
dispatchesstring (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 pertenencia de antemano e inyecta automáticamente su shared_context)
aggregatestringnoNombre 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 el progreso en vivo mientras el agente trabaja. Úsalo para tareas de larga duración. No se almacena en caché.

Las notificaciones de progreso mantienen los últimos 100 mensajes pendientes, limitados a 300 caracteres cada uno. Si la salida llega más rápido de lo que el cliente puede recibirla, las notificaciones más antiguas se omiten con un conteo; el resultado final no se ve afectado. Desconectar o cancelar la llamada a la herramienta descarta las notificaciones pendientes y deja de almacenar nuevas. El trabajador aún mantiene su espacio de concurrencia hasta que termina.

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 mediante conversación multiturno. Nunca se almacena en caché.

ParámetroTipoRequeridoDescripción
requesterstringsíAgente con el problema/contexto
responderstringsíAgente con la experiencia/herramientas
topicstringsíProblema o pregunta a discutir
max_roundsintnoMáximo de rondas de ida y vuelta (predeterminado: 3, máximo: 10)

Cada ronda cuesta hasta 2 despachos. Los agentes señalan la 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 a partir de los archivos del proyecto si se omite.

ParámetroTipoRequeridoDescripción
namestringsíNombre del agente (letras, dígitos, guiones, guiones bajos)
directorystringsíRuta a un directorio de proyecto existente (~ se expande, las rutas relativas se resuelven)
descriptionstringnoQué puede hacer este agente — se genera automáticamente si está vacío
timeoutintnoTiempo de espera en segundos (0 = 300; este es un valor predeterminado literal, no settings.default_timeout)
max_budget_usdfloatnoCosto máximo finito en USD por despacho (0 = heredar settings.default_max_budget_usd; sin límite solo cuando eso tampoco está configurado)
permission_modestringnoModo de permisos (p. ej. default, plan, bypassPermissions)
allowed_toolsstringnoHerramientas permitidas separadas por comas (p. ej. "Bash,Read,Edit")
disallowed_toolsstringnoHerramientas no permitidas separadas por comas
capabilitiesstringnoEtiquetas de capacidad separadas por comas (p. ej. "docker_logs,deploy_debug")
risky_capabilitiesstringnoEtiquetas de alto riesgo separadas por comas (p. ej. "restart_services")
instructionsstringnoÓrdenes permanentes añadidas al prompt del sistema del agente en cada despacho (consulta el protocolo de despacho)

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ámetroTipoRequeridoDescripción
namestringsíNombre del agente a actualizar
descriptionstringnoNueva descripción
timeoutintnoNuevo tiempo de espera (0 = no cambiar)
max_budget_usdfloatnoNuevo 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)
modelstringnoAnulación de modelo. "none" para limpiar
permission_modestringnoModo de permisos. "none" para limpiar
allowed_toolsstringnoSeparado por comas. "none" para limpiar
disallowed_toolsstringnoSeparado por comas. "none" para limpiar
capabilitiesstringnoSeparado por comas. "none" para limpiar
risky_capabilitiesstringnoSeparado por comas. "none" para limpiar
instructionsstringnoÓrdenes permanentes para cada despacho (reemplaza el texto). "none" para limpiar

Cambiar la configuración de un agente mediante una herramienta de mutación MCP elimina inmediatamente sus resultados en caché. Las claves de caché también incluyen la configuración del agente y los valores predeterminados de despacho, por lo que las ediciones a través de la CLI, otro servidor o YAML surten efecto en la siguiente solicitud. Un trabajador que termina con una configuración más antigua no puede sobrescribir las respuestas de la nueva configuración. Los agentes sin cambios conservan su caché.

remove_agent

Elimina un agente de la configuración.

ParámetroTipoRequeridoDescripción
namestringsíNombre del agente a eliminar

cache_stats / cache_clear

Ver la tasa de aciertos y el tamaño de la caché, o limpiar todos los resultados almacenados en caché.

Referencias de resultados — return_ref + fetch_result

Para despachos cuyo texto de resultado es grande (auditorías, volcados de logs, 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 referencia pequeña en su lugar:

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 un tiempo, no quieres bloquear tu propio espacio de herramienta durante minutos. El despacho asíncrono devuelve un job_id inmediatamente y te permite verificar más tarde 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 grupo de procesos claude de un trabajo en ejecución cuando el trabajo fue iniciado por la misma instancia del servidor (el trabajo se marca como cancelled primero, para que la escritura final del trabajador no pueda deshacerlo; el trabajo parcial se pierde pero la cola de progreso se conserva). En POSIX, los descendientes en ese grupo de procesos también se matan, liberando las tuberías heredadas y el espacio de concurrencia. Una cancelación que llega después de que el trabajo fue reclamado pero antes de que su proceso se generara también se respeta: el proceso se mata tan pronto como se inicia. Un reintento de compatibilidad que compite con la cancelación se mata cuando se registra. 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 internamente: el archivo de trabajo mantiene una cola continua (últimas 20 líneas, ~1 escritura/segundo) 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?, limit=50, agent?) lista trabajos recientes como resúmenes (filtra por pending / running / done / failed / cancelled). agent coincide con un nombre exacto, incluidos agentes desde entonces eliminados de la configuración; ambos filtros se aplican antes del límite. 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. Usa dry_run=True para previsualizar los IDs de trabajos elegibles sin eliminarlos.

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 llamante se validan como hex de 32 caracteres antes de cualquier acceso a archivos (sin recorrido de rutas). Al iniciar, el servidor recupera trabajos que una instancia bloqueada abandonó: los running atascados durante más de una hora, y los pending durante más de 24 horas, se marcan como 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íncronoCuándo usar dispatch
Tarea larga (minutos) — quieres seguir trabajandoTarea corta — necesitas la respuesta ahora mismo
Varias tareas largas que recogerás despuésVarias tareas cortas → dispatch_parallel
No te importa el almacenamiento en caché (cada llamada es un trabajo nuevo)Almacenado en caché por defecto — las solicitudes idénticas son gratuitas

Qué Herramienta Usar

EscenarioHerramienta
Pregunta rápida de una sola vez a otro proyectodispatch
Flujo de trabajo de varios pasos con seguimientosdispatch_session
Necesitas respuestas de varios agentes a la vezdispatch_parallel
Tarea larga, quieres ver el progresodispatch_stream
Dos agentes necesitan colaborardispatch_dialogue
Necesitas un resumen combinado de varios agentesdispatch_parallel con aggregate
Tarea larga — no bloquees tu espacio de herramientadispatch_async + dispatch_wait
Verificar el progreso sin bloqueardispatch_status
Tarea conocidamente larga, de una sola vezcualquier herramienta de despacho con timeout_seconds=...
Un despacho agotó el tiempodispatch_session con el session_id del error
Coordinar un conjunto de proyectos relacionadosdefine un grupo, luego dispatch(..., group=name)
Ver qué agentes forman un conjunto de trabajolist_groups / inspect_group

Recuperación de Errores

Los fallos son deterministas: verifica success, luego ramifica según error_type.

error_typeSignificadoRecuperación
permissionSe denegó una llamada a herramientaupdate_agent(name, allowed_tools="Bash,Read") (privilegio mínimo) o update_agent(name, permission_mode="bypassPermissions"), luego re-despachar. El texto de error incluye una pista con la corrección exacta.
timeoutGrupo de procesos terminado en el tiempo de esperaReanuda el trabajo parcial: dispatch_session(agent, "Continue where you left off", session_id=<from the error text>). O reintenta con un timeout_seconds= mayor — una vez que el diario tiene historial, el error nombra el valor que habría cubierto el p90 de este agente — o usa dispatch_async. Tanto los despachos ordinarios como los de streaming conservan un resultado CLI completo recibido antes de la terminación; un resultado exitoso incluye una hint de limpieza. La salida parcial sigue siendo un tiempo de espera.
not_foundFalta el directorio del agente o el CLI claudelist_agents() → verifica healthy. Vuelve a añadir el agente con una ruta existente, o ejecuta agent-dispatch doctor para encontrar lo que falta.
recursionEl anidamiento de despacho superó max_dispatch_depth (predeterminado 3)No despaches desde agentes despachados; si el anidamiento es intencional, aumenta max_dispatch_depth en la configuración.
budgetEl CLI claude terminó la sesión en el límite de gasto max_budget_usd — la respuesta está incompletaAumenta 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>).
usage_limitLa cuenta de Claude alcanzó su límite de uso/tasa — no este agente, y no se facturó nadaEspera el reinicio nombrado en el texto de error. Despachar un agente diferente no es una solución: cada agente se ejecuta en la misma cuenta. Si se repite bajo carga, baja settings.max_concurrency.
cli_errorCualquier otra cosa del subproceso claudeLee 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 a herramientas fueron bloqueadas; el resultado puede estar incompleto. Concede acceso (ver la fila de permission) y re-despacha.
  • outcome: "partial" / "blocked" — el propio veredicto del agente de que no terminó; el resultado nombra lo que falta y el hint lleva la llamada dispatch_session(...) para continuar en la misma sesión. No se almacena en caché, así que un re-despacho después de corregir la causa se ejecuta fresco.
  • parsed_result: null con response_format="json" — la respuesta no era JSON válido; el texto crudo sigue en result. Advertencia: un agente que no puede cumplir devuelve {"error": "<reason>"} — que se analiza correctamente — así que también verifica parsed_result para una clave "error".
  • budget_exceeded: true — cost_usd llegó por encima del max_budget_usd del agente (o el predeterminado de configuración) sin que el CLI detuviera la ejecución (el turno final puede superar el límite). El despacho no falló — 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 el CLI sí detuvo falla con error_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
    # instructions: |       # standing orders, appended to the system prompt on every dispatch
    #   Read-only: never restart or redeploy unless the task says so.
    #   Quote exact log lines with timestamps.
    # 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 server, across all tools
  # dispatch_protocol: true # send every agent the dispatch protocol (non-interactive,
  #                         # time budget, STATUS line → `outcome`). false = raw claude -p.
  # usage_log: true         # record every dispatch in usage.jsonl (cost, duration, outcome).
  #                         # Powers `agent-dispatch stats`, the `typical` block and the
  #                         # measured timeout suggestion. false = record nothing.
  # 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 a herramienta — añade agentes sin reiniciar.

Diario de uso — cuánto cuesta realmente tu flota

Cada despacho añade una línea a ~/.config/agent-dispatch/usage.jsonl (anulación con AGENT_DISPATCH_USAGE_LOG): agente, éxito, costo, duración, turnos, outcome, tipo de error, llamador. Los aciertos de caché también se registran, marcados cached, para que el informe pueda mostrar lo que la caché ahorra.

$ agent-dispatch stats --days 7
Usage (last 7 day(s))
  dispatches: 33, 1 served from cache
  spend:      $13.0390
  duration:   median 87s, p90 2m40s, max 10m00s
  outcomes:   blocked 1, done 27, partial 3
  failures:   timeout 1, usage_limit 1

Per agent
  analytic           18 runs  $  8.9441  median  2m04s  p90  2m51s
      done 15, partial 3
  gitlab             12 runs  $  3.7949  median    60s  p90    86s
      done 12  |  1 cached

--agent NAME lo reduce, --json emite el mismo informe como JSON.

El diario es lo que hace funcionar otras tres cosas: el bloque typical en list_agents / inspect_agent, el error de tiempo de espera que nombra un valor derivado del p90 real del agente en lugar de una estimación duplicada, y cualquier respuesta a "qué agente es caro". Apágalo con usage_log: false en la configuración.

Propiedades que vale la pena conocer: el archivo es solo del propietario (0o600); cada registro es una escritura O_APPEND única limitada muy por debajo de PIPE_BUF, así que el CLI y cada servidor en ejecución pueden compartirlo sin bloqueo y sin líneas rotas; rota a usage.jsonl.1 a ~2 MB y mantiene dos generaciones, así que está limitado a ~4 MB para siempre. Un fallo al escribirlo nunca puede fallar un despacho.

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.

Establece job_retention_days para podar registros terminales (hechos/fallidos/cancelados) más antiguos que N días cuando un servidor se inicia:

settings:
  job_retention_days: 30

El valor predeterminado es 0 — desactivado — porque esos registros son tu propia historia 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 sola vez. Previsualiza los registros elegibles antes de eliminarlos:

agent-dispatch gc --days 30 --dry-run
agent-dispatch gc --days 30

--dry-run también funciona con --all. El equivalente MCP es dispatch_gc(max_age_days=30, dry_run=True), que devuelve dry_run: true, purged: 0, would_purge, job_ids y max_age_days. Una vista previa es una instantánea; la eliminación verifica la elegibilidad de nuevo, así que su recuento puede cambiar a medida que los trabajos terminan. Registros ilegibles, marcas de tiempo inválidas y registros cuyo ID interno difiere de su nombre de archivo se omiten y se conservan para inspección.

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 los nombres de servidores MCP
  • Indicadores de pila — Docker, Rust, Go, Python, Node.js
  • Indicadores de base de datos — Prisma, Alembic, migraciones

Capacidades Explícitas

La auto-descripción es útil, pero los capabilities explícitos hacen más claro para qué sirve cada agente. Añade 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 (por ejemplo, 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
  ├─ shared process limit → bound concurrency across all dispatch tools
  └─ subprocess.Popen(["claude", "-p", ...], cwd=~/projects/infra/)
       │
       ▼
     New Claude Code session in ~/projects/infra/
       ├─ Inherits: CLAUDE.md, .mcp.json, project tools
       ├─ System prompt += dispatch protocol + the agent's standing `instructions`
       ├─ Receives structured prompt with goal/caller/context/task
       └─ Returns result (+ its own STATUS → `outcome`) → cached when complete
                    │
                    └─ one line appended to usage.jsonl (cost, duration, outcome)

Seguridad

  • Protección de recursión — la variable de entorno AGENT_DISPATCH_DEPTH rastrea el anidamiento. Límite predeterminado: 3. Mejor esfuerzo a través del límite del subproceso (ver SECURITY.md).
  • Protección de inyección de argumentos — campos CLI estructurados (session_id, model, permission_mode, nombres de herramientas) que comienzan con - se rechazan para que no puedan colar banderas claude adicionales.
  • Protección de traversal de rutas — valores job_id/ref proporcionados por el llamador se validan como hex de 32 caracteres antes de cualquier acceso al sistema de archivos.
  • Estado solo del propietario — archivos de trabajo, agents.yaml, el diario de uso y el registro del servidor se escriben todos 0o600; sus directorios son 0o700.
  • Control de costos — max_budget_usd por agente o global se pasa al CLI claude como --max-budget-usd, así que un despacho descontrolado se detiene en el límite y regresa como error_type: "budget" con un session_id reanudable. Un exceso que aterriza sobre el presupuesto sin detenerse se marca posteriormente con budget_exceeded: true + una pista.
  • Concurrencia — max_concurrency (predeterminado: 5) limita los procesos claude -p paralelos en todas las herramientas de despacho dentro de un proceso de servidor. Los despachos ordinarios, de streaming y de fondo comparten el mismo límite, y las llamadas en espera se atienden en orden de llegada, así que una cola de trabajos de fondo no puede privar a una llamada interactiva. Cancelar una llamada en espera nunca inicia su proceso; cancelar una llamada ordinaria o de streaming ya iniciada mantiene su espacio ocupado hasta que el trabajador termina. Recargar un límite cambiado preserva los espacios ocupados: bajarlo deja que el trabajo existente termine y retiene nuevos lanzamientos hasta que haya capacidad disponible. Los procesos de servidor separados tienen cada uno su propio límite.
  • Tiempo de espera — por agente o global (predeterminado: 300s). Tanto los despachos ordinarios como los de streaming ejecutan el agente en su propio grupo de procesos. En POSIX, la fecha límite mata ese grupo, incluidos los descendientes que heredaron tuberías de salida. El despacho ordinario también limita la limpieza a un segundo adicional si un descendiente se separó deliberadamente del grupo; ese proceso separado está fuera del alcance del grupo. Un resultado CLI completo sobrevive al tiempo de espera de limpieza, con una pista en resultados exitosos.
  • Caché — solicitudes (agent, task, context, caller, goal, response_format) idénticas bajo la misma configuración de agente y predeterminados de despacho devuelven resultados en caché, limitados por cache.max_size (la entrada más antigua se expulsa primero). Solo los éxitos limpios se almacenan en caché: fallos, resultados con denied_tools, resultados marcados budget_exceeded, y resultados que el propio agente informó como partial / blocked no lo están, así que la recuperación documentada de "concede acceso / aumenta el límite, luego re-despacha" nunca recibe una respuesta dañada obsoleta. Cambiar la configuración de un agente invalida sus entradas. Las sesiones y diálogos nunca se almacenan en caché. Un despacho group= pliega el shared_context del grupo en context, así que diferentes grupos se almacenan en caché por separado y un despacho simple no se ve afectado.
  • Configuración duradera — agents.yaml se escribe atómicamente (archivo temporal + renombrado), así que una escritura interrumpida nunca puede truncarlo. Cada ruta de mutación (CLI y servidor MCP por igual) también toma un bloqueo de asesoramiento entre procesos, así que las ediciones concurrentes no eliminan los agentes de los demá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 — así que en una configuración muy disputada es posible una actualización perdida, mientras que una truncada no lo es.

Ver SECURITY.md para el modelo de amenazas completo (incluido el riesgo de escalada bypassPermissions y los archivos de trabajo en disco).

CLI

ComandoDescripción
agent-dispatch initCrear configuración + registrar servidor MCP con Claude Code
agent-dispatch add <name> <dir>Añadir un agente (genera descripción automáticamente)
agent-dispatch update <name>Actualizar configuración del agente (permisos, tiempo de espera, modelo, --instructions, etc.)
agent-dispatch remove <name>Eliminar un agente
agent-dispatch listListar agentes con estado de salud y permisos
agent-dispatch group <add|list|inspect|update|remove>Gestionar grupos — conjuntos de trabajo entre proyectos de agentes
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 despacho (--stream para progreso en vivo)
agent-dispatch stats [--days N --agent X --json]Cuánto cuestan los despachos: gasto, duraciones, resultados y fallos por agente
agent-dispatch doctor [--json --strict]Diagnosticar instalación, servidores obsoletos, registro MCP, salud del agente y membresía de grupo; --strict también falla en advertencias
agent-dispatch jobs [--status STATUS --agent NAME --limit N --json]Listar resúmenes de trabajos recientes, incluidos resultados almacenados con return_ref
agent-dispatch job <id> [--json]Mostrar un trabajo; --json exporta el registro completo sin truncamiento
agent-dispatch cancel <id>Cancelar un trabajo pendiente (trabajos en ejecución: usa la herramienta MCP dispatch_cancel)
agent-dispatch gc [--days N | --all] [--dry-run]Purgar trabajos terminales más antiguos que N días (predeterminado 7; --all purga todas las edades); --dry-run previsualiza sin eliminar
agent-dispatch serveIniciar servidor MCP (stdio, usado por Claude Code)

Para verificar una instalación desde un script:

agent-dispatch doctor --json --strict > diagnostics.json

El informe contiene schema_version: 1, status general (ok, warn o fail), conteos de issues y warnings, y un arreglo de checks. Cada verificación tiene un section, status, message y un arreglo de details que contiene pasos de remediación o información de respaldo. El JSON se emite incluso cuando las verificaciones fallan. Los fallos salen con código 1; las advertencias salen con código 0 a menos que --strict esté configurado. Las mismas verificaciones y política de salida se aplican a la salida normal legible por humanos. Esto verifica la instalación y configuración; no ejecuta un despacho de pago.

Para inspeccionar el historial desde scripts o guardar un resultado completo:

agent-dispatch jobs --agent infra --status failed --limit 10 --json
agent-dispatch job <job_id> --json > job.json

jobs --json devuelve un arreglo de resúmenes compactos en el mismo formato que dispatch_jobs, o [] si no hay trabajos que coincidan. Cada resumen incluye el ID, el agente, el estado, los primeros 120 caracteres de la tarea y las marcas de tiempo; el costo del resultado, el éxito, el resultado, el tipo de error y el progreso de ejecución más reciente se incluyen cuando están disponibles. job --json devuelve el trabajo completo almacenado, incluidos tarea/contexto, progreso, texto del resultado, resultado estructurado, ID de sesión y sugerencias de recuperación cuando estén presentes. Ambos formatos preservan Unicode. Leer un trabajo fallido aún sale con éxito: verifique su status y result.success para determinar el resultado del despacho. Los IDs de trabajo faltantes o inválidos y los fallos de apertura de almacenamiento devuelven {"error": "..."} con código de salida 1. Las opciones de línea de comandos inválidas son reportadas por Click en stderr.

Los metadatos de costo inválidos o no finitos se tratan como desconocidos y se omiten de las exportaciones de resultados; no ocultan el texto del resultado almacenado. Los resúmenes de uso ignoran mediciones numéricas inutilizables, y doctor omite el tiempo de actividad cuando la marca de tiempo de inicio de un servidor es ilegible.

Requisitos

  • Python >= 3.10
  • CLI de Claude Code instalado, autenticado y en PATH (verificar: claude --version)

Licencia

MIT