Merge remote-tracking branch 'origin/main'

This commit is contained in:
Peter Steinberger
2025-12-12 16:39:27 +00:00
20 changed files with 726 additions and 149 deletions

View File

@@ -62,17 +62,19 @@ actor AgentRPC {
func send(
text: String,
thinking: String?,
session: String,
sessionKey: String,
deliver: Bool,
to: String?) async -> (ok: Bool, text: String?, error: String?)
to: String?,
channel: String? = nil) async -> (ok: Bool, text: String?, error: String?)
{
do {
let params: [String: Any] = [
"message": text,
"sessionId": session,
"sessionKey": sessionKey,
"thinking": thinking ?? "default",
"deliver": deliver,
"to": to ?? "",
"channel": channel ?? "",
"idempotencyKey": UUID().uuidString,
]
_ = try await self.controlRequest(method: "agent", params: ControlRequestParams(raw: params))

View File

@@ -57,9 +57,10 @@ enum ControlRequestHandler {
let rpcResult = await AgentRPC.shared.send(
text: trimmed,
thinking: thinking,
session: sessionKey,
sessionKey: sessionKey,
deliver: deliver,
to: to)
to: to,
channel: nil)
return rpcResult.ok
? Response(ok: true, message: rpcResult.text ?? "sent")
: Response(ok: false, message: rpcResult.error ?? "failed to send")

View File

@@ -33,10 +33,11 @@ enum VoiceWakeForwarder {
}
struct ForwardOptions: Sendable {
var session: String = "main"
var sessionKey: String = "main"
var thinking: String = "low"
var deliver: Bool = true
var to: String?
var channel: String = "last"
}
@discardableResult
@@ -45,12 +46,15 @@ enum VoiceWakeForwarder {
options: ForwardOptions = ForwardOptions()) async -> Result<Void, VoiceWakeForwardError>
{
let payload = Self.prefixedTranscript(transcript)
let channel = options.channel.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let deliver = options.deliver && channel != "webchat"
let result = await AgentRPC.shared.send(
text: payload,
thinking: options.thinking,
session: options.session,
deliver: options.deliver,
to: options.to)
sessionKey: options.sessionKey,
deliver: deliver,
to: options.to,
channel: channel)
if result.ok {
self.logger.info("voice wake forward ok")

View File

@@ -28,8 +28,8 @@ public struct Hello: Codable {
caps: [String]?,
auth: [String: AnyCodable]?,
locale: String?,
useragent: String?)
{
useragent: String?
) {
self.type = type
self.minprotocol = minprotocol
self.maxprotocol = maxprotocol
@@ -39,7 +39,6 @@ public struct Hello: Codable {
self.locale = locale
self.useragent = useragent
}
private enum CodingKeys: String, CodingKey {
case type
case minprotocol = "minProtocol"
@@ -66,8 +65,8 @@ public struct HelloOk: Codable {
server: [String: AnyCodable],
features: [String: AnyCodable],
snapshot: Snapshot,
policy: [String: AnyCodable])
{
policy: [String: AnyCodable]
) {
self.type = type
self._protocol = _protocol
self.server = server
@@ -75,7 +74,6 @@ public struct HelloOk: Codable {
self.snapshot = snapshot
self.policy = policy
}
private enum CodingKeys: String, CodingKey {
case type
case _protocol = "protocol"
@@ -96,14 +94,13 @@ public struct HelloError: Codable {
type: String,
reason: String,
expectedprotocol: Int?,
minclient: String?)
{
minclient: String?
) {
self.type = type
self.reason = reason
self.expectedprotocol = expectedprotocol
self.minclient = minclient
}
private enum CodingKeys: String, CodingKey {
case type
case reason
@@ -122,14 +119,13 @@ public struct RequestFrame: Codable {
type: String,
id: String,
method: String,
params: AnyCodable?)
{
params: AnyCodable?
) {
self.type = type
self.id = id
self.method = method
self.params = params
}
private enum CodingKeys: String, CodingKey {
case type
case id
@@ -150,15 +146,14 @@ public struct ResponseFrame: Codable {
id: String,
ok: Bool,
payload: AnyCodable?,
error: [String: AnyCodable]?)
{
error: [String: AnyCodable]?
) {
self.type = type
self.id = id
self.ok = ok
self.payload = payload
self.error = error
}
private enum CodingKeys: String, CodingKey {
case type
case id
@@ -180,15 +175,14 @@ public struct EventFrame: Codable {
event: String,
payload: AnyCodable?,
seq: Int?,
stateversion: [String: AnyCodable]?)
{
stateversion: [String: AnyCodable]?
) {
self.type = type
self.event = event
self.payload = payload
self.seq = seq
self.stateversion = stateversion
}
private enum CodingKeys: String, CodingKey {
case type
case event
@@ -220,8 +214,8 @@ public struct PresenceEntry: Codable {
tags: [String]?,
text: String?,
ts: Int,
instanceid: String?)
{
instanceid: String?
) {
self.host = host
self.ip = ip
self.version = version
@@ -233,7 +227,6 @@ public struct PresenceEntry: Codable {
self.ts = ts
self.instanceid = instanceid
}
private enum CodingKeys: String, CodingKey {
case host
case ip
@@ -254,12 +247,11 @@ public struct StateVersion: Codable {
public init(
presence: Int,
health: Int)
{
health: Int
) {
self.presence = presence
self.health = health
}
private enum CodingKeys: String, CodingKey {
case presence
case health
@@ -276,14 +268,13 @@ public struct Snapshot: Codable {
presence: [PresenceEntry],
health: AnyCodable,
stateversion: StateVersion,
uptimems: Int)
{
uptimems: Int
) {
self.presence = presence
self.health = health
self.stateversion = stateversion
self.uptimems = uptimems
}
private enum CodingKeys: String, CodingKey {
case presence
case health
@@ -304,15 +295,14 @@ public struct ErrorShape: Codable {
message: String,
details: AnyCodable?,
retryable: Bool?,
retryafterms: Int?)
{
retryafterms: Int?
) {
self.code = code
self.message = message
self.details = details
self.retryable = retryable
self.retryafterms = retryafterms
}
private enum CodingKeys: String, CodingKey {
case code
case message
@@ -334,15 +324,14 @@ public struct AgentEvent: Codable {
seq: Int,
stream: String,
ts: Int,
data: [String: AnyCodable])
{
data: [String: AnyCodable]
) {
self.runid = runid
self.seq = seq
self.stream = stream
self.ts = ts
self.data = data
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case seq
@@ -364,15 +353,14 @@ public struct SendParams: Codable {
message: String,
mediaurl: String?,
provider: String?,
idempotencykey: String)
{
idempotencykey: String
) {
self.to = to
self.message = message
self.mediaurl = mediaurl
self.provider = provider
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case to
case message
@@ -386,8 +374,10 @@ public struct AgentParams: Codable {
public let message: String
public let to: String?
public let sessionid: String?
public let sessionkey: String?
public let thinking: String?
public let deliver: Bool?
public let channel: String?
public let timeout: Int?
public let idempotencykey: String
@@ -395,40 +385,135 @@ public struct AgentParams: Codable {
message: String,
to: String?,
sessionid: String?,
sessionkey: String?,
thinking: String?,
deliver: Bool?,
channel: String?,
timeout: Int?,
idempotencykey: String)
{
idempotencykey: String
) {
self.message = message
self.to = to
self.sessionid = sessionid
self.sessionkey = sessionkey
self.thinking = thinking
self.deliver = deliver
self.channel = channel
self.timeout = timeout
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case message
case to
case sessionid = "sessionId"
case sessionkey = "sessionKey"
case thinking
case deliver
case channel
case timeout
case idempotencykey = "idempotencyKey"
}
}
public struct ChatHistoryParams: Codable {
public let sessionkey: String
public init(
sessionkey: String
) {
self.sessionkey = sessionkey
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
}
}
public struct ChatSendParams: Codable {
public let sessionkey: String
public let message: String
public let thinking: String?
public let deliver: Bool?
public let attachments: [AnyCodable]?
public let timeoutms: Int?
public let idempotencykey: String
public init(
sessionkey: String,
message: String,
thinking: String?,
deliver: Bool?,
attachments: [AnyCodable]?,
timeoutms: Int?,
idempotencykey: String
) {
self.sessionkey = sessionkey
self.message = message
self.thinking = thinking
self.deliver = deliver
self.attachments = attachments
self.timeoutms = timeoutms
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case message
case thinking
case deliver
case attachments
case timeoutms = "timeoutMs"
case idempotencykey = "idempotencyKey"
}
}
public struct ChatEvent: Codable {
public let runid: String
public let sessionkey: String
public let seq: Int
public let state: AnyCodable
public let message: AnyCodable?
public let errormessage: String?
public let usage: AnyCodable?
public let stopreason: String?
public init(
runid: String,
sessionkey: String,
seq: Int,
state: AnyCodable,
message: AnyCodable?,
errormessage: String?,
usage: AnyCodable?,
stopreason: String?
) {
self.runid = runid
self.sessionkey = sessionkey
self.seq = seq
self.state = state
self.message = message
self.errormessage = errormessage
self.usage = usage
self.stopreason = stopreason
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case sessionkey = "sessionKey"
case seq
case state
case message
case errormessage = "errorMessage"
case usage
case stopreason = "stopReason"
}
}
public struct TickEvent: Codable {
public let ts: Int
public init(
ts: Int)
{
ts: Int
) {
self.ts = ts
}
private enum CodingKeys: String, CodingKey {
case ts
}
@@ -440,12 +525,11 @@ public struct ShutdownEvent: Codable {
public init(
reason: String,
restartexpectedms: Int?)
{
restartexpectedms: Int?
) {
self.reason = reason
self.restartexpectedms = restartexpectedms
}
private enum CodingKeys: String, CodingKey {
case reason
case restartexpectedms = "restartExpectedMs"
@@ -469,17 +553,17 @@ public enum GatewayFrame: Codable {
}
switch type {
case "hello":
self = try .hello(Self.decodePayload(Hello.self, from: raw))
self = .hello(try Self.decodePayload(Hello.self, from: raw))
case "hello-ok":
self = try .helloOk(Self.decodePayload(HelloOk.self, from: raw))
self = .helloOk(try Self.decodePayload(HelloOk.self, from: raw))
case "hello-error":
self = try .helloError(Self.decodePayload(HelloError.self, from: raw))
self = .helloError(try Self.decodePayload(HelloError.self, from: raw))
case "req":
self = try .req(Self.decodePayload(RequestFrame.self, from: raw))
self = .req(try Self.decodePayload(RequestFrame.self, from: raw))
case "res":
self = try .res(Self.decodePayload(ResponseFrame.self, from: raw))
self = .res(try Self.decodePayload(ResponseFrame.self, from: raw))
case "event":
self = try .event(Self.decodePayload(EventFrame.self, from: raw))
self = .event(try Self.decodePayload(EventFrame.self, from: raw))
default:
self = .unknown(type: type, raw: raw)
}
@@ -487,26 +571,25 @@ public enum GatewayFrame: Codable {
public func encode(to encoder: Encoder) throws {
switch self {
case let .hello(v): try v.encode(to: encoder)
case let .helloOk(v): try v.encode(to: encoder)
case let .helloError(v): try v.encode(to: encoder)
case let .req(v): try v.encode(to: encoder)
case let .res(v): try v.encode(to: encoder)
case let .event(v): try v.encode(to: encoder)
case let .unknown(_, raw):
case .hello(let v): try v.encode(to: encoder)
case .helloOk(let v): try v.encode(to: encoder)
case .helloError(let v): try v.encode(to: encoder)
case .req(let v): try v.encode(to: encoder)
case .res(let v): try v.encode(to: encoder)
case .event(let v): try v.encode(to: encoder)
case .unknown(_, let raw):
var container = encoder.singleValueContainer()
try container.encode(raw)
}
}
private static func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
// Re-encode the already-decoded map using `JSONEncoder` instead of
// `JSONSerialization` because `AnyCodable` values are not bridged to
// Objective-C types and `JSONSerialization` throws an ObjC exception,
// crashing the app (seen on macOS 26.1). `JSONEncoder` understands
// `Encodable` values and stays in Swift land.
// raw is [String: AnyCodable] which is not directly JSONSerialization-compatible.
// Round-trip through JSONEncoder so AnyCodable can encode itself safely.
let data = try JSONEncoder().encode(raw)
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
}

View File

@@ -1,6 +0,0 @@
// Legacy shim: Protocol definitions now live in GatewayModels.swift generated from TypeBox.
// Kept to satisfy existing project references.
import Foundation
@available(*, deprecated, message: "Use GatewayModels.swift (GatewayFrame and payload structs)")
public enum LegacyProtocolShim {}

View File

@@ -13,9 +13,10 @@ import Testing
@Test func forwardOptionsDefaults() {
let opts = VoiceWakeForwarder.ForwardOptions()
#expect(opts.session == "main")
#expect(opts.sessionKey == "main")
#expect(opts.thinking == "low")
#expect(opts.deliver == true)
#expect(opts.to == nil)
#expect(opts.channel == "last")
}
}