algorembrant's picture
Upload 253 files
32d978d verified
Raw
History Blame Contribute Delete
7.89 kB
"""
Agent System — Core Knowledge of Agents
Infants distinguish agents from objects by detecting:
1. Self-propulsion: Agents can initiate motion without external contact
2. Goal-directedness: Agents take efficient paths toward goals
3. Contingency: Agents respond to other agents' behavior
This module provides innate priors for identifying and reasoning about
intentional agents in the environment.
Author: Algorembrant, Rembrant Oyangoren Albeos (2026)
"""
import numpy as np
from typing import Optional
class TrackedAgent:
"""An entity being evaluated for agency."""
__slots__ = ['agent_id', 'position_history', 'agency_score',
'self_propelled', 'goal_position', 'efficiency_history']
def __init__(self, agent_id: int):
self.agent_id = agent_id
self.position_history: list[np.ndarray] = []
self.agency_score = 0.0 # 0 = object, 1 = definitely an agent
self.self_propelled = False
self.goal_position: Optional[np.ndarray] = None
self.efficiency_history: list[float] = []
class AgentSystem:
"""
Innate agency detection system.
Evaluates whether tracked entities are intentional agents based on
three core cues: self-propulsion, goal-directedness, and contingency.
This is an innate prior — infants as young as 3 months make these
distinctions.
"""
def __init__(self,
self_propulsion_threshold: float = 0.3,
efficiency_threshold: float = 0.6,
history_window: int = 20):
"""
Args:
self_propulsion_threshold: Min velocity change without contact to flag self-propulsion.
efficiency_threshold: Min path efficiency to flag goal-directedness.
history_window: Number of frames to consider for agency evaluation.
"""
self.self_propulsion_threshold = self_propulsion_threshold
self.efficiency_threshold = efficiency_threshold
self.history_window = history_window
self.agents: dict[int, TrackedAgent] = {}
def update_entity(self, entity_id: int, position: np.ndarray,
was_contacted: bool = False):
"""
Update an entity's trajectory and evaluate agency cues.
Args:
entity_id: Unique identifier for this entity.
position: Current position.
was_contacted: Whether another object contacted this entity this frame.
"""
if entity_id not in self.agents:
self.agents[entity_id] = TrackedAgent(entity_id)
agent = self.agents[entity_id]
pos = np.asarray(position, dtype=np.float64)
agent.position_history.append(pos)
# Trim history
if len(agent.position_history) > self.history_window:
agent.position_history = agent.position_history[-self.history_window:]
# --- CUE 1: Self-propulsion ---
if len(agent.position_history) >= 3:
# Velocity change without external contact = self-propulsion
v_prev = agent.position_history[-2] - agent.position_history[-3]
v_curr = agent.position_history[-1] - agent.position_history[-2]
accel = np.linalg.norm(v_curr - v_prev)
if accel > self.self_propulsion_threshold and not was_contacted:
agent.self_propelled = True
# --- CUE 2: Goal-directedness ---
self._evaluate_goal_directedness(agent)
# --- Compute composite agency score ---
self._compute_agency_score(agent)
def _evaluate_goal_directedness(self, agent: TrackedAgent):
"""
Evaluate whether the entity takes efficient paths toward a goal.
Efficiency = direct_distance / path_length
Agents take short, efficient paths. Objects follow ballistic arcs.
"""
if len(agent.position_history) < 5:
return
# Use the last position as the "observed goal"
start = agent.position_history[0]
end = agent.position_history[-1]
direct_dist = np.linalg.norm(end - start)
if direct_dist < 0.01:
return # Stationary
# Compute path length
path_length = 0.0
for i in range(1, len(agent.position_history)):
path_length += np.linalg.norm(
agent.position_history[i] - agent.position_history[i-1]
)
if path_length < 0.01:
return
efficiency = direct_dist / path_length # 1.0 = perfectly direct
agent.efficiency_history.append(efficiency)
# Trim
if len(agent.efficiency_history) > 10:
agent.efficiency_history = agent.efficiency_history[-10:]
def _compute_agency_score(self, agent: TrackedAgent):
"""Combine cues into a single agency belief."""
score = 0.0
# Self-propulsion is a strong cue
if agent.self_propelled:
score += 0.5
# Goal-directedness
if agent.efficiency_history:
avg_eff = np.mean(agent.efficiency_history)
if avg_eff > self.efficiency_threshold:
score += 0.3
else:
score += 0.1 * avg_eff
# Motion variability (agents move more erratically than ballistic objects)
if len(agent.position_history) >= 3:
velocities = []
for i in range(1, len(agent.position_history)):
v = agent.position_history[i] - agent.position_history[i-1]
velocities.append(v)
if len(velocities) >= 2:
vel_array = np.array(velocities)
direction_changes = 0
for i in range(1, len(vel_array)):
dot = np.dot(vel_array[i], vel_array[i-1])
if dot < 0: # Direction reversal
direction_changes += 1
variability = direction_changes / len(vel_array)
score += 0.2 * variability
agent.agency_score = np.clip(score, 0.0, 1.0)
def is_agent(self, entity_id: int) -> bool:
"""Check if an entity is believed to be an intentional agent."""
agent = self.agents.get(entity_id)
if agent is None:
return False
return agent.agency_score > 0.5
def get_agency_score(self, entity_id: int) -> float:
"""Get the agency belief for an entity (0=object, 1=agent)."""
agent = self.agents.get(entity_id)
if agent is None:
return 0.0
return agent.agency_score
def evaluate_contingency(self, id_a: int, id_b: int) -> float:
"""
Evaluate contingency between two entities.
Contingency = one entity's actions correlate with another's.
This is a strong cue for social interaction.
Returns:
Contingency score (0 to 1).
"""
a = self.agents.get(id_a)
b = self.agents.get(id_b)
if a is None or b is None:
return 0.0
min_len = min(len(a.position_history), len(b.position_history))
if min_len < 3:
return 0.0
# Compute velocity correlation
va = np.diff(np.array(a.position_history[-min_len:]), axis=0)
vb = np.diff(np.array(b.position_history[-min_len:]), axis=0)
# Cross-correlation of velocity magnitudes
mag_a = np.linalg.norm(va, axis=1)
mag_b = np.linalg.norm(vb, axis=1)
if np.std(mag_a) < 1e-8 or np.std(mag_b) < 1e-8:
return 0.0
correlation = np.corrcoef(mag_a, mag_b)[0, 1]
return float(np.clip(abs(correlation), 0.0, 1.0))