hdcstorage / hdc /autonomy.py
AEUPH's picture
HDCSTORAGE: hyperdimensional computer, webOS and agentic MCP layer (stdlib-only Python)
ad474c9 verified
Raw History Blame Contribute Delete
17.4 kB
"""Idle, learning, readahead: the assistant's autonomous mode.
The assistant on its own is reactive — it answers what it is asked. This module
adds the three behaviours that make it *predictive* instead, and each one is a
measured quantity rather than a mood:
**Idle.** Between utterances there is time the machine would otherwise waste.
An idle tick spends it on work that is useful whichever way the conversation
goes: replaying history, computing what comes next, and pre-running the
read-only inspection commands that are most likely to be asked for.
**Learning.** Every turn leaves an intent. The sequence of intents is a trace
of behaviour, and ``(previous, next)`` pairs are learned into a
:class:`~hdc.state.PredictiveModel` — the same holographic transition table the
state layer uses. Idle *replays* that trace, and replay is not decorative: a
majority trace's cleanup confidence rises with repeated evidence, so a cycle
that was noisy after one lap sharpens on the next. Measured: see
:meth:`AutonomousMode.report`.
**Readahead.** From the last intent, predict the next ``horizon`` intents by
chaining the transition model, each hop cleaned up against the intent codebook
with its own similarity. ``PAST (trace) -> PRESENT (last intent) -> FUTURE
(readahead) -> PAST (graded on the next turn)``: every prediction is stored and
graded when the actual next utterance is classified. That grading is the
learning signal, and its rate is the number the report leads with.
**Autonomy, bounded.** Only commands on :data:`SAFE_COMMANDS` may be executed
without being asked, with a per-idle-call budget and a confidence floor. They
are read-only inspections, so the worst case of a wrong prediction is wasted
cycles, never a changed machine. When the user does ask for one of them, the
answer is already computed — which is the point: readahead trades idle time for
response time.
Honest limits: intents are a fixed set, so this predicts *which* command comes
next and not what the user will say; a first-order chain has no notion of a
conversation's subject, only of its recent shape; and the confidence numbers are
cleanup similarities, not probabilities.
"""
from __future__ import annotations
import time
from typing import Sequence
from .hypervec import DEFAULT_DIM, bundle, hvc, permute
from .state import PredictiveModel
#: Commands the autonomous mode may run unasked: read-only inspection only.
#: ``run``, ``step``, ``asm``, ``boot`` and ``clear`` are deliberately absent —
#: predicting the wrong one of those would change the machine.
SAFE_COMMANDS = frozenset({
"help", "isa", "stat", "regs", "mem", "disasm", "tools", "shader", "ring",
"state", "memory", "topology", "text", "semantic",
})
#: Confidence a prediction needs before it may execute during idle.
#:
#: Set from a measured session, not from taste. Cleanup over a 21-symbol
#: codebook has a chance level of ``1/sqrt(21) = 0.218``, and after one lap of
#: learning this model's correct next-intent predictions scored 0.18-0.39
#: (wrong ones, made cold, scored 0.00-0.06). Sweeping the floor over that
#: trace: 0.25 covers 14 of 24 turns at precision 1.00, 0.30 covers 9, and 0.45
#: covers none at all — which is why the earlier 0.45 made autonomy do nothing.
#: Executing a wrong prediction costs idle cycles and nothing else: the command
#: set below is read-only.
ACT_FLOOR = 0.25
class AutonomousMode:
"""Idle ticks, transition learning, and graded readahead over the assistant."""
def __init__(self, assistant, *, d: int | None = None, horizon: int = 3,
order: int = 1, budget: int = 4, act_floor: float = ACT_FLOOR,
enabled: bool = False):
self.assistant = assistant
self.d = d or assistant.d
self.horizon = horizon
self.order = order
self.budget = budget
self.act_floor = act_floor
self.enabled = enabled
self.intents: dict[str, int] = dict(assistant.intent_vectors)
self._names: dict[int, str] = {vector: name for name, vector in self.intents.items()}
self.model = PredictiveModel(d=self.d, alphabet=list(self.intents.values()))
self.sequence: list[str] = [] # observed intents, oldest first
self._learned = 0 # pairs already recorded
self.readahead: list[dict] = []
self.graded: int = 0
self.hits: int = 0
self.ticks: int = 0
self.replays: int = 0
self.actions: list[dict] = []
self.cache: dict[str, dict] = {} # command -> {"output", "ms"}
self.served: int = 0
self.ms_saved: float = 0.0
self.at_floor_skips: int = 0
self.log: list[dict] = []
# ------------------------------------------------------------- learning
def _key(self, window: Sequence[int]) -> int:
"""Context for an ``order``-long window of intent vectors.
Position-permuted so ``help->run`` and ``run->help`` are different keys;
with ``order=1`` it collapses to the intent itself, which is what a
deterministic command cycle needs.
"""
if len(window) == 1:
return window[0]
return bundle([permute(vector, -i, self.d) for i, vector in enumerate(reversed(window))],
self.d)
def _window(self) -> list[int]:
return [self.intents[name] for name in self.sequence[-self.order:]]
def learn_pending(self, epochs: int = 1) -> int:
"""Learn every pair not yet recorded, ``epochs`` times over.
Replay is genuine training for a majority trace: the same pair recorded
again increases its weight against crosstalk, so cleanup confidence — and
therefore prediction accuracy — improves with repetition.
"""
start = max(self._learned, self.order - 1)
learned = 0
for _ in range(max(1, epochs)):
for i in range(start, len(self.sequence) - 1):
window = [self.intents[name] for name in self.sequence[i - self.order + 1:i + 1]]
self.model.learn(self._key(window), "next", self.intents[self.sequence[i + 1]])
learned += 1
self.replays += 1
self._learned = max(self._learned, len(self.sequence) - 1)
return learned
def observe(self, intent: str, *, learn: bool = True) -> dict:
"""Record one observed intent and grade the prediction made for it.
Returns the grading: which intent was predicted, whether it was right,
and the confidence that prediction carried before the answer was known.
"""
expected = self.readahead[0] if self.readahead else None
verdict = {"predicted": (expected or {}).get("intent"), "actual": intent,
"confidence": (expected or {}).get("confidence", 0.0),
"hit": bool(expected and expected.get("intent") == intent)}
if expected is not None:
self.graded += 1
if verdict["hit"]:
self.hits += 1
self.sequence.append(intent)
if learn:
self.learn_pending()
self.readahead = self.predict()
return verdict
# ----------------------------------------------------------- prediction
def name_of(self, vector: int | None) -> str | None:
return self._names.get(vector) if vector is not None else None
def predict(self, horizon: int | None = None) -> list[dict]:
"""Chain the transition model forward from the last observed intent.
Each hop is cleaned up against the intent codebook; ``path`` is the
running confidence product, ``confidence`` the raw hop similarity, so a
three-step guess cannot look as certain as a one-step one.
"""
horizon = self.horizon if horizon is None else horizon
window = self._window()
if not window:
return []
state = self._key(window)
out: list[dict] = []
seen: set[str] = set()
path = 1.0
for depth in range(1, horizon + 1):
predicted, confidence = self.model.predict(state, "next")
name = self.name_of(predicted)
if name is None or confidence <= 0.0:
break
path *= confidence
out.append({"intent": name, "command": self._command(name),
"confidence": round(confidence, 3), "path": round(path, 3),
"depth": depth, "cached": self._command(name) in self.cache})
seen.add(name)
# chain from the *predicted* vector, not from a re-encode, so the
# whole lookahead stays in the vector space the model was built in
state = predicted
if len(seen) == len(self.intents):
break
return out
def _command(self, intent: str) -> str:
from .assistant import INTENTS
return INTENTS.get(intent, ([], ""))[1]
# ---------------------------------------------------------------- idle
def tick(self) -> dict:
"""One idle cycle: replay, predict, and pre-execute what is safe."""
self.ticks += 1
learned = self.learn_pending()
self.readahead = self.predict()
acted = []
if self.enabled:
acted = self._act()
entry = {"tick": self.ticks, "learned": learned, "acted": acted,
"readahead": [item["intent"] for item in self.readahead[:3]]}
self.log.append(entry)
return entry
def idle(self, ticks: int = 1) -> list[dict]:
return [self.tick() for _ in range(max(0, ticks))]
def _act(self, budget: int | None = None) -> list[dict]:
"""Pre-run the predicted read-only commands, within the budget."""
budget = self.budget if budget is None else budget
acted: list[dict] = []
for prediction in self.readahead:
if len(acted) >= budget:
break
command = prediction["command"]
if not command or command.split()[0] not in SAFE_COMMANDS:
continue
if command in self.cache:
continue
if prediction["confidence"] < self.act_floor:
self.at_floor_skips += 1
continue
started = time.perf_counter()
output = self.assistant.kernel.execute(command)
elapsed = (time.perf_counter() - started) * 1000.0
self.cache[command] = {"output": output, "ms": elapsed,
"confidence": prediction["confidence"]}
record = {"command": command, "intent": prediction["intent"],
"confidence": prediction["confidence"], "ms": round(elapsed, 2)}
acted.append(record)
self.actions.append(record)
return acted
# ----------------------------------------------------------------- chat
def chat(self, text: str) -> dict:
"""``Assistant.chat`` with the readahead cache in front of execution."""
intent, confidence = self.assistant.classify(text)
command = self._command(intent) if intent else ""
served = command and command in self.cache and confidence >= self.assistant.threshold
if served:
cached = self.cache[command]
self.served += 1
self.ms_saved += cached["ms"]
record = self.assistant.chat(text, execute=self._executor if served else None)
if served:
record["output"] = self.cache[command]["output"]
record["cached"] = True
record["saved_ms"] = round(self.cache[command]["ms"], 2)
verdict = self.observe(intent) if intent else None
record["graded"] = verdict
if self.enabled:
self.tick()
return record
def _executor(self, command: str) -> str:
cached = self.cache.get(command)
if cached is not None:
return cached["output"]
return self.assistant.kernel.execute(command)
# --------------------------------------------------------------- report
def chance(self) -> float:
"""Cleanup similarity a random symbol scores against the intent codebook."""
return 1.0 / max(1.0, len(self.intents) ** 0.5)
def stats(self) -> dict:
accuracy = (self.hits / self.graded) if self.graded else 0.0
return {
"enabled": self.enabled, "ticks": self.ticks, "replays": self.replays,
"observed": len(self.sequence), "transitions": self.model.transitions,
"graded": self.graded, "hits": self.hits, "accuracy": round(accuracy, 3),
"chance": round(self.chance(), 3),
"readahead": self.readahead, "cached_commands": sorted(self.cache),
"autonomous_actions": len(self.actions), "served_from_readahead": self.served,
"ms_saved": round(self.ms_saved, 1), "act_floor": self.act_floor,
"budget": self.budget,
}
def report(self) -> str:
stats = self.stats()
lines = ["autonomy: %s, horizon %d, order %d, %.2f confidence floor"
% ("enabled" if self.enabled else "disabled",
self.horizon, self.order, self.act_floor)]
lines.append(" idle ticks %-3d replays %-3d transitions %-3d observed %d"
% (stats["ticks"], stats["replays"], stats["transitions"],
stats["observed"]))
lines.append(" readahead graded %d, correct %d -> %.1f%% next-intent accuracy "
"(chance %.2f)"
% (stats["graded"], stats["hits"], 100.0 * stats["accuracy"],
stats["chance"]))
if self.readahead:
lines.append(" current lookahead: " + " -> ".join(
"%s(%.2f)" % (item["intent"], item["confidence"]) for item in self.readahead))
else:
lines.append(" current lookahead: none (observation needed first)")
lines.append(" autonomous actions %d (%d skipped under the floor), cached %d, "
"served %d, %.1f ms saved"
% (stats["autonomous_actions"], self.at_floor_skips,
len(self.cache), stats["served_from_readahead"], stats["ms_saved"]))
return "\n".join(lines)
#: A session with a shape: the same four-command cycle three times, then a break.
SESSION = (
"show the statistics", "show the registers", "show the instruction set",
"what can you do",
"show the statistics", "show the registers", "show the instruction set",
"what can you do",
"show the statistics", "show the registers", "show the instruction set",
"what can you do",
)
def simulate(session: Sequence[str] = SESSION, *, d: int = 2000, encoder=None,
enabled: bool = True, warmup: int = 1) -> dict:
"""Run a scripted session through idle + chat and report what it learned.
The point of the simulation is that the numbers in :meth:`AutonomousMode.report`
are measured on a conversation, not asserted. ``warmup`` idle ticks run
before the first utterance, so the machine starts in a cold state and has to
earn its accuracy.
"""
from .assistant import Assistant
from .terminal import Kernel
kernel = Kernel(d=d)
assistant = Assistant(kernel, d=d, encoder=encoder)
mode = AutonomousMode(assistant, d=d, enabled=enabled)
mode.idle(warmup)
per_turn: list[dict] = []
for text in session:
record = mode.chat(text)
per_turn.append({"input": text, "intent": record.get("intent"),
"graded": record.get("graded"),
"cached": record.get("cached", False),
"saved_ms": record.get("saved_ms", 0.0)})
half = len(per_turn) // 2
early = [t for t in per_turn[:half] if t["graded"]]
late = [t for t in per_turn[half:] if t["graded"]]
return {
"mode": mode, "turns": per_turn,
"early_accuracy": _rate(early), "late_accuracy": _rate(late),
"stats": mode.stats(), "report": mode.report(),
}
def _rate(turns: Sequence[dict]) -> float:
if not turns:
return 0.0
return sum(1 for turn in turns if turn["graded"]["hit"]) / len(turns)
def report(d: int = DEFAULT_DIM) -> str:
"""The printable form: a simulated session, its accuracy and its savings."""
from .semantic import load
encoder = load(d=d)
result = simulate(d=d, encoder=encoder)
lines = ["autonomy report (simulated session, %d utterances, encoder=%s)"
% (len(SESSION), "semantic" if encoder else "n-gram")]
lines.append(result["report"])
lines.append(" next-intent accuracy: first half %.0f%% -> second half %.0f%%"
% (100.0 * result["early_accuracy"], 100.0 * result["late_accuracy"]))
for turn in result["turns"][-4:]:
graded = turn["graded"] or {}
lines.append(" %-26s -> %-10s predicted %-10s %s%s"
% (turn["input"][:26], turn["intent"],
graded.get("predicted") or "-",
"hit" if graded.get("hit") else "miss",
" [readahead]" if turn["cached"] else ""))
return "\n".join(lines)
if __name__ == "__main__": # pragma: no cover - manual inspection
print(report())