# Composición de los agentes

Learn how to compose agents using agent-as-tool patterns, run trees, and streaming topology.

Source: https://goa.design/es/docs/2-goa-ai/agent-composition/

Relative links resolve against the source URL above.


Esta guía demuestra cómo componer agentes tratando un agente como una herramienta de otro, y explica cómo Goa-AI modela las ejecuciones de los agentes como un árbol con proyecciones en streaming para diferentes audiencias.

## Qué construirás

- Un agente planificador que exporta herramientas de planificación
- Un agente orquestador que utiliza las herramientas del agente planificador
- Composición entre procesos con ejecución en línea

---

## Diseño de Agentes Compuestos

Crear `design/design.go`:

```go
package design

import (
    . "goa.design/goa/v3/dsl"
    . "goa.design/goa-ai/dsl"
)

var _ = API("orchestrator", func() {})

var PlanRequest = Type("PlanRequest", func() {
    Attribute("goal", String, "Goal to plan for")
    Required("goal")
})

var PlanResult = Type("PlanResult", func() {
    Attribute("plan", String, "Generated plan")
    Required("plan")
})

var _ = Service("orchestrator", func() {
    // Planning agent that exports tools
    Agent("planner", "Planning agent", func() {
        Export("planning.tools", func() {
            Tool("create_plan", "Create a plan", func() {
                Args(PlanRequest)
                Return(PlanResult)
            })
        })
        RunPolicy(func() {
            DefaultCaps(MaxToolCalls(5))
            TimeBudget("1m")
        })
    })
    
    // Orchestrator agent that uses planning tools
    Agent("orchestrator", "Orchestration agent", func() {
        Use(AgentToolset("orchestrator", "planner", "planning.tools"))
        RunPolicy(func() {
            DefaultCaps(MaxToolCalls(10))
            TimeBudget("5m")
        })
    })
})
```

Generar código:

```bash
goa gen example.com/tutorial/design
```

---

## Implementación de planificadores

El código generado proporciona helpers para ambos agentes. Conéctalos:

```go
package main

import (
    "context"
    
    planner "example.com/tutorial/gen/orchestrator/agents/planner"
    orchestrator "example.com/tutorial/gen/orchestrator/agents/orchestrator"
    "goa.design/goa-ai/runtime/agent/runtime"
    storageinmem "goa.design/goa-ai/runtime/agent/storage/inmem"
)

func main() {
    rt := runtime.New(storageinmem.New())
    ctx := context.Background()
    
    // Register planning agent
    if err := planner.RegisterPlannerAgent(ctx, rt, planner.PlannerAgentConfig{
        Planner: &PlanningPlanner{},
    }); err != nil {
        panic(err)
    }
    
    // Register orchestrator agent (automatically uses planning tools)
    if err := orchestrator.RegisterOrchestratorAgent(ctx, rt, orchestrator.OrchestratorAgentConfig{
        Planner: &OrchestratorPlanner{},
    }); err != nil {
        panic(err)
    }
    
    // Use orchestrator agent
    client := orchestrator.NewClient(rt)
    // ... run agent ...
}
```

**Conceptos clave

- **Exportación**: Declara conjuntos de herramientas que otros agentes pueden utilizar
- **Conjunto de herramientas del agente Hace referencia a un conjunto de herramientas exportado de otro agente
- **Ejecución en línea**: Desde la perspectiva de quien llama, un agente como herramienta se comporta como una llamada a herramienta normal; el tiempo de ejecución ejecuta el agente proveedor como una ejecución hija y agrega su salida en un único `ToolResult` (con un `RunLink` de vuelta a la ejecución hija)
- **Proceso cruzado**: Los agentes pueden ejecutarse en diferentes trabajadores manteniendo un árbol de ejecución coherente; los eventos `child_run_linked` y los gestores de ejecución vinculan las llamadas de la herramienta padre a las ejecuciones de los agentes hijo para el streaming y la observabilidad

---

## Definiciones de agente generadas

La generación de código emite una única `AgentDefinition` inmutable para cada
agente. Esta definición contiene el nombre del workflow, la cola de tareas
predeterminada, los contratos de herramientas, las etiquetas obligatorias, la
política de completion y las definiciones de todos los agentes hijo accesibles
mediante herramientas respaldadas por agentes.

