JJoeven

Projects/Autonomous Ops Agent/Part 2

Metrics World and Read Tools

Simulate services, SLOs, logs, and read-only tools that never mutate world state.

The environment is a mutable dict the mutate tools will change later. Read tools must not change it. That split is a test: call get_metrics a thousand times, hashes of WORLD stay equal. This part builds the world, SLOs, log lines, and read APIs.

World schema

Each service:

  • error_rate float 0–1
  • p95_ms int
  • cpu float 0–1
  • deploy_id, prev_deploy_id
  • replicas int
  • slo_error, slo_p95_ms
  • logs list of strings (ring buffer)

A healthy checkout sits at 0.4% errors, p95 220ms. An incident snapshot starts unhealthy so the agent has work. You can also store SCENARIO = "bad_deploy" to pick log lines that mention NullPointer after deploy d44 — diagnosis should point at rollback, not scale.

Read tools

  • get_metrics(service) → copy of numeric fields (not necessarily all logs).
  • get_logs(service, n=20) → last n lines.
  • list_services() → names.
  • get_slo(service) → slo fields.

Return {"error": "unknown_service"} for typos. Copies via dict so the agent cannot mutate WORLD by accident through a returned object (in Python the numbers are immutable; lists of logs need list(...)).

Live Pythonpython
Output
Run to execute this in your browser. Nothing is sent to a server.

Step-by-step environment rules

  1. Deep-copy in tests when you need a snapshot; for the inequality check above, copy.deepcopy before reads.
  2. Cap log n. Models will request 10 million lines.
  3. Do not put secrets in logs. Fake logs have no API keys. Add a redaction function if a line matches sk-.
  4. Multiple services so diagnosis cannot be "always checkout".

SLO burn as a boolean

A tiny helper burning(service) is not a mutate tool. It is allowed in supervisor code. You can also expose it as check_slo(service) read tool so the model does not do inequality wrong. Prefer tools that compute the predicate for ops — arithmetic in LLMs is sloppy.

Note:Metrics are the observations. Diagnosis is a policy. Keep them separate so you can test burn math without the model.

Cardinality and time

Real metrics systems have labels (region, version, customer). Your WORLD can grow a labels dict later; do not let the model pass arbitrary PromQL. If you add a query_metrics(expr) tool, you have built SQL-injection-for-ops. Stick to get_metrics(service) until you have a parser.

Time is a list of snapshots if you need it: HISTORY[service] = [snapshot, ...]. The agent may read the last two to say "error_rate jumped after d44." Do not generate 10,000 fake points in the Try it box. Two snapshots already teach change, which is what diagnosis needs.

Read tools should be idempotent: same arguments, same result, until a mutate happens. If get_logs pops the ring buffer, you have built a destructive read. Tests will flake and diagnosis will lose evidence. Copy the tail; do not consume it. Metrics snapshots should be JSON-serializable so you can hash them in the audit log.

Exercise

Add a payments service that is healthy. Confirm list_services includes it. Add a log line containing sk-demo-not-real and a redact(lines) that replaces sk- tokens with sk-***. Logs tools must use it.

Check your understanding

Why must get_metrics return a copy of fields rather than the live nested dict with logs?