Decision Memory
Part of the expertise layer. Read the overview →
SmartMemory tracks decisions as first-class memory objects with confidence scoring, provenance chains, and lifecycle management. Decisions capture not just what was decided, but why and how confident the system is.
Why Decision Memory Matters
Agents and users make decisions constantly. Without tracking them:
- The same decision gets re-evaluated repeatedly
- Contradictory decisions go unnoticed
- There's no audit trail for "why did we choose X?"
Decision Memory solves this by storing decisions with full provenance, confidence scoring, and conflict detection.
Decision Types
| Type | Description | Example |
|---|---|---|
inference | Conclusion drawn from evidence | "The performance issue is caused by N+1 queries" |
preference | Stated or detected preference | "User prefers dark mode for coding" |
classification | Categorization of something | "This issue is a security vulnerability" |
choice | Explicit selection between options | "We chose PostgreSQL over MySQL" |
belief | Held belief or conviction | "Microservices are overused for small teams" |
policy | Rule or guideline to follow | "Always write tests before implementation" |
Lifecycle
Decisions follow a lifecycle:
graph LR
A[Created] --> B[Active]
B --> C[Superseded]
B --> D[Retracted]
B --> B
- Active: Current, valid decision
- Superseded: Replaced by a newer decision (linked via
SUPERSEDESedge) - Retracted: Withdrawn, no longer valid
Confidence Scoring
Each decision has a confidence score (0.0–1.0) that evolves over time:
from smartmemory.models.decision import Decision
decision = Decision(
content="PostgreSQL is better for our use case",
decision_type="choice",
confidence=0.8
)
# Supporting evidence increases confidence (diminishing returns)
decision.reinforce("benchmark_result_id")
# confidence: 0.8 → 0.82
# Contradicting evidence decreases confidence (15% penalty)
decision.contradict("mysql_benchmark_id")
# confidence: 0.82 → 0.697Automatic Decay
The DecisionConfidenceEvolver applies time-based decay:
- Decisions inactive for 30+ days start losing confidence (5% per cycle)
- Decisions below 0.1 confidence are automatically retracted
- Recently reinforced decisions are protected from decay
Belief & Contest (Dempster-Shafer)
The single confidence scalar can't distinguish a contested decision (strong
support and strong contradiction) from an unevidenced one — both look like
"medium confidence". SmartMemory adds Dempster-Shafer evidence belief over the
frame {hold, retract} to separate them:
| Field | Meaning |
|---|---|
belief_hold | committed lower bound that the decision should stand |
plausibility_hold | upper bound (adds the ignorance band) |
ignorance | how much is genuinely unknown (no evidence either way) |
contest | how disputed the decision is — high only when support and contradiction are both strong |
All four ship in decision API responses, so SDK, MCP and UI consumers get them
without recomputing the kernel. A decision with 3 supporting and 3 contradicting
pieces of evidence reports contest ≈ 0.77 while an untouched decision reports
contest = 0.0, even though both show the same stability.
The belief reads are evidence-only and carry no prior. An untouched decision reads
belief_hold = 0.0 and ignorance = 1.0 whatever its confidence, because
confidence carries the prior and the belief state carries the evidence. That is how
a caller says "the system does not know yet" rather than asserting a number it has
nothing to back. The values are recomputed from the evidence counts on every read, so
a stale persisted copy can never win.
Creating Decisions
Programmatic
from smartmemory.decisions.manager import DecisionManager
manager = DecisionManager(smart_memory)
decision = manager.create(
content="We should use Redis for caching",
decision_type="choice",
confidence=0.85,
domain="infrastructure",
tags=["caching", "architecture"],
evidence_ids=["benchmark_123", "analysis_456"],
)Via Extraction
The DecisionExtractor detects decisions in natural text:
from smartmemory.plugins.extractors.decision import DecisionExtractor
extractor = DecisionExtractor()
result = extractor.extract("""
I decided to use PostgreSQL for the new project.
I believe microservices are overkill for our team size.
We should always write integration tests for API endpoints.
""")
# result['decisions'] contains 3 detected decisions:
# - choice: "use PostgreSQL for the new project"
# - belief: "microservices are overkill for our team size"
# - policy: "always write integration tests for API endpoints"It also extracts from reasoning traces:
from smartmemory.models.reasoning import ReasoningTrace
trace = ReasoningTrace(steps=[...])
decisions = extractor.extract_from_trace(trace)
# Decisions linked to the trace via PRODUCED edgeQuerying Decisions
List Active Decisions
from smartmemory.decisions.queries import DecisionQueries
queries = DecisionQueries(smart_memory)
# All active decisions
decisions = queries.get_active_decisions()
# Filtered by domain and type
decisions = queries.get_active_decisions(
domain="infrastructure",
decision_type="choice",
min_confidence=0.7,
)Search by Topic
decisions = queries.get_decisions_about(topic="database")Provenance Chains
Every decision tracks its origin:
provenance = queries.get_decision_provenance(decision_id)
# Returns:
# {
# "decision": Decision(...),
# "reasoning_trace": ReasoningTrace(...) or None,
# "evidence": [MemoryItem(...), ...],
# "superseded": [Decision(...), ...],
# }Causal Chains
Traverse cause-and-effect relationships:
chain = queries.get_causal_chain(
decision_id,
direction="both", # "causes", "effects", or "both"
max_depth=3,
)
# Returns:
# {
# "decision": Decision(...),
# "causes": [...], # What led to this decision
# "effects": [...], # What this decision caused
# }Conflict Detection
Find decisions that may contradict each other:
conflicts = manager.find_conflicts(decision)
# Candidate gate: semantic search + content-overlap heuristic.
# Results are ranked by pairwise contest severity (how confidently both decisions
# assert their contradictory claims), most-contested first.
# Drop weakly-held conflicts:
conflicts = manager.find_conflicts(decision, min_contest=0.3)The REST route POST /memory/decisions/{id}/conflicts accepts the same
min_contest query parameter.
Superseding Decisions
When a decision is replaced:
new_decision = Decision(
content="Actually, MongoDB fits better for our document-heavy workload",
decision_type="choice",
confidence=0.9,
)
result = manager.supersede(
old_decision_id,
new_decision,
reason="New benchmarks show MongoDB handles our access patterns better",
)
# Old decision marked as "superseded"
# SUPERSEDES edge created between new → old
# Reason and timestamp recorded on the old decision's context_snapshotThe reason travels on the superseded decision itself, in
context_snapshot["superseded_reason"] alongside
context_snapshot["superseded_at"]. That matters because status and
superseded_by are machine pointers: a reader that renders only the decision
text would otherwise show replaced content as if it were still in force. Reading
the old decision now tells you it was replaced and why, with no extra lookup.
old = manager.get_decision(old_decision_id)
old.status # "superseded"
old.superseded_by # the replacement's id
old.context_snapshot["superseded_reason"] # why it was replacedRetracting Decisions
When a decision is no longer valid:
manager.retract(decision_id, reason="Requirements changed — JWT no longer needed")
# Decision status set to "retracted"
# Retracted decisions are excluded from active queriesGraduation from Plans
When a Plan completes, it can optionally graduate to a decision record — preserving the task decomposition as a permanent "we decided to do X" record:
from smartmemory.plans import PlanManager
plan_manager = PlanManager(memory)
result = plan_manager.create("Auth redesign", tasks=[...])
# ... tasks completed ...
# Graduate creates a decision with DERIVED_FROM edge back to plan
decision_id = plan_manager.complete(result["plan_id"], graduate=True)The graduated decision has decision_type="choice", source_type="imported", and an evidence link to the original plan.
Graph Edges
Decision Memory introduces these edge types:
| Edge | From → To | Meaning |
|---|---|---|
PRODUCED | ReasoningTrace → Decision | Trace generated this decision |
DERIVED_FROM | Decision → Memory | Decision based on this evidence |
SUPERSEDES | Decision → Decision | New decision replaces old |
CONTRADICTS | Decision → Decision | Decisions conflict |
INFLUENCES | Decision → Memory | Decision affects other memories |
Configuration
DecisionExtractor
from smartmemory.plugins.extractors.decision import DecisionExtractorConfig
config = DecisionExtractorConfig(
min_confidence=0.7, # Minimum confidence for extracted decisions
min_content_length=20, # Minimum content length
)DecisionConfidenceEvolver
from smartmemory.plugins.evolvers.decision_confidence import DecisionConfidenceConfig
config = DecisionConfidenceConfig(
min_confidence_threshold=0.1, # Auto-retract below this
decay_after_days=30, # Start decay after inactivity
decay_rate=0.05, # 5% confidence loss per cycle
enable_decay=True,
)REST API
Decision Memory is exposed via 12 endpoints at /memory/decisions/:
| Method | Path | Description |
|---|---|---|
| POST | /create | Create a decision |
| GET | / | List active decisions |
| GET | /search | Search by topic |
| GET | /pending | List pending decisions |
| GET | /{id} | Get a decision |
| POST | /{id}/supersede | Replace a decision |
| POST | /{id}/retract | Retract a decision |
| POST | /{id}/reinforce | Add supporting evidence |
| POST | /{id}/contradict | Add counter-evidence |
| GET | /{id}/provenance | Full provenance chain |
| GET | /{id}/causal-chain | Causal traversal |
| POST | /{id}/conflicts | Find conflicts |