Los clientes y los helpers generados para registrar workers usan la misma
definición. El código escrito a mano proporciona el planificador, los ejecutores
de herramientas y la configuración de las activities; no debe repetir la ruta,
la cola, el ID del agente hijo, los contratos de sus herramientas ni las
etiquetas obligatorias. Así, el cliente y el worker interpretan el diseño del
mismo modo. Una ejecución concreta puede elegir otra cola mediante
`WithTaskQueue`.

---

## Agentes configurados dinámicamente {#dynamic-agent-tools}

Las API de agentes dinámicos descritas aquí requieren Goa-AI v0.84.0 o posterior.

Una configuración guardada de un agente puede convertirse en herramienta sin
registrar un worker nuevo para cada configuración. El registro guarda el contrato
de la herramienta, el ID de un ejecutor existente y una referencia inmutable a la
configuración. El runtime inicia ese ejecutor como workflow hijo, con progreso,
cancelación y solicitudes de intervención humana.

### Registrar una configuración guardada

Cada `genregistry.ToolSchema` debe incluir `ConsumerContract.Kind: "agent"`, un
contrato completo de entrada y resultado, y un `AgentToolTarget`:

```go
&genregistry.AgentToolTarget{
    Executor:      "generic.agent",
    Configuration: "support/revisions/7",
}
```

La aplicación gestiona el prompt, el modelo y la política de herramientas de la
referencia. Conserva esa revisión hasta que las llamadas aceptadas hayan preparado
sus hijos. El modelo proporciona argumentos del dominio; no elige el worker ni
la revisión.

Registra las declaraciones completas mediante el cliente generado del registro:

```go
registered, err := registryClient.RegisterAgentToolset(ctx,
    &genregistry.AgentToolsetDeclaration{
        Name:  "support",
        Tools: declarations,
    },
)
if err != nil {
    return err
}
```

Repetir un registro activo idéntico tiene éxito. Para cambiarlo, pasa el
`registered.RegistrationToken` actual como `ExpectedRegistrationToken` en
`*genregistry.ReplaceAgentToolsetPayload`. Un token desactualizado devuelve
`admission_conflict`. `Unregister` retira la declaración del descubrimiento;
`ReplaceAgentToolset` puede reactivarla con su token actual.

Estas herramientas nativas de agentes no necesitan concesiones de proveedor Pulse
ni comprobaciones de salud. Registrarlas no inicia ni despliega el worker.
`CallTool` y `CallResolvedTool` las rechazan: el runtime consumidor inicia el
workflow hijo. Los registros de servicios y de agentes nativos no pueden
sobrescribirse entre sí.

### Preparar el hijo en un worker permitido

La definición de un agente permite su propio worker y sus workers hijos generados.
Para permitir otro ejecutor:

```go
executor := genspecialist.Definition()
consumer := genassistant.Definition().WithAgentExecutors(&executor)
if err := rt.RegisterAgentToolResolver(executor.Route().ID, prepareConfiguration); err != nil {
    return err
}
```

`WithAgentExecutors` acepta punteros, copia las definiciones recibidas y devuelve
una nueva. Usa `consumer` tanto en `AgentRegistration.Definition` del worker
consumidor como en `rt.ClientFor(consumer)`. Los helpers generados de registro y
cliente siguen usando su definición original. `Executor` debe coincidir con el
ID de un worker permitido; los demás destinos fallan durante el descubrimiento,
antes de llamar al modelo.

`prepareConfiguration` es código de la aplicación con la firma
`func(context.Context, string, *runtime.ToolCall) (*runtime.AgentToolConfiguration, error)`.
Regístralo en el runtime consumidor antes de cerrar los registros o iniciar
ejecuciones. Recibe la referencia seleccionada y una copia de la llamada validada.
Devuelve `Messages`, `Labels`, `Policy` y, opcionalmente, `RenderedPrompts`.
Las etiquetas amplían o reemplazan las heredadas del padre; la aplicación controla
la autorización y el ámbito. El runtime gestiona los ID de sesión y ejecución y
los enlaces al padre.

La preparación se ejecuta en una actividad cuyo resultado guardado se reutiliza
durante el replay del workflow. Tras una solicitud de intervención humana, la
continuación restaura mensajes, etiquetas, política y contrato del padre desde
el checkpoint del hijo, sin volver a cargar la configuración.

### Devolver el resultado seleccionado

`planner.PlanInput.ParentTool` y `planner.PlanResumeInput.ParentTool` exponen el
contrato aceptado por el padre. Un planner genérico puede usar `ParentTool.Result`
con `model.StructuredOutput` en su solicitud final al modelo y devolver
`planner.FinalToolResult`. Las salidas estructuradas y las llamadas a herramientas
usan solicitudes al modelo separadas. Un hijo nativo que solo devuelva texto se
rechaza. Las ejecuciones principales no tienen `ParentTool`; las herramientas de
agentes compilados conservan su comportamiento existente.

