Documentation
¶
Overview ¶
Package obs is the read-only OpenTelemetry projection of the AWF event log.
obs NEVER runs on the execution path (CLAUDE.md invariant: "everything else executes work or reads the log"). Project(events, blobs) is a pure, deterministic function from a folded log to a []Span tree; Export replays those spans into an OTel SpanExporter (stdout/file/OTLP). Attribute names are defined once in attrs.go (the swappable gen_ai.* mapping layer, pinned to OTel semconv spec v1.41.1).
Index ¶
- Constants
- func Export(ctx context.Context, spans []Span, tp *sdktrace.TracerProvider) error
- func NewOTLPProvider(ctx context.Context, endpoint string) (*sdktrace.TracerProvider, error)
- func NewOTLPProviderWithLimits(ctx context.Context, endpoint string, limits sdktrace.SpanLimits) (*sdktrace.TracerProvider, error)
- func NewStdoutProvider(w io.Writer) (*sdktrace.TracerProvider, error)
- func NewStdoutProviderWithLimits(w io.Writer, limits sdktrace.SpanLimits) (*sdktrace.TracerProvider, error)
- type ProjectOptions
- type RunStatus
- type Span
- type SpanEvent
- type SpanStatus
Constants ¶
const ( AttrWorkflowID = "awf.workflow.id" AttrWorkflowVersion = "awf.workflow.version" AttrWorkflowDigest = "awf.workflow.digest" AttrRunID = "awf.run.id" AttrRunEpoch = "awf.run.epoch" AttrNodePath = "awf.node.path" AttrNodeKind = "awf.node.kind" // leaf step spans only — one of the 12 real node kinds AttrScopeKind = "awf.scope.kind" // synthesized control-scope spans only — structural role (M1) AttrNodeOutcome = "awf.node.outcome" AttrNodeDurationMS = "awf.node.duration_ms" AttrExitCode = "awf.exit_code" AttrAgentTurns = "awf.agent.turns" AttrBranch = "awf.branch" AttrLoopIterations = "awf.loop.iterations" AttrSkipReason = "awf.skip.reason" AttrGateAttempt = "awf.gate.attempt" AttrGateStatus = "awf.gate.status" AttrCostUSD = "awf.cost.usd" AttrCostSource = "awf.cost.source" AttrGateAttempts = "awf.gate.attempts" AttrGateOutcome = "awf.gate.outcome" AttrRunCostUSD = "awf.run.cost.usd" AttrCallPath = "awf.call.path" AttrCallInputRef = "awf.call.input_ref" AttrGenAIInputTokens = "gen_ai.usage.input_tokens" AttrGenAIOutputTokens = "gen_ai.usage.output_tokens" AttrGenAICacheRead = "gen_ai.usage.cache_read.input_tokens" AttrGenAICacheCreate = "gen_ai.usage.cache_creation.input_tokens" AttrGenAIConversation = "gen_ai.conversation.id" AttrSessionID = "session.id" // gen_ai.evaluation.result event (Task 11). Not in the Go semconv package. EventGenAIEvaluation = "gen_ai.evaluation.result" AttrGenAIEvalName = "gen_ai.evaluation.name" // awf.agent.event span-event name (slice 6.2 content capture). EventNameAgentContent = "awf.agent.event" // awf.skills.selected span-event name (native skill routing metadata). EventNameSkillsSelected = "awf.skills.selected" AttrSkillsLibrary = "awf.skills.library" AttrSkillsLibraryDigest = "awf.skills.library_digest" AttrSkillsRouter = "awf.skills.router" AttrSkillsRouterVersion = "awf.skills.router_version" AttrSkillsSelectedCount = "awf.skills.selected.count" AttrSkillsRouterParamPre = "awf.skills.router_param." AttrSkillsSelectedPre = "awf.skills.selected." // Content-capture attribute names (slice 6.2). STABLE wire contract — never renamed. AttrNodeOutput = "awf.node.output" AttrNodeOutputRef = "awf.node.output_ref" AttrNodeOutputError = "awf.node.output_error" AttrNodeStdout = "awf.node.stdout" AttrNodeStdoutRef = "awf.node.stdout_ref" AttrNodeStdoutError = "awf.node.stdout_error" AttrAgentEventKind = "awf.agent.event.kind" AttrAgentEventStream = "awf.agent.event.stream" AttrAgentEventLive = "awf.agent.event.live" AttrAgentEventDisplayClass = "awf.agent.event.display_class" AttrAgentEventDisplayTool = "awf.agent.event.display_tool" AttrAgentEventDisplaySummary = "awf.agent.event.display_summary" AttrAgentEventDisplayLines = "awf.agent.event.display_lines" AttrAgentEventDisplayBytes = "awf.agent.event.display_bytes" AttrAgentEventDisplayIsError = "awf.agent.event.display_is_error" AttrAgentEventPayload = "awf.agent.event.payload" AttrAgentEventPayloadRef = "awf.agent.event.payload_ref" AttrAgentEventPayloadError = "awf.agent.event.payload_error" AttrAgentEventPayloadPreview = "awf.agent.event.payload_preview" )
Attribute names. awf.* is the STABLE contract (never renamed). gen_ai.* names follow OTel semantic-conventions spec v1.41.1 and are hand-defined here as raw strings because the Go semconv package lags (v1.37.0 at otel v1.43.0) and does not define gen_ai.evaluation.*. This file is the single swappable mapping layer: a convention bump is a one-file edit.
Variables ¶
This section is empty.
Functions ¶
func Export ¶
Export replays a projected span tree into the OTel SDK via tp's exporter. Parent-before-child order is guaranteed by sorting on path depth so a child's parent SpanContext already exists in ctxByPath. Past timestamps are honored (trace.WithTimestamp). Pending spans are ended too (OTel cannot export an open span) carrying their incomplete marker. tp's SpanProcessor should be a SimpleSpanProcessor for deterministic, synchronous, ordered export. Span NAMES are the low-cardinality s.Name (step id / scope kind), set at projection time; the unique path is in the awf.node.path attribute (R8).
R7 — error surface: per-span exporter failures are NOT returned here. SimpleSpanProcessor.OnEnd routes an ExportSpans error to the OTel GLOBAL ErrorHandler (otel.Handle) — set otel.SetErrorHandler to capture them. Export returns only ForceFlush's error (and a SimpleSpanProcessor buffers nothing to flush). The default stdout / in-memory paths in 6.1 need no delivery guarantee; a delivery-guaranteed exporter wrapper is a later concern.
func NewOTLPProvider ¶
NewOTLPProvider builds a TracerProvider exporting to an OTLP/HTTP endpoint (e.g. a local collector / Jaeger / Honeycomb). endpoint is host:port (no scheme); WithInsecure for a plaintext local collector.
func NewOTLPProviderWithLimits ¶
func NewOTLPProviderWithLimits(ctx context.Context, endpoint string, limits sdktrace.SpanLimits) (*sdktrace.TracerProvider, error)
NewOTLPProviderWithLimits is NewOTLPProvider with caller-supplied span limits.
func NewStdoutProvider ¶
func NewStdoutProvider(w io.Writer) (*sdktrace.TracerProvider, error)
NewStdoutProvider builds a TracerProvider that writes spans to w as JSON via a synchronous SimpleSpanProcessor — the zero-infra default for `awf trace` and the conformance round-trip target. Uses the SDK default ID generator (random); deterministic IDs are a test-only concern (m3), not a prod knob.
func NewStdoutProviderWithLimits ¶
func NewStdoutProviderWithLimits(w io.Writer, limits sdktrace.SpanLimits) (*sdktrace.TracerProvider, error)
NewStdoutProviderWithLimits is NewStdoutProvider with caller-supplied span limits. awf trace --capture-content passes limits with EventCountLimit / AttributePerEventCountLimit lifted (the SDK default is 128), so a long agent run's content span events aren't silently dropped at export.
Types ¶
type ProjectOptions ¶
type ProjectOptions struct {
CaptureContent bool
}
ProjectOptions tunes the projection. CaptureContent (default false) makes ProjectWithOptions dereference agent I/O + typed-output/stdout blobs and attach them to spans (slice 6.2, awf trace --capture-content); when false the blobs param is unused and the output is byte-identical to slice 6.1 (so the conformance byte-identical-replay assertion holds).
type RunStatus ¶
type RunStatus string
RunStatus is the lifecycle status awf ls surfaces for a run.
const ( RunRunning RunStatus = "running" // resolved by the CLI: incomplete + live lock holder RunPaused RunStatus = "paused" // run.paused, not subsequently resumed RunFinished RunStatus = "finished" // run.finished{ok} RunFailed RunStatus = "failed" // run.finished{permanent/rejected} OR terminal node.failed{permanent} RunResumable RunStatus = "resumable" // run.finished{retryable_failure} OR terminal node.failed{retryable_failure} — re-drivable via `awf resume` RunCancelled RunStatus = "cancelled" // run.cancelled (terminal) RunCrashed RunStatus = "crashed" // resolved by the CLI: incomplete + no live lock holder // RunIncomplete = started, no terminal event. DeriveStatus stops here; the // CLI resolves it to RunRunning (lock held) or RunCrashed (lock free) via the // sidecar flock probe (cli/runlock.go). DeriveStatus itself is pure / no OS. RunIncomplete RunStatus = "incomplete" )
func DeriveStatus ¶
DeriveStatus folds a run's log into a lifecycle status from the LOG ALONE — no liveness probe (the CLI adds that). It never returns RunRunning or RunCrashed; the "started, no terminal event" case is RunIncomplete, which the CLI splits via the lock. Deterministic and OS-free, mirroring obs.Project's pure-projection contract.
Precedence (matches the spec's awf ls definition + crash≠verdict invariant):
run.cancelled → cancelled
run.finished{ok} → finished
run.finished{retryable_failure} → resumable
run.finished{permanent/rejected} → failed
last event node.failed{retryable} → resumable
last event node.failed{permanent} → failed
latest run-control event is run.paused→ paused
otherwise → incomplete (CLI: running | crashed)
type Span ¶
type Span struct {
Path string
ParentPath string
Name string
Kind string
Scope bool // true ⇒ synthesized control-scope; false ⇒ leaf step node
Start time.Time
End time.Time
Pending bool
Status SpanStatus
StatusMsg string
Attributes map[string]any
Events []SpanEvent
}
Span is one node in the projected trace tree — the deterministic output of Project. It is plain data (no OTel SDK types) so Project(log) is byte-stable across replays (spec D6) and Export translates it to OTel at the edge.
Path is the awf.node.path (unique per run); ParentPath is "" for the run root. Attribute values are restricted to string / int64 / float64 / bool so Export can map each to an attribute.KeyValue. A Pending span (started, never finalized) has Pending=true, End set to the log's last-observed event time (deterministic "as-of" bound), and Attributes["awf.node.outcome"]="incomplete".
Scope distinguishes a real node (a leaf step built from node.started/ completed/failed — Scope=false, carries awf.node.kind ∈ the 12 real kinds) from a synthesized control-scope (Scope=true, carries awf.scope.kind). It is the single discriminant for the leaf/scope tests in the bounds fold and the cost rollup — decoupled from the Kind string so structural roles can grow without breaking those (M1). Span is CLEAN public data: no projection bookkeeping fields leak into it (start-seen / finalized tracking lives in Project's local maps, not here).
func Project ¶
Project is the default projection (no content capture). Preserved signature from slice 6.1; a thin wrapper over ProjectWithOptions.
func ProjectWithOptions ¶
func ProjectWithOptions(events []state.Event, blobs state.Blobs, opts ProjectOptions) ([]Span, error)
ProjectWithOptions folds an event log into a deterministic span tree (see Project's slice-6.1 contract). With opts.CaptureContent it additionally attaches raw agent I/O (agent.event payloads, inline or via blobs) and the typed-output / stdout blobs as span events / attributes under awf.* — opaque, never parsed (the "obs must not parse harness internals" invariant). Requires a non-nil blobs when CaptureContent is set.
type SpanStatus ¶
type SpanStatus int
SpanStatus mirrors OTel's status codes without importing the SDK into the pure projection layer (Export translates it).
const ( StatusUnset SpanStatus = iota StatusOK StatusError )