Align spans with the OTel GenAI convention (N3 WP4)
Why: acceptance clause 3 requires span names and attributes to follow the convention; today names were bare words with two ad-hoc attribute keys, and every span got a fresh random trace with no parentage. Evidence: GOWORK=off go vet + go test -count=1 ./... green; TestSpanNamesFollowGenAIConvention (name suffixes, gen_ai.* key enum, intValue usage, service.name resource, prompt/reserved-key exclusion), TestLoopSpansFollowGenAIConvention (invoke_agent root with shared trace, chat/execute_tool children with parent links, usage attrs on chat spans); the pre-existing prompt-body denylist test passes unchanged. Impact: span names become invoke_agent <agent> / chat <model> / execute_tool <tool>; attributes move to gen_ai.* with usage as intValue; one Run shares one trace with invoke_agent as root. New Config.AgentName/ProviderName (documented defaults keep old hosts meaningful); telemetry StartFunc gains WithServiceName/WithEndpoint options (defaults preserve current behavior). Refs: Agent-System-Documents#43 (N3 plan WP4)
This commit is contained in:
+75
-4
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"agent-native/runtime/llm"
|
||||
"agent-native/runtime/reminders"
|
||||
"agent-native/runtime/session"
|
||||
"agent-native/runtime/telemetry"
|
||||
"agent-native/runtime/tools"
|
||||
|
||||
"logging-system/logsys"
|
||||
@@ -116,6 +118,15 @@ type Config struct {
|
||||
// 本次恢复写入的配对摘要以系统消息注入(不持久化,已在 journal 里)。
|
||||
// 为假(默认)保持既有语义:Session 为权威上下文。
|
||||
JournalOnly bool
|
||||
// AgentName 是 invoke_agent span 的名字后缀(OTel GenAI 约定
|
||||
// "invoke_agent <agent>")。空 = "agent-native"。
|
||||
AgentName string
|
||||
// ProviderName 是 gen_ai.system 属性值(如 openai / anthropic)。
|
||||
// 空 = "openai"。
|
||||
ProviderName string
|
||||
// traceCtx 是本次 Run 的 span 关联(Run 启动时建立;指针随 Config
|
||||
// 副本传播到各轮与各工具)。
|
||||
traceCtx *traceIDs
|
||||
// Hooks 是进程内钩子。空总线不改变停止、提醒和拒绝次数。
|
||||
Hooks *hooks.Bus
|
||||
// PermissionAsk 在钩子对一次工具调用表态 ask 时向宿主请求裁决。
|
||||
@@ -176,7 +187,12 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) {
|
||||
cfg.ToolExecution = ExecSequential
|
||||
}
|
||||
serializeHostCallbacks(&cfg)
|
||||
endRun := startSpan(cfg, "invoke_agent", map[string]string{"model": cfg.Model})
|
||||
cfg.traceCtx = newTraceContext()
|
||||
endRun := startSpan(cfg, "invoke_agent "+agentName(cfg), map[string]string{
|
||||
"gen_ai.system": providerName(cfg),
|
||||
"gen_ai.agent.name": agentName(cfg),
|
||||
"gen_ai.request.model": cfg.Model,
|
||||
}, true)
|
||||
defer endRun()
|
||||
var messages []llm.Message
|
||||
if cfg.Session != nil && !cfg.JournalOnly {
|
||||
@@ -438,7 +454,11 @@ func systemText(cfg Config) string {
|
||||
}
|
||||
|
||||
func consume(ctx context.Context, cfg Config, messages []llm.Message) (llm.Event, error) {
|
||||
endSpan := startSpan(cfg, "chat", map[string]string{"model": cfg.Model})
|
||||
spanAttrs := map[string]string{
|
||||
"gen_ai.system": providerName(cfg),
|
||||
"gen_ai.request.model": cfg.Model,
|
||||
}
|
||||
endSpan := startSpan(cfg, "chat "+cfg.Model, spanAttrs, false)
|
||||
defer endSpan()
|
||||
var decls []llm.ToolDecl
|
||||
if cfg.Tools != nil {
|
||||
@@ -475,6 +495,11 @@ func consume(ctx context.Context, cfg Config, messages []llm.Message) (llm.Event
|
||||
sawEnd = true
|
||||
}
|
||||
}
|
||||
// usage 在 span 结束前补进属性(结束时过滤导出,见 telemetry 工厂)。
|
||||
if sawEnd {
|
||||
spanAttrs["gen_ai.usage.input_tokens"] = itoa(end.Usage.PromptTokens)
|
||||
spanAttrs["gen_ai.usage.output_tokens"] = itoa(end.Usage.CompletionTokens)
|
||||
}
|
||||
if !sawEnd {
|
||||
return llm.Event{Message: llm.Message{StopReason: llm.StopError}}, errString("stream ended without a message")
|
||||
}
|
||||
@@ -686,7 +711,10 @@ func runOne(ctx context.Context, cfg Config, call llm.ToolCall) llm.Message {
|
||||
emit(cfg, Event{Type: "tool_execution_end", Tool: call.Name, Text: msg.Content})
|
||||
return msg
|
||||
}
|
||||
endSpan := startSpan(cfg, "execute_tool", map[string]string{"tool": call.Name})
|
||||
endSpan := startSpan(cfg, "execute_tool "+call.Name, map[string]string{
|
||||
"gen_ai.system": providerName(cfg),
|
||||
"gen_ai.tool.name": call.Name,
|
||||
}, false)
|
||||
defer endSpan()
|
||||
res := tools.Result{Content: "no tools configured", IsError: true}
|
||||
known := cfg.Tools != nil && cfg.Tools.Has(call.Name)
|
||||
@@ -864,10 +892,38 @@ func commitToolIntent(cfg Config, call llm.ToolCall) error {
|
||||
})
|
||||
}
|
||||
|
||||
func startSpan(cfg Config, name string, attrs map[string]string) func() {
|
||||
func itoa(n int) string { return strconv.Itoa(n) }
|
||||
|
||||
// traceIDs 是一次 Run 的 span 关联:根 trace + invoke_agent 根 span。
|
||||
type traceIDs struct {
|
||||
traceID string
|
||||
rootID string
|
||||
}
|
||||
|
||||
// newTraceContext 建立一次 Run 的关联(Trace 为 nil 时也给空壳,调用方免判空)。
|
||||
func newTraceContext() *traceIDs {
|
||||
return &traceIDs{traceID: telemetry.NewTraceID(), rootID: telemetry.NewSpanID()}
|
||||
}
|
||||
|
||||
// startSpan 开一个 OTel GenAI span:root=true 时作 invoke_agent 根(无 parent),
|
||||
// 否则作子 span(parent = 根)。attrs 由调用方持有引用——存续期间可补
|
||||
// (usage 在结束时随过滤导出)。
|
||||
func startSpan(cfg Config, name string, attrs map[string]string, root bool) func() {
|
||||
if cfg.Trace == nil {
|
||||
return func() {}
|
||||
}
|
||||
if attrs == nil {
|
||||
attrs = map[string]string{}
|
||||
}
|
||||
if cfg.traceCtx != nil {
|
||||
attrs[telemetry.AttrTraceID] = cfg.traceCtx.traceID
|
||||
if root {
|
||||
attrs[telemetry.AttrSpanID] = cfg.traceCtx.rootID
|
||||
} else {
|
||||
attrs[telemetry.AttrSpanID] = telemetry.NewSpanID()
|
||||
attrs[telemetry.AttrParentSpanID] = cfg.traceCtx.rootID
|
||||
}
|
||||
}
|
||||
end := cfg.Trace(name, attrs)
|
||||
if end == nil {
|
||||
return func() {}
|
||||
@@ -875,6 +931,21 @@ func startSpan(cfg Config, name string, attrs map[string]string) func() {
|
||||
return end
|
||||
}
|
||||
|
||||
// agentName / providerName 是 OTel 属性的默认值解析。
|
||||
func agentName(cfg Config) string {
|
||||
if strings.TrimSpace(cfg.AgentName) != "" {
|
||||
return cfg.AgentName
|
||||
}
|
||||
return "agent-native"
|
||||
}
|
||||
|
||||
func providerName(cfg Config) string {
|
||||
if strings.TrimSpace(cfg.ProviderName) != "" {
|
||||
return cfg.ProviderName
|
||||
}
|
||||
return "openai"
|
||||
}
|
||||
|
||||
func clearOldResults(cfg Config, messages []llm.Message) []llm.Message {
|
||||
window := cfg.ContextWindow
|
||||
if window <= 0 {
|
||||
|
||||
Reference in New Issue
Block a user