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
@@ -5,8 +5,10 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/guardrail"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||||
)
|
||||
|
||||
// Stop reasons carried by StepResult.StopReason, consumed by the loop (M2) to
|
||||
@@ -53,6 +55,15 @@ type StepResult struct {
|
||||
ModelStopReason string
|
||||
}
|
||||
|
||||
// 护栏 span 名与本包记录事件时使用的类型:与循环侧(loop/spanlog.go)
|
||||
// 同名,使面板无论走流式路径还是 agent 路径都看到同一类 guardrail 节点
|
||||
// 与同一条日志事件(裁决 11.4.2,design/04 §9「逐层记到 guardrail」)。
|
||||
const (
|
||||
guardrailSpanInput = "guardrail.input"
|
||||
guardrailSpanOutput = "guardrail.output"
|
||||
guardrailEventDone = "guardrail.done"
|
||||
)
|
||||
|
||||
// RunStep executes one step of the agent loop. It assembles a model.Request from
|
||||
// the agent's instructions prepended as a leading system message plus the
|
||||
// conversation messages, runs input guardrails, calls the model adapter, runs
|
||||
@@ -64,11 +75,20 @@ type StepResult struct {
|
||||
// yields Final=false with the decoded ToolCalls; a guardrail denial yields
|
||||
// Final=true carrying the block reason. A nil adapter or a failed guardrail or
|
||||
// model call returns a wrapped error.
|
||||
//
|
||||
// 两侧检查都包在自己的 guardrail span 里(裁决 11.4.2):本包不持有 Tracer,
|
||||
// 因此用 trace.ChildSpan 在当前 span(循环的 step span)之下开一个子 span,
|
||||
// 未开追踪时它是空操作。**本路径保持串行内联**:输入侧在模型调用之前拦下,
|
||||
// 输出侧紧接模型返回裁决(裁决 13.4.1 的「输出侧异步」由流式主路径
|
||||
// loop.streamStep 承担——agent.RunStep 只服务静音/非流式内部流程,不落在
|
||||
// 主会话关键路径上,且它的裁决是 StepResult 的一部分)。
|
||||
func (a *Agent) RunStep(ctx context.Context, sessionID string, messages []model.Message) (*StepResult, error) {
|
||||
req := a.buildRequest(messages)
|
||||
|
||||
if a.guards != nil {
|
||||
verdict, err := a.guards.CheckInput(ctx, &req)
|
||||
gctx, gspan := trace.ChildSpan(ctx, guardrailSpanInput, trace.SpanKindGuardrail)
|
||||
verdict, err := a.guards.CheckInput(gctx, &req)
|
||||
finishGuardrailSpan(gspan, guardrailSpanInput, verdict, err)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("agent: check input: %w", err)
|
||||
}
|
||||
@@ -96,7 +116,9 @@ func (a *Agent) RunStep(ctx context.Context, sessionID string, messages []model.
|
||||
}
|
||||
|
||||
if a.guards != nil {
|
||||
verdict, err := a.guards.CheckOutput(ctx, &resp)
|
||||
gctx, gspan := trace.ChildSpan(ctx, guardrailSpanOutput, trace.SpanKindGuardrail)
|
||||
verdict, err := a.guards.CheckOutput(gctx, &resp)
|
||||
finishGuardrailSpan(gspan, guardrailSpanOutput, verdict, err)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("agent: check output: %w", err)
|
||||
}
|
||||
@@ -111,6 +133,58 @@ func (a *Agent) RunStep(ctx context.Context, sessionID string, messages []model.
|
||||
return &StepResult{Final: true, Text: resp.Content, StopReason: StopReasonStop, ModelStopReason: resp.StopReason}, nil
|
||||
}
|
||||
|
||||
// finishGuardrailSpan 把一次护栏检查的结论落到其 span 上(裁决 11.4.2):
|
||||
// props 只放检查名、裁决与耗时(全部在图的 safePropKeys 白名单内,出图时仍会
|
||||
// 再清洗一遍),命中拦截或标记时额外记一条脱敏后的 Reason 事件。拦截的 span
|
||||
// 终态为 error,面板上显红。span 为 nil(未开追踪)时是空操作。
|
||||
func finishGuardrailSpan(span *trace.Span, action string, verdict *guardrail.Verdict, err error) {
|
||||
if span == nil {
|
||||
return
|
||||
}
|
||||
allow := err == nil && (verdict == nil || verdict.Allow)
|
||||
reason := ""
|
||||
if verdict != nil {
|
||||
reason = verdict.Reason
|
||||
}
|
||||
if err != nil {
|
||||
reason = err.Error()
|
||||
}
|
||||
_ = span.SetProp("action", action)
|
||||
_ = span.SetProp("allow", allow)
|
||||
_ = span.SetProp("elapsed_ms", trace.ElapsedMS(span))
|
||||
if err != nil {
|
||||
_ = span.SetProp("error", err.Error())
|
||||
}
|
||||
if !allow || reason != "" {
|
||||
_ = span.Record(trace.Event{Type: guardrailEventDone, Props: map[string]any{
|
||||
"action": action,
|
||||
"allow": allow,
|
||||
"reason": truncateRunes(reason, maxGuardrailReasonRunes),
|
||||
}})
|
||||
}
|
||||
if allow {
|
||||
trace.FinishChild(span, trace.SpanStatusOK, nil)
|
||||
return
|
||||
}
|
||||
trace.FinishChild(span, trace.SpanStatusError, nil)
|
||||
}
|
||||
|
||||
// maxGuardrailReasonRunes 限制护栏 Reason 进入 span 事件前的长度:Reason 是
|
||||
// 面向用户的拦截说明,本就不含密钥(guardrail 包契约),这里只防它变成一大段。
|
||||
const maxGuardrailReasonRunes = 200
|
||||
|
||||
// truncateRunes 把 s 截到最多 max 个 rune,超出时补省略号;max 非正时原样返回。
|
||||
func truncateRunes(s string, max int) string {
|
||||
if max <= 0 {
|
||||
return s
|
||||
}
|
||||
r := []rune(s)
|
||||
if len(r) <= max {
|
||||
return s
|
||||
}
|
||||
return string(r[:max]) + "…"
|
||||
}
|
||||
|
||||
// ExtractToolCalls decodes tool calls from a model.Response. It prefers the
|
||||
// provider-native ToolCalls the adapters populate (Anthropic tool_use / OpenAI
|
||||
// tool_calls), which carry the provider id needed to correlate the result and
|
||||
|
||||
Reference in new issue
Block a user