rig-tap 0.2.2

Backend-agnostic observability event schema and taps for Rig agents (prompt/tool lifecycle + context-size sampling).
Documentation
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.2.2]https://github.com/ForeverAngry/rig-tap/compare/v0.2.1...v0.2.2 - 2026-06-01

### Documentation

- Align README Status block with shipped 0.2.1 ([#11]https://github.com/ForeverAngry/rig-tap/pull/11)

### Fixed

- Keep rate sampling pair coherent ([#13]https://github.com/ForeverAngry/rig-tap/pull/13)

## [0.2.1]https://github.com/ForeverAngry/rig-tap/compare/v0.2.0...v0.2.1 - 2026-05-28

### Documentation

- Bump documented crate version 0.1.0 -> 0.2.0 ([#9]https://github.com/ForeverAngry/rig-tap/pull/9)

## [0.2.0]https://github.com/ForeverAngry/rig-tap/compare/v0.1.2...v0.2.0 - 2026-05-28

### Added

- **Span correlation**`ObservabilityEvent` gains an optional
  `span_id: Option<u64>` field, auto-populated by `build_event` /
  `emit_kind` from `tracing::Span::current().id()`. Surfaced as a
  `rig_tap.span_id` tracing field (with `0` as the absent sentinel)
  and serialized into the JSON envelope as `span_id` when present
  (`skip_serializing_if = "Option::is_none"`, `#[serde(default)]` for
  legacy envelopes). Lets collectors stitch `rig-tap` events into an
  existing `tracing-opentelemetry` span waterfall without
  conversation-id post-processing. Additive — does not bump
  `SCHEMA_VERSION`. New helper `current_span_id()` exposes the same
  value for downstream producers.

- **`SamplingPolicy` trait + `RatePolicy` + `AlwaysSample`** — new
  per-event-kind sampling layer for `TelemetryHook`. The default policy
  (`AlwaysSample`) preserves the existing behaviour; callers can opt into
  per-kind downsampling via
  `TelemetryHook::with_sampling_policy(Arc::new(RatePolicy::new()...))`.
  Decisions are deterministic (fixed-seed hash of a per-event
  correlator) so `tool.invoked` / `tool.completed` pairs stay coherent
  — either both ship or both drop. `prompt.*` correlator =
  conversation id; `tool.*` correlator = internal call id.
  Documented in the README under "Sampling controls".

- **OpenTelemetry exporter recipe** — new README section documents the
  stable `rig_tap.*` attribute keys, a minimum-viable OTel Collector
  config (filter + attributes processors with optional GenAI semconv
  aliases), and the in-process `tracing-opentelemetry` wiring. No
  collector-side JSON transform is required: the `rig_tap.*` tracing
  fields are already valid OTel attribute names.
- `examples/otel_exporter_recipe.rs` (gated on `subscriber`) — emits one
  event per major family (`prompt.completed`, `tool.invoked`,
  `eval.report`) and prints the exact OTel-ready attribute set a
  collector would extract from the structured `tracing` fields, plus
  the full JSON envelope. Self-contained: pulls no `opentelemetry-*`
  dependency.

- `EventKind::EvalReport` — new `eval.report` schema variant carrying a
  single retrieval/RAG metric (`report_id`, `dataset`, `metric`, `value`,
  optional bootstrap CI `ci_low`/`ci_high`, optional baseline diff
  `baseline_value`/`delta`/`verdict`, optional `sample_size`). Producers
  emit one event per `(report_id, dataset, metric)` triple so collectors
  can filter and aggregate without parsing the envelope. Pairs with
  `rig-retrieval-evals` `MultiReport` / `ReportDiff` summaries but the
  variant is producer-agnostic. Additive — does not bump `SCHEMA_VERSION`.
- `ScalarFields` gains `dataset`, `metric`, and `verdict` columns,
  surfaced on every emitted `eval.report` event as `rig_tap.dataset`,
  `rig_tap.metric`, and `rig_tap.verdict` tracing fields for OTel
  attribute extraction.
- `EventKind::is_eval_related()` classifier.

- `examples/observe_responses_api.rs` — env-driven runnable example
  (`OPENAI_API_KEY` + optional `RIG_TAP_PROMPT` / `RIG_TAP_MODEL` /
  `RIG_TAP_SESSION_ID`) that wires `CapturingLayer` to
  `ObservedResponsesSession`, drives one turn against the live OpenAI
  Responses WebSocket endpoint, and prints the captured envelope
  sequence. Exits cleanly when `OPENAI_API_KEY` is unset so it stays
  CI-runnable.
- `tests/responses_session.rs` — fixture-driven integration test
  asserting the multi-turn lifecycle, hosted-tool extraction from raw
  `ResponsesWebSocketDoneEvent.response` payloads, error-path
  finalization, and full schema-v1 JSON round-trip.

- `openai-responses-websocket` cargo feature (off by default; forwards
  `rig/websocket`) gating a new `responses_session` module on
  non-WASM targets:
  - `ResponsesSessionObserver` — stateless state machine that ingests
    `send` / `next_event` / `close` signals and emits
    `response.session_started`, `response.turn_started`,
    `response.turn_completed`, `response.session_ended`,
    `tool.hosted_invoked`, and `tool.hosted_completed` envelopes.
  - `ObservedResponsesSession<H>` — drop-in decorator wrapping
    `rig::providers::openai::responses_api::websocket::ResponsesWebSocketSession`.
    Forwards `send` / `send_with_options` / `next_event` / `close` and
    drives the observer on each call. Lazy-finalizes the active turn
    on the next `send`, on `close`, or on `into_inner` if the caller
    stops short of a `Done` event so partial turns still emit a
    `turn_completed` envelope.
  - Hosted-tool extraction runs automatically on the raw
    `ResponsesWebSocketDoneEvent.response` payload via
    `extract_hosted_tools`, sidestepping `rig-core`'s
    `#[serde(other)] Unknown` discard path for typed `Output` items.

- `openai-responses` cargo feature (off by default, no extra runtime
  dependency) gating a new `responses_extract` module:
  - `extract_hosted_tools(payload: &serde_json::Value) -> Vec<HostedToolCall>`
    walks the `output[]` array of a Responses-API JSON payload and
    returns every hosted-tool invocation it finds (`web_search`,
    `file_search`, `computer_use`, `code_interpreter`, and any future
    `*_call` kind). Function-tool items are skipped so they don't
    double-emit on top of `tool.invoked`.
  - `emit_hosted_tools(conversation_id, response_id, payload) -> usize`
    convenience helper that emits a `tool.hosted_invoked` /
    `tool.hosted_completed` pair per call directly through `tracing`.
  - `HostedToolCall` struct (`#[non_exhaustive]`) exposing
    `tool_name`, `provider_call_id`, `call_id`, `status`, compact
    `args_json` / `result_json` summaries, the raw `Value`, and the
    `output_index`.

  The extractor operates on raw JSON because `rig-core`'s typed
  `responses_api::Output` enum carries `#[serde(other)] Unknown` and
  silently discards hosted-tool payloads at deserialization. Feed it
  the `ResponsesWebSocketDoneEvent.response` value (raw `serde_json::Value`,
  never typed-deserialized) or any pre-rig-core HTTP body.

- Schema v1.1 (additive) for OpenAI Responses-style stateful endpoints:
  - `EventKind::PromptCompleted` now carries an optional
    `previous_response_id` so consumers can reconstruct the
    server-side turn chain without joining a separate trace.
  - `EventKind::ToolHostedInvoked` / `EventKind::ToolHostedCompleted` for
    provider-native hosted tools (`web_search`, `file_search`,
    `computer_use`, `code_interpreter`) that never fire
    `PromptHook::on_tool_call`. Producers wire these from a streaming-
    chunk tap or session decorator.
  - `EventKind::ResponseSessionStarted` / `ResponseTurnStarted` /
    `ResponseTurnCompleted` / `ResponseSessionEnded` for stateful
    WebSocket-mode sessions. Each turn carries the chain ancestor and an
    observed hosted-tool-call count.
  - `EventKind::is_response_lifecycle_related()` classifier.
  - `ScalarFields::response_id` and `ScalarFields::previous_response_id`
    surface as `rig_tap.response_id` / `rig_tap.previous_response_id`
    on every emitted `tracing` event so collectors can route on them
    without parsing the JSON envelope.
- `TelemetryHook::with_previous_response_id_resolver` — per-request
  escape hatch that stamps the chain ancestor on `prompt.completed` from
  caller-tracked state (task-local, span field, session object).
  Public `PreviousResponseIdResolver<R>` alias re-exported from the
  crate root.

### Changed

- `ScalarFields` is now `#[non_exhaustive]` so future schema-additive
  releases can append new scalar correlators without a breaking change.
  Existing reads (`fields.tool_name`, `fields.kernel_id`, …) are
  unaffected; construct values via
  `ScalarFields { tool_name, ..Default::default() }` rather than the
  full struct literal.

## [0.1.2]https://github.com/ForeverAngry/rig-tap/compare/v0.1.1...v0.1.2 - 2026-05-27

### Added

- Add query helpers for captured observability events.

## [0.1.1]https://github.com/ForeverAngry/rig-tap/compare/v0.1.0...v0.1.1 - 2026-05-27

### Added

- Emit compose lifecycle telemetry

## [0.1.0] - Unreleased

### Fixed

- `DispatchObserveHook` now emits `tool.skipped` for `rig-compose` synthetic
  skip outcomes instead of reporting them as `tool.completed`.
- `rig_tap` tracing events now include stable scalar `rig_tap.*` fields next
  to the JSON envelope so OpenTelemetry collectors can route and index them
  without parsing the `event` JSON string.

### Added

- `ObservabilityEvent` v1 schema covering the prompt / tool / context /
  memory lifecycle plus additive `compose.*` kernel-loop event kinds for
  kernel start/shutdown, loop iteration, skill resolution, retry attempts,
  and recovery paths.
- `TelemetryHook<M>: PromptHook<M>` that emits `prompt.*` and `tool.*`
  events.
- `TelemetryHook::with_conversation_id_resolver` — per-request escape
  hatch that wins over `TelemetryHookConfig::conversation_id` when the
  resolver returns `Some(id)`. Wire it to a task-local or span field
  while waiting on upstream `PromptHook` per-request context.
- `TelemetryHook::with_model_resolver` — extracts the concrete model
  identifier from each `CompletionResponse<M::Response>` for routed
  providers (OpenRouter, Bedrock routing, vendor multi-model
  endpoints) where the configured model name is a logical alias.
- `ObservedMemory<M>: ConversationMemory` decorator that samples context
  size on every `load`.
- `ChainedHook<A, B>` combinator for composing two `PromptHook`s on a
  single agent. Opt in to synthetic `tool.skipped` / `tool.terminated`
  emissions via `ChainedHook::observe_with`.
- `emit` helper that serializes an event under
  `tracing::info!(target = "rig_tap", event = %json)`.
- Optional `subscriber` feature exposing `CapturingLayer`, a public
  `tracing_subscriber::Layer` that buffers decoded
  `ObservabilityEvent`s in memory. Off by default; intended for tests,
  examples, and small in-process tools.
- Optional `compose` feature exposing `DispatchObserveHook`, a
  `rig_compose::ToolDispatchHook` adapter that emits `tool.invoked`,
  `tool.completed`, and `tool.terminated` from kernel-direct dispatch. The
  same adapter now implements `rig_compose::AgentLifecycleHook` and emits
  `compose.*` kernel-loop events from `GenericAgent` step execution.