feat(ai): add Jev shadow gates (triage before, review after the LLM diagnosis)

TypeSafe System One(Jev)への最小クライアントと、診断ループに並走する
shadow ゲート 2 つを追加した。triage はアラートと事前取得サンプルからラベル・
回復中か・層を判定し、review は診断の根拠・ランブック・target を検証して
best_action を選ぶ。JEV_MODE=shadow のときだけ動き、答えは ai.jev_shadow と
診断結果の jev フィールドに記録するだけで、処理の分岐は一切変えない。
This commit is contained in:
seon committed 2026-10-08 15:10:07 +09:00
1 parent af1cb4f6dc
commit 91cdd9ada5
6 files changed
+479 -19

No files matched your search

+25 -2
View File
@@ -13,6 +13,7 @@ import (
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/agent"
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/diag"
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/harness"
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/jev"
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/openai"
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/rag"
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/tools"
@@ -94,7 +95,16 @@ func main() {
HitlURL: env("HITL_URL", "http://hitl:8081"),
}
ag := agent.New(acfg, oai, tr, hc, store, enums, pv)
log.Printf("agent up: model=%s prompt=%s labels=%d actions=%d", acfg.Model, pv, len(enums.Labels), len(enums.Actions))
jevMode := env("JEV_MODE", "off") // off | shadow (shadow: Jev answers are recorded next to the LLM diagnosis, never acted on)
var jc *jev.Client
if jevMode == "shadow" {
jc = jev.New(env("JEV_BASE_URL", ""), env("JEV_MODEL", "jev-1.13.0"), keyReader(env("JEV_KEY_FILE", "/run/secrets/typesafe-api-key")))
ag.EnableShadow(jc)
}
log.Printf("agent up: model=%s prompt=%s labels=%d actions=%d jev=%s", acfg.Model, pv, len(enums.Labels), len(enums.Actions), jevMode)
if jc != nil && !jc.KeyPresent() {
log.Printf("jev shadow: typesafe key file empty; gates record 'typesafe api key is empty' until the secret is registered")
}
// runbook ingest (needs the OpenAI key; retried in the background until the key exists)
go func() {
@@ -120,6 +130,11 @@ func main() {
out := map[string]any{"model": acfg.Model, "prompt_version": pv, "labels": enums.Labels, "actions": enums.Actions}
key, kerr := keyReader(env("OPENAI_KEY_FILE", "/run/secrets/openai-api-key"))()
out["openai_key_present"] = kerr == nil && key != ""
out["jev_mode"] = jevMode
if jc != nil {
out["jev_key_present"] = jc.KeyPresent()
out["jev_model"] = jc.Model()
}
out["rag_db"] = errText(store.Ping(c))
n, _ := store.DocCount(c)
out["rag_chunks"] = n
@@ -149,12 +164,20 @@ func main() {
defer func() { inflight-- }()
c, cancel := context.WithTimeout(context.Background(), 170*time.Second) // harness waits 3 minutes
defer cancel()
res, err := ag.Diagnose(c, req)
prefetched := ag.Prefetch(c)
tri := ag.ShadowTriage(c, req, prefetched) // nil unless JEV_MODE=shadow; recorded only
res, err := ag.DiagnoseWith(c, req, prefetched)
if err != nil {
ag.SaveShadow(context.Background(), req, "", nil, tri)
writeJSON(w, 503, map[string]string{"error": err.Error()})
return
}
rev := ag.ShadowReview(c, req, res)
ag.Dispose(c, req, res)
if tri != nil || rev != nil {
res.Jev = map[string]any{"mode": "shadow", "triage": tri, "review": rev}
}
ag.SaveShadow(context.Background(), req, res.SessionID, res, tri, rev)
writeJSON(w, 200, res)
})
srv := &http.Server{Addr: env("AGENT_ADDR", ":8080"), Handler: logging(mux), ReadHeaderTimeout: 10 * time.Second}