// // Message.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Ported from Zyquo Cloud, extended with the normalized tool-calling fields // (docs/PROVIDER-REUSE.md §5.3): `toolCalls` on assistant turns and // `toolResults` on the turn that feeds results back. Clients render these // per wire format (OpenAI `tool_calls` + role:"tool" messages; Anthropic // `tool_use` assistant blocks + `tool_result` user blocks) — Message is the // provider-neutral turn representation the agent loop works with. // import Foundation /// One turn in a conversation transcript. struct Message: Codable, Identifiable, Hashable { enum Role: String, Codable { case system case user case assistant } let id: UUID var role: Role var text: String /// Reasoning/thinking text streamed by reasoning models (collapsible in the UI). var reasoning: String? /// Image attachments (user messages, vision models). var attachments: [Attachment] /// Web-search citations (Perplexity). var citations: [Citation] /// Tool calls emitted by the model on this assistant turn. Threaded back to /// the provider verbatim so multi-step tool history round-trips correctly. var toolCalls: [ToolCall]? /// Tool results being returned to the model. Clients render these as /// role:"tool" messages (OpenAI schema) or tool_result user blocks (Anthropic). var toolResults: [ToolResult]? /// Model that produced this message (assistant turns) or was targeted (user turns). var modelID: String? var provider: ProviderID? var usage: TokenUsage? /// Estimated USD cost computed from catalog pricing at receive time. var estimatedCost: Double? var createdAt: Date /// Set while a response is streaming; exactly one message can be streaming at a time. var isStreaming: Bool = false /// Human-readable error if generation failed mid-message. var errorText: String? init( id: UUID = UUID(), role: Role, text: String, reasoning: String? = nil, attachments: [Attachment] = [], citations: [Citation] = [], toolCalls: [ToolCall]? = nil, toolResults: [ToolResult]? = nil, modelID: String? = nil, provider: ProviderID? = nil, usage: TokenUsage? = nil, estimatedCost: Double? = nil, createdAt: Date = Date() ) { self.id = id self.role = role self.text = text self.reasoning = reasoning self.attachments = attachments self.citations = citations self.toolCalls = toolCalls self.toolResults = toolResults self.modelID = modelID self.provider = provider self.usage = usage self.estimatedCost = estimatedCost self.createdAt = createdAt } /// An assistant turn carrying the model's tool calls (text may be empty). static func assistantToolCalls(_ calls: [ToolCall], text: String = "", reasoning: String? = nil) -> Message { Message(role: .assistant, text: text, reasoning: reasoning, toolCalls: calls) } /// The turn that returns tool results to the model. Multiple results in one /// turn cover parallel tool calls. static func toolResultsMessage(_ results: [ToolResult]) -> Message { Message(role: .user, text: "", toolResults: results) } } /// A file attached to a user message. Images go to vision models as base64; /// text files are injected into the prompt. struct Attachment: Codable, Identifiable, Hashable { enum Kind: String, Codable { case image case textFile } let id: UUID var kind: Kind var fileName: String /// image: raw image bytes; textFile: UTF-8 contents. var data: Data /// MIME type for images (image/png, image/jpeg, image/webp, image/gif). var mimeType: String init(id: UUID = UUID(), kind: Kind, fileName: String, data: Data, mimeType: String) { self.id = id self.kind = kind self.fileName = fileName self.data = data self.mimeType = mimeType } } /// A numbered web source backing an assistant answer (Perplexity sonar family). struct Citation: Codable, Identifiable, Hashable { let id: UUID var index: Int var url: URL var title: String? init(id: UUID = UUID(), index: Int, url: URL, title: String? = nil) { self.id = id self.index = index self.url = url self.title = title } }