# ObservMe Telemetry Semantic Conventions

## 1. Naming Strategy

ObservMe does not depend on OpenInference. It uses official OpenTelemetry GenAI semantic-convention attributes where they apply and its own namespaces for Pi-specific concepts.

Namespaces:

```text
observme.*    Extension/runtime attributes
pi.*          Pi domain attributes, including `pi.workflow.*` and `pi.agent.*` for Pi workflow/agent/subagent lineage
gen_ai.*      Official OpenTelemetry GenAI client/agent attributes and metrics where applicable
llm.*         Legacy ObservMe aliases only; do not use as the sole representation for new telemetry
```

Do not introduce a bare `agent.*` namespace for new telemetry. Use `pi.agent.*` for Pi runtime lineage and `gen_ai.agent.*` only where OpenTelemetry's GenAI agent attributes fit.

ObservMe-owned metric names use snake_case and are prefixed with `observme_`. The current runtime registers `gen_ai.client.token.usage` and `gen_ai.client.operation.duration` instruments for compatibility but does not record measurements to them. ObservMe's live `observme_*` metrics remain the stable product contract.

Span names use dotted lowercase operation names:

```text
pi.session
pi.agent.run
pi.agent.spawn
pi.agent.wait
pi.agent.join
pi.turn
pi.llm.request
pi.tool.call
pi.bash.execution
pi.compaction
pi.branch
pi.model.change
pi.thinking.change
```

`pi.model.change` and `pi.thinking.change` are reserved names with no live standalone span. Current model/thinking handlers add events to `pi.session` and emit structured logs.

## 2. Resource Attributes

Resource attributes are set once at SDK initialization. Current runtime values come from `resource.attributes`, generated telemetry-instance identity, and generated/validated agent lineage.

```text
service.name                       = observme-pi-extension
service.namespace                  = optional configured/Collector-added value; not added by the extension default
service.version                    = reserved/configurable; not added by the extension default
service.instance.id                = <uuid per ObservMe telemetry session>
telemetry.sdk.name                 = normally added by the OpenTelemetry SDK
observme.version                   = reserved/configurable; not added by the extension default
observme.instance.id               = <same generated uuid per ObservMe telemetry session>
pi.agent.id                        = <generated id per logical agent runtime>
pi.agent.parent_id                 = <validated parent agent id>, optional high-cardinality resource attribute
pi.agent.root_id                   = <root agent id>
pi.agent.display_name              = optional validated v2 child presentation label
pi.agent.role                      = root|lead|helper|worker|validator|subagent|orchestrator|reviewer|unknown
pi.agent.depth                     = 0 for root, 1+ for subagents
pi.agent.capability                = optional validated launcher-defined machine value
pi.cwd.hash                        = reserved/configurable resource key; live cwd hash is on pi.session.cwd_hash
pi.cwd.basename                    = reserved/configurable resource key
deployment.environment.name        = development|test|staging|production|custom
observme.environment               = optional compatibility alias; not added by the extension default
observme.tenant.id                 = optional tenant/routing id
pi.workflow.id                     = generated workflow/root execution id; high-cardinality resource/span/log attribute
pi.workflow.root_agent_id          = root agent id for the workflow tree
pi.user.hash                       = reserved/configurable; not added by the extension default
pi.project.name                    = optional safe project name
```

Never set raw `cwd` by default because paths often include usernames, project codenames, or customer names.

## 3. Common Span Attributes

ObservMe spans include the following fields when the relevant lifecycle state is available. The `pi.session` span carries the full common capture/convention flags; child spans add the applicable correlation fields rather than blindly repeating every key:

