Skip to content

Tool Calling ​

Define tools that LLMs can invoke and execute them automatically.

Overview ​

Conduit's tool system lets you define Swift functions that models can call during generation. The model decides when to use a tool based on the conversation context, Conduit handles argument parsing and execution, and the results flow back to the model for a final response.

Defining a Tool ​

Implement the Tool protocol:

swift
@Generable
struct WeatherArguments {
    @Guide("City name to look up weather for")
    let city: String

    @Guide("Temperature unit", .anyOf(["celsius", "fahrenheit"]))
    let unit: String
}

struct WeatherTool: Tool {
    let name = "get_weather"
    let description = "Get the current weather for a city"

    func call(arguments: WeatherArguments) async throws -> String {
        // Your implementation
        return "72F and sunny in \(arguments.city)"
    }
}

The Tool protocol requires:

PropertyTypeDescription
nameStringUnique tool identifier (defaults to type name)
descriptionStringWhat the tool does (sent to the model)
parametersGenerationSchemaJSON schema for arguments (auto-derived from @Generable Arguments)
call(arguments:)async throws -> OutputThe implementation

When Arguments conforms to Generable, the parameters schema is derived automatically.

Adding Tools to Generation ​

Pass tools through GenerateConfig:

swift
let config = GenerateConfig.default
    .tools([WeatherTool()])
    .toolChoice(.auto)  // Let the model decide

let result = try await provider.generate(
    messages: Messages {
        Message.user("What's the weather in Tokyo?")
    },
    model: .claudeSonnet45,
    config: config
)

Tool Choice Options ​

  • .auto — The model decides whether to call a tool (default)
  • .required — The model must call at least one tool
  • .tool(name:) — Force a specific tool
  • .none — Disable tool calling

The Tool Loop ​

When a model requests a tool call, the generation result includes tool calls instead of (or alongside) text. A complete tool-calling flow looks like:

swift
let tools: [any Tool] = [WeatherTool(), SearchTool()]
let config = GenerateConfig.default.tools(tools)

// Step 1: Send user message
var messages = Messages {
    Message.user("What's the weather in Paris?")
}

var result = try await provider.generate(
    messages: messages,
    model: .claudeSonnet45,
    config: config
)

// Step 2: Check for tool calls
while result.hasToolCalls {
    // Add assistant message with tool calls
    messages.append(result.assistantMessage())

    // Execute each tool call
    for toolCall in result.toolCalls {
        let tool = tools.first { $0.name == toolCall.name }!
        let output = try await executeTool(tool, arguments: toolCall.arguments)

        // Add tool result
        messages.append(Message.tool(id: toolCall.id, content: output))
    }

    // Step 3: Send tool results back to the model
    result = try await provider.generate(
        messages: messages,
        model: .claudeSonnet45,
        config: config
    )
}

// Final text response
print(result.text)

ToolExecutor ​

ToolExecutor is an actor that manages tool registration and concurrent execution with retry policies:

swift
let executor = ToolExecutor()
await executor.register(WeatherTool())
await executor.register(SearchTool())

// Execute a tool call
let output = try await executor.execute(
    toolName: "get_weather",
    arguments: "{\"city\": \"Tokyo\", \"unit\": \"celsius\"}"
)

Retry Policies ​

Configure how tool failures are retried:

swift
let executor = ToolExecutor()

// No retries (default)
await executor.setRetryPolicy(.none)

// Retry on retryable AIErrors
await executor.setRetryPolicy(.retryableAIErrors(maxAttempts: 3))

// Retry on all failures except cancellation
await executor.setRetryPolicy(.allFailures(maxAttempts: 5))

Missing Tool Policy ​

Control what happens when the model calls a tool that isn't registered:

swift
// Throw an error (default)
let executor = ToolExecutor(missingToolPolicy: .throwError)

// Return an error message as tool output
let executor = ToolExecutor(missingToolPolicy: .emitToolOutput)

Tool Output Types ​

Tool output must conform to PromptRepresentable. Common types work automatically:

  • String — Direct text output
  • Types conforming to CustomStringConvertible

Streaming Tool Calls ​

During streaming, tool calls arrive progressively as PartialToolCall values. See Streaming for details on handling partialToolCall and completedToolCalls in GenerationChunk.

ChatSession with Tools ​

ChatSession handles the tool loop automatically. See Chat Session for details:

swift
let session = ChatSession(
    provider: provider,
    model: .claudeSonnet45,
    toolExecutor: executor
)

// Tools are called and results fed back automatically
let response = try await session.send("What's the weather in Tokyo?")

Released under the MIT License.