Skip to content

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.


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)
}
CaseWhen present
.textNormal prose from user or assistant
.toolCallThe model requested a tool; id ties it to its result
.toolResultResult returned to the model after execution
.audioTranscriptSTT transcript attached to a voice turn
.widgetStructured UI payload — never forwarded to the model

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-based
ConversationMessage(
id: String = UUID().uuidString,
role: Role,
parts: [ContentPart],
timestamp: Date = Date()
)
// Convenience: single text part
ConversationMessage(
id: String = UUID().uuidString,
role: Role,
text: String,
timestamp: Date = Date()
)

Computed property

public var text: String

Joins 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 parts

Messages are immutable. Streamed deltas are accumulated by the orchestrator and delivered as a single .final(ConversationMessage) event at the end of a turn.


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.


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)
}
CaseMeaning
.thinkingExtended-thinking scratchpad text (model reasoning, not shown to users by default)
.textDeltaIncremental text chunk — append to a string buffer to build the response
.toolCallObservability notification that the agent is calling a tool; the result is recorded on the trace span, not re-emitted as an event
.widgetStructured UI payload forwarded from a tool when UIPolicy is .forward
.finalThe fully-assembled ConversationMessage at the end of the turn, including all parts
.errorUser-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.


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.


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: JSON 1.0 becomes .int(1), not .double(1.0).
  • Integer IDs larger than Int.max lose precision as .double. Carry them as .string instead.
  • .double values must be finite; NaN and ±Inf are not valid JSON and will cause encoding to fail.