# Toolsets

Learn about toolset types, execution models, validation, structured failure recovery, and tool catalogs in Goa-AI.

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

Relative links resolve against the source URL above.


Toolsets are collections of tools that agents can use. Goa-AI supports several toolset types, each with different execution models and use cases.

## Toolset Types

### Service-Owned Toolsets (Method-Backed)

Declared via `Toolset("name", func() { ... })`; tools may `BindTo` Goa service methods or be implemented by custom executors.

- Codegen emits per-toolset specs/types/codecs/transforms under `gen/<service>/toolsets/<toolset>/`
- When using the Internal Tool Registry, codegen also emits `gen/<service>/toolsets/<toolset>/provider.go` for registry-routed, service-side execution
- Agents that `Use` these toolsets import the provider specs and get typed call builders and executor factories
- Applications register executors that decode typed args (via runtime-provided codecs), optionally use transforms, call service clients, and return `ToolResult`

If you deploy the Internal Tool Registry for cross-process invocation, the owning service runs a provider loop that subscribes to `toolset:<toolsetID>:requests` and publishes results to `result:<toolUseID>`. See the [Registry docs]({{< ref "/docs/2-goa-ai/registry.md" >}}) for the provider wiring snippet.

### Agent-Implemented Toolsets (Agent-as-Tool)

Defined in an agent `Export` block, and optionally `Use`d by other agents.

- Ownership still lives with the service; the agent is the implementation
- Codegen emits provider-side export packages under `gen/<service>/agents/<agent>/exports/<export>` with `NewRegistration` and typed call builders
- Consumer-side helpers in agents that `Use` the exported toolset delegate to provider helpers while keeping routing metadata centralized
- Execution happens inline; payloads are passed as canonical JSON and decoded only at the boundary if needed for prompts

### MCP Toolsets

Declared via `Toolset(FromMCP(service, suite))` for Goa-backed MCP suites, or
`Toolset("name", FromExternalMCP(service, suite), func() { ... })` for external
MCP servers with inline tool schemas.

- Generated registration sets `DecodeInExecutor=true` so raw JSON is passed through to the MCP executor
- MCP executor decodes using its own codecs
- Generated wrappers handle JSON schemas, encoders, and HTTP or stdio transport with retries and tracing. HTTP accepts JSON and event-stream responses

### When to Use BindTo vs Inline Implementations

**Use `BindTo` when:**
- The tool should call an existing Goa service method
- You want generated transforms between tool and method types
- The service method already has the business logic you need
- You want to reuse validation and error handling from the service layer

```go
// Tool bound to existing service method
Tool("search", "Search documents", func() {
    Args(SearchPayload)
    Return(SearchResult)
    BindTo("Search")  // Calls the Search method on the same service
})
```

**Use inline implementations when:**
- The tool has custom logic not tied to a service method
- You need to orchestrate multiple service calls
- The tool is purely computational (no external calls)
- You want full control over the execution flow

```go
// Tool with custom executor implementation
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")
    })
    // No BindTo - implement in executor
})
```

For inline implementations, you write the executor logic directly:

```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)
        }
        // Custom logic: fetch multiple docs, combine, summarize
        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
}

```

### Generated Tool Schemas and Examples

Goa-AI treats the generated tool spec as the canonical model-facing contract.
For each tool payload, codegen derives JSON Schema from the Goa attribute and
precomputes the provider projections adapters need:

- the annotated schema, including authored and field-level JSON Schema examples
- the same schema with only the root `example` removed
- the raw authored top-level example JSON and parsed object example input

Only an authored top-level Goa `Example(...)` on the tool payload becomes a
provider-facing top-level tool example. Synthesized Goa examples may remain as
nested schema annotations, but they are not promoted to provider-native examples.
This prevents generated placeholder values from becoming model instructions.

Provider adapters choose the projection that matches the provider contract.
OpenAI-style tool calling can consume schema annotations directly. Direct
Anthropic and Bedrock Claude send the parsed examples as native
`input_examples` while using the schema without the root example; Bedrock carries
the Anthropic fields through `additionalModelRequestFields` when the relevant
beta contract applies.

