ObservationProtocol¶
Outcome-driven memory effects — the application layer reports how the agent used retrieved memories, and the ORM applies effects atomically.
Overview¶
ObservationProtocol provides three lifecycle hooks for passive behavioral inference on memory models:
on_read(instance)— fired when a query hydrates an instance. Delegates toAccessTrackerMixin.on_surfaced(instances, reason)— fired when a proactive system pushes memories into agent context. CreatesRecallProposalentries.on_context_used(instances, outcome_map)— fired when the application reports how the agent responded. Applies effects based on outcome.
Outcomes¶
Five outcomes drive different effects:
| Outcome | Confidence | Cycles | Pressure | Access | Predictions |
|---|---|---|---|---|---|
acted |
Corroborate (signal=0.9) | Strengthen (factor=1.2) | Resolve | Confirm | Auto-resolve (error=0.1) |
dismissed |
— | Weaken (factor=0.8) | — | Discard | Auto-resolve (error=0.5) |
deferred |
— | — | Keeps building | Discard | — |
contradicted |
Contradict (signal=0.1) | Aggressively weaken (factor=0.5) | Auto-discharge if confidence < 0.1 | Discard | Auto-resolve (error=0.9) |
used |
— | — | — | Confirm | Auto-resolve (error=0.3) |
"used" vs "deferred"¶
"used" and "deferred" are the two outcomes that do not emit a strength signal, but they are observably different:
"deferred"— agent set the memory aside without reading it. Staged reads are discarded (no confirmed-read trace). Pending predictions are left unresolved."used"— agent read and reasoned over the memory but did not cite it in the response. Staged reads are confirmed viaAccessTrackerMixin.confirm_access(). Predictions are auto-resolved with a moderate error (0.3). No confidence, cycle, or decay signal is emitted.
Use "used" when the memory informed the agent's reasoning without appearing directly in the output — a common case that "acted" overcounts and "deferred" undercounts.
outcome_map = {
memory1.db_key.redis_key: "acted", # appeared in response
memory2.db_key.redis_key: "used", # informed reasoning, not cited
memory3.db_key.redis_key: "dismissed",
# memory4 not in map → defaults to "deferred"
}
ObservationProtocol.on_context_used(memories, outcome_map)
See Metacognitive Layer for the full effects comparison table.
Migrating custom outcomes¶
If you were using a custom "echoed" outcome (or any bespoke label between "used" and "dismissed"), map it to "used" when the agent reasoned over the memory or to "dismissed" when the overlap was coincidental; on_context_used() raises ValueError on unknown labels, so coerce to a valid outcome before calling.
Usage¶
from popoto.fields.observation import ObservationProtocol
# After agent processes memories:
outcome_map = {
memory1.db_key.redis_key: "acted",
memory2.db_key.redis_key: "dismissed",
memory3.db_key.redis_key: "contradicted",
}
ObservationProtocol.on_context_used(memories, outcome_map)
Instances not in the outcome_map default to "deferred".
Proactive Surfacing¶
When a proactive system pushes memories into agent context:
This creates RecallProposal entries in a Redis sorted set for tracking. Proposals expire after 1 hour (configurable via RecallProposal.DEFAULT_TTL).
Tuning Constants¶
All constants are configurable via Defaults:
from popoto.fields.constants import Defaults
Defaults.ACTED_CONFIDENCE_SIGNAL = 0.9
Defaults.CONTRADICTED_CONFIDENCE_SIGNAL = 0.1
Defaults.ACTED_CYCLE_STRENGTHEN_FACTOR = 1.2
Defaults.DISMISSED_CYCLE_WEAKEN_FACTOR = 0.8
Defaults.CONTRADICTED_CYCLE_WEAKEN_FACTOR = 0.5
Defaults.AUTO_DISCHARGE_CONFIDENCE_THRESHOLD = 0.1
| Constant | Default | Optimal Range | Notes |
|---|---|---|---|
ACTED_CONFIDENCE_SIGNAL |
0.9 | [0.5, 1.0] | Insensitive within range |
CONTRADICTED_CONFIDENCE_SIGNAL |
0.1 | [0.05, 0.3] | Insensitive within range |
ACTED_CYCLE_STRENGTHEN_FACTOR |
1.2 | [1.0, 2.0] | CLIFF EFFECT below 1.0 |
DISMISSED_CYCLE_WEAKEN_FACTOR |
0.8 | [0.3, 1.0] | Insensitive within range |
CONTRADICTED_CYCLE_WEAKEN_FACTOR |
0.5 | [0.3, 0.8] | Insensitive within range |
AUTO_DISCHARGE_CONFIDENCE_THRESHOLD |
0.1 | [0.05, 0.3] | Insensitive within range |
Effects Matrix¶
Each row lists what the field/mixin does for each outcome. — means no effect.
| Effect | acted | used | dismissed | deferred | contradicted |
|---|---|---|---|---|---|
| ConfidenceField | strengthen | — | — | — | weaken |
| CyclicDecayField | strengthen | — | weaken | — | weaken (aggressive) |
| DecayingSortedField | touch | — | — | — | — |
| AccessTracker | confirm | confirm | discard | discard | discard |
| PredictionLedger | auto-resolve | moderate err | auto-resolve | — | auto-resolve |
Supporting notes:
- DecayingSortedField:
actedcallstouch()to refresh the decay clock. - AccessTrackerMixin:
actedandusedcallconfirm_access();dismissed,deferred, andcontradictedcalldiscard_staged_access(). - CyclicDecayField:
actedstrengthens cycles and resolves pressure;dismissedandcontradictedweaken cycles (contradictedmore aggressively). - ConfidenceField:
actedcorroborates;contradictedcontradicts. - PredictionLedgerMixin:
acted,used,dismissed, andcontradictedauto-resolve pending predictions with appropriate error values (usedmaps to moderate errorDefaults.PL_AUTO_RESOLVE_USED).
RecallProposal¶
Internal ORM infrastructure for tracking proactively surfaced memories.
- Key pattern:
$RP:{ClassName}:pending:{partition}(sorted set scored by surfaced_at) - Lifecycle: pending -> acted | used | dismissed | deferred | contradicted | expired
- TTL: 3600s (1 hour). Unresolved proposals are treated as deferred.
from popoto.fields.observation import RecallProposal
# Get pending proposals
pending = RecallProposal.get_pending(Memory, partition="default")
# Expire stale proposals
expired = RecallProposal.expire_stale(Memory, ttl=3600)
See Also¶
- Metacognitive Layer — full
"used"outcome documentation,error_summary, andAdaptiveAssembler - ConfidenceField — Bayesian certainty tracking
- CyclicDecayField — cyclical resonance and pressure
- PredictionLedger — outcome tracking
- Agent Memory overview — full primitives reference