phase 4: hermes agent — MCP tools, activity logging, 'all' role, wiring fixes
- mcp/server.go: 7 new tools (get_signal_history, get_patterns, get_skills, request_execution, get_trend, get_event_timeline, get_agent_activity), agent_activity logging middleware on every tool call. - phase3.go: QueryAgentActivity REST handler implemented (was stub). Fixed scan count mismatch in ListSkills/PatchSkill/ListSkillVersions (13 cols → 12 targets). Fixed AgentActivity cursor pagination (lexicographic → integer comparison). Fixed s.Slug → s.Name in log. - cmd/oikos/main.go: 'all' role now runs api + scheduler + notifier in one process. Replaced nil SchedulerRunner/NotifierRunner with direct scheduler.RunnerForMain() / notifier.RunnerForMain() imports. Added runWithPool helper for standalone scheduler/notifier roles. - internal/config/config.go: added HermesAgentID env var. - internal/httpapi/server.go: pass HermesAgentID to MCP handler. - docker-compose.yml: added scheduler and notifier services (dev profile). - hermes/: config.yaml, SOUL.md, skills/homelab-ops/SKILL.md. - Cleaned up: scheduler/init.go dead code, mcp/server.go pgx import guard.
This commit is contained in:
@@ -887,7 +887,7 @@ func (s *Server) ListSkills(ctx context.Context, req gen.ListSkillsRequestObject
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT s.entity_id, s.version, s.name, s.procedure, s.applies_type,
|
||||
s.action, s.pattern_ids, s.status, s.success_rate,
|
||||
s.changed_by::text, s.change_reason, s.last_used_at, s.created_at
|
||||
s.changed_by::text, s.change_reason, s.last_used_at
|
||||
FROM skills s
|
||||
WHERE ($1::text IS NULL OR s.status = $1)
|
||||
AND ($2::text IS NULL OR s.applies_type = $2)
|
||||
@@ -917,7 +917,7 @@ func (s *Server) ListSkills(ctx context.Context, req gen.ListSkillsRequestObject
|
||||
}
|
||||
seen[s.Id.String()] = true
|
||||
if err := json.Unmarshal(procBytes, &s.Procedure); err != nil {
|
||||
slog.Warn("phase3: unmarshal skill procedure", "skill", s.Slug, "error", err)
|
||||
slog.Warn("phase3: unmarshal skill procedure", "skill", s.Name, "error", err)
|
||||
}
|
||||
if len(patternIDs) > 0 {
|
||||
pids := make([]string, len(patternIDs))
|
||||
@@ -977,7 +977,7 @@ func (s *Server) PatchSkill(ctx context.Context, req gen.PatchSkillRequestObject
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT entity_id, version, name, procedure, applies_type, action,
|
||||
pattern_ids, status, success_rate, changed_by::text,
|
||||
change_reason, last_used_at, created_at
|
||||
change_reason, last_used_at
|
||||
FROM skills WHERE entity_id = $1 ORDER BY version DESC LIMIT 1`, id).
|
||||
Scan(&skill.Id, &skill.Version, &skill.Name, &procBytes, &skill.AppliesType,
|
||||
&skill.Action, &patternIDs, &skill.Status, &skill.SuccessRate,
|
||||
@@ -1022,7 +1022,7 @@ func (s *Server) ListSkillVersions(ctx context.Context, req gen.ListSkillVersion
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT entity_id, version, name, procedure, applies_type, action,
|
||||
pattern_ids, status, success_rate, changed_by::text,
|
||||
change_reason, last_used_at, created_at
|
||||
change_reason, last_used_at
|
||||
FROM skills WHERE entity_id = $1
|
||||
ORDER BY version DESC`, id)
|
||||
if err != nil {
|
||||
@@ -1739,7 +1739,82 @@ func (s *Server) GetEntityKnowledge(ctx context.Context, request gen.GetEntityKn
|
||||
// ─── Agent Activity (stub) ─────────────────────────────────────────────
|
||||
|
||||
func (s *Server) QueryAgentActivity(ctx context.Context, request gen.QueryAgentActivityRequestObject) (gen.QueryAgentActivityResponseObject, error) {
|
||||
return nil, errNotImplemented
|
||||
limit := clampLimit(request.Params.Limit)
|
||||
from := time.Now().Add(-24 * time.Hour)
|
||||
if request.Params.From != nil {
|
||||
from = *request.Params.From
|
||||
}
|
||||
to := time.Now()
|
||||
if request.Params.To != nil {
|
||||
to = *request.Params.To
|
||||
}
|
||||
|
||||
var agentID *string
|
||||
if request.Params.AgentId != nil {
|
||||
a := *request.Params.AgentId
|
||||
agentID = &a
|
||||
}
|
||||
var activityType *string
|
||||
if request.Params.ActivityType != nil {
|
||||
a := string(*request.Params.ActivityType)
|
||||
activityType = &a
|
||||
}
|
||||
var entityID *string
|
||||
if request.Params.EntityId != nil {
|
||||
a := *request.Params.EntityId
|
||||
entityID = &a
|
||||
}
|
||||
var cursorID *int
|
||||
if request.Params.Cursor != nil && *request.Params.Cursor != "" {
|
||||
if id, err := parseIntOrZero(*request.Params.Cursor); err == nil && id > 0 {
|
||||
cursorID = &id
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, ts, agent_id::text, session_id, activity_type, tool_name,
|
||||
entity_id::text, input_summary, output_summary,
|
||||
duration_ms, token_count, success, correlation_id
|
||||
FROM agent_activity
|
||||
WHERE ts >= $1 AND ts <= $2
|
||||
AND ($3::text IS NULL OR agent_id::text = $3)
|
||||
AND ($4::text IS NULL OR activity_type = $4)
|
||||
AND ($5::text IS NULL OR entity_id::text = $5)
|
||||
AND ($6::bigint IS NULL OR id < $6::bigint)
|
||||
ORDER BY id DESC
|
||||
LIMIT $7`,
|
||||
from, to, agentID, activityType, entityID, cursorID, limit+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []gen.AgentActivity{}
|
||||
for rows.Next() {
|
||||
var a gen.AgentActivity
|
||||
if err := rows.Scan(&a.Id, &a.Ts, &a.AgentId, &a.SessionId,
|
||||
&a.ActivityType, &a.ToolName, &a.EntityId,
|
||||
&a.InputSummary, &a.OutputSummary,
|
||||
&a.DurationMs, &a.TokenCount, &a.Success,
|
||||
&a.CorrelationId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, a)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, rows.Err()
|
||||
}
|
||||
|
||||
var next *string
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
lastID := fmt.Sprintf("%d", items[len(items)-1].Id)
|
||||
next = &lastID
|
||||
}
|
||||
if items == nil {
|
||||
items = []gen.AgentActivity{}
|
||||
}
|
||||
return gen.QueryAgentActivity200JSONResponse{Items: items, NextCursor: next}, nil
|
||||
}
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────
|
||||
@@ -1750,3 +1825,14 @@ func coalesceStr(s *string, def string) string {
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func parseIntOrZero(s string) (int, error) {
|
||||
var n int
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
return 0, fmt.Errorf("invalid integer: %q", s)
|
||||
}
|
||||
n = n*10 + int(c-'0')
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// contextKey for storing actor identity in request context.
|
||||
@@ -109,7 +110,13 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Hand
|
||||
r.With(combinedAuth(cfg)).Get("/api/v1/events/stream", s.serveSSE)
|
||||
|
||||
// Mount MCP at /mcp (plan R3-10)
|
||||
r.With(combinedAuth(cfg)).Handle("/mcp", mcphandler.NewHandler(pool, cfg.MCPBearerToken))
|
||||
hermesAgentID := uuid.Nil
|
||||
if cfg.HermesAgentID != "" {
|
||||
if id, err := uuid.Parse(cfg.HermesAgentID); err == nil {
|
||||
hermesAgentID = id
|
||||
}
|
||||
}
|
||||
r.With(combinedAuth(cfg)).Handle("/mcp", mcphandler.NewHandler(pool, cfg.MCPBearerToken, hermesAgentID))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user