```text
pi.session.id
pi.workflow.id
pi.workflow.root_agent_id
pi.agent.id
pi.agent.parent_id                  # if this runtime is a subagent
pi.agent.root_id
pi.agent.display_name               # if this runtime has a v2 child descriptor
pi.agent.role
pi.agent.capability                 # if this runtime has a validated capability
pi.agent.run.id                     # if inside an agent_start/agent_end lifecycle
pi.entry.id                         # if linked to an entry
pi.entry.parent_id                  # if linked to an entry
pi.entry.type                       # if linked to an entry
observme.capture.prompts            # boolean
observme.capture.responses          # boolean
observme.capture.tool_arguments     # boolean
observme.redaction.enabled          # boolean
observme.semconv.version            # ObservMe convention version
observme.replayed                   # true for explicit replay/backfill telemetry
observme.evicted                    # true when span was closed by bounded-registry eviction
observme.truncated                  # true when content was truncated before export
observme.original_length            # original character length when truncation occurred
```

## 4. Session Span

Span name:

```text
pi.session
```

Attributes:

```text
pi.session.id
pi.session.name
pi.session.cwd_hash
pi.session.parent_session_hash
pi.session.persisted
pi.session.file_hash
pi.session.version
pi.model.provider.current
pi.model.id.current
pi.thinking.level.current
```

Live events/logs:

```text
session.started
session.named
session.shutdown
```

`session_info_changed` updates active metadata and emits `session.named` on the session span and log stream. `session.error` is reserved and has no current live emission point.

Shutdown event/log attributes:

```text
pi.workflow.duration_ms
pi.workflow.status                  # ok|error|cancelled|unknown
```

## 5. Workflow, Agent, and Subagent Spans

Additional lineage attributes where known:

```text
pi.agent.display_name               # optional validated presentation label; never a correlation key
pi.agent.capability                 # optional validated launcher-defined machine value
pi.agent.orphaned                   # true when parent lineage is incomplete
```

For integration API v2 children, `pi.agent.display_name`, `pi.agent.role`, and `pi.agent.capability` are emitted consistently as the child's own resource, span, and log attributes. Parent spawn telemetry uses the child-specific names below instead, so a child's identity cannot be mistaken for the parent's identity. Display names are bounded to 128 Unicode code points and reject controls or invalid Unicode; capabilities are bounded to 64 characters and use the documented ASCII token grammar. The validated values are emitted without inferring, trimming, normalizing, or widening the launcher-supplied role.

### 5.1 Agent Run Span

Pi emits `agent_start` and `agent_end` once per user prompt lifecycle. Represent that lifecycle with a child span under `pi.session`.

Span name:

```text
pi.agent.run
```

Attributes:

```text
pi.agent.id
pi.agent.parent_id                  # optional
pi.agent.root_id
pi.agent.role                       # root|lead|helper|worker|validator|subagent|orchestrator|reviewer|unknown
pi.agent.depth
pi.agent.run.id
pi.agent.run.index
pi.agent.run.source                 # Pi-reported source passed through when present; unknown otherwise
pi.agent.run.outcome                # ok|error|cancelled|unknown
pi.agent.run.prompt.hash
pi.agent.run.prompt.length
```

Official GenAI attributes may be mirrored when they describe the Pi agent rather than a provider-side assistant:

```text
gen_ai.agent.id                     # same as pi.agent.id, span/log attribute only
gen_ai.agent.name                   # reserved; not currently emitted
gen_ai.agent.version                # reserved; not currently emitted
```

### 5.2 Subagent Spawn Span

When a tool or extension launches another Pi agent, emit a spawn span around the parent operation.

Span name:

```text
pi.agent.spawn
```

Attributes:

```text
pi.agent.spawn.id
pi.agent.spawn.type                 # tool|command|extension|unknown
pi.agent.spawn.reason               # delegated_task|parallel_search|review|tool_wrapper|unknown
pi.agent.spawn.outcome              # completed|failed|cancelled; set on terminal transition
pi.agent.spawn.tool_call_id          # if spawned from a tool call
pi.agent.spawn.command.hash          # if spawned by a command; never raw by default
pi.agent.child.id                    # technical child correlation id, if known after spawn
pi.agent.child.display_name          # validated v2 child presentation label; never the parent name
pi.agent.child.role                  # exact v2 child role: lead|helper|worker|validator
pi.agent.child.capability            # validated v2 launcher-defined machine value
pi.agent.child.count                 # current/observed child count for the parent operation
pi.agent.children.active             # active children at observation time
pi.agent.parent_id                   # current pi.agent.id
pi.agent.root_id
pi.workflow.id
pi.workflow.root_agent_id
pi.agent.depth
pi.session.id
pi.agent.spawn.trace_context_propagated
```

