# Conjuntos de herramientas

Aprende sobre los tipos de conjuntos de herramientas, modelos de ejecución, validación, recuperación estructurada de fallos y catálogos de herramientas en Goa-AI.

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

Relative links resolve against the source URL above.


Los conjuntos de herramientas son colecciones de herramientas que los agentes pueden utilizar. Goa-AI admite varios tipos de conjuntos de herramientas, cada uno con diferentes modelos de ejecución y casos de uso.

## Tipos de conjuntos de herramientas

### Conjuntos de herramientas propios del servicio (respaldados por métodos)

Declarados mediante `Toolset("name", func() { ... })`; las herramientas pueden usar `BindTo` con métodos de servicios Goa o ser implementadas por ejecutores personalizados.

- Codegen emite specs/types/codecs/transforms por conjunto de herramientas en `gen/<service>/toolsets/<toolset>/`
- Cuando se usa el Registro interno de herramientas, codegen también emite `gen/<service>/toolsets/<toolset>/provider.go` para la ejecución del lado del servicio enrutada por el registro
- Los agentes que usan `Use` con estos conjuntos de herramientas importan los specs del proveedor y obtienen constructores de llamadas tipadas y fábricas de ejecutores
- Las aplicaciones registran ejecutores que decodifican argumentos tipados (mediante codecs proporcionados en tiempo de ejecución), opcionalmente utilizan transforms, llaman a clientes de servicios y devuelven `ToolResult`

Si despliegas el Registro interno de herramientas para la invocación entre procesos, el servicio propietario ejecuta un bucle de proveedor que se suscribe a `toolset:<toolsetID>:requests` y publica los resultados en `result:<toolUseID>`. Consulta la [documentación del Registro]({{< ref "/docs/2-goa-ai/registry.md" >}}) para ver el fragmento de cableado del proveedor.

### Conjuntos de herramientas implementados por agentes (Agent-as-Tool)

Se definen en un bloque `Export` del agente, y opcionalmente se usan con `Use` desde otros agentes.

- La propiedad sigue siendo del servicio; el agente es la implementación
- Codegen emite paquetes de exportación del lado del proveedor en `gen/<service>/agents/<agent>/exports/<export>` con `NewRegistration` y constructores de llamadas tipadas
- Los helpers del lado del consumidor en los agentes que usan `Use` con el conjunto de herramientas exportado delegan en los helpers del proveedor, manteniendo centralizados los metadatos de enrutamiento
- La ejecución es en línea; las cargas se pasan como JSON canónico y se decodifican solo en el límite si se necesitan para los prompts

### Conjuntos de herramientas MCP

Se declaran mediante `Toolset(FromMCP(service, suite))` para suites MCP respaldadas por Goa, o
`Toolset("name", FromExternalMCP(service, suite), func() { ... })` para servidores MCP
externos con esquemas de herramientas en línea.

- El registro generado establece `DecodeInExecutor=true` para que el JSON crudo se pase tal cual al ejecutor MCP
- El ejecutor MCP decodifica usando sus propios codecs
- Los wrappers generados se encargan de los esquemas, los codificadores y el transporte HTTP o stdio con reintentos y trazado. HTTP acepta respuestas JSON y flujos de eventos

### Cuándo usar BindTo frente a implementaciones en línea

**Usa `BindTo` cuando:**
- La herramienta debe llamar a un método de servicio Goa existente
- Deseas transforms generados entre los tipos de la herramienta y del método
- El método de servicio ya contiene la lógica de negocio que necesitas
- Deseas reutilizar la validación y el manejo de errores de la capa de servicio

```go
// Herramienta enlazada a un método de servicio existente
Tool("search", "Search documents", func() {
    Args(SearchPayload)
    Return(SearchResult)
    BindTo("Search")  // Llama al método Search del mismo servicio
})
```

**Usa implementaciones en línea cuando:**
- La herramienta tiene lógica personalizada no ligada a un método de servicio
- Necesitas orquestar múltiples llamadas a servicios
- La herramienta es puramente computacional (sin llamadas externas)
- Deseas un control total sobre el flujo de ejecución

```go
// Herramienta con implementación de ejecutor personalizado
Tool("summarize", "Summarize multiple documents", func() {
    Args(func() {
        Attribute("doc_ids", ArrayOf(String), "Document IDs to summarize")
        Required("doc_ids")
    })
    Return(func() {
        Attribute("summary", String, "Combined summary")
        Required("summary")
    })
    // Sin BindTo - se implementa en el ejecutor
})
```

Para las implementaciones en línea, escribes la lógica del ejecutor directamente:

```go
func (e *Executor) Execute(
    ctx context.Context,
    meta *runtime.ToolCallMeta,
    call *runtime.ToolCall,
) (*runtime.ToolExecutionResult, error) {
    switch call.Name {
    case specs.Summarize:
        args, err := specs.SummarizeTool().Payload.FromJSON(call.Payload)
        if err != nil {
            return nil, fmt.Errorf("decode admitted %s payload: %w", call.Name, err)
        }
        // Lógica personalizada: obtener varios documentos, combinarlos y resumirlos
        summary := e.summarizeDocuments(ctx, args.DocIDs)
        return runtime.Executed(&planner.ToolResult{
            Name:   call.Name,
            Result: &specs.SummarizeResult{Summary: summary},
        }), nil
    }
    return runtime.Executed(&planner.ToolResult{
        Name: call.Name,
        Failure: &planner.ToolFailure{
            Kind:     planner.FailureInvalidCall,
            Error:    planner.NewToolError("unknown tool"),
            Recovery: planner.RecoveryDirective{Action: planner.RecoveryReplan},
        },
    }), nil
}

```

### Esquemas y ejemplos de herramientas generados

Goa-AI trata la especificación de herramienta generada como el contrato canónico
visible para el modelo. Para cada payload de herramienta, codegen deriva JSON
Schema desde el atributo Goa y precalcula las proyecciones que necesitan los
adaptadores de proveedores:

- el esquema anotado, incluidos los ejemplos JSON Schema de nivel raíz y de campo
- el mismo esquema con solo el `example` raíz eliminado
- el JSON sin procesar del ejemplo raíz declarado y el objeto de entrada de
  ejemplo ya parseado

Solo un `Example(...)` de Goa de nivel superior declarado en el payload de la
herramienta se convierte en un ejemplo de herramienta de nivel superior expuesto
al proveedor. Los ejemplos sintetizados por Goa pueden permanecer como
anotaciones de esquema anidadas, pero no se promocionan a ejemplos nativos del
proveedor.

Los adaptadores eligen la proyección que coincide con el contrato del proveedor.
Las llamadas a herramientas de estilo OpenAI pueden consumir directamente las
anotaciones del esquema. Anthropic directo y Bedrock Claude envían los ejemplos
parseados como `input_examples` nativos mientras usan el esquema sin el ejemplo
raíz; Bedrock transporta los campos de Anthropic mediante
`additionalModelRequestFields` cuando lo exige el contrato beta aplicable.

