> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nika.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Five observability channels. Every run emits typed events, metrics, traces, billing records, and append-only audit entries.

export const STATUS = {
  head: "95962d5cd",
  branch: "main",
  version: "0.91.0",
  cratesWorkspace: 39,
  cratesAdmitted: 39,
  cratesTarget: "42",
  wipCrates: [],
  libTests: 2989,
  clippyWarnings: 0,
  adrs: 62,
  adrsAccepted: 42,
  adrsProposed: 18,
  providers: 32,
  capabilityRules: 49,
  hygieneVectors: 38,
  hygieneGreen: 28,
  hygieneYellow: 3,
  hygieneRed: 0,
  lastUpdated: "2026-06-25"
};

Every Nika run emits a structured stream of **events**. One event is one
`Event` struct from `nika-kernel::infra::event_sink`, carried as one
line of NDJSON on stdout (or any other sink you wire in). Events are
deterministic, typed, and carry source IDs that correlate to the trace
stack.

The observability surface has **five sibling channels**, each a separate
kernel trait — so impls can be swapped, sampled, or persisted
independently:

| Channel | Trait             | Semantics                                                           |
| ------- | ----------------- | ------------------------------------------------------------------- |
| Events  | `EventSink`       | Structured telemetry. **May be sampled or dropped** under load.     |
| Metrics | `MetricsExporter` | Counters / histograms. Aggregated, low-cardinality.                 |
| Traces  | `TracerProvider`  | W3C-compatible spans. Sampled per-head.                             |
| Billing | `BillingSink`     | Token + cost ledger. **Never dropped, never sampled.**              |
| Audit   | `AuditSink`       | Append-only, persist-or-fail. Compliance (GDPR, EU AI Act, Shield). |

<Info>
  `nika run --json` streams the local event lane today. The kernel traits
  (`EventSink`, `AuditSink`, `BillingSink`, `MetricsExporter`, and
  `TracerProvider`) keep the interface stable while HTTP/SSE/OTLP exporters
  remain future interface work.
</Info>

## `Event` shape

```rust theme={"system"}
#[non_exhaustive]
pub struct Event {
    pub id: EventId,                    // ULID
    pub run_id: RunId,                  // ULID of the current run
    pub trace_id: Option<TraceId>,      // W3C trace context
    pub kind: String,                   // e.g. "task.started"
    pub timestamp_ms: u64,              // Unix epoch milliseconds
    pub payload: serde_json::Value,     // kind-specific fields
}
```

Source: `crates/nika-kernel/src/infra/event_sink.rs`. Every event shares
`id`, `run_id`, `kind`, and `timestamp_ms`; `payload` shape depends on
the `kind` discriminator.

## `EventSink` trait (L0.5)

```rust theme={"system"}
#[trait_variant::make(EventSinkDyn: Send)]
pub trait EventSink: Send + Sync + crate::sealed::Sealed {
    async fn emit(&self, event: Event) -> Result<(), NikaError>;
}
```

**Cancel-safety contract** (from the source): impls MUST NOT maintain
cross-emission state. Dropping a future mid-emit is semantically
identical to a sampled-away event. Batched sinks flush on a separate
task that owns the batch — the `emit` call itself is atomic or
all-nothing.

The trait is `Sealed` (ADR-014): external crates cannot implement it
directly; they route through the workspace-controlled adapter path.

## Event kinds

Every kind is emitted from **exactly one call site** in the codebase
(INV-024). No ambiguous duplicates — if you see `infer.chunk` on the
wire, you know exactly which crate produced it.

### Workflow-level

* `workflow.started` — a run begins.
* `workflow.completed` — run ends, with status.
* `workflow.skipped` — the run was gated off before any task started.

### Task-level

* `task.started` — a task begins executing.
* `task.completed` — a task ends successfully.
* `task.failed` — a task errored out; payload carries the typed error (`code` · `category` · `transient` · the spec error shape). On the wire the engine's event slugs are snake\_case (`task_failed`) — the dot-form here is the documentation vocabulary.
* `task.skipped` — the task's `when:` evaluated to false (or its empty
  `for_each` collection skipped it).
* `task.cancelled` — the task never ran because its default gate became
  unsatisfiable (an upstream failed) or the run was cancelled — a normal
  terminal state under gate-based propagation.
* `task.retry` — a retry attempt begins (emitted before `task.started` of the retry).

### Verb-level

From `nika-kernel::provider::InferEvent`:

* `infer.delta` — streaming text chunk (`Delta { text }`).
* `infer.tool_use_start` — agent called a tool (`ToolUseStart { id, name }`).
* `infer.tool_use_delta` — streaming partial JSON input (`ToolUseDelta { id, partial_json }`).
* `infer.thinking` — extended thinking chunk (`Thinking { text }`).
* `infer.usage` — token usage update (`Usage(TokenUsage)`).
* `infer.done` — stream completed (`Done { stop_reason, request_id, finish_reason_raw }`).

From the other verbs:

* `fetch.request` — HTTP request sent.
* `fetch.response` — HTTP response received.
* `exec.output` — a line of stdout/stderr.
* `invoke.call` — tool invoked.
* `invoke.result` — tool returned.
* `agent.iteration` — one loop turn completed.

### Meta

* `checkpoint` — serializable snapshot (`AgentCheckpoint` — used for resume).
* `taint.violation` — binding crossed a taint boundary (mirrors `AuditRecord::TaintViolation`).
* `budget.warning` — nearing a `BudgetDirective` ceiling.
* `budget.exceeded` — budget hard-stop triggered.

## OTel `GenAI` semconv bridge (Q13)

Every `InferRequest` and `InferResponse` carries a `GenAiAttrs` struct
that maps directly to the OpenTelemetry `gen_ai.*` semantic conventions:

```rust theme={"system"}
#[non_exhaustive]
pub struct GenAiAttrs {
    pub system: GenAiSystem,             // anthropic | open_ai | google | …
    pub operation: GenAiOperation,       // chat | embedding | image_generation | …
    pub response_id: Option<String>,     // gen_ai.response.id
    pub response_model: Option<String>,  // gen_ai.response.model
    pub encoding_formats: Vec<String>,   // gen_ai.request.encoding_formats
    pub conversation_id: Option<String>, // gen_ai.conversation.id
    pub agent_id: Option<String>,        // gen_ai.agent.id
    pub agent_name: Option<String>,      // gen_ai.agent.name
}
```

Source: `crates/nika-kernel/src/ai/genai.rs`. This enforces
**cross-provider parity** (Pre-launch Gate 2): no provider can silently
drop an attribute the kernel exports. When Nika emits an `infer.done`
event, the `payload.gen_ai` block is identical in shape across all
{STATUS.providers} providers.

Cross-link: [OTel GenAI spec](https://opentelemetry.io/docs/specs/semconv/gen-ai/).

## `AuditSink` — the fifth channel (Q12)

`AuditSink` is **separate** from `EventSink`. Audit records are
**append-only, persist-or-fail** — they MUST persist durably (fsync on a
local log, synchronous HTTP POST to an append-only endpoint) before the
trait method returns `Ok`. Buffering in memory is forbidden by contract.

```rust theme={"system"}
#[non_exhaustive]
pub enum AuditRecord {
    CapabilityOverride { tenant_id, capability, reason },
    TaintViolation    { tenant_id, path, severity },
    CanaryLeaked      { tenant_id, canary_id },
    BudgetExhausted   { tenant_id, dimension, cap, observed },
    PolicyDenied      { tenant_id, rule_id, target },
    KeyUsed           { tenant_id, key_id },
    SecretRedacted    { tenant_id, location },
    Extension         { ns, name, payload },   // forward-compat escape hatch
}
```

Every variant carries a `TenantId` for multi-tenant routing. `Extension`
is the forward-compat path (ADR-028) — new categories land there first
and get promoted to a first-class variant once the namespace stabilises.

Severity is separate: `Info`, `Warn`, `Critical`. A `TaintViolation`
with `Severity::Critical` should kill the run; a `CapabilityOverride`
with `Severity::Info` is an operator trail, not a failure.

<Note>
  `AuditSink` replaced an earlier `ObservabilitySink` design during the
  Q12 L0.5 architecture sweep (locked 2026-04-16, see
  `docs/architecture/l0-l05-architecture-decisions.md`). The separation
  matters: telemetry is best-effort; audit is compliance — different
  persistence guarantees, different failure modes, different sinks.
</Note>

## `BillingSink` — the ledger (separate trait)

Token + cost accounting uses its own trait because it has different
dropping semantics than `EventSink`. Billing records are **never
dropped**. Implementations persist on every call and surface failures
to the caller, who kills the run if persistence fails (revenue
integrity > workflow liveness).

See `nika-kernel::infra::billing::BillingSink` + ADR-028 §4.

## Telemetry seams (ADR-035)

Two reservations on the trace layer make the kernel OTel-ready without
breaking any v0.8x caller:

1. `SpanGuard.parent_span_id: Option<SpanId>` — explicit parent link so
   nested spans can be stitched into a tree without a thread-local
   stack (which tokio's multi-thread runtime breaks).
2. `SpanRef { trace_id, span_id }` — a copyable handle that propagates
   span identity across `tokio::mpsc` boundaries and spawned tasks
   without cloning the full `SpanGuard`.

Both land as additive, `#[non_exhaustive]` extensions. In a future minor they
bridge zero-copy into OTel's `SpanContext` when
`nika-observability-otel` admits.

## Enable the stream (target CLI)

```bash theme={"system"}
nika run workflow.nika.yaml --json             # stdout NDJSON
nika run workflow.nika.yaml --json > run.log   # file
```

The CLI JSON lane is live. HTTP/SSE/OTLP export belongs to the future
`nika-serve` interface; today the local binary owns the run stream.

## Why five channels, not one

Because they have incompatible guarantees:

* **Events** want volume + sampling. Dropping is fine.
* **Metrics** want aggregation + low cardinality. Pre-aggregated.
* **Traces** want causal structure + head sampling.
* **Billing** wants durability + no drops. Every token metered.
* **Audit** wants persist-or-fail + append-only. Compliance contract.

Forcing them through one sink loses either the sampling headroom (makes
the hot path slow) or the compliance contract (drops records under
load, fails the audit).

## Related ADRs

* **ADR-021** — YAML envelope. `EventSink` emits events that correlate
  to tasks inside `spec.tasks`, tagged with the workflow's `metadata.name`.
* **ADR-028** — forward-compat reservation policy. `Extension` variants
  in `AuditRecord` + `StopReason::Unknown(String)` in `InferResponse`.
* **ADR-035** — telemetry seams (SpanGuard parent + SpanRef).

## Read next

<CardGroup cols={2}>
  <Card title="Event source" icon="file-code" href="https://github.com/supernovae-st/nika/blob/main/crates/nika-event">
    The `nika-event` crate — every kind + payload schema in source.
  </Card>

  <Card title="Providers" icon="server" href="/concepts/providers">
    How the unified `InferEvent` stream maps to {STATUS.providers} providers.
  </Card>

  <Card title="Bindings" icon="link" href="/concepts/bindings">
    Reading `tasks.<id>.events` from other tasks.
  </Card>

  <Card title="Architecture" icon="layer-group" href="/concepts/architecture">
    L0.5 kernel traits the observability channels live in.
  </Card>
</CardGroup>
