feat(ai-core): trace instrumentation, LLM thinker, decay scheduler
Cyrene CI / backend (push) Has been cancelled
Cyrene CI / frontend (push) Has been cancelled

- orchestrator: Tracer interface with spans for intent analysis, persona,
  subsession dispatch, context build, LLM streaming and each tool call;
  traces finish when the stream completes — the admin Trace waterfall
  finally receives data
- background: Thinker generates real LLM thoughts (post-chat + silence
  triggers, recent history as context, empty/overlong outputs discarded);
  thoughts broadcast to web clients as 'thinking' WS messages; pipeline
  postChatHook triggers post-chat thinking after each round
- memory: ApplyDecayAll + ListUserIDs with a 6h decay/purge cron in main
  (was implemented but never scheduled)
This commit is contained in:
Aska committed 2026-08-20 20:57:27 +08:00
1 parent 8495a4b055
commit 8603839711
8 files changed
+338 -10

No files matched your search

+39
View File
@@ -32,6 +32,7 @@ import (
webfetch "git.yeij.top/AskaEth/Cyrene_v2/plugins/web-fetch"
websearch "git.yeij.top/AskaEth/Cyrene_v2/plugins/web-search"
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/background"
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/llm"
llmlog "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/llm"
tracepkg "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/trace"
@@ -244,6 +245,37 @@ func main() {
perfMon := perfpkg.NewMonitor(120, 10*time.Second)
perfMon.Start()
// Wire tracing into the dialogue pipeline
orch.SetTracer(pipelineTracer{traceLogger})
// === 12.5 Background Thinker ===
thinker := background.New(true, 5*time.Minute, 30*time.Minute)
thinker.SetLLM(&memory.LLMAdapterBridge{Adapter: llmSelector.DefaultAdapter()})
thinker.SetHistoryProvider(func(sessionID string, limit int) []model.Message {
msgs := ctxStore.Get(sessionID)
if len(msgs) > limit {
msgs = msgs[len(msgs)-limit:]
}
return msgs
})
thinker.Start(context.Background())
orch.SetPostChatHook(thinker.TriggerPostChat)
// === 12.6 Memory decay scheduler ===
if memSvc != nil {
go func() {
ticker := time.NewTicker(6 * time.Hour)
defer ticker.Stop()
for range ticker.C {
if n, err := memSvc.ApplyDecayAll(context.Background()); err != nil {
logger.Infof("[memory] decay run failed: %v", err)
} else if n > 0 {
logger.Infof("[memory] decay applied (%d memories updated)", n)
}
}
}()
}
gwDeps := api.Dependencies{
AuthMgr: authMgr,
Orchestrator: orch,
@@ -267,6 +299,13 @@ func main() {
wsHub := ws.NewHub(orch)
gwDeps.WSHub = wsHub
// Push background thoughts to connected web clients
go func() {
for th := range thinker.ThoughtChan() {
wsHub.Broadcast(map[string]any{"type": "thinking", "content": th.Content})
}
}()
apiRouter := api.New(gwDeps)
wsMux := http.NewServeMux()
+6
View File
@@ -40,6 +40,12 @@ func (a *traceAdapter) Recent(n int) []map[string]any {
func (a *traceAdapter) Clear() { a.Logger.Clear() }
// pipelineTracer adapts trace.Logger to orchestrator.Tracer
// (StartTrace's return value is discarded at the call site).
type pipelineTracer struct{ *tracepkg.Logger }
func (p pipelineTracer) StartTrace(id, userMsg string) { p.Logger.StartTrace(id, userMsg) }
func spanToMap(s tracepkg.Span) map[string]any {
m := map[string]any{
"name": s.Name, "label": s.Label,