package coordinator import ( "context" "log/slog" "time" "chat-agent-system/internal/jevdecide" ) // DecideAssist is the narrow port for typed jev decisions (nil-safe). // Implemented by *jevdecide.Client; injected from the app composition root. type DecideAssist interface { DecideRealtime(ctx context.Context, userInput string) (yes bool, ans jevdecide.Answer, err error) DecideProfileBucket(ctx context.Context, userInput string) (bucket string, ans jevdecide.Answer, err error) } // SetDecideAssist injects the optional jev decide client; nil disables product integration. func (e *CoordinatorEngine) SetDecideAssist(d DecideAssist) { if e != nil { e.decideAssist = d } } // resolveRealtimeQuery applies rule + optional jev shadow/assist. // Rules always run. In assist mode, high-confidence jev "yes" can only ADD recall // (rule || jevYes); it never turns a rule-true into false. func (e *CoordinatorEngine) resolveRealtimeQuery(ctx context.Context, userInput string) bool { rule := IsRealtimeInformationQuery(userInput) mode := jevdecide.CurrentMode() if mode == jevdecide.ModeOff || e == nil || e.decideAssist == nil { return rule } yes, ans, err := e.decideAssist.DecideRealtime(ctx, userInput) if err != nil { jevdecide.Global.JevFail.Add(1) slog.Info("jevdecide realtime fail-open", "err", err.Error(), "rule", rule) return rule } agree := yes == rule if agree { jevdecide.Global.Agree.Add(1) } else { jevdecide.Global.Disagree.Add(1) } slog.Info("jevdecide realtime shadow", "mode", string(mode), "rule", rule, "jev", yes, "confidence", ans.Confidence, "margin", ans.Margin(), "agree", agree) if mode != jevdecide.ModeAssist { return rule } // assist: conservative synthesis — rules are the floor if yes && ans.Reliable(jevdecide.MinAssistConf, jevdecide.MinAssistMargin) { if !rule { jevdecide.Global.AssistHit.Add(1) } return true } return rule } // shadowProfile observes jev complexity bucket without changing routing. func (e *CoordinatorEngine) shadowProfile(ctx context.Context, userInput string, profile TaskProfile) { mode := jevdecide.CurrentMode() if mode == jevdecide.ModeOff || e == nil || e.decideAssist == nil { return } start := time.Now() bucket, ans, err := e.decideAssist.DecideProfileBucket(ctx, userInput) _ = start ruleBucket := jevdecide.ProfileBucket(profile.ComplexityScore) if err != nil { jevdecide.Global.JevFail.Add(1) slog.Info("jevdecide profile fail-open", "err", err.Error(), "rule_bucket", ruleBucket) return } agree := bucket == ruleBucket if agree { jevdecide.Global.Agree.Add(1) } else { jevdecide.Global.Disagree.Add(1) } slog.Info("jevdecide profile shadow", "mode", string(mode), "rule_bucket", ruleBucket, "jev_bucket", bucket, "score", profile.ComplexityScore, "confidence", ans.Confidence, "agree", agree) }