trajectory

package
v0.34.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

otelmap.go —— 轨迹行 → OTel 属性的**契约表**(属性名 / 来源字段 / 取值域 / 基数上界)。

生命周期: Experimental(v0.33.0 定级)——本包仍会改、删、拆,不承诺任何 兼容,升级风险自负;定级理由见 api-lifecycle.yaml,分级规则见 docs/api-compat.md。

为什么单独一张表

otel.go 的映射函数回答「这一行导出去长什么样」,这张表回答「**宿主可以依赖什么**」。 分开是因为失效形态不同:映射写错会出可见的错值;契约缺失**不会报错**——宿主建看板时 只能自己猜字段名,于是同一个语义在 A 宿主叫 @turn_id@、在 B 宿主叫 @turn@,跨宿主的 看板与告警全部落空,而遥测数据照常上行。

判据:每一项都必须「有主」

判据在 @otelmap_test.go@,且是**双向**的:映射函数写出了表里没有的属性 ⇒ 红;表里列了 映射函数产不出的属性 ⇒ 也红(后者防的是「表变成了许愿池」)。另逐项判四件事:来源字段 非空、取值域写出、基数上界写出依据、属性名唯一。缺一即红。

Go 类型为什么进契约

取值域的形态写的是 **Go 类型名**(string / int64 / float64 / bool):宿主导出时按类型 选 OTel 属性类型,类型不符的属性在多数后端里**静默丢弃**(不报错、不告警),表现为 「数据看起来上行成功、看板上就是没有」。写成 Go 类型名让用例拿反射直接比对映射函数的 真实产物——这是「表」与「代码」之间唯一不会漂的对拍方式。

哪些字段不许进 metric label

只有**本仓枚举得出、也给得出上界**的属性才允许作 metric label(@LabelSafe@ 蕴含 取值域是枚举、且 @Card.Max@ 不小于取值数)。理由不是洁癖:label 的代价是时序数按取值数 **乘**上去,一个无界 label 就能让指标后端按会话数膨胀。@session_id@ / @agent_slug@ 是 典型,列在 @ForbiddenLabels@ 里——而它们**根本不在 Row 上**:结构上装不进去,不靠纪律拦。

payload.go —— 事件载荷取值助手(轨迹投影层读载荷的唯一入口)。

载荷是 map[string]any,经 JSON 往返后整数是 float64——「只认 int64」的 取值面会在宿主的发布路径上整片失明。三个助手把「缺失 / 类型不符」的 归一口诀收在一处:字符串空串、整数 0、布尔 false(布尔读成 false 是 保守方向,但调用方仍应发规范的 bool)。

