SwiftyAISwiftyAI

Search documentation

Find a docs page by title or section

3

Tools

Tools let a model ask your Swift code to do work: look up data, call an API, calculate a value, or perform an app action after approval.

SwiftyAI has two builders:

BuilderArgumentsResultUse it when
toolDecoded into a Swift Decodable typeString or any Encodable valueThe schema is known and app code should be type-safe
dynamicToolRaw [String: Any] valuesStringThe shape comes from a gateway, plugin, or external registry

Tools are just definitions until they run inside tool calling, agent UI stream, or an adapted MCP server tool.

Typed Tool

Typed tools are the best default because invalid arguments fail before your business logic runs.

import SwiftyAI
 
struct WeatherInput: Decodable {
    let city: String
}
 
let weatherTool = tool(
    name: "get_weather",
    description: "Get the current weather for a city.",
    inputSchema: .object(
        properties: [
            "city": .string(description: "City name, for example London")
        ]
    )
) { (input: WeatherInput) async throws in
    "It is 21C and clear in \(input.city)."
}

The tool result can be a String or any Encodable value. Non-string values are JSON encoded before they are sent back to the model.

Dynamic Tool

Use dynamicTool when the shape is intentionally loose.

let lookupTool = dynamicTool(
    name: "lookup_order",
    description: "Find an order by id.",
    inputSchema: .object(
        properties: [
            "orderID": .string(description: "Internal order id")
        ]
    )
) { arguments in
    let orderID = arguments["orderID"] as? String ?? "unknown"
    return #"{"orderID":"\#(orderID)","status":"shipped"}"#
}

Dynamic tools are useful at integration boundaries, but typed tools make app code easier to reason about.

Approval And Interception

ToolExecutionOptions lets the app inspect, reject, replace, or short-circuit tool calls.

HookRuns whenCommon use
onToolCallBefore approval and executionRewrite arguments, return a cached result, block unsafe inputs
approvalBefore the tool closure runsAsk the user, enforce policy, stop side effects
onToolResultAfter a tool returnsUpdate UI state, logs, or local caches
let toolOptions = ToolExecutionOptions(
    approval: { call in
        if call.name == "send_email" {
            return .reject(reason: "Email sending requires user confirmation.")
        }
 
        return .execute
    },
    onToolResult: { result in
        print("tool result:", result.name, result.isError)
    },
    parallelToolCalls: true,
    errorPolicy: .returnErrorResult
)

Use approval for user or policy gates. Use onToolCall when you want to rewrite arguments, return a cached result, or block a call before execution.

Output Schema

Tools can also describe their own output:

struct AccountInput: Decodable {
    let id: String
}
 
struct AccountStatus: Encodable {
    let status: String
    let balance: Double
}
 
let accountTool = tool(
    name: "get_account",
    description: "Return account status.",
    inputSchema: .object(
        properties: ["id": .string(description: "Account id")]
    ),
    outputSchema: .object(
        properties: [
            "status": .enumeration(["active", "paused", "closed"]),
            "balance": .number(description: "Current balance")
        ]
    )
) { (input: AccountInput) async throws in
    AccountStatus(status: "active", balance: 42.50)
}

The model still receives the actual tool result as text. outputSchema is stored on the AITool value and is useful for your app, adapters, and MCP metadata, but the current OpenAI-compatible provider encoder only sends the input parameters to model providers.

Related docs

Read tool calling for loops, maxSteps, stop conditions, and callbacks.