Spaces:
Sleeping
Sleeping
File size: 6,868 Bytes
31226fd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | """Experience Engine for controlled, single-consumption learning loops."""
from __future__ import annotations
import math
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field
from typing import Any
from src.embodiment.controlled import ControlledEmbodimentAgent
from src.embodiment.loop_contract import CycleContract, valid_tick
from src.embodiment.models import ActionCommand, EnvironmentObservation, SensorFrame
from src.embodiment.sensor import SensorAdapter
from src.embodiment.task_outcome import TaskOutcome, TaskOutcomeVerifier
from src.learning.learning_engine import LearningEngine
from src.memory import MemoryWorldModel
from src.memory.neural_episodic import NeuralEpisodicMemory
from src.profiles import BehaviorProfile
Encoder = Callable[[SensorFrame], Mapping[int, float]]
Decoder = Callable[[Any, SensorFrame], ActionCommand | tuple[ActionCommand, ...] | None]
@dataclass(frozen=True, slots=True)
class ExperienceStep:
"""Immutable audit record for one perception-action-feedback cycle."""
tick: int
frame: SensorFrame
action: ActionCommand | None
observation: EnvironmentObservation | None
reward: float
outcome: TaskOutcome | None = None
@dataclass(slots=True)
class ExperienceEngine:
"""Connect a controlled sensor loop to the real learning engine.
Rewards are accepted only from environment observations. No language
model, configuration value, or decoder output can write a reward.
A consumed cycle cannot be retried after a downstream observer fails.
"""
sensor: SensorAdapter
network: Any
encoder: Encoder
decoder: Decoder
embodiment: ControlledEmbodimentAgent
learning: LearningEngine | None = None
outcome_verifier: TaskOutcomeVerifier = field(default_factory=TaskOutcomeVerifier)
memory: MemoryWorldModel | None = None
neural_memory: NeuralEpisodicMemory | None = None
behavior_profile: BehaviorProfile | None = None
last_step: ExperienceStep | None = None
_pending_frame: SensorFrame | None = None
_pending_prediction: Any = None
_cycle: CycleContract = field(default_factory=CycleContract, init=False)
def reset(self, seed: int | None = None) -> EnvironmentObservation:
"""Reset the environment and episode-local cycle state, not safety stops."""
self._abort_cycle()
observation = self.embodiment.reset(seed)
self.last_step = None
self._cycle.reset()
episode_id = f"episode-{self.embodiment.episode}"
if self.memory is not None:
self.memory.reset_episode(episode_id)
if self.neural_memory is not None:
self.neural_memory.reset_episode(episode_id)
return observation
def _abort_cycle(self) -> None:
self._cycle.abort()
self._pending_frame = None
self._pending_prediction = None
def step(self, tick: int) -> ExperienceStep:
"""Run one complete sensor, network, action, feedback and reward step."""
self.prepare(tick)
try:
result = self.network.step()
except Exception:
self._abort_cycle()
raise
return self.complete(tick, result)
def prepare(self, tick: int) -> SensorFrame:
"""Validate and encode input before exactly one existing runtime tick."""
self._cycle.begin(tick)
try:
if not self.sensor.active:
raise RuntimeError("experience sensor is inactive")
frame = self.sensor.sample(tick)
if (
not valid_tick(frame.tick)
or frame.tick != tick
or frame.sensor_id != self.sensor.sensor_id
or frame.modality != self.sensor.modality
):
raise ValueError("sensor frame identity or tick mismatch")
currents = dict(self.encoder(frame))
if any(
type(neuron_id) is not int
or neuron_id < 0
or isinstance(value, bool)
or not math.isfinite(value)
for neuron_id, value in currents.items()
):
raise ValueError(
"encoder currents must have valid IDs and finite values"
)
self.network.inject_current_batch(currents)
self._pending_frame = frame
return frame
except Exception:
self._abort_cycle()
raise
def complete(self, tick: int, result: Any) -> ExperienceStep:
"""Consume before side effects; late failures cannot replay an action."""
self._cycle.consume(tick)
frame = self._pending_frame
self._pending_frame = None
self._pending_prediction = None
if frame is None or frame.tick != tick:
raise RuntimeError("complete() requires a matching prepare() call")
observation = None
decoded = self.decoder(result, frame)
if isinstance(decoded, tuple):
action: ActionCommand | None = (
self.behavior_profile.select_action(decoded, tick=tick)
if self.behavior_profile is not None
else (decoded[0] if decoded else None)
)
else:
action = decoded
if action is not None and (not valid_tick(action.tick) or action.tick != tick):
raise ValueError("action tick must match the prepared cycle")
prediction = (
None if self.memory is None else self.memory.predict(frame, action, tick)
)
if action is not None:
observation = self.embodiment.step(action)
outcome = (
TaskOutcome(False, False, 0.0, "no environment observation")
if observation is None
else self.outcome_verifier.verify(observation)
)
reward = outcome.reward
if not math.isfinite(reward):
raise ValueError("environment reward must be finite")
if self.learning is not None and observation is not None:
self.learning.set_reward(reward, tick)
record = ExperienceStep(tick, frame, action, observation, reward, outcome)
if self.memory is not None:
self.memory.complete(frame, action, observation, tick, prediction)
if self.neural_memory is not None:
self.neural_memory.record(result, frame, observation)
if self.behavior_profile is not None:
self.behavior_profile.update(success=outcome.success, tick=tick)
self.last_step = record
return record
def attach_runtime(self, runtime: Any) -> None:
"""Attach to a RuntimeController without taking ownership of ticks."""
runtime.add_pre_hook(self.prepare)
runtime.add_hook(self.complete)
__all__ = ["ExperienceEngine", "ExperienceStep"]
|