memory

package
v0.39.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package memory is the soul of Factor: long-term memory backed by smrti (github.com/cyqlelabs/smrti), an AtomSpace-inspired engine with Bayesian truth values, attention economics, and emotional valence, reached over a localhost REST sidecar. Everything degrades gracefully: when smrti is unreachable, recalls come back empty and stores are dropped with a log line — the agent keeps working, just without its long-term memory.

Index

Constants

View Source
const (
	SourceUser  = "user"
	SourceAgent = "agent"
)

Source values for RememberRequest. Anything the assistant authored must be marked SourceAgent: smrti extracts those turns conservatively, weights them below what the user said, and lets them decay unless the user picks them up. An unmarked reply is stored as if the user had stated it, so a single suggestion-laden answer mints dozens of permanent entities.

View Source
const (
	MethodUv   = "uv"
	MethodPipx = "pipx"
	MethodVenv = "venv"
	MethodPip  = "pip"
)

The installers an install on disk can have come from.

View Source
const InstallTimeout = 15 * time.Minute

InstallTimeout bounds one install attempt (wheels + ONNX deps are chunky).

View Source
const NumpyConstraint = "numpy<2"

NumpyConstraint rides along with smrti on machines whose CPU numpy's own wheels cannot run on. Since 2.0 those wheels target the x86-64-v2 baseline — SSE4.2 — and below it the extension modules do not merely run slowly, they execute an illegal instruction: `import numpy` dies with SIGILL, taking the engine with it. SIGILL is a signal rather than an exception, so nothing downstream can catch or retry it; the only fix is to not install that wheel. 1.x is the last line whose baseline those CPUs can execute, and smrti's dependencies are all happy with it (gliner2-onnx asks for numpy>=1.26).

View Source
const PackageName = "smrti"

PackageName is what we install; the executable it provides is BinaryName.

View Source
const UpgradeQuiet = 15 * time.Second

UpgradeQuiet is how long the graph must have gone untouched before the engine may be restarted under a running Factor. Long enough that a turn's trailing store has landed, short enough that an idle minute offers a window.

Variables

This section is empty.

Functions

func Answering added in v0.4.0

func Answering(ctx context.Context, cfg config.MemoryConfig) bool

Answering reports whether a smrti is already serving at the configured endpoint. An engine somebody runs in Docker, in a venv, or on another box is a working memory whatever this filesystem holds, so nothing should offer to install one on top of it — the same reasoning the phone status applies to a live speech server.

func BinaryName

func BinaryName() string

BinaryName is the smrti executable name for this platform.

func BuildRecallQuery

func BuildRecallQuery(history []provider.Message, current string, contextMsgs, maxChars int) string

BuildRecallQuery assembles the recall query from recent conversation context (not just the last message), keeping the most recent tail when truncating — mirrors smrti's own proxy semantics.

func EnginePid added in v0.38.0

func EnginePid() (int, bool)

EnginePid reports the engine Factor spawned and whether it is still running. Whoever stopped one is how the caller tells a supervisor that put a replacement in its place from a machine where nothing will.

func EnsureSmrti

func EnsureSmrti(ctx context.Context, command, home string, autoInstall bool, progress Progress) (path string, installed bool, err error)

EnsureSmrti returns the smrti path, installing it when missing and allowed. installed reports whether this call performed the installation.

func FindSmrti

func FindSmrti(command, home string) (string, bool)

FindSmrti resolves the smrti executable. An explicit command wins (absolute path or PATH lookup); otherwise PATH is searched, then the well-known user install directories. The returned path is what the sidecar should exec.

func FormatMemories

func FormatMemories(mems []Memory, maxCharsEach int) string

FormatMemories renders recalled memories for the system prompt: severe memories become behavioral constraints, the rest background notes, each with a confidence qualifier.

func IdleFunc added in v0.13.0

func IdleFunc(e Engine, quiet time.Duration) func() bool

IdleFunc adapts an engine to the gate the upgrade path waits on. An engine that cannot report activity — Noop, a test fake — reads as idle: there is nothing of its own to interrupt.

func Install

func Install(ctx context.Context, home string, progress Progress) (path, method string, err error)

Install installs smrti with the first available strategy and returns the resolved executable path and the strategy that produced it.

func InstalledVersion added in v0.38.0

func InstalledVersion(ctx context.Context, exe string) string

InstalledVersion reads the version of the smrti package behind exe, or "" when nothing here can say. The package's own metadata is the answer — the CLI has no --version flag — so the interpreter that runs the console script is asked for it.

