# SkillPatch skill: agent-platform-alert-configuration

This skill configures best-practice alerting policies for Google Cloud Vertex AI / Agent Platform agents running on Agent Runtime. It sets up six alerting policies covering reliability (latency, fast/slow burn error rate SLOs) and quality metrics (response quality, tool use, hallucination) using dynamic PromQL baselines with 1-week lookback retention. It also handles Online Monitor provisioning, telemetry verification, and enforces safety/confirmation tiers before executing any billable actions.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/agent-platform-alert-configuration
curl -sSL https://skillpatch.dev/install_skill/agent-platform-alert-configuration | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (19)

- `SKILL.md`
- `assets/alerts_initial_duplicate.tf`
- `assets/draft_invalid_query.tf`
- `assets/mock_bursty.json`
- `assets/mock_seasonal.json`
- `assets/mock_steady.json`
- `references/has_historical_traffic_data.md`
- `references/no_historical_traffic_data.md`
- `references/promql_queries.md`
- `references/telemetry_enablement.md`
- `scripts/analyze_traffic.py`
- `scripts/analyze_traffic_test.py`
- `scripts/check_telemetry.py`
- `scripts/check_telemetry_test.py`
- `scripts/create_online_monitor.py`
- `scripts/create_online_monitor_test.py`
- `scripts/requirements.txt`
- `scripts/validate_config.py`
- `scripts/validate_config_test.py`


### `SKILL.md`

````markdown
---
name: agent-platform-alert-configuration
metadata:
  category: AiAndMachineLearning
description: >-
  Configures best-practice alerting policies for Google Cloud Vertex AI / Agent
  Platform agents on Agent Runtime. Use when analyzing, writing, or deploying
  alerting policies to monitor agent latency, error rates, and quality metrics
  (response quality, tool use, hallucination). Also use when provisioning online
  monitors for quality evaluation, or analyzing live metrics traffic footprints.
  NOTE: This skill currently only works for the Agent Runtime. Don't use for
  configuring general GCP alert policies or non-agent GCP alerting policies.
allowed-tools: terraform gcloud python
---

# Agent Platform Alert Configuration

This skill provides dynamic threshold alerting configurations for Google Cloud /
Vertex AI Reasoning Engines (Agent Platform container deployments) using
extended 1-week lookback retention baselines. Standard static thresholds (e.g.,
"latency > 2s") cause excessive alert noise for AI agents. Dynamic PromQL
baselines solve this.

## Safety & Confirmation Tiers (CRITICAL)

Before executing any commands or writing configurations on behalf of the user,
you MUST adhere to the following safety tiers based on the action requested:

1.  **Tier R: Read-only (`check_telemetry.py`)**
    *   **Rule**: No confirmation needed. You may execute these scripts
        immediately to inspect the telemetry status of the Reasoning Engine.
2.  **Tier B: Billing & Resource Creation (`create_online_monitor.py` /
    provisioning)**
    *   **Rule**: **Explicit User Confirmation Required**. These actions incur
        additional billing charges and create cloud resources. The agent MUST
        ask the user directly for approval before proceeding.

--------------------------------------------------------------------------------

## CRITICAL RULES

*   **Always configure both Reliability and Quality alerting policies** for the
    target agent (6 policies in total):
    *   **For Reliability Monitoring**: You MUST configure exactly three
        alerting policies:
        1.  **Latency** (anomaly monitoring)
        2.  **Error Rate - Fast Burn SLO** (1-Hour Window)
        3.  **Error Rate - Slow Burn SLO** (3-Day Window)
    *   **For Quality Monitoring**: You MUST configure exactly three alerting
        policies:
        1.  **Final Response Quality**
        2.  **Tool Use Quality**
        3.  **Hallucination**
