Archived
124 lines
4.4 KiB
Swift
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
|
|
}
|
|
}
|