Si tu aplicación enruta las solicitudes de modelo a través de un servicio de
inferencia o proxy, ese límite debe transportar esas proyecciones juntas como un
`model.ToolInputContract` neutral para el proveedor. El límite no debe importar
`tools.TypeSpec` propio del generador, volver a serializar esquemas ya
decodificados ni saber qué proveedor consume cada proyección. Eliminar el
esquema sin ejemplo raíz o la entrada de ejemplo parseada impide que los
adaptadores del proveedor envíen `input_examples` nativos, aunque la
especificación generada fuera correcta.

### Resultados de herramientas acotados

Algunas herramientas devuelven de forma natural listas grandes, grafos o ventanas de series temporales. Puedes marcarlas como **vistas acotadas** para que los servicios sigan siendo responsables del recorte mientras el runtime hace cumplir y expone el contrato.

#### El contrato agent.Bounds

El tipo `agent.Bounds` es un contrato pequeño y agnóstico al proveedor que describe cómo un resultado de herramienta ha sido acotado respecto al conjunto de datos completo subyacente:

```go
type Bounds struct {
    Returned       int    // Número de elementos en la vista acotada
    Total          *int   // Total aproximado antes del truncado (opcional)
    Truncated      bool   // Si se aplicó algún tope (longitud, ventana, profundidad)
    NextCursor     *string // Cursor privado del proveedor cuando existe otra página
    RefinementHint string // Orientación sobre cómo acotar la consulta cuando se truncó
}
```

| Campo | Descripción |
|-------|-------------|
| `Returned` | Número de elementos realmente presentes en el resultado |
| `Total` | Recuento aproximado del total de elementos antes del truncado (nil si se desconoce) |
| `Truncated` | Verdadero si se aplicó algún tope (paginación, límites de profundidad, límites de tamaño) |
| `NextCursor` | Cursor opaco de la página siguiente; su visibilidad depende del contrato de paginación |
| `RefinementHint` | Orientación legible para acotar la consulta (p. ej., "Añada un filtro de fecha para reducir los resultados") |

#### Responsabilidad del servicio sobre el recorte

El runtime no calcula subconjuntos ni truncados por sí mismo; **los servicios son responsables de**:

1. **Aplicar la lógica de truncado**: paginación, límites de resultados, topes de profundidad, ventanas temporales
2. **Rellenar los metadatos de bounds del runtime**: establecer `planner.ToolResult.Bounds`
3. **Proporcionar sugerencias de refinamiento**: guiar a los usuarios/modelos sobre cómo acotar las consultas cuando los resultados están truncados

Este diseño mantiene la lógica de truncado donde reside el conocimiento del dominio (en los servicios), a la vez que ofrece un contrato uniforme para que el runtime, los planificadores y las UIs lo consuman.

#### Declarar herramientas acotadas

Utiliza el helper DSL `BoundedResult()` dentro de la definición de un `Tool`:

```go
Tool("list_devices", "List devices with pagination", func() {
    Args(func() {
        Attribute("site_id", String, "Site identifier")
        Required("site_id")
    })
    Return(func() {
        Attribute("devices", ArrayOf(Device), "Matching devices")
        Required("devices")
    })
    BoundedResult(func() {
        ContinueWith("continue_devices", "cursor")
        NextCursor("next_cursor")
    })
    BindTo("DeviceService", "ListDevices")
})

Tool("continue_devices", "Continue the available device results", func() {
    Args(func() {
        Attribute("cursor", String)
        Required("cursor")
    })
    Return(func() {
        Attribute("devices", ArrayOf(Device), "Matching devices")
        Required("devices")
    })
    BoundedResult(func() {
        Cursor("cursor")
        NextCursor("next_cursor")
    })
    BindTo("DeviceService", "ContinueDevices")
})
```

El cursor de la herramienta de continuación forma parte de su contrato de
ejecución, pero se elimina del esquema visible para el modelo. El runtime solo
anuncia la acción cuando puede continuar una única cabeza de cadena sin
ambigüedad; el modelo la llama con `{}` y no copia el cursor ni repite la
consulta original.

#### Generación de código

Cuando una herramienta está marcada con `BoundedResult()`:

- El spec de herramienta generado incluye `tools.ToolSpec.Bounds`
- El esquema JSON generado del resultado incluye los campos canónicos de acotado (`returned`, `total`,
  `truncated`, `refinement_hint`, y el opcional `next_cursor`)
- `tools.ToolSpec.Bounds` almacena los nombres JSON visibles para el modelo. Si
  el DSL nombra un atributo Goa lower-camel como `NextCursor("nextCursor")`,
  codegen emite `NextCursorField: "next_cursor"` para que esquemas, proyección
  del runtime y codecs de resultado usen la misma forma.
- `ContinueWith` mantiene el cursor en el runtime y expone al modelo una acción
  de continuación sin argumentos solo cuando hay una única cabeza activa de la
  cadena. La correspondencia exacta del cursor avanza las páginas secuenciales;
  las llamadas fuente en paralelo siguen siendo válidas, pero varias cabezas
  activas hacen que la continuación sin argumentos no esté disponible. Un contrato `Cursor` directo expone el cursor opaco en
  `next_cursor`.
- El tipo de resultado Go semántico sigue siendo específico del dominio; no necesita duplicar esos campos

Para herramientas `BindTo` respaldadas por métodos, el resultado del método de servicio enlazado todavía debe
llevar los campos canónicos de acotado para que el ejecutor generado pueda construir
`planner.ToolResult.Bounds` antes de la proyección en runtime.

```go
spec.Bounds = &tools.BoundsSpec{
    Paging: &tools.PagingSpec{
        ContinueTool:    "tools.continue_devices",
        CursorField:     "cursor",
        NextCursorField: "next_cursor",
    },
}
```

#### Implementar herramientas acotadas

Las herramientas acotadas son un contrato estricto: los servicios implementan el truncado y rellenan los metadatos de bounds en cada ruta de código exitosa.

**Contrato:**

- `Bounds.Returned` y `Bounds.Truncated` deben establecerse siempre en los resultados exitosos de herramientas acotadas.
- `Bounds.Total`, `Bounds.NextCursor` y `Bounds.RefinementHint` son opcionales y solo deben establecerse cuando se conozcan.
  El código del proveedor establece `Bounds.NextCursor` con el cursor opaco de la página siguiente.

Los ejecutores implementan el truncado y rellenan los metadatos de bounds:

```go
func (e *DeviceExecutor) Execute(ctx context.Context, meta *runtime.ToolCallMeta, call *runtime.ToolCall) (*runtime.ToolExecutionResult, error) {
    args, err := specs.ListDevicesTool().Payload.FromJSON(call.Payload)
    if err != nil {
        return nil, fmt.Errorf("decode admitted %s payload: %w", call.Name, err)
    }

    devices, total, nextCursor, truncated, err := e.repo.QueryDevices(ctx, args.SiteID, args.Cursor)
    if err != nil {
        return nil, err
    }

    return runtime.Executed(&planner.ToolResult{
        Name: call.Name,
        Result: &ListDevicesResult{
            Devices: devices,
        },
        Bounds: &agent.Bounds{
            Returned:       len(devices),
            Total:          ptr(total),
            Truncated:      truncated,
            NextCursor:     nextCursor,
            RefinementHint: "Add a status filter or reduce the site scope to see fewer results",
        },
    }), nil
}
```

#### Comportamiento en runtime

Cuando se ejecuta una herramienta acotada:

1. El runtime valida que una herramienta acotada exitosa haya devuelto `planner.ToolResult.Bounds`
2. El runtime fusiona esos bounds en el JSON emitido usando los nombres JSON visibles para el modelo generados desde `BoundedResult(...)`
3. Con `ContinueWith`, el runtime ofrece la acción vacía solo cuando una única cabeza activa de la cadena puede continuar y enlaza el cursor antes de ejecutar
4. Si otra herramienta del mismo lote paralelo requiere recuperación `finish`, no pueden iniciarse nuevas operaciones. Las consultas correctas con una página siguiente conservan sus acciones de continuación junto con las herramientas terminales que guardan el resultado final. El planificador puede obtener una página o enviar el resultado, nunca ambos en un lote. Ni una página ni una respuesta rechazada reabren operaciones. Sin páginas disponibles, solo queda la finalización terminal. Consulta la [recuperación de fallos](../runtime/#finish-recovery)
5. Con `Cursor` directo, el runtime emite el cursor opaco en `next_cursor` para la siguiente llamada del modelo
6. Los suscriptores de streams y los finalizadores acceden a los bounds para su visualización en la UI, logging o decisiones de políticas

Cuando un resultado truncado no tiene un cursor de página siguiente, el
recordatorio del runtime pide al modelo que indique los límites de la vista.
Los resultados parciales pueden sustentar respuestas útiles sobre los elementos
devueltos si ese alcance queda claro. Advertir del truncado u obtener otra
página que siga siendo parcial no permite establecer hechos sobre los elementos
que siguen omitidos. Los totales que el proveedor establece para la consulta
completa conservan ese alcance; las pruebas posteriores que sean completas por
sí mismas pueden sustentar conclusiones dentro de su propio alcance. No es
necesario obtener más páginas si las pruebas disponibles ya responden a la
pregunta.

```go
// En un suscriptor de stream
func handleToolEnd(event *stream.ToolEndEvent) {
    if event.Bounds != nil && event.Bounds.Truncated {
        log.Printf("Tool %s returned %d of %d results (truncated)",
            event.ToolName, event.Bounds.Returned, *event.Bounds.Total)
        if event.Bounds.RefinementHint != "" {
            log.Printf("Hint: %s", event.Bounds.RefinementHint)
        }
    }
}
```

#### Cuándo usar BoundedResult

Utiliza `BoundedResult()` para herramientas que:
- Devuelven listas paginadas (dispositivos, usuarios, registros, logs)
- Consultan grandes conjuntos de datos con límites de resultados
- Aplican topes de profundidad o tamaño a estructuras anidadas (grafos, árboles)
- Devuelven datos de ventanas temporales (métricas, eventos)

El contrato acotado ayuda a que:
- Los **modelos** entiendan que los resultados pueden estar incompletos y puedan pedir un refinamiento
- Las **UIs** muestren indicadores de truncado y controles de paginación
- Las **políticas** apliquen límites de tamaño y detecten consultas descontroladas

### Campos inyectados

La función DSL `Inject` marca campos específicos de la carga como "inyectados": valores de infraestructura del lado del servidor que se ocultan al LLM y que el código generado rellena antes de ejecutar la herramienta. Esto es útil para IDs de sesión, alcance por tenant/household y otros valores proporcionados en tiempo de ejecución o por el llamador.

#### Cómo funciona Inject

Cuando marcas un campo con `Inject`, la generación de código lo resuelve a una de dos fuentes determinadas en tiempo de generación:

1. **Oculto al LLM**: el campo se excluye del esquema JSON y de la lista de campos obligatorios visible para el modelo
2. **Validado en tiempo de diseño**: el campo debe ser un `String` obligatorio en la carga efectiva de la herramienta (el `Args()` explícito si se indica, o si no la carga del método enlazado)
3. **Respaldado por metadatos o por etiquetas**: un nombre cuyo Goify coincide con uno de los cinco campos fijos de `runtime.ToolCallMeta` (`run_id`/`runId`, `session_id`/`sessionId`, `turn_id`/`turnId`, `tool_call_id`/`toolCallId`, `parent_tool_call_id`/`parentToolCallId`) está **respaldado por metadatos** y se compila como una lectura directa de esos metadatos. Cualquier otro nombre está **respaldado por una etiqueta**: se compila como una búsqueda en las etiquetas de la ejecución (la clave de la etiqueta es el nombre del diseño tal cual), aplicando la validación declarada del propio campo (`Pattern`, `Length`, enum, ...) al valor de la etiqueta. Un campo respaldado por etiqueta no puede declararse en una herramienta `BindTo`, porque el protocolo del registro que usan las herramientas enlazadas y servidas por el registro no transporta etiquetas de ejecución
4. **Rellenado por el ejecutor**: ambas topologías de ejecución (los ejecutores locales en proceso y el proveedor servido por el registro) llaman a la *misma* función `Inject<Tool>` generada entre la decodificación y la ejecución, de modo que el rellenado nunca diverge según dónde se ejecute la herramienta

#### Declaración DSL

```go
Tool("get_user_data", "Get data for current user", func() {
    Args(func() {
        Attribute("session_id", String, "Current session ID")
        Attribute("query", String, "Data query")
        Required("session_id", "query")
    })
    Return(func() {
        Attribute("data", ArrayOf(String), "Query results")
        Required("data")
    })
    BindTo("UserService", "GetData")
    Inject("session_id")  // respaldado por metadatos: oculto al LLM, rellenado en tiempo de ejecución
})
```

Los campos respaldados por etiqueta funcionan igual, pero no son nombres de `runtime.ToolCallMeta`:

```go
Tool("lookup_household", "Lookup scoped to a household", func() {
    Args(func() {
        Attribute("household_id", String, "Household to scope the search to.", func() {
            Pattern("^[a-z0-9-]+$")
        })
        Attribute("query", String, "Search query.")
        Required("household_id", "query")
    })
    Inject("household_id")  // respaldado por etiqueta: se establece con WithLabels("household_id", ...)
})
```

El llamador proporciona los valores de las etiquetas al iniciar la ejecución con `runtime.WithLabels(...)`:

```go
out, err := client.Run(ctx, sessionID, messages,
    runtime.WithLabels(map[string]string{"household_id": "house-42"}),
)
```

Los campos respaldados por etiqueta de un conjunto de herramientas también contribuyen a una lista `RequiredLabels` generada, agregada por agente. `Runtime.Start`/`StartOneShot` validan las etiquetas proporcionadas por el llamador frente a esta lista **antes** de programar cualquier workflow o actividad, fallando rápido y nombrando en un solo error todas las claves que falten. Esta comprobación es un no-op para un proceso que solo dispone de un cliente `Runtime.ClientFor(route)` de pasarela/orquestación (sin registro local del agente); en esa topología, una etiqueta faltante se detecta más tarde, en cada llamada a herramienta.

#### Código generado

Los ejecutores generados respaldados por métodos llaman a una función `Inject<Tool>` generada por cada herramienta que inyecta (en el `inject.go` del conjunto de herramientas, junto a sus códecs), que copia los campos respaldados por metadatos desde `runtime.ToolCallMeta` y los respaldados por etiqueta desde las etiquetas de la ejecución sobre la carga tipada:

```go
p, err := specs.InjectGetUserData(toolArgs, meta, labels)
```

Los nombres de campo inyectados admitidos **no** son una lista fija: cualquier nombre que coincida con un campo de `runtime.ToolCallMeta` está respaldado por metadatos, y cualquier otro nombre está respaldado por una etiqueta.

#### Decodificación de cargas en ejecutores personalizados

Los `ToolCallExecutor` escritos a mano (para herramientas sin `BindTo`, registradas directamente en el runtime) no tienen ningún punto de despacho generado que llame a `Inject<Tool>` por ellos. Decodifica la carga de estas herramientas con la función `Decode<Tool>` generada por el conjunto de herramientas en lugar del códec de carga en bruto:

```go
p, err := specs.DecodeLookupHousehold(call.Payload, meta, call.Labels)
if err != nil {
    // gestiona el fallo de decodificación o de inyección (etiqueta faltante/no válida, etc.)
}
```

`Decode<Tool>` compone `<Tool>PayloadCodec.FromJSON` con `Inject<Tool>` en una sola llamada, de modo que la inyección nunca puede omitirse silenciosamente. Decodificar solo con el códec dejaría los campos inyectados en su valor cero de Go sin ningún error, ya que su etiqueta de wire es `json:"-"` (oculta al modelo) y no hay ninguna señal de "clave faltante".

#### Rellenado en runtime mediante interceptores generados

Los ejecutores de servicio generados también exponen hooks de interceptor tipados, independientes de `Inject()`. Úsalos para derivar campos de la carga del método a partir del contexto de la solicitud u otro estado de runtime, además de (o en lugar de) los campos inyectados declarados en el diseño:

```go
type SessionInterceptor struct{}

func (i *SessionInterceptor) Inject(ctx context.Context, payload any, meta *runtime.ToolCallMeta) error {
    sessionID, ok := ctx.Value(sessionKey).(string)
    if !ok {
        return fmt.Errorf("session ID not found in context")
    }

    switch p := payload.(type) {
    case *userservice.GetDataPayload:
        p.SessionID = sessionID
    }
    return nil
}

exec := usertools.NewChatUserToolsExec(
    usertools.WithClient(userClient),
    usertools.WithInterceptors(&SessionInterceptor{}),
)
```

Los interceptores registrados se ejecutan después de la llamada a `Inject<Tool>` generada, sobre la carga tipada ya decodificada.

#### Cuándo usar Inject

Usa `Inject` para campos que:
- Son necesarios para el servicio pero no deberían ser elegidos por el LLM
- Provienen del contexto de runtime (sesión, IDs de ejecución/turno/llamada) o de etiquetas de ejecución proporcionadas por el llamador (tenant, household, usuario)
- Contienen valores sensibles (tokens de autenticación, claves de API)
- Son aspectos de infraestructura (IDs de trazado, IDs de correlación)

---

## Modelos de ejecución

### Ejecución basada en actividades (por defecto)

Los conjuntos de herramientas respaldados por servicios se ejecutan mediante
actividades de Temporal (o su equivalente en otros motores):

1. El cliente de modelo validado rechaza las llamadas del proveedor que no
   cumplen el esquema antes de que las reciba el planificador. Las llamadas
   creadas por el planificador usan `planner.NewToolRequest`, que devuelve
   directamente los errores de codificación.
2. El planificador devuelve `ToolCalls []planner.ToolRequest` válidas, con un
   nombre generado, bytes de payload canónicos y un ID de llamada del proveedor
   opcional.
3. El runtime valida el plan completo, asigna cada ID de ejecución y programa
   `ExecuteToolActivity`.
4. La actividad decodifica el payload ya admitido. Un fallo aquí es un error de
   invariantes interno, no evidencia para corregir la llamada.
5. La actividad llama a
   `Execute(ctx, meta, *runtime.ToolCall)` en el registro del toolset con JSON
   canónico y el ID de ejecución asignado por el runtime.
6. La actividad vuelve a codificar el resultado con el codec generado.

### Ejecución en línea (Agent-as-Tool)

Los conjuntos de herramientas agent-as-tool se ejecutan en línea desde la perspectiva del planificador, mientras que el runtime ejecuta el agente proveedor como un run hijo real:

1. El runtime detecta `Inline=true` en el registro del conjunto de herramientas
2. Inyecta el `engine.WorkflowContext` en `ctx` para que la función `Execute` del conjunto de herramientas pueda iniciar el agente proveedor como un workflow hijo con su propio `RunID`
3. Llama a `Execute(ctx, meta, *runtime.ToolCall)` en el conjunto de herramientas
   con una carga JSON canónica y metadatos de la herramienta (incluidos el
   `RunID` y el `ToolCallID` padre)
4. El ejecutor agent-tool generado construye mensajes anidados del agente (sistema + usuario) a partir de la carga de la herramienta y ejecuta al agente proveedor como un run hijo
5. El agente anidado ejecuta un bucle completo de plan/execute/resume en su propio run; su `RunOutput` y los eventos de herramienta se agregan a un `planner.ToolResult` padre que lleva la carga de resultado, telemetría agregada, `ChildrenCount` del hijo y un `RunLink` que apunta al run hijo
6. Los suscriptores de streams emiten tanto `tool_start` / `tool_end` para la llamada a herramienta padre como un evento de enlace `child_run_linked` para que las UIs puedan construir tarjetas anidadas de agentes mientras consumen un único stream de sesión

### Materializadores de resultados

Los conjuntos de herramientas pueden registrar un materializador de resultados tipado:

```go
reg := runtime.ToolsetRegistration{
    Name: "chat.ask_question",
    Execute: runtime.ToolCallExecutorFunc(func(
        ctx context.Context,
        meta *runtime.ToolCallMeta,
        call *runtime.ToolCall,
    ) (*runtime.ToolExecutionResult, error) {
        return runtime.Executed(&planner.ToolResult{
            Name: call.Name,
            Failure: &planner.ToolFailure{
                Kind:  planner.FailureUnavailable,
                Error: planner.NewToolError("externally provided"),
                Recovery: planner.RecoveryDirective{
                    Action: planner.RecoveryReplan,
                },
            },
        }), nil
    }),
    Specs: []tools.ToolSpec{specs.SpecAskQuestion()},
    ResultMaterializer: func(ctx context.Context, meta runtime.ToolCallMeta, call *runtime.ToolCall, result *planner.ToolResult) error {
        // Adjunta aquí sidecars deterministas solo del servidor.
        result.ServerData = buildServerData(call, result)
        return nil
    },
}
```

Contrato:

- `ResultMaterializer` se ejecuta tanto en la **ruta de ejecución normal** como en la **ruta de espera con resultado proporcionado externamente**.
- Recibe el `runtime.ToolCall` validado, incluido el ID de ejecución asignado por
  el runtime, junto con el `planner.ToolResult` tipado antes de que el runtime
  codifique JSON para hooks, límites del workflow o llamadores.
- Úsalo para adjuntar `result.ServerData` o para normalizar la forma semántica del resultado de manera determinista.
- Mantenlo puro y determinista; cuando se ejecuta dentro de código de workflow no debe realizar E/S.

Este es el lugar canónico para derivar sidecars solo para observadores a partir de la carga original de la herramienta y el resultado tipado, manteniendo esos sidecars invisibles para los proveedores de modelos.

---

## Modelo centrado en el ejecutor

Los conjuntos de herramientas de servicio generados exponen helpers de registro que aceptan
implementaciones de `runtime.ToolCallExecutor` para los conjuntos de herramientas que usa un agente.

```go
if err := chat.RegisterUsedToolsets(ctx, rt,
    chat.WithSearchExecutor(searchExec),
    chat.WithProfilesExecutor(profileExec),
); err != nil {
    return err
}
```

Las aplicaciones registran una implementación de ejecutor para cada conjunto de herramientas
local consumido. El ejecutor decide cómo ejecutar la herramienta (cliente de servicio, función
personalizada, llamador del registro, etc.) y recibe metadatos explícitos por llamada mediante
`ToolCallMeta`.

**Ejemplo de ejecutor:**

```go
func Execute(ctx context.Context, meta *runtime.ToolCallMeta, call *runtime.ToolCall) (*runtime.ToolExecutionResult, error) {
    switch call.Name {
    case "orchestrator.profiles.upsert":
        args, err := profilesspecs.UpsertTool().Payload.FromJSON(call.Payload)
        if err != nil {
            return nil, fmt.Errorf("decode admitted %s payload: %w", call.Name, err)
        }
        
        // Se genera cuando la herramienta y el método vinculado usan tipos distintos compatibles.
        mp := profilesspecs.InitUpsertMethodPayload(args)
        methodRes, err := client.Upsert(ctx, mp)
        if err != nil {
            return runtime.Executed(&planner.ToolResult{
                Name: call.Name,
                Failure: &planner.ToolFailure{
                    Kind:     planner.FailureUnavailable,
                    Error:    planner.ToolErrorFromError(err),
                    Recovery: planner.RecoveryDirective{Action: planner.RecoveryReplan},
                },
            }), nil
        }
        tr := profilesspecs.InitUpsertToolResult(methodRes)
        return runtime.Executed(&planner.ToolResult{
            Name:   call.Name,
            Result: tr,
        }), nil
        
    default:
        return runtime.Executed(&planner.ToolResult{
            Name: call.Name,
            Failure: &planner.ToolFailure{
                Kind:     planner.FailureInvalidCall,
                Error:    planner.NewToolError("unknown tool"),
                Recovery: planner.RecoveryDirective{Action: planner.RecoveryReplan},
            },
        }), nil
    }
}
```

---

## Metadatos de llamada a herramienta

Los ejecutores de herramientas reciben metadatos explícitos por llamada mediante `ToolCallMeta`, en lugar de extraer valores del `context.Context`. Esto proporciona acceso directo a los identificadores con alcance de run para la correlación, la telemetría y las relaciones padre/hijo.

### Campos de ToolCallMeta

| Campo | Descripción |
|-------|-------------|
| `RunID` | Identificador duradero de ejecución del workflow del run al que pertenece esta llamada a herramienta. Estable entre reintentos; se usa para correlacionar registros de runtime y telemetría. |
| `SessionID` | Agrupa lógicamente los runs relacionados (p. ej., una conversación de chat). Los servicios suelen indexar memoria y atributos de búsqueda por sesión. |
| `TurnID` | Identifica el turno conversacional que produjo esta llamada a herramienta. Los flujos de eventos lo usan para ordenar y agrupar eventos. |
| `ToolCallID` | Identifica de forma única esta invocación de herramienta. Se usa para correlacionar eventos start/update/end y relaciones padre/hijo. |
| `ParentToolCallID` | Identificador de la llamada a herramienta padre cuando esta invocación es hija (p. ej., una herramienta lanzada por un agent-tool). Las UIs y los suscriptores lo usan para reconstruir el árbol de llamadas. |

### Firma del ejecutor

Todos los ejecutores de herramientas reciben `ToolCallMeta` como un parámetro explícito:

```go
func Execute(ctx context.Context, meta *runtime.ToolCallMeta, call *runtime.ToolCall) (*runtime.ToolExecutionResult, error) {
    // Accede al contexto del run directamente desde meta
    log.Printf("Executing tool in run %s, session %s, turn %s",
        meta.RunID, meta.SessionID, meta.TurnID)

    // Usa ToolCallID para la correlación
    span := tracer.StartSpan("tool.execute", trace.WithAttributes(
        attribute.String("tool.call_id", meta.ToolCallID),
        attribute.String("tool.parent_call_id", meta.ParentToolCallID),
    ))
    defer span.End()
    
    typedResult := buildTypedResult()
    return runtime.Executed(&planner.ToolResult{Name: call.Name, Result: typedResult}), nil
}
```

### ¿Por qué metadatos explícitos?

El patrón de metadatos explícitos ofrece varios beneficios:

- **Seguridad de tipos**: garantías en tiempo de compilación de que los identificadores necesarios están disponibles
- **Facilidad de pruebas**: es sencillo construir metadatos de prueba sin tener que simular el contexto
- **Claridad**: sin dependencias ocultas de claves de contexto ni del orden del middleware
- **Correlación**: acceso directo a las relaciones padre/hijo para llamadas anidadas de agent-tool
- **Trazabilidad**: cadena causal completa desde la entrada del usuario hasta la ejecución de la herramienta y la respuesta final

---

## Ejecución asíncrona y duradera
 
Goa-AI utiliza **Temporal Activities** para todas las ejecuciones de herramientas respaldadas por servicios. Esta arquitectura "async-first" es implícita y no requiere ningún DSL especial.
 
### Async implícito
 
Cuando un planificador decide llamar a una herramienta, el runtime no bloquea el hilo del SO. En su lugar:
 
1. El runtime programa una **Actividad de Temporal** para la llamada a herramienta.
2. El workflow del agente suspende la ejecución (guardando el estado).
3. La actividad se ejecuta (en un worker local, un worker remoto o incluso en un clúster distinto).
4. Cuando la actividad termina, el workflow se despierta, restaura el estado y continúa con el resultado.
 
Esto significa que **cada llamada a herramienta** es automáticamente paralelizable, duradera y de ejecución prolongada. **No** necesitas configurar `InterruptsAllowed` para este comportamiento asíncrono estándar.
 
### Pausa y reanudación (nivel de agente)
 
`InterruptsAllowed(true)` es distinto: permite que el **propio agente** se detenga y espere una señal externa arbitraria (como una aclaración del usuario) que *no* esté asociada a una actividad de herramienta en curso.
 
| Característica | Async implícito | Pausa y reanudación |
| :--- | :--- | :--- |
| **Alcance** | Ejecución de una única herramienta | Todo el workflow del agente |
| **Disparador** | Llamar a cualquier herramienta respaldada por servicio | Argumentos que faltan o solicitud del planificador |
| **Política requerida** | Ninguna (por defecto) | `InterruptsAllowed(true)` |
| **Caso de uso** | API lenta, trabajo por lotes, procesamiento | Human-in-the-loop, aclaración |
 
Verifica que tu caso de uso realmente requiera una pausa *a nivel de agente* antes de activar la política; a menudo, el async estándar de herramientas es suficiente.
 
### Planificadores no bloqueantes
 
Desde la perspectiva del **planificador (LLM)**, la interacción parece síncrona: el modelo solicita una herramienta, "se pausa", y después "ve" el resultado en el siguiente turno.
 
Desde la perspectiva de la **infraestructura**, es totalmente asíncrona y no bloqueante. Esto permite que un único worker de agente pequeño gestione miles de ejecuciones concurrentes y de larga duración sin quedarse sin hilos ni memoria.
 
### Supervivencia ante reinicios
 
Dado que la ejecución es duradera, puedes reiniciar todo tu backend, incluidos los workers de agente, mientras las herramientas están en plena ejecución. Cuando los sistemas vuelven a estar activos:
 
- Las actividades de herramientas pendientes serán recogidas por los workers.
- Las herramientas completadas reportarán resultados a sus workflows padre.
- Los agentes retomarán exactamente donde se quedaron.
 
Esta capacidad es esencial para construir sistemas agentivos robustos de nivel de producción que operen de manera fiable en entornos dinámicos.

---

## Transforms

Cuando una herramienta está enlazada a un método Goa mediante `BindTo`, la generación de código analiza los Arg/Return de la herramienta y el Payload/Result del método. Si las formas son compatibles, Goa emite helpers de transform con tipado seguro:

- `ToMethodPayload_<Tool>(in <ToolArgs>) (<MethodPayload>, error)`
- `ToToolReturn_<Tool>(in <MethodResult>) (<ToolReturn>, error)`

Los transforms se emiten en el paquete propietario del conjunto de herramientas (por ejemplo, `gen/<service>/toolsets/<toolset>/transforms.go`) y utilizan GoTransform de Goa para mapear campos de forma segura. Si no se emite un transform, escribe un mapeador explícito en el ejecutor.

---

## Identidad de herramienta

Cada conjunto de herramientas define identificadores de herramienta tipados (`tools.Ident`) para todas las herramientas generadas, incluidos los conjuntos no exportados. Prefiere estas constantes frente a cadenas ad hoc:

```go
import searchspecs "example.com/assistant/gen/orchestrator/toolsets/search"

// Usa una constante generada en lugar de cadenas/casts ad hoc
spec, _ := rt.ToolSpec(searchspecs.Search)
schemas, _ := rt.ToolSchema(searchspecs.Search)
```

Para los conjuntos de herramientas exportados (agent-as-tool), Goa-AI genera paquetes de exportación en `gen/<service>/agents/<agent>/exports/<export>` con:
- IDs de herramienta tipados
- Tipos alias de payload/result
- Codecs
- Constructores helper (p. ej., `New<Search>Call`)

---

## Validación y recuperación de herramientas

Goa-AI combina las **validaciones en tiempo de diseño de Goa** con un **modelo
estructurado de errores de herramienta**. Una llamada emitida por el modelo
debe superar primero la validación del esquema. Solo después de ser admitida,
un fallo de ejecución o de dominio puede indicar al runtime cómo debe continuar
el planificador.

### Tipos principales: ToolError y ToolFailure

**ToolError** (alias de `runtime/agent/toolerrors.ToolError`):
- `Message string` – resumen legible por humanos
- `Cause *ToolError` – causa anidada opcional (preserva las cadenas a través de reintentos y saltos agent-as-tool)
- Constructores: `planner.NewToolError(msg)`, `planner.NewToolErrorWithCause(msg, cause)`, `planner.ToolErrorFromError(err)`, `planner.ToolErrorf(format, args...)`

**ToolFailure** mantiene separadas la clasificación del fallo y la siguiente
transición legal del planificador:

```go
type ToolFailure struct {
    Kind     FailureKind
    Error    *ToolError
    Recovery RecoveryDirective
}

type RecoveryDirective struct {
    Action      RecoveryAction
    Issues      []*tools.FieldIssue
    PriorInput  rawjson.Message
    ExampleJSON rawjson.Message
}
```

Los tipos de fallo incluyen llamadas inválidas, rechazo de dominio, falta de
disponibilidad, límites de velocidad, timeouts, resultados malformados y
errores internos. La recuperación tiene tres acciones explícitas:

- `RecoveryCorrectCall` mantiene disponible la herramienta que falló y aporta
  evidencia estructurada para corregirla.
- `RecoveryReplan` elimina esa herramienta del siguiente turno.
- `RecoveryFinish` prohíbe nuevas operaciones y permite la finalización terminal
  o páginas anunciadas de consultas ya iniciadas.

`ToolResult` lleva un resultado tipado o un único fallo estructurado:

```go
type ToolResult struct {
    Name          tools.Ident
    Result        any
    ServerData    rawjson.Message
    ResultBytes   int
    ResultOmitted bool
    ResultOmittedReason string
    Bounds        *agent.Bounds
    Failure       *ToolFailure
    Telemetry     *telemetry.ToolTelemetry
    ToolCallID    string
    ChildrenCount int
    RunLink       *run.Handle
}
```

### Recuperación de fallos de herramientas admitidas

El patrón recomendado:

1. **Diseña herramientas con esquemas de payload sólidos** en el diseño Goa.
2. **Trata los fallos de decodificación del ejecutor como errores de
   invariantes**: los payloads inválidos del modelo y los errores de
   codificación del planificador se detienen antes de la ejecución.
3. **Devuelve `ToolFailure` para fallos de dominio posteriores a la admisión**,
   de modo que una llamada creada por el modelo pueda solicitar corrección
   cuando un payload válido incumple una regla entre campos o una regla de
   negocio.
4. **Enseña al planificador a inspeccionar `ToolOutput.Failure`**; el runtime
   usa `Recovery` para decidir si mantiene la herramienta, la elimina o exige
   la finalización.

El cliente de modelo validado usa los codecs generados para rechazar campos
desconocidos, tipos JSON incorrectos y restricciones del esquema antes de
ejecutar código del planificador o del ejecutor. Esos fallos son errores de
contrato de salida, no valores `ToolFailure`. El ejemplo siguiente parte de una
llamada creada por el modelo que ya fue admitida, pero cuyo payload
decodificado incumple `validateUpsertRule`, una regla de dominio que el esquema
no puede expresar. Cuando el fallo solicita `RecoveryCorrectCall`, el workflow
deriva la entrada anterior y el ejemplo desde la llamada del proveedor y la
especificación registrada; ignora `PriorInput` y `ExampleJSON` escritos por el
ejecutor.

**Ejemplo de ejecutor:**

```go
func Execute(ctx context.Context, meta *runtime.ToolCallMeta, call *runtime.ToolCall) (*runtime.ToolExecutionResult, error) {
    args, err := spec.UpsertTool().Payload.FromJSON(call.Payload)
    if err != nil {
        return nil, fmt.Errorf("decode admitted %s payload: %w", call.Name, err)
    }
    if err := validateUpsertRule(args); err != nil {
        return runtime.Executed(&planner.ToolResult{
            Name: call.Name,
            Failure: &planner.ToolFailure{
                Kind:  planner.FailureInvalidCall,
                Error: planner.ToolErrorFromError(err),
                Recovery: planner.RecoveryDirective{
                    Action: planner.RecoveryCorrectCall,
                },
            },
        }), nil
    }

    res, err := client.Upsert(ctx, args)
    if err != nil {
        return runtime.Executed(&planner.ToolResult{
            Name: call.Name,
            Failure: &planner.ToolFailure{
                Kind:  planner.FailureUnavailable,
                Error: planner.ToolErrorFromError(err),
                Recovery: planner.RecoveryDirective{
                    Action: planner.RecoveryReplan,
                },
            },
        }), nil
    }

    return runtime.Executed(&planner.ToolResult{Name: call.Name, Result: res}), nil
}
```

`PlanResumeInput.ToolOutputs` contiene la forma segura para el workflow de cada
llamada: los bytes canónicos de payload y resultado, más `Failure`. Para
`RecoveryCorrectCall`, los problemas de campo, la entrada anterior y el JSON de
ejemplo permiten corregir la llamada en el siguiente turno.
`RecoveryReplan` elimina la herramienta que falló; `RecoveryFinish` sigue el
[contrato de finalización tras un fallo](../runtime/#finish-recovery).
El runtime impone estas transiciones: el planificador no
las deduce del texto del error.

Solo las llamadas creadas por el proveedor pueden usar
`RecoveryCorrectCall`. Las continuaciones creadas por el runtime no tienen una
entrada creada por el modelo y deben replanificar o finalizar, sin exponer su
payload privado de ejecución.

---

## Catálogos y esquemas de herramientas

Los agentes Goa-AI generan un **catálogo único y autoritativo de herramientas** a partir de tus diseños Goa. Este catálogo impulsa:
- La publicación de herramientas al planificador (qué herramientas puede llamar el modelo)
- El descubrimiento en la UI (listas de herramientas, categorías, esquemas)
- Orquestadores externos (MCP, frontends personalizados) que necesitan specs legibles por máquina

### Specs generados y tool_schemas.json

Para cada agente, Goa-AI emite un **paquete de specs** y un **catálogo JSON**:

**Paquetes de specs (`gen/<service>/agents/<agent>/specs/...`):**
- `types.go` – structs Go de payload/result
- `codecs.go` – codecs JSON (codifican/decodifican payloads/results tipados)
- `specs.go` – entradas `[]tools.ToolSpec` con el ID canónico de la herramienta, esquemas de payload/result y sugerencias

**Catálogo JSON (`tool_schemas.json`):**

Ubicación: `gen/<service>/agents/<agent>/specs/tool_schemas.json`

Contiene una entrada por herramienta con:
- `id` – ID canónico de la herramienta (`"<service>.<toolset>.<tool>"`)
- `service`, `toolset`, `title`, `description`, `tags`
- `payload.schema` y `result.schema` (JSON Schema)

Este fichero JSON es ideal para alimentar los esquemas a proveedores LLM, construir formularios/editores de UI y herramientas de documentación sin conexión.

### APIs de introspección en runtime

En runtime, no necesitas leer `tool_schemas.json` desde disco. El runtime expone una API de introspección:

```go
agents   := rt.ListAgents()     // []agent.Ident
toolsets := rt.ListToolsets()   // []string

spec,   ok := rt.ToolSpec(toolID)              // single ToolSpec
schemas, ok := rt.ToolSchema(toolID)           // payload/result schemas
specs   := rt.ToolSpecsForAgent(chat.AgentID)  // []ToolSpec for one agent
```

Donde `toolID` es una constante `tools.Ident` tipada proveniente de un paquete generado de specs o agenttools.

### Server Data

Algunas herramientas necesitan devolver una salida rica orientada a observadores —series temporales completas,
grafos de topología, grandes conjuntos de resultados, referencias de evidencia— que es útil para las UIs
y los sistemas de auditoría, pero demasiado pesada para los proveedores de modelos. Goa-AI modela esa
salida no orientada al modelo como **server-data**.

#### Resultado para el modelo vs server-data

La distinción clave es qué datos fluyen a dónde:

| Tipo de dato | Se envía al modelo | Se almacena/emite por streaming | Propósito |
|-----------|---------------|-----------------|---------|
| **Resultado orientado al modelo** | ✓ | ✓ | Resumen acotado sobre el que razona el LLM |
| **Server-data de timeline** | ✗ | ✓ | Datos orientados a observadores para UIs, timelines, gráficos, mapas y tablas |
| **Server-data de evidencia** | ✗ | ✓ | Referencias de procedencia o evidencia de auditoría |
| **Server-data interno** | ✗ | Depende del consumidor | Adjuntos de composición de herramientas o metadatos solo de servidor |

Esta separación te permite:
- Mantener acotadas y enfocadas las ventanas de contexto del modelo
- Ofrecer visualizaciones ricas (gráficos, grafos, tablas) sin inflar los prompts del LLM
- Adjuntar datos de procedencia y auditoría que los modelos no necesitan ver
- Emitir por streaming grandes conjuntos de datos a las UIs mientras el modelo trabaja con resúmenes

#### Declarar ServerData en el DSL

Usa la función `ServerData(kind, schema)` dentro de la definición de un `Tool`:

```go
Tool("get_time_series", "Get time series data", func() {
    Args(func() {
        Attribute("device_id", String, "Device identifier")
        Attribute("start_time", String, "Start timestamp (RFC3339)")
        Attribute("end_time", String, "End timestamp (RFC3339)")
        Required("device_id", "start_time", "end_time")
    })
    // Model-facing result: bounded summary
    Return(func() {
        Attribute("summary", String, "Summary for the model")
        Attribute("count", Int, "Number of data points")
        Attribute("min_value", Float64, "Minimum value in range")
        Attribute("max_value", Float64, "Maximum value in range")
        Required("summary", "count")
    })
    // Server-data: full-fidelity data for observers (e.g., UIs)
    ServerData("metrics.time_series", func() {
        Attribute("data_points", ArrayOf(TimeSeriesPoint), "Full time series data")
        Attribute("metadata", MapOf(String, String), "Additional metadata")
        Required("data_points")
    }, func() {
        AudienceTimeline()
    })
})
```

El parámetro `kind` (p. ej., `"metrics.time_series"`) identifica el tipo de server-data para que las UIs puedan despachar los renderizadores apropiados.
La audiencia declara la intención de enrutamiento:

- `AudienceTimeline()` para payloads orientados a observadores en timeline/UI.
- `AudienceEvidence()` para procedencia o evidencia de auditoría.
- `AudienceInternal()` para payloads de composición solo de servidor.

Utiliza `FromMethodResultField("field_name")` junto con herramientas `BindTo(...)` cuando el
payload de server-data se proyecte a partir de un campo del resultado del método de servicio enlazado.

#### Specs y helpers generados

En los paquetes de specs, cada entrada `tools.ToolSpec` incluye:
- `Payload tools.TypeSpec` – esquema de entrada de la herramienta
- `Result tools.TypeSpec` – esquema de salida orientada al modelo
- `ServerData []*tools.ServerDataSpec` – payloads solo de servidor emitidos junto con el resultado

Las entradas de server-data incluyen esquemas y codecs generados para que los suscriptores puedan
decodificar los bytes JSON canónicos sin enviar esos bytes a los proveedores de modelos.

#### Patrones de uso en runtime

**En los ejecutores de herramientas**, adjunta el JSON canónico de server-data al resultado de la herramienta:

```go
func (e *Executor) Execute(
    ctx context.Context,
    meta *runtime.ToolCallMeta,
    call *runtime.ToolCall,
) (*runtime.ToolExecutionResult, error) {
    args, err := specs.GetTimeSeriesTool().Payload.FromJSON(call.Payload)
    if err != nil {
        return nil, fmt.Errorf("decode admitted %s payload: %w", call.Name, err)
    }

    // Obtiene todos los datos
    fullData, err := e.dataService.GetTimeSeries(ctx, args.DeviceID, args.StartTime, args.EndTime)
    if err != nil {
        return runtime.Executed(&planner.ToolResult{
            Name: call.Name,
            Failure: &planner.ToolFailure{
                Kind:     planner.FailureUnavailable,
                Error:    planner.ToolErrorFromError(err),
                Recovery: planner.RecoveryDirective{Action: planner.RecoveryReplan},
            },
        }), nil
    }

    // Construye el resultado acotado orientado al modelo
    result := &specs.GetTimeSeriesResult{
        Summary:  fmt.Sprintf("Retrieved %d data points from %s to %s", len(fullData.Points), args.StartTime, args.EndTime),
        Count:    len(fullData.Points),
        MinValue: fullData.Min,
        MaxValue: fullData.Max,
    }

    // Construye server-data de fidelidad completa para las UIs
    // Los codecs de server-data generados se nombran a partir de la herramienta y el kind, por ejemplo:
    // specs.GetTimeSeriesMetricsTimeSeriesServerDataCodec.ToJSON(...)
    serverData, err := buildCanonicalServerData("metrics.time_series", fullData)
    if err != nil {
        return nil, err
    }

    return runtime.Executed(&planner.ToolResult{
        Name:   call.Name,
        Result: result,
        ServerData: serverData,
    }), nil
}
```

Las herramientas respaldadas por métodos también pueden adjuntar server-data a través de providers generados y
materializadores de resultados. Un materializador es determinista y se ejecuta tanto en la ejecución normal
como en las rutas de espera con resultado proporcionado externamente:

```go
reg := runtime.ToolsetRegistration{
    Name:  "orchestrator.metrics",
    Specs: []tools.ToolSpec{specs.SpecGetTimeSeries()},
    ResultMaterializer: func(ctx context.Context, meta runtime.ToolCallMeta, call *runtime.ToolCall, result *planner.ToolResult) error {
        if len(result.ServerData) != 0 {
            return nil
        }
        result.ServerData = buildServerData(call, result)
        return nil
    },
}
```

**En suscriptores de stream o handlers de UI**, lee `ServerData` de los eventos de fin de herramienta
o de los logs del run y decodifícalo con los codecs generados para los kinds declarados:

```go
func handleToolEnd(event stream.ToolEnd) {
    if len(event.Data.ServerData) == 0 {
        return
    }
    data, err := decodeTimeSeriesServerData(event.Data.ServerData)
    if err != nil {
        log.Printf("invalid server-data: %v", err)
        return
    }
    renderTimeSeriesChart(data.DataPoints)
}
```

#### Cuándo usar ServerData

Utiliza server-data cuando:
- Los resultados de la herramienta incluyan datos demasiado grandes para el contexto del modelo (series temporales, logs, tablas grandes)
- Las UIs necesiten datos estructurados para visualización (gráficos, grafos, mapas)
- Quieras separar aquello sobre lo que razona el modelo de lo que ven los usuarios
- Los sistemas posteriores necesiten datos de fidelidad completa mientras el modelo trabaja con resúmenes

Evita server-data cuando:
- El resultado completo cabe cómodamente en el contexto del modelo
- No haya ninguna UI ni consumidor posterior que necesite los datos completos
- El resultado acotado ya contenga todo lo necesario

---

## Buenas prácticas

- **Pon las validaciones en el diseño, no en los planificadores** – Usa el DSL de atributos de Goa (`Required`, `MinLength`, `Enum`, etc.)
- **Devuelve `ToolFailure` desde los ejecutores** – Conserva la causa del error
  y elige la acción exacta de recuperación en lugar de devolver un error simple
  o provocar un panic
- **Mantén exacta la evidencia de corrección** – Usa problemas de campo
  generados, la entrada anterior canónica y un ejemplo JSON conforme al esquema
- **Enseña a los planificadores a leer los fallos** – Haz del manejo de
  `ToolOutput.Failure` una parte principal del planificador
- **Evita la revalidación dentro de los servicios** – Goa-AI asume que la validación ocurre en el límite de la herramienta

---

## Próximos pasos

- **[Composición de agentes](./agent-composition.md)** - Construye sistemas complejos con patrones agent-as-tool
- **[Integración MCP](./mcp-integration.md)** - Conecta con servidores de herramientas externos
- **[Runtime](./runtime.md)** - Comprende el flujo de ejecución de herramientas


Consulta [Búsqueda de herramientas y catálogos dinámicos](../tool-search/) para la resolución actual, contratos generados, proveedores y migración.