*   **Online Monitor Provisioning & Cost Warning**: Quality alerting policies
    rely on metrics exported by Online Monitors. You MUST ensure the Online
    Monitor is provisioned for the agent and telemetry is enabled:
    -   [ ] **Ask for Approval**: Both Online Monitors and Telemetry incur
        separate billing charges. Before provisioning them, you MUST warn the
        user about these extra costs. If not pre-approved in the prompt, you
        MUST ask a direct question in your response requesting
        confirmation/approval to proceed (e.g., "Please confirm if you approve
        the extra billing costs for the Online Monitor and Telemetry to
        proceed.").
    -   [ ] **Verify Telemetry First**: Before generating any alerting policy
        plan or provisioning Online Monitors, you MUST always verify the
        telemetry status of the Reasoning Engine first using the
        `check_telemetry.py` script as detailed in
        [Verify Telemetry Status](#verify-telemetry-status) below.
    -   [ ] **Follow the Guide**: Follow the step-by-step instructions in the
        [Online Monitor & Telemetry Provisioning](#online-monitor--telemetry-provisioning)
        section below.
*   **Brand New Agents (No Traffic History)**: When setting up alerts for a
    brand new agent, you MUST explicitly ask the user what traffic pattern they
    expect (Steady, Seasonal, or Bursty) in your response. If immediate setup is
    requested, ask the question but proceed using the default Steady/Consistent
    (Short-Window Z-Score) pattern. Follow
    [no_historical_traffic_data.md](references/no_historical_traffic_data.md).
*   **PromQL for Reliability (No MQL or Threshold Filters)**: For the 3
    reliability metrics, you MUST use `condition_prometheus_query_language` with
    PromQL. Do **NOT** use MQL or standard `condition_threshold`.
*   **Standard Threshold Filters for Agent Quality**: For the 3 agent quality
    metrics, you MUST use standard `condition_threshold` filters matching the
    monitored resource type `aiplatform.googleapis.com/OnlineEvaluator` and
    metric type `aiplatform.googleapis.com/online_evaluator/scores`. Do **NOT**
    use PromQL.
*   **Install Terraform if Necessary**: You should use terraform to deploy and
    must install terraform if you can't find a valid install.
*   **Terraform Only**: Write the generated observability configuration ONLY as
    Terraform (`.tf`) files (e.g., `alerts.tf`, `variables.tf`).
*   **Dynamic Multi-Resource Alerting (No Single-Resource Pinning)**: You MUST
    NOT hardcode specific agent IDs or resource name filters (e.g.,
    `{reasoning_engine_id="[AGENT_ID]"}` or
    `metric.labels.agent_resource_name="[AGENT_NAME]"`) in alerting conditions
    unless explicitly requested. Alerting policies must be written to cover all
    active agents in the project dynamically:
    *   **For Reliability Metrics using PromQL**: ALWAYS use grouping
        aggregations (`by (reasoning_engine_id)`) instead of filtering to a
        single ID. This allows a single alert policy to dynamically track each
        reasoning engine instance separately.
    *   **For Quality Metrics using Standard Threshold Filters**: Omit the
        `agent_resource_name` filter entirely. Configure the condition filter to
        only target the monitored resource type
        (`aiplatform.googleapis.com/OnlineEvaluator`) and metric type
        (`aiplatform.googleapis.com/online_evaluator/scores`) globally for the
        project.
*   **Check for Pre-existing Policies**: Avoid creating duplicate alert policies
    for a reasoning engine: scan the target directory or workspace to see if a
    policy already exists that targets the same metrics using aggregations
    grouped by `reasoning_engine_id`.
*   **Metric Scope Discovery & Project Inference**: Centralize alert policies in
    a Metric Scope (scoping project) to save costs. Identify if a scope is used
    and where policies should live by checking:
    1.  **GCP CLI Check**: Run `gcloud beta monitoring metrics-scopes list
        projects/[PROJECT_ID]`. If a parent scope
        `locations/global/metricsScopes/[SCOPING_PROJECT_ID]` is returned, a
        Metric Scope is active; deploy policies there.
    2.  **Infrastructure as Code Scan**: Search Terraform configurations for
        `google_monitoring_monitored_project` resources and extract the scoping
        project from the `metrics_scope` attribute.
    3.  **Ambiguity Fallback**: If unable to determine, ask the user: "Are you
        using a multi-project Cloud Monitoring Metric Scope? If so, what is the
        scoping project ID?" Deploy policies to the deduced scoping project
        (setting the `project` attribute in HCL), or default to the local
        project.
*   **Directory Inference**: Deploy configuration files to target Terraform or
    SRE folders (e.g. `monitoring/`, `ops/`, `sre/`). Use tools to locate where
    alert policies or state pointers exist in the project, rather than blindly
    writing to the current working directory.
*   **Notification Channels**: By default, never configure any notification
    channels without user input. If the user explicitly provides a notification
    channel in their prompt, configure the alerts to use it. If no notification
    channel is provided, you MUST explicitly ask the user in your final response
    if they would like to configure notification channels. **This is a mandatory
    question and you MUST NOT omit it from your response.** **IMPORTANT** Do NOT
    make assumptions about notification channels. If you search the codebase for
    a notification channel you must ALWAYS confirm with the user before using
    it.
*   **Plain English Response**: You MUST include a plain English explanation for
    what the alerts do in your response. This must explain in plain English what
    the alert measures, how the algorithm works, and what a trigger indicates.
*   **Avoid Recursive Directory Operations**: You MUST NOT run recursive listing
    or search commands (such as `ls -R`, `find .`, or raw recursive `grep`) from
    the google3 workspace root, as this will hang your session. Always target
    specific subdirectories.
*   **Background Task Cleanup**: You MUST check the status of all background
    tasks that you spawn. Before completing your execution and returning your
    final response, you MUST terminate or kill any active or hanging background
    tasks (using the `manage_task` tool with action `kill`).

--------------------------------------------------------------------------------

## Algorithm Selection & Policy Mapping Process

Alerting policies for reasoning engine agents MUST map to the correct algorithms
to ensure statistical stability and prevent alert noise or blind spots based on
data classes:

*   **Latency**: Follows workload traffic pattern (Steady -> Z-Score; Seasonal
    -> Seasonal Decomposition; Bursty -> Moving Averages).
*   **Error Rate**: ALWAYS use **Multi-Window Multi-Burn Rate SLOs** (or
    ratio-based static thresholds). Error rate is naturally sparse (normally
    `0`). When standard deviation is `0`, Z-score computation is mathematically
    unstable (division-by-zero or NaN), causing false alert storms.

To resolve the workload traffic pattern (Seasonal, Steady, or Bursty), follow
the instructions corresponding to the availability of historical metrics data:

*   **Case 1: No historical metrics data available (e.g., brand new agent)**:
    You MUST read and follow:
    [no_historical_traffic_data.md](references/no_historical_traffic_data.md)
*   **Case 2: Historical metrics data available (e.g., active agent with
    traffic)**: You MUST read and follow:
    [has_historical_traffic_data.md](references/has_historical_traffic_data.md)

--------------------------------------------------------------------------------

## Telemetry Metrics and PromQL Examples

All raw telemetry metrics for the Agent Platform are cumulative **counters**.
Because we monitor their rates or quantiles, we can optimize the PromQL queries
by using longer range windows (e.g., `[1w]`) for historical averages instead of
expensive `avg_over_time` subqueries.

Signal         | Raw Metric                                  | Type    | Description
:------------- | :------------------------------------------ | :------ | :----------
**Latency**    | `reasoning_engine_request_latencies_bucket` | Counter | Histogram bucket of request latencies
**Error Rate** | `reasoning_engine_request_count`            | Counter | Cumulative count of requests

--------------------------------------------------------------------------------

For the specific PromQL queries corresponding to each algorithm, you MUST read
and follow: [promql_queries.md](references/promql_queries.md)

--------------------------------------------------------------------------------

## Agent Quality Metrics (Online Monitor)

All agent quality evaluation metrics are exported by Online Monitors to the
monitored resource type `aiplatform.googleapis.com/OnlineEvaluator` under the
metric type `aiplatform.googleapis.com/online_evaluator/scores`.

### Metric Details & Aligners

Because the scores metric is of value type `DISTRIBUTION`, standard mean-based
PromQL or arithmetic `ALIGN_MEAN` aligners are unsupported. You MUST use a
percentile aligner (typically `ALIGN_PERCENTILE_50` to evaluate the median
score) within the `aggregations` block of your `condition_threshold`.

Signal                           | Metric Name (`evaluation_metric_name`) | Target Threshold    | Recommended Aligner
:------------------------------- | :------------------------------------- | :------------------ | :------------------
**Final Response Quality**       | `final_response_quality_v1`            | `< 0.8` (or custom) | `ALIGN_PERCENTILE_50`
**Tool Use Quality**             | `tool_use_quality_v1`                  | `< 0.8` (or custom) | `ALIGN_PERCENTILE_50`
**Hallucination (Groundedness)** | `hallucination_v1`                     | `< 0.9` (or custom) | `ALIGN_PERCENTILE_50`

### Metric Filter Example

When configuring a quality alert policy in Terraform, use the following filter
expression structure:

```filter
resource.type="aiplatform.googleapis.com/OnlineEvaluator"
AND metric.type="aiplatform.googleapis.com/online_evaluator/scores"
AND metric.labels.evaluation_metric_name="[METRIC_NAME]"
```

### Online Monitor & Telemetry Provisioning

Quality metrics are generated by the Online Monitor by evaluating trace data
exported to Cloud Trace. If telemetry is disabled on the reasoning engine, no
traces are sent, and the quality metrics will remain empty.

#### Prerequisites & Dependencies

Before executing any scripts in this skill (such as `check_telemetry.py` or
`create_online_monitor.py`), you MUST install the required dependencies in your
environment. Run this command first:

```bash
pip install -r scripts/requirements.txt
```

#### Verify Telemetry Status

Before generating any alerting policies, proposing a plan, or provisioning
Online Monitors, you MUST always check if the agent is ready to export traces by
running the telemetry checking script:

*   **Mandatory Command**: `python3 scripts/check_telemetry.py --project-id
    "[PROJECT_ID]" --agent-resource-name "[AGENT_RESOURCE_NAME]"`
    *   **Note on Parameters**: The `[AGENT_RESOURCE_NAME]` parameter MUST be
        the full resource path format
        `projects/<project_id>/locations/<location>/reasoningEngines/<agent_id>`
        (e.g. `projects/gcp-prod/locations/us-central1/reasoningEngines/556677`)
        and not just the agent ID itself.
    *   **Dependency Failures**: If package imports or dependency installation
        fails, try your best to resolve the issues (e.g., by verifying package
        installation) and run the script again with the specific project-id and
        agent-resource-name parameters. If you cannot run the script
        successfully due to missing dependencies, you MUST still attempt to run
        it first and then include the complete, pre-populated execution command
        in your final response as a verification plan (this applies even if the
        user has pre-approved provisioning).
*   **Detailed Enablement Instructions**: For details on the required
    environment variables, Terraform setups, and project dependencies, you MUST
    read and follow:
    [telemetry_enablement.md](references/telemetry_enablement.md).

#### Provision the Online Monitor

Because Online Monitors cannot be configured via Terraform, run the provisioning
script to generate the required metrics:

*   **Command**: `python3 scripts/create_online_monitor.py --project-id
    "[PROJECT_ID]" --agent-resource-name "[AGENT_RESOURCE_NAME]"
    [--sampling-percentage [PERCENTAGE]]`
*   **Sampling Rate Recommendation**: For production agents, configure a
    conservative sampling percentage (default: **10%**) to control LLM
    evaluation costs. For details, refer to
    [Continuous evaluation with online monitors](https://docs.cloud.google.com/gemini-enterprise-agent-platform/optimize/evaluation/evaluate-online).

#### Formatting the Execution Plan

If execution fails (e.g. due to sandbox restrictions or permissions), you MUST
plan it as a concrete next step. To do this, you MUST explicitly include a
section titled **"Online Monitor Provisioning Plan"** or **"Execution Plan"** in
your final response, containing the exact concrete python execution command with
all parameter values (such as project ID, region, and agent resource name) fully
populated. Do not merely state that the user should run it.

You MUST format the plan exactly as follows:

### Execution Plan: Online Monitor Provisioning

**Online Monitor Provisioning Command:**

```bash
python3 scripts/create_online_monitor.py \
  --project-id "[PROJECT_ID]" \
  --agent-resource-name "projects/[PROJECT_ID]/locations/[LOCATION]/reasoningEngines/[AGENT_ID]" \
  --sampling-percentage [PERCENTAGE]
```

**Verify Telemetry Command (Optional fallback):**

```bash
python3 scripts/check_telemetry.py \
  --project-id "[PROJECT_ID]" \
  --agent-resource-name "projects/[PROJECT_ID]/locations/[LOCATION]/reasoningEngines/[AGENT_ID]"
```

--------------------------------------------------------------------------------

## Tooling Scripts

Use the following scripts to resolve duplicates and validate configs before
presenting or applying Terraform changes:

1.  **Duplicate Check & Merge**: Checks for pre-existing alerts in the target
    folder to ensure changes are merged in-place rather than appended:
    *   Command: `python3 scripts/validate_config.py --directory [TARGET_TF_DIR]
        --engine-var "${var.reasoning_engine_id}"`
2.  **Config Linting**: Validates PromQL grammar, matching engine labels, and
    HCL structure:
    *   Command: `python3 scripts/validate_config.py --file [PATH_TO_TF_FILE]`
    *   **Self-Correction Loop**: If validation fails (exits non-zero or outputs
        errors), you MUST read the command output, locate the line/file
        containing the lint error, analyze the PromQL syntax or Terraform HCL
        issue, apply adjustments in-place, and re-run the `validate_config.py
        --file` validation. Repeat this loop until the validation script passes
        successfully.

--------------------------------------------------------------------------------

## Gotchas & Behavioral Corrections

*   **Duration Buffers (Transient Glitches)**: To avoid alerts firing on
    transient spikes, use duration/retest window buffers appropriately:
    *   **Reliability Metrics (PromQL / Cloud Monitoring)**:
        *   For short-lookback alerts querying data under 25 hours (e.g.,
            Short-Window Z-Score, Moving Averages, Fast Burn SLO), ALWAYS use a
            `duration = "300s"` (5 minutes) buffer to filter out transient cold
            start/deployment spikes.
        *   For long-lookback alerts querying data longer than 25 hours (e.g.,
            Long-Window Z-Score, Seasonal Decomposition, Slow Burn SLO),
            duration/retest windows are disabled by the platform. You must **not
            set a duration** (omit it entirely).
    *   **Quality Metrics (Standard Filters / Online Monitor)**:
        *   Always use a `duration = "300s"` (5 minutes) buffer to filter out
            transient scoring dips or evaluation outliers caused by temporary
            LLM judge congestion, or edge-case query outliers.
*   **Dynamic Baseline Adaptation Blind Spot**: Explain to users that dynamic
    statistical Z-score thresholds compare current rates to a moving statistical
    baseline. If a system degrades slowly over days, the standard baseline curve
    adapts to this slow drift, making standard Z-score alerts blind to
    persistent slow errors. Recommend a hard static threshold alert in parallel
    for strict SLA enforcement.
*   **Seasonal Decomposition Double Alerting**: The agent MUST ONLY configure
    seasonal decomposition alert policies to track spikes (e.g., latency spikes)
    OR drops AND MUST NOT use dual-direction checks (like absolute deviation).
    Explain this limitation to the user: comparing to a historical offset (e.g.,
    `offset 1w`) the alert policy triggers twice if tracking both directions
    (once for the anomaly, and once 1 week later when the anomaly becomes the
    baseline). To prevent this, the generated policy MUST only track either
    spikes (using `>`) or drops (using `<`), avoiding using `abs()`
...<truncated>
````


### `assets/alerts_initial_duplicate.tf`

```
resource "google_monitoring_alert_policy" "agent_error_rate_fast_burn" {
  project      = var.project_id
  display_name = "Agent Error Rate Fast Burn"
  combiner     = "OR"
  conditions {
    display_name = "Error Rate Fast Burn"
    condition_prometheus_query_language {
      query    = <<-EOT
        (
          sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{response_code!~"2..", reasoning_engine_id="12345"}[5m]))
          /
          sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{reasoning_engine_id="12345"}[5m]))
          > (1 - var.slo_target) * 3
        )
      EOT
      duration = "300s"
    }
  }
}

```


### `assets/draft_invalid_query.tf`

```
resource "google_monitoring_alert_policy" "draft_policy" {
  project      = var.project_id
  display_name = "Draft Policy"
  combiner     = "OR"
  conditions {
    display_name = "Draft Condition"
    condition_prometheus_query_language {
      query = "sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[5y])) / sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[5m]"
    }
  }
}

```


### `assets/mock_bursty.json`

```
[495.0469, 509.6423, 492.0803, 493.7152, 496.8687, 497.3225, 0.0292, 0.0601, 0.0767, 0.0376, 0.0635, 0.033, 0.0492, 0.0417, 0.0519, 0.0552, 0.0371, 0.0484, 0.0574, 0.0407, 0.0624, 0.0628, 0.0453, 0.0642, 0.0306, 0.0439, 0.0554, 0.0595, 0.061, 0.0598, 0.0504, 0.0569, 0.0503, 0.0353, 0.059, 0.0557, 0.0487, 0.0442, 0.0695, 0.0407, 0.0507, 0.043, 0.0495, 0.0614, 0.0541, 0.0469, 0.0581, 0.0593, 0.0493, 0.0457, 0.0427, 0.0311, 0.0655, 0.0429, 0.0384, 0.0415, 0.0386, 0.0611, 0.0491, 0.0402, 0.0419, 0.04, 0.0612, 0.0357, 0.0508, 0.0549, 0.0537, 0.0523, 0.0486, 0.0265, 0.0498, 0.045, 486.7916, 514.4725, 512.5739, 496.267, 503.5742, 500.0377, 0.0311, 0.0457, 0.0587, 0.0489, 0.04, 0.0613, 0.0535, 0.0384, 0.0443, 0.0458, 0.0402, 0.0576, 0.0446, 0.0514, 0.0678, 0.0433, 0.039, 0.05, 0.0455, 0.0467, 0.0409, 0.0345, 0.0461, 0.0371, 0.039, 0.0784, 0.0742, 0.0738, 0.0469, 0.039, 0.0555, 0.0397, 0.0472, 0.0407, 0.0589, 0.0489, 0.0532, 0.0439, 0.048, 0.0319, 0.049, 0.0469, 0.0542, 0.0453, 0.0401, 0.0508, 0.0646, 0.0486, 0.0678, 0.025, 0.0413, 0.0473, 0.0367, 0.0226, 0.0505, 0.0401, 0.042, 0.0486, 0.0478, 0.0438, 0.0562, 0.0421, 0.0614, 0.0622, 0.0342, 0.0412, 509.228, 487.2668, 494.2323, 501.5766, 510.0171, 511.7377, 0.0486, 0.0554, 0.0533, 0.0663, 0.0499, 0.0487, 0.048, 0.0371, 0.037, 0.0231, 0.0647, 0.0696, 0.0328, 0.0585, 0.054, 0.048, 0.0562, 0.049, 0.0765, 0.0591, 0.0421, 0.0391, 0.0638, 0.0473, 0.0478, 0.0515, 0.0507, 0.0616, 0.0406, 0.027, 0.0444, 0.0421, 0.0436, 0.0334, 0.0406, 0.0354, 0.0596, 0.0391, 0.0477, 0.0644, 0.0466, 0.0537, 0.0601, 0.0558, 0.0442, 0.0396, 0.0629, 0.0519, 0.0428, 0.0465, 0.0495, 0.0322, 0.0548, 0.0604, 0.0434, 0.0416, 0.0459, 0.048, 0.0702, 0.0585, 0.0518, 0.0439, 0.0508, 0.0482, 0.0384, 0.0457, 504.4112, 489.2926, 486.8359, 498.4292, 496.0102, 482.7873, 0.0619, 0.0618, 0.0709, 0.0454, 0.0557, 0.0623, 0.0554, 0.0615, 0.0516, 0.0451, 0.0547, 0.0549, 0.0568, 0.0609, 0.0516, 0.0479, 0.0618, 0.0485, 0.0418, 0.0595, 0.0287, 0.0612, 0.0585, 0.05, 0.0614, 0.046, 0.053, 0.0356, 0.0377, 0.0545, 0.0459, 0.0386, 0.0593, 0.0492, 0.0433, 0.0484, 0.065, 0.0608, 0.0505, 0.0564, 0.0442, 0.068, 0.0631, 0.0464, 0.052, 0.0592, 0.0596, 0.0601, 0.0569, 0.0535, 0.0341, 0.0646, 0.0466, 0.0524, 0.0497, 0.0605, 0.0458, 0.0384, 0.0647, 0.057, 0.0654, 0.0495, 0.0534, 0.059, 0.0398, 0.0345, 491.7545, 493.6087, 517.8153, 525.6872, 497.905, 500.9281, 0.0468, 0.0476, 0.0534, 0.0534, 0.0465, 0.0489, 0.044, 0.0427, 0.046, 0.0569, 0.0549, 0.0448, 0.0597, 0.0506, 0.0596, 0.0481, 0.068, 0.064, 0.0573, 0.0644, 0.0435, 0.0674, 0.0378, 0.0508, 0.0439, 0.0309, 0.037, 0.0456, 0.0549, 0.0463, 0.0594, 0.0467, 0.0407, 0.0636, 0.0621, 0.0475, 0.0339, 0.0391, 0.0448, 0.0508, 0.062, 0.0507, 0.0529, 0.0543, 0.0382, 0.0613, 0.0652, 0.0552, 0.0655, 0.0475, 0.0501, 0.0637, 0.0483, 0.0439, 0.0678, 0.0485, 0.042, 0.0499, 0.0286, 0.0605, 0.0635, 0.0376, 0.0433, 0.0686, 0.0597, 0.0467, 498.3306, 504.0133, 514.2054, 507.7485, 505.6508, 497.9603, 0.0654, 0.0558, 0.039, 0.0461, 0.068, 0.0318, 0.0394, 0.0555, 0.0486, 0.0387, 0.015, 0.0467, 0.0618, 0.04, 0.0385, 0.0577, 0.064, 0.0412, 0.0465, 0.0348, 0.05, 0.0425, 0.0483, 0.0255, 0.0592, 0.0315, 0.0365, 0.0476, 0.0298, 0.0339, 0.0406, 0.0316, 0.0429, 0.0489, 0.067, 0.0587, 0.0254, 0.0479, 0.0414, 0.0546, 0.0466, 0.0535, 0.0565, 0.0461, 0.0335, 0.0459, 0.0562, 0.0557, 0.0481, 0.0567, 0.049, 0.0371, 0.0517, 0.0583, 0.0631, 0.0441, 0.0512, 0.0476, 0.0365, 0.0532, 0.0406, 0.0507, 0.0433, 0.0441, 0.0494, 0.0508, 504.9506, 495.9148, 497.1774, 503.661, 495.7969, 500.2102, 0.0442, 0.0495, 0.0582, 0.0581, 0.0582, 0.0621, 0.0515, 0.0599, 0.0468, 0.0601, 0.0576, 0.0666, 0.0583, 0.0568, 0.0532, 0.045, 0.0435, 0.0585, 0.0372, 0.0656, 0.0521, 0.0561, 0.0597, 0.0525, 0.0492, 0.0285, 0.0536, 0.0556, 0.0353, 0.0519, 0.049, 0.0604, 0.0329, 0.0444, 0.048, 0.0414, 0.0519, 0.0294, 0.0273, 0.0497, 0.0555, 0.0417, 0.0537, 0.053, 0.0558, 0.0508, 0.0329, 0.05, 0.0513, 0.065, 0.035, 0.0729, 0.0492, 0.0394, 0.0472, 0.0612, 0.0505, 0.0425, 0.0569, 0.0598, 0.0501, 0.0671, 0.0711, 0.0258, 0.0503, 0.0685, 504.9866, 514.8419, 505.7171, 485.5475, 493.6777, 499.9073, 0.0634, 0.0494, 0.0452, 0.0446, 0.0519, 0.0521, 0.073, 0.0486, 0.0376, 0.0562, 0.0527, 0.0517, 0.0429, 0.0547, 0.0451, 0.0446, 0.0441, 0.0598, 0.0357, 0.0491, 0.0474, 0.0575, 0.0482, 0.0591, 0.0424, 0.0543, 0.0528, 0.0509, 0.0656, 0.0553, 0.0527, 0.061, 0.0539, 0.0475, 0.0597, 0.0723, 0.0444, 0.0564, 0.0514, 0.0509, 0.0311, 0.0436, 0.042, 0.0643, 0.0388, 0.0633, 0.0405, 0.0483, 0.046, 0.0444, 0.0344, 0.0639, 0.0612, 0.062, 0.0597, 0.0591, 0.0624, 0.0424, 0.0557, 0.0645, 0.0535, 0.0482, 0.0412, 0.0474, 0.0453, 0.0201, 504.7448, 515.2522, 506.2786, 501.7734, 492.7512, 497.6197, 0.064, 0.0363, 0.0727, 0.035, 0.0586, 0.0433, 0.0455, 0.0285, 0.0595, 0.0543, 0.0608, 0.045, 0.0634, 0.0519, 0.0483, 0.045, 0.0624, 0.0536, 0.0542, 0.0352, 0.0494, 0.04, 0.0419, 0.0315, 0.0559, 0.0489, 0.05, 0.0402, 0.0519, 0.0401, 0.0491, 0.0563, 0.0538, 0.0559, 0.0432, 0.0306, 0.0528, 0.0393, 0.0468, 0.0382, 0.0477, 0.0567, 0.0467, 0.0378, 0.0515, 0.0527, 0.0502, 0.0528, 0.0624, 0.0666, 0.0571, 0.0465, 0.0536, 0.0361, 0.0501, 0.0686, 0.0495, 0.0499, 0.0422, 0.0612, 0.0472, 0.0531, 0.0465, 0.0379, 0.0482, 0.0399, 493.2261, 494.2891, 517.2198, 506.4776, 503.1522, 493.7094, 0.0561, 0.0486, 0.0622, 0.0478, 0.0672, 0.0477, 0.0509, 0.0453, 0.0497, 0.0402, 0.0385, 0.0334, 0.0416, 0.0405, 0.0529, 0.0436, 0.0484, 0.0631, 0.0559, 0.0344, 0.0541, 0.056, 0.0486, 0.0405, 0.0536, 0.0474, 0.0527, 0.0558, 0.0542, 0.0451, 0.0339, 0.051, 0.0488, 0.0484, 0.0533, 0.0553, 0.0392, 0.0584, 0.0535, 0.0516, 0.0507, 0.0719, 0.0545, 0.0491, 0.0341, 0.0473, 0.0548, 0.0373, 0.0485, 0.0514, 0.0579, 0.0349, 0.0482, 0.0476, 0.05, 0.0542, 0.0588, 0.061, 0.076, 0.0522, 0.0546, 0.0488, 0.0492, 0.0409, 0.0482, 0.0435, 487.2326, 501.5782, 494.3931, 502.0454, 497.1529, 505.0461, 0.0644, 0.0553, 0.0464, 0.0463, 0.0521, 0.0458, 0.0465, 0.0615, 0.0529, 0.0453, 0.0519, 0.0488, 0.0449, 0.0525, 0.046, 0.0441, 0.0571, 0.0432, 0.0639, 0.0376, 0.0546, 0.054, 0.0536, 0.053, 0.059, 0.0474, 0.0462, 0.0467, 0.0699, 0.0396, 0.0608, 0.0417, 0.0554, 0.0611, 0.0378, 0.0412, 0.0587, 0.062, 0.0441, 0.0675, 0.0473, 0.0538, 0.0627, 0.0426, 0.0666, 0.0425, 0.049, 0.0461, 0.0733, 0.0503, 0.0587, 0.0617, 0.0457, 0.059, 0.0469, 0.0327, 0.0463, 0.0505, 0.0276, 0.0381, 0.0492, 0.0543, 0.0563, 0.0498, 0.0434, 0.0558, 486.3505, 507.0814, 484.7717, 509.1429, 490.9865, 515.582, 0.0457, 0.0438, 0.0398, 0.0557, 0.0379, 0.0474, 0.0543, 0.0411, 0.0409, 0.0449, 0.0364, 0.0428, 0.0498, 0.0344, 0.0501, 0.0508, 0.0519, 0.0499, 0.0265, 0.041, 0.0531, 0.0483, 0.0386, 0.0658, 0.0624, 0.0363, 0.0485, 0.0554, 0.0662, 0.0287, 0.0564, 0.0461, 0.0516, 0.047, 0.0666, 0.0695, 0.0472, 0.0407, 0.0755, 0.0543, 0.0392, 0.0612, 0.048, 0.0467, 0.0606, 0.0328, 0.0594, 0.0518, 0.0535, 0.0519, 0.0334, 0.0423, 0.0537, 0.0551, 0.0493, 0.0334, 0.0473, 0.042, 0.0423, 0.0548, 0.053, 0.0349, 0.0487, 0.0557, 0.0519, 0.0483, 500.0842, 488.1702, 518.8654, 501.2902, 491.2894, 481.5181, 0.0323, 0.0437, 0.0603, 0.0443, 0.0473, 0.0398, 0.0649, 0.0724, 0.0226, 0.0463, 0.0549, 0.0549, 0.0448, 0.0452, 0.0567, 0.0651, 0.0321, 0.043, 0.0546, 0.0397, 0.0445, 0.0632, 0.0528, 0.0447, 0.0547, 0.0364, 0.0516, 0.0545, 0.062, 0.0564, 0.0514, 0.048, 0.0475, 0.0622, 0.0624, 0.0554, 0.0504, 0.0414, 0.0365, 0.0454, 0.0578, 0.0516, 0.0628, 0.0481, 0.0337, 0.0431, 0.0583, 0.0525, 0.0512, 0.0495, 0.0449, 0.0564, 0.0447, 0.0539, 0.0502, 0.0339, 0.0654, 0.0475, 0.0584, 0.0559, 0.0498, 0.0454, 0.052, 0.0447, 0.0416, 0.0449, 483.2593, 489.9058, 498.5701, 505.5517, 479.7498, 504.7801, 0.0633, 0.0588, 0.0636, 0.0379, 0.0403, 0.0359, 0.0637, 0.0718, 0.0339, 0.0483, 0.0366, 0.0312, 0.0405, 0.0647, 0.045, 0.058, 0.0566, 0.0416, 0.0633, 0.0391, 0.0448, 0.0544, 0.047, 0.0475, 0.0581, 0.0615, 0.0319, 0.0503, 0.0626, 0.0658, 0.0248, 0.0423, 0.0508, 0.0458, 0.0572, 0.0548, 0.0554, 0.0476, 0.0482, 0.0593, 0.0553, 0.0533, 0.0511, 0.0423, 0.0424, 0.0513, 0.0604, 0.0399, 0.0533, 0.054, 0.0474, 0.0488, 0.0564, 0.0422, 0.0639, 0.0325, 0.0566, 0.0569, 0.0638, 0.0296, 0.0394, 0.0608, 0.0422, 0.0497, 0.0607, 0.0393, 489.5629, 490.1229, 499.5785, 492.5179, 500.5466, 498.8615, 0.055, 0.0434, 0.0555, 0.0585, 0.0589, 0.0544, 0.0457, 0.051, 0.036, 0.0531, 0.0591, 0.0434, 0.0639, 0.0583, 0.0696, 0.0388, 0.0508, 0.0434, 0.0459, 0.0577, 0.0615, 0.0497, 0.0466, 0.0391, 0.0495, 0.0527, 0.0563, 0.0593, 0.0444, 0.0432, 0.0633, 0.0527, 0.0329, 0.066, 0.0432, 0.0581, 0.0467, 0.0506, 0.0667, 0.0318, 0.0468, 0.0427, 0.0473, 0.0469, 0.0592, 0.0543, 0.0453, 0.0651, 0.0584, 0.0412, 0.0463, 0.0432, 0.0521, 0.0461, 0.0575, 0.0447, 0.0501, 0.044, 0.0439, 0.0585, 0.0349, 0.0523, 0.0582, 0.0631, 0.0406, 0.0622, 498.9163, 500.9065, 502.7819, 486.8057, 513.1098, 506.9739, 0.0434, 0.0441, 0.0736, 0.048, 0.0493, 0.0337, 0.0477, 0.0552, 0.0445, 0.0515, 0.0532, 0.0463, 0.06, 0.0481, 0.0491, 0.0499, 0.032, 0.0558, 0.0577, 0.0578, 0.0544, 0.0491, 0.0554, 0.0773, 0.039, 0.056, 0.0549, 0.0584, 0.0357, 0.0701, 0.0629, 0.0477, 0.0683, 0.0517, 0.0712, 0.0522, 0.0299, 0.0497, 0.0455, 0.0363, 0.0401, 0.0528, 0.0425, 0.06, 0.0332, 0.0475, 0.0547, 0.0541, 0.0444, 0.0527, 0.0396, 0.043, 0.0358, 0.0631, 0.046, 0.0401, 0.0471, 0.0636, 0.0676, 0.0411, 0.0681, 0.0495, 0.0615, 0.0588, 0.0423, 0.0431, 483.3116, 500.377, 504.1124, 497.9767, 487.6112, 504.5393, 0.0581, 0.0517, 0.0438, 0.0534, 0.0406, 0.0528, 0.0538, 0.0606, 0.0686, 0.0685, 0.0589, 0.0447, 0.0567, 0.0359, 0.0499, 0.0488, 0.045, 0.055, 0.0457, 0.0701, 0.0483, 0.069, 0.0529, 0.0401, 0.0511, 0.0714, 0.0485, 0.0509, 0.0555, 0.058, 0.0575, 0.0331, 0.0427, 0.0447, 0.0804, 0.042, 0.0514, 0.0769, 0.0551, 0.0635, 0.0585, 0.0342, 0.0454, 0.0475, 0.032, 0.0292, 0.06, 0.0692, 0.0393, 0.0488, 0.0646, 0.0605, 0.0359, 0.0496, 0.0541, 0.0312, 0.0446, 0.0444, 0.027, 0.042, 0.0513, 0.0542, 0.037, 0.0554, 0.048, 0.0487, 494.0709, 478.2863, 508.6394, 504.6632, 499.7455, 494.7167, 0.0365, 0.0541, 0.0453, 0.0558, 0.0567, 0.045, 0.0427, 0.0483, 0.0533, 0.0328, 0.0519, 0.0354, 0.0463, 0.0731, 0.0401, 0.0398, 0.0459, 0.0478, 0.037, 0.0534, 0.0564, 0.0551, 0.0535, 0.0473, 0.0405, 0.0503, 0.0552, 0.0415, 0.0479, 0.0501, 0.0726, 0.0613, 0.0635, 0.057, 0.0439, 0.0561, 0.0652, 0.0401, 0.0506, 0.0513, 0.0556, 0.0617, 0.048, 0.0484, 0.0815, 0.0452, 0.0363, 0.0514, 0.0444, 0.0483, 0.0488, 0.0272, 0.0509, 0.0523, 0.0618, 0.0517, 0.0616, 0.0664, 0.0553, 0.0546, 0.0467, 0.0562, 0.0467, 0.0265, 0.0487, 0.0614, 508.8612, 497.3183, 492.0835, 497.3452, 499.6467, 488.3025, 0.0399, 0.0492, 0.0522, 0.0558, 0.0419, 0.0307, 0.0481, 0.0598, 0.0352, 0.0414, 0.0433, 0.0665, 0.0611, 0.0467, 0.0578, 0.043, 0.0497, 0.0316, 0.0402, 0.0405, 0.0383, 0.0556, 0.0401, 0.0601, 0.0556, 0.0524, 0.0617, 0.0525, 0.0429, 0.0457, 0.0567, 0.0372, 0.0591, 0.048, 0.0562, 0.0512, 0.0422, 0.0659, 0.0476, 0.053, 0.0447, 0.042, 0.0509, 0.045, 0.0494, 0.0513, 0.0447, 0.0598, 0.056, 0.0295, 0.0409, 0.0463, 0.0402, 0.0427, 0.0564, 0.0549, 0.0339, 0.037, 0.0525, 0.047, 0.0582, 0.0435, 0.0503, 0.0413, 0.0472, 0.056, 503.8221, 488.8878, 510.4386, 499.3604, 504.3571, 504.3077, 0.0454, 0.0587, 0.0427, 0.0462, 0.0378, 0.0345, 0.072, 0.0483, 0.0459, 0.0688, 0.0326, 0.0622, 0.0525, 0.0486, 0.0477, 0.047, 0.0649, 0.0601, 0.0487, 0.0421, 0.0502, 0.0399, 0.0419, 0.047, 0.0424, 0.0435, 0.0464, 0.0396, 0.0652, 0.0287, 0.057, 0.0567, 0.0482, 0.0484, 0.0514, 0.0504, 0.057, 0.0554, 0.0459, 0.0401, 0.0531, 0.0557, 0.0578, 0.0445, 0.0748, 0.0327, 0.0684, 0.0419, 0.0489, 0.0675, 0.0686, 0.0656, 0.058, 0.0541, 0.0361, 0.0381, 0.0492, 0.0561, 0.0572, 0.0585, 0.064, 0.0642, 0.0504, 0.0291, 0.0601, 0.0478, 495.1735, 501.6769, 487.0631, 497.7347, 485.5215, 476.7783, 0.0429, 0.0646, 0.0512, 0.0577, 0.0488, 0.054, 0.031, 0.0427, 0.0412, 0.0363, 0.0511, 0.0539, 0.0535, 0.0494, 0.057, 0.0506, 0.0527, 0.0441, 0.0554, 0.0542, 0.0397, 0.0434, 0.0606, 0.0369, 0.0458, 0.0602, 0.0567, 0.0598, 0.056, 0.0464, 0.0322, 0.0705, 0.0581, 0.0411, 0.0512, 0.0437, 0.049, 0.0559, 0.0429, 0.0689, 0.0466, 0.0455, 0.0618, 0.0384, 0.0513, 0.0712, 0.0509, 0.0522, 0.0442, 0.0407, 0.0478, 0.0728, 0.0592, 0.0554, 0.0569, 0.0596, 0.0467, 0.0498, 0.0494, 0.0545, 0.0403, 0.037, 0.0426, 0.0539, 0.039, 0.0577, 488.5348, 497.8455, 486.1783, 506.1297, 484.6796, 479.9006, 0.0506, 0.0532, 0.0577, 0.0517, 0.0542, 0.0519, 0.0521, 0.0638, 0.0479, 0.0403, 0.0631, 0.0597, 0.0557, 0.0446, 0.0548, 0.0669, 0.0453, 0.0567, 0.0366, 0.0407, 0.0598, 0.0566, 0.0465, 0.0392, 0.0426, 0.0705, 0.0506, 0.0587, 0.0558, 0.0527, 0.0545, 0.0467, 0.0613, 0.0509, 0.0468, 0.0554, 0.0497, 0.054, 0.0416, 0.0323, 0.0281, 0.0353, 0.04, 0.0442, 0.0501, 0.0548, 0.0554, 0.0383, 0.069, 0.0489, 0.044, 0.049, 0.0527, 0.0535, 0.0445, 0.0654, 0.0344, 0.0548, 0.0577, 0.0495, 0.0405, 0.0508, 0.0593, 0.0508, 0.0448, 0.0296, 503.0271, 480.5126, 510.9951, 492.238, 511.5543, 509.9011, 0.0497, 0.048, 0.0695, 0.0474, 0.047, 0.0453, 0.0425, 0.0501, 0.0485, 0.0231, 0.0395, 0.0628, 0.0586, 0.0734, 0.0333, 0.0301, 0.0608, 0.0395, 0.0588, 0.0435, 0.0618, 0.0468, 0.0486, 0.065, 0.0576, 0.0436, 0.0223, 0.0659, 0.0489, 0.0527, 0.0409, 0.0488, 0.0419, 0.0431, 0.0248, 0.0573, 0.0522, 0.042, 0.0615, 0.0478, 0.0456, 0.0397, 0.0653, 0.0711, 0.0395, 0.0396, 0.0473, 0.0578, 0.0505, 0.0483, 0.0579, 0.0521, 0.0472, 0.0482, 0.0464, 0.06, 0.0498, 0.0497, 0.0499, 0.0593, 0.0459, 0.0694, 0.0413, 0.0352, 0.0524, 0.0535, 495.7048, 532.8877, 504.3582, 512.5304, 506.1438, 503.4137, 0.0408, 0.0516, 0.0554, 0.0368, 0.0269, 0.0622, 0.0563, 0.0545, 0.0472, 0.0582, 0.0361, 0.0441, 0.0442, 0.0618, 0.0473, 0.0439, 0.0584, 0.0596, 0.0487, 0.054, 0.0371, 0.0443, 0.0407, 0.0472, 0.0576, 0.0425, 0.057, 0.0583, 0.041, 0.0716, 0.0474, 0.0391, 0.0406, 0.0546, 0.048, 0.0513, 0.0266, 0.0409, 0.0352, 0.058, 0.0436, 0.0444, 0.0506, 0.062, 0.0587, 0.059, 0.0424, 0.0467, 0.0503, 0.04, 0.0491, 0.0506, 0.0572, 0.0309, 0.0468, 0.0508, 0.0677, 0.0563, 0.0447, 0.0427, 0.0568, 0.0548, 0.0411, 0.051, 0.0652, 0.0561, 492.5012, 485.416, 506.4024, 502.2154, 510.1353, 512.9235, 0.0429, 0.0502, 0.0412, 0.0325, 0.0473, 0.0582, 0.0511, 0.0499, 0.0348, 0.0514, 0.0501, 0.0501, 0.046, 0.0469, 0.0555, 0.0539, 0.0313, 0.0531, 0.0468, 0.0512, 0.0573, 0.0571, 0.0684, 0.048, 0.0569, 0.0395, 0.0566, 0.0364, 0.0588, 0.0393, 0.037, 0.0559, 0.0574, 0.0669, 0.0466, 0.0449, 0.0493, 0.0374, 0.0418, 0.0512, 0.0375, 0.028, 0.0352, 0.0507, 0.038, 0.0598, 0.0581, 0.0565, 0.0479, 0.0511, 0.0376, 0.0534, 0.0585, 0.0506, 0.037, 0.0333, 0.0478, 0.048, 0.0714, 0.0359, 0.0343, 0.0532, 0.0452, 0.0779, 0.0377, 0.0653, 490.7391, 499.8134, 497.2968, 490.421, 513.4861, 505.6125, 0.0405, 0.0628, 0.0416, 0.0634, 0.0518, 0.0421, 0.0507, 0.0415, 0.0325, 0.0638, 0.044, 0.0423, 0.0356, 0.0526, 0.0297, 0.0395, 0.0519, 0.0379, 0.0561, 0.0564, 0.0589, 0.0474, 0.0425, 0.0432, 0.0688, 0.0554, 0.0524, 0.0538, 0.0683, 0.0644, 0.0333, 0.0525, 0.0681, 0.0479, 0.0492, 0.0427, 0.055, 0.0464, 0.0631, 0.0395, 0.0468, 0.0518, 0.0462, 0.0468, 0.0522, 0.0468, 0.0511, 0.0364, 0.0622, 0.0309, 0.0433, 0.064, 0.0284, 0.0709, 0.044, 0.047, 0.0498, 0.0607, 0.0485, 0.0565, 0.0596, 0.0656, 0.0524, 0.0439, 0.0377, 0.0431, 496.7001, 494.1156, 507.7887, 480.3311, 493.4945, 507.9081, 0.0463, 0.0489, 0.0564, 0.0649, 0.0622, 0.0424, 0.0311, 0.0505, 0.0398, 0.0452, 0.0417, 0.0611, 0.0449, 0.048, 0.0596, 0.0543, 0.0473, 0.0527, 0.0654, 0.0582, 0.0485, 0.0548, 0.0517, 0.0635, 0.0465, 0.0562, 0.071, 0.0418, 0.0499, 0.0656, 0.0684, 0.0584, 0.0539, 0.0492, 0.0326, 0.0411, 0.0607, 0.0451, 0.0472, 0.052, 0.0484, 0.0454, 0.028, 0.054, 0.0498, 0.0525, 0.0459, 0.0566, 0.0414, 0.0647, 0.0581, 0.0597, 0.0477, 0.0274, 0.0394, 0.0324, 0.0504, 0.0539, 0.0705, 0.043, 0.0451, 0.0337, 0.0775, 0.0562, 0.0628, 0.0458, 496.8816, 497.3389, 477.0454, 492.3137, 499.6248, 502.896, 0.0616, 0.0526, 0.0438, 0.0414, 0.0436, 0.0684, 0.052, 0.0496, 0.0662, 0.0559, 0.0546, 0.0385, 0.0597, 0.0384, 0.049, 0.0374, 0.0661, 0.0555, 0.0464, 0.04, 0.0451, 0.0524, 0.0303, 0.0581, 0.0595, 0.049, 0.0537, 0.06, 0.0568, 0.0526, 0.0439, 0.0462, 0.0497, 0.0291, 0.0535, 0.0375, 0.0517, 0.0499, 0.0466, 0.032, 0.0546, 0.0524, 0.0513, 0.0527, 0.0414, 0.0618, 0.0503, 0.0583, 0.041, 0.0429, 0.0529, 0.0528, 0.05, 0.0625, 0.0337, 0.0505, 0.0362, 0.0674, 0.0529, 0.0655, 0.0662, 0.0437, 0.0461, 0.064, 0.0422, 0.0487, 496.1863, 488.7987, 520.2551, 507.1485, 496.1513, 486.5951, 0.0487, 0.0566, 0.0678, 0.0707, 0.052, 0.0663, 0.0524, 0.0594, 0.0522, 0.0449, 0.0653, 0.0479, 0.0435, 0.0411, 0.0589, 0.0577, 0.0579, 0.0517, 0.036, 0.0625, 0.057, 0.0541, 0.041, 0.0426, 0.0564, 0.0425, 0.0565, 0.0519, 0.0543, 0.0597, 0.0581, 0.0528, 0.0411, 0.0476, 0.0635, 0.0522, 0.0677, 0.0359, 0.0652, 0.04, 0.0358, 0.0409, 0.0398, 0.0658, 0.0403, 0.0543, 0.0479, 0.0338, 0.0545, 0.0517, 0.0378, 0.0603, 0.0414, 0.0322, 0.0603, 0.0458, 0.0384, 0.0443, 0.0441, 0.0493, 0.0378, 0.0612, 0.0547, 0.0476, 0.0678, 0.0517, 504.0298, 498.7177, 500.0053, 512.5111, 494.7635, 508.5412, 0.0429, 0.0437, 0.0406, 0.0353, 0.0551, 0.0373, 0.0592, 0.0591, 0.0466, 0.0571, 0.0548, 0.06, 0.0396, 0.0541, 0.0612, 0.0476, 0.0539, 0.0442, 0.0721, 0.0585, 0.0477, 0.0635, 0.0486, 0.0533, 0.0573, 0.0654, 0.0467, 0.0699, 0.0598, 0.0601, 0.0584, 0.0611, 0.0547, 0.0515, 0.0647, 0.0536, 0.0584, 0.0399, 0.0653, 0.0541, 0.0647, 0.0559, 0.0496, 0.0577, 0.0414, 0.0322, 0.0404, 0.0627, 0.0425, 0.0445, 0.0596, 0.0551, 0.0427, 0.0495, 0.0456, 0.0586, 0.0546, 0.0407, 0.0372, 0.0341, 0.0555, 0.057, 0.0521, 0.0412, 0.0378, 0.0579, 473.509, 504.9354, 495.7398, 495.9017, 522.1803, 506.3852, 0.0519, 0.0488, 0.0359, 0.0385, 0.0326, 0.0488, 0.0368, 0.0493, 0.0461, 0.0558, 0.0513, 0.0477, 0.0485, 0.0403, 0.054, 0.0499, 0.0707, 0.0609, 0.0519, 0.0617, 0.0387, 0.0443, 0.0359, 0.058, 0.057, 0.04, 0.0607, 0.0536, 0.0393, 0.0483, 0.0477, 0.0689, 0.048, 0.0572, 0.0507, 0.0671, 0.0429, 0.0584, 0.0499, 0.045, 0.0509, 0.0662, 0.0569, 0.0435, 0.0429, 0.0595, 0.0625, 0.0426, 0.0414, 0.0433, 0.0387, 0.0563, 0.0524, 0.0454, 0.0332, 0.0459, 0.0389, 0.0573, 0.055, 0.0559, 0.048, 0.0584, 0.0591, 0.0551, 0.063, 0.0486, 488.4266, 487.2601, 516.5337, 487.9673, 505.05, 500.4938, 0.0489, 0.0627, 0.0649, 0.043, 0.0518, 0.0519, 0.0361, 0.0564, 0.0553, 0.0497, 0.065, 0.0334, 0.0577, 0.0447, 0.0396, 0.0573, 0.0488, 0.0451, 0.0671, 0.0372, 0.0384, 0.0425, 0.053, 0.0499, 0.0493, 0.0407, 0.0403, 0.0532, 0.032, 0.0559, 0.0472, 0.061, 0.0542, 0.0548, 0.0522, 0.0278, 0.0539, 0.0484, 0.0606, 0.0418, 0.0401, 0.0606, 0.0422, 0.0574, 0.0367, 0.0493, 0.0542, 0.0485, 0.0473, 0.0587, 0.0304, 0.0416, 0.0331, 0.0407, 0.0421, 0.0539, 0.0474, 0.0557, 0.0614, 0.0644, 0.0471, 0.02, 0.053, 0.0617, 0.0377, 0.0437, 506.64, 504.7903, 505.8813, 505.423, 480.7013, 507.7932, 0.0446, 0.0717, 0.0667, 0.0488, 0.0632, 0.0432, 0.0543, 0.0553, 0.0589, 0.0358, 0.0713, 0.0537, 0.0621, 0.0469, 0.0504, 0.0489, 0.0511, 0.048, 0.0569, 0.0728, 0.0354, 0.043, 0.0313, 0.0475, 0.0487, 0.0546, 0.0448, 0.0314, 0.04, 0.0536, 0.0484, 0.0699, 0.0662, 0.0493, 0.057, 0.0562, 0.0656, 0.0585, 0.0487, 0.0412, 0.042, 0.0496, 0.0418, 0.0412, 0.0526, 0.0346, 0.041, 0.0709, 0.0288, 0.0594, 0.0524, 0.0416, 0.058, 0.0377, 0.0519, 0.0481, 0.0492, 0.0491, 0.0563, 0.0574, 0.064, 0.0448, 0.0495, 0.0557, 0.0564, 0.035, 498.6466, 506.5146, 517.4506, 482.7378, 510.3746, 486.993, 0.0537, 0.0501, 0.0436, 0.0476, 0.0545, 0.0594, 0.0492, 0.0597, 0.0377, 0.0428, 0.0354, 0.0514, 0.0536, 0.0576, 0.0509, 0.0583, 0.0322, 0.0517, 0.0427, 0.0559, 0.0455, 0.0505, 0.0657, 0.0634, 0.0503, 0.0418, 0.0587, 0.0483, 0.0474, 0.0458, 0.0415, 0.0519, 0.0617, 0.0601, 0.0452, 0.0335, 0.032, 0.0518, 0.0397, 0.0507, 0.0508, 0.0583, 0.029, 0.049, 0.0507, 0.0465, 0.0511, 0.0423, 0.0459, 0.0591, 0.0406, 0.0536, 0.0567, 0.0459, 0.0488, 0.041, 0.0536, 0.0422, 0.039, 0.057, 0.0534, 0.0559, 0.0328, 0.0328, 0.0432, 0.0442, 498.4146, 498.7709, 496.8735, 482.2647, 513.1748, 508.3904, 0.0645, 0.0411, 0.0362, 0.0452, 0.0406, 0.0489, 0.0519, 0.0555, 0.0307, 0.0456, 0.0523, 0.0678, 0.0576, 0.0452, 0.0535, 0.0659, 0.0525, 0.049, 0.0736, 0.053, 0.0581, 0.056, 0.0442, 0.0506, 0.0477, 0.0569, 0.0511
...<truncated>
```


### `assets/mock_seasonal.json`

```
[4.7839, 4.7514, 4.8759, 6.1493, 4.9799, 3.0214, 5.8835, 5.1227, 5.3582, 6.0385, 6.4151, 8.0349, 7.5183, 6.9635, 5.9753, 5.8662, 8.0833, 10.0263, 8.4879, 8.6515, 10.0138, 7.4604, 9.6161, 11.2828, 12.339, 11.168, 12.6123, 12.9562, 14.3116, 12.0402, 15.1515, 12.6334, 11.5981, 15.2569, 15.4488, 18.8076, 19.1766, 17.0533, 20.8695, 18.8261, 20.9452, 21.3926, 22.7774, 24.619, 25.1467, 25.5242, 26.7964, 27.3729, 26.5595, 27.2794, 28.5164, 30.846, 30.607, 35.38, 31.5503, 32.042, 35.7618, 37.668, 37.2268, 38.6614, 40.4927, 39.1631, 38.1258, 40.4228, 43.6152, 40.9889, 44.1766, 45.4808, 45.6046, 48.1423, 48.9083, 52.5004, 50.93, 50.0676, 51.1202, 51.6958, 55.3504, 54.0488, 55.7683, 57.9695, 56.729, 58.3386, 56.9779, 59.0721, 60.7953, 63.2159, 65.322, 64.4371, 65.783, 66.5617, 68.8479, 69.4636, 69.4284, 68.3866, 72.1338, 72.2161, 74.3404, 73.2999, 77.1081, 74.4623, 78.2005, 76.7816, 76.6199, 76.4739, 78.6989, 81.8055, 81.6261, 82.1514, 78.256, 83.5728, 84.0113, 83.0079, 83.5309, 85.0913, 88.2882, 84.7074, 86.2201, 89.4588, 87.2834, 87.9247, 89.1178, 87.5908, 90.2454, 88.5448, 92.1116, 91.1938, 94.9997, 92.3616, 94.3347, 90.657, 92.7341, 93.687, 96.0852, 91.189, 95.4192, 95.0224, 96.6167, 95.5448, 94.6931, 93.9501, 92.9565, 95.1968, 94.6696, 98.0197, 94.0835, 95.4699, 92.6037, 94.3105, 95.2205, 95.9685, 96.7872, 94.41, 92.6405, 94.8221, 94.7088, 94.4479, 92.4162, 94.9023, 93.0491, 93.6616, 94.1973, 92.8543, 92.0044, 94.4182, 91.1494, 89.9153, 90.0834, 91.6769, 89.1492, 89.2501, 89.7462, 86.6468, 84.272, 86.7391, 86.0455, 84.1828, 85.7786, 84.7441, 81.685, 83.2837, 81.5256, 78.8935, 81.081, 79.6027, 77.8075, 78.951, 78.1264, 75.7426, 76.4115, 76.5233, 73.1095, 73.9161, 72.5041, 75.0435, 67.9852, 70.9459, 68.5585, 67.9733, 70.0731, 66.2969, 68.7523, 63.8016, 64.0575, 61.856, 60.6108, 60.7896, 59.3159, 59.0828, 54.6566, 59.8493, 55.6232, 57.5134, 52.4113, 53.3819, 56.7103, 49.6771, 47.7571, 48.1986, 48.7459, 48.0969, 48.0135, 44.7155, 41.7229, 42.5019, 44.0462, 41.9194, 37.3248, 39.2516, 40.4504, 40.5975, 37.3108, 35.9992, 32.7488, 32.4254, 32.8496, 32.6312, 31.8525, 29.3797, 27.4934, 27.1888, 25.7645, 27.6203, 22.3531, 24.4994, 24.8639, 25.6741, 22.7127, 23.3354, 20.4761, 19.2128, 18.5869, 21.1729, 19.6715, 18.2247, 21.8179, 16.1187, 16.4585, 15.3666, 13.9663, 17.1842, 15.3981, 10.4846, 11.4476, 12.1715, 12.2036, 8.5246, 6.7012, 6.7812, 9.1095, 8.6562, 8.9433, 6.9051, 5.8775, 4.3536, 7.5765, 7.3528, 8.0228, 7.4758, 5.7806, 7.8886, 5.4791, 4.5325, 4.6041, 4.3902, 1.9226, 5.3308, 5.4177, 4.4673, 3.9292, 5.5684, 7.6181, 5.1537, 4.4098, 4.3803, 5.2803, 3.6266, 5.4958, 5.9564, 8.8337, 7.7048, 8.0844, 5.7283, 8.0876, 5.6791, 8.1818, 8.6878, 7.2758, 11.8101, 10.0672, 6.7866, 10.9138, 9.9362, 11.0309, 12.2276, 12.6332, 9.5251, 11.4007, 14.8689, 16.2509, 17.7599, 18.2328, 13.0839, 17.9767, 14.6192, 20.6006, 19.2619, 17.8867, 23.3443, 22.0405, 18.9967, 23.0459, 22.2944, 24.356, 24.3015, 27.8818, 24.5203, 27.9761, 31.0768, 27.9809, 30.413, 31.2852, 30.8418, 33.7511, 33.8359, 33.1934, 38.1782, 37.6557, 37.157, 37.3981, 38.1276, 42.1066, 41.0012, 42.875, 42.8778, 45.0509, 44.927, 47.2238, 46.782, 49.2815, 50.0269, 49.8251, 49.692, 50.5527, 51.832, 52.4453, 56.1977, 55.8038, 59.2007, 61.2332, 58.8154, 57.4178, 60.1237, 61.5338, 60.3596, 63.156, 64.8952, 64.7018, 65.1571, 66.0826, 70.8036, 68.7784, 68.6581, 70.2193, 73.076, 71.473, 74.0676, 75.2703, 76.1243, 77.9105, 75.7832, 79.0934, 76.9773, 78.3306, 81.4647, 81.6351, 81.1212, 80.0327, 83.3489, 82.2319, 82.447, 83.4998, 85.4645, 82.5194, 86.8312, 87.1227, 86.5393, 89.508, 89.3882, 88.0617, 88.4477, 90.9582, 88.0852, 90.2849, 90.2424, 91.6794, 92.257, 92.3413, 94.3141, 93.2594, 92.7298, 91.6504, 94.7932, 94.7255, 96.3116, 93.1952, 94.5159, 94.527, 94.6011, 94.8443, 92.3318, 96.1946, 95.9691, 96.6957, 98.3637, 94.4787, 94.8494, 94.8434, 97.6458, 92.0182, 95.2049, 92.1101, 90.263, 90.9021, 91.6377, 95.0702, 91.8997, 92.5761, 90.8666, 93.2026, 90.2874, 93.5511, 89.6994, 89.6366, 90.5434, 89.9526, 91.5897, 89.1518, 86.6593, 87.1121, 86.557, 87.9494, 86.8657, 85.5218, 86.3992, 83.6793, 83.6284, 83.4486, 84.7987, 81.672, 80.5919, 78.8328, 80.6996, 78.044, 78.6821, 74.5418, 78.1737, 73.813, 73.8346, 75.7664, 71.3431, 72.3808, 71.7559, 69.2809, 71.9174, 71.266, 65.1662, 65.0473, 67.5221, 63.4364, 63.8818, 63.7261, 63.6301, 61.312, 60.8387, 58.5747, 60.7176, 57.0712, 57.3552, 55.1062, 55.0585, 53.6698, 52.6994, 50.4422, 52.1763, 52.1211, 49.3831, 49.1356, 48.314, 47.47, 45.556, 44.4447, 44.4505, 42.2891, 39.7601, 40.296, 36.4715, 38.3443, 38.0278, 38.3222, 35.7621, 36.0013, 33.6895, 31.244, 29.7593, 29.5276, 28.0478, 30.1719, 28.0441, 28.3555, 26.7654, 27.3876, 26.5896, 23.2211, 22.9796, 21.8065, 19.4015, 22.0874, 22.1126, 19.5888, 18.1421, 19.8166, 13.9762, 17.0887, 16.3821, 14.6717, 16.9926, 15.7552, 13.3183, 13.0081, 12.0018, 11.2792, 9.5667, 13.0988, 12.0893, 11.2818, 7.9992, 12.3374, 11.7812, 8.7075, 6.7033, 6.4142, 8.016, 5.7132, 6.7464, 8.1663, 4.0294, 9.0753, 8.2147, 3.828, 3.4033, 4.9598, 3.7207, 4.7757, 1.2246, 4.6998, 7.715, 3.3251, 4.6011, 2.8084, 6.0537, 4.8333, 8.1786, 5.6952, 4.9655, 6.5091, 5.1053, 4.0577, 6.7425, 6.271, 7.0472, 7.6459, 6.6227, 8.3462, 10.6662, 6.7637, 11.4171, 8.8519, 8.4908, 13.7253, 10.7798, 13.0845, 12.7554, 9.5265, 12.7651, 12.3502, 11.6973, 11.6415, 15.0586, 16.4134, 16.7532, 16.3638, 16.2958, 16.6999, 18.0492, 18.0489, 20.0245, 21.0402, 20.4895, 22.2577, 22.2506, 24.1824, 25.76, 26.0887, 27.3634, 25.022, 27.7875, 29.6364, 31.0588, 28.5852, 32.1152, 34.5889, 33.4224, 34.467, 36.2109, 36.8389, 38.4197, 38.4618, 39.5329, 39.0866, 40.9653, 44.0655, 42.8482, 43.8735, 44.3637, 47.2588, 45.7748, 49.4029, 47.6129, 48.4133, 51.2906, 52.1581, 53.3895, 55.5158, 54.2803, 56.5382, 55.4808, 59.7029, 59.7278, 60.5446, 60.9777, 60.4685, 62.1012, 64.2844, 64.436, 65.5765, 64.27, 67.4924, 67.0857, 67.3928, 69.671, 67.9939, 72.6923, 73.778, 73.2646, 72.6878, 73.5826, 75.5843, 72.9733, 77.665, 79.1632, 78.4576, 78.929, 80.6745, 81.6441, 83.461, 82.534, 82.1428, 83.1409, 84.7449, 87.4291, 85.2205, 84.7112, 85.5567, 84.2716, 88.4473, 86.429, 87.1209, 86.4887, 88.2385, 91.2119, 88.8168, 88.5646, 91.842, 91.4286, 92.9345, 93.7227, 92.9045, 91.2556, 93.281, 94.4608, 93.0588, 91.9791, 95.9703, 93.8534, 94.0012, 94.7565, 96.3745, 95.2991, 94.4573, 96.0125, 98.2205, 97.071, 95.9665, 93.2202, 100.4568, 92.9345, 94.0573, 93.4264, 96.7567, 94.7657, 91.7926, 94.1118, 94.287, 92.7245, 91.087, 91.5243, 91.6309, 91.9745, 90.7947, 92.2918, 89.3589, 87.7622, 88.8064, 91.476, 88.1114, 91.9822, 88.6898, 85.6695, 85.6104, 86.6551, 83.9344, 88.6567, 82.9857, 84.8542, 85.3677, 82.3426, 80.326, 80.7085, 80.9449, 80.9842, 79.0105, 76.0662, 77.4578, 75.5989, 72.5361, 73.9543, 72.0298, 74.2251, 75.4162, 74.4326, 69.9189, 69.2019, 70.051, 69.7155, 66.5926, 66.0958, 68.7013, 64.3036, 62.8038, 62.7722, 62.5164, 59.1634, 59.3672, 57.0295, 57.3968, 59.6609, 56.9863, 54.7602, 52.5223, 52.476, 53.539, 50.1391, 52.1756, 46.9579, 47.6021, 49.671, 43.0662, 46.8836, 41.3493, 43.0193, 41.927, 42.4574, 35.5284, 36.4274, 37.5041, 37.463, 35.1284, 35.2039, 32.8298, 33.0884, 31.5091, 32.2424, 31.8543, 32.2464, 28.2367, 27.8785, 27.5716, 26.2128, 25.7435, 22.6561, 24.7467, 22.0183, 21.1703, 19.4513, 22.8161, 20.1185, 21.3227, 19.2904, 15.6252, 15.8681, 17.9594, 15.0187, 13.8925, 14.6361, 14.0336, 12.1922, 12.6489, 11.8661, 8.1624, 12.283, 9.8118, 12.504, 8.3593, 7.2713, 10.9976, 6.4334, 7.4712, 8.9557, 7.2194, 4.9049, 7.3648, 6.5063, 8.4591, 7.1726, 10.6203, 3.3958, 5.6511, 3.9238, 7.1171, 6.5002, 6.3023, 3.9304, 3.9251, 1.9662, 7.5643, 5.773, 4.6515, 6.4966, 6.2976, 6.7304, 2.9497, 4.453, 5.9768, 6.3978, 7.5025, 4.7767, 6.7242, 10.1058, 8.1206, 8.694, 7.6851, 9.2111, 11.1314, 8.9709, 11.0521, 8.328, 10.9697, 11.9036, 9.9009, 13.1762, 11.0345, 11.376, 13.1364, 16.608, 14.7628, 16.3089, 14.4303, 16.2835, 15.0577, 16.637, 19.4337, 18.8243, 21.7754, 20.0258, 20.7777, 23.7485, 21.6328, 21.1114, 26.141, 25.6206, 22.5737, 26.722, 26.8943, 30.9728, 28.1278, 29.2259, 29.6166, 30.2953, 31.7285, 31.5364, 33.0623, 33.1253, 34.2223, 35.0682, 37.5714, 38.2786, 38.4205, 41.9383, 42.1137, 42.5065, 43.4304, 47.4903, 46.4377, 47.7699, 44.1581, 48.0377, 46.7435, 50.3424, 52.5228, 51.9317, 54.6999, 53.3649, 56.7374, 54.5151, 57.4216, 59.4088, 60.5437, 60.505, 64.5564, 61.9707, 62.8553, 60.3255, 68.0443, 65.7562, 65.4567, 70.105, 70.8711, 70.0655, 68.6263, 72.273, 71.5679, 71.5886, 75.7435, 71.8018, 76.6617, 77.298, 80.4708, 78.6543, 75.8378, 78.4164, 80.4986, 81.6514, 83.291, 79.5669, 82.6826, 85.2876, 86.788, 87.8863, 85.1156, 87.9364, 89.4294, 89.2132, 89.0381, 89.7127, 87.7863, 87.2818, 91.3898, 89.8387, 90.7417, 90.3209, 90.9852, 92.2895, 90.7534, 92.7146, 93.2863, 93.0862, 92.3512, 94.8903, 96.7423, 93.2335, 96.168, 95.8102, 90.6351, 93.5907, 93.875, 96.0763, 95.2399, 95.7009, 95.9544, 93.6409, 93.8373, 93.0693, 94.5734, 94.5539, 94.8718, 93.3853, 95.6761, 95.511, 96.2531, 94.7774, 93.7449, 94.2109, 93.8319, 91.6341, 94.2202, 94.5181, 94.54, 91.2419, 91.2703, 90.9838, 89.9117, 90.3816, 88.7894, 88.1804, 90.5911, 87.6632, 86.9624, 87.613, 85.9264, 85.5322, 84.3767, 82.0022, 83.131, 80.8011, 82.135, 79.3459, 81.7766, 80.7944, 79.3461, 78.0164, 78.0187, 73.6614, 74.1657, 74.7776, 74.6964, 73.2257, 70.9931, 71.8447, 70.0981, 68.3001, 69.5121, 67.8086, 69.0155, 67.308, 66.4929, 64.9473, 62.264, 61.1889, 59.4401, 65.0144, 58.1206, 58.4643, 57.2736, 56.4239, 54.9976, 52.0965, 52.4247, 50.8571, 51.7727, 49.9554, 48.9692, 49.3421, 47.1629, 48.455, 45.4987, 45.4387, 42.1384, 44.1398, 41.5781, 41.8341, 40.7552, 39.044, 38.6793, 36.8297, 38.4874, 36.9367, 35.3328, 31.9201, 31.1303, 30.8806, 30.6147, 30.769, 27.8765, 30.1211, 27.5054, 23.636, 25.1085, 21.1216, 23.0615, 22.5674, 21.6901, 21.3456, 20.4834, 19.4388, 21.8522, 20.7043, 16.4674, 19.3745, 14.3905, 14.9453, 17.8158, 11.5459, 15.1613, 11.8298, 12.9222, 12.3136, 14.5932, 10.3131, 10.9253, 11.4878, 9.7068, 8.4076, 9.1273, 8.9513, 9.7515, 6.6451, 7.0401, 7.8726, 6.832, 6.5198, 6.5078, 5.2458, 4.8671, 6.5258, 4.041, 7.2718, 5.7109, 3.3527, 5.5958, 5.3473, 6.1861, 6.749, 4.4504, 3.9491, 4.1339, 3.6247, 4.0516, 5.6942, 5.4079, 4.7867, 4.5763, 6.3865, 5.3143, 4.7294, 6.3058, 6.401, 5.4777, 8.6289, 8.9829, 8.121, 8.622, 9.6019, 9.1698, 10.9777, 12.5685, 10.668, 11.6892, 12.8391, 11.5403, 10.8914, 14.4972, 11.3568, 16.9824, 16.2108, 15.8095, 16.2439, 19.4496, 16.0743, 16.4399, 17.9847, 19.2057, 18.8197, 21.3053, 22.0374, 21.3849, 25.7956, 22.1009, 23.0843, 26.3857, 26.344, 28.1266, 27.427, 27.6713, 29.1287, 29.9568, 32.6639, 32.0593, 34.8324, 35.4912, 37.0961, 37.8193, 38.5842, 39.5079, 38.5291, 39.8674, 38.4757, 42.6235, 42.1438, 44.7286, 45.8922, 44.0499, 45.5361, 47.5131, 48.2419, 50.2505, 49.2485, 52.0336, 50.4104, 50.6893, 55.974, 53.9157, 56.0057, 55.6881, 56.6658, 59.1415, 60.0807, 63.1207, 63.2666, 63.1401, 65.6013, 67.0657, 66.9267, 66.6113, 69.6515, 71.6706, 70.8938, 73.4347, 73.0344, 73.7733, 71.7095, 72.5082, 74.9264, 73.3068, 75.7195, 78.6953, 77.1688, 79.7252, 77.6437, 79.6611, 82.6532, 82.1326, 82.9418, 85.7346, 85.778, 83.1567, 87.0716, 84.778, 86.2636, 86.8847, 89.0257, 86.2587, 89.1488, 89.7572, 90.67, 89.5999, 91.8644, 89.7165, 92.9096, 92.1029, 89.8402, 92.898, 92.8808, 92.3838, 92.0979, 93.1064, 91.5472, 96.4694, 92.4429, 93.3244, 96.1936, 95.4926, 95.8129, 97.1038, 94.4354, 93.3874, 96.2557, 96.1944, 94.499, 93.0588, 96.7624, 95.6925, 92.6723, 95.7849, 92.3148, 93.5924, 92.4504, 92.9766, 92.4604, 92.5467, 94.2886, 93.3591, 93.555, 93.9247, 91.3813, 91.6944, 91.9962, 93.344, 91.7079, 91.9878, 90.9812, 90.557, 85.794, 89.2739, 87.7355, 86.9432, 85.4991, 85.063, 85.7191, 85.732, 81.5861, 83.5046, 81.6653, 80.3936, 81.707, 79.9085, 78.682, 77.7717, 78.5885, 77.3792, 74.9715, 74.5494, 74.7269, 74.7862, 71.7702, 73.2322, 70.4085, 74.8703, 68.4915, 67.8326, 68.4146, 65.8672, 67.5512, 68.093, 63.8833, 63.7909, 61.8157, 61.6468, 62.8435, 61.0812, 57.4227, 60.133, 58.5886, 56.6032, 53.9102, 54.0206, 51.7499, 52.6643, 50.507, 46.6476, 50.4076, 48.3243, 48.6746, 45.8737, 45.3837, 45.7993, 43.7361, 42.6178, 46.0425, 40.0067, 39.5579, 40.9542, 36.2002, 34.9067, 33.3871, 35.4096, 34.6287, 33.8915, 29.8461, 32.491, 31.8929, 29.0753, 28.4044, 28.5134, 27.2059, 26.7845, 23.3702, 23.9271, 21.5724, 23.5527, 22.6288, 20.9263, 20.7746, 17.2637, 20.1688, 17.4793, 17.6872, 18.4956, 15.0325, 17.3239, 15.0913, 14.3414, 14.5441, 11.2676, 13.475, 12.1396, 13.0834, 10.7036, 10.0023, 10.639, 9.5517, 7.4552, 8.52, 10.3269, 7.92, 6.4081, 8.462, 4.3026, 3.6174, 6.5271, 6.561, 6.3973, 4.5255, 5.4579, 3.4256, 5.7143, 2.8445, 4.9474, 5.4887, 2.4079, 4.227, 3.1822, 4.3244, 0.01, 2.5184, 0.1613, 0.01, 4.4283, 3.0488, 0.8545, 2.4054, 3.0577, 2.3866, 5.5072, 3.0685, 0.01, 1.777, 1.1364, 1.9657, 2.3247, 1.362, 0.7474, 4.4576, 3.348, 1.487, 3.5554, 3.5931, 4.5274, 1.0912, 3.8932, 1.8625, 4.5592, 2.5918, 0.01, 3.0457, 0.3746, 2.7848, 3.0843, 2.632, 1.0673, 1.689, 2.162, 3.1241, 5.4977, 4.6984, 2.9974, 5.6885, 1.7987, 4.9524, 3.7371, 4.4305, 5.1107, 3.2115, 5.5113, 4.375, 8.087, 4.0939, 6.5357, 5.6849, 3.036, 6.9216, 3.3741, 1.7013, 6.4142, 6.8231, 5.3834, 5.0, 7.8534, 6.6323, 4.6676, 6.8357, 3.9606, 5.0743, 4.4233, 2.7872, 4.4085, 8.0951, 4.7019, 5.6104, 4.7806, 7.9069, 6.6964, 7.5165, 6.7072, 5.1942, 5.5744, 8.3752, 7.1167, 8.4474, 2.6242, 9.1149, 6.7898, 10.8703, 6.9243, 7.8116, 8.8376, 6.6103, 9.3091, 7.1073, 10.0829, 9.4121, 9.0378, 7.2789, 8.1192, 8.9536, 5.5211, 7.8098, 10.1095, 6.8638, 10.7469, 7.947, 9.434, 7.2859, 9.7907, 9.8136, 10.878, 11.2569, 11.1569, 9.6393, 7.5343, 11.4053, 7.5079, 8.8488, 10.6537, 10.2528, 12.0148, 9.6552, 7.1604, 9.3151, 10.0199, 9.2618, 11.9335, 9.4688, 7.7099, 10.5856, 8.1837, 12.473, 10.3444, 7.8485, 11.5849, 13.5301, 6.9787, 11.346, 10.7222, 10.2626, 10.515, 10.7369, 8.9567, 12.1079, 7.9605, 12.9141, 12.2573, 8.1742, 9.9311, 12.0529, 9.1687, 9.6418, 9.2601, 11.5914, 8.9033, 9.773, 10.5691, 9.4166, 8.3959, 7.6489, 7.3478, 7.8178, 6.9672, 8.4166, 9.9144, 9.5008, 8.1336, 7.7989, 7.7865, 7.1439, 7.3196, 10.1911, 6.9573, 9.0333, 8.022, 12.5314, 6.133, 9.056, 8.2017, 7.4807, 9.044, 8.7371, 6.8317, 9.0047, 8.8781, 10.1865, 7.2342, 10.2808, 7.7048, 4.9599, 8.6316, 7.6468, 6.3669, 9.0108, 6.8233, 8.9751, 7.244, 8.4112, 8.222, 7.8291, 5.4872, 8.3303, 6.8984, 9.7838, 7.4126, 7.7352, 5.3748, 5.8263, 6.4937, 4.5059, 4.4191, 5.9244, 5.8832, 6.2159, 7.5012, 5.1379, 6.6727, 5.3476, 5.1869, 5.7771, 2.2038, 5.3192, 4.1769, 3.6545, 2.1342, 4.2163, 1.981, 6.0194, 4.273, 2.0271, 3.459, 5.5214, 3.4172, 3.809, 3.5992, 3.8836, 1.4503, 3.8567, 3.0137, 8.2084, 3.3862, 2.9513, 5.0477, 3.2623, 3.8089, 2.0514, 5.6305, 2.2924, 3.7346, 4.8091, 2.5941, 2.9924, 3.8156, 3.9045, 1.1914, 3.0464, 1.3053, 1.5561, 1.6538, 4.4043, 4.9308, 6.4013, 2.3671, 1.1504, 0.9205, 4.75, 0.01, 1.7965, 1.3921, 4.8373, 0.6651, 3.8123, 4.1063, 1.8645, 2.7931, 0.01, 1.9285, 1.724, 0.01, 2.6468, 2.548, 0.01, 0.01, 0.877, 1.2062, 1.5414, 0.5666, 0.4968, 2.475, 1.3109, 1.874, 1.702, 2.7466, 1.8454, 3.3871, 2.2691, 3.4698, 4.0304, 0.01, 3.5953, 4.9155, 0.8132, 2.2567, 1.8029, 3.036, 3.1925, 1.448, 4.4619, 1.2509, 2.3729, 4.1618, 5.6043, 1.9544, 3.9477, 3.1566, 0.5068, 1.503, 1.1855, 2.2959, 2.4854, 3.2135, 5.1505, 3.9314, 7.3422, 3.0637, 4.6802, 4.1866, 5.8946, 3.1376, 5.0456, 6.2944, 2.8437, 3.8877, 4.8325, 3.5008, 2.918, 7.0586, 4.0445, 7.0704, 3.7876, 4.3213, 5.91, 4.4881, 3.5324, 3.8849, 5.4583, 4.1351, 5.3517, 1.9195, 4.5543, 5.2074, 4.7956, 4.161, 4.4131, 5.9313, 8.1433, 3.9024, 5.6729, 7.6401, 6.7633, 6.208, 5.7594, 6.542, 6.4569, 3.4149, 5.7067, 3.9414, 8.8873, 8.2692, 5.937, 8.4101, 10.9587, 6.4335, 9.3987, 8.0745, 7.2559, 6.0316, 6.9353, 7.2546, 4.4503, 8.7957, 7.5925, 8.8381, 7.5934, 12.7917, 5.8793, 7.0355, 9.5615, 12.1089, 7.1488, 11.4462, 7.3905, 10.7685, 9.0025, 12.2941, 11.6803, 8.0926, 13.5231, 7.4872, 8.7553, 11.9885, 8.2237, 8.4115, 9.6156, 10.1428, 10.0877, 8.4295, 9.8828, 10.5814, 11.9172, 10.1878, 7.8486, 9.8014, 8.8568, 11.7934, 7.3271, 10.7033, 8.2873, 11.8133, 11.0799, 10.2682, 10.7109, 9.4605, 10.9899, 9.9103, 11.0882, 10.0841, 6.2773, 9.2295, 12.2753, 12.175, 10.1483, 9.7957, 9.4734, 10.9116, 10.822, 8.4706, 10.9156, 11.7545, 8.4531, 9.5834, 10.5355, 10.899, 9.1569, 8.9658, 9.0322, 10.2734, 10.3049, 9.5961, 7.5412, 7.9131, 10.4621, 7.6325, 9.7592, 8.8324, 8.4225, 9.3167, 11.7403, 9.6066, 8.676, 6.8541, 9.7757, 10.6542, 9.8005, 10.5774, 9.4809, 6.7474, 8.57, 9.3585, 9.5852, 7.6954, 8.2702, 8.8431, 10.1292, 9.6571, 9.0446, 8.5943, 6.386, 6.1956, 7.6314, 9.2528, 8.204, 8.3724, 5.1974, 5.1756, 8.9656, 7.1509, 5.1488, 8.3585, 7.3378, 11.293, 8.6554, 7.2146, 6.3818, 6.8705, 6.2815, 4.1904, 7.6529, 6.5868, 1.9552, 5.0467, 4.6696, 7.2969, 5.6723, 7.8825, 3.1773, 3.8642, 6.9921, 4.3226, 3.6317, 4.3442, 3.9095, 5.7737, 6.5579, 2.4168, 4.2781, 4.6533, 4.6735, 5.7191, 4.5688, 3.4398, 2.563, 1.4159, 3.5194, 4.0216, 5.3075, 3.2841, 7.2633, 3.591, 2.9097, 4.1358, 2.0405, 3.7422, 0.2665, 4.3118, 3.334, 3.4874, 3.3428, 1.1403, 1.6447, 5.2855, 3.6224, 1.9307, 3.4058, 3.335, 5.6058, 4.292, 3.1971, 1.8621, 5.1686, 2.2996, 1.1067, 0.5444, 1.0452, 1.7584, 3.4201, 3.611, 3.9389, 2.4146, 3.407, 5.5253, 3.4801, 4.4991, 0.01, 0.9481, 0.9199, 0.01, 2.1343, 4.1039, 1.2056, 2.297, 0.7199, 4.2624, 5.4387, 3.2492, 6.8574, 2.2931, 5.7508, 4.6478, 7.7574, 6.8264, 2.7384, 2.4331, 4.7876, 5.78, 6.5142, 7.1281, 8.3985, 5.6648, 5.3588, 8.8041, 8.2695, 9.0611, 8.678, 9.8247, 12.7771, 8.594, 9.6907, 13.0098, 14.935, 12.8297, 11.8159, 11.2974, 15.296, 13.654, 19.4306, 20.3349, 17.8331, 18.9084, 18.705, 21.3111, 20.3248, 21.1696, 23.874, 23.545, 26.5875, 21.7338, 24.166, 24.4657, 27.9179, 26.7596, 29.259, 27.4678, 29.9555, 34.4277, 34.334, 29.9614, 34.2372, 35.8486, 34.7951, 34.8147, 38.4007, 38.5044, 40.1329, 40.0231, 44.7177, 40.8151, 42.4104, 44.8986, 46.1986, 44.0099, 49.0028, 45.8499, 49.0279, 49.5395, 49.9202, 53.6627, 49.4226, 52.8258, 53.5449, 57.186, 59.1712, 58.9774, 60.872, 56.403, 60.9413, 59.5723, 62.0299, 63.8988, 64.6067, 67.4545, 63.3418, 65.9671, 65.9027, 66.5596, 71.4117, 71.0523, 73.9794, 74.2778, 73.447, 72.7241, 76.4883, 74.7735, 74.8098, 75.6686, 78.3493, 80.8237, 79.7495, 82.2382, 81.2551, 81.1801, 86.5881, 85.909, 84.1282, 83.2237, 82.6813, 86.3029, 85.9067, 87.5623, 87.9007, 88.607, 90.1844, 89.4425, 88.3675, 88.7597, 91.234, 89.1761, 92.2665, 92.6459, 91.8745, 91.114, 89.9473, 91.2313, 90.7633, 93.5067, 93.047, 92.5523, 95.4442, 94.5103, 94.5476, 95.4522, 96.2872, 92.4245, 95.9637, 92.2438, 93.7977, 96.5182, 96.0261, 95.9543, 95.1768, 96.9482, 92.4213, 94.1573, 94.5473, 97.3693, 95.6752, 95.491, 92.5684, 96.8412, 93.91, 91.4084, 96.1299, 90.1458, 90.6373, 91.2236, 89.7184, 91.5747, 90.7744, 90.0857, 92.2934, 86.5202, 90.5206, 86.7156, 89.6094, 89.7197, 89.7423, 85.4151, 83.3499, 84.9639, 83.7527, 80.5332, 83.6097, 80.4358, 83.2811, 81.8493, 79.2486, 78.9797, 77.8027, 76.7082, 81.2564, 74.4066, 75.0837, 75.6657, 72.7683, 71.5572, 69.6508, 72.5667, 70.971, 69.7227, 65.8604, 67.9761, 66.7377, 64.8838, 62.3997, 63.4467, 61.286, 61.7669, 60.7909, 59.2325, 60.1021, 57.7151, 57.6392, 56.5013, 56.4113, 52.4759, 52.4252, 53.3459, 51.212, 51.6887, 50.1362, 47.3701, 48.3776, 45.6918, 46.5359, 46.907, 44.7918, 45.6201, 38.8936, 39.4377, 38.9046, 37.7527, 37.9664, 35.9161, 37.1944, 36.0862, 34.3105, 31.2609, 32.6975, 31.9936, 30.6278, 29.2663, 30.456, 26.6774, 26.1052, 26.8221, 25.7955, 25.468, 22.965, 20.6337, 19.9771, 20.4255, 21.5181, 20.7868, 16.158, 18.981, 17.3628, 15.2594, 19.4534, 15.9239, 16.1463, 18.1504, 12.8383, 12.543, 14.8174, 10.0859, 11.6399, 11.0982, 12.0026, 10.6926, 8.271, 10.6745, 10.0787, 6.8489, 7.0279, 6.9251, 5.6641, 8.6797, 6.312, 6.1166, 4.733, 5.4749, 8.0921, 6.8061, 3.0976, 3.9004, 6.2209, 4.4241, 4.2665, 3.7521, 3.0141, 5.1429, 4.4403, 6.7534, 4.9963, 3.7235, 6.3082, 7.4042, 4.4201, 3.1298, 5.1462, 4.0418, 8.752, 4.4517, 6.3062, 9.293, 8.695, 6.9433, 6.885, 8.1532, 8.4508, 11.4607, 9.4087, 8.0596, 12.026, 12.5837, 11.1888, 13.2056, 12.6607, 11.328, 13.5884, 12.5821, 13.954, 14.1197, 16.6665, 17.1022, 15.5141, 16.80
...<truncated>
```


### `assets/mock_steady.json`

```
[50.1774, 50.1621, 48.2776, 47.481, 49.2367, 51.7367, 51.6358, 52.3998, 49.9103, 53.9292, 48.3077, 50.8816, 49.5168, 52.9828, 50.0645, 47.9358, 50.4837, 47.756, 48.2555, 51.8527, 49.9594, 51.7112, 48.2527, 52.5936, 49.6494, 46.0853, 48.2086, 47.2051, 47.0724, 51.021, 50.7313, 51.1816, 48.5254, 51.0634, 47.0639, 49.0342, 49.6825, 48.8594, 50.2009, 49.9545, 50.9422, 50.607, 51.9106, 49.0395, 51.303, 50.555, 50.4862, 50.2674, 45.687, 53.835, 48.1694, 48.4071, 49.3554, 47.38, 53.4918, 48.7606, 48.6324, 48.3406, 50.8741, 52.7917, 50.5455, 48.4516, 52.6456, 50.5671, 48.5552, 49.856, 50.2627, 50.4742, 50.0032, 50.0478, 53.7256, 55.6674, 50.346, 48.1656, 47.8995, 51.6263, 46.7519, 49.6364, 50.7899, 50.3633, 49.1907, 53.6755, 51.5925, 49.0583, 51.5665, 49.6321, 48.9675, 49.1848, 52.365, 48.8819, 49.8574, 52.0401, 50.6082, 50.4808, 48.6694, 54.6617, 51.6366, 45.2629, 52.6084, 47.4279, 47.2559, 48.6041, 52.5157, 49.4552, 50.4212, 46.66, 52.151, 49.4655, 47.4214, 50.6496, 50.9797, 50.17, 52.5828, 51.1673, 53.0044, 49.8904, 50.2191, 51.7284, 52.3173, 49.3338, 53.7717, 50.2307, 50.0097, 50.8837, 48.2886, 46.708, 54.1115, 52.1285, 48.1521, 50.2742, 51.2471, 49.0241, 52.8662, 49.011, 51.9848, 48.7391, 51.0143, 53.0997, 50.9489, 49.0238, 50.4433, 51.4497, 49.1439, 49.2101, 53.524, 52.7939, 48.6735, 47.3634, 47.4332, 47.7704, 49.4321, 49.8486, 47.2325, 50.3092, 44.1936, 50.2797, 53.2156, 47.5698, 44.2406, 50.3331, 51.4013, 49.1832, 50.4575, 52.5821, 53.3787, 51.196, 47.528, 50.3388, 47.9994, 49.9689, 49.5172, 48.8581, 53.0623, 49.5447, 47.0381, 50.7591, 51.3722, 50.1021, 48.4503, 49.1352, 48.8224, 48.9906, 48.5964, 49.5579, 47.2884, 50.4935, 48.4353, 51.7542, 50.7625, 53.0115, 47.2714, 52.8224, 49.7669, 51.2378, 51.7795, 51.9224, 50.1232, 50.9091, 49.8166, 51.4553, 48.2165, 50.8091, 53.0788, 54.0129, 52.1362, 52.9227, 51.0472, 52.1076, 53.8901, 49.8915, 46.8245, 49.6594, 50.372, 48.3521, 47.6043, 49.014, 47.54, 51.8253, 53.4094, 51.3508, 47.2108, 50.4141, 48.2834, 50.5595, 48.3537, 51.9657, 51.4046, 49.3588, 51.0089, 50.1264, 53.4549, 46.1969, 50.8258, 52.486, 49.2664, 49.0018, 50.3546, 48.1605, 48.9268, 47.9516, 49.6449, 45.816, 48.7945, 51.1169, 49.2429, 49.1762, 51.7979, 53.1556, 48.5255, 52.4099, 46.499, 48.3083, 51.0716, 50.5154, 50.2382, 51.7297, 49.5946, 51.525, 49.022, 48.745, 46.2502, 51.3527, 48.2644, 52.0151, 52.0194, 49.5556, 50.1745, 50.3956, 49.5899, 47.0828, 52.8915, 48.8644, 51.0589, 50.5827, 50.5793, 50.4379, 51.8242, 48.2585, 51.9841, 50.3317, 50.0785, 49.1698, 49.9092, 51.1583, 47.6561, 46.5588, 49.4505, 51.4042, 49.4019, 52.7424, 52.5921, 52.805, 52.9046, 50.5271, 51.1339, 48.6358, 44.629, 50.8108, 48.4565, 49.5977, 45.0292, 51.7951, 49.5965, 48.8185, 47.6902, 48.3748, 48.3062, 48.0579, 47.1739, 49.5804, 49.3237, 51.7188, 46.497, 49.1084, 48.2173, 50.6818, 49.8952, 51.698, 53.6764, 50.0021, 51.06, 48.5007, 49.7463, 50.0497, 54.2777, 47.7209, 49.7319, 50.0389, 48.4913, 50.7049, 50.0283, 50.2391, 48.5556, 51.4692, 48.1391, 49.2557, 51.6947, 52.7958, 48.4995, 51.2457, 49.8293, 52.088, 50.2229, 53.3941, 53.0652, 48.0535, 49.3464, 48.499, 49.8384, 46.3655, 46.6526, 52.0396, 50.6815, 51.507, 48.1906, 47.8978, 49.6129, 52.7657, 50.1508, 50.366, 52.3556, 50.7593, 50.3639, 50.8548, 49.6186, 53.1468, 49.7913, 48.559, 51.3632, 51.8933, 49.0565, 51.448, 52.1531, 43.2966, 46.5506, 50.801, 48.1389, 53.3729, 50.6481, 49.7968, 50.1235, 49.2402, 48.7162, 50.2437, 52.0058, 46.9059, 50.4816, 49.1008, 50.1722, 48.4964, 50.2877, 47.8557, 49.9594, 49.4794, 46.6695, 50.8365, 44.3763, 49.2486, 47.0188, 50.514, 49.3279, 51.6339, 49.5958, 49.5741, 52.2412, 49.0704, 49.9406, 52.8525, 51.3238, 53.9658, 50.9181, 48.9407, 52.1481, 51.8447, 49.4538, 51.0802, 49.8352, 50.9961, 50.0652, 49.7402, 53.3775, 50.9124, 47.339, 50.4168, 49.4017, 50.626, 50.4799, 50.7325, 46.6032, 46.8642, 52.8012, 49.9467, 52.6762, 51.0552, 48.9338, 46.623, 51.0239, 51.0346, 53.5024, 49.595, 50.1523, 46.3839, 48.7863, 50.4919, 49.2984, 48.2521, 52.8355, 53.2054, 47.8637, 50.1978, 49.143, 52.9696, 50.4749, 51.1946, 48.1299, 52.655, 49.4021, 53.7258, 52.1849, 48.6981, 51.0538, 50.4868, 49.5574, 51.4997, 49.9491, 50.0563, 48.7369, 48.5112, 50.1532, 50.3574, 52.1517, 47.2909, 50.225, 53.1878, 48.6439, 47.9131, 48.1468, 51.5829, 47.4677, 50.039, 48.6616, 48.8491, 49.3874, 49.2509, 49.2649, 49.8921, 50.5379, 49.093, 50.9412, 49.0408, 49.0918, 50.0837, 46.5675, 48.3581, 48.664, 47.9544, 49.5369, 48.5335, 48.1776, 52.032, 54.1649, 51.9879, 50.5962, 52.248, 50.6284, 49.4786, 50.223, 49.3283, 45.5904, 48.3173, 48.342, 50.9672, 49.038, 46.4005, 49.9428, 50.5734, 51.5386, 50.7286, 54.7812, 48.2652, 52.6343, 51.842, 48.7292, 49.7438, 48.5077, 49.7522, 49.1139, 49.1103, 49.9895, 47.9284, 52.2491, 51.7562, 48.6094, 51.7254, 47.8972, 46.432, 50.6298, 51.0931, 48.5213, 51.0524, 53.921, 56.0248, 48.2541, 50.6533, 47.5429, 55.663, 49.1283, 51.72, 50.1889, 47.8859, 49.5863, 47.2927, 48.7201, 49.4638, 50.4499, 45.4565, 49.8688, 55.106, 48.0965, 50.6517, 49.1537, 44.3556, 48.0092, 48.633, 51.694, 49.9298, 49.5594, 51.3464, 50.2583, 49.6701, 51.3364, 47.2475, 49.5875, 52.5067, 45.7197, 48.4187, 50.3126, 50.4583, 49.3273, 50.8598, 48.2269, 51.9589, 50.3894, 53.6304, 51.6932, 51.1032, 52.8322, 48.3935, 49.4135, 46.4996, 51.741, 53.3083, 49.029, 47.5657, 52.2497, 48.5254, 48.9912, 53.079, 49.2825, 49.6271, 53.2237, 51.1818, 47.8719, 53.0106, 46.5173, 52.015, 50.9804, 48.9532, 48.4422, 47.7752, 53.458, 50.2489, 48.7388, 51.1478, 52.4092, 50.6377, 51.394, 51.908, 50.1175, 50.6332, 49.7692, 48.0153, 47.466, 50.9012, 48.5832, 49.9579, 49.5046, 51.9406, 50.5599, 50.6129, 49.427, 49.724, 49.3202, 50.4992, 46.5518, 49.9605, 50.7351, 50.7865, 47.845, 49.1578, 49.2385, 48.9201, 47.5663, 47.8271, 50.0182, 50.771, 50.296, 48.257, 50.77, 49.6024, 49.0089, 48.1494, 48.9813, 51.434, 49.6321, 49.7537, 47.9979, 55.5808, 45.4701, 45.9442, 48.9503, 50.3988, 53.5471, 51.0276, 50.6016, 51.8393, 50.1902, 46.0898, 50.5097, 50.4346, 54.3393, 48.5581, 52.5148, 47.2796, 53.0656, 50.2652, 51.674, 51.4076, 51.3556, 50.1951, 48.688, 49.6806, 48.4691, 49.7746, 46.7823, 50.3445, 51.9546, 52.4038, 52.6276, 50.305, 49.6917, 49.4895, 50.7847, 51.4342, 50.6763, 47.3092, 52.7561, 50.955, 51.9814, 50.1038, 49.478, 53.8751, 47.8603, 46.6237, 53.9916, 50.4808, 49.1433, 48.8299, 45.9878, 53.7345, 51.4113, 51.02, 51.6493, 48.1707, 50.9168, 52.1714, 45.5902, 51.1919, 51.4942, 50.203, 48.2289, 49.6689, 49.0836, 50.7257, 48.9358, 51.5288, 48.9633, 48.9603, 52.3371, 49.6785, 51.0632, 47.4052, 53.0418, 47.0957, 50.1939, 49.5509, 48.4739, 49.4512, 52.4702, 53.4779, 49.5267, 49.2851, 46.2441, 53.4302, 47.8679, 48.1802, 50.2096, 51.4681, 53.0465, 54.3492, 51.5077, 49.0397, 48.6864, 49.643, 51.6089, 51.9098, 51.2339, 46.2111, 48.8677, 52.9134, 48.9338, 49.8393, 55.4536, 49.8521, 52.8469, 53.6774, 51.5164, 49.1302, 45.4399, 48.2821, 51.2866, 51.1839, 52.3445, 47.7883, 50.3989, 45.8998, 48.2344, 51.0586, 45.6355, 51.6334, 53.6613, 47.1278, 49.4823, 53.4369, 51.0013, 50.7947, 50.215, 47.2564, 49.7263, 52.5733, 50.174, 51.4434, 49.2765, 48.2571, 49.1376, 47.8939, 47.2754, 49.9602, 49.5424, 48.1192, 51.3921, 48.1368, 50.8512, 50.9968, 53.3319, 50.6841, 51.5792, 50.1494, 47.1856, 51.7114, 53.3588, 49.7514, 54.1876, 51.8477, 54.002, 49.6376, 48.2307, 50.112, 48.5475, 50.3969, 49.5167, 50.329, 49.8199, 48.0515, 48.9412, 52.5383, 49.873, 47.3183, 45.9664, 49.3551, 50.5576, 47.6396, 46.4254, 51.1216, 56.3882, 51.744, 48.0068, 49.7647, 49.7623, 47.4564, 47.1917, 49.7145, 51.5803, 50.5058, 48.6334, 51.0188, 51.0425, 48.0346, 47.5643, 51.2904, 52.2724, 50.8108, 52.3585, 52.934, 46.9433, 50.9572, 52.6319, 52.2672, 52.8544, 48.8248, 51.3635, 51.5526, 48.8037, 50.3814, 47.3366, 48.3023, 50.6551, 52.1028, 49.9984, 53.0682, 52.9786, 48.0343, 49.7972, 48.9931, 51.6465, 51.4363, 48.4126, 51.8482, 47.3587, 52.3557, 45.1217, 49.6418, 48.5842, 51.3519, 50.5051, 45.6169, 47.3092, 48.8257, 52.3676, 53.2625, 51.364, 52.8459, 48.4938, 50.8332, 50.254, 47.8404, 49.2992, 49.2863, 50.9122, 50.9908, 48.9031, 48.9362, 46.1987, 51.0106, 49.7932, 49.942, 51.0916, 49.0093, 48.0723, 48.7959, 49.3127, 50.4353, 48.7883, 49.1185, 47.3408, 49.1848, 46.5059, 48.6785, 51.0799, 50.3428, 49.6876, 49.5509, 51.8006, 47.6431, 50.8845, 48.5671, 50.5565, 49.6958, 48.9135, 50.6246, 48.7076, 47.5741, 49.034, 47.4906, 50.2903, 48.7842, 47.4722, 50.7286, 50.8036, 49.6309, 49.4154, 51.8266, 49.4577, 50.5161, 47.73, 51.1152, 49.7141, 51.0365, 49.4538, 48.4941, 47.596, 49.5874, 49.0812, 47.4056, 52.5018, 52.0513, 45.9307, 45.722, 46.9608, 50.3131, 55.3594, 47.6363, 49.3655, 52.6658, 49.0093, 52.2318, 51.8683, 49.9416, 51.7641, 51.8718, 47.1942, 49.6508, 48.7418, 50.0593, 47.5726, 48.0071, 50.1116, 49.9854, 52.7712, 50.4208, 52.8647, 52.6207, 52.2757, 42.9303, 48.454, 50.9269, 47.3536, 48.9442, 51.3372, 50.0951, 49.3023, 53.4649, 49.1558, 52.2227, 49.9006, 50.1983, 49.2829, 48.3188, 49.5568, 47.2686, 51.2223, 50.2261, 49.1198, 51.5888, 47.9155, 46.0648, 50.8429, 47.2451, 47.2094, 50.3259, 51.9326, 48.4429, 48.2156, 53.0125, 47.175, 51.6959, 50.8597, 48.5485, 49.7272, 48.2801, 48.6678, 49.6944, 48.5862, 46.7048, 51.4247, 48.7914, 51.5932, 49.6921, 55.6256, 51.1162, 47.3073, 47.2557, 48.2072, 49.1428, 46.6948, 47.748, 50.0303, 46.8739, 49.8001, 50.466, 47.9902, 51.2079, 50.1701, 52.1867, 51.3294, 49.5747, 49.6849, 50.9236, 50.7555, 46.6144, 51.8531, 48.8781, 52.2681, 50.3583, 48.7008, 49.9996, 52.881, 48.5009, 46.5808, 49.4886, 52.5188, 53.1763, 52.0655, 45.7595, 49.5257, 49.0439, 49.6471, 47.8101, 49.4499, 47.6318, 54.6402, 50.1199, 48.965, 47.521, 51.9158, 51.167, 52.324, 50.6933, 49.063, 48.5442, 48.4713, 49.1898, 51.3706, 48.8968, 50.5685, 51.5059, 49.0919, 51.957, 50.8406, 50.7663, 49.9481, 50.3468, 48.2792, 48.3231, 50.5005, 51.7888, 49.539, 51.0043, 49.5382, 48.4344, 50.025, 50.7685, 48.6225, 48.812, 49.0097, 50.3661, 47.05, 50.5486, 50.5232, 50.4107, 48.8756, 50.5599, 47.7934, 48.4776, 51.5981, 47.3427, 48.711, 48.8445, 51.2992, 47.1198, 48.822, 47.8293, 53.3122, 49.4472, 48.9613, 50.2274, 51.728, 45.8541, 47.8381, 50.4367, 48.7404, 47.1152, 48.5821, 50.3132, 50.6514, 53.9201, 46.7216, 47.0379, 47.5844, 51.9458, 48.2081, 46.9622, 51.6247, 48.5198, 52.2306, 50.4293, 48.6763, 51.7877, 51.6318, 53.0039, 49.1727, 49.363, 52.9442, 52.2231, 50.5143, 48.3, 47.9055, 47.1379, 48.0185, 49.5865, 47.6164, 51.4413, 44.8429, 48.3975, 48.6396, 51.2174, 51.0453, 47.5385, 50.4575, 47.9112, 50.5829, 47.5604, 50.2562, 52.9371, 54.0804, 52.1734, 47.9368, 48.962, 48.1336, 48.7797, 46.5044, 48.7393, 52.2356, 54.4687, 50.8694, 50.4065, 51.346, 46.8389, 48.3085, 51.6104, 49.5339, 49.2638, 51.7084, 46.1865, 52.3345, 48.3758, 51.1267, 50.0013, 51.4569, 51.9847, 47.9761, 51.6747, 51.0129, 52.8509, 48.9438, 52.3751, 51.4573, 49.1158, 52.0023, 49.2554, 51.8264, 49.7993, 50.2162, 50.1359, 52.028, 49.8266, 51.1614, 51.1993, 48.2815, 51.848, 52.9459, 51.0216, 50.8115, 48.2964, 52.5786, 46.9323, 50.2883, 53.0997, 48.622, 51.0791, 48.5474, 51.317, 47.1699, 49.1755, 48.7244, 50.2031, 46.5976, 53.0151, 50.6201, 49.0223, 52.978, 49.9524, 50.513, 50.5766, 53.1816, 50.315, 47.8646, 50.1788, 50.8397, 49.9483, 48.7007, 48.3836, 46.6061, 50.9623, 48.331, 48.715, 51.019, 52.1584, 47.9288, 49.2974, 49.2121, 51.0117, 47.92, 48.3955, 50.8401, 48.7935, 47.9428, 49.9777, 48.1673, 49.453, 52.5676, 49.6466, 49.7258, 49.0589, 46.9575, 50.3546, 52.0338, 52.5252, 55.7068, 49.4091, 50.4035, 47.8, 53.704, 51.9831, 51.9215, 51.1933, 49.7346, 44.9243, 52.5001, 51.3355, 50.5153, 48.3471, 51.735, 50.5974, 50.4011, 47.7862, 47.756, 49.8211, 49.7877, 47.9199, 53.4692, 49.1228, 49.8692, 50.5202, 52.9443, 48.4497, 49.6286, 47.451, 52.0977, 50.8523, 50.317, 50.5688, 53.6319, 49.7799, 48.8322, 48.8422, 52.0619, 51.1763, 50.9604, 48.7031, 51.093, 51.2018, 50.2121, 49.7365, 50.947, 50.7991, 49.0402, 49.3758, 46.5696, 51.1949, 49.7805, 53.0991, 46.4313, 50.3392, 51.186, 48.6028, 50.966, 50.5664, 51.059, 49.5333, 43.6465, 50.1618, 50.1882, 48.5376, 47.3494, 51.0459, 50.7929, 53.1585, 48.387, 52.371, 50.0342, 49.0074, 50.9984, 51.753, 48.9496, 53.1408, 50.0376, 47.5339, 47.7227, 52.1439, 48.8941, 50.1781, 50.2119, 49.7751, 51.9587, 49.0362, 47.7806, 45.3972, 49.763, 49.4719, 52.4765, 48.5918, 52.0464, 49.4219, 53.4414, 48.0526, 49.1977, 51.3573, 52.3769, 51.6879, 47.3298, 52.1766, 49.9814, 46.7438, 48.8824, 49.3032, 48.4016, 52.6118, 51.0738, 52.2788, 48.2396, 46.6599, 50.9043, 50.0681, 51.7265, 51.3203, 50.3118, 51.6024, 50.1533, 50.4103, 53.9569, 52.5794, 47.8961, 46.8361, 52.7146, 49.1272, 49.7367, 49.0237, 50.6542, 49.1722, 47.6193, 46.43, 45.2162, 47.8916, 52.4494, 48.1246, 49.953, 52.243, 52.6278, 48.886, 51.4226, 49.249, 46.1232, 51.9149, 50.2203, 48.2459, 50.412, 50.6673, 50.333, 48.0636, 49.6506, 50.6111, 51.5076, 49.5682, 47.8336, 50.8487, 52.4739, 48.8089, 51.0117, 52.0965, 52.1167, 52.6893, 50.5667, 52.8154, 52.7046, 48.3452, 50.6201, 50.4704, 48.6309, 49.3416, 45.984, 51.4207, 51.6978, 52.3018, 52.6974, 48.7901, 49.68, 53.0872, 52.1593, 48.5249, 45.2792, 49.5423, 47.863, 50.1253, 51.9418, 47.1057, 50.5496, 47.5685, 54.9648, 50.1032, 50.9503, 47.9119, 45.7345, 48.0823, 48.0865, 52.3452, 47.7049, 51.654, 50.843, 49.7769, 45.0271, 48.9007, 51.9415, 51.0746, 50.2992, 47.1262, 50.2521, 50.5074, 49.0278, 50.8663, 48.5794, 45.9845, 48.7135, 48.6194, 52.438, 51.439, 49.8989, 48.9065, 51.1165, 52.9883, 48.0659, 48.7218, 52.3015, 50.9617, 49.1231, 52.3105, 51.1032, 48.1144, 50.5493, 49.9856, 53.3053, 51.2148, 50.967, 50.2666, 51.0419, 47.2217, 47.0912, 50.7787, 50.7994, 49.193, 46.0038, 47.8244, 51.0756, 50.2538, 48.7173, 48.142, 50.533, 49.6983, 49.6813, 48.9913, 50.3005, 50.5654, 48.7473, 53.3254, 48.8224, 50.3326, 46.8969, 48.0021, 51.8409, 51.4528, 50.4293, 48.6549, 50.7073, 48.0141, 52.1401, 44.7421, 49.23, 48.4055, 52.5897, 49.604, 52.4748, 52.8913, 50.9095, 46.724, 48.4923, 49.0017, 50.801, 50.7593, 47.3408, 51.5692, 47.9706, 49.2922, 50.5148, 49.7404, 53.1377, 52.2787, 47.3619, 47.7486, 52.2996, 50.2266, 51.2567, 50.0502, 52.4611, 51.5433, 51.5729, 48.6466, 51.2149, 46.7796, 49.5185, 48.7291, 48.4466, 48.1264, 45.9848, 52.4768, 49.5744, 49.6455, 51.0025, 52.1304, 52.6737, 49.1368, 50.376, 48.1893, 51.4656, 50.0421, 51.8182, 51.6546, 51.5634, 51.0723, 50.1741, 47.5146, 50.6581, 53.3669, 49.1322, 48.6453, 51.2361, 53.3486, 47.9179, 45.1683, 48.7703, 50.1994, 49.7144, 49.4794, 48.9947, 48.7063, 48.3456, 50.72, 51.145, 46.8293, 53.1638, 50.883, 51.1079, 49.4343, 50.5096, 47.7528, 53.1668, 52.8298, 49.5209, 47.3625, 49.3883, 51.1826, 49.2124, 48.4087, 50.3217, 48.2524, 52.6705, 51.891, 51.3276, 48.5842, 50.4137, 52.2188, 45.8138, 50.4114, 52.8124, 53.623, 48.0989, 50.5524, 52.0886, 52.4117, 46.3207, 45.9808, 50.3086, 50.9966, 49.7509, 49.3443, 52.5561, 52.955, 48.8156, 50.3154, 47.6862, 53.471, 47.1915, 47.8598, 48.2204, 47.903, 52.5551, 54.6433, 50.5214, 49.3279, 51.4435, 48.186, 52.6755, 49.9271, 50.8411, 48.6175, 49.5912, 46.7608, 50.0374, 51.7805, 46.2969, 47.9803, 49.0477, 49.3807, 49.8195, 49.9099, 50.5675, 49.929, 46.6362, 50.6274, 52.4068, 50.5161, 46.4531, 50.4646, 50.9507, 48.0215, 50.1524, 52.5529, 53.2348, 50.253, 51.0149, 49.6698, 49.1803, 52.77, 51.9249, 51.234, 48.9469, 52.9345, 52.0809, 47.4166, 52.1772, 50.3366, 50.557, 45.9659, 47.0737, 51.979, 48.466, 51.6249, 47.3457, 47.0495, 53.8215, 51.8188, 49.7433, 50.4474, 48.5828, 50.712, 52.3589, 49.1691, 50.9713, 46.6156, 53.6031, 52.0092, 49.94, 49.3311, 52.822, 48.9905, 50.7395, 49.3722, 48.1545, 52.0777, 49.1756, 50.9284, 49.3054, 49.7311, 49.6579, 48.6903, 51.4666, 49.8264, 51.7255, 51.4936, 52.4446, 49.017, 51.3274, 49.6512, 49.9552, 48.5711, 49.0511, 51.2603, 49.316, 48.5411, 51.6966, 50.7136, 47.563, 50.2114, 52.2564, 52.1924, 48.9257, 47.7786, 49.2689, 52.6882, 51.4875, 45.4126, 51.7175, 45.5643, 53.8651, 51.6907, 49.2108, 50.2139, 51.322, 49.0132, 50.9271, 49.7409, 49.4984, 53.05, 45.9225, 50.0941, 47.2138, 49.6358, 48.5838, 55.0926, 48.9716, 49.726, 48.7632, 51.7788, 48.9797, 48.69, 53.7408, 51.5619, 50.5379, 54.8248, 48.734, 48.3748, 48.413, 52.4063, 48.7624, 52.3805, 48.7412, 46.3707, 48.3968, 50.5743, 52.3846, 52.6027, 50.7328, 51.3682, 44.7111, 50.5253, 50.0771, 49.3821, 48.6682, 51.7595, 52.2785, 53.9292, 49.6105, 48.4079, 53.7705, 49.3592, 48.9588, 47.2256, 50.8402, 51.2114, 51.6872, 50.4462, 51.0882, 51.1086, 51.0288, 52.3569, 51.7049, 50.3776, 48.9255, 51.6059, 48.8457, 49.7047, 48.5676, 54.2659, 48.0577, 51.3278, 51.3526, 51.7524, 49.9208, 50.1678, 49.6064, 44.5639, 48.4715, 52.7281, 48.3675, 50.27, 55.2752, 48.0637, 48.9907, 49.4959, 46.0031, 48.2635, 47.1427, 51.0878, 49.5058, 45.735, 51.6203, 51.9442, 49.2766, 51.8934, 51.1747, 48.1713, 50.5625, 50.3694, 48.3297, 47.9467, 50.8158, 54.6134, 49.9254, 50.4206, 47.8911, 49.4809, 49.5953, 49.2465, 48.6884, 49.9027, 48.8741, 52.4829, 54.0998, 48.4773, 49.2728, 50.8672, 51.1662, 48.473, 48.8583, 51.1732, 46.7233, 54.4301, 50.6029, 49.3918, 49.6993, 47.8532, 53.605, 49.8322, 49.1298, 53.514, 52.0639, 49.6111, 50.8725, 49.3357, 49.2756, 49.5581, 50.5949, 52.1898, 53.9551, 45.6548, 54.6447, 52.0487, 50.0331, 49.1399, 46.2638, 52.3888, 47.8108, 51.3037, 49.1705, 46.7485, 50.8368, 50.9727, 48.1284, 51.7188, 53.6108, 50.9813, 50.8628, 48.5324, 47.9213, 49.1878, 50.138, 50.0235, 49.6022, 54.9835, 51.1423, 48.0474, 47.5988, 46.5229, 47.9809, 51.1574, 49.4295, 48.6143, 51.1793, 51.0984, 50.7428, 49.9735, 49.9265, 47.3628, 46.3096, 48.3872, 48.4226, 48.3621, 49.6536, 49.0519, 47.3908, 49.7942, 50.672, 49.0149, 51.6381, 50.6309, 50.7789, 49.2529, 50.8269, 51.365, 46.1415, 52.1366, 48.9777, 51.3012, 45.3873, 49.1317, 46.602, 49.7032, 50.3629, 50.8139, 50.0414, 47.4949, 52.6055, 47.3095, 49.3274, 48.4465, 48.7949, 47.1938, 52.1712, 51.4922, 50.1433, 49.7113, 48.0801, 49.7798, 48.1839, 50.1728, 50.7875, 49.5057, 50.798, 51.1672, 51.6093, 53.9495, 47.1446, 47.5323, 47.567, 49.6044, 46.8111, 48.9391, 51.6762, 51.4585, 53.2791, 51.7073, 52.026, 48.9421, 49.2839, 51.7577, 51.7172, 46.1673, 47.2228, 48.0443, 48.2801, 52.6347, 53.6118, 50.6599, 50.6814, 53.8839, 50.8974, 51.1181, 50.7454, 50.7486, 52.6982, 51.4527, 50.1707, 49.2137, 50.5201, 46.6746, 50.9215, 50.2522, 50.6416, 47.6865, 50.9374, 50.3662, 49.986, 50.7404, 48.3407, 47.0873, 50.9082, 45.5965, 51.1901, 54.1351, 50.7416, 51.7074, 53.2554, 52.0884, 54.554, 48.2577, 49.3176, 50.9667, 46.7702, 49.9871, 50.1335, 52.5958, 52.1345, 51.5325, 51.3935, 50.1228, 50.5844, 51.5189, 52.0234, 48.5619, 50.8992, 49.8493, 50.5311, 52.3088, 49.4987, 48.156, 48.178, 48.8537, 51.219, 49.6686, 48.8039, 51.5455, 50.3199, 51.4011, 54.386, 53.7625, 50.6897, 50.6778, 52.2273, 52.5558, 49.2837, 47.5499, 51.7259, 50.4502, 51.2204, 51.0858, 48.6031, 48.9799, 52.1426, 49.6769, 48.4369, 49.4493, 50.7953, 48.9796, 53.4748, 51.6247, 47.0196, 50.4591, 53.1379, 56.5926, 49.0584, 50.0495, 47.2873, 51.1125, 50.9577, 50.5664, 47.8315, 51.5625, 53.6563, 52.7174, 50.7245, 49.8985, 51.7966, 49.7178, 44.9419, 46.5643, 52.7364, 52.6606, 49.7492, 48.4035, 49.3994, 47.4278, 49.4921, 53.494, 48.7488, 48.4112, 47.3121, 50.4114, 48.1002, 51.3008, 49.3828, 53.3626, 51.1826, 50.954, 47.1864, 50.0482, 50.6076, 50.6857, 50.7108, 50.2451, 47.6646, 45.1864, 47.7677, 52.0279, 51.6007, 52.2847, 50.6562, 50.4541, 50.0346, 46.5835, 50.3563, 48.7048, 48.7818, 48.0235, 49.8656, 51.8303, 51.5995, 48.0584, 50.4845, 49.8512, 45.9776, 51.8803, 48.7061, 51.1607, 48.4177, 51.5559, 52.4968, 50.523, 51.0863, 48.693, 49.366, 47.7621, 52.1054, 52.3727, 49.0403, 50.0758, 48.6665, 47.3819, 48.9705, 47.6056, 46.119, 50.5919, 48.0086, 49.9163, 44.6843, 50.1774, 48.6158, 48.7493, 51.741, 49.7554, 50.5983, 50.1069, 50.9624, 48.4169, 49.9767, 49.5335, 51.4178, 48.8471, 51.5066, 51.0568, 48.8379, 51.4531, 49.2016, 48.7199, 48.3987, 47.851, 50.6225, 49.6071, 49.3861, 51.7082, 51.7633, 50.2195, 46.1293, 50.9703, 46.0824, 48.2148, 49.6783, 53.0694, 45.0365, 46.9373, 48.5692, 51.3824, 46.7358, 48.3999, 52.4256, 51.7636, 48.7842, 52.0276, 50.1703, 48.1863, 48.573, 48.7447, 47.8426, 50.9177, 52.0199, 47.1195, 51.3327, 47.911, 49.8548, 50.6071, 52.0277, 49.9519, 50.49
...<truncated>
```


### `references/has_historical_traffic_data.md`

```markdown
# Has Historical Traffic Data Available

Use these instructions if the agent has historical metrics data available:

## 1. Run Traffic Analyzer Script

-   Run the `analyze_traffic.py` script to classify the metrics traffic pattern
    profile using one of the following commands:
    -   **Live Query**: `python3 scripts/analyze_traffic.py --live --project-id
        [PROJECT_ID] --reasoning-engine-id [REASONING_ENGINE_ID]`
    -   **Metrics File**: `python3 scripts/analyze_traffic.py --metrics-file
        [PATH_TO_JSON]`
-   **Handling Tool Failures**: If the `--live` command fails with
    `CredentialsMissingError` (exit code 1), report the error and instruct the
    user to run `gcloud auth application-default login` on their terminal.
-   Map the traffic pattern profile classified by the script to **Latency**:
    -   **Steady**: Maps to **Long-Window Z-Score Baseline (1-week lookback)**
        (safe since the script verified we have at least 14 days of history).
    -   **Seasonal**: Maps to **Seasonal Decomposition** (average 1w and 1d).
    -   **Bursty**: Maps to **Moving Averages** (1h baseline).

#### Decision Mapping Reference:

| Variance Ratio   | Autocorrelation | Traffic        | Assigned Latency       |
: (std_dev / mean) : (1-week lag)    : Classification : Algorithm & baseline   :
| :--------------- | :-------------- | :------------- | :--------------------- |
| `≤ 2.0`          | `≤ 0.75`        | **Steady**     | Long-Window Z-Score    |
:                  :                 :                : (1-week lookback)      :
| `≤ 2.0`          | `> 0.75`        | **Seasonal**   | Seasonal Decomposition |
:                  :                 :                : (1w & 1d avg)          :
| `> 2.0`          | Any / Not       | **Bursty**     | Moving Averages        |
:                  : Applicable      :                : (1-hour window)        :

*Example classifications:*

-   *Steady*: Low variance traffic (e.g. steady QPS) with little or no weekly
    cyclical pattern.
-   *Seasonal*: Clear daily/weekly repeating patterns with high weekly
    correlation (e.g. daily peak traffic).
-   *Bursty*: Highly volatile traffic with rapid spikes and quiet periods (e.g.
    batch job workloads).

-   **Fallback for Insufficient Data / No Traffic**:

    -   If the script fails with a `ValueError` indicating insufficient data
        points (less than 14 days of history), or if it outputs "New Agent / No
        Traffic" (inactive agent), you MUST fallback to the user inquiry
        instructions in
        [no_historical_traffic_data.md](no_historical_traffic_data.md) to ask
        the user for the expected traffic pattern.

-   Regardless of the script's output profile, the other policies MUST use their
    correct data-class defaults:

    -   **Error Rate**: ALWAYS use **Multi-Window Multi-Burn Rate SLO Alerting**
        (or ratio-based static limits).

## 2. User Notification

Clearly communicate the findings and selection at the start of your response:

1.  Explain the classified traffic profile (Seasonal, Steady, or Bursty) output
    by the metrics analysis script (citing indicators like standard deviation,
    autocorrelation, or zero-ratio from the script output). If falling back to
    user inquiry due to zero metrics or insufficient data, explain that.
2.  Propose the corresponding alerting policy mapping (Latency matching the
    traffic profile, Error Rate using SLO Burn Rate).
3.  Ask the user if this expected profile mapping is correct or if they would
    like to customize standard deviation thresholds.
4.  Provide a brief plain-English explanation of what each of the proposed
    alerts measures and how the underlying algorithms work and what they
    actually measure. Keep this explanation in the conversational response text.

```


### `references/no_historical_traffic_data.md`

```markdown
# No Historical Traffic Data Available

Use these instructions if there is no historical metrics data available for the
agent (e.g., brand new agent):

## 1. Ask the User for the Traffic Pattern and Handle Defaults

-   Because no historical metrics data is available and we do NOT perform
    traffic pattern inference based on the name, description, or context of the
    agent, you MUST explicitly ask the user directly in your response what
    traffic pattern they expect for their agent.
-   **Direct Question Format**: You MUST write a direct question in your response
    (e.g., "What traffic pattern do you expect for your agent?") and explicitly
    present the following three options for customization:
    -   **Steady/Consistent**: Maps **Latency** to **Short-Window Z-Score Baseline
        (1-hour lookback)**.
    -   **Bursty/Inconsistent**: Maps **Latency** to **Moving Averages (1-hour
        baseline)**.
    -   **Seasonal/Cyclical**: Maps **Latency** to **Seasonal Decomposition**
        (requires offsets `1d` and `1w`).
-   Inform the user that the default traffic pattern is **Steady/Consistent**
    (which maps to Short-Window Z-Score Baseline), and that you will use this
    default if they do not specify one.
-   **Handling Automated or Immediate Setup Requests**: If the user's prompt
    asks you to configure or write the alerting policies immediately (e.g., "Set
    up its alerting policies in 'monitoring/alerts.tf'"), or if you are running
    in an automated/non-interactive script, you MUST NOT pause to wait for their
    response. Instead, ask the question in your response, state that you are
    deploying the default Steady/Consistent pattern because no choice was
    specified yet, and **immediately proceed to generate and write the default
    configuration (Steady / Consistent -> Short-Window Z-Score)**.
-   Regardless of the selected traffic pattern, the other policies MUST use
    their correct data-class defaults:

    -   **Error Rate**: ALWAYS use **Multi-Window Multi-Burn Rate SLO Alerting**
        (or ratio-based static limits).

*   **Short-Window Z-Score / Moving Averages**: Require **1 hour** of traffic
    history.

*   **SLO Burn Rate (Error Rate)**: Requires up to **3 days** for the slow burn
    component, though the fast burn component (1h/5m) will work after 1 hour.

*   **Seasonal Decomposition**: Requires **1 week** of history (due to the `1w`
    offset). **WARNING:** If the user switches to Seasonal Decomposition, warn
    them that they will have a 1-week blind spot, and suggest starting with
    **Short-Window Z-Score** or **Static Thresholds** as a temporary guard.

## 2. User Notification

Clearly communicate the lack of historical data, explain the options, and detail
the immediate actions taken at the start of your response:

1.  Explain that since the agent has no historic data, you cannot automatically
    analyze the traffic pattern.
2.  Ask the user directly what traffic pattern they expect (Steady, Seasonal, or
    Bursty), detailing the mapping differences and the 1-week blind spot risk if
    they choose Seasonal.
3.  Inform the user that the default is **Steady / Consistent** (Short-Window
    Z-Score algorithm for Latency) and you will proceed with this default if
    they don't have a good idea or do not choose.
4.  If the user accepts the default, explain that you have deployed the
    Steady/Consistent default to ensure the files are configured immediately,
    but they can request an update if they prefer another pattern.
5.  Explain the warm-up periods (1 hour for Latency, up to 3 days for SLOs).
6.  Propose the rest of the configuration mapping: Error Rate (SLO Burn Rate).
7.  Provide a brief plain-English explanation of what each of the proposed
    alerts measures and how the underlying algorithms work and what they
    actually measure.

```


### `references/promql_queries.md`

````markdown
# PromQL Queries Reference

This file contains the recommended PromQL queries and template configurations
for monitoring Latency and Error Rates of Agent Platform agents.

## Table of Contents

-   [1. Latency (95th Percentile)](#1-latency-95th-percentile)
    -   [Z-Score (Steady Traffic)](#z-score-recommended-for-steady-traffic)
    -   [Moving Averages (Bursty Traffic)](#moving-averages-recommended-for-bursty-traffic)
    -   [Seasonal Decomposition (Seasonal Traffic)](#seasonal-decomposition-recommended-for-traffic-with-seasonal-or-time-of-day-component)
-   [2. Error Rate (SLO)](#2-error-rate-slo)
    -   [Fast Burn SLO](#fast-burn-slo-1-hour-and-5-minute-windows)
    -   [Slow Burn SLO](#slow-burn-slo-3-day-and-6-hour-windows)

--------------------------------------------------------------------------------

## 1. Latency (95th Percentile)

### Z-Score (Recommended for Steady Traffic)

#### Long-Window Z-Score (For Established Agents - >1 week history)

Compares the 5-minute 95th percentile latency to the 1-week baseline.

```promql
abs(
  histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[5m])) by (le, reasoning_engine_id))
  -
  histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[1w])) by (le, reasoning_engine_id))
)
/
stddev_over_time(
  (histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[5m])) by (le, reasoning_engine_id)))[1w:5m]
) > 3
```

*Note: The denominator uses a subquery `[1w:5m]` to calculate standard deviation
of the 5-minute latency over 1 week. The numerator uses `[1w]` rate directly to
avoid a second subquery for the mean.*

#### Short-Window Z-Score (For Newer Agents - >1 hour history)

Compares the 1-minute 95th percentile latency to the 1-hour baseline. Useful for
quick activation on new agents.

```promql
abs(
  histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[1m])) by (le, reasoning_engine_id))
  -
  histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[1h])) by (le, reasoning_engine_id))
)
/
stddev_over_time(
  (histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[1m])) by (le, reasoning_engine_id)))[1h:1m]
) > 3
```

### Moving Averages (Recommended for Bursty Traffic)

Compares the 5-minute latency to the 1-hour average.

```promql
histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[5m])) by (le, reasoning_engine_id))
>
1.5 * histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[1h])) by (le, reasoning_engine_id))
```

### Seasonal Decomposition (Recommended for traffic with seasonal or time-of-day component)

> [!NOTE] For the Latency alert policy, ONLY use seasonal decomposition to track
> Latency spikes. Alert policies using seasonal decomposition tracking both
> spikes and drops can falsely trigger alerts.

Compares the 5-minute latency to the average of 1-week and 1-day lookback
baselines.

```promql
histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[5m])) by (le, reasoning_engine_id))
/
(
  (
    histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[5m] offset 1d)) by (le, reasoning_engine_id))
    +
    histogram_quantile(0.95, sum(rate(aiplatform_googleapis_com:reasoning_engine_request_latencies_bucket[5m] offset 1w)) by (le, reasoning_engine_id))
  ) / 2
)
> 2
```

--------------------------------------------------------------------------------

## 2. Error Rate (SLO)

Always use Multi-Window Multi-Burn Rate SLOs. Z-score is not recommended due to
sparsity.

### Fast Burn SLO (1-Hour and 5-Minute Windows)

```promql
(
  sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{response_code!~"2.."}[5m])) by (reasoning_engine_id)
  /
  sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[5m])) by (reasoning_engine_id)
  > (1 - ${var.slo_target}) * 14.4
)
and
(
  sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{response_code!~"2.."}[1h])) by (reasoning_engine_id)
  /
  sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[1h])) by (reasoning_engine_id)
  > (1 - ${var.slo_target}) * 14.4
)
```

### Slow Burn SLO (3-Day and 6-Hour Windows)

```promql
(
  sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{response_code!~"2.."}[6h])) by (reasoning_engine_id)
  /
  sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[6h])) by (reasoning_engine_id)
  > (1 - ${var.slo_target}) * 1.0
)
and
(
  sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{response_code!~"2.."}[3d])) by (reasoning_engine_id)
  /
  sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[3d])) by (reasoning_engine_id)
  > (1 - ${var.slo_target}) * 1.0
)
```

````


### `references/telemetry_enablement.md`

````markdown
# Telemetry Status & Enablement Check Reference

## Required Telemetry Environment Variables

To successfully export evaluation traces, the agent's deployment spec MUST have
the following environment variables:

Environment Variable                                 | Required Value                                   | Description
---------------------------------------------------- | ------------------------------------------------ | -----------
`GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY`         | `"true"`                                         | Enables tracing and logging export.
`OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `"EVENT_ONLY"` or `"SPAN_AND_EVENT"` or `"true"` | Captures message payloads for evaluation.
`OTEL_SEMCONV_STABILITY_OPT_IN`                      | `"gen_ai_latest_experimental"`                   | Opts into Gen AI semantic conventions.

## How to Enable Telemetry via Terraform

Update the `google_vertex_ai_reasoning_engine` resource's `deployment_spec.env`
blocks:

```hcl
resource "google_vertex_ai_reasoning_engine" "my_agent" {
  ...
  spec {
    deployment_spec {
      env {
        name  = "GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY"
        value = "true"
      }
      env {
        name  = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
        value = "EVENT_ONLY"
      }
      env {
        name  = "OTEL_SEMCONV_STABILITY_OPT_IN"
        value = "gen_ai_latest_experimental"
      }
    }
  }
}
```

## Telemetry and Data Generation Dependencies

Even if telemetry environment variables are configured on the agent, verify
these additional dependencies. Note that quality metrics will NOT populate, and
quality metrics alert policies and the Online Monitor should be skipped if the
decision is to NOT enable the required APIs:

1.  **API Enablement**: Ensure the following APIs are enabled in the GCP
    project:

    *   **Cloud Trace API**: `cloudtrace.googleapis.com` (needed for exporting
        spans)
    *   **Observability API**: `observability.googleapis.com` (needed for trace
        storage and SQL queries by the evaluator)

    *To check if they are enabled:*

    ```bash
    gcloud services list --enabled --project="[PROJECT_ID]" \
      --filter="name:(cloudtrace.googleapis.com observability.googleapis.com)"
    ```

2.  **IAM Permissions**: The service account assigned to the agent (default or
    custom) MUST have the Cloud Trace Agent (`roles/cloudtrace.agent`) and Logs
    Writer (`roles/logging.logWriter`) roles.

````


### `scripts/analyze_traffic.py`

```
#!/usr/bin/env python3
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Traffic metrics analyzer for Agent Platform reasoning engine agents.

Computes zero-ratio, variance ratio, and 1-week autocorrelation on a 14-day
metrics time series (at 5-minute intervals) and classifies the traffic pattern
to recommend the optimal dynamic PromQL alerting threshold strategy.
"""

import argparse
import json
import math
import statistics
import sys
import time

try:
  from google.auth import exceptions as auth_exceptions
  from google.cloud import monitoring_v3

  _HAS_GCP_LIBS = True
except ImportError:
  _HAS_GCP_LIBS = False


# Number of 5-minute data points in a 7-day weekly window
# (12 points/hour * 24 hours/day * 7 days/week)
_POINTS_PER_WEEK = 2016


class CredentialsMissingError(Exception):
  """Raised when Google Cloud credentials are not found."""

  pass


def compute_metrics_and_classify(values):
  """Computes statistical metrics and classifies the traffic profile.

  Args:
      values: A list of floats representing the 5-minute rate telemetry. Should
        be at least 4032 points (14 days).

  Returns:
      A dictionary with statistical metrics and the classification profile.
  """
  required_points = 2 * _POINTS_PER_WEEK
  if len(values) < required_points:
    raise ValueError(
        f"Insufficient data points: expected at least {required_points} (14"
        f" days at 5m interval), got {len(values)}"
    )

  # We focus on the last 7 days (latest weekly points) and previous 7 days (weekly points before that)
  last_7_days = values[-_POINTS_PER_WEEK:]
  prev_7_days = values[-required_points:-_POINTS_PER_WEEK]

  # 1. Zero Ratio: proportion of 5m intervals with rate <= 0.01 in the last 7 days
  zero_ratio = sum(x <= 0.01 for x in last_7_days) / _POINTS_PER_WEEK

  # 2. Mean and Standard Deviation of the last 7 days
  mean_last = statistics.mean(last_7_days)
  stddev_last = statistics.pstdev(last_7_days)

  variance_ratio = stddev_last / mean_last if mean_last > 0.0001 else 0.0

  # 3. Autocorrelation at 1-week lag
  mean_prev = statistics.mean(prev_7_days)
  stddev_prev = statistics.pstdev(prev_7_days)

  if stddev_last > 0.0001 and stddev_prev > 0.0001:
    covariance = (
        sum(
            (x - mean_last) * (y - mean_prev)
            for x, y in zip(last_7_days, prev_7_days)
        )
        / _POINTS_PER_WEEK
    )
    autocorr_1w = covariance / (stddev_last * stddev_prev)
  else:
    autocorr_1w = 0.0

  # Decision tree classification
  if mean_last == 0.0 and zero_ratio == 1.0:
    profile = "New Agent / No Traffic"
    algorithm = "Short-Window Z-Score (1h baseline)"
    rationale = (
        "No historical traffic observed. Defaulting to Short-Window Z-Score"
        " (1h baseline) dynamic thresholding to ensure quick activation"
        " (requires 1 hour of traffic history)."
    )
  elif variance_ratio > 2.0:
    profile = "Bursty / Inconsistent"
    algorithm = "Moving Averages"
    rationale = (
        f"Variance ratio is high ({variance_ratio:.2f}), indicating volatile"
        " bursty peaks. Moving averages dynamic baseline thresholds are"
        " recommended to smooth out short-term fluctuations and prevent false"
        " pages."
    )
  elif autocorr_1w > 0.75 and variance_ratio <= 2.0:
    profile = "Seasonal / Cyclical"
    algorithm = "Seasonal Decomposition (average 1w and 1d)"
    rationale = (
        f"Auto-correlation at 1w lag is high ({autocorr_1w:.2f}) with stable"
        f" variance ratio ({variance_ratio:.2f}). Comparing against the average"
        " of 1w and 1d offsets mitigates holiday false positives and prevents"
        " diurnal false alerts during off-peak periods."
    )
  else:
    profile = "Steady / Consistent"
    algorithm = "1w Z-Score Baseline"
    rationale = (
        "Stable regular traffic pattern with low variance ratio"
        f" ({variance_ratio:.2f}) and moderate/low autocorrelation"
        f" ({autocorr_1w:.2f}). 1w Z-Score baseline is ideal."
    )

  return {
      "zero_ratio": round(zero_ratio, 4),
      "variance_ratio": round(variance_ratio, 4),
      "autocorr_1w": round(autocorr_1w, 4),
      "mean": round(mean_last, 4),
      "stddev": round(stddev_last, 4),
      "profile": profile,
      "recommended_algorithm": algorithm,
      "rationale": rationale,
  }


def align_to_grid(
    points: list[tuple[float, float]],
    end_time: float,
    num_points: int = 4032,
    interval_sec: int = 300,
) -> list[float]:
  """Aligns sparse telemetry points to a uniform time grid of fixed size.

  We anchor the grid at end_time aligned to interval_sec and look backward.

  Args:
      points: A list of tuples (timestamp, value).
      end_time: The end timestamp of the grid window.
      num_points: The exact number of points in the target grid.
      interval_sec: The grid interval in seconds.

  Returns:
      A list of floats representing the aligned values in order of time.
  """
  aligned_end = math.floor(end_time / interval_sec) * interval_sec
  aligned_start = aligned_end - num_points * interval_sec

  grid = [0.0] * num_points

  for ts, val in points:
    rounded_ts = round(ts / interval_sec) * interval_sec
    idx = int((rounded_ts - aligned_start) / interval_sec) - 1
    if 0 <= idx < num_points:
      grid[idx] += val

  return grid


def query_live_metrics(
    project_id: str, reasoning_engine_id: str
) -> list[float]:
  """Queries live Vertex AI reasoning engine request count metrics for 14 days.

  Args:
      project_id: The GCP project ID.
      reasoning_engine_id: The reasoning engine ID.

  Returns:
      A list of 4032 floats representing the 5m rate telemetry aligned to grid.

  Raises:
      CredentialsMissingError: If GCP credentials are not found.
  """
  if not _HAS_GCP_LIBS:
    raise RuntimeError("Google Cloud Client libraries are not installed.")

  try:
    client = monitoring_v3.MetricServiceClient()
    name = f"projects/{project_id}"

    now = time.time()
    seconds_per_day = 24 * 3600
    start_time = now - 14 * seconds_per_day

    interval = monitoring_v3.TimeInterval({
        "end_time": {"seconds": int(now)},
        "start_time": {"seconds": int(start_time)},
    })

    filter_str = (
        "metric.type ="
        ' "aiplatform.googleapis.com/reasoning_engine/request_count" AND'
        ' resource.type = "aiplatform.googleapis.com/ReasoningEngine" AND'
        f' resource.labels.reasoning_engine_id = "{reasoning_engine_id}"'
    )

    results = client.list_time_series(
        request={
            "name": name,
            "filter": filter_str,
            "interval": interval,
            "view": monitoring_v3.ListTimeSeriesRequest.TimeSeriesView.FULL,
            "aggregation": {
                "alignment_period": {"seconds": 300},
                "per_series_aligner": (
                    monitoring_v3.Aggregation.Aligner.ALIGN_RATE
                ),
                "cross_series_reducer": (
                    monitoring_v3.Aggregation.Reducer.REDUCE_SUM
                ),
                "group_by_fields": ["resource.labels.reasoning_engine_id"],
            },
        }
    )
  except auth_exceptions.DefaultCredentialsError as e:
    raise CredentialsMissingError(
        "No valid Google Cloud credentials found. Please run 'gcloud auth"
        " application-default login' to authenticate your local environment."
    ) from e

  points = []
  for series in results:
    for point in series.points:
      points.append((
          point.interval.start_time.ToDatetime().timestamp(),
          point.value.double_value,
      ))

  if not points:
    # No traffic detected at all. Return a 14-day grid filled with 0.0.
    return [0.0] * (2 * _POINTS_PER_WEEK)

  # Align sparse points to the 14-day 5-minute grid
  return align_to_grid(points, now, num_points=2 * _POINTS_PER_WEEK)


def main():
  parser = argparse.ArgumentParser(
      description=(
          "Analyze 14-day traffic metrics to select dynamic thresholding"
          " algorithm."
      )
  )
  parser.add_argument(
      "--metrics-file",
      type=str,
      help="Path to JSON file containing list of numbers. Use '-' for stdin.",
  )
  parser.add_argument(
      "--live",
      action="store_true",
      help="Force live GCP metrics query validation check.",
  )
  parser.add_argument(
      "--project-id",
      type=str,
      help="GCP project ID (required for live queries).",
  )
  parser.add_argument(
      "--reasoning-engine-id",
      type=str,
      help=(
          "Vertex AI Reasoning Engine numerical ID (required for live queries)."
      ),
  )
  args = parser.parse_args()

  data = None

  if args.live:
    if not args.project_id or not args.reasoning_engine_id:
      print(
          "Error: Live GCP queries require --project-id and"
          " --reasoning-engine-id to be specified.",
          file=sys.stderr,
      )
      sys.exit(1)

    try:
      data = query_live_metrics(args.project_id, args.reasoning_engine_id)
    except CredentialsMissingError as e:
      print(f"Error: {e}", file=sys.stderr)
      sys.exit(1)
    except Exception as e:
      print(f"Error executing live query: {e}", file=sys.stderr)
      sys.exit(1)

  else:
    # File/Stdin input mode
    if not args.metrics_file and sys.stdin.isatty():
      print(
          "Error: Must specify --metrics-file or pipe metrics data to stdin"
          " when not using --live.",
          file=sys.stderr,
      )
      sys.exit(1)

    try:
      if args.metrics_file == "-" or not args.metrics_file:
        data = json.load(sys.stdin)
      else:
        with open(args.metrics_file, "r") as f:
          data = json.load(f)
    except Exception as e:
      print(f"Error reading metrics data: {e}", file=sys.stderr)
      sys.exit(1)

  if not data or not isinstance(data, list):
    print(
        "Error: Metrics data must be a JSON list of numbers or parsed"
        " successfully from a source",
        file=sys.stderr,
    )
    sys.exit(1)

  try:
    results = compute_metrics_and_classify(data)
    print(json.dumps(results, indent=2))
  except ValueError as e:
    print(f"Error: {e}", file=sys.stderr)
    sys.exit(1)


if __name__ == "__main__":
  main()

```


### `scripts/analyze_traffic_test.py`

```
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unit tests for metrics data time-series parsing.

Also tests decision tree traffic profiling decisions.
"""

import datetime
import json
import os
import sys
import time
import unittest
from unittest import mock

import analyze_traffic


class AnalyzeTrafficTest(unittest.TestCase):

  def setUp(self):
    super().setUp()
    self.mock_data_dir = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets"
    )
    if not os.path.exists(self.mock_data_dir):
      raise FileNotFoundError(
          f"Could not find mock data directory: {self.mock_data_dir}"
      )

  def _load_mock_data(self, filename):
    filepath = os.path.join(self.mock_data_dir, filename)
    with open(filepath, "r") as f:
      return json.load(f)

  def test_classify_seasonal(self):
    data = self._load_mock_data("mock_seasonal.json")
    results = analyze_traffic.compute_metrics_and_classify(data)
    self.assertEqual(results["profile"], "Seasonal / Cyclical")
    self.assertEqual(
        results["recommended_algorithm"],
        "Seasonal Decomposition (average 1w and 1d)",
    )

  def test_classify_steady(self):
    data = self._load_mock_data("mock_steady.json")
    results = analyze_traffic.compute_metrics_and_classify(data)
    self.assertEqual(results["profile"], "Steady / Consistent")
    self.assertEqual(results["recommended_algorithm"], "1w Z-Score Baseline")

  def test_classify_bursty(self):
    data = self._load_mock_data("mock_bursty.json")
    results = analyze_traffic.compute_metrics_and_classify(data)
    self.assertEqual(results["profile"], "Bursty / Inconsistent")
    self.assertEqual(results["recommended_algorithm"], "Moving Averages")

  def test_classify_new_agent(self):
    data = [0.0] * 4032
    results = analyze_traffic.compute_metrics_and_classify(data)
    self.assertEqual(results["profile"], "New Agent / No Traffic")
    self.assertEqual(
        results["recommended_algorithm"], "Short-Window Z-Score (1h baseline)"
    )
    self.assertIn("No historical traffic observed", results["rationale"])

  def test_insufficient_data(self):
    with self.assertRaises(ValueError):
      analyze_traffic.compute_metrics_and_classify([1.0] * 100)

  def test_classify_constant_traffic(self):
    # Constant 10.0 traffic should be classified as Steady/Consistent
    data = [10.0] * 4032
    results = analyze_traffic.compute_metrics_and_classify(data)
    self.assertEqual(results["profile"], "Steady / Consistent")
    self.assertEqual(results["recommended_algorithm"], "1w Z-Score Baseline")

  def test_classify_very_low_constant_traffic(self):
    # Constant 0.005 traffic (below 0.01 zero threshold) should still be Steady/Consistent
    # due to mean_last being non-zero (0.005 != 0.0)
    data = [0.005] * 4032
    results = analyze_traffic.compute_metrics_and_classify(data)
    self.assertEqual(results["profile"], "Steady / Consistent")
    self.assertEqual(results["recommended_algorithm"], "1w Z-Score Baseline")

  def test_classify_near_zero_spike_falls_to_steady(self):
    # 4031 zeros and a single 0.05 value. mean_last is 0.05/2016 = 0.0000248 <= 0.0001,
    # so variance_ratio is forced to 0.0, and it falls back to Steady/Consistent.
    data = [0.0] * 4031 + [0.05]
    results = analyze_traffic.compute_metrics_and_classify(data)
    self.assertEqual(results["profile"], "Steady / Consistent")
    self.assertEqual(results["recommended_algorithm"], "1w Z-Score Baseline")

  def test_classify_single_spike_bursty(self):
    # 4031 zeros and a single 0.5 value. mean_last is 0.5/2016 = 0.000248 > 0.0001,
    # so variance_ratio is computed (~44.88 > 2.0) and it classifies as Bursty/Inconsistent.
    data = [0.0] * 4031 + [0.5]
    results = analyze_traffic.compute_metrics_and_classify(data)
    self.assertEqual(results["profile"], "Bursty / Inconsistent")
    self.assertEqual(results["recommended_algorithm"], "Moving Averages")

  @mock.patch("sys.exit")
  @mock.patch("builtins.print")
  @mock.patch("google.cloud.monitoring_v3.MetricServiceClient")
  def test_main_live_query(self, mock_client_cls, mock_print, mock_exit):
    # Ensure google.cloud.monitoring_v3 module import doesn't fail
    mock_client = mock_client_cls.return_value

    class MockTimestamp:

      def __init__(self, seconds):
        self.seconds = seconds

      def ToDatetime(self):
        return datetime.datetime.fromtimestamp(
            self.seconds, tz=datetime.timezone.utc
        )

    class MockPoint:

      def __init__(self, seconds, val):
        self.interval = mock.MagicMock()
        self.interval.start_time = MockTimestamp(seconds)
        self.value = mock.MagicMock()
        self.value.double_value = val

    class MockSeries:

      def __init__(self, points):
        self.points = points

    now = time.time()
    start_ts = now - 14 * 24 * 3600
    mock_points = [
        MockPoint(start_ts + (i + 1) * 300, 10.0) for i in range(4032)
    ]
    mock_client.list_time_series.return_value = [MockSeries(mock_points)]

    test_argv = [
        "analyze_traffic.py",
        "--live",
        "--project-id",
        "my-project",
        "--reasoning-engine-id",
        "123",
    ]
    with mock.patch.object(sys, "argv", test_argv):
      analyze_traffic.main()

    mock_print.assert_called()
    printed_str = mock_print.call_args[0][0]
    results = json.loads(printed_str)
    self.assertEqual(results["profile"], "Steady / Consistent")
    self.assertEqual(results["recommended_algorithm"], "1w Z-Score Baseline")

  def test_align_to_grid(self):
    points = [
        (0.0, 1.0),
        (10.0, 1.5),
        (600.0, 2.0),
        (2700.0, 3.0),
        (3000.0, 4.0),
    ]
    grid = analyze_traffic.align_to_grid(points, 3000.0, num_points=10)
    self.assertEqual(grid, [0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.0, 4.0])


if __name__ == "__main__":
  unittest.main()

```


### `scripts/check_telemetry.py`

```
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Helper script to check telemetry status on a Vertex AI Reasoning Engine."""

import argparse
import sys

try:
  from google.cloud import aiplatform_v1beta1
except ImportError:
  from google.cloud.aiplatform import aiplatform_v1beta1


def check_agent_telemetry(
    project_id: str, location: str, agent_resource_name: str
) -> bool:
  """Verifies if telemetry is correctly enabled on the reasoning engine."""
  client = aiplatform_v1beta1.ReasoningEngineServiceClient(
      client_options={"api_endpoint": f"{location}-aiplatform.googleapis.com"}
  )

  # Check if agent_resource_name is already a full path or needs formatting
  if not agent_resource_name.startswith("projects/"):
    name = (
        f"projects/{project_id}/locations/{location}/"
        f"reasoningEngines/{agent_resource_name}"
    )
  else:
    name = agent_resource_name

  engine = client.get_reasoning_engine(name=name)

  env_vars = {}
  for env_var in engine.spec.deployment_spec.env:
    env_vars[env_var.name] = env_var.value.strip()

  has_telemetry_toggle = (
      env_vars.get("GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY", "").lower()
      == "true"
  )
  has_capture_toggle = bool(
      env_vars.get("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "")
  )
  is_enabled = has_telemetry_toggle and has_capture_toggle

  print(f"Agent Telemetry Status: {'ENABLED' if is_enabled else 'DISABLED'}")
  if not is_enabled:
    val_telemetry = env_vars.get(
        "GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY", "MISSING"
    )
    val_capture = env_vars.get(
        "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "MISSING"
    )
    print(
        "Missing/Incorrect variables: "
        f"ENABLE_TELEMETRY={val_telemetry}, "
        f"CAPTURE_MESSAGE_CONTENT={val_capture}"
    )
  return is_enabled


def main():
  parser = argparse.ArgumentParser(
      description=(
          "Check if telemetry is enabled on a Vertex AI Reasoning Engine."
      )
  )
  parser.add_argument("--project-id", required=True, help="The GCP Project ID.")
  parser.add_argument(
      "--location", default="us-central1", help="The GCP Location/Region."
  )
  parser.add_argument(
      "--agent-resource-name",
      required=True,
      help="The agent ID or full resource path of the Reasoning Engine.",
  )

  args = parser.parse_args()
  try:
    telemetry_ok = check_agent_telemetry(
        project_id=args.project_id,
        location=args.location,
        agent_resource_name=args.agent_resource_name,
    )
    if not telemetry_ok:
      sys.exit(1)
  except Exception as e:
    print(f"Error checking telemetry status: {e}", file=sys.stderr)
    sys.exit(1)


if __name__ == "__main__":
  main()

```


### `scripts/check_telemetry_test.py`

```
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unit tests for check_telemetry.py."""

import os
import sys
import types
import unittest
from unittest import mock

script_path = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), "check_telemetry.py"
)
with open(script_path, "r") as f:
  code_content = f.read()

check_telemetry = types.ModuleType("check_telemetry")
check_telemetry.__file__ = script_path
sys.modules["check_telemetry"] = check_telemetry
exec(code_content, check_telemetry.__dict__)


class CheckTelemetryTest(unittest.TestCase):

  def setUp(self):
    super().setUp()
    self.project_id = "test-project"
    self.location = "us-central1"
    self.agent_id = "123456789"
    self.agent_resource_name = (
        f"projects/{self.project_id}/locations/{self.location}/"
        f"reasoningEngines/{self.agent_id}"
    )

  @mock.patch("check_telemetry.aiplatform_v1beta1.ReasoningEngineServiceClient")
  def test_check_agent_telemetry_enabled(self, mock_client_class):
    mock_client = mock_client_class.return_value
    mock_engine = mock.Mock()

    env_var_1 = mock.Mock()
    env_var_1.name = "GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY"
    env_var_1.value = "true"

    env_var_2 = mock.Mock()
    env_var_2.name = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
    env_var_2.value = "EVENT_ONLY"

    mock_engine.spec.deployment_spec.env = [env_var_1, env_var_2]
    mock_client.get_reasoning_engine.return_value = mock_engine

    result = check_telemetry.check_agent_telemetry(
        project_id=self.project_id,
        location=self.location,
        agent_resource_name=self.agent_id,
    )

    self.assertTrue(result)
    mock_client.get_reasoning_engine.assert_called_once_with(
        name=self.agent_resource_name
    )

  @mock.patch("check_telemetry.aiplatform_v1beta1.ReasoningEngineServiceClient")
  def test_check_agent_telemetry_disabled_missing_telemetry_toggle(
      self, mock_client_class
  ):
    mock_client = mock_client_class.return_value
    mock_engine = mock.Mock()

    env_var_1 = mock.Mock()
    env_var_1.name = "GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY"
    env_var_1.value = "false"

    env_var_2 = mock.Mock()
    env_var_2.name = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
    env_var_2.value = "EVENT_ONLY"

    mock_engine.spec.deployment_spec.env = [env_var_1, env_var_2]
    mock_client.get_reasoning_engine.return_value = mock_engine

    result = check_telemetry.check_agent_telemetry(
        project_id=self.project_id,
        location=self.location,
        agent_resource_name=self.agent_id,
    )

    self.assertFalse(result)

  @mock.patch("check_telemetry.aiplatform_v1beta1.ReasoningEngineServiceClient")
  def test_check_agent_telemetry_disabled_missing_capture_toggle(
      self, mock_client_class
  ):
    mock_client = mock_client_class.return_value
    mock_engine = mock.Mock()

    env_var_1 = mock.Mock()
    env_var_1.name = "GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY"
    env_var_1.value = "true"

    mock_engine.spec.deployment_spec.env = [env_var_1]
    mock_client.get_reasoning_engine.return_value = mock_engine

    result = check_telemetry.check_agent_telemetry(
        project_id=self.project_id,
        location=self.location,
        agent_resource_name=self.agent_id,
    )

    self.assertFalse(result)


if __name__ == "__main__":
  unittest.main()

```


### `scripts/create_online_monitor.py`

```
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Helper script to create a Vertex AI Reasoning Engine Online Monitor."""

import argparse
import sys

try:
  from google.cloud import aiplatform_v1beta1
except ImportError:
  from google.cloud.aiplatform import aiplatform_v1beta1


EXPECTED_METRICS = {
    "hallucination_v1",
    "final_response_quality_v1",
    "tool_use_quality_v1",
}


def verify_configuration(evaluator, target_sampling_percentage) -> bool:
  """Checks if the existing monitor config matches the desired settings."""
  actual_metrics = set()
  for metric_source in evaluator.metric_sources:
    spec = metric_source.metric.predefined_metric_spec
    if spec and spec.metric_spec_name:
      actual_metrics.add(spec.metric_spec_name)

  if actual_metrics != EXPECTED_METRICS:
    return False

  config = evaluator.config
  actual_sampling = 10  # Platform Default
  if config and config.random_sampling:
    actual_sampling = config.random_sampling.percentage

  if int(actual_sampling) != int(target_sampling_percentage):
    return False

  return True


def create_agent_online_monitor(
    project_id: str,
    location: str,
    agent_resource_name: str,
    sampling_percentage: int = 10,
) -> str:
  """Checks for matching monitor and creates one if none exists."""
  client = aiplatform_v1beta1.OnlineEvaluatorServiceClient(
      client_options={"api_endpoint": f"{location}-aiplatform.googleapis.com"}
  )

  parent = f"projects/{project_id}/locations/{location}"

  # 1. Look for a monitor that matches our config exactly
  request = aiplatform_v1beta1.ListOnlineEvaluatorsRequest(parent=parent)
  page_result = client.list_online_evaluators(request=request)

  matching_evaluator = None
  for evaluator in page_result:
    if evaluator.agent_resource == agent_resource_name:
      if verify_configuration(evaluator, sampling_percentage):
        matching_evaluator = evaluator
        break

  if matching_evaluator:
    print(
        f"Found matching Online Monitor: {matching_evaluator.name}. No action"
        " needed."
    )
    return matching_evaluator.name

  # 2. If no matching monitor is found, create a new one
  print("No matching Online Monitor found for agent. Creating a new one...")
  cloud_obs_spec = aiplatform_v1beta1.OnlineEvaluator.CloudObservability
  online_evaluator = aiplatform_v1beta1.OnlineEvaluator(
      display_name="agent-quality-monitor",
      agent_resource=agent_resource_name,
      cloud_observability=cloud_obs_spec(
          open_telemetry=cloud_obs_spec.OpenTelemetry(
              semconv_version="gen_ai_latest_experimental"
          ),
          trace_scope=cloud_obs_spec.TraceScope(),
      ),
      metric_sources=[
          aiplatform_v1beta1.MetricSource(
              metric=aiplatform_v1beta1.Metric(
                  predefined_metric_spec=aiplatform_v1beta1.PredefinedMetricSpec(
                      metric_spec_name=m
                  )
              )
          )
          for m in EXPECTED_METRICS
      ],
      config=aiplatform_v1beta1.OnlineEvaluator.Config(
          random_sampling=(
              aiplatform_v1beta1.OnlineEvaluator.Config.RandomSampling(
                  percentage=sampling_percentage
              )
          )
      ),
  )

  request = aiplatform_v1beta1.CreateOnlineEvaluatorRequest(
      parent=parent,
      online_evaluator=online_evaluator,
  )

  print(f"Creating Online Monitor for agent: {agent_resource_name}...")
  operation = client.create_online_evaluator(request=request)
  response = operation.result()
  print(f"Online Monitor created successfully: {response.name}")
  return response.name


def main():
  parser = argparse.ArgumentParser(
      description="Provision a Vertex AI Reasoning Engine Online Monitor."
  )
  parser.add_argument("--project-id", required=True, help="The GCP Project ID.")
  parser.add_argument(
      "--location", default="us-central1", help="The GCP Location/Region."
  )
  parser.add_argument(
      "--agent-resource-name",
      required=True,
      help="The full resource path of the Reasoning Engine agent.",
  )
  parser.add_argument(
      "--sampling-percentage",
      type=int,
      default=10,
      help=(
          "The percentage of incoming traces to evaluate (1-100). Default"
          " is 10."
      ),
  )

  args = parser.parse_args()
  try:
    create_agent_online_monitor(
        project_id=args.project_id,
        location=args.location,
        agent_resource_name=args.agent_resource_name,
        sampling_percentage=args.sampling_percentage,
    )
  except Exception as e:
    print(f"Error creating online monitor: {e}", file=sys.stderr)
    sys.exit(1)


if __name__ == "__main__":
  main()

```


### `scripts/create_online_monitor_test.py`

```
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unit tests for Vertex AI Reasoning Engine Online Monitor creation."""

import os
import sys
import types
import unittest
from unittest import mock

script_path = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), "create_online_monitor.py"
)
with open(script_path, "r") as f:
  code_content = f.read()

create_online_monitor = types.ModuleType("create_online_monitor")
create_online_monitor.__file__ = script_path
sys.modules["create_online_monitor"] = create_online_monitor
exec(code_content, create_online_monitor.__dict__)


class CreateOnlineMonitorTest(unittest.TestCase):

  @mock.patch(
      "create_online_monitor.aiplatform_v1beta1.OnlineEvaluatorServiceClient"
  )
  def test_create_agent_online_monitor_already_exists_noop(
      self, mock_client_class
  ):
    mock_client = mock_client_class.return_value

    mock_evaluator = mock.Mock()
    mock_evaluator.name = (
        "projects/my-project/locations/us-central-test/onlineEvaluators/999"
    )
    mock_evaluator.agent_resource = (
        "projects/my-project/locations/us-central-test/reasoningEngines/123"
    )

    mock_metric_sources = []
    for metric_name in [
        "hallucination_v1",
        "final_response_quality_v1",
        "tool_use_quality_v1",
    ]:
      m = mock.Mock()
      m.metric.predefined_metric_spec.metric_spec_name = metric_name
      mock_metric_sources.append(m)
    mock_evaluator.metric_sources = mock_metric_sources
    mock_evaluator.config.random_sampling.percentage = 15

    mock_client.list_online_evaluators.return_value = [mock_evaluator]

    evaluator_name = create_online_monitor.create_agent_online_monitor(
        project_id="my-project",
        location="us-central-test",
        agent_resource_name=(
            "projects/my-project/locations/us-central-test/reasoningEngines/123"
        ),
        sampling_percentage=15,
    )

    self.assertEqual(evaluator_name, mock_evaluator.name)
    mock_client.create_online_evaluator.assert_not_called()

  @mock.patch(
      "create_online_monitor.aiplatform_v1beta1.OnlineEvaluatorServiceClient"
  )
  def test_create_agent_online_monitor_not_exists_creates_new(
      self, mock_client_class
  ):
    mock_client = mock_client_class.return_value
    mock_client.list_online_evaluators.return_value = []

    mock_operation = mock.Mock()
    mock_client.create_online_evaluator.return_value = mock_operation

    mock_response = mock.Mock()
    mock_response.name = (
        "projects/my-project/locations/us-central-test/onlineEvaluators/999"
    )
    mock_operation.result.return_value = mock_response

    evaluator_name = create_online_monitor.create_agent_online_monitor(
        project_id="my-project",
        location="us-central-test",
        agent_resource_name=(
            "projects/my-project/locations/us-central-test/reasoningEngines/123"
        ),
        sampling_percentage=15,
    )

    # Verify Endpoint and Instantiation
    mock_client_class.assert_called_once_with(
        client_options={
            "api_endpoint": "us-central-test-aiplatform.googleapis.com"
        }
    )

    # Verify API Call
    mock_client.create_online_evaluator.assert_called_once()
    call_args = mock_client.create_online_evaluator.call_args[1]
    request = call_args["request"]

    self.assertEqual(
        request.parent, "projects/my-project/locations/us-central-test"
    )
    self.assertEqual(
        request.online_evaluator.display_name, "agent-quality-monitor"
    )
    self.assertEqual(
        request.online_evaluator.agent_resource,
        "projects/my-project/locations/us-central-test/reasoningEngines/123",
    )
    self.assertEqual(
        request.online_evaluator.config.random_sampling.percentage, 15
    )

    # Verify Metric Names are set
    metrics = [
        m.metric.predefined_metric_spec.metric_spec_name
        for m in request.online_evaluator.metric_sources
    ]
    self.assertIn("hallucination_v1", metrics)
    self.assertIn("final_response_quality_v1", metrics)
    self.assertIn("tool_use_quality_v1", metrics)

    self.assertEqual(evaluator_name, mock_response.name)

  @mock.patch(
      "create_online_monitor.aiplatform_v1beta1.OnlineEvaluatorServiceClient"
  )
  def test_create_agent_online_monitor_failure(self, mock_client_class):
    mock_client = mock_client_class.return_value
    mock_client.list_online_evaluators.return_value = []
    mock_client.create_online_evaluator.side_effect = Exception("API error")

    with self.assertRaises(Exception) as context:
      create_online_monitor.create_agent_online_monitor(
          project_id="my-project",
          location="us-central-test",
          agent_resource_name=(
              "projects/my-project/locations/us-central-test/"
              "reasoningEngines/123"
          ),
      )
    self.assertIn("API error", str(context.exception))

  @mock.patch("create_online_monitor.create_agent_online_monitor")
  def test_main_cli_parsing(self, mock_create):
    test_args = [
        "create_online_monitor.py",
        "--project-id",
        "cli-proj",
        "--agent-resource-name",
        "cli-agent",
        "--sampling-percentage",
        "30",
        "--location",
        "us-east4",
    ]
    with mock.patch.object(sys, "argv", test_args):
      create_online_monitor.main()

    mock_create.assert_called_once_with(
        project_id="cli-proj",
        location="us-east4",
        agent_resource_name="cli-agent",
        sampling_percentage=30,
    )


if __name__ == "__main__":
  unittest.main()

```


### `scripts/requirements.txt`

```
google-cloud-monitoring
google-cloud-aiplatform
google-auth

```


### `scripts/validate_config.py`

```
#!/usr/bin/env python3
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""HCL configuration and PromQL validation script for agent platform metrics.

Parses Terraform HCL alert policy resource blocks, detects duplicate targets
for reasoning engines, and lints Prometheus queries for correct syntax, time
windows, and essential label filters.
"""

import argparse
import glob
import json
import os
import re
import sys


def check_balanced_chars(query, open_char, close_char):
  """Checks if parenthesis or braces are balanced."""
  count = 0
  for i, char in enumerate(query):
    if char == open_char:
      count += 1
    elif char == close_char:
      count -= 1
      if count < 0:
        return f"Unbalanced '{close_char}' at position {i}"
  if count != 0:
    return f"Unbalanced '{open_char}' (net count: {count})"
  return None


def lint_query(query):
  """Runs a suite of sanity lint checks on a PromQL query.

  Args:
      query: The PromQL query string to lint.

  Returns:
      A list of string lint error messages. Empty if valid.
  """
  errors = []

  # 1. Balanced parentheses
  paren_err = check_balanced_chars(query, "(", ")")
  if paren_err:
    errors.append(f"Parentheses error: {paren_err}")

  # 2. Balanced curly braces
  brace_err = check_balanced_chars(query, "{", "}")
  if brace_err:
    errors.append(f"Curly braces error: {brace_err}")

  # 3. Time window validations (e.g., [5m], [1w:5m], [3d], [1h])
  window_matches = re.finditer(r"\[([^\]]+)\]", query)
  for match in window_matches:
    window_str = match.group(1)
    if not re.match(r"^\d+[smhdw](:(\d+[smhdw])?)?$", window_str):
      errors.append(
          "Invalid Prometheus time window/subquery interval:"
          f" '[{window_str}]' at position {match.start()}"
      )

  # 4. Lookback offset range validation (e.g. offset 1w, offset 1d)
  offset_matches = re.finditer(r"\boffset\s+(\S+)", query)
  for match in offset_matches:
    offset_str = match.group(1)
    if not re.match(r"^\d+[smhdw]$", offset_str):
      errors.append(
          f"Invalid lookback offset format: 'offset {offset_str}' at position"
          f" {match.start()}"
      )

  # 5. Ensure the query references reasoning_engine_id in a label filter
  # or grouping aggregation.
  has_group = bool(
      re.search(r"\b(by|without)\s*\([^)]*reasoning_engine_id[^)]*\)", query)
  )

  has_filter = False
  brace_matches = re.finditer(r"\{([^}]+)\}", query)
  for match in brace_matches:
    if "reasoning_engine_id" in match.group(1):
      has_filter = True
      break

  if not (has_group or has_filter):
    errors.append(
        "Query is missing 'reasoning_engine_id' reference. It must either"
        " group by 'reasoning_engine_id' using aggregations (e.g., 'by"
        " (reasoning_engine_id)') or filter on it (e.g.,"
        " '{reasoning_engine_id=\"...\"}')."
    )

  return errors


def extract_alert_policies(hcl_content):
  """Extracts resource 'google_monitoring_alert_policy' blocks and metadata."""
  policies = []
  pattern = re.compile(
      r'resource\s+"google_monitoring_alert_policy"\s+"([^"]+)"\s*\{'
  )

  for match in pattern.finditer(hcl_content):
    resource_name = match.group(1)
    start_pos = match.start()

    brace_count = 0
    end_pos = -1
    in_string = False
    escape = False

    for i in range(match.end() - 1, len(hcl_content)):
      char = hcl_content[i]
      if escape:
        escape = False
        continue
      if char == "\\":
        escape = True
        continue
      if char == '"':
        in_string = not in_string
        continue
      if not in_string:
        if char == "{":
          brace_count += 1
        elif char == "}":
          brace_count -= 1
          if brace_count == 0:
            end_pos = i + 1
            break

    if end_pos == -1:
      continue

    block_content = hcl_content[start_pos:end_pos]

    # Extract display_name
    display_name_match = re.search(
        r'display_name\s*=\s*"([^"]+)"', block_content
    )
    display_name = display_name_match.group(1) if display_name_match else ""

    # Extract PromQL queries
    queries = [
        q.group(1)
        for q in re.finditer(
            r"query\s*=\s*<<-?EOT\n(.*?)\n\s*EOT",
            block_content,
            re.DOTALL,
        )
    ]
    if not queries:
      for match in re.finditer(
          r"query\s*=\s*\"((?:[^\"\\]|\\[\s\S])*)\"", block_content
      ):
        raw_query = match.group(1)
        clean_query = re.sub(r"\\+\"", '"', raw_query).replace("\\\\", "\\")
        queries.append(clean_query)

    # Extract threshold filters
    filters = []
    filter_matches = re.finditer(
        r'filter\s*=\s*"((?:[^"\\]|\\.)*)"', block_content
    )
    for f_match in filter_matches:
      filters.append(f_match.group(1))

    # Infer signal type
    signal_type = "unknown"
    res_lower, disp_lower = resource_name.lower(), display_name.lower()
    rules = [
        ("latency", "latency", "latency"),
        ("slo_burn_rate_fast", "fast", "slo_fast"),
        ("slo_burn_rate_slow", "slow", "slo_slow"),
    ]
    for res_pat, disp_pat, sig in rules:
      if res_pat in res_lower or disp_pat in disp_lower:
        signal_type = sig
        break
    else:
      # Check threshold filters for quality metric name
      for flt in filters:
        metric_match = re.search(
            r"metric\.labels\.evaluation_metric_name\s*=\s*\\*\"([^\"\\]+)\\*\"",
            flt,
        )
        if metric_match:
          signal_type = metric_match.group(1)
          break

    engine_ids = []
    for query in queries:
      for engine_id in re.findall(
          r"reasoning_engine_id\s*=\s*\"([^\"]+)\"", query
      ):
        if engine_id not in engine_ids:
          engine_ids.append(engine_id)

    for flt in filters:
      id_matches = re.findall(
          r'reasoning_engine_id\s*=\s*\\*"([^"\\]+)\\*"', flt
      )
      for engine_id in id_matches:
        if engine_id not in engine_ids:
          engine_ids.append(engine_id)
      resource_matches = re.findall(r"reasoningEngines/([0-9]+)", flt)
      for engine_id in resource_matches:
        if engine_id not in engine_ids:
          engine_ids.append(engine_id)

    policies.append({
        "resource_name": resource_name,
        "display_name": display_name,
        "signal_type": signal_type,
        "engine_ids": engine_ids,
        "queries": queries,
        "filters": filters,
        "start_pos": start_pos,
        "end_pos": end_pos,
        "block_content": block_content,
    })

  return policies


def validate_directory_tf_files(directory, expected_engine_var=None):
  """Scans and validates all *.tf files in a given directory."""
  tf_files = glob.glob(os.path.join(directory, "*.tf"))
  all_errors = []
  all_policies = []
  duplicates = []

  target_map = {}

  for filepath in tf_files:
    filename = os.path.basename(filepath)
    try:
      with open(filepath, "r") as f:
        content = f.read()
    except Exception as e:
      all_errors.append(f"File error in '{filename}': {e}")
      continue

    policies = extract_alert_policies(content)
    for policy in policies:
      policy["filename"] = filename
      all_policies.append(policy)

      for query in policy["queries"]:
        lint_errs = lint_query(query)
        for err in lint_errs:
          all_errors.append(
              f"Lint error in '{filename}' -> resource"
              f" '{policy['resource_name']}': {err}"
          )

      engine_key = (
          policy["engine_ids"][0]
          if policy["engine_ids"]
          else expected_engine_var or "default"
      )
      key = (engine_key, policy["signal_type"])

      if key not in target_map:
        target_map[key] = []
      target_map[key].append(policy)

  for (engine, signal_type), matches in target_map.items():
    if len(matches) > 1 and signal_type != "unknown":
      duplicates.append({
          "engine_id": engine,
          "signal_type": signal_type,
          "policies": [
              {
                  "filename": p["filename"],
                  "resource_name": p["resource_name"],
                  "display_name": p["display_name"],
              }
              for p in matches
          ],
      })

  for dup in duplicates:
    policy_list = ", ".join(
        f"'{p['resource_name']}' in '{p['filename']}'" for p in dup["policies"]
    )
    all_errors.append(
        "Duplicate Target Error: Multiple alert policies are targeting the"
        f" same engine '{dup['engine_id']}' and signal '{dup['signal_type']}':"
        f" [{policy_list}]. Please apply the in-place upgrade protocol instead"
        " of appending new blocks!"
    )

  return {
      "valid": len(all_errors) == 0,
      "errors": all_errors,
      "policies_scanned_count": len(all_policies),
      "duplicates_found": duplicates,
  }


def main():
  parser = argparse.ArgumentParser(
      description=(
          "Lints HCL alerts and PromQL query targets in standard tf templates."
      )
  )
  parser.add_argument(
      "--directory",
      type=str,
      default=".",
      help="Directory containing *.tf files to scan.",
  )
  parser.add_argument(
      "--engine-var",
      type=str,
      default="${var.reasoning_engine_id}",
      help="The expected variable or literal for the reasoning engine ID.",
  )
  parser.add_argument(
      "--file",
      type=str,
      help="Validate a single specific HCL file instead of scanning directory.",
  )
  args = parser.parse_args()

  if args.file:
    try:
      with open(args.file, "r") as f:
        content = f.read()
      policies = extract_alert_policies(content)
      errors = []
      for p in policies:
        for q in p["queries"]:
          errors.extend(lint_query(q))
      if errors:
        print(f"Validation failed for '{args.file}':", file=sys.stderr)
        for err in errors:
          print(f"  - {err}", file=sys.stderr)
        sys.exit(1)
      else:
        print(f"Validation passed for '{args.file}'!")
        sys.exit(0)
    except Exception as e:
      print(f"Error reading file '{args.file}': {e}", file=sys.stderr)
      sys.exit(1)

  results = validate_directory_tf_files(args.directory, args.engine_var)
  print(json.dumps(results, indent=2))
  if not results["valid"]:
    sys.exit(1)


if __name__ == "__main__":
  main()

```


### `scripts/validate_config_test.py`

```
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unit tests for HCL configuration alert policies parsing.

Also tests duplicate detection, and PromQL query syntax validation linting.
"""

import os
import tempfile
import unittest

import validate_config


class ValidateConfigTest(unittest.TestCase):

  def test_lint_query_valid(self):
    query = (
        "sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[5m]))"
        " by (reasoning_engine_id)"
    )
    self.assertEqual(validate_config.lint_query(query), [])

  def test_lint_query_unbalanced_parentheses(self):
    query = (
        "sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[5m]))"
        " by (reasoning_engine_id"
    )
    errors = validate_config.lint_query(query)
    self.assertTrue(any("Parentheses error" in e for e in errors))

  def test_lint_query_unbalanced_braces(self):
    query = (
        'sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{response_code!~"2.."[5m]))'
        " by (reasoning_engine_id)"
    )
    errors = validate_config.lint_query(query)
    self.assertTrue(any("Curly braces error" in e for e in errors))

  def test_lint_query_invalid_window(self):
    for invalid_suffix in ("5x", "5y"):
      query = (
          "sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count"
          f"[{invalid_suffix}])) by (reasoning_engine_id)"
      )
      errors = validate_config.lint_query(query)
      self.assertTrue(
          any("Invalid Prometheus time window" in e for e in errors)
      )

  def test_lint_query_valid_subquery_intervals(self):
    queries = [
        # With resolution
        (
            "avg_over_time((sum(rate("
            "aiplatform_googleapis_com:reasoning_engine_request_count[5m]"
            ")) by (reasoning_engine_id))[1w:5m])"
        ),
        # Without resolution
        (
            "avg_over_time((sum(rate("
            "aiplatform_googleapis_com:reasoning_engine_request_count[5m]"
            ")) by (reasoning_engine_id))[1w:])"
        ),
    ]
    for query in queries:
      self.assertEqual(validate_config.lint_query(query), [])

  def test_lint_query_invalid_subquery_intervals(self):
    queries = [
        # Invalid resolution format: number only (no unit)
        (
            "avg_over_time((sum(rate("
            "aiplatform_googleapis_com:reasoning_engine_request_count[5m]"
            ")) by (reasoning_engine_id))[1w:5])"
        ),
        # Invalid resolution format: unit only (no number)
        (
            "avg_over_time((sum(rate("
            "aiplatform_googleapis_com:reasoning_engine_request_count[5m]"
            ")) by (reasoning_engine_id))[1w:m])"
        ),
    ]
    for query in queries:
      errors = validate_config.lint_query(query)
      self.assertTrue(
          any("Invalid Prometheus time window" in e for e in errors)
      )

  def test_lint_query_missing_reference(self):
    query = "sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[5m]))"
    errors = validate_config.lint_query(query)
    self.assertTrue(
        any("missing 'reasoning_engine_id' reference" in e for e in errors)
    )

  def test_lint_query_valid_with_filter(self):
    query = 'sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{reasoning_engine_id="12345"}[5m]))'
    self.assertEqual(validate_config.lint_query(query), [])

  def test_lint_query_valid_with_regex_or_prefix_filter(self):
    query = (
        'sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{reasoning_engine_id!~"dev-.*"}[5m]))'
        " by (reasoning_engine_id)"
    )
    self.assertEqual(validate_config.lint_query(query), [])

  def test_scanner_extract_valid_hcl(self):
    hcl_content = """
        resource "google_monitoring_alert_policy" "agent_latency_anomaly" {
          project      = var.project_id
          display_name = "[Agent Alert] Latency Anomaly - ${var.agent_name}"
          combiner     = "OR"

          conditions {
            display_name = "p95 Latency exceeds 3x Standard Deviation (1w baseline)"
            condition_prometheus_query_language {
              query    = <<-EOT
                sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[5m])) by (reasoning_engine_id)
              EOT
              duration = "300s"
            }
          }
        }
        """
    policies = validate_config.extract_alert_policies(hcl_content)
    self.assertEqual(len(policies), 1)
    self.assertEqual(policies[0]["resource_name"], "agent_latency_anomaly")
    self.assertEqual(policies[0]["signal_type"], "latency")

  def test_scanner_extract_escaped_inline_query(self):
    hcl_content = r"""
        resource "google_monitoring_alert_policy" "agent_latency_anomaly" {
          display_name = "[Agent Alert] Latency Anomaly - ${var.agent_name}"
          conditions {
            condition_prometheus_query_language {
              query = "sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{reasoning_engine_id=\"12345\"}[5m]))"
            }
          }
        }
        """
    policies = validate_config.extract_alert_policies(hcl_content)
    self.assertEqual(len(policies), 1)
    self.assertEqual(len(policies[0]["queries"]), 1)
    self.assertEqual(
        policies[0]["queries"][0],
        'sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{reasoning_engine_id="12345"}[5m]))',
    )
    self.assertEqual(policies[0]["engine_ids"], ["12345"])

    hcl_content_three_backslash = r"""
        resource "google_monitoring_alert_policy" "agent_latency_anomaly" {
          display_name = "[Agent Alert] Latency Anomaly - ${var.agent_name}"
          conditions {
            condition_prometheus_query_language {
              query = "sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{reasoning_engine_id=\\\"12345\\\"}[5m]))"
            }
          }
        }
        """
    policies_three = validate_config.extract_alert_policies(
        hcl_content_three_backslash
    )
    self.assertEqual(len(policies_three), 1)
    self.assertEqual(len(policies_three[0]["queries"]), 1)
    self.assertEqual(
        policies_three[0]["queries"][0],
        'sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count{reasoning_engine_id="12345"}[5m]))',
    )
    self.assertEqual(policies_three[0]["engine_ids"], ["12345"])

  def test_validator_detects_duplicates(self):
    with tempfile.TemporaryDirectory() as tmpdir:
      tf_content_1 = """
            resource "google_monitoring_alert_policy" "agent_latency_anomaly_1" {
              display_name = "[Agent Alert] Latency Anomaly - ${var.agent_name}"
              conditions {
                condition_prometheus_query_language {
                  query = "sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[5m])) by (reasoning_engine_id)"
                }
              }
            }
            """
      tf_content_2 = """
            resource "google_monitoring_alert_policy" "agent_latency_anomaly_2" {
              display_name = "[Agent Alert] Latency Anomaly - Alternative"
              conditions {
                condition_prometheus_query_language {
                  query = "sum(rate(aiplatform_googleapis_com:reasoning_engine_request_count[5m])) by (reasoning_engine_id)"
                }
              }
            }
            """
      with open(os.path.join(tmpdir, "policy1.tf"), "w") as f:
        f.write(tf_content_1)
      with open(os.path.join(tmpdir, "policy2.tf"), "w") as f:
        f.write(tf_content_2)

      results = validate_config.validate_directory_tf_files(
          tmpdir, "${var.reasoning_engine_id}"
      )
      self.assertFalse(results["valid"])
      self.assertEqual(len(results["duplicates_found"]), 1)
      self.assertEqual(results["duplicates_found"][0]["signal_type"], "latency")

  def test_scanner_extracts_quality_metric_signal_type(self):
    hcl_content = """
        resource "google_monitoring_alert_policy" "agent_final_response_quality" {
          project      = var.project_id
          display_name = "Agent Final Response Quality (Median < 0.8)"
          combiner     = "OR"
          enabled      = true

          conditions {
            display_name = "Final Response Quality Score"
            condition_threshold {
              filter          = "resource.type=\\"aiplatform.googleapis.com/OnlineEvaluator\\" AND metric.type=\\"aiplatform.googleapis.com/online_evaluator/scores\\" AND metric.labels.evaluation_metric_name=\\"final_response_quality_v1\\""
              comparison      = "COMPARISON_LT"
              threshold_value = 0.8
              duration        = "300s"
              aggregations {
                alignment_period   = "300s"
                per_series_aligner = "ALIGN_PERCENTILE_50"
              }
              trigger {
                count = 1
              }
            }
          }
        }
        """
    policies = validate_config.extract_alert_policies(hcl_content)
    self.assertEqual(len(policies), 1)
    self.assertEqual(
        policies[0]["resource_name"], "agent_final_response_quality"
    )
    self.assertEqual(policies[0]["signal_type"], "final_response_quality_v1")

  def test_validator_detects_quality_duplicates(self):
    with tempfile.TemporaryDirectory() as tmpdir:
      tf_content_1 = """
            resource "google_monitoring_alert_policy" "q1" {
              display_name = "Quality Alert 1"
              conditions {
                condition_threshold {
                  filter = "resource.type=\\"aiplatform.googleapis.com/OnlineEvaluator\\" AND metric.type=\\"aiplatform.googleapis.com/online_evaluator/scores\\" AND metric.labels.evaluation_metric_name=\\"tool_use_quality_v1\\""
                }
              }
            }
            """
      tf_content_2 = """
            resource "google_monitoring_alert_policy" "q2" {
              display_name = "Quality Alert 2"
              conditions {
                condition_threshold {
                  filter = "resource.type=\\"aiplatform.googleapis.com/OnlineEvaluator\\" AND metric.type=\\"aiplatform.googleapis.com/online_evaluator/scores\\" AND metric.labels.evaluation_metric_name=\\"tool_use_quality_v1\\""
                }
              }
            }
            """
      with open(os.path.join(tmpdir, "policy1.tf"), "w") as f:
        f.write(tf_content_1)
      with open(os.path.join(tmpdir, "policy2.tf"), "w") as f:
        f.write(tf_content_2)

      results = validate_config.validate_directory_tf_files(
          tmpdir, "${var.reasoning_engine_id}"
      )
      self.assertFalse(results["valid"])
      self.assertEqual(len(results["duplicates_found"]), 1)
      self.assertEqual(
          results["duplicates_found"][0]["signal_type"], "tool_use_quality_v1"
      )


if __name__ == "__main__":
  unittest.main()

```
