go-agentic/status.go
Snider ef81db73c1 feat(cli): add status summary, task submission, and log streaming
CLI backing functions for core agent commands:
- GetStatus/FormatStatus aggregates registry + client + allowance data
- SubmitTask + Client.CreateTask for task creation
- StreamLogs polls task updates to io.Writer

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 07:21:59 +00:00

135 lines
3.7 KiB
Go

package agentic
import (
"context"
"fmt"
"sort"
"strings"
"forge.lthn.ai/core/go/pkg/log"
)
// StatusSummary aggregates status from the agent registry, task client, and
// allowance service for CLI display.
type StatusSummary struct {
// Agents is the list of registered agents.
Agents []AgentInfo
// PendingTasks is the count of tasks with StatusPending.
PendingTasks int
// InProgressTasks is the count of tasks with StatusInProgress.
InProgressTasks int
// AllowanceRemaining maps agent ID to remaining daily tokens. -1 means unlimited.
AllowanceRemaining map[string]int64
}
// GetStatus aggregates status from the registry, client, and allowance service.
// Any of registry, client, or allowanceSvc can be nil -- those sections are
// simply skipped. Returns what we can collect without failing on nil components.
func GetStatus(ctx context.Context, registry AgentRegistry, client *Client, allowanceSvc *AllowanceService) (*StatusSummary, error) {
const op = "agentic.GetStatus"
summary := &StatusSummary{
AllowanceRemaining: make(map[string]int64),
}
// Collect agents from registry.
if registry != nil {
summary.Agents = registry.List()
}
// Count tasks by status via client.
if client != nil {
pending, err := client.ListTasks(ctx, ListOptions{Status: StatusPending})
if err != nil {
return nil, log.E(op, "failed to list pending tasks", err)
}
summary.PendingTasks = len(pending)
inProgress, err := client.ListTasks(ctx, ListOptions{Status: StatusInProgress})
if err != nil {
return nil, log.E(op, "failed to list in-progress tasks", err)
}
summary.InProgressTasks = len(inProgress)
}
// Collect allowance remaining per agent.
if allowanceSvc != nil {
for _, agent := range summary.Agents {
check, err := allowanceSvc.Check(agent.ID, "")
if err != nil {
// Skip agents whose allowance cannot be resolved.
continue
}
summary.AllowanceRemaining[agent.ID] = check.RemainingTokens
}
}
return summary, nil
}
// FormatStatus renders the summary as a human-readable table string suitable
// for CLI output.
func FormatStatus(s *StatusSummary) string {
var b strings.Builder
// Count agents by status.
available := 0
busy := 0
for _, a := range s.Agents {
switch a.Status {
case AgentAvailable:
available++
case AgentBusy:
busy++
}
}
total := len(s.Agents)
statusParts := make([]string, 0, 2)
if available > 0 {
statusParts = append(statusParts, fmt.Sprintf("%d available", available))
}
if busy > 0 {
statusParts = append(statusParts, fmt.Sprintf("%d busy", busy))
}
offline := total - available - busy
if offline > 0 {
statusParts = append(statusParts, fmt.Sprintf("%d offline", offline))
}
if len(statusParts) > 0 {
fmt.Fprintf(&b, "Agents: %d (%s)\n", total, strings.Join(statusParts, ", "))
} else {
fmt.Fprintf(&b, "Agents: %d\n", total)
}
fmt.Fprintf(&b, "Tasks: %d pending, %d in progress\n", s.PendingTasks, s.InProgressTasks)
if len(s.Agents) > 0 {
// Sort agents by ID for deterministic output.
agents := make([]AgentInfo, len(s.Agents))
copy(agents, s.Agents)
sort.Slice(agents, func(i, j int) bool { return agents[i].ID < agents[j].ID })
fmt.Fprintf(&b, "%-16s%-12s%-8s%s\n", "Agent", "Status", "Load", "Remaining")
for _, a := range agents {
load := fmt.Sprintf("%d/%d", a.CurrentLoad, a.MaxLoad)
if a.MaxLoad == 0 {
load = fmt.Sprintf("%d/-", a.CurrentLoad)
}
remaining := "unknown"
if tokens, ok := s.AllowanceRemaining[a.ID]; ok {
if tokens < 0 {
remaining = "unlimited"
} else {
remaining = fmt.Sprintf("%d tokens", tokens)
}
}
fmt.Fprintf(&b, "%-16s%-12s%-8s%s\n", a.ID, string(a.Status), load, remaining)
}
}
return b.String()
}