This commit is contained in:
13 files changed
+1452
-2
No files matched your search
@@ -0,0 +1,52 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
/**
|
||||
* Item returned in the [SubagentListResponse] from `subagent.list` (issue #1089).
|
||||
*/
|
||||
@Serializable
|
||||
data class SubagentListItem(
|
||||
@SerialName("subagent_id") val subagentId: String = "",
|
||||
val goal: String? = null,
|
||||
val status: String? = null,
|
||||
val model: String? = null,
|
||||
@SerialName("elapsed_seconds") val elapsedSeconds: Double? = null,
|
||||
@SerialName("started_at") val startedAt: Double? = null,
|
||||
@SerialName("parent_id") val parentId: String? = null,
|
||||
val depth: Int? = null,
|
||||
@SerialName("delegation_id") val delegationId: String? = null,
|
||||
@SerialName("tool_count") val toolCount: Int? = null,
|
||||
@SerialName("last_tool") val lastTool: String? = null,
|
||||
@SerialName("accepting_steer") val acceptingSteer: Boolean? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Response returned from `subagent.list` (issue #1089).
|
||||
*/
|
||||
@Serializable
|
||||
data class SubagentListResponse(
|
||||
val subagents: List<SubagentListItem> = emptyList(),
|
||||
val delegations: List<JsonElement> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Response returned from `subagent.tail` (issue #1089).
|
||||
*
|
||||
* Backend returns either `text` (desktop/TUI gateway) or `tail` (contract).
|
||||
* [content] returns the authoritative transcript text from whichever field is present.
|
||||
*/
|
||||
@Serializable
|
||||
data class SubagentTailResponse(
|
||||
@SerialName("subagent_id") val subagentId: String = "",
|
||||
val tail: String? = null,
|
||||
val text: String? = null,
|
||||
val available: Boolean? = null,
|
||||
val truncated: Boolean? = null,
|
||||
@SerialName("bytes_read") val bytesRead: Long? = null,
|
||||
) {
|
||||
/** Returns authoritative transcript text across backend schema variants. */
|
||||
fun content(): String = text ?: tail ?: ""
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.m57.hermescontrol.data.ws
|
||||
|
||||
import android.util.Log
|
||||
import com.m57.hermescontrol.data.model.SubagentListResponse
|
||||
import com.m57.hermescontrol.data.model.SubagentTailResponse
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
/**
|
||||
* WebSocket RPC repository for subagent operations: `subagent.list` and `subagent.tail` (issue #1089).
|
||||
*/
|
||||
object SubagentRepository {
|
||||
private const val TAG = "SubagentRepository"
|
||||
|
||||
private val json =
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the current active subagent roster for a given [sessionId].
|
||||
*
|
||||
* Returns null if RPC fails or is unsupported by server.
|
||||
*/
|
||||
suspend fun listSubagents(sessionId: String): SubagentListResponse? {
|
||||
if (sessionId.isBlank()) return null
|
||||
return try {
|
||||
val result =
|
||||
HermesWsClient
|
||||
.request(
|
||||
WsMethods.SUBAGENT_LIST,
|
||||
mapOf("session_id" to sessionId),
|
||||
).await()
|
||||
decode<SubagentListResponse>(result)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "subagent.list request failed for session $sessionId: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a rolling live transcript tail for [subagentId].
|
||||
*
|
||||
* Supplies [sessionId] when available so gateway authority checks pass cleanly.
|
||||
* Throws or returns null if unavailable.
|
||||
*/
|
||||
suspend fun tailSubagent(
|
||||
sessionId: String,
|
||||
subagentId: String,
|
||||
maxBytes: Int = 16384,
|
||||
): SubagentTailResponse? {
|
||||
if (subagentId.isBlank()) return null
|
||||
val params =
|
||||
mutableMapOf<String, Any>(
|
||||
"subagent_id" to subagentId,
|
||||
"max_bytes" to maxBytes,
|
||||
)
|
||||
if (sessionId.isNotBlank()) {
|
||||
params["session_id"] = sessionId
|
||||
}
|
||||
val result = HermesWsClient.request(WsMethods.SUBAGENT_TAIL, params).await()
|
||||
return decode<SubagentTailResponse>(result)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal inline fun <reified T> decode(result: Any?): T? {
|
||||
if (result == null) return null
|
||||
val element: JsonElement =
|
||||
when (result) {
|
||||
is JsonElement -> {
|
||||
result
|
||||
}
|
||||
|
||||
is Map<*, *> -> {
|
||||
anyToJsonElement(result)
|
||||
}
|
||||
|
||||
is List<*> -> {
|
||||
JsonArray(result.map { anyToJsonElement(it) })
|
||||
}
|
||||
|
||||
else -> {
|
||||
val str = result.toString()
|
||||
try {
|
||||
json.parseToJsonElement(str)
|
||||
} catch (_: Exception) {
|
||||
JsonPrimitive(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return json.decodeFromJsonElement(serializer<T>(), element)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun anyToJsonElement(value: Any?): JsonElement =
|
||||
when (value) {
|
||||
null -> {
|
||||
JsonNull
|
||||
}
|
||||
|
||||
is JsonElement -> {
|
||||
value
|
||||
}
|
||||
|
||||
is Map<*, *> -> {
|
||||
JsonObject(
|
||||
(value as Map<String, Any?>).mapValues { (_, v) -> anyToJsonElement(v) },
|
||||
)
|
||||
}
|
||||
|
||||
is List<*> -> {
|
||||
JsonArray(value.map { anyToJsonElement(it) })
|
||||
}
|
||||
|
||||
is String -> {
|
||||
JsonPrimitive(value)
|
||||
}
|
||||
|
||||
is Boolean -> {
|
||||
JsonPrimitive(value)
|
||||
}
|
||||
|
||||
is Number -> {
|
||||
JsonPrimitive(value)
|
||||
}
|
||||
|
||||
else -> {
|
||||
JsonPrimitive(value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,14 @@ object WsMethods {
|
||||
/** Kill a single background process (scoped to the active session). */
|
||||
const val PROCESS_KILL = "process.kill"
|
||||
|
||||
// ── Subagents (issue #1089) ───────────────────────────────────────────
|
||||
|
||||
/** List active subagents and delegations for a session. */
|
||||
const val SUBAGENT_LIST = "subagent.list"
|
||||
|
||||
/** Rolling tail snapshot of a subagent's live execution transcript. */
|
||||
const val SUBAGENT_TAIL = "subagent.tail"
|
||||
|
||||
// ── Billing / subscription (issue #628) ─────────────────────────────
|
||||
// Adopted from the backend release audit (hermes-agent 0bf44d557..614dc194e).
|
||||
// `credits.view` was REMOVED upstream; these replace it. `usage.bars`
|
||||
|
||||
Reference in new issue
Block a user