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.