refactor(#1375): typed params for approval.* and session.active_list (#1385)

This commit is contained in:
m57 authored and GitHub committed 2026-09-29 18:51:02 +03:00
1 parent c5b34ff471
commit 717b5b0361
11 files changed
+611 -63

No files matched your search

@@ -3,6 +3,8 @@ package com.m57.hermescontrol.data.ws
import com.m57.hermescontrol.data.model.ActiveSessionsResponse
import com.m57.hermescontrol.data.model.LiveSessionSnapshot
import com.m57.hermescontrol.data.model.SessionLiveStatus
import com.m57.hermescontrol.data.ws.contract.RpcMethods
import com.m57.hermescontrol.data.ws.contract.SessionActiveListParams
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -148,8 +150,8 @@ object SessionLiveStatusDecoder {
* Production implementation of [SessionLiveStatusSource] backed by [HermesWsClient].
*/
class HermesSessionLiveStatusSource(
private val rpcRequest: suspend (method: String, params: Map<String, Any>) -> Any? = { method, params ->
HermesWsClient.request(method, params).await()
private val rpcRequest: suspend () -> Any? = {
HermesWsClient.call(RpcMethods.SESSION_ACTIVE_LIST, SessionActiveListParams())
},
eventsProvider: () -> Flow<WsEvent> = { HermesWsClient.events },
connectionStatusProvider: () -> StateFlow<ConnectionStatus> = { HermesWsClient.connectionStatus },
@@ -159,7 +161,7 @@ class HermesSessionLiveStatusSource(
override suspend fun fetchActiveSessionsSnapshot(): LiveSessionSnapshot? =
try {
val raw = rpcRequest(WsMethods.SESSION_ACTIVE_LIST, emptyMap())
val raw = rpcRequest()
SessionLiveStatusDecoder.decodeSnapshot(raw)
} catch (e: CancellationException) {
throw e
@@ -0,0 +1,28 @@
package com.m57.hermescontrol.data.ws.contract
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ApprovalPendingParams(
@SerialName("session_id") val sessionId: String,
)
@Serializable
data class ApprovalReceivedParams(
@SerialName("session_id") val sessionId: String,
@SerialName("request_id") val requestId: String,
)
@Serializable
data class ApprovalRespondParams(
@SerialName("session_id") val sessionId: String,
@SerialName("choice") val choice: String? = null,
@SerialName("all") val all: Boolean? = null,
@SerialName("request_id") val requestId: String? = null,
)
@Serializable
data class SessionActiveListParams(
@SerialName("current_session_id") val currentSessionId: String? = null,
)
@@ -10,6 +10,14 @@ class RpcMethod<P, R>(
val result: KSerializer<R>,
)
interface TypedRpcSender {
fun <P> send(
method: RpcMethod<P, *>,
params: P,
onSent: ((String) -> Unit)? = null,
): String
}
object RpcMethods {
val SESSION_EVENTS_SINCE: RpcMethod<SessionEventsSinceParams, SessionEventsSinceResult> =
RpcMethod(
@@ -110,6 +118,38 @@ object RpcMethods {
PromptBtwResult.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy parsers.
val APPROVAL_PENDING: RpcMethod<ApprovalPendingParams, JsonElement> =
RpcMethod(
WsMethods.APPROVAL_PENDING,
ApprovalPendingParams.serializer(),
JsonElement.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy parsers.
val APPROVAL_RECEIVED: RpcMethod<ApprovalReceivedParams, JsonElement> =
RpcMethod(
WsMethods.APPROVAL_RECEIVED,
ApprovalReceivedParams.serializer(),
JsonElement.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy parsers.
val APPROVAL_RESPOND: RpcMethod<ApprovalRespondParams, JsonElement> =
RpcMethod(
WsMethods.APPROVAL_RESPOND,
ApprovalRespondParams.serializer(),
JsonElement.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy parsers.
val SESSION_ACTIVE_LIST: RpcMethod<SessionActiveListParams, JsonElement> =
RpcMethod(
WsMethods.SESSION_ACTIVE_LIST,
SessionActiveListParams.serializer(),
JsonElement.serializer(),
)
/** Every typed method. GatewayContractTest iterates this; future migrations append here. */
val all: List<RpcMethod<*, *>> =
listOf(
@@ -127,5 +167,9 @@ object RpcMethods {
SESSION_CONTEXT_BREAKDOWN,
SESSION_USAGE,
PROMPT_BTW,
APPROVAL_PENDING,
APPROVAL_RECEIVED,
APPROVAL_RESPOND,
SESSION_ACTIVE_LIST,
)
}
@@ -2,6 +2,11 @@ package com.m57.hermescontrol.ui.chat
import com.m57.hermescontrol.data.ws.WsEvent
import com.m57.hermescontrol.data.ws.WsMethods
import com.m57.hermescontrol.data.ws.contract.ApprovalPendingParams
import com.m57.hermescontrol.data.ws.contract.ApprovalReceivedParams
import com.m57.hermescontrol.data.ws.contract.ApprovalRespondParams
import com.m57.hermescontrol.data.ws.contract.RpcMethods
import com.m57.hermescontrol.data.ws.contract.TypedRpcSender
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
@@ -22,7 +27,7 @@ class ChatApprovalsDelegate(
private val ioDispatcher: CoroutineDispatcher,
private val uiState: MutableStateFlow<ChatUiState>,
private val runtimeSessionId: () -> String?,
private val wsSend: (method: String, params: Map<String, Any>, onSent: ((String) -> Unit)?) -> Unit,
private val rpc: TypedRpcSender,
private val trackRequest: (id: String, method: String) -> Unit,
private val addSystemMessage: (text: String) -> Unit,
private val respondToServerRequest: ((String, JsonElement) -> Unit)? = null,
@@ -99,13 +104,12 @@ class ChatApprovalsDelegate(
if (event.serverRequestId == null && requestId != null && sessionId != null) {
scope.launch(ioDispatcher) {
runCatching {
wsSend(
WsMethods.APPROVAL_RECEIVED,
mapOf(
"session_id" to sessionId,
"request_id" to requestId,
rpc.send(
RpcMethods.APPROVAL_RECEIVED,
ApprovalReceivedParams(
sessionId = sessionId,
requestId = requestId,
),
null,
)
}
}
@@ -115,9 +119,9 @@ class ChatApprovalsDelegate(
fun replayPendingApproval(sessionId: String) {
val targetSessionId = runtimeSessionId() ?: sessionId
scope.launch(ioDispatcher) {
wsSend(
WsMethods.APPROVAL_PENDING,
mapOf("session_id" to targetSessionId),
rpc.send(
RpcMethods.APPROVAL_PENDING,
ApprovalPendingParams(sessionId = targetSessionId),
) { id -> trackRequest(id, WsMethods.APPROVAL_PENDING) }
}
}
@@ -190,16 +194,14 @@ class ChatApprovalsDelegate(
)
return@launch
}
val params =
mutableMapOf<String, Any>(
"session_id" to sessionId,
"choice" to choice,
"all" to false,
)
if (requestId != null) params["request_id"] = requestId
wsSend(
WsMethods.APPROVAL_RESPOND,
params,
rpc.send(
RpcMethods.APPROVAL_RESPOND,
ApprovalRespondParams(
sessionId = sessionId,
choice = choice,
all = false,
requestId = requestId,
),
) { id -> trackRequest(id, WsMethods.APPROVAL_RESPOND) }
// The queue can hold more pendings — surface the next one.
replayPendingApproval(sessionId)
@@ -38,6 +38,7 @@ import com.m57.hermescontrol.data.ws.JsonRpcError
import com.m57.hermescontrol.data.ws.WsEvent
import com.m57.hermescontrol.data.ws.WsMethods
import com.m57.hermescontrol.data.ws.contract.PromptBtwParams
import com.m57.hermescontrol.data.ws.contract.RpcMethod
import com.m57.hermescontrol.data.ws.contract.RpcMethods
import com.m57.hermescontrol.data.ws.contract.SessionBranchParams
import com.m57.hermescontrol.data.ws.contract.SessionBranchWholeParams
@@ -48,6 +49,7 @@ import com.m57.hermescontrol.data.ws.contract.SessionIdParams
import com.m57.hermescontrol.data.ws.contract.SessionInterruptParams
import com.m57.hermescontrol.data.ws.contract.SessionListParams
import com.m57.hermescontrol.data.ws.contract.SessionResumeParams
import com.m57.hermescontrol.data.ws.contract.TypedRpcSender
import com.m57.hermescontrol.data.ws.toAny
import com.m57.hermescontrol.data.ws.toJsonElement
import com.m57.hermescontrol.notification.ReplyNotificationTracker
@@ -711,7 +713,14 @@ class ChatViewModel(
ioDispatcher = ioDispatcher,
uiState = _uiState,
runtimeSessionId = { runtimeSessionId },
wsSend = { method, params, onSent -> wsClient.send(method, params, onSent) },
rpc =
object : TypedRpcSender {
override fun <P> send(
method: RpcMethod<P, *>,
params: P,
onSent: ((String) -> Unit)?,
): String = wsClient.send(method, params, onSent)
},
trackRequest = { id, method -> trackRequest(id, method) },
addSystemMessage = { text -> addSystemMessage(text) },
respondToServerRequest = { id, result -> wsClient.respondToServerRequest(id, result) },