func NewTools

func NewTools(engine Engine, spaces SpacePolicy) []tools.Tool

NewTools returns the deliberate-memory tool set on top of the ambient loop.

func Runnable added in v0.14.1

func Runnable(ctx context.Context, path string) (ok bool, detail string)

Runnable reports whether the smrti at path can actually execute. Finding the file is not the same as being able to run it: an install can carry wheels this CPU has no instructions for, and `import numpy` then dies with SIGILL before smrti prints a word. A binary like that must not be adopted with a checkmark — it has to send the caller back to the installer, which knows how to constrain the install so it works here.

--help is the cheapest command that still loads the whole import chain, so the probe fails for exactly the reasons serving would. When it does, detail carries the tail of the probe's output: "cannot run" without the traceback that says why (an smrti importing fcntl on Windows, say) is a diagnosis that cannot be made from the log.

func StopEngine added in v0.38.0

func StopEngine(ctx context.Context) (int, error)

StopEngine stops the smrti Factor spawned and reports which process it was. A zero pid is an answer rather than a failure: the engine may be one somebody runs by hand, or nothing may be running at all — in both cases the newly installed code is what the next start will load.

func Upgrade added in v0.38.0

func Upgrade(ctx context.Context, exe, home string, progress Progress) (method string, err error)

Upgrade re-runs the installer behind exe so it installs the newest published smrti, and reports which installer did it. The running engine is untouched: a Python process keeps the modules it imported, so the new code only takes effect once something restarts it (StopEngine).

func UpgradeMethod added in v0.38.0

func UpgradeMethod(exe, home string) string

UpgradeMethod names the installer that owns the smrti at exe. The console script itself says which: uv and pipx both put it in a directory of their own and link it onto PATH, so the link is followed before the path is read. Anything else is a pip install — the one method that drops its script into a shared bin directory, and the fallback that is right whenever the layout is not one of the two this can recognise.

func VenvDir

func VenvDir(home string) string

VenvDir is the private virtualenv Factor falls back to.

Types

type Ambient

type Ambient struct {
	Engine         Engine
	TopK           int
	MinConfidence  float64
	QueryMsgs      int
	QueryMaxChars  int
	InjectMaxChars int
	Spaces         SpacePolicy
	// contains filtered or unexported fields
}

Ambient stores conversation exchanges and recalls context for each turn.

func NewAmbient

func NewAmbient(engine Engine, topK int, minConfidence float64, queryMsgs, queryMaxChars, injectMaxChars int, ignorePatterns []string, spaces SpacePolicy) *Ambient

func (*Ambient) MemoryPrompt

func (a *Ambient) MemoryPrompt(ctx context.Context, history []provider.Message, current string) string

MemoryPrompt recalls context for the upcoming turn. Best-effort: failures log and return "" so a memory outage never blocks a reply.

func (*Ambient) StoreExchange

func (a *Ambient) StoreExchange(channel, audience, speaker, userText, assistantText string)

StoreExchange persists both sides of a completed turn as episodes, in the space the turn's channel writes to. Call it from a goroutine; it must never block a reply. If the memory engine is still cold-booting (first sidecar start downloads models), it waits for health rather than dropping the very first memories.

func (*Ambient) WatchBridges added in v0.28.0

func (a *Ambient) WatchBridges(ctx context.Context)

WatchBridges merges the private and shared spaces whenever a gathering ends, for as long as ctx lives. It returns immediately when there is nothing to bridge — no shared space configured, or an engine that cannot merge — so a caller can start it unconditionally.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to a smrti REST server. apiKey authenticates to smrti via X-Api-Key; extractKey rides in Authorization: Bearer so smrti can forward it to the LLM used for entity extraction (the middleware accepts either header for its own auth, by design).

func NewClient

func NewClient(baseURL, apiKey, extractKey string) *Client

func (*Client) CheckHealth

func (c *Client) CheckHealth(ctx context.Context) error

CheckHealth probes /status with a short deadline and updates Healthy().

func (*Client) Close

func (c *Client) Close() error

func (*Client) Enabled

func (c *Client) Enabled() bool

func (*Client) Forget

func (c *Client) Forget(ctx context.Context, query, reason, space string) error

func (*Client) Healthy

func (c *Client) Healthy() bool

func (*Client) Idle added in v0.13.0

func (c *Client) Idle(quiet time.Duration) bool

Idle reports that nothing is reading or writing the graph right now and nothing has for quiet. Swapping the engine out from under a live request loses that memory, so the upgrade waits for this to be true.

