Files
Cyrene_v2/internal/runtime/capacity/recall.go
T
Aska 6dbb4a2e18
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat: 唤醒式架构批次——capacity/L4/会话编排/管理面板写CRUD/前端集成(77-81)
【capacity 唤醒式核心(77)】internal/runtime/capacity:(Decider L1启发式+L2上下文+人格覆盖→三态静息/唤醒/全能→允许工具集+capacity_awaken事件)。loop 外围允许集过滤(不硬阻断=能力≠边界红线)。config 加 Capacity(Enabled/MinConfidence/ToolSets)。wire buildCapacity 接入 loop。

【L4 记忆驱动意图(80)】recall.go:从记忆召回用户习惯(搞一下xx=办事)→唤醒L1/L2漏掉/升置信;群聊陌生人不读owner习惯(隔离红线);RecordIntent闭环(判定写回记忆越用越准);默认保守静息。

【会话消息编排(78)】gateway/session_orchestrator.go:每会话(模式/pending有界5/tail上下文尾巴20);Submit非阻塞(idle立即/busy入队ack/满拒绝);完成回调按序续接(tail+新消息合并增量注入非全量重放);Interrupt中断;REST 202/409+WS session_busy/pending_ack事件;不重造任务引擎复用loop。

【前端集成(79)】ws.ts 加 capacity_awaken/session_busy/pending_ack 解析;store 加 awakenState/sessionBusy;TopBar/ChatShell 加唤醒态徽标+忙态提示(不阻塞输入框);状态组件 status.tsx;Miuix风格。

【管理面板写CRUD(81)】20个写端点(POST/PUT/DELETE /admin/*,owner+审计落库):personas(真持久化YAML)/users apikey(签发掩码+撤销)/iot控制(真下发)/automations(建/触发/删)/llm(test真调用+临时)/channels(connect/close/身份测试actor白名单)/plugins(卸载)/memories(search真检索)/sessions(export)。无存储如实501不伪造。拆4文件满足≤500红线。前端9模块接真写。

【wire收口】buildCapacity接入loop+OnCapacity事件;server.go newOrchestrator接hub。

实机验证:iot ac.living control 200 真实改变 on:true;observability/metrics 200、health ok;全仓 build/vet/test 47包绿+前端tsc/vite。
2026-09-06 21:54:40 +08:00

326 lines
10 KiB
Go

package capacity
import (
"math"
"strconv"
"strings"
)
// RecallItem is one recalled memory hit handed to the decider for L4 habit
// alignment (design §2.2 L4). It is a decoupling view over memory.Result so the
// capacity package stays independent of the memory subsystem; the loop builds it
// from Retriever.Retrieve. Scope carries the memory scope value ("user" / "persona"
// / "project") so L4 can honor the isolation red line (学行为习惯, 不读陌生人敏感).
type RecallItem struct {
Content string
Scope string
Kind string
Tags []string
Score float64
}
// Judgment is the intent outcome of one round, meant to be written back into
// memory so the L4 layer learns the user's task-expression habits over time
// (design §2.4 闭环学习). It records the raw intent-layer classification (before
// any persona override) because the closed loop learns the user's intent pattern,
// not the persona's presentation. Text is the user expression that was judged — the
// habit key L4 re-matches on later rounds.
type Judgment struct {
IsTask bool
TaskKind string
State State
Confidence float64
Text string
}
// L4 tuning constants.
const (
// minHabitOverlap is the minimum longest-common-substring length (in runes) a
// recalled habit must share with the current turn before L4 treats it as a
// matching habit. 3 runes covers a short Chinese phrase (搞一下) while staying
// conservative against token noise.
minHabitOverlap = 3
// taskHabitConfidence is the wake confidence L4 assigns when it matches a
// habit-style memory (not a canonical intent record) that leans 办事. It sits
// above the default wake threshold so the habit can actually wake a missed task.
taskHabitConfidence = 0.7
// intent kind tokens stored in an intent record and understood by awakenVerdict.
taskKindComplex = "complex"
taskKindCode = "code"
taskKindQuery = "query"
taskKindOps = "ops"
// intentRecordPrefix marks a canonical intent record written back by RecordIntent.
intentRecordPrefix = "intent:"
)
// RecordIntent formats a judgment into the canonical intent-record string written
// back to memory (design §2.4). Only task judgments are worth learning — a quiet /
// non-task judgment returns "" so callers can skip persisting noise. The format is
// machine-parseable by parseIntentRecord, which L4 re-reads on later rounds to
// close the learning loop. The user expression is written in plain text so the
// keyword retriever can still surface it against a later similar turn.
func (d *Decider) RecordIntent(j Judgment) string {
if !j.IsTask {
return ""
}
return formatIntentRecord(j)
}
// formatIntentRecord encodes j as one parseable line. The text field comes last and
// is taken verbatim (it may contain ';' or '='), so parseIntentRecord reads the
// fixed prefix and keeps the remainder as the expression.
func formatIntentRecord(j Judgment) string {
return intentRecordPrefix +
"isTask=" + strconv.FormatBool(j.IsTask) +
";taskKind=" + j.TaskKind +
";confidence=" + strconv.FormatFloat(j.Confidence, 'f', -1, 64) +
";text=" + j.Text
}
// intentRecord is the parsed form of a canonical intent record.
type intentRecord struct {
isTask bool
taskKind string
confidence float64
text string
}
// parseIntentRecord decodes a canonical intent record written by formatIntentRecord.
// ok is false when content is not a well-formed intent record (missing isTask).
func parseIntentRecord(content string) (intentRecord, bool) {
rest, ok := strings.CutPrefix(content, intentRecordPrefix)
if !ok {
return intentRecord{}, false
}
// text is last and taken verbatim, so a ';' or '=' inside it cannot split it.
head, text := rest, ""
if idx := strings.Index(rest, ";text="); idx >= 0 {
head = rest[:idx]
text = rest[idx+len(";text="):]
}
rec := intentRecord{text: text}
seenTask := false
for _, f := range strings.Split(head, ";") {
k, v, cut := strings.Cut(f, "=")
if !cut {
continue
}
switch k {
case "isTask":
rec.isTask = v == "true"
seenTask = true
case "taskKind":
rec.taskKind = v
case "confidence":
if x, err := strconv.ParseFloat(v, 64); err == nil {
rec.confidence = x
}
}
}
if !seenTask {
return intentRecord{}, false
}
return rec, true
}
// l4 is the L4 habit-alignment layer (design §2.2, §2.4): it uses recalled user
// habits to correct or fill in the intent that L1/L2 could not settle. It returns
// ok=false when no habit applies, so the caller's L1/L2 verdict (or the
// conservative fallback) stands — L4 is silent unless a learned habit matches
// (向后兼容, 数据缺失时回退).
func (d *Decider) l4(current string, recall []RecallItem) (State, float64, string, bool) {
text := strings.TrimSpace(current)
if text == "" || len(recall) == 0 {
return "", 0, "", false
}
bestState, bestConf, bestTool := State(""), 0.0, ""
matched := false
for _, it := range recall {
if !allowedHabitScope(it.Scope) {
continue
}
if rec, ok := parseIntentRecord(it.Content); ok {
// A canonical record written back by RecordIntent: it knows the exact
// task kind and confidence. Only a task record with a matching expression
// testifies to task; the stored text is plaintext so overlap is matched
// against it (tolerating paraphrase).
if !rec.isTask || rec.text == "" || overlapLen(text, rec.text) < minHabitOverlap {
continue
}
state, tool := awakenVerdict(rec.taskKind)
conf := clampConf(rec.confidence)
if conf == 0 {
conf = taskHabitConfidence
}
if conf > bestConf {
bestState, bestConf, bestTool, matched = state, conf, tool, true
}
continue
}
// Habit-style memory (not written by us): treat it as a task habit only when
// it leans 办事 and shares a phrase with the current turn, so an unrelated
// preference (coffee habit) never triggers a wake.
if !leansTask(it.Content) || overlapLen(text, it.Content) < minHabitOverlap {
continue
}
conf := taskHabitConfidence
if it.Score > 0 {
conf = clampConf(taskHabitConfidence + it.Score*0.15)
}
state, tool := StateAwake, inferTool(text)
if isComplex(text) {
state = StateFull
}
if conf > bestConf {
bestState, bestConf, bestTool, matched = state, conf, tool, true
}
}
if !matched {
return "", 0, "", false
}
return bestState, bestConf, bestTool, true
}
// mergeL4 combines an L1/L2 verdict with the L4 habit verdict (design §2.2 合并规则).
// It is conservative: a strong learned habit may count a task the heuristics missed
// (quiet → awake, a 举手示意 but never a jump straight to full) or confirm/boost one
// they caught, up to full when the habit says complex; it never drags a clear task
// down to quiet (学行为习惯,不误打断).
func (d *Decider) mergeL4(s State, c float64, t string, l4s State, l4c float64, l4t string, l4ok bool) (State, float64, string) {
if !l4ok || (l4s != StateAwake && l4s != StateFull) {
return s, c, t
}
if s == StateQuiet || s == "" {
// 保守红线:静息→唤醒是"举手示意",即便习惯标记复杂也不直接跳到全能。
return StateAwake, l4c, l4t
}
st := s
if l4s == StateFull {
st = StateFull
}
return st, boostConf(c, l4c), pickTool(t, l4t)
}
// awakenVerdict maps an intent-record task kind back to a state + lit tool.
func awakenVerdict(kind string) (State, string) {
switch kind {
case taskKindComplex:
return StateFull, ""
case taskKindCode:
return StateAwake, "file"
case taskKindQuery:
return StateAwake, "web-search"
case taskKindOps:
return StateAwake, "terminal"
default:
return StateAwake, ""
}
}
// taskKindFor maps a (state, tool) to the intent kind recorded for writeback.
func taskKindFor(state State, tool string) string {
if state == StateFull {
return taskKindComplex
}
switch tool {
case "file":
return taskKindCode
case "web-search", "web-fetch", "http":
return taskKindQuery
case "terminal":
return taskKindOps
default:
return ""
}
}
// allowedHabitScope reports the scopes L4 may read for user habits. ScopeUser is the
// owner's shared habit (primary); ScopePersona / ScopeProject are readable per design
// §2.2; an empty scope (recall without scope metadata) is tolerated. Any other scope
// is refused, so a stranger / owner-private memory can never reach L4 here (design
// §2.2 红线: 学行为习惯, 不读陌生人敏感).
func allowedHabitScope(scope string) bool {
switch scope {
case "", "user", "persona", "project":
return true
default:
return false
}
}
// leansTask reports whether a habit memory is about the user's task-expression
// pattern rather than an unrelated preference, so L4 does not wake on a coffee habit.
func leansTask(content string) bool {
return containsAny(content, []string{
"办事", "任务", "布置", "要办", "让我", "要我", "去做", "来办", "干活", "做工",
"习惯用", "习惯说", "让我做", "叫我",
// ASCII
"task", "asks me to", "want me to", "has me", "habit",
})
}
// overlapLen returns the length (in runes) of the longest common substring of a and
// b — the lexical-overlap measure L4 uses to detect "the user expression this round
// echoes a phrase the memory recorded", tolerating paraphrase (搞一下那个部署 vs
// 搞一下这个部署 share 搞一下).
func overlapLen(a, b string) int {
ar, br := []rune(a), []rune(b)
if len(ar) == 0 || len(br) == 0 {
return 0
}
prev := make([]int, len(br)+1)
best := 0
for i := 1; i <= len(ar); i++ {
cur := make([]int, len(br)+1)
for j := 1; j <= len(br); j++ {
if ar[i-1] == br[j-1] {
cur[j] = prev[j-1] + 1
if cur[j] > best {
best = cur[j]
}
}
}
prev = cur
}
return best
}
// boostConf raises a confirmed task confidence above the stronger of the two sources,
// reflecting corroboration (设计: 越用越准), capped at 1.
func boostConf(a, b float64) float64 { return clampConf(math.Max(a, b) + 0.05) }
// clampConf clamps c to [0,1].
func clampConf(c float64) float64 {
if c < 0 {
return 0
}
if c > 1 {
return 1
}
return c
}
// pickTool prefers the learned habit's lit tool when non-empty, else the caller's.
func pickTool(t, l4t string) string {
if l4t != "" {
return l4t
}
return t
}
// judgmentFor builds the write-back judgment from the raw intent-layer result.
func judgmentFor(in Input, state State, conf float64, tool string) Judgment {
j := Judgment{State: state, Confidence: conf}
if n := len(in.Messages); n > 0 {
j.Text = in.Messages[n-1]
}
j.IsTask = state == StateAwake || state == StateFull
if j.IsTask {
j.TaskKind = taskKindFor(state, tool)
}
return j
}