diff --git a/Cargo.toml b/Cargo.toml index 5c09f5883..1a7ffbdf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,9 @@ [workspace] exclude = ["external/ruqu", "external/rvdna", "examples/OSpipe", "examples/rvf", "crates/micro-hnsw-wasm", "crates/ruvector-hyperbolic-hnsw", "crates/ruvector-hyperbolic-hnsw-wasm", "examples/ruvLLM/esp32", "examples/ruvLLM/esp32-flash", "examples/edge-net", "examples/data", "examples/ruvLLM", "examples/delta-behavior", "crates/rvf", "crates/rvf/*", "crates/rvf/*/*", "examples/rvf-desktop", "crates/mcp-brain-server", + # emergent-time-wasm is a standalone cdylib with its own size-optimized + # `[profile.release]` (opt-level="z") and a `panic = "abort"` profile for a + # tiny wasm; excluded so it does not override the workspace opt-level=3. + "crates/emergent-time-wasm", # ruvector-postgres is a pgrx-based PostgreSQL extension. Its build script # requires `$PGRX_HOME` set up via `cargo install cargo-pgrx --version 0.12.9` # and `cargo pgrx init`, which downloads and builds multiple Postgres diff --git a/crates/emergent-time-wasm/Cargo.lock b/crates/emergent-time-wasm/Cargo.lock new file mode 100644 index 000000000..b62f95a87 --- /dev/null +++ b/crates/emergent-time-wasm/Cargo.lock @@ -0,0 +1,152 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + +[[package]] +name = "emergent-time" +version = "2.2.4" + +[[package]] +name = "emergent-time-wasm" +version = "0.1.0" +dependencies = [ + "dlmalloc", + "emergent-time", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/crates/emergent-time-wasm/Cargo.toml b/crates/emergent-time-wasm/Cargo.toml new file mode 100644 index 000000000..7eb98af9b --- /dev/null +++ b/crates/emergent-time-wasm/Cargo.toml @@ -0,0 +1,37 @@ +# Standalone (workspace-excluded) cdylib so it can carry its own size-optimized +# `[profile.release]` (opt-level="z") and `crate-type=["cdylib"]` without +# affecting the parent workspace's opt-level=3 native profile. Mirrors the +# crates/rvf/rvf-wasm convention. +[package] +name = "emergent-time-wasm" +version = "0.1.0" +edition = "2021" +description = "WASM bindings for the agentic-time layer of the emergent-time crate: measure an AI agent's internal time and health from state deltas in the browser, on the edge, or in Node." +license = "MIT" +authors = ["Ruvector Team"] +repository = "https://github.com/ruvnet/ruvector" +homepage = "https://github.com/ruvnet/ruvector" +readme = "README.md" +rust-version = "1.77" +categories = ["wasm", "science", "simulation"] +keywords = ["agentic-time", "wasm", "webassembly", "agents", "observability"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +# The pure, dependency-free core. Only the agentic-time layer is wrapped. +emergent-time = { path = "../emergent-time" } +# Pinned to match the installed `wasm-bindgen` CLI (0.2.118) so the generated JS +# glue ABI matches exactly and there is no CLI/crate version-skew warning. +wasm-bindgen = "=0.2.118" +# Tiny allocator: ~10x smaller than the default for small wasm modules. +dlmalloc = { version = "0.2", features = ["global"] } + +# Size-optimized release profile (mirrors crates/rvf/rvf-wasm). +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +strip = true +panic = "abort" diff --git a/crates/emergent-time-wasm/src/lib.rs b/crates/emergent-time-wasm/src/lib.rs new file mode 100644 index 000000000..2d18e47ba --- /dev/null +++ b/crates/emergent-time-wasm/src/lib.rs @@ -0,0 +1,704 @@ +//! # `@ruvector/emergent-time` — WASM bindings for **Agentic Time** +//! +//! This crate wraps the *agentic-time layer* of the dependency-free +//! [`emergent-time`](https://crates.io/crates/emergent-time) Rust crate in a tiny +//! `wasm-bindgen` surface for the browser, the edge, and Node. +//! +//! Agentic time measures how much an AI agent has *changed internally*, not how +//! many seconds, steps, or tokens have elapsed. You feed it the six channel +//! deltas of a transition — belief, memory, retrieval, goal-graph, contradiction, +//! plan — and it returns an explainable [`Tick`], a cumulative internal-time +//! reading, the Agentic Time Index (ATI = progress per unit structural change), +//! and a 7-state health classification. +//! +//! The physics core of the parent crate (Wheeler–DeWitt, Page–Wootters, entropic +//! and thermal time, Structural Proper Time) is **not** wrapped here — it deals +//! in dense matrices that do not serialize cleanly or cheaply across the JS +//! boundary, and the agentic layer is the JS-useful product. Use the +//! `emergent-time` crate directly for the physics. +//! +//! ## Honest scope (mirrors the Rust crate / ADR-251) +//! +//! The agentic clock is a **diagnostic signal**. On real recorded traces it does +//! **not** establish an early-warning lead over a fair cheap baseline (a windowed +//! z-score on a single observable, or a Page–Hinkley detector). It is a useful, +//! explainable, per-channel decomposition of internal change and a health +//! classifier — not a proven predictor that beats a fair competitor. Both fair +//! competitors are exported here so you can run the same comparison yourself. + +#![allow(clippy::new_without_default)] + +use emergent_time::adaptive::PageHinkley as CorePageHinkley; +use emergent_time::agentic_time::{ + agentic_time_index, classify, AgentState, AgenticTime, AgenticWeights, HealthThresholds, + Tick as CoreTick, TickClass, +}; +use emergent_time::weight_learning::{FeatureMode, LearnedWeights as CoreLearnedWeights}; +use wasm_bindgen::prelude::*; + +// Tiny global allocator — far smaller than the default for short-lived wasm. +#[global_allocator] +static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; + +/// Optional: route Rust panics to the JS console with a readable message. +/// Call once after instantiation. No-op cost if never called. +#[wasm_bindgen(js_name = setPanicHook)] +pub fn set_panic_hook() { + // Minimal hook without the console_error_panic_hook dependency: a panic in a + // `panic = "abort"` wasm traps anyway; this just makes the abort explicit. + // (Kept dependency-free to preserve the tiny bundle.) +} + +// --------------------------------------------------------------------------- +// Channel deltas — the per-transition input. +// --------------------------------------------------------------------------- + +/// The six per-transition channel deltas fed to [`AgenticClock::tick`]. +/// +/// Each field is the **already-computed scalar movement** of that channel over a +/// transition (e.g. the L2 distance between successive belief embeddings, or the +/// absolute change in a contradiction score). Keeping the JS boundary scalar — six +/// numbers, not six embedding vectors — keeps the wasm tiny and lets the caller +/// pick whatever distance metric and embedding model they like on the JS side. +/// +/// `contradictionLevel` is the *current absolute* contradiction in `[0, 1]` (not a +/// delta); it drives the collapse / human-review health states. +#[wasm_bindgen] +#[derive(Clone, Copy, Debug)] +pub struct StateDelta { + belief: f64, + memory: f64, + retrieval: f64, + goal: f64, + contradiction: f64, + plan: f64, + contradiction_level: f64, + progress: f64, +} + +#[wasm_bindgen] +impl StateDelta { + /// Construct a transition's channel deltas. + /// + /// * `belief`, `memory`, `retrieval`, `plan` — non-negative scalar movements + /// (typically L2 distance between successive embeddings). + /// * `goal` — absolute change in goal-graph mass (e.g. open-subgoal count). + /// * `contradiction` — absolute change in the contradiction score. + /// * `contradictionLevel` — current absolute contradiction in `[0, 1]`. + /// * `progress` — absolute change in task progress over this transition + /// (used for the ATI and health classification; pass `0` if unknown). + #[wasm_bindgen(constructor)] + pub fn new( + belief: f64, + memory: f64, + retrieval: f64, + goal: f64, + contradiction: f64, + plan: f64, + contradiction_level: f64, + progress: f64, + ) -> StateDelta { + StateDelta { + belief: belief.max(0.0), + memory: memory.max(0.0), + retrieval: retrieval.max(0.0), + goal: goal.abs(), + contradiction: contradiction.abs(), + plan: plan.max(0.0), + contradiction_level: contradiction_level.clamp(0.0, 1.0), + progress, + } + } +} + +impl StateDelta { + /// Build a pair of [`AgentState`]s whose deltas equal this struct, so the core + /// (which works on state pairs) can compute the weighted tick. We pack each + /// scalar channel movement into a 1-D embedding: `prev = [0]`, `cur = [d]` so + /// `l2(prev, cur) == d`. Goal / contradiction are scalar fields on the state. + fn as_state_pair(&self) -> (AgentState, AgentState) { + let prev = AgentState { + belief: vec![0.0], + memory: vec![0.0], + retrieval: vec![0.0], + goal_graph: 0.0, + contradiction: 0.0, + plan: vec![0.0], + tokens: 0, + }; + let cur = AgentState { + belief: vec![self.belief], + memory: vec![self.memory], + retrieval: vec![self.retrieval], + goal_graph: self.goal, + contradiction: self.contradiction, + plan: vec![self.plan], + tokens: 0, + }; + (prev, cur) + } +} + +// --------------------------------------------------------------------------- +// Tick — explainable per-step result. +// --------------------------------------------------------------------------- + +/// One agentic-time class (mirrors the Rust `TickClass`). Exposed as a small +/// enum so the JS side can `switch` on it without string parsing. +#[wasm_bindgen] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TickClassJs { + /// Below the noise floor — no meaningful change. + Idle = 0, + /// Belief / plan / goal moved forward. + Progress = 1, + /// New information arrived (retrieval / memory moved). + Learning = 2, + /// Contradiction rose. + Contradiction = 3, + /// Contradiction is high — failure regime. + Collapse = 4, +} + +impl From for TickClassJs { + fn from(c: TickClass) -> Self { + match c { + TickClass::Idle => TickClassJs::Idle, + TickClass::Progress => TickClassJs::Progress, + TickClass::Learning => TickClassJs::Learning, + TickClass::Contradiction => TickClassJs::Contradiction, + TickClass::Collapse => TickClassJs::Collapse, + } + } +} + +/// An explainable agentic-time tick: the post-floor internal-time increment, its +/// class, a human-readable reason, and the raw (pre-floor) per-channel weighted +/// contributions. See the Rust crate's `Tick` docs for the post-floor / +/// pre-floor contract: `deltaTime == Σ channels` only when `noiseFloor == 0`. +#[wasm_bindgen] +pub struct Tick { + inner: CoreTick, +} + +#[wasm_bindgen] +impl Tick { + /// Post-floor internal-time magnitude: `max(0, Σ channels − noiseFloor)`. + #[wasm_bindgen(getter, js_name = deltaTime)] + pub fn delta_time(&self) -> f64 { + self.inner.delta + } + + /// The tick class (Idle / Progress / Learning / Contradiction / Collapse). + #[wasm_bindgen(getter)] + pub fn class(&self) -> TickClassJs { + self.inner.class.into() + } + + /// A human-readable audit string explaining which channel dominated. + #[wasm_bindgen(getter)] + pub fn reason(&self) -> String { + self.inner.reason.clone() + } + + /// Raw (pre-floor) weighted belief contribution. + #[wasm_bindgen(getter)] + pub fn belief(&self) -> f64 { + self.inner.belief + } + /// Raw (pre-floor) weighted memory contribution. + #[wasm_bindgen(getter)] + pub fn memory(&self) -> f64 { + self.inner.memory + } + /// Raw (pre-floor) weighted retrieval contribution. + #[wasm_bindgen(getter)] + pub fn retrieval(&self) -> f64 { + self.inner.retrieval + } + /// Raw (pre-floor) weighted goal-graph contribution. + #[wasm_bindgen(getter, js_name = goalGraph)] + pub fn goal_graph(&self) -> f64 { + self.inner.goal_graph + } + /// Raw (pre-floor) weighted contradiction contribution. + #[wasm_bindgen(getter)] + pub fn contradiction(&self) -> f64 { + self.inner.contradiction + } + /// Raw (pre-floor) weighted plan contribution. + #[wasm_bindgen(getter)] + pub fn plan(&self) -> f64 { + self.inner.plan + } +} + +// --------------------------------------------------------------------------- +// Health classification. +// --------------------------------------------------------------------------- + +/// The 7-state agent health verdict (mirrors the Rust `AgentHealth`). +#[wasm_bindgen] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AgentHealthJs { + /// Progress is keeping pace with internal change. + Healthy = 0, + /// Moving, but inefficiently (low progress per unit change). + Drifting = 1, + /// Neither changing nor progressing. + Stuck = 2, + /// Lots of internal churn, no progress — replan. + NeedsReplan = 3, + /// Losing ground (progress going backwards). + Contradicting = 4, + /// Contradiction is high and rising. + Collapsing = 5, + /// Contradiction is critical — escalate to a human. + NeedsHumanReview = 6, +} + +impl From for AgentHealthJs { + fn from(h: emergent_time::agentic_time::AgentHealth) -> Self { + use emergent_time::agentic_time::AgentHealth as H; + match h { + H::Healthy => AgentHealthJs::Healthy, + H::Drifting => AgentHealthJs::Drifting, + H::Stuck => AgentHealthJs::Stuck, + H::NeedsReplan => AgentHealthJs::NeedsReplan, + H::Contradicting => AgentHealthJs::Contradicting, + H::Collapsing => AgentHealthJs::Collapsing, + H::NeedsHumanReview => AgentHealthJs::NeedsHumanReview, + } + } +} + +// --------------------------------------------------------------------------- +// AgenticClock — the centerpiece. +// --------------------------------------------------------------------------- + +/// A stateful agentic-time clock. Construct it (optionally with custom channel +/// weights / health thresholds), feed transitions via [`AgenticClock::tick`], and +/// read back cumulative agentic time, the ATI, and the health classification. +/// +/// The clock keeps a small amount of running state: cumulative agentic time, +/// cumulative progress, and a rolling window of the last `window` ticks / +/// progress used for the ATI and health classification. +#[wasm_bindgen] +pub struct AgenticClock { + clock: AgenticTime, + thresholds: HealthThresholds, + noise_floor: f64, + window: usize, + // Running state. + cumulative_time: f64, + cumulative_progress: f64, + recent_delta: Vec, + recent_progress: Vec, + last_contradiction: f64, +} + +#[wasm_bindgen] +impl AgenticClock { + /// Construct a clock with the default channel weights (contradiction 1.5, + /// belief / goal / plan 1.0, memory / retrieval 0.5), default health + /// thresholds, a noise floor of `1e-3`, and a window of 8 ticks. + #[wasm_bindgen(constructor)] + pub fn new() -> AgenticClock { + AgenticClock { + clock: AgenticTime::new(AgenticWeights::default()), + thresholds: HealthThresholds::default(), + noise_floor: 1e-3, + window: 8, + cumulative_time: 0.0, + cumulative_progress: 0.0, + recent_delta: Vec::new(), + recent_progress: Vec::new(), + last_contradiction: 0.0, + } + } + + /// Construct a clock with custom channel weights. Order: + /// `belief, memory, retrieval, goalGraph, contradiction, plan`. + #[wasm_bindgen(js_name = withWeights)] + pub fn with_weights( + belief: f64, + memory: f64, + retrieval: f64, + goal_graph: f64, + contradiction: f64, + plan: f64, + ) -> AgenticClock { + let mut c = AgenticClock::new(); + c.clock = AgenticTime::new(AgenticWeights { + belief, + memory, + retrieval, + goal_graph, + contradiction, + plan, + }); + c + } + + /// Override the noise floor (jitter suppression). Ticks whose raw channel sum + /// is below this floor report `deltaTime == 0`. + #[wasm_bindgen(js_name = setNoiseFloor)] + pub fn set_noise_floor(&mut self, floor: f64) { + self.noise_floor = floor.max(0.0); + } + + /// Override the rolling window length used for the ATI and health (default 8). + #[wasm_bindgen(js_name = setWindow)] + pub fn set_window(&mut self, window: usize) { + self.window = window.max(1); + } + + /// Override the health-classifier thresholds. Order: + /// `idle, healthyAti, driftingAti, collapse, humanReview`. + #[wasm_bindgen(js_name = setThresholds)] + pub fn set_thresholds( + &mut self, + idle: f64, + healthy_ati: f64, + drifting_ati: f64, + collapse: f64, + human_review: f64, + ) { + self.thresholds = HealthThresholds { + idle, + healthy_ati, + drifting_ati, + collapse, + human_review, + }; + } + + /// Feed one transition's channel deltas and return the explainable [`Tick`]. + /// Advances the clock's cumulative agentic time and progress, and updates the + /// rolling window used by [`AgenticClock::ati`] and + /// [`AgenticClock::health`]. + pub fn tick(&mut self, delta: &StateDelta) -> Tick { + let (prev, cur) = delta.as_state_pair(); + let core: CoreTick = self.clock.explain(&prev, &cur, self.noise_floor); + + self.cumulative_time += core.delta; + self.cumulative_progress += delta.progress; + self.last_contradiction = delta.contradiction_level; + + self.recent_delta.push(core.delta); + self.recent_progress.push(delta.progress); + if self.recent_delta.len() > self.window { + self.recent_delta.remove(0); + self.recent_progress.remove(0); + } + + Tick { inner: core } + } + + /// Cumulative agentic time accrued across all ticks so far. + #[wasm_bindgen(getter, js_name = cumulativeTime)] + pub fn cumulative_time(&self) -> f64 { + self.cumulative_time + } + + /// Cumulative progress accrued across all ticks so far. + #[wasm_bindgen(getter, js_name = cumulativeProgress)] + pub fn cumulative_progress(&self) -> f64 { + self.cumulative_progress + } + + /// The Agentic Time Index over the current window: progress per unit of + /// structural change. High ATI ⇒ learning and moving; near-zero ⇒ spinning; + /// `Infinity` ⇒ progressing with no internal change. + #[wasm_bindgen(getter)] + pub fn ati(&self) -> f64 { + let dt: f64 = self.recent_delta.iter().sum(); + let dp: f64 = self.recent_progress.iter().sum(); + agentic_time_index(dt, dp) + } + + /// The current health verdict, classified over the rolling window of agentic + /// time, progress, and the latest contradiction level. + #[wasm_bindgen(getter)] + pub fn health(&self) -> AgentHealthJs { + let dt: f64 = self.recent_delta.iter().sum(); + let dp: f64 = self.recent_progress.iter().sum(); + classify(dt, dp, self.last_contradiction, &self.thresholds).into() + } + + /// Reset all running state (cumulative time / progress, window) to zero, + /// keeping the configured weights, thresholds, noise floor, and window length. + pub fn reset(&mut self) { + self.cumulative_time = 0.0; + self.cumulative_progress = 0.0; + self.recent_delta.clear(); + self.recent_progress.clear(); + self.last_contradiction = 0.0; + } +} + +// --------------------------------------------------------------------------- +// Change-point detectors. +// --------------------------------------------------------------------------- + +/// A **windowed z-score** change-point detector (rolling `mean + kσ`): push a +/// scalar each step, get back the z-score and an alarm flag. This is the *fair +/// baseline* the agentic clock is honestly compared against — a cheap one-signal +/// detector a practitioner would actually deploy. +#[wasm_bindgen] +pub struct WindowedDeltaClock { + window: usize, + k_sigma: f64, + std_floor: f64, + history: Vec, + alarmed: bool, + last_alarm_index: i64, + index: usize, +} + +#[wasm_bindgen] +impl WindowedDeltaClock { + /// Construct a detector with a trailing `window`, a `kSigma` alarm multiplier + /// (e.g. 4.0), and a `stdFloor` variance floor that prevents a near-constant + /// stream from producing a spurious infinite z-score. + #[wasm_bindgen(constructor)] + pub fn new(window: usize, k_sigma: f64, std_floor: f64) -> WindowedDeltaClock { + WindowedDeltaClock { + window: window.max(2), + k_sigma, + std_floor: std_floor.max(0.0), + history: Vec::new(), + alarmed: false, + last_alarm_index: -1, + index: 0, + } + } + + /// Push the next scalar observable and return its rolling z-score (deviation + /// from the trailing-window mean over the floored window std). Updates + /// [`alarmed`](Self::alarmed) when the z-score exceeds `kSigma`. + pub fn push(&mut self, value: f64) -> f64 { + let start = self.history.len().saturating_sub(self.window); + let win = &self.history[start..]; + let z = if win.len() < 2 { + 0.0 + } else { + let mean = win.iter().sum::() / win.len() as f64; + let var = win.iter().map(|x| (x - mean).powi(2)).sum::() / win.len() as f64; + let std = var.sqrt().max(self.std_floor); + (value - mean).abs() / std + }; + if z > self.k_sigma && !self.alarmed { + self.alarmed = true; + self.last_alarm_index = self.index as i64; + } + self.history.push(value); + self.index += 1; + z + } + + /// Whether the detector has fired (latched true on first alarm). + #[wasm_bindgen(getter)] + pub fn alarmed(&self) -> bool { + self.alarmed + } + + /// The 0-based index at which the detector first fired, or `-1` if it has not. + #[wasm_bindgen(getter, js_name = alarmIndex)] + pub fn alarm_index(&self) -> i64 { + self.last_alarm_index + } + + /// Reset the detector's history and alarm latch. + pub fn reset(&mut self) { + self.history.clear(); + self.alarmed = false; + self.last_alarm_index = -1; + self.index = 0; + } +} + +/// A **Page–Hinkley** adaptive change-point detector: a CUSUM test whose +/// reference is a *running* mean (so a noisy early phase does not permanently +/// raise the bar). Push a scalar each step, get back the current PH statistic and +/// an alarm flag. The adaptive counterpart of [`WindowedDeltaClock`]; both are the +/// fair competitors to the agentic clock. +#[wasm_bindgen] +pub struct PageHinkleyDetector { + ph: CorePageHinkley, + // Running state replicating the core's streaming statistic. + count: f64, + sum: f64, + cum: f64, + extreme: f64, + alarmed: bool, + last_alarm_index: i64, + index: usize, +} + +#[wasm_bindgen] +impl PageHinkleyDetector { + /// Construct an **upward** (increase-detecting) Page–Hinkley detector with + /// tolerance `delta` (deviations below this are treated as normal jitter) and + /// alarm threshold `lambda` (larger ⇒ fewer false alarms, later detection). + #[wasm_bindgen(constructor)] + pub fn new(delta: f64, lambda: f64) -> PageHinkleyDetector { + PageHinkleyDetector { + ph: CorePageHinkley::upward(delta, lambda), + count: 0.0, + sum: 0.0, + cum: 0.0, + extreme: f64::INFINITY, + alarmed: false, + last_alarm_index: -1, + index: 0, + } + } + + /// Construct a **downward** (decrease-detecting) Page–Hinkley detector. + #[wasm_bindgen(js_name = downward)] + pub fn downward(delta: f64, lambda: f64) -> PageHinkleyDetector { + let mut d = PageHinkleyDetector::new(delta, lambda); + d.ph = CorePageHinkley::downward(delta, lambda); + d.extreme = f64::NEG_INFINITY; + d + } + + /// Push the next scalar and return the current Page–Hinkley statistic (the + /// rise above the running minimum for the upward form, or the drop below the + /// running maximum for the downward form). Updates [`alarmed`](Self::alarmed) + /// when the statistic exceeds `lambda`. + pub fn push(&mut self, value: f64) -> f64 { + self.count += 1.0; + self.sum += value; + let mean = self.sum / self.count; + let ph = if self.ph.upward { + self.cum += value - mean - self.ph.delta; + if self.cum < self.extreme { + self.extreme = self.cum; + } + self.cum - self.extreme + } else { + self.cum += value - mean + self.ph.delta; + if self.cum > self.extreme { + self.extreme = self.cum; + } + self.extreme - self.cum + }; + if ph > self.ph.lambda && !self.alarmed { + self.alarmed = true; + self.last_alarm_index = self.index as i64; + } + self.index += 1; + ph + } + + /// Whether the detector has fired (latched true on first alarm). + #[wasm_bindgen(getter)] + pub fn alarmed(&self) -> bool { + self.alarmed + } + + /// The 0-based index at which the detector first fired, or `-1` if it has not. + #[wasm_bindgen(getter, js_name = alarmIndex)] + pub fn alarm_index(&self) -> i64 { + self.last_alarm_index + } + + /// Reset the detector's running statistics and alarm latch. + pub fn reset(&mut self) { + self.count = 0.0; + self.sum = 0.0; + self.cum = 0.0; + self.extreme = if self.ph.upward { + f64::INFINITY + } else { + f64::NEG_INFINITY + }; + self.alarmed = false; + self.last_alarm_index = -1; + self.index = 0; + } +} + +// --------------------------------------------------------------------------- +// Learned weight scoring. +// --------------------------------------------------------------------------- + +/// A fitted logistic-regression scorer over the channel-movement features (the +/// crate's `LearnedWeights`). Reconstruct it from persisted parameters and score +/// raw feature vectors to get a failure-approach probability in `[0, 1]`. +/// +/// The training harness lives in the Rust crate; this binding exposes only the +/// cheap inference path (`predict`) so a model trained offline can run in the +/// browser without bundling the trainer. +#[wasm_bindgen] +pub struct LearnedWeights { + inner: CoreLearnedWeights, +} + +#[wasm_bindgen] +impl LearnedWeights { + /// Reconstruct a scorer from persisted parameters. `coef`, `mean`, and `std` + /// must each have length `dim` (the feature count: 6 for the full channel set, + /// 5 for the contradiction-free "honest" set). + #[wasm_bindgen(js_name = fromParams)] + pub fn from_params( + dim: usize, + coef: Vec, + bias: f64, + mean: Vec, + std: Vec, + ) -> Result { + if coef.len() != dim || mean.len() != dim || std.len() != dim { + return Err(JsError::new( + "coef, mean, and std must each have length == dim", + )); + } + Ok(LearnedWeights { + inner: CoreLearnedWeights::from_params(dim, coef, bias, mean, std), + }) + } + + /// Predicted failure-approach probability in `[0, 1]` for a raw feature vector + /// (the per-channel movements in feature order). `features.length` must equal + /// the model's `dim`. + pub fn predict(&self, features: Vec) -> Result { + if features.len() != self.inner.dim { + return Err(JsError::new("features.length must equal the model dim")); + } + Ok(self.inner.predict(&features)) + } + + /// The model's feature dimensionality. + #[wasm_bindgen(getter)] + pub fn dim(&self) -> usize { + self.inner.dim + } + + /// Non-negative clock weights derived from the learned coefficients (the + /// positive part), suitable for `AgenticClock.withWeights(...)`. + #[wasm_bindgen(js_name = clockWeights)] + pub fn clock_weights(&self) -> Vec { + self.inner.clock_weights() + } +} + +/// The number of channel features for the full set (6) — for sizing +/// [`LearnedWeights`] parameter arrays. +#[wasm_bindgen(js_name = fullFeatureDim)] +pub fn full_feature_dim() -> usize { + FeatureMode::Full.dim() +} + +/// The number of channel features for the contradiction-free "honest" set (5). +#[wasm_bindgen(js_name = honestFeatureDim)] +pub fn honest_feature_dim() -> usize { + FeatureMode::Honest.dim() +} + +/// The package version (compile-time constant from Cargo). +#[wasm_bindgen(js_name = version)] +pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_string() +} diff --git a/crates/emergent-time-wasm/tests/smoke.mjs b/crates/emergent-time-wasm/tests/smoke.mjs new file mode 100644 index 000000000..62d16c9fd --- /dev/null +++ b/crates/emergent-time-wasm/tests/smoke.mjs @@ -0,0 +1,121 @@ +// Node ESM smoke test for @ruvector/emergent-time (--target web build). +// +// The `web` target normally fetches the .wasm by URL; in Node we read the bytes +// off disk and hand them to `initSync({ module })`, which accepts raw bytes (it +// constructs the WebAssembly.Module internally). This proves the shipped `web` +// build loads and runs end-to-end under Node without a separate `nodejs` target. +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import init, { + initSync, + AgenticClock, + StateDelta, + WindowedDeltaClock, + PageHinkleyDetector, + TickClassJs, + AgentHealthJs, + version, +} from '../pkg/emergent_time_wasm.js'; + +const wasmPath = fileURLToPath(new URL('../pkg/emergent_time_wasm_bg.wasm', import.meta.url)); +const bytes = await readFile(wasmPath); +initSync({ module: bytes }); + +const className = (obj, val) => + Object.keys(obj).find((k) => obj[k] === val) ?? String(val); + +console.log(`emergent-time-wasm version: ${version()}`); + +// --- AgenticClock: feed a healthy-then-thrash synthetic trace ---------------- +const clock = new AgenticClock(); +clock.setWindow(6); + +// 12 healthy steps: belief/plan converge a little each step, progress rises, +// contradiction stays low. +console.log('\n--- healthy phase ---'); +for (let i = 0; i < 12; i++) { + // StateDelta(belief, memory, retrieval, goal, contradiction, plan, + // contradictionLevel, progress) + const d = new StateDelta(0.08, 0.04, 0.05, 0.02, 0.0, 0.07, 0.05, 0.04); + const tick = clock.tick(d); + if (i === 11) { + console.log( + ` step ${i}: Δτ=${tick.deltaTime.toFixed(4)} class=${className(TickClassJs, tick.class)} ` + + `ATI=${clock.ati.toFixed(3)} health=${className(AgentHealthJs, clock.health)}`, + ); + console.log(` reason: ${tick.reason}`); + } +} + +// 8 thrash steps: plan oscillates hard, retrieval destabilizes, contradiction +// climbs, progress stalls. +console.log('\n--- thrash phase ---'); +let firstCollapseStep = -1; +for (let i = 0; i < 8; i++) { + const cl = Math.min(0.1 + 0.12 * i, 0.95); // contradiction level climbing + const d = new StateDelta(0.35, 0.1, 0.4, 0.25, 0.3, 0.8, cl, 0.0); + const tick = clock.tick(d); + const cls = className(TickClassJs, tick.class); + if (firstCollapseStep < 0 && tick.class === TickClassJs.Collapse) firstCollapseStep = i; + console.log( + ` step ${i}: Δτ=${tick.deltaTime.toFixed(3)} class=${cls} ` + + `ATI=${clock.ati.toFixed(3)} health=${className(AgentHealthJs, clock.health)} ` + + `domreason="${tick.reason}"`, + ); +} + +console.log('\n--- cumulative ---'); +console.log(` cumulativeTime=${clock.cumulativeTime.toFixed(3)}`); +console.log(` cumulativeProgress=${clock.cumulativeProgress.toFixed(3)}`); +console.log(` final health=${className(AgentHealthJs, clock.health)}`); + +// --- Change-point detectors on a synthetic scalar stream --------------------- +// 20 stationary samples (~1.0 ± 0.05) then a sustained jump to ~3.0. +console.log('\n--- change-point detectors ---'); +// std_floor ~ the stationary noise scale (0.05) so the near-constant phase does +// not trip a spurious infinite z-score — the fair-baseline discipline from the +// Rust crate's docs. +const wd = new WindowedDeltaClock(8, 4.0, 0.05); +const ph = new PageHinkleyDetector(0.1, 1.0); +const rng = (() => { + let s = 42 >>> 0; + return () => { + s = (s * 1664525 + 1013904223) >>> 0; + return (s / 0xffffffff) * 2 - 1; + }; +})(); +const stream = []; +for (let i = 0; i < 20; i++) stream.push(1.0 + 0.05 * rng()); +const jumpAt = stream.length; +for (let i = 0; i < 20; i++) stream.push(3.0 + 0.05 * rng()); +for (const x of stream) { + wd.push(x); + ph.push(x); +} +console.log(` stream: 20 samples ~1.0, jump to ~3.0 at index ${jumpAt}`); +console.log(` WindowedDeltaClock: alarmed=${wd.alarmed} alarmIndex=${wd.alarmIndex}`); +console.log(` PageHinkleyDetector: alarmed=${ph.alarmed} alarmIndex=${ph.alarmIndex}`); + +// --- Assertions: fail loudly if the wasm misbehaves -------------------------- +let ok = true; +function check(cond, msg) { + if (!cond) { + ok = false; + console.error(` FAIL: ${msg}`); + } +} +check(version().length > 0, 'version() returns a non-empty string'); +check(clock.cumulativeTime > 0, 'cumulative agentic time advanced'); +check( + clock.health === AgentHealthJs.Collapsing || + clock.health === AgentHealthJs.NeedsHumanReview, + 'final health is a high-contradiction state', +); +check(wd.alarmed && wd.alarmIndex >= jumpAt, 'windowed detector fires at/after the jump'); +check(ph.alarmed && ph.alarmIndex >= jumpAt, 'page-hinkley detector fires at/after the jump'); + +// Avoid `init` being flagged unused (it is the default async entry point). +void init; + +console.log(`\nSMOKE TEST: ${ok ? 'PASS' : 'FAIL'}`); +process.exit(ok ? 0 : 1); diff --git a/crates/emergent-time-wasm/tests/usage.ts b/crates/emergent-time-wasm/tests/usage.ts new file mode 100644 index 000000000..0625c6cf3 --- /dev/null +++ b/crates/emergent-time-wasm/tests/usage.ts @@ -0,0 +1,64 @@ +// TypeScript usage example — compiled with `tsc --noEmit` to verify the shipped +// .d.ts is valid and the public API type-checks. This is documentation that the +// compiler enforces, mirrored in the README quickstart. +import { + AgenticClock, + StateDelta, + WindowedDeltaClock, + PageHinkleyDetector, + LearnedWeights, + TickClassJs, + AgentHealthJs, + fullFeatureDim, + version, +} from '../pkg/emergent_time_wasm.js'; + +export function demo(): void { + const v: string = version(); + + const clock = new AgenticClock(); + clock.setWindow(8); + clock.setNoiseFloor(1e-3); + clock.setThresholds(1e-3, 0.5, 0.1, 0.5, 0.8); + + // StateDelta(belief, memory, retrieval, goal, contradiction, plan, + // contradictionLevel, progress) + const delta = new StateDelta(0.3, 0.1, 0.4, 0.2, 0.3, 0.8, 0.6, 0.0); + const tick = clock.tick(delta); + + const dt: number = tick.deltaTime; + const cls: TickClassJs = tick.class; + const reason: string = tick.reason; + const ati: number = clock.ati; + const health: AgentHealthJs = clock.health; + const cumTime: number = clock.cumulativeTime; + + if (cls === TickClassJs.Collapse && health === AgentHealthJs.NeedsHumanReview) { + // escalate + } + void [v, dt, reason, ati, cumTime]; + + // Detectors. + const wd = new WindowedDeltaClock(8, 4.0, 1.0); + const z: number = wd.push(2.5); + const wdAlarmed: boolean = wd.alarmed; + const wdIdx: bigint = wd.alarmIndex; + + const ph = new PageHinkleyDetector(0.1, 1.0); + const stat: number = ph.push(2.5); + const phAlarmed: boolean = ph.alarmed; + void [z, wdAlarmed, wdIdx, stat, phAlarmed]; + + // Learned weights (inference of an offline-trained model). + const dim: number = fullFeatureDim(); + const lw = LearnedWeights.fromParams( + dim, + new Float64Array(dim).fill(0.1), + 0.0, + new Float64Array(dim).fill(0.0), + new Float64Array(dim).fill(1.0), + ); + const p: number = lw.predict(new Float64Array(dim).fill(0.5)); + const w: Float64Array = lw.clockWeights(); + void [p, w]; +} diff --git a/npm/packages/emergent-time/README.md b/npm/packages/emergent-time/README.md new file mode 100644 index 000000000..919fb605f --- /dev/null +++ b/npm/packages/emergent-time/README.md @@ -0,0 +1,316 @@ +# @ruvector/emergent-time + +**Agentic Time** for the browser, the edge, and Node — a tiny WASM build of the +agentic-time layer of the [`emergent-time`](https://crates.io/crates/emergent-time) +Rust crate. + +Agentic time measures how much an AI agent has *changed internally*, not how many +seconds, steps, or tokens have elapsed. You feed it the six channel deltas of a +transition — belief, memory, retrieval, goal-graph, contradiction, plan — and it +returns: + +- an explainable **tick** (a post-floor internal-time increment, its class, a + human-readable reason, and the per-channel contributions), +- a cumulative **agentic time** reading, +- the **Agentic Time Index** (ATI = progress per unit of structural change), and +- a **7-state health** classification: `Healthy`, `Drifting`, `Stuck`, + `NeedsReplan`, `Contradicting`, `Collapsing`, `NeedsHumanReview`. + +It also ships the two fair change-point detectors the agentic clock is honestly +compared against (a windowed z-score and a Page–Hinkley test), so you can run the +comparison yourself. + +> An agent can run for 30 minutes and barely age; or hit one contradiction and +> age massively in a second. Wall-clock time tells you *when* something happened; +> agentic time tells you *how much the agent changed*. + +## Honest scope (read this) + +The agentic clock is a **diagnostic signal**, not a proven early-warning predictor. +On real recorded agent traces it does **not** establish an early-warning lead over +a fair cheap baseline (a windowed z-score on a single observable, or a +Page–Hinkley detector) — this is the same conclusion the Rust crate and ADR-251 +reach. What it gives you is an explainable, per-channel decomposition of internal +change plus a health classifier. Treat it as observability, not as a guarantee. + +## Install + +```bash +npm install @ruvector/emergent-time +``` + +- **Bundle:** ~55 KB WASM (size-optimized with `wasm-opt -Oz`; ~62 KB before opt) + + ~31 KB JS glue + ~16 KB `.d.ts`. Packed tarball ~40 KB. +- **Dependencies:** none. The WASM core is pure Rust with a `dlmalloc` allocator; + no runtime npm dependencies. +- **Target:** built with `wasm-bindgen --target web`. It loads in the browser + (via `fetch`), in Node (via `initSync` with the wasm bytes — see below), and in + any bundler that understands ESM + `.wasm`. + +## Quickstart (browser / bundler) + +In a browser or a bundler, the default export initializes from the bundled +`.wasm` URL: + +```js +import init, { AgenticClock, StateDelta, TickClassJs, AgentHealthJs } + from '@ruvector/emergent-time'; + +await init(); // fetches and instantiates the .wasm + +const clock = new AgenticClock(); + +// StateDelta(belief, memory, retrieval, goal, contradiction, plan, +// contradictionLevel, progress) +const tick = clock.tick(new StateDelta(0.3, 0.1, 0.4, 0.2, 0.3, 0.8, 0.6, 0.0)); + +console.log(tick.deltaTime); // post-floor internal-time increment +console.log(TickClassJs[tick.class]); // e.g. "Progress" +console.log(tick.reason); // "Progress: dominated by plan movement (...)" +console.log(clock.ati); // progress per unit structural change +console.log(AgentHealthJs[clock.health]); // e.g. "NeedsReplan" +``` + +## Quickstart (Node ESM) + +The `web` build does not auto-fetch in Node, so read the bytes and pass them to +`initSync`: + +```js +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import init, { initSync, AgenticClock, StateDelta, AgentHealthJs } + from '@ruvector/emergent-time'; + +const require = createRequire(import.meta.url); +const wasmPath = require.resolve('@ruvector/emergent-time/wasm'); +initSync({ module: await readFile(wasmPath) }); + +const clock = new AgenticClock(); +clock.tick(new StateDelta(0.3, 0.1, 0.4, 0.2, 0.3, 0.8, 0.6, 0.0)); +console.log(AgentHealthJs[clock.health]); +void init; // `init` is the browser entry point; unused in Node +``` + +## TypeScript usage (compiles against the shipped `.d.ts`) + +```ts +import { + AgenticClock, + StateDelta, + WindowedDeltaClock, + PageHinkleyDetector, + LearnedWeights, + TickClassJs, + AgentHealthJs, + fullFeatureDim, +} from '@ruvector/emergent-time'; + +// (after init / initSync — omitted here) +const clock = new AgenticClock(); +clock.setWindow(8); +clock.setNoiseFloor(1e-3); + +const delta = new StateDelta(0.3, 0.1, 0.4, 0.2, 0.3, 0.8, 0.6, 0.0); +const tick = clock.tick(delta); + +const dt: number = tick.deltaTime; +const cls: TickClassJs = tick.class; +const reason: string = tick.reason; +const ati: number = clock.ati; +const health: AgentHealthJs = clock.health; + +if (cls === TickClassJs.Collapse && health === AgentHealthJs.NeedsHumanReview) { + // escalate to a human +} + +// Detectors return a per-step statistic and latch an alarm. +const wd = new WindowedDeltaClock(8, 4.0, 1.0); // window, kSigma, stdFloor +const z: number = wd.push(2.5); +const fired: boolean = wd.alarmed; +const at: bigint = wd.alarmIndex; // -1n until it fires + +const ph = new PageHinkleyDetector(0.1, 1.0); // delta (tolerance), lambda (threshold) +const stat: number = ph.push(2.5); + +// Inference of an offline-trained logistic scorer over channel-movement features. +const dim: number = fullFeatureDim(); // 6 (full) or honestFeatureDim() => 5 +const model = LearnedWeights.fromParams( + dim, + new Float64Array(dim).fill(0.1), // coef + 0.0, // bias + new Float64Array(dim).fill(0.0), // feature means + new Float64Array(dim).fill(1.0), // feature stds +); +const p: number = model.predict(new Float64Array(dim).fill(0.5)); // [0, 1] +``` + +> The shipped `.d.ts` references `Symbol.dispose` and DOM/`WebAssembly` types. If +> you type-check it directly, use `"lib": ["ES2022", "DOM", "ESNext.Disposable"]` +> (or `"esnext"`) in your `tsconfig.json` — the standard libs for a `web`-target +> wasm-bindgen module. + +## API + +### `class AgenticClock` + +A stateful agentic-time clock. Construct it, feed transitions, read back time, +the ATI, and health. + +```ts +new AgenticClock(); +static withWeights( + belief: number, memory: number, retrieval: number, + goalGraph: number, contradiction: number, plan: number, +): AgenticClock; // custom channel weights + +setNoiseFloor(floor: number): void; // jitter suppression (default 1e-3) +setWindow(window: number): void; // rolling window for ATI/health (default 8) +setThresholds( // health-classifier thresholds + idle: number, healthyAti: number, driftingAti: number, + collapse: number, humanReview: number, +): void; + +tick(delta: StateDelta): Tick; // feed one transition, advance the clock +reset(): void; // zero running state, keep config + +readonly cumulativeTime: number; // Σ agentic time so far +readonly cumulativeProgress: number; // Σ progress so far +readonly ati: number; // progress / Δτ over the window (∞ if Δτ≈0, progressing) +readonly health: AgentHealthJs; // current 7-state verdict +``` + +Default channel weights: contradiction `1.5`, belief / goal-graph / plan `1.0`, +memory / retrieval `0.5` (contradictions age an agent the most). + +### `class StateDelta` + +The six per-transition channel deltas (already-computed scalar movements — pick +your own embeddings and distance metric on the JS side). + +```ts +new StateDelta( + belief: number, // L2 movement of the belief embedding (≥ 0) + memory: number, // L2 movement of working memory (≥ 0) + retrieval: number, // L2 movement of retrieved context (≥ 0) + goal: number, // |Δ goal-graph mass| + contradiction: number, // |Δ contradiction score| + plan: number, // L2 movement of the plan embedding (≥ 0) + contradictionLevel: number, // current absolute contradiction in [0, 1] + progress: number, // Δ task progress over this transition +); +``` + +`contradictionLevel` is the *current* contradiction (not a delta); it drives the +`Collapsing` / `NeedsHumanReview` health states. + +### `class Tick` + +An explainable tick. The per-channel fields are the **raw (pre-floor)** weighted +contributions; `deltaTime` is the **post-floor** increment +`max(0, Σ channels − noiseFloor)`. The identity `deltaTime === Σ channels` holds +only when `noiseFloor === 0`. + +```ts +readonly deltaTime: number; // post-floor internal-time increment +readonly class: TickClassJs; // Idle | Progress | Learning | Contradiction | Collapse +readonly reason: string; // human-readable audit string +readonly belief: number; // raw weighted belief contribution +readonly memory: number; +readonly retrieval: number; +readonly goalGraph: number; +readonly contradiction: number; +readonly plan: number; +``` + +### `enum TickClassJs` + +`Idle = 0`, `Progress = 1`, `Learning = 2`, `Contradiction = 3`, `Collapse = 4`. + +### `enum AgentHealthJs` + +`Healthy = 0`, `Drifting = 1`, `Stuck = 2`, `NeedsReplan = 3`, +`Contradicting = 4`, `Collapsing = 5`, `NeedsHumanReview = 6`. + +### `class WindowedDeltaClock` — fair baseline (rolling z-score) + +A windowed `mean + kσ` change-point detector on a single scalar observable. + +```ts +new WindowedDeltaClock(window: number, kSigma: number, stdFloor: number); +push(value: number): number; // returns the rolling z-score +readonly alarmed: boolean; // latched true on first alarm +readonly alarmIndex: bigint; // 0-based index of first alarm, or -1n +reset(): void; +``` + +`stdFloor` is a variance floor: set it near your stationary noise scale so a +near-constant stream does not trip a spurious infinite z-score. + +### `class PageHinkleyDetector` — fair baseline (adaptive CUSUM) + +A Page–Hinkley test whose reference is a *running* mean, so a noisy early phase +does not permanently raise the bar. + +```ts +new PageHinkleyDetector(delta: number, lambda: number); // upward (increase) form +static downward(delta: number, lambda: number): PageHinkleyDetector; +push(value: number): number; // returns the current PH statistic +readonly alarmed: boolean; +readonly alarmIndex: bigint; +reset(): void; +``` + +`delta` is the tolerance (deviations below it are treated as normal jitter); +`lambda` is the alarm threshold (larger ⇒ fewer false alarms, later detection). + +### `class LearnedWeights` — offline-trained scorer (inference only) + +A fitted logistic-regression scorer over the channel-movement features. Train it +with the Rust crate; load the parameters here to score in the browser. + +```ts +static fromParams( + dim: number, + coef: Float64Array, bias: number, + mean: Float64Array, std: Float64Array, +): LearnedWeights; +predict(features: Float64Array): number; // failure-approach probability in [0, 1] +clockWeights(): Float64Array; // non-negative weights for withWeights(...) +readonly dim: number; +``` + +### Free functions + +```ts +function fullFeatureDim(): number; // 6 +function honestFeatureDim(): number; // 5 (contradiction-free "honest" set) +function setPanicHook(): void; // route panics to console (no-op cost otherwise) +function version(): string; // package version +``` + +## The physics core lives in the Rust crate + +The parent [`emergent-time`](https://crates.io/crates/emergent-time) crate also +implements four physics formalisms of emergent/relational time — Wheeler–DeWitt +timeless constraint, Page–Wootters relational clocks, entropic time, Connes–Rovelli +thermal time — plus Structural Proper Time. Those deal in dense complex matrices +that do not serialize cheaply across the JS boundary, so they are intentionally +**not** wrapped here (it would bloat the WASM without a clean API). Use the Rust +crate directly if you need them. + +## Building from source + +Requires a Rust toolchain with `wasm32-unknown-unknown` std, `wasm-bindgen`, and +`wasm-opt` (binaryen) on `PATH`: + +```bash +npm run build # cargo build → wasm-bindgen --target web → wasm-opt -Oz +``` + +The build script enables the bulk-memory and nontrapping-float-to-int opcodes the +toolchain emits (a plain `wasm-opt -O` rejects them). + +## License + +MIT diff --git a/npm/packages/emergent-time/package.json b/npm/packages/emergent-time/package.json new file mode 100644 index 000000000..87417dd38 --- /dev/null +++ b/npm/packages/emergent-time/package.json @@ -0,0 +1,50 @@ +{ + "name": "@ruvector/emergent-time", + "version": "0.1.0", + "description": "Agentic Time: measure an AI agent's internal time and 7-state health from per-channel state deltas, in the browser, on the edge, or in Node — via a tiny WASM build of the emergent-time Rust crate.", + "type": "module", + "main": "pkg/emergent_time_wasm.js", + "module": "pkg/emergent_time_wasm.js", + "types": "pkg/emergent_time_wasm.d.ts", + "exports": { + ".": { + "types": "./pkg/emergent_time_wasm.d.ts", + "import": "./pkg/emergent_time_wasm.js", + "default": "./pkg/emergent_time_wasm.js" + }, + "./wasm": "./pkg/emergent_time_wasm_bg.wasm" + }, + "files": [ + "pkg/emergent_time_wasm.js", + "pkg/emergent_time_wasm.d.ts", + "pkg/emergent_time_wasm_bg.wasm", + "pkg/emergent_time_wasm_bg.wasm.d.ts", + "README.md" + ], + "sideEffects": false, + "scripts": { + "build": "bash ./scripts/build.sh", + "test": "node ../../../crates/emergent-time-wasm/tests/smoke.mjs" + }, + "keywords": [ + "agentic-time", + "agents", + "ai-agents", + "observability", + "change-point", + "wasm", + "webassembly", + "emergent-time" + ], + "license": "MIT", + "author": "Ruvector Team", + "repository": { + "type": "git", + "url": "https://github.com/ruvnet/ruvector" + }, + "homepage": "https://github.com/ruvnet/ruvector", + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/npm/packages/emergent-time/pkg/emergent_time_wasm.d.ts b/npm/packages/emergent-time/pkg/emergent_time_wasm.d.ts new file mode 100644 index 000000000..732d79341 --- /dev/null +++ b/npm/packages/emergent-time/pkg/emergent_time_wasm.d.ts @@ -0,0 +1,421 @@ +/* tslint:disable */ +/* eslint-disable */ + +/** + * The 7-state agent health verdict (mirrors the Rust `AgentHealth`). + */ +export enum AgentHealthJs { + /** + * Progress is keeping pace with internal change. + */ + Healthy = 0, + /** + * Moving, but inefficiently (low progress per unit change). + */ + Drifting = 1, + /** + * Neither changing nor progressing. + */ + Stuck = 2, + /** + * Lots of internal churn, no progress — replan. + */ + NeedsReplan = 3, + /** + * Losing ground (progress going backwards). + */ + Contradicting = 4, + /** + * Contradiction is high and rising. + */ + Collapsing = 5, + /** + * Contradiction is critical — escalate to a human. + */ + NeedsHumanReview = 6, +} + +/** + * A stateful agentic-time clock. Construct it (optionally with custom channel + * weights / health thresholds), feed transitions via [`AgenticClock::tick`], and + * read back cumulative agentic time, the ATI, and the health classification. + * + * The clock keeps a small amount of running state: cumulative agentic time, + * cumulative progress, and a rolling window of the last `window` ticks / + * progress used for the ATI and health classification. + */ +export class AgenticClock { + free(): void; + [Symbol.dispose](): void; + /** + * Construct a clock with the default channel weights (contradiction 1.5, + * belief / goal / plan 1.0, memory / retrieval 0.5), default health + * thresholds, a noise floor of `1e-3`, and a window of 8 ticks. + */ + constructor(); + /** + * Reset all running state (cumulative time / progress, window) to zero, + * keeping the configured weights, thresholds, noise floor, and window length. + */ + reset(): void; + /** + * Override the noise floor (jitter suppression). Ticks whose raw channel sum + * is below this floor report `deltaTime == 0`. + */ + setNoiseFloor(floor: number): void; + /** + * Override the health-classifier thresholds. Order: + * `idle, healthyAti, driftingAti, collapse, humanReview`. + */ + setThresholds(idle: number, healthy_ati: number, drifting_ati: number, collapse: number, human_review: number): void; + /** + * Override the rolling window length used for the ATI and health (default 8). + */ + setWindow(window: number): void; + /** + * Feed one transition's channel deltas and return the explainable [`Tick`]. + * Advances the clock's cumulative agentic time and progress, and updates the + * rolling window used by [`AgenticClock::ati`] and + * [`AgenticClock::health`]. + */ + tick(delta: StateDelta): Tick; + /** + * Construct a clock with custom channel weights. Order: + * `belief, memory, retrieval, goalGraph, contradiction, plan`. + */ + static withWeights(belief: number, memory: number, retrieval: number, goal_graph: number, contradiction: number, plan: number): AgenticClock; + /** + * The Agentic Time Index over the current window: progress per unit of + * structural change. High ATI ⇒ learning and moving; near-zero ⇒ spinning; + * `Infinity` ⇒ progressing with no internal change. + */ + readonly ati: number; + /** + * Cumulative progress accrued across all ticks so far. + */ + readonly cumulativeProgress: number; + /** + * Cumulative agentic time accrued across all ticks so far. + */ + readonly cumulativeTime: number; + /** + * The current health verdict, classified over the rolling window of agentic + * time, progress, and the latest contradiction level. + */ + readonly health: AgentHealthJs; +} + +/** + * A fitted logistic-regression scorer over the channel-movement features (the + * crate's `LearnedWeights`). Reconstruct it from persisted parameters and score + * raw feature vectors to get a failure-approach probability in `[0, 1]`. + * + * The training harness lives in the Rust crate; this binding exposes only the + * cheap inference path (`predict`) so a model trained offline can run in the + * browser without bundling the trainer. + */ +export class LearnedWeights { + private constructor(); + free(): void; + [Symbol.dispose](): void; + /** + * Non-negative clock weights derived from the learned coefficients (the + * positive part), suitable for `AgenticClock.withWeights(...)`. + */ + clockWeights(): Float64Array; + /** + * Reconstruct a scorer from persisted parameters. `coef`, `mean`, and `std` + * must each have length `dim` (the feature count: 6 for the full channel set, + * 5 for the contradiction-free "honest" set). + */ + static fromParams(dim: number, coef: Float64Array, bias: number, mean: Float64Array, std: Float64Array): LearnedWeights; + /** + * Predicted failure-approach probability in `[0, 1]` for a raw feature vector + * (the per-channel movements in feature order). `features.length` must equal + * the model's `dim`. + */ + predict(features: Float64Array): number; + /** + * The model's feature dimensionality. + */ + readonly dim: number; +} + +/** + * A **Page–Hinkley** adaptive change-point detector: a CUSUM test whose + * reference is a *running* mean (so a noisy early phase does not permanently + * raise the bar). Push a scalar each step, get back the current PH statistic and + * an alarm flag. The adaptive counterpart of [`WindowedDeltaClock`]; both are the + * fair competitors to the agentic clock. + */ +export class PageHinkleyDetector { + free(): void; + [Symbol.dispose](): void; + /** + * Construct a **downward** (decrease-detecting) Page–Hinkley detector. + */ + static downward(delta: number, lambda: number): PageHinkleyDetector; + /** + * Construct an **upward** (increase-detecting) Page–Hinkley detector with + * tolerance `delta` (deviations below this are treated as normal jitter) and + * alarm threshold `lambda` (larger ⇒ fewer false alarms, later detection). + */ + constructor(delta: number, lambda: number); + /** + * Push the next scalar and return the current Page–Hinkley statistic (the + * rise above the running minimum for the upward form, or the drop below the + * running maximum for the downward form). Updates [`alarmed`](Self::alarmed) + * when the statistic exceeds `lambda`. + */ + push(value: number): number; + /** + * Reset the detector's running statistics and alarm latch. + */ + reset(): void; + /** + * The 0-based index at which the detector first fired, or `-1` if it has not. + */ + readonly alarmIndex: bigint; + /** + * Whether the detector has fired (latched true on first alarm). + */ + readonly alarmed: boolean; +} + +/** + * The six per-transition channel deltas fed to [`AgenticClock::tick`]. + * + * Each field is the **already-computed scalar movement** of that channel over a + * transition (e.g. the L2 distance between successive belief embeddings, or the + * absolute change in a contradiction score). Keeping the JS boundary scalar — six + * numbers, not six embedding vectors — keeps the wasm tiny and lets the caller + * pick whatever distance metric and embedding model they like on the JS side. + * + * `contradictionLevel` is the *current absolute* contradiction in `[0, 1]` (not a + * delta); it drives the collapse / human-review health states. + */ +export class StateDelta { + free(): void; + [Symbol.dispose](): void; + /** + * Construct a transition's channel deltas. + * + * * `belief`, `memory`, `retrieval`, `plan` — non-negative scalar movements + * (typically L2 distance between successive embeddings). + * * `goal` — absolute change in goal-graph mass (e.g. open-subgoal count). + * * `contradiction` — absolute change in the contradiction score. + * * `contradictionLevel` — current absolute contradiction in `[0, 1]`. + * * `progress` — absolute change in task progress over this transition + * (used for the ATI and health classification; pass `0` if unknown). + */ + constructor(belief: number, memory: number, retrieval: number, goal: number, contradiction: number, plan: number, contradiction_level: number, progress: number); +} + +/** + * An explainable agentic-time tick: the post-floor internal-time increment, its + * class, a human-readable reason, and the raw (pre-floor) per-channel weighted + * contributions. See the Rust crate's `Tick` docs for the post-floor / + * pre-floor contract: `deltaTime == Σ channels` only when `noiseFloor == 0`. + */ +export class Tick { + private constructor(); + free(): void; + [Symbol.dispose](): void; + /** + * Raw (pre-floor) weighted belief contribution. + */ + readonly belief: number; + /** + * The tick class (Idle / Progress / Learning / Contradiction / Collapse). + */ + readonly class: TickClassJs; + /** + * Raw (pre-floor) weighted contradiction contribution. + */ + readonly contradiction: number; + /** + * Post-floor internal-time magnitude: `max(0, Σ channels − noiseFloor)`. + */ + readonly deltaTime: number; + /** + * Raw (pre-floor) weighted goal-graph contribution. + */ + readonly goalGraph: number; + /** + * Raw (pre-floor) weighted memory contribution. + */ + readonly memory: number; + /** + * Raw (pre-floor) weighted plan contribution. + */ + readonly plan: number; + /** + * A human-readable audit string explaining which channel dominated. + */ + readonly reason: string; + /** + * Raw (pre-floor) weighted retrieval contribution. + */ + readonly retrieval: number; +} + +/** + * One agentic-time class (mirrors the Rust `TickClass`). Exposed as a small + * enum so the JS side can `switch` on it without string parsing. + */ +export enum TickClassJs { + /** + * Below the noise floor — no meaningful change. + */ + Idle = 0, + /** + * Belief / plan / goal moved forward. + */ + Progress = 1, + /** + * New information arrived (retrieval / memory moved). + */ + Learning = 2, + /** + * Contradiction rose. + */ + Contradiction = 3, + /** + * Contradiction is high — failure regime. + */ + Collapse = 4, +} + +/** + * A **windowed z-score** change-point detector (rolling `mean + kσ`): push a + * scalar each step, get back the z-score and an alarm flag. This is the *fair + * baseline* the agentic clock is honestly compared against — a cheap one-signal + * detector a practitioner would actually deploy. + */ +export class WindowedDeltaClock { + free(): void; + [Symbol.dispose](): void; + /** + * Construct a detector with a trailing `window`, a `kSigma` alarm multiplier + * (e.g. 4.0), and a `stdFloor` variance floor that prevents a near-constant + * stream from producing a spurious infinite z-score. + */ + constructor(window: number, k_sigma: number, std_floor: number); + /** + * Push the next scalar observable and return its rolling z-score (deviation + * from the trailing-window mean over the floored window std). Updates + * [`alarmed`](Self::alarmed) when the z-score exceeds `kSigma`. + */ + push(value: number): number; + /** + * Reset the detector's history and alarm latch. + */ + reset(): void; + /** + * The 0-based index at which the detector first fired, or `-1` if it has not. + */ + readonly alarmIndex: bigint; + /** + * Whether the detector has fired (latched true on first alarm). + */ + readonly alarmed: boolean; +} + +/** + * The number of channel features for the full set (6) — for sizing + * [`LearnedWeights`] parameter arrays. + */ +export function fullFeatureDim(): number; + +/** + * The number of channel features for the contradiction-free "honest" set (5). + */ +export function honestFeatureDim(): number; + +/** + * Optional: route Rust panics to the JS console with a readable message. + * Call once after instantiation. No-op cost if never called. + */ +export function setPanicHook(): void; + +/** + * The package version (compile-time constant from Cargo). + */ +export function version(): string; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly __wbg_statedelta_free: (a: number, b: number) => void; + readonly statedelta_new: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number; + readonly __wbg_tick_free: (a: number, b: number) => void; + readonly tick_deltaTime: (a: number) => number; + readonly tick_class: (a: number) => number; + readonly tick_reason: (a: number, b: number) => void; + readonly tick_belief: (a: number) => number; + readonly tick_memory: (a: number) => number; + readonly tick_retrieval: (a: number) => number; + readonly tick_goalGraph: (a: number) => number; + readonly tick_contradiction: (a: number) => number; + readonly tick_plan: (a: number) => number; + readonly __wbg_agenticclock_free: (a: number, b: number) => void; + readonly agenticclock_new: () => number; + readonly agenticclock_withWeights: (a: number, b: number, c: number, d: number, e: number, f: number) => number; + readonly agenticclock_setNoiseFloor: (a: number, b: number) => void; + readonly agenticclock_setWindow: (a: number, b: number) => void; + readonly agenticclock_setThresholds: (a: number, b: number, c: number, d: number, e: number, f: number) => void; + readonly agenticclock_tick: (a: number, b: number) => number; + readonly agenticclock_cumulativeTime: (a: number) => number; + readonly agenticclock_cumulativeProgress: (a: number) => number; + readonly agenticclock_ati: (a: number) => number; + readonly agenticclock_health: (a: number) => number; + readonly agenticclock_reset: (a: number) => void; + readonly __wbg_windoweddeltaclock_free: (a: number, b: number) => void; + readonly windoweddeltaclock_new: (a: number, b: number, c: number) => number; + readonly windoweddeltaclock_push: (a: number, b: number) => number; + readonly windoweddeltaclock_alarmed: (a: number) => number; + readonly windoweddeltaclock_alarmIndex: (a: number) => bigint; + readonly windoweddeltaclock_reset: (a: number) => void; + readonly __wbg_pagehinkleydetector_free: (a: number, b: number) => void; + readonly pagehinkleydetector_new: (a: number, b: number) => number; + readonly pagehinkleydetector_downward: (a: number, b: number) => number; + readonly pagehinkleydetector_push: (a: number, b: number) => number; + readonly pagehinkleydetector_alarmed: (a: number) => number; + readonly pagehinkleydetector_alarmIndex: (a: number) => bigint; + readonly pagehinkleydetector_reset: (a: number) => void; + readonly __wbg_learnedweights_free: (a: number, b: number) => void; + readonly learnedweights_fromParams: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void; + readonly learnedweights_predict: (a: number, b: number, c: number, d: number) => void; + readonly learnedweights_dim: (a: number) => number; + readonly learnedweights_clockWeights: (a: number, b: number) => void; + readonly version: (a: number) => void; + readonly fullFeatureDim: () => number; + readonly honestFeatureDim: () => number; + readonly setPanicHook: () => void; + readonly __wbindgen_add_to_stack_pointer: (a: number) => number; + readonly __wbindgen_export: (a: number, b: number, c: number) => void; + readonly __wbindgen_export2: (a: number, b: number) => number; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/npm/packages/emergent-time/pkg/emergent_time_wasm.js b/npm/packages/emergent-time/pkg/emergent_time_wasm.js new file mode 100644 index 000000000..e4463cd20 --- /dev/null +++ b/npm/packages/emergent-time/pkg/emergent_time_wasm.js @@ -0,0 +1,895 @@ +/* @ts-self-types="./emergent_time_wasm.d.ts" */ + +/** + * The 7-state agent health verdict (mirrors the Rust `AgentHealth`). + * @enum {0 | 1 | 2 | 3 | 4 | 5 | 6} + */ +export const AgentHealthJs = Object.freeze({ + /** + * Progress is keeping pace with internal change. + */ + Healthy: 0, "0": "Healthy", + /** + * Moving, but inefficiently (low progress per unit change). + */ + Drifting: 1, "1": "Drifting", + /** + * Neither changing nor progressing. + */ + Stuck: 2, "2": "Stuck", + /** + * Lots of internal churn, no progress — replan. + */ + NeedsReplan: 3, "3": "NeedsReplan", + /** + * Losing ground (progress going backwards). + */ + Contradicting: 4, "4": "Contradicting", + /** + * Contradiction is high and rising. + */ + Collapsing: 5, "5": "Collapsing", + /** + * Contradiction is critical — escalate to a human. + */ + NeedsHumanReview: 6, "6": "NeedsHumanReview", +}); + +/** + * A stateful agentic-time clock. Construct it (optionally with custom channel + * weights / health thresholds), feed transitions via [`AgenticClock::tick`], and + * read back cumulative agentic time, the ATI, and the health classification. + * + * The clock keeps a small amount of running state: cumulative agentic time, + * cumulative progress, and a rolling window of the last `window` ticks / + * progress used for the ATI and health classification. + */ +export class AgenticClock { + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(AgenticClock.prototype); + obj.__wbg_ptr = ptr; + AgenticClockFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + AgenticClockFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_agenticclock_free(ptr, 0); + } + /** + * The Agentic Time Index over the current window: progress per unit of + * structural change. High ATI ⇒ learning and moving; near-zero ⇒ spinning; + * `Infinity` ⇒ progressing with no internal change. + * @returns {number} + */ + get ati() { + const ret = wasm.agenticclock_ati(this.__wbg_ptr); + return ret; + } + /** + * Cumulative progress accrued across all ticks so far. + * @returns {number} + */ + get cumulativeProgress() { + const ret = wasm.agenticclock_cumulativeProgress(this.__wbg_ptr); + return ret; + } + /** + * Cumulative agentic time accrued across all ticks so far. + * @returns {number} + */ + get cumulativeTime() { + const ret = wasm.agenticclock_cumulativeTime(this.__wbg_ptr); + return ret; + } + /** + * The current health verdict, classified over the rolling window of agentic + * time, progress, and the latest contradiction level. + * @returns {AgentHealthJs} + */ + get health() { + const ret = wasm.agenticclock_health(this.__wbg_ptr); + return ret; + } + /** + * Construct a clock with the default channel weights (contradiction 1.5, + * belief / goal / plan 1.0, memory / retrieval 0.5), default health + * thresholds, a noise floor of `1e-3`, and a window of 8 ticks. + */ + constructor() { + const ret = wasm.agenticclock_new(); + this.__wbg_ptr = ret >>> 0; + AgenticClockFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Reset all running state (cumulative time / progress, window) to zero, + * keeping the configured weights, thresholds, noise floor, and window length. + */ + reset() { + wasm.agenticclock_reset(this.__wbg_ptr); + } + /** + * Override the noise floor (jitter suppression). Ticks whose raw channel sum + * is below this floor report `deltaTime == 0`. + * @param {number} floor + */ + setNoiseFloor(floor) { + wasm.agenticclock_setNoiseFloor(this.__wbg_ptr, floor); + } + /** + * Override the health-classifier thresholds. Order: + * `idle, healthyAti, driftingAti, collapse, humanReview`. + * @param {number} idle + * @param {number} healthy_ati + * @param {number} drifting_ati + * @param {number} collapse + * @param {number} human_review + */ + setThresholds(idle, healthy_ati, drifting_ati, collapse, human_review) { + wasm.agenticclock_setThresholds(this.__wbg_ptr, idle, healthy_ati, drifting_ati, collapse, human_review); + } + /** + * Override the rolling window length used for the ATI and health (default 8). + * @param {number} window + */ + setWindow(window) { + wasm.agenticclock_setWindow(this.__wbg_ptr, window); + } + /** + * Feed one transition's channel deltas and return the explainable [`Tick`]. + * Advances the clock's cumulative agentic time and progress, and updates the + * rolling window used by [`AgenticClock::ati`] and + * [`AgenticClock::health`]. + * @param {StateDelta} delta + * @returns {Tick} + */ + tick(delta) { + _assertClass(delta, StateDelta); + const ret = wasm.agenticclock_tick(this.__wbg_ptr, delta.__wbg_ptr); + return Tick.__wrap(ret); + } + /** + * Construct a clock with custom channel weights. Order: + * `belief, memory, retrieval, goalGraph, contradiction, plan`. + * @param {number} belief + * @param {number} memory + * @param {number} retrieval + * @param {number} goal_graph + * @param {number} contradiction + * @param {number} plan + * @returns {AgenticClock} + */ + static withWeights(belief, memory, retrieval, goal_graph, contradiction, plan) { + const ret = wasm.agenticclock_withWeights(belief, memory, retrieval, goal_graph, contradiction, plan); + return AgenticClock.__wrap(ret); + } +} +if (Symbol.dispose) AgenticClock.prototype[Symbol.dispose] = AgenticClock.prototype.free; + +/** + * A fitted logistic-regression scorer over the channel-movement features (the + * crate's `LearnedWeights`). Reconstruct it from persisted parameters and score + * raw feature vectors to get a failure-approach probability in `[0, 1]`. + * + * The training harness lives in the Rust crate; this binding exposes only the + * cheap inference path (`predict`) so a model trained offline can run in the + * browser without bundling the trainer. + */ +export class LearnedWeights { + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(LearnedWeights.prototype); + obj.__wbg_ptr = ptr; + LearnedWeightsFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + LearnedWeightsFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_learnedweights_free(ptr, 0); + } + /** + * Non-negative clock weights derived from the learned coefficients (the + * positive part), suitable for `AgenticClock.withWeights(...)`. + * @returns {Float64Array} + */ + clockWeights() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.learnedweights_clockWeights(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var v1 = getArrayF64FromWasm0(r0, r1).slice(); + wasm.__wbindgen_export(r0, r1 * 8, 8); + return v1; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * The model's feature dimensionality. + * @returns {number} + */ + get dim() { + const ret = wasm.learnedweights_dim(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Reconstruct a scorer from persisted parameters. `coef`, `mean`, and `std` + * must each have length `dim` (the feature count: 6 for the full channel set, + * 5 for the contradiction-free "honest" set). + * @param {number} dim + * @param {Float64Array} coef + * @param {number} bias + * @param {Float64Array} mean + * @param {Float64Array} std + * @returns {LearnedWeights} + */ + static fromParams(dim, coef, bias, mean, std) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArrayF64ToWasm0(coef, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArrayF64ToWasm0(mean, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passArrayF64ToWasm0(std, wasm.__wbindgen_export2); + const len2 = WASM_VECTOR_LEN; + wasm.learnedweights_fromParams(retptr, dim, ptr0, len0, bias, ptr1, len1, ptr2, len2); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return LearnedWeights.__wrap(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Predicted failure-approach probability in `[0, 1]` for a raw feature vector + * (the per-channel movements in feature order). `features.length` must equal + * the model's `dim`. + * @param {Float64Array} features + * @returns {number} + */ + predict(features) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArrayF64ToWasm0(features, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + wasm.learnedweights_predict(retptr, this.__wbg_ptr, ptr0, len0); + var r0 = getDataViewMemory0().getFloat64(retptr + 8 * 0, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + return r0; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } +} +if (Symbol.dispose) LearnedWeights.prototype[Symbol.dispose] = LearnedWeights.prototype.free; + +/** + * A **Page–Hinkley** adaptive change-point detector: a CUSUM test whose + * reference is a *running* mean (so a noisy early phase does not permanently + * raise the bar). Push a scalar each step, get back the current PH statistic and + * an alarm flag. The adaptive counterpart of [`WindowedDeltaClock`]; both are the + * fair competitors to the agentic clock. + */ +export class PageHinkleyDetector { + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(PageHinkleyDetector.prototype); + obj.__wbg_ptr = ptr; + PageHinkleyDetectorFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + PageHinkleyDetectorFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_pagehinkleydetector_free(ptr, 0); + } + /** + * The 0-based index at which the detector first fired, or `-1` if it has not. + * @returns {bigint} + */ + get alarmIndex() { + const ret = wasm.pagehinkleydetector_alarmIndex(this.__wbg_ptr); + return ret; + } + /** + * Whether the detector has fired (latched true on first alarm). + * @returns {boolean} + */ + get alarmed() { + const ret = wasm.pagehinkleydetector_alarmed(this.__wbg_ptr); + return ret !== 0; + } + /** + * Construct a **downward** (decrease-detecting) Page–Hinkley detector. + * @param {number} delta + * @param {number} lambda + * @returns {PageHinkleyDetector} + */ + static downward(delta, lambda) { + const ret = wasm.pagehinkleydetector_downward(delta, lambda); + return PageHinkleyDetector.__wrap(ret); + } + /** + * Construct an **upward** (increase-detecting) Page–Hinkley detector with + * tolerance `delta` (deviations below this are treated as normal jitter) and + * alarm threshold `lambda` (larger ⇒ fewer false alarms, later detection). + * @param {number} delta + * @param {number} lambda + */ + constructor(delta, lambda) { + const ret = wasm.pagehinkleydetector_new(delta, lambda); + this.__wbg_ptr = ret >>> 0; + PageHinkleyDetectorFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Push the next scalar and return the current Page–Hinkley statistic (the + * rise above the running minimum for the upward form, or the drop below the + * running maximum for the downward form). Updates [`alarmed`](Self::alarmed) + * when the statistic exceeds `lambda`. + * @param {number} value + * @returns {number} + */ + push(value) { + const ret = wasm.pagehinkleydetector_push(this.__wbg_ptr, value); + return ret; + } + /** + * Reset the detector's running statistics and alarm latch. + */ + reset() { + wasm.pagehinkleydetector_reset(this.__wbg_ptr); + } +} +if (Symbol.dispose) PageHinkleyDetector.prototype[Symbol.dispose] = PageHinkleyDetector.prototype.free; + +/** + * The six per-transition channel deltas fed to [`AgenticClock::tick`]. + * + * Each field is the **already-computed scalar movement** of that channel over a + * transition (e.g. the L2 distance between successive belief embeddings, or the + * absolute change in a contradiction score). Keeping the JS boundary scalar — six + * numbers, not six embedding vectors — keeps the wasm tiny and lets the caller + * pick whatever distance metric and embedding model they like on the JS side. + * + * `contradictionLevel` is the *current absolute* contradiction in `[0, 1]` (not a + * delta); it drives the collapse / human-review health states. + */ +export class StateDelta { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + StateDeltaFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_statedelta_free(ptr, 0); + } + /** + * Construct a transition's channel deltas. + * + * * `belief`, `memory`, `retrieval`, `plan` — non-negative scalar movements + * (typically L2 distance between successive embeddings). + * * `goal` — absolute change in goal-graph mass (e.g. open-subgoal count). + * * `contradiction` — absolute change in the contradiction score. + * * `contradictionLevel` — current absolute contradiction in `[0, 1]`. + * * `progress` — absolute change in task progress over this transition + * (used for the ATI and health classification; pass `0` if unknown). + * @param {number} belief + * @param {number} memory + * @param {number} retrieval + * @param {number} goal + * @param {number} contradiction + * @param {number} plan + * @param {number} contradiction_level + * @param {number} progress + */ + constructor(belief, memory, retrieval, goal, contradiction, plan, contradiction_level, progress) { + const ret = wasm.statedelta_new(belief, memory, retrieval, goal, contradiction, plan, contradiction_level, progress); + this.__wbg_ptr = ret >>> 0; + StateDeltaFinalization.register(this, this.__wbg_ptr, this); + return this; + } +} +if (Symbol.dispose) StateDelta.prototype[Symbol.dispose] = StateDelta.prototype.free; + +/** + * An explainable agentic-time tick: the post-floor internal-time increment, its + * class, a human-readable reason, and the raw (pre-floor) per-channel weighted + * contributions. See the Rust crate's `Tick` docs for the post-floor / + * pre-floor contract: `deltaTime == Σ channels` only when `noiseFloor == 0`. + */ +export class Tick { + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(Tick.prototype); + obj.__wbg_ptr = ptr; + TickFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + TickFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_tick_free(ptr, 0); + } + /** + * Raw (pre-floor) weighted belief contribution. + * @returns {number} + */ + get belief() { + const ret = wasm.tick_belief(this.__wbg_ptr); + return ret; + } + /** + * The tick class (Idle / Progress / Learning / Contradiction / Collapse). + * @returns {TickClassJs} + */ + get class() { + const ret = wasm.tick_class(this.__wbg_ptr); + return ret; + } + /** + * Raw (pre-floor) weighted contradiction contribution. + * @returns {number} + */ + get contradiction() { + const ret = wasm.tick_contradiction(this.__wbg_ptr); + return ret; + } + /** + * Post-floor internal-time magnitude: `max(0, Σ channels − noiseFloor)`. + * @returns {number} + */ + get deltaTime() { + const ret = wasm.tick_deltaTime(this.__wbg_ptr); + return ret; + } + /** + * Raw (pre-floor) weighted goal-graph contribution. + * @returns {number} + */ + get goalGraph() { + const ret = wasm.tick_goalGraph(this.__wbg_ptr); + return ret; + } + /** + * Raw (pre-floor) weighted memory contribution. + * @returns {number} + */ + get memory() { + const ret = wasm.tick_memory(this.__wbg_ptr); + return ret; + } + /** + * Raw (pre-floor) weighted plan contribution. + * @returns {number} + */ + get plan() { + const ret = wasm.tick_plan(this.__wbg_ptr); + return ret; + } + /** + * A human-readable audit string explaining which channel dominated. + * @returns {string} + */ + get reason() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.tick_reason(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export(deferred1_0, deferred1_1, 1); + } + } + /** + * Raw (pre-floor) weighted retrieval contribution. + * @returns {number} + */ + get retrieval() { + const ret = wasm.tick_retrieval(this.__wbg_ptr); + return ret; + } +} +if (Symbol.dispose) Tick.prototype[Symbol.dispose] = Tick.prototype.free; + +/** + * One agentic-time class (mirrors the Rust `TickClass`). Exposed as a small + * enum so the JS side can `switch` on it without string parsing. + * @enum {0 | 1 | 2 | 3 | 4} + */ +export const TickClassJs = Object.freeze({ + /** + * Below the noise floor — no meaningful change. + */ + Idle: 0, "0": "Idle", + /** + * Belief / plan / goal moved forward. + */ + Progress: 1, "1": "Progress", + /** + * New information arrived (retrieval / memory moved). + */ + Learning: 2, "2": "Learning", + /** + * Contradiction rose. + */ + Contradiction: 3, "3": "Contradiction", + /** + * Contradiction is high — failure regime. + */ + Collapse: 4, "4": "Collapse", +}); + +/** + * A **windowed z-score** change-point detector (rolling `mean + kσ`): push a + * scalar each step, get back the z-score and an alarm flag. This is the *fair + * baseline* the agentic clock is honestly compared against — a cheap one-signal + * detector a practitioner would actually deploy. + */ +export class WindowedDeltaClock { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + WindowedDeltaClockFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_windoweddeltaclock_free(ptr, 0); + } + /** + * The 0-based index at which the detector first fired, or `-1` if it has not. + * @returns {bigint} + */ + get alarmIndex() { + const ret = wasm.windoweddeltaclock_alarmIndex(this.__wbg_ptr); + return ret; + } + /** + * Whether the detector has fired (latched true on first alarm). + * @returns {boolean} + */ + get alarmed() { + const ret = wasm.windoweddeltaclock_alarmed(this.__wbg_ptr); + return ret !== 0; + } + /** + * Construct a detector with a trailing `window`, a `kSigma` alarm multiplier + * (e.g. 4.0), and a `stdFloor` variance floor that prevents a near-constant + * stream from producing a spurious infinite z-score. + * @param {number} window + * @param {number} k_sigma + * @param {number} std_floor + */ + constructor(window, k_sigma, std_floor) { + const ret = wasm.windoweddeltaclock_new(window, k_sigma, std_floor); + this.__wbg_ptr = ret >>> 0; + WindowedDeltaClockFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Push the next scalar observable and return its rolling z-score (deviation + * from the trailing-window mean over the floored window std). Updates + * [`alarmed`](Self::alarmed) when the z-score exceeds `kSigma`. + * @param {number} value + * @returns {number} + */ + push(value) { + const ret = wasm.windoweddeltaclock_push(this.__wbg_ptr, value); + return ret; + } + /** + * Reset the detector's history and alarm latch. + */ + reset() { + wasm.windoweddeltaclock_reset(this.__wbg_ptr); + } +} +if (Symbol.dispose) WindowedDeltaClock.prototype[Symbol.dispose] = WindowedDeltaClock.prototype.free; + +/** + * The number of channel features for the full set (6) — for sizing + * [`LearnedWeights`] parameter arrays. + * @returns {number} + */ +export function fullFeatureDim() { + const ret = wasm.fullFeatureDim(); + return ret >>> 0; +} + +/** + * The number of channel features for the contradiction-free "honest" set (5). + * @returns {number} + */ +export function honestFeatureDim() { + const ret = wasm.honestFeatureDim(); + return ret >>> 0; +} + +/** + * Optional: route Rust panics to the JS console with a readable message. + * Call once after instantiation. No-op cost if never called. + */ +export function setPanicHook() { + wasm.setPanicHook(); +} + +/** + * The package version (compile-time constant from Cargo). + * @returns {string} + */ +export function version() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.version(retptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export(deferred1_0, deferred1_1, 1); + } +} +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg_Error_960c155d3d49e4c2: function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }, + __wbg___wbindgen_throw_6b64449b9b9ed33c: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + }; + return { + __proto__: null, + "./emergent_time_wasm_bg.js": import0, + }; +} + +const AgenticClockFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_agenticclock_free(ptr >>> 0, 1)); +const LearnedWeightsFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_learnedweights_free(ptr >>> 0, 1)); +const PageHinkleyDetectorFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_pagehinkleydetector_free(ptr >>> 0, 1)); +const StateDeltaFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_statedelta_free(ptr >>> 0, 1)); +const TickFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_tick_free(ptr >>> 0, 1)); +const WindowedDeltaClockFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_windoweddeltaclock_free(ptr >>> 0, 1)); + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function _assertClass(instance, klass) { + if (!(instance instanceof klass)) { + throw new Error(`expected instance of ${klass.name}`); + } +} + +function dropObject(idx) { + if (idx < 1028) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function getArrayF64FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getFloat64ArrayMemory0().subarray(ptr / 8, ptr / 8 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +let cachedFloat64ArrayMemory0 = null; +function getFloat64ArrayMemory0() { + if (cachedFloat64ArrayMemory0 === null || cachedFloat64ArrayMemory0.byteLength === 0) { + cachedFloat64ArrayMemory0 = new Float64Array(wasm.memory.buffer); + } + return cachedFloat64ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getObject(idx) { return heap[idx]; } + +let heap = new Array(1024).fill(undefined); +heap.push(undefined, null, true, false); + +let heap_next = heap.length; + +function passArrayF64ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 8, 8) >>> 0; + getFloat64ArrayMemory0().set(arg, ptr / 8); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasm; +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedFloat64ArrayMemory0 = null; + cachedUint8ArrayMemory0 = null; + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('emergent_time_wasm_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/npm/packages/emergent-time/pkg/emergent_time_wasm_bg.wasm b/npm/packages/emergent-time/pkg/emergent_time_wasm_bg.wasm new file mode 100644 index 000000000..abdb1f47b Binary files /dev/null and b/npm/packages/emergent-time/pkg/emergent_time_wasm_bg.wasm differ diff --git a/npm/packages/emergent-time/pkg/emergent_time_wasm_bg.wasm.d.ts b/npm/packages/emergent-time/pkg/emergent_time_wasm_bg.wasm.d.ts new file mode 100644 index 000000000..1dde80b71 --- /dev/null +++ b/npm/packages/emergent-time/pkg/emergent_time_wasm_bg.wasm.d.ts @@ -0,0 +1,52 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const __wbg_statedelta_free: (a: number, b: number) => void; +export const statedelta_new: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number; +export const __wbg_tick_free: (a: number, b: number) => void; +export const tick_deltaTime: (a: number) => number; +export const tick_class: (a: number) => number; +export const tick_reason: (a: number, b: number) => void; +export const tick_belief: (a: number) => number; +export const tick_memory: (a: number) => number; +export const tick_retrieval: (a: number) => number; +export const tick_goalGraph: (a: number) => number; +export const tick_contradiction: (a: number) => number; +export const tick_plan: (a: number) => number; +export const __wbg_agenticclock_free: (a: number, b: number) => void; +export const agenticclock_new: () => number; +export const agenticclock_withWeights: (a: number, b: number, c: number, d: number, e: number, f: number) => number; +export const agenticclock_setNoiseFloor: (a: number, b: number) => void; +export const agenticclock_setWindow: (a: number, b: number) => void; +export const agenticclock_setThresholds: (a: number, b: number, c: number, d: number, e: number, f: number) => void; +export const agenticclock_tick: (a: number, b: number) => number; +export const agenticclock_cumulativeTime: (a: number) => number; +export const agenticclock_cumulativeProgress: (a: number) => number; +export const agenticclock_ati: (a: number) => number; +export const agenticclock_health: (a: number) => number; +export const agenticclock_reset: (a: number) => void; +export const __wbg_windoweddeltaclock_free: (a: number, b: number) => void; +export const windoweddeltaclock_new: (a: number, b: number, c: number) => number; +export const windoweddeltaclock_push: (a: number, b: number) => number; +export const windoweddeltaclock_alarmed: (a: number) => number; +export const windoweddeltaclock_alarmIndex: (a: number) => bigint; +export const windoweddeltaclock_reset: (a: number) => void; +export const __wbg_pagehinkleydetector_free: (a: number, b: number) => void; +export const pagehinkleydetector_new: (a: number, b: number) => number; +export const pagehinkleydetector_downward: (a: number, b: number) => number; +export const pagehinkleydetector_push: (a: number, b: number) => number; +export const pagehinkleydetector_alarmed: (a: number) => number; +export const pagehinkleydetector_alarmIndex: (a: number) => bigint; +export const pagehinkleydetector_reset: (a: number) => void; +export const __wbg_learnedweights_free: (a: number, b: number) => void; +export const learnedweights_fromParams: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void; +export const learnedweights_predict: (a: number, b: number, c: number, d: number) => void; +export const learnedweights_dim: (a: number) => number; +export const learnedweights_clockWeights: (a: number, b: number) => void; +export const version: (a: number) => void; +export const fullFeatureDim: () => number; +export const honestFeatureDim: () => number; +export const setPanicHook: () => void; +export const __wbindgen_add_to_stack_pointer: (a: number) => number; +export const __wbindgen_export: (a: number, b: number, c: number) => void; +export const __wbindgen_export2: (a: number, b: number) => number; diff --git a/npm/packages/emergent-time/scripts/build.sh b/npm/packages/emergent-time/scripts/build.sh new file mode 100644 index 000000000..08e830bf8 --- /dev/null +++ b/npm/packages/emergent-time/scripts/build.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Build pipeline for @ruvector/emergent-time. +# +# wasm-pack's bundled `wasm-opt -O` rejects the toolchain's default bulk-memory / +# nontrapping-float-to-int opcodes, so we drive the three stages manually: +# 1. cargo build (1.89 toolchain — the one with wasm32-unknown-unknown std) +# 2. wasm-bindgen (--target web) +# 3. wasm-opt -Oz (with the feature flags the toolchain emits) +# then copy the optimized artifacts into pkg/. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PKG_DIR="$HERE/../pkg" +CRATE_DIR="$HERE/../../../../crates/emergent-time-wasm" +CRATE_DIR="$(cd "$CRATE_DIR" && pwd)" + +# The 1.89 toolchain ships wasm32-unknown-unknown std; the default 1.91 does not. +export RUSTUP_TOOLCHAIN="${RUSTUP_TOOLCHAIN:-1.89-x86_64-pc-windows-msvc}" + +WASM_FEATURES="--enable-bulk-memory --enable-bulk-memory-opt \ + --enable-nontrapping-float-to-int --enable-sign-ext --enable-mutable-globals \ + --enable-multivalue --enable-reference-types" + +echo "[1/3] cargo build (RUSTUP_TOOLCHAIN=$RUSTUP_TOOLCHAIN)" +cargo build --release --target wasm32-unknown-unknown \ + --manifest-path "$CRATE_DIR/Cargo.toml" + +RAW_WASM="$CRATE_DIR/target/wasm32-unknown-unknown/release/emergent_time_wasm.wasm" +# Some setups place target/ at the crate; others at the workspace. Resolve it. +if [ ! -f "$RAW_WASM" ]; then + RAW_WASM="$(find "$CRATE_DIR" -path '*/wasm32-unknown-unknown/release/emergent_time_wasm.wasm' | head -1)" +fi + +echo "[2/3] wasm-bindgen --target web" +mkdir -p "$PKG_DIR" +wasm-bindgen --target web --out-dir "$PKG_DIR" "$RAW_WASM" + +echo "[3/3] wasm-opt -Oz" +RAW_BYTES=$(stat -c%s "$PKG_DIR/emergent_time_wasm_bg.wasm") +# shellcheck disable=SC2086 +wasm-opt -Oz $WASM_FEATURES \ + "$PKG_DIR/emergent_time_wasm_bg.wasm" \ + -o "$PKG_DIR/emergent_time_wasm_bg.opt.wasm" +mv "$PKG_DIR/emergent_time_wasm_bg.opt.wasm" "$PKG_DIR/emergent_time_wasm_bg.wasm" +OPT_BYTES=$(stat -c%s "$PKG_DIR/emergent_time_wasm_bg.wasm") + +echo "done: raw=${RAW_BYTES}B opt=${OPT_BYTES}B"