dsl

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 23, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package dsl provides the loom-mcp design-time DSL for declaring agents, toolsets, MCP servers, registries, and run policies. These functions augment Goa's standard service DSL and drive the loom-mcp code generators; they are not used at runtime.

Overview

The DSL enables design-first development of LLM-based agents. You declare your agent's capabilities, tools, and policies in Go code, then run `loom gen` to produce type-safe packages including:

  • Agent packages with workflow definitions and planner activities
  • Tool codecs, JSON schemas, and registry entries
  • MCP server adapters and client helpers
  • Agent-as-tool composition helpers

Import the DSL alongside Goa's standard DSL:

import (
    . "github.com/CaliLuke/loom/dsl"
    . "github.com/CaliLuke/loom-mcp/dsl"
)

Mental Model

Think of the DSL as declaring intent across three domains:

**Agents** define LLM-powered planners that orchestrate tool usage. Each agent belongs to a Goa service and declares which toolsets it consumes (Use) and exports (Export) for other agents.

**Toolsets** group related tools owned by services. Tools have typed schemas (Args/Return) and can be bound to service methods (BindTo) or implemented via custom executors. Toolsets can be sourced from local definitions, MCP servers, or remote registries.

**Policies** constrain agent behavior at runtime: caps on tool calls, time budgets, history management, and prompt caching. These policies become configuration that the runtime enforces.

DSL Structure

The DSL functions must be called in the appropriate context:

API("name", func() {})           // Top-level API definition (Goa)
DisableAgentDocs()               // Inside API - disable quickstart doc generation

var MyTools = Toolset("...", func() {...})  // Top-level toolset definition
var MCPTools = Toolset(FromMCP(...))        // MCP-backed toolset
var RegTools = Toolset(FromRegistry(...))   // Registry-backed toolset
var MyRegistry = Registry("...", func() {...})  // Registry definition

Service("name", func() {         // Goa service definition
    MCP("name", "version")       // Enable MCP for this service

    Agent("name", "desc", func() {   // Inside Service
        Use(MyTools)                 // Reference toolsets
        Export("tools", func() {...}) // Export toolsets
        RunPolicy(func() {           // Inside Agent
            DefaultCaps(...)         // Inside RunPolicy
            TimeBudget("5m")
            History(func() {...})
        })
    })

    Method("search", func() {    // Goa method definition
        Tool("search", "...")    // Mark as MCP tool (requires MCP enabled)
        Resource(...)            // Mark as MCP resource
    })
})

Key Functions by Category

Agent Functions:

  • Agent declares an LLM agent within a service
  • Use declares toolset consumption
  • Export declares toolset export for agent-as-tool
  • DisableAgentDocs opts out of AGENTS_QUICKSTART.md generation
  • Passthrough forwards exported tools to service methods

Toolset Functions:

  • Toolset defines a provider-owned tool collection
  • FromMCP configures an MCP-backed toolset provider
  • FromRegistry configures a registry-backed toolset provider
  • AgentToolset references an exported toolset by coordinates
  • Tags attaches metadata labels to tools or toolsets

Tool Functions:

  • Tool declares a callable tool
  • Args defines input parameter schema
  • Return defines output result schema
  • [Artifact] defines typed artifact data (not sent to model)
  • BindTo binds a tool to a service method
  • Inject marks fields as server-injected (hidden from LLM)
  • CallHintTemplate configures call display hint template
  • ResultHintTemplate configures result display hint template
  • BoundedResult marks result as a bounded view over larger data
  • Cursor declares which payload field carries a paging cursor (optional)
  • NextCursor declares which result field carries the next-page cursor (optional)
  • Confirmation declares that a tool must be confirmed out-of-band before execution

Policy Functions:

Timing Functions (inside RunPolicy):

  • Timing groups timing configuration
  • Budget sets total wall-clock budget
  • Plan sets planner activity timeout
  • Tools sets default tool activity timeout

History Functions (inside History):

Cache Functions (inside Cache):

  • AfterSystem places cache checkpoint after system messages
  • AfterTools places cache checkpoint after tool definitions

MCP Functions:

Registry Functions:

Generated Artifacts

For each service with agents, `loom gen` produces:

  • gen/<service>/agents/<agent>/ - Agent package with workflow and activities
  • gen/<service>/agents/<agent>/specs/ - Tool specs, codecs, and JSON schemas
  • gen/<service>/agents/<agent>/specs/tool_schemas.json - Backend-agnostic tool catalog
  • gen/<service>/agents/<agent>/agenttools/ - Helpers for exported tools
  • AGENTS_QUICKSTART.md - Contextual guide (unless disabled)

For MCP-enabled services:

  • gen/mcp_<service>/ - MCP server adapter and protocol helpers
  • gen/mcp_<service>/client/ - Generated MCP client wrappers

Best Practices

Design first: Put all agent and tool schemas in the DSL. Add examples and validations to field definitions. Let codegen own schemas and codecs.

Use strong types: Define reusable Goa types (Type, ResultType) for complex tool payloads instead of inline anonymous schemas.

Keep descriptions concise: Tool descriptions are shown to LLMs. Write clear, actionable summaries that help the model choose the right tool.

Leverage BindTo: For service-backed tools, use BindTo to get generated transforms and keep tool schemas decoupled from method signatures.

Mark bounded results: Tools returning potentially large data should use BoundedResult() so the runtime can track truncation metadata. Bounded tools keep their semantic result shape domain-specific and return canonical bounds through planner.ToolResult.Bounds.

For complete documentation and examples, see docs/dsl.md in the repository.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func APIVersion

func APIVersion(version string)

APIVersion sets the registry API version. This is used as a path segment in registry API calls (e.g., "v1", "2024-11-05").

