From 9205cece65910c38230b4d457a59f4e680199c1f Mon Sep 17 00:00:00 2001 From: Smokey <150986568+jacisjake@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:26:30 -0600 Subject: [PATCH] feat(mode): Jev solo/conspiracy/split classification on nest requests (#22) --- jev.go | 58 ++++++++++++++++++++++++++++++++ nestapi.go | 99 +++++++++++++++++++++++++++++++++--------------------- nestlog.go | 21 +++++++----- 3 files changed, 131 insertions(+), 47 deletions(-) diff --git a/jev.go b/jev.go index fef8523..0f4b2d7 100644 --- a/jev.go +++ b/jev.go @@ -155,6 +155,64 @@ func deref(f *float64) float64 { return *f } +// modeQuestions classifies how a request should be taken up (Epic C). The +// escape option "other" keeps Jev from forcing a wrong class; mode is discrete, +// so choice, not score (JEV_STRATEGIES.md strategy 3). +var modeQuestions = map[string]any{ + "mode": map[string]any{ + "type": "choice", + "instructions": "How should this chatroom request be taken up?", + "criteria": map[string]string{ + "solo": "Simple: one crow answers it directly.", + "conspiracy": "A judgment call worth several distinct readings: several crows take the same request from their different strengths.", + "split": "Large with distinct parts: separate subtasks, one owner each.", + "other": "None of these fits well.", + }, + }, +} + +// ClassifyMode returns the mode when Jev is set and its choice is at or above +// the band. "other" reads as unsure (not ok) so callers fall back to a header. +func (j *JevClient) ClassifyMode(ctx context.Context, text string) (string, map[string]float64, bool) { + if j == nil { + return "", nil, false + } + answers, _, _, err := j.SystemOne(ctx, text, modeQuestions) + if err != nil { + return "", nil, false + } + a := answers["mode"] + if a.Choice == "" || a.Choice == "other" || a.Confidence == nil || *a.Confidence < j.threshold { + return "", nil, false + } + return a.Choice, a.Probabilities, true +} + +// ClassifyMode classifies a request under the same guardrails as any call: +// flock cap, timeout and flight log, with the probabilities recorded. Returns +// the mode and ok; below-band or absent Jev means not ok. +func (f *Flock) ClassifyMode(ctx context.Context, text string, budgets ...*Budget) (string, bool) { + if f.jev == nil { + return "", false + } + for _, b := range budgets { + if err := b.Check(); err != nil { + return "", false + } + } + f.sem <- struct{}{} + defer func() { <-f.sem }() + cctx, cancel := context.WithTimeout(ctx, f.g.CrowTimeout) + defer cancel() + start := time.Now() + mode, probs, ok := f.jev.ClassifyMode(cctx, text) + e := Event{Kind: "mode", Item: text, Crow: "Sentry", Model: f.jev.model, Ms: time.Since(start).Milliseconds(), Prob: probs, Mode: mode} + if f.log != nil { + f.log.Write(e) + } + return mode, ok +} + func (c JevCall) confident(threshold float64) bool { return c.ModeConf >= threshold && c.TaskTypeConf >= threshold } diff --git a/nestapi.go b/nestapi.go index 72d9967..ed47452 100644 --- a/nestapi.go +++ b/nestapi.go @@ -42,20 +42,33 @@ func (s *Server) getNestMessages(w http.ResponseWriter, r *http.Request) { all := s.msgs.All(nestID) out := make([]map[string]any, 0, len(all)) for _, m := range all { - out = append(out, map[string]any{ - "msg_id": m.MsgID(), - "kind": m.Kind, - "author": m.Author, - "text": m.Text, - "to": m.To, - "refers_to": m.RefersTo, - "tagged_by": m.TaggedBy, - "ts": m.Timestamp, - }) + out = append(out, nestRecord(m)) } writeJSON(w, http.StatusOK, out) } +// nestRecord is the EVENTS.md wire shape for one message. +func nestRecord(m Message) map[string]any { + rec := map[string]any{ + "msg_id": m.MsgID(), + "kind": m.Kind, + "author": m.Author, + "text": m.Text, + "to": m.To, + "refers_to": m.RefersTo, + "tagged_by": m.TaggedBy, + "ts": m.Timestamp, + } + if m.Mode != "" { + rec["mode"] = m.Mode + rec["mode_tagged_by"] = m.ModeTagged + } + if m.Invited > 0 { + rec["invited"] = m.Invited + } + return rec +} + func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { nestID := r.PathValue("nest_id") nest, found := s.nests.Get(nestID) @@ -68,6 +81,7 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { Author string `json:"author"` Text string `json:"text"` Kind string `json:"kind"` + Mode string `json:"mode"` To string `json:"to"` RefersTo string `json:"refers_to"` } @@ -115,15 +129,20 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { } } + // Mode (Epic C, same rule shape as kind): Jev first, header escape, "" when unsure. + mode, modeBy := s.tagMode(kind, body.Mode, body.Text) + msg := Message{ - NestID: nestID, - Kind: kind, - Author: body.Author, - Text: body.Text, - To: body.To, - RefersTo: body.RefersTo, - TaggedBy: taggedBy, - Timestamp: time.Now().UTC().Format(time.RFC3339), + NestID: nestID, + Kind: kind, + Author: body.Author, + Text: body.Text, + To: body.To, + RefersTo: body.RefersTo, + TaggedBy: taggedBy, + Mode: mode, + ModeTagged: modeBy, + Timestamp: time.Now().UTC().Format(time.RFC3339), } record, err := s.msgs.Append(msg) if err != nil { @@ -139,16 +158,27 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { go s.respondToRequest(nest, record) } - writeJSON(w, http.StatusCreated, map[string]any{ - "msg_id": record.MsgID(), - "kind": record.Kind, - "author": record.Author, - "text": record.Text, - "to": record.To, - "refers_to": record.RefersTo, - "tagged_by": record.TaggedBy, - "ts": record.Timestamp, - }) + writeJSON(w, http.StatusCreated, nestRecord(record)) +} + +var modePostable = map[string]bool{"solo": true, "conspiracy": true, "split": true} + +// tagMode classifies a request's mode (Jev first, header escape). Only requests +// carry a mode; other kinds get "". +func (s *Server) tagMode(kind, header, text string) (string, string) { + if kind != "request" { + return "", "" + } + if m, ok := s.p.Flock.ClassifyMode(s.ctx, text); ok { + if modePostable[header] && header != m { + return m, "disagreement" + } + return m, "jev" + } + if modePostable[header] { + return header, "header" + } + return "", "default" } // tagKind runs the A1 rule: Jev first; Jev unsure -> the poster's header; @@ -169,16 +199,9 @@ func (s *Server) tagKind(header, text string) (string, string) { } func (s *Server) emitNestMessage(m Message) { - s.hub.PublishNest(m.NestID, "nest.message", map[string]any{ - "nest_id": m.NestID, - "msg_id": m.MsgID(), - "kind": m.Kind, - "author": m.Author, - "text": m.Text, - "to": m.To, - "refers_to": m.RefersTo, - "tagged_by": m.TaggedBy, - }) + rec := nestRecord(m) + rec["nest_id"] = m.NestID + s.hub.PublishNest(m.NestID, "nest.message", rec) } // respondToRequest is the must-respond rule: the addressed crow answers with diff --git a/nestlog.go b/nestlog.go index ec892b4..587c800 100644 --- a/nestlog.go +++ b/nestlog.go @@ -13,15 +13,18 @@ import ( // contract. IDs are assigned by the store; MsgID is "-" and is // what the contract exposes. type Message struct { - NestID string `json:"nest_id"` - ID int `json:"id"` - Kind string `json:"kind"` - Author string `json:"author"` - Text string `json:"text"` - To string `json:"to,omitempty"` - RefersTo string `json:"refers_to,omitempty"` - TaggedBy string `json:"tagged_by,omitempty"` - Timestamp string `json:"ts"` + NestID string `json:"nest_id"` + ID int `json:"id"` + Kind string `json:"kind"` + Author string `json:"author"` + Text string `json:"text"` + To string `json:"to,omitempty"` + RefersTo string `json:"refers_to,omitempty"` + TaggedBy string `json:"tagged_by,omitempty"` + Mode string `json:"mode,omitempty"` // solo | conspiracy | split (requests only, Epic C) + ModeTagged string `json:"mode_tagged_by,omitempty"` + Invited int `json:"invited,omitempty"` + Timestamp string `json:"ts"` } func (m Message) MsgID() string { return fmt.Sprintf("%s-%d", m.NestID, m.ID) }