- 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), 文档同步由主会话统一落
95 lines
3.4 KiB
Go
95 lines
3.4 KiB
Go
package trace
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
)
|
||
|
||
// maxSummaryRunes bounds how many runes of a user string / tool result are
|
||
// rendered into a node label or prop summary, so sensitive or long content is
|
||
// collapsed to a safe, dashboard-friendly prefix.
|
||
const maxSummaryRunes = 80
|
||
|
||
// safeSpanProps carries only a whitelist of span props to the frontend, plus a
|
||
// desensitized tool result summary. It never forwards raw arguments or secrets, and
|
||
// every string is scrubbed of credential shapes and truncated, so long or sensitive
|
||
// content cannot leak a full payload (design/19 §3).
|
||
func safeSpanProps(rec SpanRecord) map[string]any {
|
||
var props map[string]any
|
||
for _, k := range safePropKeys {
|
||
v, ok := rec.Props[k]
|
||
if !ok {
|
||
continue
|
||
}
|
||
if props == nil {
|
||
props = make(map[string]any)
|
||
}
|
||
props[k] = redactValue(v)
|
||
}
|
||
if rec.Kind == SpanKindTool {
|
||
if v, ok := rec.Props["result"]; ok {
|
||
if props == nil {
|
||
props = make(map[string]any)
|
||
}
|
||
props["result_summary"] = RedactText(summarize(v))
|
||
}
|
||
}
|
||
return props
|
||
}
|
||
|
||
// safePropKeys are the span prop keys forwarded to a node's props.
|
||
//
|
||
// 键分五类,全部非敏感(值仍会逐个经 redactValue 清洗):
|
||
//
|
||
// - 既有:stop_reason / step / error / error_kind / tool / persona;
|
||
// - tool 参数摘要(裁决 11.4.3):args_keys(全量参数名)、args_summary
|
||
// (仅放行键的值,长度已在 loop/spanlog.go 侧截断),密钥类键的值绝不在其中;
|
||
// - guardrail(裁决 11.4.2):allow(裁决结果)、action(检查名)、elapsed_ms(耗时);
|
||
// - memory(裁决 11.4.2):hits / count / failed(命中、写入与失败条数)、
|
||
// bytes(注入块字节数)、scope / action(记忆作用域与操作名)—— 只记元数据,
|
||
// 不含任何记忆正文;
|
||
// - orchestration(裁决 11.4.2):subagent(被派发的子代理 id)。
|
||
//
|
||
// persona 是子代理分支根节点上的「目标角色 id」:orchestration/scheduler.go 只在
|
||
// subagent_spawn 显式指定角色时才设置它,且该 id 必须先通过角色注册表校验
|
||
// (ErrUnknownPersona 会拒绝未注册的 id,orchestration/persona.go),因此它是注册表里
|
||
// 的角色标识,不是用户内容、不是提示词、更不是凭据——同一个 id 本来就随 GET /personas
|
||
// 与会话材料对外可见。它是 Docs/api-client.md §3.5(v0.47)对客户端的承诺
|
||
// (design/22 §8-2:让流程图显示「这个子代理是以谁的身份在替班」),漏出白名单就会被
|
||
// safeSpanProps 静默丢掉。未指定角色派发时该键不存在,符合 v0.47「纯增量」的约定。
|
||
var safePropKeys = []string{
|
||
"stop_reason", "step", "error", "error_kind", "tool", "persona",
|
||
"args_keys", "args_summary",
|
||
"allow", "action", "elapsed_ms",
|
||
"hits", "count", "failed", "bytes", "scope",
|
||
"subagent",
|
||
}
|
||
|
||
// summarize renders an arbitrary value as a short text string for a prop summary.
|
||
func summarize(v any) string {
|
||
switch x := v.(type) {
|
||
case nil:
|
||
return ""
|
||
case string:
|
||
return x
|
||
default:
|
||
b, err := json.Marshal(x)
|
||
if err != nil {
|
||
return fmt.Sprintf("%v", x)
|
||
}
|
||
return string(b)
|
||
}
|
||
}
|
||
|
||
// truncate cuts s to at most max runes, appending an ellipsis when it overflows.
|
||
func truncate(s string, max int) string {
|
||
if max <= 0 {
|
||
return s
|
||
}
|
||
r := []rune(s)
|
||
if len(r) <= max {
|
||
return s
|
||
}
|
||
return string(r[:max]) + "…"
|
||
}
|