The child-specific descriptor attributes are present on the parent spawn start and terminal span events/logs, including launcher failure, completion, and cancellation. Launcher success and terminal child completion are distinct lifecycle transitions: capability-aware launchers call `completeSubagentLaunch()` immediately after obtaining a usable transport handle, while `completeSubagent()` reports the later child outcome. API v1 starts omit them. If W3C trace context is propagated to the child, the child `pi.session` span should continue the trace. If not, link the child trace back using span links/log attributes and the lineage fields above.

### 5.3 Agent Wait and Join Spans

When a parent agent waits for a child or collects child results, emit wait/join spans or span events so the multi-agent critical path is visible.

Span names:

```text
pi.agent.wait
pi.agent.join
```

Attributes:

```text
pi.workflow.id
pi.agent.id
pi.agent.child.id                  # high-cardinality span/log attribute only
pi.agent.spawn.id                  # high-cardinality span/log attribute only
pi.agent.wait.reason               # dependency|rate_limit|child_running|unknown
pi.agent.join.status               # waiting|completed|failed|cancelled|timeout|unknown
pi.agent.child.status              # starting|active|completed|failed|cancelled|orphaned
pi.agent.failure.propagated         # true if child failure failed the parent operation
pi.agent.children.active
pi.agent.child.count
```

## 6. Turn Span

Span name:

```text
pi.turn
```

Attributes:

```text
pi.turn.id
pi.turn.index
pi.turn.outcome                     # ok|error|cancelled|unknown
pi.turn.branch_path_hash
pi.turn.user_message.hash
pi.turn.user_message.length
pi.turn.user_message.image_count
pi.model.provider.current
pi.model.id.current
```

Status:

- OK if completed without error
- ERROR if LLM/tool fatal error ends the turn
- UNSET if interrupted or unknown

## 7. LLM / GenAI Request Span

Span name:

```text
pi.llm.request
```

For backend interoperability, also follow the OpenTelemetry GenAI client-span guidance where practical: the provider call span may be named `{gen_ai.operation.name} {gen_ai.request.model}` (for example `chat claude-sonnet-4-5`) or kept as `pi.llm.request` with the official `gen_ai.*` attributes below. If both an internal Pi wrapper span and a provider client span are emitted, make the provider span a child of the turn span and avoid double-counting metrics.

Required or recommended official GenAI attributes:

```text
gen_ai.operation.name                         # chat|generate_content|text_completion
gen_ai.provider.name
gen_ai.request.model
gen_ai.response.model                         # if provider reports a different response model
gen_ai.response.id                            # if available; high-cardinality span/log attribute only
gen_ai.response.finish_reasons                # map Pi stopReason to array where possible
gen_ai.request.temperature
gen_ai.request.max_tokens
gen_ai.usage.input_tokens
gen_ai.usage.output_tokens
gen_ai.usage.cache_read.input_tokens
gen_ai.usage.cache_creation.input_tokens
gen_ai.usage.reasoning.output_tokens          # if provider reports reasoning/thinking tokens
gen_ai.conversation.id                        # pi.session.id when safe to expose in spans/logs
error.type                                    # error class when the operation fails
http.response.status_code                     # provider HTTP status when the provider response reports one
```

ObservMe/Pi-specific LLM attributes:

