bd0c8c0dd3
agents/ ahora solo contiene carpetas de agentes (config, reglas, prompts). El runtime (Agent, Robot, Runner, registry, handler, commands, llm, memory) vive en devagents/ como package devagents. Cambios: - git mv agents/*.go → devagents/*.go - package agents → package devagents en todos los archivos movidos - Actualizar imports en agents/*/agent.go, cmd/launcher/, dev-scripts/ - Actualizar docs: CLAUDE.md, rules/, docs/e2ee.md, issues pendientes Build y tests pasan sin errores. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
405 lines
11 KiB
Go
405 lines
11 KiB
Go
package devagents
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/enmanuel/agents/pkg/command"
|
|
"github.com/enmanuel/agents/pkg/decision"
|
|
"github.com/enmanuel/agents/shell/logger"
|
|
)
|
|
|
|
// registerBuiltinCommands registers all built-in command handlers.
|
|
func (a *Agent) registerBuiltinCommands() {
|
|
a.commands["help"] = a.cmdHelp
|
|
a.commands["tools"] = a.cmdTools
|
|
a.commands["tool"] = a.cmdTool
|
|
a.commands["ping"] = a.cmdPing
|
|
a.commands["status"] = a.cmdStatus
|
|
a.commands["info"] = a.cmdInfo
|
|
a.commands["clear"] = a.cmdClear
|
|
a.commands["prompts"] = a.cmdPrompts
|
|
a.commands["version"] = a.cmdVersion
|
|
a.commands["metrics"] = a.cmdMetrics
|
|
}
|
|
|
|
// cmdHelp lists all available commands (built-in + agent-specific).
|
|
func (a *Agent) cmdHelp(_ context.Context, _ decision.MessageContext) string {
|
|
var b strings.Builder
|
|
b.WriteString("**Comandos disponibles:**\n\n")
|
|
|
|
// Built-in commands
|
|
for _, spec := range command.Builtins() {
|
|
if spec.Hidden {
|
|
continue
|
|
}
|
|
writeSpec(&b, spec)
|
|
}
|
|
|
|
// Agent-specific commands (registered via RegisterCommand)
|
|
if len(a.customSpecs) > 0 {
|
|
b.WriteString("\n**Comandos del agente:**\n\n")
|
|
for _, spec := range a.customSpecs {
|
|
if spec.Hidden {
|
|
continue
|
|
}
|
|
writeSpec(&b, spec)
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// writeSpec formats a single command spec for the help output.
|
|
func writeSpec(b *strings.Builder, spec command.Spec) {
|
|
aliases := ""
|
|
if len(spec.Aliases) > 0 {
|
|
aliases = " (" + strings.Join(prefixAll(spec.Aliases, "!"), ", ") + ")"
|
|
}
|
|
usage := spec.Usage
|
|
if usage == "" {
|
|
usage = "!" + spec.Name
|
|
}
|
|
fmt.Fprintf(b, "- `%s`%s — %s\n", usage, aliases, spec.Description)
|
|
}
|
|
|
|
// cmdTools lists all tools registered in the agent's tool registry.
|
|
func (a *Agent) cmdTools(_ context.Context, _ decision.MessageContext) string {
|
|
names := a.toolReg.Names()
|
|
if len(names) == 0 {
|
|
return "No hay tools registradas."
|
|
}
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "**Tools disponibles (%d):**\n\n", len(names))
|
|
for _, name := range names {
|
|
t, _ := a.toolReg.Get(name)
|
|
fmt.Fprintf(&b, "- **%s** — %s\n", t.Def.Name, t.Def.Description)
|
|
for _, p := range t.Def.Parameters {
|
|
req := ""
|
|
if p.Required {
|
|
req = " *(requerido)*"
|
|
}
|
|
fmt.Fprintf(&b, " - `%s`: %s%s\n", p.Name, p.Description, req)
|
|
}
|
|
}
|
|
b.WriteString("\nUso: `!tool <nombre> [key=value ...]`")
|
|
return b.String()
|
|
}
|
|
|
|
// cmdTool executes a tool directly with key=value args.
|
|
func (a *Agent) cmdTool(ctx context.Context, msgCtx decision.MessageContext) string {
|
|
if len(msgCtx.Args) == 0 {
|
|
return "Uso: `!tool <nombre> [key=value ...]`\nUsa `!tools` para ver tools disponibles."
|
|
}
|
|
|
|
toolName := msgCtx.Args[0]
|
|
if _, ok := a.toolReg.Get(toolName); !ok {
|
|
return fmt.Sprintf("Tool %q no encontrada. Usa `!tools` para ver tools disponibles.", toolName)
|
|
}
|
|
|
|
// Parse remaining args as key=value
|
|
parsed := command.ParseArgs(msgCtx.Args[1:])
|
|
argsJSON := command.ArgsToJSON(parsed.Named)
|
|
|
|
a.logger.Info("executing tool via command",
|
|
"tool", toolName,
|
|
"args", argsJSON,
|
|
)
|
|
|
|
result := a.toolReg.ExecuteForRoom(ctx, toolName, argsJSON, msgCtx.RoomID)
|
|
if result.Err != nil {
|
|
return fmt.Sprintf("Error ejecutando %s: %s", toolName, result.Err)
|
|
}
|
|
|
|
return fmt.Sprintf("%s:\n%s", toolName, result.Output)
|
|
}
|
|
|
|
// cmdPing responds with pong and timestamp.
|
|
func (a *Agent) cmdPing(_ context.Context, _ decision.MessageContext) string {
|
|
return fmt.Sprintf("pong — %s", time.Now().Format(time.RFC3339))
|
|
}
|
|
|
|
// cmdStatus shows agent uptime and active rooms.
|
|
func (a *Agent) cmdStatus(_ context.Context, _ decision.MessageContext) string {
|
|
uptime := time.Since(a.startTime).Truncate(time.Second)
|
|
|
|
a.windowsMu.RLock()
|
|
roomCount := len(a.windows)
|
|
a.windowsMu.RUnlock()
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "**Estado de %s:**\n\n", a.cfg.Agent.Name)
|
|
fmt.Fprintf(&b, "- **Uptime:** %s\n", uptime)
|
|
fmt.Fprintf(&b, "- **Rooms activos:** %d\n", roomCount)
|
|
fmt.Fprintf(&b, "- **Window size:** %d\n", a.windowSize)
|
|
fmt.Fprintf(&b, "- **Tools:** %d\n", a.toolReg.Len())
|
|
|
|
if a.llm != nil {
|
|
fmt.Fprintf(&b, "- **LLM:** %s/%s\n", a.cfg.LLM.Primary.Provider, a.cfg.LLM.Primary.Model)
|
|
} else {
|
|
b.WriteString("- **LLM:** no configurado\n")
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// cmdInfo shows agent metadata, personality, capabilities, and configuration.
|
|
func (a *Agent) cmdInfo(_ context.Context, _ decision.MessageContext) string {
|
|
var b strings.Builder
|
|
|
|
// === Identidad ===
|
|
b.WriteString("## Identidad\n\n")
|
|
fmt.Fprintf(&b, "- **Nombre:** %s\n", a.cfg.Agent.Name)
|
|
fmt.Fprintf(&b, "- **ID:** `%s`\n", a.cfg.Agent.ID)
|
|
if a.cfg.Agent.Version != "" {
|
|
fmt.Fprintf(&b, "- **Version:** %s\n", a.cfg.Agent.Version)
|
|
}
|
|
fmt.Fprintf(&b, "- **Descripcion:** %s\n", a.cfg.Agent.Description)
|
|
if len(a.cfg.Agent.Tags) > 0 {
|
|
fmt.Fprintf(&b, "- **Tags:** %v\n", a.cfg.Agent.Tags)
|
|
}
|
|
|
|
// === Personalidad ===
|
|
if a.personality.Role != "" || a.personality.Communication.Personality != "" {
|
|
b.WriteString("\n## Personalidad\n\n")
|
|
if a.personality.Role != "" {
|
|
fmt.Fprintf(&b, "- **Rol:** %s\n", a.personality.Role)
|
|
}
|
|
if a.personality.Tone != "" {
|
|
fmt.Fprintf(&b, "- **Tono:** %s\n", a.personality.Tone)
|
|
}
|
|
if a.personality.Communication.Formality != "" {
|
|
fmt.Fprintf(&b, "- **Formalidad:** %s\n", a.personality.Communication.Formality)
|
|
}
|
|
if a.personality.Communication.Personality != "" {
|
|
fmt.Fprintf(&b, "- **Tipo:** %s\n", a.personality.Communication.Personality)
|
|
}
|
|
if a.personality.Communication.Humor != "" && a.personality.Communication.Humor != "none" {
|
|
fmt.Fprintf(&b, "- **Humor:** %s\n", a.personality.Communication.Humor)
|
|
}
|
|
}
|
|
|
|
// === LLM ===
|
|
if a.cfg.LLM.Primary.Provider != "" {
|
|
b.WriteString("\n## LLM\n\n")
|
|
fmt.Fprintf(&b, "- **Provider:** %s\n", a.cfg.LLM.Primary.Provider)
|
|
fmt.Fprintf(&b, "- **Modelo:** %s\n", a.cfg.LLM.Primary.Model)
|
|
if a.cfg.LLM.ToolUse.Enabled {
|
|
fmt.Fprintf(&b, "- **Tools:** habilitadas (max %d iteraciones)\n", a.cfg.LLM.ToolUse.MaxIterations)
|
|
}
|
|
}
|
|
|
|
// === Tools ===
|
|
toolCount := a.toolReg.Len()
|
|
if toolCount > 0 {
|
|
b.WriteString("\n## Tools disponibles\n\n")
|
|
fmt.Fprintf(&b, "- **Total:** %d tools\n", toolCount)
|
|
// Lista de tools (nombres)
|
|
toolNames := a.toolReg.Names()
|
|
if len(toolNames) > 0 && len(toolNames) <= 20 {
|
|
b.WriteString("- **Lista:** ")
|
|
for i, name := range toolNames {
|
|
if i > 0 {
|
|
b.WriteString(", ")
|
|
}
|
|
fmt.Fprintf(&b, "`%s`", name)
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
// === Skills ===
|
|
if a.cfg.Skills.Enabled {
|
|
b.WriteString("\n## Skills\n\n")
|
|
b.WriteString("- **Habilitadas:** si\n")
|
|
if len(a.cfg.Skills.Categories) > 0 {
|
|
fmt.Fprintf(&b, "- **Categorias:** %v\n", a.cfg.Skills.Categories)
|
|
}
|
|
if a.skillLoader != nil {
|
|
if metas, err := a.skillLoader.LoadMeta(); err == nil {
|
|
fmt.Fprintf(&b, "- **Cantidad:** %d skills\n", len(metas))
|
|
}
|
|
}
|
|
}
|
|
|
|
// === Knowledge ===
|
|
hasPrivate := a.cfg.Tools.Knowledge.Enabled
|
|
hasShared := a.cfg.Tools.SharedKnowledge.Enabled
|
|
if hasPrivate || hasShared {
|
|
b.WriteString("\n## Knowledge\n\n")
|
|
if hasPrivate {
|
|
b.WriteString("- **Privado:** habilitado\n")
|
|
}
|
|
if hasShared {
|
|
b.WriteString("- **Compartido:** habilitado\n")
|
|
}
|
|
}
|
|
|
|
// === Memoria ===
|
|
if a.cfg.Memory.Enabled {
|
|
b.WriteString("\n## Memoria\n\n")
|
|
fmt.Fprintf(&b, "- **Habilitada:** si\n")
|
|
fmt.Fprintf(&b, "- **Window size:** %d mensajes\n", a.windowSize)
|
|
}
|
|
|
|
// === Schedules ===
|
|
if len(a.cfg.Schedules) > 0 {
|
|
b.WriteString("\n## Schedules\n\n")
|
|
fmt.Fprintf(&b, "- **Cron jobs:** %d configurados\n", len(a.cfg.Schedules))
|
|
}
|
|
|
|
// === Uptime ===
|
|
uptime := time.Since(a.startTime).Round(time.Second)
|
|
b.WriteString("\n## Uptime\n\n")
|
|
fmt.Fprintf(&b, "- **Activo desde:** %s\n", uptime)
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// cmdPrompts lists available prompt-commands.
|
|
func (a *Agent) cmdPrompts(_ context.Context, _ decision.MessageContext) string {
|
|
if len(a.promptCmds) == 0 {
|
|
return "No hay prompt-commands disponibles."
|
|
}
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "**Prompt-commands disponibles (%d):**\n\n", len(a.promptCmds))
|
|
for name := range a.promptCmds {
|
|
fmt.Fprintf(&b, "- `!%s`\n", name)
|
|
}
|
|
b.WriteString("\nUso: `!<nombre> [detalles adicionales...]`")
|
|
return b.String()
|
|
}
|
|
|
|
// cmdClear clears the conversation window for the current room.
|
|
func (a *Agent) cmdClear(_ context.Context, msgCtx decision.MessageContext) string {
|
|
a.ClearWindow(msgCtx.RoomID)
|
|
return "Ventana de conversacion limpiada."
|
|
}
|
|
|
|
// cmdVersion shows the agent version.
|
|
func (a *Agent) cmdVersion(_ context.Context, _ decision.MessageContext) string {
|
|
v := a.cfg.Agent.Version
|
|
if v == "" {
|
|
v = "sin version"
|
|
}
|
|
return fmt.Sprintf("%s %s", a.cfg.Agent.Name, v)
|
|
}
|
|
|
|
// cmdMetrics aggregates today's log data and returns a summary table.
|
|
func (a *Agent) cmdMetrics(_ context.Context, _ decision.MessageContext) string {
|
|
if a.logDir == "" {
|
|
return "Metricas no disponibles: directorio de logs no configurado."
|
|
}
|
|
|
|
entries, err := logger.ReadDayLogs(a.logDir, a.cfg.Agent.ID, time.Now().UTC())
|
|
if err != nil || len(entries) == 0 {
|
|
return "No hay logs disponibles para hoy."
|
|
}
|
|
|
|
var (
|
|
totalMessages int
|
|
totalCommands int
|
|
totalLLMCalls int
|
|
totalLLMTokens int64
|
|
totalLLMLatency int64
|
|
totalToolCalls int
|
|
totalToolErrors int
|
|
totalErrors int
|
|
)
|
|
|
|
for _, raw := range entries {
|
|
var m map[string]any
|
|
if json.Unmarshal(raw, &m) != nil {
|
|
continue
|
|
}
|
|
|
|
msg, _ := m["msg"].(string)
|
|
level, _ := m["level"].(string)
|
|
|
|
switch msg {
|
|
case "handling event":
|
|
totalMessages++
|
|
case "command_received":
|
|
totalCommands++
|
|
case "tool_exec_end":
|
|
totalToolCalls++
|
|
if d, ok := m["duration_ms"]; ok {
|
|
totalLLMLatency += toInt64(d) // reused for tool latency aggregation if needed
|
|
}
|
|
case "tool_exec_error":
|
|
totalToolCalls++
|
|
totalToolErrors++
|
|
case "LLM responded":
|
|
totalLLMCalls++
|
|
}
|
|
|
|
// Count tokens from any log line that has the field
|
|
if t, ok := m["tokens_used"]; ok {
|
|
totalLLMTokens += toInt64(t)
|
|
}
|
|
|
|
// Count LLM latency from duration_ms on LLM-related entries
|
|
if msg == "LLM responded" || msg == "LLM call failed" {
|
|
if d, ok := m["duration_ms"]; ok {
|
|
totalLLMLatency += toInt64(d)
|
|
}
|
|
}
|
|
|
|
if level == "ERROR" {
|
|
totalErrors++
|
|
}
|
|
}
|
|
|
|
var avgLLMLatency string
|
|
if totalLLMCalls > 0 {
|
|
avgLLMLatency = fmt.Sprintf("%d ms", totalLLMLatency/int64(totalLLMCalls))
|
|
} else {
|
|
avgLLMLatency = "n/a"
|
|
}
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "**Metricas de %s — %s:**\n\n", a.cfg.Agent.Name, time.Now().UTC().Format("2006-01-02"))
|
|
b.WriteString("| Metrica | Valor |\n")
|
|
b.WriteString("|---------|-------|\n")
|
|
fmt.Fprintf(&b, "| Mensajes recibidos | %d |\n", totalMessages)
|
|
fmt.Fprintf(&b, "| Comandos ejecutados | %d |\n", totalCommands)
|
|
fmt.Fprintf(&b, "| Llamadas LLM | %d |\n", totalLLMCalls)
|
|
fmt.Fprintf(&b, "| Tokens LLM (total) | %d |\n", totalLLMTokens)
|
|
fmt.Fprintf(&b, "| Latencia LLM (media) | %s |\n", avgLLMLatency)
|
|
fmt.Fprintf(&b, "| Llamadas a tools | %d |\n", totalToolCalls)
|
|
fmt.Fprintf(&b, "| Errores de tools | %d |\n", totalToolErrors)
|
|
fmt.Fprintf(&b, "| Errores totales | %d |\n", totalErrors)
|
|
fmt.Fprintf(&b, "| Entradas de log | %d |\n", len(entries))
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// toInt64 converts a JSON number (float64) to int64.
|
|
func toInt64(v any) int64 {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return int64(n)
|
|
case int64:
|
|
return n
|
|
case json.Number:
|
|
i, _ := n.Int64()
|
|
return i
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// prefixAll adds a prefix to each string in a slice.
|
|
func prefixAll(ss []string, prefix string) []string {
|
|
out := make([]string, len(ss))
|
|
for i, s := range ss {
|
|
out[i] = prefix + s
|
|
}
|
|
return out
|
|
}
|