Skip to the content.

Tool System Guide

core/tool defines the LLM function-calling contract, a registry/catalog directory, the executor dispatcher, and a middleware chain.

Roles

Type Responsibility
Registry mutable, thread-safe directory of tools
Catalog read-side view (Get, Definitions)
Executor dispatches every call through middleware

Registry.Add / Registry.Remove register and unregister tools at runtime (removal closes io.Closer tools); deferred sources use this surface to publish tools discovered after construction.

Tool contract

type Tool interface {
    Definition() message.ToolDefinition
    Execute(ctx context.Context, arguments string) (string, error)
}

Definition carries Name, Description, and InputSchema. arguments is a JSON-encoded object.

Deployment resources

Tool sources feed an assembly:

resources:
  sim:
    kind: tool.Source
    impl: sim
  tools:
    kind: tool.Assembly
    impl: memory
    deps:
      tool: sim

Dynamic injection is configured in the assembly settings:

resources:
  tools:
    kind: tool.Assembly
    impl: memory
    deps:
      tool: sim
    settings:
      dynamic:
        default: deferred
        exposures:
          tool_search: always

MCP servers are registered as a tool.Source/mcp resource. Attach is best-effort: a server that is unreachable at startup is retried in the background with exponential backoff, and its tools are published to the registry the moment it connects. A server that dies later is reconnected the same way; its tools stay registered and calls fail with a per-server NotAvailable error until the connection is restored. Configuration errors (a rejected connection, an invalid spec) still fail the deployment. The settings subtree declares one or more servers:

resources:
  sim:
    kind: tool.Source
    impl: mcp
    settings:
      servers:
        - name: filesystem
          transport: stdio           # stdio | http
          command: npx
          args: ["-y", "@modelcontextprotocol/server-filesystem"]
          env: {TOKEN: ${env:MCP_TOKEN}}
          prefix: fs                  # tool namespace; default "<name>__"
          resources: true             # bridge list_resources / read_resource tools
          required: true              # host should WaitReady before serving
        - name: remote
          transport: http
          url: https://mcp.example.com/mcp
          headers: {Authorization: "Bearer ${env:MCP_TOKEN}"}
          http_timeout: 30s

required: true marks a server the host cannot start without; hosts await Source.WaitReady so a background give-up surfaces as an error instead of a silent missing tool set. Middleware lives in core/tool/middleware.

Middleware chain

tool.Assembly/middleware is the memory assembly plus a settings-declared middleware chain:

resources:
  tools:
    kind: tool.Assembly
    impl: middleware
    deps:
      tool: sim
    settings:
      middlewares:
        recover: {enabled: true}
        timeout: {default: 30s}
        concurrency: {limit: 8}

Each entry is optional; absent entries are skipped. recover converts a panicking tool (or inner middleware) into an IsError result instead of crashing the caller's goroutine; timeout.default bounds each call with a Go duration (calls that already carry a deadline pass through); concurrency.limit caps in-flight executions, with excess callers waiting (respecting context cancellation). The plain memory impl rejects the middlewares key.

A model that calls a deferred tool before tool_search has exposed it is rejected at response validation with a distinguishable undefined_tool error rather than the generic invalid_provider_response. The inference node's undefined_tool_recovery config turns that rejection into in-conversation feedback that sends the model back to tool_search; see graph.md for the recovery loop.

See runtime.md for per-session dynamic catalogs.