```text
pi.llm.api
pi.llm.request.thinking_level
pi.llm.request.message_count
pi.llm.request.tool_schema_count
pi.llm.request.input_chars
pi.llm.request.id                            # in-process provider request correlation
pi.llm.stop_reason
pi.llm.error_message_hash
pi.llm.usage.total_tokens
pi.llm.usage.cache_write_1h_tokens            # Anthropic-specific when available
pi.llm.cost.input_usd
pi.llm.cost.output_usd
pi.llm.cost.cache_read_usd
pi.llm.cost.cache_write_usd
pi.llm.cost.total_usd
pi.llm.tool_call_count                       # replayed assistant-message tool-call count
```

Optional content fields when enabled and redacted:

```text
gen_ai.input.messages                         # only if explicit content capture is enabled
gen_ai.output.messages                        # only if explicit content capture is enabled
pi.llm.prompt.redacted
pi.llm.response.redacted
pi.llm.thinking.redacted
pi.llm.content.kind                         # log attribute: prompt|response|thinking
```

When prompt, response, or thinking capture is explicitly enabled, ObservMe exports already-redacted content to both the LLM span attributes above and correlated OTEL logs. Keep capture disabled by default; for high-volume production deployments, prefer dashboards and retention policies that treat these opt-in attributes and log bodies as sensitive even after redaction.

## 8. Tool Call Span

Span name:

```text
pi.tool.call
```

Attributes:

```text
pi.tool.call.id
pi.tool.name
pi.tool.category                 # shell|filesystem|network|custom|unknown
pi.tool.arguments.hash
pi.tool.arguments.size
pi.tool.result.size
pi.tool.result.hash
pi.tool.success
pi.tool.error
pi.tool.error_class
gen_ai.tool.call.id                 # optional official alias for GenAI tool-call correlation
gen_ai.tool.name                    # optional official alias
gen_ai.tool.type                    # function|extension (datastore is reserved; not currently emitted)
```

Optional:

```text
pi.tool.arguments.redacted
pi.tool.result.redacted
gen_ai.tool.call.arguments          # only when explicit content capture is enabled and redacted
gen_ai.tool.call.result             # only when explicit content capture is enabled and redacted
```

## 9. Bash Execution Span

Span name:

```text
pi.bash.execution
```

Attributes:

```text
pi.bash.command.hash
pi.bash.command.redacted          # optional
pi.bash.exit_code
pi.bash.cancelled
pi.bash.truncated
pi.bash.output.size
pi.bash.output.hash
pi.bash.output.redacted           # optional
pi.bash.full_output_path_present
pi.bash.exclude_from_context
```

## 10. Branch Span

Span name:

```text
pi.branch
```

Attributes:

```text
pi.branch.from_id
pi.branch.to_id
pi.branch.common_ancestor_id        # if available
pi.branch.path_hash
pi.leaf.id
pi.branch.summary.hash
pi.branch.summary.length
pi.branch.from_hook
pi.branch.read_files_count
pi.branch.modified_files_count
```

## 11. Compaction Span

Span name:

```text
pi.compaction
```

Attributes:

```text
pi.compaction.first_kept_entry_id
pi.compaction.tokens_before
pi.compaction.summary.hash
pi.compaction.summary.length
pi.compaction.from_hook
pi.compaction.reason
pi.compaction.will_retry
pi.compaction.read_files_count
pi.compaction.modified_files_count
```

## 12. Metrics

### 12.1 Counters

```text
observme_sessions_started_total
observme_sessions_shutdown_total
observme_workflows_started_total
observme_workflows_completed_total
observme_workflow_errors_total
observme_agent_runs_total
observme_agent_run_errors_total
observme_subagents_spawned_total
observme_subagent_spawn_failures_total
observme_orphan_agents_total
observme_trace_context_propagation_failures_total
observme_child_agent_failures_total
observme_parent_recovered_from_child_failure_total
observme_turns_started_total
observme_turns_completed_total
observme_llm_requests_total
observme_llm_errors_total
observme_tool_calls_total
observme_tool_failures_total
observme_bash_executions_total
observme_bash_failures_total
observme_model_changes_total
observme_thinking_level_changes_total
observme_compactions_total
observme_branches_total
observme_telemetry_dropped_total
observme_export_errors_total
observme_redaction_failures_total
observme_events_observed_total
observme_handler_errors_total
```

