~ / work / k8s-agent

k8sAgent: an agentic SRE copilot for Kubernetes_

Not a chatbot with cluster access — an agent designed like an SRE works: plan the investigation, gather evidence in parallel, critique its own conclusion against live cluster state, and only answer when the evidence converges. Built by someone who has operated 150+ clusters and 500+ GPUs, for the questions those fleets generate all day.

role
architect & author
core stack
Python · LiteLLM (AWS Bedrock, model-agnostic) · MCP tool layer · LangGraph-style control loop · asyncio
llm ops
Langfuse — tracing, prompt versioning, golden-set evaluation · per-query token budgets
safety
RBAC-scoped read-only ServiceAccount · mutations only as GitOps PRs behind human approval

the thesis

Every platform team is one team supporting forty. The bottleneck isn't fixing things — it's the endless stream of questions: what's running here, why is that crashing, who's eating this node. Each one costs a senior engineer a context switch. k8sAgent's design goal is precise: answer the questions a mid-level SRE could answer, with the evidence a senior SRE would demand, at the cost of an API call.

That framing drives everything below. A demo agent answers questions; a production agent has to be right, provably, or say it isn't sure. The architecture is organized around that difference.

the convergence loop

The heart of the system is a state machine — plan, act, observe, critique — that iterates until the answer is supported by evidence or the agent explicitly asks for clarification. No single-shot answers on diagnostic queries, ever:

query + intent router PLAN pick tools + budget ACT parallel MCP tools OBSERVE schema-filter + _hint CRITIQUE evidence sufficient? contradictions? ANSWER + EVIDENCE every claim cites tool output ASK / RE-PLAN low confidence → clarify, don't guess converged not yet loop (max N iterations, token budget enforced) Langfuse traces every hop · scores · prompt versions · eval runs

Three properties make the loop production-shaped rather than demo-shaped:

Bounded. Hard iteration cap and a per-query token budget. An agent that can loop forever is an agent that can spend forever — the budget is enforced in the controller, not suggested in the prompt.

Self-critical. The critique step re-reads the assembled evidence against the original question: are all claims backed by a tool result? do any two results contradict? is a follow-up tool call cheaper than a wrong answer? Diagnosis-class queries route through this gate every iteration.

Honest under uncertainty. Below the confidence threshold, the agent asks a clarifying question or states exactly which evidence is missing. "I don't know yet, here's why" is a first-class terminal state — the one most agents are missing.

system architecture

 interaction layer     web console · Slack · CLI  (same API)
        │
 intent router         small model, temp 0 — domain (k8s/aws) + activity
        │                (list · describe · diagnose) — cheap, fails safe
        ▼
 orchestrator          LangGraph-style state machine — the convergence loop
        │                per-query budget · iteration cap · confidence gate
        ▼
 MCP tool layer        k8s_tool · aws_tool · file_tool — stdio contract,
        │                parallel fan-out, read-only ServiceAccount
        ▼
 context engine        YAML → schema extraction → LLM key-filter (batched,
        │                persistent cache) → projected JSON + _hintobservability         Langfuse traces · scores · prompt registry · evals

Model-agnostic by construction. LiteLLM abstracts the provider — Bedrock today, anything tomorrow — so model choice is a config value per pipeline stage: a small fast model for routing and schema filtering, a strong model for diagnosis. Each stage's model, prompt version, and cost are independently swappable and independently measured.

Context engineering as a subsystem. Raw kubectl -o yaml would drown any model. The context engine extracts each resource's schema, has a small LLM select relevant keys for this query (one batched call per iteration), projects the YAML down, and attaches an intent-aware _hint so formatting knowledge travels with the data. Schema decisions persist in a cache that survives restarts; trivial queries bypass the filter entirely via regex. Token spend per query drops by an order of magnitude before the answer model sees a byte.

evaluation: the part that makes it an engineering project