APIVersion must appear in a Registry expression.

If not specified, defaults to "v1".

Example:

Registry("corp", func() {
    URL("https://registry.corp.internal")
    APIVersion("v1")
})

func AfterSystem

func AfterSystem()

AfterSystem configures the cache policy to place a checkpoint after all system messages. Providers that support prompt caching interpret this as a cache boundary immediately following the system preamble.

AfterSystem must appear inside a Cache expression.

func AfterTools

func AfterTools()

AfterTools configures the cache policy to place a checkpoint after tool definitions. Providers that support tool-level cache checkpoints interpret this as a cache boundary immediately following the tool configuration section.

AfterTools must appear inside a Cache expression.

func Agent

func Agent(name, description string, dsl func()) *expragents.AgentExpr

Agent defines an LLM-based agent associated with the current service. A service may provide one or more agents. An agent consists of a system prompt, optional toolset dependencies, and a run policy. Agents can export toolsets for consumption by other agents or services.

Agent must appear in a Service expression.

Agent takes three arguments: - name: the name of the agent - description: a description of the agent - dsl: a function that defines the agent's system prompt, tools, and run policy

The dsl function can use the following helpers: - Use / Export: declare the toolsets the agent consumes or exposes. - RunPolicy: defines the run policy for the agent.

Example:

var DataIngestToolset = Toolset("data-ingest", func() {
	Tool("csv-uploader", func() {
		Input(func() { /* CSV file fields */ })
		Output(func() { /* Validation result */ })
	})
})

Agent("docs-agent", "Agent for managing documentation workflows", func() {
	Use("summarization-tools", func() {
		Tool("document-summarizer", "Summarize documents", func() {
			Args(func() { /* Document fields */ })
			Return(func() { /* Summary fields */ })
		})
	})
	Use(DataIngestToolset)
	Export("text-processing-suite", func() {
		Tool("doc-abstractor", "Create document abstracts", func() {
			Args(func() { /* Document fields */ })
			Return(func() { /* Summary fields */ })
		})
	})
	RunPolicy(func() {
		DefaultCaps(MaxToolCalls(5), MaxConsecutiveFailedToolCalls(2))
		TimeBudget("30s")
	})
})

func AgentToolset

func AgentToolset(service, agent, toolset string) *agentsexpr.ToolsetExpr

AgentToolset references a toolset exported by another agent identified by service and agent names. Use inside an Agent's Uses block to explicitly depend on an exported toolset when inference is not possible or ambiguous.

