// 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 */ 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 }