feat(trace,loop,orchestration): memory/guardrail/orchestration 真 span + 参数摘要脱敏 + Guardrail 输出侧异步(裁决 11.4.2/11.4.3/13.4.1)
- 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:
1 parent
0dfad675b8
commit
b99268d019
14 files changed
+1396
-42
No files matched your search
@@ -9,14 +9,29 @@ import (
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||||
)
|
||||
|
||||
// guardrailCheckTimeout 限制一次后台输出侧护栏检查。它只管辖脱离
|
||||
// 出来的检查 context,因此一个挂起的检查不会让 goroutine 泄露到进程
|
||||
// 生命周期结束,也不会拖住任何回合。
|
||||
const guardrailCheckTimeout = 30 * time.Second
|
||||
|
||||
// auditGuardrailOutputBlock 是输出侧护栏在后台判定拦截时发出的审计
|
||||
// "what"。输出侧检查已移出关键路径(裁决 13.4.1),拦截因此必须闭环:
|
||||
// 一条审计事件 + guardrail 节点上的 verdict,「异步」不等于「可绕过」。
|
||||
const auditGuardrailOutputBlock = "guardrail.output.block"
|
||||
|
||||
// streamStep 驱动一次流式模型调用。它是
|
||||
// agent.RunStep 的流式对应物:它从 agent 的公开表面构建请求、
|
||||
// 运行循环的输入护栏、把增量流经 OnStream、运行输出
|
||||
// 护栏并解码所有工具调用。由于 RunStep 不暴露流式
|
||||
// 路径,该方法归循环所有;它镜像 agent 的语义,使两者
|
||||
// 除增量回调外行为一致。
|
||||
//
|
||||
// 两侧护栏的分工(裁决 13.4.1,design/04 §5):**输入侧是模型调用前的同步
|
||||
// 前置门**(恶意输入不得先消耗一次模型调用),**输出侧是异步并行检查**
|
||||
// (模型产出不等待检查即可继续流转,结果异步落定)。
|
||||
func (l *Loop) streamStep(ctx context.Context, messages []model.Message) (*stepOutcome, error) {
|
||||
ag := l.agentFor(ctx)
|
||||
adapter := ag.Adapter()
|
||||
@@ -30,7 +45,11 @@ func (l *Loop) streamStep(ctx context.Context, messages []model.Message) (*stepO
|
||||
req := l.buildRequest(ctx, messages, ag, knobs)
|
||||
|
||||
if l.guards != nil {
|
||||
verdict, err := l.guards.CheckInput(ctx, &req)
|
||||
// 输入侧同步前置门:检查包在自己的 guardrail span 里
|
||||
// (裁决 11.4.2),因此护栏也进了全链路流程图。
|
||||
gctx, gspan := l.span(ctx, spanNameGuardrailInput, trace.SpanKindGuardrail)
|
||||
verdict, err := l.guards.CheckInput(gctx, &req)
|
||||
l.finishGuardrailSpan(gspan, spanNameGuardrailInput, verdict, err)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loop: check input: %w", err)
|
||||
}
|
||||
@@ -65,15 +84,8 @@ func (l *Loop) streamStep(ctx context.Context, messages []model.Message) (*stepO
|
||||
return nil, fmt.Errorf("loop: chat stream: %w", err)
|
||||
}
|
||||
|
||||
if l.guards != nil {
|
||||
verdict, err := l.guards.CheckOutput(ctx, &resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loop: check output: %w", err)
|
||||
}
|
||||
if verdict != nil && !verdict.Allow {
|
||||
return &stepOutcome{final: true, text: verdict.Reason, stopReason: agent.StopReasonGuardrailOutput}, nil
|
||||
}
|
||||
}
|
||||
// 输出侧不阻塞回合:检查在后台跑,本回合带着模型产出继续。
|
||||
l.checkOutputAsync(ctx, &resp)
|
||||
|
||||
if calls, ok := decodeToolCalls(resp); ok {
|
||||
return &stepOutcome{final: false, toolCalls: calls, stopReason: agent.StopReasonToolCalls,
|
||||
@@ -83,6 +95,37 @@ func (l *Loop) streamStep(ctx context.Context, messages []model.Message) (*stepO
|
||||
modelStopReason: resp.StopReason, model: resp.Model}, nil
|
||||
}
|
||||
|
||||
// checkOutputAsync 在后台运行输出侧护栏检查(裁决 13.4.1:真并发、不阻塞
|
||||
// 主会话)。启动 span 与「运行中」占位节点留在同步路径上,节点的出现顺序
|
||||
// 才与真实发生顺序一致;检查本身脱离本步的 context 跑(保留会话/actor 值,
|
||||
// 另加超时兜底),因此慢检查绝不拖住回合。
|
||||
//
|
||||
// 结果闭环在 guardrail 节点上(allow / 耗时 / 脱敏后的 Reason,裁决 11.4.2),
|
||||
// 判定拦截时再发一条审计事件 —— 输出侧的拦截始终留痕,只是不再挡住已产出的
|
||||
// 回复(这正是「真并发」与「可绕过」的区别:输入侧的前置门依旧同步拦下)。
|
||||
func (l *Loop) checkOutputAsync(ctx context.Context, resp *model.Response) {
|
||||
if l.guards == nil || resp == nil {
|
||||
return
|
||||
}
|
||||
gctx, span := l.span(ctx, spanNameGuardrailOutput, trace.SpanKindGuardrail)
|
||||
bg, cancel := context.WithTimeout(context.WithoutCancel(gctx), guardrailCheckTimeout)
|
||||
go func() {
|
||||
defer cancel()
|
||||
verdict, err := l.guards.CheckOutput(bg, resp)
|
||||
l.finishGuardrailSpan(span, spanNameGuardrailOutput, verdict, err)
|
||||
if verdict != nil && !verdict.Allow {
|
||||
// Reason 由 guardrail 包保证不含被匹配的密钥(其契约),
|
||||
// 这里再经 RedactText 走一遍凭据形状清洗 + 截断,使审计面
|
||||
// 与出图面用同一判据。
|
||||
l.emitAudit(AuditEvent{
|
||||
Who: string(sessionEnvFrom(bg).ActorEnv),
|
||||
What: auditGuardrailOutputBlock,
|
||||
Result: trace.RedactText(verdict.Reason),
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// broadcasts 报告某流式事件是否可推给 UI。只有
|
||||
// 思考帧会被扣下,且仅当该 turn 的角色把
|
||||
// showToUser 解析为 false 时;文本、工具调用与工具进度帧
|
||||
|
||||
Reference in new issue
Block a user