feat(chat/activity): Self-improvement event badges for memory & skill updates (#744)
* feat(chat/activity): Self-improvement event badges for memory & skill updates (#737) * feat(chat): Handle background review.summary WS events for self-improvement notices --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
2cf41ead06
commit
1b596746af
12 files changed
+381
-1
No files matched your search
@@ -0,0 +1,42 @@
|
|||||||
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data structures for the Hermes agent self-improvement / learning graph endpoint
|
||||||
|
* GET /api/learning/graph
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class LearningGraphNode(
|
||||||
|
val id: String,
|
||||||
|
val label: String,
|
||||||
|
val kind: String, // "skill" | "memory"
|
||||||
|
val timestamp: Long? = null,
|
||||||
|
val category: String? = null,
|
||||||
|
@SerialName("useCount") val useCount: Int = 0,
|
||||||
|
val state: String? = null,
|
||||||
|
@SerialName("createdBy") val createdBy: String? = null,
|
||||||
|
val pinned: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LearningGraphEdge(
|
||||||
|
val source: String,
|
||||||
|
val target: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LearningGraphMemoryCard(
|
||||||
|
val source: String,
|
||||||
|
val timestamp: Long? = null,
|
||||||
|
val title: String,
|
||||||
|
val body: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LearningGraphResponse(
|
||||||
|
val nodes: List<LearningGraphNode> = emptyList(),
|
||||||
|
val edges: List<LearningGraphEdge> = emptyList(),
|
||||||
|
val memory: List<LearningGraphMemoryCard> = emptyList(),
|
||||||
|
)
|
||||||
@@ -34,6 +34,7 @@ import com.m57.hermescontrol.data.model.HookResponse
|
|||||||
import com.m57.hermescontrol.data.model.KanbanBoardResponse
|
import com.m57.hermescontrol.data.model.KanbanBoardResponse
|
||||||
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
|
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
|
||||||
import com.m57.hermescontrol.data.model.KanbanTask
|
import com.m57.hermescontrol.data.model.KanbanTask
|
||||||
|
import com.m57.hermescontrol.data.model.LearningGraphResponse
|
||||||
import com.m57.hermescontrol.data.model.LogResponse
|
import com.m57.hermescontrol.data.model.LogResponse
|
||||||
import com.m57.hermescontrol.data.model.ManagedDirectoryCreate
|
import com.m57.hermescontrol.data.model.ManagedDirectoryCreate
|
||||||
import com.m57.hermescontrol.data.model.ManagedFileActionResponse
|
import com.m57.hermescontrol.data.model.ManagedFileActionResponse
|
||||||
@@ -667,6 +668,11 @@ interface HermesApiService {
|
|||||||
suspend fun getPortal(): Response<PortalResponse>
|
suspend fun getPortal(): Response<PortalResponse>
|
||||||
|
|
||||||
// ── Admin: Curator ────────────────────────────────────────────────
|
// ── Admin: Curator ────────────────────────────────────────────────
|
||||||
|
@GET("api/learning/graph")
|
||||||
|
suspend fun getLearningGraph(
|
||||||
|
@Query("profile") profile: String? = null,
|
||||||
|
): Response<LearningGraphResponse>
|
||||||
|
|
||||||
@GET("api/curator")
|
@GET("api/curator")
|
||||||
suspend fun getCurator(): Response<CuratorResponse>
|
suspend fun getCurator(): Response<CuratorResponse>
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,11 @@ object EventParser {
|
|||||||
WsEvent.BackgroundComplete(payload)
|
WsEvent.BackgroundComplete(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"review.summary" -> {
|
||||||
|
val text = (payload?.get("text") as? String)?.trim() ?: ""
|
||||||
|
WsEvent.ReviewSummary(text, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
"session.updated" -> {
|
"session.updated" -> {
|
||||||
WsEvent.SessionUpdated(payload)
|
WsEvent.SessionUpdated(payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,16 @@ sealed class WsEvent {
|
|||||||
val sessionId: String? = null,
|
val sessionId: String? = null,
|
||||||
) : WsEvent()
|
) : WsEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Self-improvement background review summary event.
|
||||||
|
* Emitted when background review patches a skill or saves a memory.
|
||||||
|
* Payload: `{ text: "💾 Self-improvement review: ..." }`
|
||||||
|
*/
|
||||||
|
data class ReviewSummary(
|
||||||
|
val text: String,
|
||||||
|
val sessionId: String? = null,
|
||||||
|
) : WsEvent()
|
||||||
|
|
||||||
// ── Status ───────────────────────────────────────────────────────────
|
// ── Status ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
data class StatusUpdate(
|
data class StatusUpdate(
|
||||||
|
|||||||
@@ -471,12 +471,70 @@ private fun AssistantBubble(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelfImprovementReviewCard(
|
||||||
|
content: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val cleanText =
|
||||||
|
content
|
||||||
|
.removePrefix("💾")
|
||||||
|
.replace(Regex("^\\s*Self-improvement review:\\s*", RegexOption.IGNORE_CASE), "")
|
||||||
|
.trim()
|
||||||
|
val isSkill =
|
||||||
|
cleanText.contains("skill", ignoreCase = true) ||
|
||||||
|
cleanText.contains("SKILL.md", ignoreCase = true)
|
||||||
|
val icon = if (isSkill) "⚡" else "🧠"
|
||||||
|
val title =
|
||||||
|
if (isSkill) {
|
||||||
|
"Self-Improvement Review • Skill Patched"
|
||||||
|
} else {
|
||||||
|
"Self-Improvement Review • Memory Updated"
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||||
|
.testTag("self_improvement_review_card"),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(text = icon, fontSize = 16.sp)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = cleanText,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SystemBubble(
|
private fun SystemBubble(
|
||||||
message: ChatMessage,
|
message: ChatMessage,
|
||||||
onRespondApproval: (String) -> Unit = {},
|
onRespondApproval: (String) -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
if (message.content.contains("Self-improvement review:", ignoreCase = true)) {
|
||||||
|
SelfImprovementReviewCard(content = message.content, modifier = modifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
@@ -941,7 +999,13 @@ fun parseToolOutput(
|
|||||||
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val usage = dataSource.get("usage")?.takeIf { !it.isJsonNull }?.asString
|
val usage = dataSource.get("usage")?.takeIf { !it.isJsonNull }?.asString
|
||||||
|
|
||||||
val summaryText = "💾 $action${target?.let { " ($it)" } ?: ""}"
|
val summaryText =
|
||||||
|
when (action.lowercase()) {
|
||||||
|
"add" -> "🧠 Memory Saved${target?.let { " ($it)" } ?: ""}"
|
||||||
|
"replace", "update" -> "🧠 Memory Updated${target?.let { " ($it)" } ?: ""}"
|
||||||
|
"remove", "delete" -> "🧠 Memory Removed${target?.let { " ($it)" } ?: ""}"
|
||||||
|
else -> "🧠 Memory $action${target?.let { " ($it)" } ?: ""}"
|
||||||
|
}
|
||||||
val mainOutput =
|
val mainOutput =
|
||||||
when {
|
when {
|
||||||
errorMsg != null -> "❌ $errorMsg"
|
errorMsg != null -> "❌ $errorMsg"
|
||||||
@@ -972,6 +1036,48 @@ fun parseToolOutput(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Skill Manage-specific formatting (Self-Improvement) ──
|
||||||
|
if (resolvedToolName == "skill_manage") {
|
||||||
|
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||||
|
val name = argsObj?.get("name")?.takeIf { !it.isJsonNull }?.asString
|
||||||
|
val category = argsObj?.get("category")?.takeIf { !it.isJsonNull }?.asString
|
||||||
|
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||||
|
val success = dataSource.get("success")?.takeIf { !it.isJsonNull }?.asBoolean ?: true
|
||||||
|
val msg = dataSource.get("message")?.takeIf { !it.isJsonNull }?.asString
|
||||||
|
|
||||||
|
val summaryText =
|
||||||
|
when (action.lowercase()) {
|
||||||
|
"create" -> "⚡ Skill Created${name?.let { ": $it" } ?: ""}"
|
||||||
|
"patch" -> "⚡ Skill Patched${name?.let { ": $it" } ?: ""}"
|
||||||
|
"edit" -> "⚡ Skill Edited${name?.let { ": $it" } ?: ""}"
|
||||||
|
"delete" -> "⚡ Skill Archived${name?.let { ": $it" } ?: ""}"
|
||||||
|
"write_file" -> "⚡ Skill File Written${name?.let { ": $it" } ?: ""}"
|
||||||
|
"remove_file" -> "⚡ Skill File Removed${name?.let { ": $it" } ?: ""}"
|
||||||
|
else ->
|
||||||
|
"⚡ Skill ${action.replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) it.titlecase(java.util.Locale.getDefault()) else it.toString()
|
||||||
|
}}${name?.let { ": $it" } ?: ""}"
|
||||||
|
}
|
||||||
|
val mainOutput =
|
||||||
|
when {
|
||||||
|
errorMsg != null -> "❌ $errorMsg"
|
||||||
|
!success -> "❌ Skill operation failed"
|
||||||
|
msg != null -> "✅ $msg"
|
||||||
|
name != null -> "✅ Skill '$name' $action succeeded${category?.let { " [$it]" } ?: ""}"
|
||||||
|
else -> "✅ Skill $action done"
|
||||||
|
}
|
||||||
|
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||||
|
return ParsedToolData(
|
||||||
|
toolName = resolvedToolName,
|
||||||
|
args = args,
|
||||||
|
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||||
|
summaryText = summaryText,
|
||||||
|
mainOutput = mainOutput,
|
||||||
|
durationSec = duration,
|
||||||
|
isRunning = isRunning,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Cronjob-specific formatting ──
|
// ── Cronjob-specific formatting ──
|
||||||
if (resolvedToolName == "cronjob") {
|
if (resolvedToolName == "cronjob") {
|
||||||
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ object ChatWsEventReducer {
|
|||||||
is WsEvent.ToolProgress -> event.sessionId
|
is WsEvent.ToolProgress -> event.sessionId
|
||||||
is WsEvent.ToolGenerating -> event.sessionId
|
is WsEvent.ToolGenerating -> event.sessionId
|
||||||
is WsEvent.SubagentEvent -> event.sessionId
|
is WsEvent.SubagentEvent -> event.sessionId
|
||||||
|
is WsEvent.ReviewSummary -> event.sessionId
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (eventSessionId != null && currentSessionId != null && eventSessionId != currentSessionId) {
|
if (eventSessionId != null && currentSessionId != null && eventSessionId != currentSessionId) {
|
||||||
@@ -95,6 +96,8 @@ object ChatWsEventReducer {
|
|||||||
|
|
||||||
is WsEvent.ClarifyRequest -> onClarifyRequest(state, streamingState, event)
|
is WsEvent.ClarifyRequest -> onClarifyRequest(state, streamingState, event)
|
||||||
|
|
||||||
|
is WsEvent.ReviewSummary -> onReviewSummary(state, streamingState, event)
|
||||||
|
|
||||||
is WsEvent.RpcError -> onRpcError(state, streamingState, event)
|
is WsEvent.RpcError -> onRpcError(state, streamingState, event)
|
||||||
|
|
||||||
is WsEvent.GatewayError -> onGatewayError(state, streamingState, event)
|
is WsEvent.GatewayError -> onGatewayError(state, streamingState, event)
|
||||||
@@ -506,6 +509,25 @@ object ChatWsEventReducer {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── ReviewSummary (Self-improvement background review) ──────────────
|
||||||
|
|
||||||
|
private fun onReviewSummary(
|
||||||
|
state: ChatUiState,
|
||||||
|
streamingState: StreamingState,
|
||||||
|
event: WsEvent.ReviewSummary,
|
||||||
|
): ReducerResult {
|
||||||
|
if (event.text.isBlank()) return ReducerResult(state = state, streamingState = streamingState)
|
||||||
|
val systemMessage =
|
||||||
|
ChatMessage(
|
||||||
|
role = MessageRole.SYSTEM,
|
||||||
|
content = event.text,
|
||||||
|
)
|
||||||
|
return ReducerResult(
|
||||||
|
state = state.copy(messages = state.messages + systemMessage),
|
||||||
|
streamingState = streamingState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ── RpcError ──────────────────────────────────────────────────────
|
// ── RpcError ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun onRpcError(
|
private fun onRpcError(
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -69,6 +70,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
@@ -288,6 +290,9 @@ fun SystemScreen(
|
|||||||
// ── 3. Curator ─────────────────────────────────────────
|
// ── 3. Curator ─────────────────────────────────────────
|
||||||
curatorSection(state, spacing, statusColors, viewModel)
|
curatorSection(state, spacing, statusColors, viewModel)
|
||||||
|
|
||||||
|
// ── 3.5 Self-Improvement & Learning ───────────────────
|
||||||
|
selfImprovementSection(state, spacing, statusColors)
|
||||||
|
|
||||||
// ── 4. Gateway ─────────────────────────────────────────
|
// ── 4. Gateway ─────────────────────────────────────────
|
||||||
gatewaySection(state, spacing, statusColors, viewModel)
|
gatewaySection(state, spacing, statusColors, viewModel)
|
||||||
|
|
||||||
@@ -707,6 +712,122 @@ private fun LazyListScope.curatorSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.selfImprovementSection(
|
||||||
|
state: SystemUiState,
|
||||||
|
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||||
|
statusColors: com.m57.hermescontrol.theme.HermesStatusColors,
|
||||||
|
) {
|
||||||
|
val graph = state.learningGraph
|
||||||
|
item {
|
||||||
|
SectionHeader(title = stringResource(R.string.system_sec_self_improvement))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(spacing.md)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_self_improvement_desc),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(spacing.sm))
|
||||||
|
|
||||||
|
if (graph == null || graph.nodes.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.system_self_improvement_no_activity),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val skillNodes = graph.nodes.filter { it.kind == "skill" }
|
||||||
|
val memoryNodes = graph.nodes.filter { it.kind == "memory" }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||||
|
) {
|
||||||
|
StatCard(
|
||||||
|
label = "Learned Skills",
|
||||||
|
value = "${skillNodes.size}",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
StatCard(
|
||||||
|
label = "Memories & Facts",
|
||||||
|
value = "${memoryNodes.size}",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(spacing.md))
|
||||||
|
Text(
|
||||||
|
text = "Recent Agent Self-Improvement Events",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(spacing.xs))
|
||||||
|
|
||||||
|
val sortedNodes =
|
||||||
|
graph.nodes
|
||||||
|
.filter { it.timestamp != null || it.kind == "skill" }
|
||||||
|
.sortedByDescending { it.timestamp ?: 0L }
|
||||||
|
.take(8)
|
||||||
|
|
||||||
|
sortedNodes.forEach { node ->
|
||||||
|
Surface(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val icon = if (node.kind == "skill") "⚡" else "🧠"
|
||||||
|
Text(text = icon, fontSize = 16.sp)
|
||||||
|
Spacer(modifier = Modifier.width(spacing.xs))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = node.label,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
val cat = node.category ?: node.kind
|
||||||
|
val creator = node.createdBy?.let { " • by $it" } ?: ""
|
||||||
|
Text(
|
||||||
|
text = "[$cat]$creator",
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
node.useCount.takeIf { it > 0 }?.let { uses ->
|
||||||
|
StatusBadge(
|
||||||
|
text = "$uses uses",
|
||||||
|
status = StatusBadgeType.INFO,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun LazyListScope.gatewaySection(
|
private fun LazyListScope.gatewaySection(
|
||||||
state: SystemUiState,
|
state: SystemUiState,
|
||||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.m57.hermescontrol.data.model.CuratorResponse
|
|||||||
import com.m57.hermescontrol.data.model.DebugShareResponse
|
import com.m57.hermescontrol.data.model.DebugShareResponse
|
||||||
import com.m57.hermescontrol.data.model.DoctorResponse
|
import com.m57.hermescontrol.data.model.DoctorResponse
|
||||||
import com.m57.hermescontrol.data.model.HookResponse
|
import com.m57.hermescontrol.data.model.HookResponse
|
||||||
|
import com.m57.hermescontrol.data.model.LearningGraphResponse
|
||||||
import com.m57.hermescontrol.data.model.MemoryResponse
|
import com.m57.hermescontrol.data.model.MemoryResponse
|
||||||
import com.m57.hermescontrol.data.model.PortalResponse
|
import com.m57.hermescontrol.data.model.PortalResponse
|
||||||
import com.m57.hermescontrol.data.model.StatusResponse
|
import com.m57.hermescontrol.data.model.StatusResponse
|
||||||
@@ -40,6 +41,7 @@ data class SystemUiState(
|
|||||||
val portal: PortalResponse? = null,
|
val portal: PortalResponse? = null,
|
||||||
val curator: CuratorResponse? = null,
|
val curator: CuratorResponse? = null,
|
||||||
val memory: MemoryResponse? = null,
|
val memory: MemoryResponse? = null,
|
||||||
|
val learningGraph: LearningGraphResponse? = null,
|
||||||
val credentials: List<CredentialPoolProvider> = emptyList(),
|
val credentials: List<CredentialPoolProvider> = emptyList(),
|
||||||
val checkpoints: CheckpointsResponse? = null,
|
val checkpoints: CheckpointsResponse? = null,
|
||||||
val hooks: HookResponse? = null,
|
val hooks: HookResponse? = null,
|
||||||
@@ -100,6 +102,7 @@ class SystemViewModel :
|
|||||||
val portalDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getPortal() } }
|
val portalDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getPortal() } }
|
||||||
val curatorDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getCurator() } }
|
val curatorDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getCurator() } }
|
||||||
val memoryDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getMemory() } }
|
val memoryDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getMemory() } }
|
||||||
|
val learningDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getLearningGraph() } }
|
||||||
val credDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getCredentialPool() } }
|
val credDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getCredentialPool() } }
|
||||||
val checkpointsDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getCheckpoints() } }
|
val checkpointsDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getCheckpoints() } }
|
||||||
val hooksDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getHooks() } }
|
val hooksDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getHooks() } }
|
||||||
@@ -112,6 +115,7 @@ class SystemViewModel :
|
|||||||
val portalResult = portalDeferred.await()
|
val portalResult = portalDeferred.await()
|
||||||
val curatorResult = curatorDeferred.await()
|
val curatorResult = curatorDeferred.await()
|
||||||
val memoryResult = memoryDeferred.await()
|
val memoryResult = memoryDeferred.await()
|
||||||
|
val learningResult = learningDeferred.await()
|
||||||
val credResult = credDeferred.await()
|
val credResult = credDeferred.await()
|
||||||
val checkpointsResult = checkpointsDeferred.await()
|
val checkpointsResult = checkpointsDeferred.await()
|
||||||
val hooksResult = hooksDeferred.await()
|
val hooksResult = hooksDeferred.await()
|
||||||
@@ -126,6 +130,7 @@ class SystemViewModel :
|
|||||||
portal = (portalResult as? NetworkResult.Success)?.data,
|
portal = (portalResult as? NetworkResult.Success)?.data,
|
||||||
curator = (curatorResult as? NetworkResult.Success)?.data,
|
curator = (curatorResult as? NetworkResult.Success)?.data,
|
||||||
memory = (memoryResult as? NetworkResult.Success)?.data,
|
memory = (memoryResult as? NetworkResult.Success)?.data,
|
||||||
|
learningGraph = (learningResult as? NetworkResult.Success)?.data,
|
||||||
credentials =
|
credentials =
|
||||||
((credResult as? NetworkResult.Success)?.data)?.providers ?: emptyList(),
|
((credResult as? NetworkResult.Success)?.data)?.providers ?: emptyList(),
|
||||||
checkpoints = (checkpointsResult as? NetworkResult.Success)?.data,
|
checkpoints = (checkpointsResult as? NetworkResult.Success)?.data,
|
||||||
|
|||||||
@@ -325,6 +325,10 @@
|
|||||||
|
|
||||||
<!-- Curator section -->
|
<!-- Curator section -->
|
||||||
<string name="system_sec_curator">Skill curator</string>
|
<string name="system_sec_curator">Skill curator</string>
|
||||||
|
<!-- Self-Improvement & Learning Activity -->
|
||||||
|
<string name="system_sec_self_improvement">Self-Improvement & Learning Activity</string>
|
||||||
|
<string name="system_self_improvement_desc">Chronological log of agent skills, memories, and fact store updates learned over time.</string>
|
||||||
|
<string name="system_self_improvement_no_activity">No self-improvement events recorded yet.</string>
|
||||||
<string name="system_status_active">active</string>
|
<string name="system_status_active">active</string>
|
||||||
<string name="system_status_paused">paused</string>
|
<string name="system_status_paused">paused</string>
|
||||||
<string name="system_status_disabled">disabled</string>
|
<string name="system_status_disabled">disabled</string>
|
||||||
|
|||||||
@@ -375,4 +375,27 @@ class ChatWsEventReducerTest {
|
|||||||
assertEquals("Write tests", result.state.todos[0].content)
|
assertEquals("Write tests", result.state.todos[0].content)
|
||||||
assertTrue(result.state.todos[0].isCompleted)
|
assertTrue(result.state.todos[0].isCompleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReviewSummary_addsSystemMessage() {
|
||||||
|
val state = ChatUiState(currentSessionId = "session-1")
|
||||||
|
val event =
|
||||||
|
WsEvent.ReviewSummary(
|
||||||
|
text = "💾 Self-improvement review: Skill 'android-ci' patched",
|
||||||
|
sessionId = "session-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
val result =
|
||||||
|
ChatWsEventReducer.reduce(
|
||||||
|
state = state,
|
||||||
|
streamingState = StreamingState(),
|
||||||
|
event = event,
|
||||||
|
currentSessionId = "session-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(1, result.state.messages.size)
|
||||||
|
val msg = result.state.messages.first()
|
||||||
|
assertEquals(MessageRole.SYSTEM, msg.role)
|
||||||
|
assertEquals("💾 Self-improvement review: Skill 'android-ci' patched", msg.content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -319,4 +319,33 @@ class ToolBubbleParsingTest {
|
|||||||
assertTrue(editDiff!!.contains("-val x = 1"))
|
assertTrue(editDiff!!.contains("-val x = 1"))
|
||||||
assertTrue(editDiff.contains("+val x = 2"))
|
assertTrue(editDiff.contains("+val x = 2"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseSkillManage_createAndPatch() {
|
||||||
|
val createJson =
|
||||||
|
"""{
|
||||||
|
"tool_id": "call_sm_1",
|
||||||
|
"name": "skill_manage",
|
||||||
|
"args": {"action": "patch", "name": "our-workflow"},
|
||||||
|
"result": {"success": true, "message": "Patched skill our-workflow"}
|
||||||
|
}"""
|
||||||
|
val parsed = parseToolOutput(createJson, "skill_manage", false)
|
||||||
|
assertNotNull(parsed)
|
||||||
|
assertEquals("⚡ Skill Patched: our-workflow", parsed!!.summaryText)
|
||||||
|
assertEquals("✅ Patched skill our-workflow", parsed.mainOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseMemory_selfImprovementBadge() {
|
||||||
|
val memJson =
|
||||||
|
"""{
|
||||||
|
"tool_id": "call_mem_1",
|
||||||
|
"name": "memory",
|
||||||
|
"args": {"action": "add", "target": "user"},
|
||||||
|
"result": {"success": true, "usage": "1,200 / 2,200 chars"}
|
||||||
|
}"""
|
||||||
|
val parsed = parseToolOutput(memJson, "memory", false)
|
||||||
|
assertNotNull(parsed)
|
||||||
|
assertEquals("🧠 Memory Saved (user)", parsed!!.summaryText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -48,4 +48,11 @@ class SystemViewModelTest {
|
|||||||
viewModel.closeUpdateConfirm()
|
viewModel.closeUpdateConfirm()
|
||||||
assertFalse(viewModel.uiState.value.updateConfirmOpen)
|
assertFalse(viewModel.uiState.value.updateConfirmOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial uiState has null learningGraph`() {
|
||||||
|
val viewModel = SystemViewModel()
|
||||||
|
|
||||||
|
org.junit.Assert.assertNull(viewModel.uiState.value.learningGraph)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user