# DSL Reference

Complete reference for Goa-AI's DSL functions - agents, toolsets, policies, and MCP integration.

Source: https://goa.design/docs/2-goa-ai/dsl-reference/

Relative links resolve against the source URL above.


This document provides a complete reference for Goa-AI's DSL functions. Use it alongside the [Runtime](./runtime.md) guide to understand how designs translate into runtime behavior.

## DSL Quick Reference


| Function                                                | Context                  | Description                                                                                                        |
| ------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| **Agent Functions**                                     |                          |                                                                                                                    |
| `Agent`                                                 | Service                  | Defines an LLM-based agent                                                                                         |
| `Completion`                                            | Service                  | Declares a service-owned typed direct assistant-output contract                                                    |
| `Use`                                                   | Agent                    | Declares toolset consumption                                                                                       |
| `Export`                                                | Agent, Service           | Exposes toolsets to other agents                                                                                   |
| `AgentToolset`                                          | Use argument             | References toolset from another agent                                                                              |
| `Passthrough`                                           | Tool (in Export)         | Deterministic forwarding to service method                                                                         |
| `DisableAgentDocs`                                      | API                      | Disables AGENTS_QUICKSTART.md generation                                                                           |
| **Toolset Functions**                                   |                          |                                                                                                                    |
| `Toolset`                                               | Top-level                | Declares a provider-owned toolset                                                                                  |
| `FromMCP`                                               | Toolset argument         | Configures MCP-backed toolset                                                                                      |
| `FromRegistry`                                          | Toolset argument         | Configures registry-backed toolset                                                                                 |
| `Description`                                           | Toolset                  | Sets toolset description                                                                                           |
| **Tool Functions**                                      |                          |                                                                                                                    |
| `Tool`                                                  | Toolset, Method          | Defines a callable tool                                                                                            |
| `Args`                                                  | Tool                     | Defines input parameter schema                                                                                     |
| `Return`                                                | Tool, Completion         | Defines the model-visible result schema                                                                            |
| `ServerData`                                            | Tool                     | Defines server-only data schema (never sent to model providers)                                                    |
| `FromMethodResultField`                                 | ServerData               | Projects server-data from a bound service method result field                                                     |
| `AudienceTimeline`                                      | ServerData               | Marks server-data as timeline/UI eligible (default)                                                               |
| `AudienceInternal`                                      | ServerData               | Marks server-data as an internal composition attachment                                                           |
| `AudienceEvidence`                                      | ServerData               | Marks server-data as provenance or audit evidence                                                                 |
| `BoundedResult`                                         | Tool                     | Declares a runtime-owned bounded-result contract; optional sub-DSL can declare paging cursor fields                |
| `Cursor`                                                | BoundedResult            | Declares which payload field carries an opaque next-page cursor (optional)                                         |
| `ContinueWith`                                          | BoundedResult            | Delegates mechanical pagination to a sibling continuation action whose cursor is bound by the runtime              |
| `NextCursor`                                            | BoundedResult            | Declares the projected result field name for the next-page cursor (optional)                                       |
| `Tags`                                                  | Tool, Toolset            | Attaches metadata labels                                                                                           |
| `Meta`                                                  | Tool                     | Attaches inert, named design metadata emitted in `ToolSpec.Meta`                                                   |
| `BindTo`                                                | Tool                     | Binds tool to service method                                                                                       |
| `Inject`                                                | Tool                     | Marks fields as runtime-injected                                                                                   |
| `CallHintTemplate`                                      | Tool                     | Display template for invocations                                                                                   |
| `ResultHintTemplate`                                    | Tool                     | Display template for results                                                                                       |
| `ResultReminder`                                        | Tool                     | Static system reminder after tool result                                                                           |
| `Confirmation`                                          | Tool                     | Requires explicit out-of-band confirmation before execution                                                        |
| `TerminalRun`                                           | Tool                     | Marks the tool terminal and bookkeeping: successful execution completes the run without a follow-up planner turn |
| `Bookkeeping`                                           | Tool                     | Marks a control record: no retrieval or consecutive-failure budget, while the exact call/result remains durable |
| **Policy Functions**                                    |                          |                                                                                                                    |
| `RunPolicy`                                             | Agent                    | Configures execution constraints                                                                                   |
| `DefaultCaps`                                           | RunPolicy                | Sets resource limits                                                                                               |
| `MaxToolCalls`                                          | DefaultCaps              | Maximum tool invocations                                                                                           |
| `MaxRecoveryTurns`                                     | DefaultCaps              | Maximum replacement planner calls after rejected output                                                            |
| `TimeBudget`                                            | RunPolicy                | Simple wall-clock limit                                                                                            |
| `Timing`                                                | RunPolicy                | Fine-grained timeout configuration                                                                                 |
| `Budget`                                                | Timing                   | Overall run budget                                                                                                 |
| `Plan`                                                  | Timing                   | Planner activity timeout                                                                                           |
| `Tools`                                                 | Timing                   | Tool activity timeout                                                                                              |
| `History`                                               | RunPolicy                | Conversation history management                                                                                    |
| `KeepRecentTurns`                                       | History                  | Sliding window policy                                                                                              |
| `CompressAtTurns`                                       | History                  | Turn-count trigger for model-assisted summarization                                                                |
| `CompressAtMaxInputTokens`                              | History                  | Runtime token-count trigger for model-assisted summarization                                                       |
| `KeepMaxTurns`                                          | History                  | Exact-retention cap by newest whole turns                                                                          |
| `KeepMaxInputTokens`                                    | History                  | Exact-retention cap by runtime token count, keeping only whole turns                                                |
| `Cache`                                                 | RunPolicy                | Prompt caching configuration                                                                                       |
| `AfterSystem`                                           | Cache                    | Checkpoint after system messages                                                                                   |
| `AfterTools`                                            | Cache                    | Checkpoint after tool definitions                                                                                  |
| `InterruptsAllowed`                                     | RunPolicy                | Enable pause/resume                                                                                                |
| `OnMissingFields`                                       | RunPolicy                | Validation behavior                                                                                                |
| **MCP Functions**                                       |                          |                                                                                                                    |
| `MCP`                                                   | Service                  | Enables MCP support                                                                                                |
| `ProtocolVersion`                                       | MCP option               | Sets MCP protocol version                                                                                          |
| `Tool`                                                  | Method                   | Marks a method as an MCP tool in an MCP-enabled service                                                            |
| `Toolset(FromMCP(...))`                                 | Top-level                | Declares a Goa-backed MCP-derived toolset                                                                          |
| `Toolset("name", FromExternalMCP(...), func() { ... })` | Top-level                | Declares an external MCP toolset with inline schemas                                                               |
| `Resource`                                              | Method                   | Marks method as MCP resource                                                                                       |
| `StaticPrompt`                                          | Service                  | Adds static prompt template                                                                                        |
| **Registry Functions**                                  |                          |                                                                                                                    |
| `Registry`                                              | Top-level                | Declares a registry source                                                                                         |
| `URL`                                                   | Registry                 | Sets registry endpoint                                                                                             |
| `APIVersion`                                            | Registry                 | Sets API version                                                                                                   |
| `Timeout`                                               | Registry                 | Sets HTTP timeout                                                                                                  |
| `Retry`                                                 | Registry                 | Configures retry policy                                                                                            |
| `SyncInterval`                                          | Registry                 | Sets catalog refresh interval                                                                                      |
| `CacheTTL`                                              | Registry                 | Sets local cache duration                                                                                          |
| `Federation`                                            | Registry                 | Configures external registry imports                                                                               |
| `Include`                                               | Federation               | Glob patterns to import                                                                                            |
| `Exclude`                                               | Federation               | Glob patterns to skip                                                                                              |
| `PublishTo`                                             | Export                   | Configures registry publication                                                                                    |
| `Version`                                               | Toolset                  | Pins registry toolset version                                                                                      |
| **Schema Functions**                                    |                          |                                                                                                                    |
| `Attribute`                                             | Args, Return, ServerData | Defines schema field (general use)                                                                                 |
| `Field`                                                 | Args, Return, ServerData | Defines numbered proto field (gRPC)                                                                                |
| `Required`                                              | Schema                   | Marks fields as required                                                                                           |
| `Example`                                               | Schema                   | Attaches an explicit example; authored top-level tool payload examples become provider-native examples and structured correction evidence |

### Evaluation DSL

`goa.design/goa-ai/eval/dsl` adds evaluation suites to an application's Goa v3
design. `Suite` declares a suite at the design top level or inside `Agent`;
`Scenario` declares one case; `Input` declares the typed value passed to the
generated hook. Goa v3 `Description` and `Timeout` plus Goa-AI `Tags` complete
each case. The runner owns exact ID and tag selection, bounded concurrency,
and the four examples that verify a semantic judge before it evaluates
answers. See [Generated Evaluations](evaluations/) for the generated hook and
runtime contracts.

### Tool Payload Examples

For tool payload schemas, an authored top-level Goa `Example(...)` is the source
for provider-facing top-level examples. Codegen preserves that example in the
generated tool spec as raw JSON and parsed object input, and also emits both the
annotated schema and a schema with only the root `example` removed.

