feat(ai): add recent metrics events
All checks were successful
Security Scan / security (push) Successful in 10s
Test / test (push) Successful in 1m16s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 05:35:56 +00:00
parent 97b1854857
commit 3c0d9f853c
3 changed files with 84 additions and 4 deletions

View file

@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"slices"
"sort"
"sync"
"time"
@ -152,11 +153,21 @@ func Summary(events []Event) map[string]any {
}
}
recent := make([]Event, len(events))
copy(recent, events)
sort.SliceStable(recent, func(i, j int) bool {
return recent[i].Timestamp.After(recent[j].Timestamp)
})
if len(recent) > 10 {
recent = recent[:10]
}
return map[string]any{
"total": len(events),
"by_type": sortedMap(byType),
"by_repo": sortedMap(byRepo),
"by_agent": sortedMap(byAgent),
"events": briefEvents(recent),
}
}
@ -181,3 +192,22 @@ func sortedMap(m map[string]int) []map[string]any {
}
return result
}
// briefEvents converts events into the compact shape used by metrics_query.
func briefEvents(events []Event) []map[string]any {
result := make([]map[string]any, len(events))
for i, ev := range events {
item := map[string]any{
"type": ev.Type,
"timestamp": ev.Timestamp,
}
if ev.AgentID != "" {
item["agent_id"] = ev.AgentID
}
if ev.Repo != "" {
item["repo"] = ev.Repo
}
result[i] = item
}
return result
}

View file

@ -228,9 +228,9 @@ func TestSummary_Good_Empty(t *testing.T) {
func TestSummary_Good(t *testing.T) {
events := []Event{
{Type: "build", Repo: "core-php", AgentID: "agent-1"},
{Type: "build", Repo: "core-php", AgentID: "agent-2"},
{Type: "test", Repo: "core-api", AgentID: "agent-1"},
{Type: "build", Repo: "core-php", AgentID: "agent-1", Timestamp: time.Date(2026, 3, 15, 9, 0, 0, 0, time.UTC)},
{Type: "build", Repo: "core-php", AgentID: "agent-2", Timestamp: time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)},
{Type: "test", Repo: "core-api", AgentID: "agent-1", Timestamp: time.Date(2026, 3, 15, 11, 0, 0, 0, time.UTC)},
}
s := Summary(events)
@ -248,6 +248,39 @@ func TestSummary_Good(t *testing.T) {
if byType[0]["key"] != "build" || byType[0]["count"] != 2 {
t.Errorf("expected build:2 first, got %v:%v", byType[0]["key"], byType[0]["count"])
}
recent, _ := s["events"].([]map[string]any)
if len(recent) != 3 {
t.Fatalf("expected 3 recent events, got %d", len(recent))
}
if recent[0]["type"] != "test" {
t.Errorf("expected newest event first, got %v", recent[0]["type"])
}
if _, ok := recent[0]["timestamp"].(time.Time); !ok {
t.Errorf("expected timestamp to be time.Time, got %T", recent[0]["timestamp"])
}
}
func TestSummary_Good_RecentEventsLimit(t *testing.T) {
events := make([]Event, 0, 12)
for i := 0; i < 12; i++ {
events = append(events, Event{
Type: "type",
Timestamp: time.Date(2026, 3, 15, 12, i, 0, 0, time.UTC),
})
}
s := Summary(events)
recent, _ := s["events"].([]map[string]any)
if len(recent) != 10 {
t.Fatalf("expected 10 recent events, got %d", len(recent))
}
if recent[0]["timestamp"].(time.Time).Minute() != 11 {
t.Errorf("expected newest event first, got minute %d", recent[0]["timestamp"].(time.Time).Minute())
}
if recent[9]["timestamp"].(time.Time).Minute() != 2 {
t.Errorf("expected tenth newest event last, got minute %d", recent[9]["timestamp"].(time.Time).Minute())
}
}
// --- sortedMap ---

View file

@ -6,10 +6,10 @@ import (
"fmt"
"time"
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core/ai/ai"
"dappco.re/go/core/i18n"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
)
var (
@ -94,6 +94,23 @@ func runMetrics() error {
cli.Blank()
}
// Recent events
if recent, ok := summary["events"].([]map[string]any); ok && len(recent) > 0 {
cli.Print("%s\n", cli.DimStyle.Render("Recent events:"))
for _, entry := range recent {
ts, _ := entry["timestamp"].(time.Time)
agent, _ := entry["agent_id"].(string)
repo, _ := entry["repo"].(string)
cli.Print(" %-20s %-24s %-20s %-20s\n",
ts.Format(time.RFC3339),
entry["type"],
agent,
repo,
)
}
cli.Blank()
}
if len(events) == 0 {
cli.Text(i18n.T("cmd.ai.metrics.none_found"))
}