### 12.2 Token and Cost Counters

```text
observme_llm_input_tokens_total
observme_llm_output_tokens_total
observme_llm_cache_read_tokens_total
observme_llm_cache_write_tokens_total
observme_llm_cache_write_1h_tokens_total
observme_llm_reasoning_tokens_total
observme_llm_total_tokens_total
observme_llm_cost_usd_total
```

### 12.3 Histograms

```text
observme_workflow_duration_ms
observme_agent_run_duration_ms
observme_agent_lifetime_duration_ms       # registered, not recorded by current handlers
observme_subagent_spawn_duration_ms
observme_agent_wait_duration_ms
observme_agent_join_duration_ms
observme_agent_tree_depth
observme_agent_tree_width
observme_agent_fanout_count
observme_turn_duration_ms
observme_llm_request_duration_ms
observme_tool_duration_ms
observme_bash_duration_ms
observme_compaction_tokens_before
observme_prompt_size_chars
observme_response_size_chars
observme_tool_result_size_chars
observme_handler_duration_ms
```

### 12.4 Gauges

```text
observme_active_spans
observme_active_agents                              # compatibility lifecycle claim
observme_agent_lease_expires_unixtime_seconds      # asynchronous gauge, unit s
```

#### 12.4.1 Active-agent lease and clock contract

This section is the single source of truth for active-agent lease constants and semantics. Runtime configuration, metric metadata, PromQL, dashboards, alerts, examples, and tests must reference these values rather than define different local windows.

| Contract item | Required value |
|---|---|
| Lease metric | `observme_agent_lease_expires_unixtime_seconds` |
| OTel instrument and unit | Asynchronous gauge, unit `s` |
| Observation value | `(wallClockUnixMillis + metrics.activeAgentLeaseDurationMillis) / 1000`; fractional Unix seconds are valid |
| Configuration | `metrics.activeAgentLeaseDurationMillis`; `OBSERVME_ACTIVE_AGENT_LEASE_DURATION_MS` is its environment override |
| Default lease | `60000` ms (60 seconds) |
| Inclusive duration bounds | `10000` ms through `300000` ms (10 seconds through 5 minutes) |
| Export relationship | Lease duration must be at least `(2 * metrics.exportIntervalMillis) + 5000` ms |
| Supported wall-clock skew | Producer and Prometheus clocks must differ by no more than 5 seconds |
| Future-timestamp horizon | A lease later than `time() + 305` seconds is pathological and must not count active |

`observme_active_agents` remains the clean lifecycle claim. An agent is operationally active only when its claim is positive and the matching lease is finite, later than Prometheus `time()`, and no later than the future-timestamp horizon. Missing, expired, malformed, non-finite, or pathologically future leases fail closed. Lease renewal occurs once per SDK metric collection while the session is active, including otherwise idle sessions; it does not depend on turns, tool calls, or a background heartbeat timer.

On clean shutdown, deactivate the lease before the final metric force-flush and record the matching `observme_active_agents` decrement. This makes the leased result zero after normal export/scrape propagation without waiting for expiry. If shutdown telemetry cannot run, the last exported lease is allowed to expire. With synchronized clocks, an ungraceful exit converges to zero within the configured lease plus one Prometheus scrape/evaluation interval. Across the full supported positive-skew budget, the measurable worst-case bound is the configured lease plus 5 seconds plus one scrape/evaluation interval; for the defaults and a 15-second scrape interval, that is at most 80 seconds.

The only per-runtime Prometheus join and deduplication key is the Collector-normalized `observme_instance_id` resource label. ObservMe generates its UUID independently for each telemetry session and never derives it from prompts, responses, content, paths, commands, GitHub run IDs, sessions, workflows, or logical agents. `service.instance.id` may carry the same resource value for OTel interoperability, but active-agent PromQL joins on `observme_instance_id` and deduplicates repeated exporter/scrape representations with `max by (observme_instance_id)`. The lease observation itself may carry only existing bounded metric labels; the instance identity comes from resource-to-metric conversion and must not become a dashboard variable or legend.

