Conversations

agent.conversation is the durable record of everything the agent said and did — readable any time, and the input every new request is built from. The same history replays against any provider.

The shape of history

Conversation is a value type holding [Message]. Each Message has an id, a role, and ordered content items.

MessageRole has exactly two cases — .user and .assistant. Tool results travel inside .user messages: Message.isPureToolResult is true when a message exists only to carry results back to the model, so you can tell those apart from typed user input.

Content Appears on Carries
.text(String) both roles plain text
.toolCall(ToolCall) assistant a tool request the model made
.toolResult(toolCallId:content:isError:) user your executor's outcome, keyed to the call id
.image(ImageRef) user image input — see send images

What a turn appends

send() appends your text as a user message, then records everything the turn produces. A plain exchange appends user → assistant. A turn that runs a tool records the whole exchange:

user        "What's the weather in Tokyo?"
assistant   .toolCall  weather.current
user        .toolResult "22°C, clear"          (isPureToolResult)
assistant   "It's 22°C and clear in Tokyo."

That fidelity is what makes resuming lossless — the next provider sees the calls and results, not a summary. What a failed or cancelled turn leaves behind is part of the turn contract: how a turn works.

Resume a conversation

makeAgent(conversation:) injects history into a new session:

let saved = Conversation(messages: [
    Message(role: .user, content: [.text("What's the capital of France?")]),
    Message(role: .assistant, content: [.text("Paris.")]),
])

let resumed = try runtime.makeAgent(
    provider: .anthropic(apiKey: key),
    role: AgentRole(staticPersona: "You are a helpful assistant."),
    conversation: saved
)
try await resumed.send("How many people live there?")

The first request carries all three messages — the model answers with full context. Conversation has value semantics: the session copies what you pass at creation, so appending to your own instance afterward changes nothing in the session.

Resume across providers

History is provider-neutral. A conversation recorded against one provider replays against any other — cloud to cloud, on-device to cloud, cloud to on-device. The session serializes the injected history, including recorded tool calls and tool results, to whichever provider it now talks to.

That cuts one specific way for privacy: a conversation recorded on-device contains real tool output from your app. Resume it against a cloud provider and that content leaves the device with the first request. See control what leaves the device.

Persist across launches

Conversation is Codable. Encode it to save a session and decode to restore one — text, tool calls, tool results, images, and content order all round-trip:

let data = try JSONEncoder().encode(agent.conversation)
try data.write(to: saveURL)

// next launch:
let restored = try JSONDecoder().decode(Conversation.self, from: data)
let agent = try runtime.makeAgent(
    provider: spec, role: role, conversation: restored
)

The encoded form is the SDK's own stable format — independent of any provider's wire shape — and carries a version marker. Decoding fails loud with a typed ConversationCodingError on an unrecognized version or content kind rather than silently dropping data.

One caveat for images: an ImageRef.fileURL persists its absolute path verbatim. That path can dangle on another device or after a reinstall, and it reveals local details (the username, your directory layout). Before syncing, exporting, or logging a conversation, convert image refs to .data or a redacted model you own. Live turns are unaffected — the SDK already loads file URLs into bytes at the send boundary.

Prefer your own storage schema? Map messages into a type you control and rebuild on the way back:

struct StoredMessage {   // your model — Codable, SwiftData, whatever you use
    let isUser: Bool
    let text: String
}

let rebuilt = Conversation(messages: stored.map { record in
    Message(role: record.isUser ? .user : .assistant,
            content: [.text(record.text)])
})

A text-only mapping like this drops recorded tool exchanges. To resume mid-task with full fidelity, store the .toolCall and .toolResult content too and rebuild those items in the same order — or just encode the whole Conversation.