agent: optimize and evaluate local-model Kubernetes exploration - #185
Conversation
#177) Baseline on the #69 pack (qwen3:4b, small profile, 24 scenarios) measured four prompt-caused failures; each gets a targeted rule, pinned by a new prompt-invariant test: - exit-code over-anchoring: the worked example taught 137=OOMKilled, so a liveness-probe kill (exit 137) was misdiagnosed as OOM. The example now reads reason=OOMKilled exit=137 and an explicit rule says the reason string, not the exit code, names the cause. - pointer-chasing stopped one hop short: unbound-PVC and service-endpoint answers ended at the pointer; new rule fetches the object a result points at before answering. - decisive reason strings never quoted: a correct backoff-limit diagnosis failed grading because BackoffLimitExceeded was never cited; new rule quotes decisive reasons verbatim. - healthy negative controls diagnosed as faults: new rule allows a plain healthy verdict and marks stopped restarts as history, not a live fault. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…es (#177) Round 2 (after +2 scenarios on round 1): liveness kills attributed to the kubelet, healthy verdicts allow 'recovered', PVC chains extend to the storage class. Round 3 (measured contrastive rule-outs tripping the misdiagnosis gate): single-root-cause rule — never name faults ruled out; 'not X but Y' still claims X. Reason strings copied word-for-word. Round 4 (measured example-parroting and narrated plans): the worked example is marked method-only and never-reuse; plans must become tool calls; healthy verdicts name the passing checks (available, ready, endpoints present). Each rule is pinned in the prompt-invariant test with the measured failure it answers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ion (#177) Rounds 5-6, measured on a 10-scenario targeted subset x3 reps (qwen3:4b): the OOM worked example was parroted verbatim for liveness kills even with a method-only marker, so the example now demonstrates the discrimination itself - last-exit=137 plus a quoted 'Liveness probe failed' event, the event reason deciding, the exit code cited alongside. Round 5 fixed the liveness parrot (3/3) but dropped the exit-code citation habit (oom-killed 3/3 -> 1/3 missing '137'); round 6 restored it by citing exit codes in both the rule and the example answer. Healthy verdicts are mechanical now: start the answer with healthy and name the passing checks; a ready pod is healthy even after restarts. Targeted subset (the 8 measured failures + 2 regression guards): baseline 4/10 -> round 6 22/30 across three reps, with liveness-probe, oom-killed, quota-blocked, job-backoff and crashloop-app-panic all 3/3. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The remaining eval failures had deterministic evidence gaps rather than prompt gaps: diagnose_pod reduced mounted PVCs to phase-only lines and presented previous-instance errors without an explicit current verdict. Add a current-health projection before historical evidence, and include PVC storage class plus warning events in RELATED. This exposes missing StorageClasses in one call and tells small models when old restart logs are history rather than a live fault. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Small models often stop at ProgressDeadlineExceeded instead of following the Deployment -> ReplicaSet -> Pod chain to the concrete container failure. Add diagnose_workload for Deployments: it gathers parent conditions/events, walks immutable owner UIDs, and embeds bounded diagnoses for non-ready owned pods. The tool is read-only on agent and MCP surfaces and mirrors the workload describe view in follow mode. The live-like eval fixtures omitted ownerReference.uid even though the API always supplies it; add an integrity test and repair those fixtures so the fake cluster exercises the same ownership traversal as Kubernetes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Treat diagnose_workload as an alternative ground-truth route for the stuck-rollout scenario, while retaining the original manual get_resource/diagnose_pod path. Fixture-integrity tests execute the new route through the real ToolExecutor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ording (#177) The enriched diagnose_pod now fetches a mounted PVC's warning events, so credit that one-call route as an alternative to a direct get_events call. Also accept semantically equivalent 'deployment is stuck' and 'rollout is stalled' wording that the compound rollout report reliably produces. Both routes remain fixture-integrity tested through the real executor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address review findings: classify successfully completed pods separately from unhealthy pods, distinguish defaulted PVC StorageClasses from an explicit no-class request, and isolate disappearing pods while rendering a Deployment diagnosis so parent and sibling evidence survive rollout churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Define the local-installable model candidate set, MacBook memory tiers, two-stage promotion gate, conversational journey scoring, retained AKS D32 Spot pool, model storage, grades, and canonical scoreboard protocol. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Large local models on CPU exceed the provider's fixed 60-second read timeout even after warm-up. Add a configurable provider timeout and expose it to the eval CLI as KORVID_EVAL_TIMEOUT_SECONDS, while preserving the 60-second production default. Empty timeout exceptions now surface their type instead of producing blank diagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the 8GB Mac/consumer Windows tier, record its AKS smoke findings, distinguish practical local recommendations from enthusiast/server references, and promote Qwen3 1.7B as the evidence-capable 8GB candidate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add strict multi-turn journey fixtures, a persistent AgentRuntime runner, deterministic per-turn grading for evidence/corrections/call budgets/UI intent, JSON/Markdown CLI output, and a fail-closed live adapter restricted to the dedicated contract-test context and owned namespace prefix. Seed journeys for broad triage with user correction, logs-to-events pivot, and healthy-stop behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the application's collision-safe discovery alias builder so the core pods resource cannot be overwritten by metrics.k8s.io PodMetrics. Add the guarded real-cluster triage journey used against isolated faults in the dedicated contract-test cluster. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document the three evaluation tiers, all 23 task scenarios, offline and real-cluster journeys, exact reproduction commands, model-specific failure analysis, and usability verdicts. Keep generated raw outputs on the append-only eval-results branch and link its archive, metadata, and checksums. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address pre-PR review findings: confine every live read to the owned namespace and reject cluster-scoped access; count every emitted call, including discarded parallel calls, for budgets and stale-target checks; validate JSON argument types; strengthen the live corrective-action grade; and make rollout diagnosis robust before discovery, readiness gates, and same-name Pod replacement races. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prevent generic or explicitly valid image-tag wording from satisfying the live corrective-action turn; only the known invalid image or clear invalid/nonexistent/wrong tag language receives credit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Improves local-model Kubernetes diagnostics and adds conversational evaluation infrastructure.
Changes:
- Adds richer Pod and Deployment diagnostics.
- Adds offline/live multi-turn journey evaluation.
- Documents methodology, scenarios, and model results.
Reviewed changes
Copilot reviewed 42 out of 42 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
tests/tools/test_registry.py |
Registers workload diagnosis. |
tests/tools/test_follow.py |
Tests workload UI following. |
tests/tools/test_executor.py |
Tests enhanced diagnostics. |
tests/tools/test_diagnose.py |
Tests current-health projection. |
tests/providers/test_openai_compat.py |
Tests configurable timeout. |
tests/evals/test_scenarios_pack.py |
Validates evidence paths and UIDs. |
tests/evals/test_live_journey.py |
Tests live safety adapter. |
tests/evals/test_journeys_cli.py |
Tests journey CLI options. |
tests/evals/test_journey.py |
Tests journey schema. |
tests/evals/test_journey_runner.py |
Tests persistent journey execution. |
tests/evals/test_cli.py |
Tests eval timeout configuration. |
tests/agent/test_runtime.py |
Tests empty provider errors. |
tests/agent/test_profiles.py |
Pins small-model prompt rules. |
src/korvid/tools/registry.py |
Defines diagnose_workload. |
src/korvid/tools/follow.py |
Mirrors workload reads in UI. |
src/korvid/tools/executor.py |
Implements compound diagnostics. |
src/korvid/tools/diagnose.py |
Adds current Pod health. |
src/korvid/providers/openai_compat.py |
Supports custom read timeouts. |
src/korvid/k8s/models.py |
Tracks Pod Ready conditions. |
src/korvid/evals/scenarios/stuck-rollout.yaml |
Adds workload evidence route. |
src/korvid/evals/scenarios/quota-blocked-scheduling.yaml |
Repairs owner UID. |
src/korvid/evals/scenarios/pvc-pending-no-storageclass.yaml |
Adds compound PVC evidence. |
src/korvid/evals/scenarios/job-backoff-limit-exceeded.yaml |
Repairs owner UID. |
src/korvid/evals/scenarios/healthy-deployment.yaml |
Repairs ownership chain. |
src/korvid/evals/scenarios/crashloop-app-panic.yaml |
Repairs ownership chain. |
src/korvid/evals/live_journeys/triage-and-correct.yaml |
Defines live correction journey. |
src/korvid/evals/live_journey.py |
Adds namespace-bound live adapter. |
src/korvid/evals/journeys/triage-and-correct.yaml |
Adds correction journey. |
src/korvid/evals/journeys/logs-to-events.yaml |
Adds evidence-pivot journey. |
src/korvid/evals/journeys/healthy-stop.yaml |
Adds healthy-stop journey. |
src/korvid/evals/journeys_cli.py |
Adds journey CLI. |
src/korvid/evals/journey.py |
Defines strict journey schema. |
src/korvid/evals/journey_runner.py |
Runs and grades conversations. |
src/korvid/evals/fake_kube.py |
Generalizes fixture support. |
src/korvid/evals/__main__.py |
Reads eval timeout environment setting. |
src/korvid/agent/runtime.py |
Improves empty exception messages. |
src/korvid/agent/prompts.py |
Tunes small-model policy. |
docs/evals/scoreboard.md |
Publishes model results. |
docs/evals/scenarios.md |
Catalogs evaluation scenarios. |
docs/evals/methodology.md |
Documents evaluation protocol. |
docs/dev/specs/2026-08-04-local-model-aks-evaluation-design.md |
Defines evaluation design. |
docs/agent.md |
Documents journey commands. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
Suppressed comments (6)
src/korvid/tools/diagnose.py:188
- Completed init containers are included in
_container_statuses(), and their Kubernetesreadyfield is false once they terminate. Consequently, a Running Pod withReady=True, all app containers ready, and any successful init container is reported asUNHEALTHY NOW. Compute readiness from regular container statuses only; init statuses can still contribute to restart/completion history.
all_ready = bool(statuses) and all(cs.get("ready") is True for _prefix, cs in statuses)
docs/evals/scoreboard.md:7
- This row promotes Qwen3 8B to “Recommended default” from a task score explicitly based on one run. The linked issue's publication protocol requires the 23-scenario task pack and journey pack at least three times with mean and variance, and currently labels this task result provisional. Complete the three-run task matrix and publish its statistics, or keep this verdict provisional.
| **Qwen3 8B** | 16GB Mac / 8GB VRAM Windows | 20/23 (87%, one run) | **6/9 (66.7%)** | **3/3 (100%)** | 0 / 0 | **Recommended default** |
src/korvid/tools/executor.py:1177
- The workload report can append three 2,200-character diagnoses plus up to roughly 4,000 characters of parent sections, then
compact_resultremoves the middle to fit 8,000 characters (and the small profile compacts it again to 3,000). With valid large event/log payloads, entire first or middle Pod diagnoses—including their headers and distinct causes—can disappear, so not every selected non-ready Pod is actually exposed to the model. Budget the complete report before rendering, for example by expanding one Pod and retaining short attributed summaries for every sibling.
report.extend(
f" {line}"
for line in compact_result(
diagnosis, self._DIAGNOSE_WORKLOAD_POD_BUDGET
).splitlines()
src/korvid/evals/main.py:57
float()acceptsnanandinf, and both bypass this<= 0check, so invalid environment values are accepted even though the option promises a positive timeout. Reject non-finite values before constructing the HTTP client.
if timeout_seconds <= 0:
raise SystemExit("KORVID_EVAL_TIMEOUT_SECONDS must be a positive number.")
tests/evals/test_journey.py:138
- The issue this PR closes requires at least eight conversational journeys, but this test pins the bundled pack to only three. The missing standalone coverage includes Deployment ownership traversal, cross-namespace comparison, and RBAC-limited exploration, so the published 9-turn scores are not results for the required journey pack. Add the remaining journeys before treating #176 as complete.
src/korvid/evals/journey_runner.py:97 - Unlike the existing task runner, journey results do not record write attempts or safety violations, and
_TurnTallyignoresToolCallFinished.okfor write tools. A model-emitted write call is therefore folded into “malformed” and the journey report cannot substantiate the claimed zero-safety-violation invariant. Trackwrite_attemptsand successful write calls separately and include them in success/reporting, as required by #176.
malformed_tool_calls: int
forbidden_target_calls: int
error: str | None
Fix workload status projection and whole-report budgeting, ignore completed init containers in current readiness, reject non-finite eval timeouts, and record journey write attempts/safety violations. Clarify that current conversation and Qwen3 8B results are provisional three-journey/one-task-run findings while #176 tracks the eight-journey and three-repetition target. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Suppressed review finding also addressed in 6b210a7: journey results now record |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
src/korvid/evals/journeys_cli.py:66
- A missing or empty
--journeysdirectory currently produces an empty table and exits successfully, so a path typo can look like a completed evaluation. Reject an empty pack here, matching the task CLI's fail-closed check insrc/korvid/evals/__main__.py:180-182.
journeys = load_journeys(args.journeys)
src/korvid/tools/executor.py:1164
- The selected Pods depend on Kubernetes LIST order, which is not a guaranteed diagnostic ordering. When more than three non-ready Pods exist across old and current ReplicaSets, historical Pods can consume the expansion budget and hide the current rollout's failing Pod. Rank Pods deterministically—preferably by owning ReplicaSet revision/creation and then Pod name—before applying this slice.
selected = non_ready[: self._DIAGNOSE_MAX_WORKLOAD_PODS]
omitted = non_ready[self._DIAGNOSE_MAX_WORKLOAD_PODS :]
src/korvid/evals/journeys_cli.py:114
- The CLI returns nonzero for provider/runtime failures, but neither the markdown table nor stderr exposes
turn.error; a timeout therefore reports only a failed aggregate with no actionable cause. Print each journey/run/turn error before returning, as the task evaluator'sexit_code()does.
errored = any(
turn.error is not None for report in reports for run in report.runs for turn in run.turns
)
return 1 if errored else 0
Use a readiness-only Pod status verdict unless warnings and conditions are also clear, rank rollout Pods by newest ReplicaSet revision before applying the expansion cap, and make journey CLI fail closed on empty packs while printing per-turn provider/runtime errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Suppressed round-2 findings addressed in 0a0d343:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/korvid/evals/journey_runner.py:219
- Valid cluster-scoped calls are still misgraded when the model uses the canonical kind spelling (for example
get_resource(kind="Node", name="x")) or surrounding whitespace.ToolExecutorstrips and lowercaseskind, but this lookup uses the raw value, so it missesNodeand incrementswrong_namespace_calls, causing an otherwise valid turn to fail. Normalize the resource key before resolving its scope (and useviewfor navigation calls, which names the resource in that field).
kind = arguments.get("kind")
meta = _RESOURCE_ALIASES.get(kind) if isinstance(kind, str) else None
return meta is None or meta.namespaced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the latest suppressed review finding in commit |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/korvid/tools/executor.py:1255
- A child Pod failure outside
ApiStatusError/ValueError(for example, a transport or response-decoding error fromKubeClient.get_object) escapes this loop, soToolExecutor.executereplaces the entire workload report withERROR:and loses the already collected Deployment and sibling evidence. The other best-effort reads in this diagnostic catch arbitrary read failures; isolate each selected Pod the same way so one transient child failure cannot discard the parent diagnosis.
except (ApiStatusError, ValueError) as exc:
diagnosis = f"unavailable ({exc})"
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the latest suppressed finding in commit |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/korvid/tools/executor.py:1253
selectedis priority-sorted newest-first, but the small profile later head/tail-compacts this 8,000-character report to 3,000 characters. With three verbose Pod blocks, the first/highest-priority Pod's decisive events and logs land in the discarded middle while the last/lower-priority Pod occupies the preserved tail. The added regression only verifies the duplicated summary headers/phases, not that each selected diagnosis's evidence remains model-visible. Arrange the detailed blocks or add compact synopses so the highest-priority evidence survives the small-profile cap.
for pod in selected:
try:
diagnosis = await self._diagnose_pod(
{"pod": pod.name, "namespace": namespace},
expected_uid=pod.uid,
expected_owner_uids=replica_uids,
)
src/korvid/evals/scenarios/stuck-rollout.yaml:27
- Adding
diagnose_workloadonly to the second evidence group does not actually credit the advertised one-call route. The grader ANDs evidence groups, soget_resourceremains mandatory even thoughdiagnose_workloadreturns bothProgressDeadlineExceededandImagePullBackOff; a model following the new tool guidance still fails evidence grading unless it makes a redundant call. Makediagnose_workloadan alternative in the first group as well.
- tool: diagnose_workload
args: {kind: deployments, name: api, namespace: shop}
contains: ImagePullBackOff
Count actual live namespace-bound read rejections in journey grading, and preserve the highest-priority rollout Pod diagnosis at the small-profile tail cap.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Review follow-up in |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/korvid/agent/prompts.py:117
- The worked example invents a
/liveendpoint even though the stated tool result only reports a liveness-probe timeout. Probe paths vary by workload, so this teaches the small model to assert unsupported evidence and conflicts with the prompt's grounding rule. Keep the recommendation path-agnostic (or include the actual path in the example evidence).
"137; fix the /live endpoint or relax the probe timeout."
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the latest suppressed prompt finding in |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/korvid/evals/live_journey.py:43
- The fail-closed guard accepts the bare prefix
korvid-agent-eval-.guard_namespace_ownership()then derives an empty run ID, so a namespace withkorvid.dev/eval-run: ""passes despite having no run-specific identity. Require a non-empty suffix before connecting to the cluster.
if not namespace.startswith(NAMESPACE_PREFIX):
raise ValueError(
f"live journey namespace prefix must be {NAMESPACE_PREFIX!r}, got {namespace!r}"
)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the latest suppressed live-guard finding in |
Summary
Advances #176. The standardized matrix remains open for the eight-journey pack and three task repetitions per published model.
Makes Korvid's local/on-prem agent behavior measurable and improves small-model Kubernetes exploration from prompt policy through deterministic evidence tools and conversational evaluation.
Runtime and tool improvements
smallprofile prompt from measured Qwen failures: reason-over-exit-code diagnosis, one-root-cause answers, healthy/recovered handling, evidence citation, no narrated tool plans, and an anti-anchoring worked example.diagnose_podwith explicit current health, successful-completion handling, PVC StorageClass and provisioning events, and clearer separation of historical restart evidence.diagnose_workloadfor Deployment rollout diagnosis: conditions/events, immutable owner-UID traversal, bounded non-ready Pod expansion, readiness-gate handling, and replacement/disappearance race isolation.KORVID_EVAL_TIMEOUT_SECONDSfor slow local inference while retaining upstream corporate-CA support and the 60-second production default.Evaluation harness
AgentRuntimeacross turns.aks-korvid-contract-testandkorvid-agent-eval-*namespaces, with namespace-confined read operations and no model write surface.Documentation and measured results
docs/evals/.eval-resultsbranch with metadata and SHA-256 checksums.Key measured results under the standardized AKS protocol:
The real journey created actual isolated AKS states (healthy Pod, missing ConfigMap, nonexistent image), tested broad discovery → user redirection → visible evidence, deleted the run namespace, stopped the contract cluster, and returned the retained model pool to zero nodes.
Safety
Verification
make check