ruvector/npm/packages/emergent-time
rUv 90a1dc12e1
feat(emergent-time): @ruvector/emergent-time WASM package for Agentic Time (#566)
Wrap the agentic-time layer of the dependency-free `emergent-time` crate in a
tiny wasm-bindgen surface for the browser, edge, and Node.

- crates/emergent-time-wasm: standalone cdylib (workspace-excluded so it carries
  its own opt-level="z" / lto / strip / panic=abort release profile and dlmalloc
  global allocator, mirroring crates/rvf/rvf-wasm). Hand-rolled getters, no serde,
  to keep the wasm tiny.
- SDK surface: AgenticClock (tick → explainable Tick{class,reason,deltaTime,
  per-channel}; cumulativeTime, ATI, 7-state health), StateDelta, Tick,
  TickClassJs, AgentHealthJs, WindowedDeltaClock + PageHinkleyDetector
  change-point detectors, LearnedWeights inference, version().
- Physics core (Wheeler-DeWitt / Page-Wootters / entropic / thermal / Structural
  Proper Time) deliberately not wrapped: dense matrices don't serialize cheaply
  over the JS boundary and would bloat the wasm. Documented in the README.
- npm/packages/emergent-time: package.json (@ruvector/emergent-time@0.1.0, ESM,
  main/module/types → pkg, files include pkg + README, publishConfig public),
  detailed README, build.sh pipeline (cargo @1.89 → wasm-bindgen --target web →
  wasm-opt -Oz with bulk-memory/nontrapping-float-to-int enabled), and the built
  pkg/ (wasm + JS glue + .d.ts).

Validation: wasm raw 62475B / opt 55009B (wasm-tools VALID); Node ESM smoke test
passes end-to-end (AgenticClock Healthy→Drifting→NeedsReplan→Collapsing→
NeedsHumanReview, cumulativeTime 19.36, both detectors fire at the planted jump);
tsc --noEmit --strict on a usage example against the shipped .d.ts exits 0;
npm pack --dry-run lists README.md + .wasm + .js + .d.ts.

Honest scope (mirrors ADR-251): the agentic clock is a diagnostic signal; it does
not establish an early-warning lead over a fair baseline on real traces. Both
fair baselines (windowed z-score, Page-Hinkley) are exported.

Co-authored-by: ruv <ruvnet@users.noreply.github.com>
2026-06-22 09:52:00 -04:00
..
pkg feat(emergent-time): @ruvector/emergent-time WASM package for Agentic Time (#566) 2026-06-22 09:52:00 -04:00
scripts feat(emergent-time): @ruvector/emergent-time WASM package for Agentic Time (#566) 2026-06-22 09:52:00 -04:00
package.json feat(emergent-time): @ruvector/emergent-time WASM package for Agentic Time (#566) 2026-06-22 09:52:00 -04:00
README.md feat(emergent-time): @ruvector/emergent-time WASM package for Agentic Time (#566) 2026-06-22 09:52:00 -04:00

@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 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 PageHinkley 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 PageHinkley 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

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:

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:

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)

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.

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).

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.

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.

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 PageHinkley test whose reference is a running mean, so a noisy early phase does not permanently raise the bar.

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.

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

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 crate also implements four physics formalisms of emergent/relational time — WheelerDeWitt timeless constraint, PageWootters relational clocks, entropic time, ConnesRovelli 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:

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