If your application routes model requests through an inference service or proxy,
that boundary should carry the projections together as a provider-neutral
`model.ToolInputContract`. The boundary should not import generator-only
`tools.TypeSpec`, re-marshal decoded schemas, or know which provider consumes
which projection. Dropping the schema-without-root-example or parsed example
input prevents provider adapters from sending native `input_examples`, even
though the generated tool spec was correct.

The validated model client still needs an executable input validator. Build
local generated tools with `model.ToolDefinitionFromSpec`, which retains the
generated payload decoder. Use `model.AdvertisedToolInputFromSchema` for a
caller-authored schema. `model.ToolInputFromContract` reconstructs projections
after a gateway or proxy hop; it does not replace the generated decoder on the
planner-facing side.

### Bounded Tool Results

Some tools naturally return large lists, graphs, or time-series windows. You can mark these as **bounded views** so that services remain responsible for trimming while the runtime enforces and surfaces the contract.

#### The agent.Bounds Contract

The `agent.Bounds` type describes how a tool result has been bounded relative to the full underlying data set. For paged tools, providers put the opaque next-page cursor in `NextCursor`. Whether the model sees that cursor depends on the paging contract: `ContinueWith` keeps it runtime-owned, while `Cursor` exposes it as part of a self-paging tool's public interaction.

```go
type Bounds struct {
    Returned       int    // Number of items in the bounded view
    Total          *int   // Best-effort total before truncation (optional)
    Truncated      bool   // Whether any caps were applied (length, window, depth)
    NextCursor     *string // Non-empty opaque cursor for the next page (optional)
    RefinementHint string // Guidance on how to narrow the query when truncated
}
```

| Field | Description |
|-------|-------------|
| `Returned` | Count of items actually present in the result |
| `Total` | Best-effort count of total items before truncation (nil if unknown) |
| `Truncated` | True if any caps were applied (pagination, depth limits, size limits) |
| `NextCursor` | Non-empty opaque cursor for the next page; valid only for a paging-enabled tool when `Truncated` is true |
| `RefinementHint` | Human-readable guidance for narrowing the query (e.g., "Add a date filter to reduce results") |

#### Service Responsibility for Trimming

The runtime does not compute subsets or truncation itself—**services are responsible for**:

1. **Applying truncation logic**: Pagination, result limits, depth caps, time windows
2. **Populating runtime bounds metadata**: Setting `planner.ToolResult.Bounds`
3. **Providing refinement hints**: Guiding users/models on how to narrow queries when results are truncated

This design keeps truncation logic where domain knowledge lives (in services) while providing a uniform contract for the runtime, planners, and UIs to consume.

#### Declaring Bounded Tools

Use the DSL helper `BoundedResult()` inside a `Tool` definition:

```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")
})
```

The continuation tool's cursor exists in its execution contract but is removed
from the model-facing schema. The runtime advertises the action only while one
unambiguous live chain head can continue, so the model calls it with `{}` and
does not copy a cursor or repeat the original query.

#### Code Generation

When a tool is marked with `BoundedResult()`:

- The generated tool spec includes `tools.ToolSpec.Bounds`
- The generated JSON result schema includes the canonical bounded fields (`returned`, `total`,
  `truncated`, `refinement_hint`, and optional `next_cursor`)
- `tools.ToolSpec.Bounds` stores the **model-facing JSON field names**. If the
  Goa DSL names a lower-camel attribute such as `NextCursor("nextCursor")`,
  codegen emits `NextCursorField: "next_cursor"` so schemas, runtime
  projection, and result-codec validation use one spelling.
- `ContinueWith` generates a dedicated continuation relationship. The source
  result does not expose `next_cursor`, and the continuation's model-facing
  payload is an empty object. Independent source invocations may run in
  parallel; the continuation is available only when one invocation can be
  selected without another argument.
- A tool declared directly with `Cursor` exposes `next_cursor` and accepts it
  on the next model-authored call.
- The semantic Go result type stays domain-specific; it does not need to duplicate those fields

For method-backed `BindTo` tools, the bound service method result still needs to
carry the canonical bounded fields so the generated executor can build
`planner.ToolResult.Bounds` before runtime projection.

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

#### Implementing Bounded Tools

