Messages & events
Every turn moves three kinds of data: what the user sent (AgentInput), the conversation history ([ConversationMessage]), and the stream of events an agent returns (AsyncThrowingStream<AgentEvent, any Error>). This page documents those types in full.
public enum Role: String, Sendable, Codable, Hashable { case user case assistant case system case tool}Used on every ConversationMessage. .tool marks messages that carry tool results back to the model.
ContentPart
Section titled “ContentPart”A message is a bag of heterogeneous parts rather than a plain string.
public enum ContentPart: Sendable, Codable, Hashable { case text(String) case toolCall(id: String, name: String, arguments: JSONValue) case toolResult(id: String, content: JSONValue) case audioTranscript(String) case widget(UIPayload)}| Case | When present |
|---|---|
.text | Normal prose from user or assistant |
.toolCall | The model requested a tool; id ties it to its result |
.toolResult | Result returned to the model after execution |
.audioTranscript | STT transcript attached to a voice turn |
.widget | Structured UI payload — never forwarded to the model |
ConversationMessage
Section titled “ConversationMessage”public struct ConversationMessage: Sendable, Codable, Hashable, Identifiable { public let id: String public let role: Role public let parts: [ContentPart] public let timestamp: Date}Initializers
// Parts-basedConversationMessage( id: String = UUID().uuidString, role: Role, parts: [ContentPart], timestamp: Date = Date())
// Convenience: single text partConversationMessage( id: String = UUID().uuidString, role: Role, text: String, timestamp: Date = Date())Computed property
public var text: StringJoins all .text parts in order; returns "" when none are present.
Example
let msg = ConversationMessage(role: .user, text: "What are the live odds?")print(msg.text) // "What are the live odds?"
let mixed = ConversationMessage(role: .assistant, parts: [ .text("The odds are "), .toolCall(id: "c1", name: "getOdds", arguments: ["eventId": 42]), .text(".")])print(mixed.text) // "The odds are ." — only text partsMessages are immutable. Streamed deltas are accumulated by the orchestrator and delivered as a single .final(ConversationMessage) event at the end of a turn.
AgentInput
Section titled “AgentInput”The input side of a turn. Currently text-only; continuous audio is handled by VoiceAssistant.
public enum AgentInput: Sendable { case text(String)
public var text: String}let input = AgentInput.text("Show me today's matches")print(input.text) // "Show me today's matches"See Agents for how AgentProtocol.process(_:history:context:) receives this.
AgentEvent
Section titled “AgentEvent”The AsyncThrowingStream that AgentProtocol.process returns emits these cases:
public enum AgentEvent: Sendable { case thinking(String) case textDelta(String) case toolCall(id: String, name: String, arguments: JSONValue) case widget(UIPayload) case final(ConversationMessage) case error(String)}| Case | Meaning |
|---|---|
.thinking | Extended-thinking scratchpad text (model reasoning, not shown to users by default) |
.textDelta | Incremental text chunk — append to a string buffer to build the response |
.toolCall | Observability notification that the agent is calling a tool; the result is recorded on the trace span, not re-emitted as an event |
.widget | Structured UI payload forwarded from a tool when UIPolicy is .forward |
.final | The fully-assembled ConversationMessage at the end of the turn, including all parts |
.error | User-facing error string (e.g. “network unavailable”); hard failures are thrown through the stream instead |
Consuming the stream
let stream = agent.process(.text("Hello"), history: [], context: ctx)
var buffer = ""for try await event in stream { switch event { case .textDelta(let chunk): buffer += chunk case .final(let message): print("Turn complete:", message.text) case .error(let msg): print("Agent error:", msg) default: break }}For widget rendering, see UI overview. For tracing .toolCall spans, see Tracing overview.
AgentContext
Section titled “AgentContext”Passed alongside every process call; carries per-turn identity and an optional live trace span.
public struct AgentContext: Sendable { public let userId: String public let sessionId: String public let params: [String: JSONValue] public let span: (any SpanHandle)?}AgentContext( userId: String, sessionId: String, params: [String: JSONValue] = [:], span: (any SpanHandle)? = nil)params is the escape hatch for request-scoped data (locale, feature flags, A/B variants) that agents need but that does not belong in the message history. See Tracing overview for SpanHandle usage.
JSONValue
Section titled “JSONValue”Schema-less JSON used for tool arguments, tool results, AgentContext.params, and trace payloads.
public enum JSONValue: Sendable, Hashable { case null case bool(Bool) case int(Int) case double(Double) case string(String) case array([JSONValue]) case object([String: JSONValue])}JSONValue is Codable and conforms to all relevant ExpressibleBy*Literal protocols, so you can build values inline without explicit case syntax:
let args: JSONValue = [ "eventId": 42, "live": true, "market": "1X2", "minOdds": 1.5]Number gotchas
- Whole-number doubles decode to
.int: JSON1.0becomes.int(1), not.double(1.0). - Integer IDs larger than
Int.maxlose precision as.double. Carry them as.stringinstead. .doublevalues must be finite;NaNand±Infare not valid JSON and will cause encoding to fail.
See also
Section titled “See also”- Agents overview — how
AgentProtocolconsumes these types - Orchestrator overview — how history is assembled and routed between agents
- Storage overview — how
ConversationMessageis persisted - UI overview —
UIPayloadandUIPolicy - Tracing overview — attaching spans to
AgentContext