TestFlight / Build and upload (push) Successful in 5m29s
Integrate the shared Agent room, durable outbox, cursor replay and read state on iPhone and iPad. Reuse message rendering with native messaging layout and expandable tool details. Validated with 58 Xcode package tests, six Simulator UI scenarios, read-only production connectivity and independent review.
57 lines
2.5 KiB
Swift
57 lines
2.5 KiB
Swift
import Foundation
|
|
|
|
protocol AgentSocket: Sendable {
|
|
func receive() async throws -> Data
|
|
func send(_ text: String) async throws
|
|
func close() async
|
|
func closeCode() async -> Int
|
|
}
|
|
|
|
protocol AgentAPIClienting: Sendable {
|
|
func agentDescriptor() async throws -> AgentDescriptor
|
|
func agentMessages(after: String?, before: String?) async throws -> AgentPage
|
|
func agentReadState() async throws -> AgentReadState
|
|
func agentMarkRead(cursor: String) async throws -> AgentReadState
|
|
func agentSubmit(content: String, requestID: String) async throws -> AgentReceipt
|
|
func agentSocket(after: String) async throws -> any AgentSocket
|
|
}
|
|
|
|
actor AgentURLSocket: AgentSocket {
|
|
private let task: URLSessionWebSocketTask
|
|
init(task: URLSessionWebSocketTask) { self.task = task }
|
|
func receive() async throws -> Data {
|
|
switch try await task.receive() {
|
|
case let .string(text): return Data(text.utf8)
|
|
case .data: throw AgentFailure(message: "Agent sent a non-JSON text frame.")
|
|
@unknown default: throw AgentFailure(message: "Unsupported Agent frame.")
|
|
}
|
|
}
|
|
func send(_ text: String) async throws { try await task.send(.string(text)) }
|
|
func close() { task.cancel(with: .goingAway, reason: nil) }
|
|
func closeCode() -> Int { task.closeCode.rawValue }
|
|
}
|
|
|
|
extension APIConfiguration {
|
|
func agentWebSocketURL() throws -> URL {
|
|
guard var parts = URLComponents(url: baseURL, resolvingAgainstBaseURL: false),
|
|
["http", "https"].contains(parts.scheme), parts.host != nil,
|
|
parts.user == nil, parts.password == nil else { throw APIError.invalidBaseURL }
|
|
parts.scheme = parts.scheme == "https" ? "wss" : "ws"
|
|
parts.path = parts.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")).isEmpty
|
|
? "/v1/agent/ws" : "/" + parts.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + "/v1/agent/ws"
|
|
parts.query = nil
|
|
parts.fragment = nil
|
|
guard let url = parts.url else { throw APIError.invalidBaseURL }
|
|
return url
|
|
}
|
|
func agentAuthFrame(after: String) throws -> String {
|
|
let token = authToken?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let value: JSONValue = .object([
|
|
"type": .string("auth"),
|
|
"token": token.flatMap { $0.isEmpty ? nil : JSONValue.string($0) } ?? .null,
|
|
"after": .string(try AgentCursor.normalize(after))
|
|
])
|
|
return String(decoding: try JSONEncoder().encode(value), as: UTF8.self)
|
|
}
|
|
}
|