> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instructorphp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 03 runtime wiring

# Runtime Wiring

In normal application code, telemetry is usually driven by runtime events rather
than manual `openRoot()` calls.

The pattern is:

1. create one shared event dispatcher
2. create one shared `Telemetry` instance
3. create the projectors for the runtimes you use
4. attach them through `RuntimeEventBridge`
5. build your runtime objects with the same event dispatcher

## Minimal Wiring Example

```php theme={null}
use Cognesy\Agents\Telemetry\AgentsTelemetryProjector;
use Cognesy\Events\Dispatchers\EventDispatcher;
use Cognesy\Http\Telemetry\HttpClientTelemetryProjector;
use Cognesy\Polyglot\Telemetry\PolyglotTelemetryProjector;
use Cognesy\Telemetry\Application\Projector\CompositeTelemetryProjector;
use Cognesy\Telemetry\Application\Projector\RuntimeEventBridge;

$events = new EventDispatcher('app');

(new RuntimeEventBridge(new CompositeTelemetryProjector([
    new AgentsTelemetryProjector($telemetry),
    new PolyglotTelemetryProjector($telemetry),
    new HttpClientTelemetryProjector($telemetry),
])))->attachTo($events);
// @doctest id="d187"
```

Pass `$events` into the runtime objects that should emit telemetry.

## Which Projectors To Add

Add only the projectors for the packages you actually use:

* agents: `AgentsTelemetryProjector`
* agent control: `AgentCtrlTelemetryProjector`
* instructor: `InstructorTelemetryProjector`
* polyglot: `PolyglotTelemetryProjector`
* http client: `HttpClientTelemetryProjector`

If a package has no matching projector attached, its events will not be turned
into telemetry.

## Metric Catalog

Each projector also emits metrics through `Telemetry::metric()`, using the
canonical types from `Cognesy\Metrics\Data\*` (`Counter`, `Gauge`, `Histogram`,
`Timer`). This is what the two runtime projectors emit today.

### `PolyglotTelemetryProjector`

| Metric                                 | Type      | Emitted on                                                                                            | Tags                                                                                                                               |
| -------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `inference.client.token.usage.input`   | Histogram | `InferenceUsageReported` (skipped when the token count is `null`)                                     | `inference.execution.id`, `inference.response.model`, `inference.usage.final`                                                      |
| `inference.client.token.usage.output`  | Histogram | same                                                                                                  | same                                                                                                                               |
| `inference.client.token.usage.total`   | Histogram | same, and `EmbeddingsResponseReceived`                                                                | same / embeddings attribute bag                                                                                                    |
| `inference.client.operation.count`     | Counter   | `InferenceCompleted` -> `inference.outcome=success`; `InferenceFailed` -> `inference.outcome=failure` | `inference.outcome`, `inference.finish_reason` (success), `http.response.status_code` (failure)                                    |
| `inference.client.operation.duration`  | Timer     | `InferenceCompleted`, when a non-negative duration is available                                       | same as the success counter                                                                                                        |
| `inference.client.attempt.count`       | Counter   | `InferenceAttemptSucceeded` -> success; `InferenceAttemptFailed` -> failure                           | `inference.outcome`, `inference.finish_reason` (success), `error.type` + `http.response.status_code` + `inference.retry` (failure) |
| `inference.client.attempt.duration`    | Timer     | either attempt event, when a non-negative duration is available                                       | same as its counter                                                                                                                |
| `inference.embeddings.operation.count` | Counter   | `EmbeddingsResponseReceived` -> success; `EmbeddingsFailed` -> failure                                | `inference.outcome`, `inference.response.model` (success), `http.response.status_code` (failure)                                   |

`PolyglotTelemetryProjector` emits no Gauge. There is no point-in-time resource
level on the inference path — per-call sizes are distributions, not levels — and
forcing one would misrepresent the data. Gauges belong where a loop actually
carries state, which is the agents projector below.

### `AgentsTelemetryProjector`

