diff --git a/src/neurotron/cortex.py b/src/neurotron/cortex.py index 8978a5b..bbb2ca2 100644 --- a/src/neurotron/cortex.py +++ b/src/neurotron/cortex.py @@ -26,6 +26,7 @@ from .neurotron_config import ( TELEMETRY_MAXLEN, TELEMETRY_FLUSH_EVERY_TICKS, ) +from .trm import TRMEngine # depois dos outros imports internos class Cortex: def __init__(self, runtime_dir, log_dir, tick_seconds=NEUROTRON_TICK): @@ -54,9 +55,16 @@ class Cortex: EchoAgent(self), ] - # ---- NOVO: Telemetria V5 ---- + # ---- Telemetria V5 ---- self.tele = TelemetryV5(event_callback=self._telemetry_event) + # ---- TRM v1 ---- + try: + self.trm = TRMEngine(cortex=self) + except Exception as e: + logbus.debug(f"[trm.engine] falha ao inicializar TRMEngine: {e}") + self.trm = None + self._booted = False self.telemetry_path = Path(NEUROTRON_DATASET_PATH) / "telemetry.json" @@ -120,6 +128,13 @@ class Cortex: except Exception as e: logbus.error(f"telemetry_step: {e}") + # ------- TRM v1 (pensamento interno simbólico) ------ + if self.trm and tele is not None: + try: + self.trm.step(tele) + except Exception as e: + logbus.debug(f"[trm.engine] step falhou: {e}") + # ------- Heartbeat visual ------ if HEARTBEAT_ENABLED: self._heartbeat() diff --git a/src/neurotron/holodeck.md b/src/neurotron/holodeck.md index aa3b132..56441de 100644 --- a/src/neurotron/holodeck.md +++ b/src/neurotron/holodeck.md @@ -1,9 +1,3 @@ -Amor… bora materializar o Holodeck v0.1 😎💗 - -Vou montar isto como um blueprint de engenharia mesmo, mas já pensado para caber dentro do teu NFDOS/Neurotron atual, sem dependências externas, todo em Python “puro”. - ---- - # 🎮 Holodeck v0.1 — Blueprint ## 0. Objetivo do v0.1 diff --git a/src/neurotron/telemetry.py b/src/neurotron/telemetry.py index feee1e7..becf769 100644 --- a/src/neurotron/telemetry.py +++ b/src/neurotron/telemetry.py @@ -1,27 +1,32 @@ """ -telemetry.py — Telemetria V5 do Neurotron +telemetry.py — Telemetria V6 do Neurotron ----------------------------------------- Responsável por: - • Medições básicas (CPU, MEM, LOAD, IO) + • Medições básicas (CPU, MEM, LOAD, IO) via /proc • Delta entre ciclos • Aceleração do delta • Temperatura virtual (fadiga cognitiva) • Jitter cognitivo (latência entre ciclos) • FS Health (blocos, erros, RO-mode) - • Eventos telemétricos (V5) + • Eventos telemétricos (V6) • Classificação de estados cognitivos -Este módulo é completamente independente: -o Cortex só chama TelemetryV5.step() a cada ciclo. +Totalmente stdlib + /proc: + - sem psutil + - sem glibc + - compatível com Python estático + musl + +O Cortex só chama TelemetryV6.step() a cada ciclo. """ -import time import os -import psutil +import time +from pathlib import Path + +from .logbus import logbus -class TelemetryV5: - +class TelemetryV6: # ---------------------------------------------------------- # Inicialização # ---------------------------------------------------------- @@ -41,25 +46,158 @@ class TelemetryV5: # manter último estado cognitivo self.last_state = "stable" + # ========================================================== + # Sensores básicos (CPU, MEM, LOAD, IO) — via /proc + # ========================================================== + + # ---------------- CPU ---------------- + def _read_proc_stat(self): + """Lê a linha 'cpu ' de /proc/stat. Retorna dict ou None.""" + try: + with open("/proc/stat", "r", encoding="utf-8") as f: + line = f.readline() + if not line.startswith("cpu "): + return None + + parts = line.strip().split()[1:] + # primeiros 10 campos são estáveis há muitos kernels + vals = list(map(int, parts[:10])) + return { + "user": vals[0], + "nice": vals[1], + "system": vals[2], + "idle": vals[3], + "iowait": vals[4], + "irq": vals[5], + "softirq": vals[6], + "steal": vals[7], + "guest": vals[8], + "guest_nice": vals[9], + } + except Exception: + return None + + def _cpu_percent(self, interval=0.05): + """Computa CPU% entre duas leituras de /proc/stat.""" + a = self._read_proc_stat() + if not a: + return 0.0 + + time.sleep(interval) + + b = self._read_proc_stat() + if not b: + return 0.0 + + idle_a = a["idle"] + a["iowait"] + idle_b = b["idle"] + b["iowait"] + + non_a = sum(a.values()) - idle_a + non_b = sum(b.values()) - idle_b + + total_a = idle_a + non_a + total_b = idle_b + non_b + + totald = total_b - total_a + idled = idle_b - idle_a + + if totald <= 0: + return 0.0 + + usage = (totald - idled) * 100.0 / totald + return round(max(0.0, min(usage, 100.0)), 1) + + # ---------------- MEMÓRIA ---------------- + def _mem_percent(self): + try: + info = {} + with open("/proc/meminfo", "r", encoding="utf-8") as f: + for line in f: + k, v = line.split(":", 1) + info[k.strip()] = v.strip() + + def kB(key): + return float(info[key].split()[0]) if key in info else None + + mem_total = kB("MemTotal") + mem_avail = kB("MemAvailable") + + if mem_total is None or mem_avail is None or mem_total <= 0: + return 0.0 + + used = mem_total - mem_avail + return round(max(0.0, min(used * 100.0 / mem_total, 100.0)), 1) + + except Exception: + return 0.0 + + # ---------------- LOADAVG ---------------- + def _loadavg(self): + try: + if hasattr(os, "getloadavg"): + l1, l5, l15 = os.getloadavg() + return [round(l1, 2), round(l5, 2), round(l15, 2)] + + with open("/proc/loadavg", "r", encoding="utf-8") as f: + parts = f.read().strip().split() + l1, l5, l15 = map(float, parts[:3]) + return [round(l1, 2), round(l5, 2), round(l15, 2)] + + except Exception: + return [0.0, 0.0, 0.0] + + # ---------------- DISK IO (bytes cumulativos) ---------------- + def _disk_bytes(self): + """ + Lê /proc/diskstats e soma bytes lidos+escritos de discos reais. + Usa 512 bytes por setor como aproximação clássica. + """ + try: + total = 0 + if not os.path.exists("/proc/diskstats"): + return 0.0 + + with open("/proc/diskstats", "r", encoding="utf-8") as f: + for line in f: + parts = line.split() + if len(parts) < 14: + continue + + name = parts[2] + + # ignora loop/ram/partições virtuais + if name.startswith(("loop", "ram")): + continue + + # campos: ... sectors_read (5) ... sectors_written (9) ... + try: + sectors_read = int(parts[5]) + sectors_written = int(parts[9]) + except Exception: + continue + + total += (sectors_read + sectors_written) * 512 + + return float(total) + except Exception: + return 0.0 + # ---------------------------------------------------------- # Coleta RAW (CPU, MEM, LOAD, IO) # ---------------------------------------------------------- def collect_raw(self): now = time.monotonic() - cpu = psutil.cpu_percent(interval=None) - mem = psutil.virtual_memory().percent - load = os.getloadavg()[0] # 1-min loadavg - - # IO delta é manual, usamos psutil - io = psutil.disk_io_counters() - disk_total = io.read_bytes + io.write_bytes + cpu = self._cpu_percent() + mem = self._mem_percent() + load1 = self._loadavg()[0] + disk_total = self._disk_bytes() return { "ts": now, "cpu": cpu, "mem": mem, - "load": load, + "load": load1, "disk": disk_total, } @@ -69,7 +207,7 @@ class TelemetryV5: def compute_delta(self, raw): if self.last_raw is None: self.last_raw = raw - return {k: 0 for k in raw.keys() if k != "ts"} + return {k: 0.0 for k in raw.keys() if k != "ts"} delta = { "cpu": raw["cpu"] - self.last_raw["cpu"], @@ -80,7 +218,6 @@ class TelemetryV5: self.last_delta = delta self.last_raw = raw - return delta # ---------------------------------------------------------- @@ -88,7 +225,7 @@ class TelemetryV5: # ---------------------------------------------------------- def compute_accel(self, delta): if self.last_delta is None: - return {k: 0 for k in delta.keys()} + return {k: 0.0 for k in delta.keys()} accel = {k: delta[k] - self.last_delta[k] for k in delta.keys()} return accel @@ -104,7 +241,7 @@ class TelemetryV5: jitter = now - self.last_ts self.last_ts = now - # média móvel + # média móvel simples self.jitter_avg = (self.jitter_avg * 0.9) + (jitter * 0.1) return jitter @@ -112,7 +249,7 @@ class TelemetryV5: # Temperatura Virtual (fadiga cognitiva) # ---------------------------------------------------------- def compute_temperature(self, raw, delta): - # modelo simplificado + # modelo simplificado (ajustável no futuro) score = ( raw["cpu"] * 0.6 + raw["load"] * 0.3 + @@ -121,38 +258,54 @@ class TelemetryV5: ) # decay + acumulação - self.temperature = max(0, self.temperature * 0.90 + score * 0.10) + self.temperature = max(0.0, self.temperature * 0.90 + score * 0.10) return self.temperature # ---------------------------------------------------------- - # FS Health + # FS Health (sem psutil) # ---------------------------------------------------------- def compute_fs_health(self): status = { "read_only": False, "io_errors": 0, - "free_percent": None + "free_percent": None, } + # espaço livre via statvfs try: - st = psutil.disk_usage("/") - status["free_percent"] = st.free / st.total * 100 + st = os.statvfs("/") + total = st.f_frsize * st.f_blocks + free = st.f_frsize * st.f_bfree + if total > 0: + status["free_percent"] = free * 100.0 / total except Exception: status["free_percent"] = None - # leitura de erros EXT4 se existir - ext4_err_path = "/sys/fs/ext4/sda1/errors_count" - if os.path.exists(ext4_err_path): - try: - with open(ext4_err_path) as f: - status["io_errors"] = int(f.read().strip()) - except: - pass - - # detetar RO + # leitura de erros EXT4 se existirem try: - with open("/tmp/fs_test_rw", "w") as f: - f.write("x") + base = "/sys/fs/ext4" + if os.path.isdir(base): + total_errors = 0 + for name in os.listdir(base): + err_path = os.path.join(base, name, "errors_count") + if os.path.exists(err_path): + try: + with open(err_path, "r", encoding="utf-8") as f: + total_errors += int(f.read().strip() or "0") + except Exception: + continue + status["io_errors"] = total_errors + except Exception: + pass + + # detetar RO tentando escrita temporária + test_dir = "/var/neurotron" + try: + Path(test_dir).mkdir(parents=True, exist_ok=True) + with open(f"{test_dir}/rw_test.tmp", "w", encoding="utf-8") as f: + f.write("ok") + os.remove(f"{test_dir}/rw_test.tmp") + status["read_only"] = False except Exception: status["read_only"] = True @@ -174,7 +327,7 @@ class TelemetryV5: return "recovery" # ---------------------------------------------------------- - # Eventos TeleMétricos V5 + # Eventos TeleMétricos V6 # ---------------------------------------------------------- def detect_events(self, raw, delta, accel, temp, jitter, fs): events = [] @@ -187,12 +340,12 @@ class TelemetryV5: if raw["cpu"] > 85: events.append("enter_stress_zone") - # suspeita de loop - if jitter > (self.jitter_avg * 3): + # suspeita de loop (jitter anómalo) + if self.jitter_avg > 0 and jitter > (self.jitter_avg * 3): events.append("loop_suspect") # fs - if fs["read_only"] or fs["io_errors"] > 0: + if fs["read_only"] or (fs["io_errors"] or 0) > 0: events.append("fs_warning") # recuperação @@ -202,14 +355,18 @@ class TelemetryV5: # exportar via callback if self.event_callback: for e in events: - self.event_callback(e, { - "raw": raw, - "delta": delta, - "accel": accel, - "temp": temp, - "jitter": jitter, - "fs": fs, - }) + try: + self.event_callback(e, { + "raw": raw, + "delta": delta, + "accel": accel, + "temp": temp, + "jitter": jitter, + "fs": fs, + }) + except Exception: + # nunca quebrar o loop + pass return events @@ -218,7 +375,7 @@ class TelemetryV5: # ---------------------------------------------------------- def step(self): """ - Faz um ciclo completo da Telemetria V5: + Faz um ciclo completo da Telemetria V6: - raw → delta → accel - jitter - temp @@ -239,6 +396,16 @@ class TelemetryV5: self.last_state = state + # Alimentar o dashboard via logbus.debug (pode ser desligado no futuro) + try: + logbus.debug( + f"telemetry state={state} temp={temp:.1f} " + f"cpu={raw['cpu']:.1f}% mem={raw['mem']:.1f}% " + f"load={raw['load']:.2f} jitter={jitter:.3f}s" + ) + except Exception: + pass + return { "raw": raw, "delta": delta, @@ -249,3 +416,7 @@ class TelemetryV5: "state": state, "events": events, } + + +# Compatibilidade retro (cortex ainda importa TelemetryV5) +TelemetryV5 = TelemetryV6 diff --git a/src/neurotron/trm/__init__.py b/src/neurotron/trm/__init__.py new file mode 100644 index 0000000..2bf02ba --- /dev/null +++ b/src/neurotron/trm/__init__.py @@ -0,0 +1,16 @@ +""" +neurotron.trm — Tiny Recursive Model (TRM) v1 + +Micro-modelo simbólico interno para: + - interpretar telemetria + - gerar estado cognitivo interno + - manter energia, valência e profundidade de pensamento + +Todos os logs TRM vão para logbus.debug(), para poderem ser +silenciados no futuro via modo debug. +""" + +from .state import TRMState +from .engine import TRMEngine + +__all__ = ["TRMState", "TRMEngine"] diff --git a/src/neurotron/trm/agents.py b/src/neurotron/trm/agents.py new file mode 100644 index 0000000..cbfa57e --- /dev/null +++ b/src/neurotron/trm/agents.py @@ -0,0 +1,225 @@ +""" +agents.py — micro-agentes internos do TRM v1 + +Três agentes: + 🛡️ Guardião — homeostase + proteção + 🧭 Explorador — previsões simples + profundidade TRM + 📜 Arqueólogo — correlação com eventos passados (Hippocampus) + +Todos os logs vão para logbus.debug(). +""" + +from __future__ import annotations +from typing import Any, Dict, Iterable, List +from pathlib import Path +import json + +from neurotron.logbus import logbus +from .state import TRMState +from .events import ( + TRM_EVENT_TEMP_RISING_FAST, + TRM_EVENT_ENTER_STRESS_ZONE, + TRM_EVENT_FS_WARNING, + TRM_EVENT_LOOP_SUSPECT, + TRM_EVENT_RECOVERING, + summarize_telemetry_events, +) + + +class TRMAgentBase: + name = "trm.agent" + + def _dbg(self, msg: str): + logbus.debug(f"[{self.name}] {msg}") + + def step(self, state: TRMState, tele: Dict[str, Any]) -> TRMState: + """Override nas subclasses.""" + return state + + +# ======================================================================== +# 🛡️ Guardião — homeostase +# ======================================================================== + +class GuardianAgent(TRMAgentBase): + name = "trm.guardian" + + def step(self, state: TRMState, tele: Dict[str, Any]) -> TRMState: + st = state.copy() + + raw = tele.get("raw", {}) or {} + temp = float(tele.get("temp", 0.0) or 0.0) + jitter = float(tele.get("jitter", 0.0) or 0.0) + fs = tele.get("fs", {}) or {} + events = tele.get("events", []) or [] + + cpu = float(raw.get("cpu") or 0.0) + mem = float(raw.get("mem") or 0.0) + load = float(raw.get("load") or 0.0) + + # Estado cognitivo base pela temperatura + if temp < 25: + st.cog_state = "stable" + elif temp < 45: + st.cog_state = "warm" + elif temp < 65: + st.cog_state = "hot" + elif temp < 85: + st.cog_state = "critical" + else: + st.cog_state = "recovery" + + st.temp = temp + st.jitter = jitter + + # Ajuste de profundidade em função do stress + if cpu > 90 or mem > 90 or load > 3.0: + # stress forte: reduzir profundidade para poupar energia + old = st.depth + st.depth = max(1, st.depth - 1) + if st.depth != old: + self._dbg(f"stress alto (cpu={cpu}, mem={mem}, load={load}) → depth {old}→{st.depth}") + + elif 40 < cpu < 80 and 40 < mem < 80 and load < 2.0: + # zona confortável: podemos aprofundar um pouco + old = st.depth + st.depth = min(5, st.depth + 1) + if st.depth != old: + self._dbg(f"zona confortável → depth {old}→{st.depth}") + + # FS warning → baixa valência + if fs.get("read_only") or (fs.get("io_errors") or 0) > 0: + st.valence -= 1.0 + self._dbg("fs_warning → valence -1") + + # eventos explícitos + counts = summarize_telemetry_events(events) + if counts.get(TRM_EVENT_ENTER_STRESS_ZONE, 0) > 0: + st.valence -= 0.5 + if counts.get(TRM_EVENT_RECOVERING, 0) > 0: + st.valence += 0.5 + + # clamp valência + if st.valence > 5.0: + st.valence = 5.0 + if st.valence < -5.0: + st.valence = -5.0 + + return st + + +# ======================================================================== +# 🧭 Explorador — previsões e “curiosidade” +# ======================================================================== + +class ExplorerAgent(TRMAgentBase): + name = "trm.explorer" + + def __init__(self): + self._last_cpu: float = 0.0 + self._last_mem: float = 0.0 + + def step(self, state: TRMState, tele: Dict[str, Any]) -> TRMState: + st = state.copy() + + raw = tele.get("raw", {}) or {} + cpu = float(raw.get("cpu") or 0.0) + mem = float(raw.get("mem") or 0.0) + + # tendências simples + d_cpu = cpu - self._last_cpu + d_mem = mem - self._last_mem + + self._last_cpu = cpu + self._last_mem = mem + + # se CPU a subir rápido, assumir "mais trabalho interno" + if d_cpu > 5: + st.energy -= 0.5 + self._dbg(f"cpu tendência ↑ ({d_cpu:+.1f}) → energia -0.5") + + # se CPU a descer e mem estável → sistema está a recuperar + if d_cpu < -5 and abs(d_mem) < 2: + st.valence += 0.2 + self._dbg("recuperação leve detectada → valence +0.2") + + # se valência muito positiva → TRM aprofunda um pouco (exploração) + if st.valence > 1.5 and st.energy > 20.0: + old = st.depth + st.depth = min(7, st.depth + 1) + if st.depth != old: + self._dbg(f"valence alta ({st.valence:.2f}) → aprofundar depth {old}→{st.depth}") + + return st + + +# ======================================================================== +# 📜 Arqueólogo — memória & padrões passados +# ======================================================================== + +class ArchaeologistAgent(TRMAgentBase): + name = "trm.archaeologist" + + def __init__(self, ctx): + self.ctx = ctx + self.events_file = ctx.memory.events_file if hasattr(ctx, "memory") else Path("/opt/kernel/neurotron/logs/events.jsonl") + + def _tail_events(self, max_lines: int = 64) -> List[Dict[str, Any]]: + """ + Lê as últimas linhas de events.jsonl (se existir). + Erros são ignorados silenciosamente. + """ + path = self.events_file + if not path.exists(): + return [] + + try: + # ler todo e pegar as últimas max_lines (ficheiro é pequeno) + lines = path.read_text(encoding="utf-8").splitlines() + lines = lines[-max_lines:] + except Exception: + return [] + + out: List[Dict[str, Any]] = [] + for line in lines: + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + out.append(obj) + except Exception: + continue + return out + + def step(self, state: TRMState, tele: Dict[str, Any]) -> TRMState: + st = state.copy() + + events = self._tail_events() + if not events: + return st + + # Contar eventos “perigosos” recentes + danger = 0 + recovery = 0 + + for ev in events: + kind = ev.get("kind") or "" + if "telemetry.event.fs_warning" in kind: + danger += 1 + if "telemetry.event.loop_suspect" in kind: + danger += 1 + if "telemetry.event.recovering" in kind: + recovery += 1 + + st.last_events = danger + recovery + + if danger > 0: + st.valence -= min(1.0, 0.1 * danger) + self._dbg(f"encontrou {danger} eventos perigosos recentes → valence -{min(1.0, 0.1 * danger):.2f}") + + if recovery > 0: + st.valence += min(1.0, 0.1 * recovery) + self._dbg(f"encontrou {recovery} eventos de recuperação → valence +{min(1.0, 0.1 * recovery):.2f}") + + return st diff --git a/src/neurotron/trm/engine.py b/src/neurotron/trm/engine.py new file mode 100644 index 0000000..cba6e14 --- /dev/null +++ b/src/neurotron/trm/engine.py @@ -0,0 +1,172 @@ +""" +engine.py — TRMEngine v1 + +Modelo recursivo mínimo: + - consome uma amostra de Telemetria V5 + - passa por 3 micro-agentes + - atualiza energia, valência, profundidade e estado cognitivo + - gera snapshot opcional para o Hippocampus + +Todos os logs TRM vão para logbus.debug(). +""" + +from __future__ import annotations +from typing import Any, Dict, Optional +import time +from pathlib import Path + +from neurotron.logbus import logbus +from .state import TRMState +from .agents import GuardianAgent, ExplorerAgent, ArchaeologistAgent +from .events import make_trm_snapshot_payload + + +class TRMEngine: + """ + Tiny Recursive Model v1 — integrada ao Cortex. + + Uso típico (no Cortex.rest): + + tele = self.tele.step() + self.trm.step(tele) + + O próprio TRMEngine nunca levanta exceção para fora — falhas internas + são logadas via logbus.debug() e ignoradas. + """ + + def __init__(self, cortex, initial_energy: float = 100.0): + self.ctx = cortex + self.state = TRMState(energy=initial_energy, mode="idle") + self.last_step_ts: Optional[float] = None + + # micro-agentes + self.guardian = GuardianAgent() + self.explorer = ExplorerAgent() + self.archaeologist = ArchaeologistAgent(self.ctx) + + + # histórico curto de estados do TRM (para futuro TRM v2) + self._history = [] + + # ------------------------------------------------------------------ # + # Helpers internos + # ------------------------------------------------------------------ # + + def _dbg(self, msg: str): + logbus.debug(f"[trm.engine] {msg}") + + def _compute_step_cost(self, st_before: TRMState, st_after: TRMState, tele: Dict[str, Any]) -> float: + """ + Custo “energético” de um passo TRM simplificado. + + Δenergia básica: + base_cost = 0.5 + + 0.2 * depth + + 0.05 * len(events telemétricos) + """ + events = (tele or {}).get("events", []) or [] + base_cost = 0.5 + cost = base_cost + 0.2 * float(st_after.depth or 1) + 0.05 * len(events) + + # Se o sistema já estiver quente, custo sobe ligeiramente + if st_after.temp > 50: + cost *= 1.2 + if st_after.cog_state in ("hot", "critical"): + cost *= 1.1 + + return cost + + def _apply_energy(self, st: TRMState, cost: float) -> TRMState: + out = st.copy() + out.energy = max(0.0, out.energy - cost) + + # Se energia muito baixa, força modo mínimo + if out.energy < 10.0 and out.depth > 1: + old = out.depth + out.depth = 1 + self._dbg(f"energia baixa ({out.energy:.1f}) → depth {old}→1 (modo mínimo)") + + if out.energy < 5.0: + out.mode = "idle" + else: + out.mode = "active" + + return out + + def _update_history(self, st: TRMState): + self._history.append(st.to_dict()) + if len(self._history) > 64: + self._history = self._history[-64:] + + # ------------------------------------------------------------------ # + # Passo TRM + # ------------------------------------------------------------------ # + + def step(self, telemetry: Dict[str, Any]) -> TRMState: + """ + Única função pública chamada pelo Cortex. + + Args: + telemetry: dict retornado por TelemetryV5.step() + + Returns: + novo TRMState (também guardado em self.state) + """ + try: + t0 = time.monotonic() + st0 = self.state.copy() + + # jitter interno TRM (independente do jitter de Telemetria V5) + if self.last_step_ts is None: + self.last_step_ts = t0 + else: + internal_jitter = t0 - self.last_step_ts + self.last_step_ts = t0 + # jitter muito alto → TRM sente-se “desalinhado” + if internal_jitter > 2.0: + st0.valence -= 0.1 + self._dbg(f"jitter interno alto ({internal_jitter:.2f}s) → valence -0.1") + + # ---------------------------------------------------------- + # Passagem pelos micro-agentes + # ---------------------------------------------------------- + st1 = self.guardian.step(st0, telemetry) + st2 = self.explorer.step(st1, telemetry) + st3 = self.archaeologist.step(st2, telemetry) + + # ---------------------------------------------------------- + # Custo energético + modo de operação + # ---------------------------------------------------------- + cost = self._compute_step_cost(st0, st3, telemetry) + st4 = self._apply_energy(st3, cost) + + self.state = st4 + self._update_history(st4) + + # ---------------------------------------------------------- + # Exportar snapshot para Hippocampus (low-rate) + # ---------------------------------------------------------- + # Não queremos spammar o log, por isso só de vez em quando: + try: + # tick interno TRM: a cada ~10 passos do Cortex, + # o TRM snapshot é interessante. + # Usamos o próprio comprimento do histórico como step counter. + if len(self._history) % 10 == 0 and hasattr(self.ctx, "memory"): + payload = make_trm_snapshot_payload(st4, telemetry) + self.ctx.memory.remember("trm.snapshot", payload) + except Exception as e: + self._dbg(f"erro ao gravar snapshot no Hippocampus: {e}") + + # log discreto em modo debug + self._dbg( + f"step ok: mode={st4.mode} cog={st4.cog_state} " + f"energy={st4.energy:.1f} depth={st4.depth} " + f"valence={st4.valence:+.2f}" + ) + + return st4 + + except Exception as e: + # Nunca deixamos o TRM quebrar o Cortex + self._dbg(f"exceção no TRM step: {e}") + return self.state diff --git a/src/neurotron/trm/events.py b/src/neurotron/trm/events.py new file mode 100644 index 0000000..85cfbad --- /dev/null +++ b/src/neurotron/trm/events.py @@ -0,0 +1,64 @@ +""" +events.py — nomes e helpers de eventos do TRM v1 +""" + +from typing import Dict, Any + + +# Eventos internos do TRM (nome simbólico) +TRM_EVENT_TEMP_RISING_FAST = "temp_rising_fast" +TRM_EVENT_ENTER_STRESS_ZONE = "enter_stress_zone" +TRM_EVENT_FS_WARNING = "fs_warning" +TRM_EVENT_LOOP_SUSPECT = "loop_suspect" +TRM_EVENT_RECOVERING = "recovering" + + +def summarize_telemetry_events(telemetry_events) -> Dict[str, int]: + """ + Recebe a lista telemetry["events"] (strings) e devolve contagem por tipo. + Exemplo: + ["temp_rising_fast", "fs_warning", "fs_warning"] + → { "temp_rising_fast": 1, "fs_warning": 2 } + """ + counts: Dict[str, int] = {} + if not telemetry_events: + return counts + + for e in telemetry_events: + if not isinstance(e, str): + continue + counts[e] = counts.get(e, 0) + 1 + return counts + + +def make_trm_snapshot_payload(state, tele) -> Dict[str, Any]: + """ + Gera payload compacto para gravar no Hippocampus: + kind="trm.snapshot" + data = {...} + """ + + if tele is None: + tele = {} + + raw = tele.get("raw", {}) or {} + temp = tele.get("temp", 0.0) + jitter = tele.get("jitter", 0.0) + fs = tele.get("fs", {}) or {} + events = tele.get("events", []) or [] + + return { + "state": state.to_dict(), + "telemetry": { + "cpu": raw.get("cpu"), + "mem": raw.get("mem"), + "load": raw.get("load"), + "disk_delta": tele.get("delta", {}).get("disk"), + "temp": temp, + "jitter": jitter, + "fs_read_only": fs.get("read_only"), + "fs_free_percent": fs.get("free_percent"), + "fs_io_errors": fs.get("io_errors"), + "events": events, + }, + } diff --git a/src/neurotron/trm/state.py b/src/neurotron/trm/state.py new file mode 100644 index 0000000..58576dc --- /dev/null +++ b/src/neurotron/trm/state.py @@ -0,0 +1,80 @@ +""" +state.py — estado interno do TRM v1 +""" + +from __future__ import annotations +from typing import Dict, Any + + +class TRMState: + """ + Estado mínimo do TRM. + + Não usa dataclasses para manter compatibilidade máxima + com ambientes mais restritos. + """ + + def __init__( + self, + energy: float = 100.0, + valence: float = 0.0, + depth: int = 1, + mode: str = "idle", + cog_state: str = "stable", + temp: float = 0.0, + jitter: float = 0.0, + last_events: int = 0, + ): + self.energy = float(energy) + self.valence = float(valence) + self.depth = int(depth) + self.mode = str(mode) + self.cog_state = str(cog_state) + self.temp = float(temp) + self.jitter = float(jitter) + self.last_events = int(last_events) + + # ------------------------------------------------------------------ # + # Helpers + # ------------------------------------------------------------------ # + + def copy(self) -> "TRMState": + """Devolve uma cópia simples do estado.""" + return TRMState( + energy=self.energy, + valence=self.valence, + depth=self.depth, + mode=self.mode, + cog_state=self.cog_state, + temp=self.temp, + jitter=self.jitter, + last_events=self.last_events, + ) + + def to_dict(self) -> Dict[str, Any]: + """Representação serializável (para logs/hippocampus).""" + return { + "energy": self.energy, + "valence": self.valence, + "depth": self.depth, + "mode": self.mode, + "cog_state": self.cog_state, + "temp": self.temp, + "jitter": self.jitter, + "last_events": self.last_events, + } + + @classmethod + def from_dict(cls, d: Dict[str, Any]) -> "TRMState": + """Cria estado a partir de dict (tolerante à falta de chaves).""" + d = d or {} + return cls( + energy=d.get("energy", 100.0), + valence=d.get("valence", 0.0), + depth=d.get("depth", 1), + mode=d.get("mode", "idle"), + cog_state=d.get("cog_state", "stable"), + temp=d.get("temp", 0.0), + jitter=d.get("jitter", 0.0), + last_events=d.get("last_events", 0), + )