project_sideeffect.go —— 副作用行与快照行的**投影**(v0.33.0,issue #99)。

排障两问的轨迹面:「这个副作用到底施加了吗」(授予 / 施加 / 对账 / 裁决) 与「恢复点停在哪个序号」(快照提交)。单成文件是**重新分组**,不是拆行数: 副作用投影有自己的配对语义(幂等键配对、授予段→对账段接续、点观察), 与工具行的 call_id 配对、轮次/步骤的括号段是三套不同的账。

形态上是包级函数而不是 Projector 的方法:投影器的行为面按行族分组, 每族一组函数——「这个投影器支持哪些行」由 apply 的分发面回答, 不靠方法数堆在类型上。

Package trajectory 事件流 → 轨迹行的投影器(机制面)。

「这一轮到底发生了什么、每段花了多久」这件事,宿主此前只能自己从事件流里拼: 起止时刻靠猜,工具调用与结果按 call_id 手工配对,配对缺了没人知道。各宿主 各拼一套,于是同一个问题的答案会漂——尤其是**时长这一个数**:有人拿载荷 体积估、有人拿 token 数除、有人按事件时刻相减,三种口径混进同一张表, 谁也说不清哪一列是实测、哪一列是猜的。

本包把这件事收成一条机制面:**吃本仓 eventbus 的事件,产出轨迹行**。

三条纪律:

  1. **实测与估算不得混同**(本包存在的核心理由)。行的时长**只有一个源**: 起止时刻相减。Row 里**不存**时长字段——时长是 Duration() 现算的。 于是「按载荷体积估一个时长」这件事**无处安放**:不是靠纪律约束,是 结构上装不进去。没有实测值就是没有实测值(Duration 返回 ok=false), 不拿 0 冒充「瞬时完成」,也不拿估算值填进实测字段。

  2. **配对不完整要显形**。工具调用与结果按 call_id 配对;只有调用没有结果、 结果重复、结果没有对应调用、同号重复声明——四种缺口都进 Issues(), 不静默丢弃。

  3. **未完成不是完成**。只有开始没有结束的行,状态是 open(未收口), 不是「时长 0 的已完成行」。Finalize() 把「不会再有新事件」落成显式事实, 未收口的行原样报出。

事件流是追加式的,故投影是**增量**的:先喂一半再喂后半,既有行被就地更新 (补上结束时刻、时长随之变成实测),而不是追加出第二条行。

边界:本包是机制面,不含任何宿主领域知识;**不做前端渲染**——各宿主技术栈 不同,轨迹行是数据不是视图(要文案就用 Options.Summarize 注入)。

Index

Constants

View Source
const (
	// AttrRowID 行标识(与 Row.ID 同值)。
	AttrRowID = "harness.trajectory.row_id"
	// AttrKind 行类目(turn / step / tool_call / tool_result)。
	AttrKind = "harness.trajectory.kind"
	// AttrRowStatus 行的收口状态(open / closed / orphan)。
	AttrRowStatus = "harness.trajectory.status"
	// AttrTurnSeq 所属轮次序号(1 起;轮次外的事件不写此项)。
	AttrTurnSeq = "harness.turn.seq"
	// AttrStepID 所属环节号(仅带环节号的行)。
	AttrStepID = "harness.step.id"
	// AttrToolName 工具名(仅工具行;**属性**而不是 span 名,见头注纪律 1)。
	AttrToolName = "harness.tool.name"
	// AttrToolCallID 工具调用标识(配对键,宿主跨 span join 用)。
	AttrToolCallID = "harness.tool.call_id"
	// AttrToolPairing 配对状态(paired / missing_result / duplicate_result / …)。
	AttrToolPairing = "harness.tool.pairing"
	// AttrToolDupResults 重复结果计数(>0 才写)。
	AttrToolDupResults = "harness.tool.duplicate_results"
	// AttrDurationMs 实测时长(毫秒,float64)。**仅实测时出现**(见头注纪律 2)。
	AttrDurationMs = "harness.trajectory.duration_ms"
	// AttrSummary 行摘要(宿主可用 Options.Summarize 覆盖;空则不写)。
	AttrSummary = "harness.trajectory.summary"

	// ── 副作用行(v0.33.0,issue #99)─────────────────────────────
	// 排障第一问「这个副作用到底施加了吗」的属性面。属性命名与既有三类同构:
	// 领域段(harness.sideeffect.*)+ 语义后缀。
	// AttrSideEffectKey 幂等键(配对键:同 Key 的授予段 / 对账段 / 点观察聚合到
	// 同一行标识下;宿主跨 span join 用)。无界标识——禁止作 metric label。
	AttrSideEffectKey = "harness.sideeffect.key"
	// AttrSideEffectPhase 副作用阶段(granted / completed / abandoned / uncertain /
	// resolved_applied / resolved_cleared / replayed / contended)。
	AttrSideEffectPhase = "harness.sideeffect.phase"
	// AttrSideEffectResource 目标资源标识(租约的独占单位)。无界——禁止作 metric label。
	AttrSideEffectResource = "harness.sideeffect.resource"
	// AttrSideEffectFence 栅栏令牌(授予段 / 点行为观察到的代数;非授予段不写)。
	AttrSideEffectFence = "harness.sideeffect.fence"
	// AttrSideEffectSafety 重试安全性声明(unknown / safe / unsafe,与
	// sideeffect.RetrySafety.String() 逐字一致;空声明不写——沉默按最弱处理,
	// 属性面不替宿主补默认值)。
	AttrSideEffectSafety = "harness.sideeffect.retry_safety"

	// ── 快照行(v0.33.0,issue #99)──────────────────────────────
	// 排障第二问「恢复点停在哪个序号」的属性面。
	// AttrCheckpointRunID 编排运行标识(宿主分配的稳定标识)。无界——禁止作 metric label。
	AttrCheckpointRunID = "harness.checkpoint.run_id"
	// AttrCheckpointSeq 快照序号(严格递增;0 不会出现——seq=0 的快照不是一次真实提交)。
	AttrCheckpointSeq = "harness.checkpoint.seq"
	// AttrCheckpointStatus 快照的编排状态(取值域见 contract 的状态常量集;
	// 本包**引用**那份词表而不复制——契约表里用 Note 指过去,抄一份就是第二真相)。
	AttrCheckpointStatus = "harness.checkpoint.status"
)

OTel 属性名(本仓命名空间)。宿主据此建 dashboard 与告警;改名即破坏兼容。

View Source
const TraceBoundary = TracePerTurn

TraceBoundary 本仓选定的 trace 切分单位。

依据见 otel.go 头注「trace 边界」:选「轮」不是习惯,是因为 Row.ID 的唯一性 范围恰好是轮内——把边界挪到环节,标识唯一性与 trace 内唯一性就不再是同一个范围。

Variables

View Source
var AttrSpecs = []AttrSpec{
	{
		Name:   AttrRowID,
		Source: "Row.ID",
		Domain: Domain{Kind: "string", Note: "行标识;形态由投影器决定(turn#N / step#N / tool#<call_id> / <kind>#-<seq>),唯一范围是轮内"},
		Card:   Cardinality{Why: "取值随行数增长(行数由事件流长度决定),本仓定不出上界"},
	},
	{
		Name:   AttrKind,
		Source: "Row.Kind",
		Domain: Domain{Kind: "string", Values: []string{
			string(KindTurn), string(KindStep), string(KindToolCall), string(KindToolResult),
			string(KindSideEffect), string(KindCheckpoint),
		}},
		Card:      Cardinality{Max: 6, Why: "Kind 词表固定六值(与 Span.Name 同一词表:新增类目必须同步这里与用例)"},
		LabelSafe: true,
	},
	{
		Name:   AttrRowStatus,
		Source: "Row.Status",
		Domain: Domain{Kind: "string", Values: []string{
			string(StatusOpen), string(StatusClosed), string(StatusOrphan),
		}},
		Card:      Cardinality{Max: 3, Why: "Status 词表固定三值"},
		LabelSafe: true,
	},
	{
		Name:   AttrTurnSeq,
		Source: "Row.Turn",
		Domain: Domain{Kind: "int64", Note: "1 起的轮次序号;轮次外的事件(Turn 为 0)不写此项"},
		Card:   Cardinality{Why: "随会话轮数增长,本仓定不出上界"},
	},
	{
		Name:   AttrStepID,
		Source: "Row.StepID(Row.HasStep 为真时才写:环节号 0 是合法值,不能靠零值判有无)",
		Domain: Domain{Kind: "int64", Note: "环节号,由调用方给定(uint64 全域)"},
		Card:   Cardinality{Why: "取值由调用方决定,本仓定不出上界"},
	},
	{
		Name:   AttrToolName,
		Source: "Row.Tool(仅工具行)",
		Domain: Domain{Kind: "string", Note: "工具名,来自工具调用事件载荷"},
		Card:   Cardinality{Why: "取值数由宿主的工具目录规模决定,本仓看不到那个目录;宿主若要按工具聚合,应在 label 侧用自己的白名单收敛"},
	},
	{
		Name:   AttrToolCallID,
		Source: "Row.CallID(仅工具行)",
		Domain: Domain{Kind: "string", Note: "配对键:一次工具调用一个值,宿主跨 span join 用"},
		Card:   Cardinality{Why: "调用次数无上界"},
	},
	{
		Name:   AttrToolPairing,
		Source: "Row.Pairing(仅工具行,且不为 PairingNone)",
		Domain: Domain{Kind: "string", Values: []string{
			string(PairingPaired), string(PairingMissingResult), string(PairingDuplicateResult),
			string(PairingUnpairedResult), string(PairingDuplicateCall),
		}},
		Card:      Cardinality{Max: 5, Why: "Pairing 词表五值(PairingNone 是「不适用」,不写进属性)"},
		LabelSafe: true,
	},
	{
		Name:   AttrToolDupResults,
		Source: "Row.DupResults(仅工具行,且 >0)",
		Domain: Domain{Kind: "int64", Note: "重复结果计数,1 起;没有重复就没有这个属性"},
		Card:   Cardinality{Why: "重复次数无上界(正常情况下恒为 1)"},
	},
	{
		Name:   AttrDurationMs,
		Source: "Row.Duration()(StartAt 与 EndAt 相减,不是单独存的字段)",
		Domain: Domain{Kind: "float64", Note: "实测毫秒;**仅实测时出现**——没有实测就没有这个属性,不写 0、不估一个"},
		Card:   Cardinality{Why: "连续值,本仓定不出上界"},
	},
	{
		Name:   AttrSummary,
		Source: "Row.Summary(可由 Options.Summarize 覆盖)",
		Domain: Domain{Kind: "string", Note: "宿主注入的展示文案;内核默认标签不含任何领域词"},
		Card:   Cardinality{Why: "自由文本,本仓定不出上界"},
	},

	{
		Name:   AttrSideEffectKey,
		Source: "Row.SideEffectKey(仅副作用行)",
		Domain: Domain{Kind: "string", Note: "幂等键:一个逻辑动作一个值(宿主构造,须稳定);同 Key 的授予段/对账段/点观察共用一个行标识,宿主跨 span join 用"},
		Card:   Cardinality{Why: "动作数无上界(随运行规模增长)"},
	},
	{
		Name:   AttrSideEffectPhase,
		Source: "Row.SideEffectPhase(仅副作用行)",
		Domain: Domain{Kind: "string", Values: []string{
			string(SideEffectGranted), string(SideEffectCompleted), string(SideEffectAbandoned),
			string(SideEffectUncertain), string(SideEffectResolvedApplied), string(SideEffectResolvedCleared),
			string(SideEffectReplayed), string(SideEffectContended),
		}},
		Card:      Cardinality{Max: 8, Why: "Phase 词表固定八值(授予/施加/放弃/转未知/裁决两态/两个点观察;与 sideeffect 的 Outcome 词表同源,新增须同步这里与用例)"},
		LabelSafe: true,
	},
	{
		Name:   AttrSideEffectResource,
		Source: "Row.SideEffectResource(仅副作用行)",
		Domain: Domain{Kind: "string", Note: "目标资源标识:租约的独占单位(宿主定义)"},
		Card:   Cardinality{Why: "资源数由宿主的领域决定,本仓定不出上界"},
	},
	{
		Name:   AttrSideEffectFence,
		Source: "Row.SideEffectFence(仅副作用行,且 >0;授予段与点行为才携带)",
		Domain: Domain{Kind: "int64", Note: "栅栏令牌:同一资源上单调递增的代数,由存储侧在原子操作里分配"},
		Card:   Cardinality{Why: "随接管次数增长,本仓定不出上界"},
	},
	{
		Name:      AttrSideEffectSafety,
		Source:    "Row.SideEffectSafety(仅副作用行,且非空——空声明不写,不替宿主补默认值)",
		Domain:    Domain{Kind: "string", Values: []string{"unknown", "safe", "unsafe"}},
		Card:      Cardinality{Max: 3, Why: "与 sideeffect.RetrySafety 的 String() 逐字一致(unknown=未声明,沉默按最弱处理)"},
		LabelSafe: true,
	},

	{
		Name:   AttrCheckpointRunID,
		Source: "Row.CheckpointRunID(仅快照行)",
		Domain: Domain{Kind: "string", Note: "编排运行标识(宿主分配,须稳定且可跨进程重建)"},
		Card:   Cardinality{Why: "运行数随宿主的编排规模增长,本仓定不出上界"},
	},
	{
		Name:   AttrCheckpointSeq,
		Source: "Row.CheckpointSeq(仅快照行,且 >0——seq=0 的快照不是一次真实提交)",
		Domain: Domain{Kind: "int64", Note: "快照序号:同一 RunID 下严格递增(存储侧 CAS 判据)"},
		Card:   Cardinality{Why: "随步骤推进次数增长,本仓定不出上界"},
	},
	{
		Name:   AttrCheckpointStatus,
		Source: "Row.CheckpointStatus(仅快照行,且非空)",
		Domain: Domain{Kind: "string", Note: "快照的编排状态:取值域**应当**是 contract 包的状态常量集(created/running/waiting_approval/succeeded/failed/cancelled/timed_out),但值由宿主写入快照——本仓保证不了宿主不写别的,故不列 Values、不标 label-safe(对宿主写面的保证不该由本包代签)"},
		Card:   Cardinality{Why: "词表本身七值,但写入面在宿主,本仓给不出可保证的上界"},
	},
}

AttrSpecs 映射契约表(顺序 = otel.go 的常量声明顺序,便于逐行对读)。

View Source
var ForbiddenLabels = []ForbiddenLabel{
	{
		Name: "session_id",
		Why:  "一次会话一个取值:进 label 会让指标时序数按会话数膨胀,且会话标识是可关联标识——进了指标后端就等于把它变成长期可查询的面",
	},
	{
		Name: "agent_slug",
		Why:  "取值数随部署的 agent 目录增长,本仓定不出上界;同一部署内它也更像维度而不是枚举",
	},
	{
		Name: "payload",
		Why:  "事件载荷:自由文本、基数无界,且可能含未脱敏内容——映射面刻意不读它(见 otel.go 头注「映射面不含 payload」)",
	},
}

ForbiddenLabels 禁区清单。

这三项都不在 Row 上——写在这里是给宿主的:它们看起来「很像维度」(也确实是有用的 关联键),但落进 metric label 就变成无界基数。要关联会话,用 trace_id(真相源在 @oekit/tracing@),那是**标识**不是维度。

Functions

This section is empty.

Types

type AttrSpec

type AttrSpec struct {
	// Name 属性名(必是 otel.go 里的某个 Attr* 常量,且全表不重复)。
	Name string
	// Source 来源字段:Row 上的哪一个字段(派生值写清怎么算出来的)。
	Source string
	// Domain 取值域。
	Domain Domain
	// Card 基数上界。
	Card Cardinality
	// LabelSafe 能否作为 metric label;为真必须「枚举取值 + 本仓给得出上界」。
	LabelSafe bool
}

AttrSpec 一个属性的映射契约。

func AttrSpecOf

func AttrSpecOf(name string) (AttrSpec, bool)

AttrSpecOf 按属性名取契约(不存在时 ok=false)。

type Cardinality

type Cardinality struct {
	// Max 本仓能保证的取值上界;0 = 本仓定不出上界。
	Max int
	// Why 上界依据(Max 为 0 时写清为什么定不出)。
	Why string
}

Cardinality 属性基数上界。

type Domain

type Domain struct {
	// Kind 取值形态,取 **Go 类型名**(string / int64 / float64 / bool)。
	// 见头注「Go 类型为什么进契约」。
	Kind string
	// Values 枚举取值(按本仓词表顺序);空 = 非枚举。
	Values []string
	// Note 取值域说明。非枚举必写(枚举的取值域已由 Values 表达完整)。
	Note string
}

Domain 属性取值域。

type ForbiddenLabel

type ForbiddenLabel struct {
	// Name 语义名。
	Name string
	// Why 为什么禁止。
	Why string
}

ForbiddenLabel 明确禁止作为 metric label 的语义(宿主不得自行补进 label)。

type Issue

type Issue struct {
	// Pairing 缺口类目(missing_result / duplicate_result / unpaired_result /
	// duplicate_call)。
	Pairing Pairing
	// CallID 相关工具调用标识(无关联调用时为空)。
	CallID string
	// Seq 相关行的序号。
	Seq int
}

Issue 配对缺口(可观测面):配对不完整时显式报出,不静默丢弃。

type Kind

type Kind string

Kind 轨迹行的类目。

const (
	// KindTurn 轮次行:一轮 turn 的起止(turn_started / turn_ended)。
	KindTurn Kind = "turn"
	// KindStep 步骤行:一个环节的起止(step_started / step_finished)。
	KindStep Kind = "step"
	// KindToolCall 工具调用行:调用时刻为起,配对到的结果时刻为止。
	KindToolCall Kind = "tool_call"
	// KindToolResult 工具结果行:**只在结果没有对应调用时**出现——
	// 配对缺口要显形,不能因为「没有调用行可挂」就把结果丢掉。
	KindToolResult Kind = "tool_result"
	// KindSideEffect 副作用行(v0.33.0,issue #99):一次副作用动作的
	// 授予 → 收口段(granted 起,completed/abandoned/uncertain 止),
	// 以及该 Key 的对账段(uncertain 起,resolved 止)。排障第一问
	// 「这个副作用到底施加了吗」的答案面。
	KindSideEffect Kind = "sideeffect"
	// KindCheckpoint 快照行(v0.33.0,issue #99):一次编排进度快照的成功落库
	// (checkpoint_committed)。**点事件**:无括号段,落成孤儿行(只有终点的行)
	// ——没有实测时长就不写时长,不拿 0 冒充。排障第二问「恢复点停在哪个序号」
	// 的答案面。
	KindCheckpoint Kind = "checkpoint"
)

type Options

type Options struct {
	// Summarize 摘要注入点:宿主自定义行标签(展示文案归宿主,内核不认识
	// 任何领域词)。返回空串时沿用本包的通用标签;nil 时全程用通用标签。
	//
	// 只在行创建时调用一次——行的可读字段(类目、标识、工具名、时刻)在创建
	// 后不再变化,故摘要不需要跟着配对状态刷新。
	Summarize func(Row) string
}

Options 投影选项。

type Pairing

type Pairing string

Pairing 工具调用与结果的配对状态(非工具行恒为 PairingNone)。

const (
	// PairingNone 不适用(非工具行)。
	PairingNone Pairing = ""
	// PairingPaired 调用与结果已配对:起止齐备,时长为实测。
	PairingPaired Pairing = "paired"
	// PairingMissingResult 尚未观察到结果。可能仍在途;投影收口后仍如此,
	// 即确认为「等了也没等到」。
	PairingMissingResult Pairing = "missing_result"
	// PairingDuplicateResult 同一 call_id 收到第二个及以后的结果。
	PairingDuplicateResult Pairing = "duplicate_result"
	// PairingUnpairedResult 结果没有对应的调用。
	PairingUnpairedResult Pairing = "unpaired_result"
	// PairingDuplicateCall 同一 call_id 收到第二个调用声明。
	PairingDuplicateCall Pairing = "duplicate_call"
)

type Projector

type Projector struct {
	// contains filtered or unexported fields
}

Projector 增量投影器:事件流 → 轨迹行。

并发安全:事件流可能由多个生产者投递,故 Apply 内部加锁。

func NewProjector

func NewProjector(opts Options) *Projector

NewProjector 构造投影器。

func (*Projector) Apply

func (p *Projector) Apply(ev eventbus.Event)

Apply 投一条事件(增量投影)。

已经被投过的事件不会被重复计入:后续事件只会**更新**既有行(补结束时刻、 把配对状态从「未配对」改成「已配对」),不会追加出第二条行。

func (*Projector) ApplyAll

func (p *Projector) ApplyAll(evs []eventbus.Event)

ApplyAll 依次投多条事件(增量投影;与逐条 Apply 等价)。

func (*Projector) Finalize

func (p *Projector) Finalize() []Row

Finalize 收口投影:把「不会再有新事件」落成显式事实。

返回仍未收口的行(未完成行)。这些行是 open 态:它们有起点、缺终点, 不是「时长 0 的已完成行」。工具调用行若此时仍无结果,会被记成 missing_result 缺口——到这一步才知道「等不到」是真的等不到。

不锁死投影器:宿主若继续投事件,投影照常更新,相应缺口也会被清掉。

func (*Projector) Issues

func (p *Projector) Issues() []Issue

Issues 配对缺口清单(累计)。

口径:只有「当前仍成立」的缺口会被后来的事件清掉(缺结果等到了结果、 无对应的结果等到了调用);「发生过」的缺口(重复结果、重复声明)永远留在 清单里。同一 (类目, call_id) 只记一条——重复次数由行上的 DupResults 承载。

func (*Projector) Len

func (p *Projector) Len() int

Len 当前轨迹行数。

func (*Projector) Rows

func (p *Projector) Rows() []Row

Rows 当前轨迹行快照(按行序号升序)。

返回拷贝:调用方改动拿到的切片不影响投影器内部状态。

type Row

type Row struct {
	// Seq 行序号(1 起,按创建顺序):宿主据此排序,投影内稳定。
	Seq int
	// Kind 行类目。
	Kind Kind
	// ID 行标识:轮次行 "turn#1";带环节号的步骤行 "step#7";
	// 工具行 "tool#<call_id>";副作用行 "sideeffect#<key>"(同 Key 的多段
	// ——授予段、对账段——共用一个标识:它们是同一个动作的先后两段);
	// 快照行 "checkpoint#<run_id>#<seq>";无自然键的行用 "step#-<seq>" 形式兜底
	// ("-" 前缀表示「这个标识是投影生成的,不是业务自然键」)。
	// 格式的唯一生成点是 turnRowID / stepRowID / toolRowID / sideeffectRowID /
	// checkpointRowID / seqID——otel 的父子关系复用同一组函数,另写一份格式
	// 父子就会连不上。
	ID string
	// Turn 所属轮次序号(1 起);轮次之外的事件为 0。
	Turn int
	// HasStep 是否带环节号(环节号 0 是合法值,故用独立标志位表达「有没有」)。
	HasStep bool
	// StepID 所属环节号(HasStep 为真时有效)。
	StepID uint64
	// CallID 工具调用标识(仅工具行;配对键)。
	CallID string
	// Tool 工具名(仅工具行)。
	Tool string
	// SideEffectKey 幂等键(仅副作用行;同 Key 的授予段与对账段共用它配对)。
	SideEffectKey string
	// SideEffectPhase 副作用阶段(仅副作用行)。打开的授予段为 granted;
	// 收口时写成终态(completed / abandoned / uncertain);对账段收口时写成
	// resolved_applied / resolved_cleared;点行为 replayed / contended。
	SideEffectPhase SideEffectPhase
	// SideEffectResource 目标资源标识(仅副作用行;租约的独占单位)。
	SideEffectResource string
	// SideEffectFence 栅栏令牌(仅副作用行;授予段/点行为观察到的代数)。
	SideEffectFence uint64
	// SideEffectSafety 重试安全性声明(仅副作用行;取值域 unknown/safe/unsafe,
	// 与 sideeffect.RetrySafety 的 String() 形态逐字一致)。
	SideEffectSafety string
	// CheckpointRunID 编排运行标识(仅快照行)。
	CheckpointRunID string
	// CheckpointSeq 快照序号(仅快照行;「恢复点停在哪个序号」的答案)。
	CheckpointSeq uint64
	// CheckpointStatus 快照的编排状态(仅快照行;取值域见 contract 的状态常量集,
	// 本包不复制那份词表——引用而不是抄写,才不会漂出第二份真相)。
	CheckpointStatus string
	// StartAt 起始时刻(取自事件时刻;缺失时零值)。
	StartAt time.Time
	// EndAt 结束时刻(取自事件时刻;未收口时零值)。
	EndAt time.Time
	// Status 收口状态。
	Status Status
	// Pairing 配对状态(仅工具行)。
	Pairing Pairing
	// DupResults 重复结果计数(同一 call_id 收到第二个及以后的结果时累加)。
	DupResults int
	// Summary 行摘要(默认通用标签;宿主可用 Options.Summarize 覆盖)。
	Summary string
}

Row 轨迹行:一轮 turn 里的一段有起止的过程。

行里**没有**时长字段——时长由 Duration() 从起止时刻现算(见包头纪律 1)。 存一个时长字段就会多出第二个真相:事件时刻与存下来的时长各说各话, 而估算值也就能混进同一个字段冒充实测。

func (Row) Complete

func (r Row) Complete() bool

Complete 行是否已收口(起止齐备)。

与 Duration 的区别:orphan 行(只有结束没有开始)同样没有实测时长, 但它的缺口不是「还在等」,而是「起点从未有过」——两件事要分开看。

func (Row) Duration

func (r Row) Duration() (time.Duration, bool)

Duration 返回实测耗时与「是否实测」。

时长只有这一个源:起止时刻相减。行内不存时长——存了就会有两个真相, 而估算值也就能混进同一个字段冒充实测。没有实测值时返回 ok=false: 不拿 0 冒充「瞬时完成」,更不拿载荷体积估一个填进来。

结束早于开始(事件时刻本身不自洽,例如结果先于调用到达)时同样返回 ok=false:负时长不是实测值。

type SideEffectPhase added in v0.34.0

type SideEffectPhase string

SideEffectPhase 副作用行所处的阶段(与 sideeffect 包的 Outcome / 生命周期词表对齐; 本包不 import sideeffect——投影层只吃事件载荷字符串,取值域在契约表里逐字声明, 两边的一致性由契约用例钉住)。

const (
	// SideEffectGranted 闸已授予执行权,动作进行中(行打开,未收口)。
	SideEffectGranted SideEffectPhase = "granted"
	// SideEffectCompleted 施加并记账成功(Complete 被接受)。
	SideEffectCompleted SideEffectPhase = "completed"
	// SideEffectAbandoned 显式放弃:确定未生效,或下游按幂等键去重(可安全重试)。
	SideEffectAbandoned SideEffectPhase = "abandoned"
	// SideEffectUncertain 结果未知:可能已生效。该行的授予段就此收口,
	// 同 Key 的对账段开启(等 resolved 收口;收不了口会在 Finalize 显形)。
	SideEffectUncertain SideEffectPhase = "uncertain"
	// SideEffectResolvedApplied 对账裁决「已生效」:Key 记为已完成,后续取回结果。
	SideEffectResolvedApplied SideEffectPhase = "resolved_applied"
	// SideEffectResolvedCleared 对账裁决「未生效」:未知标记清除,Key 重新可执行。
	SideEffectResolvedCleared SideEffectPhase = "resolved_cleared"
	// SideEffectReplayed 点观察:动作早已完成,本次取回既有结果(无括号段,
	// 落成孤儿行——原动作发生在别的进程里,这里只有「观测到重放」这一刻)。
	SideEffectReplayed SideEffectPhase = "replayed"
	// SideEffectContended 点观察:资源正被他人持有 / 该 Key 有未裁决尝试。
	// 双 Worker 争用的证据面。
	SideEffectContended SideEffectPhase = "contended"
)

type Span

type Span struct {
	// Name span 名(低基数,取值域见 Kind)。
	Name string
	// ParentID 父 span 的行标识(与 Row.ID 同源,见头注「trace 边界」);
	// 空 = 根,宿主应为它开一个新 trace。
	ParentID string
	// Status span 状态。
	Status SpanStatus
	// StartAt 起点(未收口时为零值,宿主导出时可自行取当前时刻)。
	StartAt time.Time
	// EndAt 终点(未收口时为零值)。
	EndAt time.Time
	// Attributes 属性表(键取本文件的 Attr* 常量,值是 string/int64/float64)。
	Attributes map[string]any
}

Span 一行轨迹的 span 表达(不含 exporter 相关字段:那是投递侧的事)。

func SpanOf

func SpanOf(r Row) Span

SpanOf 把一行轨迹映射成一个 span。

纯函数、不分配时间戳(不读 @time.Now()@):导出时刻由宿主决定, 内核读时钟会让同一份轨迹在两次导出里得出不同的时间——那是第二个真相。

func SpansOf

func SpansOf(rows []Row) []Span

SpansOf 批量映射(保持输入顺序 = 行序)。

与逐条调用 @SpanOf@ 的唯一差别是**解析悬空父**:父行不在本批内时把 @ParentID@ 清成空,这一行随之成为根(见头注)。宿主导出一批行时应当用这个入口—— 逐条导出的断链 span 在多数后端里表现为「trace 缺了一段」,而不会报错。

type SpanStatus

type SpanStatus string

SpanStatus span 状态(OTel 的三值词表)。

const (
	// SpanStatusUnset 未定:这一行还没收口,或「仍在等结果」。
	//
	// 缺省就是这个——把未定硬记成 error 会让真正的事故淹在噪声里。
	SpanStatusUnset SpanStatus = "unset"
	// SpanStatusOK 已完成且没有观察到问题。
	SpanStatusOK SpanStatus = "ok"
	// SpanStatusError 观察到**发生过**的问题(不是「还没等到」)。
	SpanStatusError SpanStatus = "error"
)

type Status

type Status string

Status 轨迹行的收口状态。

const (
	// StatusOpen 已开始、未收口:缺结束事件(可能仍在途,也可能是缺口)。
	StatusOpen Status = "open"
	// StatusClosed 起止齐备:行已收口。
	StatusClosed Status = "closed"
	// StatusOrphan 只有结束事件、没有开始事件:起点缺失,配不出时长。
	StatusOrphan Status = "orphan"
)

type TraceGranularity

type TraceGranularity string

TraceGranularity trace 的切分单位。

const (
	// TracePerTurn 一轮 turn 一个 trace(**本仓选它**)。
	TracePerTurn TraceGranularity = "turn"
	// TracePerStep 一个环节一个 trace(本仓不选:行标识只在轮内唯一,
	// 同号环节会在两个 trace 里撞同一个标识)。
	TracePerStep TraceGranularity = "step"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL