SwiftyAISwiftyAI

Search documentation

Find a docs page by title or section

2

Tool Calling

generateWithTools and streamWithTools run a model-tool loop. The model can answer directly, request one or more tools, receive tool results, and continue until a stop condition ends the loop.

APIOutputBest for
generateWithToolsFinal AIStepResultBackground tasks, server actions, tests, single final answer
streamWithToolsAIAgentChunk event stream plus final result callbackUIs that need step text and tool progress
createAgentUIStreamHigher-level AgentEvent streamFrontend state machines and timeline rendering

One Loop

let result = try await generateWithTools(
    model: model,
    prompt: "Should I bring an umbrella in London today?",
    tools: [weatherTool],
    options: GenerationOptions(system: "Use tools when live data is needed."),
    maxSteps: 4,
    stopWhen: [isLoopFinished()],
    onStepFinish: { step in
        print("step:", step.index)
        print("tool calls:", step.toolCalls.map(\.name))
    }
)
 
print(result.text)

AIStepResult contains the final text, all steps, usage, finish reason, and isMaxStepsExceeded.

Stop Conditions

maxSteps is always enforced. stopWhen lets you end earlier.

let result = try await generateWithTools(
    model: model,
    prompt: "Look up the order, then stop before sending any email.",
    tools: [lookupOrderTool, sendEmailTool],
    maxSteps: 5,
    stopWhen: [
        isLoopFinished(),
        hasToolCall("send_email"),
        stepCountIs(3)
    ]
)

Important detail: stop conditions are evaluated before tool calls for that step are executed. hasToolCall("send_email") can be used as a safety boundary.

For custom logic, create an AIStopCondition:

let stopOnManyTools = AIStopCondition(name: "tooManyTools") { context in
    context.step.toolCalls.count > 2
}

Stream A Tool Loop

streamWithTools yields AIAgentChunk values for model step text, tool calls, and tool results. It runs the same non-token tool loop as generateWithTools; when a step returns assistant text, that step's text is emitted as one chunk.

let stream = streamWithTools(
    model: model,
    prompt: "Find the order status and explain it to the customer.",
    tools: [lookupOrderTool],
    onChunk: { chunk in
        if !chunk.text.isEmpty {
            print(chunk.text, terminator: "")
        }
    },
    onFinish: { result in
        print("steps:", result.steps.count)
    }
)
 
for try await chunk in stream {
    if let call = chunk.toolCall {
        print("calling:", call.name)
    }
    if let result = chunk.toolResult {
        print("tool returned:", result.name)
    }
}

Use agent UI stream when a UI wants higher-level events such as stepStarted, toolCallStarted, and agentFinished. Use streamText for token-style assistant text streaming without tools.

Tool Policies

Tool execution can be controlled per loop:

PolicyBehavior
.returnErrorResultConvert thrown tool errors into AIToolResult(isError: true) and let the model continue
.failFastThrow immediately when a tool fails
parallelToolCalls: trueExecute tool calls from the same step concurrently and preserve result order
parallelToolCalls: falseExecute calls serially when tools share mutable state or rate limits
let options = ToolExecutionOptions(
    onToolCall: { call in
        if call.name == "lookup_order", call.arguments.contains("demo") {
            return .returnResult(#"{"status":"demo"}"#)
        }
        return .execute
    },
    onToolResult: { result in
        print("tool result:", result.name)
    },
    errorPolicy: .failFast
)

returnErrorResult keeps the loop alive by sending tool errors back to the model. failFast throws immediately.

Tool Call Decisions

approval and onToolCall both return ToolCallDecision:

CaseEffect
.executeRun the tool normally
.reject(reason: String)Skip execution and feed reason back to the model as an error result
.returnResult(String)Skip execution and feed the supplied string back as a successful result
.replaceArguments(String)Replace the model's tool arguments with a sanitized JSON string before execution