Steer tool use

By default the model decides whether to call tools. send(_:toolChoice:) overrides that decision per turn — force at least one call, or forbid calls entirely.

// The model decides (default)
try await agent.send("Trim the intro clip.")

// Force at least one tool call
try await agent.send("Trim the intro clip.", toolChoice: .required)

// Forbid tool calls this turn
try await agent.send("Summarize what you changed.", toolChoice: .none)

ToolChoice has three values: .auto, .required, and .none. .auto is the default everywhere and adds nothing to the request.

How .required behaves

.required forces the turn's first provider round trip only. Once the model has attempted a call, the follow-up round trips revert to .auto so it can read the tool results and produce a final answer — otherwise it would be forced into endless calls. An attempted call satisfies the requirement even if a guard later denies it.

On a provider that runs tools inside its own session (the on-device model), there is no per-round-trip seam: the forced mode applies to the provider's whole internal turn.

Violations fail loud

A model that ignores the instruction — .required with zero attempted calls, or .none with at least one — fails the turn with a typed error:

do {
    try await agent.send("Just answer in prose.", toolChoice: .none)
} catch AgentSessionError.toolChoiceNotHonored(let requested, let observedToolCalls) {
    print("asked for \(requested), model attempted \(observedToolCalls) calls")
}

A violating turn persists no assistant message and rolls back its undo transaction — your message stays as the conversation's last entry, ready to resend. .none is enforced before execution: a forbidden call is caught before any executor runs.

Validated before anything leaves

A non-.auto choice against a provider without native tool-choice support throws AgentSessionError.toolChoiceUnsupported before any request is built and before your message is appended — a misconfigured send leaves the session untouched. Silently dropping the instruction is never an option: a dropped .none would let tools run after you demanded none.

Two misconfigurations also fail up front with AgentSessionError.invalidConfiguration: .required with a zero tool-call budget (see bound the turn), and .required when no tools are available to the provider.

Provider support

Provider Tool choice
Anthropic native
OpenAI native
Gemini native
AgentKit Cloud native
Your backend off by default — opt in via custom capabilities when your backend honors the emitted field
Apple on-device OS 27+ with an app built against the OS 27 SDK; below that the capability reports false and a non-auto choice throws before any request

Next