Bounded tools are a hard contract: services implement truncation and populate bounds metadata on every successful code path.

**Contract:**

- `Bounds.Returned` and `Bounds.Truncated` must always be set on successful bounded tool results.
- `Bounds.Total` and `Bounds.RefinementHint` are optional and should only be set when known.
- `Bounds.NextCursor` is optional, but when present it must point to a non-empty
  string, the generated tool spec must declare paging, and `Bounds.Truncated`
  must be true. Provider code sets it to the opaque cursor for the next page.

Executors implement truncation and populate bounds metadata:

```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, nil)
    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
}
```

#### Runtime Behavior

When a bounded tool executes:

1. The runtime validates that a successful bounded tool returned `planner.ToolResult.Bounds`
2. The runtime merges those bounds into the emitted JSON using the model-facing
   JSON field names generated from `BoundedResult(...)`
3. For `ContinueWith`, the runtime exposes the empty continuation action only
   when result history has exactly one live chain head, then binds the cursor
   and retained query fields before execution. Exact cursor lineage advances
   sequential pages. Parallel source invocations remain valid; multiple live
   heads make the no-argument continuation unavailable
4. If another tool in the same parallel batch requires `finish` recovery, the
   failed tool cannot run again and no new domain work can start. Continuation
   actions remain available for successful queries that already returned a
   next-page cursor, alongside terminal bookkeeping tools that save the final
   result. The planner may fetch a page or submit the result, never both in one
   batch. Neither a page nor a rejected response reopens new operations. Without
   a live page, only terminal completion remains. See [tool failure recovery](../runtime/#finish-recovery)
5. For direct `Cursor`, the runtime projects the opaque cursor into
   `next_cursor` and the model supplies it on the next call
6. Stream subscribers and finalizers access bounds for UI display, logging, or
   policy decisions

When a truncated result has no next-page cursor, the runtime reminder asks the
model to state the view's limits. Partial results can still support useful
answers about the returned items when that scope is clear. Disclosing truncation
or fetching another still-partial page does not establish facts about items
still omitted. Provider-established full-query totals retain their full-query
scope; later, independently complete evidence supports conclusions within its
own scope. Extra pagination is not required when the available evidence already
answers the question.

```go
// In a stream subscriber
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)
        }
    }
}
```

#### When to Use BoundedResult

Use `BoundedResult()` for tools that:
- Return paginated lists (devices, users, records, logs)
- Query large datasets with result limits
- Apply depth or size caps to nested structures (graphs, trees)
- Return time-windowed data (metrics, events)

The bounded contract helps:
- **Models** understand that results may be incomplete and can request refinement
- **UIs** display truncation indicators and pagination controls
- **Policies** enforce size limits and detect runaway queries

### Injected Fields

The `Inject` DSL function marks specific payload fields as "injected"—server-side infrastructure values that are hidden from the LLM but populated by generated code before the tool executes. This is useful for session IDs, tenant/household scoping, and other runtime- or caller-provided values.

#### How Inject Works

When you mark a field with `Inject`, codegen resolves it to one of two generation-time sources:

1. **Hidden from LLM**: the field is excluded from the JSON schema and the model-facing required list sent to the model provider.
2. **Validated at design time**: the field must be a required `String` on the tool's effective payload (the explicit `Args()` when given, otherwise the bound method's payload).
3. **Meta-backed or label-backed**: a name that Goify's to one of the five fixed `runtime.ToolCallMeta` fields (`run_id`/`runId`, `session_id`/`sessionId`, `turn_id`/`turnId`, `tool_call_id`/`toolCallId`, `parent_tool_call_id`/`parentToolCallId`) is **meta-backed** and compiles to a direct meta read. Every other name is **label-backed**: it compiles to a run-label lookup (the label key is the design name verbatim), with the field's own declared validation (`Pattern`, `Length`, enum, ...) applied to the label value. A label-backed field cannot be declared on a `BindTo` tool, because the registry wire protocol used by registry-served bound tools carries no run labels.
4. **Executor population**: both execution topologies (local in-process executors and the registry-served provider) call the *same* generated `Inject<Tool>` function between decode and execute, so population never diverges by where a tool runs.

#### DSL Declaration

