Subagentes: como Claude Code delega trabajo a "clones" de si mismo
Nota: Los fragmentos de codigo que aparecen en esta serie son recreaciones en Python basadas en la arquitectura y patrones descritos publicamente tras la filtracion. No se reproduce codigo original de Anthropic. El objetivo es puramente educativo: entender los conceptos de ingenieria detras de una herramienta de IA de produccion.
1. Que es un sub,agente (y que no es)
Un sub,agente no es una herramienta. Una herramienta lee un archivo, ejecuta un comando, busca en la web. Es una operacion atomica. Un sub,agente es otra cosa: es una instancia completa del loop agentico que vimos en el Post 2. Tiene su propio ciclo de pensar,actuar,observar, sus propias herramientas, su propio presupuesto de tokens, y su propia conversacion.
La analogia mas precisa: si una herramienta es una llamada a funcion, un sub,agente es un spawn de un proceso mental.
El agente principal (el "padre") llama a una herramienta especial llamada Agent (antes se llamaba Task — el nombre legacy todavia se acepta como alias). Le pasa una descripcion corta, un prompt detallado, y opcionalmente el tipo de agente que quiere. El sistema crea el clon, lo ejecuta, y cuando termina, devuelve un unico mensaje al padre con el resultado.
# Recreacion simplificada de la invocacion del AgentTool
class AgentTool:
NAME = "Agent"
LEGACY_NAME = "Task" # alias por retrocompatibilidad
MAX_RESULT_SIZE = 100_000 # caracteres
def call(self, description, prompt, subagent_type=None,
model=None, run_in_background=False):
"""Spawn de un sub,agente."""
# 1. Resolver que tipo de agente usar
agent_def = self.resolve_agent(subagent_type)
# 2. Construir el pool de herramientas del hijo
worker_tools = self.filter_tools_for_agent(agent_def)
# 3. Decidir si correr sync o async
if run_in_background or agent_def.background:
return self.launch_async(agent_defprompt, worker_tools)
else:
return self.run_sync(agent_def, prompt, worker_tools)
def filter_tools_for_agent(self, agent_def):
"""Los hijos NO pueden usar todas las herramientas del padre."""
banned = {
"ExitPlanMode", "EnterPlanMode",
"AskUserQuestion", "Workflow"
}
tools = self.parent_tools , banned
if agent_def.disallowed_tools:
tools ,= set(agent_def.disallowed_tools)
return toolsFijate en un detalle importante: los sub,agentes tienen prohibido usar ciertas herramientas del padre. No pueden entrar en modo plan, no pueden hacer preguntas al usuario, no pueden lanzar workflows. Esto no es arbitrario — es un mecanismo de contencion. Un sub,agente trabaja aislado y devuelve un resultado; no deberia poder cambiar el estado de la conversacion principal.
2. Los 6 tipos built,in
Claude Code viene con al menos 6 tipos de sub,agentes predefinidos. Cada uno tiene su propio prompt de siste, su propio conjunto de herramientas, y en algunos casos su propio modelo:
# Recreacion de las definiciones de agentes built,in
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class AgentDefinition:
agent_type: str
tools: list[str]
model: Optional[str] = None # None = hereda del padre
source: str = "built,in"
omit_claude_md: bool = False # no heredar CLAUDE.md
disallowed_tools: list[str] = field(default_factory=list)
background: bool = False
color: Optional[str] = None
# Las herramientas que NINGUN sub,agente puede usar
READ_ONLY_BANNED = ["Agent", "ExitPlanMode", "Edit", "Write", "NotebookEdit"]
GENERAL_PURPOSE = AgentDefinition(
agent_type="general,purpose",
tools=["*"], # acceso total
)
EXPLORE = AgentDefinition(
agent_type="Explore",
model="haiku", # modelo rapido y barato
omit_claude_md=True, # no necesita contexto del proyecto
disallowed_tools=READ_ONLY_BANNED, # solo lectura
)
PLAN = AgentDefinition(
agent_type="Plan",
model="inherit", # mismo modelo que el padre
omit_claude_md=True,
disallowed_tools=READ_ONLY_BANNED, # solo lectura
)
BASH = AgentDefinition(
agent_type="Bash",
tools=["Shell"], # solo puede ejecutar comandos
)
STATUSLINE_SETUP = AgentDefinition(
agent_type="statusline,setup",
tools=["Read", "Edit"], # solo leer y editar
model="sonnet",
color="orange",
)
CLAUDE_CODE_GUIDE = AgentDefinition(
agent_type="claude,code,guide",
model="haiku", # modelo rapido
tools=["Glob", "Grep", "Read", "WebFetch", "WebSearch"],
)
VERIFICATION = AgentDefinition(
agent_type="verification",
model="inherit",
color="red",
background=True, # siempre corre en segundo plano
disallowed_tools=READ_ONLY_BANNED, # solo lectura
)Algunos detalles que merecen atencion:
Explore usa Haiku por defecto. Su unico trabajo es leer codigo y buscar archivos. No necesita el modelo mas potente — necesita velocidad. El prompt de Explore incluye una advertencia en mayusculas: "CRITICAL: READ,ONLY MODE , NO FILE MODIFICATIONS". No puede crear, modificar, eliminar, mover ni copiar archivos. Si le pides que ejecute bash, solo puede usar comandos de lectura (ls, git status, git log, find).
Plan hereda el modelo del padre. A diferencia de Explore, Plan necesita razonar sobre arquitectura, asi que usa el mismo modelo que el agente principal. Pero sigue siendo de solo lectura — diseña, no implementa.
Verification es el auditor adversarial. Es el unico agente que corre en background por defecto. Su prompt le pide explicitamente que sea esceptico: que no se fie de que el codigo "parece correcto", sino que lo demuestre ejecutando tests y verificando outputs. Incluye estrategias de "reconocimiento de racionalizacion" para detectar cuando esta engañandose a si mismo. El ver final es PASS, FAIL o PARTIAL.
Explore y Plan son "one,shot". El sistema los marca internamente como ONE_SHOT_BUILTIN_AGENT_TYPES, lo que significa que no mantienen estado entre invocaciones — se crean, trabajan, devuelven resultado, y desaparecen.
3. El ciclo de vida de un sub,agente
Cuando el AgentTool recibe una peticion, el proceso es mas complejo de lo que parece:
# Recreacion del ciclo de vida completo de un sub,agente
class AgentLifecycle:
def spawn(self, agent_def, prompt, parent_context):
"""Fase 1: Preparacion."""
# Inicializar servidores MCP si el agente los requiere
self.init_mcp_servers(agent_def)
# Construir contexto: ¿hereda CLAUDE.md?
context = self.build_context(agent_def, parent_context)
# Resolver herramientas disponibles
tools = self.resolve_tools(agent_def)
# El pensamiento extendido se DESACTIVA para sub,agentes normales
thinking = {"type": "disabled"}
# Construir mpt de sistema
system_prompt = self.build_system_prompt(agent_def)
return context, tools, thinking, system_prompt
def execute(self, context, prompt, tools, max_turns=None):
"""Fase 2: Ejecucion — un loop agentico completo."""
messages = [{"role": "user", "content": prompt}]
# El sub,agente tiene su propio bucle (el del Post 2)
for message in agentic_loop(messages, tools, max_turns):
self.record_transcript(message)
yield message
def finalize(self, messages):
"""Fase 3: Resultado — extraer el ultimo mensaje."""
last_assistant = self.get_last_assistant_message(messages)
return {
"agent_id": self.id,
"agent_type": self.agent_type,
"content": last_assistant,
"total_tool_uses": self.tool_count,
"total_duration_ms": self.elapsed_ms,
"total_tokens": self.token_count,
}
def cleanup(self):
"""Fase 4: Limpieza — litodo."""
self.close_mcp_connections()
self.stop_shell_tasks()
self.clear_file_cache()
self.save_transcript()Hay un detalle sutil pero importante: el pensamiento extendido (extended thinking) se desactiva para sub,agentes normales. El razonamiento es que el sub,agente ya tiene un scope limitado y no necesita la capacidad de razonamiento profundo del modelo completo. Esto ahorra tokens significativamente.
Otro detalle: el resultado que el padre recibe incluye metricas — cuantas herramientas uso el hijo, cuanto tardo, cuantos tokens consumio. Esto permite al padre (y al sistema de telemetria) tener visibilidad sobre el coste real de delegar trabajo.
4. Que hereda un sub,agente (y que no)
Esta es la parte mas sutil del sistema. Un sub,agente no recibe una copia exacta del contexto del padre. Recibe una version filtrada:
| Hereda | No hereda |
|,,,|,,,|
| CLAUDE.md (salvo Explore/Plan) | Historial completo de conversacion |
| Contexto del usuario | Tool relts grandes |
| Contexto del sistema | Estado de otras herramientas |
| Permisos del padre | Pensamiento extendido |
| Git status (salvo Explore/Plan) | Errores previos del padre |
Los agentes Explore y Plan tienen omit_claude_md=True, lo que significa que no reciben el CLAUDE.md ni el git status. El razonamiento es que estos agentes trabajan a un nivel tan generico (buscar archivos, diseñar arquitectura) que el contexto especifico del proyecto les seria mas ruido que señal.
El agente claude,code,guide va un paso mas alla: al arrancar, descarga documentacion actualizada de dos URLs publicas de Anthropic. Esto le permite responder preguntas sobre Claude Code con informacion que puede ser mas reciente que su propio training data.
5. Foreground vs background: el auto,background de 2 minutos
Un sub,agente puede correr en primer plano (sync) o en segundo plano (async). La decision no siempre la toma el usuario:
# Recreacion de la logica de decision sync/async
PROGRESS_THRESHOLD_ = 2_000 # mostrar hint de background a los 2s
AUTO_BACKGROUND_MS = 120_000 # auto,background a los 2 minutos
def should_run_async(run_in_background, agent_def, is_coordinator):
"""Decide si el agente corre en background."""
return (
run_in_background # el usuario lo pidio
or agent_def.background # el agente lo exige (ej: verification)
or is_coordinator # el coordinador SIEMPRE lanza async
)
class ForegroundRunner:
def run(self, agent):
"""Ejecuta un agente en foreground con auto,background."""
start = time.time()
for message in agent.execute():
elapsed = (time.time() , start) * 1000
# A los 2 segundos: mostrar hint de "puedes mandar esto a background"
if elapsed > PROGRESS_THRESHOLD_MS:
self.show_background_hint()
# A los 2 minutos: auto,background
if elapsed > AUTO_BACKGROUND_MS:
self.transition_to_background(agent)
return self.async_result(agent)
yield messageSi un agente esta corriendo en foreground y pasan 2 minutos sin que termine, el sistema lo pasa a background automaticamente. Esto evita que el agente principal se quede bloqueado esperando a un hijo lento.
La transicion mid,ejecucion es delicada. El sistema tiene que:
Parar el iterador de foreground (con un timeout de 1 segundo por si la limpieza de MCP se cuelga)
Reconstruir el tracker de progreso desde los mensajes ya recolectados
Lanzar una continuacion async con el mismo controlador de abort
Devolver un resultado
async_launchedinmediatamente
Cuando un agente en background termina, el resultado llega como una notificacion que incluye el ID del agente, su estado (completed, failed o killed), un resumen, y metricas de uso.
6. El sistema de resume: agentes que recuerdan
Los sub,agentes pueden resumirse. Si lanzas un agente, termina, y luego necesitas que continue donde lo dejo, puedes pasar su agent_id al parametro resume:
# Recreacion del sistema de resume de agentes
class AgentResume:
def resume(self, agent_id):
"""Reconstruye un agente terminado para que continue."""
# 1. Leer la transcripcion guardada
transcript = self.read_transcript(agent_id)
metadata = self.read_metadata(agent_id)
# 2. Limpiar mensajes problematicos
messages = transcript.messages
messages = self.filter_whitespace_only_assistant(messages)
messages = self.filter_orphaned_thinking(messages)
messages = self.filter_unresolved_tool_uses(messages)
# 3. Reconstruir estado
state = self.reconstruct_state(messages)
# 4. Si tenia un worktree, verificar que sigue existiendo
if metadata.worktree_path:
self.verify_worktree(metadata.worktree_path)
# 5. Resolver tipo de agente (fallback a general,purpose)
agent_def = self.find_agent(metadata.agent_type)
if not agent_def:
agent_def = GENERAL_PURPOSE
return state, agent_defLos tres filtros de limpieza son interesantes:
, Mensajes de asistente solo con whitespace: a veces el modelo genera un mensaje vacio antes de usar una herramienta. Se eliminan para no confundir al modelo al resumir.
, Thinking huerfano: si el modelo estaba en medio de "pensar" cuando se corto, esos bloques de pensamiento sin conclusion se eliminan.
, Tool uses sin resolver: si el modelo pidio usar una herramienta pero la ejecucion se interrumpio antes de obtener el resultado, esas peticiones pendientes se eliminan. Reanudar con tool_use sin tool_result causaria un error de la API.
7. Comunicacion entre agentes: mensajeria y equipos
Los sub,agentes no tienen por que trabajar aislados. Claude Code incluye un sistema completo de mensajeria entre agentes:
# Recreacion del sistema de mensajeria inter,agente
import json
from pathlib import Path
class SendMessage:
"""Herramienta para que un agente envie un mensaje a otro."""
def send(self, to, summary, message):
"""
to: nombre del destinatario, "*" para broadcast
summary: resumen corto del mensaje
message: contenido completo (texto o estructurado)
"""
if to == "*":
# Broadcast a todos los miembros del equipo menos al remitente
for member in self.team.members:
if member.name != self.sender:
self.deliver(member, message)
else:
self.deliver(self.find_member(to), message)
def deliver(self, recipient, message):
"""Entrega via buzon de correo basado en archivos."""
inbox_path = (
Path(".claude/teams")
/ self.team_name
/ "inboxes"
/ f"{recipient.name}.json"
)
with file_lock(inbox_path, retries=10, timeout_range=(5, 100)):
inbox = json.loads(inbox_path.read_text())
inbox.append({
"from": self.sender,
"content": message,
"timestamp": now_iso(),
"read": False,
})
inbox_path.write_text(json.dumps(inbox))El sistema usa un buzon de correo basado en archivos JSON con file locking. Cada agente en un equipo tiene su propio archivo de inbox en .claude/teams/{equipo}/inboxes/{nombre}.json. Los mensajes se formatean como XML internamente:
<teammate,message from="explorer" timestamp="2026,04,01T10:30:00Z" message,id="abc123">
He encontrado 3 endpoints relevantes en src/api/routes.ts
</teammate,message>Ademas de mensajes de texto libre, el sistema soporta mensajes estructurados para protocolos especificos:
, shutdown_request / shutdown_response — pedir a un agente que termine
, plan_approval_request / plan_approval_response — el lider aprueba o rechaza un plan
, Mensajes de permisos, asignacion de tareas, y actualizaciones de modo
8. Equipos: la estructura de "eo plano"
Los agentes pueden organizarse en equipos. El TeamCreateTool crea un equipo con un lider y un directorio en disco:
# Recreacion del sistema de equipos
class Team:
def create(self, team_name, description=None):
"""Crea un equipo con el agente actual como lider."""
team = {
"name": team_name,
"description": description,
"created_at": time.time(),
"lead_agent_id": f"team,lead@{team_name}",
"members": [{
"name": "team,lead",
"agent_type": self.agent_type,
"joined_at": time.time(),
"cwd": os.getcwd(),
}],
}
# Guardar en disco
team_dir = Path(f".claude/teams/{team_name}")
team_dir.mkdir(parents=True, exist_ok=True)
(team_dir / "team.json").write_text(json.dumps(team))
# Crear directorio de tareas y buzones
(team_dir / "inboxes").mkdir(exist_ok=True)
return teamLa estructura es plana: un lider y N trabajadores. No hay jerarquia dentro del equipo. Y hay una restriccion explicita: los teammates no pueden crear otros teammates. El error dice literalmente: "Teammates cannot spawn other teammates — the team roster is flat."
Para disolver un equipo, primero hay que pedir a todos los miembros que paren (shutdown_request), esperar a que confirmen, y solo entonces llamar a TeamDeleteTool. Si intentas disolver un equipo con miembros activos, el sistema te lo impide.
9. Agentes personalizados
Puedes definir tus propios tipos de agentes creando un archivo .md o .json en .claude/agents/ (a nivel de usuario o de proyecto). El formato markdown usa frontmatter para la configuracion:
# Recreacion del parser de agentes personalizados
AGENT_TYPE_REGEX = r"^[a,zA,Z0,9][a,zA,Z0,9,]*[a,zA,Z0,9]$"
MIN_TYPE_LENGTH = 3
MAX_TYPE_LENGTH = 50
MIN_DESCRIPTION_LENGTH = 10
MIN_PROMPT_LENGTH = 20
def validate_custom_agent(agent):
"""Valida que un agente pernalizado sea correcto."""
import re
if not re.match(AGENT_TYPE_REGEX, agent["type"]):
raise ValueError("El tipo solo puede contener letras, numeros y guiones")
if len(agent["type"]) < MIN_TYPE_LENGTH:
raise ValueError(f"El tipo debe tener al menos {MIN_TYPE_LENGTH} caracteres")
if len(agent["type"]) > MAX_TYPE_LENGTH:
raise ValueError(f"El tipo no puede superar {MAX_TYPE_LENGTH} caracteres")
if len(agent.get("description", "")) < MIN_DESCRIPTION_LENGTH:
raise ValueError("La descripcion debe tener al menos 10 caracteres")
if len(agent.get("prompt", "")) < MIN_PROMPT_LENGTH:
raise ValueError("El prompt debe tener al menos 20 caracteres")Los agentes personalizados se cargan desde multiples ubicaciones con un sistema de prioridades. Si defines un agente con el mismo nombre en dos sitios, el de mayor prioridad gana:
built,in < plugin < user < project < cli,flag < managedEsto significa que un agente definido en tu proyecto (.claude/agents/) sobreescribe a uno built,in del mismo nombre. Y un agente "managed" (gestionado externamente) sobreescribe a todo.
Los agentes personalizados pueden especificar: modelo, color, herramientas permitidas/prohibidas, skills, servidores MCP requeridos, modo de permisos, limite de turnos, si corren en background, y si trabajan en un worktree aislado.
10. El modo coordinador
El modo coordinador es la expresion maxima del sistema multi,agente. Cuando se activa, el agente principal deja de tener acceso a herramientas de codigo y se convierte en un director que solo puede hacer tres cosas: lanzar agentes, enviar mensajes, y parar agentes.
# Recreacion del modo coordinador
COORDINATOR_TOOLS = {"Agent", "SendMessage", "TaskStop"}
def is_coordinator_mode():
"""Doble gate: feature flag + variable de entorno."""
return (
feature_enabled("COORDINATOR_MODE")
and os.environ.get("CLAUDE_CODE_COORDINATOR_MODE") == "1"
)El prompt del coordinador define un flujo de trabajo en 4 fases:
| Fase | Quien trabaja | Objetivo |
|,,,|,,,|,,,|
| Investigacion | Workers (en paralelo) | Explorar el codebase |
| Sintesis | Coordinador | Leer hallazgos, diseñar especificaciones |
| Implementacion | Workers | Hacer cambios concretos |
| Verificacion | Workers | Demostrar que los cambios funcionan |
El coordinador tiene reglas estrictas: nunca debe fabricar resultados de agentes, nunca debe usar un worker para comprobar a otro worker, y despues de lanzar agentes debe decirle al usuario que lanzo y terminar su respuesta — no quedarse esperando.
Una de las decisiones mas interesantes del prompt del coordinador es la matriz de decision "continuar vs crear nuevo". Cuando un worker termina y hay mas trabajo, el coordinador debe decidir si reutilizar ese worker (enviandole un mensaje para continuar) o crear uno nuevo. La regla: si el contexto se solapa mucho (mismos archivos, mismo tema), continua. Si el scope cambia, crea uno fresco.
11. Curiosidades
La lista dgentes se movio para no romper la cache
El sistema descubrio que el 10,2% de los tokens de cache_creation del fleet se desperdiciaban porque la lista de agentes disponibles cambiaba entre turnos (por ejemplo, al instalar un plugin o definir un agente nuevo). Cada cambio en el system prompt invalida la cache de prefijo. La solucion: mover la lista de agentes a un "attachment" dentro de los mensajes en vez del system prompt, para que los cambios no invaliden la cache.
Fork: el clon que hereda TODO
Existe un tipo de agente experimental llamado fork que no aparece en la lista publica. A diferencia de los agentes normales, un fork hereda el system prompt exacto del padre para maximizar la reutilizacion de cache. El truco: el hijo recibe el mensaje completo del padre (incluyendo todos los bloques tool_use) con placeholders en los resultados, y luego un mensaje adicional con su directiva especifica. Hay una proteccion doble contra forks recursivos: una comprobacion por querySource (que sobrevive a la compactacion) y un escaneo de mensajes buscando el tag <fork,boilerplate>.
El lider del equipo no es un teammate
El lider de un equipo no se considera a si mismo un "teammate" — isTeammate() devuelve false para el. Su ID es deterministico (team,lead@{nombre_equipo}) y no se le asigna la variable de entorno CLAUDE_CODE_AGENT_ID. ¿Por que? Porque si se la asignaran, isTeammate() devolveria true, lo que activaria el polling de inbox y romperia el flujo del lider.
Los teammates no pueden lanzar agentes en background
Si un teammate in,process intenta lanzar un agente en background, el sistema lo bloquea. El razonamiento: los teammates in,process ya comparten el proceso de Node.js, y lanzar background tasks desde dentro de un contexto compartido crearia problemas de gestion de recursos.
La verificacion automatica tras 3+ tareas
Cuando un agente completa la ultima tarea de una lista con 3 o mas items, el sistema automaticamente le recuerda que deberia lanzar un agente de veriacion. Es un nudge, no una obligacion — pero esta diseñado para que la verificacion sea el camino de menor resistencia.
,,,
Fuentes
, Analisis basado en la filtracion del codigo fuente de Claude Code v2.1.88, documentada en el Post 1 de esta serie
, The Verge — "Claude Code leak exposes a Tamagotchi,style 'pet' and an always,on agent"
, Zscaler ThreatLabz — Analisis tecnico del leak
,,,
Esta serie es un proyecto de Levante — un cliente de escritorio open source para trabajar con MCPs, modelos de IA y herramientas en una sola interfaz. Si te interesa la arquitectura de herramientas de IA, te interesa Levante.
Saul Gomez es fundador de Levante, un cliente de escritorio open source para trabajar con modelos de IA.
