Files
Sybil-2/ios/Packages/Sybil/Sources/Sybil/SybilAgentTransport.swift
T
buzzert 4389f805f4
TestFlight / Build and upload (push) Successful in 5m29s
ios: add persistent Sybil Agent room
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.
2026-10-14 13:34:37 -07:00

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)
}
}