Files
noema/cmd/noemactl/events.go
T
Andy Dixon 85d769b245 feat: add immutable cognitive event store, live streams and event explorer
Phase 3. Events are the append-only record of everything a mind does
(ADR-0002). pkg/events defines the closed type vocabulary, categories,
evidence references and the observation payload with explicit trust
levels. The store validates and appends inside the caller's transaction,
publishes to a bounded fan-out bus only after commit, queries by seq
cursor with type/category/source/salience/confidence/time/search
filters, and walks provenance: cause, parents, children, cited evidence,
derived events and correlation groups. Durable per-consumer cursors never
move backwards.

Server-Sent Events streams (per mind and global) subscribe before
replaying history after Last-Event-ID so nothing is missed, apply the
same filter vocabulary live, and heartbeat. Operators can inject
observations tagged with operator trust for demos and tests.

UI: live thought stream with filters and paging, forensic event page
with causal chain and evidence navigation, dashboard important events.
Personality and status changes now leave operator_action events in the
mind's own history. CLI: event inspect/list/tail, observe.
2026-06-17 20:02:22 +01:00

109 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"strings"
"time"
)
func init() {
extraCommands = append(extraCommands,
command{"event", "inspect <id>|list <mind> [-type t] [-n 50]|tail <mind> – cognitive events", cmdEvent},
command{"observe", "<mind> <text> – inject an operator observation", cmdObserve},
)
}
func printEvent(ev map[string]any) {
fmt.Printf("%s %-22s %-12s %s [sal %.2f conf %.2f]\n", fmt.Sprint(ev["occurred_at"])[11:19], ev["type"], ev["source"], ev["summary"], num(ev["salience"]), num(ev["confidence"]))
}
func num(v any) float64 { f, _ := v.(float64); return f }
func cmdEvent(args []string) int {
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "usage: noemactl event inspect <id> | list <mind> [-type t] [-category c] [-n 50] [-json] | tail <mind>")
return exitUsage
}
switch args[0] {
case "inspect":
var ctx map[string]any
if code, err := apiRequest("GET", "/api/v1/events/"+args[1]+"/context", nil, &ctx); err != nil {
return exitFor(code, err)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(ctx) //nolint:errcheck
return exitOK
case "list":
fs := flag.NewFlagSet("event list", flag.ContinueOnError)
typ := fs.String("type", "", "event type")
cat := fs.String("category", "", "category")
n := fs.Int("n", 50, "count")
asJSON := fs.Bool("json", false, "raw JSON")
if fs.Parse(args[2:]) != nil {
return exitUsage
}
var out struct{ Items []map[string]any }
q := fmt.Sprintf("?limit=%d&type=%s&category=%s", *n, *typ, *cat)
if code, err := apiRequest("GET", "/api/v1/minds/"+args[1]+"/events"+q, nil, &out); err != nil {
return exitFor(code, err)
}
if *asJSON {
json.NewEncoder(os.Stdout).Encode(out.Items) //nolint:errcheck
return exitOK
}
for i := len(out.Items) - 1; i >= 0; i-- {
printEvent(out.Items[i])
}
return exitOK
case "tail":
base := os.Getenv("NOEMA_URL")
if base == "" {
base = "http://127.0.0.1:8080"
}
req, _ := http.NewRequest("GET", strings.TrimRight(base, "/")+"/api/v1/minds/"+args[1]+"/events/stream", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("NOEMA_API_TOKEN"))
res, err := (&http.Client{Timeout: 0}).Do(req)
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
return exitFail
}
defer res.Body.Close()
if res.StatusCode != 200 {
return exitFor(res.StatusCode, fmt.Errorf("%s", res.Status))
}
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "data: ") {
var ev map[string]any
if json.Unmarshal([]byte(line[6:]), &ev) == nil {
printEvent(ev)
}
}
}
return exitOK
}
fmt.Fprintln(os.Stderr, "unknown event subcommand", args[0])
return exitUsage
}
func cmdObserve(args []string) int {
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "usage: noemactl observe <mind> <text...>")
return exitUsage
}
var ev map[string]any
if code, err := apiRequest("POST", "/api/v1/minds/"+args[0]+"/observations", map[string]any{"text": strings.Join(args[1:], " "), "source": "noemactl"}, &ev); err != nil {
return exitFor(code, err)
}
fmt.Printf("recorded observation %v (seq %v) at %v\n", ev["id"], ev["seq"], time.Now().Format(time.RFC3339))
return exitOK
}