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). |
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.Event shape
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)
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 seeinfer.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βswhen:evaluated to false (or its emptyfor_eachcollection 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 beforetask.startedof the retry).
Verb-level
Fromnika-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 }).
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 (mirrorsAuditRecord::TaintViolation).budget.warningβ nearing aBudgetDirectiveceiling.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:
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
providers.
Cross-link: OTel GenAI spec.
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.
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.
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.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: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).SpanRef { trace_id, span_id }β a copyable handle that propagates span identity acrosstokio::mpscboundaries and spawned tasks without cloning the fullSpanGuard.
#[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)
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.
Related ADRs
- ADR-021 β YAML envelope.
EventSinkemits events that correlate to tasks insidespec.tasks, tagged with the workflowβsmetadata.name. - ADR-028 β forward-compat reservation policy.
Extensionvariants inAuditRecord+StopReason::Unknown(String)inInferResponse. - ADR-035 β telemetry seams (SpanGuard parent + SpanRef).
Read next
Event source
The
nika-event crate β every kind + payload schema in source.Providers
How the unified
InferEvent stream maps to providers.Bindings
Reading
tasks.<id>.events from other tasks.Architecture
L0.5 kernel traits the observability channels live in.