Observability¶
You cannot operate what you cannot see. ZigAI covers three layers: lifecycle hooks for application logic, structured diagnostics for logs, and OpenTelemetry for traces and metrics. All three see the same run, and none of them receives credentials.
Lifecycle hooks¶
Agent.hooks and capability hooks receive the same ordered event stream. It
covers run boundaries, model requests, tool validation and execution, output
validation, errors, retries, and stream events before and after delivery.
Hook values are borrowed and callbacks run synchronously. Copy only what you
need to retain. A hook error stops the run and is reported through
run_error.
Structured diagnostics¶
Agent.diagnostics maps lifecycle activity to structured, leveled events. It
does not depend on a logging package; provide a DiagnosticSink callback
that bridges to the backend used by the application.
.diagnostics = .{
.sink = my_diagnostic_sink,
.minimum_level = .info,
.sensitive_values = &.{api_token},
},
Prompt, output, tool arguments, and tool results are omitted unless
capture_content = true. String values are redacted before truncation, and
attribute counts, keys, and values have explicit byte limits. Sink failures
are fail-open by default; set fail_open = false when diagnostics are
required for the run to proceed. Events and attributes are borrowed for the
callback only.
OpenTelemetry¶
Configure Agent.telemetry to export OpenTelemetry-shaped spans and metrics:
.telemetry = .{
.io = init.io,
.exporter = my_otel_exporter,
.cost_estimator = my_cost_estimator,
},
Each run has one trace with child model-request, tool-validation,
tool-execution, and output-validation spans. Correlated lifecycle events
cover model, tool, retry, stream, deferred, and enqueued-message phases.
Metrics cover latency, request, validation, and tool counts, retries,
cached/reasoning/audio token usage, and optional cost. The exporter is a
small synchronous bridge to your OpenTelemetry SDK or OTLP pipeline.
Exporter.eventFn is optional for existing span-and-metric bridges.
MCP clients and servers accept McpTelemetry. It emits semantic
client/server operation spans and duration metrics, adds method, JSON-RPC,
protocol, transport, session, tool, prompt, resource, and task attributes,
and propagates W3C traceparent through params._meta.
Buffered export¶
Keep exporter work away from the agent path with a bounded buffer:
var buffered = zigai.BufferedTelemetryExporter.init(
allocator,
init.io,
my_otel_exporter,
1_024,
);
defer buffered.deinit();
buffered.overflow = .drop_oldest;
const agent = zigai.Agent{
.model = model,
.telemetry = .{
.io = init.io,
.exporter = buffered.exporter(),
},
};
// Run during graceful shutdown, after agent work has stopped.
_ = try buffered.shutdown();
The buffer deep-copies admitted signals. Choose .drop_newest,
.drop_oldest, or .reject for saturation. flush and shutdown serialize
downstream calls, and stats reports pending and dropped work. Drops also
emit the zigai.telemetry.dropped counter with a bounded reason attribute.
Prompt capture is opt-in
Prompt capture is disabled by default. Set content.prompts to .raw or
.redacted only when the destination and data policy are ready. Redacted
mode uses an application callback, and both modes enforce configured
content and attribute-size bounds before exporter callbacks run.
Pass RunOptions.telemetry_parent to continue an upstream trace. Exporter
failures are fail-open by default, so telemetry does not break an agent run.
Usage and cost¶
Every provider response has RequestUsage. Agent results have RunUsage,
which also includes request attempts, tool calls, provider latency, and
total run duration.
Cached and audio tokens are subsets of the input or output totals. Reasoning
tokens are a subset of output tokens. Provider counters without a portable
field stay available through usage.details.
Cost estimation is opt-in:
var result = try (zigai.Agent{
.model = model,
.price_table = zigai.pricing.builtin,
}).run(allocator, "Hello");
defer result.deinit();
if (result.usage.cost) |cost| {
std.debug.print("estimated cost: ${d:.6}\n", .{cost.usd()});
}
pricing.builtin is generated from a pinned
pydantic/genai-prices v2
snapshot. It covers provider aliases, model match rules, tiered prices,
modalities, and request-based charges without fetching prices at runtime.
Use pricing.builtin_version to persist its version. Unknown models and
unpriced usage produce no estimate. Pass your own PriceTable for negotiated
contracts, regional prices, or time-based discounts. Money is stored as
integer nano-USD.