import Foundation import OSLog struct ControlRequestParams: @unchecked Sendable { let raw: [String: AnyHashable] } actor AgentRPC { static let shared = AgentRPC() private let logger = Logger(subsystem: "com.steipete.clawdis", category: "agent.rpc") private let gateway = GatewayChannel() private var configured = false private var gatewayURL: URL { let port = GatewayEnvironment.gatewayPort() return URL(string: "ws://127.0.0.1:\(port)")! } private var gatewayToken: String? { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] } func start() async throws { if self.configured { return } await self.gateway.configure(url: self.gatewayURL, token: self.gatewayToken) self.configured = true } func shutdown() async { // no-op for WS; socket managed by GatewayChannel } func setHeartbeatsEnabled(_ enabled: Bool) async -> Bool { do { _ = try await self.controlRequest( method: "set-heartbeats", params: ControlRequestParams(raw: ["enabled": AnyHashable(enabled)])) return true } catch { self.logger.error("setHeartbeatsEnabled failed \(error.localizedDescription, privacy: .public)") return false } } func status() async -> (ok: Bool, error: String?) { do { let data = try await controlRequest(method: "status") if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], (obj["ok"] as? Bool) ?? true { return (true, nil) } return (false, "status error") } catch { return (false, error.localizedDescription) } } func send( text: String, thinking: String?, session: String, deliver: Bool, to: String?) async -> (ok: Bool, text: String?, error: String?) { do { let params: [String: AnyHashable] = [ "message": AnyHashable(text), "sessionId": AnyHashable(session), "thinking": AnyHashable(thinking ?? "default"), "deliver": AnyHashable(deliver), "to": AnyHashable(to ?? ""), "idempotencyKey": AnyHashable(UUID().uuidString), ] _ = try await self.controlRequest(method: "agent", params: ControlRequestParams(raw: params)) return (true, nil, nil) } catch { return (false, nil, error.localizedDescription) } } func controlRequest(method: String, params: ControlRequestParams? = nil) async throws -> Data { try await self.start() let rawParams = params?.raw.reduce(into: [String: AnyCodable]()) { $0[$1.key] = AnyCodable($1.value) } return try await self.gateway.request(method: method, params: rawParams) } }