Files
noema/internal/events/helpers.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

78 lines
2.0 KiB
Go

package events
import (
"context"
"strings"
"github.com/andydixon/noema/pkg/events"
"github.com/andydixon/noema/pkg/ids"
)
// OperatorAction records an administrative change as a cognitive event so the
// mind's history shows when and why it was altered from outside.
func (s *Store) OperatorAction(ctx context.Context, mind ids.ID, actor, action, summary string, details map[string]any) {
if details == nil {
details = map[string]any{}
}
details["action"], details["actor"] = action, actor
s.Append(ctx, events.New{MindID: mind, Type: events.OperatorAction, Source: "operator", Summary: summary, Salience: 0.6, Confidence: 1, //nolint:errcheck
Metadata: map[string]any{"action": action, "actor": actor}, Payload: details})
}
// Matches applies the parts of a Filter that can be evaluated in memory, for
// live streams.
func (f Filter) Matches(ev events.Event) bool {
if !f.MindID.IsZero() && ev.MindID != f.MindID {
return false
}
if len(f.Types) > 0 && !containsType(f.Types, ev.Type) {
return false
}
if len(f.Categories) > 0 && !containsCat(f.Categories, ev.Category) {
return false
}
if len(f.Sources) > 0 && !containsStr(f.Sources, ev.Source) {
return false
}
if ev.Salience < f.MinSalience || ev.Confidence < f.MinConfidence {
return false
}
if f.MaxConfidence > 0 && ev.Confidence > f.MaxConfidence {
return false
}
if !f.CorrelationID.IsZero() && ev.CorrelationID != f.CorrelationID {
return false
}
if q := strings.TrimSpace(f.Search); q != "" && !strings.Contains(strings.ToLower(ev.Summary), strings.ToLower(q)) {
return false
}
return true
}
func containsType(xs []events.Type, x events.Type) bool {
for _, y := range xs {
if y == x {
return true
}
}
return false
}
func containsCat(xs []events.Category, x events.Category) bool {
for _, y := range xs {
if y == x {
return true
}
}
return false
}
func containsStr(xs []string, x string) bool {
for _, y := range xs {
if y == x {
return true
}
}
return false
}