Una actividad de planificación posterior descubre los registros nuevos. Reemplazar
una declaración no cambia la configuración, el esquema de resultado ni la aprobación
pendiente de una llamada aceptada. El resultado mantiene el enlace a la ejecución
hija.

### Actualizar antes de publicar

Actualiza el registro, los workers ejecutores y todos los consumidores que puedan
descubrir declaraciones nativas antes de publicarlas. Los consumidores anteriores
las rechazan. Las huellas de los servicios, los mensajes de los proveedores y los
registros de servicio persistidos no cambian. Los registros de versiones anteriores
no pueden leer entradas nativas, ni siquiera retiradas; los workers anteriores no
pueden restaurar checkpoints de hijos nativos. Para volver a esas versiones, primero
hay que eliminar esas entradas y terminar sus ejecuciones.

---

## Passthrough: Reenvío determinista de herramientas

Para las herramientas exportadas que deben pasar por alto el planificador por completo y reenviar directamente a un método de servicio, utilice `Passthrough`. Esto es útil cuando:

- Desea un comportamiento determinista y predecible (sin toma de decisiones LLM)
- La herramienta es una simple envoltura alrededor de un método de servicio existente
- Necesita una latencia garantizada sin sobrecarga del planificador

### Cuándo utilizar el Passthrough frente a la ejecución normal

| Escenario | Utilizar Passthrough | Utilizar Ejecución Normal | Escenario | Utilizar Passthrough | Utilizar Ejecución Normal
|----------|-----------------|----------------------|
| Operaciones CRUD simples
| Herramientas de registro/auditoría
| Herramientas que requieren razonamiento LLM

| Herramientas que pueden necesitar reintentos con pistas | | ✓ |

### Declaración DSL

```go
Export("logging-tools", func() {
    Tool("log_message", "Log a message", func() {
        Args(func() {
            Attribute("level", String, "Log level", func() {
                Enum("debug", "info", "warn", "error")
            })
            Attribute("message", String, "Message to log")
            Required("level", "message")
        })
        Return(func() {
            Attribute("logged", Boolean, "Whether the message was logged")
            Required("logged")
        })
        // Bypass planner, forward directly to LoggingService.LogMessage
        Passthrough("log_message", "LoggingService", "LogMessage")
    })
})
```

### Comportamiento en tiempo de ejecución

Cuando un agente consumidor llama a una herramienta passthrough:

1. El tiempo de ejecución recibe la llamada a la herramienta desde el planificador del consumidor
2. En lugar de invocar al planificador del agente proveedor, llama directamente al método del servicio de destino
3. El resultado se devuelve al consumidor sin ningún procesamiento LLM

Esto proporciona:
- **Latencia predecible**: Sin retardo de inferencia LLM
- **Comportamiento determinista**: La misma entrada siempre produce la misma salida
- **Eficiencia de costes**: Sin uso de tokens para operaciones sencillas

---

## Ejecutar árboles y sesiones

Goa-AI modela la ejecución como un **árbol de ejecuciones y herramientas**:

{{< figure src="/images/diagrams/RunTree.svg" alt="Hierarchical agent execution with run trees" >}}

- **Run** - una ejecución de un agente:
  - Identificada por un `RunID`
  - Descrito por `run.Context` (RunID, SessionID, TurnID, labels, caps)
  - El `storage.Store` de la aplicación registra de forma duradera el estado de la ejecución y sus registros inmutables en una sola operación

- **Sesión**: una conversación o flujo de trabajo que abarca una o más ejecuciones:
  - `SessionID` agrupa ejecuciones relacionadas (por ejemplo, chat multiturno)
  - Las interfaces de usuario suelen mostrar una sesión cada vez

- **Árbol de ejecuciones**: relaciones padre/hijo entre ejecuciones y herramientas:
  - Ejecución de agente de nivel superior (por ejemplo, `chat`)
  - Ejecuciones de agente secundarias (agente como herramienta, por ejemplo, `ada`, `diagnostics`)
  - Herramientas de servicio por debajo de esos agentes

El runtime mantiene este árbol usando:

- `run.Handle` - un manejador ligero con `RunID`, `AgentID`, `ParentRunID`, `ParentToolCallID`
- Ayudantes de agente como herramienta y registros de conjunto de herramientas que **siempre crean ejecuciones hijo reales** para agentes anidados (sin hacks ocultos en línea)