Prompts change; without regression testing, every change is a gamble. The eval harness is built on Langfuse: a golden set of cluster questions with known-correct answers (seeded from real fleet scenarios — CrashLoopBackOff with bad image, OOMKilled with memory pressure, pending pods with taint mismatches), replayed against every prompt or model change. Each run scores correctness (does the answer match ground truth), groundedness (is every claim traceable to a tool result), and efficiency (iterations and tokens to convergence). A prompt change that improves tone but regresses groundedness doesn't ship. Every production trace feeds the same pipeline — real queries that converge slowly become tomorrow's eval cases. A gate run looks like this (representative output):

shell · eval gate (representative run)
$ python -m evals.run --suite golden-v3 --gate groundedness>=0.98

suite: golden-v3 · 24 scenarios          prompt: diagnose@v14
─────────────────────────────────────────────────────────────
correctness    23/24  (0.958)            ✓ gate ≥ 0.95
groundedness   1.000                     ✓ gate ≥ 0.98
efficiency     avg 2.1 iterations · 9.3k tokens/query
vs diagnose@v13: +1 correctness · −8% tokens

RESULT: SHIP — promoted to prompt registry as diagnose@v14

the safety model

Designed with the same posture as fleet tooling: the read path is scoped, the write path is gated.

The agent runs under a dedicated read-only RBAC ServiceAccount — its permissions are a Kubernetes object you can audit, not a promise in a prompt. Namespace scoping limits blast radius per deployment.

Mutations never execute directly — by design, the write path (roadmap v2) ships changes only as GitOps pull requests — the agent proposes the manifest diff, a human approves, ArgoCD applies. The agent gets the leverage of action with the audit trail and rollback story of GitOps, and an occasional wrong proposal costs a PR review instead of an incident.

Every interaction is traced end-to-end in Langfuse — who asked what, which tools ran, what the model saw, why it answered as it did. An agent you can't replay is an agent you can't trust in a postmortem.

console wireframe

The interface is designed around a rule: never show a conclusion without its evidence. Answer on the left; the tool results it cites and the live trace on the right — one click from any claim to the JSON that supports it:

⎈ k8sAgent cluster: prod-payments ▾ rbac: read-only trace ↗ langfuse CONVERSATION you: why is checkout-api crashlooping? agent · converged in 2 iterations · 6 tool calls Root cause: OOMKilled. Container limit 512Mi; actual usage peaks 740Mi after v2.3.1 deploy. [evidence 1] pod events — OOMKilled ×14 [evidence 2] metrics — memory 740Mi > limit [evidence 3] deploy history — limit unchanged in v2.3.1 → PR: raise limit to 1Gi + HPA target ask about this cluster… EVIDENCE · tool results k8s.pod_events → 14 × OOMKilled _hint: table · 3 keys of 41 kept k8s.pod_metrics → mem 740Mi / 512Mi _hint: threshold compare k8s.rollout_history → v2.3.1 (2h ago) _hint: timeline TRACE · this answer route 0.2s · ▮▮ plan 0.8s ▮▮▮▮ tools ×6 parallel 1.9s critique 1.1s → converged ✓ tokens: 8.4k / 40k budget · $0.011 groundedness 1.0 · iterations 2/5

from the code: intent-aware grounding

The _hint generator reads the user's intent, not just the data — formatting knowledge travels with each tool result:

python · controller.py (excerpt)
if format_type == "list":
    if any(w in q for w in ["how many", "count", "total"]):
        return f"User wants a count. One sentence: there are {item_count} items."
    if any(w in q for w in ["failing", "crashloop", "oomkilled"]):
        return "Present ALL items as a table: name, namespace, status, reason. " \
               "Do not drop rows. Do not give fix steps — just the table."
# "failing pods" = a table, NOT a diagnosis — activity wins over keywords

roadmap

v1 — shipped
read path: routing, convergence loop, MCP tools, context engine, tracing, web console
v1.x — hardening
golden-set expansion, per-team RBAC profiles, Slack surface, cost dashboards per team
v2 — write path
GitOps PR proposals behind approval gates; runbook-aware remediation suggestions
v3 — proactive
subscribed to fleet events: the agent opens the investigation before the human asks
KubernetesAgentic AIMCP LiteLLM · BedrockLangfuseLangGraph-style loopAI × SRE
← prev: self-service onboarding next: k8lens →