R7: pluggable SelectorPolicy seam + local-OSS (Ollama) confirmatory agent arm

Adds the RQ3 agent-selector backend as a reproducible, offline measurement
decision — not relay/data-plane traffic. Containment is untouched: the model
query is a local call to localhost:11434.

- selector.py: SelectorPolicy ABC seam; static/random/agent(heuristic) built-ins;
  make_policy() resolves (strategy, agent_backend); run_selection writes a
  write-once selector.json provenance sidecar.
- agent_selector.py: OllamaAgentPolicy — reproducible confirmatory arm
  (temp=0 + per-run seed, decisions cached keyed by (seed, state-hash),
  deterministic heuristic fallback on any model/parse failure, model weights
  digest pinned for the manifest). ClaudeExploratoryPolicy — EXPLORATORY-only
  stub: makes no paid call, refuses to serve as a confirmatory backend.
- provenance.py: additive selector_backend field so an agent-selected run pins
  the exact model id + weights digest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-07-19 18:15:28 -07:00
parent 12fd2537e3
commit 0734d18989
4 changed files with 570 additions and 42 deletions
+216
View File
@@ -0,0 +1,216 @@
"""R7 — Agent selector backends (RQ3 treatment arm).
Two backends plug into the :class:`~cmd_chat.sor.selector.SelectorPolicy` seam:
* :class:`OllamaAgentPolicy` — the **reproducible confirmatory** agent arm. It
asks a *local, open-source* model (served by Ollama on localhost) to rank the
live pool and pick a rebuild circuit. Reproducibility is engineered in, not
hoped for:
- inference is pinned (``temperature=0`` + per-run ``seed``);
- every decision is **cached keyed by (seed, state-hash)**, so a replay is
byte-identical regardless of any model nondeterminism;
- any model/parse/validation failure falls back to the deterministic local
heuristic, so a run never breaks and never silently drifts;
- the model id + weights **digest** are recorded for the provenance manifest.
This is a *local* call to ``localhost:11434`` — no external target, no relay
traffic; it is a measurement decision, not part of the data plane.
* :class:`ClaudeExploratoryPolicy` — an **EXPLORATORY-only** hook for a Claude
Code / frontier arm. It is deliberately inert: it makes **no paid model call**
and refuses to run as a confirmatory backend (the paid frontier arm is human +
budget gated, GOAL autonomy envelope (c)). It exists so the seam is documented,
not so it can spend.
"""
from __future__ import annotations
import hashlib
import json
import urllib.error
import urllib.request
from typing import Any, Dict, List, Optional
from cmd_chat.sor.selector import HeuristicAgentPolicy, SelectorContext, SelectorPolicy
DEFAULT_MODEL = "qwen2.5:3b"
DEFAULT_HOST = "http://localhost:11434"
def _state_hash(live: List[str], ctx: SelectorContext) -> str:
"""A stable hash of the decision state — the same live pool + churn history +
seed + hops always hashes identically, so it is a sound cache key."""
blob = json.dumps(
{
"seed": ctx.seed,
"hops": ctx.hops,
"live": sorted(live),
"kills": {n: ctx.kill_counts.get(n, 0) for n in sorted(live)},
},
sort_keys=True,
).encode("utf-8")
return hashlib.sha256(blob).hexdigest()
class OllamaAgentPolicy(SelectorPolicy):
"""Local-OSS-model rebuild policy (reproducible confirmatory agent arm)."""
name = "agent-ollama"
def __init__(
self,
model: str = DEFAULT_MODEL,
host: str = DEFAULT_HOST,
timeout: float = 30.0,
cache: Optional[Dict[str, List[str]]] = None,
) -> None:
self.model = model
self.host = host.rstrip("/")
self.timeout = timeout
self._cache: Dict[str, List[str]] = cache if cache is not None else {}
self._fallback = HeuristicAgentPolicy()
self._model_digest: Optional[str] = None
self._digest_resolved = False
self._decisions: List[Dict[str, Any]] = []
# -- provenance ------------------------------------------------------- #
def _resolve_digest(self) -> Optional[str]:
"""Best-effort: fetch the model's content digest from Ollama so the exact
weights are pinned into the run manifest. Never raises."""
if self._digest_resolved:
return self._model_digest
self._digest_resolved = True
try:
req = urllib.request.Request(f"{self.host}/api/tags")
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
tags = json.loads(resp.read().decode("utf-8"))
for m in tags.get("models", []):
if m.get("name") == self.model or m.get("model") == self.model:
self._model_digest = m.get("digest")
break
except Exception: # noqa: BLE001
self._model_digest = None
return self._model_digest
def provenance(self) -> Dict[str, Any]:
n_model = sum(1 for d in self._decisions if d.get("source") == "model")
n_cache = sum(1 for d in self._decisions if d.get("source") == "cache")
n_fallback = sum(1 for d in self._decisions if d.get("source") == "fallback")
return {
"policy": self.name,
"model": self.model,
"model_digest": self._resolve_digest(),
"host": self.host,
"options": {"temperature": 0, "seed": "per-run", "format": "json"},
"decisions": {
"total": len(self._decisions),
"model": n_model,
"cache": n_cache,
"fallback": n_fallback,
},
}
def decisions(self) -> List[Dict[str, Any]]:
"""The per-state decision log (state-hash, chosen circuit, source) — folded
into the run provenance sidecar so an agent run is fully auditable."""
return list(self._decisions)
# -- decision --------------------------------------------------------- #
def choose(self, live: List[str], ctx: SelectorContext) -> Optional[List[str]]:
if len(live) < ctx.hops:
return None
key = f"{ctx.seed}:{_state_hash(live, ctx)}"
if key in self._cache:
chosen = list(self._cache[key])
self._decisions.append({"state": key, "chosen": chosen, "source": "cache"})
return chosen
chosen = self._query_model(live, ctx)
source = "model"
if not self._valid(chosen, live, ctx.hops):
chosen = self._fallback.choose(live, ctx) # deterministic, never breaks
source = "fallback"
assert chosen is not None # pool>=hops guaranteed above
self._cache[key] = list(chosen)
self._decisions.append({"state": key, "chosen": list(chosen), "source": source})
return list(chosen)
def _valid(self, chosen: Optional[List[str]], live: List[str], hops: int) -> bool:
if not isinstance(chosen, list) or len(chosen) != hops:
return False
if len(set(chosen)) != hops:
return False
pool = set(live)
return all(isinstance(c, str) and c in pool for c in chosen)
def _query_model(self, live: List[str], ctx: SelectorContext) -> Optional[List[str]]:
"""Ask the local model for a rebuild circuit. Returns a list of node ids or
``None`` on any transport/parse failure (caller falls back)."""
prompt = self._build_prompt(live, ctx)
payload = json.dumps(
{
"model": self.model,
"prompt": prompt,
"stream": False,
"format": "json",
"options": {"temperature": 0, "seed": ctx.seed, "num_predict": 160},
}
).encode("utf-8")
try:
req = urllib.request.Request(
f"{self.host}/api/generate", data=payload,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
body = json.loads(resp.read().decode("utf-8"))
text = body.get("response", "")
parsed = json.loads(text)
except (urllib.error.URLError, TimeoutError, ValueError, json.JSONDecodeError):
return None
except Exception: # noqa: BLE001 — never let the model break a run
return None
# Accept {"circuit":[...]} or a bare [...].
if isinstance(parsed, dict):
parsed = parsed.get("circuit") or parsed.get("nodes")
if not isinstance(parsed, list):
return None
return [str(x) for x in parsed][: ctx.hops]
def _build_prompt(self, live: List[str], ctx: SelectorContext) -> str:
kills = {n: ctx.kill_counts.get(n, 0) for n in sorted(live)}
return (
"You are a path selector for a research relay measurement. Choose the "
f"{ctx.hops} most churn-stable nodes to rebuild a circuit. A node that "
"has been killed more often is less stable; prefer fewer past kills, "
"break ties by node id ascending. Return ONLY strict JSON of the form "
'{"circuit": ["<id>", ...]} with exactly '
f"{ctx.hops} distinct ids chosen from the candidate pool.\n"
f"Candidate pool (id: past_kills): {json.dumps(kills, sort_keys=True)}\n"
)
class ClaudeExploratoryPolicy(SelectorPolicy):
"""EXPLORATORY-only Claude Code / frontier hook. Inert by design: it makes NO
paid model call and cannot serve as a confirmatory arm. The paid frontier arm
is human + budget gated (GOAL autonomy envelope (c)); wiring it live is a
deliberate, separately-authorized step — not something this build performs."""
name = "agent-claude-exploratory"
def __init__(self, allow_exploratory: bool = False) -> None:
self.allow_exploratory = allow_exploratory
def choose(self, live: List[str], ctx: SelectorContext) -> Optional[List[str]]:
raise NotImplementedError(
"Claude Code agent arm is EXPLORATORY-only and is not wired for "
"confirmatory runs: no paid-model call is made in this build (GOAL "
"envelope (c), human + budget gated). Use agent_backend='ollama' for "
"the reproducible local-model arm."
)
def provenance(self) -> Dict[str, Any]:
return {
"policy": self.name,
"status": "exploratory-stub",
"paid_calls": False,
"wired": False,
}