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 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ámetroTipoRequeridoDescripción
namestringNombre del agente de list_agents
preview_linesintnoMá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:

  • descriptionorientado 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_contexthechos orientados al miembro (nombres de stacks, 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 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ámetroTipoRequeridoDescripción
agentstringNombre del agente de list_agents
taskstringQué 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
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áximo de caracteres del texto del resultado a incluir en la respuesta de referencia (predeterminado 500).
timeout_secondsintnoAnulació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.
groupstringnoNombre 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ámetroTipoRequeridoDescripción
agentstringNombre del agente
taskstringTarea o mensaje de seguimiento
session_idstringnoDe la respuesta anterior — vacío para nueva sesión
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 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ámetroTipoRequeridoDescripción
dispatchesstring (JSON)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)
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 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ámetroTipoRequeridoDescripción
requesterstringAgente con el problema/contexto
responderstringAgente con la experiencia/herramientas
topicstringProblema 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 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ámetroTipoRequeridoDescripción
namestringNombre del agente (letras, dígitos, guiones, guiones bajos)
directorystringRuta 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 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")

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
namestringNombre 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 del 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

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ámetroTipoRequeridoDescripción
namestringNombre 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í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)En caché por defecto — las solicitudes idénticas son gratuitas

Qué Herramienta Usar

EscenarioHerramienta
Pregunta rápida y puntual 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 ranura de herramientadispatch_async + dispatch_wait
Verificar el progreso sin bloqueardispatch_status
Tarea conocidamente larga, puntualcualquier 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 de herramientaupdate_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.
timeoutProceso eliminado 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= 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_foundDirectorio del agente o CLI de claude faltantelist_agents() → verifica healthy. Vuelve a agregar el agente con una ruta existente, o ejecuta agent-dispatch doctor para encontrar lo que falta.
recursionEl 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.
budgetLa CLI de claude terminó la sesión en el límite de gasto de 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>).
cli_errorCualquier otra cosa del subproceso de 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 de herramientas fueron bloqueadas; el resultado puede estar incompleto. Otorga acceso (ver la fila de permission) y re-despacha.
  • parsed_result: null con response_format="json" — la respuesta no era JSON válido; el texto sin procesar 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 de "error".
  • budget_exceeded: truecost_usd superó el max_budget_usd del 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 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
    # 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 0desactivado — 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_DEPTH rastrea 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 banderas claude adicionales.
  • Guardia contra recorrido de rutas — los valores job_id/ref proporcionados 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) y agents.yaml (0o600) se escriben solo para el propietario; sus directorios son 0o700.
  • Control de costosmax_budget_usd por agente o global se pasa al CLI claude como --max-budget-usd, de modo que un envío descontrolado se detiene en el límite y regresa como error_type: "budget" con un session_id reanudable. Un exceso que supere el presupuesto sin detenerse se marca posteriormente con budget_exceeded: true y una pista.
  • Concurrenciamax_concurrency (predeterminado: 5) limita los procesos claude -p paralelos. Nota: las rutas de envío síncronas y asíncronas usan semáforos separados, por lo que el peor caso total es 2 × 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 por cache.max_size (la entrada más antigua se elimina primero). Solo se almacenan en caché los éxitos limpios: los fallos, los resultados con denied_tools y los resultados marcados con budget_exceeded no 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ío group= pliega el shared_context del grupo en context, por lo que diferentes grupos se almacenan en caché por separado y un envío simple no se ve afectado.
  • Configuración duraderaagents.yaml se 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

ComandoDescripción
agent-dispatch initCrear 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 listListar 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 doctorDiagnosticar 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 serveIniciar 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