func (*Client) MergeSpaces added in v0.28.0

func (c *Client) MergeSpaces(ctx context.Context, space, other string, minJaccard float64) (int, error)

MergeSpaces grows the bridge between two spaces and reports how many atoms it materialized. The engine names the bridge itself, commutatively, so the same pair always lands in the same space however the call is ordered.

func (*Client) Recall

func (c *Client) Recall(ctx context.Context, query string, topK int, minConfidence float64, scope Scope) ([]Memory, error)

func (*Client) Reflect

func (c *Client) Reflect(ctx context.Context) (map[string]any, error)

func (*Client) Remember

func (c *Client) Remember(ctx context.Context, req RememberRequest) (string, error)

func (*Client) SpaceSupport added in v0.12.0

func (c *Client) SpaceSupport() (bool, string)

SpaceSupport reports what the last status probe said about space routing.

func (*Client) Status

func (c *Client) Status(ctx context.Context) (map[string]any, error)

type Engine

type Engine interface {
	Remember(ctx context.Context, req RememberRequest) (string, error)
	Recall(ctx context.Context, query string, topK int, minConfidence float64, scope Scope) ([]Memory, error)
	Forget(ctx context.Context, query, reason, space string) error
	Reflect(ctx context.Context) (map[string]any, error)
	Status(ctx context.Context) (map[string]any, error)
	Enabled() bool // false only for the disabled (off-mode) engine
	Healthy() bool // reachable right now
	// SpaceSupport reports whether the engine routes per-request memory
	// spaces and, when it does, the space it writes to by default. Both come
	// from the last status probe, so before the first probe it reads as no
	// support — space fields are then omitted and behavior matches an engine
	// that never had them.
	SpaceSupport() (bool, string)
	Close() error
}

Engine is the memory seam. The production implementation talks to smrti; tests use fakes; "off" mode uses Noop.

func NewEngine

func NewEngine(ctx context.Context, cfg config.MemoryConfig, extract ExtractSettings, logDir string) (Engine, error)

NewEngine builds the memory engine for the configured mode. The returned engine is usable immediately; health flips asynchronously.

type ExtractSettings

type ExtractSettings struct {
	Mode  string // hybrid | llm | local
	URL   string // upstream base WITHOUT /v1 (smrti appends /v1/chat/completions)
	Model string
	Key   string
}

ExtractSettings configure smrti's entity-extraction LLM calls.

func DeriveExtract

func DeriveExtract(cfg config.MemoryConfig, providerCfg config.ProviderConfig) ExtractSettings

DeriveExtract picks extraction settings: explicit config wins, then the first OpenAI-compatible provider candidate, else pure-local extraction.

type Memory

type Memory struct {
	ID          string   `json:"id"`
	Label       string   `json:"label"`
	Content     string   `json:"content"`
	Type        string   `json:"type"`
	Probability float64  `json:"probability"`
	Confidence  float64  `json:"confidence"`
	STI         float64  `json:"sti"`
	LTI         float64  `json:"lti"`
	Valence     float64  `json:"valence"`
	Intensity   float64  `json:"intensity"`
	Severity    Severity `json:"severity"`
	Salience    float64  `json:"salience"`
	Similarity  float64  `json:"similarity"`
	Space       string   `json:"space"`
}

type Noop

type Noop struct{}

Noop is the disabled-memory engine.

func (Noop) Close

func (Noop) Close() error

func (Noop) Enabled

func (Noop) Enabled() bool

func (Noop) Forget

func (Noop) Healthy

func (Noop) Healthy() bool

func (Noop) Recall

func (Noop) Recall(context.Context, string, int, float64, Scope) ([]Memory, error)

func (Noop) Reflect

func (Noop) Reflect(context.Context) (map[string]any, error)

func (Noop) Remember

func (Noop) SpaceSupport added in v0.12.0

func (Noop) SpaceSupport() (bool, string)

func (Noop) Status

func (Noop) Status(context.Context) (map[string]any, error)

type Progress

type Progress func(format string, args ...any)

Progress reports installer steps to whoever is watching (wizard, logs).

type RememberRequest

type RememberRequest struct {
	Content     string
	Type        string // episode | belief | goal (default episode)
	Probability float64
	Valence     *float64 // nil = smrti auto-estimates from content
	Evidence    string   // beliefs only
	Source      string   // user | agent (empty means user)
	Space       string   // memory space to write to (empty = engine default)
}

type Scope added in v0.12.0

type Scope struct {
	Space      string
	ReadSpaces []string
}