Lifecycle and failure behavior is fixed as follows:

| Condition | Required leased-active behavior |
|---|---|
| Clean exit | Deactivate before final flush; the active claim becomes zero without waiting for lease expiry. |
| `SIGTERM` | Use the clean path when Pi delivers shutdown; otherwise use the same expiry behavior as an abrupt exit. |
| Crash, `SIGKILL`, cancelled GitHub job, or runner/power loss | No cleanup is required for correctness; the last lease expires within the documented convergence bound. |
| Duplicate session start, reload, or resume | Deactivate and flush the replaced session before activating its replacement. If replacement cleanup is interrupted, the old instance lease expires independently. |
| Startup failure or partial runtime construction | Never leave an activated observable callback; dispose any registered callback. |
| Collector or OTLP outage | Cached positive claims stop counting when their last delivered leases expire. A still-running producer becomes active again after export recovers and a fresh lease arrives. Backend unavailability itself remains an unknown/no-query state. |
| Clock skew or clock movement | A producer clock ahead can delay expiry and one behind can fail closed early. Clocks outside the supported skew are unsupported; non-finite observations are omitted and values beyond the future horizon do not count. GitHub-hosted runners satisfy the synchronized-clock expectation; self-hosted runners must use reliable time synchronization. |

A renewable lease is a bounded liveness signal, not proof that the operating-system process is reachable at the exact query instant. `observme_active_agents` remains emitted with its existing name and clean-lifecycle behavior for compatibility, so the change is additive under the semantic-convention versioning policy and does not require a major convention-version increment. Consumers that need crash-safe live counts must migrate from raw active-claim sums to the leased definition.

### 12.5 Lifecycle recording points

- Record `observme_tool_result_size_chars` once from the finalized `tool_execution_end` result, using the same bounded tool labels as tool-call metrics.
- Increment `observme_agent_run_errors_total` only when an ended agent run is classified as failed.
- Record `observme_workflow_duration_ms` once for a root workflow at shutdown from its stored start time, with bounded `status=ok|error|cancelled|unknown` derived from observed Pi terminal payloads rather than the shutdown reason.
- Record `observme_subagent_spawn_duration_ms` exactly once from spawn start through either successful transport-handle acquisition (`completeSubagentLaunch()`) or pre-handle launcher failure (`failSubagent()`). Later wait, join, and terminal child completion transitions never re-record it. Existing v1/v2 providers remain compatible because launcher completion is an optional additive method; clients capability-detect it.
- Increment `observme_child_agent_failures_total` when an actual child completion/join reports failed status; a launcher failure before child creation is only a spawn failure.
- Increment `observme_parent_recovered_from_child_failure_total` once when a failed child is joined with `failurePropagated=false` and parent recovery is confirmed.
- Repeated child completion/join observations cannot increment the same failure or recovery transition twice. The session retains at most `limits.maxActiveSubagentSpawns` exact child accounting states and archives evicted failure/recovery transition fingerprints in a fixed-size, no-false-negative membership filter. A first observation that is absent from both stores is counted before insertion; an archived duplicate is suppressed, while a later first recovery for an archived failure can still be counted separately. The archive never clears bits during the session, so an evicted transition cannot be recounted. Fixed-space fingerprint collisions can conservatively suppress a novel transition after accounting pressure, but cannot cause duplicate overcounting.

### 12.6 Export Health self-observability contract

The `ObservMe Export Health` dashboard is a self-observability contract for the ObservMe runtime, not a replacement for `/obs status` or `/obs health`. A healthy local session should still produce positive liveness signals while failure-only signals remain at zero or no matching log rows.

Dashboard-driving signals:

```text
observme_events_observed_total              # counter; one handled Pi event observed by a live telemetry session
observme_handler_duration_ms                # histogram; handler lifecycle latency
observme_handler_errors_total               # counter; safe-handler failures
observme_active_spans                       # gauge; active SDK spans by bounded operation
observme_telemetry_dropped_total            # counter; local queue/registry drops by bounded reason
observme_export_errors_total                # counter; exporter failures by bounded reason/error class
observme_redaction_failures_total           # counter; dropped fields caused by redaction exceptions
```

Export Health Loki events:

```text
telemetry.dropped                           # emitted with telemetry-drop counter updates
redaction.failed                            # emitted with redaction-failure counter updates
export.failed                               # emitted with export-error counter updates
trace_context.propagation_failed            # emitted when subagent trace propagation fails
handler.failed                              # emitted when safe handler isolation catches an exception
```

Healthy-state semantics:

- `observme_events_observed_total` is the primary liveness denominator for the dashboard and Observability Export SLO.
- Failure counters should be queryable as zero when no matching failures happened in the selected range.
- Failure log tables can be empty in a healthy range; empty `telemetry.dropped`, `redaction.failed`, `export.failed`, and `trace_context.propagation_failed` tables mean no matching failures were observed, not that ingestion is broken.
- Collector/export health is considered healthy when events are observed and local drop/export-error rates remain zero.

Metric labels for these signals are restricted to the low-cardinality allowlist below. In practice, Export Health metrics may use only `operation`, `reason`, `error_class`, and `status` as needed. High-cardinality identifiers such as session, workflow, agent, trace, span, entry, prompt, raw path, raw command, and raw error-message values must stay out of Prometheus labels.

This telemetry contract must not require changing project trust behavior, `/obs status`, `/obs health`, the configured local OTLP endpoint, Grafana authentication/profile, or intentionally enabled local debug capture settings.

### 12.7 Reserved official GenAI metrics

The runtime registers these histogram instruments but current handlers never call `record()` on them:

```text
gen_ai.client.token.usage
gen_ai.client.operation.duration
```

Use the live `observme_llm_*` counters and `observme_llm_request_duration_ms` histogram. Do not build dashboards or alerts that assume the reserved GenAI instruments contain data.

## 13. Metric Labels

Allowed low-cardinality labels:

```text
provider
model
tool_name
tool_category
environment
operation
status
error_class
reason                                       # bounded enum only, e.g. span_registry_full|export_timeout
agent_role                                  # root|lead|helper|worker|validator|subagent|orchestrator|reviewer|unknown
agent_capability                            # reserved for a separate bounded allowlist contract; not currently emitted
subagent_depth                              # bounded bucket or small integer
spawn_type                                  # tool|command|extension|unknown
spawn_reason                                # delegated_task|parallel_search|review|tool_wrapper|unknown
pi_version                                  # reserved; not currently emitted
observme_version                            # reserved; not currently emitted
token_type                                    # reserved Prometheus-normalized alias for gen_ai.token.type; not currently emitted
```

Forbidden high-cardinality labels:

```text
session_id
workflow_id
workflow_root_agent_id
agent_id
parent_agent_id
child_agent_id
agent_run_id
spawn_id
spawn_tool_call_id
trace_id
span_id
entry_id
tool_call_id
raw_command
raw_prompt
raw_path
raw_error_message
```

High-cardinality values belong in span attributes or logs, not metrics. If a backend/exporter converts resource attributes into metric labels, drop `pi.workflow.*`, `pi.agent.*`, `pi.session.id`, trace IDs, span IDs, and spawn IDs from the metrics pipeline first. Generated `service.instance.id` / `observme.instance.id` may remain as backend labels when needed to keep concurrent metric streams distinct; dashboard PromQL must aggregate over them instead of grouping by them.

## 14. Logs

Log body should be a short event summary. Detailed data goes into structured OTEL log attributes.

Common log attributes:

```text
event.name
event.category
pi.session.id
pi.workflow.id
pi.workflow.root_agent_id
pi.agent.id
pi.agent.parent_id
pi.agent.root_id
pi.agent.display_name                       # optional validated v2 child identity; never a metric label
pi.agent.role
pi.agent.capability                         # optional validated child identity; never a metric label
pi.agent.run.id
pi.turn.id
trace_id
span_id
severity
error.type
handler                             # failed handler's Pi event name on handler.failed logs
observme.config.source
observme.config.rejection.issue_codes
observme.config.rejection.issue_count
observme.config.rejection.rejected_sources
observme.config.safe_fallback_applied
```

Configuration rejection diagnostics keep the bounded accepted effective source separate from the rejected source list and the whole-config safe-fallback flag. A rejected layer is ignored without claiming safe defaults were applied when accepted layers remain valid; `observme.config.safe_fallback_applied` is true only when merged-config validation replaced the merged result. Diagnostics never include rejected values, raw validation messages, paths, headers, environment values, or custom regular expressions.

Role values remain explicit across contract generations: `root` is the ObservMe topology root; integration API v2 children use exactly `lead`, `helper`, `worker`, or `validator`; API v1 children use `subagent`; and `orchestrator`, `reviewer`, and `unknown` remain distinguishable historical values. Role is descriptive telemetry and grants no authority. `pi.agent.display_name`, `pi.agent.capability`, and their parent-side `pi.agent.child.*` forms are span/log/resource attributes only and must not be promoted to metric labels.

Operational `tool.call.completed` and `tool.call.failed` logs include every available common session/workflow/agent/run/turn correlation field plus `pi.tool.call.id`, `pi.tool.name`, `pi.tool.category`, matching `trace_id`/`span_id`, success, and bounded `error.type` for failures. These operational logs never copy raw tool arguments, results, prompts, commands, paths, or error messages. When `capture.toolResults` is explicitly enabled and the capture policy succeeds, failed-tool output is emitted separately as the `tool.error.captured` log body with `event.category="tool_content"`; redaction remains enabled by default and capture failures emit no content.

Explicit replay logs may also include:

```text
pi.message.role
pi.message.content_length
```

Event names:

```text
config.rejected
session.started
session.named
session.shutdown
session.error
session.duplicate_start
workflow.started
workflow.completed
workflow.failed
workflow.cancelled
workflow.unknown
agent.run.started
agent.run.completed
agent.run.failed
agent.run.cancelled
agent.run.unknown
agent.spawn.started
agent.spawn.completed
agent.spawn.failed
agent.spawn.cancelled
agent.wait.started
agent.wait.completed
agent.join.started
agent.join.completed
agent.orphaned
trace_context.propagation_failed
turn.started
turn.completed
turn.failed
turn.cancelled
turn.unknown
llm.request.started
llm.request.completed
llm.request.failed
llm.prompt.captured
llm.response.captured
llm.thinking.captured
message.replayed
tool.call.started
tool.call.completed
tool.call.failed
tool.error.captured
bash.completed
model.changed
thinking.changed
branch.created
compaction.created
redaction.failed
export.failed
handler.failed
telemetry.dropped
```

`session.error` remains in the public name registry but has no current live emission point. Content events are opt-in in live telemetry; explicit backfill can reuse lifecycle/content event names for replayed session entries and marks every replayed record with `observme.replayed=true`.

## 15. Exemplars

Metrics that correspond to traces should include exemplars where supported:

- `observme_workflow_duration_ms`
- `observme_agent_run_duration_ms`
- `observme_agent_wait_duration_ms`
- `observme_agent_join_duration_ms`
- `observme_llm_request_duration_ms`
- `observme_tool_duration_ms`
- `observme_turn_duration_ms`
- `observme_llm_cost_usd_total`

For Loki OTLP ingestion, remember that attribute names containing dots are normalized with underscores in Loki queries (for example `event.name` becomes `event_name`, and `pi.session.id` becomes `pi_session_id`).

## 16. Versioning

Semantic conventions are versioned independently:

```text
observme.semconv.version = 1.0.0
```

Breaking changes require major version increments.
