popoto.fields.observation¶
popoto.fields.observation
¶
ObservationProtocol + RecallProposal — Outcome-driven memory effects.
Provides lifecycle hooks for passive behavioral inference on memory models. The application layer reports how the agent used retrieved memories; the ORM applies effects atomically.
Three hooks
on_read(instance): Fire when query hydrates an instance. Delegates to AccessTrackerMixin staging.on_surfaced(instances, reason): Fire when proactive system pushes memories into agent context. Creates RecallProposal entries.on_context_used(instances, outcome_map): Fire when application reports how the agent responded. Applies effects based on outcome.
Five outcomes
acted: Memory content appeared in agent's response. Strengthen.dismissed: Agent explicitly ignored/rejected. Weaken.deferred: Agent didn't address it. No effects, pressure builds.contradicted: Agent explicitly contradicted. Aggressively weaken.used: Agent consumed the memory (read + reasoned) but did not act on it in the response. Confirms the staged read and auto-resolves predictions with a moderate error, but does NOT touch ConfidenceField, CyclicDecayField, or DecayingSortedField. Observable distinction fromdeferred:usedrecords a confirmed-read trace,deferreddiscards staged reads.
See Also
For the effects-per-field matrix (what each outcome does to ConfidenceField, CyclicDecayField, DecayingSortedField, AccessTracker, PredictionLedger), see the "Effects Matrix" section of docs/features/observation-protocol.md.
RecallProposal
Internal ORM infrastructure for tracking proactively surfaced memories. Redis ZSET keyed by model class and partition, scored by surfaced_at. TTL-based expiration (default 1 hour).
Example
from popoto import ObservationProtocol, RecallProposal
After agent processes memories:¶
outcome_map = { memory1.db_key.redis_key: "acted", memory2.db_key.redis_key: "dismissed", } ObservationProtocol.on_context_used(memories, outcome_map)
ACTED_CONFIDENCE_SIGNAL = Defaults.ACTED_CONFIDENCE_SIGNAL
module-attribute
¶
Confidence signal sent to ConfidenceField on 'acted' outcome. Higher values corroborate the memory more strongly. Optimal range: [0.5, 1.0]. Insensitive within this range (nDCG stable).
CONTRADICTED_CONFIDENCE_SIGNAL = Defaults.CONTRADICTED_CONFIDENCE_SIGNAL
module-attribute
¶
Confidence signal sent to ConfidenceField on 'contradicted' outcome. Lower values contradict the memory more aggressively. Optimal range: [0.05, 0.3]. Insensitive within this range (nDCG stable).
ACTED_CYCLE_STRENGTHEN_FACTOR = Defaults.ACTED_CYCLE_STRENGTHEN_FACTOR
module-attribute
¶
CyclicDecayField amplification factor on 'acted' outcome. CLIFF EFFECT: values < 1.0 cause a 23% nDCG drop in temporal scheduling. Must be >= 1.0. Optimal range: [1.0, 2.0]. Default 1.2 is safe.
DISMISSED_CYCLE_WEAKEN_FACTOR = Defaults.DISMISSED_CYCLE_WEAKEN_FACTOR
module-attribute
¶
CyclicDecayField damping factor on 'dismissed' outcome. Values < 1.0 weaken the cycle amplitude. Optimal range: [0.3, 1.0]. Insensitive within this range.
CONTRADICTED_CYCLE_WEAKEN_FACTOR = Defaults.CONTRADICTED_CYCLE_WEAKEN_FACTOR
module-attribute
¶
CyclicDecayField aggressive damping factor on 'contradicted' outcome. Values < 1.0 weaken the cycle amplitude; lower = more aggressive. Optimal range: [0.3, 0.8]. Insensitive within this range.
AUTO_DISCHARGE_CONFIDENCE_THRESHOLD = Defaults.AUTO_DISCHARGE_CONFIDENCE_THRESHOLD
module-attribute
¶
Below this confidence level, pressure is auto-resolved on contradicted outcome. Very low confidence records should not continue building pressure. Optimal range: [0.05, 0.3]. Insensitive within this range.
CONFIDENCE_EPSILON = Defaults.CONFIDENCE_EPSILON
module-attribute
¶
Internal float-boundary tolerance for the auto-discharge comparison. Confidence values within this epsilon of the threshold are NOT below it. Not user config.
ObservationProtocol
¶
Lifecycle hooks for passive behavioral inference on memory models.
All methods are static — the protocol is a stateless coordinator that dispatches effects based on outcome type.
Source code in src/popoto/fields/observation.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
on_read(instance, pipeline=None)
staticmethod
¶
Fire when query hydrates an instance. Delegates to AccessTrackerMixin staging.
If the instance's model uses AccessTrackerMixin, this calls
instance.on_read(). Otherwise it's a no-op.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instance
|
A Model instance that was just read from Redis. |
required | |
pipeline
|
Optional Redis pipeline for batch operations. |
None
|
Source code in src/popoto/fields/observation.py
on_surfaced(instances, reason='proactive', partition=None, pipeline=None)
staticmethod
¶
Fire when proactive system pushes memories into agent context.
Creates RecallProposal entries for tracking. Side-effect-free on the memories themselves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instances
|
List of Model instances being surfaced. |
required | |
reason
|
Why the memories were surfaced. Default "proactive". |
'proactive'
|
|
partition
|
Optional partition key for multi-agent setups. |
None
|
|
pipeline
|
Optional Redis pipeline for batch operations. |
None
|
Source code in src/popoto/fields/observation.py
on_context_used(instances, outcome_map, pipeline=None)
staticmethod
¶
Fire when application reports how agent responded to surfaced memories.
For each instance, looks up its outcome in outcome_map and applies the corresponding effects atomically.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instances
|
List of Model instances that were in the agent's context. |
required | |
outcome_map
|
Dict mapping instance Redis keys (str) to outcome strings: "acted", "used", "dismissed", "deferred", "contradicted". Instances not in the map default to "deferred". |
required | |
pipeline
|
Optional Redis pipeline for batch operations. |
None
|
Note
This method validates outcome_map strictly against
VALID_OUTCOMES. Application-specific outcomes (e.g. a custom
"echoed" label) must be coerced to one of the five valid
values before calling, otherwise a ValueError is raised.
See docs/features/observation-protocol.md (where the
protocol lives) for guidance on mapping bespoke outcomes into
the canonical vocabulary.
Signalling a correction with contradicted:
outcome_map is {redis_key: outcome} and has no slot for a
second instance, so a caller that knows what corrected a memory
names it by setting the private _superseded_by attribute on the
contradicted instance before reporting the outcome::
stale._superseded_by = corrected
ObservationProtocol.on_context_used(
[stale], {stale.db_key.redis_key: "contradicted"}
)
On a model declaring a ValidityField this closes ``stale``'s
validity interval and writes the supersession edge to ``corrected``
(issue #580). Without the attribute — and on every model with no
ValidityField — ``contradicted`` behaves exactly as before. See
``_apply_supersession``.
Unsaved instances
An unsaved member of instances is skipped, for every outcome.
Its field effects raise internally and are swallowed, it
contributes no commands, and the remaining members of the batch
still get their full effects — one unsaved instance never aborts
the batch (issue #583). The model methods themselves
(touch, confirm_access, strengthen_cycle,
weaken_cycle, resolve_pressure) are unchanged and still
raise TypeError for direct callers; the degradation belongs
to this protocol layer only.
Raises:
| Type | Description |
|---|---|
ValueError
|
If any outcome string is not a valid outcome. Being unsaved is never a reason this method raises. |
Source code in src/popoto/fields/observation.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
RecallProposal
¶
Internal tracking for proactively surfaced memories.
Key pattern: $RP:{ClassName}:pending:{partition} -> ZSET scored by surfaced_at Statuses: pending -> acted | used | dismissed | deferred | contradicted | expired TTL: default 3600s (1 hour). Unresolved proposals treated as deferred.
This is internal ORM infrastructure, not a user-facing Model.
Source code in src/popoto/fields/observation.py
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 | |
create_batch(instances, reason='proactive', partition=None, pipeline=None)
classmethod
¶
Create pending proposals for a batch of instances.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instances
|
List of Model instances being surfaced. |
required | |
reason
|
Why the memories were surfaced. |
'proactive'
|
|
partition
|
Optional partition key. |
None
|
|
pipeline
|
Optional Redis pipeline for batch operations. |
None
|
Source code in src/popoto/fields/observation.py
resolve(instance, outcome, partition=None, pipeline=None)
classmethod
¶
Remove a resolved proposal from the pending set.
Idempotent — returns 0 if already removed (e.g., by expiration).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instance
|
The Model instance whose proposal to resolve. |
required | |
outcome
|
The outcome string (for logging). |
required | |
partition
|
Optional partition key. |
None
|
|
pipeline
|
Optional Redis pipeline for batch operations. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
Number of members removed (0 or 1). |
Source code in src/popoto/fields/observation.py
expire_stale(model_class, partition=None, ttl=None, pipeline=None)
classmethod
¶
Remove proposals older than TTL. Returns expired member keys.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_class
|
The Model class to check. |
required | |
partition
|
Optional partition key. |
None
|
|
ttl
|
TTL in seconds. Default DEFAULT_TTL (3600). |
None
|
|
pipeline
|
Optional Redis pipeline for batch operations. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
list |
List of expired member key strings. |
Source code in src/popoto/fields/observation.py
get_pending(model_class, partition=None)
classmethod
¶
Return all pending proposals as (member_key, surfaced_at) pairs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_class
|
The Model class to check. |
required | |
partition
|
Optional partition key. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
list |
List of (member_key_str, surfaced_at_float) tuples. |