Files
openclaw/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatGatewayPayloadCodec.swift
T
Peter SteinbergerandGitHub d7de67ae02 feat: ask_user follow-ups — harness convergence, channel finalization + reactions, native cards, docked web panel (#110372)
* feat(ask-user): follow-up harness slice

* feat(ask-user): follow-up channels slice

* feat(ask-user): follow-up native slice

* feat(ui): dock question panel above composer with stepper and compact stream summaries

* docs: refresh follow-up integration maps

* test(ui): align terminal summary proof

* fix(infra): echo declared option answers in terminal status when free-text is allowed

* fix(infra): keep reaction answering when display labels are formatter-adjusted

* fix(agents): settle plain-text claims only after question registration commits

* fix(agents,apps): commit-ordered claim persistence, claim-aware prompt delivery, non-blocking question refresh

* fix(harness,infra): reaction-appropriate question copy, caller presentations honored

* fix(native,infra,agents): local-expiry eviction, value-addressed reactions, reserve-before-request

* fix(infra,android): dual-mode question resolver for compact callbacks; reset terminal retention on replayed pending

* fix(harness,discord): claim-aware prompt delivery in run helper; escape finalization labels

* fix(macos): merge transient-content visibility with question cards after main sync

* fix: repair ask user follow-up CI

* test: update limited bootstrap scope expectation

* fix: retain shared question card API

* chore: refresh native i18n inventory
2026-07-18 03:32:39 -04:00

159 lines
6.1 KiB
Swift

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 {
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 decoded.models.map(self.modelChoice)
}
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) -> OpenClawChatModelChoice {
let name = model.name.trimmingCharacters(in: .whitespacesAndNewlines)
return OpenClawChatModelChoice(
modelID: model.id,
name: name.isEmpty ? model.id : model.name,
provider: model.provider,
contextWindow: model.contextwindow,
reasoning: model.reasoning)
}
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 "sessions.changed":
guard let payload = frame.payload,
let change = try? GatewayPayloadDecoding.decode(
payload,
as: OpenClawChatSessionsChangedEvent.self)
else { return nil }
return .sessionsChanged(change)
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 }
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 "question.requested":
guard let payload = frame.payload,
let question = try? GatewayPayloadDecoding.decode(payload, as: QuestionRecord.self)
else { return nil }
return .questionRequested(question)
case "question.resolved":
guard let payload = frame.payload,
let resolved = try? GatewayPayloadDecoding.decode(
payload,
as: OpenClawQuestionResolvedEvent.self)
else { return nil }
return .questionResolved(resolved)
default:
return nil
}
}
}