Provider adapters use those projections directly. Schema-annotation providers use
the annotated JSON Schema. Direct Anthropic and Bedrock Claude use native
`input_examples` with the schema-without-root-example projection, with Bedrock
passing the Anthropic fields through `additionalModelRequestFields` when the beta
contract requires it. Generated or synthesized examples are never promoted to
top-level provider examples.

## Prompt Management (v1 Integration Path)

Goa-AI v1 does **not** require a dedicated prompt DSL (`Prompt(...)`, `Prompts(...)`).
Prompt management is currently runtime-driven:

- Register baseline prompt specs in `runtime.PromptRegistry`.
- Configure scoped overrides with `runtime.WithPromptStore(...)`.
- Render prompts from planners using `PlannerContext.RenderPrompt(...)`.
- Attach rendered prompt provenance to model calls with `model.Request.PromptRefs`.

For agent-as-tool flows, map tool IDs to prompt IDs using runtime options such as
`runtime.WithPromptSpec(...)` on agent-tool registrations.
This is optional: when no consumer-side prompt content is configured, the runtime
uses the canonical JSON tool payload as the nested user message and provider
planners can render their own prompts with injected server-side context.

### Field vs Attribute

Both `Field` and `Attribute` define schema fields, but they serve different purposes:

`**Attribute(name, type, description, dsl)`** - General-purpose schema definition:

- Used for JSON-only schemas
- No field numbering required
- Simpler syntax for most use cases

```go
Args(func() {
    Attribute("query", String, "Search query")
    Attribute("limit", Int, "Maximum results", func() {
        Default(10)
    })
    Required("query")
})
```

`**Field(number, name, type, description, dsl)**` - Numbered fields for gRPC/protobuf:

- Required when generating gRPC services
- Field numbers must be unique and stable
- Use when your service exposes both HTTP and gRPC transports

```go
Args(func() {
    Field(1, "query", String, "Search query")
    Field(2, "limit", Int, "Maximum results", func() {
        Default(10)
    })
    Required("query")
})
```

**When to use which:**

- Use `Attribute` for agent tools that only use JSON (most common case)
- Use `Field` when your Goa service has gRPC transport and tools bind to those methods
- Mixing is allowed but not recommended within the same schema

## Overview

Goa-AI extends Goa's DSL with functions for declaring agents, toolsets, and runtime policies. The DSL is evaluated by Goa's `eval` engine, so the same rules apply as with the standard service/transport DSL: expressions must be invoked in the proper context, and attribute definitions reuse Goa's type system (`Attribute`, `Field`, validations, examples, etc.).

### Import Path

Add the agents DSL to your Goa design packages:

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

### Entry Point

Declare agents inside a regular Goa `Service` definition. The DSL augments Goa's design tree and is processed during `goa gen`.

### Outcome

Running `goa gen` produces:

- Agent packages (`gen/<service>/agents/<agent>`) with workflow definitions, planner activities, and registration helpers
- Service-owned completion packages (`gen/<service>/completions`) when the service declares `Completion(...)`
- Toolset owner packages (`gen/<service>/toolsets/<toolset>`) with typed payload/result structs, specs, codecs, and (when applicable) transforms
- Activity handlers for plan/execute/resume loops
- Registration helpers that wire the design into the runtime

A contextual `AGENTS_QUICKSTART.md` is regenerated at the module root unless
disabled via `DisableAgentDocs()`.

`goa example` is separate. It creates runnable `cmd/`, bootstrap, planner, and
example-executor files only when they do not already exist. Those files belong
to the application and are never overwritten on later runs. Generated files
under `gen/` and `AGENTS_QUICKSTART.md` continue to refresh from the design.

### Quickstart Example

```go
package design

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

var DocsToolset = Toolset("docs.search", func() {
    Tool("search", "Search indexed documentation", func() {
        Args(func() {
            Attribute("query", String, "Search phrase")
            Attribute("limit", Int, "Max results", func() { Default(5) })
            Required("query")
        })
        Return(func() {
            Attribute("documents", ArrayOf(String), "Matched snippets")
            Required("documents")
        })
        Tags("docs", "search")
    })
})

var AssistantSuite = Toolset(FromMCP("assistant", "assistant-mcp"))

var _ = Service("orchestrator", func() {
    Description("Human front door for the knowledge agent.")

    Agent("chat", "Conversational runner", func() {
        Use(DocsToolset)
        Use(AssistantSuite)
        Export("chat.tools", func() {
            Tool("summarize_status", "Produce operator-ready summaries", func() {
                Args(func() {
                    Attribute("prompt", String, "User instructions")
                    Required("prompt")
                })
                Return(func() {
                    Attribute("summary", String, "Assistant response")
                    Required("summary")
                })
                Tags("chat")
            })
        })
        RunPolicy(func() {
            DefaultCaps(
                MaxToolCalls(8),
                MaxRecoveryTurns(3),
            )
            TimeBudget("2m")
        })
    })
})
```

Running `goa gen example.com/assistant/design` produces:

- `gen/orchestrator/agents/chat`: workflow + planner activities + agent registry
- `gen/orchestrator/agents/chat/specs`: agent tool catalog (aggregated `ToolSpec`s + `tool_schemas.json`)
- `gen/orchestrator/toolsets/<toolset>`: toolset-owned types/specs/codecs/transforms for service-owned toolsets
- `gen/orchestrator/agents/chat/exports/<export>`: exported toolsets (agent-as-tool) packages
- MCP-aware registration helpers when an MCP-backed toolset is referenced via `Use`

### Typed Tool Descriptors

Each per-toolset specs package defines a typed identifier and a `<Tool>Tool()`
descriptor that pairs that identifier with generated payload/result codecs:

```go
const (
    Search tools.Ident = "orchestrator.search.search"
)

func SearchTool() tools.TypedTool[*SearchPayload, *SearchResult] {
    // Returns fresh generated specs and codecs.
}
```

Construct planner-authored calls with
`planner.NewToolRequest(SearchTool(), payload)`. Generated accessors return
fresh copies, so mutating one returned schema or example cannot change later
model requests.

### Service-Owned Typed Completions

Tools are not the only structured contract Goa-AI can own. Use
`Completion(...)` when the assistant should return a typed final answer directly
instead of issuing a tool call:

```go
var Draft = Type("Draft", func() {
    Attribute("name", String, "Task name")
    Attribute("goal", String, "Outcome-style goal")
    Required("name", "goal")
})

var _ = Service("tasks", func() {
    Completion("draft_from_transcript", "Produce a task draft directly", func() {
        Return(Draft)
    })
})
```

Completion names are part of the structured-output contract. They must be
1-64 ASCII characters, may contain letters, digits, `_`, and `-`, and must
start with a letter or digit.

`goa gen` emits a package under `gen/<service>/completions` with:

- typed result and union types
- private schemas and generated codecs
- public `Complete<Name>(ctx, client, req)` helpers
- typed `StreamComplete<Name>(ctx, client, req)` helpers
- `<Name>Example()` when the root result has an authored `Example(...)`

Unary helpers decode the accepted assistant response directly and expose the
exact provider response as `Response.ModelResponse`. Streaming helpers return
`completion.Streamer[T]`: `Recv` yields preview fragments, while `Value()`
becomes available only after clean end-of-stream and validation. There is no
public decoder for unchecked chunks.

Generated completion helpers reject tool-enabled requests and caller-supplied
`StructuredOutput`. Providers that do not implement structured output fail
explicitly with `model.ErrStructuredOutputUnsupported`. Invalid unary or
streamed output returns a non-retryable `planner.OutputContractError` without a
correction model request.
The generated schema remains the canonical service contract; model adapters may
normalize it for provider-specific constrained decoding, but they must reject
providers that cannot represent the declared contract.

### Cross-Process Inline Composition

When agent A declares it "uses" a toolset exported by agent B, Goa-AI wires composition automatically:

- The exporter (agent B) package includes generated `agenttools` helpers
- The consumer (agent A) agent registry uses those helpers when you `Use(AgentToolset("service", "agent", "toolset"))`
- The generated `Execute` function builds nested planner messages, runs the provider agent as a child workflow, and adapts the nested agent's `RunOutput` into a `planner.ToolResult`

This yields a single deterministic workflow per agent run and a linked run tree for composition.

---

## Agent Functions

### Agent

`Agent(name, description, dsl)` declares an agent inside a `Service`. It records the service-scoped agent metadata and attaches toolsets via `Use` and `Export`.

**Context**: Inside `Service`

Each agent becomes a runtime registration with:

- A workflow definition and Temporal activity handlers
- PlanStart/PlanResume activities with DSL-derived retry/timeout options
- A `Register<Agent>` helper that registers workflows, activities, and toolsets

```go
var _ = Service("orchestrator", func() {
    Agent("chat", "Conversational runner", func() {
        Use(DocsToolset)
        Export("chat.tools", func() {
            // tools defined here
        })
        RunPolicy(func() {
            DefaultCaps(MaxToolCalls(8))
            TimeBudget("2m")
        })
    })
})
```

### Use

`Use(value, dsl)` declares that an agent consumes a toolset. The toolset can be:

- A top-level `Toolset` variable
- A toolset declared with `FromMCP(...)` or `FromExternalMCP(...)`
- An inline toolset definition (string name + DSL)
- An `AgentToolset` reference for agent-as-tool composition