```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")  // meta-backed: hidden from LLM, populated at runtime
})
```

Label-backed fields work the same way but are not `runtime.ToolCallMeta` names:

```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")  // label-backed: set via WithLabels("household_id", ...)
})
```

Callers supply label values by starting the run with `runtime.WithLabels(...)`:

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

A toolset's label-backed fields also contribute to a generated
`RequiredLabels` list, aggregated per agent. `Runtime.Start`/`StartOneShot`
validate the caller-supplied labels against this list **before** scheduling
any workflow or activity, failing fast with every missing key named in one
error. This check is a no-op for a process that only holds a
`Runtime.ClientFor(route)` gateway/orchestration client (no local agent
registration); in that topology a missing label is instead caught later,
per tool call.

#### Generated Code

Generated method-backed executors call one generated `Inject<Tool>` function
per injecting tool (in the toolset's `inject.go`, beside its codecs), which
copies meta-backed fields from `runtime.ToolCallMeta` and label-backed
fields from the run's labels onto the typed payload:

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

Supported injected field names are **not** a fixed list: any name that
matches a `runtime.ToolCallMeta` field is meta-backed, and every other name
is label-backed.

#### Decoding Payloads in Custom Executors

Hand-written `ToolCallExecutor`s (for tools with no `BindTo`, registered
directly with the runtime) have no generated dispatch to call
`Inject<Tool>` for them. Decode these tools' payloads with the toolset's
generated `Decode<Tool>` function instead of the raw payload codec:

```go
p, err := specs.DecodeLookupHousehold(call.Payload, meta, call.Labels)
if err != nil {
    // handle decode or injection failure (missing/invalid label, etc.)
}
```

`Decode<Tool>` composes `<Tool>PayloadCodec.FromJSON` with `Inject<Tool>` in
one call, so injection can never be silently skipped. Decoding with the
codec alone would leave injected fields at their Go zero value with no
error, since their wire tag is `json:"-"` (hidden from the model) and there
is no "missing key" signal.

#### Runtime Population via Generated Interceptors

Generated service executors also expose typed interceptor hooks, independent
of `Inject()`. Use them to derive method payload fields from request context
or other runtime state, in addition to or instead of design-declared
injected fields:

```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{}),
)
```

Registered interceptors run after the generated `Inject<Tool>` call, on the
already-decoded typed payload.

#### When to Use Inject

Use `Inject` for fields that:
- Are required by the service but shouldn't be chosen by the LLM
- Come from runtime context (session, run/turn/tool-call IDs) or caller-supplied run labels (tenant, household, user)
- Contain sensitive values (auth tokens, API keys)
- Are infrastructure concerns (tracing IDs, correlation IDs)

---

## Execution Models

### Activity-Based Execution (Default)

Service-backed toolsets execute via Temporal activities (or equivalent in other engines):

1. The validated model client rejects schema-invalid provider calls before
   planner code receives them. Planner-authored calls use
   `planner.NewToolRequest`, which returns encoding failures directly.
2. The planner returns schema-valid `ToolCalls []planner.ToolRequest` with a
   generated tool name, canonical payload bytes, and an optional provider call
   ID.
3. The runtime validates the whole plan, assigns each execution ID, and
   schedules `ExecuteToolActivity`.
4. The activity decodes the already admitted payload. A decode failure is an
   internal invariant error, not correction evidence.
5. The activity calls the toolset registration's
   `Execute(ctx, meta, *runtime.ToolCall)` with canonical JSON and the
   runtime-assigned execution ID.
6. The activity re-encodes the result with the generated result codec.

### Inline Execution (Agent-as-Tool)

Agent-as-tool toolsets execute inline from the planner's perspective while the runtime runs the provider agent as a real child run:

1. The runtime detects `Inline=true` on the toolset registration
2. It injects the `engine.WorkflowContext` into `ctx` so the toolset's `Execute` function can start the provider agent as a child workflow with its own `RunID`
3. It calls the toolset's `Execute(ctx, meta, *runtime.ToolCall)` with canonical
   JSON payload and tool metadata (including parent `RunID` and `ToolCallID`)
4. The generated agent-tool executor builds nested agent messages (system + user) from the tool payload and runs the provider agent as a child run
5. The nested agent executes a full plan/execute/resume loop in its own run; its `RunOutput` and tool events are aggregated into a parent `planner.ToolResult` that carries the result payload, aggregated telemetry, child `ChildrenCount`, and a `RunLink` pointing at the child run
6. Stream subscribers emit both `tool_start` / `tool_end` for the parent tool call and a `child_run_linked` link event so UIs can build nested agent cards while consuming a single session stream

### Result Materializers

Toolsets may register a typed result materializer:

```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 {
        // Attach deterministic, server-only sidecars here.
        result.ServerData = buildServerData(call, result)
        return nil
    },
}
```

Contract:

- `ResultMaterializer` runs on both the **normal execution path** and the **externally provided-result await path**.
- It receives the validated `runtime.ToolCall`, including its runtime-assigned
  execution ID, plus the typed `planner.ToolResult` before the runtime encodes
  JSON for hooks, workflow boundaries, or callers.
- Use it to attach `result.ServerData` or to normalize the semantic result shape in a deterministic way.
- Keep it pure and deterministic; when it runs inside workflow code it must not perform I/O.

This is the canonical place to derive observer-only sidecars from the original tool payload and the typed result while keeping those sidecars invisible to model providers.

---

## Executor-First Model

Generated service toolsets expose registration helpers that accept
`runtime.ToolCallExecutor` implementations for the toolsets an agent uses.

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

Applications register an executor implementation for each consumed local
toolset. The executor decides how to run the tool (service client, custom
function, registry caller, etc.) and receives explicit per-call metadata via
`ToolCallMeta`.

**Executor Example:**

```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)
        }
        
        // Generated when the tool and bound method use compatible distinct types.
        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
    }
}
```

---

## Tool Call Metadata

Tool executors receive explicit per-call metadata via `ToolCallMeta` rather than fishing values from `context.Context`. This provides direct access to run-scoped identifiers for correlation, telemetry, and parent/child relationships.

### ToolCallMeta Fields

| Field | Description |
|-------|-------------|
| `RunID` | Durable workflow execution identifier of the run that owns this tool call. Stable across retries; used to correlate runtime records and telemetry. |
| `SessionID` | Logically groups related runs (e.g., a chat conversation). Services typically index memory and search attributes by session. |
| `TurnID` | Identifies the conversational turn that produced this tool call. Event streams use it to order and group events. |
| `ToolCallID` | Uniquely identifies this tool invocation. Used to correlate start/update/end events and parent/child relationships. |
| `ParentToolCallID` | Identifier of the parent tool call when this invocation is a child (e.g., a tool launched by an agent-tool). UIs and subscribers use it to reconstruct the call tree. |

### Executor Signature

All tool executors receive `ToolCallMeta` as an explicit parameter:

```go
func Execute(ctx context.Context, meta *runtime.ToolCallMeta, call *runtime.ToolCall) (*runtime.ToolExecutionResult, error) {
    // Access run context directly from meta
    log.Printf("Executing tool in run %s, session %s, turn %s", 
        meta.RunID, meta.SessionID, meta.TurnID)
    
    // Use ToolCallID for correlation
    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
}
```

### Why Explicit Metadata?

The explicit metadata pattern provides several benefits:

- **Type safety**: Compile-time guarantees that required identifiers are available
- **Testability**: Easy to construct test metadata without mocking context
- **Clarity**: No hidden dependencies on context keys or middleware ordering
- **Correlation**: Direct access to parent/child relationships for nested agent-tool calls
- **Traceability**: Complete causal chain from user input to tool execution to final response

---

## Async & Durable Execution
 
Goa-AI uses **Temporal Activities** for all service-backed tool executions. This "async-first" architecture is implicit and requires no special DSL.
 
### Implicit Async
 
When a planner decides to call a tool, the runtime does not block the OS thread. Instead:
 
1. The runtime schedules a **Temporal Activity** for the tool call.
2. The agent workflow suspends execution (saving state).
3. The activity executes (on a local worker, remote worker, or even a different cluster).
4. When the activity completes, the workflow wakes up, restores state, and resumes with the result.
 
This means **every tool call** is automatically parallelizable, durable, and long-running. You do **not** need to configure `InterruptsAllowed` for this standard async behavior.
 
### Pause & Resume (Agent-Level)
 
`InterruptsAllowed(true)` is distinct: it allows the **Agent itself** to pause and wait for an arbitrary external signal (like a user's clarification) that is *not* tied to a currently running tool activity.
 
| Feature | Implicit Async | Pause & Resume |
| :--- | :--- | :--- |
| **Scope** | Single Tool Execution | Entire Agent Workflow |
| **Trigger** | Calling any service-backed tool | Missing arguments or Planner request |
| **Policy Required** | None (Default) | `InterruptsAllowed(true)` |
| **Use Case** | Slow API, Batch Job, processing | Human-in-the-loop, Clarification |
 
Ensure you verify that your use case requires *agent-level* pausing before enabling the policy; often, standard tool async is sufficient.
 
### Non-Blocking Planners
 
From the perspective of the **planner (LLM)**, the interaction feels synchronous: the model requests a tool, "pauses", and then "sees" the result in the next turn.
 
From the perspective of the **infrastructure**, it is fully asynchronous and non-blocking. This allows a single small agent worker to manage thousands of concurrent long-running agent executions without running out of threads or memory.
 
### Survival Across Restarts
 
Because execution is durable, you can restart your entire backend—including the agent workers—while tools are mid-execution. When the systems come back up:
 
- Pending tool activities will be picked up by workers.
- Completed tools will report results to their parent workflows.
- Agents will resume exactly where they left off.
 
This capability is essential for building robust, production-grade agentic systems that operate reliably in dynamic environments.

---

## Transforms

When a tool is bound to a Goa method via `BindTo`, code generation analyzes the tool Arg/Return and the method Payload/Result. If the shapes are compatible, Goa emits type-safe transform helpers:

- `Init<Tool>MethodPayload(in <ToolPayload>) <MethodPayload>` converts the
  generated tool payload into the bound Goa method payload.
- `Init<Tool>ToolResult(in <MethodResult>) <ToolResult>` converts the bound Goa
  method result into the generated tool result.

Transforms are emitted under the toolset owner package (for example
`gen/<service>/toolsets/<toolset>/transforms.go`) and use Goa's GoTransform to
safely map fields. Each helper has one return value; generated Go type
references determine its exact pointer/value signature. If a transform isn't
emitted, write an explicit mapper in the executor.

---

## Tool Identity

Each toolset defines typed tool identifiers (`tools.Ident`) for all generated tools—including non-exported toolsets. Prefer these constants over ad-hoc strings:

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

// Use a generated constant instead of ad-hoc strings/casts
spec, _ := rt.ToolSpec(searchspecs.Search)
schemas, _ := rt.ToolSchema(searchspecs.Search)
```

For exported toolsets (agent-as-tool), Goa-AI generates export packages under `gen/<service>/agents/<agent>/exports/<export>` with:
- Typed tool IDs
- Alias payload/result types
- Codecs
- One typed `<Tool>Tool()` descriptor pairing each tool ID with its payload and
  result codecs; pass it to `planner.NewToolRequest`

---

## Tool Validation and Recovery

Goa-AI combines **Goa's design-time validations** with a **structured tool error model** to give LLM planners a powerful way to **repair invalid tool calls automatically**.

### Core Types: ToolError and ToolFailure

**ToolError** (alias to `runtime/agent/toolerrors.ToolError`):
- `Message string` – human-readable summary
- `Cause *ToolError` – optional nested cause (preserves chains across retries and agent-as-tool hops)
- Constructors: `planner.NewToolError(msg)`, `planner.NewToolErrorWithCause(msg, cause)`, `planner.ToolErrorFromError(err)`, `planner.ToolErrorf(format, args...)`

**ToolFailure** keeps failure classification separate from the next legal
planner transition:

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

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

Failure kinds include invalid calls, domain rejection, unavailability, rate
limits, timeouts, malformed results, and internal errors. Recovery has three
explicit actions:

- `RecoveryCorrectCall` keeps the failed tool available and supplies structured
  correction evidence.
- `RecoveryReplan` removes the failed tool from the next planner turn.
- `RecoveryFinish` forbids new operations while permitting terminal completion
  or advertised pages of queries already started.

`ToolResult` carries either a typed result or one structured failure:

```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
}
```

### Recovering from Admitted Tool Failures

The recommended pattern:

1. **Design tools with strong payload schemas** (Goa design)
2. **Treat executor decode failures as invariant errors** because model-emitted
   invalid payloads and planner encoding failures stop before execution
3. **Return `ToolFailure` for domain failures after admission** so a
   model-authored call may request correction when a valid payload violates a
   cross-field or business rule
4. **Teach your planner** to inspect `ToolOutput.Failure`; the runtime uses its
   `Recovery` action to decide whether the same tool remains available, is
   removed for replanning, or the run must finish

The validated model client uses generated codecs to reject unknown fields, JSON
type mismatches, and schema constraints before planner or executor code runs.
Those failures are output-contract errors, not `ToolFailure` values. The example
below instead starts with an admitted model-authored call whose decoded payload
violates `validateUpsertRule`, a domain rule that the payload schema cannot
express. When that failure requests `RecoveryCorrectCall`, the workflow derives
the prior input and example from the provider call and registered tool
specification; it ignores executor-authored `PriorInput` and `ExampleJSON`.

**Example Executor:**

```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` contains the workflow-safe form of each call:
canonical payload/result bytes plus `Failure`. For `RecoveryCorrectCall`, field
issues, prior input, and example JSON let the next planner turn correct the
call. `RecoveryReplan` removes the failed tool from that next turn.
`RecoveryFinish` follows the [finish-recovery contract](../runtime/#finish-recovery).
The runtime enforces these
transitions; planners do not infer them from error text. Only provider-authored
calls can use `RecoveryCorrectCall`. Runtime-created continuations have no
model-authored input and must replan or finish instead of exposing their
execution payload.

---

## Tool Catalogs and Schemas

Goa-AI agents generate a **single, authoritative catalog of tools** from your Goa designs. This catalog powers:
- Planner tool advertisement (which tools the model can call)
- UI discovery (tool lists, categories, schemas)
- External orchestrators (MCP, custom frontends) that need machine-readable specs

### Generated Specs and tool_schemas.json

For each agent, Goa-AI emits a **specs package** and a **JSON catalog**:

**Specs packages (`gen/<service>/agents/<agent>/specs/...`):**
- `types.go` – payload/result Go structs
- `codecs.go` – JSON codecs (encode/decode typed payloads/results, enforce
  closed-object keys, and produce structured validation issues)
- `specs.go` – `[]tools.ToolSpec` entries with canonical tool ID,
  payload/result schemas, hints, plus one typed `tools.TypedTool` descriptor
  per tool (for example `SummarizeDocTool`) pairing the tool identifier with
  its typed payload and result codecs

Generated spec accessors return fresh copies. Application mutation of one
returned schema, example, or codec wrapper cannot alter later model requests.
Use `<Tool>Tool().Payload.FromJSON(...)` for ordinary payloads and the generated
`Decode<Tool>(payload, meta, labels)` helper for tools with injected fields.
Do not retain or mutate spec internals as shared runtime state.

### No-Result Tools

A method-backed tool whose Goa service method has no result generates an empty
result `TypeSpec`: no schema and no result codec. Its executor reports success
without inventing a payload:

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

The generated typed descriptor uses an empty `tools.JSONCodec[any]` for that
result. `PlanResume` observes successful completion with empty result bytes.

**JSON catalog (`tool_schemas.json`):**

Location: `gen/<service>/agents/<agent>/specs/tool_schemas.json`

Contains one entry per tool with:
- `id` – canonical tool ID (`"<service>.<toolset>.<tool>"`)
- `service`, `toolset`, `title`, `description`, `tags`
- `payload.schema` and `result.schema` (JSON Schema)

This JSON file is ideal for feeding schemas to LLM providers, building UI forms/editors, and offline documentation tooling.

### Runtime Introspection APIs

At runtime, you do not need to read `tool_schemas.json` from disk. The runtime exposes an introspection API:

```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
```

Where `toolID` is a typed `tools.Ident` constant from a generated specs or agenttools package.

### Server Data

Some tools need to return rich observer-facing output - full time series,
topology graphs, large result sets, evidence references - that is useful for UIs
and audit systems but too heavy for model providers. Goa-AI models that
non-model output as **server-data**.

#### Model-Facing vs Server Data

The key distinction is what data flows where:

| Data Type | Sent to Model | Stored/Streamed | Purpose |
|-----------|---------------|-----------------|---------|
| **Model-facing result** | ✓ | ✓ | Bounded summary the LLM reasons about |
| **Timeline server-data** | ✗ | ✓ | Observer-facing data for UIs, timelines, charts, maps, and tables |
| **Evidence server-data** | ✗ | ✓ | Provenance references or audit evidence |
| **Internal server-data** | ✗ | Depends on consumer | Tool-composition attachments or server-only metadata |

This separation lets you:
- Keep model context windows bounded and focused
- Provide rich visualizations (charts, graphs, tables) without bloating LLM prompts
- Attach provenance and audit data that models don't need to see
- Stream large datasets to UIs while the model works with summaries

#### Declaring ServerData in DSL

Use the `ServerData(kind, schema)` function inside a `Tool` definition:

```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()
    })
})
```

The `kind` parameter (e.g., `"metrics.time_series"`) identifies the server-data kind so UIs can dispatch appropriate renderers.
The audience declares routing intent:

- `AudienceTimeline()` for observer-facing timeline/UI payloads.
- `AudienceEvidence()` for provenance or audit evidence.
- `AudienceInternal()` for server-only composition payloads.

Use `FromMethodResultField("field_name")` with `BindTo(...)` tools when the
server-data payload is projected from a field on the bound service method result.

#### Generated Specs and Helpers

In the specs packages, each `tools.ToolSpec` entry includes:
- `Payload tools.TypeSpec` – tool input schema
- `Result tools.TypeSpec` – model-facing output schema
- `ServerData []*tools.ServerDataSpec` – server-only payloads emitted alongside the result

Server-data entries include generated schemas and codecs so subscribers can
decode canonical JSON bytes without sending those bytes to model providers.

#### Runtime Usage Patterns

**In tool executors**, attach canonical server-data JSON to the tool result:

```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)
    }
    
    // Fetch full data
    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
    }
    
    // Build bounded model-facing result
    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,
    }
    
    // Build full-fidelity server-data for UIs
    // Generated server-data codecs are named from the tool and kind, for example:
    // 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
}
```

Method-backed tools can also attach server-data through generated providers and
result materializers. A materializer is deterministic and runs on both normal
execution and externally provided-result await paths:

```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
    },
}
```

**In stream subscribers or UI handlers**, read `ServerData` from tool end events
or run logs and decode it with the generated codecs for the declared kinds:

```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)
}
```

#### When to Use ServerData

Use server-data when:
- Tool results include data too large for model context (time series, logs, large tables)
- UIs need structured data for visualization (charts, graphs, maps)
- You want to separate what the model reasons about from what users see
- Downstream systems need full-fidelity data while the model works with summaries

Avoid server-data when:
- The full result fits comfortably in model context
- There's no UI or downstream consumer that needs the full data
- The bounded result already contains everything needed

---

## Best Practices

- **Put validations in the design, not in planners** – Use Goa's attribute DSL (`Required`, `MinLength`, `Enum`, etc.)
- **Return `ToolFailure` from executors** – Preserve the error cause and choose
  the exact recovery action instead of returning a plain error or panic
- **Keep correction evidence exact** – Use generated field issues, canonical
  prior input, and a schema-compliant JSON example
- **Teach planners to read failures** – Make `ToolOutput.Failure` handling a
  first-class part of the planner
- **Avoid re-validating inside services** – Goa-AI assumes validation happens at the tool boundary

---

## Next Steps

- **[Agent Composition](./agent-composition.md)** - Build complex systems with agent-as-tool patterns
- **[MCP Integration](./mcp-integration.md)** - Connect to external tool servers
- **[Runtime](./runtime.md)** - Understand tool execution flow


See [Tool search and dynamic catalogs](../tool-search/) for current source resolution, generated contracts, provider behavior, and migration.

