Implement Sophia MVP scaffold (stages 0–3 + topology pivot)
Six-crate Rust workspace (core/sim/llm/store/server/bin) backing a Three.js + WebGL frontend. Live at http://127.0.0.1:7777 via `cargo run`. Sim - Event-driven scheduler with min-heap, per-engram tick, staggered Spawn events (40 ms apart) so each engram's flight is visually readable. - Solid-donut torus topology (replaces original spherical density-driven shell, see Topology Pivot in docs/system-analysis.md). Configurable major/minor radii in config.toml; live `POST /api/galaxy/:id/resize`. - Physics: Verlet integration + friction; in-hole pull + galactic spin (CCW around +z) for spiral-ejection ejection from the donut centre; soft tube boundary with velocity-reflecting wall. - Cosine-weighted gravity (kiddo k-NN within radius 25, threshold 0.50) and synapse formation (threshold 0.62) gated to inside-the-tube only. - LM Studio integration via OpenAI-compatible REST: batched embeddings, optional Bearer auth, semaphore-bounded parallel ops per §13.5. Server - axum HTTP + WebSocket. Routes: /healthz, /api/galaxy CRUD, /seed, /ingest, /resize, /engrams/:id, /ws/galaxy/:id/events. - Binary 12-byte-aligned position frames at ~20 Hz; JSON for sparse events (Hello, EngramCreated, SynapseCreated, TorusUpdated). - Layered config: config.toml (defaults) + config.local.toml (secrets, gitignored) merged on startup. Frontend - Vite + vanilla TypeScript + three.js 0.169. - Engrams render as additive bloom-friendly point sprites with a per-engram hash-driven hue rotation and breathing pulse. - Comet-style velocity-aligned trails; additive ribbon synapses whose endpoints track engram positions every frame. - UnrealBloomPass + ACES tone-mapping for the linked-particles look. - HUD shows torus dims, engram + synapse counts, LM Studio status; controls for seed, ingest, and live torus resize. Docs - README replaced with docs/IDEA.md; system-analysis.md updated with the topology pivot decisions and Galaxy Ejection refinement notes (the implementation plan lives in ~/.claude/plans, gitignored). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
crates/sophia-core/Cargo.toml
Normal file
13
crates/sophia-core/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "sophia-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
104
crates/sophia-core/src/dto.rs
Normal file
104
crates/sophia-core/src/dto.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
//! Data-transfer objects shared between the simulation and the server.
|
||||
//!
|
||||
//! These types cross the channel between `sophia-sim` and `sophia-server`,
|
||||
//! and several are also serialized to JSON over WebSocket text frames.
|
||||
//! `PositionFrame` is broadcast as a binary frame instead — see
|
||||
//! `sophia-server::ws` for the wire encoding.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::engram::EngramState;
|
||||
use crate::ids::{EngramId, GalaxyId, SynapseId};
|
||||
use crate::manifest::Manifest;
|
||||
use crate::synapse::Synapse;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GalaxyInfo {
|
||||
pub id: GalaxyId,
|
||||
pub name: String,
|
||||
pub engram_count: usize,
|
||||
pub center: [f32; 3],
|
||||
pub major_radius: f32,
|
||||
pub minor_radius: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EngramSnapshot {
|
||||
pub id: EngramId,
|
||||
pub instance_idx: u32,
|
||||
pub position: [f32; 3],
|
||||
pub size: f32,
|
||||
pub state: EngramState,
|
||||
}
|
||||
|
||||
/// Wire-friendly synapse — same as `Synapse` but always serialized in the
|
||||
/// canonical (a, b) order. Sent over WS for both initial Hello and live
|
||||
/// `SynapseCreated` events.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SynapseDto {
|
||||
pub id: SynapseId,
|
||||
pub a: EngramId,
|
||||
pub b: EngramId,
|
||||
pub weight: f32,
|
||||
}
|
||||
|
||||
impl From<&Synapse> for SynapseDto {
|
||||
fn from(s: &Synapse) -> Self {
|
||||
Self { id: s.id, a: s.a, b: s.b, weight: s.weight }
|
||||
}
|
||||
}
|
||||
|
||||
/// Detailed view returned by `GET /api/galaxy/:gid/engrams/:eid`. Includes the
|
||||
/// manifest (potentially long) but only the slate's norm + dim — the full
|
||||
/// embedding vector would be wasteful to send on every inspector click.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct EngramDetail {
|
||||
pub id: EngramId,
|
||||
pub instance_idx: u32,
|
||||
pub position: [f32; 3],
|
||||
pub size: f32,
|
||||
pub state: EngramState,
|
||||
pub age: u32,
|
||||
pub manifest: Option<Manifest>,
|
||||
pub slate_dim: Option<usize>,
|
||||
pub slate_norm: Option<f32>,
|
||||
}
|
||||
|
||||
/// Position frame: a downsampled bundle of all engram positions at a moment in
|
||||
/// time. Encoded to a binary WS frame at the wire layer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PositionFrame {
|
||||
pub t_ms: u32,
|
||||
/// Indexed by `instance_idx` (dense 0..n).
|
||||
pub positions: Vec<[f32; 3]>,
|
||||
}
|
||||
|
||||
/// Events emitted by the simulation to subscribed WS clients.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum SimEvent {
|
||||
Hello {
|
||||
galaxy: GalaxyInfo,
|
||||
engrams: Vec<EngramSnapshot>,
|
||||
synapses: Vec<SynapseDto>,
|
||||
},
|
||||
EngramCreated {
|
||||
snapshot: EngramSnapshot,
|
||||
},
|
||||
/// A new synapse formed between two engrams. (Stage 3 only emits creates;
|
||||
/// updates and removes arrive in later stages.)
|
||||
SynapseCreated {
|
||||
synapse: SynapseDto,
|
||||
},
|
||||
/// Torus shape changed (initial publish at subscribe + on every resize).
|
||||
/// Replaces the legacy `BBoxUpdated` event from the spherical topology.
|
||||
TorusUpdated {
|
||||
center: [f32; 3],
|
||||
major_radius: f32,
|
||||
minor_radius: f32,
|
||||
},
|
||||
/// Position frame is sent over the wire as a binary frame; this variant
|
||||
/// carries the in-process payload.
|
||||
#[serde(skip)]
|
||||
PositionFrame(PositionFrame),
|
||||
}
|
||||
54
crates/sophia-core/src/engram.rs
Normal file
54
crates/sophia-core/src/engram.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use glam::Vec3;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ids::EngramId;
|
||||
use crate::manifest::Manifest;
|
||||
use crate::slate::Slate;
|
||||
|
||||
/// Engram state machine — see §5 of `docs/system-analysis.md`.
|
||||
///
|
||||
/// Stage 1 only uses `Idle`. Later stages add `Searching`, `Conversing`,
|
||||
/// `Synthesizing`, `Memorize`, `Decaying`, `Deprecated`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EngramState {
|
||||
Idle = 0,
|
||||
Searching = 1,
|
||||
Conversing = 2,
|
||||
Synthesizing = 3,
|
||||
Memorize = 4,
|
||||
Decaying = 5,
|
||||
Deprecated = 6,
|
||||
}
|
||||
|
||||
impl EngramState {
|
||||
pub fn as_u8(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// One unit of knowledge — the fundamental agent.
|
||||
///
|
||||
/// Stage 2 adds `manifest` (the source content) and `slate` (an embedding for
|
||||
/// fast similarity). Stage 4 adds taxonomy/goals/memories on top of that.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Engram {
|
||||
pub id: EngramId,
|
||||
/// Slot index for the visualization's `InstancedMesh`. Assigned at birth,
|
||||
/// stable for the engram's lifetime, dense within a galaxy.
|
||||
pub instance_idx: u32,
|
||||
pub position: Vec3,
|
||||
/// Velocity carried over between ticks — gives the simulation momentum so
|
||||
/// motion glides instead of jittering. Integrated with friction in
|
||||
/// `sophia_sim::physics::tick`.
|
||||
pub velocity: Vec3,
|
||||
pub size: f32,
|
||||
pub state: EngramState,
|
||||
/// Ticks since birth. Drives curiosity decay (see `physics::tick`).
|
||||
pub age: u32,
|
||||
/// What this engram represents. `None` for synthetic seed engrams.
|
||||
pub manifest: Option<Manifest>,
|
||||
/// Embedding vector — System-1 layer of the Universal Slate. `None` for
|
||||
/// synthetic seed engrams.
|
||||
pub slate: Option<Slate>,
|
||||
}
|
||||
77
crates/sophia-core/src/galaxy.rs
Normal file
77
crates/sophia-core/src/galaxy.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use glam::Vec3;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ids::GalaxyId;
|
||||
|
||||
/// Geometry of a galaxy's Space of Recollection — a solid 3D torus volume.
|
||||
///
|
||||
/// `major_radius` is the distance from the donut's center to the centerline
|
||||
/// of the tube; `minor_radius` is the tube's own radius. Engrams live inside
|
||||
/// the tube. The torus is fixed-size (manually resized via API), not
|
||||
/// density-driven — see the Topology Pivot in the implementation plan.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct GalaxyShape {
|
||||
pub major_radius: f32,
|
||||
pub minor_radius: f32,
|
||||
}
|
||||
|
||||
impl GalaxyShape {
|
||||
pub const DEFAULT_MAJOR: f32 = 100.0;
|
||||
pub const DEFAULT_MINOR: f32 = 30.0;
|
||||
|
||||
/// "Great Reflection" — the void at the very center of the donut hole.
|
||||
/// New engrams materialize here and fly outward into the tube. From the
|
||||
/// center, every direction in the xy-plane heads toward the tube; physics
|
||||
/// + a small initial outward velocity does the rest.
|
||||
pub fn birth_point(&self, center: Vec3) -> Vec3 {
|
||||
center
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), &'static str> {
|
||||
if !(self.major_radius.is_finite() && self.minor_radius.is_finite()) {
|
||||
return Err("radii must be finite");
|
||||
}
|
||||
if self.minor_radius <= 0.1 {
|
||||
return Err("minor_radius must be > 0.1");
|
||||
}
|
||||
if self.major_radius <= self.minor_radius {
|
||||
return Err("major_radius must exceed minor_radius");
|
||||
}
|
||||
if self.major_radius > 10_000.0 {
|
||||
return Err("major_radius must be <= 10_000");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GalaxyShape {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
major_radius: Self::DEFAULT_MAJOR,
|
||||
minor_radius: Self::DEFAULT_MINOR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Galaxy metadata — the set of Engrams it contains lives inside the simulation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Galaxy {
|
||||
pub id: GalaxyId,
|
||||
pub name: String,
|
||||
/// Galaxy origin in world coords. Always `Vec3::ZERO` in v1; reserved for
|
||||
/// future multi-galaxy layouts.
|
||||
#[serde(skip)]
|
||||
pub center: Vec3,
|
||||
pub shape: GalaxyShape,
|
||||
}
|
||||
|
||||
impl Galaxy {
|
||||
pub fn new(name: impl Into<String>, shape: GalaxyShape) -> Self {
|
||||
Self {
|
||||
id: GalaxyId::new(),
|
||||
name: name.into(),
|
||||
center: Vec3::ZERO,
|
||||
shape,
|
||||
}
|
||||
}
|
||||
}
|
||||
50
crates/sophia-core/src/ids.rs
Normal file
50
crates/sophia-core/src/ids.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct EngramId(pub Uuid);
|
||||
|
||||
impl EngramId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::now_v7())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EngramId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct GalaxyId(pub Uuid);
|
||||
|
||||
impl GalaxyId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::now_v7())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GalaxyId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SynapseId(pub Uuid);
|
||||
|
||||
impl SynapseId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::now_v7())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SynapseId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
23
crates/sophia-core/src/lib.rs
Normal file
23
crates/sophia-core/src/lib.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
//! Sophia domain types. Pure data — no I/O, no async.
|
||||
//!
|
||||
//! See `docs/system-analysis.md` §1 (System Boundary) and §5 (Engram Dynamics)
|
||||
//! for the conceptual model.
|
||||
|
||||
pub mod dto;
|
||||
pub mod engram;
|
||||
pub mod galaxy;
|
||||
pub mod ids;
|
||||
pub mod manifest;
|
||||
pub mod slate;
|
||||
pub mod synapse;
|
||||
|
||||
pub use dto::{EngramDetail, EngramSnapshot, GalaxyInfo, PositionFrame, SimEvent, SynapseDto};
|
||||
pub use engram::{Engram, EngramState};
|
||||
pub use galaxy::{Galaxy, GalaxyShape};
|
||||
pub use ids::{EngramId, GalaxyId, SynapseId};
|
||||
pub use manifest::Manifest;
|
||||
pub use slate::Slate;
|
||||
pub use synapse::{canonical_pair, Synapse};
|
||||
|
||||
// Re-export glam::Vec3 so consumers don't all need to depend on glam directly.
|
||||
pub use glam::Vec3;
|
||||
26
crates/sophia-core/src/manifest.rs
Normal file
26
crates/sophia-core/src/manifest.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// What an Engram *is* — its source content and self-description.
|
||||
///
|
||||
/// Stage 2 only carries `Text` (paragraphs from ingest). Later stages add
|
||||
/// taxonomy/goals (Stage 4 introspection) and richer modalities.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum Manifest {
|
||||
Text { content: String },
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
pub fn short_label(&self, max_chars: usize) -> String {
|
||||
match self {
|
||||
Manifest::Text { content } => {
|
||||
if content.chars().count() <= max_chars {
|
||||
content.clone()
|
||||
} else {
|
||||
let truncated: String = content.chars().take(max_chars).collect();
|
||||
format!("{truncated}…")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
crates/sophia-core/src/slate.rs
Normal file
35
crates/sophia-core/src/slate.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Universal Slate (per §13.1) — Stage 2 layer 1 only: a dense embedding
|
||||
/// vector. The deeper LLM-comparison layer arrives in Stage 4.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Slate(pub Vec<f32>);
|
||||
|
||||
impl Slate {
|
||||
pub fn dim(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn norm(&self) -> f32 {
|
||||
self.0.iter().map(|x| x * x).sum::<f32>().sqrt()
|
||||
}
|
||||
|
||||
/// Cosine similarity in `[-1, 1]`. Returns 0 if either vector is zero or
|
||||
/// dimensions disagree (the latter can happen across embedding model
|
||||
/// changes — see §13 risk #3).
|
||||
pub fn cosine(&self, other: &Slate) -> f32 {
|
||||
if self.0.len() != other.0.len() {
|
||||
return 0.0;
|
||||
}
|
||||
let mut dot = 0.0_f32;
|
||||
let mut a2 = 0.0_f32;
|
||||
let mut b2 = 0.0_f32;
|
||||
for (a, b) in self.0.iter().zip(other.0.iter()) {
|
||||
dot += a * b;
|
||||
a2 += a * a;
|
||||
b2 += b * b;
|
||||
}
|
||||
let denom = (a2.sqrt() * b2.sqrt()).max(1e-9);
|
||||
dot / denom
|
||||
}
|
||||
}
|
||||
25
crates/sophia-core/src/synapse.rs
Normal file
25
crates/sophia-core/src/synapse.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
//! Synapse — a bidirectional connection between two Engrams formed when
|
||||
//! they spend time close together with a high cosine similarity on their
|
||||
//! Slates. See `docs/system-analysis.md` §1 + §3 (Stage 3 of the impl plan).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ids::{EngramId, SynapseId};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Synapse {
|
||||
pub id: SynapseId,
|
||||
/// Canonical pair endpoints — `a` is always the lexicographically smaller
|
||||
/// uuid so `(a, b)` is order-independent.
|
||||
pub a: EngramId,
|
||||
pub b: EngramId,
|
||||
/// Strength in `[-1, 1]` — currently set to the cosine similarity at
|
||||
/// formation time. Stage 3 doesn't update it; future stages may.
|
||||
pub weight: f32,
|
||||
}
|
||||
|
||||
/// Lay two ids out canonically (lower uuid first) so a `(a, b)` pair lookup
|
||||
/// is order-independent.
|
||||
pub fn canonical_pair(x: EngramId, y: EngramId) -> (EngramId, EngramId) {
|
||||
if x.0 <= y.0 { (x, y) } else { (y, x) }
|
||||
}
|
||||
Reference in New Issue
Block a user