Antes de ejecutar un planificador hijo, `storage.Store.StartChildRun` guarda
juntos el vínculo con el padre, los metadatos del hijo y su primer registro.
Para un padre sin sesión, `StartOneShotChildRun` realiza la misma operación sin
inventar una sesión. La primera llamada exige que el padre exista, no tenga
sesión y siga activo. Un reintento exacto sigue siendo válido después de que el
padre termine porque el vínculo ya está guardado; un reintento modificado o un
nuevo hijo después de ese final se rechaza.

Si el registro de la herramienta del padre renderiza un prompt para el hijo, el
runtime prepara ese prompt en una activity antes de iniciar el workflow hijo.
La activity devuelve exactamente un éxito o un fallo. El éxito solo contiene
los mensajes exactos y los eventos de renderizado guardados en el historial del
workflow. El workflow obtiene la identidad de la ejecución hija, sesión, padre,
herramienta y etiquetas de la llamada original ya registrada, en lugar de
aceptar esa identidad de la activity. El replay usa así el texto original y
nunca lee del almacenamiento una versión posterior del prompt.

Los IDs de los workflows hijos de Temporal incluyen el ID exacto de la llamada
a herramienta asignado por el runtime. Así, las llamadas paralelas al mismo
agente anidado siguen siendo distintas; una versión que cambie esta derivación
no es compatible con los workflows hijos que ya están en curso.

---

## Agente-como-Herramienta y RunLink

Cuando un agente utiliza otro agente como herramienta:

1. El runtime inicia un **child run** para el agente proveedor con su propio `RunID`
2. Realiza un seguimiento de la vinculación padre/hijo en `run.Context`
3. Ejecuta un bucle completo de planificación/ejecución/reanudación en el agente hijo

El resultado de la herramienta padre (`planner.ToolResult`) lleva:

```go
RunLink *run.Handle
```

Este `RunLink` permite:
- Los planificadores razonar sobre la ejecución hija (por ejemplo, para auditoría/registro)
- Interfaces de usuario para crear "tarjetas de agente" anidadas y renderizar eventos de la ejecución hija filtrando el flujo de sesión por `run_id`
- Herramientas externas para navegar desde una ejecución padre a sus hijas sin adivinar

---

## Flujos propiedad de la sesión

Goa-AI publica eventos `stream.Event` en un único **flujo propiedad de la sesión**:

- `session/<session_id>`

Ese flujo contiene eventos para todas las ejecuciones de la sesión, incluidas ejecuciones anidadas de agentes (agent-as-tool). Cada evento lleva `run_id` y `session_id`, y el runtime emite:

- `child_run_linked`: vincula una llamada de herramienta padre (`tool_call_id`) con la ejecución hija (`child_run_id`)
- `run_stream_end`: marcador explícito que significa “no habrá más eventos visibles para esta ejecución”

Los consumidores se suscriben **una vez por sesión** y cierran SSE/WebSocket al observar `run_stream_end` para el `run_id` activo.

```go
import "goa.design/goa-ai/runtime/agent/stream"

events, errs, cancel, err := sub.Subscribe(ctx, "session/session-123")
if err != nil {
    panic(err)
}
defer cancel()

activeRunID := "run-123"
for {
    select {
    case evt, ok := <-events:
        if !ok {
            return
        }
        if evt.Type() == stream.EventRunStreamEnd && evt.RunID() == activeRunID {
            return
        }
    case err := <-errs:
        panic(err)
    }
}
```

---

## Perfiles de flujo

`stream.StreamProfile` describe qué tipos de eventos se emiten para una audiencia.

### Estructura de StreamProfile

```go
type StreamProfile struct {
    Assistant          bool // assistant_reply
    AssistantTurns     bool // assistant_turn
    Thoughts           bool // planner_thought
    PromptRendered     bool // prompt_rendered
    ToolStart          bool // tool_start
    ToolUpdate         bool // tool_update
    ToolEnd            bool // tool_end
    AwaitClarification bool // await_clarification
    AwaitConfirmation  bool // await_confirmation
    AwaitQuestions     bool // await_questions
    AwaitExternalTools bool // await_external_tools
    ToolAuthorization  bool // tool_authorization
    Usage              bool // usage
    Workflow           bool // workflow
    ChildRuns          bool // child_run_linked (herramienta padre → ejecución hija)
}
```

### Perfiles incorporados

Goa-AI proporciona perfiles incorporados para casos de uso comunes:

