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:
| Builder | Arguments | Result | Use it when |
|---|---|---|---|
tool | Decoded into a Swift Decodable type | String or any Encodable value | The schema is known and app code should be type-safe |
dynamicTool | Raw [String: Any] values | String | The 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.
| Hook | Runs when | Common use |
|---|---|---|
onToolCall | Before approval and execution | Rewrite arguments, return a cached result, block unsafe inputs |
approval | Before the tool closure runs | Ask the user, enforce policy, stop side effects |
onToolResult | After a tool returns | Update 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.