Tools

Typed tools, execution context, approvals, and client-side calls.

A tool is anything conforming to AIToolProtocol: a name, a description, JSON Schema parameters, and an async execute. The closure-based Tool covers most cases.

let search = Tool(
  name: "search",
  description: "Search the product catalog.",
  parameters: Schema.object(["query": .string()])
) { arguments in
  let query = arguments["query"]?.stringValue ?? ""
  return try await catalog.search(query)
}

Typed arguments

Tool.typed decodes arguments into a Decodable before your code runs:

struct SearchArgs: Decodable { let query: String }

let search = Tool.typed(
  name: "search",
  description: "Search the product catalog.",
  parameters: Schema.object(["query": .string()])
) { (args: SearchArgs) in
  try await catalog.search(args.query)
}

Execution context

Tools can see which call they are servicing, the step messages, and per-request context that should never ride in the prompt. This is the AI SDK's tool execution options plus toolsContext:

let orders = Tool(
  name: "list_orders",
  description: "List recent orders for the signed-in user.",
  parameters: Schema.object([:])
) { _, options in
  let userID = options.context?["userID"]?.stringValue ?? "anonymous"
  return try await store.orders(for: userID)
}

let result = try await generateText(
  model: model,
  prompt: "What did I order recently?",
  tools: [orders],
  toolsContext: ["list_orders": ["userID": .string("user-7")]]
)

options.toolCallID and options.messages are there too.

Human-in-the-loop approvals

A tool can require user approval before it runs. The loop pauses with a toolApprovalRequest, your app answers, and execution resumes on the next turn; denials surface to the model as denied results.

let delete = Tool(
  name: "delete_file",
  description: "Delete a file.",
  parameters: Schema.object(["path": .string()]),
  needsApproval: true
) { arguments in
  try await files.delete(arguments["path"]?.stringValue ?? "")
}

Approval can also depend on the arguments: pass needsApproval: { arguments in ... }. In chat UIs, respond with ChatSession.addToolApprovalResponse(approvalID:approved:).

Client-side tools

A tool without an executor ends the turn with the call unexecuted. The model asked; your app answers, whenever it's ready:

let pickPhoto = Tool(
  name: "pick_photo",
  description: "Ask the user to pick a photo.",
  parameters: Schema.object([:])
)

// later, in the chat UI:
chat.addToolResult(toolCallID: tool.toolCallID, result: ["photoID": "IMG_0042"])

Provider-executed tools

Some providers run their own server-side tools — live web and X search, code execution, file search, computer use — and stream the calls and results back in the same turn. Each provider exposes typed builders under <Model>.Tools; drop them in tools: next to your own:

let result = try await generateText(
  model: XaiModel("grok-4.5"),
  prompt: "What shipped in AI this week?",
  tools: [
    XaiModel.Tools.webSearch(allowedDomains: ["arxiv.org"]),
    XaiModel.Tools.xSearch(allowedXHandles: ["xai"]),
  ]
)

These carry no executor: the provider runs them, so the loop never asks your app to and the turn doesn't pause. The calls and results arrive as .toolCall / .toolResult parts with toolCall.providerExecuted == true (web citations also surface as .source), and each provider ignores builders that belong to another — so mixing them is safe. Each provider page lists its catalog: xAI, OpenAI, Google, Anthropic.

For a server-side tool without a typed builder yet, construct one directly with ProviderDefinedTool(provider:id:name:args:)args is the native tool entry the provider expects.

Validation

When parameters is built from the Schema DSL, arguments are validated before execution and malformed calls become error results for the model to correct, instead of crashing your tool.