import Foundation import OpenClawKit import OpenClawProtocol public enum OpenClawChatSessionKey { public static func agentID(from sessionKey: String?) -> String? { let parts = (sessionKey ?? "") .trimmingCharacters(in: .whitespacesAndNewlines) .split(separator: ":", omittingEmptySubsequences: false) guard parts.count >= 3, parts[0].lowercased() == "agent" else { return nil } let agentID = String(parts[1]).trimmingCharacters(in: .whitespacesAndNewlines) return agentID.isEmpty ? nil : agentID } } /// Canonical gateway payload mapping shared by the native Apple chat transports. public enum OpenClawChatGatewayPayloadCodec { public static func decodeSessionsList(_ data: Data, agentID: String?) throws -> OpenClawChatSessionsListResponse { let decoded = try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: data) return OpenClawChatSessionsListResponse( ts: decoded.ts, path: decoded.path, count: decoded.count, totalCount: decoded.totalCount, offset: decoded.offset, nextOffset: decoded.nextOffset, hasMore: decoded.hasMore, defaults: decoded.defaults, sessions: decoded.sessions.map { row in var row = row row.agentId = OpenClawChatSessionKey.agentID(from: row.key) ?? row.agentId ?? agentID return row }) } public static func decodeAgentsList(_ data: Data) throws -> OpenClawChatAgentsListResponse { let result = try JSONDecoder().decode(AgentsListResult.self, from: data) return OpenClawChatAgentsListResponse( defaultId: result.defaultid, agents: result.agents.filter(\.isSelectableAgent).map { OpenClawChatAgentChoice( id: $0.id, name: $0.name, emoji: $0.identity?["emoji"]?.value as? String, workspaceGit: $0.workspacegit) }, sessionRoutingContract: OpenClawChatSessionRoutingContract.make( scope: result.scope.value as? String, mainKey: result.mainkey, defaultAgentID: result.defaultid)) } public static func decodeProgressCard(_ data: Data, agentID: String?) throws -> ProgressCard? { let result = try JSONDecoder().decode(ProgressCardGetResult.self, from: data) guard !(result.card.value is NSNull) else { return nil } let card = try GatewayPayloadDecoding.decode(result.card, as: ProgressCard.self) if let agentID, OpenClawChatSessionKey.agentID(from: card.sessionkey)?.lowercased() != agentID.lowercased() { throw NSError(domain: "OpenClawChatTransport", code: 0, userInfo: [ NSLocalizedDescriptionKey: "Progress card response belongs to another agent.", ]) } return card } public static func decodeQuestionAnswer(_ data: Data) throws -> QuestionAnswers { struct AnsweredQuestion: Decodable { enum Status: String, Decodable { case answered } let status: Status let answers: QuestionAnswers } return try JSONDecoder().decode(AnsweredQuestion.self, from: data).answers } private struct AgentWaitResponse: Decodable { var status: String? var endedAt: Double? var error: String? var stopReason: String? var livenessState: String? var yielded: Bool? var pendingError: Bool? var timeoutPhase: String? var providerStarted: Bool? var aborted: Bool? } public static func decodeAgentWaitObservation(_ data: Data) throws -> OpenClawChatRunObservation { let decoded = try JSONDecoder().decode(AgentWaitResponse.self, from: data) return OpenClawChatRunObservation.fromWaitResponse( status: decoded.status, endedAt: decoded.endedAt, error: decoded.error, stopReason: decoded.stopReason, livenessState: decoded.livenessState, yielded: decoded.yielded, pendingError: decoded.pendingError, timeoutPhase: decoded.timeoutPhase, providerStarted: decoded.providerStarted, aborted: decoded.aborted) } public static func decodeModelChoices(_ data: Data) throws -> [OpenClawChatModelChoice] { let decoded = try JSONDecoder().decode(ModelsListResult.self, from: data) return try decoded.models.map(self.modelChoice) } public static func decodeModelCatalog(_ data: Data) throws -> OpenClawChatModelCatalogSnapshot { let decoded = try JSONDecoder().decode(ModelsListResult.self, from: data) return try OpenClawChatModelCatalogSnapshot( choices: decoded.models.map(self.modelChoice), availabilityIsSessionScoped: true, refreshFailed: decoded.refreshfailed == true) } public static func decodeSessionRoutingIdentity(_ data: Data) throws -> OpenClawChatSessionRoutingIdentity { let decoded = try JSONDecoder().decode(AgentsListResult.self, from: data) guard let identity = OpenClawChatSessionRoutingIdentity( scope: decoded.scope.value as? String, mainSessionKey: decoded.mainkey, defaultAgentID: decoded.defaultid) else { throw CancellationError() } return identity } public static func modelChoice(_ model: ModelChoice) throws -> OpenClawChatModelChoice { let name = model.name.trimmingCharacters(in: .whitespacesAndNewlines) return try OpenClawChatModelChoice( modelID: model.id, name: name.isEmpty ? model.id : model.name, provider: model.provider, available: model.available, manualSelectionAllowed: model.manualselectionallowed, unavailableReason: model.unavailablereason?.value as? String, unavailableUntil: model.unavailableuntil, contextWindow: model.contextwindow, reasoning: model.reasoning, supportsFastMode: model.supportsfastmode, effectiveFastMode: model.effectivefastmode.map { try GatewayPayloadDecoding.decode($0) }, thinkingLevels: model.thinkinglevels.map { try GatewayPayloadDecoding.decode(AnyCodable($0)) }, thinkingDefault: model.thinkingdefault, input: model.input.map { try GatewayPayloadDecoding.decode(AnyCodable($0)) }, agentRuntime: model.agentruntime.map { try GatewayPayloadDecoding.decode(AnyCodable($0)) }) } public static func commandChoice(_ entry: CommandEntry) -> OpenClawChatCommandChoice { let sourceValue = (entry.source.value as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() let source: OpenClawChatCommandChoice.Source = switch sourceValue { case "native": .command case "skill": .skill case "plugin": .plugin default: .unknown } let aliases = (entry.textaliases ?? []) .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } let id = [ source.rawValue, entry.name.trimmingCharacters(in: .whitespacesAndNewlines), aliases.first ?? "", ].joined(separator: ":") return OpenClawChatCommandChoice( id: id, name: entry.name, textAliases: aliases, description: entry.description, source: source, acceptsArgs: entry.acceptsargs) } public static func event(from frame: EventFrame) -> OpenClawChatTransportEvent? { switch frame.event { case "tick": return .tick case "chat.metadata.changed", "config.changed": return .chatMetadataChanged case "sessions.changed": guard let payload = frame.payload, let change = try? GatewayPayloadDecoding.decode( payload, as: OpenClawChatSessionsChangedEvent.self) else { return nil } return .sessionsChanged(change) case "session.observer": guard let payload = frame.payload, let digest = try? GatewayPayloadDecoding.decode( payload, as: SessionObserverDigest.self) else { return nil } return .sessionObserver(digest) case "seqGap": return .seqGap case "health": guard let payload = frame.payload else { return nil } let ok = (try? GatewayPayloadDecoding.decode( payload, as: OpenClawGatewayHealthOK.self))?.ok ?? true return .health(ok: ok) case "chat": guard let payload = frame.payload, let chat = try? GatewayPayloadDecoding.decode( payload, as: OpenClawChatEventPayload.self) else { return nil } return .chat(chat) case "session.message": guard let payload = frame.payload, let message = try? GatewayPayloadDecoding.decode( payload, as: OpenClawSessionMessageEventPayload.self) else { return nil } if var canonicalMessage = message.message, canonicalMessage.transcriptMessageID? .trimmingCharacters(in: .whitespacesAndNewlines).isEmpty != false, let messageID = message.messageId?.trimmingCharacters(in: .whitespacesAndNewlines), !messageID.isEmpty { // Live events carry durable transcript identity on their envelope. // Preserve it on the row so history cannot replay the same message. canonicalMessage.transcriptMessageID = messageID return .sessionMessage(OpenClawSessionMessageEventPayload( sessionKey: message.sessionKey, agentId: message.agentId, message: canonicalMessage, messageId: message.messageId, messageSeq: message.messageSeq, hasActiveRun: message.hasActiveRun, activeRunIds: message.activeRunIds, activeRunIdsPresent: message.activeRunIdsPresent)) } return .sessionMessage(message) case "agent": guard let payload = frame.payload, let agent = try? GatewayPayloadDecoding.decode( payload, as: OpenClawAgentEventPayload.self) else { return nil } return .agent(agent) case "progressCard.changed": guard let payload = frame.payload, let event = try? GatewayPayloadDecoding.decode( payload, as: ProgressCardChangedEvent.self) else { return nil } return .progressCardChanged(event) default: return self.secondaryEvent(from: frame) } } private static func secondaryEvent(from frame: EventFrame) -> OpenClawChatTransportEvent? { guard let payload = frame.payload else { return nil } switch frame.event { case "task": return (try? GatewayPayloadDecoding.decode(payload, as: OpenClawChatTaskEvent.self)) .map(OpenClawChatTransportEvent.task) case "question.requested": return (try? GatewayPayloadDecoding.decode(payload, as: QuestionRecord.self)) .map(OpenClawChatTransportEvent.questionRequested) case "question.resolved": return (try? GatewayPayloadDecoding.decode(payload, as: OpenClawQuestionResolvedEvent.self)) .map(OpenClawChatTransportEvent.questionResolved) default: return nil } } }