When to use AgentToolset vs Toolset:

  • Prefer Toolset(X) when you already have an expression handle (e.g., a top-level Toolset variable or an agent's exported Toolset). loom-mcp will infer a RemoteAgent provider automatically when exactly one agent in a different service Exports a toolset with the same name.
  • Use AgentToolset(service, agent, toolset) when you:
  • Do not have an expression handle to the exported toolset, or
  • Have ambiguity (multiple agents export a toolset with the same name), or
  • Want to be explicit in the design for clarity.

AgentToolset(service, agent, toolset)

  • service: Goa service name that owns the exporting agent
  • agent: Agent name in that service
  • toolset: Exported toolset name in that agent

The referenced toolset is resolved from the design, and a local reference is recorded with its Origin set to the defining toolset. Provider information is inferred during validation and will classify this as a RemoteAgent provider when the owner service differs from the consumer service.

func Args

func Args(val any, args ...any)

Args defines the input parameter schema for a tool. Use Args inside a Tool DSL to specify what arguments the tool accepts when invoked by an agent or LLM.

Args follows the same patterns as Goa's Payload function for methods. It accepts:

  • 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

When using a function to define the schema inline, you can use:

  • Attribute(name, type, description) to define each parameter
  • Required(...) to mark parameters as required
  • All Goa attribute DSL functions (MinLength, Maximum, Pattern, etc.)

Example (inline schema):

Tool("search", "Search documents", func() {
    Args(func() {
        Attribute("query", String, "Search query text")
        Attribute("limit", Int, "Maximum number of results")
        Attribute("filters", MapOf(String, String), "Search filters")
        Required("query")
    })
    Return(func() { ... })
})

Example (reuse existing type):

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

Tool("search", "Search documents", func() {
    Args(SearchParams)
    Return(func() { ... })
})

Example (primitive type for simple tools):

Tool("echo", "Echo a message", func() {
    Args(String)  // Single string parameter
    Return(String)
})

If Args is not called, the tool accepts no parameters (empty/null payload).

func Audience

func Audience(value ServerDataAudience)

Audience declares who this server-data payload is intended for.

Audience is used by downstream consumers (timeline projection, UI renderers, persistence sinks) to route server-data without relying on kind naming conventions.

Allowed values are:

  • "timeline": persisted and eligible for UI rendering and transcript export
  • "internal": tool-composition attachment; not persisted or rendered
  • "evidence": provenance references; persisted separately from timeline cards

func AudienceEvidence

func AudienceEvidence()

AudienceEvidence declares that the current server-data payload carries provenance references and should be persisted separately from timeline cards.

func AudienceInternal

func AudienceInternal()

AudienceInternal declares that the current server-data payload is intended only for downstream tool composition.

func AudienceTimeline

func AudienceTimeline()

AudienceTimeline declares that the current server-data payload is intended for persistence and UI rendering in the session timeline.

func BindTo

func BindTo(args ...string)

BindTo associates a tool with a Goa service method implementation. Use BindTo inside a Tool DSL to specify which service method executes the tool when invoked. This enables tools to reuse existing service logic and separates tool schema definitions from implementation.

BindTo accepts one or two arguments:

  • BindTo(methodName) - binds to a method in the agent's service
  • BindTo(serviceName, methodName) - binds to a method in a different service

The method name should match the Goa method name (case-insensitive). When using two arguments, the service name should match the Goa service name.

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 will transform between tool and method types
  • Method payload/result validation still applies

Example (bind to method in same service):

Service("docs", func() {
	Method("search_documents", func() {
		Payload(func() { ... })
		Result(func() { ... })
	})
	Agent("assistant", "Helper", func() {
		Use("doc-tools", func() {
			Tool("search", "Search documents", func() {
				Args(func() { ... })  // Can differ from method payload
				Return(func() { ... }) // Can differ from method result
				BindTo("search_documents")
			})
		})
	})
})

Example (bind to method in different service):

Tool("notify", "Send notification", func() {
    Args(func() {
        Attribute("message", String)
    })
    BindTo("notifications", "send")  // notifications.send method
})

BindTo is optional. Tools without BindTo are external tools that must be implemented through custom executors or MCP callers.

func BoundedResult

func BoundedResult(fns ...func())

BoundedResult marks the current tool's result as a bounded view over a potentially larger data set. Bounded tools must return truncation metadata in planner.ToolResult.Bounds so runtimes can guide planners and users when results are partial. The semantic result shape remains fully domain-specific; BoundedResult does not add or validate top-level result fields. Instead it declares an explicit out-of-band bounds contract that codegen, runtimes, and method-backed executors can honor consistently.

BoundedResult may optionally include a DSL function to configure boundedness details. For example, cursor-based pagination fields can be declared via Cursor and NextCursor inside the optional DSL block:

Tool("search", "Search docs", func() {
    Args(SearchArgs)
    Return(SearchResult)
    BoundedResult(func() {
        Cursor("cursor")
        NextCursor("next_cursor")
    })
})

Cursor-based pagination contract:

  • Cursor values are opaque.
  • When paging, callers must keep all other parameters unchanged and only set the payload cursor field to the value returned in bounds as the next-page cursor.

BoundedResult must appear in a Tool expression.

func Budget

func Budget(duration string)

Budget sets the total wall-clock budget for an agent run. The runtime will cancel the run if execution exceeds this duration. Budget is equivalent to TimeBudget and can be used either inside Timing or directly inside RunPolicy.

Budget must appear inside a RunPolicy or Timing expression.

Budget takes a single argument which is a Go duration string. Valid formats include combinations of hours, minutes, seconds, and milliseconds:

  • "30s" — 30 seconds
  • "5m" — 5 minutes
  • "2h30m" — 2 hours 30 minutes
  • "1m30s" — 1 minute 30 seconds

Choose budgets that match your downstream SLAs. Consider network latency, model response times, and the complexity of tool operations.

Example:

RunPolicy(func() {
    Timing(func() {
        Budget("10m")
    })
})

func Cache

func Cache(fn func())

Cache defines the prompt caching policy for the current agent. It configures where the runtime should place cache checkpoints relative to system prompts and tool definitions for providers that support caching.

Cache must appear inside a RunPolicy expression.

Example:

RunPolicy(func() {
    Cache(func() {
        AfterSystem()
        AfterTools()
    })
})

func CacheTTL

func CacheTTL(duration string)

CacheTTL sets the local cache duration for registry data.

CacheTTL must appear in a Registry expression.

CacheTTL takes a duration string (e.g., "1h", "24h", "30m").

Example:

Registry("corp", func() {
    URL("https://registry.corp.internal")
    CacheTTL("1h")
})

func CallHintTemplate

func CallHintTemplate(s string)

CallHintTemplate configures a display template for tool invocations. The template is rendered with the tool's payload to produce a concise hint shown during execution. Templates are compiled with missingkey=error.

CallHintTemplate must appear in a Tool expression.

CallHintTemplate takes a single string argument which is the Go template text. Keep templates concise (≤ 140 characters recommended).

Example:

Tool("search", "Search documents", func() {
    Args(func() {
        Attribute("query", String)
        Attribute("limit", Int)
    })
    CallHintTemplate("Searching for: {{ .Query }} (limit: {{ .Limit }})")
})

func Compress

func Compress(triggerAt, keepRecent int)

Compress configures a history policy that summarizes older turns once a trigger threshold is reached, retaining the most recent keepRecent turns in full fidelity. The runtime uses the configured thresholds to construct a compression policy; applications supply the model client via generated configuration when Compress is enabled.

Compress must appear inside a History expression. At most one of KeepRecentTurns or Compress may be configured.

Example:

RunPolicy(func() {
    History(func() {
        Compress(30, 10) // trigger at 30 turns, keep 10 recent
    })
})

func Confirmation

func Confirmation(dsl func())

Confirmation declares that the current tool always requires explicit out-of-band operator confirmation before execution.

Confirmation must appear inside a Tool DSL in a Toolset.

The runtime enforces confirmation using the ask_question-style protocol: a confirmation tool is invoked out-of-band via AwaitExternalTools, and the tool is executed only after the user approves it.

func Cursor

func Cursor(field string)

Cursor declares which optional String field on the tool payload carries the cursor for cursor-based pagination. Cursor must be used inside BoundedResult.

func DefaultCaps

func DefaultCaps(opts ...CapsOption)

DefaultCaps configures resource limits for agent execution. Use DefaultCaps to control how many tools the agent can invoke and how many consecutive failures are tolerated before stopping execution.

DefaultCaps must appear in a RunPolicy expression.

DefaultCaps takes zero or more CapsOption arguments (created via MaxToolCalls and MaxConsecutiveFailedToolCalls).

Example:

RunPolicy(func() {
    DefaultCaps(
        MaxToolCalls(20),
        MaxConsecutiveFailedToolCalls(3),
    )
})

func DeniedResultTemplate

func DeniedResultTemplate(tmpl string)

DeniedResultTemplate sets the JSON template used to construct a schema-compliant tool result when the user denies confirmation. The template is executed with the tool payload value and must render valid JSON.

func DisableAgentDocs

func DisableAgentDocs()

DisableAgentDocs disables generation of the AGENTS_QUICKSTART.md quickstart guide.

Call DisableAgentDocs() inside your API design to opt-out of generating the contextual agent quickstart README at the module root. This affects only the documentation artifact; all other generated code is unaffected.

Example:

var _ = API("assistant", func() {
    // ...
    DisableAgentDocs()
})

func DynamicPrompt

func DynamicPrompt(name, description string)

DynamicPrompt marks the current method as a dynamic prompt generator. The method's payload defines parameters that customize the generated prompt, and the result contains the generated message sequence.

DynamicPrompt must appear in a Method expression within a service that has MCP enabled.

DynamicPrompt takes two arguments:

  • name: the prompt identifier
  • description: human-readable prompt description

Example:

Method("code_review", func() {
    Payload(func() {
        Attribute("language", String)
        Attribute("code", String)
    })
    Result(ArrayOf(Message))
    DynamicPrompt("code_review", "Generate code review prompt")
})

func Exclude

func Exclude(patterns ...string)

Exclude specifies glob patterns for namespaces to skip from a federated registry source. Exclude patterns are applied after Include patterns.

Exclude must appear in a Federation expression.

Exclude takes a variadic list of glob patterns.

Example:

Federation(func() {
    Include("*")
    Exclude("experimental/*", "deprecated/*")
})

func Export

func Export(value any, fn ...func()) *expragents.ToolsetExpr

Export declares that the current agent or service exports the specified toolset for other agents to consume. Providers typically declare reusable toolsets at the top level via Toolset or MCPToolset, then reference them from agents or services with Export.

func Federation

func Federation(fn func())

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

Federation must appear in a Registry expression.

Inside the Federation DSL function, use:

  • Include: specifies glob patterns for namespaces to import
  • Exclude: specifies glob patterns for namespaces to skip

Example:

Registry("anthropic", func() {
    URL("https://registry.anthropic.com/v1")
    Federation(func() {
        Include("web-search", "code-execution")
        Exclude("experimental/*")
    })
})

func FromMCP

func FromMCP(service, toolset string) *agentsexpr.ProviderExpr

FromMCP configures a toolset to be backed by an MCP server. Use FromMCP as a provider option when declaring a Toolset.

FromMCP takes:

  • service: Goa service name that owns the MCP server
  • toolset: MCP server name (also used as the toolset name if not specified)

Example:

var MCPTools = Toolset(FromMCP("assistant-service", "assistant-mcp"))

Or with an explicit name:

var MCPTools = Toolset("my-tools", FromMCP("assistant-service", "assistant-mcp"))

func FromMethodResultField

func FromMethodResultField(name string)

FromMethodResultField declares that the server-data payload is sourced from the bound method result field.

func FromRegistry

func FromRegistry(registry *agentsexpr.RegistryExpr, toolset string) *agentsexpr.ProviderExpr

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

FromRegistry takes:

  • registry: the RegistryExpr returned by Registry()
  • toolset: name of the toolset in the registry (also used as the toolset name if not specified)

Example:

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

var RegistryTools = Toolset(FromRegistry(CorpRegistry, "data-tools"))

Or with an explicit name:

var RegistryTools = Toolset("my-tools", FromRegistry(CorpRegistry, "data-tools"))

For version pinning, use the Version DSL inside the Toolset:

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

func History

func History(fn func())

History defines how the agent runtime manages conversation history before each planner invocation. It can either:

  • KeepRecentTurns(N) to retain only the last N turns, or
  • Compress(triggerAt, keepRecent) to summarize older turns once the trigger threshold is reached.

At most one history policy may be configured per agent.

History must appear inside a RunPolicy expression.

Example:

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

func Idempotent

func Idempotent()

Idempotent marks a tool as idempotent within a run transcript.

When set, orchestration layers may treat repeated tool calls with identical arguments as redundant and avoid executing them once a successful result already exists in the transcript.

Use Idempotent only for tools whose result is a pure function of their arguments *for the lifetime of a run transcript*. If a tool answers questions about changing state (for example, "current mode") but does not accept a time or version parameter, it is not transcript-idempotent and should not be tagged.

Default: tools are not idempotent across a transcript unless explicitly tagged.

func Include

func Include(patterns ...string)

Include specifies glob patterns for namespaces to import from a federated registry source. If no Include patterns are specified, all namespaces are included by default.

Include must appear in a Federation expression.

Include takes a variadic list of glob patterns.

Example:

Federation(func() {
    Include("web-search", "code-execution", "data-*")
})

func Inject

func Inject(names ...string)

Inject marks specific payload fields as "injected" (server-side infrastructure). Injected fields are:

  1. Hidden from the LLM (excluded from the JSON schema).
  2. Exposed in the generated struct with a Setter method.
  3. Intended to be populated by runtime hooks (ToolInterceptor).

Example:

Tool("get_data", func() {
    BindTo("data_service", "get")
    // "session_id" is required by the service but hidden from the LLM
    Inject("session_id")
})

func InterruptsAllowed

func InterruptsAllowed(allowed bool)

InterruptsAllowed configures whether user interruptions are permitted during agent execution. When enabled, users can interrupt running agents to provide guidance or stop execution.

InterruptsAllowed must appear in a RunPolicy expression.

InterruptsAllowed takes a single boolean argument.

Example:

RunPolicy(func() {
    InterruptsAllowed(true)
})

func KeepRecentTurns

func KeepRecentTurns(n int)

KeepRecentTurns configures a history policy that retains only the most recent N user/assistant turns while preserving system prompts and tool exchanges.

KeepRecentTurns must appear inside a History expression.

Example:

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

func MCP

func MCP(name, version string, opts ...func(*exprmcp.MCPExpr))

MCP enables Model Context Protocol (MCP) support for the current service. It configures the service to expose tools, resources, and prompts via the MCP protocol. Once enabled, use Resource, Tool (in Method context), and related DSL functions within service methods to define MCP capabilities.

MCP must appear in a Service expression.

MCP takes two required arguments and an optional list of configuration functions:

  • name: the MCP server name (used in MCP handshake)
  • version: the server version string
  • opts: optional configuration functions (e.g., ProtocolVersion)

Example:

Service("calculator", func() {
    MCP("calc", "1.0.0", ProtocolVersion("2025-06-18"))
    Method("add", func() {
        Payload(func() {
            Attribute("a", Int)
            Attribute("b", Int)
        })
        Result(func() {
            Attribute("sum", Int)
        })
        Tool("add", "Add two numbers")
    })
})

func NextCursor

func NextCursor(field string)

NextCursor declares the canonical field name for the next-page cursor in the bounded paging contract. Providers return the actual cursor through planner.ToolResult.Bounds.NextCursor; codegen and runtimes then project that value into the model-visible result JSON using this field name. NextCursor must be used inside BoundedResult.

func Notification

func Notification(name, description string)

Notification marks the current method as an MCP notification sender. The method's payload defines the notification message structure.

Notification must appear in a Method expression within a service that has MCP enabled.

Notification takes two arguments:

  • name: the notification identifier
  • description: human-readable notification description

Example:

Method("progress_update", func() {
    Payload(func() {
        Attribute("task_id", String)
        Attribute("progress", Int)
    })
    Notification("progress", "Task progress notification")
})

func OnMissingFields

func OnMissingFields(action string)

OnMissingFields configures how the agent responds when tool invocation validation detects missing required fields. This allows you to control whether the agent should stop, wait for user input, or continue execution.

OnMissingFields must appear in a RunPolicy expression.

OnMissingFields takes a single string argument. 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

Example:

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

func Passthrough

func Passthrough(toolName string, target any, methodNameOpt ...string)

Passthrough defines deterministic forwarding for an exported tool to a Goa service method. It must appear within the DSL of a Tool nested under Export.

Passthrough accepts a tool name and a target, which can be:

  • A *goaexpr.MethodExpr (e.g., Passthrough("tool", MyService.MyMethod))
  • A service name and method name (e.g., Passthrough("tool", "MyService", "MyMethod"))

Example:

Export("logging-tools", func() {
    Tool("log_message", "Log a message", func() {
        Args(func() { /* ... */ })
        Return(func() { /* ... */ })
        Passthrough("log_message", "LoggingService", "LogMessage")
    })
})

func Plan

func Plan(duration string)

Plan sets the timeout for PlanStart and PlanResume planner activities. These activities invoke your planner implementation and typically involve LLM API calls. If the planner does not respond within this duration, the activity is retried according to the retry policy.

Plan must appear inside a RunPolicy or Timing expression.

Plan takes a single argument which is a Go duration string (e.g., "30s", "1m", "2m30s"). Choose a timeout that accommodates your model provider's typical response latency plus a safety margin for retries.

Example:

RunPolicy(func() {
    Timing(func() {
        Plan("45s")  // Allow 45 seconds for planner activities
    })
})

func PromptTemplate

func PromptTemplate(tmpl string)

PromptTemplate sets the operator-facing prompt template rendered during confirmation. The template is executed with the tool payload value.

func ProtocolVersion

func ProtocolVersion(version string) func(*exprmcp.MCPExpr)

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

ProtocolVersion takes a single argument which is the protocol version string.

Example:

Service("calculator", func() {
    MCP("calc", "1.0.0", ProtocolVersion("2025-06-18"))
})

func PublishTo

func PublishTo(registry *agentsexpr.RegistryExpr)

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

PublishTo must appear in a Toolset expression that is being exported.

PublishTo takes a registry expression returned by Registry().

Example:

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

func Registry

func Registry(name string, fn ...func()) *agentsexpr.RegistryExpr

Registry declares a registry source for tool discovery. Registries are centralized catalogs of MCP servers, toolsets, and agents that can be discovered and consumed by loom-mcp agents.

Registry must appear at the top level of a design.

Registry takes a name and an optional DSL function to configure the registry:

  • name: unique identifier for this registry within the design
  • fn: optional configuration function

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

Example:

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

Example with federation:

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

func Resource

func Resource(name, uri, mimeType string)

Resource marks the current method as an MCP resource provider. The method's result becomes the resource content returned when clients read the resource.

Resource must appear in a Method expression within a service that has MCP enabled.

Resource takes three arguments:

  • name: the resource name (used in MCP resource list)
  • uri: the resource URI (e.g., "file:///docs/readme.md")
  • mimeType: the content MIME type (e.g., "text/plain", "application/json")

Example:

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

func ResultHintTemplate

func ResultHintTemplate(s string)

ResultHintTemplate configures a display template for tool results. The template is rendered after execution to produce a concise preview.

Result templates receive an explicit runtime-owned wrapper:

  • `.Result` is the tool's typed semantic result value.
  • `.Bounds` is the runtime-owned bounded-result metadata when the tool returned `planner.ToolResult.Bounds`, otherwise nil.

Templates are compiled with missingkey=error.

ResultHintTemplate must appear in a Tool expression.

ResultHintTemplate takes a single string argument which is the Go template text.

Example:

Tool("search", "Search documents", func() {
    Return(func() {
        Attribute("count", Int)
        Attribute("results", ArrayOf(String))
    })
    ResultHintTemplate("Found {{ .Result.Count }} results")
})

func ResultReminder

func ResultReminder(s string)

ResultReminder 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.

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

This DSL function 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, per-run caps, and can be added or removed based on runtime conditions.

ResultReminder must appear in a Tool expression.

Example:

Tool("get_time_series", "Get Time Series", func() {
    Args(GetTimeSeriesToolArgs)
    Return(GetTimeSeriesToolReturn)
    ResultReminder("The user sees a rendered graph of this data.")
})

func Retry

func Retry(maxRetries int, backoff string)

Retry configures the retry policy for failed registry requests.

Retry must appear in a Registry expression.

Retry takes:

  • maxRetries: maximum number of retry attempts
  • backoff: initial backoff duration between retries (e.g., "1s", "500ms")

Example:

Registry("corp", func() {
    URL("https://registry.corp.internal")
    Retry(3, "1s")
})

func Return

func Return(val any, args ...any)

Return defines the output result schema for a tool. Use Return inside a Tool DSL to specify what data structure the tool produces when successfully executed.

Return follows the same patterns as Goa's Result function for methods. It accepts:

  • 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 outputs

When using a function to define the schema inline, you can use:

  • Attribute(name, type, description) to define each result field
  • Required(...) to mark fields as always present
  • All Goa attribute DSL functions (Example, MinLength, etc.)

Example (inline schema):

Tool("analyze", "Analyze document", func() {
    Args(func() { ... })
    Return(func() {
        Attribute("summary", String, "Document summary")
        Attribute("keywords", ArrayOf(String), "Extracted keywords")
        Attribute("score", Float64, "Confidence score")
        Required("summary", "keywords", "score")
    })
})

Example (reuse existing type):

var AnalysisResult = ResultType("application/vnd.analysis", func() {
    Attribute("summary", String)
    Attribute("keywords", ArrayOf(String))
    Required("summary")
})

Tool("analyze", "Analyze document", func() {
    Args(func() { ... })
    Return(AnalysisResult)
})

Example (primitive type for simple tools):

Tool("count_words", "Count words in text", func() {
    Args(String)
    Return(Int)  // Returns single integer count
})

If Return is not called, the tool produces no output (empty/null result).

func RunPolicy

func RunPolicy(fn func())

RunPolicy defines execution constraints for the current agent. Use RunPolicy to configure resource limits, timeouts, history management, and runtime behaviors that govern how the agent executes. These policies are enforced by the runtime during agent execution.

RunPolicy must appear in an Agent expression.

RunPolicy takes a single argument which is the defining DSL function.

The DSL function may use:

  • DefaultCaps to set capability limits (tool calls, consecutive failures)
  • TimeBudget to set maximum execution duration
  • InterruptsAllowed to enable or disable user interruptions
  • OnMissingFields to configure validation behavior
  • History to configure how conversation history is truncated or compressed
  • Cache to configure prompt caching hints for supported providers

Example:

Agent("assistant", "Helper agent", func() {
    RunPolicy(func() {
        DefaultCaps(MaxToolCalls(10), MaxConsecutiveFailedToolCalls(3))
        TimeBudget("5m")
        InterruptsAllowed(true)
        OnMissingFields("await_clarification")
        History(func() {
            KeepRecentTurns(20)
        })
    })
})

func ServerData

func ServerData(kind string, schema any, args ...any)

ServerData declares typed server-only data emitted alongside a tool result. Server data is never sent to model providers; it exists for UIs, observability, and persistence layers.

Each ServerData entry has:

  • a Kind identifier used by consumers (UIs, sinks) to dispatch decoders, and
  • a schema type, which loom-mcp uses to generate a JSON codec so values remain workflow-safe (canonical JSON bytes) and can be decoded reliably by tools, runtimes, and downstream consumers.

ServerData does not define emission policy. Tool implementations may emit or omit server-data items depending on their own contracts. Consumers that need “model-controlled” server-data should express it explicitly in the tool schema.

Usage patterns:

Typed server-data (tool-implemented):

Tool("get_time_series", "Get Time Series", func() {
    Args(GetTimeSeriesToolArgs)
    Return(GetTimeSeriesToolReturn)
    ServerData("atlas.time_series", GetTimeSeriesSidecar)
})

Typed server-data sourced from a bound method result field:

Tool("get_time_series", "Get Time Series", func() {
    // ...
    BindTo("GetTimeSeries")
    ServerData("aura.evidence", Evidence, func() {
        Description("Evidence references emitted for persistence and downstream UIs.")
        FromMethodResultField("evidence")
    })
})

Typed server-data with a compact description:

Tool("get_time_series", "Get Time Series", func() {
    // ...
    ServerData("atlas.time_series", GetTimeSeriesSidecar, "Time-series chart rendered in the UI.")
})

func StaticPrompt

func StaticPrompt(name, description string, messages ...string)

StaticPrompt adds a static prompt template to the MCP server. Static prompts provide pre-defined message sequences that clients can use without parameters.

StaticPrompt must appear in a Service expression with MCP enabled.

StaticPrompt takes a name, description, and a list of role-content pairs:

  • name: the prompt identifier
  • description: human-readable prompt description
  • messages: alternating role and content strings (e.g., "user", "text", "system", "text")

Example:

Service("assistant", func() {
    MCP("assistant", "1.0")
    StaticPrompt("greeting", "Friendly greeting",
        "system", "You are a helpful assistant",
        "user", "Hello!")
})

func Subscription

func Subscription(resourceName string)

Subscription marks the current method as a subscription handler for a watchable resource. The method is invoked when clients subscribe to the resource identified by resourceName.

Subscription must appear in a Method expression within a service that has MCP enabled.

Subscription takes a single argument which is the resource name to subscribe to. The resource name must match a WatchableResource declaration.

Example:

Method("subscribe_status", func() {
    Payload(func() {
        Attribute("uri", String)
    })
    Result(String)
    Subscription("status")
})

func SubscriptionMonitor

func SubscriptionMonitor(name string)

SubscriptionMonitor marks the current method as a server-sent events (SSE) monitor for subscription updates. The method streams subscription change events to connected clients.

SubscriptionMonitor must appear in a Method expression within a service that has MCP enabled.

SubscriptionMonitor takes a single argument which is the monitor name.

Example:

Method("watch_subscriptions", func() {
    StreamingResult(func() {
        Attribute("resource", String)
        Attribute("event", String)
    })
    SubscriptionMonitor("subscriptions")
})

func SyncInterval

func SyncInterval(duration string)

SyncInterval sets how often to refresh the registry catalog.

SyncInterval must appear in a Registry expression.

SyncInterval takes a duration string (e.g., "5m", "1h", "30s").

Example:

Registry("corp", func() {
    URL("https://registry.corp.internal")
    SyncInterval("5m")
})

func Tags

func Tags(values ...string)

Tags attaches metadata labels to a tool for categorization and filtering. Tags can be used by agents, planners, or monitoring systems to organize and discover tools based on their capabilities or domains.

Tags accepts a variadic list of strings. Each tag should be a simple lowercase identifier or category name. Common patterns include:

  • Domain categories: "nlp", "database", "api", "filesystem"
  • Capability types: "read", "write", "search", "transform"
  • Risk levels: "safe", "destructive", "external"
  • Performance hints: "slow", "fast", "cached"

Example (domain and capability tags):

Tool("search_docs", "Search documentation", func() {
    Args(func() { ... })
    Return(func() { ... })
    Tags("search", "documentation", "nlp")
})

Example (risk and performance tags):

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

Tags are optional. Tools without tags are still fully functional but may be harder to organize in systems with many available tools.

func TerminalRun

func TerminalRun()

TerminalRun marks the current tool as terminal for the run: after the tool executes, the runtime should complete the run immediately without requesting a follow-up planner PlanResume/finalization turn.

TerminalRun must appear in a Tool expression.

func TimeBudget

func TimeBudget(duration string)

TimeBudget sets the maximum execution time for the agent. The agent will be stopped if it exceeds this duration.

TimeBudget must appear in a RunPolicy expression.

TimeBudget takes a single argument which is a duration string (e.g., "30s", "5m", "1h").

Example:

RunPolicy(func() {
    TimeBudget("5m")
})

func Timing

func Timing(fn func())

Timing groups timing configuration for an agent's run. Use Timing inside a RunPolicy to configure fine-grained timeouts for different phases of execution.

Timing provides a cleaner way to organize multiple duration settings compared to calling Budget, Plan, and Tools directly inside RunPolicy. Both approaches are valid and produce equivalent configurations.

Timing must appear inside a RunPolicy expression.

Available settings inside Timing:

  • Budget: total wall-clock time budget for the entire run
  • Plan: timeout for Plan and Resume planner activities
  • Tools: default timeout for tool execution activities

Example:

RunPolicy(func() {
    Timing(func() {
        Budget("10m")  // Total run time
        Plan("45s")    // Planner timeout
        Tools("2m")    // Tool timeout
    })
})

Without Timing grouping (equivalent):

RunPolicy(func() {
    TimeBudget("10m")  // Note: Budget and TimeBudget are equivalent
})

func Tool

func Tool(name string, args ...any)

Tool declares a tool for agents or MCP servers. It has two distinct use cases:

  1. Inside a Toolset (agent tools): Declares a tool with inline argument and return schemas. Use this for custom tools in agent toolsets or external MCP servers where you manually define the schemas.

  2. Inside a Method (MCP tools): Marks a Goa service method as an MCP tool. The method's payload becomes the tool input schema and the method result becomes the tool output schema. This automatically exposes the method via the service's MCP server.

Tool takes two required arguments and one optional DSL function:

  • name: the tool identifier
  • description: a concise summary presented to the LLM
  • dsl (optional): configuration block (only for toolset tools, ignored for method tools)

Inside toolsets, the DSL function can use:

  • Args: defines the input parameter schema
  • Return: defines the output result schema
  • Tags: attaches metadata labels
  • BindTo: binds to a service method for implementation (optional)
  • Inject: marks fields as infrastructure-only (hidden from LLM)

Example (toolset tool with inline schemas):

Toolset("utils", func() {
    Tool("summarize", "Summarize a document", func() {
        Args(func() {
            Attribute("text", String, "Document text")
            Required("text")
        })
        Return(func() {
            Attribute("summary", String, "Summary text")
        })
        Tags("nlp", "summarization")
    })
})

Example (external MCP tool with inline schemas):

var RemoteSearch = Toolset("remote", FromMCP("search-service", "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)
})

Example (service-backed tool with inheritance):

Toolset("docs", func() {
    Tool("search_docs", func() {
        BindTo("doc_service", "search")
        Inject("session_id")
    })
})

func Tools

func Tools(duration string)

Tools sets the default timeout for ExecuteTool activities. This timeout applies to individual tool invocations when the tool executor runs the underlying implementation (service method call, MCP request, or nested agent execution).

Tools must appear inside a RunPolicy or Timing expression.

Tools takes a single argument which is a Go duration string (e.g., "30s", "2m", "5m"). The appropriate timeout depends on your tool implementations:

  • Fast lookups: 10-30 seconds
  • Database queries: 30 seconds to 2 minutes
  • External API calls: 1-5 minutes
  • Long-running operations: 5+ minutes (consider async patterns)

Example:

RunPolicy(func() {
    Timing(func() {
        Tools("2m")  // Allow 2 minutes per tool execution
    })
})

func Toolset

func Toolset(args ...any) *agentsexpr.ToolsetExpr

Toolset defines a provider-owned group of related tools. Declare toolsets at the top level using Toolset(...) and reference them from agents via Use / Export.

Tools declared inside a Toolset may be:

  • Bound to Goa service methods via BindTo, in which case codegen emits transforms and client helpers.
  • Backed by MCP tools declared with the MCP DSL (MCP + Tool) and exposed via Toolset with FromMCP provider option.
  • Implemented by custom executors or agent logic when left unbound.

Toolset accepts a single form:

  • Toolset("name", func()) declares a new toolset with the given name and tools.

Example (provider toolset definition):

var CommonTools = Toolset("common", func() {
    Tool("notify", "Send notification", func() {
        Args(func() {
            Attribute("message", String, "Message to send")
            Required("message")
        })
    })
})

Agents consume this toolset via Use:

Agent("assistant", "helper", func() {
    Use(CommonTools, func() {
        Tool("notify") // reference existing tool by name
    })
})

For MCP-backed toolsets, use FromMCP provider option:

var MCPTools = Toolset(FromMCP("assistant-service", "assistant-mcp"))

For registry-backed toolsets, use FromRegistry provider option:

var RegistryTools = Toolset(FromRegistry(CorpRegistry, "data-tools"))

Toolset accepts these forms:

  • Toolset("name", func()) - local toolset with inline schemas
  • Toolset(FromMCP(service, toolset)) - MCP-backed toolset (name derived from toolset)
  • Toolset(FromRegistry(registry, toolset)) - registry-backed toolset (name derived from toolset)
  • Toolset("name", FromMCP(...)) - MCP-backed with explicit name
  • Toolset("name", FromRegistry(...)) - registry-backed with explicit name
  • Toolset(FromMCP(...), func()) - MCP-backed with additional config

func Use

func Use(value any, fn ...func()) *expragents.ToolsetExpr

Use declares that the current agent consumes the specified toolset. The value may be either:

  • A *expragents.ToolsetExpr returned by Toolset or MCPToolset (provider-owned)
  • A string name for an inline, agent-local toolset definition

An optional DSL function can:

  • Subset tools from a referenced provider toolset by name (Tool("name"))
  • Define ad-hoc tools local to this agent

Example (referencing a provider toolset and subsetting):

var CommonTools = Toolset("common", func() {
    Tool("notify", "Send notification", func() { ... })
    Tool("log", "Log message", func() { ... })
})

Agent("assistant", "helper", func() {
    Use(CommonTools, func() {
        Tool("notify") // consume only a subset
    })
})

Example (inline agent-local toolset):

Agent("planner", "Session planner", func() {
    Use("adhoc", func() {
        Tool("foo", "Foo tool", func() { ... })
    })
})

func UseAgentToolset deprecated

func UseAgentToolset(service, agent, toolset string) *agentsexpr.ToolsetExpr

UseAgentToolset is an alias for AgentToolset. Prefer AgentToolset in new designs; this alias exists for readability in some codebases.

Deprecated: Use AgentToolset instead. This function will be removed in a future release.

func WatchableResource

func WatchableResource(name, uri, mimeType string)

WatchableResource marks the current method as an MCP resource that supports subscriptions. Clients can subscribe to receive notifications when the resource content changes.

WatchableResource must appear in a Method expression within a service that has MCP enabled.

WatchableResource takes three arguments:

  • name: the resource name (used in MCP resource list)
  • uri: the resource URI (e.g., "file:///logs/app.log")
  • mimeType: the content MIME type (e.g., "text/plain")

Example:

Method("system_status", func() {
    Result(func() {
        Attribute("status", String)
        Attribute("uptime", Int)
    })
    WatchableResource("status", "status://system", "application/json")
})

Types

type CapsOption

type CapsOption func(*expragents.CapsExpr)

CapsOption defines a functional option for configuring per-run resource limits on agent execution.

func MaxConsecutiveFailedToolCalls

func MaxConsecutiveFailedToolCalls(n int) CapsOption

MaxConsecutiveFailedToolCalls configures the maximum number of consecutive tool failures before the agent stops execution. Use this with DefaultCaps to prevent runaway failure loops.

MaxConsecutiveFailedToolCalls takes a single integer argument specifying the maximum consecutive failure count.

Example:

DefaultCaps(MaxConsecutiveFailedToolCalls(3))

func MaxToolCalls

func MaxToolCalls(n int) CapsOption

MaxToolCalls configures the maximum number of tool invocations allowed during agent execution. Use this with DefaultCaps to limit total tool usage.

MaxToolCalls takes a single integer argument specifying the maximum count.

Example:

DefaultCaps(MaxToolCalls(15))

type ServerDataAudience

type ServerDataAudience string

ServerDataAudience declares who a server-data payload is intended for.

const (
	// AudienceTimeline indicates the payload is persisted and eligible for UI rendering.
	ServerDataAudienceTimeline ServerDataAudience = "timeline"
	// AudienceInternal indicates the payload is an internal tool-composition attachment.
	ServerDataAudienceInternal ServerDataAudience = "internal"
	// AudienceEvidence indicates the payload carries provenance references.
	ServerDataAudienceEvidence ServerDataAudience = "evidence"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL