plug-play-memory 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,461 @@
1
+ """
2
+ Judge for plug-play-memory — brain-inspired, LLM-free by default.
3
+
4
+ Three layers, evaluated in order:
5
+
6
+ 1. RulesJudge — deterministic per-action rules (handles 80% of cases)
7
+ 2. HeuristicEnricher — regex patterns on the raw field (catches recurring,
8
+ refactor, security, flaky, surprise cases)
9
+ 3. SalienceScorer — 4-factor score (outcome + prediction error + magnitude
10
+ + schema contradiction) → decides tagged vs consolidated
11
+
12
+ The brain parallel:
13
+ - Rules ≈ reflex arcs (fast, hardwired)
14
+ - Heuristics ≈ pattern recognition (mid-speed, learned)
15
+ - Salience ≈ amygdala tagging (slow, contextual)
16
+ - tagged flag ≈ synaptic tag (fragile until consolidated)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from dataclasses import dataclass
23
+ from typing import Any
24
+
25
+ from .events import AgentEvent
26
+
27
+
28
+ @dataclass
29
+ class JudgeDecision:
30
+ should_save: bool
31
+ category: str = "transient" # decision|error|achievement|convention|fact|transient|knowledge_gap
32
+ tier: str = "archival" # core|recall|archival
33
+ key: str | None = None
34
+ importance: str = "normal" # low|normal|high
35
+ salience_score: float = 0.5
36
+ summary: str = ""
37
+ tagged: bool = False # True = needs consolidation before durable
38
+ discard_reason: str = ""
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Layer 1: deterministic per-action rules
43
+ # ---------------------------------------------------------------------------
44
+
45
+ ACTION_RULES: dict[str, dict[str, JudgeDecision]] = {
46
+ # ---- CODING ----
47
+ "file_edited": {
48
+ "default": JudgeDecision(
49
+ should_save=False, discard_reason="routine file edit"
50
+ ),
51
+ "failure": JudgeDecision(
52
+ should_save=True, category="error", tier="archival",
53
+ importance="high", key="edit_failure",
54
+ summary="File edit failed",
55
+ ),
56
+ },
57
+ "git_pushed": {
58
+ "default": JudgeDecision(
59
+ should_save=True, category="achievement", tier="recall",
60
+ importance="normal", summary="Pushed to git",
61
+ ),
62
+ },
63
+ "test_run": {
64
+ "success": JudgeDecision(
65
+ should_save=False, discard_reason="passing tests aren't memorable"
66
+ ),
67
+ "failure": JudgeDecision(
68
+ should_save=True, category="error", tier="archival",
69
+ importance="high", key="test_failure",
70
+ summary="Test run failed",
71
+ ),
72
+ "partial": JudgeDecision(
73
+ should_save=True, category="error", tier="archival",
74
+ importance="normal", key="test_partial",
75
+ summary="Some tests failed",
76
+ ),
77
+ },
78
+ "decision_made": {
79
+ "default": JudgeDecision(
80
+ should_save=True, category="decision", tier="archival",
81
+ importance="high", summary="Decision made",
82
+ ),
83
+ },
84
+ "error_encountered": {
85
+ "default": JudgeDecision(
86
+ should_save=True, category="error", tier="archival",
87
+ importance="high", summary="Error encountered",
88
+ ),
89
+ },
90
+ "deployed": {
91
+ "success": JudgeDecision(
92
+ should_save=True, category="achievement", tier="recall",
93
+ importance="normal", summary="Deployed successfully",
94
+ ),
95
+ "failure": JudgeDecision(
96
+ should_save=True, category="error", tier="archival",
97
+ importance="high", key="deploy_failure",
98
+ summary="Deployment failed",
99
+ ),
100
+ },
101
+ "migration_run": {
102
+ "success": JudgeDecision(
103
+ should_save=False, discard_reason="successful migration is routine"
104
+ ),
105
+ "failure": JudgeDecision(
106
+ should_save=True, category="error", tier="archival",
107
+ importance="high", key="migration_failure",
108
+ summary="Migration failed",
109
+ ),
110
+ },
111
+
112
+ # ---- STUDY ----
113
+ "question_asked": {
114
+ "default": JudgeDecision(
115
+ should_save=False, discard_reason="asking is not memorable"
116
+ ),
117
+ },
118
+ "answer_attempted": {
119
+ "success": JudgeDecision(
120
+ should_save=False, discard_reason="correct answer is routine"
121
+ ),
122
+ "failure": JudgeDecision(
123
+ should_save=True, category="error", tier="archival",
124
+ importance="high", key="misconception",
125
+ summary="Wrong answer — possible misconception",
126
+ ),
127
+ "partial": JudgeDecision(
128
+ should_save=True, category="error", tier="archival",
129
+ importance="normal", key="partial_understanding",
130
+ summary="Partial answer — incomplete understanding",
131
+ ),
132
+ },
133
+ "concept_mastered": {
134
+ "default": JudgeDecision(
135
+ should_save=True, category="achievement", tier="recall",
136
+ importance="normal", key="mastery",
137
+ summary="Concept mastered",
138
+ ),
139
+ },
140
+ "knowledge_gap_found": {
141
+ "default": JudgeDecision(
142
+ should_save=True, category="error", tier="archival",
143
+ importance="high", key="knowledge_gap",
144
+ summary="Knowledge gap identified",
145
+ ),
146
+ },
147
+ "goal_set": {
148
+ "default": JudgeDecision(
149
+ should_save=True, category="fact", tier="core",
150
+ importance="high", key="goal",
151
+ summary="Goal set",
152
+ ),
153
+ },
154
+ "review_scheduled": {
155
+ "default": JudgeDecision(
156
+ should_save=True, category="convention", tier="recall",
157
+ importance="normal", key="review_scheduled",
158
+ summary="Review scheduled",
159
+ ),
160
+ },
161
+
162
+ # ---- FINANCIAL ----
163
+ "make_payment": {
164
+ "success": JudgeDecision(
165
+ should_save=True, category="achievement", tier="recall",
166
+ importance="normal", summary="Payment made",
167
+ ),
168
+ "failure": JudgeDecision(
169
+ should_save=True, category="error", tier="archival",
170
+ importance="high", key="payment_failure",
171
+ summary="Payment failed",
172
+ ),
173
+ },
174
+ "categorize_transaction": {
175
+ "default": JudgeDecision(
176
+ should_save=False, discard_reason="routine categorization"
177
+ ),
178
+ },
179
+ "set_budget_limit": {
180
+ "default": JudgeDecision(
181
+ should_save=True, category="decision", tier="core",
182
+ importance="high", key="budget_limit",
183
+ summary="Budget limit set",
184
+ ),
185
+ },
186
+ "read_account_balance": {
187
+ "default": JudgeDecision(
188
+ should_save=False, discard_reason="balance read is transient"
189
+ ),
190
+ },
191
+
192
+ # ---- COMMON ----
193
+ "fact_observed": {
194
+ "default": JudgeDecision(
195
+ should_save=True, category="fact", tier="core",
196
+ importance="normal", summary="Fact observed",
197
+ ),
198
+ },
199
+ "task_completed": {
200
+ "success": JudgeDecision(
201
+ should_save=True, category="achievement", tier="recall",
202
+ importance="normal", summary="Task completed",
203
+ ),
204
+ "failure": JudgeDecision(
205
+ should_save=True, category="error", tier="archival",
206
+ importance="high", key="task_failure",
207
+ summary="Task failed",
208
+ ),
209
+ },
210
+ }
211
+
212
+
213
+ class RulesJudge:
214
+ """Layer 1: deterministic rules. Free, instant, predictable."""
215
+
216
+ def evaluate(self, event: AgentEvent) -> JudgeDecision:
217
+ action_rules = ACTION_RULES.get(event.action)
218
+ if not action_rules:
219
+ # Unknown action — conservative save with low importance
220
+ return JudgeDecision(
221
+ should_save=True, category="fact", tier="archival",
222
+ importance="low", salience_score=0.4,
223
+ summary=f"{event.action.replace('_', ' ').capitalize()}",
224
+ )
225
+
226
+ if event.outcome in action_rules:
227
+ decision = action_rules[event.outcome]
228
+ else:
229
+ decision = action_rules.get(
230
+ "default",
231
+ JudgeDecision(should_save=False, discard_reason="no rule for outcome"),
232
+ )
233
+ # Return a copy so callers can mutate freely
234
+ return JudgeDecision(
235
+ should_save=decision.should_save,
236
+ category=decision.category,
237
+ tier=decision.tier,
238
+ key=decision.key,
239
+ importance=decision.importance,
240
+ salience_score=decision.salience_score,
241
+ summary=decision.summary,
242
+ tagged=decision.tagged,
243
+ discard_reason=decision.discard_reason,
244
+ )
245
+
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # Layer 2: heuristic enrichment on the raw field
249
+ # ---------------------------------------------------------------------------
250
+
251
+ HEURISTIC_PATTERNS: list[tuple[Any, dict[str, Any]]] = [
252
+ # Recurring / repeating — escalate importance (prediction error over time)
253
+ (re.compile(r"\b(recurring|again|repeated|persistent|keeps failing|still failing)\b", re.I),
254
+ {"importance": "high", "category": "error", "should_save": True}),
255
+ # Refactor / rewrite — save as decision
256
+ (re.compile(r"\b(refactor|rewrote|restructur|overhaul)\b", re.I),
257
+ {"should_save": True, "category": "decision", "tier": "archival", "importance": "high"}),
258
+ # Flaky / intermittent — downgrade to convention
259
+ (re.compile(r"\b(flaky|intermittent|random fail|sometimes fails)\b", re.I),
260
+ {"category": "convention", "importance": "low"}),
261
+ # Security keywords — always save high
262
+ (re.compile(r"\b(security|vulnerab|CVE|exploit|inject|breach|leak)\b", re.I),
263
+ {"should_save": True, "category": "error", "importance": "high"}),
264
+ # Recurring misconception (study)
265
+ (re.compile(r"\b(misconception|confuses|confused|mixes up|keeps getting wrong)\b", re.I),
266
+ {"should_save": True, "category": "error", "importance": "high", "tier": "archival"}),
267
+ ]
268
+
269
+
270
+ class HeuristicEnricher:
271
+ """Layer 2: regex patterns on raw text. Catches what rules miss."""
272
+
273
+ def enrich(self, decision: JudgeDecision, event: AgentEvent) -> JudgeDecision:
274
+ text = event.raw or ""
275
+ for pattern, overrides in HEURISTIC_PATTERNS:
276
+ if pattern.search(text):
277
+ for k, v in overrides.items():
278
+ setattr(decision, k, v)
279
+ # Magnitude heuristics on structured fields
280
+ lines_changed = event.inputs.get("lines_changed", 0) or 0
281
+ if lines_changed > 100:
282
+ decision.should_save = True
283
+ decision.importance = "high"
284
+ decision.category = "decision"
285
+ elif lines_changed > 30 and not decision.should_save:
286
+ decision.should_save = True
287
+ decision.importance = "normal"
288
+ decision.category = "decision"
289
+ # Amount-based escalation (financial)
290
+ amount = event.inputs.get("amount", 0) or 0
291
+ if amount > 5000:
292
+ decision.importance = "high"
293
+ return decision
294
+
295
+
296
+ # ---------------------------------------------------------------------------
297
+ # Layer 3: salience scoring (amygdala tag)
298
+ # ---------------------------------------------------------------------------
299
+
300
+ class SalienceScorer:
301
+ """
302
+ 4-factor salience score, brain-inspired:
303
+ 1. Outcome severity (emotional salience)
304
+ 2. Prediction error (novelty / surprise)
305
+ 3. Magnitude (size of effect)
306
+ 4. Schema fit (does this contradict existing memory?)
307
+ Returns score 0.0-1.0. Above threshold → consolidate; below → tag (synaptic).
308
+ """
309
+
310
+ def __init__(self, history_lookup: Any = None):
311
+ # history_lookup(agent_id, action) -> count of past successes
312
+ self.history_lookup = history_lookup
313
+
314
+ def score(self, event: AgentEvent, decision: JudgeDecision) -> tuple[float, bool]:
315
+ s = 0.0
316
+
317
+ # 1. Outcome severity
318
+ if event.outcome == "failure":
319
+ s += 0.4
320
+ elif event.outcome == "partial":
321
+ s += 0.2
322
+ # success adds 0 (routine)
323
+
324
+ # 2. Prediction error — if this action usually succeeds, failure is surprising
325
+ if self.history_lookup and event.outcome == "failure":
326
+ past_successes = self.history_lookup(event.agent_id, event.action)
327
+ if past_successes > 5:
328
+ s += 0.3 # expected to succeed but failed = high surprise
329
+ elif past_successes == 0:
330
+ s += 0.1 # first time failing
331
+
332
+ # 3. Magnitude
333
+ magnitude = 0
334
+ magnitude += (event.inputs.get("lines_changed", 0) or 0) / 100
335
+ magnitude += (event.inputs.get("amount", 0) or 0) / 1000
336
+ # severity may be a string ("high"/"normal"/"low") — normalize
337
+ sev = event.inputs.get("severity", 0) or 0
338
+ if isinstance(sev, str):
339
+ sev_map = {"high": 10, "normal": 5, "low": 2}
340
+ sev = sev_map.get(sev.lower(), 0)
341
+ magnitude += sev / 10
342
+ if magnitude > 1:
343
+ s += 0.2
344
+
345
+ # 4. Decision/category already indicates importance
346
+ if decision.category in ("decision", "error", "knowledge_gap"):
347
+ s += 0.2
348
+ if decision.importance == "high":
349
+ s += 0.1
350
+
351
+ s = min(s, 1.0)
352
+ # Tag if score is in the uncertain band — fragile until consolidation
353
+ tagged = 0.3 <= s < 0.6 and decision.category not in ("decision", "fact", "knowledge_gap")
354
+ return s, tagged
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Composite judge
359
+ # ---------------------------------------------------------------------------
360
+
361
+ class Judge:
362
+ """The full judge pipeline: rules → heuristics → salience."""
363
+
364
+ def __init__(self, history_lookup: Any = None):
365
+ self.rules = RulesJudge()
366
+ self.heuristics = HeuristicEnricher()
367
+ self.scorer = SalienceScorer(history_lookup=history_lookup)
368
+
369
+ def evaluate(self, event: AgentEvent) -> JudgeDecision:
370
+ # Layer 1
371
+ decision = self.rules.evaluate(event)
372
+ # Layer 2
373
+ decision = self.heuristics.enrich(decision, event)
374
+ # If discarded by rules+heuristics, skip salience scoring
375
+ if not decision.should_save:
376
+ return decision
377
+ # Layer 3
378
+ score, tagged = self.scorer.score(event, decision)
379
+ decision.salience_score = score
380
+ decision.tagged = tagged
381
+ # Enrich summary with structured details
382
+ decision.summary = self._build_summary(event, decision)
383
+ return decision
384
+
385
+ def _build_summary(self, event: AgentEvent, decision: JudgeDecision) -> str:
386
+ """Construct a one-line summary from event fields, capped at 100 chars."""
387
+ # If decision has a template summary AND we didn't change category via heuristic,
388
+ # enrich it with structured details. Otherwise build a fresh summary.
389
+ template_summaries = {
390
+ "Decision made", "Error encountered", "Fact observed",
391
+ "File edit failed", "Test run failed", "Some tests failed",
392
+ "Deployment failed", "Migration failed", "Concept mastered",
393
+ "Knowledge gap identified", "Goal set", "Pushed to git",
394
+ "Deployed successfully", "Task completed", "Task failed",
395
+ "Wrong answer — possible misconception", "Partial answer — incomplete understanding",
396
+ "Payment made", "Payment failed", "Budget limit set", "Review scheduled",
397
+ }
398
+
399
+ # Build a fresh summary if the decision was escalated from "discard" by heuristics
400
+ # (in which case the original summary was empty or a discard reason)
401
+ if not decision.summary or decision.summary not in template_summaries:
402
+ # Build from scratch — useful when heuristics escalated a "discard" to "save"
403
+ parts = []
404
+ cat_label = decision.category.replace("_", " ").capitalize() if decision.category != "transient" else "Event"
405
+ parts.append(cat_label)
406
+ topic = event.inputs.get("topic") or event.outputs.get("topic")
407
+ path = event.inputs.get("path")
408
+ branch = event.inputs.get("branch")
409
+ choice = event.outputs.get("choice")
410
+ reason = event.outputs.get("reason")
411
+ misconception = event.outputs.get("misconception")
412
+ target = event.inputs.get("target")
413
+ amount = event.inputs.get("amount")
414
+ diff = event.outputs.get("diff_summary") or event.outputs.get("notes") or event.outputs.get("error")
415
+ if topic:
416
+ parts.append(f"({topic})")
417
+ if path:
418
+ parts.append(f"[{path}]")
419
+ if branch:
420
+ parts.append(f"branch={branch}")
421
+ if choice:
422
+ parts.append(f"→ {choice}")
423
+ if misconception:
424
+ parts.append(f"— {misconception}")
425
+ if reason:
426
+ parts.append(f"({str(reason)[:50]})")
427
+ if target:
428
+ parts.append(f"to {target}")
429
+ if amount:
430
+ parts.append(f"₹{amount}")
431
+ if diff and not (topic or choice or misconception):
432
+ parts.append(f"— {str(diff)[:60]}")
433
+ return " ".join(parts)[:100]
434
+
435
+ # Enrich the existing template summary
436
+ parts = [decision.summary.rstrip(".")]
437
+ topic = event.inputs.get("topic") or event.outputs.get("topic")
438
+ path = event.inputs.get("path")
439
+ branch = event.inputs.get("branch")
440
+ choice = event.outputs.get("choice")
441
+ reason = event.outputs.get("reason")
442
+ misconception = event.outputs.get("misconception")
443
+ target = event.inputs.get("target")
444
+ amount = event.inputs.get("amount")
445
+ if topic:
446
+ parts.append(f"({topic})")
447
+ if path:
448
+ parts.append(f"[{path}]")
449
+ if branch:
450
+ parts.append(f"branch={branch}")
451
+ if choice:
452
+ parts.append(f"→ {choice}")
453
+ if misconception:
454
+ parts.append(f"— {misconception}")
455
+ if reason:
456
+ parts.append(f"({str(reason)[:60]})")
457
+ if target:
458
+ parts.append(f"to {target}")
459
+ if amount:
460
+ parts.append(f"₹{amount}")
461
+ return " ".join(parts)[:100]
@@ -0,0 +1,18 @@
1
+ # Baseline policy. Loaded first for every agent.
2
+ # Cannot be weakened by custom policies — deny rules here are absolute.
3
+ name: _base
4
+ description: Deny-all baseline with defense-in-depth absolute denies.
5
+ default: deny
6
+
7
+ deny_rules:
8
+ # These are denied NO MATTER WHAT custom policies say.
9
+ - action: delete_database
10
+ - action: drop_table
11
+ - action: format_disk
12
+ - action: rce
13
+ - action: exfiltrate_data
14
+ - action: modify_audit_log
15
+ - action: access_credentials
16
+ - action: modify_secrets
17
+
18
+ rules: []
@@ -0,0 +1,53 @@
1
+ name: coding
2
+ description: Built-in policy for coding agents (editors, debuggers, refactorers).
3
+ default: deny
4
+
5
+ rules:
6
+ # Memory ops
7
+ - action: read_memory
8
+ allow: true
9
+ - action: write_memory
10
+ allow: true
11
+ when:
12
+ category: [decision, error, achievement, convention, fact]
13
+ - action: delete_memory
14
+ allow: true
15
+ requires: [older_than_days_30]
16
+ - action: recall_memory
17
+ allow: true
18
+
19
+ # Coding actions
20
+ - action: file_edited
21
+ allow: true
22
+ - action: file_read
23
+ allow: true
24
+ - action: git_pushed
25
+ allow: true
26
+ requires: [approved_by_user]
27
+ - action: git_committed
28
+ allow: true
29
+ - action: test_run
30
+ allow: true
31
+ - action: build_run
32
+ allow: true
33
+ - action: lint_run
34
+ allow: true
35
+ - action: dependency_installed
36
+ allow: true
37
+ requires: [approved_by_user]
38
+ - action: deployed
39
+ allow: true
40
+ requires: [approved_by_user]
41
+ - action: migration_run
42
+ allow: true
43
+ requires: [approved_by_user]
44
+
45
+ # Hard denials
46
+ - action: modify_ci_config
47
+ allow: false
48
+ - action: access_production_database
49
+ allow: false
50
+ - action: send_email
51
+ allow: false
52
+ - action: make_purchase
53
+ allow: false
@@ -0,0 +1,50 @@
1
+ name: financial
2
+ description: Built-in policy for financial agents (budgeting, payments, advisory).
3
+ default: deny
4
+
5
+ rules:
6
+ # Memory ops — financial data is sensitive; lock down deletes
7
+ - action: read_memory
8
+ allow: true
9
+ - action: write_memory
10
+ allow: true
11
+ when:
12
+ category: [decision, error, achievement, convention, fact]
13
+ - action: delete_memory
14
+ allow: false
15
+ - action: recall_memory
16
+ allow: true
17
+
18
+ # Read-only financial actions
19
+ - action: read_account_balance
20
+ allow: true
21
+ - action: read_transaction_history
22
+ allow: true
23
+ - action: categorize_transaction
24
+ allow: true
25
+ - action: generate_budget_report
26
+ allow: true
27
+ - action: set_budget_limit
28
+ allow: true
29
+ requires: [approved_by_user]
30
+ - action: fact_observed
31
+ allow: true
32
+
33
+ # Tiered payment authorization (uses nested `when` syntax — M2 fix)
34
+ - action: make_payment
35
+ allow: true
36
+ requires: [approved_by_user]
37
+ when:
38
+ amount: {le: 1000}
39
+ - action: make_payment
40
+ allow: true
41
+ requires: [approved_by_user, second_factor_verified]
42
+ when:
43
+ amount: {le: 10000}
44
+ # Implicit: payments > 10000 denied by default
45
+
46
+ # Hard denials
47
+ - action: share_data_with_third_party
48
+ allow: false
49
+ - action: execute_trade
50
+ allow: false
@@ -0,0 +1,50 @@
1
+ name: study
2
+ description: Built-in policy for study/learning agents (tutors, prep companions).
3
+ default: deny
4
+
5
+ rules:
6
+ # Memory ops — students can read/write but NOT delete their own history
7
+ - action: read_memory
8
+ allow: true
9
+ - action: write_memory
10
+ allow: true
11
+ when:
12
+ category: [fact, decision, error, achievement, knowledge_gap, convention]
13
+ - action: delete_memory
14
+ allow: false
15
+ - action: recall_memory
16
+ allow: true
17
+
18
+ # Study actions
19
+ - action: question_asked
20
+ allow: true
21
+ - action: answer_attempted
22
+ allow: true
23
+ - action: concept_mastered
24
+ allow: true
25
+ - action: knowledge_gap_found
26
+ allow: true
27
+ - action: goal_set
28
+ allow: true
29
+ - action: review_scheduled
30
+ allow: true
31
+ - action: fact_observed
32
+ allow: true
33
+
34
+ # Study agents must NOT do these
35
+ - action: send_email
36
+ allow: false
37
+ - action: make_purchase
38
+ allow: false
39
+ - action: access_internet
40
+ allow: false
41
+ - action: execute_code
42
+ allow: false
43
+
44
+ # Can sync to external study tools ONLY with explicit user approval
45
+ - action: sync_to_flashcard_app
46
+ allow: true
47
+ requires: [approved_by_user]
48
+ - action: sync_to_notes_app
49
+ allow: true
50
+ requires: [approved_by_user]