**Context**: Inside `Agent`

```go
Agent("chat", "Conversational runner", func() {
    // Reference a top-level toolset
    Use(DocsToolset)
    
    // Reference with subsetting
    Use(CommonTools, func() {
        Tool("notify") // consume only this tool from CommonTools
    })
    
    // Reference an MCP toolset declared at top level
    Use(AssistantSuite)
    
    // Inline agent-local toolset definition
    Use("helpers", func() {
        Tool("answer", "Answer a question", func() {
            // tool definition
        })
    })
    
    // Agent-as-tool composition
    Use(AgentToolset("service", "agent", "toolset"))
})
```

### Export

`Export(value, dsl)` declares toolsets exposed to other agents or services. Exported toolsets can be consumed by other agents via `Use(AgentToolset(...))`.

**Context**: Inside `Agent` or `Service`

```go
Agent("planner", "Planning agent", func() {
    Export("planning.tools", func() {
        Tool("create_plan", "Create a plan", func() {
            Args(func() {
                Attribute("goal", String, "Goal to plan for")
                Required("goal")
            })
            Return(func() {
                Attribute("plan", String, "Generated plan")
                Required("plan")
            })
        })
    })
})
```

### AgentToolset

`AgentToolset(service, agent, toolset)` references a toolset exported by another agent. This enables agent-as-tool composition.

**Context**: Argument to `Use`

Use `AgentToolset` when:

- You don't have an expression handle to the exported toolset
- Multiple agents export toolsets with the same name
- You want to be explicit in the design for clarity

```go
// Agent A exports tools
Agent("planner", func() {
    Export("planning.tools", func() { /* tools */ })
})

// Agent B uses Agent A's tools
Agent("orchestrator", func() {
    Use(AgentToolset("service", "planner", "planning.tools"))
})
```

### Passthrough

`Passthrough(toolName, target, methodName)` defines deterministic forwarding for an exported tool to a Goa service method. This bypasses the planner entirely.

**Context**: Inside `Tool` nested under `Export`

```go
Export("logging-tools", func() {
    Tool("log_message", "Log a message", func() {
        Args(func() {
            Attribute("message", String, "Message to log")
            Required("message")
        })
        Return(func() {
            Attribute("logged", Boolean, "Whether the message was logged")
        })
        Passthrough("log_message", "LoggingService", "LogMessage")
    })
})
```

### DisableAgentDocs

`DisableAgentDocs()` disables generation of `AGENTS_QUICKSTART.md` at the module root.

**Context**: Inside `API`

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

---

## Toolset Functions

### Toolset

`Toolset(name, dsl)` declares a provider-owned toolset at the top level. When declared at top level, the toolset becomes globally reusable; agents reference it via `Use`.

**Context**: Top-level

```go
var DocsToolset = Toolset("docs.search", func() {
    Description("Tools for searching documentation")
    Tool("search", "Search indexed documentation", func() {
        Args(func() {
            Attribute("query", String, "Search phrase")
            Required("query")
        })
        Return(func() {
            Attribute("documents", ArrayOf(String), "Matched snippets")
            Required("documents")
        })
    })
})
```

Toolsets can include a description using the standard `Description()` DSL function:

```go
var DataToolset = Toolset("data-tools", func() {
    Description("Tools for data processing and analysis")
    Tool("analyze", "Analyze dataset", func() {
        Args(func() {
            Attribute("dataset_id", String, "Dataset identifier")
            Required("dataset_id")
        })
        Return(func() {
            Attribute("insights", ArrayOf(String), "Analysis insights")
            Required("insights")
        })
    })
})
```

### Tool

`Tool(name, description, dsl)` defines a callable capability inside a toolset.

**Context**: Inside `Toolset` or `Method`

Code generation emits:

- Payload/result Go structs in `tool_specs/types.go`
- JSON codecs (`tool_specs/codecs.go`)
- JSON Schema definitions consumed by planners
- Tool registry entries with helper prompts and metadata

```go
Tool("search", "Search indexed documentation", func() {
    Title("Document Search")
    Args(func() {
        Attribute("query", String, "Search phrase")
        Attribute("limit", Int, "Max results", func() { Default(5) })
        Required("query")
    })
    Return(func() {
        Attribute("documents", ArrayOf(String), "Matched snippets")
        Required("documents")
    })
    CallHintTemplate("Searching for: {{ .Query }}")
    ResultHintTemplate("Found {{ len .Result.Documents }} documents")
    Tags("docs", "search")
})
```

### Args and Return

`Args(...)` and `Return(...)` define payload/result types using the standard Goa attribute DSL.

**Context**: Inside `Tool`

You can use:

- A function to define an inline object schema with `Attribute()` calls
- A Goa user type (Type, ResultType, etc.) to reuse existing type definitions
- A primitive type (String, Int, etc.) for simple single-value inputs/outputs

```go
Tool("search", "Search documentation", func() {
    Args(func() {
        Attribute("query", String, "Search phrase")
        Attribute("limit", Int, "Max results", func() {
            Default(5)
            Minimum(1)
            Maximum(100)
        })
        Required("query")
    })
    Return(func() {
        Attribute("documents", ArrayOf(String), "Matched snippets")
        Attribute("count", Int, "Number of results")
        Required("documents", "count")
    })
})
```

`Return` is optional for method-backed tools whose service method has no
result. Code generation then emits an empty result `TypeSpec`: no result schema
and no result codec. The executor reports successful completion with
`&planner.ToolResult{Name: call.Name}` and does not invent a JSON value.

**Reusing types:**

```go
var SearchParams = Type("SearchParams", func() {
    Attribute("query", String)
    Attribute("limit", Int)
    Required("query")
})

Tool("search", "Search documents", func() {
    Args(SearchParams)
    Return(func() {
        Attribute("results", ArrayOf(String))
    })
})
```

### ServerData

`ServerData(kind, val, args...)` defines typed server-only output emitted alongside a tool result. Server-data is never sent to model providers.
Timeline server-data is typically projected into observer-facing UI cards, charts, tables, or maps while keeping the model-facing result bounded and token-efficient. Evidence and internal audiences let downstream consumers route provenance or composition-only data without relying on kind naming conventions.

**Context**: Inside `Tool`

**Parameters:**

- `kind`: A string identifier for the server-data kind (e.g., `"metrics.time_series"`, `"control.narrative"`, `"audit.evidence"`). This allows consumers to identify and handle different server-data projections appropriately.
- `val`: The schema definition, following the same patterns as `Args` and `Return`—either a function with `Attribute()` calls, a Goa user type, or a primitive type.

**Audience routing (`Audience`*):**

Each `ServerData` entry declares an audience that downstream consumers use to route the payload without relying on kind naming conventions:

- `"timeline"`: persisted and eligible for observer-facing projection (e.g., UI/timeline cards)
- `"internal"`: tool-composition attachment; not persisted or rendered
- `"evidence"`: provenance references; persisted separately from timeline cards

Set the audience inside the `ServerData` DSL block:

```go
ServerData("metrics.time_series.chart_points", TimeSeriesServerData, func() {
    AudienceInternal()
    FromMethodResultField("chart_sidecar")
})

ServerData("audit.evidence", ArrayOf(Evidence), func() {
    AudienceEvidence()
    FromMethodResultField("evidence")
})
```

**When to use ServerData:**

- When tool results need to include full-fidelity data for UIs (charts, graphs, tables) while keeping model payloads bounded
- When you want to attach large result sets that would exceed model context limits
- When downstream consumers need structured data that the model doesn't need to see

```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")
    })
    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")
    })
    ServerData("metrics.time_series", func() {
        Attribute("data_points", ArrayOf(TimeSeriesPoint), "Full time series data")
        Attribute("metadata", MapOf(String, String), "Additional metadata")
        Required("data_points")
    })
})
```

**Using a Goa type for the server-data schema:**

```go
var TimeSeriesServerData = Type("TimeSeriesServerData", func() {
    Attribute("data_points", ArrayOf(TimeSeriesPoint), "Full time series data")
    Attribute("unit", String, "Measurement unit")
    Attribute("resolution", String, "Data resolution (e.g., '1m', '5m', '1h')")
    Required("data_points")
})

Tool("get_metrics", "Get device metrics", func() {
    Args(func() {
        Attribute("device_id", String, "Device identifier")
        Required("device_id")
    })
    Return(func() {
        Attribute("summary", String, "Metrics summary for the model")
        Attribute("point_count", Int, "Number of data points")
        Required("summary", "point_count")
    })
    ServerData("metrics.query", TimeSeriesServerData)
})
```

**Runtime access:**

At runtime, server-data emitted by tools is carried on
`planner.ToolResult.ServerData`. Decode those canonical JSON bytes with the
generated server-data codecs for the tool's declared kinds:

```go
// In a stream subscriber or result handler
func handleToolResult(result *planner.ToolResult) {
    if len(result.ServerData) > 0 {
        // Decode with the generated server-data codecs for this tool.
    }
}
```

### BoundedResult

`BoundedResult()` marks the current tool's result as a bounded view over a potentially larger data set.
It declares a runtime-owned bounds contract while keeping the authored result type semantic and
domain-specific.

