Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

114 lines
3.6 KiB
Go

package insight
import (
"fmt"
"strings"
"go.kenn.io/agentsview/internal/activity"
)
// RangeSummary is the deterministic, prompt-input contract derived from
// activity.Report for multi-day ranges. Field order and formatting are
// fixed so the same report always yields the same prompt text.
type RangeSummary struct {
ActiveMinutes float64
IdleMinutes float64
AgentMinutes float64
Sessions int
DistinctProjects int
DistinctModels int
OutputTokens int
Cost float64
PeakAgents int
PeakAt string // "" when nil
TopProjects []KeyMinutes // top-N by agent-minutes, deterministic order
TopModels []KeyMinutes
TopAgents []KeyMinutes
}
// KeyMinutes pairs a breakdown key with its agent-minutes total.
type KeyMinutes struct {
Key string
AgentMinutes float64
}
// SummarizeReport builds a RangeSummary from an activity.Report, taking the
// top topN entries of each breakdown (already minutes-desc, key-asc from the
// aggregator) so the result is deterministic.
func SummarizeReport(r activity.Report, topN int) RangeSummary {
peakAt := ""
if r.Peak.At != nil {
peakAt = *r.Peak.At
}
return RangeSummary{
ActiveMinutes: r.Totals.ActiveMinutes,
IdleMinutes: r.Totals.IdleMinutes,
AgentMinutes: r.Totals.AgentMinutes,
Sessions: r.Totals.Sessions,
DistinctProjects: r.Totals.DistinctProjects,
DistinctModels: r.Totals.DistinctModels,
OutputTokens: r.Totals.OutputTokens,
Cost: r.Totals.Cost,
PeakAgents: r.Peak.Agents,
PeakAt: peakAt,
TopProjects: topKeyMinutes(r.ByProject, topN),
TopModels: topKeyMinutes(r.ByModel, topN),
TopAgents: topKeyMinutes(r.ByAgent, topN),
}
}
// topKeyMinutes copies up to topN entries from src verbatim, preserving the
// aggregator's deterministic minutes-desc, key-asc order.
func topKeyMinutes(src []activity.KeyMinutes, topN int) []KeyMinutes {
if topN < 0 {
topN = 0
}
out := make([]KeyMinutes, 0, min(topN, len(src)))
for i, km := range src {
if i >= topN {
break
}
out = append(out, KeyMinutes{Key: km.Key, AgentMinutes: km.AgentMinutes})
}
return out
}
// WriteTo renders the summary into the prompt builder deterministically. The
// section and field order are fixed so the same RangeSummary always produces
// the same text.
func (s RangeSummary) WriteTo(b *strings.Builder) {
b.WriteString("## Range Summary\n\n")
fmt.Fprintf(b, "- Sessions: %d\n", s.Sessions)
fmt.Fprintf(b, "- Active minutes: %.1f\n", s.ActiveMinutes)
fmt.Fprintf(b, "- Idle minutes: %.1f\n", s.IdleMinutes)
fmt.Fprintf(b, "- Agent minutes: %.1f\n", s.AgentMinutes)
fmt.Fprintf(b, "- Distinct projects: %d\n", s.DistinctProjects)
fmt.Fprintf(b, "- Distinct models: %d\n", s.DistinctModels)
fmt.Fprintf(b, "- Output tokens: %d\n", s.OutputTokens)
fmt.Fprintf(b, "- Cost: %.2f\n", s.Cost)
fmt.Fprintf(b, "- Peak concurrency: %d", s.PeakAgents)
if s.PeakAt != "" {
fmt.Fprintf(b, " at %s", s.PeakAt)
}
b.WriteString("\n\n")
writeKeyMinutes(b, "### Top Projects", s.TopProjects)
writeKeyMinutes(b, "### Top Models", s.TopModels)
writeKeyMinutes(b, "### Top Agents", s.TopAgents)
}
// writeKeyMinutes renders a titled breakdown list, or a placeholder line when
// the breakdown is empty, keeping the section present for determinism.
func writeKeyMinutes(b *strings.Builder, title string, items []KeyMinutes) {
b.WriteString(title)
b.WriteString("\n\n")
if len(items) == 0 {
b.WriteString("- (none)\n\n")
return
}
for _, km := range items {
fmt.Fprintf(b, "- %s: %.1f agent-minutes\n", km.Key, km.AgentMinutes)
}
b.WriteString("\n")
}