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.
| API | Output | Best for |
|---|---|---|
generateWithTools | Final AIStepResult | Background tasks, server actions, tests, single final answer |
streamWithTools | AIAgentChunk event stream plus final result callback | UIs that need step text and tool progress |
createAgentUIStream | Higher-level AgentEvent stream | Frontend 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:
| Policy | Behavior |
|---|---|
.returnErrorResult | Convert thrown tool errors into AIToolResult(isError: true) and let the model continue |
.failFast | Throw immediately when a tool fails |
parallelToolCalls: true | Execute tool calls from the same step concurrently and preserve result order |
parallelToolCalls: false | Execute 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:
| Case | Effect |
|---|---|
.execute | Run 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 |