**Context**: Inside `Tool`

Canonical model-visible fields:

- `returned` (required, `Int`)
- `truncated` (required, `Boolean`)
- `total` (optional, `Int`)
- `refinement_hint` (optional, `String`)
- `next_cursor` (optional, `String`) when declared via `NextCursor(...)` and
  exposed by a direct `Cursor` contract

`BoundedResult` is the single source of truth for that contract:

- codegen records it in generated `tools.ToolSpec.Bounds`
- codegen projects the canonical bounded fields into the generated JSON result schema
- generated `tools.ToolSpec.Bounds` stores model-facing JSON field names, so
  lower-camel Goa attributes such as `nextCursor` are exposed as `next_cursor`
- successful bounded executions must set `planner.ToolResult.Bounds`
- the runtime projects provider-owned bounds into encoded `tool_result` JSON, result-hint template data,
hooks, and stream events
- `ContinueWith` keeps the provider cursor out of the model contract and
  exposes a no-argument continuation action only while a single live chain
  head can continue; exact cursor lineage advances sequential pages and
  parallel source invocations remain valid. Multiple live heads make the
  no-argument continuation unavailable
- direct `Cursor` exposes the opaque provider cursor in `next_cursor`

```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), "List of devices")
        Required("devices")
    })
    BoundedResult(func() {
        ContinueWith("continue_devices", "cursor")
        NextCursor("next_cursor")
    })
})

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

The continuation tool's cursor remains required for execution but is absent
from its model-facing schema. The runtime advertises the tool only while the
single live result-chain head has another page. The model
calls it with `{}`; the runtime supplies the cursor and, when necessary, the
retained query payload. A planner result may contain at most one call for the
same dedicated continuation action; it may contain multiple calls to the source
tool.

Tool-facing return types must not declare `returned`, `total`, `truncated`,
`refinement_hint`, or `next_cursor` just so the model can see them. Keep the semantic result focused
on domain data. Method-backed tools may still use richer service method result types internally, but
the Goa-AI tool contract comes from `BoundedResult(...)`, not duplicated tool return fields. Within
those bound method results, only `returned` and `truncated` may be required; `total`,
`refinement_hint`, and `next_cursor` remain optional parts of the bounds contract.

**Service Responsibility:**

Services are responsible for:

1. Applying their own truncation logic (pagination, limits, depth caps)
2. Populating `planner.ToolResult.Bounds`
3. Setting `Bounds.NextCursor` to the provider cursor when another page exists
4. Optionally providing a `RefinementHint` when results are truncated

The runtime does not compute subsets or truncation itself. It only validates and projects the bounds
metadata that tools report.

**When to Use BoundedResult:**

- Tools that return paginated lists (devices, users, records)
- Tools that query large datasets with result limits
- Tools that apply depth or size caps to nested structures
- Any tool where the model needs to understand that results may be incomplete

**Runtime Behavior:**

```go
result := &planner.ToolResult{
    Result: &ListDevicesResult{
        Devices: devices,
    },
    Bounds: &agent.Bounds{
        Returned:       len(devices),
        Total:          ptr(total),
        Truncated:      truncated,
        NextCursor:     nextCursor,
        RefinementHint: refinementHint,
    },
}
```

`agent.Bounds.NextCursor` has type `*string`. Set it only when the generated
tool spec declares paging, `Truncated` is true, and the pointed-to cursor is
non-empty. Leave it nil for complete results and non-paged bounded tools; the
runtime rejects bounds that violate these rules.

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 model-facing JSON field names generated from `BoundedResult(...)`.
3. For `ContinueWith`, the cursor stays runtime-owned and the dedicated empty
   action is made available only for one unambiguous live chain head.
4. For direct `Cursor`, `Bounds.NextCursor` is emitted in `next_cursor` and the
   model supplies that opaque value on the next call.

Tools can include a display title using the standard `Title()` DSL function:

```go
Tool("web_search", "Search the web", func() {
    Title("Web Search")
    Args(func() { /* ... */ })
})
```

### Confirmation

`Confirmation(dsl)` declares that a tool must be explicitly approved out-of-band before it
executes. This is intended for **operator-sensitive** tools (writes, deletes, commands).

**Context**: Inside `Tool`

At generation time, Goa-AI records confirmation policy in the generated tool
spec. At runtime, the workflow ends with a `RunSuspension` whose first pending
item contains the confirmation request. The continuation workflow executes the
tool only after the caller submits an explicit approval through
`AgentClient.Continue`.

Minimal example:

```go
Tool("dangerous_write", "Write a stateful change", func() {
    Args(DangerousWriteArgs)
    Return(DangerousWriteResult)
    Confirmation(func() {
        Title("Confirm change")
        PromptTemplate(`Approve write: set {{ .key }} to {{ json .value }}`)
        DeniedResultTemplate(`{"summary":"Cancelled","key":{{ json .key }}}`)
    })
})
```

Notes:

- The runtime owns how confirmation is requested. Render the first pending item
  when its kind is `confirmation`, then pass an
  `api.PendingInputResponse{Confirmation: ...}` to `AgentClient.Continue`.
  See the Runtime guide for the expected payloads and execution flow.
- Confirmation templates (`PromptTemplate` and `DeniedResultTemplate`) are Go `text/template` strings
executed with `missingkey=error`. In addition to the standard template functions (e.g. `printf`),
Goa-AI provides:
  - `json v` → JSON encodes `v` (useful for optional pointer fields or embedding structured values).
  - `quote s` → returns a Go-escaped quoted string (like `fmt.Sprintf("%q", s)`).
- Confirmation can also be enabled dynamically at runtime via `runtime.WithToolConfirmation(...)`
(useful for environment-based policies or per-deployment overrides).

### CallHintTemplate and ResultHintTemplate

`CallHintTemplate(template)` and `ResultHintTemplate(template)` configure display templates for tool invocations and results. Templates are Go text/template strings rendered with typed Go values to produce concise hints shown during and after execution.

**Context**: Inside `Tool`

**Key Points:**

- Call templates receive the typed payload as the template root (for example, `.Query`, `.DeviceID`)
- Result templates receive an explicit wrapper where semantic fields live under `.Result` and bounded metadata lives under `.Bounds`
- Keep hints concise: ≤140 characters recommended for clean UI display
- Templates are compiled with `missingkey=error`—all referenced fields must exist
- Use Go field names, not JSON keys
- Use `{{ if .Field }}` or `{{ with .Field }}` blocks for optional fields

**Runtime contract:**

- Hook constructors do not render hints. Tool call scheduled events default to `DisplayHint==""`.
- The runtime enriches and persists a durable default call hint at publish time from the typed template when
payload decoding succeeds.
- Tool registration requires a non-empty metadata title. When typed decoding fails or no template is
registered, the runtime uses that title as the display hint. Malformed payloads still fail at the tool
boundary; the metadata title only keeps the attempted work renderable. Hints are never rendered against raw
JSON bytes.
- If a producer explicitly sets `DisplayHint` (non-empty) before publishing the hook event, the runtime treats
it as authoritative and does not overwrite it.
- For per-consumer wording changes, configure `runtime.WithHintOverrides` on the runtime. Overrides take
precedence over DSL-authored templates for streamed `tool_start` events.

**Basic Example:**

```go
Tool("search", "Search documents", func() {
    Args(func() {
        Attribute("query", String, "Search phrase")
        Attribute("limit", Int, "Maximum results", func() { Default(10) })
        Required("query")
    })
    Return(func() {
        Attribute("count", Int, "Number of results found")
        Attribute("results", ArrayOf(String), "Matching documents")
        Required("count", "results")
    })
    CallHintTemplate("Searching for: {{ .Query }}")
    ResultHintTemplate("Found {{ .Result.Count }} results")
})
```

**Typed Struct Fields:**

Call templates receive the generated Go payload struct as the template root.
Result templates receive the runtime preview wrapper, so semantic fields live
under `.Result` and bounded metadata lives under `.Bounds`. Field names follow
Go naming conventions (PascalCase), not JSON conventions (snake_case or camelCase):

```go
// DSL definition
Tool("get_device_status", "Get device status", func() {
    Args(func() {
        Attribute("device_id", String, "Device identifier")      // JSON: device_id
        Attribute("include_metrics", Boolean, "Include metrics") // JSON: include_metrics
        Required("device_id")
    })
    Return(func() {
        Attribute("device_name", String, "Device name")          // JSON: device_name
        Attribute("is_online", Boolean, "Online status")         // JSON: is_online
        Attribute("last_seen", String, "Last seen timestamp")    // JSON: last_seen
        Required("device_name", "is_online")
    })
    // Use Go field names (PascalCase), not JSON keys
    CallHintTemplate("Checking status of {{ .DeviceID }}")
    ResultHintTemplate("{{ .Result.DeviceName }}: {{ if .Result.IsOnline }}online{{ else }}offline{{ end }}")
})
```

**Handling Optional Fields:**

Use conditional blocks for optional fields to avoid template errors:

```go
Tool("list_items", "List items with optional filter", func() {
    Args(func() {
        Attribute("category", String, "Optional category filter")
        Attribute("limit", Int, "Maximum items", func() { Default(50) })
    })
    Return(func() {
        Attribute("items", ArrayOf(Item), "Matching items")
        Attribute("total", Int, "Total count")
        Attribute("truncated", Boolean, "Results were truncated")
        Required("items", "total")
    })
    CallHintTemplate("Listing items{{ with .Category }} in {{ . }}{{ end }}")
    ResultHintTemplate("{{ .Result.Total }} items{{ if .Result.Truncated }} (truncated){{ end }}")
})
```

**Built-in Template Functions:**

The runtime provides these helper functions for hint templates:


| Function   | Description                      | Example                      |
| ---------- | -------------------------------- | ---------------------------- |
| `join`     | Join string slice with separator | `{{ join .Tags ", " }}`      |
| `count`    | Count elements in a slice        | `{{ count .Results }} items` |
| `truncate` | Truncate string to N characters  | `{{ truncate .Query 20 }}`   |


**Complete Example with All Features:**

```go
Tool("analyze_data", "Analyze dataset", func() {
    Args(func() {
        Attribute("dataset_id", String, "Dataset identifier")
        Attribute("analysis_type", String, "Type of analysis", func() {
            Enum("summary", "detailed", "comparison")
        })
        Attribute("filters", ArrayOf(String), "Optional filters")
        Required("dataset_id", "analysis_type")
    })
    Return(func() {
        Attribute("insights", ArrayOf(String), "Analysis insights")
        Attribute("metrics", MapOf(String, Float64), "Computed metrics")
        Attribute("processing_time_ms", Int, "Processing time in milliseconds")
        Required("insights", "processing_time_ms")
    })
    CallHintTemplate("Analyzing {{ .DatasetID }} ({{ .AnalysisType }})")
    ResultHintTemplate("{{ count .Result.Insights }} insights in {{ .Result.ProcessingTimeMs }}ms")
})
```

### ResultReminder

`ResultReminder(text)` configures a static system reminder that is injected into the conversation after the tool result is returned. Use this to provide backstage guidance to the model about how to interpret or present the result to the user.

**Context**: Inside `Tool`

The reminder text is automatically wrapped in `<system-reminder>` tags by the runtime. Do not include the tags in the text.

**Static vs Dynamic Reminders:**

`ResultReminder` is for static, design-time reminders that apply every time the tool is called. For dynamic reminders that depend on runtime state or tool result content, use `PlannerContext.AddReminder()` in your planner implementation instead. Dynamic reminders support:

- Rate limiting (minimum turns between emissions)
- Per-run caps (maximum emissions per run)
- Runtime addition/removal based on conditions
- Priority tiers (safety vs guidance)

**Basic Example:**

```go
Tool("get_time_series", "Get time series data", func() {
    Args(func() {
        Attribute("device_id", String, "Device identifier")
        Attribute("start_time", String, "Start timestamp")
        Attribute("end_time", String, "End timestamp")
        Required("device_id", "start_time", "end_time")
    })
    Return(func() {
        Attribute("series", ArrayOf(DataPoint), "Time series data points")
        Attribute("summary", String, "Summary for the model")
        Required("series", "summary")
    })
    ResultReminder("The user sees a rendered graph of this data in the UI.")
})
```

**When to Use ResultReminder:**

- When the UI renders tool results in a special way (charts, graphs, tables) that the model should know about
- When the model should avoid repeating information that's already visible to the user
- When there's important context about how results are presented that affects how the model should respond
- When you want consistent guidance that applies to every invocation of the tool

**Multiple Tools with Reminders:**

When multiple tools in a single turn have result reminders, they are combined into a single system message:

```go
Tool("get_metrics", "Get device metrics", func() {
    Args(func() { /* ... */ })
    Return(func() { /* ... */ })
    ResultReminder("Metrics are displayed as a dashboard widget.")
})

Tool("get_alerts", "Get active alerts", func() {
    Args(func() { /* ... */ })
    Return(func() { /* ... */ })
    ResultReminder("Alerts are shown in a priority-sorted list with severity indicators.")
})
```

**Dynamic Reminders via PlannerContext:**

For reminders that depend on runtime conditions, use the planner API instead:

```go
// In your planner implementation
func (p *MyPlanner) PlanResume(ctx context.Context, input *planner.PlanResumeInput) (*planner.PlanResult, error) {
    // Add a dynamic reminder based on tool results
    for _, tr := range input.ToolOutputs {
        if tr.Name != specs.GetTimeSeries || tr.Failure != nil {
            continue
        }
        result, err := specs.UnmarshalGetTimeSeriesResult(tr.Result)
        if err != nil {
            return nil, err
        }
        if hasAnomalies(result) {
            input.Agent.AddReminder(reminder.Reminder{
                ID:   "anomaly_detected",
                Text: "Anomalies were detected in the time series. Highlight these to the user.",
                Priority: reminder.TierGuidance,
            })
        }
    }
    // ... rest of planner logic
}
```

### Tags

`Tags(values...)` annotates tools or toolsets with flat labels for generic
policy and UI filtering. Toolset tags are inherited by their tools.

**Context**: Inside `Tool` or `Toolset`

Common tag patterns:

- Domain categories: `"nlp"`, `"database"`, `"api"`, `"filesystem"`
- Capability types: `"read"`, `"write"`, `"search"`, `"transform"`
- Risk levels: `"safe"`, `"destructive"`, `"external"`

```go
Tool("delete_file", "Delete a file", func() {
    Args(func() { /* ... */ })
    Tags("filesystem", "write", "destructive")
})
```

### Meta

Goa's standard `Meta(name, values...)` DSL attaches a named design-time
annotation to the current tool. Goa-AI code generation preserves it in
`ToolSpec.Meta` as `map[string][]string`.

```go
Tool("resolve_source", "Resolve a selectable source", func() {
    Args(ResolveSourceArgs)
    Return(ResolveSourceResult)
    Meta("example.chat.supplies_tool_input")
})
```

Use `Meta` for a stable, consumer-owned contract that is not a generic policy
category. Metadata is inert: Goa-AI does not change scheduling, visibility,
budgets, retries, or terminal behavior merely because a key is present. The
planner, policy, or UI that interprets a key owns and documents its semantics.

Keep the built-in contracts canonical:

- use `Tags` for generic allow/deny and capability filtering,
- use `Bookkeeping` and `TerminalRun` for accounting and terminal behavior,
- use `ToolFailure` and its `Recovery` action for per-result failure handling,
- use planner fields such as `SynthesizeAfterTools` for per-batch transitions.

### BindTo

`BindTo("Method")` or `BindTo("Service", "Method")` associates a tool with a Goa service method.

**Context**: Inside `Tool`

When a tool is bound to a method:

- The tool's `Args` schema can differ from the method's `Payload`
- The tool's `Return` schema can differ from the method's `Result`
- Generated adapters transform between tool and method types

```go
var _ = Service("orchestrator", func() {
    Method("Search", func() {
        Payload(SearchPayload)
        Result(SearchResult)
    })
    
    Agent("chat", func() {
        Use("docs", func() {
            Tool("search", "Search documentation", func() {
                Args(SearchPayload)
                Return(SearchResult)
                BindTo("Search") // binds to method on same service
            })
        })
    })
})
```

### Inject

`Inject(fields...)` marks specific payload fields as server-populated. Injected fields are:

1. Hidden from the LLM (excluded from the JSON schema and the model-facing required list)
2. Required `String` fields on the tool's effective payload (the explicit `Args()` when given, otherwise the bound method's payload)
3. Populated by generated code, from one of two generation-time-resolved sources: `runtime.ToolCallMeta` or run labels

**Context**: Inside `Tool`

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), the field's own declared validation (`Pattern`,
`Length`, enum, ...) is applied to the label value, and callers must supply
it via `runtime.WithLabels(...)` when starting the run. 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.

```go
Tool("get_data", "Get data for current session", 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))
    })
    BindTo("data_service", "get")
    Inject("session_id") // meta-backed: hidden from LLM, set by the runtime
})

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

Codegen emits one `Inject<Tool>` function per injecting tool, in the
toolset's generated `inject.go`, which both execution topologies (local
in-process executors and the registry-served provider) call identically
between decode and execute. A toolset also carries a generated
`RequiredLabels` list; `Runtime.Start`/`StartOneShot` validate it against
`WithLabels(...)`-supplied labels before scheduling any workflow or
activity, failing fast with every missing key named in one error.

Hand-written `ToolCallExecutor`s (for tools with no `BindTo`, registered
directly with the runtime) have no generated call site. They should decode
these tools' payloads with the generated `Decode<Tool>(payload, meta,
labels) (*<Tool>Payload, error)` function instead of the raw payload
codec's `FromJSON`: `Decode<Tool>` composes the codec 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:"-"`.