Scope names the space a call writes to and the overlay a recall reads. The zero value means "the engine's configured default"; with it every request payload stays byte-identical to pre-space builds. The client drops the fields entirely when the engine has not advertised space support, so a non-zero Scope against an old engine degrades to today's single space instead of silently misrouting.

type Severity

type Severity string
const (
	SeverityCriticalWarning  Severity = "critical_warning"
	SeverityKnownAntipattern Severity = "known_antipattern"
	SeverityContext          Severity = "context"
)

type Sidecar

type Sidecar struct {
	// contains filtered or unexported fields
}

Sidecar supervises a `smrti serve rest` child process (or, in external mode, just health-checks a server someone else runs). If a healthy server already listens on the configured port, it is adopted instead of spawning a duplicate.

func (*Sidecar) Close

func (s *Sidecar) Close() error

func (*Sidecar) Enabled

func (s *Sidecar) Enabled() bool

func (*Sidecar) Forget

func (s *Sidecar) Forget(ctx context.Context, query, reason, space string) error

func (*Sidecar) Healthy

func (s *Sidecar) Healthy() bool

func (*Sidecar) Idle added in v0.13.0

func (s *Sidecar) Idle(quiet time.Duration) bool

Idle reports that the graph has been untouched for quiet — what the upgrade path waits for before restarting the engine underneath a live Factor.

func (*Sidecar) MergeSpaces added in v0.28.0

func (s *Sidecar) MergeSpaces(ctx context.Context, space, other string, minJaccard float64) (int, error)

MergeSpaces forwards the bridge merge to the supervised engine.

func (*Sidecar) Recall

func (s *Sidecar) Recall(ctx context.Context, query string, topK int, minConfidence float64, scope Scope) ([]Memory, error)

func (*Sidecar) Reflect

func (s *Sidecar) Reflect(ctx context.Context) (map[string]any, error)

func (*Sidecar) Remember

func (s *Sidecar) Remember(ctx context.Context, req RememberRequest) (string, error)

func (*Sidecar) SpaceSupport added in v0.12.0

func (s *Sidecar) SpaceSupport() (bool, string)

func (*Sidecar) Status

func (s *Sidecar) Status(ctx context.Context) (map[string]any, error)

type SpaceMerger added in v0.28.0

type SpaceMerger interface {
	MergeSpaces(ctx context.Context, space, other string, minJaccard float64) (int, error)
}

SpaceMerger is the optional capability of an engine that can grow a bridge between two spaces. It is optional rather than part of Engine because an older smrti has no such route: without it the partition simply stands unbridged until the engine's own epoch gets to it, which is a loss of recall quality and never of correctness.

type SpacePolicy added in v0.12.0

type SpacePolicy struct {
	Strategy string // "origin" (default) or "single"
	Main     string
	System   string
	// Shared holds what was said with company present. Empty means the split
	// is unavailable, which callers must read as "cannot isolate" rather than
	// as "isolate into Main".
	Shared string
}

SpacePolicy decides which memory space a turn writes to and which overlay it reads, keyed by the channel the turn arrived on and by who can hear the reply. Real conversations (cli, telegram, phone) write to Main; machine-originated turns (cron, jobs, heartbeat) write to System so operational chatter stops crowding conversational recall — and each side still reads the other as an overlay. Strategy "single" (or a zero policy) turns the split off.

Shared is the one overlay that is deliberately one-directional. A turn somebody else can hear writes there and reads only there, so nothing said in private is spoken back into a room with a guest in it; a private turn reads Shared too, because the user was there for all of it and should not have to be alone to remember their own conversation. That asymmetry is the whole feature, and the engine enforces the half that matters: a space reads its entire overlay but only ever mutates its own write space, so a private turn physically cannot write back into Shared.

func NewSpacePolicy added in v0.12.0

func NewSpacePolicy(strategy, main, system, shared string) (SpacePolicy, error)

NewSpacePolicy validates the configured strategy. An unrecognised value is an error rather than a silent fallback: defaulting it to origin would turn the split on for someone who wrote "single" with a typo, which is the opposite of what they asked for.

func (SpacePolicy) Scope added in v0.12.0

func (p SpacePolicy) Scope(channel, audience string) (Scope, bool)

Scope resolves the space a turn writes to and the overlay it reads. It reports ok=false when the turn must not recall at all — an audience this policy cannot isolate. Serving such a turn from the one space that holds everything is precisely the leak the split exists to prevent, so recall is skipped instead of quietly widened.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL