This repository has been archived on 2026-10-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
hope-voice-ios/Sources/LiveKit/ControlChannel.swift
T

124 lines
4.4 KiB
Swift

import Foundation
import LiveKit
/// Outbound helpers for agent control topics and inbound parsing of
/// voice-control / conversation-control data messages.
@MainActor
final class ControlChannel: NSObject, RoomDelegate {
enum InboundEvent {
case agentReady
case taskEvent(TaskEventPayload)
}
struct TaskEventPayload: Equatable {
let type: String
let taskId: String?
let description: String?
let role: String?
let content: String?
let result: String?
let error: String?
let createdAt: Double?
}
var onEvent: ((InboundEvent) -> Void)?
private let decoder = JSONDecoder()
/// Inbound voice-control handling. Returns true when the message was consumed.
func handleVoiceControl(_ data: Data, session: VoiceSession) -> Bool {
guard let json = try? decoder.decode([String: JSONValue].self, from: data),
let type = json["type"]?.stringValue else { return false }
switch type {
case "agent_ready":
Task { @MainActor [weak session] in
session?.agentReadyReceived()
}
return true
default:
return false
}
}
/// Inbound task_event handling (topic-agnostic per web UI). The agent publishes
/// {type:"task_event", task_id, event, ...} — the discriminator is `event`, not `type`.
func handleTaskEvent(_ data: Data) -> Bool {
guard let json = try? decoder.decode([String: JSONValue].self, from: data),
json["type"]?.stringValue == "task_event",
let type = json["event"]?.stringValue,
["created", "step", "completed", "failed"].contains(type) else { return false }
let taskObj = json["task"]?.objectValue ?? [:]
let payload = TaskEventPayload(
type: type,
taskId: json["task_id"]?.stringValue,
description: json["description"]?.stringValue ?? taskObj["description"]?.stringValue,
role: json["role"]?.stringValue,
content: json["content"]?.stringValue,
result: json["result"]?.stringValue,
error: json["error"]?.stringValue,
createdAt: json["created_at"]?.doubleValue ?? taskObj["created_at"]?.doubleValue
)
onEvent?(.taskEvent(payload))
return true
}
// MARK: Outbound (topic'd JSON over reliable data streams)
private func publish(_ object: [String: Any], topic: String, room: Room) async throws {
let data = try JSONSerialization.data(withJSONObject: object)
try await room.localParticipant.publish(data: data, options: DataPublishOptions(topic: topic, reliable: true))
}
func setVoice(_ voiceID: String, room: Room) async throws {
try await publish(["type": "set_voice", "voice": voiceID], topic: "voice-control", room: room)
}
func setMode(_ mode: String, room: Room) async throws {
try await publish(["type": "set_mode", "mode": mode], topic: "conversation-control", room: room)
}
}
// MARK: - Minimal JSON value decoder for flexible agent payloads
indirect enum JSONValue: Decodable {
case string(String)
case number(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
case null
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() { self = .null }
else if let value = try? container.decode(Bool.self) { self = .bool(value) }
else if let value = try? container.decode(Double.self) { self = .number(value) }
else if let value = try? container.decode(String.self) { self = .string(value) }
else if let value = try? container.decode([JSONValue].self) { self = .array(value) }
else if let value = try? container.decode([String: JSONValue].self) { self = .object(value) }
else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value") }
}
var stringValue: String? {
if case .string(let value) = self { return value }
return nil
}
var doubleValue: Double? {
if case .number(let value) = self { return value }
return nil
}
var objectValue: [String: JSONValue]? {
if case .object(let value) = self { return value }
return nil
}
var arrayValue: [JSONValue]? {
if case .array(let value) = self { return value }
return nil
}
}