agent-memory-bridge 0.32.1__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.
Files changed (94) hide show
  1. agent_mem_bridge/__init__.py +1 -0
  2. agent_mem_bridge/__main__.py +4 -0
  3. agent_mem_bridge/archive_snapshot.py +175 -0
  4. agent_mem_bridge/classifier.py +227 -0
  5. agent_mem_bridge/cli.py +1204 -0
  6. agent_mem_bridge/client_config.py +313 -0
  7. agent_mem_bridge/codex_rollout.py +833 -0
  8. agent_mem_bridge/command_provider.py +250 -0
  9. agent_mem_bridge/consolidation.py +989 -0
  10. agent_mem_bridge/context_evaluation.py +344 -0
  11. agent_mem_bridge/context_manifest.py +791 -0
  12. agent_mem_bridge/contradiction.py +82 -0
  13. agent_mem_bridge/cross_client_activation.py +271 -0
  14. agent_mem_bridge/current_release_contract.py +99 -0
  15. agent_mem_bridge/database_maintenance.py +769 -0
  16. agent_mem_bridge/durable_data_policy.py +86 -0
  17. agent_mem_bridge/dynamic_state.py +978 -0
  18. agent_mem_bridge/embedding_index.py +516 -0
  19. agent_mem_bridge/embedding_scheduler.py +268 -0
  20. agent_mem_bridge/enrichment_rules.py +53 -0
  21. agent_mem_bridge/evidence_inspect.py +466 -0
  22. agent_mem_bridge/exporters.py +81 -0
  23. agent_mem_bridge/failure_report.py +152 -0
  24. agent_mem_bridge/filesystem_safety.py +70 -0
  25. agent_mem_bridge/first_run.py +320 -0
  26. agent_mem_bridge/governance_trigger.py +185 -0
  27. agent_mem_bridge/healthcheck.py +218 -0
  28. agent_mem_bridge/index_health.py +144 -0
  29. agent_mem_bridge/knowledge_explorer.py +911 -0
  30. agent_mem_bridge/learning_candidates.py +297 -0
  31. agent_mem_bridge/learning_policy.py +255 -0
  32. agent_mem_bridge/lineage.py +174 -0
  33. agent_mem_bridge/log_maintenance.py +35 -0
  34. agent_mem_bridge/mcp_boundary.py +196 -0
  35. agent_mem_bridge/onboarding.py +531 -0
  36. agent_mem_bridge/onboarding_contract.py +281 -0
  37. agent_mem_bridge/paths.py +779 -0
  38. agent_mem_bridge/poll_cursor.py +67 -0
  39. agent_mem_bridge/procedure_governance.py +248 -0
  40. agent_mem_bridge/profile_assembly.py +146 -0
  41. agent_mem_bridge/profile_bundle.py +144 -0
  42. agent_mem_bridge/profile_migration.py +417 -0
  43. agent_mem_bridge/project_init.py +229 -0
  44. agent_mem_bridge/promotion.py +264 -0
  45. agent_mem_bridge/promotion_governance.py +493 -0
  46. agent_mem_bridge/proof.py +375 -0
  47. agent_mem_bridge/provenance.py +68 -0
  48. agent_mem_bridge/public_surface.py +214 -0
  49. agent_mem_bridge/query.py +1137 -0
  50. agent_mem_bridge/recall_eligibility.py +164 -0
  51. agent_mem_bridge/recall_first.py +312 -0
  52. agent_mem_bridge/record_projection.py +438 -0
  53. agent_mem_bridge/reflex.py +1098 -0
  54. agent_mem_bridge/relation_metadata.py +88 -0
  55. agent_mem_bridge/release_contract.py +1791 -0
  56. agent_mem_bridge/repository.py +966 -0
  57. agent_mem_bridge/repository_bootstrap.py +306 -0
  58. agent_mem_bridge/repository_snapshot_store.py +371 -0
  59. agent_mem_bridge/retrieval_feedback.py +1059 -0
  60. agent_mem_bridge/review_queue.py +472 -0
  61. agent_mem_bridge/review_workflow.py +252 -0
  62. agent_mem_bridge/revisions.py +342 -0
  63. agent_mem_bridge/run_consolidation.py +1105 -0
  64. agent_mem_bridge/run_ledger.py +2422 -0
  65. agent_mem_bridge/run_outcome_authority.py +39 -0
  66. agent_mem_bridge/run_projection.py +735 -0
  67. agent_mem_bridge/run_verification_receipts.py +404 -0
  68. agent_mem_bridge/schema.py +2793 -0
  69. agent_mem_bridge/server.py +1338 -0
  70. agent_mem_bridge/service.py +321 -0
  71. agent_mem_bridge/service_health.py +94 -0
  72. agent_mem_bridge/service_lock.py +166 -0
  73. agent_mem_bridge/session_closeout.py +113 -0
  74. agent_mem_bridge/setup_apply.py +921 -0
  75. agent_mem_bridge/setup_planner.py +473 -0
  76. agent_mem_bridge/signals.py +703 -0
  77. agent_mem_bridge/source_sync.py +137 -0
  78. agent_mem_bridge/state_io.py +42 -0
  79. agent_mem_bridge/stdio_probe.py +441 -0
  80. agent_mem_bridge/storage.py +1204 -0
  81. agent_mem_bridge/structured_record.py +111 -0
  82. agent_mem_bridge/sync_notes.py +116 -0
  83. agent_mem_bridge/task_brief.py +551 -0
  84. agent_mem_bridge/task_memory.py +1407 -0
  85. agent_mem_bridge/telemetry.py +224 -0
  86. agent_mem_bridge/telemetry_summary.py +287 -0
  87. agent_mem_bridge/watcher.py +597 -0
  88. agent_mem_bridge/watcher_health.py +90 -0
  89. agent_memory_bridge-0.32.1.dist-info/METADATA +236 -0
  90. agent_memory_bridge-0.32.1.dist-info/RECORD +94 -0
  91. agent_memory_bridge-0.32.1.dist-info/WHEEL +5 -0
  92. agent_memory_bridge-0.32.1.dist-info/entry_points.txt +2 -0
  93. agent_memory_bridge-0.32.1.dist-info/licenses/LICENSE +21 -0
  94. agent_memory_bridge-0.32.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ """agent-memory-bridge package."""
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+ from shutil import copy2
7
+ from typing import Any
8
+
9
+ ROOT_FILES = (
10
+ "HOW-TO-USE-COLE.md",
11
+ "architecture.md",
12
+ )
13
+
14
+ MEMORY_ROOT_FILES = (
15
+ "memory/.claude-memory-guard.md",
16
+ "memory/MEMORY.md",
17
+ "memory/QUEUE.md",
18
+ "memory/REDLINE.md",
19
+ "memory/status.md",
20
+ )
21
+
22
+ SNAPSHOT_DIRS = (
23
+ "memory/core",
24
+ "memory/team",
25
+ "memory/workflows",
26
+ "memory/workspace",
27
+ )
28
+
29
+ LIVE_SOURCE_FILES = (
30
+ "HOW-TO-USE-COLE.md",
31
+ "memory/.claude-memory-guard.md",
32
+ "memory/MEMORY.md",
33
+ "memory/QUEUE.md",
34
+ "memory/REDLINE.md",
35
+ "memory/core/core.md",
36
+ "memory/core/decision-making.md",
37
+ "memory/core/persona.md",
38
+ )
39
+
40
+
41
+ def build_default_snapshot_root(source_root: Path, *, timestamp: datetime | None = None) -> Path:
42
+ moment = timestamp or datetime.now(UTC)
43
+ stamp = moment.strftime("%Y-%m-%d-source-snapshot")
44
+ return Path(source_root).resolve() / "archive" / stamp
45
+
46
+
47
+ def build_default_live_manifest_path(source_root: Path) -> Path:
48
+ return Path(source_root).resolve() / "live-source-manifest.json"
49
+
50
+
51
+ def create_profile_archive_snapshot(
52
+ source_root: Path,
53
+ snapshot_root: Path,
54
+ *,
55
+ compare_report: dict[str, Any] | None = None,
56
+ ) -> dict[str, Any]:
57
+ source_root = Path(source_root).resolve()
58
+ snapshot_root = Path(snapshot_root).resolve()
59
+ copied_files: list[str] = []
60
+
61
+ for relative_path in iter_snapshot_relative_paths(source_root):
62
+ source_path = source_root / relative_path
63
+ target_path = snapshot_root / relative_path
64
+ target_path.parent.mkdir(parents=True, exist_ok=True)
65
+ copy2(source_path, target_path)
66
+ copied_files.append(relative_path.as_posix())
67
+
68
+ manifest = {
69
+ "created_at": datetime.now(UTC).isoformat(),
70
+ "source_root": str(source_root),
71
+ "snapshot_root": str(snapshot_root),
72
+ "file_count": len(copied_files),
73
+ "files": copied_files,
74
+ "compare_report": compare_report,
75
+ "migration_safe": bool(
76
+ compare_report
77
+ and compare_report.get("missing_count") == 0
78
+ and compare_report.get("extra_count") == 0
79
+ and compare_report.get("content_mismatch_count") == 0
80
+ and compare_report.get("namespace_mismatch_count") == 0
81
+ ),
82
+ }
83
+ manifest_path = snapshot_root / "manifest.json"
84
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
85
+ manifest["manifest_path"] = str(manifest_path)
86
+ return manifest
87
+
88
+
89
+ # Legacy compatibility alias for older migration helpers.
90
+ create_cole_archive_snapshot = create_profile_archive_snapshot
91
+
92
+
93
+ def write_live_source_manifest(
94
+ source_root: Path,
95
+ manifest_path: Path,
96
+ *,
97
+ relative_paths: list[Path] | None = None,
98
+ ) -> dict[str, Any]:
99
+ source_root = Path(source_root).resolve()
100
+ manifest_path = Path(manifest_path).resolve()
101
+ files = relative_paths or iter_live_source_relative_paths(source_root)
102
+ rendered_files = [path.as_posix() for path in files]
103
+ manifest = {
104
+ "created_at": datetime.now(UTC).isoformat(),
105
+ "source_root": str(source_root),
106
+ "manifest_path": str(manifest_path),
107
+ "file_count": len(rendered_files),
108
+ "files": rendered_files,
109
+ }
110
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
111
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
112
+ return manifest
113
+
114
+
115
+ def iter_snapshot_relative_paths(source_root: Path) -> list[Path]:
116
+ root = Path(source_root).resolve()
117
+ files: list[Path] = []
118
+
119
+ for relative in ROOT_FILES:
120
+ candidate = root / relative
121
+ if candidate.is_file():
122
+ files.append(Path(relative))
123
+
124
+ for relative in MEMORY_ROOT_FILES:
125
+ candidate = root / relative
126
+ if candidate.is_file():
127
+ files.append(Path(relative))
128
+
129
+ for relative_dir in SNAPSHOT_DIRS:
130
+ directory = root / relative_dir
131
+ if not directory.is_dir():
132
+ continue
133
+ for path in sorted(directory.rglob("*.md")):
134
+ files.append(path.relative_to(root))
135
+
136
+ deduped: list[Path] = []
137
+ seen: set[Path] = set()
138
+ for relative_path in files:
139
+ if relative_path in seen:
140
+ continue
141
+ seen.add(relative_path)
142
+ deduped.append(relative_path)
143
+ return deduped
144
+
145
+
146
+ def iter_live_source_relative_paths(source_root: Path) -> list[Path]:
147
+ root = Path(source_root).resolve()
148
+ files: list[Path] = []
149
+ for relative in LIVE_SOURCE_FILES:
150
+ candidate = root / relative
151
+ if candidate.is_file():
152
+ files.append(Path(relative))
153
+ return files
154
+
155
+
156
+ def load_manifest_relative_paths(manifest_path: Path) -> list[Path]:
157
+ manifest = load_manifest(manifest_path)
158
+ return [Path(item) for item in manifest.get("files", [])]
159
+
160
+
161
+ def load_manifest(manifest_path: Path) -> dict[str, Any]:
162
+ path = Path(manifest_path).resolve()
163
+ with path.open("r", encoding="utf-8") as handle:
164
+ data = json.load(handle)
165
+ return data if isinstance(data, dict) else {}
166
+
167
+
168
+ def find_latest_snapshot_manifest(source_root: Path) -> Path | None:
169
+ archive_root = Path(source_root).resolve() / "archive"
170
+ if not archive_root.is_dir():
171
+ return None
172
+ manifests = sorted(archive_root.glob("*-source-snapshot/manifest.json"))
173
+ if not manifests:
174
+ return None
175
+ return manifests[-1]
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+ from dataclasses import dataclass
6
+ from typing import Any, Literal
7
+
8
+ from .command_provider import CommandLimits, CommandProviderError, run_json_command
9
+
10
+ ClassifierMode = Literal["off", "shadow", "assist"]
11
+ ClassifierProvider = Literal["command"]
12
+ ALLOWED_CLASSIFIER_TAG_PREFIXES = ("domain:", "topic:")
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class ClassifierConfig:
17
+ mode: ClassifierMode = "off"
18
+ provider: ClassifierProvider = "command"
19
+ command: str | tuple[str, ...] = ""
20
+ timeout_seconds: float = 10.0
21
+ batch_size: int = 16
22
+ minimum_confidence: float = 0.6
23
+ trusted_shell: bool = False
24
+ max_input_bytes: int = 1_000_000
25
+ max_output_bytes: int = 2_000_000
26
+ max_stderr_bytes: int = 65_536
27
+ env_allowlist: tuple[str, ...] = ()
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class EnrichmentCandidate:
32
+ key: str
33
+ text: str
34
+ fallback_tags: tuple[str, ...] = ()
35
+ title: str = ""
36
+ source_id: str = ""
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class Classification:
41
+ key: str
42
+ tags: tuple[str, ...]
43
+ domains: tuple[str, ...]
44
+ topics: tuple[str, ...]
45
+ classifier_suggested_tags: tuple[str, ...] = ()
46
+ confidence: float | None = None
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ClassificationBatchOutcome:
51
+ predictions: dict[str, Classification]
52
+ requested_count: int
53
+ error: str | None = None
54
+
55
+
56
+ class EnrichmentClassifier:
57
+ def __init__(self, config: ClassifierConfig) -> None:
58
+ if not math.isfinite(config.minimum_confidence) or not 0.0 <= config.minimum_confidence <= 1.0:
59
+ raise ValueError("minimum_confidence must be a finite value between 0 and 1")
60
+ self.config = config
61
+
62
+ @property
63
+ def active(self) -> bool:
64
+ command_configured = (
65
+ bool(self.config.command.strip()) if isinstance(self.config.command, str) else bool(self.config.command)
66
+ )
67
+ return self.config.mode != "off" and command_configured
68
+
69
+ def classify(self, candidates: list[EnrichmentCandidate]) -> ClassificationBatchOutcome:
70
+ if not candidates:
71
+ return ClassificationBatchOutcome(predictions={}, requested_count=0)
72
+ if not self.active:
73
+ return ClassificationBatchOutcome(predictions={}, requested_count=len(candidates))
74
+
75
+ predictions: dict[str, Classification] = {}
76
+ for batch in self._batched(candidates):
77
+ outcome = self._classify_batch(batch)
78
+ predictions.update(outcome.predictions)
79
+ if outcome.error:
80
+ return ClassificationBatchOutcome(
81
+ predictions=predictions,
82
+ requested_count=len(candidates),
83
+ error=outcome.error,
84
+ )
85
+ return ClassificationBatchOutcome(predictions=predictions, requested_count=len(candidates))
86
+
87
+ def accepted_tags(self, classification: Classification | None) -> list[str]:
88
+ if classification is None:
89
+ return []
90
+ confidence = classification.confidence
91
+ if confidence is None or not math.isfinite(confidence) or not 0.0 <= confidence <= 1.0:
92
+ return []
93
+ if confidence < self.config.minimum_confidence:
94
+ return []
95
+ return self._policy_tags(list(classification.tags))
96
+
97
+ def _classify_batch(self, batch: list[EnrichmentCandidate]) -> ClassificationBatchOutcome:
98
+ payload = {
99
+ "items": [
100
+ {
101
+ "key": candidate.key,
102
+ "text": candidate.text,
103
+ "title": candidate.title,
104
+ "source_id": candidate.source_id,
105
+ "fallback_tags": list(candidate.fallback_tags),
106
+ }
107
+ for candidate in batch
108
+ ]
109
+ }
110
+ try:
111
+ completed = run_json_command(
112
+ self.config.command,
113
+ payload,
114
+ timeout_seconds=self.config.timeout_seconds,
115
+ trusted_shell=self.config.trusted_shell,
116
+ limits=CommandLimits(
117
+ max_input_bytes=self.config.max_input_bytes,
118
+ max_stdout_bytes=self.config.max_output_bytes,
119
+ max_stderr_bytes=self.config.max_stderr_bytes,
120
+ ),
121
+ env_allowlist=self.config.env_allowlist,
122
+ )
123
+ except CommandProviderError as exc:
124
+ return ClassificationBatchOutcome(
125
+ predictions={},
126
+ requested_count=len(batch),
127
+ error=f"classifier command failed: {exc}",
128
+ )
129
+
130
+ if completed.returncode != 0:
131
+ return ClassificationBatchOutcome(
132
+ predictions={},
133
+ requested_count=len(batch),
134
+ error=(
135
+ f"classifier command failed with exit code {completed.returncode} "
136
+ f"(fingerprint={completed.fingerprint})"
137
+ ),
138
+ )
139
+
140
+ try:
141
+ raw = json.loads(completed.stdout)
142
+ except json.JSONDecodeError as exc:
143
+ return ClassificationBatchOutcome(
144
+ predictions={},
145
+ requested_count=len(batch),
146
+ error=f"classifier returned invalid JSON ({exc.__class__.__name__})",
147
+ )
148
+
149
+ items = raw.get("items", raw) if isinstance(raw, dict) else raw
150
+ if not isinstance(items, list):
151
+ return ClassificationBatchOutcome(
152
+ predictions={},
153
+ requested_count=len(batch),
154
+ error="classifier output must be a list or an object with an items list",
155
+ )
156
+
157
+ predictions: dict[str, Classification] = {}
158
+ for item in items:
159
+ if not isinstance(item, dict):
160
+ continue
161
+ key = str(item.get("key", "")).strip()
162
+ if not key:
163
+ continue
164
+ classifier_suggested_tags = self._normalize_tags(item)
165
+ tags = self._policy_tags(classifier_suggested_tags)
166
+ predictions[key] = Classification(
167
+ key=key,
168
+ tags=tuple(tags),
169
+ domains=tuple(tag for tag in tags if tag.startswith("domain:")),
170
+ topics=tuple(tag for tag in tags if tag.startswith("topic:")),
171
+ classifier_suggested_tags=tuple(classifier_suggested_tags),
172
+ confidence=self._normalize_confidence(item.get("confidence")),
173
+ )
174
+
175
+ return ClassificationBatchOutcome(predictions=predictions, requested_count=len(batch))
176
+
177
+ def _normalize_tags(self, item: dict[str, Any]) -> list[str]:
178
+ tags: list[str] = []
179
+ raw_tags = item.get("tags", [])
180
+ tag_values = raw_tags if isinstance(raw_tags, (list, tuple)) else ()
181
+ for value in tag_values:
182
+ if isinstance(value, str):
183
+ normalized = value.strip()
184
+ if normalized:
185
+ tags.append(normalized)
186
+ for field, prefix in (("domains", "domain:"), ("topics", "topic:")):
187
+ raw_values = item.get(field, [])
188
+ values = raw_values if isinstance(raw_values, (list, tuple)) else ()
189
+ for value in values:
190
+ if not isinstance(value, str):
191
+ continue
192
+ normalized = value.strip()
193
+ if not normalized:
194
+ continue
195
+ tags.append(normalized if normalized.startswith(prefix) else f"{prefix}{normalized}")
196
+ seen: set[str] = set()
197
+ result: list[str] = []
198
+ for tag in tags:
199
+ if tag in seen:
200
+ continue
201
+ seen.add(tag)
202
+ result.append(tag)
203
+ return result
204
+
205
+ @staticmethod
206
+ def _policy_tags(tags: list[str]) -> list[str]:
207
+ return [
208
+ tag
209
+ for tag in tags
210
+ if any(tag.startswith(prefix) and len(tag) > len(prefix) for prefix in ALLOWED_CLASSIFIER_TAG_PREFIXES)
211
+ ]
212
+
213
+ @staticmethod
214
+ def _normalize_confidence(value: Any) -> float | None:
215
+ if value is None:
216
+ return None
217
+ try:
218
+ normalized = float(value)
219
+ except (TypeError, ValueError):
220
+ return None
221
+ if not math.isfinite(normalized) or not 0.0 <= normalized <= 1.0:
222
+ return None
223
+ return normalized
224
+
225
+ def _batched(self, candidates: list[EnrichmentCandidate]) -> list[list[EnrichmentCandidate]]:
226
+ batch_size = max(1, int(self.config.batch_size))
227
+ return [candidates[index : index + batch_size] for index in range(0, len(candidates), batch_size)]