From 72b7af73bd26e01abadf974929f3f17c8b99efb1 Mon Sep 17 00:00:00 2001 From: emozilla Date: Thu, 25 Jun 2026 23:39:17 -0400 Subject: [PATCH] refactor(telemetry): drop "plane" terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the telemetry tiers away from the borrowed control-plane/data-plane jargon to plain language, across code, CLI output, config, and docs: - "local plane" -> "local telemetry" - "aggregate plane" -> "aggregate metrics" - "trajectories plane" -> "trajectories" / "telemetry.trajectories" - "three planes with a hard wall" -> "three settings, isolated from each other" User-facing `hermes telemetry status` now reads "Local telemetry: on" / "Aggregate metrics: off" / "Content export: off (trajectories disabled)". The OTLP resource attribute key telemetry.plane is renamed to telemetry.scope (wire-level identifier; nothing consumes it yet). No behavior change — wording only. Status renders identically apart from the labels; tests updated to match the new strings. --- agent/insights.py | 4 +- agent/telemetry/emitter.py | 4 +- agent/telemetry/events.py | 4 +- agent/telemetry/exporter_bulk.py | 6 +- agent/telemetry/metrics.py | 2 +- agent/telemetry/otlp_exporter.py | 6 +- agent/telemetry/policy.py | 10 +-- agent/telemetry/redaction.py | 6 +- cli-config.yaml.example | 16 ++--- docs/observability/telemetry.md | 78 ++++++++++++------------ hermes_cli/config.py | 18 +++--- hermes_cli/main.py | 22 +++---- hermes_cli/subcommands/telemetry.py | 10 +-- plugins/telemetry/__init__.py | 8 +-- tests/telemetry/test_cli_telemetry.py | 10 +-- tests/telemetry/test_export_redaction.py | 2 +- tests/telemetry/test_governance.py | 2 +- 17 files changed, 105 insertions(+), 103 deletions(-) diff --git a/agent/insights.py b/agent/insights.py index 304410707ed..b9370b35cc5 100644 --- a/agent/insights.py +++ b/agent/insights.py @@ -181,7 +181,7 @@ class InsightsEngine: } # ========================================================================= - # Telemetry (observability) — from the tel_* tables (local plane) + # Telemetry (observability) — from the tel_* tables (local telemetry) # ========================================================================= def _compute_telemetry(self, cutoff: float) -> Dict[str, Any]: @@ -1067,7 +1067,7 @@ class InsightsEngine: lines.append(f" {ts['label']:<20} {ts['value']:<18} ({ts['date']}, {ts['session_id']})") lines.append("") - # Telemetry / observability (local plane) — only when data exists + # Telemetry / observability (local telemetry) — only when data exists tel = report.get("telemetry") or {} if tel: self._append_telemetry_section(lines, tel) diff --git a/agent/telemetry/emitter.py b/agent/telemetry/emitter.py index d6f3788cca0..6b628ea5626 100644 --- a/agent/telemetry/emitter.py +++ b/agent/telemetry/emitter.py @@ -1,4 +1,4 @@ -"""Local-plane telemetry emitter: fire-and-forget queue + background writer. +"""Local telemetry emitter: fire-and-forget queue + background writer. The emitter is the single seam between instrumentation (the telemetry plugin's hook callbacks) and durable storage. Its contract is the hot-path invariant: @@ -16,7 +16,7 @@ Mechanism: * The writer uses its own sqlite connection to state.db, separate from SessionDB, so telemetry writes never contend with or corrupt session writes. -Local plane only. Nothing here uploads anywhere. +Local telemetry only. Nothing here uploads anywhere. """ from __future__ import annotations diff --git a/agent/telemetry/events.py b/agent/telemetry/events.py index f084659f726..4125aaff414 100644 --- a/agent/telemetry/events.py +++ b/agent/telemetry/events.py @@ -1,4 +1,4 @@ -"""Typed local-plane telemetry events. +"""Typed local telemetry events. These dataclasses are the rows written to the local JSONL log and the ``tel_*`` SQLite tables. They record the values observed for each run — model id, provider, tool @@ -11,7 +11,7 @@ import time from dataclasses import dataclass, field, asdict from typing import Any, Dict, Optional -# ── local-plane events (real values) ──────────────────────────────────────── +# ── local telemetry events (real values) ──────────────────────────────────── def _now_ns() -> int: diff --git a/agent/telemetry/exporter_bulk.py b/agent/telemetry/exporter_bulk.py index e0dd1e26d04..32be393f723 100644 --- a/agent/telemetry/exporter_bulk.py +++ b/agent/telemetry/exporter_bulk.py @@ -3,7 +3,7 @@ Two data domains, both written to an operator-chosen destination: * Telemetry: the tel_* rows + events.jsonl (structural observability). - * Content (opt-in via the trajectories plane): sessions + messages, with every + * Content (opt-in via telemetry.trajectories): sessions + messages, with every content field (message body, reasoning, raw tool-call args) passed through the redaction pipeline (secrets always stripped; PII per content_redaction). @@ -103,10 +103,10 @@ def export( ) -> Dict[str, int]: """Write telemetry (+ optional content) to ``out``. Returns counts. - ``include_content`` is honored only when the trajectories plane is enabled in + ``include_content`` is honored only when telemetry.trajectories is enabled in ``config``; otherwise content is forced off and only structural data is written. """ - # Trajectories gate: a flag cannot override the consent plane. + # Trajectories gate: a flag cannot override the config setting. content_allowed = include_content and redaction.content_export_enabled(config) counts = {"telemetry": 0, "sessions": 0, "content_included": int(content_allowed)} diff --git a/agent/telemetry/metrics.py b/agent/telemetry/metrics.py index 389a9ad5e79..fda0b4ee413 100644 --- a/agent/telemetry/metrics.py +++ b/agent/telemetry/metrics.py @@ -51,7 +51,7 @@ def workflow_summary( *, conn: Optional[sqlite3.Connection] = None, ) -> Dict[str, Any]: - """Run-level counters + duration percentiles (local plane, exact).""" + """Run-level counters + duration percentiles (local telemetry, exact).""" with _cursor(conn, db_path) as c: where = _since_clause(since_ns) total = c.execute(f"SELECT COUNT(*) n FROM tel_runs{where}").fetchone()["n"] diff --git a/agent/telemetry/otlp_exporter.py b/agent/telemetry/otlp_exporter.py index 141c10b0164..32912b9da27 100644 --- a/agent/telemetry/otlp_exporter.py +++ b/agent/telemetry/otlp_exporter.py @@ -16,7 +16,7 @@ Notes: and is fail-isolated, so an export error cannot affect a run. Spans carry structural telemetry by default. Message content is included only when the -trajectories plane is enabled, and always passes through the export redaction pipeline. +trajectories is enabled, and always passes through the export redaction pipeline. """ from __future__ import annotations @@ -119,7 +119,7 @@ def _make_provider(config: Dict[str, Any]): sdk = _require_sdk() resource = sdk["Resource"].create({ "service.name": "hermes-agent", - "telemetry.plane": "local", # never aggregate + "telemetry.scope": "local", # never aggregate metrics }) provider = sdk["TracerProvider"](resource=resource) processor = sdk["BatchSpanProcessor"](build_exporter(config)) @@ -129,7 +129,7 @@ def _make_provider(config: Dict[str, Any]): # ── event -> span attribute mapping (real values) ─────────────────────────── def _span_attrs(ev: Dict[str, Any]) -> Dict[str, Any]: - """Span attributes for an event — the real recorded values (local plane).""" + """Span attributes for an event — the real recorded values (local telemetry).""" kind = ev.get("event") attrs: Dict[str, Any] = {"hermes.event": kind or "unknown"} keep_by_kind = { diff --git a/agent/telemetry/policy.py b/agent/telemetry/policy.py index 6090857eab5..f98797c2b4f 100644 --- a/agent/telemetry/policy.py +++ b/agent/telemetry/policy.py @@ -1,10 +1,10 @@ -"""Telemetry consent posture and the aggregate-plane gate. +"""Telemetry consent posture and the aggregate-metrics gate. Consent is a single config field, ``telemetry.consent_state``: * "unknown" — no choice recorded; never uploads (the default). - * "local" — declined the aggregate plane; local plane only. - * "aggregate" — opted in to the aggregate plane. + * "local" — declined aggregate metrics; local telemetry only. + * "aggregate" — opted in to aggregate metrics. The config file is the source of truth: set ``telemetry.consent_state`` with ``hermes config set`` (or a managed-scope pin). Callers that gate behavior read @@ -15,7 +15,7 @@ consult. ``allow_aggregate`` is the hard gate. An administrator pins ``telemetry.allow_aggregate: false`` through the managed-scope layer (``/etc/hermes/config.yaml``), which takes precedence over the user's config; when -it is false, the aggregate plane is off regardless of ``consent_state``. +it is false, aggregate metrics are off regardless of ``consent_state``. """ from __future__ import annotations @@ -49,7 +49,7 @@ def ensure_install_id(config: Dict[str, Any]) -> str: def may_upload_aggregate(config: Dict[str, Any]) -> bool: - """Whether the aggregate plane may upload — the gate a future uploader consults. + """Whether aggregate metrics may upload — the gate a future uploader consults. True only when the admin hard gate allows it AND the user has opted in via ``telemetry.consent_state``. diff --git a/agent/telemetry/redaction.py b/agent/telemetry/redaction.py index a65ea695172..01090abb22a 100644 --- a/agent/telemetry/redaction.py +++ b/agent/telemetry/redaction.py @@ -6,7 +6,7 @@ Two independent controls: disables this. Wraps ``agent/redact.py::redact_sensitive_text(force=True)``. * Whether message bodies, reasoning, and raw tool arguments are exportable at all is - governed by the trajectories plane (``telemetry.trajectories.enabled``, default + governed by the trajectories setting (``telemetry.trajectories.enabled``, default off, admin-pinnable), not by a redaction mode. With trajectories off, content is dropped. With it on, content is exportable and ``content_redaction`` (none|pii) controls how much is scrubbed; secrets are still always stripped. @@ -62,7 +62,7 @@ def redact_for_export( """Redact a single content string for export. Secrets are ALWAYS stripped. Then PII is stripped when content_mode is 'pii'. - Callers gate *whether content is exported at all* via the trajectories plane + Callers gate *whether content is exported at all* via telemetry.trajectories (see ``content_export_enabled``); this function only scrubs content that the caller has already decided to export. """ @@ -75,7 +75,7 @@ def redact_for_export( def content_export_enabled(config: Optional[Dict[str, Any]]) -> bool: - """True only when the trajectories plane is explicitly enabled. + """True only when telemetry.trajectories is explicitly enabled. This is the consent gate for exporting message bodies / reasoning / raw tool args. Default off. Admin-pinnable via managed scope (telemetry.trajectories.enabled). diff --git a/cli-config.yaml.example b/cli-config.yaml.example index 89ace2dd657..982708c3422 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -1417,13 +1417,13 @@ display: # ============================================================================= # Telemetry & Observability # ============================================================================= -# Three planes with a hard wall between them: +# Three settings, isolated from each other: # local — full-fidelity observability you own. Default ON. # aggregate — opt-in metadata to Nous (no uploader ships yet). Default OFF. # trajectories — content trajectories for training. Separate consent (later). # -# The local plane records real values (actual models, providers, tool names) — -# your own data, on your machine. The aggregate plane is opt-in and has no +# Local telemetry records real values (actual models, providers, tool names) — +# your own data, on your machine. Aggregate metrics are opt-in and have no # uploader today; if one ships it would summarize at that egress boundary. # # Enterprise locking: any telemetry.* key can be pinned by an administrator via @@ -1431,16 +1431,16 @@ display: # value. To hard-forbid egress on a locked-down deployment, pin # `telemetry.allow_aggregate: false` there. # telemetry: -# # Local plane: event log + SQLite index in state.db. Never leaves your -# # machine unless you export it or opt into the aggregate plane. +# # Local telemetry: event log + SQLite index in state.db. Never leaves your +# # machine unless you export it or opt into aggregate metrics. # local: true -# # Hard gate for the aggregate plane. When false, the aggregate plane is off +# # Hard gate for aggregate metrics. When false, aggregate metrics are off # # regardless of consent_state. Pin false via managed scope to forbid egress. # allow_aggregate: true -# # Aggregate-plane consent (the opt-in). No uploader ships yet. +# # Aggregate-metrics consent (the opt-in). No uploader ships yet. # # unknown (no choice — never uploads) | local (declined) | aggregate (opted in) # consent_state: unknown -# # Stable install id (aggregate plane only). Empty = mint on first use; +# # Stable install id (aggregate metrics only). Empty = mint on first use; # # clear it to rotate. # install_id: "" # # Local event-log retention before rotation (days). diff --git a/docs/observability/telemetry.md b/docs/observability/telemetry.md index cdffc43922b..98747bb635a 100644 --- a/docs/observability/telemetry.md +++ b/docs/observability/telemetry.md @@ -6,57 +6,57 @@ agent does — workflows, model calls, tool calls, errors — to your own machin stack. It is private by default and never sends your data to Nous unless you explicitly opt in to anonymous aggregate metrics. -This page explains the whole feature: the three planes, what's captured, the +This page explains the whole feature: the three telemetry settings, what's captured, the `hermes telemetry` commands, and how an enterprise streams or exports all of its data to its own infrastructure. > Looking for the **plugin hook contract** (how to write your own observer plugin)? > That's in [`README.md`](./README.md). This page is about the built-in telemetry -> plane and its CLI. +> system and its CLI. -## The three planes +## The three settings -Telemetry is organized into three planes with a hard wall between them: +Telemetry has three settings, isolated from each other: -| Plane | What it holds | Default | Destination | +| Setting | What it holds | Default | Destination | | --- | --- | --- | --- | | **local** | Full-fidelity observability — runs, model/tool calls (real model & provider names), durations, errors | **on** | your machine only | | **aggregate** | Opt-in metadata to Nous (no uploader ships yet) | **off** (opt-in) | Nous, only if you enable it | | **trajectories** | Full message content / reasoning / raw tool args | **off** (opt-in) | your own export destinations only | -The local plane is the one you'll use day to day — it records the real values that -happened (actual model ids, providers, tool names). The aggregate plane is the only -thing that could ever leave for Nous; it is opt-in, default-off, and has no uploader -today. The trajectories plane unlocks full-content export to *your own* destinations — -it is never wired to Nous. +Local telemetry is the one you'll use day to day — it records the real values that +happened (actual model ids, providers, tool names). Aggregate metrics are the only +thing that could ever leave for Nous; they are opt-in, default-off, and have no uploader +today. Trajectories unlock full-content export to *your own* destinations — never wired +to Nous. -## Local plane — always-on observability +## Local telemetry — always-on observability -The local plane is implemented as a bundled `telemetry` plugin that listens to Hermes +Local telemetry is implemented as a bundled `telemetry` plugin that listens to Hermes lifecycle hooks (model calls, tool calls, session start/finalize) and writes events to: - an append-only JSONL log at `~/.hermes/telemetry/events.jsonl` (the source of truth) - indexed `tel_*` tables in `state.db` (for fast queries and rollups) Writes are fire-and-forget on a background thread: telemetry can never block, slow, or -fail a model call or tool call. If the local plane is disabled (`telemetry.local: false`) +fail a model call or tool call. If local telemetry is disabled (`telemetry.local: false`) the plugin does not load at all. ### Seeing your local data ```bash hermes insights # usage report — now includes an "Observability" section -hermes telemetry status # planes, consent, export posture, local data volume +hermes telemetry status # settings, consent, export posture, local data volume ``` The `insights` Observability section shows workflow counts and success rate, duration -p50/p95, tool failure rates by category, provider/model-class mix, and cache hit rate — +p50/p95, tool failure rates by category, provider/model mix, and cache hit rate — all computed locally with exact values. ## `hermes telemetry` commands ```text -hermes telemetry status Show planes, consent state, export posture, local volume +hermes telemetry status Show settings, consent state, export posture, local volume hermes telemetry preview Show the aggregate events that would be produced (local) hermes telemetry export Export local telemetry to a file/stream or OTLP endpoint ``` @@ -65,14 +65,14 @@ Consent and the install id are plain config, not separate verbs — set them wit `hermes config set` (or a managed-scope pin): ```bash -hermes config set telemetry.consent_state aggregate # opt in to the aggregate plane -hermes config set telemetry.consent_state local # opt out (local plane stays on) +hermes config set telemetry.consent_state aggregate # opt in to aggregate metrics +hermes config set telemetry.consent_state local # opt out (local telemetry stays on) hermes config set telemetry.install_id "" # reset the install id (mints a new one) ``` -### Aggregate plane (opt-in) +### Aggregate metrics (opt-in) -The aggregate plane is **off by default** and has **no uploader today** — nothing is +Aggregate metrics are **off by default** and have **no uploader today** — nothing is sent to Nous. Consent lives in `telemetry.consent_state` (`unknown` / `local` / `aggregate`); setting it to `aggregate` records the opt-in for if/when an uploader ships. If one is built, it would summarize at that egress boundary. @@ -99,11 +99,11 @@ hermes telemetry export --out dump.json --format json --since 7 By default the export is **structural** — runs, model/tool-call metadata, session shells with message *counts* but no message bodies. -### Including content (trajectories plane) +### Including content (trajectories) -To export full message content, enable the trajectories plane. This is a deliberate, -separate consent — it's how an enterprise opts into exporting work-product content to its -own store: +To export full message content, enable trajectories. This is a deliberate, separate +consent — it's how an enterprise opts into exporting work-product content to its own +store: ```yaml # config.yaml @@ -117,8 +117,8 @@ telemetry: hermes telemetry export --out full.ndjson --include-content ``` -`--include-content` is a no-op unless the trajectories plane is enabled — the consent -plane governs, not the flag. +`--include-content` is a no-op unless trajectories are enabled — the config setting +governs, not the flag. ### Live streaming to your OpenTelemetry Collector / SIEM (OTLP) @@ -137,17 +137,19 @@ telemetry: enabled: true endpoint: "https://collector.your-corp.internal:4318/v1/traces" headers_env: # secrets by reference — env var NAMES, not values - Authorization: CORP_OTLP_TOKEN + Authorization: MY_OTLP_TOKEN_ENVVAR ``` +Set the referenced environment variable, then run the export: + ```bash -export CORP_OTLP_TOKEN="..." # the actual token lives in the environment hermes telemetry export --otlp # drain current telemetry to your collector ``` -Span attributes are structural by default. For authentication, the config holds the -*name* of an environment variable rather than the secret itself; the value is read at -export time and is never written to config or logged. +The token value lives only in the environment variable named by `headers_env`. Span +attributes are structural by default. The config holds the *name* of an environment +variable rather than the secret itself; the value is read at export time and is never +written to config or logged. ## Redaction @@ -166,8 +168,8 @@ fails closed: if the redactor can't run, the raw string is not emitted. ```yaml telemetry: - local: true # local plane (default on) - allow_aggregate: true # hard gate; pin false to forbid the aggregate plane entirely + local: true # local telemetry (default on) + allow_aggregate: true # hard gate; pin false to forbid aggregate metrics entirely consent_state: unknown # aggregate opt-in: unknown | local | aggregate install_id: "" # stable anon id; "" mints one; clear to rotate retention_days: 90 # local event-log retention @@ -189,7 +191,7 @@ layer (`/etc/hermes/config.yaml`), which wins over the user's value on a per-key There is no telemetry-specific policy block — to lock down a fleet, pin the keys you care about. Common examples: -- `telemetry.allow_aggregate: false` — the aggregate plane stays off even if +- `telemetry.allow_aggregate: false` — aggregate metrics stay off even if `consent_state` is set to `aggregate`. - `telemetry.export.otlp.endpoint` — point every install at the corporate collector. - `telemetry.trajectories.enabled` — centrally decide whether content export is allowed. @@ -202,8 +204,8 @@ secret values. ## Privacy summary - Local telemetry never leaves your machine. -- The aggregate plane (the only thing that could go to Nous) is opt-in, default-off, - and has no uploader today — nothing is sent. +- Aggregate metrics (the only thing that could go to Nous) are opt-in, default-off, + and have no uploader today — nothing is sent. - All export surfaces (file, OTLP) point at *your* destinations. -- Secrets are always redacted on export; content export is off until you enable the - trajectories plane; PII redaction is a knob. +- Secrets are always redacted on export; content export is off until you enable + trajectories; PII redaction is a knob. diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 18247ca48e4..22290ca9ef3 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -2175,7 +2175,7 @@ DEFAULT_CONFIG = { "redact_pii": False, # When True, hash user IDs and strip phone numbers from LLM context }, - # Telemetry & observability. Three planes with a hard wall between them: + # Telemetry & observability. Three settings, isolated from each other: # local — full-fidelity local observability the user owns (real # models, providers, tool names). Default ON. # aggregate — opt-in metadata to Nous (no uploader ships yet). Default OFF. @@ -2186,30 +2186,30 @@ DEFAULT_CONFIG = { # value on a per-key basis. There is no telemetry-specific policy block — pin # e.g. `telemetry.allow_aggregate: false` in managed scope to hard-forbid egress. "telemetry": { - # Local plane: event log + SQLite index in state.db. The user's own data; - # never leaves the machine unless they export it or opt into aggregate. + # Local telemetry: event log + SQLite index in state.db. The user's own data; + # never leaves the machine unless they export it or opt into aggregate metrics. "local": True, - # Hard gate for the aggregate plane. When False, the aggregate plane is off + # Hard gate for aggregate metrics. When False, aggregate metrics are off # regardless of consent_state. Intended to be pinned False by an administrator # via managed scope on locked-down or air-gapped deployments. Default True # (consent_state still governs the opt-in). "allow_aggregate": True, - # Aggregate-plane consent — the single source of truth for the opt-in: + # Aggregate-metrics consent — the single source of truth for the opt-in: # "unknown" (no choice made — never uploads), "local" (declined), # "aggregate" (opted in). Set with `hermes config set telemetry.consent_state` # or a managed-scope pin. Non-interactive installs sit at "unknown". "consent_state": "unknown", - # Stable install identifier (aggregate plane only). Empty string means "mint a - # fresh UUID on first use"; clear it to rotate. Never sent on the local plane. + # Stable install identifier (aggregate metrics only). Empty string means "mint a + # fresh UUID on first use"; clear it to rotate. Never sent by local telemetry. "install_id": "", - # Local event-log retention before rotation (days). Local plane only. + # Local event-log retention before rotation (days). Local telemetry only. "retention_days": 90, # Keep secret redaction on even at full local capture (a SIEM full of live # credentials is a bigger attack target). Admin may override via managed scope. "redact_secrets": True, # Content redaction for exports / support bundles: "none" | "pii". "content_redaction": "none", - # Trajectories plane: full message content / reasoning / raw tool args. + # Trajectories: full message content / reasoning / raw tool args. # Off by default. When enabled, full content becomes exportable to the # configured destination — always secret-redacted, and PII-redacted per # content_redaction. This is the consent gate for content export and is diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 41f6565caa0..f8bcea1a57d 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -14339,17 +14339,17 @@ def cmd_telemetry(args): if action == "status": print("Telemetry status") print("─" * 56) - print(f" Local plane: {'on' if local_enabled else 'off'} " + print(f" Local telemetry: {'on' if local_enabled else 'off'} " f"(telemetry.local)") - print(f" Aggregate plane: {'on' if aggregate_enabled else 'off'} " + print(f" Aggregate metrics: {'on' if aggregate_enabled else 'off'} " f"(opt-in; consent_state={consent_state})") if consent_state != policy.CONSENT_AGGREGATE and allow_aggregate: - print(" opt in: hermes config set telemetry.consent_state aggregate") + print(" opt in: hermes config set telemetry.consent_state aggregate") if not allow_aggregate: - print(" ⚠ allow_aggregate is false (egress hard-disabled)") - print(f" Install id: {install_id}") - print(" Upload: DISABLED — no server yet. Aggregate is computed " - "locally only.") + print(" ⚠ allow_aggregate is false (egress hard-disabled)") + print(f" Install id: {install_id}") + print(" Upload: DISABLED — no server yet. Aggregate metrics are " + "computed locally only.") print() # Export posture — where YOUR data can flow (never Nous). Values only; # lock-state is managed scope's concern, surfaced by its own tooling. @@ -14371,12 +14371,12 @@ def cmd_telemetry(args): print(f" SDK: {_sdk}") else: print(" OTLP: disabled (telemetry.export.otlp.enabled)") - # Content gate (trajectories plane) + redaction posture. + # Content gate (telemetry.trajectories) + redaction posture. if redaction.content_export_enabled(config): - print(f" Content export: on (trajectories plane) — message " + print(f" Content export: on (trajectories enabled) — message " f"content exportable") else: - print(" Content export: off (trajectories plane) — structural " + print(" Content export: off (trajectories disabled) — structural " "telemetry only") print(" Secret redaction: on (always)") print(f" PII redaction: {redaction.content_mode_for(config)}") @@ -14468,7 +14468,7 @@ def cmd_telemetry(args): content_ok = redaction.content_export_enabled(config) if want_content and not content_ok: print("⚠ --include-content ignored: telemetry.trajectories.enabled is false.") - print(" Enable the trajectories plane (admin/config) to export message content.") + print(" Enable trajectories (admin/config) to export message content.") print(" Exporting structural telemetry only.") if not getattr(args, "out", None): diff --git a/hermes_cli/subcommands/telemetry.py b/hermes_cli/subcommands/telemetry.py index 65ff344fbb9..791cdeb05b4 100644 --- a/hermes_cli/subcommands/telemetry.py +++ b/hermes_cli/subcommands/telemetry.py @@ -1,7 +1,7 @@ """``hermes telemetry`` subcommand parser. Telemetry control and inspection. ``preview`` shows the per-run summary events that -would be produced for the aggregate plane; there is no uploader, so it terminates as a +would be produced for aggregate metrics; there is no uploader, so it terminates as a local view. The handler is injected to avoid importing ``main`` (mirrors the insights subcommand). @@ -18,14 +18,14 @@ def build_telemetry_parser(subparsers, *, cmd_telemetry: Callable) -> None: "telemetry", help="Inspect local telemetry and export it", description=( - "Local-first telemetry. The local plane records observability on this " - "machine. The aggregate plane is opt-in (set telemetry.consent_state via " - "`hermes config set`); it has no uploader and is shown only via `preview`." + "Local-first telemetry. Local telemetry records observability on this " + "machine. Aggregate metrics are opt-in (set telemetry.consent_state via " + "`hermes config set`); they have no uploader and are shown only via `preview`." ), ) sub = p.add_subparsers(dest="telemetry_action") - sub.add_parser("status", help="Show telemetry planes, consent state, and local data volume") + sub.add_parser("status", help="Show telemetry settings, consent state, and local data volume") prev = sub.add_parser( "preview", diff --git a/plugins/telemetry/__init__.py b/plugins/telemetry/__init__.py index ba61d65054b..43ca8a10e7c 100644 --- a/plugins/telemetry/__init__.py +++ b/plugins/telemetry/__init__.py @@ -1,13 +1,13 @@ -"""Telemetry plugin — wires Hermes lifecycle hooks to the local-plane emitter. +"""Telemetry plugin — wires Hermes lifecycle hooks to the local telemetry emitter. This is the *only* instrumentation seam. It registers observational hooks (which core -already invokes fail-open) and translates each into a typed local-plane telemetry -event handed to ``agent.telemetry.emitter``. There are zero edits to core call sites: +already invokes fail-open) and translates each into a typed local telemetry event +handed to ``agent.telemetry.emitter``. There are zero edits to core call sites: the hooks already carry model/provider/usage/duration/tool data. Everything here is best-effort and fail-open — a raised exception in a hook callback is swallowed by core, and we additionally guard each callback so a telemetry bug can never -disturb a session. No content, no network: local plane only. +disturb a session. No content, no network: local telemetry only. Hooks consumed: on_session_start -> begin a run context (trace_id/run_id), buffer a run row diff --git a/tests/telemetry/test_cli_telemetry.py b/tests/telemetry/test_cli_telemetry.py index df597223c88..d38424df47c 100644 --- a/tests/telemetry/test_cli_telemetry.py +++ b/tests/telemetry/test_cli_telemetry.py @@ -62,7 +62,7 @@ def test_preview_shows_real_values(home, capsys): def test_status_reflects_consent_set_via_config(home, capsys): # Opting in is a plain config write now (no `enable` verb). status should - # reflect consent_state=aggregate as the aggregate plane being on. + # reflect consent_state=aggregate as aggregate metrics being on. from hermes_cli.config import load_config, save_config cfg = load_config() cfg.setdefault("telemetry", {})["consent_state"] = "aggregate" @@ -70,17 +70,17 @@ def test_status_reflects_consent_set_via_config(home, capsys): _run("status") out = capsys.readouterr().out assert "consent_state=aggregate" in out - assert "Aggregate plane: on" in out + assert "Aggregate metrics: on" in out def test_status_shows_optin_hint_when_unknown(home, capsys): _run("status") out = capsys.readouterr().out - assert "Aggregate plane: off" in out + assert "Aggregate metrics: off" in out assert "config set telemetry.consent_state aggregate" in out -def test_allow_aggregate_false_keeps_plane_off_in_status(home, capsys): +def test_allow_aggregate_false_keeps_metrics_off_in_status(home, capsys): # Even with consent opted in, a managed allow_aggregate:false wins. from hermes_cli.config import load_config, save_config cfg = load_config() @@ -90,5 +90,5 @@ def test_allow_aggregate_false_keeps_plane_off_in_status(home, capsys): save_config(cfg) _run("status") out = capsys.readouterr().out - assert "Aggregate plane: off" in out + assert "Aggregate metrics: off" in out assert "allow_aggregate is false" in out diff --git a/tests/telemetry/test_export_redaction.py b/tests/telemetry/test_export_redaction.py index 7fc84c8e205..4ae0473bfba 100644 --- a/tests/telemetry/test_export_redaction.py +++ b/tests/telemetry/test_export_redaction.py @@ -2,7 +2,7 @@ Invariants: * Secrets ALWAYS stripped, every mode, no flag disables it. - * Content gated by the trajectories plane, not a redaction mode. + * Content gated by telemetry.trajectories, not a redaction mode. * PII stripped in 'pii' mode; structure preserved (codec-aware). """ diff --git a/tests/telemetry/test_governance.py b/tests/telemetry/test_governance.py index 4c94cab5df9..97740540367 100644 --- a/tests/telemetry/test_governance.py +++ b/tests/telemetry/test_governance.py @@ -62,7 +62,7 @@ def test_export_block_reflects_trajectories_gate(home, capsys): c.setdefault("telemetry", {})["trajectories"] = {"enabled": True} save_config(c) out = _status(capsys) - assert "Content export: on (trajectories plane)" in out + assert "Content export: on (trajectories enabled)" in out def test_token_env_not_set_shows_not_set(home, capsys):