obs

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

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

View Source
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

func Export(ctx context.Context, spans []Span, tp *sdktrace.TracerProvider) error

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

func NewOTLPProvider(ctx context.Context, endpoint string) (*sdktrace.TracerProvider, error)

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

func DeriveStatus(events []state.Event) RunStatus

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

func Project(events []state.Event, blobs state.Blobs) ([]Span, error)

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 SpanEvent

type SpanEvent struct {
	Name       string
	Time       time.Time
	Attributes map[string]any
}

SpanEvent is a point-in-time event on a span (e.g. gen_ai.evaluation.result).

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
)

Jump to

Keyboard shortcuts

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