Telemetry catalog
Every event type published by quorum-rs::telemetry.
Subject layout
telemetry.agent.{agent_id}.{event_type}
The agent_id is bound by the agent's telemetry JWT, which grants publish permission only for telemetry.agent.{agent_id}.>.
Multi-endpoint deployments. Agents publish to one or more destinations in parallel via [TelemetryEmitterMux]. Each endpoint owns its own NATS connection, credentials, and subject prefix. mux.emit(&event) fans out to every endpoint; mux.emit_for("name", &event) targets one. Endpoint names must be unique within the list — TelemetryEmitterMux::new returns TelemetryMuxError::DuplicateNames on collision.
Per-endpoint subject_prefix defaults to telemetry.agent when omitted; setting it explicitly takes precedence (e.g. tenant.x.telemetry for tenanted deployments). The telemetry JWT is bound to the prefix the agent uses, so any custom prefix must be reflected in the JWT permissions in lockstep.
Config schema
telemetry:
enabled: true
endpoints:
- name: service # service operator's NATS
nats_url: nats://orchestrator.example.com:4222
creds: /etc/nsed/agent-service.creds
# subject_prefix omitted → defaults to telemetry.agent
- name: own # agent operator's own dashboard
nats_url: nats://my-grafana-stack.local:4222
creds: /etc/nsed/agent-own.creds
subject_prefix: tenant.x.telemetry # explicit override
An empty endpoints list (or block omitted) with enabled: true is allowed and results in no telemetry emitted — the mux falls through silently. Operators relying on telemetry should declare at least one endpoint.
Overlay merges replace the list wholesale — env-specific overlays must restate every endpoint, not just the additions.
Common fields
Every agent-side event carries these fields:
| Field | Type | Description |
|---|---|---|
agent_id |
string | Configured agent name |
job_id |
string | Deliberation identifier |
round |
int | Round number (1-indexed) |
phase |
"propose" | "evaluate" | "consensus_check" |
Current phase |
ts |
int | Unix milliseconds at emission time |
trace_id |
string | 32-char hex (128-bit) for cross-process correlation |
Every event also carries a serde "type" discriminant (e.g. "type": "task_completed").
Events
llm_request_start
Fired when the agent dispatches a call to its LLM provider.
| Field | Type | Description |
|---|---|---|
request_id |
string | Correlates with the terminal event |
model |
string | Configured model id |
provider_id |
string | e.g. openrouter, together_ai |
attempt |
int | 1 for first try; 2+ on retries |
estimated_input_tokens |
int | Heuristic, for planning |
context_utilization_pct |
float | 100 × estimated_input_tokens / context_window. 0.0 when the agent doesn't expose context_window |
recent_tool_output_bytes |
int | Running tool_call_executed.output_bytes total for this task so far. Pairs with the per-call values to attribute context bloat |
Not sent: message text, system prompt, tool schemas.
llm_request_complete
Fired when the LLM response finishes.
| Field | Type | Description |
|---|---|---|
request_id |
string | Matches the prior llm_request_start |
latency_ms |
int | Wall-clock request duration |
ttft_ms |
int | null | Time to first streamed token (null for non-streaming providers) |
generation_ms |
int | null | First-token to finish (null for non-streaming providers) |
input_tokens |
int | From provider usage stats |
output_tokens |
int | From provider usage stats |
reasoning_tokens |
int | For reasoning-model families |
cached_tokens |
int | Prompt-cache hits |
cache_write_tokens |
int | null | Prompt tokens written into the provider's cache. null when the backend does not report cache accounting |
cost_usd |
float | Our own estimate, priced from the model's configured rate against the token counts |
reported_cost_usd |
float | null | What the provider said the call cost, in USD, read from the non-standard usage.cost field. null for backends that report no cost — self-hosted and direct-to-vendor endpoints never do. Where both are present, this one is the charge and cost_usd is the guess |
finish_reason |
enum | stop / length / tool_calls / error |
provider_backend |
string | null | e.g. openrouter/deepinfra |
claim_assessments_emitted |
int | null | Evaluate-phase only |
disagreements_emitted |
int | null | Evaluate-phase only |
messages_chars |
int | Char count of the serialized input messages JSON. Tokenizer-agnostic request-size proxy; pairs with input_tokens to spot tokenization anomalies |
max_tokens_requested |
int | null | The max_tokens cap the agent asked the provider for; correlates finish_reason == length with reactive-shrink retry paths |
response_chars |
int | Char count of the generated content across choices, post-strip-reasoning. Pairs with output_tokens |
tool_calls_emitted |
int | Number of tool calls the model produced in this request. Distinct from tool_call_executed (per-call) and task_completed.tool_call_count (cumulative) |
max_tokens_shrunk_to_floor |
bool | true only when the SDK's shrink-guard had to clamp max_tokens UP to its floor (shrink_info.floor_used) — the bloat post-mortem case. A non-floor adaptive shrink leaves this false, so dashboards GROUP BY this column to find genuine truncation candidates without being polluted by routine adaptive shrinks. Joins with finish_reason == length to surface shrink-induced truncation |
available_space_at_dispatch |
int | null | Remaining context window when the call left the SDK (context_window - estimated_input_tokens). null when the provider strategy doesn't expose context_window |
Not sent: response content, tool call arguments, reasoning text.
llm_request_failed
Fired on any call that did not return a well-formed response.
| Field | Type | Description |
|---|---|---|
request_id |
string | Matches the prior llm_request_start |
error_class |
enum | transport / rate_limit / payment_required / server_error / context_overflow / parse / other |
http_status |
int | null | When applicable |
retry_after_ms |
int | null | For 429 / 503 with retry hint |
latency_ms |
int | Up to failure point |
provider_id |
string | Which provider |
provider_backend |
string | null | Sub-backend when the provider fans out |
Not sent: error body text, stack trace, raw provider response.
llm_request_stalled
Fired every 30s while an in-flight llm_request_start has no terminal event. Implemented by LlmRequestSpan: start() spawns a heartbeat task; complete() / fail() notify cancellation before emitting the terminal event so no llm_request_stalled is published after the matching terminal. First emission is at +30s (not +0s) — a request that completes inside the first interval emits zero stalled events.
| Field | Type | Description |
|---|---|---|
request_id |
string | Matches the original start |
elapsed_ms |
int | Time since dispatch |
ttft_received |
bool | false = provider never sent a first token. Currently always false — providers that surface streaming partials need to wire this. |
last_token_ms |
int | null | When the last token arrived. Today always null for the same reason. |
api_error
Fired by the agent's status-server middleware on every HTTP response with status >= 400. Mirrors the orchestrator's api_error event but omits operator_principal — the agent dashboard is loopback-bound and not authenticated as an operator.
| Field | Type | Description |
|---|---|---|
http_status |
int | HTTP status returned to the client |
error_code |
string | null | Stable error code when supplied by the handler; null for raw 4xx/5xx |
endpoint |
string | Matched route template (e.g. /api/status); falls back to raw path |
method |
string | HTTP method |
duration_ms |
int | Wall-clock from request receipt to response |
tool_call_executed
Fired when the agent runs an internal tool.
| Field | Type | Description |
|---|---|---|
tool_name |
string | Which tool |
latency_ms |
int | Execution duration |
success |
bool | Whether the tool returned vs errored |
output_bytes |
int | Length of the tool result payload. Pairs with llm_request_start.recent_tool_output_bytes for context-bloat attribution |
output_tokens_estimated |
int | ceil(output_bytes / 4), always populated by the built-in agent. The schema permits null for custom callers that genuinely have no tokenizer hint; consumers that only ingest the built-in agent's events can treat this column as guaranteed. |
truncated |
bool | true when the tool's own max_bytes cap clipped the result. Per-tool wrapper-dependent — currently always false until each tool surfaces a structured truncated marker |
paginated |
bool | true when the tool emitted a next_offset cursor. Per-tool wrapper-dependent in the same way |
Not sent: tool arguments, tool output, search queries.
deliberation_context_assembled
Fired once per propose / evaluate call, recording what prior-round context the agent assembled into its prompt and whether it wrote its scratchpad. Lets a dashboard confirm a serving agent actually inspects its own past proposals/evals across rounds — the same signals quorum smoke-test prints in-process.
| Field | Type | Description |
|---|---|---|
scratchpad_loaded_chars |
int | Scratchpad characters loaded into the prompt from the persistent store |
scratchpad_written |
bool | true when the agent wrote its scratchpad during this call |
scratchpad_written_chars |
int | Size of the scratchpad the agent produced |
prior_own_proposal_included |
bool | true when the prior round's own proposal was fed back into the prompt |
prior_score_included |
bool | true when the prior round's own score was fed back in |
prior_critiques_count |
int | Number of prior-round evaluator critiques fed into the prompt |
candidates_count |
int | Candidates fed to the evaluate phase (0 in propose) |
previous_round_matrix_included |
bool | true when the cross-agent previous-round matrix was included |
retry_loop_attempt
Fired each time the structured-output retry loop iterates.
| Field | Type | Description |
|---|---|---|
attempt |
int | Current iteration |
reason |
enum | empty_content / schema_error / truncated / hallucinated_tool |
cumulative_latency_ms |
int | All attempts so far this task |
cumulative_cost_usd |
float | Cost burned so far across retries |
cumulative_input_tokens |
int | Token-budget pressure indicator |
cumulative_output_tokens |
int | Ditto |
Not sent: the offending text, the parse error detail.
task_accepted / task_completed / task_failed
Task-level bookends.
| Field | Type | Description |
|---|---|---|
dispatch_delay_ms |
int | Orchestrator-publish to agent pickup |
task_publish_ts |
int | null | task_accepted only: Unix milliseconds the orchestrator stamped on the task envelope at publish time. null on payloads from an older orchestrator that does not stamp a publish timestamp |
job_age_at_accept_ms |
int | null | task_accepted only: agent_receive_ts − task_publish_ts. Distinguishes a fresh dispatch (sub-second) from a resurrection (minutes / hours after a container restart). null when task_publish_ts is unset |
queue_wait_ms |
int | null | task_accepted to first llm_request_start (omitted until wired — see below) |
duration_ms |
int | End-to-end wall-clock (completed/failed only) |
phase_budget_remaining_ms |
int | 0 = hit the wall |
llm_attempts |
int | null | Total LLM request count, 1 + retries (omitted until wired) |
tool_call_count |
int | null | Internal tool calls executed (omitted until wired) |
pending_publish_depth |
int | null | > 0 at submit time = NATS backpressure (omitted until wired) |
failure_class |
enum | task_failed only: llm_exhausted / tool_error / timeout / context_overflow / parse_retry_exhausted / empty_content_after_retries |
reason |
string | null | task_failed only: the head of the failure's rendered error chain, with the provider detail where one exists. Omitted when no detail was captured |
Not sent: what the agent proposed.
Population status (current implementation)
The four fields below are typed Option<u64>/Option<u32> and the
worker emits null (omitted from the JSON via
#[serde(skip_serializing_if = "Option::is_none")]) until the
wiring to populate them is in place. Operators
should treat absent fields as "not yet measured", not as
"measured zero" — those are different signals.
| Field | Status |
|---|---|
queue_wait_ms |
omitted. Needs first-llm_request_start timestamp recorded by the agent |
llm_attempts |
omitted. Needs the structured-output retry counter exposed by each NsedAgent impl |
tool_call_count |
omitted. Needs AgentResponse.tool_usage summed per-task |
pending_publish_depth |
omitted. Needs async_nats::Client introspection at submit time |
The schema slot exists today so consumers can lock onto the contract;
once population lands the values become Some(n) without a wire-shape
change.
nats_connection_state
Fired on every state transition of the agent's NATS client.
These events are process-level (no task scope), so the common
envelope's job_id, round, and phase are omitted. trace_id
keeps the catalog's 32-char lowercase-hex shape but hashes a
session-less (agent_id, uuid_v4) seed instead of the task
tuple — distinct events get distinct traces (so one agent's
state transitions don't alias under a single id), but consumers
parsing trace_id see a uniform shape across the whole catalog.
| Field | Type | Description |
|---|---|---|
state |
enum | connected / disconnected / reconnecting / closed |
reconnects_so_far |
int | Cumulative reconnect count |
pending_publish_depth |
int | null | Local publish buffer depth (omitted until wired — see below) |
buffer_bytes |
int | null | Local publish buffer size in bytes (omitted until wired — see below) |
Population status (current implementation)
pending_publish_depth and buffer_bytes are Option<u32> /
Option<u64> and the worker emits null (omitted from the JSON)
until async_nats::Client::statistics() (or equivalent
introspection) is wired. Treat
absent fields as "not yet measured", not "buffer is empty".
context_emergency_shrink
Fires once per LLM call where the SDK clamped max_tokens UP to its
floor (typically 200). Pairs with
llm_request_complete.max_tokens_shrunk_to_floor: both surface the
same shrink_info.floor_used condition. Healthy available > floor
adaptive shrinks leave both unset.
| Field | Type | Description |
|---|---|---|
available_space |
int | True remaining headroom at the moment the SDK applied the floor (saturating non-negative). Pre-clamp value, so 0 and 199 are reported as 0/199 rather than collapsing to the floor — dashboards see actual context pressure |
requested_max |
int | max_tokens the agent originally asked for |
floor_used |
int | The 200-or-similar floor the SDK rewrote max_tokens to |
estimated_input |
int | What we tried to send to the provider |
context_window |
int | Model's hard context limit |
recent_tool_outputs |
array | Top-N (default 5) {tool, bytes} contributors to bloat in this task |
Not sent: tool result content, message text, prompt fragments. Only
tool (a public name) and bytes (a count) are in
recent_tool_outputs — content is never disclosed.
The recent_tool_outputs array is currently emitted empty. The
per-task running ledger is owned by generate_structured_output and
threaded into react_loop as a &mut u64, so structured-output
schema retries inherit the prior attempt's bloat. The top-N
attribution is added in a follow-up that reuses that same per-task
ledger.
claude_subprocess_spawn / _exit / _session_lock_collision
Lifecycle events for provider_type: claude_cli. Emitted around each
run_claude_attempt. The session jsonl at
~/.claude/projects/<munged>/<session_id>.jsonl is treated as the
implicit lock — non-zero size before spawn means a prior run left
state that the next spawn will collide with.
claude_subprocess_spawn:
| Field | Type | Description |
|---|---|---|
session_id |
string | Stable UUID — namespaces claude's session jsonl at ~/.claude/projects/<munged_working_dir>/<session_id>.jsonl (/s in the working dir replaced with -) |
lock_present_at_spawn |
bool | true = previous run failed to release the lock; this spawn will collide |
claude_subprocess_exit (correlates by session_id):
| Field | Type | Description |
|---|---|---|
session_id |
string | Same UUID as the matching spawn event |
exit_code |
int | Process exit code |
wallclock_ms |
int | Time alive |
session_lock_released |
bool | true = post-exit cleanup removed the lock; false = leak that will collide with the next spawn |
claude_session_lock_collision (fired by spawn on prior-lock discovery):
| Field | Type | Description |
|---|---|---|
session_id |
string | Session that collided |
prior_lock_age_secs |
int | mtime delta on the lock file at discovery |
prior_pid |
int | null | PID parsed from the lock file when claude writes one |
Not sent: lock file contents, command line, env vars, stdout / stderr
text. Only the path-shaped session_id is emitted.
prompt_exposure_detected
Paired with the PromptExposureMiddleware guardrail. Fires when the guardrail sees dictionary hits on terminal-tool content.
Note. The event variant exists in the SDK schema for completeness, but
quorum-rsdoes not ship a default detector that emits it. Agents can attach anyOutputLeakDetectorimplementation viaProposerEvaluatorAgent::with_output_guard(...); whether this event fires depends on the chosen detector.
| Field | Type | Description |
|---|---|---|
terminal_tool |
string | Which terminal tool produced the scanned content |
blocked |
bool | true = guardrail rejected + forced retry; false = under threshold |
hit_count |
int | Total hits across all categories |
response_length_chars |
int | Length of the scanned content |
suspicion_score |
float | Log-scaled score: hit_count * log2(1 + len / unit_chars) |
xml_tag_hits |
int | Detections against XML-tag dictionary |
tool_name_hits |
int | Detections against tool-name dictionary |
instruction_hits |
int | Detections against instruction-phrase dictionary |
wrong_acronym_hits |
int | Known-wrong NSED expansions |
sample_hits |
string[] | Capped sample of dictionary-sourced hit labels |
Not sent: scanned response content, proposal text, evaluation justifications.
Invariant: hit_count == xml_tag_hits + tool_name_hits + instruction_hits + wrong_acronym_hits.
Events NOT emitted
The following are explicitly never sent. Redaction is enforced at the type layer — the struct fields do not exist on any TelemetryEvent variant:
- Prompts, system instructions, personas, thinking rules
thought_process- Proposal content, evaluation justifications, claim assessments
- User queries or follow-up nudges
- Raw LLM response bodies or tool outputs
- Token strings, API keys, or secret material
- NATS credentials or session cookies