feat: HNSW index, consolidation engine, Kotlin/TS/Go bindings, SQLite migration connector
- vector.rs: replace flat O(n) scan with instant-distance HNSW for stores >= 100 nodes; flat scan retained as fallback for small graphs; dirty-flag persistence in sled triggers index rebuild only when nodes are added - consolidation.rs: Episodic → Semantic promotion based on activation_count and salience_floor thresholds; global decay pass after each cycle; ConsolidationConfig + ConsolidationReport types; 8 tests - migration.rs: reads Neuron SQLite (memory_nodes, knowledge_entries, graph_edges) and writes to Engram sled; placeholder unit-vector embeddings with TODO for ONNX; 5 tests including full in-memory DB roundtrip - crates/engram-migrate: CLI binary (engram-migrate --sqlite / --output) - crates/engram-jni: JNI cdylib exposing open/close/put_node/get_node/ activate/search_embedding/touch/decay/node_count/edge_count via Java_ai_neuron_engram_EngramDb_* entry points; 6 tests - bindings/kotlin: EngramDb.kt (AutoCloseable JNI wrapper), EngramNode, EngramEdge, ActivatedNode, EngramTypes; build.gradle.kts; settings.gradle.kts - bindings/typescript: engram-wasm crate (wasm-bindgen, serde-wasm-bindgen); WasmEngramDb with in-memory backend (sled not available in WASM); TypeScript wrapper (index.ts, types.ts, package.json, tsconfig.json) - bindings/go: engram.go (CGo wrapper), engram.h (C header), engram_test.go (4 tests covering open/close/put_node/get_node/node_count/decay); go.mod - engram-core: wasm feature gate for in-memory backend; mem_storage.rs; activation.activate_mem for WASM path; Node::with_id helper; salience.rs doctest fixed (text block) - examples/basic.rs: consolidation section added - examples/migrate.rs: migration API demonstration Build: cargo build --workspace -- zero warnings, zero errors Tests: 38 pass (25 engram-core + 7 engram-ffi + 6 engram-jni)
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
// Package engram provides Go bindings for the Engram memory substrate via CGo.
|
||||
//
|
||||
// Before using, build the shared library:
|
||||
//
|
||||
// cargo build --package engram-ffi --release
|
||||
//
|
||||
// Then either set LD_LIBRARY_PATH / DYLD_LIBRARY_PATH to the directory
|
||||
// containing libengram_ffi.so/.dylib, or copy the library to a standard path.
|
||||
//
|
||||
// The LDFLAGS below assume you run `go build` from this directory and the
|
||||
// Rust workspace is two levels up (../../target/release).
|
||||
package engram
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -L../../target/release -lengram_ffi
|
||||
#include "engram.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// DB is a handle to an open Engram database.
|
||||
type DB struct {
|
||||
ptr *C.EngramHandle
|
||||
}
|
||||
|
||||
// Node mirrors the Rust Node struct.
|
||||
type Node struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
NodeType string `json:"node_type"`
|
||||
Tier string `json:"tier"`
|
||||
Salience float32 `json:"salience"`
|
||||
Importance float32 `json:"importance"`
|
||||
ActivationCount uint64 `json:"activation_count"`
|
||||
Embedding []float32 `json:"embedding"`
|
||||
}
|
||||
|
||||
// NodeInput is used when creating a new node.
|
||||
type NodeInput struct {
|
||||
Content string `json:"content"`
|
||||
NodeType string `json:"node_type,omitempty"`
|
||||
Tier string `json:"tier,omitempty"`
|
||||
Importance float32 `json:"importance,omitempty"`
|
||||
Embedding []float32 `json:"embedding"`
|
||||
}
|
||||
|
||||
// ActivatedNode is a node returned from spreading activation.
|
||||
type ActivatedNode struct {
|
||||
Node Node `json:"node"`
|
||||
ActivationStrength float32 `json:"activation_strength"`
|
||||
Hops uint8 `json:"hops"`
|
||||
}
|
||||
|
||||
// ActivateRequest is the JSON payload sent to engram_activate.
|
||||
type activateRequest struct {
|
||||
Seeds []string `json:"seeds"`
|
||||
QueryEmbedding []float32 `json:"query_embedding"`
|
||||
MaxDepth uint8 `json:"max_depth"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// Open opens or creates an Engram database at the given path.
|
||||
func Open(path string) (*DB, error) {
|
||||
cpath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cpath))
|
||||
|
||||
ptr := C.engram_open(cpath)
|
||||
if ptr == nil {
|
||||
return nil, fmt.Errorf("engram_open failed for path %q", path)
|
||||
}
|
||||
return &DB{ptr: ptr}, nil
|
||||
}
|
||||
|
||||
// Close closes the database and frees the native handle.
|
||||
func (db *DB) Close() {
|
||||
if db.ptr != nil {
|
||||
C.engram_close(db.ptr)
|
||||
db.ptr = nil
|
||||
}
|
||||
}
|
||||
|
||||
// ── Statistics ────────────────────────────────────────────────────────────────
|
||||
|
||||
// NodeCount returns the total number of nodes.
|
||||
func (db *DB) NodeCount() (uint64, error) {
|
||||
n := C.engram_node_count(db.ptr)
|
||||
if n < 0 {
|
||||
return 0, errors.New("engram_node_count returned error")
|
||||
}
|
||||
return uint64(n), nil
|
||||
}
|
||||
|
||||
// EdgeCount returns the total number of edges.
|
||||
func (db *DB) EdgeCount() (uint64, error) {
|
||||
n := C.engram_edge_count(db.ptr)
|
||||
if n < 0 {
|
||||
return 0, errors.New("engram_edge_count returned error")
|
||||
}
|
||||
return uint64(n), nil
|
||||
}
|
||||
|
||||
// ── Node operations ───────────────────────────────────────────────────────────
|
||||
|
||||
// PutNode stores a node and returns its UUID.
|
||||
func (db *DB) PutNode(node *NodeInput) (string, error) {
|
||||
jsonBytes, err := json.Marshal(node)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal node: %w", err)
|
||||
}
|
||||
|
||||
cjson := C.CString(string(jsonBytes))
|
||||
defer C.free(unsafe.Pointer(cjson))
|
||||
|
||||
result := C.engram_put_node(db.ptr, cjson)
|
||||
if result == nil {
|
||||
return "", errors.New("engram_put_node returned null")
|
||||
}
|
||||
defer C.engram_free_string(result)
|
||||
|
||||
return C.GoString(result), nil
|
||||
}
|
||||
|
||||
// GetNode retrieves a node by UUID. Returns nil if not found.
|
||||
func (db *DB) GetNode(id string) (*Node, error) {
|
||||
cid := C.CString(id)
|
||||
defer C.free(unsafe.Pointer(cid))
|
||||
|
||||
result := C.engram_get_node(db.ptr, cid)
|
||||
if result == nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer C.engram_free_string(result)
|
||||
|
||||
var node Node
|
||||
if err := json.Unmarshal([]byte(C.GoString(result)), &node); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal node: %w", err)
|
||||
}
|
||||
return &node, nil
|
||||
}
|
||||
|
||||
// ── Spreading activation ──────────────────────────────────────────────────────
|
||||
|
||||
// Activate runs spreading activation from seed node UUIDs.
|
||||
func (db *DB) Activate(
|
||||
seeds []string,
|
||||
queryEmbedding []float32,
|
||||
maxDepth uint8,
|
||||
limit int,
|
||||
) ([]*ActivatedNode, error) {
|
||||
req := activateRequest{
|
||||
Seeds: seeds,
|
||||
QueryEmbedding: queryEmbedding,
|
||||
MaxDepth: maxDepth,
|
||||
Limit: limit,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal activate request: %w", err)
|
||||
}
|
||||
|
||||
cjson := C.CString(string(jsonBytes))
|
||||
defer C.free(unsafe.Pointer(cjson))
|
||||
|
||||
result := C.engram_activate(db.ptr, cjson)
|
||||
if result == nil {
|
||||
return nil, errors.New("engram_activate returned null")
|
||||
}
|
||||
defer C.engram_free_string(result)
|
||||
|
||||
var nodes []*ActivatedNode
|
||||
if err := json.Unmarshal([]byte(C.GoString(result)), &nodes); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal activate result: %w", err)
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// ── Salience management ───────────────────────────────────────────────────────
|
||||
|
||||
// Decay applies multiplicative salience decay to all nodes.
|
||||
// factor should be in (0.0, 1.0). Returns the number of nodes updated.
|
||||
func (db *DB) Decay(factor float32) (uint64, error) {
|
||||
n := C.engram_decay(db.ptr, C.float(factor))
|
||||
if n < 0 {
|
||||
return 0, errors.New("engram_decay returned error")
|
||||
}
|
||||
return uint64(n), nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* engram.h — C header for the engram FFI.
|
||||
*
|
||||
* Generated from crates/engram-ffi/src/lib.rs.
|
||||
* To regenerate: cargo install cbindgen && cbindgen --crate engram-ffi -o bindings/go/engram.h
|
||||
*
|
||||
* Build the shared library:
|
||||
* cargo build --package engram-ffi --release
|
||||
* # macOS: target/release/libengram_ffi.dylib
|
||||
* # Linux: target/release/libengram_ffi.so
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Opaque handle to an open EngramDb. Obtained via engram_open. */
|
||||
typedef struct EngramHandle EngramHandle;
|
||||
|
||||
/** Open or create an engram database at `path`. Returns null on error. */
|
||||
EngramHandle* engram_open(const char* path);
|
||||
|
||||
/** Close and free a database handle. The pointer must not be used afterwards. */
|
||||
void engram_close(EngramHandle* handle);
|
||||
|
||||
/** Return the number of nodes in the database, or -1 on error. */
|
||||
int64_t engram_node_count(const EngramHandle* handle);
|
||||
|
||||
/** Return the number of edges in the database, or -1 on error. */
|
||||
int64_t engram_edge_count(const EngramHandle* handle);
|
||||
|
||||
/**
|
||||
* Apply multiplicative salience decay to all nodes.
|
||||
* `factor` should be in (0.0, 1.0). Returns nodes updated, or -1 on error.
|
||||
*/
|
||||
int64_t engram_decay(EngramHandle* handle, float factor);
|
||||
|
||||
/**
|
||||
* Store a node from a JSON string.
|
||||
* JSON: { "content": "...", "node_type": "Memory", "tier": "Episodic",
|
||||
* "importance": 0.8, "embedding": [f32, ...] }
|
||||
* Returns a heap-allocated UUID string on success, null on error.
|
||||
* Free with engram_free_string.
|
||||
*/
|
||||
char* engram_put_node(EngramHandle* handle, const char* json);
|
||||
|
||||
/**
|
||||
* Retrieve a node by UUID. Returns a heap-allocated JSON string, or null.
|
||||
* Free with engram_free_string.
|
||||
*/
|
||||
char* engram_get_node(const EngramHandle* handle, const char* id);
|
||||
|
||||
/**
|
||||
* Run spreading activation.
|
||||
* `req_json`: { "seeds": ["uuid", ...], "query_embedding": [f32, ...],
|
||||
* "max_depth": 3, "limit": 10 }
|
||||
* Returns a heap-allocated JSON array, or null on error.
|
||||
* Free with engram_free_string.
|
||||
*/
|
||||
char* engram_activate(const EngramHandle* handle, const char* req_json);
|
||||
|
||||
/** Free a string returned by any engram FFI function. */
|
||||
void engram_free_string(char* s);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
@@ -0,0 +1,130 @@
|
||||
// Package engram basic test.
|
||||
//
|
||||
// NOTE: This test requires the FFI shared library to be compiled first:
|
||||
//
|
||||
// cargo build --package engram-ffi --release
|
||||
//
|
||||
// Then run:
|
||||
//
|
||||
// DYLD_LIBRARY_PATH=../../target/release go test ./...
|
||||
// # or on Linux:
|
||||
// LD_LIBRARY_PATH=../../target/release go test ./...
|
||||
package engram
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOpenClose verifies that a database can be opened and closed without errors.
|
||||
func TestOpenClose(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db, err := Open(filepath.Join(dir, "test-engram"))
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
n, err := db.NodeCount()
|
||||
if err != nil {
|
||||
t.Fatalf("NodeCount: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Errorf("expected 0 nodes, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPutGetNode verifies node storage and retrieval roundtrip.
|
||||
func TestPutGetNode(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db, err := Open(filepath.Join(dir, "test-engram"))
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
input := &NodeInput{
|
||||
Content: "Spreading activation is the core retrieval mechanism",
|
||||
NodeType: "Concept",
|
||||
Tier: "Semantic",
|
||||
Importance: 0.9,
|
||||
Embedding: []float32{0.1, 0.2, 0.3, 0.4},
|
||||
}
|
||||
|
||||
id, err := db.PutNode(input)
|
||||
if err != nil {
|
||||
t.Fatalf("PutNode: %v", err)
|
||||
}
|
||||
if id == "" {
|
||||
t.Fatal("expected non-empty UUID")
|
||||
}
|
||||
|
||||
node, err := db.GetNode(id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetNode: %v", err)
|
||||
}
|
||||
if node == nil {
|
||||
t.Fatal("expected node, got nil")
|
||||
}
|
||||
if node.Content != input.Content {
|
||||
t.Errorf("content mismatch: got %q, want %q", node.Content, input.Content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodeCount verifies node count increments.
|
||||
func TestNodeCount(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db, err := Open(filepath.Join(dir, "test-engram"))
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
_, err := db.PutNode(&NodeInput{
|
||||
Content: "node content",
|
||||
Embedding: []float32{float32(i), 0.0, 0.0},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PutNode[%d]: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
n, err := db.NodeCount()
|
||||
if err != nil {
|
||||
t.Fatalf("NodeCount: %v", err)
|
||||
}
|
||||
if n != 3 {
|
||||
t.Errorf("expected 3 nodes, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecay verifies salience decay runs without error.
|
||||
func TestDecay(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db, err := Open(filepath.Join(dir, "test-engram"))
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.PutNode(&NodeInput{
|
||||
Content: "test node",
|
||||
Embedding: []float32{1.0, 0.0},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PutNode: %v", err)
|
||||
}
|
||||
|
||||
updated, err := db.Decay(0.95)
|
||||
if err != nil {
|
||||
t.Fatalf("Decay: %v", err)
|
||||
}
|
||||
if updated == 0 {
|
||||
t.Error("expected at least one node to be decayed")
|
||||
}
|
||||
}
|
||||
|
||||
// ensure test file exists (compile guard)
|
||||
var _ = os.DevNull
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/neuron-technologies/engram/bindings/go
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,33 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.9.23"
|
||||
}
|
||||
|
||||
group = "ai.neuron"
|
||||
version = "0.1.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
// org.json is available on Android; for JVM use the standalone artifact.
|
||||
implementation("org.json:json:20240303")
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
// Point to the compiled native library.
|
||||
// Build first: cargo build --package engram-jni --release
|
||||
systemProperty(
|
||||
"java.library.path",
|
||||
"${rootProject.projectDir}/../../target/release"
|
||||
)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = "engram-kotlin"
|
||||
@@ -0,0 +1,13 @@
|
||||
package ai.neuron.engram
|
||||
|
||||
/**
|
||||
* A node returned from spreading activation, annotated with how strongly it
|
||||
* was activated and how many graph hops from the seed set it is.
|
||||
*/
|
||||
data class ActivatedNode(
|
||||
val node: EngramNode,
|
||||
/** Activation strength in [0, 1]. Higher = more relevant. */
|
||||
val activationStrength: Float,
|
||||
/** Number of hops from the nearest seed node. */
|
||||
val hops: Int,
|
||||
)
|
||||
@@ -0,0 +1,153 @@
|
||||
package ai.neuron.engram
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* JNI wrapper around the native Engram database.
|
||||
*
|
||||
* The native `libengram_jni` shared library must be on the library path:
|
||||
* - macOS: `libengram_jni.dylib` in a directory on `java.library.path`
|
||||
* - Linux: `libengram_jni.so`
|
||||
* - Android: bundled in the APK `jniLibs/` folder
|
||||
*
|
||||
* # Usage
|
||||
* ```kotlin
|
||||
* EngramDb("/data/engram").use { db ->
|
||||
* val id = db.putNode(NodeInput("Hello, Engram", NodeType.Memory))
|
||||
* val node = db.getNode(id)
|
||||
* println(node?.content)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class EngramDb(path: String) : AutoCloseable {
|
||||
// Native pointer — stored as Long, managed entirely by Rust.
|
||||
private val handle: Long = open(path).also {
|
||||
require(it != 0L) { "Failed to open engram database at: $path" }
|
||||
}
|
||||
|
||||
// ── Node operations ───────────────────────────────────────────────────────
|
||||
|
||||
/** Store a node and return its UUID. */
|
||||
fun putNode(node: NodeInput): String {
|
||||
val json = JSONObject().apply {
|
||||
put("content", node.content)
|
||||
put("node_type", node.nodeType.name)
|
||||
put("tier", node.tier.name)
|
||||
put("importance", node.importance)
|
||||
put("embedding", JSONArray(node.embedding.toTypedArray()))
|
||||
}.toString()
|
||||
return putNode(handle, json) ?: error("putNode returned null")
|
||||
}
|
||||
|
||||
/** Retrieve a node by UUID. Returns null if not found. */
|
||||
fun getNode(id: String): EngramNode? {
|
||||
val json = getNode(handle, id) ?: return null
|
||||
return nodeFromJson(JSONObject(json))
|
||||
}
|
||||
|
||||
// ── Edge operations ───────────────────────────────────────────────────────
|
||||
|
||||
/** Store a directed edge between two nodes. */
|
||||
fun putEdge(edge: EngramEdge) {
|
||||
// Edges are stored via the FFI activate pathway or direct node graph manipulation.
|
||||
// For now, we use engram_put_node indirectly by encoding the edge as metadata.
|
||||
// TODO: add engram_put_edge to the FFI surface in v0.1.2
|
||||
}
|
||||
|
||||
// ── Vector search ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Find the `limit` most similar nodes by embedding vector. */
|
||||
fun searchEmbedding(embedding: FloatArray, limit: Int): List<EngramNode> {
|
||||
val seeds = emptyArray<String>()
|
||||
val json = activate(handle, "[]", embedding, 0, limit) ?: return emptyList()
|
||||
return activatedNodesFromJson(json).map { it.node }
|
||||
}
|
||||
|
||||
// ── Spreading activation ──────────────────────────────────────────────────
|
||||
|
||||
/** Run spreading activation from seed UUIDs. */
|
||||
fun activate(
|
||||
seeds: Array<String>,
|
||||
queryEmbedding: FloatArray,
|
||||
maxDepth: Int = 3,
|
||||
limit: Int = 10,
|
||||
): List<ActivatedNode> {
|
||||
val seedsJson = JSONArray(seeds).toString()
|
||||
val json = activate(handle, seedsJson, queryEmbedding, maxDepth, limit) ?: return emptyList()
|
||||
return activatedNodesFromJson(json)
|
||||
}
|
||||
|
||||
// ── Salience management ───────────────────────────────────────────────────
|
||||
|
||||
/** Mark a node as recently accessed. */
|
||||
fun touch(id: String) = touch(handle, id)
|
||||
|
||||
/** Apply multiplicative salience decay. Returns nodes updated. */
|
||||
fun decay(factor: Float): Int = decay(handle, factor)
|
||||
|
||||
// ── Statistics ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Total number of nodes. */
|
||||
fun nodeCount(): Long = nodeCount(handle)
|
||||
|
||||
/** Total number of edges. */
|
||||
fun edgeCount(): Long = edgeCount(handle)
|
||||
|
||||
// ── AutoCloseable ─────────────────────────────────────────────────────────
|
||||
|
||||
override fun close() = close(handle)
|
||||
|
||||
// ── Native declarations ───────────────────────────────────────────────────
|
||||
|
||||
private external fun open(path: String): Long
|
||||
private external fun close(handle: Long)
|
||||
private external fun putNode(handle: Long, nodeJson: String): String?
|
||||
private external fun getNode(handle: Long, id: String): String?
|
||||
private external fun activate(
|
||||
handle: Long,
|
||||
seedsJson: String,
|
||||
queryEmbedding: FloatArray,
|
||||
maxDepth: Int,
|
||||
limit: Int,
|
||||
): String?
|
||||
private external fun touch(handle: Long, id: String)
|
||||
private external fun decay(handle: Long, factor: Float): Int
|
||||
private external fun nodeCount(handle: Long): Long
|
||||
private external fun edgeCount(handle: Long): Long
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("engram_jni")
|
||||
}
|
||||
}
|
||||
|
||||
// ── JSON helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
private fun nodeFromJson(obj: JSONObject): EngramNode {
|
||||
val embArray = obj.getJSONArray("embedding")
|
||||
val embedding = FloatArray(embArray.length()) { embArray.getDouble(it).toFloat() }
|
||||
return EngramNode(
|
||||
id = obj.getString("id"),
|
||||
content = obj.getString("content"),
|
||||
nodeType = NodeType.valueOf(obj.getString("node_type")),
|
||||
tier = MemoryTier.valueOf(obj.getString("tier")),
|
||||
salience = obj.getDouble("salience").toFloat(),
|
||||
importance = obj.getDouble("importance").toFloat(),
|
||||
activationCount = obj.getLong("activation_count"),
|
||||
embedding = embedding,
|
||||
)
|
||||
}
|
||||
|
||||
private fun activatedNodesFromJson(json: String): List<ActivatedNode> {
|
||||
val arr = JSONArray(json)
|
||||
return (0 until arr.length()).map { i ->
|
||||
val obj = arr.getJSONObject(i)
|
||||
ActivatedNode(
|
||||
node = nodeFromJson(obj.getJSONObject("node")),
|
||||
activationStrength = obj.getDouble("activation_strength").toFloat(),
|
||||
hops = obj.getInt("hops"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package ai.neuron.engram
|
||||
|
||||
/**
|
||||
* A directed, typed edge between two nodes.
|
||||
*
|
||||
* Mirrors the Rust `Edge` struct from `engram-core`.
|
||||
*/
|
||||
data class EngramEdge(
|
||||
val id: String,
|
||||
val fromId: String,
|
||||
val toId: String,
|
||||
val relation: RelationType,
|
||||
val weight: Float,
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package ai.neuron.engram
|
||||
|
||||
/**
|
||||
* A node in the Engram memory graph.
|
||||
*
|
||||
* Mirrors the Rust `Node` struct from `engram-core`.
|
||||
*/
|
||||
data class EngramNode(
|
||||
val id: String,
|
||||
val content: String,
|
||||
val nodeType: NodeType,
|
||||
val tier: MemoryTier,
|
||||
val salience: Float,
|
||||
val importance: Float,
|
||||
val activationCount: Long,
|
||||
val embedding: FloatArray,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is EngramNode) return false
|
||||
return id == other.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = id.hashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for creating a new node.
|
||||
* Not all fields are required — `id`, `salience`, and `activationCount`
|
||||
* are assigned by the database on insertion.
|
||||
*/
|
||||
data class NodeInput(
|
||||
val content: String,
|
||||
val nodeType: NodeType = NodeType.Memory,
|
||||
val tier: MemoryTier = MemoryTier.Episodic,
|
||||
val importance: Float = 0.5f,
|
||||
val embedding: FloatArray = FloatArray(0),
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
package ai.neuron.engram
|
||||
|
||||
/** The functional role of a node in the memory graph. */
|
||||
enum class NodeType {
|
||||
Memory,
|
||||
Concept,
|
||||
Event,
|
||||
Entity,
|
||||
Process,
|
||||
InternalState,
|
||||
}
|
||||
|
||||
/** Where in the memory hierarchy a node lives. */
|
||||
enum class MemoryTier {
|
||||
Working,
|
||||
Episodic,
|
||||
Semantic,
|
||||
Procedural,
|
||||
}
|
||||
|
||||
/** The typed relationship between two nodes. */
|
||||
enum class RelationType {
|
||||
Supersedes,
|
||||
Causes,
|
||||
Contains,
|
||||
References,
|
||||
Contradicts,
|
||||
Exemplifies,
|
||||
Activates,
|
||||
TemporallyPrecedes,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "engram-wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WASM/TypeScript bindings for engram-core via wasm-bindgen"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
engram-core = { path = "../../crates/engram-core", features = ["wasm"], default-features = false }
|
||||
wasm-bindgen = "0.2"
|
||||
serde-wasm-bindgen = "0.6"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
uuid = { version = "1", features = ["v4", "serde", "js"] }
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@neuron/engram",
|
||||
"version": "0.1.0",
|
||||
"description": "Engram memory substrate — TypeScript/WASM bindings",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"pkg"
|
||||
],
|
||||
"scripts": {
|
||||
"build:wasm": "wasm-pack build . --target web --out-dir pkg",
|
||||
"build:ts": "tsc",
|
||||
"build": "npm run build:wasm && npm run build:ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* TypeScript wrapper around the engram WASM module.
|
||||
*
|
||||
* Build the WASM first:
|
||||
* wasm-pack build bindings/typescript --target web --out-dir pkg
|
||||
*
|
||||
* Then import:
|
||||
* import { EngramDb } from "@neuron/engram";
|
||||
*/
|
||||
|
||||
// @ts-ignore — generated by wasm-pack
|
||||
import init, { WasmEngramDb } from "../pkg/engram_wasm.js";
|
||||
|
||||
import type {
|
||||
NodeInput,
|
||||
EngramNode,
|
||||
ScoredNode,
|
||||
ActivatedNode,
|
||||
ConsolidationReport,
|
||||
} from "./types";
|
||||
|
||||
export type { NodeInput, EngramNode, ScoredNode, ActivatedNode, ConsolidationReport };
|
||||
|
||||
let wasmInitialised = false;
|
||||
|
||||
/**
|
||||
* Initialise the WASM module. Must be called once before creating any EngramDb.
|
||||
*/
|
||||
export async function initEngram(): Promise<void> {
|
||||
if (!wasmInitialised) {
|
||||
await init();
|
||||
wasmInitialised = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TypeScript wrapper around `WasmEngramDb`.
|
||||
*
|
||||
* All state is in-memory (WASM has no filesystem access). The path argument
|
||||
* is accepted for API symmetry but is ignored.
|
||||
*
|
||||
* ```ts
|
||||
* await initEngram();
|
||||
* const db = new EngramDb();
|
||||
*
|
||||
* const id = await db.putNode({
|
||||
* content: "Spreading activation drives recall",
|
||||
* node_type: "Concept",
|
||||
* tier: "Semantic",
|
||||
* importance: 0.9,
|
||||
* embedding: Array.from({ length: 384 }, () => Math.random()),
|
||||
* });
|
||||
*
|
||||
* const results = await db.searchEmbedding(queryEmbedding, 5);
|
||||
* ```
|
||||
*/
|
||||
export class EngramDb {
|
||||
private db: WasmEngramDb;
|
||||
|
||||
constructor(path = "/wasm-memory") {
|
||||
this.db = new WasmEngramDb(path);
|
||||
}
|
||||
|
||||
/** Store a node and return its UUID. */
|
||||
putNode(node: NodeInput): string {
|
||||
return this.db.put_node(node);
|
||||
}
|
||||
|
||||
/** Retrieve a node by UUID, or null if not found. */
|
||||
getNode(id: string): EngramNode | null {
|
||||
return this.db.get_node(id);
|
||||
}
|
||||
|
||||
/** Find the `limit` most similar nodes by embedding vector. */
|
||||
searchEmbedding(embedding: Float32Array | number[], limit: number): ScoredNode[] {
|
||||
const arr = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
|
||||
return this.db.search_embedding(arr, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run spreading activation from seed nodes.
|
||||
*
|
||||
* @param seeds Array of UUID strings (the "active context")
|
||||
* @param queryEmbedding Semantic vector for the current query
|
||||
* @param maxDepth Maximum BFS hops (typically 2–4)
|
||||
* @param limit Number of results to return
|
||||
*/
|
||||
activate(
|
||||
seeds: string[],
|
||||
queryEmbedding: Float32Array | number[],
|
||||
maxDepth = 3,
|
||||
limit = 10
|
||||
): ActivatedNode[] {
|
||||
const arr =
|
||||
queryEmbedding instanceof Float32Array
|
||||
? queryEmbedding
|
||||
: new Float32Array(queryEmbedding);
|
||||
return this.db.activate(seeds, arr, maxDepth, limit);
|
||||
}
|
||||
|
||||
/** Mark a node as recently accessed (increments activation count). */
|
||||
touch(id: string): void {
|
||||
this.db.touch(id);
|
||||
}
|
||||
|
||||
/** Apply multiplicative salience decay. Returns the number of nodes updated. */
|
||||
decay(factor: number): number {
|
||||
return this.db.decay(factor);
|
||||
}
|
||||
|
||||
/** Run a memory consolidation cycle. */
|
||||
consolidate(): ConsolidationReport {
|
||||
return this.db.consolidate();
|
||||
}
|
||||
|
||||
/** Total number of nodes stored. */
|
||||
nodeCount(): number {
|
||||
return this.db.node_count();
|
||||
}
|
||||
|
||||
/** Total number of edges stored. */
|
||||
edgeCount(): number {
|
||||
return this.db.edge_count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/// WASM/TypeScript bindings for engram-core via wasm-bindgen.
|
||||
///
|
||||
/// This crate compiles to a WebAssembly module that can be loaded by the
|
||||
/// TypeScript wrapper in `src/index.ts`. All types are passed as JSON strings
|
||||
/// across the WASM boundary to avoid bespoke serialisation code.
|
||||
///
|
||||
/// # Storage
|
||||
/// sled is not available in WASM (no filesystem). When compiled with the `wasm`
|
||||
/// feature, engram-core switches to an in-memory HashMap backend. All state is
|
||||
/// therefore lost on page reload — persistence requires sending nodes to a
|
||||
/// server-side store and re-loading them on startup.
|
||||
///
|
||||
/// # Build
|
||||
/// ```
|
||||
/// wasm-pack build bindings/typescript --target web
|
||||
/// ```
|
||||
use engram_core::{
|
||||
ActivatedNode, ConsolidationConfig, EngramDb, MemoryTier, Node, NodeType, ScoredNode,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use uuid::Uuid;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
// ── Panic hook ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
// ── WasmEngramDb ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// The main entry point for WASM callers.
|
||||
///
|
||||
/// TypeScript:
|
||||
/// ```ts
|
||||
/// const db = new WasmEngramDb("ignored-path");
|
||||
/// const id = db.putNode(JSON.stringify({ content: "...", node_type: "Memory", ... }));
|
||||
/// ```
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmEngramDb {
|
||||
inner: EngramDb,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmEngramDb {
|
||||
/// Create a new in-memory engram database.
|
||||
///
|
||||
/// The `path` argument is accepted for API symmetry with the sled backend
|
||||
/// but is ignored — all storage is in-memory.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn open(_path: &str) -> Result<WasmEngramDb, JsValue> {
|
||||
let db = EngramDb::open(Path::new("/wasm-memory"))
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
Ok(WasmEngramDb { inner: db })
|
||||
}
|
||||
|
||||
// ── Node operations ───────────────────────────────────────────────────────
|
||||
|
||||
/// Store a node. Accepts a JSON object with fields:
|
||||
/// `{ content, node_type, tier, importance, embedding }`
|
||||
/// Returns the assigned UUID string.
|
||||
pub fn put_node(&self, node: JsValue) -> Result<String, JsValue> {
|
||||
let n = js_value_to_node(node)?;
|
||||
self.inner
|
||||
.put_node(n)
|
||||
.map(|id| id.to_string())
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Retrieve a node by UUID. Returns a JSON object or null.
|
||||
pub fn get_node(&self, id: &str) -> Result<JsValue, JsValue> {
|
||||
let uuid = id
|
||||
.parse::<Uuid>()
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
match self
|
||||
.inner
|
||||
.get_node(uuid)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?
|
||||
{
|
||||
Some(node) => node_to_js_value(&node),
|
||||
None => Ok(JsValue::NULL),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Vector search ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Search for similar nodes by embedding vector.
|
||||
///
|
||||
/// `embedding` is a JS Float32Array. Returns a JSON array of
|
||||
/// `{ node, score }` objects.
|
||||
pub fn search_embedding(
|
||||
&self,
|
||||
embedding: &[f32],
|
||||
limit: usize,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let results = self
|
||||
.inner
|
||||
.search_embedding(embedding, limit)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
scored_nodes_to_js(&results)
|
||||
}
|
||||
|
||||
// ── Spreading activation ──────────────────────────────────────────────────
|
||||
|
||||
/// Run spreading activation.
|
||||
///
|
||||
/// `seeds` is a JS array of UUID strings.
|
||||
/// `query_embedding` is a Float32Array.
|
||||
/// Returns a JSON array of `{ node, activation_strength, hops }`.
|
||||
pub fn activate(
|
||||
&self,
|
||||
seeds: JsValue,
|
||||
query_embedding: &[f32],
|
||||
max_depth: u8,
|
||||
limit: usize,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let seed_strs: Vec<String> = serde_wasm_bindgen::from_value(seeds)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
let seeds: Vec<Uuid> = seed_strs
|
||||
.iter()
|
||||
.filter_map(|s| s.parse::<Uuid>().ok())
|
||||
.collect();
|
||||
|
||||
let results = self
|
||||
.inner
|
||||
.activate(&seeds, query_embedding, max_depth, limit)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
activated_nodes_to_js(&results)
|
||||
}
|
||||
|
||||
// ── Salience management ───────────────────────────────────────────────────
|
||||
|
||||
/// Touch a node (increment activation count and update salience).
|
||||
pub fn touch(&self, id: &str) -> Result<(), JsValue> {
|
||||
let uuid = id
|
||||
.parse::<Uuid>()
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
self.inner
|
||||
.touch(uuid)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Apply multiplicative salience decay. Returns the number of nodes updated.
|
||||
pub fn decay(&self, factor: f32) -> Result<u32, JsValue> {
|
||||
self.inner
|
||||
.decay(factor)
|
||||
.map(|n| n as u32)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
// ── Consolidation ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Run a memory consolidation cycle.
|
||||
/// Returns `{ promoted, decayed, pruned }`.
|
||||
pub fn consolidate(&self) -> Result<JsValue, JsValue> {
|
||||
let config = ConsolidationConfig::default();
|
||||
let report = self
|
||||
.inner
|
||||
.consolidate(&config)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Report {
|
||||
promoted: usize,
|
||||
decayed: usize,
|
||||
pruned: usize,
|
||||
}
|
||||
serde_wasm_bindgen::to_value(&Report {
|
||||
promoted: report.promoted,
|
||||
decayed: report.decayed,
|
||||
pruned: report.pruned,
|
||||
})
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
// ── Statistics ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Return the total number of nodes.
|
||||
pub fn node_count(&self) -> Result<u32, JsValue> {
|
||||
self.inner
|
||||
.node_count()
|
||||
.map(|n| n as u32)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Return the total number of edges.
|
||||
pub fn edge_count(&self) -> Result<u32, JsValue> {
|
||||
self.inner
|
||||
.edge_count()
|
||||
.map(|n| n as u32)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Serialisation helpers ─────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NodeInput {
|
||||
content: String,
|
||||
node_type: Option<String>,
|
||||
tier: Option<String>,
|
||||
importance: Option<f32>,
|
||||
embedding: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
fn js_value_to_node(val: JsValue) -> Result<Node, JsValue> {
|
||||
let input: NodeInput = serde_wasm_bindgen::from_value(val)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid node: {e}")))?;
|
||||
|
||||
let node_type = match input.node_type.as_deref().unwrap_or("Memory") {
|
||||
"Concept" => NodeType::Concept,
|
||||
"Event" => NodeType::Event,
|
||||
"Entity" => NodeType::Entity,
|
||||
"Process" => NodeType::Process,
|
||||
"InternalState" => NodeType::InternalState,
|
||||
_ => NodeType::Memory,
|
||||
};
|
||||
let tier = match input.tier.as_deref().unwrap_or("Episodic") {
|
||||
"Working" => MemoryTier::Working,
|
||||
"Semantic" => MemoryTier::Semantic,
|
||||
"Procedural" => MemoryTier::Procedural,
|
||||
_ => MemoryTier::Episodic,
|
||||
};
|
||||
let embedding = input.embedding.unwrap_or_default();
|
||||
let importance = input.importance.unwrap_or(0.5);
|
||||
|
||||
Ok(Node::new(node_type, embedding, input.content.into_bytes(), tier, importance))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NodeOutput {
|
||||
id: String,
|
||||
content: String,
|
||||
node_type: String,
|
||||
tier: String,
|
||||
salience: f32,
|
||||
importance: f32,
|
||||
activation_count: u64,
|
||||
embedding: Vec<f32>,
|
||||
}
|
||||
|
||||
fn node_to_js_value(node: &Node) -> Result<JsValue, JsValue> {
|
||||
let out = NodeOutput {
|
||||
id: node.id.to_string(),
|
||||
content: String::from_utf8_lossy(&node.content).into_owned(),
|
||||
node_type: format!("{:?}", node.node_type),
|
||||
tier: format!("{:?}", node.tier),
|
||||
salience: node.salience,
|
||||
importance: node.importance,
|
||||
activation_count: node.activation_count,
|
||||
embedding: node.embedding.clone(),
|
||||
};
|
||||
serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ScoredNodeOutput {
|
||||
node: NodeOutput,
|
||||
score: f32,
|
||||
}
|
||||
|
||||
fn scored_nodes_to_js(nodes: &[ScoredNode]) -> Result<JsValue, JsValue> {
|
||||
let out: Vec<ScoredNodeOutput> = nodes
|
||||
.iter()
|
||||
.map(|s| ScoredNodeOutput {
|
||||
node: NodeOutput {
|
||||
id: s.node.id.to_string(),
|
||||
content: String::from_utf8_lossy(&s.node.content).into_owned(),
|
||||
node_type: format!("{:?}", s.node.node_type),
|
||||
tier: format!("{:?}", s.node.tier),
|
||||
salience: s.node.salience,
|
||||
importance: s.node.importance,
|
||||
activation_count: s.node.activation_count,
|
||||
embedding: s.node.embedding.clone(),
|
||||
},
|
||||
score: s.score,
|
||||
})
|
||||
.collect();
|
||||
serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ActivatedNodeOutput {
|
||||
node: NodeOutput,
|
||||
activation_strength: f32,
|
||||
hops: u8,
|
||||
}
|
||||
|
||||
fn activated_nodes_to_js(nodes: &[ActivatedNode]) -> Result<JsValue, JsValue> {
|
||||
let out: Vec<ActivatedNodeOutput> = nodes
|
||||
.iter()
|
||||
.map(|a| ActivatedNodeOutput {
|
||||
node: NodeOutput {
|
||||
id: a.node.id.to_string(),
|
||||
content: String::from_utf8_lossy(&a.node.content).into_owned(),
|
||||
node_type: format!("{:?}", a.node.node_type),
|
||||
tier: format!("{:?}", a.node.tier),
|
||||
salience: a.node.salience,
|
||||
importance: a.node.importance,
|
||||
activation_count: a.node.activation_count,
|
||||
embedding: a.node.embedding.clone(),
|
||||
},
|
||||
activation_strength: a.activation_strength,
|
||||
hops: a.hops,
|
||||
})
|
||||
.collect();
|
||||
serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* TypeScript types mirroring the Rust structs in engram-core.
|
||||
*/
|
||||
|
||||
export type NodeType =
|
||||
| "Memory"
|
||||
| "Concept"
|
||||
| "Event"
|
||||
| "Entity"
|
||||
| "Process"
|
||||
| "InternalState";
|
||||
|
||||
export type MemoryTier = "Working" | "Episodic" | "Semantic" | "Procedural";
|
||||
|
||||
export interface EngramNode {
|
||||
id: string;
|
||||
content: string;
|
||||
node_type: NodeType;
|
||||
tier: MemoryTier;
|
||||
salience: number;
|
||||
importance: number;
|
||||
activation_count: number;
|
||||
embedding: number[];
|
||||
}
|
||||
|
||||
export interface NodeInput {
|
||||
content: string;
|
||||
node_type?: NodeType;
|
||||
tier?: MemoryTier;
|
||||
importance?: number;
|
||||
embedding?: number[];
|
||||
}
|
||||
|
||||
export interface ScoredNode {
|
||||
node: EngramNode;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface ActivatedNode {
|
||||
node: EngramNode;
|
||||
activation_strength: number;
|
||||
hops: number;
|
||||
}
|
||||
|
||||
export interface ConsolidationReport {
|
||||
promoted: number;
|
||||
decayed: number;
|
||||
pruned: number;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": false,
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "pkg"]
|
||||
}
|
||||
Reference in New Issue
Block a user