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
- func Answering(ctx context.Context, cfg config.MemoryConfig) bool
- func BinaryName() string
- func BuildRecallQuery(history []provider.Message, current string, contextMsgs, maxChars int) string
- func EnginePid() (int, bool)
- func EnsureSmrti(ctx context.Context, command, home string, autoInstall bool, progress Progress) (path string, installed bool, err error)
- func FindSmrti(command, home string) (string, bool)
- func FormatMemories(mems []Memory, maxCharsEach int) string
- func IdleFunc(e Engine, quiet time.Duration) func() bool
- func Install(ctx context.Context, home string, progress Progress) (path, method string, err error)
- func InstalledVersion(ctx context.Context, exe string) string
- func NewTools(engine Engine, spaces SpacePolicy) []tools.Tool
- func Runnable(ctx context.Context, path string) (ok bool, detail string)
- func StopEngine(ctx context.Context) (int, error)
- func Upgrade(ctx context.Context, exe, home string, progress Progress) (method string, err error)
- func UpgradeMethod(exe, home string) string
- func VenvDir(home string) string
- type Ambient
- type Client
- func (c *Client) CheckHealth(ctx context.Context) error
- func (c *Client) Close() error
- func (c *Client) Enabled() bool
- func (c *Client) Forget(ctx context.Context, query, reason, space string) error
- func (c *Client) Healthy() bool
- func (c *Client) Idle(quiet time.Duration) bool
- func (c *Client) MergeSpaces(ctx context.Context, space, other string, minJaccard float64) (int, error)
- func (c *Client) Recall(ctx context.Context, query string, topK int, minConfidence float64, ...) ([]Memory, error)
- func (c *Client) Reflect(ctx context.Context) (map[string]any, error)
- func (c *Client) Remember(ctx context.Context, req RememberRequest) (string, error)
- func (c *Client) SpaceSupport() (bool, string)
- func (c *Client) Status(ctx context.Context) (map[string]any, error)
- type Engine
- type ExtractSettings
- type Memory
- type Noop
- func (Noop) Close() error
- func (Noop) Enabled() bool
- func (Noop) Forget(context.Context, string, string, string) error
- func (Noop) Healthy() bool
- func (Noop) Recall(context.Context, string, int, float64, Scope) ([]Memory, error)
- func (Noop) Reflect(context.Context) (map[string]any, error)
- func (Noop) Remember(context.Context, RememberRequest) (string, error)
- func (Noop) SpaceSupport() (bool, string)
- func (Noop) Status(context.Context) (map[string]any, error)
- type Progress
- type RememberRequest
- type Scope
- type Severity
- type Sidecar
- func (s *Sidecar) Close() error
- func (s *Sidecar) Enabled() bool
- func (s *Sidecar) Forget(ctx context.Context, query, reason, space string) error
- func (s *Sidecar) Healthy() bool
- func (s *Sidecar) Idle(quiet time.Duration) bool
- func (s *Sidecar) MergeSpaces(ctx context.Context, space, other string, minJaccard float64) (int, error)
- func (s *Sidecar) Recall(ctx context.Context, query string, topK int, minConfidence float64, ...) ([]Memory, error)
- func (s *Sidecar) Reflect(ctx context.Context) (map[string]any, error)
- func (s *Sidecar) Remember(ctx context.Context, req RememberRequest) (string, error)
- func (s *Sidecar) SpaceSupport() (bool, string)
- func (s *Sidecar) Status(ctx context.Context) (map[string]any, error)
- type SpaceMerger
- type SpacePolicy
Constants ¶
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.
const ( MethodUv = "uv" MethodPipx = "pipx" MethodVenv = "venv" MethodPip = "pip" )
The installers an install on disk can have come from.
const InstallTimeout = 15 * time.Minute
InstallTimeout bounds one install attempt (wheels + ONNX deps are chunky).
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).
const PackageName = "smrti"
PackageName is what we install; the executable it provides is BinaryName.
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 ¶
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
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 ¶
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 ¶
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
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 ¶
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
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
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
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
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
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.
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 (*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 ¶
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
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 (*Client) CheckHealth ¶
CheckHealth probes /status with a short deadline and updates Healthy().
func (*Client) Idle ¶ added in v0.13.0
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) SpaceSupport ¶ added in v0.12.0
SpaceSupport reports what the last status probe said about space routing.
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) SpaceSupport ¶ added in v0.12.0
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
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 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) Idle ¶ added in v0.13.0
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) SpaceSupport ¶ added in v0.12.0
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
// 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.