feat(trace,loop,orchestration): memory/guardrail/orchestration 真 span + 参数摘要脱敏 + Guardrail 输出侧异步(裁决 11.4.2/11.4.3/13.4.1)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 11.4.2:三类节点首次真进图 —— memory.recall/memory.write(loop/memory.go 召回与写入点)、
  guardrail.input/output(流式主路径两侧 + agent.RunStep 静音路径)、orchestration.spawn
  (scheduler 每次派发/收束);新增 trace.ChildSpan 让不持有 Tracer 的叶子包也能开子 span
- 11.4.3(设计先行,规格先写进 design/19 §1.1):tool 节点参数摘要 —— 放行表 argValueKeys +
  trace.IsSecretKey 统一判据(密钥类键不给值)+ 单值 80 rune / ≤24 条 + 出图前再清洗;
  前端 NodeDetail 新增「参数摘要」区,未放行键显示「值未展示」
- 13.4.1:流式主路径输出侧护栏改后台 goroutine(脱离 context、30s 兜底、不阻塞回合),
  输入侧仍在模型调用前同步拦下;拦截闭环到 guardrail 节点 + 审计事件
- 拆文件:scheduler.go 加 span 后 546 行 → 编排 span 助手拆出 scheduler_spans.go(回到 492)
- 契约:无帧名/事件形状变化;graph_update 节点 props 新增**可选**键(tool args_keys/args_summary、
  guardrail action/allow/elapsed_ms、memory action/scope/hits/…、orchestration subagent/persona),
  文档同步由主会话统一落
This commit is contained in:
AskaEth committed 2026-10-04 12:31:20 +08:00
1 parent 0dfad675b8
commit b99268d019
14 files changed
+1396 -42

No files matched your search

+30 -4
View File
@@ -7,6 +7,7 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// memoryExtractTimeout 限制单次后台提取。它只管辖
@@ -75,12 +76,21 @@ func (l *Loop) memoryBlock(ctx context.Context, messages []model.Message) string
// 角色日常记忆(ScopePersona)属敏感项,绝不注入
// (design/03 §5「不泄露主人敏感信息」+ 主人 2026-10-03
// 裁决「用户习惯记忆与不敏感数据:可查、不可写」)。
//
// 召回本身包在一个 memory span 里(裁决 11.4.2,design/19 §1
// 旁路节点):span 只记元数据(作用域/命中条数/注入块字节数/耗时),
// 绝不记召回了什么内容。
func (l *Loop) retrieveMemoryBlock(ctx context.Context, env SessionEnv, personaID, query string) (string, error) {
if !env.Restricted() {
return l.retriever.Inject(ctx, personaID, query, 0)
rctx, span := l.span(ctx, spanNameMemoryRecall, trace.SpanKindMemory)
block, err := l.retriever.Inject(rctx, personaID, query, 0)
l.finishMemoryRecallSpan(span, "owner", 0, len(block), err)
return block, err
}
results, err := l.retriever.Retrieve(ctx, personaID, query, 0)
rctx, span := l.span(ctx, spanNameMemoryRecall, trace.SpanKindMemory)
results, err := l.retriever.Retrieve(rctx, personaID, query, 0)
if err != nil {
l.finishMemoryRecallSpan(span, "restricted", 0, 0, err)
return "", err
}
shared := make([]memory.Result, 0, len(results))
@@ -90,9 +100,12 @@ func (l *Loop) retrieveMemoryBlock(ctx context.Context, env SessionEnv, personaI
}
}
if len(shared) == 0 {
l.finishMemoryRecallSpan(span, "restricted", 0, 0, nil)
return "", nil
}
return formatMemoryBlock(shared, "群聊隔离"), nil
block := formatMemoryBlock(shared, "群聊隔离")
l.finishMemoryRecallSpan(span, "restricted", len(shared), len(block), nil)
return block, nil
}
// sharedScope 报告 scope 是否为群聊/陌生人可查的非敏感共享域:
@@ -278,18 +291,31 @@ func (l *Loop) extractMemoryAsync(ctx context.Context, messages []model.Message,
// (memory.FormatSourceConversation),无条件覆盖 extractor
// 产出值 —— 模型自由文本绝无机会落库为来源。store
// 失败会被记录日志,绝不致命,因此一条被拒的条目无法中止一个 turn。
//
// 整批写入包在一个 memory span 里(裁决 11.4.2,design/19 §1 旁路节点):
// 只记写入/失败条数与耗时,绝不记写入了什么内容;没有条目时不产生节点
// (span 绝不宣告未运行的工作)。
func (l *Loop) persistMemories(ctx context.Context, items []memory.MemoryItem, step int) {
if len(items) == 0 {
return
}
personaID := l.personaFor(ctx)
source := memory.FormatSourceConversation(loopSession(ctx), loopTurn(ctx))
wctx, span := l.span(ctx, spanNameMemoryWrite, trace.SpanKindMemory)
written, failed := 0, 0
for _, it := range items {
if personaID != "" {
it.PersonaID = personaID
}
it.SourceConversation = source
if _, err := l.memory.Add(ctx, it); err != nil {
if _, err := l.memory.Add(wctx, it); err != nil {
failed++
l.log.Warn("memory store", "step", step, "error", err)
continue
}
written++
}
l.finishMemoryWriteSpan(span, written, failed, nil)
}
// queryFromMessages 从对话导出检索查询:最后一个用户