【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。
326 lines
10 KiB
Go
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
|
|
}
|