An agent with tools

Give the model functions, bound the loop, delegate to subagents.

Covers Examples/Features/05-Tools.swift, 10-Agent.swift, and 20-SubagentsAndContext.swift. You end up with a reusable agent that calls your code and knows when to stop.

Write a tool

A tool is a name, a description the model reads, a JSON Schema for the arguments, and a closure. The loop handles the rest: run, feed back, continue.

let getTime = Tool(
  name: "current_time",
  description: "Returns the current time in a timezone.",
  parameters: ["type": "object",
               "properties": ["tz": ["type": "string"]],
               "required": ["tz"]]
) { args in
  .string(Date().formatted())
}

Prefer typed arguments? Decode them:

struct WeatherArgs: Decodable { var city: String }

let weather = Tool.typed(
  name: "weather",
  description: "Current weather for a city",
  parameters: ["type": "object",
               "properties": ["city": ["type": "string"]],
               "required": ["city"]]
) { (args: WeatherArgs) in
  try await OpenMeteoWeather.current(city: args.city)
}

OpenMeteoWeather.current first resolves the city with Open-Meteo's geocoding endpoint, then requests current temperature, apparent temperature, humidity, weather code, and wind speed. The complete, compile-checked client is in Examples/Support.swift; Open-Meteo requires no API key for this use. The structured result includes source and sourceURL so your UI can retain the required attribution. See the official forecast and geocoding documentation.

Bound the loop

Without a limit the model could ping-pong forever. stopWhen caps it:

let result = try await generateText(
  model: AnthropicModel("claude-opus-4-8"),
  prompt: "What time is it in Asia/Kolkata?",
  tools: [getTime],
  stopWhen: [stepCountIs(4)],
  onStepFinish: { step in print("step:", step.finishReason) }
)
print(result.text)
print("steps: \(result.stepCount)")

Make it reusable

Agent captures the model, instructions, tools, and settings in one value you call again and again:

let assistant = Agent(
  model: AnthropicModel("claude-sonnet-5"),
  instructions: "You are a terse weather assistant.",
  tools: [weather],
  stopWhen: [isStepCount(4)]
)

let result = try await assistant.generate(prompt: "Weather in Mumbai?")
for try await delta in assistant.stream(prompt: "And in Tokyo?").textStream {
  print(delta, terminator: "")
}

An Agent is also a chat transport: ChatSession(transport: assistant) gives you a chat UI over it with no server.

Pass request context without polluting the prompt

User ids and session handles don't belong in the prompt. toolsContext delivers them straight to the tool:

let orders = Tool(
  name: "list_orders",
  description: "List recent orders for the signed-in user.",
  parameters: ["type": "object"]
) { _, options in
  let userID = options.context?["userID"]?.stringValue ?? "anonymous"
  return .string("Orders for \(userID): #1001, #1002")
}

let result = try await generateText(
  model: model,
  prompt: "What did I order recently?",
  tools: [orders],
  toolsContext: ["list_orders": ["userID": .string("user-7")]]
)

Delegate to subagents

An orchestrator stays small by handing focused work to specialists. asTool turns any agent into a tool:

let researcher = Agent(
  model: model,
  instructions: "You research questions and answer with dense facts."
)
let writer = Agent(
  model: model,
  instructions: "You turn notes into friendly prose."
)

let orchestrator = Agent(
  model: model,
  instructions: "Plan the work, delegate to specialists, then combine.",
  tools: [
    researcher.asTool(name: "researcher", description: "Delegate research."),
    writer.asTool(name: "writer", description: "Delegate drafting.")
  ]
)

Final code

WeatherAgent.swift
import AI
import Foundation

struct WeatherArgs: Decodable { var city: String }

let weather = Tool.typed(
  name: "weather",
  description: "Current weather for a city",
  parameters: ["type": "object",
               "properties": ["city": ["type": "string"]],
               "required": ["city"]]
) { (args: WeatherArgs) in
  try await OpenMeteoWeather.current(city: args.city)
}

let assistant = Agent(
  model: AnthropicModel("claude-sonnet-5"),
  instructions: "You are a terse weather assistant.",
  tools: [weather],
  stopWhen: [isStepCount(4)]
)

func run() async throws {
  let result = try await assistant.generate(prompt: "Weather in Mumbai?")
  print(result.text)
}

One more trick from the examples: prepareStep swaps to a cheaper model once the expensive one has done the thinking:

prepareStep: { context in
  context.stepNumber >= 3 ? PrepareStepResult(model: cheaperModel) : nil
}