`WithInterceptors` (see [Toolsets](toolsets.md#injected-fields)) is a
separate, still-supported mechanism for user-supplied hooks that run after
`Inject<Tool>` on the generated service executor's typed payload; it is
independent of which `Inject()` fields a design declares.

### TerminalRun

`TerminalRun()` marks the current tool as terminal for the run. Once the tool executes successfully, the runtime completes the run immediately after publishing the tool result without requesting a follow-up `PlanResume`/finalization turn. Goa-AI automatically classifies every terminal tool as bookkeeping, so `TerminalRun()` is the only declaration needed.

**Context**: Inside `Tool`

Use `TerminalRun()` for tools whose result is the user-facing terminal output of the run—for example, a final-report renderer or a "commit this run" tool. The tool result is the run's terminal artifact; extra model narration is neither needed nor desirable.

```go
Tool("commit_task", "Commit the terminal task artifact", func() {
    Args(TaskCompletionArgs)
    Return(TaskCompletionResult)
    TerminalRun()
})
```

**Runtime behavior:**

- Codegen records the flag on `tools.ToolSpec.TerminalRun`.
- After a successful terminal tool call, the runtime finalizes the run without calling `PlanResume`.
- Codegen also records the tool as bookkeeping. A terminal tool therefore consumes no retrieval or consecutive-failure budget and can still be admitted after the retrieval budget is exhausted.

### Bookkeeping

`Bookkeeping()` marks the current tool as a control record whose success does not independently schedule another planner turn. It consumes neither the run-level `MaxToolCalls` retrieval budget nor the consecutive-failure allowance.

**Context**: Inside `Tool`

Use `Bookkeeping()` for structured status markers, transition declarations, findings, and terminal commits. Do not use it for a snapshot or lookup result whose success must schedule later planner reasoning.

```go
Tool("set_step_status", "Update step status", func() {
    Args(SetStepStatusArgs)
    Return(SetStepStatusResult)
    Bookkeeping()
})
```

**Runtime behavior:**

- Codegen records the flag on `tools.ToolSpec.Bookkeeping`.
- Bookkeeping calls contribute zero cost to `MaxToolCalls` and do not change the consecutive-failure counter.
- Each model-authored tool-call batch is atomic. The runtime admits the whole batch when all budgeted calls fit, or rejects the whole batch when they do not. It never removes individual calls from the provider response.
- Calls and results remain durable stream/run-log events and remain in the provider transcript. Successful bookkeeping results are omitted only from compact future `ToolOutputs`.
- A failed bookkeeping result enters another planner turn according to
  `ToolFailure.Recovery`: correct the same call, replan without that tool, or
  finish.
- Unknown tools are treated as budgeted; only tools declared `Bookkeeping()` in the DSL (or marked bookkeeping on the runtime `ToolSpec`) are exempt.
- A bookkeeping-only turn must resolve in the same turn (`TerminalRun()`, `FinalResponse`, `FinalToolResult`, or await/pause).

**Terminal commits:**

A terminal-commit tool needs only `TerminalRun()` because terminal tools imply bookkeeping:

```go
Tool("commit_task", "Commit the terminal task artifact", func() {
    Args(TaskCompletionArgs)
    Return(TaskCompletionResult)
    TerminalRun()  // no retrieval cost; ends the run once it succeeds
})
```

The commit tool can be admitted with no retrieval budget remaining. Once it succeeds, the run is done without a follow-up planner turn.

## Policy Functions

### RunPolicy

`RunPolicy(dsl)` configures execution limits enforced at runtime. It's declared inside an `Agent` and contains policy settings like caps, time budgets, history management, and interruption handling.

**Context**: Inside `Agent`

**Available Policy Functions:**

- `DefaultCaps` – independent limits for tool calls and replacement planner calls
- `TimeBudget` – simple wall-clock limit for the entire run
- `Timing` – fine-grained timeouts for budget, planning, and tool activities (advanced)
- `History` – conversation history management (sliding window or compression)
- `InterruptsAllowed` – enable pause/resume for human-in-the-loop
- `OnMissingFields` – validation behavior for missing required fields

```go
Agent("chat", "Conversational runner", func() {
    RunPolicy(func() {
        DefaultCaps(
            MaxToolCalls(8),
            MaxRecoveryTurns(3),
        )
        TimeBudget("2m")
        InterruptsAllowed(true)
        OnMissingFields("await_clarification")

        History(func() {
            KeepRecentTurns(20)
        })
    })
})
```

### DefaultCaps

`DefaultCaps(opts...)` applies capability caps to prevent runaway loops and enforce execution limits.

**Context**: Inside `RunPolicy`

```go
RunPolicy(func() {
    DefaultCaps(
        MaxToolCalls(8),
        MaxRecoveryTurns(3),
    )
})
```

**MaxToolCalls(n)**: Sets the maximum number of budgeted tool invocations allowed per run. Tools declared `Bookkeeping()` are exempt from this cap and do not count toward `n`. When the budget is exhausted, the runtime stops scheduling budgeted calls and finalizes the run through the planner with termination reason `tool_cap`.

**MaxRecoveryTurns(n)**: Sets the maximum number of replacement planner calls
the runtime may schedule after rejected tool or model output. Successful
budgeted tool work starts a fresh allowance. The terminal finalization call
used after exhaustion is not a replacement attempt and does not count toward
`n`. When omitted, the runtime allows three recovery turns.

### TimeBudget

`TimeBudget(duration)` enforces a wall-clock limit on agent execution. Duration is specified as a string (e.g., `"2m"`, `"30s"`).

**Context**: Inside `RunPolicy`

```go
RunPolicy(func() {
    TimeBudget("2m") // 2 minutes
})
```

For fine-grained control over individual activity timeouts, use `Timing` instead.

### Timing

`Timing(dsl)` provides fine-grained timeout configuration as an alternative to `TimeBudget`. While `TimeBudget` sets a single overall limit, `Timing` lets you control timeouts at three levels: the overall run budget, planner activities (LLM inference), and tool execution activities.

**Context**: Inside `RunPolicy`

**When to use Timing vs TimeBudget:**

- Use `TimeBudget` for simple cases where a single wall-clock limit is sufficient
- Use `Timing` when you need different timeouts for planning vs tool execution—for example, when tools make slow external API calls but you want fast LLM responses

```go
RunPolicy(func() {
    Timing(func() {
        Budget("10m")   // overall wall-clock budget for the entire run
        Plan("45s")     // timeout for Plan/Resume activities (LLM inference)
        Tools("2m")     // default timeout for ExecuteTool activities
    })
})
```

`Timing` stays at the semantic runtime layer. `Plan(...)` and `Tools(...)`
bound how long a healthy planner or tool attempt may execute once it starts.
They do not configure workflow-engine mechanics such as queue-wait timeouts or
heartbeat liveness. If you use the Temporal adapter, configure those mechanics
with `temporal.Options.ActivityDefaults`.

**Timing Functions:**


| Function           | Description                                | Affects                                            |
| ------------------ | ------------------------------------------ | -------------------------------------------------- |
| `Budget(duration)` | Total wall-clock budget for the run        | Entire run lifecycle                               |
| `Plan(duration)`   | Timeout for Plan and Resume activities     | LLM inference calls via planner                    |
| `Tools(duration)`  | Default timeout for ExecuteTool activities | Tool execution (service calls, MCP, agent-as-tool) |


**How Timing Affects Runtime Behavior:**

The runtime translates these DSL values into engine-agnostic attempt budgets:

- `Budget` sets the semantic wall-clock budget for the run. The runtime enforces
that budget for planner/tool work and derives the engine run timeout as
`Budget + FinalizerGrace + engine headroom` so the final planner resume turn
and terminal cleanup still have room to finish.
- `Plan` becomes the attempt budget for `PlanStart` and `PlanResume`
- `Tools` becomes the default attempt budget for `ExecuteTool`

Temporal-specific queue-wait and liveness behavior are layered on separately by
the Temporal adapter.

**Complete Example:**

```go
Agent("data-processor", "Processes large datasets", func() {
    Use(DataToolset)
    RunPolicy(func() {
        DefaultCaps(MaxToolCalls(20))
        Timing(func() {
            Budget("30m")   // long-running data jobs
            Plan("1m")      // LLM decisions should be quick
            Tools("5m")     // data operations may take time
        })
    })
})
```

### Cache

`Cache(dsl)` configures prompt caching behavior for the agent. It specifies where the runtime should place cache checkpoints relative to system prompts and tool definitions for providers that support caching.

**Context**: Inside `RunPolicy`

Prompt caching can significantly reduce inference costs and latency by allowing providers to reuse previously processed content. The Cache function lets you define checkpoint boundaries that providers use to determine what content can be cached.

```go
RunPolicy(func() {
    Cache(func() {
        AfterSystem()  // checkpoint after system messages
        AfterTools()   // checkpoint after tool definitions
    })
})
```

**Cache Checkpoint Functions:**


| Function        | Description                                                                                                                                          |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AfterSystem()` | Places a cache checkpoint after all system messages. Providers interpret this as a cache boundary immediately following the system preamble.         |
| `AfterTools()`  | Places a cache checkpoint after tool definitions. Providers interpret this as a cache boundary immediately following the tool configuration section. |


**Provider Support:**

Not all providers support prompt caching, and support varies by checkpoint type:


| Provider                | AfterSystem | AfterTools |
| ----------------------- | ----------- | ---------- |
| Bedrock (Claude models) | ✓           | ✓          |
| Bedrock (Nova models)   | ✓           | ✗          |


Providers that do not support caching ignore these options. The runtime validates provider-specific constraints—for example, requesting `AfterTools` with a Nova model returns an error.

**When to Use Cache:**

- Use `AfterSystem()` when your system prompt is stable across turns and you want to avoid re-processing it
- Use `AfterTools()` when your tool definitions are stable and you want to cache the tool configuration
- Combine both for maximum caching benefit with supported providers

**Complete Example:**

```go
Agent("assistant", "Conversational assistant", func() {
    Use(DocsToolset)
    Use(SearchToolset)
    RunPolicy(func() {
        DefaultCaps(MaxToolCalls(10))
        TimeBudget("5m")
        Cache(func() {
            AfterSystem()  // cache the system prompt
            AfterTools()   // cache tool definitions (Claude only)
        })
    })
})
```

### History

`History(dsl)` defines how the runtime prepares conversation history on the
first `PrepareMessages` call in each planner activity. Decisions that do not
read messages skip this work; message-dependent decisions still prepare history
even when they do not call a model. See
[Preparing conversation messages](../runtime/#preparing-conversation-messages)
for lifetime and error handling. History policies transform the messages while
preserving:

- System prompts at the start of the conversation
- Logical turn boundaries (user + assistant + tool calls/results as atomic units)

At most one history policy may be configured per agent.

**Context**: Inside `RunPolicy`

Two standard policies are available:

**KeepRecentTurns (Sliding Window):**

`KeepRecentTurns(n)` retains only the most recent N user/assistant turns while preserving system prompts and tool exchanges. This is the simplest approach for bounding context size.

```go
RunPolicy(func() {
    History(func() {
        KeepRecentTurns(20) // Keep the last 20 user/assistant turns
    })
})
```

**Parameters:**

- `n`: Number of recent turns to keep (must be > 0)

**Compression (Model-Assisted Summarization):**

Compression summarizes older turns using a model while keeping a bounded exact tail of recent whole turns. It separates two decisions:

- `CompressAtTurns(n)` and `CompressAtMaxInputTokens(n)` decide when summarization runs. If both are set, either trigger may start compression.
- `KeepMaxTurns(n)` and `KeepMaxInputTokens(n)` decide which newest complete turns remain exact after summarization. If both are set, both retention caps apply.

Token budgets are counted at runtime through the configured history model
because tokenization is model-specific. One count includes preserved system
messages, candidate complete turns, and the currently advertised tools.
`CompressAtMaxInputTokens` is exclusive: a request exactly at the threshold
fits; a larger request triggers compression. `KeepMaxInputTokens` never
truncates a turn; the runtime walks backward from the newest turn and keeps only
complete turns that fit.

```go
RunPolicy(func() {
    History(func() {
        // Summarize when the provider-visible input grows too large.
        CompressAtMaxInputTokens(120_000)

        // Keep a bounded exact tail of newest whole turns.
        KeepMaxInputTokens(40_000)
        KeepMaxTurns(12)
    })
})
```

**Compression functions:**

- `CompressAtTurns(n)`: Trigger once at least `n` logical turns are present.
- `CompressAtMaxInputTokens(n)`: Trigger once the provider-visible transcript exceeds `n` input tokens.
- `KeepMaxTurns(n)`: Keep at most `n` newest whole turns exact.
- `KeepMaxInputTokens(n)`: Keep newest whole turns whose runtime input-token count fits `n`.

At least one `CompressAt...` trigger and at least one `KeepMax...` retention budget are required for compression.

**HistoryModel Requirement:**

When using compression, you must supply a `model.Client` via the generated
`HistoryModel` field on the agent config. The runtime uses this client with
`ModelClassSmall` to summarize older turns and, when a token budget is
configured, to count the provider-visible request. Token-budget compression
requires exact `model.TokenCounter` support. Bedrock uses Runtime `CountTokens`
where available, but returns `model.ErrTokenCountingUnsupported` for
structured-output requests and for Claude Opus 4.7, Sonnet 5, and Mythos 5,
which require AWS's separate Mantle endpoint.

```go
// Generated agent config includes HistoryModel when compression is configured.
cfg := chat.ChatAgentConfig{
    Planner:      &ChatPlanner{},
    HistoryModel: smallModelClient,
}
if err := chat.RegisterChatAgent(ctx, rt, cfg); err != nil {
    log.Fatal(err)
}
```

The DSL values are generated defaults. Deployments can override them when the selected model has a different context window or operating budget:

```go
cfg := chat.ChatAgentConfig{
    Planner:      &ChatPlanner{},
    HistoryModel: smallModelClient,
    HistoryCompression: &runtime.HistoryCompressionConfig{
        CompressAtMaxInputTokens: 180_000,
        KeepMaxInputTokens:       60_000,
        KeepMaxTurns:             16,
    },
}
```

If `HistoryModel` is not provided when compression is configured, registration will fail.

**Turn Boundary Preservation:**

Both policies preserve logical turn boundaries as atomic units. A "turn" consists of:

1. A user message
2. The assistant's response (text and/or tool calls)
3. Any tool results from that response

This ensures the model always sees complete interaction sequences, never partial turns that could confuse context.

### InterruptsAllowed

`InterruptsAllowed(bool)` signals that human-in-the-loop interruptions should be honored. When enabled, the runtime supports pause/resume operations, which are essential for clarification loops and durable await states.

**Context**: Inside `RunPolicy`

**Key Benefits:**

- Enables the agent to **pause** execution when missing required information (see `OnMissingFields`).
- Allows the planner to **await** user input via clarification tools.
- Ensures the agent state is preserved exclusively during the pause, consuming no compute resources until resumed.

```go
RunPolicy(func() {
    // Enable pause/resume capability
    InterruptsAllowed(true)
    
    // Automatically pause when required tool arguments are missing
    OnMissingFields("await_clarification")
})
```

### OnMissingFields

`OnMissingFields(action)` configures how the agent responds when tool invocation validation detects missing required fields.

**Context**: Inside `RunPolicy`

Valid values:

- `"finalize"`: Stop execution when required fields are missing
- `"await_clarification"`: Pause and wait for user to provide missing information
- `"resume"`: Continue execution despite missing fields
- `""` (empty): Let the planner decide based on context

```go
RunPolicy(func() {
    OnMissingFields("await_clarification")
})
```

### Complete Policy Example

```go
Agent("chat", "Conversational runner", func() {
    RunPolicy(func() {
        DefaultCaps(
            MaxToolCalls(8),
            MaxRecoveryTurns(3),
        )
        Timing(func() {
            Budget("5m")
            Plan("30s")
            Tools("1m")
        })
        InterruptsAllowed(true)
        OnMissingFields("await_clarification")
        History(func() {
            CompressAtMaxInputTokens(120_000)
            KeepMaxInputTokens(40_000)
            KeepMaxTurns(12)
        })
    })
})
```

---

## MCP Functions

Goa-AI provides DSL functions for declaring Model Context Protocol (MCP) servers within Goa services.

### MCP

`MCP(name, version, opts...)` enables MCP support for the current service. It configures the service to expose tools, resources, and prompts via the MCP protocol.

**Context**: Inside `Service`

```go
Service("calculator", func() {
    Description("Calculator MCP server")
    
    MCP("calc", "1.0.0")
    JSONRPC(func() {
        POST("/mcp")
    })
    
    Method("add", func() {
        Payload(func() {
            Attribute("a", Int, "First number")
            Attribute("b", Int, "Second number")
            Required("a", "b")
        })
        Result(func() {
            Attribute("sum", Int, "Result of addition")
            Required("sum")
        })
        Tool("add", "Add two numbers")
    })
})
```

### ProtocolVersion

`ProtocolVersion(version)` configures the MCP protocol version supported by the server. It returns a configuration function for use with `MCP`.

**Context**: Option argument to `MCP`

```go
Service("calculator", func() {
    // Use the default protocol supported by Goa-AI.
    MCP("calc", "1.0.0")
    JSONRPC(func() {
        POST("/mcp")
    })
})
```

### Tool (in Method Context)

Inside a `Method` in an MCP-enabled service, `Tool(name, description)` marks the current method as an MCP tool. The method's payload becomes the tool input schema and the result becomes the output schema.

**Context**: Inside `Method` (service must have MCP enabled)

```go
Method("search", func() {
    Payload(func() {
        Attribute("query", String, "Search query")
        Attribute("limit", Int, "Maximum results", func() { Default(10) })
        Required("query")
    })
    Result(func() {
        Attribute("results", ArrayOf(String), "Search results")
        Required("results")
    })
    Tool("search", "Search documents by query")
})
```

### Toolset with `FromMCP` / `FromExternalMCP`

Use `Toolset(FromMCP(service, toolset))` for Goa-defined MCP servers in the same design, or `Toolset("name", FromExternalMCP(service, toolset), func() { ... })` for external MCP servers with inline schemas.

**Context**: Top-level

**Goa-backed MCP server:**

```go
var AssistantSuite = Toolset(FromMCP("assistant", "assistant-mcp"))

var _ = Service("orchestrator", func() {
    Agent("chat", func() {
        Use(AssistantSuite)
    })
})
```

`FromMCP` must point at a Goa service in the same design that declares `MCP(...)`.
The generator lifts tool schemas from that service's MCP methods.

**External MCP server with inline schemas:**

```go
var RemoteSearch = Toolset("remote-search", FromExternalMCP("remote", "search"), func() {
    Tool("web_search", "Search the web", func() {
        Args(func() { Attribute("query", String) })
        Return(func() { Attribute("results", ArrayOf(String)) })
    })
})

Agent("helper", "", func() {
    Use(RemoteSearch)
})
```

`FromExternalMCP` requires inline `Tool(...)` declarations because the external
server's schemas do not come from the local Goa design.

### Resource

`Resource(name, uri, mimeType)` marks a method as an MCP resource provider.

**Context**: Inside `Method` (service must have MCP enabled)

```go
Method("readme", func() {
    Result(String)
    Resource("readme", "file:///docs/README.md", "text/markdown")
})
```

### StaticPrompt

`StaticPrompt(name, description, messages...)` adds a static prompt template.

**Context**: Inside `Service`

```go
Service("assistant", func() {
    MCP("assistant-mcp", "1.0")
    JSONRPC(func() {
        POST("/mcp")
    })
    
    StaticPrompt("greeting", "Friendly greeting",
        "system", "You are a helpful assistant",
        "user", "Hello!")
})
```

### Complete MCP Server Example

```go
var _ = Service("assistant", func() {
    Description("MCP server example")
    
    MCP("assistant-mcp", "1.0.0")
    JSONRPC(func() {
        POST("/mcp")
    })
    
    StaticPrompt("greeting", "Friendly greeting",
        "system", "You are a helpful assistant",
        "user", "Hello!")
    
    Method("search", func() {
        Description("Search documents")
        Payload(func() {
            Attribute("query", String, "Search query")
            Required("query")
        })
        Result(func() {
            Attribute("results", ArrayOf(String), "Search results")
            Required("results")
        })
        Tool("search", "Search documents by query")
    })
    
    Method("get_readme", func() {
        Result(String)
        Resource("readme", "file:///README.md", "text/markdown")
    })
    
})
```

---

## Registry Functions

Goa-AI provides DSL functions for declaring and consuming tool registries—centralized catalogs of MCP servers, toolsets, and agents that can be discovered and consumed by agents.

### Registry

`Registry(name, dsl)` declares a registry source for tool discovery. Registries are centralized catalogs that can be discovered and consumed by goa-ai agents.

**Context**: Top-level

Inside the DSL function, use:

- `URL`: sets the registry endpoint URL (required)
- `Description`: sets a human-readable description
- `APIVersion`: sets the registry API version (defaults to "v1")
- `Security`: references Goa security schemes for authentication
- `Timeout`: sets HTTP request timeout
- `Retry`: configures retry policy for failed requests
- `SyncInterval`: sets how often to refresh the catalog
- `CacheTTL`: sets local cache duration
- `Federation`: configures external registry import settings

```go
var CorpRegistry = Registry("corp-registry", func() {
    Description("Corporate tool registry")
    URL("https://registry.corp.internal")
    APIVersion("v1")
    Security(CorpAPIKey)
    Timeout("30s")
    Retry(3, "1s")
    SyncInterval("5m")
    CacheTTL("1h")
})
```

**Configuration Options:**


| Function                     | Description                      | Example                                 |
| ---------------------------- | -------------------------------- | --------------------------------------- |
| `URL(endpoint)`              | Registry endpoint URL (required) | `URL("https://registry.corp.internal")` |
| `APIVersion(version)`        | API version path segment         | `APIVersion("v1")`                      |
| `Timeout(duration)`          | HTTP request timeout             | `Timeout("30s")`                        |
| `Retry(maxRetries, backoff)` | Retry policy for failed requests | `Retry(3, "1s")`                        |
| `SyncInterval(duration)`     | Catalog refresh interval         | `SyncInterval("5m")`                    |
| `CacheTTL(duration)`         | Local cache duration             | `CacheTTL("1h")`                        |


### Federation

`Federation(dsl)` configures external registry import settings. Use Federation inside a Registry declaration to specify which namespaces to import from a federated source.

**Context**: Inside `Registry`

Inside the Federation DSL function, use:

- `Include`: specifies glob patterns for namespaces to import
- `Exclude`: specifies glob patterns for namespaces to skip

```go
var AnthropicRegistry = Registry("anthropic", func() {
    Description("Anthropic MCP Registry")
    URL("https://registry.anthropic.com/v1")
    Security(AnthropicOAuth)
    Federation(func() {
        Include("web-search", "code-execution", "data-*")
        Exclude("experimental/*", "deprecated/*")
    })
    SyncInterval("1h")
    CacheTTL("24h")
})
```

**Include and Exclude:**

- `Include(patterns...)`: Specifies glob patterns for namespaces to import. If no Include patterns are specified, all namespaces are included by default.
- `Exclude(patterns...)`: Specifies glob patterns for namespaces to skip. Exclude patterns are applied after Include patterns.

### FromRegistry

`FromRegistry(registry, toolset)` configures a toolset to be sourced from a registry. Use FromRegistry as a provider option when declaring a Toolset.

**Context**: Argument to `Toolset`

```go
var CorpRegistry = Registry("corp", func() {
    URL("https://registry.corp.internal")
})

// Basic usage - toolset name derived from registry toolset name
var RegistryTools = Toolset(FromRegistry(CorpRegistry, "data-tools"))

// With explicit name
var MyTools = Toolset("my-tools", FromRegistry(CorpRegistry, "data-tools"))

// With additional configuration
var ConfiguredTools = Toolset(FromRegistry(CorpRegistry, "data-tools"), func() {
    Version("1.2.3")
})
```

Registry-backed toolsets can be pinned to a specific version using the standard `Version()` DSL function:

```go
var CorpRegistry = Registry("corp", func() {
    URL("https://registry.corp.internal")
})

var PinnedTools = Toolset("stable-tools", FromRegistry(CorpRegistry, "data-tools"), func() {
    Version("1.2.3")
})
```

Consume a named `Toolset(FromRegistry(...))` with `Use`, or consume a whole `Registry` directly. Add `Deferred()` inside `Use` for native tool search. Generated agents resolve current contracts once per planning activity. Connect already-built clustered registry and Pulse clients using `rt.RegisterRegistry` before runs; `Definition()` and `NewClient(rt)` have no catalog arguments.

Providers publish generated `ToolSchemas()` including confirmation, pagination, field metadata, and server-only data. Dynamic service tools use those saved contracts; agent/control integration stays compiled. Named sources must exist and match a version pin; whole registries may be empty. Duplicate/overlapping consumption and inline or exported registry tool declarations are rejected.

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

### PublishTo

`PublishTo(registry)` configures registry publication for an exported toolset. Use PublishTo inside an Export DSL to specify which registries the toolset should be published to.

**Context**: Inside `Toolset` (when being exported)

```go
var CorpRegistry = Registry("corp", func() {
    URL("https://registry.corp.internal")
})

var LocalTools = Toolset("utils", func() {
    Tool("summarize", "Summarize text", func() {
        Args(func() { Attribute("text", String) })
        Return(func() { Attribute("summary", String) })
    })
})

Agent("data-agent", "Data processing agent", func() {
    Use(LocalTools)
    Export(LocalTools, func() {
        PublishTo(CorpRegistry)
        Tags("data", "etl")
    })
})
```

### Complete Registry Example

```go
package design

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

// Define registries
var CorpRegistry = Registry("corp-registry", func() {
    Description("Corporate tool registry")
    URL("https://registry.corp.internal")
    APIVersion("v1")
    Security(CorpAPIKey)
    Timeout("30s")
    Retry(3, "1s")
    SyncInterval("5m")
    CacheTTL("1h")
})

var AnthropicRegistry = Registry("anthropic", func() {
    Description("Anthropic MCP Registry")
    URL("https://registry.anthropic.com/v1")
    Federation(func() {
        Include("web-search", "code-execution")
        Exclude("experimental/*")
    })
    SyncInterval("1h")
    CacheTTL("24h")
})

// Consume toolsets from registries
var DataTools = Toolset(FromRegistry(CorpRegistry, "data-tools"), func() {
    Version("2.1.0")
})

var SearchTools = Toolset(FromRegistry(AnthropicRegistry, "web-search"))

// Local toolset to publish
var AnalyticsTools = Toolset("analytics", func() {
    Tool("analyze", "Analyze dataset", func() {
        Args(func() {
            Attribute("dataset_id", String, "Dataset identifier")
            Required("dataset_id")
        })
        Return(func() {
            Attribute("insights", ArrayOf(String), "Analysis insights")
            Required("insights")
        })
    })
})

var _ = Service("orchestrator", func() {
    Agent("analyst", "Data analysis agent", func() {
        Use(DataTools)
        Use(SearchTools)
        Use(AnalyticsTools)
        
        // Export and publish to registry
        Export(AnalyticsTools, func() {
            PublishTo(CorpRegistry)
            Tags("analytics", "data")
        })
        
        RunPolicy(func() {
            DefaultCaps(MaxToolCalls(10))
            TimeBudget("5m")
        })
    })
})
```

---

## Next Steps

- **[Runtime](./runtime.md)** - Understand how designs translate into runtime behavior
- **[Toolsets](./toolsets.md)** - Deep dive into toolset execution models
- **[MCP Integration](./mcp-integration.md)** - Runtime wiring for MCP servers