| Metric                               | Type      | Emitted on                                                                                              | Tags                                                                                               |
| ------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `inference.client.token.usage.total` | Histogram | `TokenUsageReported`                                                                                    | `agent.id`, `inference.execution.id`, `agent.operation`, `agent.parent_id` (+ envelope attributes) |
| `agent.context.message_count`        | Gauge     | `AgentStepStarted`                                                                                      | `agent.is_subagent`                                                                                |
| `agent.step.count`                   | Counter   | `AgentStepCompleted`                                                                                    | `agent.has_tool_calls`, `inference.finish_reason`, `agent.is_subagent`                             |
| `agent.step.duration`                | Timer     | `AgentStepCompleted`, when a non-negative duration is available                                         | same                                                                                               |
| `agent.execution.count`              | Counter   | `AgentExecutionCompleted` -> `agent.outcome=success`; `AgentExecutionFailed` -> `agent.outcome=failure` | `agent.status`, `agent.is_subagent`, `agent.outcome`, `error.type` (failure only)                  |
| `agent.execution.steps`              | Histogram | same two events                                                                                         | same                                                                                               |
| `agent.tool_call.count`              | Counter   | `ToolCallCompleted` -> `agent.outcome=success\|error`; `ToolCallBlocked` -> `agent.outcome=blocked`     | `agent.tool`, `agent.outcome`                                                                      |
| `agent.tool_call.duration`           | Timer     | `ToolCallCompleted`, when a non-negative duration is available                                          | `agent.tool`, `agent.outcome`                                                                      |
| `agent.subagent.depth`               | Gauge     | `SubagentSpawning`                                                                                      | `agent.subagent`                                                                                   |
| `agent.subagent.count`               | Counter   | `SubagentCompleted`                                                                                     | `agent.subagent`, `agent.subagent.status`                                                          |

### Type Policy

* event and failure totals -> `Counter`
* durations -> `Timer`
* point-in-time state (current context size, current subagent nesting depth) ->
  `Gauge`
* distributions (token counts, steps per run) -> `Histogram`

`Timer::create()` throws on a negative duration, so every duration Timer above
is guarded: a projector must not let an observability path take down the call
it is observing just because a clock went backwards. Each Timer is skipped, not
clamped, when the duration is missing or negative.

### Tag Discipline

Metric tags are aggregation dimensions, not span attributes, so every tag value
must be low-cardinality: tool names, subagent names, statuses, outcomes,
booleans. Per-run identifiers (`agent.id`, execution ids) belong on spans, not
tags — one tag value per run means one time series per run in the metrics
backend.

`inference.client.token.usage.*` is the single deliberate exception, and it is
exempt from the rule as a whole rather than for one tag: it is a
correlation-carrying metric, not an aggregation-friendly one. It carries
`inference.execution.id` because `LangfusePayloadMapper::matchesMetric()`
correlates a metric back to its span by that id, which already makes it one
series per run — so the `agent.id` the agents projector also puts on it costs
nothing further and stays useful for grouping token spend by agent. Every other
metric above carries no id tag at all, so it never matches an observation and
never reaches `attributesForMetric()` — which returns `[]` for unknown names
anyway.

`Cognesy\Telemetry\Domain\Metric\MetricNames` holds the token-usage metric names
and the `inference.execution.id` / `inference.usage.final` tag keys shared by
`PolyglotTelemetryProjector`, `AgentsTelemetryProjector` and
`LangfusePayloadMapper`, so the three cannot drift apart. Metric names used by a
single producer (`agent.*`, `inference.embeddings.*`) stay private to that
producer and are not in `MetricNames`.

## Practical Examples

The examples directory has working end-to-end setups:

* `examples/D05_AgentTroubleshooting/TelemetryLangfuse/run.php`
* `examples/D05_AgentTroubleshooting/TelemetryLogfire/run.php`
* `examples/D05_AgentTroubleshooting/SubagentTelemetryLangfuse/run.php`
* `examples/D05_AgentTroubleshooting/SubagentTelemetryLogfire/run.php`

These examples keep normal console output and add telemetry export at the same
time.