- `stream.DefaultProfile()` emite todos los tipos de eventos.
- `stream.UserChatProfile()` es adecuado para UIs de usuario final.
- `stream.AgentDebugProfile()` es adecuado para vistas de depuración/desarrollador.
- `stream.MetricsProfile()` emite sólo `Usage` y `Workflow`.

En el modelo de streaming propiedad de la sesión, no se requieren suscripciones separadas para ejecuciones hijas. `child_run_linked` existe para construir el árbol de ejecuciones y adjuntar eventos al “agent card” correcto mientras se consume un único flujo `session/<session_id>`.

### Cableado de perfiles a suscriptores

Aplique perfiles al crear suscriptores de flujos:

```go
import "goa.design/goa-ai/runtime/agent/stream"

// Create a subscriber with the user chat profile
chatSub, err := stream.NewSubscriberWithProfile(chatSink, stream.UserChatProfile())
if err != nil {
    return err
}

// Create a subscriber with the debug profile
debugSub, err := stream.NewSubscriberWithProfile(debugSink, stream.AgentDebugProfile())
if err != nil {
    return err
}

// Create a subscriber with the metrics profile
metricsSub, err := stream.NewSubscriberWithProfile(metricsSink, stream.MetricsProfile())
if err != nil {
    return err
}
```

### Creación de perfiles personalizados

Para necesidades especializadas, cree perfiles personalizados configurando campos individuales:

```go
// Custom profile: tools and workflow only, no thoughts or assistant replies
toolsOnlyProfile := stream.StreamProfile{
    ToolStart:   true,
    ToolUpdate:  true,
    ToolEnd:     true,
    Workflow:    true,
    ChildRuns:   true,
}

// Custom profile: everything except usage (for privacy-sensitive contexts)
noUsageProfile := stream.DefaultProfile()
noUsageProfile.Usage = false

sub, err := stream.NewSubscriberWithProfile(sink, toolsOnlyProfile)
```

### Pautas de selección de perfiles

| Audiencia | Perfil Recomendado | Justificación |
|----------|---------------------|---------------|
| UI de chat para el usuario final | `UserChatProfile()` | Estructura limpia con tarjetas de agente anidadas |
| Consola de administración/depuración | `AgentDebugProfile()` | Visibilidad completa de herramientas, esperas y fases |
| Métricas/facturación | `MetricsProfile()` | Eventos mínimos para agregación |
| Registro de auditorías | `DefaultProfile()` | Registro completo con campos de correlación por ejecución |
| Cuadros de mando en tiempo real | Personalizado (workflow + usage) | Sólo seguimiento de estado y costes |

Las aplicaciones eligen el perfil al cablear los sumideros y los puentes (p. ej., Pulse, SSE, WebSocket) de modo que:
- Las interfaces de usuario de chat se mantienen limpias y estructuradas (tarjetas anidadas impulsadas por `child_run_linked`)
- Las consolas de depuración pueden ver el detalle completo en el mismo flujo de sesión
- Las canalizaciones de métricas ven lo suficiente para agregar el uso y los estados

---

## Diseño de UIs con Run Trees

Dado el modelo de árbol de ejecución + streaming, una interfaz de usuario de chat típica puede:

1. Suscribirse al flujo de sesión (`session/<session_id>`) usando un perfil de chat de usuario.
2. Seguir la ejecución activa (`active_run_id`) y renderizar:
   - Respuestas del asistente (`assistant_reply`)
   - Ciclo de vida de herramientas (`tool_start`/`tool_update`/`tool_end`)
   - Enlaces a ejecuciones hijas (`child_run_linked`) como **Tarjetas de agente** anidadas por `child_run_id`
3. Para cada tarjeta, renderizar la línea de tiempo de la ejecución hija filtrando el mismo flujo de sesión por `run_id == child_run_id` (sin suscripciones adicionales).
4. Cerrar SSE/WebSocket cuando observes `run_stream_end` para `active_run_id`.

La idea clave: **la topología de ejecución (árbol de ejecución) se conserva por IDs y eventos de enlace**, y el streaming es un único log ordenado por sesión que proyectas en carriles/tarjetas filtrando por `run_id`.

---

## Próximos Pasos

- **[Integración MCP](./mcp-integration.md)** - Conectar con servidores de herramientas externos
- **[Memoria y sesiones](./memory-sessions.md)** - Gestionar el estado con transcripciones y almacenes de memoria
- **[Producción](./production.md)** - Despliegue con Temporal y streaming UI

